refactor(omo-codex): rename ultragoal component to ulw-loop
Fully rename the ultragoal component to ulw-loop so the identifier matches the ulw-loop skill it powers. Renames the component directory, nested skill, TS identifiers (UlwLoop / ULW_LOOP_* / ulwLoop*), the omo ulw-loop CLI subcommand, the .omo/ulw-loop state directory, the OMO_ULW_LOOP_STEER directive token, and the @code-yeongyu/codex-ulw-loop package. Also threads Atlas-style right-sized parallel worker delegation and a Prometheus-style QA + maximum-parallelism plan into the ulw-loop skill, with a critical post-subagent QA gate. Updates aggregate wiring (plugin package components, hooks.json, sync-skills), the install-codex agent-link test fixture, and user docs.
This commit is contained in:
+1
-1
@@ -22,7 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Per-agent `displayName` for i18n. Agents can present localized names in UI and logs. (PR #4081)
|
- Per-agent `displayName` for i18n. Agents can present localized names in UI and logs. (PR #4081)
|
||||||
- Grok family models registered with `reasoningEffort` support. (PR #4186)
|
- Grok family models registered with `reasoningEffort` support. (PR #4186)
|
||||||
- CLI `setup` alias for `install`. Either command runs the interactive setup wizard. (PR #4174)
|
- CLI `setup` alias for `install`. Either command runs the interactive setup wizard. (PR #4174)
|
||||||
- Codex CLI Light edition (`omo-codex`): one-command install via `bunx omo install --platform=codex` or the new `lazycodex` bin entry. Vendored Codex plugin namespace `omo` with rules, comment-checker, LSP, ultrawork, and ultragoal components. Plugin lands in `~/.codex/plugins/cache/sisyphuslabs/omo/` and is enabled in `~/.codex/config.toml`. Idempotent installer (re-running is safe).
|
- Codex CLI Light edition (`omo-codex`): one-command install via `bunx omo install --platform=codex` or the new `lazycodex` bin entry. Vendored Codex plugin namespace `omo` with rules, comment-checker, LSP, ultrawork, and ulw-loop components. Plugin lands in `~/.codex/plugins/cache/sisyphuslabs/omo/` and is enabled in `~/.codex/config.toml`. Idempotent installer (re-running is safe).
|
||||||
- New `--platform <opencode|codex|both>` install flag (default `opencode`). Replaces the previous Codex-as-optional-addon model — `--platform=codex` installs only the Codex Light edition, `--platform=both` installs both editions in one run.
|
- New `--platform <opencode|codex|both>` install flag (default `opencode`). Replaces the previous Codex-as-optional-addon model — `--platform=codex` installs only the Codex Light edition, `--platform=both` installs both editions in one run.
|
||||||
- Three new bin entries: `omo` (short alias) and `lazycodex` (auto-defaults `--platform=codex`). Existing `oh-my-opencode` and `oh-my-openagent` continue to work unchanged.
|
- Three new bin entries: `omo` (short alias) and `lazycodex` (auto-defaults `--platform=codex`). Existing `oh-my-opencode` and `oh-my-openagent` continue to work unchanged.
|
||||||
- New PostHog telemetry stream `omo_codex_daily_active` distinguishing omo-codex installations from omo-opencode. Independent opt-out via `OMO_CODEX_DISABLE_POSTHOG=1` or `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`; global `OMO_DISABLE_POSTHOG` and `OMO_SEND_ANONYMOUS_TELEMETRY` still suppress both products.
|
- New PostHog telemetry stream `omo_codex_daily_active` distinguishing omo-codex installations from omo-opencode. Independent opt-out via `OMO_CODEX_DISABLE_POSTHOG=1` or `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`; global `OMO_DISABLE_POSTHOG` and `OMO_SEND_ANONYMOUS_TELEMETRY` still suppress both products.
|
||||||
|
|||||||
+3
-3
@@ -123,7 +123,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
|||||||
|
|
||||||
匿名のテレメトリは、アクティブなインストール数(DAU/WAU/MAU)の集計のためにデフォルトで有効になっています。マシン1台につきUTC日あたり最大1回イベントが送信され、ハッシュ化されたインストール識別子を使用し、生のホスト名は使用せず、PostHog person profile も作成されません。無効化するには `OMO_SEND_ANONYMOUS_TELEMETRY=0` または `OMO_DISABLE_POSTHOG=1` を設定してください。[プライバシーポリシー](docs/legal/privacy-policy.md)と[利用規約](docs/legal/terms-of-service.md)をご覧ください。
|
匿名のテレメトリは、アクティブなインストール数(DAU/WAU/MAU)の集計のためにデフォルトで有効になっています。マシン1台につきUTC日あたり最大1回イベントが送信され、ハッシュ化されたインストール識別子を使用し、生のホスト名は使用せず、PostHog person profile も作成されません。無効化するには `OMO_SEND_ANONYMOUS_TELEMETRY=0` または `OMO_DISABLE_POSTHOG=1` を設定してください。[プライバシーポリシー](docs/legal/privacy-policy.md)と[利用規約](docs/legal/terms-of-service.md)をご覧ください。
|
||||||
|
|
||||||
**Ultimate と Light:** oh-my-openagent は同じ製品の 2 つのエディションとして提供されます。**Ultimate エディション**(`bunx omo install` または `--platform=opencode`、デフォルト)は OpenCode 上のフル機能で、11 エージェント、54+ フック、Team Mode、すべての MCP、スラッシュコマンド、IntentGate モードを提供します。**Light エディション**(`bunx omo install --platform=codex`)は OpenAI Codex CLI のプラグインシステムへ綺麗に移植できる 5 コンポーネント(`rules`、`comment-checker`、`lsp`、`ultrawork`、`ultragoal`)のみを提供します。`bunx lazycodex install` は `--platform=codex` のショートカット別名です。両方を同時にインストールするには `--platform=both`。Codex 専用テレメトリは `OMO_CODEX_DISABLE_POSTHOG=1` または `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0` で無効化できます。
|
**Ultimate と Light:** oh-my-openagent は同じ製品の 2 つのエディションとして提供されます。**Ultimate エディション**(`bunx omo install` または `--platform=opencode`、デフォルト)は OpenCode 上のフル機能で、11 エージェント、54+ フック、Team Mode、すべての MCP、スラッシュコマンド、IntentGate モードを提供します。**Light エディション**(`bunx omo install --platform=codex`)は OpenAI Codex CLI のプラグインシステムへ綺麗に移植できる 5 コンポーネント(`rules`、`comment-checker`、`lsp`、`ultrawork`、`ulw-loop`)のみを提供します。`bunx lazycodex install` は `--platform=codex` のショートカット別名です。両方を同時にインストールするには `--platform=both`。Codex 専用テレメトリは `OMO_CODEX_DISABLE_POSTHOG=1` または `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0` で無効化できます。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| | 機能 | Editions | 何をするのか |
|
| | 機能 | Editions | 何をするのか |
|
||||||
| :---: | :------------------------------------------------------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---: | :------------------------------------------------------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 🤖 | **規律あるエージェント (Discipline Agents)** | Ultimate | Sisyphus が Hephaestus、Oracle、Librarian、Explore をオーケストレーションします。完全な AI 開発チームが並列で動きます。 |
|
| 🤖 | **規律あるエージェント (Discipline Agents)** | Ultimate | Sisyphus が Hephaestus、Oracle、Librarian、Explore をオーケストレーションします。完全な AI 開発チームが並列で動きます。 |
|
||||||
| 🧩 | **Codex CLI Light Edition** | Light | OpenAI Codex CLI 上で動作する omo の 5 つの移植コンポーネント (rules, comment-checker, LSP, ultrawork, ultragoal)。インストール: `bunx omo install --platform=codex`。 |
|
| 🧩 | **Codex CLI Light Edition** | Light | OpenAI Codex CLI 上で動作する omo の 5 つの移植コンポーネント (rules, comment-checker, LSP, ultrawork, ulw-loop)。インストール: `bunx omo install --platform=codex`。 |
|
||||||
| 👥 | **Team Mode** (v4.0, オプトイン) | Ultimate | リードエージェント + 最大 8 メンバーの並列実行、リアルタイム tmux 可視化、専用 `team_*` ツール群。`hyperplan`(5 人の敵対的批評家)と `security-research`(3 人のハンター + 2 人の PoC エンジニア)を駆動します。[ドキュメント →](docs/guide/team-mode.md) |
|
| 👥 | **Team Mode** (v4.0, オプトイン) | Ultimate | リードエージェント + 最大 8 メンバーの並列実行、リアルタイム tmux 可視化、専用 `team_*` ツール群。`hyperplan`(5 人の敵対的批評家)と `security-research`(3 人のハンター + 2 人の PoC エンジニア)を駆動します。[ドキュメント →](docs/guide/team-mode.md) |
|
||||||
| ⚡ | **`ultrawork` / `ulw`** | Both | 一言で OK。すべてのエージェント (Ultimate) または Codex `ultrawork` コンポーネント (Light) がアクティブになり、終わるまで止まりません。 |
|
| ⚡ | **`ultrawork` / `ulw`** | Both | 一言で OK。すべてのエージェント (Ultimate) または Codex `ultrawork` コンポーネント (Light) がアクティブになり、終わるまで止まりません。 |
|
||||||
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | ユーザーの真の意図を分析してから分類・行動します。`search` / `analyze` / `team` / `hyperplan` をトリガー。(Light は `ulw` / `ultrawork` のみフック。) |
|
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | ユーザーの真の意図を分析してから分類・行動します。`search` / `analyze` / `team` / `hyperplan` をトリガー。(Light は `ulw` / `ultrawork` のみフック。) |
|
||||||
@@ -167,7 +167,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | エージェントがサボる?システムが首根っこを掴んで戻します。あなたのタスクは必ず終わります。 |
|
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | エージェントがサボる?システムが首根っこを掴んで戻します。あなたのタスクは必ず終わります。 |
|
||||||
| 💬 | **コメントチェッカー** | Both | コメントから AI 臭い無駄話を排除。両エディションで同じ `@code-yeongyu/comment-checker` バイナリが動作。 |
|
| 💬 | **コメントチェッカー** | Both | コメントから AI 臭い無駄話を排除。両エディションで同じ `@code-yeongyu/comment-checker` バイナリが動作。 |
|
||||||
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` の階層的コンテキスト注入。Ultimate はフック、Light は `rules` コンポーネント。 |
|
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` の階層的コンテキスト注入。Ultimate はフック、Light は `rules` コンポーネント。 |
|
||||||
| 🧬 | **Ultragoal** | Light | `.omo/ultragoal/` evidence audit ベースの永続的マルチゴール オーケストレーション。現在は Codex 専用; OpenCode 側への移植はロードマップ。 |
|
| 🧬 | **Ulw Loop** | Light | `.omo/ulw-loop/` evidence audit ベースの永続的マルチゴール オーケストレーション。現在は Codex 専用; OpenCode 側への移植はロードマップ。 |
|
||||||
| 🖥️ | **Tmux 統合** | Ultimate | 完全なインタラクティブターミナル。REPL、デバッガー、TUI アプリがすべてリアルタイムで動きます。 |
|
| 🖥️ | **Tmux 統合** | Ultimate | 完全なインタラクティブターミナル。REPL、デバッガー、TUI アプリがすべてリアルタイムで動きます。 |
|
||||||
| 🔌 | **Claude Code 互換性** | Ultimate | 既存のフック、コマンド、スキル、MCP、プラグイン?すべてここでそのまま動きます。(Codex は独自のネイティブプラグインシステムを保有。) |
|
| 🔌 | **Claude Code 互換性** | Ultimate | 既存のフック、コマンド、スキル、MCP、プラグイン?すべてここでそのまま動きます。(Codex は独自のネイティブプラグインシステムを保有。) |
|
||||||
| 🎯 | **スキル内蔵 MCP** | Ultimate | スキルが独自の MCP サーバーを持ち歩きます。コンテキストが肥大化しません。 |
|
| 🎯 | **スキル内蔵 MCP** | Ultimate | スキルが独自の MCP サーバーを持ち歩きます。コンテキストが肥大化しません。 |
|
||||||
|
|||||||
+3
-3
@@ -124,7 +124,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
|||||||
|
|
||||||
익명 텔레메트리는 활성 설치 수(DAU/WAU/MAU) 집계를 위해 기본적으로 활성화되어 있습니다. 머신당 UTC 하루에 최대 1회만 이벤트가 전송되며, 해시된 설치 식별자를 사용하고 원시 호스트명은 절대 사용하지 않으며 PostHog person profile은 생성되지 않습니다. `OMO_SEND_ANONYMOUS_TELEMETRY=0` 또는 `OMO_DISABLE_POSTHOG=1`로 비활성화할 수 있습니다. [개인정보처리방침](docs/legal/privacy-policy.md)과 [서비스 이용약관](docs/legal/terms-of-service.md)을 참조하세요.
|
익명 텔레메트리는 활성 설치 수(DAU/WAU/MAU) 집계를 위해 기본적으로 활성화되어 있습니다. 머신당 UTC 하루에 최대 1회만 이벤트가 전송되며, 해시된 설치 식별자를 사용하고 원시 호스트명은 절대 사용하지 않으며 PostHog person profile은 생성되지 않습니다. `OMO_SEND_ANONYMOUS_TELEMETRY=0` 또는 `OMO_DISABLE_POSTHOG=1`로 비활성화할 수 있습니다. [개인정보처리방침](docs/legal/privacy-policy.md)과 [서비스 이용약관](docs/legal/terms-of-service.md)을 참조하세요.
|
||||||
|
|
||||||
**Ultimate vs Light:** oh-my-openagent는 같은 제품의 두 에디션으로 출시됩니다. **Ultimate 에디션**(`bunx omo install` 또는 `--platform=opencode`, 기본값)은 OpenCode 위에서 풀 기능 — 11 agent, 54+ hook, Team Mode, 모든 MCP, 슬래시 명령, IntentGate 모드 — 을 제공합니다. **Light 에디션**(`bunx omo install --platform=codex`)은 OpenAI Codex CLI의 플러그인 시스템에 깔끔히 포팅되는 5개 컴포넌트(`rules`, `comment-checker`, `lsp`, `ultrawork`, `ultragoal`)만 제공합니다. `bunx lazycodex install`은 `--platform=codex`의 단축 별칭입니다. 둘 다 설치하려면 `--platform=both`. Codex 전용 텔레메트리는 `OMO_CODEX_DISABLE_POSTHOG=1` 또는 `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`으로 비활성화할 수 있습니다.
|
**Ultimate vs Light:** oh-my-openagent는 같은 제품의 두 에디션으로 출시됩니다. **Ultimate 에디션**(`bunx omo install` 또는 `--platform=opencode`, 기본값)은 OpenCode 위에서 풀 기능 — 11 agent, 54+ hook, Team Mode, 모든 MCP, 슬래시 명령, IntentGate 모드 — 을 제공합니다. **Light 에디션**(`bunx omo install --platform=codex`)은 OpenAI Codex CLI의 플러그인 시스템에 깔끔히 포팅되는 5개 컴포넌트(`rules`, `comment-checker`, `lsp`, `ultrawork`, `ulw-loop`)만 제공합니다. `bunx lazycodex install`은 `--platform=codex`의 단축 별칭입니다. 둘 다 설치하려면 `--platform=both`. Codex 전용 텔레메트리는 `OMO_CODEX_DISABLE_POSTHOG=1` 또는 `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`으로 비활성화할 수 있습니다.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -156,7 +156,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| | 기능 | Editions | 하는 일 |
|
| | 기능 | Editions | 하는 일 |
|
||||||
| :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 🤖 | **Discipline Agents** | Ultimate | Sisyphus가 Hephaestus, Oracle, Librarian, Explore를 지휘합니다. 병렬로 도는 풀스택 AI 개발팀. |
|
| 🤖 | **Discipline Agents** | Ultimate | Sisyphus가 Hephaestus, Oracle, Librarian, Explore를 지휘합니다. 병렬로 도는 풀스택 AI 개발팀. |
|
||||||
| 🧩 | **Codex CLI Light Edition** | Light | OpenAI Codex CLI에서 동작하는 omo의 5개 포팅 컴포넌트(rules, comment-checker, LSP, ultrawork, ultragoal). 설치: `bunx omo install --platform=codex`. |
|
| 🧩 | **Codex CLI Light Edition** | Light | OpenAI Codex CLI에서 동작하는 omo의 5개 포팅 컴포넌트(rules, comment-checker, LSP, ultrawork, ulw-loop). 설치: `bunx omo install --platform=codex`. |
|
||||||
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | 리드 에이전트 + 최대 8명의 병렬 멤버, 실시간 tmux 시각화, 전용 `team_*` 도구. `hyperplan`(5명의 적대적 비평가)과 `security-research`(3명의 헌터 + 2명의 PoC 엔지니어)를 구동합니다. [문서 →](docs/guide/team-mode.md) |
|
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | 리드 에이전트 + 최대 8명의 병렬 멤버, 실시간 tmux 시각화, 전용 `team_*` 도구. `hyperplan`(5명의 적대적 비평가)과 `security-research`(3명의 헌터 + 2명의 PoC 엔지니어)를 구동합니다. [문서 →](docs/guide/team-mode.md) |
|
||||||
| ⚡ | **`ultrawork` / `ulw`** | Both | 한 단어. 모든 에이전트(Ultimate)나 Codex `ultrawork` 컴포넌트(Light)가 켜집니다. 끝날 때까지 멈추지 않습니다. |
|
| ⚡ | **`ultrawork` / `ulw`** | Both | 한 단어. 모든 에이전트(Ultimate)나 Codex `ultrawork` 컴포넌트(Light)가 켜집니다. 끝날 때까지 멈추지 않습니다. |
|
||||||
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | 분류하거나 행동하기 전에 사용자의 진짜 의도부터 분석합니다. `search` / `analyze` / `team` / `hyperplan` 트리거. (Light는 `ulw` / `ultrawork`만 hook.) |
|
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | 분류하거나 행동하기 전에 사용자의 진짜 의도부터 분석합니다. `search` / `analyze` / `team` / `hyperplan` 트리거. (Light는 `ulw` / `ultrawork`만 hook.) |
|
||||||
@@ -168,7 +168,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | 에이전트가 놀고 있나요? 시스템이 다시 끌어옵니다. 당신의 작업은 반드시 끝납니다. |
|
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | 에이전트가 놀고 있나요? 시스템이 다시 끌어옵니다. 당신의 작업은 반드시 끝납니다. |
|
||||||
| 💬 | **Comment Checker** | Both | 주석에 AI 슬롭 금지. 동일한 `@code-yeongyu/comment-checker` 바이너리가 두 에디션 모두에서 동작. |
|
| 💬 | **Comment Checker** | Both | 주석에 AI 슬롭 금지. 동일한 `@code-yeongyu/comment-checker` 바이너리가 두 에디션 모두에서 동작. |
|
||||||
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` 계층형 컨텍스트 주입. Ultimate은 hook, Light는 `rules` 컴포넌트. |
|
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` 계층형 컨텍스트 주입. Ultimate은 hook, Light는 `rules` 컴포넌트. |
|
||||||
| 🧬 | **Ultragoal** | Light | `.omo/ultragoal/` evidence audit 기반 영속 멀티 골 오케스트레이션. 현재 Codex 전용; OpenCode 사이드 포팅은 로드맵에 있음. |
|
| 🧬 | **Ulw Loop** | Light | `.omo/ulw-loop/` evidence audit 기반 영속 멀티 골 오케스트레이션. 현재 Codex 전용; OpenCode 사이드 포팅은 로드맵에 있음. |
|
||||||
| 🖥️ | **Tmux Integration** | Ultimate | 풀 인터랙티브 터미널. REPL, 디버거, TUI 전부 라이브. |
|
| 🖥️ | **Tmux Integration** | Ultimate | 풀 인터랙티브 터미널. REPL, 디버거, TUI 전부 라이브. |
|
||||||
| 🔌 | **Claude Code Compatible** | Ultimate | 쓰시던 hook, command, skill, MCP, plugin 전부 그대로 동작합니다. (Codex는 자체 플러그인 시스템 보유.) |
|
| 🔌 | **Claude Code Compatible** | Ultimate | 쓰시던 hook, command, skill, MCP, plugin 전부 그대로 동작합니다. (Codex는 자체 플러그인 시스템 보유.) |
|
||||||
| 🎯 | **Skill-Embedded MCPs** | Ultimate | 스킬이 자기만의 MCP 서버를 들고 다닙니다. 컨텍스트 낭비 없음. |
|
| 🎯 | **Skill-Embedded MCPs** | Ultimate | 스킬이 자기만의 MCP 서버를 들고 다닙니다. 컨텍스트 낭비 없음. |
|
||||||
|
|||||||
@@ -103,8 +103,8 @@ Install oh-my-openagent. Type `ultrawork`. Done.
|
|||||||
|
|
||||||
oh-my-openagent ships in two editions of the same product:
|
oh-my-openagent ships in two editions of the same product:
|
||||||
|
|
||||||
- **Ultimate Edition (omo for OpenCode)** — full omo. 11 agents, 54+ lifecycle hooks, 5 built-in MCPs, all slash commands, Team Mode, ultragoal, ultrawork, hashline edits — everything.
|
- **Ultimate Edition (omo for OpenCode)** — full omo. 11 agents, 54+ lifecycle hooks, 5 built-in MCPs, all slash commands, Team Mode, ulw-loop, ultrawork, hashline edits — everything.
|
||||||
- **Light Edition (omo for Codex CLI)** — the 5 components that port cleanly to Codex's plugin system: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ultragoal`. No agent orchestration, no `team_*` tools, no built-in MCPs beyond LSP — Codex CLI's own surface does that work.
|
- **Light Edition (omo for Codex CLI)** — the 5 components that port cleanly to Codex's plugin system: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ulw-loop`. No agent orchestration, no `team_*` tools, no built-in MCPs beyond LSP — Codex CLI's own surface does that work.
|
||||||
|
|
||||||
Pick the edition(s) you want.
|
Pick the edition(s) you want.
|
||||||
|
|
||||||
@@ -202,7 +202,7 @@ Even with only the following subscriptions, `ultrawork` works well (this project
|
|||||||
| | Feature | Edition | What it does |
|
| | Feature | Edition | What it does |
|
||||||
| :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 🤖 | **Discipline Agents** | Ultimate | Sisyphus orchestrates Hephaestus, Oracle, Librarian, Explore. A full AI dev team in parallel. |
|
| 🤖 | **Discipline Agents** | Ultimate | Sisyphus orchestrates Hephaestus, Oracle, Librarian, Explore. A full AI dev team in parallel. |
|
||||||
| 🧩 | **Codex CLI Light Edition** | Light | The 5 portable components of omo (rules, comment-checker, LSP, ultrawork, ultragoal) running inside OpenAI Codex CLI. Install via `bunx omo install --platform=codex`. |
|
| 🧩 | **Codex CLI Light Edition** | Light | The 5 portable components of omo (rules, comment-checker, LSP, ultrawork, ulw-loop) running inside OpenAI Codex CLI. Install via `bunx omo install --platform=codex`. |
|
||||||
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | Lead agent + up to 8 parallel members, real-time tmux visualization, dedicated `team_*` tools. Powers `hyperplan` (5 hostile critics) and `security-research` (3 hunters + 2 PoC engineers). [Docs →](docs/guide/team-mode.md) |
|
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | Lead agent + up to 8 parallel members, real-time tmux visualization, dedicated `team_*` tools. Powers `hyperplan` (5 hostile critics) and `security-research` (3 hunters + 2 PoC engineers). [Docs →](docs/guide/team-mode.md) |
|
||||||
| ⚡ | **`ultrawork` / `ulw`** | Both | One word. Every agent activates. Doesn't stop until done. |
|
| ⚡ | **`ultrawork` / `ulw`** | Both | One word. Every agent activates. Doesn't stop until done. |
|
||||||
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | Analyzes true user intent before classifying or acting. No more literal misinterpretations. (Light edition only recognises the `ultrawork`/`ulw` keyword.) |
|
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | Analyzes true user intent before classifying or acting. No more literal misinterpretations. (Light edition only recognises the `ultrawork`/`ulw` keyword.) |
|
||||||
@@ -215,7 +215,7 @@ Even with only the following subscriptions, `ultrawork` works well (this project
|
|||||||
| ✅ | **Todo Enforcer** | Ultimate | Agent goes idle? System yanks it back. Your task gets done, period. |
|
| ✅ | **Todo Enforcer** | Ultimate | Agent goes idle? System yanks it back. Your task gets done, period. |
|
||||||
| 💬 | **Comment Checker** | Both | No AI slop in comments. Code reads like a senior wrote it. |
|
| 💬 | **Comment Checker** | Both | No AI slop in comments. Code reads like a senior wrote it. |
|
||||||
| 📐 | **Rules Injection** (`AGENTS.md` / `.omo/rules/**`) | Both | Project rules and AGENTS.md auto-loaded into the agent's context at every prompt. |
|
| 📐 | **Rules Injection** (`AGENTS.md` / `.omo/rules/**`) | Both | Project rules and AGENTS.md auto-loaded into the agent's context at every prompt. |
|
||||||
| 🎯 | **Ultragoal** | Both | Durable multi-goal orchestration with evidence audit, backed by `.omo/ultragoal/`. |
|
| 🎯 | **Ulw Loop** | Both | Durable multi-goal orchestration with evidence audit, backed by `.omo/ulw-loop/`. |
|
||||||
| 🖥️ | **Tmux Integration** | Ultimate | Full interactive terminal. REPLs, debuggers, TUIs. All live. |
|
| 🖥️ | **Tmux Integration** | Ultimate | Full interactive terminal. REPLs, debuggers, TUIs. All live. |
|
||||||
| 🔌 | **Claude Code Compatible** | Ultimate | Your hooks, commands, skills, MCPs, and plugins? All work here. |
|
| 🔌 | **Claude Code Compatible** | Ultimate | Your hooks, commands, skills, MCPs, and plugins? All work here. |
|
||||||
| 🧬 | **Skill-Embedded MCPs** | Ultimate | Skills carry their own MCP servers. No context bloat. |
|
| 🧬 | **Skill-Embedded MCPs** | Ultimate | Skills carry their own MCP servers. No context bloat. |
|
||||||
|
|||||||
+3
-3
@@ -121,7 +121,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
|||||||
|
|
||||||
Анонимная телеметрия включена по умолчанию для подсчёта активных установок (DAU/WAU/MAU). Не более одного события на машину за UTC-сутки, использует хешированный идентификатор установки, никогда не использует исходное имя хоста, и не создаёт PostHog person profile. Можно отключить через `OMO_SEND_ANONYMOUS_TELEMETRY=0` или `OMO_DISABLE_POSTHOG=1`. См. [Политику конфиденциальности](docs/legal/privacy-policy.md) и [Условия обслуживания](docs/legal/terms-of-service.md).
|
Анонимная телеметрия включена по умолчанию для подсчёта активных установок (DAU/WAU/MAU). Не более одного события на машину за UTC-сутки, использует хешированный идентификатор установки, никогда не использует исходное имя хоста, и не создаёт PostHog person profile. Можно отключить через `OMO_SEND_ANONYMOUS_TELEMETRY=0` или `OMO_DISABLE_POSTHOG=1`. См. [Политику конфиденциальности](docs/legal/privacy-policy.md) и [Условия обслуживания](docs/legal/terms-of-service.md).
|
||||||
|
|
||||||
**Ultimate и Light:** oh-my-openagent поставляется в двух редакциях одного продукта. **Ultimate** (`bunx omo install` или `--platform=opencode`, по умолчанию) — полнофункциональная редакция поверх OpenCode: 11 агентов, 54+ хука, Team Mode, все MCP, все слэш-команды, режимы IntentGate. **Light** (`bunx omo install --platform=codex`) — только 5 компонентов omo, которые портируются в систему плагинов OpenAI Codex CLI: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ultragoal`. `bunx lazycodex install` — это сокращённый псевдоним для `--platform=codex`. Чтобы установить обе редакции одной командой, используйте `--platform=both`. Телеметрию только для Codex можно отключить через `OMO_CODEX_DISABLE_POSTHOG=1` или `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`.
|
**Ultimate и Light:** oh-my-openagent поставляется в двух редакциях одного продукта. **Ultimate** (`bunx omo install` или `--platform=opencode`, по умолчанию) — полнофункциональная редакция поверх OpenCode: 11 агентов, 54+ хука, Team Mode, все MCP, все слэш-команды, режимы IntentGate. **Light** (`bunx omo install --platform=codex`) — только 5 компонентов omo, которые портируются в систему плагинов OpenAI Codex CLI: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ulw-loop`. `bunx lazycodex install` — это сокращённый псевдоним для `--platform=codex`. Чтобы установить обе редакции одной командой, используйте `--platform=both`. Телеметрию только для Codex можно отключить через `OMO_CODEX_DISABLE_POSTHOG=1` или `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0`.
|
||||||
|
|
||||||
------
|
------
|
||||||
|
|
||||||
@@ -154,7 +154,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| | Функция | Editions | Что делает |
|
| | Функция | Editions | Что делает |
|
||||||
| --- | -------------------------------------------------------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
| --- | -------------------------------------------------------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||||
| 🤖 | **Дисциплинированные агенты** | Ultimate | Sisyphus оркестрирует Hephaestus, Oracle, Librarian, Explore. Полноценная AI-команда разработки в параллельном режиме. |
|
| 🤖 | **Дисциплинированные агенты** | Ultimate | Sisyphus оркестрирует Hephaestus, Oracle, Librarian, Explore. Полноценная AI-команда разработки в параллельном режиме. |
|
||||||
| 🧩 | **Codex CLI Light Edition** | Light | 5 компонентов omo, портированных в OpenAI Codex CLI (rules, comment-checker, LSP, ultrawork, ultragoal). Установка: `bunx omo install --platform=codex`. |
|
| 🧩 | **Codex CLI Light Edition** | Light | 5 компонентов omo, портированных в OpenAI Codex CLI (rules, comment-checker, LSP, ultrawork, ulw-loop). Установка: `bunx omo install --platform=codex`. |
|
||||||
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | Лид-агент + до 8 параллельных участников, визуализация в tmux в реальном времени, выделенные инструменты `team_*`. Питает `hyperplan` (5 враждебных критиков) и `security-research` (3 охотника + 2 PoC-инженера). [Документация →](docs/guide/team-mode.md) |
|
| 👥 | **Team Mode** (v4.0, opt-in) | Ultimate | Лид-агент + до 8 параллельных участников, визуализация в tmux в реальном времени, выделенные инструменты `team_*`. Питает `hyperplan` (5 враждебных критиков) и `security-research` (3 охотника + 2 PoC-инженера). [Документация →](docs/guide/team-mode.md) |
|
||||||
| ⚡ | **`ultrawork` / `ulw`** | Both | Одно слово. Все агенты (Ultimate) или Codex-компонент `ultrawork` (Light) активируются. Не останавливается, пока задача не выполнена. |
|
| ⚡ | **`ultrawork` / `ulw`** | Both | Одно слово. Все агенты (Ultimate) или Codex-компонент `ultrawork` (Light) активируются. Не останавливается, пока задача не выполнена. |
|
||||||
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | Анализирует истинное намерение пользователя перед классификацией и действием. Триггеры `search` / `analyze` / `team` / `hyperplan`. (Light хукает только `ulw` / `ultrawork`.) |
|
| 🚪 | **[IntentGate](https://factory.ai/news/terminal-bench)** | Ultimate | Анализирует истинное намерение пользователя перед классификацией и действием. Триггеры `search` / `analyze` / `team` / `hyperplan`. (Light хукает только `ulw` / `ultrawork`.) |
|
||||||
@@ -166,7 +166,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | Агент завис? Система немедленно возвращает его в работу. Ваша задача будет выполнена, точка. |
|
| ✅ | **Todo Enforcer** (Boulder) | Ultimate | Агент завис? Система немедленно возвращает его в работу. Ваша задача будет выполнена, точка. |
|
||||||
| 💬 | **Comment Checker** | Both | Никакого AI-мусора в комментариях. Тот же бинарник `@code-yeongyu/comment-checker` работает в обеих редакциях. |
|
| 💬 | **Comment Checker** | Both | Никакого AI-мусора в комментариях. Тот же бинарник `@code-yeongyu/comment-checker` работает в обеих редакциях. |
|
||||||
| 📜 | **Rules Injection** | Both | Иерархическое внедрение контекста из `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**`. В Ultimate это хук, в Light — компонент `rules`. |
|
| 📜 | **Rules Injection** | Both | Иерархическое внедрение контекста из `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**`. В Ultimate это хук, в Light — компонент `rules`. |
|
||||||
| 🧬 | **Ultragoal** | Light | Долговечная оркестрация нескольких целей с аудитом доказательств в `.omo/ultragoal/`. Сейчас только в Codex; порт в сторону OpenCode в дорожной карте. |
|
| 🧬 | **Ulw Loop** | Light | Долговечная оркестрация нескольких целей с аудитом доказательств в `.omo/ulw-loop/`. Сейчас только в Codex; порт в сторону OpenCode в дорожной карте. |
|
||||||
| 🖥️ | **Интеграция с Tmux** | Ultimate | Полноценный интерактивный терминал. REPL, дебаггеры, TUI. Всё живое. |
|
| 🖥️ | **Интеграция с Tmux** | Ultimate | Полноценный интерактивный терминал. REPL, дебаггеры, TUI. Всё живое. |
|
||||||
| 🔌 | **Совместимость с Claude Code** | Ultimate | Ваши хуки, команды, навыки, MCP и плагины? Всё работает без изменений. (У Codex своя нативная плагин-система.) |
|
| 🔌 | **Совместимость с Claude Code** | Ultimate | Ваши хуки, команды, навыки, MCP и плагины? Всё работает без изменений. (У Codex своя нативная плагин-система.) |
|
||||||
| 🎯 | **MCP, встроенные в навыки** | Ultimate | Навыки несут собственные MCP-серверы. Никакого раздувания контекста. |
|
| 🎯 | **MCP, встроенные в навыки** | Ultimate | Навыки несут собственные MCP-серверы. Никакого раздувания контекста. |
|
||||||
|
|||||||
+3
-3
@@ -123,7 +123,7 @@ curl -s https://raw.githubusercontent.com/code-yeongyu/oh-my-openagent/refs/head
|
|||||||
|
|
||||||
匿名遥测默认开启,用于统计活跃安装数(DAU/WAU/MAU)。每台机器每个 UTC 日最多发送一次事件,使用哈希化的安装标识符,绝不会使用原始主机名,且不会创建 PostHog person profile。可通过 `OMO_SEND_ANONYMOUS_TELEMETRY=0` 或 `OMO_DISABLE_POSTHOG=1` 禁用。详见 [隐私政策](docs/legal/privacy-policy.md) 和 [服务条款](docs/legal/terms-of-service.md)。
|
匿名遥测默认开启,用于统计活跃安装数(DAU/WAU/MAU)。每台机器每个 UTC 日最多发送一次事件,使用哈希化的安装标识符,绝不会使用原始主机名,且不会创建 PostHog person profile。可通过 `OMO_SEND_ANONYMOUS_TELEMETRY=0` 或 `OMO_DISABLE_POSTHOG=1` 禁用。详见 [隐私政策](docs/legal/privacy-policy.md) 和 [服务条款](docs/legal/terms-of-service.md)。
|
||||||
|
|
||||||
**Ultimate 与 Light:** oh-my-openagent 以同一产品的两个版本发布。**Ultimate 版本**(`bunx omo install` 或 `--platform=opencode`,默认值)在 OpenCode 上提供完整功能 —— 11 个智能体、54+ 个生命周期钩子、Team Mode、所有 MCP、所有斜杠命令、IntentGate 模式。**Light 版本**(`bunx omo install --platform=codex`)仅提供能够干净地移植到 OpenAI Codex CLI 插件系统的 5 个组件(`rules`、`comment-checker`、`lsp`、`ultrawork`、`ultragoal`)。`bunx lazycodex install` 是 `--platform=codex` 的快捷别名。要同时安装两个版本,使用 `--platform=both`。Codex 专用遥测可通过 `OMO_CODEX_DISABLE_POSTHOG=1` 或 `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0` 禁用。
|
**Ultimate 与 Light:** oh-my-openagent 以同一产品的两个版本发布。**Ultimate 版本**(`bunx omo install` 或 `--platform=opencode`,默认值)在 OpenCode 上提供完整功能 —— 11 个智能体、54+ 个生命周期钩子、Team Mode、所有 MCP、所有斜杠命令、IntentGate 模式。**Light 版本**(`bunx omo install --platform=codex`)仅提供能够干净地移植到 OpenAI Codex CLI 插件系统的 5 个组件(`rules`、`comment-checker`、`lsp`、`ultrawork`、`ulw-loop`)。`bunx lazycodex install` 是 `--platform=codex` 的快捷别名。要同时安装两个版本,使用 `--platform=both`。Codex 专用遥测可通过 `OMO_CODEX_DISABLE_POSTHOG=1` 或 `OMO_CODEX_SEND_ANONYMOUS_TELEMETRY=0` 禁用。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -155,7 +155,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| | 特性 | Editions | 功能说明 |
|
| | 特性 | Editions | 功能说明 |
|
||||||
| :---: | :-------------------------------------------------------------- | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
| :---: | :-------------------------------------------------------------- | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||||
| 🤖 | **自律军团 (Discipline Agents)** | Ultimate | Sisyphus 负责调度 Hephaestus、Oracle、Librarian 和 Explore。一支完整的 AI 开发团队并行工作。 |
|
| 🤖 | **自律军团 (Discipline Agents)** | Ultimate | Sisyphus 负责调度 Hephaestus、Oracle、Librarian 和 Explore。一支完整的 AI 开发团队并行工作。 |
|
||||||
| 🧩 | **Codex CLI Light Edition** | Light | 在 OpenAI Codex CLI 中运行的 omo 的 5 个可移植组件 (rules, comment-checker, LSP, ultrawork, ultragoal)。安装: `bunx omo install --platform=codex`。 |
|
| 🧩 | **Codex CLI Light Edition** | Light | 在 OpenAI Codex CLI 中运行的 omo 的 5 个可移植组件 (rules, comment-checker, LSP, ultrawork, ulw-loop)。安装: `bunx omo install --platform=codex`。 |
|
||||||
| 👥 | **Team Mode** (v4.0, 选择性启用) | Ultimate | 领导 Agent + 最多 8 个并行成员,实时 tmux 可视化,专用 `team_*` 工具家族。驱动 `hyperplan`(5 个敌对评论者) 和 `security-research`(3 个猎手 + 2 个 PoC 工程师)。[文档 →](docs/guide/team-mode.md) |
|
| 👥 | **Team Mode** (v4.0, 选择性启用) | Ultimate | 领导 Agent + 最多 8 个并行成员,实时 tmux 可视化,专用 `team_*` 工具家族。驱动 `hyperplan`(5 个敌对评论者) 和 `security-research`(3 个猎手 + 2 个 PoC 工程师)。[文档 →](docs/guide/team-mode.md) |
|
||||||
| ⚡ | **`ultrawork` / `ulw`** | Both | 一键触发,所有智能体(Ultimate)或 Codex `ultrawork` 组件(Light)出动。任务完成前绝不罢休。 |
|
| ⚡ | **`ultrawork` / `ulw`** | Both | 一键触发,所有智能体(Ultimate)或 Codex `ultrawork` 组件(Light)出动。任务完成前绝不罢休。 |
|
||||||
| 🚪 | **[IntentGate 意图门](https://factory.ai/news/terminal-bench)** | Ultimate | 真正行动前,先分析用户的真实意图。触发 `search` / `analyze` / `team` / `hyperplan`。(Light 仅 hook `ulw` / `ultrawork`。) |
|
| 🚪 | **[IntentGate 意图门](https://factory.ai/news/terminal-bench)** | Ultimate | 真正行动前,先分析用户的真实意图。触发 `search` / `analyze` / `team` / `hyperplan`。(Light 仅 hook `ulw` / `ultrawork`。) |
|
||||||
@@ -167,7 +167,7 @@ Read this and tell me why it's not just another boilerplate: https://raw.githubu
|
|||||||
| ✅ | **Todo 强制执行** (Boulder) | Ultimate | Agent 想要摸鱼?系统直接揪着领子拽回来。你的任务,必须完成。 |
|
| ✅ | **Todo 强制执行** (Boulder) | Ultimate | Agent 想要摸鱼?系统直接揪着领子拽回来。你的任务,必须完成。 |
|
||||||
| 💬 | **注释审查员** | Both | 剔除带有浓烈 AI 味的冗余注释。同一个 `@code-yeongyu/comment-checker` 二进制在两个版本中运行。 |
|
| 💬 | **注释审查员** | Both | 剔除带有浓烈 AI 味的冗余注释。同一个 `@code-yeongyu/comment-checker` 二进制在两个版本中运行。 |
|
||||||
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` 的分层上下文注入。Ultimate 中为 hook,Light 中为 `rules` 组件。 |
|
| 📜 | **Rules Injection** | Both | `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` 的分层上下文注入。Ultimate 中为 hook,Light 中为 `rules` 组件。 |
|
||||||
| 🧬 | **Ultragoal** | Light | 基于 `.omo/ultragoal/` 证据审计的持久化多目标编排。目前仅 Codex 可用; OpenCode 侧的移植在路线图上。 |
|
| 🧬 | **Ulw Loop** | Light | 基于 `.omo/ulw-loop/` 证据审计的持久化多目标编排。目前仅 Codex 可用; OpenCode 侧的移植在路线图上。 |
|
||||||
| 🖥️ | **Tmux 集成** | Ultimate | 完整的交互式终端支持。跑 REPL、用调试器、用 TUI 工具,全都在实时会话中完成。 |
|
| 🖥️ | **Tmux 集成** | Ultimate | 完整的交互式终端支持。跑 REPL、用调试器、用 TUI 工具,全都在实时会话中完成。 |
|
||||||
| 🔌 | **Claude Code 兼容** | Ultimate | 你现有的 Hooks、命令、技能、MCP 和插件?全都能无缝迁移过来。(Codex 拥有其自己的原生插件系统。) |
|
| 🔌 | **Claude Code 兼容** | Ultimate | 你现有的 Hooks、命令、技能、MCP 和插件?全都能无缝迁移过来。(Codex 拥有其自己的原生插件系统。) |
|
||||||
| 🎯 | **技能内嵌 MCP** | Ultimate | 技能自带其所需的 MCP 服务器。按需开启,不会撑爆你的上下文窗口。 |
|
| 🎯 | **技能内嵌 MCP** | Ultimate | 技能自带其所需的 MCP 服务器。按需开启,不会撑爆你的上下文窗口。 |
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
oh-my-openagent ships in **two editions** of the same product:
|
oh-my-openagent ships in **two editions** of the same product:
|
||||||
|
|
||||||
- **Ultimate Edition (omo for [OpenCode](https://opencode.ai))** — the full omo experience. 11 discipline agents, 54+ lifecycle hooks, all built-in MCPs, every slash command, Team Mode, ultragoal, hashline edits, the works.
|
- **Ultimate Edition (omo for [OpenCode](https://opencode.ai))** — the full omo experience. 11 discipline agents, 54+ lifecycle hooks, all built-in MCPs, every slash command, Team Mode, ulw-loop, hashline edits, the works.
|
||||||
- **Light Edition (omo for [OpenAI Codex CLI](https://github.com/openai/codex))** — the 5 components that port cleanly to Codex's plugin system: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ultragoal`. No agent orchestration, no `team_*` tools, no built-in web/docs/code search MCPs — Codex CLI's native surface does that work.
|
- **Light Edition (omo for [OpenAI Codex CLI](https://github.com/openai/codex))** — the 5 components that port cleanly to Codex's plugin system: `rules`, `comment-checker`, `lsp`, `ultrawork`, `ulw-loop`. No agent orchestration, no `team_*` tools, no built-in web/docs/code search MCPs — Codex CLI's native surface does that work.
|
||||||
|
|
||||||
Most users want **Ultimate**. Pick **Light** if you are already invested in Codex CLI. Pick **both** if you want OMO available wherever you happen to be working that day.
|
Most users want **Ultimate**. Pick **Light** if you are already invested in Codex CLI. Pick **both** if you want OMO available wherever you happen to be working that day.
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@ grep -A4 'marketplaces.sisyphuslabs' ~/.codex/config.toml
|
|||||||
grep -A2 'omo@sisyphuslabs' ~/.codex/config.toml
|
grep -A2 'omo@sisyphuslabs' ~/.codex/config.toml
|
||||||
|
|
||||||
# Component binaries linked?
|
# Component binaries linked?
|
||||||
ls ~/.local/bin/ | grep -E 'omo-(rules|comment-checker|lsp|ultrawork|ultragoal)'
|
ls ~/.local/bin/ | grep -E 'omo-(rules|comment-checker|lsp|ultrawork|ulw-loop)'
|
||||||
|
|
||||||
# Codex CLI sees the plugin?
|
# Codex CLI sees the plugin?
|
||||||
codex --help
|
codex --help
|
||||||
@@ -546,7 +546,7 @@ Skip this section if `--platform=opencode`. Otherwise, the user installed the **
|
|||||||
#### What was installed
|
#### What was installed
|
||||||
|
|
||||||
- **Plugin cache:** `~/.codex/plugins/cache/sisyphuslabs/omo/<version>/`
|
- **Plugin cache:** `~/.codex/plugins/cache/sisyphuslabs/omo/<version>/`
|
||||||
- **Component binaries:** `~/.local/bin/omo-rules`, `omo-comment-checker`, `omo-lsp`, `omo-ultrawork`, `omo-ultragoal` (or `$CODEX_LOCAL_BIN_DIR/omo-*` if set)
|
- **Component binaries:** `~/.local/bin/omo-rules`, `omo-comment-checker`, `omo-lsp`, `omo-ultrawork`, `omo-ulw-loop` (or `$CODEX_LOCAL_BIN_DIR/omo-*` if set)
|
||||||
- **Codex config edits:** `~/.codex/config.toml` gained `[features] plugins = true`, `[features] plugin_hooks = true`, `[marketplaces.sisyphuslabs]` pointing at `https://github.com/code-yeongyu/lazycodex.git`, `[plugins."omo@sisyphuslabs"]`, and SHA256-pinned `[hooks.state."omo@sisyphuslabs:..."]` entries
|
- **Codex config edits:** `~/.codex/config.toml` gained `[features] plugins = true`, `[features] plugin_hooks = true`, `[marketplaces.sisyphuslabs]` pointing at `https://github.com/code-yeongyu/lazycodex.git`, `[plugins."omo@sisyphuslabs"]`, and SHA256-pinned `[hooks.state."omo@sisyphuslabs:..."]` entries
|
||||||
|
|
||||||
#### The 5 components
|
#### The 5 components
|
||||||
@@ -557,7 +557,7 @@ Skip this section if `--platform=opencode`. Otherwise, the user installed the **
|
|||||||
| `comment-checker` | TypeScript | `PostToolUse` (`apply_patch`, `edit`, `write`) | Blocks AI-slop comment patterns in generated code |
|
| `comment-checker` | TypeScript | `PostToolUse` (`apply_patch`, `edit`, `write`) | Blocks AI-slop comment patterns in generated code |
|
||||||
| `lsp` | TypeScript + MCP | MCP server + post-edit hooks | Exposes LSP diagnostics, navigation, symbols, rename via MCP |
|
| `lsp` | TypeScript + MCP | MCP server + post-edit hooks | Exposes LSP diagnostics, navigation, symbols, rename via MCP |
|
||||||
| `ultrawork` | Python | `SessionStart` + keyword detector | Detects `ulw`/`ultrawork` keyword; syncs bundled agent TOML files into `$CODEX_HOME/agents` |
|
| `ultrawork` | Python | `SessionStart` + keyword detector | Detects `ulw`/`ultrawork` keyword; syncs bundled agent TOML files into `$CODEX_HOME/agents` |
|
||||||
| `ultragoal` | TypeScript | Durable orchestration via `.omo/ultragoal/` | Multi-goal orchestration with evidence audit trail |
|
| `ulw-loop` | TypeScript | Durable orchestration via `.omo/ulw-loop/` | Multi-goal orchestration with evidence audit trail |
|
||||||
|
|
||||||
#### Coexistence with OpenCode
|
#### Coexistence with OpenCode
|
||||||
|
|
||||||
@@ -768,7 +768,7 @@ rm -rf ~/.codex/plugins/cache/sisyphuslabs
|
|||||||
|
|
||||||
# 3. Optional: remove the component symlinks
|
# 3. Optional: remove the component symlinks
|
||||||
rm -f ~/.local/bin/omo-rules ~/.local/bin/omo-comment-checker \
|
rm -f ~/.local/bin/omo-rules ~/.local/bin/omo-comment-checker \
|
||||||
~/.local/bin/omo-lsp ~/.local/bin/omo-ultrawork ~/.local/bin/omo-ultragoal
|
~/.local/bin/omo-lsp ~/.local/bin/omo-ultrawork ~/.local/bin/omo-ulw-loop
|
||||||
```
|
```
|
||||||
|
|
||||||
## Operational notes
|
## Operational notes
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Native Codex marketplace for the `omo` plugin.
|
|||||||
- `components/rules`: injects local project rule files into Codex context through lifecycle hooks.
|
- `components/rules`: injects local project rule files into Codex context through lifecycle hooks.
|
||||||
- `components/lsp`: exposes Language Server Protocol diagnostics, navigation, symbols, and rename tools through MCP and post-edit hooks.
|
- `components/lsp`: exposes Language Server Protocol diagnostics, navigation, symbols, and rename tools through MCP and post-edit hooks.
|
||||||
- `components/ultrawork`: injects the ultrawork orchestration directive when a user prompt contains `ultrawork` or `ulw`.
|
- `components/ultrawork`: injects the ultrawork orchestration directive when a user prompt contains `ultrawork` or `ulw`.
|
||||||
- `components/ultragoal`: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit (`.omo/ultragoal/`).
|
- `components/ulw-loop`: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit (`.omo/ulw-loop/`).
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# @oh-my-opencode/omo-codex
|
# @oh-my-opencode/omo-codex
|
||||||
|
|
||||||
Codex harness adapter for **oh-my-openagent**. Brings the OMO experience (rules injection, comment checker, LSP MCP, ultrawork, ultragoal, start-work continuation) into [OpenAI Codex CLI](https://github.com/openai/codex) through Codex's native plugin system.
|
Codex harness adapter for **oh-my-openagent**. Brings the OMO experience (rules injection, comment checker, LSP MCP, ultrawork, ulw-loop, start-work continuation) into [OpenAI Codex CLI](https://github.com/openai/codex) through Codex's native plugin system.
|
||||||
|
|
||||||
## Layout
|
## Layout
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ Codex harness adapter for **oh-my-openagent**. Brings the OMO experience (rules
|
|||||||
- `comment-checker` (TypeScript) - runs `@code-yeongyu/comment-checker` after `apply_patch` / `edit` / `write` tool use.
|
- `comment-checker` (TypeScript) - runs `@code-yeongyu/comment-checker` after `apply_patch` / `edit` / `write` tool use.
|
||||||
- `lsp` (TypeScript + LSP MCP) - exposes LSP diagnostics, navigation, symbols, rename via MCP + post-edit hooks.
|
- `lsp` (TypeScript + LSP MCP) - exposes LSP diagnostics, navigation, symbols, rename via MCP + post-edit hooks.
|
||||||
- `ultrawork` (TypeScript) - keyword detector (`ulw` / `ultrawork`) that injects the full ultrawork directive; bundled agent TOML files are installed into `CODEX_HOME/agents`.
|
- `ultrawork` (TypeScript) - keyword detector (`ulw` / `ultrawork`) that injects the full ultrawork directive; bundled agent TOML files are installed into `CODEX_HOME/agents`.
|
||||||
- `ultragoal` (TypeScript) - durable multi-goal orchestration backed by `.omo/ultragoal/` evidence audit.
|
- `ulw-loop` (TypeScript) - durable multi-goal orchestration backed by `.omo/ulw-loop/` evidence audit.
|
||||||
- `start-work-continuation` (TypeScript) - `Stop` / `SubagentStop` continuation hook for `.omo/boulder.json` start-work plans.
|
- `start-work-continuation` (TypeScript) - `Stop` / `SubagentStop` continuation hook for `.omo/boulder.json` start-work plans.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -75,5 +75,5 @@ The bundled component implementations come from the Sisyphus Labs Codex plugin f
|
|||||||
- [code-yeongyu/codex-comment-checker](https://github.com/code-yeongyu/codex-comment-checker)
|
- [code-yeongyu/codex-comment-checker](https://github.com/code-yeongyu/codex-comment-checker)
|
||||||
- [code-yeongyu/codex-lsp](https://github.com/code-yeongyu/codex-lsp)
|
- [code-yeongyu/codex-lsp](https://github.com/code-yeongyu/codex-lsp)
|
||||||
- [code-yeongyu/codex-ultrawork](https://github.com/code-yeongyu/codex-ultrawork)
|
- [code-yeongyu/codex-ultrawork](https://github.com/code-yeongyu/codex-ultrawork)
|
||||||
- [code-yeongyu/codex-ultragoal](https://github.com/code-yeongyu/codex-ultragoal)
|
- [code-yeongyu/codex-ulw-loop](https://github.com/code-yeongyu/codex-ulw-loop)
|
||||||
- [code-yeongyu/codex-start-work-continuation](https://github.com/code-yeongyu/codex-start-work-continuation)
|
- [code-yeongyu/codex-start-work-continuation](https://github.com/code-yeongyu/codex-start-work-continuation)
|
||||||
|
|||||||
@@ -8,6 +8,6 @@ Internally each component remains isolated under `components/`:
|
|||||||
- `components/rules`
|
- `components/rules`
|
||||||
- `components/lsp`
|
- `components/lsp`
|
||||||
- `components/ultrawork`
|
- `components/ultrawork`
|
||||||
- `components/ultragoal`
|
- `components/ulw-loop`
|
||||||
|
|
||||||
The root plugin manifest exports one Codex plugin named `omo`, with aggregate hooks, skills, and the LSP MCP server.
|
The root plugin manifest exports one Codex plugin named `omo`, with aggregate hooks, skills, and the LSP MCP server.
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
---
|
|
||||||
name: ulw-loop
|
|
||||||
description: Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.
|
|
||||||
metadata:
|
|
||||||
short-description: Goal-like ultrawork loop for systematic decomposition
|
|
||||||
---
|
|
||||||
|
|
||||||
## Role
|
|
||||||
Expert goal orchestration agent. Plan multi-goal work that survives across turns and sessions.
|
|
||||||
Use GPT-5.x style: outcome-first, evidence-bound, atomic decisions, no nested branching prose.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Deliver every goal in `.omo/ultragoal/goals.json` end-to-end.
|
|
||||||
Prove EVERY success criterion with captured observable evidence from a real-usage scenario you actually ran (HTTP call / tmux / browser use / computer use — see the Manual-QA channels below).
|
|
||||||
TESTS ALONE NEVER PROVE DONE. A green test suite is supporting evidence, not completion proof.
|
|
||||||
Audit each pass, fail, block, steering change, and checkpoint in `.omo/ultragoal/ledger.jsonl`.
|
|
||||||
|
|
||||||
## Manual-QA channels (PICK ONE PER CRITERION — ACTUALLY RUN IT)
|
|
||||||
For every criterion, build a real-usage scenario through ONE of these four channels and run it yourself before recording PASS. The full test suite being green is NEVER verification on its own.
|
|
||||||
|
|
||||||
1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
|
|
||||||
2. **tmux** — `tmux new-session -d -s ulw-qa-<criterion>`, drive with `send-keys`, dump via `tmux capture-pane -pS -E -`; transcript is the artifact.
|
|
||||||
3. **Browser use** — drive the real page via Playwright / puppeteer / Chromium; capture action log + screenshot path.
|
|
||||||
4. **Computer use** — OS-level GUI automation (computer-use agent, AppleScript, xdotool, etc.) against the running app; capture action log + screenshot.
|
|
||||||
|
|
||||||
Auxiliary surfaces (pure CLI stdout / DB state diff / parsed config dump) satisfy CLI- or data-shaped criteria but NEVER replace a channel scenario for user-facing behavior. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
|
||||||
|
|
||||||
## Artifacts
|
|
||||||
- `.omo/ultragoal/brief.md`: original brief and durable constraints.
|
|
||||||
- `.omo/ultragoal/goals.json`: goals with embedded `successCriteria` per goal.
|
|
||||||
- `.omo/ultragoal/ledger.jsonl`: append-only audit trail.
|
|
||||||
- Read artifacts before resuming, steering, or checkpointing.
|
|
||||||
- Never invent state outside `.omo/ultragoal` artifacts or `omo ultragoal status --json`.
|
|
||||||
|
|
||||||
## Bootstrap
|
|
||||||
Do all three steps before execution. No edits, goal tools, or checkpointing before bootstrap completes.
|
|
||||||
|
|
||||||
### 1. Create goals from the brief
|
|
||||||
Resolve the CLI before the first command. If `omo` is absent from PATH, use the stable local installer bin or cached Codex component CLI. This is the same ultragoal CLI, so PATH absence is not a blocker. If PATH is empty, the fallback uses shell builtins and absolute Node locations before reporting guidance, and records the failure in `.omo/ultragoal/bootstrap-notepad.md`.
|
|
||||||
```sh
|
|
||||||
if command -v omo >/dev/null 2>&1; then
|
|
||||||
ULTRAGOAL_CLI=omo
|
|
||||||
else
|
|
||||||
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
||||||
ULTRAGOAL_CLI=
|
|
||||||
if [ -f "$CODEX_HOME/bin/omo" ] || [ -x "$CODEX_HOME/bin/omo" ]; then
|
|
||||||
ULTRAGOAL_CLI="$CODEX_HOME/bin/omo"
|
|
||||||
else
|
|
||||||
for candidate in "$CODEX_HOME"/plugins/cache/sisyphuslabs/omo/*/components/ultragoal/dist/cli.js; do
|
|
||||||
[ -f "$candidate" ] || continue
|
|
||||||
ULTRAGOAL_CLI="$candidate"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
ULTRAGOAL_NODE="$(command -v node 2>/dev/null || true)"
|
|
||||||
if [ -z "$ULTRAGOAL_NODE" ]; then
|
|
||||||
for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
|
||||||
[ -x "$candidate" ] || continue
|
|
||||||
ULTRAGOAL_NODE="$candidate"
|
|
||||||
break
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$ULTRAGOAL_CLI" ] && [ -n "$ULTRAGOAL_NODE" ]; then
|
|
||||||
omo() { "$ULTRAGOAL_NODE" "$ULTRAGOAL_CLI" "$@"; }
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${ULTRAGOAL_CLI:-}" ]; then
|
|
||||||
/bin/mkdir -p .omo/ultragoal 2>/dev/null || mkdir -p .omo/ultragoal 2>/dev/null || true
|
|
||||||
NOTE="${NOTE:-.omo/ultragoal/bootstrap-notepad.md}"
|
|
||||||
printf '%s\n' "omo executable missing from PATH; cached ultragoal CLI not found under ${CODEX_HOME:-$HOME/.codex}." >> "$NOTE" 2>/dev/null || true
|
|
||||||
printf '%s\n' "Install with bunx omo install --platform=codex or set CODEX_LOCAL_BIN_DIR to a PATH directory." >&2
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
If `ULTRAGOAL_CLI` is empty, open the durable notepad first, record the missing CLI evidence, then surface the installer issue.
|
|
||||||
|
|
||||||
Run one form:
|
|
||||||
```sh
|
|
||||||
omo ultragoal create-goals --brief "<brief>" --json
|
|
||||||
omo ultragoal create-goals --brief-file <path> --json
|
|
||||||
cat <brief> | omo ultragoal create-goals --from-stdin --json
|
|
||||||
```
|
|
||||||
Write state through the CLI path. Do not hand-edit state files.
|
|
||||||
|
|
||||||
### 2. Refine success criteria per goal
|
|
||||||
Define pass/fail acceptance criteria before launching execution lanes. Include the command, artifact, or manual check that will prove success.
|
|
||||||
Each goal MUST carry 3+ `successCriteria` covering happy path, edge, regression, and adversarial risk.
|
|
||||||
For each criterion set: `id`, `scenario`, `expectedEvidence`, adversarial classes, stop condition, and the Manual-QA channel (HTTP call / tmux / browser use / computer use) that will exercise it.
|
|
||||||
Apply ultraqa classes where relevant: malformed input, repeated interruptions, prompt injection, cancel/resume, stale state, dirty worktree, hung or long commands, flaky tests, misleading success output.
|
|
||||||
Use evidence verbs from the channel table (tmux transcript, curl status+body, browser screenshot, computer-use action log, CLI stdout, DB diff, parsed config dump) — not vibes.
|
|
||||||
"Tests pass" is supporting signal, NEVER completion proof. Every criterion needs its own channel scenario, built fresh and exercised every time.
|
|
||||||
Record manual QA notes when behavior is user-visible.
|
|
||||||
Revise any criterion that lacks observable `expectedEvidence` or a named channel before execution.
|
|
||||||
|
|
||||||
### 3. Inspect state
|
|
||||||
Run `omo ultragoal status --json`.
|
|
||||||
Read pending goals, criteria IDs, current ledger head, blockers, and aggregate Codex objective.
|
|
||||||
|
|
||||||
## Execution Loop
|
|
||||||
Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures at 3.
|
|
||||||
|
|
||||||
### Acquire Next Goal
|
|
||||||
1. Run `omo ultragoal complete-goals --json` and read the handoff, including criteria.
|
|
||||||
2. Call `get_goal` and inspect active Codex state.
|
|
||||||
3. Apply this table exactly:
|
|
||||||
|
|
||||||
| get_goal result | action |
|
|
||||||
|-----------------|--------|
|
|
||||||
| no active goal | Call `create_goal` with the handoff payload. |
|
|
||||||
| same aggregate objective active | Continue the current ultragoal story. |
|
|
||||||
| different goal active | STOP. Checkpoint blocked and surface the conflict. |
|
|
||||||
4. If retrying failed work, run `omo ultragoal complete-goals --retry-failed --json`.
|
|
||||||
5. Never create a second Codex goal for the same aggregate objective.
|
|
||||||
|
|
||||||
### Per-Criterion Cycle
|
|
||||||
1. PLAN: read `criterion.scenario`, `criterion.expectedEvidence`, prior ledger entries, and safety bounds.
|
|
||||||
2. Register atomic todos: `path: <action> for <criterion> - verify by <check>`.
|
|
||||||
3. EXECUTE-AS-SCENARIO: do one bounded change, then ACTUALLY run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use — see the channel table above). The unit suite being green is NEVER substitute for running the channel scenario.
|
|
||||||
4. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump.
|
|
||||||
5. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 3 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD`. Missing receipt → record BLOCKED, not PASS.
|
|
||||||
6. RECORD exactly one result:
|
|
||||||
- PASS: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> | <cleanup receipt>" --json`
|
|
||||||
- FAIL: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
|
||||||
- BLOCKED: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status blocked --evidence "<observable>" --notes "<safety/blocker/leftover-state>" --json`
|
|
||||||
7. If actual does not match expected, diagnose, fix minimally, and rerun the SAME criterion (including a fresh cleanup).
|
|
||||||
8. After 3 same-criterion failures, exit the goal with diagnosis.
|
|
||||||
9. After 5 cycles on one goal without all criteria passing, checkpoint failed.
|
|
||||||
10. Continue only when the next pending criterion has a concrete `expectedEvidence` target.
|
|
||||||
|
|
||||||
### Goal Completion
|
|
||||||
1. Confirm every criterion is `pass` with `omo ultragoal criteria --goal-id <id> --json`.
|
|
||||||
2. Call `get_goal` for a fresh snapshot.
|
|
||||||
3. Run `omo ultragoal checkpoint --goal-id <id> --status complete --evidence "<criteria evidence summary>" --codex-goal-json <snapshot> --json`.
|
|
||||||
4. If blocked or failed, checkpoint with `--status blocked` or `--status failed` and include diagnosis evidence.
|
|
||||||
5. If this is the final goal, run the final quality gate first and pass `--quality-gate-json`.
|
|
||||||
|
|
||||||
## Final Quality Gate
|
|
||||||
Trigger only when one goal remains and all its criteria are passing.
|
|
||||||
1. Run targeted verification for changed behavior.
|
|
||||||
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
|
||||||
3. Rerun verification after cleanup.
|
|
||||||
4. Run `$code-review`.
|
|
||||||
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
|
||||||
6. If review is non-clean, run `omo ultragoal record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
|
||||||
7. If clean, checkpoint final completion:
|
|
||||||
```sh
|
|
||||||
omo ultragoal checkpoint --goal-id <id> --status complete --evidence "<e2e evidence + manual QA notes>" --codex-goal-json <snapshot> --quality-gate-json <json-or-path> --json
|
|
||||||
```
|
|
||||||
`--quality-gate-json` shape:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"aiSlopCleaner": { "status": "passed", "evidence": "cleaner report" },
|
|
||||||
"verification": { "status": "passed", "commands": ["npm test"], "evidence": "post-cleaner verification" },
|
|
||||||
"codeReview": { "recommendation": "APPROVE", "architectStatus": "CLEAR", "evidence": "review synthesis" },
|
|
||||||
"criteriaCoverage": { "totalCriteria": N, "passCount": N, "adversarialClassesCovered": ["malformed_input", "..."] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dynamic Steering
|
|
||||||
Use steering only for structured evidence-backed mutation. Reject natural-language steering requests.
|
|
||||||
|
|
||||||
| Kind | When to use | Required fields |
|
|
||||||
|------|-------------|-----------------|
|
|
||||||
| add_subgoal | Real blocker found; new story required | `--title`, `--objective`, `--evidence`, `--rationale` |
|
|
||||||
| split_subgoal | Story too large; needs decomposition | `--goal-id`, `--children` JSON, `--evidence`, `--rationale` |
|
|
||||||
| reorder_pending | Discovered dependency order | `--order` JSON array of ids, `--evidence`, `--rationale` |
|
|
||||||
| revise_pending_wording | Title/objective ambiguous | `--goal-id`, `--title?`, `--objective?`, `--evidence`, `--rationale` |
|
|
||||||
| revise_criterion | Criterion lacks observable PASS evidence | `--goal-id`, `--criterion-id`, `--scenario?`, `--expected-evidence?`, `--evidence`, `--rationale` |
|
|
||||||
| annotate_ledger | Audit-only note | `--evidence`, `--rationale` |
|
|
||||||
| mark_blocked_superseded | Old story replaced by new evidence | `--goal-id`, `--replacements?`, `--evidence`, `--rationale` |
|
|
||||||
|
|
||||||
Command form: `omo ultragoal steer --kind <kind> [<kind-specific-fields>] --evidence "<...>" --rationale "<...>" --json`.
|
|
||||||
Structured prompt directives accepted: `OMO_ULTRAGOAL_STEER: { ... }`, `omo.ultragoal.steer: {...}`, `omo ultragoal steer: {...}`.
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
1. NEVER call `update_goal` mid-aggregate; only on final story after the quality gate passes.
|
|
||||||
2. NEVER call `create_goal` when `get_goal` shows a different active goal.
|
|
||||||
3. NEVER mark `criterion.status == "pass"` without captured observable evidence in `record-evidence`.
|
|
||||||
4. NEVER bypass the criteria gate at checkpoint; all criteria must be `pass` before `--status complete`.
|
|
||||||
5. Baseline build/lint/typecheck/test commands are necessary evidence, NOT SUFFICIENT completion proof. Criteria coverage with observable evidence is the gate.
|
|
||||||
6. Treat `.omo/ultragoal/ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
|
||||||
7. Per-story Codex goal mode is opt-in only with `--codex-goal-mode per-story`; default is aggregate.
|
|
||||||
8. Structured steering directives mutate state through validation; normal prose does not.
|
|
||||||
9. Evidence MUST be observable from the real surface: tmux transcript, curl status+body, browser/Playwright assertion, CLI stdout, DB state diff, parsed config dump.
|
|
||||||
10. Apply ultraqa's 9 adversarial classes where relevant per goal: malformed input, prompt injection, cancel/resume, stale state, dirty worktree, hung commands, flaky tests, misleading success output, repeated interruptions.
|
|
||||||
11. After completing an aggregate ultragoal run, clear the Codex goal manually with `/goal clear` before starting another in the same session.
|
|
||||||
12. The shell command emits a model-facing handoff; only the Codex agent calls `get_goal`, `create_goal`, or `update_goal` tools.
|
|
||||||
13. NEVER record `--status pass` while a QA-spawned process, `tmux` session, browser context, bound port, container, or temp file / dir is still alive. The evidence string MUST include the cleanup receipt. Leftover runtime state = BLOCKED, not PASS.
|
|
||||||
|
|
||||||
## Stop Rules
|
|
||||||
- All goals complete plus all criteria `pass` plus final quality gate clean: DONE.
|
|
||||||
- 3x same criterion failure: checkpoint failed, surface diagnosis.
|
|
||||||
- 5 cycles on one goal without all-pass: checkpoint failed, surface.
|
|
||||||
- Safety boundary such as destructive command, secret exfiltration, or production write: block and surface a safe substitute.
|
|
||||||
- Codex `get_goal` reports a different active goal: checkpoint blocker, stop, surface.
|
|
||||||
- Leftover state from QA (live process, `tmux` session, browser context, bound port, temp dir): NOT pass. Clean up, append the receipt, then continue.
|
|
||||||
- User issues `/cancel`: release in-progress state cleanly and do not auto-resume.
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { join } from "node:path";
|
|
||||||
import { ULTRAGOAL_BRIEF, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER } from "./types.js";
|
|
||||||
|
|
||||||
export function ultragoalDir(repoRoot: string): string {
|
|
||||||
return join(repoRoot, ULTRAGOAL_DIR);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ultragoalBriefPath(repoRoot: string): string {
|
|
||||||
return join(ultragoalDir(repoRoot), ULTRAGOAL_BRIEF);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ultragoalGoalsPath(repoRoot: string): string {
|
|
||||||
return join(ultragoalDir(repoRoot), ULTRAGOAL_GOALS);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function ultragoalLedgerPath(repoRoot: string): string {
|
|
||||||
return join(ultragoalDir(repoRoot), ULTRAGOAL_LEDGER);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function repoRelative(absolutePath: string, repoRoot: string): string {
|
|
||||||
const slashPrefix = `${repoRoot}/`;
|
|
||||||
const backslashPrefix = `${repoRoot}\\`;
|
|
||||||
if (absolutePath.startsWith(slashPrefix)) return absolutePath.slice(slashPrefix.length).split("\\").join("/");
|
|
||||||
if (absolutePath.startsWith(backslashPrefix))
|
|
||||||
return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
|
|
||||||
return absolutePath.split("\\").join("/");
|
|
||||||
}
|
|
||||||
-1
@@ -1 +0,0 @@
|
|||||||
{ "goal": { "objective": "Complete the durable ultragoal plan", "status": "active" } }
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
|
||||||
|
|
||||||
import {
|
|
||||||
repoRelative,
|
|
||||||
ultragoalBriefPath,
|
|
||||||
ultragoalDir,
|
|
||||||
ultragoalGoalsPath,
|
|
||||||
ultragoalLedgerPath,
|
|
||||||
} from "../src/paths.ts";
|
|
||||||
|
|
||||||
describe("ultragoalDir(repo)", () => {
|
|
||||||
it("returns repo + '/.omo/ultragoal'", () => {
|
|
||||||
// when/then
|
|
||||||
expect(ultragoalDir("/repo")).toBe("/repo/.omo/ultragoal");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("ultragoal*Path helpers", () => {
|
|
||||||
it("compose artifact filenames under ultragoalDir", () => {
|
|
||||||
// when/then
|
|
||||||
expect(ultragoalBriefPath("/r")).toBe("/r/.omo/ultragoal/brief.md");
|
|
||||||
expect(ultragoalGoalsPath("/r")).toBe("/r/.omo/ultragoal/goals.json");
|
|
||||||
expect(ultragoalLedgerPath("/r")).toBe("/r/.omo/ultragoal/ledger.jsonl");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("repoRelative", () => {
|
|
||||||
it("strips repo prefix when path is inside repo", () => {
|
|
||||||
// when/then
|
|
||||||
expect(repoRelative("/repo/.omo/ultragoal/goals.json", "/repo")).toBe(".omo/ultragoal/goals.json");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns absolute when path is outside repo", () => {
|
|
||||||
// when/then
|
|
||||||
expect(repoRelative("/elsewhere/file", "/repo")).toBe("/elsewhere/file");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
+3
-3
@@ -37,9 +37,9 @@ Conventions for human contributors and AI agents working on this repository.
|
|||||||
|
|
||||||
## Branding
|
## Branding
|
||||||
|
|
||||||
- Repo artifacts live under `.omo/ultragoal/` paths.
|
- Repo artifacts live under `.omo/ulw-loop/` paths.
|
||||||
- Environment variables use the `OMO_ULTRAGOAL_*` prefix.
|
- Environment variables use the `OMO_ULW_LOOP_*` prefix.
|
||||||
- CLI commands use the `omo ultragoal` form.
|
- CLI commands use the `omo ulw-loop` form.
|
||||||
- Do not use any alternate legacy CLI alias anywhere.
|
- Do not use any alternate legacy CLI alias anywhere.
|
||||||
|
|
||||||
## Build and Hooks
|
## Build and Hooks
|
||||||
+1
-1
@@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
## [0.1.0] - unreleased
|
## [0.1.0] - unreleased
|
||||||
|
|
||||||
- Initial scaffold of codex-ultragoal plugin.
|
- Initial scaffold of codex-ulw-loop plugin.
|
||||||
- Per-Criterion Cycle: `EXECUTE` is now **EXECUTE-AS-SCENARIO** — the agent must run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use; see new `## Manual-QA channels` section). Inserted a new **CLEAN (PAIRED, NEVER SKIP)** step that tears down every QA-spawned process / `tmux` session / browser context / container / port / temp dir before recording evidence; the cleanup receipt is embedded in the `--evidence` string. Missing receipt → record BLOCKED, not PASS. Added Constraint #13 and a Stop Rule for leftover state.
|
- Per-Criterion Cycle: `EXECUTE` is now **EXECUTE-AS-SCENARIO** — the agent must run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use; see new `## Manual-QA channels` section). Inserted a new **CLEAN (PAIRED, NEVER SKIP)** step that tears down every QA-spawned process / `tmux` session / browser context / container / port / temp dir before recording evidence; the cleanup receipt is embedded in the `--evidence` string. Missing receipt → record BLOCKED, not PASS. Added Constraint #13 and a Stop Rule for leftover state.
|
||||||
- New top-level **`## Manual-QA channels`** section explicitly enumerates the four channels (HTTP call, tmux, Browser use, Computer use) with concrete commands and required artifacts. Goal section now declares **TESTS ALONE NEVER PROVE DONE**: a green test suite is supporting evidence, never completion proof. Criterion-refinement step 2 requires each criterion to name its channel up front.
|
- New top-level **`## Manual-QA channels`** section explicitly enumerates the four channels (HTTP call, tmux, Browser use, Computer use) with concrete commands and required artifacts. Goal section now declares **TESTS ALONE NEVER PROVE DONE**: a green test suite is supporting evidence, never completion proof. Criterion-refinement step 2 requires each criterion to name its channel up front.
|
||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
codex-ultragoal
|
codex-ulw-loop
|
||||||
|
|
||||||
This package ports the oh-my-codex ultragoal feature into a Codex plugin repository.
|
This package ports the oh-my-codex ulw-loop feature into a Codex plugin repository.
|
||||||
|
|
||||||
The plugin targets Codex plugin manifests and plugin-bundled lifecycle hooks.
|
The plugin targets Codex plugin manifests and plugin-bundled lifecycle hooks.
|
||||||
The orchestration engine is added in later port waves.
|
The orchestration engine is added in later port waves.
|
||||||
+10
-10
@@ -1,4 +1,4 @@
|
|||||||
# codex-ultragoal
|
# codex-ulw-loop
|
||||||
|
|
||||||
[](#) [](LICENSE)
|
[](#) [](LICENSE)
|
||||||
|
|
||||||
@@ -8,13 +8,13 @@ Codex plugin scaffold for durable repo-native multi-goal orchestration with embe
|
|||||||
|
|
||||||
| Subcommand | Purpose |
|
| Subcommand | Purpose |
|
||||||
|------------|---------|
|
|------------|---------|
|
||||||
| `omo ultragoal create-goals` | Create repo-native goals from a brief and seed criteria. |
|
| `omo ulw-loop create-goals` | Create repo-native goals from a brief and seed criteria. |
|
||||||
| `omo ultragoal record-evidence` | Record observable evidence for the active criterion. |
|
| `omo ulw-loop record-evidence` | Record observable evidence for the active criterion. |
|
||||||
| `omo ultragoal criteria` | Inspect or revise goal success criteria. |
|
| `omo ulw-loop criteria` | Inspect or revise goal success criteria. |
|
||||||
| `omo ultragoal complete-goals` | Complete eligible goals after criteria pass. |
|
| `omo ulw-loop complete-goals` | Complete eligible goals after criteria pass. |
|
||||||
| `omo ultragoal checkpoint` | Refuse completion until criteria and evidence gates pass. |
|
| `omo ulw-loop checkpoint` | Refuse completion until criteria and evidence gates pass. |
|
||||||
| `omo ultragoal steer` | Apply steering updates to the plan. |
|
| `omo ulw-loop steer` | Apply steering updates to the plan. |
|
||||||
| `omo ultragoal status` | Report active goal, criteria, and evidence state. |
|
| `omo ulw-loop status` | Report active goal, criteria, and evidence state. |
|
||||||
|
|
||||||
Wave 1 is scaffold only. Command behavior lands in later waves.
|
Wave 1 is scaffold only. Command behavior lands in later waves.
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@ The plugin ships:
|
|||||||
|
|
||||||
- `.codex-plugin/plugin.json` for Codex plugin discovery.
|
- `.codex-plugin/plugin.json` for Codex plugin discovery.
|
||||||
- `hooks/hooks.json` for the `UserPromptSubmit` hook.
|
- `hooks/hooks.json` for the `UserPromptSubmit` hook.
|
||||||
- `skills/ultragoal/` as the future skill directory.
|
- `skills/ulw-loop/` as the future skill directory.
|
||||||
|
|
||||||
The hook command is:
|
The hook command is:
|
||||||
|
|
||||||
@@ -71,5 +71,5 @@ This plugin runs locally. The scaffold does not call a network service by itself
|
|||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
- [oh-my-codex](https://github.com/code-yeongyu/oh-my-codex) - source project for the ultragoal port.
|
- [oh-my-codex](https://github.com/code-yeongyu/oh-my-codex) - source project for the ulw-loop port.
|
||||||
- [lazycodex](https://github.com/code-yeongyu/lazycodex) - Sisyphus Labs Codex marketplace repository.
|
- [lazycodex](https://github.com/code-yeongyu/lazycodex) - Sisyphus Labs Codex marketplace repository.
|
||||||
+2
-2
@@ -7,7 +7,7 @@
|
|||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
"statusMessage": "checking ultragoal steering"
|
"statusMessage": "checking ulw-loop steering"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
"statusMessage": "enforcing unlimited ultragoal budget"
|
"statusMessage": "enforcing unlimited ulw-loop budget"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
+5
-5
@@ -1,22 +1,22 @@
|
|||||||
{
|
{
|
||||||
"name": "@code-yeongyu/codex-ultragoal",
|
"name": "@code-yeongyu/codex-ulw-loop",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
"description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"packageManager": "npm@11.12.1",
|
"packageManager": "npm@11.12.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"homepage": "https://github.com/code-yeongyu/codex-ultragoal",
|
"homepage": "https://github.com/code-yeongyu/codex-ulw-loop",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/code-yeongyu/codex-ultragoal.git"
|
"url": "git+https://github.com/code-yeongyu/codex-ulw-loop.git"
|
||||||
},
|
},
|
||||||
"bugs": {
|
"bugs": {
|
||||||
"url": "https://github.com/code-yeongyu/codex-ultragoal/issues"
|
"url": "https://github.com/code-yeongyu/codex-ulw-loop/issues"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"codex",
|
"codex",
|
||||||
"codex-plugin",
|
"codex-plugin",
|
||||||
"ultragoal",
|
"ulw-loop",
|
||||||
"goal-mode",
|
"goal-mode",
|
||||||
"orchestration",
|
"orchestration",
|
||||||
"evidence",
|
"evidence",
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
---
|
||||||
|
name: ulw-loop
|
||||||
|
description: Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.
|
||||||
|
metadata:
|
||||||
|
short-description: Goal-like ultrawork loop for systematic decomposition
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role
|
||||||
|
Expert goal orchestration agent. You conduct; right-sized parallel subagents play. Plan multi-goal work that survives across turns and sessions, fan independent work out to workers, QA every result yourself, record only proven evidence.
|
||||||
|
Use GPT-5.x style: outcome-first, evidence-bound, atomic decisions, no nested branching prose.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Deliver every goal in `.omo/ulw-loop/goals.json` end-to-end.
|
||||||
|
Prove EVERY success criterion with captured observable evidence from a real-usage scenario you actually ran (HTTP call / tmux / browser use / computer use — see the Manual-QA channels below).
|
||||||
|
TESTS ALONE NEVER PROVE DONE. A green test suite is supporting evidence, not completion proof.
|
||||||
|
Audit each pass, fail, block, steering change, and checkpoint in `.omo/ulw-loop/ledger.jsonl`.
|
||||||
|
|
||||||
|
## Manual-QA channels (PICK ONE PER CRITERION — ACTUALLY RUN IT)
|
||||||
|
For every criterion, build a real-usage scenario through ONE of these four channels and run it yourself before recording PASS. The full test suite being green is NEVER verification on its own.
|
||||||
|
|
||||||
|
1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
|
||||||
|
2. **tmux** — `tmux new-session -d -s ulw-qa-<criterion>`, drive with `send-keys`, dump via `tmux capture-pane -pS -E -`; transcript is the artifact.
|
||||||
|
3. **Browser use** — drive the real page via Playwright / puppeteer / Chromium; capture action log + screenshot path.
|
||||||
|
4. **Computer use** — OS-level GUI automation (computer-use agent, AppleScript, xdotool, etc.) against the running app; capture action log + screenshot.
|
||||||
|
|
||||||
|
Auxiliary surfaces (pure CLI stdout / DB state diff / parsed config dump) satisfy CLI- or data-shaped criteria but NEVER replace a channel scenario for user-facing behavior. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
||||||
|
|
||||||
|
## Delegation model (ATLAS-STYLE — YOU CONDUCT, WORKERS PLAY)
|
||||||
|
You read, search, plan, integrate, and QA. You DELEGATE every code edit, test write, bug fix, and QA execution to a right-sized `spawn_agent` worker, then verify what comes back. Fan out independent tasks in PARALLEL in a single response; serialize only on a NAMED dependency (one task consumes another's output or edits the same file).
|
||||||
|
|
||||||
|
Size each worker to the task — never spend `xhigh` on a one-liner, never send a race condition to a mini. Pass `model` + `reasoning_effort` per call (an override needs a non-full-history fork mode):
|
||||||
|
|
||||||
|
| Task shape | agent_type | model | reasoning_effort |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Trivial / mechanical (rename, move, obvious one-liner, config edit) | `worker` | `gpt-5.4-mini` | `low` |
|
||||||
|
| Pure implementation against a clear spec (new function, endpoint, test from a named pattern) | `worker` | `gpt-5.3-codex` | `high` |
|
||||||
|
| Deep debugging / race / perf / subtle cross-module reasoning | `worker` | `gpt-5.5` | `xhigh` |
|
||||||
|
| QA execution (drive a channel, capture evidence) | `worker` | `gpt-5.3-codex` | `high` |
|
||||||
|
| Read-only codebase search | `explorer` | role default | role default |
|
||||||
|
| External library / docs research | `librarian` | role default | role default |
|
||||||
|
| Final verification audit | `codex-ultrawork-reviewer` | role default | role default |
|
||||||
|
|
||||||
|
Every worker message MUST carry: goal + exact files in scope; the failing test / reproduction required before production code; constraints + project rules; the verification commands to run; the ONE Manual-QA channel and the exact evidence artifact to capture. Workers have NO interview context — be exhaustive, and forward accumulated learnings to every next worker. Track running workers; `wait_agent` for results, `close_agent` when done.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
- `.omo/ulw-loop/brief.md`: original brief and durable constraints.
|
||||||
|
- `.omo/ulw-loop/goals.json`: goals with embedded `successCriteria` per goal.
|
||||||
|
- `.omo/ulw-loop/ledger.jsonl`: append-only audit trail.
|
||||||
|
- Read artifacts before resuming, steering, or checkpointing.
|
||||||
|
- Never invent state outside `.omo/ulw-loop` artifacts or `omo ulw-loop status --json`.
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
Do all three steps before execution. No edits, goal tools, or checkpointing before bootstrap completes.
|
||||||
|
|
||||||
|
### 1. Create goals from the brief
|
||||||
|
Resolve the CLI before the first command. If `omo` is absent from PATH, use the stable local installer bin or cached Codex component CLI. This is the same ulw-loop CLI, so PATH absence is not a blocker. If PATH is empty, the fallback uses shell builtins and absolute Node locations before reporting guidance, and records the failure in `.omo/ulw-loop/bootstrap-notepad.md`.
|
||||||
|
```sh
|
||||||
|
if command -v omo >/dev/null 2>&1; then
|
||||||
|
ULW_LOOP_CLI=omo
|
||||||
|
else
|
||||||
|
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
||||||
|
ULW_LOOP_CLI=
|
||||||
|
if [ -f "$CODEX_HOME/bin/omo" ] || [ -x "$CODEX_HOME/bin/omo" ]; then
|
||||||
|
ULW_LOOP_CLI="$CODEX_HOME/bin/omo"
|
||||||
|
else
|
||||||
|
for candidate in "$CODEX_HOME"/plugins/cache/sisyphuslabs/omo/*/components/ulw-loop/dist/cli.js; do
|
||||||
|
[ -f "$candidate" ] || continue
|
||||||
|
ULW_LOOP_CLI="$candidate"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
ULW_LOOP_NODE="$(command -v node 2>/dev/null || true)"
|
||||||
|
if [ -z "$ULW_LOOP_NODE" ]; then
|
||||||
|
for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
||||||
|
[ -x "$candidate" ] || continue
|
||||||
|
ULW_LOOP_NODE="$candidate"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ULW_LOOP_CLI" ] && [ -n "$ULW_LOOP_NODE" ]; then
|
||||||
|
omo() { "$ULW_LOOP_NODE" "$ULW_LOOP_CLI" "$@"; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${ULW_LOOP_CLI:-}" ]; then
|
||||||
|
/bin/mkdir -p .omo/ulw-loop 2>/dev/null || mkdir -p .omo/ulw-loop 2>/dev/null || true
|
||||||
|
NOTE="${NOTE:-.omo/ulw-loop/bootstrap-notepad.md}"
|
||||||
|
printf '%s\n' "omo executable missing from PATH; cached ulw-loop CLI not found under ${CODEX_HOME:-$HOME/.codex}." >> "$NOTE" 2>/dev/null || true
|
||||||
|
printf '%s\n' "Install with bunx omo install --platform=codex or set CODEX_LOCAL_BIN_DIR to a PATH directory." >&2
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
If `ULW_LOOP_CLI` is empty, open the durable notepad first, record the missing CLI evidence, then surface the installer issue.
|
||||||
|
|
||||||
|
Run one form:
|
||||||
|
```sh
|
||||||
|
omo ulw-loop create-goals --brief "<brief>" --json
|
||||||
|
omo ulw-loop create-goals --brief-file <path> --json
|
||||||
|
cat <brief> | omo ulw-loop create-goals --from-stdin --json
|
||||||
|
```
|
||||||
|
Write state through the CLI path. Do not hand-edit state files.
|
||||||
|
|
||||||
|
### 2. Refine success criteria + a Prometheus-grade QA and parallelism plan per goal
|
||||||
|
Gather context BEFORE planning — fire parallel `explorer` / `librarian` workers plus your own read-only tools; never plan blind.
|
||||||
|
Define pass/fail acceptance criteria before launching execution lanes. Include the command, artifact, or manual check that will prove success.
|
||||||
|
Each goal MUST carry 3+ `successCriteria` covering happy path, edge, regression, and adversarial risk.
|
||||||
|
For each criterion set, concretely and upfront: `id`, `scenario` (the exact tool — curl / tmux / playwright / computer-use — plus exact steps with specific inputs and a binary pass/fail), `expectedEvidence` (the exact artifact path, e.g. `.omo/ulw-loop/evidence/<goal>-<criterion>.<ext>`), adversarial classes, stop condition, and the Manual-QA channel (HTTP call / tmux / browser use / computer use) that will exercise it. Vague QA ("verify it works") is a rejected criterion — revise it before execution.
|
||||||
|
Apply ultraqa classes where relevant: malformed input, repeated interruptions, prompt injection, cancel/resume, stale state, dirty worktree, hung or long commands, flaky tests, misleading success output.
|
||||||
|
Use evidence verbs from the channel table (tmux transcript, curl status+body, browser screenshot, computer-use action log, CLI stdout, DB diff, parsed config dump) — not vibes.
|
||||||
|
"Tests pass" is supporting signal, NEVER completion proof. Every criterion needs its own channel scenario, built fresh and exercised every time.
|
||||||
|
|
||||||
|
**Plan for maximum parallelism.** Decompose each goal's criteria into atomic tasks (Implementation + its Test = ONE task, never split) and group them into dependency waves. Target 5–8 tasks per wave; <3 per wave (except the final wave) means under-splitting — extract shared prerequisites into Wave 1. For each task record its wave, what it blocks, what blocks it, the worker tier from the Delegation table, and its QA scenario + evidence path. Build a dependency matrix (Task | Depends on | Blocks | Can parallelize with) and name the critical path. Anything not on a real dependency edge MUST share a wave and dispatch together.
|
||||||
|
Record manual QA notes when behavior is user-visible.
|
||||||
|
Revise any criterion that lacks observable `expectedEvidence` or a named channel before execution.
|
||||||
|
|
||||||
|
### 3. Inspect state
|
||||||
|
Run `omo ulw-loop status --json`.
|
||||||
|
Read pending goals, criteria IDs, current ledger head, blockers, and aggregate Codex objective.
|
||||||
|
|
||||||
|
## Execution Loop
|
||||||
|
Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures at 3.
|
||||||
|
|
||||||
|
### Acquire Next Goal
|
||||||
|
1. Run `omo ulw-loop complete-goals --json` and read the handoff, including criteria.
|
||||||
|
2. Call `get_goal` and inspect active Codex state.
|
||||||
|
3. Apply this table exactly:
|
||||||
|
|
||||||
|
| get_goal result | action |
|
||||||
|
|-----------------|--------|
|
||||||
|
| no active goal | Call `create_goal` with the handoff payload. |
|
||||||
|
| same aggregate objective active | Continue the current ulw-loop story. |
|
||||||
|
| different goal active | STOP. Checkpoint blocked and surface the conflict. |
|
||||||
|
4. If retrying failed work, run `omo ulw-loop complete-goals --retry-failed --json`.
|
||||||
|
5. Never create a second Codex goal for the same aggregate objective.
|
||||||
|
|
||||||
|
### Per-Criterion Cycle
|
||||||
|
1. PLAN: read `criterion.scenario`, `criterion.expectedEvidence`, prior ledger entries, and safety bounds. Identify which tasks in the current wave are independent.
|
||||||
|
2. Register atomic todos: `path: <action> for <criterion> - verify by <check>`.
|
||||||
|
3. DELEGATE-IN-PARALLEL: dispatch every independent task in the wave at once via right-sized `spawn_agent` workers (Delegation table). Each worker does strict TDD on its task: RED first (the failing assertion must fail for the RIGHT reason — no syntax/import error), then the SMALLEST GREEN change; a GREEN needing >~20 lines means the test was too coarse — instruct a split. Serialize only on a NAMED dependency.
|
||||||
|
4. INTEGRATE + CRITICAL SELF-QA (EVERY WORKER RETURN): do NOT trust the worker's report. Read the diff yourself, re-run its tests, and run LSP diagnostics on the changed files. Treat "done" as a claim to disprove. If the diff drifts, the test is hollow, or evidence is missing, RESPAWN the worker with the specific failure context. Forward every finding/learning to subsequent workers.
|
||||||
|
5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use — see the channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA worker (`worker`, `gpt-5.3-codex`, `high`) whose ONLY job is to drive the channel and write the artifact to the named evidence path. The unit suite being green is NEVER substitute. If the scenario FAILS, respawn the implementing worker with the captured failure — do not hand-patch around it.
|
||||||
|
6. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump. No artifact written at the evidence path — not done; record BLOCKED and respawn QA.
|
||||||
|
7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND `close_agent` on every finished worker. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; close_agent w-3`. Missing receipt → record BLOCKED, not PASS.
|
||||||
|
8. RECORD exactly one result:
|
||||||
|
- PASS: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> | <cleanup receipt>" --json`
|
||||||
|
- FAIL: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
||||||
|
- BLOCKED: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status blocked --evidence "<observable>" --notes "<safety/blocker/leftover-state>" --json`
|
||||||
|
9. If actual does not match expected, diagnose, respawn the right-sized worker with the failure context to fix minimally, and rerun the SAME criterion (including a fresh cleanup).
|
||||||
|
10. After 3 same-criterion failures, exit the goal with diagnosis.
|
||||||
|
11. After 5 cycles on one goal without all criteria passing, checkpoint failed.
|
||||||
|
12. Continue only when the next pending criterion has a concrete `expectedEvidence` target.
|
||||||
|
|
||||||
|
### Goal Completion
|
||||||
|
1. Confirm every criterion is `pass` with `omo ulw-loop criteria --goal-id <id> --json`.
|
||||||
|
2. Call `get_goal` for a fresh snapshot.
|
||||||
|
3. Run `omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<criteria evidence summary>" --codex-goal-json <snapshot> --json`.
|
||||||
|
4. If blocked or failed, checkpoint with `--status blocked` or `--status failed` and include diagnosis evidence.
|
||||||
|
5. If this is the final goal, run the final quality gate first and pass `--quality-gate-json`.
|
||||||
|
|
||||||
|
## Final Quality Gate
|
||||||
|
Trigger only when one goal remains and all its criteria are passing.
|
||||||
|
1. Run targeted verification for changed behavior.
|
||||||
|
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
||||||
|
3. Rerun verification after cleanup.
|
||||||
|
4. Run `$code-review`.
|
||||||
|
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
||||||
|
6. If review is non-clean, run `omo ulw-loop record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
||||||
|
7. If clean, checkpoint final completion:
|
||||||
|
```sh
|
||||||
|
omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<e2e evidence + manual QA notes>" --codex-goal-json <snapshot> --quality-gate-json <json-or-path> --json
|
||||||
|
```
|
||||||
|
`--quality-gate-json` shape:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"aiSlopCleaner": { "status": "passed", "evidence": "cleaner report" },
|
||||||
|
"verification": { "status": "passed", "commands": ["npm test"], "evidence": "post-cleaner verification" },
|
||||||
|
"codeReview": { "recommendation": "APPROVE", "architectStatus": "CLEAR", "evidence": "review synthesis" },
|
||||||
|
"criteriaCoverage": { "totalCriteria": N, "passCount": N, "adversarialClassesCovered": ["malformed_input", "..."] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Steering
|
||||||
|
Use steering only for structured evidence-backed mutation. Reject natural-language steering requests.
|
||||||
|
|
||||||
|
| Kind | When to use | Required fields |
|
||||||
|
|------|-------------|-----------------|
|
||||||
|
| add_subgoal | Real blocker found; new story required | `--title`, `--objective`, `--evidence`, `--rationale` |
|
||||||
|
| split_subgoal | Story too large; needs decomposition | `--goal-id`, `--children` JSON, `--evidence`, `--rationale` |
|
||||||
|
| reorder_pending | Discovered dependency order | `--order` JSON array of ids, `--evidence`, `--rationale` |
|
||||||
|
| revise_pending_wording | Title/objective ambiguous | `--goal-id`, `--title?`, `--objective?`, `--evidence`, `--rationale` |
|
||||||
|
| revise_criterion | Criterion lacks observable PASS evidence | `--goal-id`, `--criterion-id`, `--scenario?`, `--expected-evidence?`, `--evidence`, `--rationale` |
|
||||||
|
| annotate_ledger | Audit-only note | `--evidence`, `--rationale` |
|
||||||
|
| mark_blocked_superseded | Old story replaced by new evidence | `--goal-id`, `--replacements?`, `--evidence`, `--rationale` |
|
||||||
|
|
||||||
|
Command form: `omo ulw-loop steer --kind <kind> [<kind-specific-fields>] --evidence "<...>" --rationale "<...>" --json`.
|
||||||
|
Structured prompt directives accepted: `OMO_ULW_LOOP_STEER: { ... }`, `omo.ulw-loop.steer: {...}`, `omo ulw-loop steer: {...}`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
1. NEVER call `update_goal` mid-aggregate; only on final story after the quality gate passes.
|
||||||
|
2. NEVER call `create_goal` when `get_goal` shows a different active goal.
|
||||||
|
3. NEVER mark `criterion.status == "pass"` without captured observable evidence in `record-evidence`.
|
||||||
|
4. NEVER bypass the criteria gate at checkpoint; all criteria must be `pass` before `--status complete`.
|
||||||
|
5. Baseline build/lint/typecheck/test commands are necessary evidence, NOT SUFFICIENT completion proof. Criteria coverage with observable evidence is the gate.
|
||||||
|
6. Treat `.omo/ulw-loop/ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
||||||
|
7. Per-story Codex goal mode is opt-in only with `--codex-goal-mode per-story`; default is aggregate.
|
||||||
|
8. Structured steering directives mutate state through validation; normal prose does not.
|
||||||
|
9. Evidence MUST be observable from the real surface: tmux transcript, curl status+body, browser/Playwright assertion, CLI stdout, DB state diff, parsed config dump.
|
||||||
|
10. Apply ultraqa's 9 adversarial classes where relevant per goal: malformed input, prompt injection, cancel/resume, stale state, dirty worktree, hung commands, flaky tests, misleading success output, repeated interruptions.
|
||||||
|
11. After completing an aggregate ulw-loop run, clear the Codex goal manually with `/goal clear` before starting another in the same session.
|
||||||
|
12. The shell command emits a model-facing handoff; only the Codex agent calls `get_goal`, `create_goal`, or `update_goal` tools.
|
||||||
|
13. NEVER record `--status pass` while a QA-spawned process, `tmux` session, browser context, bound port, container, or temp file / dir is still alive, or while any worker is still open. The evidence string MUST include the cleanup receipt. Leftover runtime state = BLOCKED, not PASS.
|
||||||
|
14. DELEGATE all code edits, test writes, fixes, and QA execution to right-sized `spawn_agent` workers (Delegation table); you read, search, plan, integrate, and QA. NEVER record `--status pass` from a worker's self-report — only from evidence you re-verified yourself. Dispatch independent tasks in parallel; serialize only on a NAMED dependency.
|
||||||
|
|
||||||
|
## Stop Rules
|
||||||
|
- All goals complete plus all criteria `pass` plus final quality gate clean: DONE.
|
||||||
|
- 3x same criterion failure: checkpoint failed, surface diagnosis.
|
||||||
|
- 5 cycles on one goal without all-pass: checkpoint failed, surface.
|
||||||
|
- Safety boundary such as destructive command, secret exfiltration, or production write: block and surface a safe substitute.
|
||||||
|
- Codex `get_goal` reports a different active goal: checkpoint blocker, stop, surface.
|
||||||
|
- Leftover state from QA (live process, `tmux` session, browser context, bound port, temp dir): NOT pass. Clean up, append the receipt, then continue.
|
||||||
|
- User issues `/cancel`: release in-progress state cleanly and do not auto-resume.
|
||||||
+1
-1
@@ -2,5 +2,5 @@ interface:
|
|||||||
display_name: "ulw loop"
|
display_name: "ulw loop"
|
||||||
short_description: "Goal-like ultrawork loop for systematic decomposition"
|
short_description: "Goal-like ultrawork loop for systematic decomposition"
|
||||||
search_terms:
|
search_terms:
|
||||||
- "ultragoal"
|
- "ulw-loop"
|
||||||
default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints."
|
default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints."
|
||||||
+37
-37
@@ -6,23 +6,23 @@ import { resolve } from "node:path";
|
|||||||
import { formatCodexGoalReconciliation, readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
import { formatCodexGoalReconciliation, readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
||||||
import { requireAllCriteriaPass } from "./evidence.js";
|
import { requireAllCriteriaPass } from "./evidence.js";
|
||||||
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
||||||
import { ultragoalBriefPath } from "./paths.js";
|
import { ulwLoopBriefPath } from "./paths.js";
|
||||||
import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js";
|
import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
||||||
import { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, sameBlockerOccurrences, validateQualityGate } from "./quality-gate.js";
|
import { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, sameBlockerOccurrences, validateQualityGate } from "./quality-gate.js";
|
||||||
import type { UltragoalAggregateCompletion, UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalQualityGate } from "./types.js";
|
import type { UlwLoopAggregateCompletion, UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopQualityGate } from "./types.js";
|
||||||
import { iso, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js";
|
import { iso, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
export interface CheckpointUltragoalArgs { readonly goalId: string; readonly status: "complete" | "failed" | "blocked"; readonly evidence: string; readonly codexGoalJson?: string; readonly qualityGateJson?: string }
|
export interface CheckpointUlwLoopArgs { readonly goalId: string; readonly status: "complete" | "failed" | "blocked"; readonly evidence: string; readonly codexGoalJson?: string; readonly qualityGateJson?: string }
|
||||||
export interface CheckpointUltragoalResult { readonly plan: UltragoalPlan; readonly goal: UltragoalItem; readonly ledgerEntry: UltragoalLedgerEntry; readonly aggregateCompletion?: UltragoalAggregateCompletion }
|
export interface CheckpointUlwLoopResult { readonly plan: UlwLoopPlan; readonly goal: UlwLoopItem; readonly ledgerEntry: UlwLoopLedgerEntry; readonly aggregateCompletion?: UlwLoopAggregateCompletion }
|
||||||
|
|
||||||
function ultragoalFail(message: string, code: string): never { throw new UltragoalError(message, code); }
|
function ulwLoopFail(message: string, code: string): never { throw new UlwLoopError(message, code); }
|
||||||
function normalizeObjective(value: string): string { return value.replace(/\s+/g, " ").trim(); }
|
function normalizeObjective(value: string): string { return value.replace(/\s+/g, " ").trim(); }
|
||||||
function nonEmptyEvidence(value: string): string { const trimmed = value.trim(); return trimmed || ultragoalFail("Evidence must be a non-empty string.", "ultragoal_evidence_required"); }
|
function nonEmptyEvidence(value: string): string { const trimmed = value.trim(); return trimmed || ulwLoopFail("Evidence must be a non-empty string.", "ulw_loop_evidence_required"); }
|
||||||
function findGoal(plan: UltragoalPlan, goalId: string): UltragoalItem { const goal = plan.goals.find((candidate) => candidate.id === goalId); return goal ?? ultragoalFail(`Unknown ultragoal id: ${goalId}.`, "ultragoal_goal_not_found"); }
|
function findGoal(plan: UlwLoopPlan, goalId: string): UlwLoopItem { const goal = plan.goals.find((candidate) => candidate.id === goalId); return goal ?? ulwLoopFail(`Unknown ulw-loop id: ${goalId}.`, "ulw_loop_goal_not_found"); }
|
||||||
|
|
||||||
function textMentionsUltragoalPlanArtifact(value: string | undefined): boolean {
|
function textMentionsUlwLoopPlanArtifact(value: string | undefined): boolean {
|
||||||
const normalized = (value ?? "").toLowerCase();
|
const normalized = (value ?? "").toLowerCase();
|
||||||
return normalized.includes(ULTRAGOAL_DIR.toLowerCase()) || normalized.includes(ULTRAGOAL_GOALS.toLowerCase()) || normalized.includes(ULTRAGOAL_LEDGER.toLowerCase());
|
return normalized.includes(ULW_LOOP_DIR.toLowerCase()) || normalized.includes(ULW_LOOP_GOALS.toLowerCase()) || normalized.includes(ULW_LOOP_LEDGER.toLowerCase());
|
||||||
}
|
}
|
||||||
function textMentionsGoalId(value: string | undefined, goalId: string): boolean { return (value ?? "").toLowerCase().includes(goalId.toLowerCase()); }
|
function textMentionsGoalId(value: string | undefined, goalId: string): boolean { return (value ?? "").toLowerCase().includes(goalId.toLowerCase()); }
|
||||||
function textHasCompletionValidationEvidence(value: string | undefined): boolean {
|
function textHasCompletionValidationEvidence(value: string | undefined): boolean {
|
||||||
@@ -32,12 +32,12 @@ function textHasCompletionValidationEvidence(value: string | undefined): boolean
|
|||||||
return done && verified;
|
return done && verified;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function snapshotObjectiveMapsToUltragoalPlan(repoRoot: string, snapshotObjective: string): Promise<boolean> {
|
async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot: string, snapshotObjective: string): Promise<boolean> {
|
||||||
const actual = normalizeObjective(snapshotObjective).toLowerCase();
|
const actual = normalizeObjective(snapshotObjective).toLowerCase();
|
||||||
if (textMentionsUltragoalPlanArtifact(actual)) return true;
|
if (textMentionsUlwLoopPlanArtifact(actual)) return true;
|
||||||
if (actual.length < 24 || !existsSync(ultragoalBriefPath(repoRoot))) return false;
|
if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot))) return false;
|
||||||
try {
|
try {
|
||||||
const brief = normalizeObjective(await readFile(ultragoalBriefPath(repoRoot), "utf8")).toLowerCase();
|
const brief = normalizeObjective(await readFile(ulwLoopBriefPath(repoRoot), "utf8")).toLowerCase();
|
||||||
return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
|
return brief.length >= 24 && (brief.includes(actual) || actual.includes(brief));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) return false;
|
if (error instanceof Error) return false;
|
||||||
@@ -45,28 +45,28 @@ async function snapshotObjectiveMapsToUltragoalPlan(repoRoot: string, snapshotOb
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function canReconcileCompletedTaskScopedAggregateSnapshot(repoRoot: string, plan: UltragoalPlan, goal: UltragoalItem, snapshotObjective: string, evidence: string): Promise<boolean> {
|
async function canReconcileCompletedTaskScopedAggregateSnapshot(repoRoot: string, plan: UlwLoopPlan, goal: UlwLoopItem, snapshotObjective: string, evidence: string): Promise<boolean> {
|
||||||
if (codexGoalMode(plan) !== "aggregate") return false;
|
if (codexGoalMode(plan) !== "aggregate") return false;
|
||||||
if (goal.status !== "in_progress" || plan.activeGoalId !== goal.id) return false;
|
if (goal.status !== "in_progress" || plan.activeGoalId !== goal.id) return false;
|
||||||
if (isFinalRunCompletionCandidate(plan, goal)) return snapshotObjectiveMapsToUltragoalPlan(repoRoot, snapshotObjective);
|
if (isFinalRunCompletionCandidate(plan, goal)) return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective);
|
||||||
if (!textMentionsUltragoalPlanArtifact(evidence) || !textMentionsGoalId(evidence, goal.id)) return false;
|
if (!textMentionsUlwLoopPlanArtifact(evidence) || !textMentionsGoalId(evidence, goal.id)) return false;
|
||||||
if (!textHasCompletionValidationEvidence(evidence)) return false;
|
if (!textHasCompletionValidationEvidence(evidence)) return false;
|
||||||
return snapshotObjectiveMapsToUltragoalPlan(repoRoot, snapshotObjective);
|
return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective);
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCompletedLegacyGoalRemediation(goal: UltragoalItem): string {
|
function buildCompletedLegacyGoalRemediation(goal: UlwLoopItem): string {
|
||||||
return [
|
return [
|
||||||
"If get_goal returns a different completed legacy/thread objective, do not repeat --status complete in this thread.",
|
"If get_goal returns a different completed legacy/thread objective, do not repeat --status complete in this thread.",
|
||||||
`Record a non-terminal blocker with: omo ultragoal checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<different completed get_goal JSON or path>".`,
|
`Record a non-terminal blocker with: omo ulw-loop checkpoint --goal-id ${goal.id} --status blocked --evidence "<completed legacy Codex goal blocks create_goal in this thread>" --codex-goal-json "<different completed get_goal JSON or path>".`,
|
||||||
"Then continue only from a Codex goal context with no active/completed conflicting goal, in the same repo/worktree, and create the intended goal there.",
|
"Then continue only from a Codex goal context with no active/completed conflicting goal, in the same repo/worktree, and create the intended goal there.",
|
||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildTaskScopedAggregateReconciliationHint(goal: UltragoalItem, final: boolean): string {
|
function buildTaskScopedAggregateReconciliationHint(goal: UlwLoopItem, final: boolean): string {
|
||||||
if (final) {
|
if (final) {
|
||||||
return ` Final task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress final OMO goal and the completed get_goal objective to map to the ultragoal brief or artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
return ` Final task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress final OMO goal and the completed get_goal objective to map to the ulw-loop brief or artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
||||||
}
|
}
|
||||||
return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ultragoal/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ultragoal brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
return ` Completed task-scoped aggregate reconciliation requires the checkpoint goal to be the active in-progress OMO goal, evidence that names that active OMO goal id, names .omo/ulw-loop/goals.json or ledger.jsonl, includes completed implementation plus validation/review evidence, and a get_goal objective that maps to the ulw-loop brief/artifact. ${buildCompletedLegacyGoalRemediation(goal)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readJsonInput(raw: string | undefined, repoRoot: string): Promise<unknown> {
|
async function readJsonInput(raw: string | undefined, repoRoot: string): Promise<unknown> {
|
||||||
@@ -74,15 +74,15 @@ async function readJsonInput(raw: string | undefined, repoRoot: string): Promise
|
|||||||
const trimmed = raw.trim();
|
const trimmed = raw.trim();
|
||||||
try { return JSON.parse(trimmed); } catch (error) { if (!(error instanceof SyntaxError)) throw error; }
|
try { return JSON.parse(trimmed); } catch (error) { if (!(error instanceof SyntaxError)) throw error; }
|
||||||
const path = resolve(repoRoot, trimmed);
|
const path = resolve(repoRoot, trimmed);
|
||||||
if (!existsSync(path)) return ultragoalFail("Quality gate JSON is neither valid JSON nor a readable path.", "ultragoal_json_input_invalid");
|
if (!existsSync(path)) return ulwLoopFail("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
|
||||||
try { return JSON.parse(await readFile(path, "utf8")); } catch (error) { return ultragoalFail(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ultragoal_json_input_invalid"); }
|
try { return JSON.parse(await readFile(path, "utf8")); } catch (error) { return ulwLoopFail(`Quality gate path does not contain valid JSON${error instanceof Error ? `: ${error.message}` : "."}`, "ulw_loop_json_input_invalid"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeAggregateCompletion(now: string, evidence: string, codexGoal: unknown): UltragoalAggregateCompletion {
|
function makeAggregateCompletion(now: string, evidence: string, codexGoal: unknown): UlwLoopAggregateCompletion {
|
||||||
return { status: "complete", completedAt: now, evidence, codexGoal };
|
return { status: "complete", completedAt: now, evidence, codexGoal };
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyBlockedOrFailed(goal: UltragoalItem, plan: UltragoalPlan, status: "failed" | "blocked", evidence: string, now: string): void {
|
function applyBlockedOrFailed(goal: UlwLoopItem, plan: UlwLoopPlan, status: "failed" | "blocked", evidence: string, now: string): void {
|
||||||
const signature = classifyExternalAuthorizationBlocker(evidence);
|
const signature = classifyExternalAuthorizationBlocker(evidence);
|
||||||
const occurrences = signature === null ? 0 : sameBlockerOccurrences(plan, signature) + 1;
|
const occurrences = signature === null ? 0 : sameBlockerOccurrences(plan, signature) + 1;
|
||||||
const needsDecision = signature !== null && occurrences >= 3;
|
const needsDecision = signature !== null && occurrences >= 3;
|
||||||
@@ -95,15 +95,15 @@ function applyBlockedOrFailed(goal: UltragoalItem, plan: UltragoalPlan, status:
|
|||||||
if (plan.activeGoalId === goal.id) delete plan.activeGoalId;
|
if (plan.activeGoalId === goal.id) delete plan.activeGoalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ledgerKind(status: CheckpointUltragoalArgs["status"], goal: UltragoalItem, aggregateCompletion: UltragoalAggregateCompletion | undefined): UltragoalLedgerEntry["kind"] {
|
function ledgerKind(status: CheckpointUlwLoopArgs["status"], goal: UlwLoopItem, aggregateCompletion: UlwLoopAggregateCompletion | undefined): UlwLoopLedgerEntry["kind"] {
|
||||||
if (aggregateCompletion !== undefined) return "aggregate_completed";
|
if (aggregateCompletion !== undefined) return "aggregate_completed";
|
||||||
if (status === "complete") return "goal_completed";
|
if (status === "complete") return "goal_completed";
|
||||||
if (goal.status === "needs_user_decision") return "goal_needs_user_decision";
|
if (goal.status === "needs_user_decision") return "goal_needs_user_decision";
|
||||||
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
return status === "blocked" ? "goal_blocked" : "goal_failed";
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildLedger(now: string, args: CheckpointUltragoalArgs, goal: UltragoalItem, qualityGate: UltragoalQualityGate | undefined, codexGoal: unknown, aggregateCompletion: UltragoalAggregateCompletion | undefined): UltragoalLedgerEntry {
|
function buildLedger(now: string, args: CheckpointUlwLoopArgs, goal: UlwLoopItem, qualityGate: UlwLoopQualityGate | undefined, codexGoal: unknown, aggregateCompletion: UlwLoopAggregateCompletion | undefined): UlwLoopLedgerEntry {
|
||||||
const entry: UltragoalLedgerEntry = { at: now, kind: ledgerKind(args.status, goal, aggregateCompletion), goalId: goal.id, status: goal.status, evidence: args.evidence };
|
const entry: UlwLoopLedgerEntry = { at: now, kind: ledgerKind(args.status, goal, aggregateCompletion), goalId: goal.id, status: goal.status, evidence: args.evidence };
|
||||||
if (codexGoal !== undefined) entry.codexGoal = codexGoal;
|
if (codexGoal !== undefined) entry.codexGoal = codexGoal;
|
||||||
if (qualityGate !== undefined) entry.qualityGate = qualityGate;
|
if (qualityGate !== undefined) entry.qualityGate = qualityGate;
|
||||||
if (goal.blockerSignature !== undefined) entry.blockerSignature = goal.blockerSignature;
|
if (goal.blockerSignature !== undefined) entry.blockerSignature = goal.blockerSignature;
|
||||||
@@ -112,15 +112,15 @@ function buildLedger(now: string, args: CheckpointUltragoalArgs, goal: Ultragoal
|
|||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function checkpointUltragoal(repoRoot: string, args: CheckpointUltragoalArgs): Promise<CheckpointUltragoalResult> {
|
export async function checkpointUlwLoop(repoRoot: string, args: CheckpointUlwLoopArgs): Promise<CheckpointUlwLoopResult> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const goal = findGoal(plan, args.goalId);
|
const goal = findGoal(plan, args.goalId);
|
||||||
if (args.status === "complete") requireAllCriteriaPass(goal);
|
if (args.status === "complete") requireAllCriteriaPass(goal);
|
||||||
const evidence = nonEmptyEvidence(args.evidence);
|
const evidence = nonEmptyEvidence(args.evidence);
|
||||||
const now = iso();
|
const now = iso();
|
||||||
let aggregateCompletion: UltragoalAggregateCompletion | undefined;
|
let aggregateCompletion: UlwLoopAggregateCompletion | undefined;
|
||||||
let qualityGate: UltragoalQualityGate | undefined;
|
let qualityGate: UlwLoopQualityGate | undefined;
|
||||||
let codexGoal: unknown;
|
let codexGoal: unknown;
|
||||||
if (args.status === "complete") {
|
if (args.status === "complete") {
|
||||||
const aggregate = codexGoalMode(plan) === "aggregate";
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
||||||
@@ -131,7 +131,7 @@ export async function checkpointUltragoal(repoRoot: string, args: CheckpointUltr
|
|||||||
if (!reconciliation.ok) {
|
if (!reconciliation.ok) {
|
||||||
const objective = snapshot?.objective;
|
const objective = snapshot?.objective;
|
||||||
const taskScoped = snapshot?.available === true && snapshot.status === "complete" && objective !== undefined && normalizeObjective(objective) !== normalizeObjective(expectedCodexObjective(plan, goal)) && await canReconcileCompletedTaskScopedAggregateSnapshot(repoRoot, plan, goal, objective, evidence);
|
const taskScoped = snapshot?.available === true && snapshot.status === "complete" && objective !== undefined && normalizeObjective(objective) !== normalizeObjective(expectedCodexObjective(plan, goal)) && await canReconcileCompletedTaskScopedAggregateSnapshot(repoRoot, plan, goal, objective, evidence);
|
||||||
if (!taskScoped) throw new UltragoalError(`${formatCodexGoalReconciliation(reconciliation)}${aggregate && snapshot?.status === "complete" && objective !== undefined ? buildTaskScopedAggregateReconciliationHint(goal, final) : ""}`, "ultragoal_codex_snapshot_mismatch");
|
if (!taskScoped) throw new UlwLoopError(`${formatCodexGoalReconciliation(reconciliation)}${aggregate && snapshot?.status === "complete" && objective !== undefined ? buildTaskScopedAggregateReconciliationHint(goal, final) : ""}`, "ulw_loop_codex_snapshot_mismatch");
|
||||||
aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
||||||
}
|
}
|
||||||
if (final) aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
if (final) aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal);
|
||||||
+6
-6
@@ -1,7 +1,7 @@
|
|||||||
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { UltragoalError } from "./types.js";
|
import { UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
type RecordEvidenceCliArgs = { readonly goalId: string; readonly criterionId: string; readonly status: "pass" | "fail" | "blocked"; readonly evidence: string; readonly notes?: string };
|
type RecordEvidenceCliArgs = { readonly goalId: string; readonly criterionId: string; readonly status: "pass" | "fail" | "blocked"; readonly evidence: string; readonly notes?: string };
|
||||||
|
|
||||||
@@ -59,7 +59,7 @@ export async function readJsonInput(value: string | undefined): Promise<unknown
|
|||||||
try { return JSON.parse(looksLikeJson(value) ? value : await readFile(value, "utf8")); }
|
try { return JSON.parse(looksLikeJson(value) ? value : await readFile(value, "utf8")); }
|
||||||
catch (error) {
|
catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "unknown error";
|
const message = error instanceof Error ? error.message : "unknown error";
|
||||||
throw new UltragoalError(`Invalid JSON input: ${message}`, "ULTRAGOAL_JSON_INPUT_INVALID", { cause: error });
|
throw new UlwLoopError(`Invalid JSON input: ${message}`, "ULW_LOOP_JSON_INPUT_INVALID", { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +69,14 @@ export async function parseCodexGoalJson(value: string | undefined): Promise<str
|
|||||||
try { JSON.parse(raw); return raw; }
|
try { JSON.parse(raw); return raw; }
|
||||||
catch (error) {
|
catch (error) {
|
||||||
const message = error instanceof Error ? error.message : "unknown error";
|
const message = error instanceof Error ? error.message : "unknown error";
|
||||||
throw new UltragoalError(`Invalid --codex-goal-json: ${message}`, "ULTRAGOAL_CODEX_GOAL_JSON_INVALID", { cause: error });
|
throw new UlwLoopError(`Invalid --codex-goal-json: ${message}`, "ULW_LOOP_CODEX_GOAL_JSON_INVALID", { cause: error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function required(argv: readonly string[], flag: string, code: string): string {
|
function required(argv: readonly string[], flag: string, code: string): string {
|
||||||
const value = readValue(argv, flag)?.trim();
|
const value = readValue(argv, flag)?.trim();
|
||||||
if (value) return value;
|
if (value) return value;
|
||||||
throw new UltragoalError(`Missing ${flag}.`, code, { details: { flag } });
|
throw new UlwLoopError(`Missing ${flag}.`, code, { details: { flag } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function evidenceStatus(value: string): RecordEvidenceCliArgs["status"] {
|
function evidenceStatus(value: string): RecordEvidenceCliArgs["status"] {
|
||||||
@@ -84,12 +84,12 @@ function evidenceStatus(value: string): RecordEvidenceCliArgs["status"] {
|
|||||||
case "pass": return "pass";
|
case "pass": return "pass";
|
||||||
case "fail": return "fail";
|
case "fail": return "fail";
|
||||||
case "blocked": return "blocked";
|
case "blocked": return "blocked";
|
||||||
default: throw new UltragoalError("Invalid --status; expected pass, fail, or blocked.", "ULTRAGOAL_EVIDENCE_STATUS_INVALID", { details: { status: value } });
|
default: throw new UlwLoopError("Invalid --status; expected pass, fail, or blocked.", "ULW_LOOP_EVIDENCE_STATUS_INVALID", { details: { status: value } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseRecordEvidenceArgs(argv: readonly string[]): RecordEvidenceCliArgs {
|
export function parseRecordEvidenceArgs(argv: readonly string[]): RecordEvidenceCliArgs {
|
||||||
const result = { goalId: required(argv, "--goal-id", "ULTRAGOAL_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULTRAGOAL_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULTRAGOAL_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULTRAGOAL_EVIDENCE_REQUIRED") };
|
const result = { goalId: required(argv, "--goal-id", "ULW_LOOP_GOAL_ID_REQUIRED"), criterionId: required(argv, "--criterion-id", "ULW_LOOP_CRITERION_ID_REQUIRED"), status: evidenceStatus(required(argv, "--status", "ULW_LOOP_EVIDENCE_STATUS_REQUIRED")), evidence: required(argv, "--evidence", "ULW_LOOP_EVIDENCE_REQUIRED") };
|
||||||
const notes = readValue(argv, "--notes")?.trim();
|
const notes = readValue(argv, "--notes")?.trim();
|
||||||
return notes ? { ...result, notes } : result;
|
return notes ? { ...result, notes } : result;
|
||||||
}
|
}
|
||||||
+40
-40
@@ -1,28 +1,28 @@
|
|||||||
// biome-ignore-all format: keep cli-commands dispatcher under the 200 pure LOC budget.
|
// biome-ignore-all format: keep cli-commands dispatcher under the 200 pure LOC budget.
|
||||||
import { readFile } from "node:fs/promises";
|
import { readFile } from "node:fs/promises";
|
||||||
import { checkpointUltragoal } from "./checkpoint.js";
|
import { checkpointUlwLoop } from "./checkpoint.js";
|
||||||
import { hasFlag, parseCodexGoalJson, parseRecordEvidenceArgs, positionalText, readStdin, readValue } from "./cli-arg-parser.js";
|
import { hasFlag, parseCodexGoalJson, parseRecordEvidenceArgs, positionalText, readStdin, readValue } from "./cli-arg-parser.js";
|
||||||
import { blockedDecisionHandoff, normalizeCodexGoalMode, printJson, printStatus, ULTRAGOAL_HELP } from "./cli-output.js";
|
import { blockedDecisionHandoff, normalizeCodexGoalMode, printJson, printStatus, ULW_LOOP_HELP } from "./cli-output.js";
|
||||||
import { parseSteeringProposal, printSteerResult } from "./cli-steering.js";
|
import { parseSteeringProposal, printSteerResult } from "./cli-steering.js";
|
||||||
import { buildCodexGoalInstruction } from "./codex-goal-instruction.js";
|
import { buildCodexGoalInstruction } from "./codex-goal-instruction.js";
|
||||||
import { recordEvidence } from "./evidence.js";
|
import { recordEvidence } from "./evidence.js";
|
||||||
import { addUltragoalGoal, createUltragoalPlan, startNextUltragoal, summarizeUltragoalPlan } from "./plan-crud.js";
|
import { addUlwLoopGoal, createUlwLoopPlan, startNextUlwLoop, summarizeUlwLoopPlan } from "./plan-crud.js";
|
||||||
import { readUltragoalPlan } from "./plan-io.js";
|
import { readUlwLoopPlan } from "./plan-io.js";
|
||||||
import { recordFinalReviewBlockers } from "./review-blockers.js";
|
import { recordFinalReviewBlockers } from "./review-blockers.js";
|
||||||
import { steerUltragoal } from "./steering.js";
|
import { steerUlwLoop } from "./steering.js";
|
||||||
import type { UltragoalItem } from "./types.js";
|
import type { UlwLoopItem } from "./types.js";
|
||||||
import { UltragoalError } from "./types.js";
|
import { UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
type CheckpointStatus = "complete" | "failed" | "blocked";
|
type CheckpointStatus = "complete" | "failed" | "blocked";
|
||||||
|
|
||||||
export async function ultragoalCommand(argv: readonly string[]): Promise<number> {
|
export async function ulwLoopCommand(argv: readonly string[]): Promise<number> {
|
||||||
const command = argv[0] ?? "help";
|
const command = argv[0] ?? "help";
|
||||||
const rest = argv.slice(1);
|
const rest = argv.slice(1);
|
||||||
const repoRoot = process.cwd();
|
const repoRoot = process.cwd();
|
||||||
const json = hasFlag(rest, "--json");
|
const json = hasFlag(rest, "--json");
|
||||||
try {
|
try {
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "help": case "--help": case "-h": process.stdout.write(`${ULTRAGOAL_HELP}\n`); return 0;
|
case "help": case "--help": case "-h": process.stdout.write(`${ULW_LOOP_HELP}\n`); return 0;
|
||||||
case "create-goals": return await createGoals(repoRoot, rest, json);
|
case "create-goals": return await createGoals(repoRoot, rest, json);
|
||||||
case "status": return await status(repoRoot, json);
|
case "status": return await status(repoRoot, json);
|
||||||
case "complete-goals": return await completeGoals(repoRoot, rest, json);
|
case "complete-goals": return await completeGoals(repoRoot, rest, json);
|
||||||
@@ -32,12 +32,12 @@ export async function ultragoalCommand(argv: readonly string[]): Promise<number>
|
|||||||
case "criteria": return await criteria(repoRoot, rest, json);
|
case "criteria": return await criteria(repoRoot, rest, json);
|
||||||
case "record-evidence": return await captureEvidence(repoRoot, rest, json);
|
case "record-evidence": return await captureEvidence(repoRoot, rest, json);
|
||||||
case "record-review-blockers": return await reviewBlockers(repoRoot, rest, json);
|
case "record-review-blockers": return await reviewBlockers(repoRoot, rest, json);
|
||||||
default: process.stdout.write(`${ULTRAGOAL_HELP}\n`); return 1;
|
default: process.stdout.write(`${ULW_LOOP_HELP}\n`); return 1;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UltragoalError) process.stderr.write(`[ultragoal] ${error.message}\n`);
|
if (error instanceof UlwLoopError) process.stderr.write(`[ulw-loop] ${error.message}\n`);
|
||||||
else if (error instanceof Error) process.stderr.write(`[ultragoal] unexpected: ${error.message}\n`);
|
else if (error instanceof Error) process.stderr.write(`[ulw-loop] unexpected: ${error.message}\n`);
|
||||||
else process.stderr.write("[ultragoal] unknown error\n");
|
else process.stderr.write("[ulw-loop] unknown error\n");
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,26 +45,26 @@ export async function ultragoalCommand(argv: readonly string[]): Promise<number>
|
|||||||
async function createGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function createGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const briefFile = readValue(argv, "--brief-file");
|
const briefFile = readValue(argv, "--brief-file");
|
||||||
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
const brief = readValue(argv, "--brief") ?? (briefFile === undefined ? undefined : await readFile(briefFile, "utf8")) ?? (hasFlag(argv, "--from-stdin") ? await readStdin() : undefined) ?? positionalText(argv);
|
||||||
if (!brief.trim()) throw new UltragoalError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULTRAGOAL_BRIEF_REQUIRED");
|
if (!brief.trim()) throw new UlwLoopError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULW_LOOP_BRIEF_REQUIRED");
|
||||||
const plan = await createUltragoalPlan(repoRoot, { brief, codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")), force: hasFlag(argv, "--force") });
|
const plan = await createUlwLoopPlan(repoRoot, { brief, codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")), force: hasFlag(argv, "--force") });
|
||||||
if (json) printJson({ ok: true, plan, summary: summarizeUltragoalPlan(plan) });
|
if (json) printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) });
|
||||||
else process.stdout.write(`ultragoal plan created: ${plan.goals.length} goal(s)\nbrief: ${plan.briefPath}\ngoals: ${plan.goalsPath}\nledger: ${plan.ledgerPath}\n`);
|
else process.stdout.write(`ulw-loop plan created: ${plan.goals.length} goal(s)\nbrief: ${plan.briefPath}\ngoals: ${plan.goalsPath}\nledger: ${plan.ledgerPath}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function status(repoRoot: string, json: boolean): Promise<number> {
|
async function status(repoRoot: string, json: boolean): Promise<number> {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
if (json) printJson({ ok: true, plan, summary: summarizeUltragoalPlan(plan) });
|
if (json) printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) });
|
||||||
else printStatus(plan);
|
else printStatus(plan);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function completeGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function completeGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const result = await startNextUltragoal(repoRoot, { retryFailed: hasFlag(argv, "--retry-failed") });
|
const result = await startNextUlwLoop(repoRoot, { retryFailed: hasFlag(argv, "--retry-failed") });
|
||||||
if ("done" in result) {
|
if ("done" in result) {
|
||||||
const handoff = blockedDecisionHandoff(result.plan);
|
const handoff = blockedDecisionHandoff(result.plan);
|
||||||
if (json) printJson({ ok: true, done: true, blocked: handoff.length > 0, handoff, summary: summarizeUltragoalPlan(result.plan), plan: result.plan });
|
if (json) printJson({ ok: true, done: true, blocked: handoff.length > 0, handoff, summary: summarizeUlwLoopPlan(result.plan), plan: result.plan });
|
||||||
else process.stdout.write(`${handoff || "ultragoal: all goals complete"}\n`);
|
else process.stdout.write(`${handoff || "ulw-loop: all goals complete"}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
const instruction = buildCodexGoalInstruction({ plan: result.plan, goal: result.goal });
|
const instruction = buildCodexGoalInstruction({ plan: result.plan, goal: result.goal });
|
||||||
@@ -78,31 +78,31 @@ async function checkpoint(repoRoot: string, argv: readonly string[], json: boole
|
|||||||
const statusValue = checkpointStatus(required(argv, "--status"));
|
const statusValue = checkpointStatus(required(argv, "--status"));
|
||||||
const evidence = required(argv, "--evidence");
|
const evidence = required(argv, "--evidence");
|
||||||
const codexGoalJson = await parseCodexGoalJson(required(argv, "--codex-goal-json"));
|
const codexGoalJson = await parseCodexGoalJson(required(argv, "--codex-goal-json"));
|
||||||
if (codexGoalJson === undefined) throw new UltragoalError("Missing --codex-goal-json.", "ULTRAGOAL_CODEX_GOAL_JSON_REQUIRED");
|
if (codexGoalJson === undefined) throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
|
||||||
const qualityGateJson = readValue(argv, "--quality-gate-json");
|
const qualityGateJson = readValue(argv, "--quality-gate-json");
|
||||||
const result = await checkpointUltragoal(repoRoot, qualityGateJson === undefined ? { goalId, status: statusValue, evidence, codexGoalJson } : { goalId, status: statusValue, evidence, codexGoalJson, qualityGateJson });
|
const result = await checkpointUlwLoop(repoRoot, qualityGateJson === undefined ? { goalId, status: statusValue, evidence, codexGoalJson } : { goalId, status: statusValue, evidence, codexGoalJson, qualityGateJson });
|
||||||
if (json) printJson({ ok: true, ...result, summary: summarizeUltragoalPlan(result.plan) });
|
if (json) printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
|
||||||
else process.stdout.write(`ultragoal checkpoint: ${result.goal.id} -> ${result.goal.status}\n`);
|
else process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function steer(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function steer(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const proposal = await parseSteeringProposal(argv);
|
const proposal = await parseSteeringProposal(argv);
|
||||||
const result = await steerUltragoal(repoRoot, proposal);
|
const result = await steerUlwLoop(repoRoot, proposal);
|
||||||
printSteerResult(result, json);
|
printSteerResult(result, json);
|
||||||
return result.accepted ? 0 : 1;
|
return result.accepted ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function addGoal(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function addGoal(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const result = await addUltragoalGoal(repoRoot, { title: required(argv, "--title"), objective: required(argv, "--objective") });
|
const result = await addUlwLoopGoal(repoRoot, { title: required(argv, "--title"), objective: required(argv, "--objective") });
|
||||||
if (json) printJson({ ok: true, plan: result.plan, goal: result.goal, summary: summarizeUltragoalPlan(result.plan) });
|
if (json) printJson({ ok: true, plan: result.plan, goal: result.goal, summary: summarizeUlwLoopPlan(result.plan) });
|
||||||
else { process.stdout.write(`ultragoal added goal: ${result.goal.id}\n`); printStatus(result.plan); }
|
else { process.stdout.write(`ulw-loop added goal: ${result.goal.id}\n`); printStatus(result.plan); }
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function criteria(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function criteria(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const goalId = required(argv, "--goal-id");
|
const goalId = required(argv, "--goal-id");
|
||||||
const goal = findGoal(await readUltragoalPlan(repoRoot), goalId);
|
const goal = findGoal(await readUlwLoopPlan(repoRoot), goalId);
|
||||||
if (json) printJson({ ok: true, goalId: goal.id, criteria: goal.successCriteria });
|
if (json) printJson({ ok: true, goalId: goal.id, criteria: goal.successCriteria });
|
||||||
else process.stdout.write(`criteria for ${goal.id}:\n${goal.successCriteria.map((c) => `- ${c.id} [${c.status}] (${c.userModel}) ${c.scenario} evidence: ${c.capturedEvidence ?? "pending"}`).join("\n")}\n`);
|
else process.stdout.write(`criteria for ${goal.id}:\n${goal.successCriteria.map((c) => `- ${c.id} [${c.status}] (${c.userModel}) ${c.scenario} evidence: ${c.capturedEvidence ?? "pending"}`).join("\n")}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -110,33 +110,33 @@ async function criteria(repoRoot: string, argv: readonly string[], json: boolean
|
|||||||
|
|
||||||
async function captureEvidence(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function captureEvidence(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const result = await recordEvidence(repoRoot, parseRecordEvidenceArgs(argv));
|
const result = await recordEvidence(repoRoot, parseRecordEvidenceArgs(argv));
|
||||||
if (json) printJson({ ok: true, ...result, summary: summarizeUltragoalPlan(result.plan) });
|
if (json) printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) });
|
||||||
else process.stdout.write(`ultragoal evidence recorded: ${result.goal.id}/${result.criterion.id} -> ${result.criterion.status}\n`);
|
else process.stdout.write(`ulw-loop evidence recorded: ${result.goal.id}/${result.criterion.id} -> ${result.criterion.status}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function reviewBlockers(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
async function reviewBlockers(repoRoot: string, argv: readonly string[], json: boolean): Promise<number> {
|
||||||
const codexGoalJson = await parseCodexGoalJson(required(argv, "--codex-goal-json"));
|
const codexGoalJson = await parseCodexGoalJson(required(argv, "--codex-goal-json"));
|
||||||
if (codexGoalJson === undefined) throw new UltragoalError("Missing --codex-goal-json.", "ULTRAGOAL_CODEX_GOAL_JSON_REQUIRED");
|
if (codexGoalJson === undefined) throw new UlwLoopError("Missing --codex-goal-json.", "ULW_LOOP_CODEX_GOAL_JSON_REQUIRED");
|
||||||
const result = await recordFinalReviewBlockers(repoRoot, { goalId: required(argv, "--goal-id"), title: required(argv, "--title"), objective: required(argv, "--objective"), evidence: required(argv, "--evidence"), codexGoalJson });
|
const result = await recordFinalReviewBlockers(repoRoot, { goalId: required(argv, "--goal-id"), title: required(argv, "--title"), objective: required(argv, "--objective"), evidence: required(argv, "--evidence"), codexGoalJson });
|
||||||
if (json) printJson({ ok: true, plan: result.plan, blockedGoal: result.blockedGoal, goal: result.newGoal, ledgerEntries: result.ledgerEntries, summary: summarizeUltragoalPlan(result.plan) });
|
if (json) printJson({ ok: true, plan: result.plan, blockedGoal: result.blockedGoal, goal: result.newGoal, ledgerEntries: result.ledgerEntries, summary: summarizeUlwLoopPlan(result.plan) });
|
||||||
else process.stdout.write(`ultragoal final review blockers recorded: ${result.blockedGoal.id} -> review_blocked; added ${result.newGoal.id}\n`);
|
else process.stdout.write(`ulw-loop final review blockers recorded: ${result.blockedGoal.id} -> review_blocked; added ${result.newGoal.id}\n`);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
function required(argv: readonly string[], flag: string): string {
|
function required(argv: readonly string[], flag: string): string {
|
||||||
const value = readValue(argv, flag)?.trim();
|
const value = readValue(argv, flag)?.trim();
|
||||||
if (value) return value;
|
if (value) return value;
|
||||||
throw new UltragoalError(`Missing ${flag}.`, "ULTRAGOAL_ARGUMENT_MISSING", { details: { flag } });
|
throw new UlwLoopError(`Missing ${flag}.`, "ULW_LOOP_ARGUMENT_MISSING", { details: { flag } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkpointStatus(value: string): CheckpointStatus {
|
function checkpointStatus(value: string): CheckpointStatus {
|
||||||
if (value === "complete" || value === "failed" || value === "blocked") return value;
|
if (value === "complete" || value === "failed" || value === "blocked") return value;
|
||||||
throw new UltragoalError("Missing or invalid --status; expected complete, failed, or blocked.", "ULTRAGOAL_STATUS_INVALID", { details: { status: value } });
|
throw new UlwLoopError("Missing or invalid --status; expected complete, failed, or blocked.", "ULW_LOOP_STATUS_INVALID", { details: { status: value } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGoal(plan: { readonly goals: readonly UltragoalItem[] }, goalId: string): UltragoalItem {
|
function findGoal(plan: { readonly goals: readonly UlwLoopItem[] }, goalId: string): UlwLoopItem {
|
||||||
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
||||||
if (goal !== undefined) return goal;
|
if (goal !== undefined) return goal;
|
||||||
throw new UltragoalError(`Unknown ultragoal id: ${goalId}.`, "ULTRAGOAL_GOAL_NOT_FOUND", { details: { goalId } });
|
throw new UlwLoopError(`Unknown ulw-loop id: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { details: { goalId } });
|
||||||
}
|
}
|
||||||
+20
-20
@@ -1,16 +1,16 @@
|
|||||||
import type { UltragoalCodexGoalMode, UltragoalItem, UltragoalPlan } from "./types.js";
|
import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan } from "./types.js";
|
||||||
import { UltragoalError } from "./types.js";
|
import { UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
export const ULTRAGOAL_HELP = `Usage:
|
export const ULW_LOOP_HELP = `Usage:
|
||||||
omo ultragoal create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json]
|
omo ulw-loop create-goals --brief "..." [--brief-file <path>] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json]
|
||||||
omo ultragoal status [--json]
|
omo ulw-loop status [--json]
|
||||||
omo ultragoal complete-goals [--retry-failed] [--json]
|
omo ulw-loop complete-goals [--retry-failed] [--json]
|
||||||
omo ultragoal criteria --goal-id <id> [--json]
|
omo ulw-loop criteria --goal-id <id> [--json]
|
||||||
omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass|fail|blocked --evidence "..." [--notes "..."] [--json]
|
||||||
omo ultragoal checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json]
|
omo ulw-loop checkpoint --goal-id <id> --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json]
|
||||||
omo ultragoal steer --kind <kind> ... --evidence "..." --rationale "..." [--json]
|
omo ulw-loop steer --kind <kind> ... --evidence "..." --rationale "..." [--json]
|
||||||
omo ultragoal add-goal --title "..." --objective "..." [--json]
|
omo ulw-loop add-goal --title "..." --objective "..." [--json]
|
||||||
omo ultragoal record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]`;
|
omo ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]`;
|
||||||
|
|
||||||
type CriteriaCounts = { readonly pass: number; readonly total: number };
|
type CriteriaCounts = { readonly pass: number; readonly total: number };
|
||||||
|
|
||||||
@@ -18,16 +18,16 @@ export function printJson(value: unknown): void {
|
|||||||
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function criteriaCounts(goal: UltragoalItem): CriteriaCounts {
|
function criteriaCounts(goal: UlwLoopItem): CriteriaCounts {
|
||||||
let pass = 0;
|
let pass = 0;
|
||||||
for (const criterion of goal.successCriteria) if (criterion.status === "pass") pass += 1;
|
for (const criterion of goal.successCriteria) if (criterion.status === "pass") pass += 1;
|
||||||
return { pass, total: goal.successCriteria.length };
|
return { pass, total: goal.successCriteria.length };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printStatus(plan: UltragoalPlan): void {
|
export function printStatus(plan: UlwLoopPlan): void {
|
||||||
let totalCriteria = 0;
|
let totalCriteria = 0;
|
||||||
let passCriteria = 0;
|
let passCriteria = 0;
|
||||||
const lines = ["ultragoal status", "", "goals:"];
|
const lines = ["ulw-loop status", "", "goals:"];
|
||||||
for (const goal of plan.goals) {
|
for (const goal of plan.goals) {
|
||||||
const counts = criteriaCounts(goal);
|
const counts = criteriaCounts(goal);
|
||||||
totalCriteria += counts.total;
|
totalCriteria += counts.total;
|
||||||
@@ -39,23 +39,23 @@ export function printStatus(plan: UltragoalPlan): void {
|
|||||||
process.stdout.write(`${lines.join("\n")}\n`);
|
process.stdout.write(`${lines.join("\n")}\n`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function blockedDecisionHandoff(plan: UltragoalPlan): string {
|
export function blockedDecisionHandoff(plan: UlwLoopPlan): string {
|
||||||
const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
|
const blocked = plan.goals.find((goal) => goal.status === "needs_user_decision" && goal.nonRetriable);
|
||||||
if (blocked === undefined) return "";
|
if (blocked === undefined) return "";
|
||||||
return [
|
return [
|
||||||
"ultragoal: blocked on repeated external authorization; no retryable failed goals remain.",
|
"ulw-loop: blocked on repeated external authorization; no retryable failed goals remain.",
|
||||||
`Goal: ${blocked.id} - ${blocked.title}`,
|
`Goal: ${blocked.id} - ${blocked.title}`,
|
||||||
`Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
|
`Required external decision: ${blocked.requiredExternalDecision ?? "provide the missing authorization or choose a different unblock path"}.`,
|
||||||
"Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path.",
|
"Do not run complete-goals --retry-failed again until external state changes or the user authorizes an unblock path.",
|
||||||
].join("\n");
|
].join("\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCodexGoalMode(value: string | undefined): UltragoalCodexGoalMode {
|
export function normalizeCodexGoalMode(value: string | undefined): UlwLoopCodexGoalMode {
|
||||||
if (value === undefined) return "aggregate";
|
if (value === undefined) return "aggregate";
|
||||||
if (value === "aggregate" || value === "per_story") return value;
|
if (value === "aggregate" || value === "per_story") return value;
|
||||||
throw new UltragoalError(
|
throw new UlwLoopError(
|
||||||
"Invalid --codex-goal-mode; expected aggregate or per_story.",
|
"Invalid --codex-goal-mode; expected aggregate or per_story.",
|
||||||
"ULTRAGOAL_CODEX_GOAL_MODE_INVALID",
|
"ULW_LOOP_CODEX_GOAL_MODE_INVALID",
|
||||||
{ details: { value } },
|
{ details: { value } },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
+29
-29
@@ -1,63 +1,63 @@
|
|||||||
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
||||||
import { parseGoalArg, readJsonInput, readValue } from "./cli-arg-parser.js";
|
import { parseGoalArg, readJsonInput, readValue } from "./cli-arg-parser.js";
|
||||||
import { printJson, printStatus } from "./cli-output.js";
|
import { printJson, printStatus } from "./cli-output.js";
|
||||||
import type { SteerUltragoalResult, UltragoalSteeringChildGoal, UltragoalSteeringMutationKind, UltragoalSteeringProposal, UltragoalSteeringSource, UltragoalSuccessCriterionUserModel } from "./types.js";
|
import type { SteerUlwLoopResult, UlwLoopSteeringChildGoal, UlwLoopSteeringMutationKind, UlwLoopSteeringProposal, UlwLoopSteeringSource, UlwLoopSuccessCriterionUserModel } from "./types.js";
|
||||||
import { ULTRAGOAL_STEERING_MUTATION_KINDS, ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS, UltragoalError } from "./types.js";
|
import { ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UltragoalSteeringSource[];
|
const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UlwLoopSteeringSource[];
|
||||||
|
|
||||||
export type CliSteeringProposal = UltragoalSteeringProposal & { readonly goalId?: string; readonly scenario?: string; readonly expectedEvidence?: string; readonly userModel?: UltragoalSuccessCriterionUserModel };
|
export type CliSteeringProposal = UlwLoopSteeringProposal & { readonly goalId?: string; readonly scenario?: string; readonly expectedEvidence?: string; readonly userModel?: UlwLoopSuccessCriterionUserModel };
|
||||||
|
|
||||||
function isKind(value: string | undefined): value is UltragoalSteeringMutationKind { return value !== undefined && ULTRAGOAL_STEERING_MUTATION_KINDS.some((kind) => kind === value); }
|
function isKind(value: string | undefined): value is UlwLoopSteeringMutationKind { return value !== undefined && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value); }
|
||||||
function isSource(value: string | undefined): value is UltragoalSteeringSource { return value !== undefined && SOURCES.some((source) => source === value); }
|
function isSource(value: string | undefined): value is UlwLoopSteeringSource { return value !== undefined && SOURCES.some((source) => source === value); }
|
||||||
function isModel(value: string): value is UltragoalSuccessCriterionUserModel { return ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value); }
|
function isModel(value: string): value is UlwLoopSuccessCriterionUserModel { return ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value); }
|
||||||
function fail(message: string, code: string, details: Record<string, unknown>): never { throw new UltragoalError(message, code, { details }); }
|
function fail(message: string, code: string, details: Record<string, unknown>): never { throw new UlwLoopError(message, code, { details }); }
|
||||||
function text(value: string | undefined, field: string): string | undefined { if (value === undefined) return undefined; const trimmed = value.trim(); if (trimmed.length > 0) return trimmed; return fail(`Empty ${field}.`, "ULTRAGOAL_STEERING_FIELD_EMPTY", { field }); }
|
function text(value: string | undefined, field: string): string | undefined { if (value === undefined) return undefined; const trimmed = value.trim(); if (trimmed.length > 0) return trimmed; return fail(`Empty ${field}.`, "ULW_LOOP_STEERING_FIELD_EMPTY", { field }); }
|
||||||
function required(argv: readonly string[], flag: string): string { const value = text(readValue(argv, flag), flag); return value ?? fail(`Missing ${flag}.`, "ULTRAGOAL_STEERING_FIELD_REQUIRED", { flag }); }
|
function required(argv: readonly string[], flag: string): string { const value = text(readValue(argv, flag), flag); return value ?? fail(`Missing ${flag}.`, "ULW_LOOP_STEERING_FIELD_REQUIRED", { flag }); }
|
||||||
function requiredGoal(argv: readonly string[]): string { const value = text(parseGoalArg(argv), "--goal-id"); return value ?? fail("Missing --goal-id.", "ULTRAGOAL_GOAL_ID_REQUIRED", { flag: "--goal-id" }); }
|
function requiredGoal(argv: readonly string[]): string { const value = text(parseGoalArg(argv), "--goal-id"); return value ?? fail("Missing --goal-id.", "ULW_LOOP_GOAL_ID_REQUIRED", { flag: "--goal-id" }); }
|
||||||
function readObject(value: object, key: string): unknown { return Object.entries(value).find(([name]) => name === key)?.[1]; }
|
function readObject(value: object, key: string): unknown { return Object.entries(value).find(([name]) => name === key)?.[1]; }
|
||||||
function isPlain(value: unknown): value is object { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
function isPlain(value: unknown): value is object { return typeof value === "object" && value !== null && !Array.isArray(value); }
|
||||||
function objectText(value: object, key: string): string | undefined { const candidate = readObject(value, key); return typeof candidate === "string" ? candidate : undefined; }
|
function objectText(value: object, key: string): string | undefined { const candidate = readObject(value, key); return typeof candidate === "string" ? candidate : undefined; }
|
||||||
|
|
||||||
export function parseSteeringKind(argv: readonly string[]): UltragoalSteeringMutationKind {
|
export function parseSteeringKind(argv: readonly string[]): UlwLoopSteeringMutationKind {
|
||||||
const value = readValue(argv, "--kind");
|
const value = readValue(argv, "--kind");
|
||||||
if (isKind(value)) return value;
|
if (isKind(value)) return value;
|
||||||
return value === undefined ? fail("Missing --kind.", "ULTRAGOAL_STEERING_KIND_REQUIRED", { flag: "--kind" }) : fail(`Invalid --kind: ${value}.`, "ULTRAGOAL_STEERING_KIND_INVALID", { value, expected: ULTRAGOAL_STEERING_MUTATION_KINDS });
|
return value === undefined ? fail("Missing --kind.", "ULW_LOOP_STEERING_KIND_REQUIRED", { flag: "--kind" }) : fail(`Invalid --kind: ${value}.`, "ULW_LOOP_STEERING_KIND_INVALID", { value, expected: ULW_LOOP_STEERING_MUTATION_KINDS });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSteeringSource(argv: readonly string[]): UltragoalSteeringSource {
|
export function parseSteeringSource(argv: readonly string[]): UlwLoopSteeringSource {
|
||||||
const value = readValue(argv, "--source");
|
const value = readValue(argv, "--source");
|
||||||
if (value === undefined) return "cli";
|
if (value === undefined) return "cli";
|
||||||
return isSource(value) ? value : fail(`Invalid --source: ${value}.`, "ULTRAGOAL_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
|
return isSource(value) ? value : fail(`Invalid --source: ${value}.`, "ULW_LOOP_STEERING_SOURCE_INVALID", { value, expected: SOURCES });
|
||||||
}
|
}
|
||||||
|
|
||||||
function child(value: unknown): UltragoalSteeringChildGoal | null {
|
function child(value: unknown): UlwLoopSteeringChildGoal | null {
|
||||||
if (!isPlain(value)) return null;
|
if (!isPlain(value)) return null;
|
||||||
const title = text(objectText(value, "title"), "title"); const objective = text(objectText(value, "objective"), "objective");
|
const title = text(objectText(value, "title"), "title"); const objective = text(objectText(value, "objective"), "objective");
|
||||||
if (title === undefined || objective === undefined) return null;
|
if (title === undefined || objective === undefined) return null;
|
||||||
return { title, objective };
|
return { title, objective };
|
||||||
}
|
}
|
||||||
|
|
||||||
async function children(argv: readonly string[], flag: string, needed: boolean): Promise<UltragoalSteeringChildGoal[]> {
|
async function children(argv: readonly string[], flag: string, needed: boolean): Promise<UlwLoopSteeringChildGoal[]> {
|
||||||
const input = needed ? required(argv, flag) : text(readValue(argv, flag), flag);
|
const input = needed ? required(argv, flag) : text(readValue(argv, flag), flag);
|
||||||
if (input === undefined) return [];
|
if (input === undefined) return [];
|
||||||
const raw = await readJsonInput(input);
|
const raw = await readJsonInput(input);
|
||||||
if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULTRAGOAL_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
||||||
const parsed: UltragoalSteeringChildGoal[] = [];
|
const parsed: UlwLoopSteeringChildGoal[] = [];
|
||||||
for (const item of raw) { const next = child(item); if (next === null) return fail(`${flag} entries require title/objective.`, "ULTRAGOAL_STEERING_CHILD_INVALID", { flag }); parsed.push(next); }
|
for (const item of raw) { const next = child(item); if (next === null) return fail(`${flag} entries require title/objective.`, "ULW_LOOP_STEERING_CHILD_INVALID", { flag }); parsed.push(next); }
|
||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stringArray(argv: readonly string[], flag: string): Promise<string[]> {
|
async function stringArray(argv: readonly string[], flag: string): Promise<string[]> {
|
||||||
const raw = await readJsonInput(required(argv, flag));
|
const raw = await readJsonInput(required(argv, flag));
|
||||||
if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULTRAGOAL_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag });
|
||||||
const values: string[] = [];
|
const values: string[] = [];
|
||||||
for (const item of raw) { if (typeof item !== "string") return fail(`${flag} entries must be strings.`, "ULTRAGOAL_STEERING_STRING_ARRAY_REQUIRED", { flag }); values.push(text(item, flag) ?? ""); }
|
for (const item of raw) { if (typeof item !== "string") return fail(`${flag} entries must be strings.`, "ULW_LOOP_STEERING_STRING_ARRAY_REQUIRED", { flag }); values.push(text(item, flag) ?? ""); }
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
function model(value: string | undefined): UltragoalSuccessCriterionUserModel | undefined { const trimmed = text(value, "--user-model"); if (trimmed === undefined) return undefined; return isModel(trimmed) ? trimmed : fail(`Invalid --user-model: ${trimmed}.`, "ULTRAGOAL_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS }); }
|
function model(value: string | undefined): UlwLoopSuccessCriterionUserModel | undefined { const trimmed = text(value, "--user-model"); if (trimmed === undefined) return undefined; return isModel(trimmed) ? trimmed : fail(`Invalid --user-model: ${trimmed}.`, "ULW_LOOP_STEERING_USER_MODEL_INVALID", { value: trimmed, expected: ULW_LOOP_SUCCESS_CRITERION_USER_MODELS }); }
|
||||||
function neverKind(kind: never): never { return fail(`Unsupported steering kind: ${String(kind)}.`, "ULTRAGOAL_STEERING_KIND_UNSUPPORTED", { kind }); }
|
function neverKind(kind: never): never { return fail(`Unsupported steering kind: ${String(kind)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind }); }
|
||||||
|
|
||||||
export async function parseSteeringProposal(argv: readonly string[]): Promise<CliSteeringProposal> {
|
export async function parseSteeringProposal(argv: readonly string[]): Promise<CliSteeringProposal> {
|
||||||
const kind = parseSteeringKind(argv); const source = parseSteeringSource(argv); const base = { kind, source, evidence: required(argv, "--evidence"), rationale: required(argv, "--rationale") };
|
const kind = parseSteeringKind(argv); const source = parseSteeringSource(argv); const base = { kind, source, evidence: required(argv, "--evidence"), rationale: required(argv, "--rationale") };
|
||||||
@@ -65,15 +65,15 @@ export async function parseSteeringProposal(argv: readonly string[]): Promise<Cl
|
|||||||
case "add_subgoal": return normalizeSteeringProposal({ ...base, title: required(argv, "--title"), objective: required(argv, "--objective") });
|
case "add_subgoal": return normalizeSteeringProposal({ ...base, title: required(argv, "--title"), objective: required(argv, "--objective") });
|
||||||
case "split_subgoal": { const goalId = requiredGoal(argv); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) }); }
|
case "split_subgoal": { const goalId = requiredGoal(argv); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, childGoals: await children(argv, "--children", true) }); }
|
||||||
case "reorder_pending": return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray(argv, "--order") });
|
case "reorder_pending": return normalizeSteeringProposal({ ...base, pendingOrder: await stringArray(argv, "--order") });
|
||||||
case "revise_pending_wording": { const goalId = requiredGoal(argv); const revisedTitle = readValue(argv, "--title"); const revisedObjective = readValue(argv, "--objective"); if (revisedTitle === undefined && revisedObjective === undefined) return fail("revise_pending_wording requires --title or --objective.", "ULTRAGOAL_STEERING_UPDATE_REQUIRED", { kind }); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...(revisedTitle === undefined ? {} : { revisedTitle }), ...(revisedObjective === undefined ? {} : { revisedObjective }) }); }
|
case "revise_pending_wording": { const goalId = requiredGoal(argv); const revisedTitle = readValue(argv, "--title"); const revisedObjective = readValue(argv, "--objective"); if (revisedTitle === undefined && revisedObjective === undefined) return fail("revise_pending_wording requires --title or --objective.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind }); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...(revisedTitle === undefined ? {} : { revisedTitle }), ...(revisedObjective === undefined ? {} : { revisedObjective }) }); }
|
||||||
case "revise_criterion": { const goalId = requiredGoal(argv); const criterionId = required(argv, "--criterion-id"); const scenario = readValue(argv, "--scenario"); const expectedEvidence = readValue(argv, "--expected-evidence"); const userModel = model(readValue(argv, "--user-model")); if (scenario === undefined && expectedEvidence === undefined && userModel === undefined) return fail("revise_criterion requires scenario, expected-evidence, or user-model.", "ULTRAGOAL_STEERING_UPDATE_REQUIRED", { kind }); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...(scenario === undefined ? {} : { scenario }), ...(expectedEvidence === undefined ? {} : { expectedEvidence }), ...(userModel === undefined ? {} : { userModel }) }); }
|
case "revise_criterion": { const goalId = requiredGoal(argv); const criterionId = required(argv, "--criterion-id"); const scenario = readValue(argv, "--scenario"); const expectedEvidence = readValue(argv, "--expected-evidence"); const userModel = model(readValue(argv, "--user-model")); if (scenario === undefined && expectedEvidence === undefined && userModel === undefined) return fail("revise_criterion requires scenario, expected-evidence, or user-model.", "ULW_LOOP_STEERING_UPDATE_REQUIRED", { kind }); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, criterionId, ...(scenario === undefined ? {} : { scenario }), ...(expectedEvidence === undefined ? {} : { expectedEvidence }), ...(userModel === undefined ? {} : { userModel }) }); }
|
||||||
case "annotate_ledger": return normalizeSteeringProposal(base);
|
case "annotate_ledger": return normalizeSteeringProposal(base);
|
||||||
case "mark_blocked_superseded": { const goalId = requiredGoal(argv); const childGoals = await children(argv, "--replacements", false); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...(childGoals.length === 0 ? {} : { childGoals }) }); }
|
case "mark_blocked_superseded": { const goalId = requiredGoal(argv); const childGoals = await children(argv, "--replacements", false); return normalizeSteeringProposal({ ...base, goalId, targetGoalId: goalId, ...(childGoals.length === 0 ? {} : { childGoals }) }); }
|
||||||
default: return neverKind(kind);
|
default: return neverKind(kind);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizedChildren(values: readonly UltragoalSteeringChildGoal[] | undefined): UltragoalSteeringChildGoal[] | undefined { if (values === undefined) return undefined; return values.map((item) => ({ title: text(item.title, "child.title") ?? "", objective: text(item.objective, "child.objective") ?? "" })); }
|
function normalizedChildren(values: readonly UlwLoopSteeringChildGoal[] | undefined): UlwLoopSteeringChildGoal[] | undefined { if (values === undefined) return undefined; return values.map((item) => ({ title: text(item.title, "child.title") ?? "", objective: text(item.objective, "child.objective") ?? "" })); }
|
||||||
function normalizedStrings(values: readonly string[] | undefined, field: string): string[] | undefined { if (values === undefined) return undefined; return values.map((value) => text(value, field) ?? ""); }
|
function normalizedStrings(values: readonly string[] | undefined, field: string): string[] | undefined { if (values === undefined) return undefined; return values.map((value) => text(value, field) ?? ""); }
|
||||||
|
|
||||||
export function normalizeSteeringProposal(proposal: CliSteeringProposal): CliSteeringProposal {
|
export function normalizeSteeringProposal(proposal: CliSteeringProposal): CliSteeringProposal {
|
||||||
@@ -84,10 +84,10 @@ export function normalizeSteeringProposal(proposal: CliSteeringProposal): CliSte
|
|||||||
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...(goalId === undefined ? {} : { goalId }), ...(targetGoalId === undefined ? {} : { targetGoalId }), ...(targetGoalIds === undefined ? {} : { targetGoalIds }), ...(criterionId === undefined ? {} : { criterionId }), ...(title === undefined ? {} : { title }), ...(objective === undefined ? {} : { objective }), ...(childGoals === undefined ? {} : { childGoals }), ...(revisedTitle === undefined ? {} : { revisedTitle }), ...(revisedObjective === undefined ? {} : { revisedObjective }), ...(pendingOrder === undefined ? {} : { pendingOrder }), ...(blockedReason === undefined ? {} : { blockedReason }), ...(proposal.after === undefined ? {} : { after: proposal.after }), ...(directiveText === undefined ? {} : { directiveText }), ...(promptSignature === undefined ? {} : { promptSignature }), ...(idempotencyKey === undefined ? {} : { idempotencyKey }), ...(proposal.now === undefined ? {} : { now: proposal.now }), ...(scenario === undefined ? {} : { scenario }), ...(expectedEvidence === undefined ? {} : { expectedEvidence }), ...(proposal.userModel === undefined ? {} : { userModel: proposal.userModel }) };
|
return { kind: proposal.kind, source: proposal.source, evidence, rationale, ...(goalId === undefined ? {} : { goalId }), ...(targetGoalId === undefined ? {} : { targetGoalId }), ...(targetGoalIds === undefined ? {} : { targetGoalIds }), ...(criterionId === undefined ? {} : { criterionId }), ...(title === undefined ? {} : { title }), ...(objective === undefined ? {} : { objective }), ...(childGoals === undefined ? {} : { childGoals }), ...(revisedTitle === undefined ? {} : { revisedTitle }), ...(revisedObjective === undefined ? {} : { revisedObjective }), ...(pendingOrder === undefined ? {} : { pendingOrder }), ...(blockedReason === undefined ? {} : { blockedReason }), ...(proposal.after === undefined ? {} : { after: proposal.after }), ...(directiveText === undefined ? {} : { directiveText }), ...(promptSignature === undefined ? {} : { promptSignature }), ...(idempotencyKey === undefined ? {} : { idempotencyKey }), ...(proposal.now === undefined ? {} : { now: proposal.now }), ...(scenario === undefined ? {} : { scenario }), ...(expectedEvidence === undefined ? {} : { expectedEvidence }), ...(proposal.userModel === undefined ? {} : { userModel: proposal.userModel }) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function printSteerResult(result: SteerUltragoalResult, json: boolean): void {
|
export function printSteerResult(result: SteerUlwLoopResult, json: boolean): void {
|
||||||
if (json) { printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, deduped: result.deduped, audit: result.audit, plan: result.plan }); return; }
|
if (json) { printJson({ ok: result.accepted, accepted: result.accepted, rejectedReasons: result.rejectedReasons, deduped: result.deduped, audit: result.audit, plan: result.plan }); return; }
|
||||||
const outcome = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
|
const outcome = result.deduped ? "deduped" : result.accepted ? "accepted" : "rejected";
|
||||||
process.stdout.write(`ultragoal steer: ${outcome} ${result.audit.kind}\n`);
|
process.stdout.write(`ulw-loop steer: ${outcome} ${result.audit.kind}\n`);
|
||||||
if (result.rejectedReasons.length > 0) process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}\n`);
|
if (result.rejectedReasons.length > 0) process.stdout.write(`rejected: ${result.rejectedReasons.join("; ")}\n`);
|
||||||
if (result.audit.idempotencyKey !== undefined) process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}\n`);
|
if (result.audit.idempotencyKey !== undefined) process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}\n`);
|
||||||
printStatus(result.plan);
|
printStatus(result.plan);
|
||||||
+5
-5
@@ -1,9 +1,9 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
import { ultragoalCommand } from "./cli-commands.js";
|
import { ulwLoopCommand } from "./cli-commands.js";
|
||||||
import { runPreToolUseGoalBudgetGuardCli, runUltragoalHookCli } from "./codex-hook.js";
|
import { runPreToolUseGoalBudgetGuardCli, runUlwLoopHookCli } from "./codex-hook.js";
|
||||||
|
|
||||||
const TOP_LEVEL_HELP =
|
const TOP_LEVEL_HELP =
|
||||||
"Usage:\n omo ultragoal <subcommand> [args]\n omo hook user-prompt-submit (Codex UserPromptSubmit hook)\n omo help | --help | -h (this message)\n\nRun `omo ultragoal help` for ultragoal subcommands.\n";
|
"Usage:\n omo ulw-loop <subcommand> [args]\n omo hook user-prompt-submit (Codex UserPromptSubmit hook)\n omo help | --help | -h (this message)\n\nRun `omo ulw-loop help` for ulw-loop subcommands.\n";
|
||||||
|
|
||||||
async function main(): Promise<number> {
|
async function main(): Promise<number> {
|
||||||
const argv = process.argv.slice(2);
|
const argv = process.argv.slice(2);
|
||||||
@@ -12,11 +12,11 @@ async function main(): Promise<number> {
|
|||||||
process.stdout.write(TOP_LEVEL_HELP);
|
process.stdout.write(TOP_LEVEL_HELP);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (command === "ultragoal") return ultragoalCommand(argv.slice(1));
|
if (command === "ulw-loop") return ulwLoopCommand(argv.slice(1));
|
||||||
if (command === "hook") {
|
if (command === "hook") {
|
||||||
const sub = argv[1];
|
const sub = argv[1];
|
||||||
if (sub === "user-prompt-submit") {
|
if (sub === "user-prompt-submit") {
|
||||||
await runUltragoalHookCli(process.stdin, process.stdout);
|
await runUlwLoopHookCli(process.stdin, process.stdout);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if (sub === "pre-tool-use") {
|
if (sub === "pre-tool-use") {
|
||||||
+22
-22
@@ -1,40 +1,40 @@
|
|||||||
import { codexGoalMode, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
import { codexGoalMode, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
||||||
import type { UltragoalCodexGoalMode, UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "./types.js";
|
import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
|
||||||
|
|
||||||
export interface CodexCreateGoalPayload {
|
export interface CodexCreateGoalPayload {
|
||||||
readonly objective: string;
|
readonly objective: string;
|
||||||
readonly status: "active";
|
readonly status: "active";
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalGoalInstruction {
|
export interface UlwLoopGoalInstruction {
|
||||||
readonly text: string;
|
readonly text: string;
|
||||||
readonly json: CodexCreateGoalPayload;
|
readonly json: CodexCreateGoalPayload;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildCodexGoalInstruction(args: {
|
export function buildCodexGoalInstruction(args: {
|
||||||
readonly plan: UltragoalPlan;
|
readonly plan: UlwLoopPlan;
|
||||||
readonly goal: UltragoalItem;
|
readonly goal: UlwLoopItem;
|
||||||
readonly isFinal?: boolean;
|
readonly isFinal?: boolean;
|
||||||
}): UltragoalGoalInstruction {
|
}): UlwLoopGoalInstruction {
|
||||||
const mode = codexGoalMode(args.plan);
|
const mode = codexGoalMode(args.plan);
|
||||||
const createGoal = buildCreateGoalPayload(args.plan, args.goal);
|
const createGoal = buildCreateGoalPayload(args.plan, args.goal);
|
||||||
const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
|
const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
|
||||||
return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal };
|
return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCreateGoalPayload(plan: UltragoalPlan, goal: UltragoalItem): CodexCreateGoalPayload {
|
function buildCreateGoalPayload(plan: UlwLoopPlan, goal: UlwLoopItem): CodexCreateGoalPayload {
|
||||||
return { objective: expectedCodexObjective(plan, goal), status: "active" };
|
return { objective: expectedCodexObjective(plan, goal), status: "active" };
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildText(
|
function buildText(
|
||||||
mode: UltragoalCodexGoalMode,
|
mode: UlwLoopCodexGoalMode,
|
||||||
plan: UltragoalPlan,
|
plan: UlwLoopPlan,
|
||||||
goal: UltragoalItem,
|
goal: UlwLoopItem,
|
||||||
createGoal: CodexCreateGoalPayload,
|
createGoal: CodexCreateGoalPayload,
|
||||||
isFinal: boolean,
|
isFinal: boolean,
|
||||||
): string {
|
): string {
|
||||||
return joinLines([
|
return joinLines([
|
||||||
mode === "aggregate" ? "Ultragoal aggregate-goal handoff" : "Ultragoal active-goal handoff",
|
mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
|
||||||
`Mode: ${mode}`,
|
`Mode: ${mode}`,
|
||||||
`Plan: ${plan.goalsPath}`,
|
`Plan: ${plan.goalsPath}`,
|
||||||
`Ledger: ${plan.ledgerPath}`,
|
`Ledger: ${plan.ledgerPath}`,
|
||||||
@@ -56,26 +56,26 @@ function buildText(
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function modeConstraintLines(mode: UltragoalCodexGoalMode, isFinal: boolean): readonly string[] {
|
function modeConstraintLines(mode: UlwLoopCodexGoalMode, isFinal: boolean): readonly string[] {
|
||||||
if (mode === "per_story") {
|
if (mode === "per_story") {
|
||||||
return [
|
return [
|
||||||
"- First call get_goal. If no active goal exists, call create_goal with the payload below.",
|
"- First call get_goal. If no active goal exists, call create_goal with the payload below.",
|
||||||
"- If a different active Codex goal exists, finish/checkpoint that goal before starting this ultragoal.",
|
"- If a different active Codex goal exists, finish/checkpoint that goal before starting this ulw-loop.",
|
||||||
"- Work only this goal until its completion audit passes.",
|
"- Work only this goal until its completion audit passes.",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
return [
|
return [
|
||||||
"- Codex goal = the whole omo ultragoal run; OMO G001/G002/etc. = ledger stories.",
|
"- Codex goal = the whole omo ulw-loop run; OMO G001/G002/etc. = ledger stories.",
|
||||||
"- First call get_goal. If no active goal exists, call create_goal with the aggregate payload below.",
|
"- First call get_goal. If no active goal exists, call create_goal with the aggregate payload below.",
|
||||||
"- If get_goal reports the same aggregate objective as active, continue this OMO story without creating a new Codex goal.",
|
"- If get_goal reports the same aggregate objective as active, continue this OMO story without creating a new Codex goal.",
|
||||||
"- If a different active or incomplete Codex goal exists, finish/checkpoint that goal before starting this ultragoal.",
|
"- If a different active or incomplete Codex goal exists, finish/checkpoint that goal before starting this ulw-loop.",
|
||||||
isFinal
|
isFinal
|
||||||
? "- This is the final story; update_goal is allowed only after the mandatory quality gate passes."
|
? "- This is the final story; update_goal is allowed only after the mandatory quality gate passes."
|
||||||
: "- This is not the final story: do not call update_goal yet; the aggregate Codex goal must remain active while later OMO stories remain.",
|
: "- This is not the final story: do not call update_goal yet; the aggregate Codex goal must remain active while later OMO stories remain.",
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkpointLines(mode: UltragoalCodexGoalMode): readonly string[] {
|
function checkpointLines(mode: UlwLoopCodexGoalMode): readonly string[] {
|
||||||
const failureLine =
|
const failureLine =
|
||||||
"- If blocked or failed, checkpoint with --status failed and the failure evidence; rerun complete-goals --retry-failed to resume.";
|
"- If blocked or failed, checkpoint with --status failed and the failure evidence; rerun complete-goals --retry-failed to resume.";
|
||||||
if (mode === "per_story") return [failureLine];
|
if (mode === "per_story") return [failureLine];
|
||||||
@@ -85,25 +85,25 @@ function checkpointLines(mode: UltragoalCodexGoalMode): readonly string[] {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function activeGoalLines(goal: UltragoalItem): readonly string[] {
|
function activeGoalLines(goal: UlwLoopItem): readonly string[] {
|
||||||
return ["Active goal:", `- id: ${goal.id}`, `- title: ${goal.title}`, `- objective: ${goal.objective}`];
|
return ["Active goal:", `- id: ${goal.id}`, `- title: ${goal.title}`, `- objective: ${goal.objective}`];
|
||||||
}
|
}
|
||||||
|
|
||||||
function successCriteriaLines(criteria: readonly UltragoalSuccessCriterion[]): readonly string[] {
|
function successCriteriaLines(criteria: readonly UlwLoopSuccessCriterion[]): readonly string[] {
|
||||||
if (criteria.length === 0) return ["Success criteria:", "- No success criteria recorded for this goal."];
|
if (criteria.length === 0) return ["Success criteria:", "- No success criteria recorded for this goal."];
|
||||||
return ["Success criteria:", ...criteria.map(formatCriterionLine)];
|
return ["Success criteria:", ...criteria.map(formatCriterionLine)];
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatCriterionLine(criterion: UltragoalSuccessCriterion): string {
|
function formatCriterionLine(criterion: UlwLoopSuccessCriterion): string {
|
||||||
const remainingWork = criterion.status === "pending" ? " remaining work:" : "";
|
const remainingWork = criterion.status === "pending" ? " remaining work:" : "";
|
||||||
return `-${remainingWork} [${criterion.id}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
|
return `-${remainingWork} [${criterion.id}] (${criterion.userModel}) ${criterion.scenario} — expect: ${criterion.expectedEvidence} — status: ${criterion.status}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalSection(goal: UltragoalItem, isFinal: boolean, aggregate: boolean): string {
|
function finalSection(goal: UlwLoopItem, isFinal: boolean, aggregate: boolean): string {
|
||||||
if (!isFinal)
|
if (!isFinal)
|
||||||
return "- This is not the final ultragoal story; do not run the final ai-slop-cleaner/$code-review gate yet.";
|
return "- This is not the final ulw-loop story; do not run the final ai-slop-cleaner/$code-review gate yet.";
|
||||||
const blockerCommand = `omo ultragoal record-review-blockers --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
const blockerCommand = `omo ulw-loop record-review-blockers --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
|
||||||
const checkpointCommand = `omo ultragoal checkpoint --goal-id ${goal.id} --status complete --evidence "<tests/files/PR evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
const checkpointCommand = `omo ulw-loop checkpoint --goal-id ${goal.id} --status complete --evidence "<tests/files/PR evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
|
||||||
return joinLines([
|
return joinLines([
|
||||||
"Final story — run mandatory quality gate before update_goal:",
|
"Final story — run mandatory quality gate before update_goal:",
|
||||||
"- Run ai-slop-cleaner on changed files even when it is a no-op, rerun verification, then run $code-review.",
|
"- Run ai-slop-cleaner on changed files even when it is a no-op, rerun verification, then run $code-review.",
|
||||||
+7
-7
@@ -1,4 +1,4 @@
|
|||||||
import { parseUltragoalSteeringDirective, steerUltragoal } from "./steering.js";
|
import { parseUlwLoopSteeringDirective, steerUlwLoop } from "./steering.js";
|
||||||
|
|
||||||
export interface UserPromptSubmitPayload {
|
export interface UserPromptSubmitPayload {
|
||||||
readonly cwd: string;
|
readonly cwd: string;
|
||||||
@@ -35,7 +35,7 @@ interface PreToolUseHookOutput {
|
|||||||
|
|
||||||
const CREATE_GOAL_TOOL_NAME = "create_goal";
|
const CREATE_GOAL_TOOL_NAME = "create_goal";
|
||||||
const GOAL_BUDGET_WARNING =
|
const GOAL_BUDGET_WARNING =
|
||||||
"Do not set token_budget on create_goal. Omit the budget field so the goal stays unlimited; ultrawork and ultragoal runs must always use unlimited goals.";
|
"Do not set token_budget on create_goal. Omit the budget field so the goal stays unlimited; ultrawork and ulw-loop runs must always use unlimited goals.";
|
||||||
|
|
||||||
export function parseUserPromptSubmitPayload(raw: string): UserPromptSubmitPayload | null {
|
export function parseUserPromptSubmitPayload(raw: string): UserPromptSubmitPayload | null {
|
||||||
if (raw.trim().length === 0) return null;
|
if (raw.trim().length === 0) return null;
|
||||||
@@ -59,12 +59,12 @@ export function parsePreToolUsePayload(raw: string): PreToolUsePayload | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function applyUserPromptUltragoalSteering(payload: UserPromptSubmitPayload): Promise<string> {
|
export async function applyUserPromptUlwLoopSteering(payload: UserPromptSubmitPayload): Promise<string> {
|
||||||
try {
|
try {
|
||||||
if (payload.hook_event_name !== "UserPromptSubmit") return "";
|
if (payload.hook_event_name !== "UserPromptSubmit") return "";
|
||||||
const proposal = parseUltragoalSteeringDirective(payload.prompt);
|
const proposal = parseUlwLoopSteeringDirective(payload.prompt);
|
||||||
if (proposal === null) return "";
|
if (proposal === null) return "";
|
||||||
const result = await steerUltragoal(payload.cwd, proposal);
|
const result = await steerUlwLoop(payload.cwd, proposal);
|
||||||
if (!result.accepted) return "";
|
if (!result.accepted) return "";
|
||||||
return JSON.stringify({
|
return JSON.stringify({
|
||||||
status: "accepted",
|
status: "accepted",
|
||||||
@@ -93,11 +93,11 @@ export function applyPreToolUseGoalBudgetGuard(payload: PreToolUsePayload): stri
|
|||||||
return `${JSON.stringify(output)}\n`;
|
return `${JSON.stringify(output)}\n`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runUltragoalHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
|
export async function runUlwLoopHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const payload = parseUserPromptSubmitPayload(await readAll(stdin));
|
const payload = parseUserPromptSubmitPayload(await readAll(stdin));
|
||||||
if (payload === null) return;
|
if (payload === null) return;
|
||||||
const output = await applyUserPromptUltragoalSteering(payload);
|
const output = await applyUserPromptUlwLoopSteering(payload);
|
||||||
if (output.length > 0) stdout.write(output);
|
if (output.length > 0) stdout.write(output);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) return;
|
if (error instanceof Error) return;
|
||||||
+23
-23
@@ -1,15 +1,15 @@
|
|||||||
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
// biome-ignore-all format: keep this module under the mandated pure LOC budget.
|
||||||
import { hasAllCriteriaPass } from "./goal-status.js";
|
import { hasAllCriteriaPass } from "./goal-status.js";
|
||||||
import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js";
|
import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "./types.js";
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
|
||||||
import { iso, UltragoalError } from "./types.js";
|
import { iso, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
type EvidenceStatus = "pass" | "fail" | "blocked";
|
type EvidenceStatus = "pass" | "fail" | "blocked";
|
||||||
type RecordEvidenceArgs = { readonly goalId: string; readonly criterionId: string; readonly status: EvidenceStatus; readonly evidence: string; readonly notes?: string };
|
type RecordEvidenceArgs = { readonly goalId: string; readonly criterionId: string; readonly status: EvidenceStatus; readonly evidence: string; readonly notes?: string };
|
||||||
|
|
||||||
function ultragoalFail(message: string, code: string, details: Record<string, unknown>): never { throw new UltragoalError(message, code, { details }); }
|
function ulwLoopFail(message: string, code: string, details: Record<string, unknown>): never { throw new UlwLoopError(message, code, { details }); }
|
||||||
|
|
||||||
function ledgerKind(status: EvidenceStatus): UltragoalLedgerEntry["kind"] {
|
function ledgerKind(status: EvidenceStatus): UlwLoopLedgerEntry["kind"] {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case "pass":
|
case "pass":
|
||||||
return "evidence_captured";
|
return "evidence_captured";
|
||||||
@@ -18,25 +18,25 @@ function ledgerKind(status: EvidenceStatus): UltragoalLedgerEntry["kind"] {
|
|||||||
case "blocked":
|
case "blocked":
|
||||||
return "criterion_blocked";
|
return "criterion_blocked";
|
||||||
default:
|
default:
|
||||||
return ultragoalFail("Invalid criterion status.", "ULTRAGOAL_CRITERION_STATUS_INVALID", { status });
|
return ulwLoopFail("Invalid criterion status.", "ULW_LOOP_CRITERION_STATUS_INVALID", { status });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function findGoal(plan: UltragoalPlan, goalId: string): UltragoalItem {
|
function findGoal(plan: UlwLoopPlan, goalId: string): UlwLoopItem {
|
||||||
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
const goal = plan.goals.find((candidate) => candidate.id === goalId);
|
||||||
return goal ?? ultragoalFail(`Ultragoal goal not found: ${goalId}.`, "ULTRAGOAL_GOAL_NOT_FOUND", { goalId });
|
return goal ?? ulwLoopFail(`UlwLoop goal not found: ${goalId}.`, "ULW_LOOP_GOAL_NOT_FOUND", { goalId });
|
||||||
}
|
}
|
||||||
|
|
||||||
function findCriterion(goal: UltragoalItem, criterionId: string): UltragoalSuccessCriterion {
|
function findCriterion(goal: UlwLoopItem, criterionId: string): UlwLoopSuccessCriterion {
|
||||||
const criterion = goal.successCriteria.find((candidate) => candidate.id === criterionId);
|
const criterion = goal.successCriteria.find((candidate) => candidate.id === criterionId);
|
||||||
return criterion ?? ultragoalFail(`Success criterion not found: ${criterionId}.`, "ULTRAGOAL_CRITERION_NOT_FOUND", { goalId: goal.id, criterionId });
|
return criterion ?? ulwLoopFail(`Success criterion not found: ${criterionId}.`, "ULW_LOOP_CRITERION_NOT_FOUND", { goalId: goal.id, criterionId });
|
||||||
}
|
}
|
||||||
|
|
||||||
function nonEmptyEvidence(evidence: string): string { const trimmed = evidence.trim(); return trimmed || ultragoalFail("Evidence must be a non-empty string.", "ULTRAGOAL_EVIDENCE_REQUIRED", {}); }
|
function nonEmptyEvidence(evidence: string): string { const trimmed = evidence.trim(); return trimmed || ulwLoopFail("Evidence must be a non-empty string.", "ULW_LOOP_EVIDENCE_REQUIRED", {}); }
|
||||||
|
|
||||||
export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs): Promise<{ plan: UltragoalPlan; goal: UltragoalItem; criterion: UltragoalSuccessCriterion; ledgerEntry: UltragoalLedgerEntry }> {
|
export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem; criterion: UlwLoopSuccessCriterion; ledgerEntry: UlwLoopLedgerEntry }> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const goal = findGoal(plan, args.goalId);
|
const goal = findGoal(plan, args.goalId);
|
||||||
const criterion = findCriterion(goal, args.criterionId);
|
const criterion = findCriterion(goal, args.criterionId);
|
||||||
const evidence = nonEmptyEvidence(args.evidence);
|
const evidence = nonEmptyEvidence(args.evidence);
|
||||||
@@ -50,7 +50,7 @@ export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs)
|
|||||||
goal.updatedAt = capturedAt;
|
goal.updatedAt = capturedAt;
|
||||||
plan.updatedAt = capturedAt;
|
plan.updatedAt = capturedAt;
|
||||||
await writePlan(repoRoot, plan);
|
await writePlan(repoRoot, plan);
|
||||||
const ledgerEntry: UltragoalLedgerEntry = {
|
const ledgerEntry: UlwLoopLedgerEntry = {
|
||||||
at: capturedAt,
|
at: capturedAt,
|
||||||
kind,
|
kind,
|
||||||
goalId: goal.id,
|
goalId: goal.id,
|
||||||
@@ -66,9 +66,9 @@ export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs)
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function markCriteriaPendingResetForGoal(repoRoot: string, goalId: string): Promise<{ plan: UltragoalPlan; resetCount: number }> {
|
export async function markCriteriaPendingResetForGoal(repoRoot: string, goalId: string): Promise<{ plan: UlwLoopPlan; resetCount: number }> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const goal = findGoal(plan, goalId);
|
const goal = findGoal(plan, goalId);
|
||||||
const now = iso();
|
const now = iso();
|
||||||
const before = goal.successCriteria.map((criterion) => ({ id: criterion.id, status: criterion.status, capturedEvidence: criterion.capturedEvidence, capturedAt: criterion.capturedAt ?? null }));
|
const before = goal.successCriteria.map((criterion) => ({ id: criterion.id, status: criterion.status, capturedEvidence: criterion.capturedEvidence, capturedAt: criterion.capturedAt ?? null }));
|
||||||
@@ -86,7 +86,7 @@ export async function markCriteriaPendingResetForGoal(repoRoot: string, goalId:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function criteriaSummary(plan: UltragoalPlan): { totalCriteria: number; passCount: number; pendingCount: number; failCount: number; blockedCount: number; goalsWithUnresolvedCriteria: string[] } {
|
export function criteriaSummary(plan: UlwLoopPlan): { totalCriteria: number; passCount: number; pendingCount: number; failCount: number; blockedCount: number; goalsWithUnresolvedCriteria: string[] } {
|
||||||
let totalCriteria = 0;
|
let totalCriteria = 0;
|
||||||
let passCount = 0;
|
let passCount = 0;
|
||||||
let pendingCount = 0;
|
let pendingCount = 0;
|
||||||
@@ -103,7 +103,7 @@ export function criteriaSummary(plan: UltragoalPlan): { totalCriteria: number; p
|
|||||||
case "pending": pendingCount += 1; break;
|
case "pending": pendingCount += 1; break;
|
||||||
case "fail": failCount += 1; break;
|
case "fail": failCount += 1; break;
|
||||||
case "blocked": blockedCount += 1; break;
|
case "blocked": blockedCount += 1; break;
|
||||||
default: ultragoalFail("Invalid criterion status.", "ULTRAGOAL_CRITERION_STATUS_INVALID", { status: criterion.status });
|
default: ulwLoopFail("Invalid criterion status.", "ULW_LOOP_CRITERION_STATUS_INVALID", { status: criterion.status });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (unresolved) goalsWithUnresolvedCriteria.push(goal.id);
|
if (unresolved) goalsWithUnresolvedCriteria.push(goal.id);
|
||||||
@@ -111,11 +111,11 @@ export function criteriaSummary(plan: UltragoalPlan): { totalCriteria: number; p
|
|||||||
return { totalCriteria, passCount, pendingCount, failCount, blockedCount, goalsWithUnresolvedCriteria };
|
return { totalCriteria, passCount, pendingCount, failCount, blockedCount, goalsWithUnresolvedCriteria };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function unresolvedCriteriaOf(goal: UltragoalItem): UltragoalSuccessCriterion[] { return goal.successCriteria.filter((criterion) => criterion.status !== "pass"); }
|
export function unresolvedCriteriaOf(goal: UlwLoopItem): UlwLoopSuccessCriterion[] { return goal.successCriteria.filter((criterion) => criterion.status !== "pass"); }
|
||||||
|
|
||||||
export function requireAllCriteriaPass(goal: UltragoalItem): void {
|
export function requireAllCriteriaPass(goal: UlwLoopItem): void {
|
||||||
if (hasAllCriteriaPass(goal)) return;
|
if (hasAllCriteriaPass(goal)) return;
|
||||||
throw new UltragoalError(`Goal ${goal.id} has unresolved success criteria.`, "ultragoal_criteria_not_all_pass", {
|
throw new UlwLoopError(`Goal ${goal.id} has unresolved success criteria.`, "ulw_loop_criteria_not_all_pass", {
|
||||||
details: { goalId: goal.id, unresolved: unresolvedCriteriaOf(goal).map((criterion) => ({ id: criterion.id, status: criterion.status })) },
|
details: { goalId: goal.id, unresolved: unresolvedCriteriaOf(goal).map((criterion) => ({ id: criterion.id, status: criterion.status })) },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
+22
-22
@@ -1,23 +1,23 @@
|
|||||||
import type {
|
import type {
|
||||||
UltragoalCodexGoalMode,
|
UlwLoopCodexGoalMode,
|
||||||
UltragoalItem,
|
UlwLoopItem,
|
||||||
UltragoalPlan,
|
UlwLoopPlan,
|
||||||
UltragoalStatus,
|
UlwLoopStatus,
|
||||||
UltragoalSuccessCriterion,
|
UlwLoopSuccessCriterion,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
|
|
||||||
export const ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE: string =
|
export const ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE: string =
|
||||||
"Complete the durable ultragoal plan in .omo/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omo/ultragoal/ledger.jsonl as the audit trail.";
|
"Complete the durable ulw-loop plan in .omo/ulw-loop/goals.json, including later accepted/appended stories, under the original brief constraints; use .omo/ulw-loop/ledger.jsonl as the audit trail.";
|
||||||
|
|
||||||
export function codexGoalMode(plan: UltragoalPlan): UltragoalCodexGoalMode {
|
export function codexGoalMode(plan: UlwLoopPlan): UlwLoopCodexGoalMode {
|
||||||
return plan.codexGoalMode ?? "per_story";
|
return plan.codexGoalMode ?? "per_story";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isResolvedStatus(status: UltragoalStatus): boolean {
|
function isResolvedStatus(status: UlwLoopStatus): boolean {
|
||||||
return status === "complete";
|
return status === "complete";
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSupersededResolved(goal: UltragoalItem, plan: UltragoalPlan): boolean {
|
function isSupersededResolved(goal: UlwLoopItem, plan: UlwLoopPlan): boolean {
|
||||||
if (goal.steeringStatus !== "superseded") return false;
|
if (goal.steeringStatus !== "superseded") return false;
|
||||||
const replacements = goal.supersededBy ?? [];
|
const replacements = goal.supersededBy ?? [];
|
||||||
if (replacements.length === 0) return false;
|
if (replacements.length === 0) return false;
|
||||||
@@ -27,16 +27,16 @@ function isSupersededResolved(goal: UltragoalItem, plan: UltragoalPlan): boolean
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCompletionBlocking(goal: UltragoalItem, plan: UltragoalPlan): boolean {
|
function isCompletionBlocking(goal: UlwLoopItem, plan: UlwLoopPlan): boolean {
|
||||||
if (goal.steeringStatus === "superseded") return !isSupersededResolved(goal, plan);
|
if (goal.steeringStatus === "superseded") return !isSupersededResolved(goal, plan);
|
||||||
if (goal.steeringStatus === "blocked") return true;
|
if (goal.steeringStatus === "blocked") return true;
|
||||||
return !isResolvedStatus(goal.status);
|
return !isResolvedStatus(goal.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCompletionBlockingForFinalCandidate(
|
function isCompletionBlockingForFinalCandidate(
|
||||||
candidate: UltragoalItem,
|
candidate: UlwLoopItem,
|
||||||
finalCandidate: UltragoalItem,
|
finalCandidate: UlwLoopItem,
|
||||||
plan: UltragoalPlan,
|
plan: UlwLoopPlan,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (candidate.id === finalCandidate.id) return false;
|
if (candidate.id === finalCandidate.id) return false;
|
||||||
if (candidate.steeringStatus === "superseded") {
|
if (candidate.steeringStatus === "superseded") {
|
||||||
@@ -51,34 +51,34 @@ function isCompletionBlockingForFinalCandidate(
|
|||||||
return isCompletionBlocking(candidate, plan);
|
return isCompletionBlocking(candidate, plan);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isUltragoalDone(plan: UltragoalPlan): boolean {
|
export function isUlwLoopDone(plan: UlwLoopPlan): boolean {
|
||||||
if (plan.aggregateCompletion?.status === "complete") return true;
|
if (plan.aggregateCompletion?.status === "complete") return true;
|
||||||
return plan.goals.every((goal) => !isCompletionBlocking(goal, plan));
|
return plan.goals.every((goal) => !isCompletionBlocking(goal, plan));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isFinalRunCompletionCandidate(plan: UltragoalPlan, goal: UltragoalItem): boolean {
|
export function isFinalRunCompletionCandidate(plan: UlwLoopPlan, goal: UlwLoopItem): boolean {
|
||||||
return (
|
return (
|
||||||
isCompletionBlocking(goal, plan) &&
|
isCompletionBlocking(goal, plan) &&
|
||||||
plan.goals.every((candidate) => !isCompletionBlockingForFinalCandidate(candidate, goal, plan))
|
plan.goals.every((candidate) => !isCompletionBlockingForFinalCandidate(candidate, goal, plan))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function aggregateCodexObjective(plan: UltragoalPlan): string {
|
export function aggregateCodexObjective(plan: UlwLoopPlan): string {
|
||||||
return plan.codexObjective ?? ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE;
|
return plan.codexObjective ?? ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function expectedCodexObjective(plan: UltragoalPlan, goal: UltragoalItem): string {
|
export function expectedCodexObjective(plan: UlwLoopPlan, goal: UlwLoopItem): string {
|
||||||
return codexGoalMode(plan) === "aggregate" ? aggregateCodexObjective(plan) : goal.objective;
|
return codexGoalMode(plan) === "aggregate" ? aggregateCodexObjective(plan) : goal.objective;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compatibleCodexObjectives(plan: UltragoalPlan): readonly string[] {
|
export function compatibleCodexObjectives(plan: UlwLoopPlan): readonly string[] {
|
||||||
return [aggregateCodexObjective(plan), ...(plan.codexObjectiveAliases ?? [])];
|
return [aggregateCodexObjective(plan), ...(plan.codexObjectiveAliases ?? [])];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hasAllCriteriaPass(goal: UltragoalItem): boolean {
|
export function hasAllCriteriaPass(goal: UlwLoopItem): boolean {
|
||||||
return goal.successCriteria.length > 0 && goal.successCriteria.every((criterion) => criterion.status === "pass");
|
return goal.successCriteria.length > 0 && goal.successCriteria.every((criterion) => criterion.status === "pass");
|
||||||
}
|
}
|
||||||
|
|
||||||
export function firstUnresolvedCriterion(goal: UltragoalItem): UltragoalSuccessCriterion | undefined {
|
export function firstUnresolvedCriterion(goal: UlwLoopItem): UlwLoopSuccessCriterion | undefined {
|
||||||
return goal.successCriteria.find((criterion) => criterion.status !== "pass");
|
return goal.successCriteria.find((criterion) => criterion.status !== "pass");
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { join } from "node:path";
|
||||||
|
import { ULW_LOOP_BRIEF, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER } from "./types.js";
|
||||||
|
|
||||||
|
export function ulwLoopDir(repoRoot: string): string {
|
||||||
|
return join(repoRoot, ULW_LOOP_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ulwLoopBriefPath(repoRoot: string): string {
|
||||||
|
return join(ulwLoopDir(repoRoot), ULW_LOOP_BRIEF);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ulwLoopGoalsPath(repoRoot: string): string {
|
||||||
|
return join(ulwLoopDir(repoRoot), ULW_LOOP_GOALS);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ulwLoopLedgerPath(repoRoot: string): string {
|
||||||
|
return join(ulwLoopDir(repoRoot), ULW_LOOP_LEDGER);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function repoRelative(absolutePath: string, repoRoot: string): string {
|
||||||
|
const slashPrefix = `${repoRoot}/`;
|
||||||
|
const backslashPrefix = `${repoRoot}\\`;
|
||||||
|
if (absolutePath.startsWith(slashPrefix)) return absolutePath.slice(slashPrefix.length).split("\\").join("/");
|
||||||
|
if (absolutePath.startsWith(backslashPrefix))
|
||||||
|
return absolutePath.slice(backslashPrefix.length).split("\\").join("/");
|
||||||
|
return absolutePath.split("\\").join("/");
|
||||||
|
}
|
||||||
+30
-30
@@ -2,22 +2,22 @@
|
|||||||
import { existsSync } from "node:fs";
|
import { existsSync } from "node:fs";
|
||||||
import { mkdir, writeFile } from "node:fs/promises";
|
import { mkdir, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "./goal-status.js";
|
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "./goal-status.js";
|
||||||
import { ultragoalBriefPath, ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "./paths.js";
|
import { ulwLoopBriefPath, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "./paths.js";
|
||||||
import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js";
|
import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
||||||
import type { UltragoalCodexGoalMode, UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "./types.js";
|
import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
|
||||||
import { iso, ULTRAGOAL_BRIEF, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js";
|
import { iso, ULW_LOOP_BRIEF, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
export type UltragoalPlanSummary = { readonly total: number; readonly pending: number; readonly in_progress: number; readonly complete: number; readonly failed: number; readonly blocked: number; readonly review_blocked: number; readonly needs_user_decision: number; readonly superseded: number; readonly criteria: { readonly total: number; readonly pass: number; readonly pending: number; readonly fail: number; readonly blocked: number } };
|
export type UlwLoopPlanSummary = { readonly total: number; readonly pending: number; readonly in_progress: number; readonly complete: number; readonly failed: number; readonly blocked: number; readonly review_blocked: number; readonly needs_user_decision: number; readonly superseded: number; readonly criteria: { readonly total: number; readonly pass: number; readonly pending: number; readonly fail: number; readonly blocked: number } };
|
||||||
|
|
||||||
function cleanLine(line: string): string { return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim(); }
|
function cleanLine(line: string): string { return line.replace(/^\s*(?:[-*+]\s+|\d+[.)]\s+)/, "").trim(); }
|
||||||
function normalizeObjective(value: string): string { return value.replace(/\s+/g, " ").trim(); }
|
function normalizeObjective(value: string): string { return value.replace(/\s+/g, " ").trim(); }
|
||||||
function titleFromObjective(objective: string, fallback: string): string { const firstLine = objective.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? fallback; return firstLine.length > 72 ? `${firstLine.slice(0, 69).trimEnd()}...` : firstLine; }
|
function titleFromObjective(objective: string, fallback: string): string { const firstLine = objective.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? fallback; return firstLine.length > 72 ? `${firstLine.slice(0, 69).trimEnd()}...` : firstLine; }
|
||||||
function normalizeGoalId(title: string, index: number): string { const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 36).replace(/-+$/g, ""); return `G${String(index + 1).padStart(3, "0")}${slug ? `-${slug}` : ""}`; }
|
function normalizeGoalId(title: string, index: number): string { const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 36).replace(/-+$/g, ""); return `G${String(index + 1).padStart(3, "0")}${slug ? `-${slug}` : ""}`; }
|
||||||
function assertNonEmpty(value: string | undefined, label: string): string { const trimmed = value?.trim(); if (!trimmed) throw new UltragoalError(`Missing ${label}.`, "ULTRAGOAL_ARGUMENT_MISSING"); return trimmed; }
|
function assertNonEmpty(value: string | undefined, label: string): string { const trimmed = value?.trim(); if (!trimmed) throw new UlwLoopError(`Missing ${label}.`, "ULW_LOOP_ARGUMENT_MISSING"); return trimmed; }
|
||||||
function truncateObjective(objective: string): string { return objective.length > 80 ? `${objective.slice(0, 77).trimEnd()}...` : objective; }
|
function truncateObjective(objective: string): string { return objective.length > 80 ? `${objective.slice(0, 77).trimEnd()}...` : objective; }
|
||||||
|
|
||||||
export function seedDefaultSuccessCriteria(goalIndex: number, objective: string): UltragoalSuccessCriterion[] {
|
export function seedDefaultSuccessCriteria(goalIndex: number, objective: string): UlwLoopSuccessCriterion[] {
|
||||||
const subject = truncateObjective(normalizeObjective(objective) || `Goal ${goalIndex + 1}`);
|
const subject = truncateObjective(normalizeObjective(objective) || `Goal ${goalIndex + 1}`);
|
||||||
const rows = [
|
const rows = [
|
||||||
["C001", "happy", `happy path for: ${subject}`, `Replace via revise_criterion with observable happy-path proof for goal ${goalIndex + 1}.`],
|
["C001", "happy", `happy path for: ${subject}`, `Replace via revise_criterion with observable happy-path proof for goal ${goalIndex + 1}.`],
|
||||||
@@ -34,44 +34,44 @@ export function deriveGoalCandidates(brief: string): Array<{ title: string; obje
|
|||||||
return selected.map((objective, index) => ({ title: titleFromObjective(objective, `Goal ${index + 1}`), objective }));
|
return selected.map((objective, index) => ({ title: titleFromObjective(objective, `Goal ${index + 1}`), objective }));
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(title: string, objective: string, index: number, now: string): UltragoalItem {
|
function makeGoal(title: string, objective: string, index: number, now: string): UlwLoopItem {
|
||||||
const cleanTitle = assertNonEmpty(title, "title");
|
const cleanTitle = assertNonEmpty(title, "title");
|
||||||
const cleanObjective = assertNonEmpty(objective, "objective");
|
const cleanObjective = assertNonEmpty(objective, "objective");
|
||||||
return { id: normalizeGoalId(cleanTitle, index), title: cleanTitle, objective: cleanObjective, status: "pending", successCriteria: seedDefaultSuccessCriteria(index, cleanObjective), attempt: 0, createdAt: now, updatedAt: now };
|
return { id: normalizeGoalId(cleanTitle, index), title: cleanTitle, objective: cleanObjective, status: "pending", successCriteria: seedDefaultSuccessCriteria(index, cleanObjective), attempt: 0, createdAt: now, updatedAt: now };
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendGoalToPlan(plan: UltragoalPlan, title: string, objective: string, now: string): UltragoalItem {
|
function appendGoalToPlan(plan: UlwLoopPlan, title: string, objective: string, now: string): UlwLoopItem {
|
||||||
const goal = makeGoal(title, objective, plan.goals.length, now);
|
const goal = makeGoal(title, objective, plan.goals.length, now);
|
||||||
plan.goals.push(goal);
|
plan.goals.push(goal);
|
||||||
plan.updatedAt = now;
|
plan.updatedAt = now;
|
||||||
return goal;
|
return goal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isScheduleEligible(goal: UltragoalItem): boolean { return goal.steeringStatus !== "superseded" && goal.steeringStatus !== "blocked"; }
|
function isScheduleEligible(goal: UlwLoopItem): boolean { return goal.steeringStatus !== "superseded" && goal.steeringStatus !== "blocked"; }
|
||||||
|
|
||||||
function clearGoalBlockerFields(goal: UltragoalItem): void {
|
function clearGoalBlockerFields(goal: UlwLoopItem): void {
|
||||||
for (const key of ["blockedReason", "blockerSignature", "blockerOccurrenceCount", "requiredExternalDecision", "nonRetriable", "failedAt", "failureReason"] as const) delete goal[key];
|
for (const key of ["blockedReason", "blockerSignature", "blockerOccurrenceCount", "requiredExternalDecision", "nonRetriable", "failedAt", "failureReason"] as const) delete goal[key];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createUltragoalPlan(repoRoot: string, args: { brief: string; codexGoalMode?: UltragoalCodexGoalMode; force?: boolean }): Promise<UltragoalPlan> {
|
export async function createUlwLoopPlan(repoRoot: string, args: { brief: string; codexGoalMode?: UlwLoopCodexGoalMode; force?: boolean }): Promise<UlwLoopPlan> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
if (!args.force && existsSync(ultragoalGoalsPath(repoRoot))) throw new UltragoalError(`Refusing to overwrite existing ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}; pass --force to recreate it.`, "ULTRAGOAL_PLAN_EXISTS");
|
if (!args.force && existsSync(ulwLoopGoalsPath(repoRoot))) throw new UlwLoopError(`Refusing to overwrite existing ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}; pass --force to recreate it.`, "ULW_LOOP_PLAN_EXISTS");
|
||||||
const now = iso();
|
const now = iso();
|
||||||
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
|
const goals = deriveGoalCandidates(args.brief).map((goal, index) => makeGoal(goal.title, goal.objective, index, now));
|
||||||
const plan: UltragoalPlan = { version: 1, createdAt: now, updatedAt: now, briefPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_BRIEF}`, goalsPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}`, ledgerPath: `${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER}`, codexGoalMode: args.codexGoalMode ?? "aggregate", goals };
|
const plan: UlwLoopPlan = { version: 1, createdAt: now, updatedAt: now, briefPath: `${ULW_LOOP_DIR}/${ULW_LOOP_BRIEF}`, goalsPath: `${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}`, ledgerPath: `${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER}`, codexGoalMode: args.codexGoalMode ?? "aggregate", goals };
|
||||||
if (plan.codexGoalMode === "aggregate") plan.codexObjective = ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE;
|
if (plan.codexGoalMode === "aggregate") plan.codexObjective = ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE;
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
await writeFile(ultragoalBriefPath(repoRoot), args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`, "utf8");
|
await writeFile(ulwLoopBriefPath(repoRoot), args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`, "utf8");
|
||||||
await writePlan(repoRoot, plan);
|
await writePlan(repoRoot, plan);
|
||||||
await writeFile(ultragoalLedgerPath(repoRoot), "", "utf8");
|
await writeFile(ulwLoopLedgerPath(repoRoot), "", "utf8");
|
||||||
await appendLedger(repoRoot, { at: now, kind: "plan_created", message: `${goals.length} goal(s) created` });
|
await appendLedger(repoRoot, { at: now, kind: "plan_created", message: `${goals.length} goal(s) created` });
|
||||||
return plan;
|
return plan;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function addUltragoalGoal(repoRoot: string, args: { title: string; objective: string }): Promise<{ plan: UltragoalPlan; goal: UltragoalItem }> {
|
export async function addUlwLoopGoal(repoRoot: string, args: { title: string; objective: string }): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem }> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const now = iso();
|
const now = iso();
|
||||||
const goal = appendGoalToPlan(plan, args.title, args.objective, now);
|
const goal = appendGoalToPlan(plan, args.title, args.objective, now);
|
||||||
await writePlan(repoRoot, plan);
|
await writePlan(repoRoot, plan);
|
||||||
@@ -80,13 +80,13 @@ export async function addUltragoalGoal(repoRoot: string, args: { title: string;
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function startNextUltragoal(repoRoot: string, args: { retryFailed?: boolean } = {}): Promise<{ plan: UltragoalPlan; goal: UltragoalItem; resumed: boolean } | { done: true; plan: UltragoalPlan }> {
|
export async function startNextUlwLoop(repoRoot: string, args: { retryFailed?: boolean } = {}): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem; resumed: boolean } | { done: true; plan: UlwLoopPlan }> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const now = iso();
|
const now = iso();
|
||||||
if (plan.aggregateCompletion?.status === "complete") return { done: true, plan };
|
if (plan.aggregateCompletion?.status === "complete") return { done: true, plan };
|
||||||
const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
|
const existing = plan.goals.find((goal) => goal.status === "in_progress" && isScheduleEligible(goal));
|
||||||
if (existing) { await appendLedger(repoRoot, { at: now, kind: "goal_resumed", goalId: existing.id, status: existing.status, message: "Resuming active ultragoal" }); return { plan, goal: existing, resumed: true }; }
|
if (existing) { await appendLedger(repoRoot, { at: now, kind: "goal_resumed", goalId: existing.id, status: existing.status, message: "Resuming active ulw-loop" }); return { plan, goal: existing, resumed: true }; }
|
||||||
let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
|
let next = plan.goals.find((goal) => goal.status === "pending" && isScheduleEligible(goal));
|
||||||
if (!next && args.retryFailed) {
|
if (!next && args.retryFailed) {
|
||||||
next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
|
next = plan.goals.find((goal) => goal.status === "failed" && !goal.nonRetriable && isScheduleEligible(goal));
|
||||||
@@ -106,8 +106,8 @@ export async function startNextUltragoal(repoRoot: string, args: { retryFailed?:
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function summarizeUltragoalPlan(plan: UltragoalPlan): UltragoalPlanSummary {
|
export function summarizeUlwLoopPlan(plan: UlwLoopPlan): UlwLoopPlanSummary {
|
||||||
const countStatus = (status: UltragoalItem["status"]): number => plan.goals.filter((goal) => goal.status === status).length;
|
const countStatus = (status: UlwLoopItem["status"]): number => plan.goals.filter((goal) => goal.status === status).length;
|
||||||
const countCriteria = (status: UltragoalSuccessCriterion["status"]): number => plan.goals.reduce((sum, goal) => sum + goal.successCriteria.filter((criterion) => criterion.status === status).length, 0);
|
const countCriteria = (status: UlwLoopSuccessCriterion["status"]): number => plan.goals.reduce((sum, goal) => sum + goal.successCriteria.filter((criterion) => criterion.status === status).length, 0);
|
||||||
return { total: plan.goals.length, pending: countStatus("pending"), in_progress: countStatus("in_progress"), complete: countStatus("complete"), failed: countStatus("failed"), blocked: countStatus("blocked"), review_blocked: countStatus("review_blocked"), needs_user_decision: countStatus("needs_user_decision"), superseded: plan.goals.filter((goal) => goal.steeringStatus === "superseded").length, criteria: { total: plan.goals.reduce((sum, goal) => sum + goal.successCriteria.length, 0), pass: countCriteria("pass"), pending: countCriteria("pending"), fail: countCriteria("fail"), blocked: countCriteria("blocked") } };
|
return { total: plan.goals.length, pending: countStatus("pending"), in_progress: countStatus("in_progress"), complete: countStatus("complete"), failed: countStatus("failed"), blocked: countStatus("blocked"), review_blocked: countStatus("review_blocked"), needs_user_decision: countStatus("needs_user_decision"), superseded: plan.goals.filter((goal) => goal.steeringStatus === "superseded").length, criteria: { total: plan.goals.reduce((sum, goal) => sum + goal.successCriteria.length, 0), pass: countCriteria("pass"), pending: countCriteria("pending"), fail: countCriteria("fail"), blocked: countCriteria("blocked") } };
|
||||||
}
|
}
|
||||||
+25
-25
@@ -1,12 +1,12 @@
|
|||||||
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
||||||
|
|
||||||
import { repoRelative, ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "./paths.js";
|
import { repoRelative, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "./paths.js";
|
||||||
import type { UltragoalLedgerEntry, UltragoalPlan } from "./types.js";
|
import type { UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
|
||||||
import { iso, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js";
|
import { iso, ULW_LOOP_DIR, ULW_LOOP_GOALS, ULW_LOOP_LEDGER, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
const AGGREGATE_CODEX_OBJECTIVE = `Complete the durable ultragoal plan in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}, including later accepted/appended stories, under the original brief constraints; use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the audit trail.`;
|
const AGGREGATE_CODEX_OBJECTIVE = `Complete the durable ulw-loop plan in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}, including later accepted/appended stories, under the original brief constraints; use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the audit trail.`;
|
||||||
const LEGACY_OBJECTIVE_PREFIX = `Complete all ultragoal stories in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}: `;
|
const LEGACY_OBJECTIVE_PREFIX = `Complete all ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `;
|
||||||
const LEGACY_OBJECTIVE = `Complete all ultragoal stories listed in ${ULTRAGOAL_DIR}/${ULTRAGOAL_GOALS}. Use ${ULTRAGOAL_DIR}/${ULTRAGOAL_LEDGER} as the durable audit trail.`;
|
const LEGACY_OBJECTIVE = `Complete all ulw-loop stories listed in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}. Use ${ULW_LOOP_DIR}/${ULW_LOOP_LEDGER} as the durable audit trail.`;
|
||||||
const locks = new Map<string, Promise<unknown>>();
|
const locks = new Map<string, Promise<unknown>>();
|
||||||
|
|
||||||
function hasCode(error: unknown, code: string): boolean {
|
function hasCode(error: unknown, code: string): boolean {
|
||||||
@@ -17,11 +17,11 @@ function isLegacyEnumeratedAggregateObjective(objective: string | undefined): ob
|
|||||||
return objective === LEGACY_OBJECTIVE || Boolean(objective?.startsWith(LEGACY_OBJECTIVE_PREFIX));
|
return objective === LEGACY_OBJECTIVE || Boolean(objective?.startsWith(LEGACY_OBJECTIVE_PREFIX));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSteeringKind(value: unknown): value is UltragoalLedgerEntry["kind"] {
|
function isSteeringKind(value: unknown): value is UlwLoopLedgerEntry["kind"] {
|
||||||
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised";
|
return value === "steering_accepted" || value === "steering_rejected" || value === "criteria_revised";
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function withUltragoalMutationLock<T>(repoRoot: string, fn: () => Promise<T>): Promise<T> {
|
export async function withUlwLoopMutationLock<T>(repoRoot: string, fn: () => Promise<T>): Promise<T> {
|
||||||
const prior = locks.get(repoRoot) ?? Promise.resolve();
|
const prior = locks.get(repoRoot) ?? Promise.resolve();
|
||||||
const run = prior.then(fn, fn);
|
const run = prior.then(fn, fn);
|
||||||
locks.set(
|
locks.set(
|
||||||
@@ -31,22 +31,22 @@ export async function withUltragoalMutationLock<T>(repoRoot: string, fn: () => P
|
|||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readUltragoalPlan(repoRoot: string): Promise<UltragoalPlan> {
|
export async function readUlwLoopPlan(repoRoot: string): Promise<UlwLoopPlan> {
|
||||||
const path = ultragoalGoalsPath(repoRoot);
|
const path = ulwLoopGoalsPath(repoRoot);
|
||||||
let raw: string;
|
let raw: string;
|
||||||
try {
|
try {
|
||||||
raw = await readFile(path, "utf8");
|
raw = await readFile(path, "utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (!hasCode(error, "ENOENT")) throw error;
|
if (!hasCode(error, "ENOENT")) throw error;
|
||||||
throw new UltragoalError(
|
throw new UlwLoopError(
|
||||||
`No ultragoal plan found at ${repoRelative(path, repoRoot)}. Run \`omo ultragoal create-goals ...\` first.`,
|
`No ulw-loop plan found at ${repoRelative(path, repoRoot)}. Run \`omo ulw-loop create-goals ...\` first.`,
|
||||||
"ULTRAGOAL_PLAN_MISSING",
|
"ULW_LOOP_PLAN_MISSING",
|
||||||
{ cause: error },
|
{ cause: error },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const parsed: UltragoalPlan = JSON.parse(raw);
|
const parsed: UlwLoopPlan = JSON.parse(raw);
|
||||||
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
if (parsed.version !== 1 || !Array.isArray(parsed.goals)) {
|
||||||
throw new UltragoalError(`Invalid ultragoal plan at ${repoRelative(path, repoRoot)}.`, "ULTRAGOAL_PLAN_INVALID");
|
throw new UlwLoopError(`Invalid ulw-loop plan at ${repoRelative(path, repoRoot)}.`, "ULW_LOOP_PLAN_INVALID");
|
||||||
}
|
}
|
||||||
const previousObjective = parsed.codexObjective;
|
const previousObjective = parsed.codexObjective;
|
||||||
if (
|
if (
|
||||||
@@ -69,30 +69,30 @@ export async function readUltragoalPlan(repoRoot: string): Promise<UltragoalPlan
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function writePlan(repoRoot: string, plan: UltragoalPlan): Promise<void> {
|
export async function writePlan(repoRoot: string, plan: UlwLoopPlan): Promise<void> {
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
const path = ultragoalGoalsPath(repoRoot);
|
const path = ulwLoopGoalsPath(repoRoot);
|
||||||
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
|
||||||
await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
||||||
await rename(tmpPath, path);
|
await rename(tmpPath, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function appendLedger(repoRoot: string, entry: UltragoalLedgerEntry): Promise<void> {
|
export async function appendLedger(repoRoot: string, entry: UlwLoopLedgerEntry): Promise<void> {
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
await appendFile(ultragoalLedgerPath(repoRoot), `${JSON.stringify(entry)}\n`, "utf8");
|
await appendFile(ulwLoopLedgerPath(repoRoot), `${JSON.stringify(entry)}\n`, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function readSteeringLedgerEntries(repoRoot: string): Promise<UltragoalLedgerEntry[]> {
|
export async function readSteeringLedgerEntries(repoRoot: string): Promise<UlwLoopLedgerEntry[]> {
|
||||||
let raw: string;
|
let raw: string;
|
||||||
try {
|
try {
|
||||||
raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8");
|
raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (hasCode(error, "ENOENT")) return [];
|
if (hasCode(error, "ENOENT")) return [];
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const entries: UltragoalLedgerEntry[] = [];
|
const entries: UlwLoopLedgerEntry[] = [];
|
||||||
for (const line of raw.split(/\r?\n/).filter(Boolean)) {
|
for (const line of raw.split(/\r?\n/).filter(Boolean)) {
|
||||||
const entry: UltragoalLedgerEntry = JSON.parse(line);
|
const entry: UlwLoopLedgerEntry = JSON.parse(line);
|
||||||
if (isSteeringKind(entry.kind)) entries.push(entry);
|
if (isSteeringKind(entry.kind)) entries.push(entry);
|
||||||
}
|
}
|
||||||
return entries;
|
return entries;
|
||||||
+8
-8
@@ -1,5 +1,5 @@
|
|||||||
import type { UltragoalItem, UltragoalPlan, UltragoalQualityGate } from "./types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopQualityGate } from "./types.js";
|
||||||
import { UltragoalError } from "./types.js";
|
import { UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
const BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
const BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
|
||||||
const URL_PATTERN = /https?:\/\/\S+/g;
|
const URL_PATTERN = /https?:\/\/\S+/g;
|
||||||
@@ -14,7 +14,7 @@ const GHCR_401_PATTERN = /\b(401|unauthorized|anonymous pull|authentication requ
|
|||||||
const GHCR_403_PATTERN = /\b(403|forbidden|read packages|package api)\b/;
|
const GHCR_403_PATTERN = /\b(403|forbidden|read packages|package api)\b/;
|
||||||
|
|
||||||
function invalid(message: string, field: string): never {
|
function invalid(message: string, field: string): never {
|
||||||
throw new UltragoalError(message, "ULTRAGOAL_QUALITY_GATE_INVALID", { details: { field } });
|
throw new UlwLoopError(message, "ULW_LOOP_QUALITY_GATE_INVALID", { details: { field } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||||
@@ -42,7 +42,7 @@ function stringArray(value: unknown, field: string): string[] {
|
|||||||
return value.map((item) => nonEmptyString(item, field));
|
return value.map((item) => nonEmptyString(item, field));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateQualityGate(input: unknown): UltragoalQualityGate {
|
export function validateQualityGate(input: unknown): UlwLoopQualityGate {
|
||||||
const gate = section(input, "qualityGate");
|
const gate = section(input, "qualityGate");
|
||||||
const cleaner = section(gate["aiSlopCleaner"], "aiSlopCleaner");
|
const cleaner = section(gate["aiSlopCleaner"], "aiSlopCleaner");
|
||||||
const verification = section(gate["verification"], "verification");
|
const verification = section(gate["verification"], "verification");
|
||||||
@@ -61,7 +61,7 @@ export function validateQualityGate(input: unknown): UltragoalQualityGate {
|
|||||||
const cleanerEvidence = nonEmptyString(cleaner["evidence"], "aiSlopCleaner.evidence");
|
const cleanerEvidence = nonEmptyString(cleaner["evidence"], "aiSlopCleaner.evidence");
|
||||||
const verificationEvidence = nonEmptyString(verification["evidence"], "verification.evidence");
|
const verificationEvidence = nonEmptyString(verification["evidence"], "verification.evidence");
|
||||||
const reviewEvidence = nonEmptyString(review["evidence"], "codeReview.evidence");
|
const reviewEvidence = nonEmptyString(review["evidence"], "codeReview.evidence");
|
||||||
const result: UltragoalQualityGate = {
|
const result: UlwLoopQualityGate = {
|
||||||
aiSlopCleaner: { status: "passed", evidence: cleanerEvidence },
|
aiSlopCleaner: { status: "passed", evidence: cleanerEvidence },
|
||||||
verification: { status: "passed", commands, evidence: verificationEvidence },
|
verification: { status: "passed", commands, evidence: verificationEvidence },
|
||||||
codeReview: { recommendation: "APPROVE", architectStatus: "CLEAR", evidence: reviewEvidence },
|
codeReview: { recommendation: "APPROVE", architectStatus: "CLEAR", evidence: reviewEvidence },
|
||||||
@@ -86,17 +86,17 @@ export function classifyExternalAuthorizationBlocker(evidence: string): string |
|
|||||||
return `GHCR_PULL_ACCESS:${status || "AUTHORIZATION_REQUIRED"}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`;
|
return `GHCR_PULL_ACCESS:${status || "AUTHORIZATION_REQUIRED"}:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function nestedBlockerSignature(goal: UltragoalItem): string | null {
|
function nestedBlockerSignature(goal: UlwLoopItem): string | null {
|
||||||
const blocker = Reflect.get(goal, "blocker");
|
const blocker = Reflect.get(goal, "blocker");
|
||||||
const signature = isRecord(blocker) ? blocker["signature"] : null;
|
const signature = isRecord(blocker) ? blocker["signature"] : null;
|
||||||
return typeof signature === "string" ? signature : null;
|
return typeof signature === "string" ? signature : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sameBlockerOccurrences(plan: UltragoalPlan, signature: string): number {
|
export function sameBlockerOccurrences(plan: UlwLoopPlan, signature: string): number {
|
||||||
return plan.goals.filter((goal) => goal.blockerSignature === signature || nestedBlockerSignature(goal) === signature)
|
return plan.goals.filter((goal) => goal.blockerSignature === signature || nestedBlockerSignature(goal) === signature)
|
||||||
.length;
|
.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function clearGoalBlockerFields(goal: UltragoalItem): void {
|
export function clearGoalBlockerFields(goal: UlwLoopItem): void {
|
||||||
for (const key of BLOCKER_FIELD_KEYS) Reflect.deleteProperty(goal, key);
|
for (const key of BLOCKER_FIELD_KEYS) Reflect.deleteProperty(goal, key);
|
||||||
}
|
}
|
||||||
+18
-18
@@ -3,20 +3,20 @@
|
|||||||
import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js";
|
||||||
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js";
|
||||||
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
import { seedDefaultSuccessCriteria } from "./plan-crud.js";
|
||||||
import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js";
|
import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan } from "./types.js";
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "./types.js";
|
||||||
import { iso, UltragoalError } from "./types.js";
|
import { iso, UlwLoopError } from "./types.js";
|
||||||
|
|
||||||
export interface RecordFinalReviewBlockersArgs { readonly goalId: string; readonly title: string; readonly objective: string; readonly evidence: string; readonly codexGoalJson: string }
|
export interface RecordFinalReviewBlockersArgs { readonly goalId: string; readonly title: string; readonly objective: string; readonly evidence: string; readonly codexGoalJson: string }
|
||||||
export interface RecordFinalReviewBlockersResult { readonly plan: UltragoalPlan; readonly blockedGoal: UltragoalItem; readonly newGoal: UltragoalItem; readonly ledgerEntries: UltragoalLedgerEntry[] }
|
export interface RecordFinalReviewBlockersResult { readonly plan: UlwLoopPlan; readonly blockedGoal: UlwLoopItem; readonly newGoal: UlwLoopItem; readonly ledgerEntries: UlwLoopLedgerEntry[] }
|
||||||
|
|
||||||
const BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
|
const BLOCKER_FIELDS = "blockedReason blockerSignature blockerOccurrenceCount requiredExternalDecision nonRetriable failedAt failureReason completedAt blocker blockerEvidence blockerOccurrences blockedAt".split(" ");
|
||||||
|
|
||||||
function ultragoalError(message: string, code: string): never {
|
function ulwLoopError(message: string, code: string): never {
|
||||||
throw new UltragoalError(message, code);
|
throw new UlwLoopError(message, code);
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextGoalId(plan: UltragoalPlan): string {
|
function nextGoalId(plan: UlwLoopPlan): string {
|
||||||
const max = plan.goals.reduce((current, goal) => {
|
const max = plan.goals.reduce((current, goal) => {
|
||||||
const digits = /^G(\d+)/u.exec(goal.id)?.[1];
|
const digits = /^G(\d+)/u.exec(goal.id)?.[1];
|
||||||
return digits === undefined ? current : Math.max(current, Number(digits));
|
return digits === undefined ? current : Math.max(current, Number(digits));
|
||||||
@@ -24,9 +24,9 @@ function nextGoalId(plan: UltragoalPlan): string {
|
|||||||
return `G${String(max + 1).padStart(3, "0")}`;
|
return `G${String(max + 1).padStart(3, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendBlockerGoal(plan: UltragoalPlan, args: RecordFinalReviewBlockersArgs, now: string): UltragoalItem {
|
function appendBlockerGoal(plan: UlwLoopPlan, args: RecordFinalReviewBlockersArgs, now: string): UlwLoopItem {
|
||||||
const index = plan.goals.length;
|
const index = plan.goals.length;
|
||||||
const goal: UltragoalItem = {
|
const goal: UlwLoopItem = {
|
||||||
id: nextGoalId(plan),
|
id: nextGoalId(plan),
|
||||||
title: args.title,
|
title: args.title,
|
||||||
objective: args.objective,
|
objective: args.objective,
|
||||||
@@ -44,17 +44,17 @@ export async function recordFinalReviewBlockers(
|
|||||||
repoRoot: string,
|
repoRoot: string,
|
||||||
args: RecordFinalReviewBlockersArgs,
|
args: RecordFinalReviewBlockersArgs,
|
||||||
): Promise<RecordFinalReviewBlockersResult> {
|
): Promise<RecordFinalReviewBlockersResult> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const goal = plan.goals.find((candidate) => candidate.id === args.goalId);
|
const goal = plan.goals.find((candidate) => candidate.id === args.goalId);
|
||||||
if (goal === undefined) ultragoalError(`Unknown ultragoal id: ${args.goalId}`, "ultragoal_goal_not_found");
|
if (goal === undefined) ulwLoopError(`Unknown ulw-loop id: ${args.goalId}`, "ulw_loop_goal_not_found");
|
||||||
if (goal.status !== "in_progress") ultragoalError(`${goal.id} is ${goal.status}.`, "ultragoal_goal_not_in_progress");
|
if (goal.status !== "in_progress") ulwLoopError(`${goal.id} is ${goal.status}.`, "ulw_loop_goal_not_in_progress");
|
||||||
if (!isFinalRunCompletionCandidate(plan, goal)) ultragoalError(`${goal.id} is not final.`, "ultragoal_not_final_story");
|
if (!isFinalRunCompletionCandidate(plan, goal)) ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story");
|
||||||
|
|
||||||
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot);
|
||||||
const aggregate = codexGoalMode(plan) === "aggregate";
|
const aggregate = codexGoalMode(plan) === "aggregate";
|
||||||
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expectedCodexObjective(plan, goal), ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
const reconciliation = reconcileCodexGoalSnapshot(snapshot, { expectedObjective: expectedCodexObjective(plan, goal), ...(aggregate ? { acceptedObjectives: compatibleCodexObjectives(plan) } : {}), allowedStatuses: ["active"], requireSnapshot: true, requireComplete: false });
|
||||||
if (!reconciliation.ok) ultragoalError(reconciliation.errors.join(" "), "ultragoal_codex_snapshot_mismatch");
|
if (!reconciliation.ok) ulwLoopError(reconciliation.errors.join(" "), "ulw_loop_codex_snapshot_mismatch");
|
||||||
|
|
||||||
const now = iso();
|
const now = iso();
|
||||||
for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
|
for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field);
|
||||||
@@ -67,9 +67,9 @@ export async function recordFinalReviewBlockers(
|
|||||||
plan.updatedAt = now;
|
plan.updatedAt = now;
|
||||||
|
|
||||||
const codexGoal = reconciliation.snapshot.raw;
|
const codexGoal = reconciliation.snapshot.raw;
|
||||||
const blockedEntry: UltragoalLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal };
|
const blockedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal };
|
||||||
const addedEntry: UltragoalLedgerEntry = { at: now, kind: "goal_added", goalId: newGoal.id, status: newGoal.status, evidence: args.evidence, message: newGoal.title };
|
const addedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_added", goalId: newGoal.id, status: newGoal.status, evidence: args.evidence, message: newGoal.title };
|
||||||
const summaryEntry: UltragoalLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
|
const summaryEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal, message: `Review blockers recorded; appended ${newGoal.id}.` };
|
||||||
Reflect.set(summaryEntry, "kind", "blocker_recorded");
|
Reflect.set(summaryEntry, "kind", "blocker_recorded");
|
||||||
const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
|
const ledgerEntries = [blockedEntry, addedEntry, summaryEntry];
|
||||||
await writePlan(repoRoot, plan);
|
await writePlan(repoRoot, plan);
|
||||||
+44
-44
@@ -1,21 +1,21 @@
|
|||||||
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
// biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget
|
||||||
import { isUltragoalDone } from "./goal-status.js";
|
import { isUlwLoopDone } from "./goal-status.js";
|
||||||
import { appendLedger, readSteeringLedgerEntries, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js";
|
import { appendLedger, readSteeringLedgerEntries, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js";
|
||||||
import type {
|
import type {
|
||||||
SteerUltragoalResult,
|
SteerUlwLoopResult,
|
||||||
UltragoalItem,
|
UlwLoopItem,
|
||||||
UltragoalLedgerEntry,
|
UlwLoopLedgerEntry,
|
||||||
UltragoalPlan,
|
UlwLoopPlan,
|
||||||
UltragoalSteeringAudit,
|
UlwLoopSteeringAudit,
|
||||||
UltragoalSteeringChildGoal,
|
UlwLoopSteeringChildGoal,
|
||||||
UltragoalSteeringMutationKind,
|
UlwLoopSteeringMutationKind,
|
||||||
UltragoalSteeringProposal,
|
UlwLoopSteeringProposal,
|
||||||
UltragoalSteeringSource,
|
UlwLoopSteeringSource,
|
||||||
UltragoalSuccessCriterionUserModel,
|
UlwLoopSuccessCriterionUserModel,
|
||||||
} from "./types.js";
|
} from "./types.js";
|
||||||
import { iso, ULTRAGOAL_STEERING_MUTATION_KINDS, ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
import { iso, ULW_LOOP_STEERING_MUTATION_KINDS, ULW_LOOP_SUCCESS_CRITERION_USER_MODELS } from "./types.js";
|
||||||
|
|
||||||
const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UltragoalSteeringSource[];
|
const SOURCES = ["user_prompt_submit", "finding", "cli"] as const satisfies readonly UlwLoopSteeringSource[];
|
||||||
const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
const PROTECTED = new Set(["aggregateCompletion", "codexObjective", "codexObjectiveAliases", "originalConstraints", "qualityGate", "status", "completedAt", "completionStatus"]);
|
||||||
const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; const isPlain = (value: unknown): value is object => isObject(value) && !Array.isArray(value);
|
const isObject = (value: unknown): value is object => typeof value === "object" && value !== null; const isPlain = (value: unknown): value is object => isObject(value) && !Array.isArray(value);
|
||||||
const read = (value: object, key: string): unknown => Object.entries(value).find(([name]) => name === key)?.[1];
|
const read = (value: object, key: string): unknown => Object.entries(value).find(([name]) => name === key)?.[1];
|
||||||
@@ -24,9 +24,9 @@ const text = (value: object, key: string): string | undefined => {
|
|||||||
const candidate = read(value, key);
|
const candidate = read(value, key);
|
||||||
return isText(candidate) ? candidate.trim() : undefined;
|
return isText(candidate) ? candidate.trim() : undefined;
|
||||||
};
|
};
|
||||||
const isKind = (value: unknown): value is UltragoalSteeringMutationKind => typeof value === "string" && ULTRAGOAL_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
const isKind = (value: unknown): value is UlwLoopSteeringMutationKind => typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value);
|
||||||
const isSource = (value: unknown): value is UltragoalSteeringSource => typeof value === "string" && SOURCES.some((source) => source === value);
|
const isSource = (value: unknown): value is UlwLoopSteeringSource => typeof value === "string" && SOURCES.some((source) => source === value);
|
||||||
const isModel = (value: unknown): value is UltragoalSuccessCriterionUserModel => typeof value === "string" && ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
const isModel = (value: unknown): value is UlwLoopSuccessCriterionUserModel => typeof value === "string" && ULW_LOOP_SUCCESS_CRITERION_USER_MODELS.some((model) => model === value);
|
||||||
const texts = (value: object, key: string): string[] => {
|
const texts = (value: object, key: string): string[] => {
|
||||||
const candidate = read(value, key);
|
const candidate = read(value, key);
|
||||||
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : [];
|
return Array.isArray(candidate) && candidate.every((item) => typeof item === "string") ? candidate : [];
|
||||||
@@ -44,7 +44,7 @@ const after = (proposal: object): object | undefined => {
|
|||||||
};
|
};
|
||||||
const revised = (proposal: object, direct: string, nested: string): string | undefined => text(proposal, direct) ?? text(after(proposal) ?? proposal, nested);
|
const revised = (proposal: object, direct: string, nested: string): string | undefined => text(proposal, direct) ?? text(after(proposal) ?? proposal, nested);
|
||||||
|
|
||||||
function child(value: unknown): UltragoalSteeringChildGoal | null {
|
function child(value: unknown): UlwLoopSteeringChildGoal | null {
|
||||||
if (!isPlain(value)) return null;
|
if (!isPlain(value)) return null;
|
||||||
const title = text(value, "title");
|
const title = text(value, "title");
|
||||||
const objective = text(value, "objective");
|
const objective = text(value, "objective");
|
||||||
@@ -60,7 +60,7 @@ function childValues(proposal: object): unknown[] {
|
|||||||
return Array.isArray(fromAfter) ? fromAfter : [];
|
return Array.isArray(fromAfter) ? fromAfter : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const children = (proposal: object): UltragoalSteeringChildGoal[] => childValues(proposal).map(child).filter((item): item is UltragoalSteeringChildGoal => item !== null);
|
const children = (proposal: object): UlwLoopSteeringChildGoal[] => childValues(proposal).map(child).filter((item): item is UlwLoopSteeringChildGoal => item !== null);
|
||||||
const pendingOrder = (proposal: object): string[] => {
|
const pendingOrder = (proposal: object): string[] => {
|
||||||
const direct = texts(proposal, "pendingOrder");
|
const direct = texts(proposal, "pendingOrder");
|
||||||
return direct.length > 0 ? direct : texts(after(proposal) ?? proposal, "pendingGoalIds");
|
return direct.length > 0 ? direct : texts(after(proposal) ?? proposal, "pendingGoalIds");
|
||||||
@@ -82,13 +82,13 @@ function weakens(value: unknown): boolean {
|
|||||||
return /\b(skip|bypass|weaken|remove|omit|auto[-\s]?complete|mark complete|complete faster)\b/.test(valueText) && /\b(test|tests|verification|review|quality gate|complete|completion)\b/.test(valueText);
|
return /\b(skip|bypass|weaken|remove|omit|auto[-\s]?complete|mark complete|complete faster)\b/.test(valueText) && /\b(test|tests|verification|review|quality gate|complete|completion)\b/.test(valueText);
|
||||||
}
|
}
|
||||||
|
|
||||||
function auditFor(proposal: unknown, reasons: string[]): UltragoalSteeringAudit {
|
function auditFor(proposal: unknown, reasons: string[]): UlwLoopSteeringAudit {
|
||||||
const object = isPlain(proposal) ? proposal : undefined;
|
const object = isPlain(proposal) ? proposal : undefined;
|
||||||
const kindRaw = object === undefined ? undefined : read(object, "kind");
|
const kindRaw = object === undefined ? undefined : read(object, "kind");
|
||||||
const sourceRaw = object === undefined ? undefined : read(object, "source");
|
const sourceRaw = object === undefined ? undefined : read(object, "source");
|
||||||
const evidence = object === undefined ? "" : (text(object, "evidence") ?? "");
|
const evidence = object === undefined ? "" : (text(object, "evidence") ?? "");
|
||||||
const rationale = object === undefined ? "" : (text(object, "rationale") ?? "");
|
const rationale = object === undefined ? "" : (text(object, "rationale") ?? "");
|
||||||
const audit: UltragoalSteeringAudit = { kind: isKind(kindRaw) ? kindRaw : "annotate_ledger", source: isSource(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
|
const audit: UlwLoopSteeringAudit = { kind: isKind(kindRaw) ? kindRaw : "annotate_ledger", source: isSource(sourceRaw) ? sourceRaw : "cli", targetGoalIds: object === undefined ? [] : targets(object), evidence, rationale, invariant: { accepted: reasons.length === 0, structuralInvariantAccepted: reasons.length === 0, evidenceBackedNecessity: evidence.length > 0 && rationale.length > 0, noEasierCompletion: !weakens(proposal), rejectedReasons: reasons, reasons } };
|
||||||
if (object === undefined) return audit;
|
if (object === undefined) return audit;
|
||||||
const criterionId = text(object, "criterionId");
|
const criterionId = text(object, "criterionId");
|
||||||
const directiveText = text(object, "directiveText");
|
const directiveText = text(object, "directiveText");
|
||||||
@@ -101,7 +101,7 @@ function auditFor(proposal: unknown, reasons: string[]): UltragoalSteeringAudit
|
|||||||
return audit;
|
return audit;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateUltragoalSteeringProposal(plan: UltragoalPlan, proposal: unknown): UltragoalSteeringAudit {
|
export function validateUlwLoopSteeringProposal(plan: UlwLoopPlan, proposal: unknown): UlwLoopSteeringAudit {
|
||||||
const reasons: string[] = [];
|
const reasons: string[] = [];
|
||||||
if (!isPlain(proposal)) reasons.push("proposal must be an object");
|
if (!isPlain(proposal)) reasons.push("proposal must be an object");
|
||||||
const object = isPlain(proposal) ? proposal : {};
|
const object = isPlain(proposal) ? proposal : {};
|
||||||
@@ -112,16 +112,16 @@ export function validateUltragoalSteeringProposal(plan: UltragoalPlan, proposal:
|
|||||||
if (text(object, "rationale") === undefined) reasons.push("missing rationale");
|
if (text(object, "rationale") === undefined) reasons.push("missing rationale");
|
||||||
if (hasProtected(proposal)) reasons.push("protected payload");
|
if (hasProtected(proposal)) reasons.push("protected payload");
|
||||||
if (weakens(proposal)) reasons.push("weakened completion");
|
if (weakens(proposal)) reasons.push("weakened completion");
|
||||||
if (isUltragoalDone(plan)) reasons.push("plan already complete");
|
if (isUlwLoopDone(plan)) reasons.push("plan already complete");
|
||||||
if (isKind(kind)) validateKind(plan, object, kind, reasons);
|
if (isKind(kind)) validateKind(plan, object, kind, reasons);
|
||||||
return auditFor(proposal, reasons);
|
return auditFor(proposal, reasons);
|
||||||
}
|
}
|
||||||
|
|
||||||
function goal(plan: UltragoalPlan, id: string | undefined): UltragoalItem | undefined {
|
function goal(plan: UlwLoopPlan, id: string | undefined): UlwLoopItem | undefined {
|
||||||
return id === undefined ? undefined : plan.goals.find((item) => item.id === id);
|
return id === undefined ? undefined : plan.goals.find((item) => item.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateKind(plan: UltragoalPlan, proposal: object, kind: UltragoalSteeringMutationKind, reasons: string[]): void {
|
function validateKind(plan: UlwLoopPlan, proposal: object, kind: UlwLoopSteeringMutationKind, reasons: string[]): void {
|
||||||
const target = goal(plan, targets(proposal)[0]);
|
const target = goal(plan, targets(proposal)[0]);
|
||||||
if (kind === "add_subgoal" && (text(proposal, "title") === undefined || text(proposal, "objective") === undefined)) reasons.push("add_subgoal requires title/objective");
|
if (kind === "add_subgoal" && (text(proposal, "title") === undefined || text(proposal, "objective") === undefined)) reasons.push("add_subgoal requires title/objective");
|
||||||
if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined) reasons.push(`${kind} requires target`);
|
if ((kind === "split_subgoal" || kind === "revise_pending_wording" || kind === "mark_blocked_superseded") && target === undefined) reasons.push(`${kind} requires target`);
|
||||||
@@ -134,7 +134,7 @@ function validateKind(plan: UltragoalPlan, proposal: object, kind: UltragoalStee
|
|||||||
if (kind === "revise_criterion") validateCriterion(plan, proposal, reasons);
|
if (kind === "revise_criterion") validateCriterion(plan, proposal, reasons);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateOrder(plan: UltragoalPlan, proposal: object, reasons: string[]): void {
|
function validateOrder(plan: UlwLoopPlan, proposal: object, reasons: string[]): void {
|
||||||
const requested = pendingOrder(proposal);
|
const requested = pendingOrder(proposal);
|
||||||
const pending = plan.goals.filter((item) => item.status === "pending" && item.steeringStatus === undefined).map((item) => item.id);
|
const pending = plan.goals.filter((item) => item.status === "pending" && item.steeringStatus === undefined).map((item) => item.id);
|
||||||
if (requested.length === 0) reasons.push("reorder_pending requires ids");
|
if (requested.length === 0) reasons.push("reorder_pending requires ids");
|
||||||
@@ -142,7 +142,7 @@ function validateOrder(plan: UltragoalPlan, proposal: object, reasons: string[])
|
|||||||
if (requested.some((id) => !pending.includes(id))) reasons.push("unknown pending id");
|
if (requested.some((id) => !pending.includes(id))) reasons.push("unknown pending id");
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCriterion(plan: UltragoalPlan, proposal: object, reasons: string[]): void {
|
function validateCriterion(plan: UlwLoopPlan, proposal: object, reasons: string[]): void {
|
||||||
const target = goal(plan, targets(proposal)[0]);
|
const target = goal(plan, targets(proposal)[0]);
|
||||||
const criterionId = text(proposal, "criterionId");
|
const criterionId = text(proposal, "criterionId");
|
||||||
if (target === undefined) reasons.push("revise_criterion requires goalId");
|
if (target === undefined) reasons.push("revise_criterion requires goalId");
|
||||||
@@ -152,7 +152,7 @@ function validateCriterion(plan: UltragoalPlan, proposal: object, reasons: strin
|
|||||||
if (model !== undefined && !isModel(model)) reasons.push("invalid userModel");
|
if (model !== undefined && !isModel(model)) reasons.push("invalid userModel");
|
||||||
}
|
}
|
||||||
|
|
||||||
function nextId(plan: UltragoalPlan, offset: number): string {
|
function nextId(plan: UlwLoopPlan, offset: number): string {
|
||||||
const max = plan.goals.reduce((current, item) => {
|
const max = plan.goals.reduce((current, item) => {
|
||||||
const digits = /^G(\d+)$/u.exec(item.id)?.[1];
|
const digits = /^G(\d+)$/u.exec(item.id)?.[1];
|
||||||
return digits === undefined ? current : Math.max(current, Number(digits));
|
return digits === undefined ? current : Math.max(current, Number(digits));
|
||||||
@@ -160,18 +160,18 @@ function nextId(plan: UltragoalPlan, offset: number): string {
|
|||||||
return `G${String(max + offset).padStart(3, "0")}`;
|
return `G${String(max + offset).padStart(3, "0")}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(plan: UltragoalPlan, childGoal: UltragoalSteeringChildGoal, evidence: string, now: string, offset: number): UltragoalItem {
|
function makeGoal(plan: UlwLoopPlan, childGoal: UlwLoopSteeringChildGoal, evidence: string, now: string, offset: number): UlwLoopItem {
|
||||||
return { id: nextId(plan, offset), title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: [], attempt: 0, createdAt: now, updatedAt: now, evidence };
|
return { id: nextId(plan, offset), title: childGoal.title, objective: childGoal.objective, status: "pending", successCriteria: [], attempt: 0, createdAt: now, updatedAt: now, evidence };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applySteeringMutation(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, audit: UltragoalSteeringAudit): UltragoalPlan {
|
export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit): UlwLoopPlan {
|
||||||
const next = structuredClone(plan);
|
const next = structuredClone(plan);
|
||||||
if (!audit.invariant.accepted) return next;
|
if (!audit.invariant.accepted) return next;
|
||||||
const now = proposal.now?.toISOString() ?? iso();
|
const now = proposal.now?.toISOString() ?? iso();
|
||||||
if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
|
if (proposal.kind === "add_subgoal") next.goals.push(makeGoal(next, { title: proposal.title ?? "", objective: proposal.objective ?? "" }, proposal.evidence, now, 1));
|
||||||
if (proposal.kind === "reorder_pending") {
|
if (proposal.kind === "reorder_pending") {
|
||||||
const order = pendingOrder(proposal);
|
const order = pendingOrder(proposal);
|
||||||
next.goals = [...order.map((id) => goal(next, id)).filter((item): item is UltragoalItem => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
|
next.goals = [...order.map((id) => goal(next, id)).filter((item): item is UlwLoopItem => item !== undefined), ...next.goals.filter((item) => !order.includes(item.id))];
|
||||||
}
|
}
|
||||||
if (proposal.kind === "revise_pending_wording") reviseWording(next, proposal, now);
|
if (proposal.kind === "revise_pending_wording") reviseWording(next, proposal, now);
|
||||||
if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now);
|
if (proposal.kind === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now);
|
||||||
@@ -180,7 +180,7 @@ export function applySteeringMutation(plan: UltragoalPlan, proposal: UltragoalSt
|
|||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reviseWording(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, now: string): void {
|
function reviseWording(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
||||||
const target = goal(plan, targets(proposal)[0]);
|
const target = goal(plan, targets(proposal)[0]);
|
||||||
if (target === undefined) return;
|
if (target === undefined) return;
|
||||||
target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
|
target.title = revised(proposal, "revisedTitle", "title") ?? target.title;
|
||||||
@@ -190,7 +190,7 @@ function reviseWording(plan: UltragoalPlan, proposal: UltragoalSteeringProposal,
|
|||||||
target.updatedAt = now;
|
target.updatedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
function splitOrBlock(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, now: string): void {
|
function splitOrBlock(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
||||||
const target = goal(plan, targets(proposal)[0]);
|
const target = goal(plan, targets(proposal)[0]);
|
||||||
if (target === undefined) return;
|
if (target === undefined) return;
|
||||||
const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
|
const replacements = children(proposal).map((item, index) => makeGoal(plan, item, proposal.evidence, now, index + 1));
|
||||||
@@ -210,7 +210,7 @@ function splitOrBlock(plan: UltragoalPlan, proposal: UltragoalSteeringProposal,
|
|||||||
if (plan.activeGoalId === target.id) delete plan.activeGoalId;
|
if (plan.activeGoalId === target.id) delete plan.activeGoalId;
|
||||||
}
|
}
|
||||||
|
|
||||||
function reviseCriterion(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, now: string): void {
|
function reviseCriterion(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, now: string): void {
|
||||||
const target = goal(plan, targets(proposal)[0]);
|
const target = goal(plan, targets(proposal)[0]);
|
||||||
const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
|
const index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1;
|
||||||
const current = target?.successCriteria[index];
|
const current = target?.successCriteria[index];
|
||||||
@@ -220,12 +220,12 @@ function reviseCriterion(plan: UltragoalPlan, proposal: UltragoalSteeringProposa
|
|||||||
target.updatedAt = now;
|
target.updatedAt = now;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isProposal(value: unknown): value is UltragoalSteeringProposal {
|
function isProposal(value: unknown): value is UlwLoopSteeringProposal {
|
||||||
return isPlain(value) && isKind(read(value, "kind")) && isSource(read(value, "source")) && isText(read(value, "evidence")) && isText(read(value, "rationale"));
|
return isPlain(value) && isKind(read(value, "kind")) && isSource(read(value, "source")) && isText(read(value, "evidence")) && isText(read(value, "rationale"));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseUltragoalSteeringDirective(text: string): UltragoalSteeringProposal | null {
|
export function parseUlwLoopSteeringDirective(text: string): UlwLoopSteeringProposal | null {
|
||||||
const match = /(?:^|\s)(?:OMO_ULTRAGOAL_STEER|omo\.ultragoal\.steer|omo ultragoal steer):\s*([\s\S]+)$/u.exec(text);
|
const match = /(?:^|\s)(?:OMO_ULW_LOOP_STEER|omo\.ulw-loop\.steer|omo ulw-loop steer):\s*([\s\S]+)$/u.exec(text);
|
||||||
if (match?.[1] === undefined) return null;
|
if (match?.[1] === undefined) return null;
|
||||||
try {
|
try {
|
||||||
const parsed: unknown = JSON.parse(match[1].trim());
|
const parsed: unknown = JSON.parse(match[1].trim());
|
||||||
@@ -236,16 +236,16 @@ export function parseUltragoalSteeringDirective(text: string): UltragoalSteering
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function steerUltragoal(repoRoot: string, proposal: UltragoalSteeringProposal): Promise<SteerUltragoalResult> {
|
export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal): Promise<SteerUlwLoopResult> {
|
||||||
return withUltragoalMutationLock(repoRoot, async () => {
|
return withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
const key = proposal.idempotencyKey ?? proposal.promptSignature;
|
||||||
const prior = key === undefined ? undefined : (await readSteeringLedgerEntries(repoRoot)).find((entry) => entry.steering?.invariant.accepted === true && (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key));
|
const prior = key === undefined ? undefined : (await readSteeringLedgerEntries(repoRoot)).find((entry) => entry.steering?.invariant.accepted === true && (entry.idempotencyKey === key || entry.steering.idempotencyKey === key || entry.steering.promptSignature === key));
|
||||||
if (prior?.steering !== undefined) return { plan, accepted: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [], deduped: true };
|
if (prior?.steering !== undefined) return { plan, accepted: true, audit: { ...prior.steering, deduped: true }, rejectedReasons: [], deduped: true };
|
||||||
const audit = validateUltragoalSteeringProposal(plan, proposal);
|
const audit = validateUlwLoopSteeringProposal(plan, proposal);
|
||||||
const accepted = audit.invariant.accepted;
|
const accepted = audit.invariant.accepted;
|
||||||
const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
|
const next = accepted ? applySteeringMutation(plan, proposal, audit) : plan;
|
||||||
const finalAudit: UltragoalSteeringAudit = { ...audit, before: plan };
|
const finalAudit: UlwLoopSteeringAudit = { ...audit, before: plan };
|
||||||
if (accepted) finalAudit.after = next;
|
if (accepted) finalAudit.after = next;
|
||||||
if (accepted) await writePlan(repoRoot, next);
|
if (accepted) await writePlan(repoRoot, next);
|
||||||
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()));
|
await appendLedger(repoRoot, ledgerEntry(proposal, finalAudit, proposal.now?.toISOString() ?? iso()));
|
||||||
@@ -253,8 +253,8 @@ export async function steerUltragoal(repoRoot: string, proposal: UltragoalSteeri
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function ledgerEntry(proposal: UltragoalSteeringProposal, audit: UltragoalSteeringAudit, at: string): UltragoalLedgerEntry {
|
function ledgerEntry(proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit, at: string): UlwLoopLedgerEntry {
|
||||||
const entry: UltragoalLedgerEntry = { at, kind: audit.invariant.accepted ? (proposal.kind === "revise_criterion" ? "criteria_revised" : "steering_accepted") : "steering_rejected", evidence: proposal.evidence, message: proposal.rationale, steering: audit, mutationKind: proposal.kind };
|
const entry: UlwLoopLedgerEntry = { at, kind: audit.invariant.accepted ? (proposal.kind === "revise_criterion" ? "criteria_revised" : "steering_accepted") : "steering_rejected", evidence: proposal.evidence, message: proposal.rationale, steering: audit, mutationKind: proposal.kind };
|
||||||
const goalId = audit.targetGoalIds[0];
|
const goalId = audit.targetGoalIds[0];
|
||||||
if (goalId !== undefined) entry.goalId = goalId;
|
if (goalId !== undefined) entry.goalId = goalId;
|
||||||
if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId;
|
if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId;
|
||||||
+61
-61
@@ -1,9 +1,9 @@
|
|||||||
export const ULTRAGOAL_DIR = ".omo/ultragoal";
|
export const ULW_LOOP_DIR = ".omo/ulw-loop";
|
||||||
export const ULTRAGOAL_BRIEF = "brief.md";
|
export const ULW_LOOP_BRIEF = "brief.md";
|
||||||
export const ULTRAGOAL_GOALS = "goals.json";
|
export const ULW_LOOP_GOALS = "goals.json";
|
||||||
export const ULTRAGOAL_LEDGER = "ledger.jsonl";
|
export const ULW_LOOP_LEDGER = "ledger.jsonl";
|
||||||
|
|
||||||
export type UltragoalStatus =
|
export type UlwLoopStatus =
|
||||||
| "pending"
|
| "pending"
|
||||||
| "in_progress"
|
| "in_progress"
|
||||||
| "complete"
|
| "complete"
|
||||||
@@ -12,11 +12,11 @@ export type UltragoalStatus =
|
|||||||
| "review_blocked"
|
| "review_blocked"
|
||||||
| "needs_user_decision";
|
| "needs_user_decision";
|
||||||
|
|
||||||
export type UltragoalCodexGoalMode = "aggregate" | "per_story";
|
export type UlwLoopCodexGoalMode = "aggregate" | "per_story";
|
||||||
|
|
||||||
export type UltragoalSteeringStatus = "superseded" | "blocked";
|
export type UlwLoopSteeringStatus = "superseded" | "blocked";
|
||||||
|
|
||||||
export const ULTRAGOAL_STEERING_MUTATION_KINDS = [
|
export const ULW_LOOP_STEERING_MUTATION_KINDS = [
|
||||||
"add_subgoal",
|
"add_subgoal",
|
||||||
"split_subgoal",
|
"split_subgoal",
|
||||||
"reorder_pending",
|
"reorder_pending",
|
||||||
@@ -25,22 +25,22 @@ export const ULTRAGOAL_STEERING_MUTATION_KINDS = [
|
|||||||
"annotate_ledger",
|
"annotate_ledger",
|
||||||
"mark_blocked_superseded",
|
"mark_blocked_superseded",
|
||||||
] as const satisfies readonly string[];
|
] as const satisfies readonly string[];
|
||||||
export type UltragoalSteeringMutationKind = (typeof ULTRAGOAL_STEERING_MUTATION_KINDS)[number];
|
export type UlwLoopSteeringMutationKind = (typeof ULW_LOOP_STEERING_MUTATION_KINDS)[number];
|
||||||
|
|
||||||
export type UltragoalSteeringSource = "user_prompt_submit" | "finding" | "cli";
|
export type UlwLoopSteeringSource = "user_prompt_submit" | "finding" | "cli";
|
||||||
|
|
||||||
export const ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS = [
|
export const ULW_LOOP_SUCCESS_CRITERION_USER_MODELS = [
|
||||||
"happy",
|
"happy",
|
||||||
"edge",
|
"edge",
|
||||||
"regression",
|
"regression",
|
||||||
"adversarial",
|
"adversarial",
|
||||||
] as const satisfies readonly string[];
|
] as const satisfies readonly string[];
|
||||||
export type UltragoalSuccessCriterionUserModel = (typeof ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS)[number];
|
export type UlwLoopSuccessCriterionUserModel = (typeof ULW_LOOP_SUCCESS_CRITERION_USER_MODELS)[number];
|
||||||
|
|
||||||
export const ULTRAGOAL_CRITERION_STATUSES = ["pending", "pass", "fail", "blocked"] as const satisfies readonly string[];
|
export const ULW_LOOP_CRITERION_STATUSES = ["pending", "pass", "fail", "blocked"] as const satisfies readonly string[];
|
||||||
export type UltragoalCriterionStatus = (typeof ULTRAGOAL_CRITERION_STATUSES)[number];
|
export type UlwLoopCriterionStatus = (typeof ULW_LOOP_CRITERION_STATUSES)[number];
|
||||||
|
|
||||||
export const ULTRAGOAL_LEDGER_EVENT_KINDS = [
|
export const ULW_LOOP_LEDGER_EVENT_KINDS = [
|
||||||
"plan_created",
|
"plan_created",
|
||||||
"goal_started",
|
"goal_started",
|
||||||
"goal_resumed",
|
"goal_resumed",
|
||||||
@@ -61,20 +61,20 @@ export const ULTRAGOAL_LEDGER_EVENT_KINDS = [
|
|||||||
"criterion_blocked",
|
"criterion_blocked",
|
||||||
"criteria_revised",
|
"criteria_revised",
|
||||||
] as const satisfies readonly string[];
|
] as const satisfies readonly string[];
|
||||||
export type UltragoalLedgerEventKind = (typeof ULTRAGOAL_LEDGER_EVENT_KINDS)[number];
|
export type UlwLoopLedgerEventKind = (typeof ULW_LOOP_LEDGER_EVENT_KINDS)[number];
|
||||||
|
|
||||||
export interface UltragoalSuccessCriterion {
|
export interface UlwLoopSuccessCriterion {
|
||||||
readonly id: string;
|
readonly id: string;
|
||||||
readonly scenario: string;
|
readonly scenario: string;
|
||||||
readonly userModel: UltragoalSuccessCriterionUserModel;
|
readonly userModel: UlwLoopSuccessCriterionUserModel;
|
||||||
readonly expectedEvidence: string;
|
readonly expectedEvidence: string;
|
||||||
capturedEvidence: string | null;
|
capturedEvidence: string | null;
|
||||||
status: UltragoalCriterionStatus;
|
status: UlwLoopCriterionStatus;
|
||||||
capturedAt?: string;
|
capturedAt?: string;
|
||||||
notes?: string;
|
notes?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalSteeringInvariantResult {
|
export interface UlwLoopSteeringInvariantResult {
|
||||||
accepted: boolean;
|
accepted: boolean;
|
||||||
structuralInvariantAccepted: boolean;
|
structuralInvariantAccepted: boolean;
|
||||||
evidenceBackedNecessity: boolean;
|
evidenceBackedNecessity: boolean;
|
||||||
@@ -83,21 +83,21 @@ export interface UltragoalSteeringInvariantResult {
|
|||||||
reasons?: string[];
|
reasons?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalSteeringChildGoal {
|
export interface UlwLoopSteeringChildGoal {
|
||||||
title: string;
|
title: string;
|
||||||
objective: string;
|
objective: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalSteeringAfterPayload {
|
export interface UlwLoopSteeringAfterPayload {
|
||||||
title?: string;
|
title?: string;
|
||||||
objective?: string;
|
objective?: string;
|
||||||
pendingGoalIds?: string[];
|
pendingGoalIds?: string[];
|
||||||
children?: UltragoalSteeringChildGoal[];
|
children?: UlwLoopSteeringChildGoal[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalSteeringProposal {
|
export interface UlwLoopSteeringProposal {
|
||||||
kind: UltragoalSteeringMutationKind;
|
kind: UlwLoopSteeringMutationKind;
|
||||||
source: UltragoalSteeringSource;
|
source: UlwLoopSteeringSource;
|
||||||
targetGoalId?: string;
|
targetGoalId?: string;
|
||||||
targetGoalIds?: string[];
|
targetGoalIds?: string[];
|
||||||
criterionId?: string;
|
criterionId?: string;
|
||||||
@@ -105,48 +105,48 @@ export interface UltragoalSteeringProposal {
|
|||||||
rationale: string;
|
rationale: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
objective?: string;
|
objective?: string;
|
||||||
childGoals?: UltragoalSteeringChildGoal[];
|
childGoals?: UlwLoopSteeringChildGoal[];
|
||||||
revisedTitle?: string;
|
revisedTitle?: string;
|
||||||
revisedObjective?: string;
|
revisedObjective?: string;
|
||||||
pendingOrder?: string[];
|
pendingOrder?: string[];
|
||||||
blockedReason?: string;
|
blockedReason?: string;
|
||||||
after?: UltragoalSteeringAfterPayload;
|
after?: UlwLoopSteeringAfterPayload;
|
||||||
directiveText?: string;
|
directiveText?: string;
|
||||||
promptSignature?: string;
|
promptSignature?: string;
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalSteeringAudit {
|
export interface UlwLoopSteeringAudit {
|
||||||
kind: UltragoalSteeringMutationKind;
|
kind: UlwLoopSteeringMutationKind;
|
||||||
source: UltragoalSteeringSource;
|
source: UlwLoopSteeringSource;
|
||||||
targetGoalIds: string[];
|
targetGoalIds: string[];
|
||||||
criterionId?: string;
|
criterionId?: string;
|
||||||
before?: unknown;
|
before?: unknown;
|
||||||
after?: unknown;
|
after?: unknown;
|
||||||
evidence: string;
|
evidence: string;
|
||||||
rationale: string;
|
rationale: string;
|
||||||
invariant: UltragoalSteeringInvariantResult;
|
invariant: UlwLoopSteeringInvariantResult;
|
||||||
directiveText?: string;
|
directiveText?: string;
|
||||||
promptSignature?: string;
|
promptSignature?: string;
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
deduped?: boolean;
|
deduped?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SteerUltragoalResult {
|
export interface SteerUlwLoopResult {
|
||||||
plan: UltragoalPlan;
|
plan: UlwLoopPlan;
|
||||||
accepted: boolean;
|
accepted: boolean;
|
||||||
audit: UltragoalSteeringAudit;
|
audit: UlwLoopSteeringAudit;
|
||||||
rejectedReasons: string[];
|
rejectedReasons: string[];
|
||||||
deduped: boolean;
|
deduped: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalItem {
|
export interface UlwLoopItem {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
objective: string;
|
objective: string;
|
||||||
status: UltragoalStatus;
|
status: UlwLoopStatus;
|
||||||
successCriteria: UltragoalSuccessCriterion[];
|
successCriteria: UlwLoopSuccessCriterion[];
|
||||||
attempt: number;
|
attempt: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
@@ -156,7 +156,7 @@ export interface UltragoalItem {
|
|||||||
reviewBlockedAt?: string;
|
reviewBlockedAt?: string;
|
||||||
evidence?: string;
|
evidence?: string;
|
||||||
failureReason?: string;
|
failureReason?: string;
|
||||||
steeringStatus?: UltragoalSteeringStatus;
|
steeringStatus?: UlwLoopSteeringStatus;
|
||||||
supersededBy?: string[];
|
supersededBy?: string[];
|
||||||
supersedes?: string[];
|
supersedes?: string[];
|
||||||
blockedReason?: string;
|
blockedReason?: string;
|
||||||
@@ -168,54 +168,54 @@ export interface UltragoalItem {
|
|||||||
steeringRationale?: string;
|
steeringRationale?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalAggregateCompletion {
|
export interface UlwLoopAggregateCompletion {
|
||||||
status: "complete";
|
status: "complete";
|
||||||
completedAt: string;
|
completedAt: string;
|
||||||
evidence: string;
|
evidence: string;
|
||||||
codexGoal?: unknown;
|
codexGoal?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalPlan {
|
export interface UlwLoopPlan {
|
||||||
version: 1;
|
version: 1;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
briefPath: string;
|
briefPath: string;
|
||||||
goalsPath: string;
|
goalsPath: string;
|
||||||
ledgerPath: string;
|
ledgerPath: string;
|
||||||
codexGoalMode?: UltragoalCodexGoalMode;
|
codexGoalMode?: UlwLoopCodexGoalMode;
|
||||||
codexObjective?: string;
|
codexObjective?: string;
|
||||||
codexObjectiveAliases?: string[];
|
codexObjectiveAliases?: string[];
|
||||||
aggregateCompletion?: UltragoalAggregateCompletion;
|
aggregateCompletion?: UlwLoopAggregateCompletion;
|
||||||
activeGoalId?: string;
|
activeGoalId?: string;
|
||||||
goals: UltragoalItem[];
|
goals: UlwLoopItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalLedgerEntry {
|
export interface UlwLoopLedgerEntry {
|
||||||
at: string;
|
at: string;
|
||||||
kind: UltragoalLedgerEventKind;
|
kind: UlwLoopLedgerEventKind;
|
||||||
goalId?: string;
|
goalId?: string;
|
||||||
criterionId?: string;
|
criterionId?: string;
|
||||||
status?: UltragoalStatus;
|
status?: UlwLoopStatus;
|
||||||
criterionStatus?: UltragoalCriterionStatus;
|
criterionStatus?: UlwLoopCriterionStatus;
|
||||||
message?: string;
|
message?: string;
|
||||||
codexGoal?: unknown;
|
codexGoal?: unknown;
|
||||||
evidence?: string;
|
evidence?: string;
|
||||||
capturedEvidence?: string;
|
capturedEvidence?: string;
|
||||||
qualityGate?: UltragoalQualityGate;
|
qualityGate?: UlwLoopQualityGate;
|
||||||
steering?: UltragoalSteeringAudit;
|
steering?: UlwLoopSteeringAudit;
|
||||||
before?: unknown;
|
before?: unknown;
|
||||||
after?: unknown;
|
after?: unknown;
|
||||||
mutationKind?: UltragoalSteeringMutationKind;
|
mutationKind?: UlwLoopSteeringMutationKind;
|
||||||
idempotencyKey?: string;
|
idempotencyKey?: string;
|
||||||
blockerSignature?: string;
|
blockerSignature?: string;
|
||||||
blockerOccurrenceCount?: number;
|
blockerOccurrenceCount?: number;
|
||||||
requiredExternalDecision?: string;
|
requiredExternalDecision?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateUltragoalOptions {
|
export interface CreateUlwLoopOptions {
|
||||||
brief: string;
|
brief: string;
|
||||||
goals?: Array<{ title?: string; objective: string }>;
|
goals?: Array<{ title?: string; objective: string }>;
|
||||||
codexGoalMode?: UltragoalCodexGoalMode;
|
codexGoalMode?: UlwLoopCodexGoalMode;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
force?: boolean;
|
force?: boolean;
|
||||||
}
|
}
|
||||||
@@ -227,7 +227,7 @@ export interface StartNextOptions {
|
|||||||
|
|
||||||
export interface CheckpointOptions {
|
export interface CheckpointOptions {
|
||||||
goalId: string;
|
goalId: string;
|
||||||
status: Extract<UltragoalStatus, "complete" | "failed"> | "blocked";
|
status: Extract<UlwLoopStatus, "complete" | "failed"> | "blocked";
|
||||||
evidence?: string;
|
evidence?: string;
|
||||||
codexGoal?: unknown;
|
codexGoal?: unknown;
|
||||||
qualityGate?: unknown;
|
qualityGate?: unknown;
|
||||||
@@ -235,36 +235,36 @@ export interface CheckpointOptions {
|
|||||||
now?: Date;
|
now?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AddUltragoalGoalOptions {
|
export interface AddUlwLoopGoalOptions {
|
||||||
title: string;
|
title: string;
|
||||||
objective: string;
|
objective: string;
|
||||||
evidence?: string;
|
evidence?: string;
|
||||||
now?: Date;
|
now?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RecordFinalReviewBlockersOptions extends AddUltragoalGoalOptions {
|
export interface RecordFinalReviewBlockersOptions extends AddUlwLoopGoalOptions {
|
||||||
goalId: string;
|
goalId: string;
|
||||||
codexGoal?: unknown;
|
codexGoal?: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalQualityGate {
|
export interface UlwLoopQualityGate {
|
||||||
aiSlopCleaner: { status: "passed"; evidence: string };
|
aiSlopCleaner: { status: "passed"; evidence: string };
|
||||||
verification: { status: "passed"; commands: string[]; evidence: string };
|
verification: { status: "passed"; commands: string[]; evidence: string };
|
||||||
codeReview: { recommendation: "APPROVE"; architectStatus: "CLEAR"; evidence: string };
|
codeReview: { recommendation: "APPROVE"; architectStatus: "CLEAR"; evidence: string };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UltragoalErrorOptions {
|
export interface UlwLoopErrorOptions {
|
||||||
readonly cause?: unknown;
|
readonly cause?: unknown;
|
||||||
readonly details?: Record<string, unknown>;
|
readonly details?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UltragoalError extends Error {
|
export class UlwLoopError extends Error {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly details?: Record<string, unknown>;
|
readonly details?: Record<string, unknown>;
|
||||||
|
|
||||||
constructor(message: string, code: string, opts?: UltragoalErrorOptions) {
|
constructor(message: string, code: string, opts?: UlwLoopErrorOptions) {
|
||||||
super(message, opts?.cause === undefined ? undefined : { cause: opts.cause });
|
super(message, opts?.cause === undefined ? undefined : { cause: opts.cause });
|
||||||
this.name = "UltragoalError";
|
this.name = "UlwLoopError";
|
||||||
this.code = code;
|
this.code = code;
|
||||||
if (opts?.details !== undefined) {
|
if (opts?.details !== undefined) {
|
||||||
this.details = opts.details;
|
this.details = opts.details;
|
||||||
+52
-52
@@ -4,52 +4,52 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { checkpointUltragoal } from "../src/checkpoint.js";
|
import { checkpointUlwLoop } from "../src/checkpoint.js";
|
||||||
import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
||||||
import { ultragoalBriefPath, ultragoalDir, ultragoalLedgerPath } from "../src/paths.js";
|
import { ulwLoopBriefPath, ulwLoopDir, ulwLoopLedgerPath } from "../src/paths.js";
|
||||||
import { writePlan } from "../src/plan-io.js";
|
import { writePlan } from "../src/plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
const QUALITY_GATE_PATH = join(process.cwd(), "test", "fixtures", "sample-quality-gate.json");
|
const QUALITY_GATE_PATH = join(process.cwd(), "test", "fixtures", "sample-quality-gate.json");
|
||||||
|
|
||||||
function criterion(id: string, status: UltragoalSuccessCriterion["status"]): UltragoalSuccessCriterion {
|
function criterion(id: string, status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion {
|
||||||
return { id, scenario: `${id} scenario`, userModel: "happy", expectedEvidence: `${id} proof`, capturedEvidence: status === "pass" ? `${id} passed` : null, status };
|
return { id, scenario: `${id} scenario`, userModel: "happy", expectedEvidence: `${id} proof`, capturedEvidence: status === "pass" ? `${id} passed` : null, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
function goal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function goal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return { id: "G001", title: "Build auth", objective: "Implement JWT auth endpoint", status: "in_progress", successCriteria: [criterion("C001", "pass"), criterion("C002", "pass"), criterion("C003", "pass")], attempt: 1, createdAt: NOW, updatedAt: NOW, ...overrides };
|
return { id: "G001", title: "Build auth", objective: "Implement JWT auth endpoint", status: "in_progress", successCriteria: [criterion("C001", "pass"), criterion("C002", "pass"), criterion("C003", "pass")], attempt: 1, createdAt: NOW, updatedAt: NOW, ...overrides };
|
||||||
}
|
}
|
||||||
|
|
||||||
function plan(goals: UltragoalItem[], overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function plan(goals: UlwLoopItem[], overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
const result: UltragoalPlan = { version: 1, createdAt: NOW, updatedAt: NOW, briefPath: ".omo/ultragoal/brief.md", goalsPath: ".omo/ultragoal/goals.json", ledgerPath: ".omo/ultragoal/ledger.jsonl", codexGoalMode: "aggregate", codexObjective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, goals };
|
const result: UlwLoopPlan = { version: 1, createdAt: NOW, updatedAt: NOW, briefPath: ".omo/ulw-loop/brief.md", goalsPath: ".omo/ulw-loop/goals.json", ledgerPath: ".omo/ulw-loop/ledger.jsonl", codexGoalMode: "aggregate", codexObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, goals };
|
||||||
Object.assign(result, overrides);
|
Object.assign(result, overrides);
|
||||||
const activeGoalId = goals.find((candidate) => candidate.status === "in_progress")?.id;
|
const activeGoalId = goals.find((candidate) => candidate.status === "in_progress")?.id;
|
||||||
if (result.activeGoalId === undefined && activeGoalId !== undefined) result.activeGoalId = activeGoalId;
|
if (result.activeGoalId === undefined && activeGoalId !== undefined) result.activeGoalId = activeGoalId;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function samplePlan(overrides: Partial<UltragoalPlan> = {}): Promise<UltragoalPlan> {
|
async function samplePlan(overrides: Partial<UlwLoopPlan> = {}): Promise<UlwLoopPlan> {
|
||||||
const fixture: UltragoalPlan = JSON.parse(await readFile(new URL("./fixtures/sample-plan.json", import.meta.url), "utf8"));
|
const fixture: UlwLoopPlan = JSON.parse(await readFile(new URL("./fixtures/sample-plan.json", import.meta.url), "utf8"));
|
||||||
return plan(fixture.goals.map((item, index) => goal({ ...item, attempt: index + 1, createdAt: NOW, updatedAt: NOW })), overrides);
|
return plan(fixture.goals.map((item, index) => goal({ ...item, attempt: index + 1, createdAt: NOW, updatedAt: NOW })), overrides);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function repoWith(seed: UltragoalPlan): Promise<string> {
|
async function repoWith(seed: UlwLoopPlan): Promise<string> {
|
||||||
const repo = await mkdtemp(join(tmpdir(), "ug-checkpoint-"));
|
const repo = await mkdtemp(join(tmpdir(), "ug-checkpoint-"));
|
||||||
await mkdir(ultragoalDir(repo), { recursive: true });
|
await mkdir(ulwLoopDir(repo), { recursive: true });
|
||||||
await writePlan(repo, seed);
|
await writePlan(repo, seed);
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
function snapshot(status: "active" | "complete", objective = ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE): string {
|
function snapshot(status: "active" | "complete", objective = ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE): string {
|
||||||
return JSON.stringify({ goal: { objective, status } });
|
return JSON.stringify({ goal: { objective, status } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function lastLedger(repo: string): Promise<UltragoalLedgerEntry> {
|
async function lastLedger(repo: string): Promise<UlwLoopLedgerEntry> {
|
||||||
const last = (await readFile(ultragoalLedgerPath(repo), "utf8")).trim().split(/\r?\n/).at(-1);
|
const last = (await readFile(ulwLoopLedgerPath(repo), "utf8")).trim().split(/\r?\n/).at(-1);
|
||||||
if (last === undefined) throw new Error("expected ledger entry");
|
if (last === undefined) throw new Error("expected ledger entry");
|
||||||
const entry: UltragoalLedgerEntry = JSON.parse(last);
|
const entry: UlwLoopLedgerEntry = JSON.parse(last);
|
||||||
return entry;
|
return entry;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,80 +57,80 @@ async function expectCode(action: () => Promise<unknown>, code: string): Promise
|
|||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(UltragoalError);
|
expect(error).toBeInstanceOf(UlwLoopError);
|
||||||
if (!(error instanceof UltragoalError)) throw error;
|
if (!(error instanceof UlwLoopError)) throw error;
|
||||||
expect(error.code).toBe(code);
|
expect(error.code).toBe(code);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("Expected UltragoalError");
|
throw new Error("Expected UlwLoopError");
|
||||||
}
|
}
|
||||||
|
|
||||||
function passGoal(id: string, overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function passGoal(id: string, overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return goal({ id, successCriteria: [criterion("C001", "pass"), criterion("C002", "pass"), criterion("C003", "pass")], ...overrides });
|
return goal({ id, successCriteria: [criterion("C001", "pass"), criterion("C002", "pass"), criterion("C003", "pass")], ...overrides });
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("checkpointUltragoal status=complete criteria gate", () => {
|
describe("checkpointUlwLoop status=complete criteria gate", () => {
|
||||||
it("THROWS ultragoal_criteria_not_all_pass when any criterion is pending", async () => {
|
it("THROWS ulw_loop_criteria_not_all_pass when any criterion is pending", async () => {
|
||||||
const repo = await repoWith(await samplePlan({ goals: [goal({ successCriteria: [criterion("C001", "pass"), criterion("C002", "pending"), criterion("C003", "pass")] })] }));
|
const repo = await repoWith(await samplePlan({ goals: [goal({ successCriteria: [criterion("C001", "pass"), criterion("C002", "pending"), criterion("C003", "pass")] })] }));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "done" }), "ultragoal_criteria_not_all_pass");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "done" }), "ulw_loop_criteria_not_all_pass");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("THROWS when any criterion is fail or blocked", async () => {
|
it("THROWS when any criterion is fail or blocked", async () => {
|
||||||
for (const status of ["fail", "blocked"] satisfies UltragoalSuccessCriterion["status"][]) {
|
for (const status of ["fail", "blocked"] satisfies UlwLoopSuccessCriterion["status"][]) {
|
||||||
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pass"), criterion("C002", status), criterion("C003", "pass")] })]));
|
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pass"), criterion("C002", status), criterion("C003", "pass")] })]));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "done" }), "ultragoal_criteria_not_all_pass");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "done" }), "ulw_loop_criteria_not_all_pass");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("THROWS when criteria list is empty", async () => {
|
it("THROWS when criteria list is empty", async () => {
|
||||||
const repo = await repoWith(plan([goal({ successCriteria: [] }), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([goal({ successCriteria: [] }), goal({ id: "G002", status: "pending" })]));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "done", codexGoalJson: snapshot("active") }), "ultragoal_criteria_not_all_pass");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "done", codexGoalJson: snapshot("active") }), "ulw_loop_criteria_not_all_pass");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ACCEPTS complete when ALL criteria pass (with valid snapshot)", async () => {
|
it("ACCEPTS complete when ALL criteria pass (with valid snapshot)", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "implementation done and tests passed", codexGoalJson: snapshot("active") });
|
const result = await checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "implementation done and tests passed", codexGoalJson: snapshot("active") });
|
||||||
expect(result.goal.status).toBe("complete");
|
expect(result.goal.status).toBe("complete");
|
||||||
expect((await lastLedger(repo)).kind).toBe("goal_completed");
|
expect((await lastLedger(repo)).kind).toBe("goal_completed");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkpointUltragoal reconciliation (status=complete)", () => {
|
describe("checkpointUlwLoop reconciliation (status=complete)", () => {
|
||||||
it("succeeds when snapshot objective matches expected (aggregate active)", async () => {
|
it("succeeds when snapshot objective matches expected (aggregate active)", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
||||||
await expect(checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("active") })).resolves.toMatchObject({ goal: { status: "complete" } });
|
await expect(checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("active") })).resolves.toMatchObject({ goal: { status: "complete" } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on mismatched objective", async () => {
|
it("throws on mismatched objective", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("active", "wrong objective") }), "ultragoal_codex_snapshot_mismatch");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("active", "wrong objective") }), "ulw_loop_codex_snapshot_mismatch");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws on mismatched status (snapshot complete when expected active)", async () => {
|
it("throws on mismatched status (snapshot complete when expected active)", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("complete") }), "ultragoal_codex_snapshot_mismatch");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "work complete and validation passed", codexGoalJson: snapshot("complete") }), "ulw_loop_codex_snapshot_mismatch");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkpointUltragoal final story", () => {
|
describe("checkpointUlwLoop final story", () => {
|
||||||
it("requires quality-gate-json for the final goal complete", async () => {
|
it("requires quality-gate-json for the final goal complete", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }));
|
const repo = await repoWith(plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }));
|
||||||
await expectCode(() => checkpointUltragoal(repo, { goalId: "G002", status: "complete", evidence: "final work complete and validation passed", codexGoalJson: snapshot("complete") }), "ULTRAGOAL_QUALITY_GATE_INVALID");
|
await expectCode(() => checkpointUlwLoop(repo, { goalId: "G002", status: "complete", evidence: "final work complete and validation passed", codexGoalJson: snapshot("complete") }), "ULW_LOOP_QUALITY_GATE_INVALID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts final story when quality gate JSON includes valid criteriaCoverage", async () => {
|
it("accepts final story when quality gate JSON includes valid criteriaCoverage", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }));
|
const repo = await repoWith(plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G002", status: "complete", evidence: "final work complete and validation passed", codexGoalJson: snapshot("complete"), qualityGateJson: QUALITY_GATE_PATH });
|
const result = await checkpointUlwLoop(repo, { goalId: "G002", status: "complete", evidence: "final work complete and validation passed", codexGoalJson: snapshot("complete"), qualityGateJson: QUALITY_GATE_PATH });
|
||||||
expect(result.aggregateCompletion?.status).toBe("complete");
|
expect(result.aggregateCompletion?.status).toBe("complete");
|
||||||
expect(result.plan.aggregateCompletion?.status).toBe("complete");
|
expect(result.plan.aggregateCompletion?.status).toBe("complete");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("ACCEPTS complete when task-scoped completed Codex objective maps to the ultragoal brief", async () => {
|
it("ACCEPTS complete when task-scoped completed Codex objective maps to the ulw-loop brief", async () => {
|
||||||
const taskObjective = "Fix ultragoal objective mismatch and install local ulw";
|
const taskObjective = "Fix ulw-loop objective mismatch and install local ulw";
|
||||||
const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
|
const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
|
||||||
await writeFile(ultragoalBriefPath(repo), `${taskObjective}\n`, "utf8");
|
await writeFile(ulwLoopBriefPath(repo), `${taskObjective}\n`, "utf8");
|
||||||
|
|
||||||
const result = await checkpointUltragoal(repo, {
|
const result = await checkpointUlwLoop(repo, {
|
||||||
goalId: "G001",
|
goalId: "G001",
|
||||||
status: "complete",
|
status: "complete",
|
||||||
evidence: "final implementation complete and quality gate passed",
|
evidence: "final implementation complete and quality gate passed",
|
||||||
@@ -144,10 +144,10 @@ describe("checkpointUltragoal final story", () => {
|
|||||||
|
|
||||||
it("explains final task-scoped objective mapping when completed Codex objective is unrelated", async () => {
|
it("explains final task-scoped objective mapping when completed Codex objective is unrelated", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
|
const repo = await repoWith(plan([passGoal("G001")], { activeGoalId: "G001" }));
|
||||||
await writeFile(ultragoalBriefPath(repo), "Fix ultragoal objective mismatch and install local ulw\n", "utf8");
|
await writeFile(ulwLoopBriefPath(repo), "Fix ulw-loop objective mismatch and install local ulw\n", "utf8");
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
checkpointUltragoal(repo, {
|
checkpointUlwLoop(repo, {
|
||||||
goalId: "G001",
|
goalId: "G001",
|
||||||
status: "complete",
|
status: "complete",
|
||||||
evidence: "final implementation complete and quality gate passed",
|
evidence: "final implementation complete and quality gate passed",
|
||||||
@@ -158,10 +158,10 @@ describe("checkpointUltragoal final story", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkpointUltragoal status=failed", () => {
|
describe("checkpointUlwLoop status=failed", () => {
|
||||||
it("sets goal.status=failed, goal.failedAt, appends ledger", async () => {
|
it("sets goal.status=failed, goal.failedAt, appends ledger", async () => {
|
||||||
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G001", status: "failed", evidence: "tests failed" });
|
const result = await checkpointUlwLoop(repo, { goalId: "G001", status: "failed", evidence: "tests failed" });
|
||||||
expect(result.goal.status).toBe("failed");
|
expect(result.goal.status).toBe("failed");
|
||||||
expect(result.goal.failedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/u);
|
expect(result.goal.failedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/u);
|
||||||
expect((await lastLedger(repo)).kind).toBe("goal_failed");
|
expect((await lastLedger(repo)).kind).toBe("goal_failed");
|
||||||
@@ -169,27 +169,27 @@ describe("checkpointUltragoal status=failed", () => {
|
|||||||
|
|
||||||
it("classifies external authorization blocker signatures", async () => {
|
it("classifies external authorization blocker signatures", async () => {
|
||||||
const repo = await repoWith(plan([goal()]));
|
const repo = await repoWith(plan([goal()]));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G001", status: "failed", evidence: "ghcr.io returned 401 authentication required because token missing" });
|
const result = await checkpointUlwLoop(repo, { goalId: "G001", status: "failed", evidence: "ghcr.io returned 401 authentication required because token missing" });
|
||||||
expect(result.goal.blockerSignature).toBe("GHCR_PULL_ACCESS:HTTP_401_ANONYMOUS:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED");
|
expect(result.goal.blockerSignature).toBe("GHCR_PULL_ACCESS:HTTP_401_ANONYMOUS:GHCR_VISIBILITY_OR_CREDENTIAL_REQUIRED");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("after 3 same-signature blockers, marks needs_user_decision + nonRetriable", async () => {
|
it("after 3 same-signature blockers, marks needs_user_decision + nonRetriable", async () => {
|
||||||
const repo = await repoWith(plan([goal({ id: "G001", status: "failed", blockerSignature: "EXTERNAL_AUTHORIZATION_REQUIRED" }), goal({ id: "G002", status: "blocked", blockerSignature: "EXTERNAL_AUTHORIZATION_REQUIRED" }), goal({ id: "G003" })], { activeGoalId: "G003" }));
|
const repo = await repoWith(plan([goal({ id: "G001", status: "failed", blockerSignature: "EXTERNAL_AUTHORIZATION_REQUIRED" }), goal({ id: "G002", status: "blocked", blockerSignature: "EXTERNAL_AUTHORIZATION_REQUIRED" }), goal({ id: "G003" })], { activeGoalId: "G003" }));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G003", status: "failed", evidence: "Registry returned 401 because credentials are missing" });
|
const result = await checkpointUlwLoop(repo, { goalId: "G003", status: "failed", evidence: "Registry returned 401 because credentials are missing" });
|
||||||
expect(result.goal.status).toBe("needs_user_decision");
|
expect(result.goal.status).toBe("needs_user_decision");
|
||||||
expect(result.goal.nonRetriable).toBe(true);
|
expect(result.goal.nonRetriable).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("skips the criteria gate for failed status", async () => {
|
it("skips the criteria gate for failed status", async () => {
|
||||||
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
||||||
await expect(checkpointUltragoal(repo, { goalId: "G001", status: "failed", evidence: "not done" })).resolves.toMatchObject({ goal: { status: "failed" } });
|
await expect(checkpointUlwLoop(repo, { goalId: "G001", status: "failed", evidence: "not done" })).resolves.toMatchObject({ goal: { status: "failed" } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkpointUltragoal status=blocked", () => {
|
describe("checkpointUlwLoop status=blocked", () => {
|
||||||
it("preserves blocker fields + appends ledger", async () => {
|
it("preserves blocker fields + appends ledger", async () => {
|
||||||
const repo = await repoWith(plan([goal()]));
|
const repo = await repoWith(plan([goal()]));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G001", status: "blocked", evidence: "ghcr.io requires token and credentials are missing" });
|
const result = await checkpointUlwLoop(repo, { goalId: "G001", status: "blocked", evidence: "ghcr.io requires token and credentials are missing" });
|
||||||
expect(result.goal.status).toBe("blocked");
|
expect(result.goal.status).toBe("blocked");
|
||||||
expect(result.goal.blockedReason).toContain("ghcr.io");
|
expect(result.goal.blockedReason).toContain("ghcr.io");
|
||||||
expect(result.goal.blockerSignature).toContain("GHCR_PULL_ACCESS");
|
expect(result.goal.blockerSignature).toContain("GHCR_PULL_ACCESS");
|
||||||
@@ -198,16 +198,16 @@ describe("checkpointUltragoal status=blocked", () => {
|
|||||||
|
|
||||||
it("skips the criteria gate for blocked status", async () => {
|
it("skips the criteria gate for blocked status", async () => {
|
||||||
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
const repo = await repoWith(plan([goal({ successCriteria: [criterion("C001", "pending")] })]));
|
||||||
await expect(checkpointUltragoal(repo, { goalId: "G001", status: "blocked", evidence: "waiting for approval" })).resolves.toMatchObject({ goal: { status: "blocked" } });
|
await expect(checkpointUlwLoop(repo, { goalId: "G001", status: "blocked", evidence: "waiting for approval" })).resolves.toMatchObject({ goal: { status: "blocked" } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkpointUltragoal rebrand", () => {
|
describe("checkpointUlwLoop rebrand", () => {
|
||||||
it("does not emit legacy brand token in any returned text or ledger payload", async () => {
|
it("does not emit legacy brand token in any returned text or ledger payload", async () => {
|
||||||
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
const repo = await repoWith(plan([passGoal("G001"), goal({ id: "G002", status: "pending" })]));
|
||||||
const result = await checkpointUltragoal(repo, { goalId: "G001", status: "complete", evidence: "implementation done in .omo/ultragoal/goals.json for G001 and validation passed", codexGoalJson: snapshot("active") });
|
const result = await checkpointUlwLoop(repo, { goalId: "G001", status: "complete", evidence: "implementation done in .omo/ulw-loop/goals.json for G001 and validation passed", codexGoalJson: snapshot("active") });
|
||||||
const forbidden = ["o", "m", "x"].join("");
|
const forbidden = ["o", "m", "x"].join("");
|
||||||
const payload = `${JSON.stringify(result)}\n${await readFile(ultragoalLedgerPath(repo), "utf8")}`.toLowerCase();
|
const payload = `${JSON.stringify(result)}\n${await readFile(ulwLoopLedgerPath(repo), "utf8")}`.toLowerCase();
|
||||||
expect(payload).not.toContain(forbidden);
|
expect(payload).not.toContain(forbidden);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+39
-39
@@ -3,8 +3,8 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { ultragoalCommand } from "../src/cli-commands.ts";
|
import { ulwLoopCommand } from "../src/cli-commands.ts";
|
||||||
import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
||||||
|
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
let out: string[];
|
let out: string[];
|
||||||
@@ -38,12 +38,12 @@ function stdoutJson(): Record<string, unknown> {
|
|||||||
return JSON.parse(out.join(""));
|
return JSON.parse(out.join(""));
|
||||||
}
|
}
|
||||||
function codexSnapshot(status: "active" | "complete" = "active"): string {
|
function codexSnapshot(status: "active" | "complete" = "active"): string {
|
||||||
return JSON.stringify({ goal: { objective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, status } });
|
return JSON.stringify({ goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status } });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createPlan(brief = "- Goal A\n- Goal B"): Promise<Record<string, unknown>> {
|
async function createPlan(brief = "- Goal A\n- Goal B"): Promise<Record<string, unknown>> {
|
||||||
resetOutput();
|
resetOutput();
|
||||||
expect(await ultragoalCommand(["create-goals", "--brief", brief, "--json"])).toBe(0);
|
expect(await ulwLoopCommand(["create-goals", "--brief", brief, "--json"])).toBe(0);
|
||||||
const parsed = stdoutJson();
|
const parsed = stdoutJson();
|
||||||
resetOutput();
|
resetOutput();
|
||||||
return parsed;
|
return parsed;
|
||||||
@@ -51,7 +51,7 @@ async function createPlan(brief = "- Goal A\n- Goal B"): Promise<Record<string,
|
|||||||
|
|
||||||
async function passCriterion(goalId: string, criterionId: string): Promise<void> {
|
async function passCriterion(goalId: string, criterionId: string): Promise<void> {
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"record-evidence",
|
"record-evidence",
|
||||||
"--goal-id",
|
"--goal-id",
|
||||||
goalId,
|
goalId,
|
||||||
@@ -66,41 +66,41 @@ async function passCriterion(goalId: string, criterionId: string): Promise<void>
|
|||||||
resetOutput();
|
resetOutput();
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("ultragoalCommand help", () => {
|
describe("ulwLoopCommand help", () => {
|
||||||
it("prints usage when no subcommand", async () => {
|
it("prints usage when no subcommand", async () => {
|
||||||
expect(await ultragoalCommand([])).toBe(0);
|
expect(await ulwLoopCommand([])).toBe(0);
|
||||||
expect(out.join("")).toContain("omo ultragoal");
|
expect(out.join("")).toContain("omo ulw-loop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand create-goals", () => {
|
describe("ulwLoopCommand create-goals", () => {
|
||||||
it("creates plan + writes 3 artifacts + seeds criteria per goal", async () => {
|
it("creates plan + writes 3 artifacts + seeds criteria per goal", async () => {
|
||||||
const code = await ultragoalCommand(["create-goals", "--brief", "- Goal A\n- Goal B", "--json"]);
|
const code = await ulwLoopCommand(["create-goals", "--brief", "- Goal A\n- Goal B", "--json"]);
|
||||||
|
|
||||||
expect(code).toBe(0);
|
expect(code).toBe(0);
|
||||||
const parsed = stdoutJson();
|
const parsed = stdoutJson();
|
||||||
expect(parsed).toMatchObject({ ok: true });
|
expect(parsed).toMatchObject({ ok: true });
|
||||||
expect(parsed).toHaveProperty("plan.goals.0.successCriteria.0.id", "C001");
|
expect(parsed).toHaveProperty("plan.goals.0.successCriteria.0.id", "C001");
|
||||||
expect(await readFile(join(testDir, ".omo/ultragoal/brief.md"), "utf8")).toContain("Goal A");
|
expect(await readFile(join(testDir, ".omo/ulw-loop/brief.md"), "utf8")).toContain("Goal A");
|
||||||
expect(await readFile(join(testDir, ".omo/ultragoal/goals.json"), "utf8")).toContain("successCriteria");
|
expect(await readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).toContain("successCriteria");
|
||||||
expect(await readFile(join(testDir, ".omo/ultragoal/ledger.jsonl"), "utf8")).toContain("plan_created");
|
expect(await readFile(join(testDir, ".omo/ulw-loop/ledger.jsonl"), "utf8")).toContain("plan_created");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand status", () => {
|
describe("ulwLoopCommand status", () => {
|
||||||
it("prints plan summary including criteria counts", async () => {
|
it("prints plan summary including criteria counts", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(await ultragoalCommand(["status"])).toBe(0);
|
expect(await ulwLoopCommand(["status"])).toBe(0);
|
||||||
expect(out.join("")).toContain("criteria: 0/6 pass");
|
expect(out.join("")).toContain("criteria: 0/6 pass");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand complete-goals", () => {
|
describe("ulwLoopCommand complete-goals", () => {
|
||||||
it("starts the next goal and returns a Codex instruction", async () => {
|
it("starts the next goal and returns a Codex instruction", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(await ultragoalCommand(["complete-goals", "--json"])).toBe(0);
|
expect(await ulwLoopCommand(["complete-goals", "--json"])).toBe(0);
|
||||||
expect(stdoutJson()).toMatchObject({
|
expect(stdoutJson()).toMatchObject({
|
||||||
ok: true,
|
ok: true,
|
||||||
goal: { status: "in_progress" },
|
goal: { status: "in_progress" },
|
||||||
@@ -109,12 +109,12 @@ describe("ultragoalCommand complete-goals", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand record-evidence", () => {
|
describe("ulwLoopCommand record-evidence", () => {
|
||||||
it("records evidence + returns updated criterion", async () => {
|
it("records evidence + returns updated criterion", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"record-evidence",
|
"record-evidence",
|
||||||
"--goal-id",
|
"--goal-id",
|
||||||
"G001-goal-a",
|
"G001-goal-a",
|
||||||
@@ -137,7 +137,7 @@ describe("ultragoalCommand record-evidence", () => {
|
|||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"record-evidence",
|
"record-evidence",
|
||||||
"--goal-id",
|
"--goal-id",
|
||||||
"G404",
|
"G404",
|
||||||
@@ -149,22 +149,22 @@ describe("ultragoalCommand record-evidence", () => {
|
|||||||
"x",
|
"x",
|
||||||
]),
|
]),
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(err.join("")).toContain("[ultragoal]");
|
expect(err.join("")).toContain("[ulw-loop]");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns 1 + error on missing flags", async () => {
|
it("returns 1 + error on missing flags", async () => {
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand(["record-evidence", "--criterion-id", "C001", "--status", "pass", "--evidence", "x"]),
|
await ulwLoopCommand(["record-evidence", "--criterion-id", "C001", "--status", "pass", "--evidence", "x"]),
|
||||||
).toBe(1);
|
).toBe(1);
|
||||||
expect(err.join("")).toContain("Missing --goal-id");
|
expect(err.join("")).toContain("Missing --goal-id");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand criteria", () => {
|
describe("ulwLoopCommand criteria", () => {
|
||||||
it("lists criteria for a goal", async () => {
|
it("lists criteria for a goal", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(await ultragoalCommand(["criteria", "--goal-id", "G001-goal-a"])).toBe(0);
|
expect(await ulwLoopCommand(["criteria", "--goal-id", "G001-goal-a"])).toBe(0);
|
||||||
expect(out.join("")).toContain("C001");
|
expect(out.join("")).toContain("C001");
|
||||||
expect(out.join("")).toContain("happy");
|
expect(out.join("")).toContain("happy");
|
||||||
});
|
});
|
||||||
@@ -172,18 +172,18 @@ describe("ultragoalCommand criteria", () => {
|
|||||||
it("supports --json output", async () => {
|
it("supports --json output", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(await ultragoalCommand(["criteria", "--goal-id", "G001-goal-a", "--json"])).toBe(0);
|
expect(await ulwLoopCommand(["criteria", "--goal-id", "G001-goal-a", "--json"])).toBe(0);
|
||||||
expect(stdoutJson()).toMatchObject({ ok: true, goalId: "G001-goal-a" });
|
expect(stdoutJson()).toMatchObject({ ok: true, goalId: "G001-goal-a" });
|
||||||
expect(stdoutJson()).toHaveProperty("criteria.0.id", "C001");
|
expect(stdoutJson()).toHaveProperty("criteria.0.id", "C001");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand checkpoint", () => {
|
describe("ulwLoopCommand checkpoint", () => {
|
||||||
it("REJECTS status=complete when criteria pending", async () => {
|
it("REJECTS status=complete when criteria pending", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"checkpoint",
|
"checkpoint",
|
||||||
"--goal-id",
|
"--goal-id",
|
||||||
"G001-goal-a",
|
"G001-goal-a",
|
||||||
@@ -205,7 +205,7 @@ describe("ultragoalCommand checkpoint", () => {
|
|||||||
await passCriterion("G001-goal-a", "C003");
|
await passCriterion("G001-goal-a", "C003");
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"checkpoint",
|
"checkpoint",
|
||||||
"--goal-id",
|
"--goal-id",
|
||||||
"G001-goal-a",
|
"G001-goal-a",
|
||||||
@@ -222,12 +222,12 @@ describe("ultragoalCommand checkpoint", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand steer", () => {
|
describe("ulwLoopCommand steer", () => {
|
||||||
it("dispatches to the steering engine", async () => {
|
it("dispatches to the steering engine", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await ultragoalCommand([
|
await ulwLoopCommand([
|
||||||
"steer",
|
"steer",
|
||||||
"--kind",
|
"--kind",
|
||||||
"add_subgoal",
|
"add_subgoal",
|
||||||
@@ -250,25 +250,25 @@ describe("ultragoalCommand steer", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand add-goal", () => {
|
describe("ulwLoopCommand add-goal", () => {
|
||||||
it("appends a pending goal", async () => {
|
it("appends a pending goal", async () => {
|
||||||
await createPlan();
|
await createPlan();
|
||||||
|
|
||||||
expect(await ultragoalCommand(["add-goal", "--title", "Later", "--objective", "Do later", "--json"])).toBe(0);
|
expect(await ulwLoopCommand(["add-goal", "--title", "Later", "--objective", "Do later", "--json"])).toBe(0);
|
||||||
expect(stdoutJson()).toMatchObject({ ok: true, goal: { title: "Later", status: "pending" } });
|
expect(stdoutJson()).toMatchObject({ ok: true, goal: { title: "Later", status: "pending" } });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand unknown", () => {
|
describe("ulwLoopCommand unknown", () => {
|
||||||
it("returns 1 + prints help on unknown subcommand", async () => {
|
it("returns 1 + prints help on unknown subcommand", async () => {
|
||||||
expect(await ultragoalCommand(["wat"])).toBe(1);
|
expect(await ulwLoopCommand(["wat"])).toBe(1);
|
||||||
expect(out.join("")).toContain("omo ultragoal");
|
expect(out.join("")).toContain("omo ulw-loop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ultragoalCommand error handling", () => {
|
describe("ulwLoopCommand error handling", () => {
|
||||||
it("returns 1 + prints [ultragoal] prefix on UltragoalError", async () => {
|
it("returns 1 + prints [ulw-loop] prefix on UlwLoopError", async () => {
|
||||||
expect(await ultragoalCommand(["status"])).toBe(1);
|
expect(await ulwLoopCommand(["status"])).toBe(1);
|
||||||
expect(err.join("")).toContain("[ultragoal]");
|
expect(err.join("")).toContain("[ulw-loop]");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+26
-26
@@ -13,13 +13,13 @@ import {
|
|||||||
readRepeated,
|
readRepeated,
|
||||||
readValue,
|
readValue,
|
||||||
} from "../src/cli-arg-parser.js";
|
} from "../src/cli-arg-parser.js";
|
||||||
import { normalizeCodexGoalMode, printStatus, ULTRAGOAL_HELP } from "../src/cli-output.js";
|
import { normalizeCodexGoalMode, printStatus, ULW_LOOP_HELP } from "../src/cli-output.js";
|
||||||
import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
function criterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function criterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path returns 200",
|
scenario: "happy path returns 200",
|
||||||
@@ -31,7 +31,7 @@ function criterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultragoa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function goal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function goal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Auth endpoint",
|
title: "Auth endpoint",
|
||||||
@@ -49,14 +49,14 @@ function goal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function plan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function plan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
activeGoalId: "G001",
|
activeGoalId: "G001",
|
||||||
goals: [goal()],
|
goals: [goal()],
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -159,7 +159,7 @@ describe("parseRecordEvidenceArgs", () => {
|
|||||||
it("throws when goal-id missing", () => {
|
it("throws when goal-id missing", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
parseRecordEvidenceArgs(["record-evidence", "--criterion-id", "C001", "--status", "pass", "--evidence", "x"]),
|
parseRecordEvidenceArgs(["record-evidence", "--criterion-id", "C001", "--status", "pass", "--evidence", "x"]),
|
||||||
).toThrow(UltragoalError);
|
).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when status is not pass|fail|blocked", () => {
|
it("throws when status is not pass|fail|blocked", () => {
|
||||||
@@ -175,7 +175,7 @@ describe("parseRecordEvidenceArgs", () => {
|
|||||||
"--evidence",
|
"--evidence",
|
||||||
"x",
|
"x",
|
||||||
]),
|
]),
|
||||||
).toThrow(UltragoalError);
|
).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("includes optional --notes when present", () => {
|
it("includes optional --notes when present", () => {
|
||||||
@@ -197,24 +197,24 @@ describe("parseRecordEvidenceArgs", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ULTRAGOAL_HELP", () => {
|
describe("ULW_LOOP_HELP", () => {
|
||||||
it("mentions omo ultragoal + every subcommand", () => {
|
it("mentions omo ulw-loop + every subcommand", () => {
|
||||||
expect(ULTRAGOAL_HELP).toContain("omo ultragoal");
|
expect(ULW_LOOP_HELP).toContain("omo ulw-loop");
|
||||||
expect(ULTRAGOAL_HELP).toContain("create-goals");
|
expect(ULW_LOOP_HELP).toContain("create-goals");
|
||||||
expect(ULTRAGOAL_HELP).toContain("complete-goals");
|
expect(ULW_LOOP_HELP).toContain("complete-goals");
|
||||||
expect(ULTRAGOAL_HELP).toContain("status");
|
expect(ULW_LOOP_HELP).toContain("status");
|
||||||
expect(ULTRAGOAL_HELP).toContain("checkpoint");
|
expect(ULW_LOOP_HELP).toContain("checkpoint");
|
||||||
expect(ULTRAGOAL_HELP).toContain("steer");
|
expect(ULW_LOOP_HELP).toContain("steer");
|
||||||
expect(ULTRAGOAL_HELP).toContain("record-evidence");
|
expect(ULW_LOOP_HELP).toContain("record-evidence");
|
||||||
expect(ULTRAGOAL_HELP).toContain("criteria");
|
expect(ULW_LOOP_HELP).toContain("criteria");
|
||||||
expect(ULTRAGOAL_HELP).toContain("add-goal");
|
expect(ULW_LOOP_HELP).toContain("add-goal");
|
||||||
expect(ULTRAGOAL_HELP).toContain("record-review-blockers");
|
expect(ULW_LOOP_HELP).toContain("record-review-blockers");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never mentions the legacy typo", () => {
|
it("never mentions the legacy typo", () => {
|
||||||
const typo = ["o", "m", "x"].join("");
|
const typo = ["o", "m", "x"].join("");
|
||||||
|
|
||||||
expect(ULTRAGOAL_HELP).not.toMatch(new RegExp(typo, "i"));
|
expect(ULW_LOOP_HELP).not.toMatch(new RegExp(typo, "i"));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -244,7 +244,7 @@ describe("normalizeCodexGoalMode", () => {
|
|||||||
expect(normalizeCodexGoalMode("per_story")).toBe("per_story");
|
expect(normalizeCodexGoalMode("per_story")).toBe("per_story");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when invalid", () => {
|
it("throws UlwLoopError when invalid", () => {
|
||||||
expect(() => normalizeCodexGoalMode("per-story")).toThrow(UltragoalError);
|
expect(() => normalizeCodexGoalMode("per-story")).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+17
-17
@@ -11,24 +11,24 @@ import {
|
|||||||
parseSteeringSource,
|
parseSteeringSource,
|
||||||
printSteerResult,
|
printSteerResult,
|
||||||
} from "../src/cli-steering.js";
|
} from "../src/cli-steering.js";
|
||||||
import type { SteerUltragoalResult, UltragoalPlan } from "../src/types.js";
|
import type { SteerUlwLoopResult, UlwLoopPlan } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
function plan(): UltragoalPlan {
|
function plan(): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals: [],
|
goals: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function steerResult(overrides: Partial<SteerUltragoalResult> = {}): SteerUltragoalResult {
|
function steerResult(overrides: Partial<SteerUlwLoopResult> = {}): SteerUlwLoopResult {
|
||||||
return {
|
return {
|
||||||
plan: plan(),
|
plan: plan(),
|
||||||
accepted: true,
|
accepted: true,
|
||||||
@@ -73,11 +73,11 @@ describe("parseSteeringKind", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("throws when --kind missing", () => {
|
it("throws when --kind missing", () => {
|
||||||
expect(() => parseSteeringKind([])).toThrow(UltragoalError);
|
expect(() => parseSteeringKind([])).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when kind unknown", () => {
|
it("throws when kind unknown", () => {
|
||||||
expect(() => parseSteeringKind(["--kind", "bogus"])).toThrow(UltragoalError);
|
expect(() => parseSteeringKind(["--kind", "bogus"])).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -128,13 +128,13 @@ describe("parseSteeringProposal add_subgoal", () => {
|
|||||||
"--rationale",
|
"--rationale",
|
||||||
"y",
|
"y",
|
||||||
]),
|
]),
|
||||||
).rejects.toThrow(UltragoalError);
|
).rejects.toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when --evidence missing", async () => {
|
it("throws when --evidence missing", async () => {
|
||||||
await expect(
|
await expect(
|
||||||
parseSteeringProposal(["--kind", "add_subgoal", "--title", "New", "--objective", "Build", "--rationale", "y"]),
|
parseSteeringProposal(["--kind", "add_subgoal", "--title", "New", "--objective", "Build", "--rationale", "y"]),
|
||||||
).rejects.toThrow(UltragoalError);
|
).rejects.toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -214,7 +214,7 @@ describe("parseSteeringProposal revise_criterion", () => {
|
|||||||
"--rationale",
|
"--rationale",
|
||||||
"y",
|
"y",
|
||||||
]),
|
]),
|
||||||
).rejects.toThrow(UltragoalError);
|
).rejects.toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when goal-id missing", async () => {
|
it("throws when goal-id missing", async () => {
|
||||||
@@ -231,7 +231,7 @@ describe("parseSteeringProposal revise_criterion", () => {
|
|||||||
"--rationale",
|
"--rationale",
|
||||||
"y",
|
"y",
|
||||||
]),
|
]),
|
||||||
).rejects.toThrow(UltragoalError);
|
).rejects.toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when criterion-id missing", async () => {
|
it("throws when criterion-id missing", async () => {
|
||||||
@@ -248,7 +248,7 @@ describe("parseSteeringProposal revise_criterion", () => {
|
|||||||
"--rationale",
|
"--rationale",
|
||||||
"y",
|
"y",
|
||||||
]),
|
]),
|
||||||
).rejects.toThrow(UltragoalError);
|
).rejects.toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -387,7 +387,7 @@ describe("normalizeSteeringProposal", () => {
|
|||||||
it("rejects empty evidence after trim", () => {
|
it("rejects empty evidence after trim", () => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
normalizeSteeringProposal({ kind: "annotate_ledger", source: "cli", evidence: " ", rationale: "y" }),
|
normalizeSteeringProposal({ kind: "annotate_ledger", source: "cli", evidence: " ", rationale: "y" }),
|
||||||
).toThrow(UltragoalError);
|
).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -401,7 +401,7 @@ describe("printSteerResult", () => {
|
|||||||
it("prints human-readable when json=false", () => {
|
it("prints human-readable when json=false", () => {
|
||||||
const output = captureStdout(() => printSteerResult(steerResult(), false));
|
const output = captureStdout(() => printSteerResult(steerResult(), false));
|
||||||
|
|
||||||
expect(output).toContain("ultragoal steer: accepted add_subgoal");
|
expect(output).toContain("ulw-loop steer: accepted add_subgoal");
|
||||||
expect(output).toContain("ultragoal status");
|
expect(output).toContain("ulw-loop status");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+13
-13
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { buildCodexGoalInstruction } from "../src/codex-goal-instruction.js";
|
import { buildCodexGoalInstruction } from "../src/codex-goal-instruction.js";
|
||||||
import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
||||||
import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path",
|
scenario: "happy path",
|
||||||
@@ -18,7 +18,7 @@ function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Goal one",
|
title: "Goal one",
|
||||||
@@ -32,24 +32,24 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function makePlan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals: [],
|
goals: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("buildCodexGoalInstruction aggregate mode", () => {
|
describe("buildCodexGoalInstruction aggregate mode", () => {
|
||||||
it("references the aggregate handoff and the .omo/ultragoal/goals.json artifact", () => {
|
it("references the aggregate handoff and the .omo/ulw-loop/goals.json artifact", () => {
|
||||||
const { text } = buildCodexGoalInstruction({ plan: makePlan({ codexGoalMode: "aggregate" }), goal: makeGoal() });
|
const { text } = buildCodexGoalInstruction({ plan: makePlan({ codexGoalMode: "aggregate" }), goal: makeGoal() });
|
||||||
expect(text).toContain("aggregate");
|
expect(text).toContain("aggregate");
|
||||||
expect(text).toContain(".omo/ultragoal/goals.json");
|
expect(text).toContain(".omo/ulw-loop/goals.json");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("given aggregate mode when rendering create_goal payload then omits numeric limits", () => {
|
it("given aggregate mode when rendering create_goal payload then omits numeric limits", () => {
|
||||||
@@ -58,7 +58,7 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
|
|||||||
goal: makeGoal(),
|
goal: makeGoal(),
|
||||||
});
|
});
|
||||||
expect(json).toEqual({
|
expect(json).toEqual({
|
||||||
objective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE,
|
objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
||||||
status: "active",
|
status: "active",
|
||||||
});
|
});
|
||||||
expect(text).toContain("objective and status only");
|
expect(text).toContain("objective and status only");
|
||||||
@@ -146,8 +146,8 @@ describe("buildCodexGoalInstruction rebrand audit", () => {
|
|||||||
expect(text).not.toMatch(new RegExp(legacyBrand, "i"));
|
expect(text).not.toMatch(new RegExp(legacyBrand, "i"));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("references .omo/ultragoal in artifact paths", () => {
|
it("references .omo/ulw-loop in artifact paths", () => {
|
||||||
const { text } = buildCodexGoalInstruction({ plan: makePlan({ codexGoalMode: "aggregate" }), goal: makeGoal() });
|
const { text } = buildCodexGoalInstruction({ plan: makePlan({ codexGoalMode: "aggregate" }), goal: makeGoal() });
|
||||||
expect(text).toContain(".omo/ultragoal");
|
expect(text).toContain(".omo/ulw-loop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+1
-1
@@ -92,7 +92,7 @@ describe("readCodexGoalSnapshotInput", () => {
|
|||||||
|
|
||||||
// then
|
// then
|
||||||
expect(snapshot?.available).toBe(true);
|
expect(snapshot?.available).toBe(true);
|
||||||
expect(snapshot?.objective).toBe("Complete the durable ultragoal plan");
|
expect(snapshot?.objective).toBe("Complete the durable ulw-loop plan");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws CodexGoalSnapshotError when input is neither JSON nor a path", async () => {
|
it("throws CodexGoalSnapshotError when input is neither JSON nor a path", async () => {
|
||||||
+35
-35
@@ -6,34 +6,34 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
applyPreToolUseGoalBudgetGuard,
|
applyPreToolUseGoalBudgetGuard,
|
||||||
applyUserPromptUltragoalSteering,
|
applyUserPromptUlwLoopSteering,
|
||||||
type PreToolUsePayload,
|
type PreToolUsePayload,
|
||||||
parseUserPromptSubmitPayload,
|
parseUserPromptSubmitPayload,
|
||||||
runPreToolUseGoalBudgetGuardCli,
|
runPreToolUseGoalBudgetGuardCli,
|
||||||
runUltragoalHookCli,
|
runUlwLoopHookCli,
|
||||||
type UserPromptSubmitPayload,
|
type UserPromptSubmitPayload,
|
||||||
} from "../src/codex-hook.js";
|
} from "../src/codex-hook.js";
|
||||||
import { ultragoalDir } from "../src/paths.js";
|
import { ulwLoopDir } from "../src/paths.js";
|
||||||
import { writePlan } from "../src/plan-io.js";
|
import { writePlan } from "../src/plan-io.js";
|
||||||
import type { UltragoalPlan } from "../src/types.js";
|
import type { UlwLoopPlan } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
async function bootstrapPlanRepo(): Promise<string> {
|
async function bootstrapPlanRepo(): Promise<string> {
|
||||||
const repoRoot = await mkdtemp(join(tmpdir(), "ug-hook-"));
|
const repoRoot = await mkdtemp(join(tmpdir(), "ug-hook-"));
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
await writePlan(repoRoot, samplePlan());
|
await writePlan(repoRoot, samplePlan());
|
||||||
return repoRoot;
|
return repoRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
function samplePlan(): UltragoalPlan {
|
function samplePlan(): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals: [
|
goals: [
|
||||||
{
|
{
|
||||||
id: "G001",
|
id: "G001",
|
||||||
@@ -70,7 +70,7 @@ function preToolPayload(toolName: string, toolInput: unknown): PreToolUsePayload
|
|||||||
|
|
||||||
function payloadWithRuntimeEvent(hookEventName: string): UserPromptSubmitPayload {
|
function payloadWithRuntimeEvent(hookEventName: string): UserPromptSubmitPayload {
|
||||||
const input = payload(
|
const input = payload(
|
||||||
'OMO_ULTRAGOAL_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'OMO_ULW_LOOP_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
"/tmp",
|
"/tmp",
|
||||||
);
|
);
|
||||||
Object.defineProperty(input, "hook_event_name", { value: hookEventName });
|
Object.defineProperty(input, "hook_event_name", { value: hookEventName });
|
||||||
@@ -93,7 +93,7 @@ describe("parseUserPromptSubmitPayload", () => {
|
|||||||
const raw = await readFile("test/fixtures/user-prompt-submit.json", "utf8");
|
const raw = await readFile("test/fixtures/user-prompt-submit.json", "utf8");
|
||||||
const parsed = parseUserPromptSubmitPayload(raw);
|
const parsed = parseUserPromptSubmitPayload(raw);
|
||||||
expect(parsed?.hook_event_name).toBe("UserPromptSubmit");
|
expect(parsed?.hook_event_name).toBe("UserPromptSubmit");
|
||||||
expect(parsed?.prompt).toContain("OMO_ULTRAGOAL_STEER");
|
expect(parsed?.prompt).toContain("OMO_ULW_LOOP_STEER");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for empty input", () => {
|
it("returns null for empty input", () => {
|
||||||
@@ -109,12 +109,12 @@ describe("parseUserPromptSubmitPayload", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("applyUserPromptUltragoalSteering - OMO directive patterns", () => {
|
describe("applyUserPromptUlwLoopSteering - OMO directive patterns", () => {
|
||||||
it("processes OMO_ULTRAGOAL_STEER: prompt and returns audit text on success", async () => {
|
it("processes OMO_ULW_LOOP_STEER: prompt and returns audit text on success", async () => {
|
||||||
const repoRoot = await bootstrapPlanRepo();
|
const repoRoot = await bootstrapPlanRepo();
|
||||||
const out = await applyUserPromptUltragoalSteering(
|
const out = await applyUserPromptUlwLoopSteering(
|
||||||
payload(
|
payload(
|
||||||
'OMO_ULTRAGOAL_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'OMO_ULW_LOOP_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
repoRoot,
|
repoRoot,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -122,22 +122,22 @@ describe("applyUserPromptUltragoalSteering - OMO directive patterns", () => {
|
|||||||
expect(out).toContain("annotate_ledger");
|
expect(out).toContain("annotate_ledger");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes omo.ultragoal.steer: pattern", async () => {
|
it("processes omo.ulw-loop.steer: pattern", async () => {
|
||||||
const repoRoot = await bootstrapPlanRepo();
|
const repoRoot = await bootstrapPlanRepo();
|
||||||
const out = await applyUserPromptUltragoalSteering(
|
const out = await applyUserPromptUlwLoopSteering(
|
||||||
payload(
|
payload(
|
||||||
'omo.ultragoal.steer: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'omo.ulw-loop.steer: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
repoRoot,
|
repoRoot,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
expect(out).toContain("accepted");
|
expect(out).toContain("accepted");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("processes omo ultragoal steer: pattern", async () => {
|
it("processes omo ulw-loop steer: pattern", async () => {
|
||||||
const repoRoot = await bootstrapPlanRepo();
|
const repoRoot = await bootstrapPlanRepo();
|
||||||
const out = await applyUserPromptUltragoalSteering(
|
const out = await applyUserPromptUlwLoopSteering(
|
||||||
payload(
|
payload(
|
||||||
'omo ultragoal steer: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'omo ulw-loop steer: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
repoRoot,
|
repoRoot,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -145,30 +145,30 @@ describe("applyUserPromptUltragoalSteering - OMO directive patterns", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("applyUserPromptUltragoalSteering - non-matching prompts", () => {
|
describe("applyUserPromptUlwLoopSteering - non-matching prompts", () => {
|
||||||
it("returns empty string when no directive in prompt", async () => {
|
it("returns empty string when no directive in prompt", async () => {
|
||||||
expect(await applyUserPromptUltragoalSteering(payload("just a normal user message", "/tmp"))).toBe("");
|
expect(await applyUserPromptUlwLoopSteering(payload("just a normal user message", "/tmp"))).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty for OMX_ULTRAGOAL_STEER (deprecated marker - must reject)", async () => {
|
it("returns empty for OMX_ULW_LOOP_STEER (deprecated marker - must reject)", async () => {
|
||||||
expect(
|
expect(
|
||||||
await applyUserPromptUltragoalSteering(
|
await applyUserPromptUlwLoopSteering(
|
||||||
payload('OMX_ULTRAGOAL_STEER: {"kind":"annotate_ledger","evidence":"x","rationale":"y"}', "/tmp"),
|
payload('OMX_ULW_LOOP_STEER: {"kind":"annotate_ledger","evidence":"x","rationale":"y"}', "/tmp"),
|
||||||
),
|
),
|
||||||
).toBe("");
|
).toBe("");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty when hook_event_name is not UserPromptSubmit", async () => {
|
it("returns empty when hook_event_name is not UserPromptSubmit", async () => {
|
||||||
expect(await applyUserPromptUltragoalSteering(payloadWithRuntimeEvent("PostToolUse"))).toBe("");
|
expect(await applyUserPromptUlwLoopSteering(payloadWithRuntimeEvent("PostToolUse"))).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("applyUserPromptUltragoalSteering - error swallowing", () => {
|
describe("applyUserPromptUlwLoopSteering - error swallowing", () => {
|
||||||
it("returns empty (never throws) when plan does not exist", async () => {
|
it("returns empty (never throws) when plan does not exist", async () => {
|
||||||
const repoRoot = await mkdtemp(join(tmpdir(), "ug-nohook-"));
|
const repoRoot = await mkdtemp(join(tmpdir(), "ug-nohook-"));
|
||||||
const out = await applyUserPromptUltragoalSteering(
|
const out = await applyUserPromptUlwLoopSteering(
|
||||||
payload(
|
payload(
|
||||||
'OMO_ULTRAGOAL_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'OMO_ULW_LOOP_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
repoRoot,
|
repoRoot,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -176,30 +176,30 @@ describe("applyUserPromptUltragoalSteering - error swallowing", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("returns empty when steering proposal is malformed JSON after marker", async () => {
|
it("returns empty when steering proposal is malformed JSON after marker", async () => {
|
||||||
const out = await applyUserPromptUltragoalSteering(payload("OMO_ULTRAGOAL_STEER: {bad", "/tmp"));
|
const out = await applyUserPromptUlwLoopSteering(payload("OMO_ULW_LOOP_STEER: {bad", "/tmp"));
|
||||||
expect(out).toBe("");
|
expect(out).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("runUltragoalHookCli (stdin/stdout integration)", () => {
|
describe("runUlwLoopHookCli (stdin/stdout integration)", () => {
|
||||||
it("reads stdin, applies steering, writes audit to stdout", async () => {
|
it("reads stdin, applies steering, writes audit to stdout", async () => {
|
||||||
const repoRoot = await bootstrapPlanRepo();
|
const repoRoot = await bootstrapPlanRepo();
|
||||||
const stdin = Readable.from([
|
const stdin = Readable.from([
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
payload(
|
payload(
|
||||||
'OMO_ULTRAGOAL_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
'OMO_ULW_LOOP_STEER: {"kind":"annotate_ledger","source":"user_prompt_submit","evidence":"x","rationale":"y"}',
|
||||||
repoRoot,
|
repoRoot,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
const capture = captureStdout();
|
const capture = captureStdout();
|
||||||
await runUltragoalHookCli(stdin, capture.stdout);
|
await runUlwLoopHookCli(stdin, capture.stdout);
|
||||||
expect(capture.read().length).toBeGreaterThan(0);
|
expect(capture.read().length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("writes nothing when stdin is empty", async () => {
|
it("writes nothing when stdin is empty", async () => {
|
||||||
const capture = captureStdout();
|
const capture = captureStdout();
|
||||||
await runUltragoalHookCli(Readable.from([""]), capture.stdout);
|
await runUlwLoopHookCli(Readable.from([""]), capture.stdout);
|
||||||
expect(capture.read()).toBe("");
|
expect(capture.read()).toBe("");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+12
-12
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { requireAllCriteriaPass } from "../src/evidence.js";
|
import { requireAllCriteriaPass } from "../src/evidence.js";
|
||||||
import type { UltragoalItem, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path login returns 200",
|
scenario: "happy path login returns 200",
|
||||||
@@ -18,7 +18,7 @@ function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Auth endpoint",
|
title: "Auth endpoint",
|
||||||
@@ -50,7 +50,7 @@ describe("requireAllCriteriaPass", () => {
|
|||||||
expect(() => requireAllCriteriaPass(goal)).not.toThrow();
|
expect(() => requireAllCriteriaPass(goal)).not.toThrow();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when any criterion pending", () => {
|
it("throws UlwLoopError when any criterion pending", () => {
|
||||||
// given
|
// given
|
||||||
const goal = makeGoal({
|
const goal = makeGoal({
|
||||||
successCriteria: [
|
successCriteria: [
|
||||||
@@ -61,7 +61,7 @@ describe("requireAllCriteriaPass", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// when / then
|
// when / then
|
||||||
expect(() => requireAllCriteriaPass(goal)).toThrow(UltragoalError);
|
expect(() => requireAllCriteriaPass(goal)).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when any fail/blocked too", () => {
|
it("throws when any fail/blocked too", () => {
|
||||||
@@ -70,11 +70,11 @@ describe("requireAllCriteriaPass", () => {
|
|||||||
const goal2 = makeGoal({ successCriteria: [makeCriterion({ id: "C001", status: "blocked" })] });
|
const goal2 = makeGoal({ successCriteria: [makeCriterion({ id: "C001", status: "blocked" })] });
|
||||||
|
|
||||||
// when / then
|
// when / then
|
||||||
expect(() => requireAllCriteriaPass(goal1)).toThrow(UltragoalError);
|
expect(() => requireAllCriteriaPass(goal1)).toThrow(UlwLoopError);
|
||||||
expect(() => requireAllCriteriaPass(goal2)).toThrow(UltragoalError);
|
expect(() => requireAllCriteriaPass(goal2)).toThrow(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("UltragoalError includes details.goalId + details.unresolved", () => {
|
it("UlwLoopError includes details.goalId + details.unresolved", () => {
|
||||||
// given
|
// given
|
||||||
const goal = makeGoal({
|
const goal = makeGoal({
|
||||||
id: "G001",
|
id: "G001",
|
||||||
@@ -90,9 +90,9 @@ describe("requireAllCriteriaPass", () => {
|
|||||||
requireAllCriteriaPass(goal);
|
requireAllCriteriaPass(goal);
|
||||||
expect.fail("expected throw");
|
expect.fail("expected throw");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(UltragoalError);
|
expect(error).toBeInstanceOf(UlwLoopError);
|
||||||
if (!(error instanceof UltragoalError)) throw error;
|
if (!(error instanceof UlwLoopError)) throw error;
|
||||||
expect(error.code).toBe("ultragoal_criteria_not_all_pass");
|
expect(error.code).toBe("ulw_loop_criteria_not_all_pass");
|
||||||
expect(error.details?.["goalId"]).toBe("G001");
|
expect(error.details?.["goalId"]).toBe("G001");
|
||||||
expect(Array.isArray(error.details?.["unresolved"])).toBe(true);
|
expect(Array.isArray(error.details?.["unresolved"])).toBe(true);
|
||||||
}
|
}
|
||||||
+20
-20
@@ -9,34 +9,34 @@ import {
|
|||||||
recordEvidence,
|
recordEvidence,
|
||||||
unresolvedCriteriaOf,
|
unresolvedCriteriaOf,
|
||||||
} from "../src/evidence.js";
|
} from "../src/evidence.js";
|
||||||
import { ultragoalDir } from "../src/paths.js";
|
import { ulwLoopDir } from "../src/paths.js";
|
||||||
import { readUltragoalPlan, writePlan } from "../src/plan-io.js";
|
import { readUlwLoopPlan, writePlan } from "../src/plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
async function bootstrapRepo(plan: UltragoalPlan): Promise<string> {
|
async function bootstrapRepo(plan: UlwLoopPlan): Promise<string> {
|
||||||
const repo = await mkdtemp(join(tmpdir(), "ug-evidence-"));
|
const repo = await mkdtemp(join(tmpdir(), "ug-evidence-"));
|
||||||
await mkdir(ultragoalDir(repo), { recursive: true });
|
await mkdir(ulwLoopDir(repo), { recursive: true });
|
||||||
await writePlan(repo, plan);
|
await writePlan(repo, plan);
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLastLedgerEntry(repo: string): Promise<UltragoalLedgerEntry> {
|
async function readLastLedgerEntry(repo: string): Promise<UlwLoopLedgerEntry> {
|
||||||
const lines = (await readFile(join(repo, ".omo/ultragoal/ledger.jsonl"), "utf8")).trim().split("\n");
|
const lines = (await readFile(join(repo, ".omo/ulw-loop/ledger.jsonl"), "utf8")).trim().split("\n");
|
||||||
const last = lines.at(-1);
|
const last = lines.at(-1);
|
||||||
if (last === undefined) throw new Error("expected ledger entry");
|
if (last === undefined) throw new Error("expected ledger entry");
|
||||||
return JSON.parse(last);
|
return JSON.parse(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
function firstGoal(plan: UltragoalPlan): UltragoalItem {
|
function firstGoal(plan: UlwLoopPlan): UlwLoopItem {
|
||||||
const goal = plan.goals.at(0);
|
const goal = plan.goals.at(0);
|
||||||
if (goal === undefined) throw new Error("expected goal");
|
if (goal === undefined) throw new Error("expected goal");
|
||||||
return goal;
|
return goal;
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path login returns 200",
|
scenario: "happy path login returns 200",
|
||||||
@@ -48,7 +48,7 @@ function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Auth endpoint",
|
title: "Auth endpoint",
|
||||||
@@ -66,16 +66,16 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function makePlan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
codexGoalMode: "aggregate",
|
codexGoalMode: "aggregate",
|
||||||
codexObjective: "Complete the durable ultragoal plan in .omo/ultragoal/goals.json",
|
codexObjective: "Complete the durable ulw-loop plan in .omo/ulw-loop/goals.json",
|
||||||
codexObjectiveAliases: [],
|
codexObjectiveAliases: [],
|
||||||
goals: [makeGoal()],
|
goals: [makeGoal()],
|
||||||
...overrides,
|
...overrides,
|
||||||
@@ -114,7 +114,7 @@ describe("recordEvidence (status=pass)", () => {
|
|||||||
|
|
||||||
await recordEvidence(repo, { goalId: "G001", criterionId: "C001", status: "pass", evidence: "observable proof" });
|
await recordEvidence(repo, { goalId: "G001", criterionId: "C001", status: "pass", evidence: "observable proof" });
|
||||||
|
|
||||||
const criterion = firstGoal(await readUltragoalPlan(repo)).successCriteria.find((c) => c.id === "C001");
|
const criterion = firstGoal(await readUlwLoopPlan(repo)).successCriteria.find((c) => c.id === "C001");
|
||||||
expect(criterion?.status).toBe("pass");
|
expect(criterion?.status).toBe("pass");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -157,7 +157,7 @@ describe("recordEvidence error cases", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
recordEvidence(repo, { goalId: "GUNKNOWN", criterionId: "C001", status: "pass", evidence: "x" }),
|
recordEvidence(repo, { goalId: "GUNKNOWN", criterionId: "C001", status: "pass", evidence: "x" }),
|
||||||
).rejects.toBeInstanceOf(UltragoalError);
|
).rejects.toBeInstanceOf(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when criterionId not found within goal", async () => {
|
it("throws when criterionId not found within goal", async () => {
|
||||||
@@ -165,7 +165,7 @@ describe("recordEvidence error cases", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
recordEvidence(repo, { goalId: "G001", criterionId: "CUNKNOWN", status: "pass", evidence: "x" }),
|
recordEvidence(repo, { goalId: "G001", criterionId: "CUNKNOWN", status: "pass", evidence: "x" }),
|
||||||
).rejects.toBeInstanceOf(UltragoalError);
|
).rejects.toBeInstanceOf(UlwLoopError);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws when evidence is empty/whitespace", async () => {
|
it("throws when evidence is empty/whitespace", async () => {
|
||||||
@@ -173,7 +173,7 @@ describe("recordEvidence error cases", () => {
|
|||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
recordEvidence(repo, { goalId: "G001", criterionId: "C001", status: "pass", evidence: " " }),
|
recordEvidence(repo, { goalId: "G001", criterionId: "C001", status: "pass", evidence: " " }),
|
||||||
).rejects.toBeInstanceOf(UltragoalError);
|
).rejects.toBeInstanceOf(UlwLoopError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
{ "goal": { "objective": "Complete the durable ulw-loop plan", "status": "active" } }
|
||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"version": 1,
|
"version": 1,
|
||||||
"createdAt": "2026-05-23T00:00:00.000Z",
|
"createdAt": "2026-05-23T00:00:00.000Z",
|
||||||
"codexGoalMode": "aggregate",
|
"codexGoalMode": "aggregate",
|
||||||
"codexObjective": "Complete the durable ultragoal plan in .omo/ultragoal/goals.json...",
|
"codexObjective": "Complete the durable ulw-loop plan in .omo/ulw-loop/goals.json...",
|
||||||
"codexObjectiveAliases": [],
|
"codexObjectiveAliases": [],
|
||||||
"goals": [
|
"goals": [
|
||||||
{
|
{
|
||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"hook_event_name": "UserPromptSubmit",
|
"hook_event_name": "UserPromptSubmit",
|
||||||
"model": "gpt-5.5",
|
"model": "gpt-5.5",
|
||||||
"permission_mode": "default",
|
"permission_mode": "default",
|
||||||
"prompt": "OMO_ULTRAGOAL_STEER: {\"kind\":\"annotate_ledger\",\"source\":\"user_prompt_submit\",\"evidence\":\"test note\",\"rationale\":\"testing hook\"}",
|
"prompt": "OMO_ULW_LOOP_STEER: {\"kind\":\"annotate_ledger\",\"source\":\"user_prompt_submit\",\"evidence\":\"test note\",\"rationale\":\"testing hook\"}",
|
||||||
"session_id": "s1",
|
"session_id": "s1",
|
||||||
"transcript_path": "/tmp/transcript.log",
|
"transcript_path": "/tmp/transcript.log",
|
||||||
"turn_id": "t1"
|
"turn_id": "t1"
|
||||||
+21
-21
@@ -8,14 +8,14 @@ import {
|
|||||||
firstUnresolvedCriterion,
|
firstUnresolvedCriterion,
|
||||||
hasAllCriteriaPass,
|
hasAllCriteriaPass,
|
||||||
isFinalRunCompletionCandidate,
|
isFinalRunCompletionCandidate,
|
||||||
isUltragoalDone,
|
isUlwLoopDone,
|
||||||
ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE,
|
ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
||||||
} from "../src/goal-status.js";
|
} from "../src/goal-status.js";
|
||||||
import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path",
|
scenario: "happy path",
|
||||||
@@ -27,7 +27,7 @@ function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Goal one",
|
title: "Goal one",
|
||||||
@@ -41,20 +41,20 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function makePlan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals: [],
|
goals: [],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("isUltragoalDone", () => {
|
describe("isUlwLoopDone", () => {
|
||||||
it("returns true when all goals complete", () => {
|
it("returns true when all goals complete", () => {
|
||||||
// given
|
// given
|
||||||
const plan = makePlan({
|
const plan = makePlan({
|
||||||
@@ -62,7 +62,7 @@ describe("isUltragoalDone", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const done = isUltragoalDone(plan);
|
const done = isUlwLoopDone(plan);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(done).toBe(true);
|
expect(done).toBe(true);
|
||||||
@@ -73,7 +73,7 @@ describe("isUltragoalDone", () => {
|
|||||||
const plan = makePlan({ goals: [makeGoal({ status: "complete" }), makeGoal({ id: "G002", status: "pending" })] });
|
const plan = makePlan({ goals: [makeGoal({ status: "complete" }), makeGoal({ id: "G002", status: "pending" })] });
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const done = isUltragoalDone(plan);
|
const done = isUlwLoopDone(plan);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(done).toBe(false);
|
expect(done).toBe(false);
|
||||||
@@ -91,7 +91,7 @@ describe("isUltragoalDone", () => {
|
|||||||
const plan = makePlan({ goals: [superseded, replacement] });
|
const plan = makePlan({ goals: [superseded, replacement] });
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const done = isUltragoalDone(plan);
|
const done = isUlwLoopDone(plan);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(done).toBe(true);
|
expect(done).toBe(true);
|
||||||
@@ -155,7 +155,7 @@ describe("expectedCodexObjective", () => {
|
|||||||
expect(objective).toBe("aggregate objective");
|
expect(objective).toBe("aggregate objective");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aggregate mode falls back to ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE when codexObjective missing", () => {
|
it("aggregate mode falls back to ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE when codexObjective missing", () => {
|
||||||
// given
|
// given
|
||||||
const goal = makeGoal({ objective: "story objective" });
|
const goal = makeGoal({ objective: "story objective" });
|
||||||
const plan = makePlan({ codexGoalMode: "aggregate" });
|
const plan = makePlan({ codexGoalMode: "aggregate" });
|
||||||
@@ -164,7 +164,7 @@ describe("expectedCodexObjective", () => {
|
|||||||
const objective = expectedCodexObjective(plan, goal);
|
const objective = expectedCodexObjective(plan, goal);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(objective).toBe(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE);
|
expect(objective).toBe(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("per_story mode returns goal.objective", () => {
|
it("per_story mode returns goal.objective", () => {
|
||||||
@@ -189,12 +189,12 @@ describe("aggregateCodexObjective", () => {
|
|||||||
expect(objective).toBe("aggregate objective");
|
expect(objective).toBe("aggregate objective");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("falls back to ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE", () => {
|
it("falls back to ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE", () => {
|
||||||
// when
|
// when
|
||||||
const objective = aggregateCodexObjective(makePlan());
|
const objective = aggregateCodexObjective(makePlan());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(objective).toBe(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE);
|
expect(objective).toBe(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -317,11 +317,11 @@ describe("firstUnresolvedCriterion", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE", () => {
|
describe("ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE", () => {
|
||||||
it("references the .omo/ultragoal path and excludes the legacy workspace", () => {
|
it("references the .omo/ulw-loop path and excludes the legacy workspace", () => {
|
||||||
const legacyWorkspace = [".", "om", "x"].join("");
|
const legacyWorkspace = [".", "om", "x"].join("");
|
||||||
|
|
||||||
expect(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE).toContain(".omo/ultragoal");
|
expect(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE).toContain(".omo/ulw-loop");
|
||||||
expect(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE).not.toContain(legacyWorkspace);
|
expect(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE).not.toContain(legacyWorkspace);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+21
-21
@@ -55,7 +55,7 @@ describe("hooks/hooks.json", () => {
|
|||||||
expect(command).toContain("hook user-prompt-submit");
|
expect(command).toContain("hook user-prompt-submit");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given ultragoal component is enabled #when hooks are inspected #then create_goal PreToolUse guard is registered", async () => {
|
it("#given ulw-loop component is enabled #when hooks are inspected #then create_goal PreToolUse guard is registered", async () => {
|
||||||
const text = await readText("hooks/hooks.json");
|
const text = await readText("hooks/hooks.json");
|
||||||
|
|
||||||
expect(text).toContain('"PreToolUse"');
|
expect(text).toContain('"PreToolUse"');
|
||||||
@@ -71,67 +71,67 @@ describe("src/cli.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("skills/ultragoal/SKILL.md", () => {
|
describe("skills/ulw-loop/SKILL.md", () => {
|
||||||
it("exists", async () => {
|
it("exists", async () => {
|
||||||
const info = await stat(join(repoRoot, "skills/ultragoal/SKILL.md"));
|
const info = await stat(join(repoRoot, "skills/ulw-loop/SKILL.md"));
|
||||||
expect(info.isFile()).toBe(true);
|
expect(info.isFile()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given Codex skill hinting #when ultragoal skill metadata is inspected #then ulw-loop is the primary mention name", async () => {
|
it("#given Codex skill hinting #when ulw-loop skill metadata is inspected #then ulw-loop is the primary mention name", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
|
|
||||||
expect(text).toMatch(/^---\nname: ulw-loop\n/m);
|
expect(text).toMatch(/^---\nname: ulw-loop\n/m);
|
||||||
expect(text).toContain("Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.");
|
expect(text).toContain("Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.");
|
||||||
expect(text).toContain("short-description: Goal-like ultrawork loop for systematic decomposition");
|
expect(text).toContain("short-description: Goal-like ultrawork loop for systematic decomposition");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given Codex dollar hinting #when querying ulw-loop #then ulw-loop surfaces the ultragoal alias", async () => {
|
it("#given Codex dollar hinting #when querying ulw-loop #then ulw-loop surfaces the ulw-loop alias", async () => {
|
||||||
const text = await readText("skills/ultragoal/agents/openai.yaml");
|
const text = await readText("skills/ulw-loop/agents/openai.yaml");
|
||||||
|
|
||||||
expect(text).toContain('display_name: "ulw loop"');
|
expect(text).toContain('display_name: "ulw loop"');
|
||||||
expect(text).not.toContain("ulw-loop / ultragoal");
|
expect(text).not.toContain("ulw-loop / ulw-loop");
|
||||||
expect(text).toContain('short_description: "Goal-like ultrawork loop for systematic decomposition"');
|
expect(text).toContain('short_description: "Goal-like ultrawork loop for systematic decomposition"');
|
||||||
expect(text).toContain("Use $ulw-loop");
|
expect(text).toContain("Use $ulw-loop");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given Codex dollar hinting #when querying ultragoal #then ultragoal remains discoverable as an alias", async () => {
|
it("#given Codex dollar hinting #when querying ulw-loop #then ulw-loop remains discoverable as an alias", async () => {
|
||||||
const text = await readText("skills/ultragoal/agents/openai.yaml");
|
const text = await readText("skills/ulw-loop/agents/openai.yaml");
|
||||||
|
|
||||||
expect(text).toContain("search_terms:");
|
expect(text).toContain("search_terms:");
|
||||||
expect(text).toContain('- "ultragoal"');
|
expect(text).toContain('- "ulw-loop"');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("contains no omx references", async () => {
|
it("contains no omx references", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
expect(text.toLowerCase()).not.toContain("omx");
|
expect(text.toLowerCase()).not.toContain("omx");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("references the success criteria and record-evidence vocabulary", async () => {
|
it("references the success criteria and record-evidence vocabulary", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
expect(text.toLowerCase()).toMatch(/success criteria|successcriteria/);
|
expect(text.toLowerCase()).toMatch(/success criteria|successcriteria/);
|
||||||
expect(text.toLowerCase()).toContain("record-evidence");
|
expect(text.toLowerCase()).toContain("record-evidence");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given omo is absent from PATH #when bootstrap instructions are read #then local cached CLI fallback is documented", async () => {
|
it("#given omo is absent from PATH #when bootstrap instructions are read #then local cached CLI fallback is documented", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
|
|
||||||
expect(text).toContain("If `omo` is absent from PATH");
|
expect(text).toContain("If `omo` is absent from PATH");
|
||||||
expect(text).toContain("ULTRAGOAL_CLI");
|
expect(text).toContain("ULW_LOOP_CLI");
|
||||||
expect(text).toContain("components/ultragoal/dist/cli.js");
|
expect(text).toContain("components/ulw-loop/dist/cli.js");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("#given empty PATH #when bootstrap instructions are read #then handles empty PATH without losing notepad bootstrap", async () => {
|
it("#given empty PATH #when bootstrap instructions are read #then handles empty PATH without losing notepad bootstrap", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
|
|
||||||
expect(text).toContain("If PATH is empty");
|
expect(text).toContain("If PATH is empty");
|
||||||
expect(text).toContain("ULTRAGOAL_NODE");
|
expect(text).toContain("ULW_LOOP_NODE");
|
||||||
expect(text).toContain(".omo/ultragoal/bootstrap-notepad.md");
|
expect(text).toContain(".omo/ulw-loop/bootstrap-notepad.md");
|
||||||
expect(text).not.toContain("ls -1");
|
expect(text).not.toContain("ls -1");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses the .omo workspace path", async () => {
|
it("uses the .omo workspace path", async () => {
|
||||||
const text = await readText("skills/ultragoal/SKILL.md");
|
const text = await readText("skills/ulw-loop/SKILL.md");
|
||||||
expect(text).toContain(".omo/ultragoal");
|
expect(text).toContain(".omo/ulw-loop");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { repoRelative, ulwLoopBriefPath, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.ts";
|
||||||
|
|
||||||
|
describe("ulwLoopDir(repo)", () => {
|
||||||
|
it("returns repo + '/.omo/ulw-loop'", () => {
|
||||||
|
// when/then
|
||||||
|
expect(ulwLoopDir("/repo")).toBe("/repo/.omo/ulw-loop");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("ulw-loop*Path helpers", () => {
|
||||||
|
it("compose artifact filenames under ulwLoopDir", () => {
|
||||||
|
// when/then
|
||||||
|
expect(ulwLoopBriefPath("/r")).toBe("/r/.omo/ulw-loop/brief.md");
|
||||||
|
expect(ulwLoopGoalsPath("/r")).toBe("/r/.omo/ulw-loop/goals.json");
|
||||||
|
expect(ulwLoopLedgerPath("/r")).toBe("/r/.omo/ulw-loop/ledger.jsonl");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("repoRelative", () => {
|
||||||
|
it("strips repo prefix when path is inside repo", () => {
|
||||||
|
// when/then
|
||||||
|
expect(repoRelative("/repo/.omo/ulw-loop/goals.json", "/repo")).toBe(".omo/ulw-loop/goals.json");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns absolute when path is outside repo", () => {
|
||||||
|
// when/then
|
||||||
|
expect(repoRelative("/elsewhere/file", "/repo")).toBe("/elsewhere/file");
|
||||||
|
});
|
||||||
|
});
|
||||||
+43
-43
@@ -3,18 +3,18 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { ultragoalBriefPath, ultragoalGoalsPath, ultragoalLedgerPath } from "../src/paths.js";
|
import { ulwLoopBriefPath, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
|
||||||
import {
|
import {
|
||||||
addUltragoalGoal,
|
addUlwLoopGoal,
|
||||||
createUltragoalPlan,
|
createUlwLoopPlan,
|
||||||
deriveGoalCandidates,
|
deriveGoalCandidates,
|
||||||
seedDefaultSuccessCriteria,
|
seedDefaultSuccessCriteria,
|
||||||
startNextUltragoal,
|
startNextUlwLoop,
|
||||||
summarizeUltragoalPlan,
|
summarizeUlwLoopPlan,
|
||||||
} from "../src/plan-crud.js";
|
} from "../src/plan-crud.js";
|
||||||
import { writePlan } from "../src/plan-io.js";
|
import { writePlan } from "../src/plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
|
|
||||||
@@ -27,20 +27,20 @@ async function readBriefFixture(): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ledgerKinds(repoRoot: string): Promise<string[]> {
|
async function ledgerKinds(repoRoot: string): Promise<string[]> {
|
||||||
const raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8");
|
const raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8");
|
||||||
return raw
|
return raw
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => JSON.parse(line).kind);
|
.map((line) => JSON.parse(line).kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
function criterion(status: UltragoalSuccessCriterion["status"]): UltragoalSuccessCriterion {
|
function criterion(status: UlwLoopSuccessCriterion["status"]): UlwLoopSuccessCriterion {
|
||||||
const [base] = seedDefaultSuccessCriteria(0, "Implement auth endpoint");
|
const [base] = seedDefaultSuccessCriteria(0, "Implement auth endpoint");
|
||||||
if (base === undefined) throw new Error("expected seeded criterion");
|
if (base === undefined) throw new Error("expected seeded criterion");
|
||||||
return { ...base, status };
|
return { ...base, status };
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Build auth service",
|
title: "Build auth service",
|
||||||
@@ -54,20 +54,20 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(goals: UltragoalItem[]): UltragoalPlan {
|
function makePlan(goals: UlwLoopItem[]): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
codexGoalMode: "aggregate",
|
codexGoalMode: "aggregate",
|
||||||
goals,
|
goals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduled(result: Awaited<ReturnType<typeof startNextUltragoal>>) {
|
function scheduled(result: Awaited<ReturnType<typeof startNextUlwLoop>>) {
|
||||||
if ("done" in result) throw new Error("expected scheduled goal");
|
if ("done" in result) throw new Error("expected scheduled goal");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
@@ -93,20 +93,20 @@ describe("seedDefaultSuccessCriteria", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createUltragoalPlan", () => {
|
describe("createUlwLoopPlan", () => {
|
||||||
it("creates .omo/ultragoal/{brief.md, goals.json, ledger.jsonl} in repoRoot", async () => {
|
it("creates .omo/ulw-loop/{brief.md, goals.json, ledger.jsonl} in repoRoot", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
const brief = await readBriefFixture();
|
const brief = await readBriefFixture();
|
||||||
|
|
||||||
await createUltragoalPlan(repoRoot, { brief });
|
await createUlwLoopPlan(repoRoot, { brief });
|
||||||
|
|
||||||
expect(await readFile(ultragoalBriefPath(repoRoot), "utf8")).toBe(brief.endsWith("\n") ? brief : `${brief}\n`);
|
expect(await readFile(ulwLoopBriefPath(repoRoot), "utf8")).toBe(brief.endsWith("\n") ? brief : `${brief}\n`);
|
||||||
expect(await readFile(ultragoalGoalsPath(repoRoot), "utf8")).toContain("G001-build-the-jwt-auth-endpoint");
|
expect(await readFile(ulwLoopGoalsPath(repoRoot), "utf8")).toContain("G001-build-the-jwt-auth-endpoint");
|
||||||
expect(await ledgerKinds(repoRoot)).toEqual(["plan_created"]);
|
expect(await ledgerKinds(repoRoot)).toEqual(["plan_created"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("seeds at least 3 successCriteria per goal", async () => {
|
it("seeds at least 3 successCriteria per goal", async () => {
|
||||||
const plan = await createUltragoalPlan(await makeRepo(), { brief: await readBriefFixture() });
|
const plan = await createUlwLoopPlan(await makeRepo(), { brief: await readBriefFixture() });
|
||||||
|
|
||||||
expect(plan.goals).toHaveLength(3);
|
expect(plan.goals).toHaveLength(3);
|
||||||
expect(plan.goals.every((goal) => goal.successCriteria.length >= 3)).toBe(true);
|
expect(plan.goals.every((goal) => goal.successCriteria.length >= 3)).toBe(true);
|
||||||
@@ -114,17 +114,17 @@ describe("createUltragoalPlan", () => {
|
|||||||
|
|
||||||
it("refuses overwrite of an existing plan without --force", async () => {
|
it("refuses overwrite of an existing plan without --force", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
await createUltragoalPlan(repoRoot, { brief: "first" });
|
await createUlwLoopPlan(repoRoot, { brief: "first" });
|
||||||
|
|
||||||
await expect(createUltragoalPlan(repoRoot, { brief: "second" })).rejects.toThrow(UltragoalError);
|
await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow(UlwLoopError);
|
||||||
await expect(createUltragoalPlan(repoRoot, { brief: "second" })).rejects.toThrow("Refusing to overwrite");
|
await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow("Refusing to overwrite");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("aggregate is the default codexGoalMode", async () => {
|
it("aggregate is the default codexGoalMode", async () => {
|
||||||
const plan = await createUltragoalPlan(await makeRepo(), { brief: "Ship the feature" });
|
const plan = await createUlwLoopPlan(await makeRepo(), { brief: "Ship the feature" });
|
||||||
|
|
||||||
expect(plan.codexGoalMode).toBe("aggregate");
|
expect(plan.codexGoalMode).toBe("aggregate");
|
||||||
expect(plan.codexObjective).toContain(".omo/ultragoal/goals.json");
|
expect(plan.codexObjective).toContain(".omo/ulw-loop/goals.json");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -150,12 +150,12 @@ describe("deriveGoalCandidates", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("addUltragoalGoal", () => {
|
describe("addUlwLoopGoal", () => {
|
||||||
it("appends a new goal to plan with seeded successCriteria", async () => {
|
it("appends a new goal to plan with seeded successCriteria", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
await createUltragoalPlan(repoRoot, { brief: "Build auth" });
|
await createUlwLoopPlan(repoRoot, { brief: "Build auth" });
|
||||||
|
|
||||||
const { plan, goal } = await addUltragoalGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" });
|
const { plan, goal } = await addUlwLoopGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" });
|
||||||
|
|
||||||
expect(plan.goals).toHaveLength(2);
|
expect(plan.goals).toHaveLength(2);
|
||||||
expect(goal.id).toBe("G002-add-rate-limit");
|
expect(goal.id).toBe("G002-add-rate-limit");
|
||||||
@@ -164,20 +164,20 @@ describe("addUltragoalGoal", () => {
|
|||||||
|
|
||||||
it("appends a ledger entry for goal_added", async () => {
|
it("appends a ledger entry for goal_added", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
await createUltragoalPlan(repoRoot, { brief: "Build auth" });
|
await createUlwLoopPlan(repoRoot, { brief: "Build auth" });
|
||||||
|
|
||||||
await addUltragoalGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" });
|
await addUlwLoopGoal(repoRoot, { title: "Add rate limit", objective: "Throttle login" });
|
||||||
|
|
||||||
expect(await ledgerKinds(repoRoot)).toEqual(["plan_created", "goal_added"]);
|
expect(await ledgerKinds(repoRoot)).toEqual(["plan_created", "goal_added"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("startNextUltragoal", () => {
|
describe("startNextUlwLoop", () => {
|
||||||
it("picks the first pending goal", async () => {
|
it("picks the first pending goal", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
await createUltragoalPlan(repoRoot, { brief: "- First\n- Second" });
|
await createUlwLoopPlan(repoRoot, { brief: "- First\n- Second" });
|
||||||
|
|
||||||
const result = scheduled(await startNextUltragoal(repoRoot, {}));
|
const result = scheduled(await startNextUlwLoop(repoRoot, {}));
|
||||||
|
|
||||||
expect(result.goal.id).toBe("G001-first");
|
expect(result.goal.id).toBe("G001-first");
|
||||||
expect(result.goal.status).toBe("in_progress");
|
expect(result.goal.status).toBe("in_progress");
|
||||||
@@ -186,11 +186,11 @@ describe("startNextUltragoal", () => {
|
|||||||
|
|
||||||
it("resumes the in_progress goal when one exists", async () => {
|
it("resumes the in_progress goal when one exists", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
const plan = await createUltragoalPlan(repoRoot, { brief: "- First\n- Second" });
|
const plan = await createUlwLoopPlan(repoRoot, { brief: "- First\n- Second" });
|
||||||
const active = makeGoal({ ...plan.goals[1], status: "in_progress" });
|
const active = makeGoal({ ...plan.goals[1], status: "in_progress" });
|
||||||
await writePlan(repoRoot, { ...plan, goals: [makeGoal({ ...plan.goals[0] }), active], activeGoalId: active.id });
|
await writePlan(repoRoot, { ...plan, goals: [makeGoal({ ...plan.goals[0] }), active], activeGoalId: active.id });
|
||||||
|
|
||||||
const result = scheduled(await startNextUltragoal(repoRoot, {}));
|
const result = scheduled(await startNextUlwLoop(repoRoot, {}));
|
||||||
|
|
||||||
expect(result.goal.id).toBe(active.id);
|
expect(result.goal.id).toBe(active.id);
|
||||||
expect(result.resumed).toBe(true);
|
expect(result.resumed).toBe(true);
|
||||||
@@ -199,10 +199,10 @@ describe("startNextUltragoal", () => {
|
|||||||
it("with retryFailed picks first failed (non-blocked) goal", async () => {
|
it("with retryFailed picks first failed (non-blocked) goal", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
const failed = makeGoal({ status: "failed", failureReason: "flake" });
|
const failed = makeGoal({ status: "failed", failureReason: "flake" });
|
||||||
await mkdir(join(repoRoot, ".omo", "ultragoal"), { recursive: true });
|
await mkdir(join(repoRoot, ".omo", "ulw-loop"), { recursive: true });
|
||||||
await writePlan(repoRoot, makePlan([failed]));
|
await writePlan(repoRoot, makePlan([failed]));
|
||||||
|
|
||||||
const result = scheduled(await startNextUltragoal(repoRoot, { retryFailed: true }));
|
const result = scheduled(await startNextUlwLoop(repoRoot, { retryFailed: true }));
|
||||||
|
|
||||||
expect(result.goal.id).toBe("G001");
|
expect(result.goal.id).toBe("G001");
|
||||||
expect(result.goal.attempt).toBe(1);
|
expect(result.goal.attempt).toBe(1);
|
||||||
@@ -211,16 +211,16 @@ describe("startNextUltragoal", () => {
|
|||||||
|
|
||||||
it("returns { done: true } when no eligible goals remain", async () => {
|
it("returns { done: true } when no eligible goals remain", async () => {
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
await mkdir(join(repoRoot, ".omo", "ultragoal"), { recursive: true });
|
await mkdir(join(repoRoot, ".omo", "ulw-loop"), { recursive: true });
|
||||||
await writePlan(repoRoot, makePlan([makeGoal({ status: "complete" })]));
|
await writePlan(repoRoot, makePlan([makeGoal({ status: "complete" })]));
|
||||||
|
|
||||||
const result = await startNextUltragoal(repoRoot, {});
|
const result = await startNextUlwLoop(repoRoot, {});
|
||||||
|
|
||||||
expect(result).toMatchObject({ done: true });
|
expect(result).toMatchObject({ done: true });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("summarizeUltragoalPlan", () => {
|
describe("summarizeUlwLoopPlan", () => {
|
||||||
it("counts goals by status", () => {
|
it("counts goals by status", () => {
|
||||||
const plan = makePlan([
|
const plan = makePlan([
|
||||||
makeGoal({ id: "G001", status: "pending" }),
|
makeGoal({ id: "G001", status: "pending" }),
|
||||||
@@ -232,7 +232,7 @@ describe("summarizeUltragoalPlan", () => {
|
|||||||
makeGoal({ id: "G007", status: "needs_user_decision", steeringStatus: "superseded" }),
|
makeGoal({ id: "G007", status: "needs_user_decision", steeringStatus: "superseded" }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(summarizeUltragoalPlan(plan)).toMatchObject({
|
expect(summarizeUlwLoopPlan(plan)).toMatchObject({
|
||||||
total: 7,
|
total: 7,
|
||||||
pending: 1,
|
pending: 1,
|
||||||
in_progress: 1,
|
in_progress: 1,
|
||||||
@@ -251,6 +251,6 @@ describe("summarizeUltragoalPlan", () => {
|
|||||||
makeGoal({ id: "G002", successCriteria: [criterion("fail"), criterion("blocked"), criterion("pending")] }),
|
makeGoal({ id: "G002", successCriteria: [criterion("fail"), criterion("blocked"), criterion("pending")] }),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(summarizeUltragoalPlan(plan).criteria).toEqual({ total: 5, pass: 1, pending: 2, fail: 1, blocked: 1 });
|
expect(summarizeUlwLoopPlan(plan).criteria).toEqual({ total: 5, pass: 1, pending: 2, fail: 1, blocked: 1 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+32
-32
@@ -2,22 +2,22 @@ import { copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { beforeEach, describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it } from "vitest";
|
||||||
import { ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "../src/paths.js";
|
import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js";
|
||||||
import {
|
import {
|
||||||
appendLedger,
|
appendLedger,
|
||||||
readSteeringLedgerEntries,
|
readSteeringLedgerEntries,
|
||||||
readUltragoalPlan,
|
readUlwLoopPlan,
|
||||||
withUltragoalMutationLock,
|
withUlwLoopMutationLock,
|
||||||
writePlan,
|
writePlan,
|
||||||
} from "../src/plan-io.js";
|
} from "../src/plan-io.js";
|
||||||
import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
const STABLE_OBJECTIVE =
|
const STABLE_OBJECTIVE =
|
||||||
"Complete the durable ultragoal plan in .omo/ultragoal/goals.json, including later accepted/appended stories, under the original brief constraints; use .omo/ultragoal/ledger.jsonl as the audit trail.";
|
"Complete the durable ulw-loop plan in .omo/ulw-loop/goals.json, including later accepted/appended stories, under the original brief constraints; use .omo/ulw-loop/ledger.jsonl as the audit trail.";
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Build auth service",
|
title: "Build auth service",
|
||||||
@@ -31,14 +31,14 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function makePlan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
codexGoalMode: "aggregate",
|
codexGoalMode: "aggregate",
|
||||||
codexObjective: STABLE_OBJECTIVE,
|
codexObjective: STABLE_OBJECTIVE,
|
||||||
codexObjectiveAliases: [],
|
codexObjectiveAliases: [],
|
||||||
@@ -47,7 +47,7 @@ function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function entry(kind: UltragoalLedgerEntry["kind"], goalId = "G001"): UltragoalLedgerEntry {
|
function entry(kind: UlwLoopLedgerEntry["kind"], goalId = "G001"): UlwLoopLedgerEntry {
|
||||||
return { at: NOW, kind, goalId };
|
return { at: NOW, kind, goalId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,17 +55,17 @@ async function makeRepo(): Promise<string> {
|
|||||||
return mkdtemp(join(tmpdir(), "ug-io-"));
|
return mkdtemp(join(tmpdir(), "ug-io-"));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function writeRawPlan(repoRoot: string, plan: UltragoalPlan): Promise<void> {
|
async function writeRawPlan(repoRoot: string, plan: UlwLoopPlan): Promise<void> {
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
await writeFile(ultragoalGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
await writeFile(ulwLoopGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readLedgerLines(repoRoot: string): Promise<string[]> {
|
async function readLedgerLines(repoRoot: string): Promise<string[]> {
|
||||||
const raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8");
|
const raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8");
|
||||||
return raw.split(/\r?\n/).filter(Boolean);
|
return raw.split(/\r?\n/).filter(Boolean);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("readUltragoalPlan", () => {
|
describe("readUlwLoopPlan", () => {
|
||||||
let repoRoot = "";
|
let repoRoot = "";
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -73,19 +73,19 @@ describe("readUltragoalPlan", () => {
|
|||||||
repoRoot = await makeRepo();
|
repoRoot = await makeRepo();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when goals.json is missing", async () => {
|
it("throws UlwLoopError when goals.json is missing", async () => {
|
||||||
// when/then
|
// when/then
|
||||||
await expect(readUltragoalPlan(repoRoot)).rejects.toThrow(UltragoalError);
|
await expect(readUlwLoopPlan(repoRoot)).rejects.toThrow(UlwLoopError);
|
||||||
await expect(readUltragoalPlan(repoRoot)).rejects.toThrow("omo ultragoal create-goals");
|
await expect(readUlwLoopPlan(repoRoot)).rejects.toThrow("omo ulw-loop create-goals");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns parsed plan when fixture is present", async () => {
|
it("returns parsed plan when fixture is present", async () => {
|
||||||
// given
|
// given
|
||||||
await mkdir(ultragoalDir(repoRoot), { recursive: true });
|
await mkdir(ulwLoopDir(repoRoot), { recursive: true });
|
||||||
await copyFile(join(process.cwd(), "test", "fixtures", "sample-plan.json"), ultragoalGoalsPath(repoRoot));
|
await copyFile(join(process.cwd(), "test", "fixtures", "sample-plan.json"), ulwLoopGoalsPath(repoRoot));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(plan.version).toBe(1);
|
expect(plan.version).toBe(1);
|
||||||
@@ -96,16 +96,16 @@ describe("readUltragoalPlan", () => {
|
|||||||
|
|
||||||
it("migrates legacy aggregate objective on read + writes aggregate_objective_migrated ledger entry + retains alias", async () => {
|
it("migrates legacy aggregate objective on read + writes aggregate_objective_migrated ledger entry + retains alias", async () => {
|
||||||
// given
|
// given
|
||||||
const legacyObjective = "Complete all ultragoal stories in .omo/ultragoal/goals.json: G001 Build auth service";
|
const legacyObjective = "Complete all ulw-loop stories in .omo/ulw-loop/goals.json: G001 Build auth service";
|
||||||
await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
|
await writeRawPlan(repoRoot, makePlan({ codexObjective: legacyObjective }));
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const plan = await readUltragoalPlan(repoRoot);
|
const plan = await readUlwLoopPlan(repoRoot);
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
|
expect(plan.codexObjective).toBe(STABLE_OBJECTIVE);
|
||||||
expect(plan.codexObjectiveAliases).toContain(legacyObjective);
|
expect(plan.codexObjectiveAliases).toContain(legacyObjective);
|
||||||
const persisted = JSON.parse(await readFile(ultragoalGoalsPath(repoRoot), "utf8"));
|
const persisted = JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"));
|
||||||
expect(persisted).toMatchObject({ codexObjective: STABLE_OBJECTIVE, codexObjectiveAliases: [legacyObjective] });
|
expect(persisted).toMatchObject({ codexObjective: STABLE_OBJECTIVE, codexObjectiveAliases: [legacyObjective] });
|
||||||
const lines = await readLedgerLines(repoRoot);
|
const lines = await readLedgerLines(repoRoot);
|
||||||
expect(lines).toHaveLength(1);
|
expect(lines).toHaveLength(1);
|
||||||
@@ -125,9 +125,9 @@ describe("writePlan", () => {
|
|||||||
await writePlan(repoRoot, makePlan());
|
await writePlan(repoRoot, makePlan());
|
||||||
|
|
||||||
// then
|
// then
|
||||||
const raw = await readFile(ultragoalGoalsPath(repoRoot), "utf8");
|
const raw = await readFile(ulwLoopGoalsPath(repoRoot), "utf8");
|
||||||
expect(JSON.parse(raw)).toMatchObject({ version: 1, goals: [{ id: "G001" }] });
|
expect(JSON.parse(raw)).toMatchObject({ version: 1, goals: [{ id: "G001" }] });
|
||||||
expect((await readdir(ultragoalDir(repoRoot))).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
expect((await readdir(ulwLoopDir(repoRoot))).filter((name) => name.endsWith(".tmp"))).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("overwrites existing file", async () => {
|
it("overwrites existing file", async () => {
|
||||||
@@ -139,7 +139,7 @@ describe("writePlan", () => {
|
|||||||
await writePlan(repoRoot, makePlan({ codexObjective: "second" }));
|
await writePlan(repoRoot, makePlan({ codexObjective: "second" }));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(JSON.parse(await readFile(ultragoalGoalsPath(repoRoot), "utf8"))).toMatchObject({
|
expect(JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"))).toMatchObject({
|
||||||
codexObjective: "second",
|
codexObjective: "second",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -166,7 +166,7 @@ describe("appendLedger", () => {
|
|||||||
await appendLedger(repoRoot, entry("goal_completed"));
|
await appendLedger(repoRoot, entry("goal_completed"));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(await readFile(ultragoalLedgerPath(repoRoot), "utf8")).toContain("goal_completed");
|
expect(await readFile(ulwLoopLedgerPath(repoRoot), "utf8")).toContain("goal_completed");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("preserves prior entries", async () => {
|
it("preserves prior entries", async () => {
|
||||||
@@ -209,7 +209,7 @@ describe("readSteeringLedgerEntries", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("withUltragoalMutationLock", () => {
|
describe("withUlwLoopMutationLock", () => {
|
||||||
it("serializes concurrent invocations", async () => {
|
it("serializes concurrent invocations", async () => {
|
||||||
// given
|
// given
|
||||||
const repoRoot = await makeRepo();
|
const repoRoot = await makeRepo();
|
||||||
@@ -221,7 +221,7 @@ describe("withUltragoalMutationLock", () => {
|
|||||||
// when
|
// when
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
[1, 2, 3].map((_) =>
|
[1, 2, 3].map((_) =>
|
||||||
withUltragoalMutationLock(repoRoot, async () => {
|
withUlwLoopMutationLock(repoRoot, async () => {
|
||||||
active += 1;
|
active += 1;
|
||||||
maxActive = Math.max(maxActive, active);
|
maxActive = Math.max(maxActive, active);
|
||||||
const current = Number(await readFile(counterPath, "utf8"));
|
const current = Number(await readFile(counterPath, "utf8"));
|
||||||
+22
-22
@@ -9,8 +9,8 @@ import {
|
|||||||
sameBlockerOccurrences,
|
sameBlockerOccurrences,
|
||||||
validateQualityGate,
|
validateQualityGate,
|
||||||
} from "../src/quality-gate.js";
|
} from "../src/quality-gate.js";
|
||||||
import type { UltragoalItem, UltragoalPlan } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
const VALID_GATE = {
|
const VALID_GATE = {
|
||||||
@@ -20,7 +20,7 @@ const VALID_GATE = {
|
|||||||
criteriaCoverage: { totalCriteria: 2, passCount: 2, adversarialClassesCovered: ["malformed_input"] },
|
criteriaCoverage: { totalCriteria: 2, passCount: 2, adversarialClassesCovered: ["malformed_input"] },
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
interface GoalWithBlocker extends UltragoalItem {
|
interface GoalWithBlocker extends UlwLoopItem {
|
||||||
blocker?: { readonly signature: string };
|
blocker?: { readonly signature: string };
|
||||||
blockerEvidence?: string;
|
blockerEvidence?: string;
|
||||||
blockerOccurrences?: number;
|
blockerOccurrences?: number;
|
||||||
@@ -31,17 +31,17 @@ function makeGate(overrides: Record<string, unknown> = {}): Record<string, unkno
|
|||||||
return { ...VALID_GATE, ...overrides };
|
return { ...VALID_GATE, ...overrides };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getQualityGateError(input: unknown): UltragoalError {
|
function getQualityGateError(input: unknown): UlwLoopError {
|
||||||
try {
|
try {
|
||||||
validateQualityGate(input);
|
validateQualityGate(input);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof UltragoalError) return error;
|
if (error instanceof UlwLoopError) return error;
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
throw new Error("Expected UltragoalError");
|
throw new Error("Expected UlwLoopError");
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Goal one",
|
title: "Goal one",
|
||||||
@@ -55,14 +55,14 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(goals: UltragoalItem[]): UltragoalPlan {
|
function makePlan(goals: UlwLoopItem[]): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals,
|
goals,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -81,39 +81,39 @@ describe("validateQualityGate", () => {
|
|||||||
expect(gate).toMatchObject({ criteriaCoverage: { totalCriteria: 9, passCount: 9 } });
|
expect(gate).toMatchObject({ criteriaCoverage: { totalCriteria: 9, passCount: 9 } });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when aiSlopCleaner missing", () => {
|
it("throws UlwLoopError when aiSlopCleaner missing", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(makeGate({ aiSlopCleaner: undefined }));
|
const error = getQualityGateError(makeGate({ aiSlopCleaner: undefined }));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(error.code).toBe("ULTRAGOAL_QUALITY_GATE_INVALID");
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when verification missing", () => {
|
it("throws UlwLoopError when verification missing", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(makeGate({ verification: undefined }));
|
const error = getQualityGateError(makeGate({ verification: undefined }));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(error.code).toBe("ULTRAGOAL_QUALITY_GATE_INVALID");
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when codeReview missing", () => {
|
it("throws UlwLoopError when codeReview missing", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(makeGate({ codeReview: undefined }));
|
const error = getQualityGateError(makeGate({ codeReview: undefined }));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(error.code).toBe("ULTRAGOAL_QUALITY_GATE_INVALID");
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when criteriaCoverage missing (NEW)", () => {
|
it("throws UlwLoopError when criteriaCoverage missing (NEW)", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(makeGate({ criteriaCoverage: undefined }));
|
const error = getQualityGateError(makeGate({ criteriaCoverage: undefined }));
|
||||||
|
|
||||||
// then
|
// then
|
||||||
expect(error.code).toBe("ULTRAGOAL_QUALITY_GATE_INVALID");
|
expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when criteriaCoverage.passCount < totalCriteria (NEW)", () => {
|
it("throws UlwLoopError when criteriaCoverage.passCount < totalCriteria (NEW)", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(
|
const error = getQualityGateError(
|
||||||
makeGate({ criteriaCoverage: { totalCriteria: 3, passCount: 2, adversarialClassesCovered: [] } }),
|
makeGate({ criteriaCoverage: { totalCriteria: 3, passCount: 2, adversarialClassesCovered: [] } }),
|
||||||
@@ -123,7 +123,7 @@ describe("validateQualityGate", () => {
|
|||||||
expect(error.message).toContain("criteriaCoverage.passCount");
|
expect(error.message).toContain("criteriaCoverage.passCount");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when codeReview.recommendation is not APPROVE", () => {
|
it("throws UlwLoopError when codeReview.recommendation is not APPROVE", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(
|
const error = getQualityGateError(
|
||||||
makeGate({ codeReview: { ...VALID_GATE.codeReview, recommendation: "COMMENT" } }),
|
makeGate({ codeReview: { ...VALID_GATE.codeReview, recommendation: "COMMENT" } }),
|
||||||
@@ -133,7 +133,7 @@ describe("validateQualityGate", () => {
|
|||||||
expect(error.message).toContain("recommendation");
|
expect(error.message).toContain("recommendation");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws UltragoalError when architectStatus is not CLEAR", () => {
|
it("throws UlwLoopError when architectStatus is not CLEAR", () => {
|
||||||
// when
|
// when
|
||||||
const error = getQualityGateError(
|
const error = getQualityGateError(
|
||||||
makeGate({ codeReview: { ...VALID_GATE.codeReview, architectStatus: "WATCH" } }),
|
makeGate({ codeReview: { ...VALID_GATE.codeReview, architectStatus: "WATCH" } }),
|
||||||
+32
-32
@@ -3,16 +3,16 @@ import { tmpdir } from "node:os";
|
|||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js";
|
||||||
import { ultragoalDir, ultragoalLedgerPath } from "../src/paths.js";
|
import { ulwLoopDir, ulwLoopLedgerPath } from "../src/paths.js";
|
||||||
import { writePlan } from "../src/plan-io.js";
|
import { writePlan } from "../src/plan-io.js";
|
||||||
import { recordFinalReviewBlockers } from "../src/review-blockers.js";
|
import { recordFinalReviewBlockers } from "../src/review-blockers.js";
|
||||||
import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js";
|
import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js";
|
||||||
import { UltragoalError } from "../src/types.js";
|
import { UlwLoopError } from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
const VALID_SNAPSHOT_JSON = JSON.stringify({
|
const VALID_SNAPSHOT_JSON = JSON.stringify({
|
||||||
goal: { objective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, status: "active" },
|
goal: { objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status: "active" },
|
||||||
});
|
});
|
||||||
|
|
||||||
const validArgs = {
|
const validArgs = {
|
||||||
@@ -23,7 +23,7 @@ const validArgs = {
|
|||||||
codexGoalJson: VALID_SNAPSHOT_JSON,
|
codexGoalJson: VALID_SNAPSHOT_JSON,
|
||||||
};
|
};
|
||||||
|
|
||||||
function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "happy path",
|
scenario: "happy path",
|
||||||
@@ -35,11 +35,11 @@ function makeCriterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultr
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function makeGoal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Build durable plan",
|
title: "Build durable plan",
|
||||||
objective: "Complete one ultragoal story",
|
objective: "Complete one ulw-loop story",
|
||||||
status: "pending",
|
status: "pending",
|
||||||
successCriteria: [makeCriterion()],
|
successCriteria: [makeCriterion()],
|
||||||
attempt: 1,
|
attempt: 1,
|
||||||
@@ -49,49 +49,49 @@ function makeGoal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makePlan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function makePlan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
codexGoalMode: "aggregate",
|
codexGoalMode: "aggregate",
|
||||||
codexObjective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE,
|
codexObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE,
|
||||||
goals: [makeGoal({ status: "in_progress" })],
|
goals: [makeGoal({ status: "in_progress" })],
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootstrapRepo(plan: UltragoalPlan): Promise<string> {
|
async function bootstrapRepo(plan: UlwLoopPlan): Promise<string> {
|
||||||
const repo = await mkdtemp(join(tmpdir(), "ug-review-blockers-"));
|
const repo = await mkdtemp(join(tmpdir(), "ug-review-blockers-"));
|
||||||
await mkdir(ultragoalDir(repo), { recursive: true });
|
await mkdir(ulwLoopDir(repo), { recursive: true });
|
||||||
await writePlan(repo, plan);
|
await writePlan(repo, plan);
|
||||||
return repo;
|
return repo;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function ledgerKinds(repo: string): Promise<string[]> {
|
async function ledgerKinds(repo: string): Promise<string[]> {
|
||||||
const raw = await readFile(ultragoalLedgerPath(repo), "utf8");
|
const raw = await readFile(ulwLoopLedgerPath(repo), "utf8");
|
||||||
return raw
|
return raw
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => JSON.parse(line).kind);
|
.map((line) => JSON.parse(line).kind);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function expectUltragoalCode(action: () => Promise<unknown>, code: string): Promise<void> {
|
async function expectUlwLoopCode(action: () => Promise<unknown>, code: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error).toBeInstanceOf(UltragoalError);
|
expect(error).toBeInstanceOf(UlwLoopError);
|
||||||
if (!(error instanceof UltragoalError)) throw error;
|
if (!(error instanceof UlwLoopError)) throw error;
|
||||||
expect(error.code).toBe(code);
|
expect(error.code).toBe(code);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
throw new Error("Expected UltragoalError");
|
throw new Error("Expected UlwLoopError");
|
||||||
}
|
}
|
||||||
|
|
||||||
function finalPlan(): UltragoalPlan {
|
function finalPlan(): UlwLoopPlan {
|
||||||
return makePlan({
|
return makePlan({
|
||||||
activeGoalId: "G002",
|
activeGoalId: "G002",
|
||||||
goals: [
|
goals: [
|
||||||
@@ -129,42 +129,42 @@ describe("recordFinalReviewBlockers happy path", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("recordFinalReviewBlockers error cases", () => {
|
describe("recordFinalReviewBlockers error cases", () => {
|
||||||
it("throws ultragoal_goal_not_found for unknown goalId", async () => {
|
it("throws ulw_loop_goal_not_found for unknown goalId", async () => {
|
||||||
const repo = await bootstrapRepo(finalPlan());
|
const repo = await bootstrapRepo(finalPlan());
|
||||||
await expectUltragoalCode(
|
await expectUlwLoopCode(
|
||||||
() => recordFinalReviewBlockers(repo, { ...validArgs, goalId: "G999" }),
|
() => recordFinalReviewBlockers(repo, { ...validArgs, goalId: "G999" }),
|
||||||
"ultragoal_goal_not_found",
|
"ulw_loop_goal_not_found",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws ultragoal_goal_not_in_progress when goal.status !== in_progress", async () => {
|
it("throws ulw_loop_goal_not_in_progress when goal.status !== in_progress", async () => {
|
||||||
const repo = await bootstrapRepo(
|
const repo = await bootstrapRepo(
|
||||||
makePlan({
|
makePlan({
|
||||||
goals: [makeGoal({ id: "G001", status: "in_progress" }), makeGoal({ id: "G002", status: "pending" })],
|
goals: [makeGoal({ id: "G001", status: "in_progress" }), makeGoal({ id: "G002", status: "pending" })],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await expectUltragoalCode(() => recordFinalReviewBlockers(repo, validArgs), "ultragoal_goal_not_in_progress");
|
await expectUlwLoopCode(() => recordFinalReviewBlockers(repo, validArgs), "ulw_loop_goal_not_in_progress");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws ultragoal_not_final_story when other unresolved goals remain", async () => {
|
it("throws ulw_loop_not_final_story when other unresolved goals remain", async () => {
|
||||||
const repo = await bootstrapRepo(
|
const repo = await bootstrapRepo(
|
||||||
makePlan({
|
makePlan({
|
||||||
goals: [makeGoal({ id: "G001", status: "in_progress" }), makeGoal({ id: "G002", status: "pending" })],
|
goals: [makeGoal({ id: "G001", status: "in_progress" }), makeGoal({ id: "G002", status: "pending" })],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
await expectUltragoalCode(
|
await expectUlwLoopCode(
|
||||||
() => recordFinalReviewBlockers(repo, { ...validArgs, goalId: "G001" }),
|
() => recordFinalReviewBlockers(repo, { ...validArgs, goalId: "G001" }),
|
||||||
"ultragoal_not_final_story",
|
"ulw_loop_not_final_story",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("throws ultragoal_codex_snapshot_mismatch when objective mismatches", async () => {
|
it("throws ulw_loop_codex_snapshot_mismatch when objective mismatches", async () => {
|
||||||
const repo = await bootstrapRepo(finalPlan());
|
const repo = await bootstrapRepo(finalPlan());
|
||||||
const codexGoalJson = JSON.stringify({ goal: { objective: "wrong", status: "active" } });
|
const codexGoalJson = JSON.stringify({ goal: { objective: "wrong", status: "active" } });
|
||||||
|
|
||||||
await expectUltragoalCode(
|
await expectUlwLoopCode(
|
||||||
() => recordFinalReviewBlockers(repo, { ...validArgs, codexGoalJson }),
|
() => recordFinalReviewBlockers(repo, { ...validArgs, codexGoalJson }),
|
||||||
"ultragoal_codex_snapshot_mismatch",
|
"ulw_loop_codex_snapshot_mismatch",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+49
-52
@@ -2,20 +2,20 @@ import { mkdtemp, readFile } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { ultragoalGoalsPath } from "../src/paths.js";
|
import { ulwLoopGoalsPath } from "../src/paths.js";
|
||||||
import { readSteeringLedgerEntries, readUltragoalPlan, writePlan } from "../src/plan-io.js";
|
import { readSteeringLedgerEntries, readUlwLoopPlan, writePlan } from "../src/plan-io.js";
|
||||||
import {
|
import {
|
||||||
applySteeringMutation,
|
applySteeringMutation,
|
||||||
parseUltragoalSteeringDirective,
|
parseUlwLoopSteeringDirective,
|
||||||
steerUltragoal,
|
steerUlwLoop,
|
||||||
validateUltragoalSteeringProposal,
|
validateUlwLoopSteeringProposal,
|
||||||
} from "../src/steering.js";
|
} from "../src/steering.js";
|
||||||
import type {
|
import type {
|
||||||
UltragoalItem,
|
UlwLoopItem,
|
||||||
UltragoalPlan,
|
UlwLoopPlan,
|
||||||
UltragoalSteeringProposal,
|
UlwLoopSteeringProposal,
|
||||||
UltragoalSuccessCriterion,
|
UlwLoopSuccessCriterion,
|
||||||
UltragoalSuccessCriterionUserModel,
|
UlwLoopSuccessCriterionUserModel,
|
||||||
} from "../src/types.js";
|
} from "../src/types.js";
|
||||||
|
|
||||||
const NOW = "2026-05-23T00:00:00.000Z";
|
const NOW = "2026-05-23T00:00:00.000Z";
|
||||||
@@ -24,11 +24,11 @@ type CriterionSteeringFields = {
|
|||||||
readonly goalId?: string;
|
readonly goalId?: string;
|
||||||
readonly scenario?: string;
|
readonly scenario?: string;
|
||||||
readonly expectedEvidence?: string;
|
readonly expectedEvidence?: string;
|
||||||
readonly userModel?: UltragoalSuccessCriterionUserModel;
|
readonly userModel?: UlwLoopSuccessCriterionUserModel;
|
||||||
};
|
};
|
||||||
type SteeringInput = UltragoalSteeringProposal & CriterionSteeringFields;
|
type SteeringInput = UlwLoopSteeringProposal & CriterionSteeringFields;
|
||||||
|
|
||||||
function criterion(overrides: Partial<UltragoalSuccessCriterion> = {}): UltragoalSuccessCriterion {
|
function criterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
|
||||||
return {
|
return {
|
||||||
id: "C001",
|
id: "C001",
|
||||||
scenario: "old scenario",
|
scenario: "old scenario",
|
||||||
@@ -40,7 +40,7 @@ function criterion(overrides: Partial<UltragoalSuccessCriterion> = {}): Ultragoa
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function goal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
function goal(overrides: Partial<UlwLoopItem> = {}): UlwLoopItem {
|
||||||
return {
|
return {
|
||||||
id: "G001",
|
id: "G001",
|
||||||
title: "Build auth service",
|
title: "Build auth service",
|
||||||
@@ -54,14 +54,14 @@ function goal(overrides: Partial<UltragoalItem> = {}): UltragoalItem {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function plan(overrides: Partial<UltragoalPlan> = {}): UltragoalPlan {
|
function plan(overrides: Partial<UlwLoopPlan> = {}): UlwLoopPlan {
|
||||||
return {
|
return {
|
||||||
version: 1,
|
version: 1,
|
||||||
createdAt: NOW,
|
createdAt: NOW,
|
||||||
updatedAt: NOW,
|
updatedAt: NOW,
|
||||||
briefPath: ".omo/ultragoal/brief.md",
|
briefPath: ".omo/ulw-loop/brief.md",
|
||||||
goalsPath: ".omo/ultragoal/goals.json",
|
goalsPath: ".omo/ulw-loop/goals.json",
|
||||||
ledgerPath: ".omo/ultragoal/ledger.jsonl",
|
ledgerPath: ".omo/ulw-loop/ledger.jsonl",
|
||||||
goals: [
|
goals: [
|
||||||
goal(),
|
goal(),
|
||||||
goal({ id: "G002", title: "Rate limit", objective: "Throttle login" }),
|
goal({ id: "G002", title: "Rate limit", objective: "Throttle login" }),
|
||||||
@@ -83,18 +83,18 @@ function steering(overrides: Partial<SteeringInput> = {}): SteeringInput {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function repoWithPlan(seed: UltragoalPlan = plan()): Promise<string> {
|
async function repoWithPlan(seed: UlwLoopPlan = plan()): Promise<string> {
|
||||||
const repoRoot = await mkdtemp(join(tmpdir(), "ug-steer-"));
|
const repoRoot = await mkdtemp(join(tmpdir(), "ug-steer-"));
|
||||||
await writePlan(repoRoot, seed);
|
await writePlan(repoRoot, seed);
|
||||||
return repoRoot;
|
return repoRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("validateUltragoalSteeringProposal", () => {
|
describe("validateUlwLoopSteeringProposal", () => {
|
||||||
it("accepts valid add_subgoal", async () => {
|
it("accepts valid add_subgoal", async () => {
|
||||||
const proposal: unknown = JSON.parse(
|
const proposal: unknown = JSON.parse(
|
||||||
await readFile(join(process.cwd(), "test/fixtures/steering-proposal.json"), "utf8"),
|
await readFile(join(process.cwd(), "test/fixtures/steering-proposal.json"), "utf8"),
|
||||||
);
|
);
|
||||||
expect(validateUltragoalSteeringProposal(plan(), proposal).invariant.accepted).toBe(true);
|
expect(validateUlwLoopSteeringProposal(plan(), proposal).invariant.accepted).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it.each([
|
||||||
@@ -104,26 +104,23 @@ describe("validateUltragoalSteeringProposal", () => {
|
|||||||
["protected payload mutations", { after: { codexObjective: "replace", qualityGate: { status: "passed" } } }],
|
["protected payload mutations", { after: { codexObjective: "replace", qualityGate: { status: "passed" } } }],
|
||||||
["weakened completion text", { objective: "skip tests and mark complete faster" }],
|
["weakened completion text", { objective: "skip tests and mark complete faster" }],
|
||||||
])("rejects %s", (_name, overrides) => {
|
])("rejects %s", (_name, overrides) => {
|
||||||
const audit = validateUltragoalSteeringProposal(plan(), { ...steering(), ...overrides });
|
const audit = validateUlwLoopSteeringProposal(plan(), { ...steering(), ...overrides });
|
||||||
expect(audit.invariant.accepted).toBe(false);
|
expect(audit.invariant.accepted).toBe(false);
|
||||||
expect(audit.invariant.rejectedReasons.length).toBeGreaterThan(0);
|
expect(audit.invariant.rejectedReasons.length).toBeGreaterThan(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects when plan already complete", () => {
|
it("rejects when plan already complete", () => {
|
||||||
const done = plan({ goals: [goal({ status: "complete" }), goal({ id: "G002", status: "complete" })] });
|
const done = plan({ goals: [goal({ status: "complete" }), goal({ id: "G002", status: "complete" })] });
|
||||||
expect(validateUltragoalSteeringProposal(done, steering()).invariant.accepted).toBe(false);
|
expect(validateUlwLoopSteeringProposal(done, steering()).invariant.accepted).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects split_subgoal without children", () => {
|
it("rejects split_subgoal without children", () => {
|
||||||
const audit = validateUltragoalSteeringProposal(
|
const audit = validateUlwLoopSteeringProposal(plan(), steering({ kind: "split_subgoal", targetGoalId: "G001" }));
|
||||||
plan(),
|
|
||||||
steering({ kind: "split_subgoal", targetGoalId: "G001" }),
|
|
||||||
);
|
|
||||||
expect(audit.invariant.accepted).toBe(false);
|
expect(audit.invariant.accepted).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("rejects reorder_pending with unknown goal id", () => {
|
it("rejects reorder_pending with unknown goal id", () => {
|
||||||
const audit = validateUltragoalSteeringProposal(
|
const audit = validateUlwLoopSteeringProposal(
|
||||||
plan(),
|
plan(),
|
||||||
steering({ kind: "reorder_pending", pendingOrder: ["missing"] }),
|
steering({ kind: "reorder_pending", pendingOrder: ["missing"] }),
|
||||||
);
|
);
|
||||||
@@ -134,7 +131,7 @@ describe("validateUltragoalSteeringProposal", () => {
|
|||||||
["new scenario", { scenario: "new precise scenario" }],
|
["new scenario", { scenario: "new precise scenario" }],
|
||||||
["new expectedEvidence", { expectedEvidence: "specific command output" }],
|
["new expectedEvidence", { expectedEvidence: "specific command output" }],
|
||||||
])("accepts valid revise_criterion with %s", (_name, update) => {
|
])("accepts valid revise_criterion with %s", (_name, update) => {
|
||||||
const audit = validateUltragoalSteeringProposal(
|
const audit = validateUlwLoopSteeringProposal(
|
||||||
plan(),
|
plan(),
|
||||||
steering({ kind: "revise_criterion", goalId: "G001", criterionId: "C001", ...update }),
|
steering({ kind: "revise_criterion", goalId: "G001", criterionId: "C001", ...update }),
|
||||||
);
|
);
|
||||||
@@ -146,16 +143,16 @@ describe("validateUltragoalSteeringProposal", () => {
|
|||||||
["unknown criterionId", { goalId: "G001", criterionId: "missing", scenario: "new" }],
|
["unknown criterionId", { goalId: "G001", criterionId: "missing", scenario: "new" }],
|
||||||
["no updates", { goalId: "G001", criterionId: "C001" }],
|
["no updates", { goalId: "G001", criterionId: "C001" }],
|
||||||
])("rejects revise_criterion with %s", (_name, overrides) => {
|
])("rejects revise_criterion with %s", (_name, overrides) => {
|
||||||
const audit = validateUltragoalSteeringProposal(plan(), steering({ kind: "revise_criterion", ...overrides }));
|
const audit = validateUlwLoopSteeringProposal(plan(), steering({ kind: "revise_criterion", ...overrides }));
|
||||||
expect(audit.invariant.accepted).toBe(false);
|
expect(audit.invariant.accepted).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("steerUltragoal", () => {
|
describe("steerUlwLoop", () => {
|
||||||
it("add_subgoal: appends goal + ledger entry", async () => {
|
it("add_subgoal: appends goal + ledger entry", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(repoRoot, steering({ idempotencyKey: "add" }));
|
const result = await steerUlwLoop(repoRoot, steering({ idempotencyKey: "add" }));
|
||||||
const persisted = await readUltragoalPlan(repoRoot);
|
const persisted = await readUlwLoopPlan(repoRoot);
|
||||||
expect(result.accepted).toBe(true);
|
expect(result.accepted).toBe(true);
|
||||||
expect(persisted.goals.at(-1)).toMatchObject({ id: "G004", title: "Investigate auth blocker" });
|
expect(persisted.goals.at(-1)).toMatchObject({ id: "G004", title: "Investigate auth blocker" });
|
||||||
expect((await readSteeringLedgerEntries(repoRoot)).at(-1)).toMatchObject({
|
expect((await readSteeringLedgerEntries(repoRoot)).at(-1)).toMatchObject({
|
||||||
@@ -166,7 +163,7 @@ describe("steerUltragoal", () => {
|
|||||||
|
|
||||||
it("split_subgoal: creates children + supersedes parent", async () => {
|
it("split_subgoal: creates children + supersedes parent", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({
|
steering({
|
||||||
kind: "split_subgoal",
|
kind: "split_subgoal",
|
||||||
@@ -180,7 +177,7 @@ describe("steerUltragoal", () => {
|
|||||||
|
|
||||||
it("reorder_pending: changes goal order", async () => {
|
it("reorder_pending: changes goal order", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({ kind: "reorder_pending", pendingOrder: ["G002", "G001"] }),
|
steering({ kind: "reorder_pending", pendingOrder: ["G002", "G001"] }),
|
||||||
);
|
);
|
||||||
@@ -189,7 +186,7 @@ describe("steerUltragoal", () => {
|
|||||||
|
|
||||||
it("revise_pending_wording: updates title/objective", async () => {
|
it("revise_pending_wording: updates title/objective", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({
|
steering({
|
||||||
kind: "revise_pending_wording",
|
kind: "revise_pending_wording",
|
||||||
@@ -207,14 +204,14 @@ describe("steerUltragoal", () => {
|
|||||||
it("annotate_ledger: ledger-only, no plan mutation", async () => {
|
it("annotate_ledger: ledger-only, no plan mutation", async () => {
|
||||||
const seed = plan();
|
const seed = plan();
|
||||||
const repoRoot = await repoWithPlan(seed);
|
const repoRoot = await repoWithPlan(seed);
|
||||||
const result = await steerUltragoal(repoRoot, steering({ kind: "annotate_ledger" }));
|
const result = await steerUlwLoop(repoRoot, steering({ kind: "annotate_ledger" }));
|
||||||
expect(result.plan.goals).toEqual(seed.goals);
|
expect(result.plan.goals).toEqual(seed.goals);
|
||||||
expect(await readFile(ultragoalGoalsPath(repoRoot), "utf8")).toBe(`${JSON.stringify(seed, null, 2)}\n`);
|
expect(await readFile(ulwLoopGoalsPath(repoRoot), "utf8")).toBe(`${JSON.stringify(seed, null, 2)}\n`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("mark_blocked_superseded with children: supersede + replace", async () => {
|
it("mark_blocked_superseded with children: supersede + replace", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({
|
steering({
|
||||||
kind: "mark_blocked_superseded",
|
kind: "mark_blocked_superseded",
|
||||||
@@ -228,7 +225,7 @@ describe("steerUltragoal", () => {
|
|||||||
|
|
||||||
it("mark_blocked_superseded without children: blocks goal", async () => {
|
it("mark_blocked_superseded without children: blocks goal", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({ kind: "mark_blocked_superseded", targetGoalId: "G001", blockedReason: "external blocker" }),
|
steering({ kind: "mark_blocked_superseded", targetGoalId: "G001", blockedReason: "external blocker" }),
|
||||||
);
|
);
|
||||||
@@ -242,7 +239,7 @@ describe("steerUltragoal", () => {
|
|||||||
it.each(["pending", "pass"] as const)("revise_criterion: works on a %s criterion", async (status) => {
|
it.each(["pending", "pass"] as const)("revise_criterion: works on a %s criterion", async (status) => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
const criterionId = status === "pending" ? "C001" : "C002";
|
const criterionId = status === "pending" ? "C001" : "C002";
|
||||||
const result = await steerUltragoal(
|
const result = await steerUlwLoop(
|
||||||
repoRoot,
|
repoRoot,
|
||||||
steering({
|
steering({
|
||||||
kind: "revise_criterion",
|
kind: "revise_criterion",
|
||||||
@@ -261,7 +258,7 @@ describe("steerUltragoal", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("revise_criterion: updates the targeted criterion in plan", () => {
|
it("revise_criterion: updates the targeted criterion in plan", () => {
|
||||||
const audit = validateUltragoalSteeringProposal(
|
const audit = validateUlwLoopSteeringProposal(
|
||||||
plan(),
|
plan(),
|
||||||
steering({ kind: "revise_criterion", goalId: "G001", criterionId: "C001", scenario: "new value" }),
|
steering({ kind: "revise_criterion", goalId: "G001", criterionId: "C001", scenario: "new value" }),
|
||||||
);
|
);
|
||||||
@@ -275,30 +272,30 @@ describe("steerUltragoal", () => {
|
|||||||
|
|
||||||
it("idempotency: same idempotencyKey produces deduped true second time", async () => {
|
it("idempotency: same idempotencyKey produces deduped true second time", async () => {
|
||||||
const repoRoot = await repoWithPlan();
|
const repoRoot = await repoWithPlan();
|
||||||
await steerUltragoal(repoRoot, steering({ idempotencyKey: "same-key" }));
|
await steerUlwLoop(repoRoot, steering({ idempotencyKey: "same-key" }));
|
||||||
const second = await steerUltragoal(repoRoot, steering({ idempotencyKey: "same-key" }));
|
const second = await steerUlwLoop(repoRoot, steering({ idempotencyKey: "same-key" }));
|
||||||
expect(second.deduped).toBe(true);
|
expect(second.deduped).toBe(true);
|
||||||
expect((await readUltragoalPlan(repoRoot)).goals).toHaveLength(4);
|
expect((await readUlwLoopPlan(repoRoot)).goals).toHaveLength(4);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("parseUltragoalSteeringDirective", () => {
|
describe("parseUlwLoopSteeringDirective", () => {
|
||||||
it.each(["OMO_ULTRAGOAL_STEER", "omo.ultragoal.steer", "omo ultragoal steer"])("parses %s pattern", (marker) => {
|
it.each(["OMO_ULW_LOOP_STEER", "omo.ulw-loop.steer", "omo ulw-loop steer"])("parses %s pattern", (marker) => {
|
||||||
expect(parseUltragoalSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toMatchObject({
|
expect(parseUlwLoopSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toMatchObject({
|
||||||
kind: "add_subgoal",
|
kind: "add_subgoal",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when no marker", () => {
|
it("returns null when no marker", () => {
|
||||||
expect(parseUltragoalSteeringDirective(JSON.stringify(steering()))).toBeNull();
|
expect(parseUlwLoopSteeringDirective(JSON.stringify(steering()))).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when JSON malformed after marker", () => {
|
it("returns null when JSON malformed after marker", () => {
|
||||||
expect(parseUltragoalSteeringDirective("OMO_ULTRAGOAL_STEER: {bad json")).toBeNull();
|
expect(parseUlwLoopSteeringDirective("OMO_ULW_LOOP_STEER: {bad json")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null for deprecated markers", () => {
|
it("returns null for deprecated markers", () => {
|
||||||
const marker = ["OM", "X_ULTRAGOAL_STEER"].join("");
|
const marker = ["OM", "X_ULW_LOOP_STEER"].join("");
|
||||||
expect(parseUltragoalSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toBeNull();
|
expect(parseUlwLoopSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
+21
-21
@@ -2,56 +2,56 @@ import { describe, expect, it } from "vitest";
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
iso,
|
iso,
|
||||||
ULTRAGOAL_BRIEF,
|
ULW_LOOP_BRIEF,
|
||||||
ULTRAGOAL_CRITERION_STATUSES,
|
ULW_LOOP_CRITERION_STATUSES,
|
||||||
ULTRAGOAL_DIR,
|
ULW_LOOP_DIR,
|
||||||
ULTRAGOAL_GOALS,
|
ULW_LOOP_GOALS,
|
||||||
ULTRAGOAL_LEDGER,
|
ULW_LOOP_LEDGER,
|
||||||
ULTRAGOAL_STEERING_MUTATION_KINDS,
|
ULW_LOOP_STEERING_MUTATION_KINDS,
|
||||||
ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS,
|
ULW_LOOP_SUCCESS_CRITERION_USER_MODELS,
|
||||||
UltragoalError,
|
UlwLoopError,
|
||||||
} from "../src/types.ts";
|
} from "../src/types.ts";
|
||||||
|
|
||||||
describe("ultragoal domain constants", () => {
|
describe("ulw-loop domain constants", () => {
|
||||||
describe("when checking workspace paths", () => {
|
describe("when checking workspace paths", () => {
|
||||||
it("then ULTRAGOAL_DIR points to the omo workspace", () => {
|
it("then ULW_LOOP_DIR points to the omo workspace", () => {
|
||||||
expect(ULTRAGOAL_DIR).toBe(".omo/ultragoal");
|
expect(ULW_LOOP_DIR).toBe(".omo/ulw-loop");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("then artifact filenames are stable", () => {
|
it("then artifact filenames are stable", () => {
|
||||||
expect(ULTRAGOAL_BRIEF).toBe("brief.md");
|
expect(ULW_LOOP_BRIEF).toBe("brief.md");
|
||||||
expect(ULTRAGOAL_GOALS).toBe("goals.json");
|
expect(ULW_LOOP_GOALS).toBe("goals.json");
|
||||||
expect(ULTRAGOAL_LEDGER).toBe("ledger.jsonl");
|
expect(ULW_LOOP_LEDGER).toBe("ledger.jsonl");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when checking steering mutation kinds", () => {
|
describe("when checking steering mutation kinds", () => {
|
||||||
it("then includes the new revise_criterion kind", () => {
|
it("then includes the new revise_criterion kind", () => {
|
||||||
expect(ULTRAGOAL_STEERING_MUTATION_KINDS).toContain("revise_criterion");
|
expect(ULW_LOOP_STEERING_MUTATION_KINDS).toContain("revise_criterion");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("then totals 7 kinds", () => {
|
it("then totals 7 kinds", () => {
|
||||||
expect(ULTRAGOAL_STEERING_MUTATION_KINDS).toHaveLength(7);
|
expect(ULW_LOOP_STEERING_MUTATION_KINDS).toHaveLength(7);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when checking criterion user models", () => {
|
describe("when checking criterion user models", () => {
|
||||||
it("then exposes 4 user models including adversarial", () => {
|
it("then exposes 4 user models including adversarial", () => {
|
||||||
expect(ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS).toEqual(["happy", "edge", "regression", "adversarial"]);
|
expect(ULW_LOOP_SUCCESS_CRITERION_USER_MODELS).toEqual(["happy", "edge", "regression", "adversarial"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when checking criterion statuses", () => {
|
describe("when checking criterion statuses", () => {
|
||||||
it("then exposes pending/pass/fail/blocked", () => {
|
it("then exposes pending/pass/fail/blocked", () => {
|
||||||
expect(ULTRAGOAL_CRITERION_STATUSES).toEqual(["pending", "pass", "fail", "blocked"]);
|
expect(ULW_LOOP_CRITERION_STATUSES).toEqual(["pending", "pass", "fail", "blocked"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("UltragoalError", () => {
|
describe("UlwLoopError", () => {
|
||||||
describe("when constructed with code", () => {
|
describe("when constructed with code", () => {
|
||||||
it("then is an Error instance carrying the code", () => {
|
it("then is an Error instance carrying the code", () => {
|
||||||
const err = new UltragoalError("bad", "TEST_CODE");
|
const err = new UlwLoopError("bad", "TEST_CODE");
|
||||||
|
|
||||||
expect(err).toBeInstanceOf(Error);
|
expect(err).toBeInstanceOf(Error);
|
||||||
expect(err.code).toBe("TEST_CODE");
|
expect(err.code).toBe("TEST_CODE");
|
||||||
@@ -60,7 +60,7 @@ describe("UltragoalError", () => {
|
|||||||
|
|
||||||
it("then accepts optional cause + details", () => {
|
it("then accepts optional cause + details", () => {
|
||||||
const cause = new Error("upstream");
|
const cause = new Error("upstream");
|
||||||
const err = new UltragoalError("wrap", "WRAP", { cause, details: { goalId: "G001" } });
|
const err = new UlwLoopError("wrap", "WRAP", { cause, details: { goalId: "G001" } });
|
||||||
|
|
||||||
expect(err.cause).toBe(cause);
|
expect(err.cause).toBe(cause);
|
||||||
expect(err.details).toEqual({ goalId: "G001" });
|
expect(err.details).toEqual({ goalId: "G001" });
|
||||||
@@ -45,9 +45,9 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${PLUGIN_ROOT}/components/ultragoal/dist/cli.js\" hook user-prompt-submit",
|
"command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook user-prompt-submit",
|
||||||
"timeout": 10,
|
"timeout": 10,
|
||||||
"statusMessage": "checking OMO ultragoal steering"
|
"statusMessage": "checking OMO ulw-loop steering"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
"hooks": [
|
"hooks": [
|
||||||
{
|
{
|
||||||
"type": "command",
|
"type": "command",
|
||||||
"command": "node \"${PLUGIN_ROOT}/components/ultragoal/dist/cli.js\" hook pre-tool-use",
|
"command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook pre-tool-use",
|
||||||
"timeout": 5,
|
"timeout": 5,
|
||||||
"statusMessage": "enforcing OMO unlimited goal budget"
|
"statusMessage": "enforcing OMO unlimited goal budget"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"components/lsp",
|
"components/lsp",
|
||||||
"components/telemetry",
|
"components/telemetry",
|
||||||
"components/start-work-continuation",
|
"components/start-work-continuation",
|
||||||
"components/ultragoal",
|
"components/ulw-loop",
|
||||||
"components/ultrawork"
|
"components/ultrawork"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ const skillSources = [
|
|||||||
["comment-checker", "components/comment-checker/skills/comment-checker"],
|
["comment-checker", "components/comment-checker/skills/comment-checker"],
|
||||||
["lsp", "components/lsp/skills/lsp"],
|
["lsp", "components/lsp/skills/lsp"],
|
||||||
["rules", "components/rules/skills/rules"],
|
["rules", "components/rules/skills/rules"],
|
||||||
["ultragoal", "components/ultragoal/skills/ultragoal"],
|
["ulw-loop", "components/ulw-loop/skills/ulw-loop"],
|
||||||
];
|
];
|
||||||
|
|
||||||
const opencodeOnlyOrchestrationPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/;
|
const opencodeOnlyOrchestrationPattern = /\b(?:call_omo_agent|background_output|team_[a-z_]+|task)\s*\(/;
|
||||||
|
|||||||
@@ -1,198 +0,0 @@
|
|||||||
---
|
|
||||||
name: ulw-loop
|
|
||||||
description: Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.
|
|
||||||
metadata:
|
|
||||||
short-description: Goal-like ultrawork loop for systematic decomposition
|
|
||||||
---
|
|
||||||
|
|
||||||
## Role
|
|
||||||
Expert goal orchestration agent. Plan multi-goal work that survives across turns and sessions.
|
|
||||||
Use GPT-5.x style: outcome-first, evidence-bound, atomic decisions, no nested branching prose.
|
|
||||||
|
|
||||||
## Goal
|
|
||||||
Deliver every goal in `.omo/ultragoal/goals.json` end-to-end.
|
|
||||||
Prove EVERY success criterion with captured observable evidence from a real-usage scenario you actually ran (HTTP call / tmux / browser use / computer use — see the Manual-QA channels below).
|
|
||||||
TESTS ALONE NEVER PROVE DONE. A green test suite is supporting evidence, not completion proof.
|
|
||||||
Audit each pass, fail, block, steering change, and checkpoint in `.omo/ultragoal/ledger.jsonl`.
|
|
||||||
|
|
||||||
## Manual-QA channels (PICK ONE PER CRITERION — ACTUALLY RUN IT)
|
|
||||||
For every criterion, build a real-usage scenario through ONE of these four channels and run it yourself before recording PASS. The full test suite being green is NEVER verification on its own.
|
|
||||||
|
|
||||||
1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
|
|
||||||
2. **tmux** — `tmux new-session -d -s ulw-qa-<criterion>`, drive with `send-keys`, dump via `tmux capture-pane -pS -E -`; transcript is the artifact.
|
|
||||||
3. **Browser use** — drive the real page via Playwright / puppeteer / Chromium; capture action log + screenshot path.
|
|
||||||
4. **Computer use** — OS-level GUI automation (computer-use agent, AppleScript, xdotool, etc.) against the running app; capture action log + screenshot.
|
|
||||||
|
|
||||||
Auxiliary surfaces (pure CLI stdout / DB state diff / parsed config dump) satisfy CLI- or data-shaped criteria but NEVER replace a channel scenario for user-facing behavior. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
|
||||||
|
|
||||||
## Artifacts
|
|
||||||
- `.omo/ultragoal/brief.md`: original brief and durable constraints.
|
|
||||||
- `.omo/ultragoal/goals.json`: goals with embedded `successCriteria` per goal.
|
|
||||||
- `.omo/ultragoal/ledger.jsonl`: append-only audit trail.
|
|
||||||
- Read artifacts before resuming, steering, or checkpointing.
|
|
||||||
- Never invent state outside `.omo/ultragoal` artifacts or `omo ultragoal status --json`.
|
|
||||||
|
|
||||||
## Bootstrap
|
|
||||||
Do all three steps before execution. No edits, goal tools, or checkpointing before bootstrap completes.
|
|
||||||
|
|
||||||
### 1. Create goals from the brief
|
|
||||||
Resolve the CLI before the first command. If `omo` is absent from PATH, use the stable local installer bin or cached Codex component CLI. This is the same ultragoal CLI, so PATH absence is not a blocker. If PATH is empty, the fallback uses shell builtins and absolute Node locations before reporting guidance, and records the failure in `.omo/ultragoal/bootstrap-notepad.md`.
|
|
||||||
```sh
|
|
||||||
if command -v omo >/dev/null 2>&1; then
|
|
||||||
ULTRAGOAL_CLI=omo
|
|
||||||
else
|
|
||||||
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
|
||||||
ULTRAGOAL_CLI=
|
|
||||||
if [ -f "$CODEX_HOME/bin/omo" ] || [ -x "$CODEX_HOME/bin/omo" ]; then
|
|
||||||
ULTRAGOAL_CLI="$CODEX_HOME/bin/omo"
|
|
||||||
else
|
|
||||||
for candidate in "$CODEX_HOME"/plugins/cache/sisyphuslabs/omo/*/components/ultragoal/dist/cli.js; do
|
|
||||||
[ -f "$candidate" ] || continue
|
|
||||||
ULTRAGOAL_CLI="$candidate"
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
ULTRAGOAL_NODE="$(command -v node 2>/dev/null || true)"
|
|
||||||
if [ -z "$ULTRAGOAL_NODE" ]; then
|
|
||||||
for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
|
||||||
[ -x "$candidate" ] || continue
|
|
||||||
ULTRAGOAL_NODE="$candidate"
|
|
||||||
break
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$ULTRAGOAL_CLI" ] && [ -n "$ULTRAGOAL_NODE" ]; then
|
|
||||||
omo() { "$ULTRAGOAL_NODE" "$ULTRAGOAL_CLI" "$@"; }
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${ULTRAGOAL_CLI:-}" ]; then
|
|
||||||
/bin/mkdir -p .omo/ultragoal 2>/dev/null || mkdir -p .omo/ultragoal 2>/dev/null || true
|
|
||||||
NOTE="${NOTE:-.omo/ultragoal/bootstrap-notepad.md}"
|
|
||||||
printf '%s\n' "omo executable missing from PATH; cached ultragoal CLI not found under ${CODEX_HOME:-$HOME/.codex}." >> "$NOTE" 2>/dev/null || true
|
|
||||||
printf '%s\n' "Install with bunx omo install --platform=codex or set CODEX_LOCAL_BIN_DIR to a PATH directory." >&2
|
|
||||||
fi
|
|
||||||
```
|
|
||||||
If `ULTRAGOAL_CLI` is empty, open the durable notepad first, record the missing CLI evidence, then surface the installer issue.
|
|
||||||
|
|
||||||
Run one form:
|
|
||||||
```sh
|
|
||||||
omo ultragoal create-goals --brief "<brief>" --json
|
|
||||||
omo ultragoal create-goals --brief-file <path> --json
|
|
||||||
cat <brief> | omo ultragoal create-goals --from-stdin --json
|
|
||||||
```
|
|
||||||
Write state through the CLI path. Do not hand-edit state files.
|
|
||||||
|
|
||||||
### 2. Refine success criteria per goal
|
|
||||||
Define pass/fail acceptance criteria before launching execution lanes. Include the command, artifact, or manual check that will prove success.
|
|
||||||
Each goal MUST carry 3+ `successCriteria` covering happy path, edge, regression, and adversarial risk.
|
|
||||||
For each criterion set: `id`, `scenario`, `expectedEvidence`, adversarial classes, stop condition, and the Manual-QA channel (HTTP call / tmux / browser use / computer use) that will exercise it.
|
|
||||||
Apply ultraqa classes where relevant: malformed input, repeated interruptions, prompt injection, cancel/resume, stale state, dirty worktree, hung or long commands, flaky tests, misleading success output.
|
|
||||||
Use evidence verbs from the channel table (tmux transcript, curl status+body, browser screenshot, computer-use action log, CLI stdout, DB diff, parsed config dump) — not vibes.
|
|
||||||
"Tests pass" is supporting signal, NEVER completion proof. Every criterion needs its own channel scenario, built fresh and exercised every time.
|
|
||||||
Record manual QA notes when behavior is user-visible.
|
|
||||||
Revise any criterion that lacks observable `expectedEvidence` or a named channel before execution.
|
|
||||||
|
|
||||||
### 3. Inspect state
|
|
||||||
Run `omo ultragoal status --json`.
|
|
||||||
Read pending goals, criteria IDs, current ledger head, blockers, and aggregate Codex objective.
|
|
||||||
|
|
||||||
## Execution Loop
|
|
||||||
Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures at 3.
|
|
||||||
|
|
||||||
### Acquire Next Goal
|
|
||||||
1. Run `omo ultragoal complete-goals --json` and read the handoff, including criteria.
|
|
||||||
2. Call `get_goal` and inspect active Codex state.
|
|
||||||
3. Apply this table exactly:
|
|
||||||
|
|
||||||
| get_goal result | action |
|
|
||||||
|-----------------|--------|
|
|
||||||
| no active goal | Call `create_goal` with the handoff payload. |
|
|
||||||
| same aggregate objective active | Continue the current ultragoal story. |
|
|
||||||
| different goal active | STOP. Checkpoint blocked and surface the conflict. |
|
|
||||||
4. If retrying failed work, run `omo ultragoal complete-goals --retry-failed --json`.
|
|
||||||
5. Never create a second Codex goal for the same aggregate objective.
|
|
||||||
|
|
||||||
### Per-Criterion Cycle
|
|
||||||
1. PLAN: read `criterion.scenario`, `criterion.expectedEvidence`, prior ledger entries, and safety bounds.
|
|
||||||
2. Register atomic todos: `path: <action> for <criterion> - verify by <check>`.
|
|
||||||
3. EXECUTE-AS-SCENARIO: do one bounded change, then ACTUALLY run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use — see the channel table above). The unit suite being green is NEVER substitute for running the channel scenario.
|
|
||||||
4. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump.
|
|
||||||
5. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 3 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD`. Missing receipt → record BLOCKED, not PASS.
|
|
||||||
6. RECORD exactly one result:
|
|
||||||
- PASS: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> | <cleanup receipt>" --json`
|
|
||||||
- FAIL: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
|
||||||
- BLOCKED: `omo ultragoal record-evidence --goal-id <id> --criterion-id <id> --status blocked --evidence "<observable>" --notes "<safety/blocker/leftover-state>" --json`
|
|
||||||
7. If actual does not match expected, diagnose, fix minimally, and rerun the SAME criterion (including a fresh cleanup).
|
|
||||||
8. After 3 same-criterion failures, exit the goal with diagnosis.
|
|
||||||
9. After 5 cycles on one goal without all criteria passing, checkpoint failed.
|
|
||||||
10. Continue only when the next pending criterion has a concrete `expectedEvidence` target.
|
|
||||||
|
|
||||||
### Goal Completion
|
|
||||||
1. Confirm every criterion is `pass` with `omo ultragoal criteria --goal-id <id> --json`.
|
|
||||||
2. Call `get_goal` for a fresh snapshot.
|
|
||||||
3. Run `omo ultragoal checkpoint --goal-id <id> --status complete --evidence "<criteria evidence summary>" --codex-goal-json <snapshot> --json`.
|
|
||||||
4. If blocked or failed, checkpoint with `--status blocked` or `--status failed` and include diagnosis evidence.
|
|
||||||
5. If this is the final goal, run the final quality gate first and pass `--quality-gate-json`.
|
|
||||||
|
|
||||||
## Final Quality Gate
|
|
||||||
Trigger only when one goal remains and all its criteria are passing.
|
|
||||||
1. Run targeted verification for changed behavior.
|
|
||||||
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
|
||||||
3. Rerun verification after cleanup.
|
|
||||||
4. Run `$code-review`.
|
|
||||||
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
|
||||||
6. If review is non-clean, run `omo ultragoal record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
|
||||||
7. If clean, checkpoint final completion:
|
|
||||||
```sh
|
|
||||||
omo ultragoal checkpoint --goal-id <id> --status complete --evidence "<e2e evidence + manual QA notes>" --codex-goal-json <snapshot> --quality-gate-json <json-or-path> --json
|
|
||||||
```
|
|
||||||
`--quality-gate-json` shape:
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"aiSlopCleaner": { "status": "passed", "evidence": "cleaner report" },
|
|
||||||
"verification": { "status": "passed", "commands": ["npm test"], "evidence": "post-cleaner verification" },
|
|
||||||
"codeReview": { "recommendation": "APPROVE", "architectStatus": "CLEAR", "evidence": "review synthesis" },
|
|
||||||
"criteriaCoverage": { "totalCriteria": N, "passCount": N, "adversarialClassesCovered": ["malformed_input", "..."] }
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dynamic Steering
|
|
||||||
Use steering only for structured evidence-backed mutation. Reject natural-language steering requests.
|
|
||||||
|
|
||||||
| Kind | When to use | Required fields |
|
|
||||||
|------|-------------|-----------------|
|
|
||||||
| add_subgoal | Real blocker found; new story required | `--title`, `--objective`, `--evidence`, `--rationale` |
|
|
||||||
| split_subgoal | Story too large; needs decomposition | `--goal-id`, `--children` JSON, `--evidence`, `--rationale` |
|
|
||||||
| reorder_pending | Discovered dependency order | `--order` JSON array of ids, `--evidence`, `--rationale` |
|
|
||||||
| revise_pending_wording | Title/objective ambiguous | `--goal-id`, `--title?`, `--objective?`, `--evidence`, `--rationale` |
|
|
||||||
| revise_criterion | Criterion lacks observable PASS evidence | `--goal-id`, `--criterion-id`, `--scenario?`, `--expected-evidence?`, `--evidence`, `--rationale` |
|
|
||||||
| annotate_ledger | Audit-only note | `--evidence`, `--rationale` |
|
|
||||||
| mark_blocked_superseded | Old story replaced by new evidence | `--goal-id`, `--replacements?`, `--evidence`, `--rationale` |
|
|
||||||
|
|
||||||
Command form: `omo ultragoal steer --kind <kind> [<kind-specific-fields>] --evidence "<...>" --rationale "<...>" --json`.
|
|
||||||
Structured prompt directives accepted: `OMO_ULTRAGOAL_STEER: { ... }`, `omo.ultragoal.steer: {...}`, `omo ultragoal steer: {...}`.
|
|
||||||
|
|
||||||
## Constraints
|
|
||||||
1. NEVER call `update_goal` mid-aggregate; only on final story after the quality gate passes.
|
|
||||||
2. NEVER call `create_goal` when `get_goal` shows a different active goal.
|
|
||||||
3. NEVER mark `criterion.status == "pass"` without captured observable evidence in `record-evidence`.
|
|
||||||
4. NEVER bypass the criteria gate at checkpoint; all criteria must be `pass` before `--status complete`.
|
|
||||||
5. Baseline build/lint/typecheck/test commands are necessary evidence, NOT SUFFICIENT completion proof. Criteria coverage with observable evidence is the gate.
|
|
||||||
6. Treat `.omo/ultragoal/ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
|
||||||
7. Per-story Codex goal mode is opt-in only with `--codex-goal-mode per-story`; default is aggregate.
|
|
||||||
8. Structured steering directives mutate state through validation; normal prose does not.
|
|
||||||
9. Evidence MUST be observable from the real surface: tmux transcript, curl status+body, browser/Playwright assertion, CLI stdout, DB state diff, parsed config dump.
|
|
||||||
10. Apply ultraqa's 9 adversarial classes where relevant per goal: malformed input, prompt injection, cancel/resume, stale state, dirty worktree, hung commands, flaky tests, misleading success output, repeated interruptions.
|
|
||||||
11. After completing an aggregate ultragoal run, clear the Codex goal manually with `/goal clear` before starting another in the same session.
|
|
||||||
12. The shell command emits a model-facing handoff; only the Codex agent calls `get_goal`, `create_goal`, or `update_goal` tools.
|
|
||||||
13. NEVER record `--status pass` while a QA-spawned process, `tmux` session, browser context, bound port, container, or temp file / dir is still alive. The evidence string MUST include the cleanup receipt. Leftover runtime state = BLOCKED, not PASS.
|
|
||||||
|
|
||||||
## Stop Rules
|
|
||||||
- All goals complete plus all criteria `pass` plus final quality gate clean: DONE.
|
|
||||||
- 3x same criterion failure: checkpoint failed, surface diagnosis.
|
|
||||||
- 5 cycles on one goal without all-pass: checkpoint failed, surface.
|
|
||||||
- Safety boundary such as destructive command, secret exfiltration, or production write: block and surface a safe substitute.
|
|
||||||
- Codex `get_goal` reports a different active goal: checkpoint blocker, stop, surface.
|
|
||||||
- Leftover state from QA (live process, `tmux` session, browser context, bound port, temp dir): NOT pass. Clean up, append the receipt, then continue.
|
|
||||||
- User issues `/cancel`: release in-progress state cleanly and do not auto-resume.
|
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
---
|
||||||
|
name: ulw-loop
|
||||||
|
description: Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps.
|
||||||
|
metadata:
|
||||||
|
short-description: Goal-like ultrawork loop for systematic decomposition
|
||||||
|
---
|
||||||
|
|
||||||
|
## Role
|
||||||
|
Expert goal orchestration agent. You conduct; right-sized parallel subagents play. Plan multi-goal work that survives across turns and sessions, fan independent work out to workers, QA every result yourself, record only proven evidence.
|
||||||
|
Use GPT-5.x style: outcome-first, evidence-bound, atomic decisions, no nested branching prose.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
Deliver every goal in `.omo/ulw-loop/goals.json` end-to-end.
|
||||||
|
Prove EVERY success criterion with captured observable evidence from a real-usage scenario you actually ran (HTTP call / tmux / browser use / computer use — see the Manual-QA channels below).
|
||||||
|
TESTS ALONE NEVER PROVE DONE. A green test suite is supporting evidence, not completion proof.
|
||||||
|
Audit each pass, fail, block, steering change, and checkpoint in `.omo/ulw-loop/ledger.jsonl`.
|
||||||
|
|
||||||
|
## Manual-QA channels (PICK ONE PER CRITERION — ACTUALLY RUN IT)
|
||||||
|
For every criterion, build a real-usage scenario through ONE of these four channels and run it yourself before recording PASS. The full test suite being green is NEVER verification on its own.
|
||||||
|
|
||||||
|
1. **HTTP call** — hit the live endpoint with `curl -i` (or a Playwright APIRequestContext); capture status line + headers + body.
|
||||||
|
2. **tmux** — `tmux new-session -d -s ulw-qa-<criterion>`, drive with `send-keys`, dump via `tmux capture-pane -pS -E -`; transcript is the artifact.
|
||||||
|
3. **Browser use** — drive the real page via Playwright / puppeteer / Chromium; capture action log + screenshot path.
|
||||||
|
4. **Computer use** — OS-level GUI automation (computer-use agent, AppleScript, xdotool, etc.) against the running app; capture action log + screenshot.
|
||||||
|
|
||||||
|
Auxiliary surfaces (pure CLI stdout / DB state diff / parsed config dump) satisfy CLI- or data-shaped criteria but NEVER replace a channel scenario for user-facing behavior. `--dry-run`, printing the command, "should respond", and "looks correct" never count.
|
||||||
|
|
||||||
|
## Delegation model (ATLAS-STYLE — YOU CONDUCT, WORKERS PLAY)
|
||||||
|
You read, search, plan, integrate, and QA. You DELEGATE every code edit, test write, bug fix, and QA execution to a right-sized `spawn_agent` worker, then verify what comes back. Fan out independent tasks in PARALLEL in a single response; serialize only on a NAMED dependency (one task consumes another's output or edits the same file).
|
||||||
|
|
||||||
|
Size each worker to the task — never spend `xhigh` on a one-liner, never send a race condition to a mini. Pass `model` + `reasoning_effort` per call (an override needs a non-full-history fork mode):
|
||||||
|
|
||||||
|
| Task shape | agent_type | model | reasoning_effort |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Trivial / mechanical (rename, move, obvious one-liner, config edit) | `worker` | `gpt-5.4-mini` | `low` |
|
||||||
|
| Pure implementation against a clear spec (new function, endpoint, test from a named pattern) | `worker` | `gpt-5.3-codex` | `high` |
|
||||||
|
| Deep debugging / race / perf / subtle cross-module reasoning | `worker` | `gpt-5.5` | `xhigh` |
|
||||||
|
| QA execution (drive a channel, capture evidence) | `worker` | `gpt-5.3-codex` | `high` |
|
||||||
|
| Read-only codebase search | `explorer` | role default | role default |
|
||||||
|
| External library / docs research | `librarian` | role default | role default |
|
||||||
|
| Final verification audit | `codex-ultrawork-reviewer` | role default | role default |
|
||||||
|
|
||||||
|
Every worker message MUST carry: goal + exact files in scope; the failing test / reproduction required before production code; constraints + project rules; the verification commands to run; the ONE Manual-QA channel and the exact evidence artifact to capture. Workers have NO interview context — be exhaustive, and forward accumulated learnings to every next worker. Track running workers; `wait_agent` for results, `close_agent` when done.
|
||||||
|
|
||||||
|
## Artifacts
|
||||||
|
- `.omo/ulw-loop/brief.md`: original brief and durable constraints.
|
||||||
|
- `.omo/ulw-loop/goals.json`: goals with embedded `successCriteria` per goal.
|
||||||
|
- `.omo/ulw-loop/ledger.jsonl`: append-only audit trail.
|
||||||
|
- Read artifacts before resuming, steering, or checkpointing.
|
||||||
|
- Never invent state outside `.omo/ulw-loop` artifacts or `omo ulw-loop status --json`.
|
||||||
|
|
||||||
|
## Bootstrap
|
||||||
|
Do all three steps before execution. No edits, goal tools, or checkpointing before bootstrap completes.
|
||||||
|
|
||||||
|
### 1. Create goals from the brief
|
||||||
|
Resolve the CLI before the first command. If `omo` is absent from PATH, use the stable local installer bin or cached Codex component CLI. This is the same ulw-loop CLI, so PATH absence is not a blocker. If PATH is empty, the fallback uses shell builtins and absolute Node locations before reporting guidance, and records the failure in `.omo/ulw-loop/bootstrap-notepad.md`.
|
||||||
|
```sh
|
||||||
|
if command -v omo >/dev/null 2>&1; then
|
||||||
|
ULW_LOOP_CLI=omo
|
||||||
|
else
|
||||||
|
CODEX_HOME="${CODEX_HOME:-$HOME/.codex}"
|
||||||
|
ULW_LOOP_CLI=
|
||||||
|
if [ -f "$CODEX_HOME/bin/omo" ] || [ -x "$CODEX_HOME/bin/omo" ]; then
|
||||||
|
ULW_LOOP_CLI="$CODEX_HOME/bin/omo"
|
||||||
|
else
|
||||||
|
for candidate in "$CODEX_HOME"/plugins/cache/sisyphuslabs/omo/*/components/ulw-loop/dist/cli.js; do
|
||||||
|
[ -f "$candidate" ] || continue
|
||||||
|
ULW_LOOP_CLI="$candidate"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
ULW_LOOP_NODE="$(command -v node 2>/dev/null || true)"
|
||||||
|
if [ -z "$ULW_LOOP_NODE" ]; then
|
||||||
|
for candidate in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
|
||||||
|
[ -x "$candidate" ] || continue
|
||||||
|
ULW_LOOP_NODE="$candidate"
|
||||||
|
break
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -n "$ULW_LOOP_CLI" ] && [ -n "$ULW_LOOP_NODE" ]; then
|
||||||
|
omo() { "$ULW_LOOP_NODE" "$ULW_LOOP_CLI" "$@"; }
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${ULW_LOOP_CLI:-}" ]; then
|
||||||
|
/bin/mkdir -p .omo/ulw-loop 2>/dev/null || mkdir -p .omo/ulw-loop 2>/dev/null || true
|
||||||
|
NOTE="${NOTE:-.omo/ulw-loop/bootstrap-notepad.md}"
|
||||||
|
printf '%s\n' "omo executable missing from PATH; cached ulw-loop CLI not found under ${CODEX_HOME:-$HOME/.codex}." >> "$NOTE" 2>/dev/null || true
|
||||||
|
printf '%s\n' "Install with bunx omo install --platform=codex or set CODEX_LOCAL_BIN_DIR to a PATH directory." >&2
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
If `ULW_LOOP_CLI` is empty, open the durable notepad first, record the missing CLI evidence, then surface the installer issue.
|
||||||
|
|
||||||
|
Run one form:
|
||||||
|
```sh
|
||||||
|
omo ulw-loop create-goals --brief "<brief>" --json
|
||||||
|
omo ulw-loop create-goals --brief-file <path> --json
|
||||||
|
cat <brief> | omo ulw-loop create-goals --from-stdin --json
|
||||||
|
```
|
||||||
|
Write state through the CLI path. Do not hand-edit state files.
|
||||||
|
|
||||||
|
### 2. Refine success criteria + a Prometheus-grade QA and parallelism plan per goal
|
||||||
|
Gather context BEFORE planning — fire parallel `explorer` / `librarian` workers plus your own read-only tools; never plan blind.
|
||||||
|
Define pass/fail acceptance criteria before launching execution lanes. Include the command, artifact, or manual check that will prove success.
|
||||||
|
Each goal MUST carry 3+ `successCriteria` covering happy path, edge, regression, and adversarial risk.
|
||||||
|
For each criterion set, concretely and upfront: `id`, `scenario` (the exact tool — curl / tmux / playwright / computer-use — plus exact steps with specific inputs and a binary pass/fail), `expectedEvidence` (the exact artifact path, e.g. `.omo/ulw-loop/evidence/<goal>-<criterion>.<ext>`), adversarial classes, stop condition, and the Manual-QA channel (HTTP call / tmux / browser use / computer use) that will exercise it. Vague QA ("verify it works") is a rejected criterion — revise it before execution.
|
||||||
|
Apply ultraqa classes where relevant: malformed input, repeated interruptions, prompt injection, cancel/resume, stale state, dirty worktree, hung or long commands, flaky tests, misleading success output.
|
||||||
|
Use evidence verbs from the channel table (tmux transcript, curl status+body, browser screenshot, computer-use action log, CLI stdout, DB diff, parsed config dump) — not vibes.
|
||||||
|
"Tests pass" is supporting signal, NEVER completion proof. Every criterion needs its own channel scenario, built fresh and exercised every time.
|
||||||
|
|
||||||
|
**Plan for maximum parallelism.** Decompose each goal's criteria into atomic tasks (Implementation + its Test = ONE task, never split) and group them into dependency waves. Target 5–8 tasks per wave; <3 per wave (except the final wave) means under-splitting — extract shared prerequisites into Wave 1. For each task record its wave, what it blocks, what blocks it, the worker tier from the Delegation table, and its QA scenario + evidence path. Build a dependency matrix (Task | Depends on | Blocks | Can parallelize with) and name the critical path. Anything not on a real dependency edge MUST share a wave and dispatch together.
|
||||||
|
Record manual QA notes when behavior is user-visible.
|
||||||
|
Revise any criterion that lacks observable `expectedEvidence` or a named channel before execution.
|
||||||
|
|
||||||
|
### 3. Inspect state
|
||||||
|
Run `omo ulw-loop status --json`.
|
||||||
|
Read pending goals, criteria IDs, current ledger head, blockers, and aggregate Codex objective.
|
||||||
|
|
||||||
|
## Execution Loop
|
||||||
|
Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures at 3.
|
||||||
|
|
||||||
|
### Acquire Next Goal
|
||||||
|
1. Run `omo ulw-loop complete-goals --json` and read the handoff, including criteria.
|
||||||
|
2. Call `get_goal` and inspect active Codex state.
|
||||||
|
3. Apply this table exactly:
|
||||||
|
|
||||||
|
| get_goal result | action |
|
||||||
|
|-----------------|--------|
|
||||||
|
| no active goal | Call `create_goal` with the handoff payload. |
|
||||||
|
| same aggregate objective active | Continue the current ulw-loop story. |
|
||||||
|
| different goal active | STOP. Checkpoint blocked and surface the conflict. |
|
||||||
|
4. If retrying failed work, run `omo ulw-loop complete-goals --retry-failed --json`.
|
||||||
|
5. Never create a second Codex goal for the same aggregate objective.
|
||||||
|
|
||||||
|
### Per-Criterion Cycle
|
||||||
|
1. PLAN: read `criterion.scenario`, `criterion.expectedEvidence`, prior ledger entries, and safety bounds. Identify which tasks in the current wave are independent.
|
||||||
|
2. Register atomic todos: `path: <action> for <criterion> - verify by <check>`.
|
||||||
|
3. DELEGATE-IN-PARALLEL: dispatch every independent task in the wave at once via right-sized `spawn_agent` workers (Delegation table). Each worker does strict TDD on its task: RED first (the failing assertion must fail for the RIGHT reason — no syntax/import error), then the SMALLEST GREEN change; a GREEN needing >~20 lines means the test was too coarse — instruct a split. Serialize only on a NAMED dependency.
|
||||||
|
4. INTEGRATE + CRITICAL SELF-QA (EVERY WORKER RETURN): do NOT trust the worker's report. Read the diff yourself, re-run its tests, and run LSP diagnostics on the changed files. Treat "done" as a claim to disprove. If the diff drifts, the test is hollow, or evidence is missing, RESPAWN the worker with the specific failure context. Forward every finding/learning to subsequent workers.
|
||||||
|
5. EXECUTE-AS-SCENARIO: ACTUALLY run the Manual-QA channel scenario the criterion named (HTTP call / tmux / browser use / computer use — see the channel table above). Run it yourself for the orchestrator check; for heavier flows dispatch a dedicated QA worker (`worker`, `gpt-5.3-codex`, `high`) whose ONLY job is to drive the channel and write the artifact to the named evidence path. The unit suite being green is NEVER substitute. If the scenario FAILS, respawn the implementing worker with the captured failure — do not hand-patch around it.
|
||||||
|
6. CAPTURE: collect the observable artifact path: transcript, stdout, screenshot, assertion, status+body, diff, or parsed dump. No artifact written at the evidence path — not done; record BLOCKED and respawn QA.
|
||||||
|
7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND `close_agent` on every finished worker. Embed a one-line cleanup receipt in the evidence string, e.g. `cleanup: killed 12345; tmux kill-session ulw-qa-foo; rm -rf /tmp/ulw.aB12cD; close_agent w-3`. Missing receipt → record BLOCKED, not PASS.
|
||||||
|
8. RECORD exactly one result:
|
||||||
|
- PASS: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status pass --evidence "<observable> | <cleanup receipt>" --json`
|
||||||
|
- FAIL: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status fail --evidence "<observable> | <cleanup receipt>" --notes "<diagnosis>" --json`
|
||||||
|
- BLOCKED: `omo ulw-loop record-evidence --goal-id <id> --criterion-id <id> --status blocked --evidence "<observable>" --notes "<safety/blocker/leftover-state>" --json`
|
||||||
|
9. If actual does not match expected, diagnose, respawn the right-sized worker with the failure context to fix minimally, and rerun the SAME criterion (including a fresh cleanup).
|
||||||
|
10. After 3 same-criterion failures, exit the goal with diagnosis.
|
||||||
|
11. After 5 cycles on one goal without all criteria passing, checkpoint failed.
|
||||||
|
12. Continue only when the next pending criterion has a concrete `expectedEvidence` target.
|
||||||
|
|
||||||
|
### Goal Completion
|
||||||
|
1. Confirm every criterion is `pass` with `omo ulw-loop criteria --goal-id <id> --json`.
|
||||||
|
2. Call `get_goal` for a fresh snapshot.
|
||||||
|
3. Run `omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<criteria evidence summary>" --codex-goal-json <snapshot> --json`.
|
||||||
|
4. If blocked or failed, checkpoint with `--status blocked` or `--status failed` and include diagnosis evidence.
|
||||||
|
5. If this is the final goal, run the final quality gate first and pass `--quality-gate-json`.
|
||||||
|
|
||||||
|
## Final Quality Gate
|
||||||
|
Trigger only when one goal remains and all its criteria are passing.
|
||||||
|
1. Run targeted verification for changed behavior.
|
||||||
|
2. Run `ai-slop-cleaner` on changed files. If no relevant edits exist, record a passed no-op cleaner report.
|
||||||
|
3. Rerun verification after cleanup.
|
||||||
|
4. Run `$code-review`.
|
||||||
|
5. Clean review means `codeReview.recommendation == "APPROVE"` and `codeReview.architectStatus == "CLEAR"`.
|
||||||
|
6. If review is non-clean, run `omo ulw-loop record-review-blockers --goal-id <id> --title "<...>" --objective "<...>" --evidence "<review findings>" --codex-goal-json <snapshot> --json`.
|
||||||
|
7. If clean, checkpoint final completion:
|
||||||
|
```sh
|
||||||
|
omo ulw-loop checkpoint --goal-id <id> --status complete --evidence "<e2e evidence + manual QA notes>" --codex-goal-json <snapshot> --quality-gate-json <json-or-path> --json
|
||||||
|
```
|
||||||
|
`--quality-gate-json` shape:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"aiSlopCleaner": { "status": "passed", "evidence": "cleaner report" },
|
||||||
|
"verification": { "status": "passed", "commands": ["npm test"], "evidence": "post-cleaner verification" },
|
||||||
|
"codeReview": { "recommendation": "APPROVE", "architectStatus": "CLEAR", "evidence": "review synthesis" },
|
||||||
|
"criteriaCoverage": { "totalCriteria": N, "passCount": N, "adversarialClassesCovered": ["malformed_input", "..."] }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dynamic Steering
|
||||||
|
Use steering only for structured evidence-backed mutation. Reject natural-language steering requests.
|
||||||
|
|
||||||
|
| Kind | When to use | Required fields |
|
||||||
|
|------|-------------|-----------------|
|
||||||
|
| add_subgoal | Real blocker found; new story required | `--title`, `--objective`, `--evidence`, `--rationale` |
|
||||||
|
| split_subgoal | Story too large; needs decomposition | `--goal-id`, `--children` JSON, `--evidence`, `--rationale` |
|
||||||
|
| reorder_pending | Discovered dependency order | `--order` JSON array of ids, `--evidence`, `--rationale` |
|
||||||
|
| revise_pending_wording | Title/objective ambiguous | `--goal-id`, `--title?`, `--objective?`, `--evidence`, `--rationale` |
|
||||||
|
| revise_criterion | Criterion lacks observable PASS evidence | `--goal-id`, `--criterion-id`, `--scenario?`, `--expected-evidence?`, `--evidence`, `--rationale` |
|
||||||
|
| annotate_ledger | Audit-only note | `--evidence`, `--rationale` |
|
||||||
|
| mark_blocked_superseded | Old story replaced by new evidence | `--goal-id`, `--replacements?`, `--evidence`, `--rationale` |
|
||||||
|
|
||||||
|
Command form: `omo ulw-loop steer --kind <kind> [<kind-specific-fields>] --evidence "<...>" --rationale "<...>" --json`.
|
||||||
|
Structured prompt directives accepted: `OMO_ULW_LOOP_STEER: { ... }`, `omo.ulw-loop.steer: {...}`, `omo ulw-loop steer: {...}`.
|
||||||
|
|
||||||
|
## Constraints
|
||||||
|
1. NEVER call `update_goal` mid-aggregate; only on final story after the quality gate passes.
|
||||||
|
2. NEVER call `create_goal` when `get_goal` shows a different active goal.
|
||||||
|
3. NEVER mark `criterion.status == "pass"` without captured observable evidence in `record-evidence`.
|
||||||
|
4. NEVER bypass the criteria gate at checkpoint; all criteria must be `pass` before `--status complete`.
|
||||||
|
5. Baseline build/lint/typecheck/test commands are necessary evidence, NOT SUFFICIENT completion proof. Criteria coverage with observable evidence is the gate.
|
||||||
|
6. Treat `.omo/ulw-loop/ledger.jsonl` as the durable audit trail; checkpoint after every success or failure.
|
||||||
|
7. Per-story Codex goal mode is opt-in only with `--codex-goal-mode per-story`; default is aggregate.
|
||||||
|
8. Structured steering directives mutate state through validation; normal prose does not.
|
||||||
|
9. Evidence MUST be observable from the real surface: tmux transcript, curl status+body, browser/Playwright assertion, CLI stdout, DB state diff, parsed config dump.
|
||||||
|
10. Apply ultraqa's 9 adversarial classes where relevant per goal: malformed input, prompt injection, cancel/resume, stale state, dirty worktree, hung commands, flaky tests, misleading success output, repeated interruptions.
|
||||||
|
11. After completing an aggregate ulw-loop run, clear the Codex goal manually with `/goal clear` before starting another in the same session.
|
||||||
|
12. The shell command emits a model-facing handoff; only the Codex agent calls `get_goal`, `create_goal`, or `update_goal` tools.
|
||||||
|
13. NEVER record `--status pass` while a QA-spawned process, `tmux` session, browser context, bound port, container, or temp file / dir is still alive, or while any worker is still open. The evidence string MUST include the cleanup receipt. Leftover runtime state = BLOCKED, not PASS.
|
||||||
|
14. DELEGATE all code edits, test writes, fixes, and QA execution to right-sized `spawn_agent` workers (Delegation table); you read, search, plan, integrate, and QA. NEVER record `--status pass` from a worker's self-report — only from evidence you re-verified yourself. Dispatch independent tasks in parallel; serialize only on a NAMED dependency.
|
||||||
|
|
||||||
|
## Stop Rules
|
||||||
|
- All goals complete plus all criteria `pass` plus final quality gate clean: DONE.
|
||||||
|
- 3x same criterion failure: checkpoint failed, surface diagnosis.
|
||||||
|
- 5 cycles on one goal without all-pass: checkpoint failed, surface.
|
||||||
|
- Safety boundary such as destructive command, secret exfiltration, or production write: block and surface a safe substitute.
|
||||||
|
- Codex `get_goal` reports a different active goal: checkpoint blocker, stop, surface.
|
||||||
|
- Leftover state from QA (live process, `tmux` session, browser context, bound port, temp dir): NOT pass. Clean up, append the receipt, then continue.
|
||||||
|
- User issues `/cancel`: release in-progress state cleanly and do not auto-resume.
|
||||||
+1
-1
@@ -2,5 +2,5 @@ interface:
|
|||||||
display_name: "ulw loop"
|
display_name: "ulw loop"
|
||||||
short_description: "Goal-like ultrawork loop for systematic decomposition"
|
short_description: "Goal-like ultrawork loop for systematic decomposition"
|
||||||
search_terms:
|
search_terms:
|
||||||
- "ultragoal"
|
- "ulw-loop"
|
||||||
default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints."
|
default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints."
|
||||||
@@ -47,7 +47,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i
|
|||||||
"components/rules/dist/cli.js",
|
"components/rules/dist/cli.js",
|
||||||
"components/start-work-continuation/dist/cli.js",
|
"components/start-work-continuation/dist/cli.js",
|
||||||
"components/telemetry/dist/cli.js",
|
"components/telemetry/dist/cli.js",
|
||||||
"components/ultragoal/dist/cli.js",
|
"components/ulw-loop/dist/cli.js",
|
||||||
"components/ultrawork/dist/cli.js",
|
"components/ultrawork/dist/cli.js",
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -55,10 +55,10 @@ test("#given isolated components #when hooks are inspected #then commands stay i
|
|||||||
for (const marker of componentMarkers) {
|
for (const marker of componentMarkers) {
|
||||||
assert.match(text, new RegExp(marker.replaceAll("/", "\\/")));
|
assert.match(text, new RegExp(marker.replaceAll("/", "\\/")));
|
||||||
}
|
}
|
||||||
assert.doesNotMatch(text, /codex-(comment-checker|lsp|rules|telemetry|ultragoal|ultrawork)@/);
|
assert.doesNotMatch(text, /codex-(comment-checker|lsp|rules|telemetry|ulw-loop|ultrawork)@/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ultragoal guards budgeted create_goal calls", async () => {
|
test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ulw-loop guards budgeted create_goal calls", async () => {
|
||||||
// given
|
// given
|
||||||
const hooks = await readJson("hooks/hooks.json");
|
const hooks = await readJson("hooks/hooks.json");
|
||||||
const text = JSON.stringify(hooks);
|
const text = JSON.stringify(hooks);
|
||||||
@@ -67,7 +67,7 @@ test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ult
|
|||||||
const preToolUseGroups = hooks.hooks.PreToolUse;
|
const preToolUseGroups = hooks.hooks.PreToolUse;
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert.match(text, /components\/ultragoal\/dist\/cli\.js/);
|
assert.match(text, /components\/ulw-loop\/dist\/cli\.js/);
|
||||||
assert.match(text, /hook pre-tool-use/);
|
assert.match(text, /hook pre-tool-use/);
|
||||||
assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^create_goal$"]);
|
assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^create_goal$"]);
|
||||||
});
|
});
|
||||||
@@ -128,8 +128,8 @@ test("#given component directories #when scanned #then only intentional resource
|
|||||||
"rules",
|
"rules",
|
||||||
"start-work-continuation",
|
"start-work-continuation",
|
||||||
"telemetry",
|
"telemetry",
|
||||||
"ultragoal",
|
|
||||||
"ultrawork",
|
"ultrawork",
|
||||||
|
"ulw-loop",
|
||||||
]);
|
]);
|
||||||
for (const name of componentNames) {
|
for (const name of componentNames) {
|
||||||
const expectedManifest = expectedComponentManifests.get(name);
|
const expectedManifest = expectedComponentManifests.get(name);
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ const expectedSkills = [
|
|||||||
"review-work",
|
"review-work",
|
||||||
"rules",
|
"rules",
|
||||||
"start-work",
|
"start-work",
|
||||||
"ultragoal",
|
"ulw-loop",
|
||||||
];
|
];
|
||||||
|
|
||||||
test("#given synced aggregate Codex skills #when inspected #then component and shared skills are present", async () => {
|
test("#given synced aggregate Codex skills #when inspected #then component and shared skills are present", async () => {
|
||||||
@@ -41,9 +41,9 @@ test("#given synced aggregate Codex skills #when inspected #then component and s
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("#given synced ultragoal skill #when Codex hint metadata is inspected #then ulw-loop surfaces the ultragoal alias", async () => {
|
test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then ulw-loop surfaces the ulw-loop alias", async () => {
|
||||||
// given
|
// given
|
||||||
const skillRoot = join(root, "skills", "ultragoal");
|
const skillRoot = join(root, "skills", "ulw-loop");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
|
const skill = await readFile(join(skillRoot, "SKILL.md"), "utf8");
|
||||||
@@ -53,21 +53,21 @@ test("#given synced ultragoal skill #when Codex hint metadata is inspected #then
|
|||||||
assert.match(skill, /^---\nname: ulw-loop\n/m);
|
assert.match(skill, /^---\nname: ulw-loop\n/m);
|
||||||
assert.match(skill, /Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps\./);
|
assert.match(skill, /Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps\./);
|
||||||
assert.match(interfaceMetadata, /display_name: "ulw loop"/);
|
assert.match(interfaceMetadata, /display_name: "ulw loop"/);
|
||||||
assert.doesNotMatch(interfaceMetadata, /ulw-loop \/ ultragoal/);
|
assert.doesNotMatch(interfaceMetadata, /ulw-loop \/ ulw-loop/);
|
||||||
assert.match(interfaceMetadata, /short_description: "Goal-like ultrawork loop for systematic decomposition"/);
|
assert.match(interfaceMetadata, /short_description: "Goal-like ultrawork loop for systematic decomposition"/);
|
||||||
assert.match(interfaceMetadata, /default_prompt: "Use \$ulw-loop/);
|
assert.match(interfaceMetadata, /default_prompt: "Use \$ulw-loop/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("#given synced ultragoal skill #when Codex hint metadata is inspected #then ultragoal remains discoverable as an alias", async () => {
|
test("#given synced ulw-loop skill #when Codex hint metadata is inspected #then ulw-loop remains discoverable as an alias", async () => {
|
||||||
// given
|
// given
|
||||||
const skillRoot = join(root, "skills", "ultragoal");
|
const skillRoot = join(root, "skills", "ulw-loop");
|
||||||
|
|
||||||
// when
|
// when
|
||||||
const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8");
|
const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8");
|
||||||
|
|
||||||
// then
|
// then
|
||||||
assert.match(interfaceMetadata, /search_terms:/);
|
assert.match(interfaceMetadata, /search_terms:/);
|
||||||
assert.match(interfaceMetadata, /- "ultragoal"/);
|
assert.match(interfaceMetadata, /- "ulw-loop"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("#given synced aggregate Codex skills #when they contain OpenCode orchestration examples #then Codex tool compatibility guidance is injected", async () => {
|
test("#given synced aggregate Codex skills #when they contain OpenCode orchestration examples #then Codex tool compatibility guidance is injected", async () => {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ async function makeFixture(): Promise<{ codexHome: string; pluginRoot: string }>
|
|||||||
const codexHome = join(root, "codex")
|
const codexHome = join(root, "codex")
|
||||||
const pluginRoot = join(root, "plugin")
|
const pluginRoot = join(root, "plugin")
|
||||||
await mkdir(join(pluginRoot, "components", "ultrawork", "agents"), { recursive: true })
|
await mkdir(join(pluginRoot, "components", "ultrawork", "agents"), { recursive: true })
|
||||||
await mkdir(join(pluginRoot, "components", "ultragoal", "agents"), { recursive: true })
|
await mkdir(join(pluginRoot, "components", "ulw-loop", "agents"), { recursive: true })
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"),
|
join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"),
|
||||||
'name = "explorer"\n',
|
'name = "explorer"\n',
|
||||||
@@ -22,7 +22,7 @@ async function makeFixture(): Promise<{ codexHome: string; pluginRoot: string }>
|
|||||||
'name = "librarian"\n',
|
'name = "librarian"\n',
|
||||||
)
|
)
|
||||||
await writeFile(
|
await writeFile(
|
||||||
join(pluginRoot, "components", "ultragoal", "agents", "planner.toml"),
|
join(pluginRoot, "components", "ulw-loop", "agents", "planner.toml"),
|
||||||
'name = "planner"\n',
|
'name = "planner"\n',
|
||||||
)
|
)
|
||||||
return { codexHome, pluginRoot }
|
return { codexHome, pluginRoot }
|
||||||
@@ -160,7 +160,7 @@ describe("linkCachedPluginAgents", () => {
|
|||||||
// then
|
// then
|
||||||
const targets = linked.map((entry) => entry.target).sort()
|
const targets = linked.map((entry) => entry.target).sort()
|
||||||
expect(targets).toContain(join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"))
|
expect(targets).toContain(join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"))
|
||||||
expect(targets).toContain(join(pluginRoot, "components", "ultragoal", "agents", "planner.toml"))
|
expect(targets).toContain(join(pluginRoot, "components", "ulw-loop", "agents", "planner.toml"))
|
||||||
})
|
})
|
||||||
|
|
||||||
test("returns empty list when plugin has no bundled agents", async () => {
|
test("returns empty list when plugin has no bundled agents", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user