From 8c6e8e498695f784914c9ce8bfe5311f98c65413 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Fri, 29 May 2026 12:38:22 +0900 Subject: [PATCH] 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. --- CHANGELOG.md | 2 +- README.ja.md | 6 +- README.ko.md | 6 +- README.md | 8 +- README.ru.md | 6 +- README.zh-cn.md | 6 +- docs/guide/installation.md | 12 +- packages/omo-codex/MARKETPLACE.md | 2 +- packages/omo-codex/README.md | 6 +- packages/omo-codex/plugin/README.md | 2 +- .../ultragoal/skills/ultragoal/SKILL.md | 198 ---------------- .../plugin/components/ultragoal/src/paths.ts | 27 --- .../test/fixtures/codex-goal-snapshot.json | 1 - .../components/ultragoal/test/paths.test.ts | 37 --- .../{ultragoal => ulw-loop}/.gitattributes | 0 .../{ultragoal => ulw-loop}/.gitignore | 0 .../{ultragoal => ulw-loop}/AGENTS.md | 6 +- .../{ultragoal => ulw-loop}/CHANGELOG.md | 2 +- .../{ultragoal => ulw-loop}/LICENSE | 0 .../components/{ultragoal => ulw-loop}/NOTICE | 4 +- .../{ultragoal => ulw-loop}/README.md | 20 +- .../{ultragoal => ulw-loop}/biome.json | 0 .../{ultragoal => ulw-loop}/hooks/hooks.json | 4 +- .../{ultragoal => ulw-loop}/package.json | 10 +- .../skills/ulw-loop}/.gitkeep | 0 .../ulw-loop/skills/ulw-loop/SKILL.md | 221 ++++++++++++++++++ .../skills/ulw-loop}/agents/openai.yaml | 2 +- .../{ultragoal => ulw-loop}/src/.gitkeep | 0 .../{ultragoal => ulw-loop}/src/checkpoint.ts | 74 +++--- .../src/cli-arg-parser.ts | 12 +- .../src/cli-commands.ts | 80 +++---- .../{ultragoal => ulw-loop}/src/cli-output.ts | 40 ++-- .../src/cli-steering.ts | 58 ++--- .../{ultragoal => ulw-loop}/src/cli.ts | 10 +- .../src/codex-goal-instruction.ts | 44 ++-- .../src/codex-goal-snapshot.ts | 0 .../{ultragoal => ulw-loop}/src/codex-hook.ts | 14 +- .../{ultragoal => ulw-loop}/src/evidence.ts | 46 ++-- .../src/goal-status.ts | 44 ++-- .../plugin/components/ulw-loop/src/paths.ts | 27 +++ .../{ultragoal => ulw-loop}/src/plan-crud.ts | 60 ++--- .../{ultragoal => ulw-loop}/src/plan-io.ts | 50 ++-- .../src/quality-gate.ts | 16 +- .../src/review-blockers.ts | 36 +-- .../{ultragoal => ulw-loop}/src/steering.ts | 88 +++---- .../{ultragoal => ulw-loop}/src/types.ts | 122 +++++----- .../test/checkpoint.test.ts | 104 ++++----- .../test/cli-commands.test.ts | 78 +++---- .../test/cli-helpers.test.ts | 52 ++--- .../test/cli-steering.test.ts | 34 +-- .../test/codex-goal-instruction.test.ts | 26 +-- .../test/codex-goal-snapshot.test.ts | 2 +- .../test/codex-hook.test.ts | 70 +++--- .../test/evidence-criteria-gate.test.ts | 24 +- .../test/evidence.test.ts | 40 ++-- .../test/fixtures/.gitkeep | 0 .../test/fixtures/codex-goal-snapshot.json | 1 + .../test/fixtures/sample-brief.md | 0 .../test/fixtures/sample-plan.json | 2 +- .../test/fixtures/sample-quality-gate.json | 0 .../test/fixtures/steering-proposal.json | 0 .../test/fixtures/user-prompt-submit.json | 2 +- .../test/goal-status.test.ts | 42 ++-- .../test/package-smoke.test.ts | 42 ++-- .../components/ulw-loop/test/paths.test.ts | 31 +++ .../test/plan-crud.test.ts | 86 +++---- .../test/plan-io.test.ts | 64 ++--- .../test/quality-gate.test.ts | 44 ++-- .../test/review-blockers.test.ts | 64 ++--- .../test/steering.test.ts | 101 ++++---- .../test/types.test.ts | 42 ++-- .../tsconfig.build.json | 0 .../{ultragoal => ulw-loop}/tsconfig.json | 0 .../{ultragoal => ulw-loop}/vitest.config.ts | 0 packages/omo-codex/plugin/hooks/hooks.json | 6 +- packages/omo-codex/plugin/package.json | 2 +- .../omo-codex/plugin/scripts/sync-skills.mjs | 2 +- .../plugin/skills/ultragoal/SKILL.md | 198 ---------------- .../skills/{ultragoal => ulw-loop}/.gitkeep | 0 .../omo-codex/plugin/skills/ulw-loop/SKILL.md | 221 ++++++++++++++++++ .../ulw-loop}/agents/openai.yaml | 2 +- .../omo-codex/plugin/test/aggregate.test.mjs | 10 +- .../plugin/test/sync-skills.test.mjs | 14 +- .../link-cached-plugin-agents.test.ts | 6 +- 84 files changed, 1429 insertions(+), 1392 deletions(-) delete mode 100644 packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/SKILL.md delete mode 100644 packages/omo-codex/plugin/components/ultragoal/src/paths.ts delete mode 100644 packages/omo-codex/plugin/components/ultragoal/test/fixtures/codex-goal-snapshot.json delete mode 100644 packages/omo-codex/plugin/components/ultragoal/test/paths.test.ts rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/.gitattributes (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/.gitignore (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/AGENTS.md (88%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/CHANGELOG.md (95%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/LICENSE (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/NOTICE (57%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/README.md (69%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/biome.json (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/hooks/hooks.json (79%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/package.json (79%) rename packages/omo-codex/plugin/components/{ultragoal/skills/ultragoal => ulw-loop/skills/ulw-loop}/.gitkeep (100%) create mode 100644 packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md rename packages/omo-codex/plugin/{skills/ultragoal => components/ulw-loop/skills/ulw-loop}/agents/openai.yaml (93%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/.gitkeep (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/checkpoint.ts (59%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/cli-arg-parser.ts (81%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/cli-commands.ts (58%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/cli-output.ts (52%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/cli-steering.ts (66%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/cli.ts (64%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/codex-goal-instruction.ts (68%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/codex-goal-snapshot.ts (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/codex-hook.ts (89%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/evidence.ts (60%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/goal-status.ts (57%) create mode 100644 packages/omo-codex/plugin/components/ulw-loop/src/paths.ts rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/plan-crud.ts (63%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/plan-io.ts (51%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/quality-gate.ts (90%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/review-blockers.ts (60%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/steering.ts (73%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/src/types.ts (58%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/checkpoint.test.ts (55%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/cli-commands.test.ts (68%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/cli-helpers.test.ts (78%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/cli-steering.test.ts (91%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/codex-goal-instruction.test.ts (82%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/codex-goal-snapshot.test.ts (98%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/codex-hook.test.ts (71%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/evidence-criteria-gate.test.ts (71%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/evidence.test.ts (83%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/.gitkeep (100%) create mode 100644 packages/omo-codex/plugin/components/ulw-loop/test/fixtures/codex-goal-snapshot.json rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/sample-brief.md (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/sample-plan.json (97%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/sample-quality-gate.json (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/steering-proposal.json (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/fixtures/user-prompt-submit.json (55%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/goal-status.test.ts (84%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/package-smoke.test.ts (78%) create mode 100644 packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/plan-crud.test.ts (68%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/plan-io.test.ts (69%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/quality-gate.test.ts (78%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/review-blockers.test.ts (67%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/steering.test.ts (71%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/test/types.test.ts (55%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/tsconfig.build.json (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/tsconfig.json (100%) rename packages/omo-codex/plugin/components/{ultragoal => ulw-loop}/vitest.config.ts (100%) delete mode 100644 packages/omo-codex/plugin/skills/ultragoal/SKILL.md rename packages/omo-codex/plugin/skills/{ultragoal => ulw-loop}/.gitkeep (100%) create mode 100644 packages/omo-codex/plugin/skills/ulw-loop/SKILL.md rename packages/omo-codex/plugin/{components/ultragoal/skills/ultragoal => skills/ulw-loop}/agents/openai.yaml (93%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a32e22430..3419f47ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) - Grok family models registered with `reasoningEffort` support. (PR #4186) - 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 ` 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. - 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. diff --git a/README.ja.md b/README.ja.md index a1f531c85..95f483515 100644 --- a/README.ja.md +++ b/README.ja.md @@ -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)をご覧ください。 -**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 | 何をするのか | | :---: | :------------------------------------------------------- | :------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🤖 | **規律あるエージェント (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) | | ⚡ | **`ultrawork` / `ulw`** | Both | 一言で OK。すべてのエージェント (Ultimate) または Codex `ultrawork` コンポーネント (Light) がアクティブになり、終わるまで止まりません。 | | 🚪 | **[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 | エージェントがサボる?システムが首根っこを掴んで戻します。あなたのタスクは必ず終わります。 | | 💬 | **コメントチェッカー** | Both | コメントから AI 臭い無駄話を排除。両エディションで同じ `@code-yeongyu/comment-checker` バイナリが動作。 | | 📜 | **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 アプリがすべてリアルタイムで動きます。 | | 🔌 | **Claude Code 互換性** | Ultimate | 既存のフック、コマンド、スキル、MCP、プラグイン?すべてここでそのまま動きます。(Codex は独自のネイティブプラグインシステムを保有。) | | 🎯 | **スキル内蔵 MCP** | Ultimate | スキルが独自の MCP サーバーを持ち歩きます。コンテキストが肥大化しません。 | diff --git a/README.ko.md b/README.ko.md index db8cd4a34..63c019c6d 100644 --- a/README.ko.md +++ b/README.ko.md @@ -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)을 참조하세요. -**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 | 하는 일 | | :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🤖 | **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) | | ⚡ | **`ultrawork` / `ulw`** | Both | 한 단어. 모든 에이전트(Ultimate)나 Codex `ultrawork` 컴포넌트(Light)가 켜집니다. 끝날 때까지 멈추지 않습니다. | | 🚪 | **[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 | 에이전트가 놀고 있나요? 시스템이 다시 끌어옵니다. 당신의 작업은 반드시 끝납니다. | | 💬 | **Comment Checker** | Both | 주석에 AI 슬롭 금지. 동일한 `@code-yeongyu/comment-checker` 바이너리가 두 에디션 모두에서 동작. | | 📜 | **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 전부 라이브. | | 🔌 | **Claude Code Compatible** | Ultimate | 쓰시던 hook, command, skill, MCP, plugin 전부 그대로 동작합니다. (Codex는 자체 플러그인 시스템 보유.) | | 🎯 | **Skill-Embedded MCPs** | Ultimate | 스킬이 자기만의 MCP 서버를 들고 다닙니다. 컨텍스트 낭비 없음. | diff --git a/README.md b/README.md index 715312d59..35db74809 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ Install oh-my-openagent. Type `ultrawork`. Done. 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. -- **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. +- **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`, `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. @@ -202,7 +202,7 @@ Even with only the following subscriptions, `ultrawork` works well (this project | | Feature | Edition | What it does | | :---: | :------------------------------------------------------- | :------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🤖 | **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) | | ⚡ | **`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.) | @@ -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. | | 💬 | **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. | -| 🎯 | **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. | | 🔌 | **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. | diff --git a/README.ru.md b/README.ru.md index 3790f0ed8..c70cdae30 100644 --- a/README.ru.md +++ b/README.ru.md @@ -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). -**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 | Что делает | | --- | -------------------------------------------------------- | :------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 🤖 | **Дисциплинированные агенты** | 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) | | ⚡ | **`ultrawork` / `ulw`** | Both | Одно слово. Все агенты (Ultimate) или Codex-компонент `ultrawork` (Light) активируются. Не останавливается, пока задача не выполнена. | | 🚪 | **[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 | Агент завис? Система немедленно возвращает его в работу. Ваша задача будет выполнена, точка. | | 💬 | **Comment Checker** | Both | Никакого AI-мусора в комментариях. Тот же бинарник `@code-yeongyu/comment-checker` работает в обеих редакциях. | | 📜 | **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. Всё живое. | | 🔌 | **Совместимость с Claude Code** | Ultimate | Ваши хуки, команды, навыки, MCP и плагины? Всё работает без изменений. (У Codex своя нативная плагин-система.) | | 🎯 | **MCP, встроенные в навыки** | Ultimate | Навыки несут собственные MCP-серверы. Никакого раздувания контекста. | diff --git a/README.zh-cn.md b/README.zh-cn.md index 8f3106ff9..eff61632d 100644 --- a/README.zh-cn.md +++ b/README.zh-cn.md @@ -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)。 -**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 | 功能说明 | | :---: | :-------------------------------------------------------------- | :------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | 🤖 | **自律军团 (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) | | ⚡ | **`ultrawork` / `ulw`** | Both | 一键触发,所有智能体(Ultimate)或 Codex `ultrawork` 组件(Light)出动。任务完成前绝不罢休。 | | 🚪 | **[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 想要摸鱼?系统直接揪着领子拽回来。你的任务,必须完成。 | | 💬 | **注释审查员** | Both | 剔除带有浓烈 AI 味的冗余注释。同一个 `@code-yeongyu/comment-checker` 二进制在两个版本中运行。 | | 📜 | **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 工具,全都在实时会话中完成。 | | 🔌 | **Claude Code 兼容** | Ultimate | 你现有的 Hooks、命令、技能、MCP 和插件?全都能无缝迁移过来。(Codex 拥有其自己的原生插件系统。) | | 🎯 | **技能内嵌 MCP** | Ultimate | 技能自带其所需的 MCP 服务器。按需开启,不会撑爆你的上下文窗口。 | diff --git a/docs/guide/installation.md b/docs/guide/installation.md index 945b7cb82..7b5f5d40b 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -2,8 +2,8 @@ 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. -- **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. +- **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`, `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. @@ -242,7 +242,7 @@ grep -A4 'marketplaces.sisyphuslabs' ~/.codex/config.toml grep -A2 'omo@sisyphuslabs' ~/.codex/config.toml # 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 --help @@ -546,7 +546,7 @@ Skip this section if `--platform=opencode`. Otherwise, the user installed the ** #### What was installed - **Plugin cache:** `~/.codex/plugins/cache/sisyphuslabs/omo//` -- **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 #### 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 | | `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` | -| `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 @@ -768,7 +768,7 @@ rm -rf ~/.codex/plugins/cache/sisyphuslabs # 3. Optional: remove the component symlinks 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 diff --git a/packages/omo-codex/MARKETPLACE.md b/packages/omo-codex/MARKETPLACE.md index 410b2d8f2..063c779f4 100644 --- a/packages/omo-codex/MARKETPLACE.md +++ b/packages/omo-codex/MARKETPLACE.md @@ -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/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/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 diff --git a/packages/omo-codex/README.md b/packages/omo-codex/README.md index b2342a844..efd5d8e47 100644 --- a/packages/omo-codex/README.md +++ b/packages/omo-codex/README.md @@ -1,6 +1,6 @@ # @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 @@ -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. - `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`. -- `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. ## 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-lsp](https://github.com/code-yeongyu/codex-lsp) - [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) diff --git a/packages/omo-codex/plugin/README.md b/packages/omo-codex/plugin/README.md index a5a9bdc73..f5144a269 100644 --- a/packages/omo-codex/plugin/README.md +++ b/packages/omo-codex/plugin/README.md @@ -8,6 +8,6 @@ Internally each component remains isolated under `components/`: - `components/rules` - `components/lsp` - `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. diff --git a/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/SKILL.md b/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/SKILL.md deleted file mode 100644 index 86918797d..000000000 --- a/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/SKILL.md +++ /dev/null @@ -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-`, 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 "" --json -omo ultragoal create-goals --brief-file --json -cat | 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: for - verify by `. -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-`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :` 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 --criterion-id --status pass --evidence " | " --json` - - FAIL: `omo ultragoal record-evidence --goal-id --criterion-id --status fail --evidence " | " --notes "" --json` - - BLOCKED: `omo ultragoal record-evidence --goal-id --criterion-id --status blocked --evidence "" --notes "" --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 --json`. -2. Call `get_goal` for a fresh snapshot. -3. Run `omo ultragoal checkpoint --goal-id --status complete --evidence "" --codex-goal-json --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 --title "<...>" --objective "<...>" --evidence "" --codex-goal-json --json`. -7. If clean, checkpoint final completion: -```sh -omo ultragoal checkpoint --goal-id --status complete --evidence "" --codex-goal-json --quality-gate-json --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 [] --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. diff --git a/packages/omo-codex/plugin/components/ultragoal/src/paths.ts b/packages/omo-codex/plugin/components/ultragoal/src/paths.ts deleted file mode 100644 index 835d65362..000000000 --- a/packages/omo-codex/plugin/components/ultragoal/src/paths.ts +++ /dev/null @@ -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("/"); -} diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/codex-goal-snapshot.json b/packages/omo-codex/plugin/components/ultragoal/test/fixtures/codex-goal-snapshot.json deleted file mode 100644 index f88a3c5e7..000000000 --- a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/codex-goal-snapshot.json +++ /dev/null @@ -1 +0,0 @@ -{ "goal": { "objective": "Complete the durable ultragoal plan", "status": "active" } } diff --git a/packages/omo-codex/plugin/components/ultragoal/test/paths.test.ts b/packages/omo-codex/plugin/components/ultragoal/test/paths.test.ts deleted file mode 100644 index aa35f4600..000000000 --- a/packages/omo-codex/plugin/components/ultragoal/test/paths.test.ts +++ /dev/null @@ -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"); - }); -}); diff --git a/packages/omo-codex/plugin/components/ultragoal/.gitattributes b/packages/omo-codex/plugin/components/ulw-loop/.gitattributes similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/.gitattributes rename to packages/omo-codex/plugin/components/ulw-loop/.gitattributes diff --git a/packages/omo-codex/plugin/components/ultragoal/.gitignore b/packages/omo-codex/plugin/components/ulw-loop/.gitignore similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/.gitignore rename to packages/omo-codex/plugin/components/ulw-loop/.gitignore diff --git a/packages/omo-codex/plugin/components/ultragoal/AGENTS.md b/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md similarity index 88% rename from packages/omo-codex/plugin/components/ultragoal/AGENTS.md rename to packages/omo-codex/plugin/components/ulw-loop/AGENTS.md index bc76bb207..54f1a3c03 100644 --- a/packages/omo-codex/plugin/components/ultragoal/AGENTS.md +++ b/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md @@ -37,9 +37,9 @@ Conventions for human contributors and AI agents working on this repository. ## Branding -- Repo artifacts live under `.omo/ultragoal/` paths. -- Environment variables use the `OMO_ULTRAGOAL_*` prefix. -- CLI commands use the `omo ultragoal` form. +- Repo artifacts live under `.omo/ulw-loop/` paths. +- Environment variables use the `OMO_ULW_LOOP_*` prefix. +- CLI commands use the `omo ulw-loop` form. - Do not use any alternate legacy CLI alias anywhere. ## Build and Hooks diff --git a/packages/omo-codex/plugin/components/ultragoal/CHANGELOG.md b/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md similarity index 95% rename from packages/omo-codex/plugin/components/ultragoal/CHANGELOG.md rename to packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md index 72a3e0986..7145a3b66 100644 --- a/packages/omo-codex/plugin/components/ultragoal/CHANGELOG.md +++ b/packages/omo-codex/plugin/components/ulw-loop/CHANGELOG.md @@ -2,6 +2,6 @@ ## [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. - 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. diff --git a/packages/omo-codex/plugin/components/ultragoal/LICENSE b/packages/omo-codex/plugin/components/ulw-loop/LICENSE similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/LICENSE rename to packages/omo-codex/plugin/components/ulw-loop/LICENSE diff --git a/packages/omo-codex/plugin/components/ultragoal/NOTICE b/packages/omo-codex/plugin/components/ulw-loop/NOTICE similarity index 57% rename from packages/omo-codex/plugin/components/ultragoal/NOTICE rename to packages/omo-codex/plugin/components/ulw-loop/NOTICE index 4b0ea0736..feeab2f7f 100644 --- a/packages/omo-codex/plugin/components/ultragoal/NOTICE +++ b/packages/omo-codex/plugin/components/ulw-loop/NOTICE @@ -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 orchestration engine is added in later port waves. diff --git a/packages/omo-codex/plugin/components/ultragoal/README.md b/packages/omo-codex/plugin/components/ulw-loop/README.md similarity index 69% rename from packages/omo-codex/plugin/components/ultragoal/README.md rename to packages/omo-codex/plugin/components/ulw-loop/README.md index d783ab301..49eef5c3c 100644 --- a/packages/omo-codex/plugin/components/ultragoal/README.md +++ b/packages/omo-codex/plugin/components/ulw-loop/README.md @@ -1,4 +1,4 @@ -# codex-ultragoal +# codex-ulw-loop [![ci](https://img.shields.io/badge/ci-pending-lightgrey.svg)](#) [![license: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) @@ -8,13 +8,13 @@ Codex plugin scaffold for durable repo-native multi-goal orchestration with embe | Subcommand | Purpose | |------------|---------| -| `omo ultragoal create-goals` | Create repo-native goals from a brief and seed criteria. | -| `omo ultragoal record-evidence` | Record observable evidence for the active criterion. | -| `omo ultragoal criteria` | Inspect or revise goal success criteria. | -| `omo ultragoal complete-goals` | Complete eligible goals after criteria pass. | -| `omo ultragoal checkpoint` | Refuse completion until criteria and evidence gates pass. | -| `omo ultragoal steer` | Apply steering updates to the plan. | -| `omo ultragoal status` | Report active goal, criteria, and evidence state. | +| `omo ulw-loop create-goals` | Create repo-native goals from a brief and seed criteria. | +| `omo ulw-loop record-evidence` | Record observable evidence for the active criterion. | +| `omo ulw-loop criteria` | Inspect or revise goal success criteria. | +| `omo ulw-loop complete-goals` | Complete eligible goals after criteria pass. | +| `omo ulw-loop checkpoint` | Refuse completion until criteria and evidence gates pass. | +| `omo ulw-loop steer` | Apply steering updates to the plan. | +| `omo ulw-loop status` | Report active goal, criteria, and evidence state. | 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. - `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: @@ -71,5 +71,5 @@ This plugin runs locally. The scaffold does not call a network service by itself ## 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. diff --git a/packages/omo-codex/plugin/components/ultragoal/biome.json b/packages/omo-codex/plugin/components/ulw-loop/biome.json similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/biome.json rename to packages/omo-codex/plugin/components/ulw-loop/biome.json diff --git a/packages/omo-codex/plugin/components/ultragoal/hooks/hooks.json b/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json similarity index 79% rename from packages/omo-codex/plugin/components/ultragoal/hooks/hooks.json rename to packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json index f674eabff..c0d4402dd 100644 --- a/packages/omo-codex/plugin/components/ultragoal/hooks/hooks.json +++ b/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json @@ -7,7 +7,7 @@ "type": "command", "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit", "timeout": 10, - "statusMessage": "checking ultragoal steering" + "statusMessage": "checking ulw-loop steering" } ] } @@ -20,7 +20,7 @@ "type": "command", "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use", "timeout": 5, - "statusMessage": "enforcing unlimited ultragoal budget" + "statusMessage": "enforcing unlimited ulw-loop budget" } ] } diff --git a/packages/omo-codex/plugin/components/ultragoal/package.json b/packages/omo-codex/plugin/components/ulw-loop/package.json similarity index 79% rename from packages/omo-codex/plugin/components/ultragoal/package.json rename to packages/omo-codex/plugin/components/ulw-loop/package.json index 77782d91c..06f46f6ef 100644 --- a/packages/omo-codex/plugin/components/ultragoal/package.json +++ b/packages/omo-codex/plugin/components/ulw-loop/package.json @@ -1,22 +1,22 @@ { - "name": "@code-yeongyu/codex-ultragoal", + "name": "@code-yeongyu/codex-ulw-loop", "version": "0.1.0", "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.", "type": "module", "packageManager": "npm@11.12.1", "license": "MIT", - "homepage": "https://github.com/code-yeongyu/codex-ultragoal", + "homepage": "https://github.com/code-yeongyu/codex-ulw-loop", "repository": { "type": "git", - "url": "git+https://github.com/code-yeongyu/codex-ultragoal.git" + "url": "git+https://github.com/code-yeongyu/codex-ulw-loop.git" }, "bugs": { - "url": "https://github.com/code-yeongyu/codex-ultragoal/issues" + "url": "https://github.com/code-yeongyu/codex-ulw-loop/issues" }, "keywords": [ "codex", "codex-plugin", - "ultragoal", + "ulw-loop", "goal-mode", "orchestration", "evidence", diff --git a/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/.gitkeep b/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/.gitkeep similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/.gitkeep rename to packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/.gitkeep diff --git a/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md b/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md new file mode 100644 index 000000000..7e83e5ca6 --- /dev/null +++ b/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md @@ -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-`, 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 "" --json +omo ulw-loop create-goals --brief-file --json +cat | 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/-.`), 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: for - verify by `. +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-`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :` 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 --criterion-id --status pass --evidence " | " --json` + - FAIL: `omo ulw-loop record-evidence --goal-id --criterion-id --status fail --evidence " | " --notes "" --json` + - BLOCKED: `omo ulw-loop record-evidence --goal-id --criterion-id --status blocked --evidence "" --notes "" --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 --json`. +2. Call `get_goal` for a fresh snapshot. +3. Run `omo ulw-loop checkpoint --goal-id --status complete --evidence "" --codex-goal-json --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 --title "<...>" --objective "<...>" --evidence "" --codex-goal-json --json`. +7. If clean, checkpoint final completion: +```sh +omo ulw-loop checkpoint --goal-id --status complete --evidence "" --codex-goal-json --quality-gate-json --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 [] --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. diff --git a/packages/omo-codex/plugin/skills/ultragoal/agents/openai.yaml b/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml similarity index 93% rename from packages/omo-codex/plugin/skills/ultragoal/agents/openai.yaml rename to packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml index f6855ddbb..a3c91a292 100644 --- a/packages/omo-codex/plugin/skills/ultragoal/agents/openai.yaml +++ b/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/agents/openai.yaml @@ -2,5 +2,5 @@ interface: display_name: "ulw loop" short_description: "Goal-like ultrawork loop for systematic decomposition" search_terms: - - "ultragoal" + - "ulw-loop" default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints." diff --git a/packages/omo-codex/plugin/components/ultragoal/src/.gitkeep b/packages/omo-codex/plugin/components/ulw-loop/src/.gitkeep similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/src/.gitkeep rename to packages/omo-codex/plugin/components/ulw-loop/src/.gitkeep diff --git a/packages/omo-codex/plugin/components/ultragoal/src/checkpoint.ts b/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts similarity index 59% rename from packages/omo-codex/plugin/components/ultragoal/src/checkpoint.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts index 151a4607b..2b99700b7 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/checkpoint.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts @@ -6,23 +6,23 @@ import { resolve } from "node:path"; import { formatCodexGoalReconciliation, readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js"; import { requireAllCriteriaPass } from "./evidence.js"; import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js"; -import { ultragoalBriefPath } from "./paths.js"; -import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js"; +import { ulwLoopBriefPath } from "./paths.js"; +import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js"; import { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, sameBlockerOccurrences, validateQualityGate } from "./quality-gate.js"; -import type { UltragoalAggregateCompletion, UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalQualityGate } from "./types.js"; -import { iso, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js"; +import type { UlwLoopAggregateCompletion, UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopQualityGate } 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 CheckpointUltragoalResult { readonly plan: UltragoalPlan; readonly goal: UltragoalItem; readonly ledgerEntry: UltragoalLedgerEntry; readonly aggregateCompletion?: UltragoalAggregateCompletion } +export interface CheckpointUlwLoopArgs { readonly goalId: string; readonly status: "complete" | "failed" | "blocked"; readonly evidence: string; readonly codexGoalJson?: string; readonly qualityGateJson?: string } +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 nonEmptyEvidence(value: string): string { const trimmed = value.trim(); return trimmed || ultragoalFail("Evidence must be a non-empty string.", "ultragoal_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 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: 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(); - 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 textHasCompletionValidationEvidence(value: string | undefined): boolean { @@ -32,12 +32,12 @@ function textHasCompletionValidationEvidence(value: string | undefined): boolean return done && verified; } -async function snapshotObjectiveMapsToUltragoalPlan(repoRoot: string, snapshotObjective: string): Promise { +async function snapshotObjectiveMapsToUlwLoopPlan(repoRoot: string, snapshotObjective: string): Promise { const actual = normalizeObjective(snapshotObjective).toLowerCase(); - if (textMentionsUltragoalPlanArtifact(actual)) return true; - if (actual.length < 24 || !existsSync(ultragoalBriefPath(repoRoot))) return false; + if (textMentionsUlwLoopPlanArtifact(actual)) return true; + if (actual.length < 24 || !existsSync(ulwLoopBriefPath(repoRoot))) return false; 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)); } catch (error) { 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 { +async function canReconcileCompletedTaskScopedAggregateSnapshot(repoRoot: string, plan: UlwLoopPlan, goal: UlwLoopItem, snapshotObjective: string, evidence: string): Promise { if (codexGoalMode(plan) !== "aggregate") return false; if (goal.status !== "in_progress" || plan.activeGoalId !== goal.id) return false; - if (isFinalRunCompletionCandidate(plan, goal)) return snapshotObjectiveMapsToUltragoalPlan(repoRoot, snapshotObjective); - if (!textMentionsUltragoalPlanArtifact(evidence) || !textMentionsGoalId(evidence, goal.id)) return false; + if (isFinalRunCompletionCandidate(plan, goal)) return snapshotObjectiveMapsToUlwLoopPlan(repoRoot, snapshotObjective); + if (!textMentionsUlwLoopPlanArtifact(evidence) || !textMentionsGoalId(evidence, goal.id)) 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 [ "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 "" --codex-goal-json "".`, + `Record a non-terminal blocker with: omo ulw-loop checkpoint --goal-id ${goal.id} --status blocked --evidence "" --codex-goal-json "".`, "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(" "); } -function buildTaskScopedAggregateReconciliationHint(goal: UltragoalItem, final: boolean): string { +function buildTaskScopedAggregateReconciliationHint(goal: UlwLoopItem, final: boolean): string { 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 { @@ -74,15 +74,15 @@ async function readJsonInput(raw: string | undefined, repoRoot: string): Promise const trimmed = raw.trim(); try { return JSON.parse(trimmed); } catch (error) { if (!(error instanceof SyntaxError)) throw error; } 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"); - 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"); } + 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 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 }; } -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 occurrences = signature === null ? 0 : sameBlockerOccurrences(plan, signature) + 1; 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; } -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 (status === "complete") return "goal_completed"; if (goal.status === "needs_user_decision") return "goal_needs_user_decision"; return status === "blocked" ? "goal_blocked" : "goal_failed"; } -function buildLedger(now: string, args: CheckpointUltragoalArgs, goal: UltragoalItem, qualityGate: UltragoalQualityGate | undefined, codexGoal: unknown, aggregateCompletion: UltragoalAggregateCompletion | undefined): UltragoalLedgerEntry { - const entry: UltragoalLedgerEntry = { at: now, kind: ledgerKind(args.status, goal, aggregateCompletion), goalId: goal.id, status: goal.status, evidence: args.evidence }; +function buildLedger(now: string, args: CheckpointUlwLoopArgs, goal: UlwLoopItem, qualityGate: UlwLoopQualityGate | undefined, codexGoal: unknown, aggregateCompletion: UlwLoopAggregateCompletion | undefined): UlwLoopLedgerEntry { + 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 (qualityGate !== undefined) entry.qualityGate = qualityGate; if (goal.blockerSignature !== undefined) entry.blockerSignature = goal.blockerSignature; @@ -112,15 +112,15 @@ function buildLedger(now: string, args: CheckpointUltragoalArgs, goal: Ultragoal return entry; } -export async function checkpointUltragoal(repoRoot: string, args: CheckpointUltragoalArgs): Promise { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function checkpointUlwLoop(repoRoot: string, args: CheckpointUlwLoopArgs): Promise { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); const goal = findGoal(plan, args.goalId); if (args.status === "complete") requireAllCriteriaPass(goal); const evidence = nonEmptyEvidence(args.evidence); const now = iso(); - let aggregateCompletion: UltragoalAggregateCompletion | undefined; - let qualityGate: UltragoalQualityGate | undefined; + let aggregateCompletion: UlwLoopAggregateCompletion | undefined; + let qualityGate: UlwLoopQualityGate | undefined; let codexGoal: unknown; if (args.status === "complete") { const aggregate = codexGoalMode(plan) === "aggregate"; @@ -131,7 +131,7 @@ export async function checkpointUltragoal(repoRoot: string, args: CheckpointUltr if (!reconciliation.ok) { 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); - 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); } if (final) aggregateCompletion = makeAggregateCompletion(now, evidence, codexGoal); diff --git a/packages/omo-codex/plugin/components/ultragoal/src/cli-arg-parser.ts b/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts similarity index 81% rename from packages/omo-codex/plugin/components/ultragoal/src/cli-arg-parser.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts index e9028f4ce..7d051e151 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/cli-arg-parser.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/cli-arg-parser.ts @@ -1,7 +1,7 @@ // biome-ignore-all format: keep this module under the mandated pure LOC budget. 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 }; @@ -59,7 +59,7 @@ export async function readJsonInput(value: string | undefined): Promise { +export async function ulwLoopCommand(argv: readonly string[]): Promise { const command = argv[0] ?? "help"; const rest = argv.slice(1); const repoRoot = process.cwd(); const json = hasFlag(rest, "--json"); try { 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 "status": return await status(repoRoot, json); case "complete-goals": return await completeGoals(repoRoot, rest, json); @@ -32,12 +32,12 @@ export async function ultragoalCommand(argv: readonly string[]): Promise case "criteria": return await criteria(repoRoot, rest, json); case "record-evidence": return await captureEvidence(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) { - if (error instanceof UltragoalError) process.stderr.write(`[ultragoal] ${error.message}\n`); - else if (error instanceof Error) process.stderr.write(`[ultragoal] unexpected: ${error.message}\n`); - else process.stderr.write("[ultragoal] unknown error\n"); + if (error instanceof UlwLoopError) process.stderr.write(`[ulw-loop] ${error.message}\n`); + else if (error instanceof Error) process.stderr.write(`[ulw-loop] unexpected: ${error.message}\n`); + else process.stderr.write("[ulw-loop] unknown error\n"); return 1; } } @@ -45,26 +45,26 @@ export async function ultragoalCommand(argv: readonly string[]): Promise async function createGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise { 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); - if (!brief.trim()) throw new UltragoalError("Missing brief text. Pass --brief, --brief-file, --from-stdin, or positional text.", "ULTRAGOAL_BRIEF_REQUIRED"); - const plan = await createUltragoalPlan(repoRoot, { brief, codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")), force: hasFlag(argv, "--force") }); - if (json) printJson({ ok: true, plan, summary: summarizeUltragoalPlan(plan) }); - else process.stdout.write(`ultragoal plan created: ${plan.goals.length} goal(s)\nbrief: ${plan.briefPath}\ngoals: ${plan.goalsPath}\nledger: ${plan.ledgerPath}\n`); + 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 createUlwLoopPlan(repoRoot, { brief, codexGoalMode: normalizeCodexGoalMode(readValue(argv, "--codex-goal-mode")), force: hasFlag(argv, "--force") }); + if (json) printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) }); + 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; } async function status(repoRoot: string, json: boolean): Promise { - const plan = await readUltragoalPlan(repoRoot); - if (json) printJson({ ok: true, plan, summary: summarizeUltragoalPlan(plan) }); + const plan = await readUlwLoopPlan(repoRoot); + if (json) printJson({ ok: true, plan, summary: summarizeUlwLoopPlan(plan) }); else printStatus(plan); return 0; } async function completeGoals(repoRoot: string, argv: readonly string[], json: boolean): Promise { - const result = await startNextUltragoal(repoRoot, { retryFailed: hasFlag(argv, "--retry-failed") }); + const result = await startNextUlwLoop(repoRoot, { retryFailed: hasFlag(argv, "--retry-failed") }); if ("done" in result) { const handoff = blockedDecisionHandoff(result.plan); - if (json) printJson({ ok: true, done: true, blocked: handoff.length > 0, handoff, summary: summarizeUltragoalPlan(result.plan), plan: result.plan }); - else process.stdout.write(`${handoff || "ultragoal: all goals complete"}\n`); + if (json) printJson({ ok: true, done: true, blocked: handoff.length > 0, handoff, summary: summarizeUlwLoopPlan(result.plan), plan: result.plan }); + else process.stdout.write(`${handoff || "ulw-loop: all goals complete"}\n`); return 0; } 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 evidence = required(argv, "--evidence"); 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 result = await checkpointUltragoal(repoRoot, qualityGateJson === undefined ? { goalId, status: statusValue, evidence, codexGoalJson } : { goalId, status: statusValue, evidence, codexGoalJson, qualityGateJson }); - if (json) printJson({ ok: true, ...result, summary: summarizeUltragoalPlan(result.plan) }); - else process.stdout.write(`ultragoal checkpoint: ${result.goal.id} -> ${result.goal.status}\n`); + 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: summarizeUlwLoopPlan(result.plan) }); + else process.stdout.write(`ulw-loop checkpoint: ${result.goal.id} -> ${result.goal.status}\n`); return 0; } async function steer(repoRoot: string, argv: readonly string[], json: boolean): Promise { const proposal = await parseSteeringProposal(argv); - const result = await steerUltragoal(repoRoot, proposal); + const result = await steerUlwLoop(repoRoot, proposal); printSteerResult(result, json); return result.accepted ? 0 : 1; } async function addGoal(repoRoot: string, argv: readonly string[], json: boolean): Promise { - const result = await addUltragoalGoal(repoRoot, { title: required(argv, "--title"), objective: required(argv, "--objective") }); - if (json) printJson({ ok: true, plan: result.plan, goal: result.goal, summary: summarizeUltragoalPlan(result.plan) }); - else { process.stdout.write(`ultragoal added goal: ${result.goal.id}\n`); printStatus(result.plan); } + 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: summarizeUlwLoopPlan(result.plan) }); + else { process.stdout.write(`ulw-loop added goal: ${result.goal.id}\n`); printStatus(result.plan); } return 0; } async function criteria(repoRoot: string, argv: readonly string[], json: boolean): Promise { 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 }); 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; @@ -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 { const result = await recordEvidence(repoRoot, parseRecordEvidenceArgs(argv)); - if (json) printJson({ ok: true, ...result, summary: summarizeUltragoalPlan(result.plan) }); - else process.stdout.write(`ultragoal evidence recorded: ${result.goal.id}/${result.criterion.id} -> ${result.criterion.status}\n`); + if (json) printJson({ ok: true, ...result, summary: summarizeUlwLoopPlan(result.plan) }); + else process.stdout.write(`ulw-loop evidence recorded: ${result.goal.id}/${result.criterion.id} -> ${result.criterion.status}\n`); return 0; } async function reviewBlockers(repoRoot: string, argv: readonly string[], json: boolean): Promise { 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 }); - if (json) printJson({ ok: true, plan: result.plan, blockedGoal: result.blockedGoal, goal: result.newGoal, ledgerEntries: result.ledgerEntries, summary: summarizeUltragoalPlan(result.plan) }); - else process.stdout.write(`ultragoal final review blockers recorded: ${result.blockedGoal.id} -> review_blocked; added ${result.newGoal.id}\n`); + 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(`ulw-loop final review blockers recorded: ${result.blockedGoal.id} -> review_blocked; added ${result.newGoal.id}\n`); return 0; } function required(argv: readonly string[], flag: string): string { const value = readValue(argv, flag)?.trim(); 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 { 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); 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 } }); } diff --git a/packages/omo-codex/plugin/components/ultragoal/src/cli-output.ts b/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts similarity index 52% rename from packages/omo-codex/plugin/components/ultragoal/src/cli-output.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts index ef7ed253f..688ba4fb9 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/cli-output.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts @@ -1,16 +1,16 @@ -import type { UltragoalCodexGoalMode, UltragoalItem, UltragoalPlan } from "./types.js"; -import { UltragoalError } from "./types.js"; +import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan } from "./types.js"; +import { UlwLoopError } from "./types.js"; -export const ULTRAGOAL_HELP = `Usage: - omo ultragoal create-goals --brief "..." [--brief-file ] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json] - omo ultragoal status [--json] - omo ultragoal complete-goals [--retry-failed] [--json] - omo ultragoal criteria --goal-id [--json] - omo ultragoal record-evidence --goal-id --criterion-id --status pass|fail|blocked --evidence "..." [--notes "..."] [--json] - omo ultragoal checkpoint --goal-id --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json] - omo ultragoal steer --kind ... --evidence "..." --rationale "..." [--json] - omo ultragoal add-goal --title "..." --objective "..." [--json] - omo ultragoal record-review-blockers --goal-id --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]`; +export const ULW_LOOP_HELP = `Usage: + omo ulw-loop create-goals --brief "..." [--brief-file ] [--from-stdin] [--codex-goal-mode aggregate|per_story] [--force] [--json] + omo ulw-loop status [--json] + omo ulw-loop complete-goals [--retry-failed] [--json] + omo ulw-loop criteria --goal-id [--json] + omo ulw-loop record-evidence --goal-id --criterion-id --status pass|fail|blocked --evidence "..." [--notes "..."] [--json] + omo ulw-loop checkpoint --goal-id --status complete|failed|blocked --evidence "..." --codex-goal-json <...> [--quality-gate-json <...>] [--json] + omo ulw-loop steer --kind ... --evidence "..." --rationale "..." [--json] + omo ulw-loop add-goal --title "..." --objective "..." [--json] + omo ulw-loop record-review-blockers --goal-id --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]`; 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`); } -function criteriaCounts(goal: UltragoalItem): CriteriaCounts { +function criteriaCounts(goal: UlwLoopItem): CriteriaCounts { let pass = 0; for (const criterion of goal.successCriteria) if (criterion.status === "pass") pass += 1; return { pass, total: goal.successCriteria.length }; } -export function printStatus(plan: UltragoalPlan): void { +export function printStatus(plan: UlwLoopPlan): void { let totalCriteria = 0; let passCriteria = 0; - const lines = ["ultragoal status", "", "goals:"]; + const lines = ["ulw-loop status", "", "goals:"]; for (const goal of plan.goals) { const counts = criteriaCounts(goal); totalCriteria += counts.total; @@ -39,23 +39,23 @@ export function printStatus(plan: UltragoalPlan): void { 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); if (blocked === undefined) 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}`, `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.", ].join("\n"); } -export function normalizeCodexGoalMode(value: string | undefined): UltragoalCodexGoalMode { +export function normalizeCodexGoalMode(value: string | undefined): UlwLoopCodexGoalMode { if (value === undefined) return "aggregate"; if (value === "aggregate" || value === "per_story") return value; - throw new UltragoalError( + throw new UlwLoopError( "Invalid --codex-goal-mode; expected aggregate or per_story.", - "ULTRAGOAL_CODEX_GOAL_MODE_INVALID", + "ULW_LOOP_CODEX_GOAL_MODE_INVALID", { details: { value } }, ); } diff --git a/packages/omo-codex/plugin/components/ultragoal/src/cli-steering.ts b/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts similarity index 66% rename from packages/omo-codex/plugin/components/ultragoal/src/cli-steering.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts index d0794da81..e7f6b8b99 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/cli-steering.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/cli-steering.ts @@ -1,63 +1,63 @@ // biome-ignore-all format: keep this module under the mandated pure LOC budget. import { parseGoalArg, readJsonInput, readValue } from "./cli-arg-parser.js"; import { printJson, printStatus } from "./cli-output.js"; -import type { SteerUltragoalResult, UltragoalSteeringChildGoal, UltragoalSteeringMutationKind, UltragoalSteeringProposal, UltragoalSteeringSource, UltragoalSuccessCriterionUserModel } from "./types.js"; -import { ULTRAGOAL_STEERING_MUTATION_KINDS, ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS, UltragoalError } from "./types.js"; +import type { SteerUlwLoopResult, UlwLoopSteeringChildGoal, UlwLoopSteeringMutationKind, UlwLoopSteeringProposal, UlwLoopSteeringSource, UlwLoopSuccessCriterionUserModel } 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 isSource(value: string | undefined): value is UltragoalSteeringSource { 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 fail(message: string, code: string, details: Record): never { throw new UltragoalError(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 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 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 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 UlwLoopSteeringSource { return value !== undefined && SOURCES.some((source) => source === 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): 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}.`, "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}.`, "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.", "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 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; } -export function parseSteeringKind(argv: readonly string[]): UltragoalSteeringMutationKind { +export function parseSteeringKind(argv: readonly string[]): UlwLoopSteeringMutationKind { const value = readValue(argv, "--kind"); 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"); 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; const title = text(objectText(value, "title"), "title"); const objective = text(objectText(value, "objective"), "objective"); if (title === undefined || objective === undefined) return null; return { title, objective }; } -async function children(argv: readonly string[], flag: string, needed: boolean): Promise { +async function children(argv: readonly string[], flag: string, needed: boolean): Promise { const input = needed ? required(argv, flag) : text(readValue(argv, flag), flag); if (input === undefined) return []; const raw = await readJsonInput(input); - if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULTRAGOAL_STEERING_JSON_ARRAY_REQUIRED", { flag }); - const parsed: UltragoalSteeringChildGoal[] = []; - 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); } + if (!Array.isArray(raw)) return fail(`${flag} must be a JSON array.`, "ULW_LOOP_STEERING_JSON_ARRAY_REQUIRED", { flag }); + const parsed: UlwLoopSteeringChildGoal[] = []; + 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; } async function stringArray(argv: readonly string[], flag: string): Promise { 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[] = []; - 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; } -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 neverKind(kind: never): never { return fail(`Unsupported steering kind: ${String(kind)}.`, "ULTRAGOAL_STEERING_KIND_UNSUPPORTED", { kind }); } +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)}.`, "ULW_LOOP_STEERING_KIND_UNSUPPORTED", { kind }); } export async function parseSteeringProposal(argv: readonly string[]): Promise { 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 ({ 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) ?? ""); } 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 }) }; } -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; } 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.audit.idempotencyKey !== undefined) process.stdout.write(`idempotency-key: ${result.audit.idempotencyKey}\n`); printStatus(result.plan); diff --git a/packages/omo-codex/plugin/components/ultragoal/src/cli.ts b/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts similarity index 64% rename from packages/omo-codex/plugin/components/ultragoal/src/cli.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/cli.ts index cc1ec57d4..ed9d5fd9a 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/cli.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts @@ -1,9 +1,9 @@ #!/usr/bin/env node -import { ultragoalCommand } from "./cli-commands.js"; -import { runPreToolUseGoalBudgetGuardCli, runUltragoalHookCli } from "./codex-hook.js"; +import { ulwLoopCommand } from "./cli-commands.js"; +import { runPreToolUseGoalBudgetGuardCli, runUlwLoopHookCli } from "./codex-hook.js"; const TOP_LEVEL_HELP = - "Usage:\n omo ultragoal [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 [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 { const argv = process.argv.slice(2); @@ -12,11 +12,11 @@ async function main(): Promise { process.stdout.write(TOP_LEVEL_HELP); return 0; } - if (command === "ultragoal") return ultragoalCommand(argv.slice(1)); + if (command === "ulw-loop") return ulwLoopCommand(argv.slice(1)); if (command === "hook") { const sub = argv[1]; if (sub === "user-prompt-submit") { - await runUltragoalHookCli(process.stdin, process.stdout); + await runUlwLoopHookCli(process.stdin, process.stdout); return 0; } if (sub === "pre-tool-use") { diff --git a/packages/omo-codex/plugin/components/ultragoal/src/codex-goal-instruction.ts b/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts similarity index 68% rename from packages/omo-codex/plugin/components/ultragoal/src/codex-goal-instruction.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts index b1994eb83..ccbbdc787 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/codex-goal-instruction.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts @@ -1,40 +1,40 @@ 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 { readonly objective: string; readonly status: "active"; } -export interface UltragoalGoalInstruction { +export interface UlwLoopGoalInstruction { readonly text: string; readonly json: CodexCreateGoalPayload; } export function buildCodexGoalInstruction(args: { - readonly plan: UltragoalPlan; - readonly goal: UltragoalItem; + readonly plan: UlwLoopPlan; + readonly goal: UlwLoopItem; readonly isFinal?: boolean; -}): UltragoalGoalInstruction { +}): UlwLoopGoalInstruction { const mode = codexGoalMode(args.plan); const createGoal = buildCreateGoalPayload(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 }; } -function buildCreateGoalPayload(plan: UltragoalPlan, goal: UltragoalItem): CodexCreateGoalPayload { +function buildCreateGoalPayload(plan: UlwLoopPlan, goal: UlwLoopItem): CodexCreateGoalPayload { return { objective: expectedCodexObjective(plan, goal), status: "active" }; } function buildText( - mode: UltragoalCodexGoalMode, - plan: UltragoalPlan, - goal: UltragoalItem, + mode: UlwLoopCodexGoalMode, + plan: UlwLoopPlan, + goal: UlwLoopItem, createGoal: CodexCreateGoalPayload, isFinal: boolean, ): string { return joinLines([ - mode === "aggregate" ? "Ultragoal aggregate-goal handoff" : "Ultragoal active-goal handoff", + mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff", `Mode: ${mode}`, `Plan: ${plan.goalsPath}`, `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") { return [ "- 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.", ]; } 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.", "- 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 ? "- 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.", ]; } -function checkpointLines(mode: UltragoalCodexGoalMode): readonly string[] { +function checkpointLines(mode: UlwLoopCodexGoalMode): readonly string[] { const failureLine = "- 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]; @@ -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}`]; } -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."]; return ["Success criteria:", ...criteria.map(formatCriterionLine)]; } -function formatCriterionLine(criterion: UltragoalSuccessCriterion): string { +function formatCriterionLine(criterion: UlwLoopSuccessCriterion): string { const remainingWork = criterion.status === "pending" ? " remaining work:" : ""; 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) - return "- This is not the final ultragoal 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 "" --evidence "" --codex-goal-json ""`; - const checkpointCommand = `omo ultragoal checkpoint --goal-id ${goal.id} --status complete --evidence "" --codex-goal-json "" --quality-gate-json ""`; + return "- This is not the final ulw-loop story; do not run the final ai-slop-cleaner/$code-review gate yet."; + const blockerCommand = `omo ulw-loop record-review-blockers --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "" --evidence "" --codex-goal-json ""`; + const checkpointCommand = `omo ulw-loop checkpoint --goal-id ${goal.id} --status complete --evidence "" --codex-goal-json "" --quality-gate-json ""`; return joinLines([ "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.", diff --git a/packages/omo-codex/plugin/components/ultragoal/src/codex-goal-snapshot.ts b/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/src/codex-goal-snapshot.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-snapshot.ts diff --git a/packages/omo-codex/plugin/components/ultragoal/src/codex-hook.ts b/packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts similarity index 89% rename from packages/omo-codex/plugin/components/ultragoal/src/codex-hook.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts index 20bab204b..b6e487d1b 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/codex-hook.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/codex-hook.ts @@ -1,4 +1,4 @@ -import { parseUltragoalSteeringDirective, steerUltragoal } from "./steering.js"; +import { parseUlwLoopSteeringDirective, steerUlwLoop } from "./steering.js"; export interface UserPromptSubmitPayload { readonly cwd: string; @@ -35,7 +35,7 @@ interface PreToolUseHookOutput { const CREATE_GOAL_TOOL_NAME = "create_goal"; 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 { 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 { +export async function applyUserPromptUlwLoopSteering(payload: UserPromptSubmitPayload): Promise { try { if (payload.hook_event_name !== "UserPromptSubmit") return ""; - const proposal = parseUltragoalSteeringDirective(payload.prompt); + const proposal = parseUlwLoopSteeringDirective(payload.prompt); if (proposal === null) return ""; - const result = await steerUltragoal(payload.cwd, proposal); + const result = await steerUlwLoop(payload.cwd, proposal); if (!result.accepted) return ""; return JSON.stringify({ status: "accepted", @@ -93,11 +93,11 @@ export function applyPreToolUseGoalBudgetGuard(payload: PreToolUsePayload): stri return `${JSON.stringify(output)}\n`; } -export async function runUltragoalHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise { +export async function runUlwLoopHookCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise { try { const payload = parseUserPromptSubmitPayload(await readAll(stdin)); if (payload === null) return; - const output = await applyUserPromptUltragoalSteering(payload); + const output = await applyUserPromptUlwLoopSteering(payload); if (output.length > 0) stdout.write(output); } catch (error) { if (error instanceof Error) return; diff --git a/packages/omo-codex/plugin/components/ultragoal/src/evidence.ts b/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts similarity index 60% rename from packages/omo-codex/plugin/components/ultragoal/src/evidence.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts index 4d8014813..e4b25127a 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/evidence.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/evidence.ts @@ -1,15 +1,15 @@ // biome-ignore-all format: keep this module under the mandated pure LOC budget. import { hasAllCriteriaPass } from "./goal-status.js"; -import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js"; -import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "./types.js"; -import { iso, UltragoalError } from "./types.js"; +import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js"; +import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js"; +import { iso, UlwLoopError } from "./types.js"; type EvidenceStatus = "pass" | "fail" | "blocked"; 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): never { throw new UltragoalError(message, code, { details }); } +function ulwLoopFail(message: string, code: string, details: Record): never { throw new UlwLoopError(message, code, { details }); } -function ledgerKind(status: EvidenceStatus): UltragoalLedgerEntry["kind"] { +function ledgerKind(status: EvidenceStatus): UlwLoopLedgerEntry["kind"] { switch (status) { case "pass": return "evidence_captured"; @@ -18,25 +18,25 @@ function ledgerKind(status: EvidenceStatus): UltragoalLedgerEntry["kind"] { case "blocked": return "criterion_blocked"; 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); - 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); - 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 }> { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem; criterion: UlwLoopSuccessCriterion; ledgerEntry: UlwLoopLedgerEntry }> { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); const goal = findGoal(plan, args.goalId); const criterion = findCriterion(goal, args.criterionId); const evidence = nonEmptyEvidence(args.evidence); @@ -50,7 +50,7 @@ export async function recordEvidence(repoRoot: string, args: RecordEvidenceArgs) goal.updatedAt = capturedAt; plan.updatedAt = capturedAt; await writePlan(repoRoot, plan); - const ledgerEntry: UltragoalLedgerEntry = { + const ledgerEntry: UlwLoopLedgerEntry = { at: capturedAt, kind, 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 }> { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function markCriteriaPendingResetForGoal(repoRoot: string, goalId: string): Promise<{ plan: UlwLoopPlan; resetCount: number }> { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); const goal = findGoal(plan, goalId); const now = iso(); 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 passCount = 0; let pendingCount = 0; @@ -103,7 +103,7 @@ export function criteriaSummary(plan: UltragoalPlan): { totalCriteria: number; p case "pending": pendingCount += 1; break; case "fail": failCount += 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); @@ -111,11 +111,11 @@ export function criteriaSummary(plan: UltragoalPlan): { totalCriteria: number; p 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; - 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 })) }, }); } diff --git a/packages/omo-codex/plugin/components/ultragoal/src/goal-status.ts b/packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts similarity index 57% rename from packages/omo-codex/plugin/components/ultragoal/src/goal-status.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts index a140c4bd8..0bc8c3e26 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/goal-status.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/goal-status.ts @@ -1,23 +1,23 @@ import type { - UltragoalCodexGoalMode, - UltragoalItem, - UltragoalPlan, - UltragoalStatus, - UltragoalSuccessCriterion, + UlwLoopCodexGoalMode, + UlwLoopItem, + UlwLoopPlan, + UlwLoopStatus, + UlwLoopSuccessCriterion, } from "./types.js"; -export const ULTRAGOAL_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."; +export const ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE: string = + "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"; } -function isResolvedStatus(status: UltragoalStatus): boolean { +function isResolvedStatus(status: UlwLoopStatus): boolean { return status === "complete"; } -function isSupersededResolved(goal: UltragoalItem, plan: UltragoalPlan): boolean { +function isSupersededResolved(goal: UlwLoopItem, plan: UlwLoopPlan): boolean { if (goal.steeringStatus !== "superseded") return false; const replacements = goal.supersededBy ?? []; 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 === "blocked") return true; return !isResolvedStatus(goal.status); } function isCompletionBlockingForFinalCandidate( - candidate: UltragoalItem, - finalCandidate: UltragoalItem, - plan: UltragoalPlan, + candidate: UlwLoopItem, + finalCandidate: UlwLoopItem, + plan: UlwLoopPlan, ): boolean { if (candidate.id === finalCandidate.id) return false; if (candidate.steeringStatus === "superseded") { @@ -51,34 +51,34 @@ function isCompletionBlockingForFinalCandidate( return isCompletionBlocking(candidate, plan); } -export function isUltragoalDone(plan: UltragoalPlan): boolean { +export function isUlwLoopDone(plan: UlwLoopPlan): boolean { if (plan.aggregateCompletion?.status === "complete") return true; 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 ( isCompletionBlocking(goal, plan) && plan.goals.every((candidate) => !isCompletionBlockingForFinalCandidate(candidate, goal, plan)) ); } -export function aggregateCodexObjective(plan: UltragoalPlan): string { - return plan.codexObjective ?? ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE; +export function aggregateCodexObjective(plan: UlwLoopPlan): string { + 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; } -export function compatibleCodexObjectives(plan: UltragoalPlan): readonly string[] { +export function compatibleCodexObjectives(plan: UlwLoopPlan): readonly string[] { 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"); } -export function firstUnresolvedCriterion(goal: UltragoalItem): UltragoalSuccessCriterion | undefined { +export function firstUnresolvedCriterion(goal: UlwLoopItem): UlwLoopSuccessCriterion | undefined { return goal.successCriteria.find((criterion) => criterion.status !== "pass"); } diff --git a/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts b/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts new file mode 100644 index 000000000..078af60df --- /dev/null +++ b/packages/omo-codex/plugin/components/ulw-loop/src/paths.ts @@ -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("/"); +} diff --git a/packages/omo-codex/plugin/components/ultragoal/src/plan-crud.ts b/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts similarity index 63% rename from packages/omo-codex/plugin/components/ultragoal/src/plan-crud.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts index e40b5fc0e..1ea7ec6db 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/plan-crud.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts @@ -2,22 +2,22 @@ import { existsSync } from "node:fs"; import { mkdir, writeFile } from "node:fs/promises"; -import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "./goal-status.js"; -import { ultragoalBriefPath, ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "./paths.js"; -import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js"; -import type { UltragoalCodexGoalMode, UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "./types.js"; -import { iso, ULTRAGOAL_BRIEF, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js"; +import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "./goal-status.js"; +import { ulwLoopBriefPath, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "./paths.js"; +import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js"; +import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } 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 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 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; } -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 rows = [ ["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 })); } -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 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 }; } -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); plan.goals.push(goal); plan.updatedAt = now; 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]; } -export async function createUltragoalPlan(repoRoot: string, args: { brief: string; codexGoalMode?: UltragoalCodexGoalMode; force?: boolean }): Promise { - return withUltragoalMutationLock(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"); +export async function createUlwLoopPlan(repoRoot: string, args: { brief: string; codexGoalMode?: UlwLoopCodexGoalMode; force?: boolean }): Promise { + return withUlwLoopMutationLock(repoRoot, async () => { + 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 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 }; - if (plan.codexGoalMode === "aggregate") plan.codexObjective = ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE; - await mkdir(ultragoalDir(repoRoot), { recursive: true }); - await writeFile(ultragoalBriefPath(repoRoot), args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`, "utf8"); + 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 = ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE; + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); + await writeFile(ulwLoopBriefPath(repoRoot), args.brief.endsWith("\n") ? args.brief : `${args.brief}\n`, "utf8"); 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` }); return plan; }); } -export async function addUltragoalGoal(repoRoot: string, args: { title: string; objective: string }): Promise<{ plan: UltragoalPlan; goal: UltragoalItem }> { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function addUlwLoopGoal(repoRoot: string, args: { title: string; objective: string }): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem }> { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); const now = iso(); const goal = appendGoalToPlan(plan, args.title, args.objective, now); 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 }> { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function startNextUlwLoop(repoRoot: string, args: { retryFailed?: boolean } = {}): Promise<{ plan: UlwLoopPlan; goal: UlwLoopItem; resumed: boolean } | { done: true; plan: UlwLoopPlan }> { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); const now = iso(); if (plan.aggregateCompletion?.status === "complete") return { done: true, plan }; 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)); if (!next && args.retryFailed) { 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 { - const countStatus = (status: UltragoalItem["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); +export function summarizeUlwLoopPlan(plan: UlwLoopPlan): UlwLoopPlanSummary { + const countStatus = (status: UlwLoopItem["status"]): number => plan.goals.filter((goal) => goal.status === status).length; + 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") } }; } diff --git a/packages/omo-codex/plugin/components/ultragoal/src/plan-io.ts b/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts similarity index 51% rename from packages/omo-codex/plugin/components/ultragoal/src/plan-io.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts index d8df442cf..6b769dbf8 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/plan-io.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/plan-io.ts @@ -1,12 +1,12 @@ import { appendFile, mkdir, readFile, rename, writeFile } from "node:fs/promises"; -import { repoRelative, ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "./paths.js"; -import type { UltragoalLedgerEntry, UltragoalPlan } from "./types.js"; -import { iso, ULTRAGOAL_DIR, ULTRAGOAL_GOALS, ULTRAGOAL_LEDGER, UltragoalError } from "./types.js"; +import { repoRelative, ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "./paths.js"; +import type { UlwLoopLedgerEntry, UlwLoopPlan } 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 LEGACY_OBJECTIVE_PREFIX = `Complete all ultragoal stories in ${ULTRAGOAL_DIR}/${ULTRAGOAL_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 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 ulw-loop stories in ${ULW_LOOP_DIR}/${ULW_LOOP_GOALS}: `; +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>(); 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)); } -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"; } -export async function withUltragoalMutationLock(repoRoot: string, fn: () => Promise): Promise { +export async function withUlwLoopMutationLock(repoRoot: string, fn: () => Promise): Promise { const prior = locks.get(repoRoot) ?? Promise.resolve(); const run = prior.then(fn, fn); locks.set( @@ -31,22 +31,22 @@ export async function withUltragoalMutationLock(repoRoot: string, fn: () => P return run; } -export async function readUltragoalPlan(repoRoot: string): Promise { - const path = ultragoalGoalsPath(repoRoot); +export async function readUlwLoopPlan(repoRoot: string): Promise { + const path = ulwLoopGoalsPath(repoRoot); let raw: string; try { raw = await readFile(path, "utf8"); } catch (error) { if (!hasCode(error, "ENOENT")) throw error; - throw new UltragoalError( - `No ultragoal plan found at ${repoRelative(path, repoRoot)}. Run \`omo ultragoal create-goals ...\` first.`, - "ULTRAGOAL_PLAN_MISSING", + throw new UlwLoopError( + `No ulw-loop plan found at ${repoRelative(path, repoRoot)}. Run \`omo ulw-loop create-goals ...\` first.`, + "ULW_LOOP_PLAN_MISSING", { cause: error }, ); } - const parsed: UltragoalPlan = JSON.parse(raw); + const parsed: UlwLoopPlan = JSON.parse(raw); 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; if ( @@ -69,30 +69,30 @@ export async function readUltragoalPlan(repoRoot: string): Promise { - await mkdir(ultragoalDir(repoRoot), { recursive: true }); - const path = ultragoalGoalsPath(repoRoot); +export async function writePlan(repoRoot: string, plan: UlwLoopPlan): Promise { + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); + const path = ulwLoopGoalsPath(repoRoot); const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`; await writeFile(tmpPath, `${JSON.stringify(plan, null, 2)}\n`, "utf8"); await rename(tmpPath, path); } -export async function appendLedger(repoRoot: string, entry: UltragoalLedgerEntry): Promise { - await mkdir(ultragoalDir(repoRoot), { recursive: true }); - await appendFile(ultragoalLedgerPath(repoRoot), `${JSON.stringify(entry)}\n`, "utf8"); +export async function appendLedger(repoRoot: string, entry: UlwLoopLedgerEntry): Promise { + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); + await appendFile(ulwLoopLedgerPath(repoRoot), `${JSON.stringify(entry)}\n`, "utf8"); } -export async function readSteeringLedgerEntries(repoRoot: string): Promise { +export async function readSteeringLedgerEntries(repoRoot: string): Promise { let raw: string; try { - raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8"); + raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8"); } catch (error) { if (hasCode(error, "ENOENT")) return []; throw error; } - const entries: UltragoalLedgerEntry[] = []; + const entries: UlwLoopLedgerEntry[] = []; 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); } return entries; diff --git a/packages/omo-codex/plugin/components/ultragoal/src/quality-gate.ts b/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts similarity index 90% rename from packages/omo-codex/plugin/components/ultragoal/src/quality-gate.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts index b5e08d41f..4303b99ae 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/quality-gate.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts @@ -1,5 +1,5 @@ -import type { UltragoalItem, UltragoalPlan, UltragoalQualityGate } from "./types.js"; -import { UltragoalError } from "./types.js"; +import type { UlwLoopItem, UlwLoopPlan, UlwLoopQualityGate } from "./types.js"; +import { UlwLoopError } from "./types.js"; const BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" "); 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/; 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 { @@ -42,7 +42,7 @@ function stringArray(value: unknown, field: string): string[] { 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 cleaner = section(gate["aiSlopCleaner"], "aiSlopCleaner"); const verification = section(gate["verification"], "verification"); @@ -61,7 +61,7 @@ export function validateQualityGate(input: unknown): UltragoalQualityGate { const cleanerEvidence = nonEmptyString(cleaner["evidence"], "aiSlopCleaner.evidence"); const verificationEvidence = nonEmptyString(verification["evidence"], "verification.evidence"); const reviewEvidence = nonEmptyString(review["evidence"], "codeReview.evidence"); - const result: UltragoalQualityGate = { + const result: UlwLoopQualityGate = { aiSlopCleaner: { status: "passed", evidence: cleanerEvidence }, verification: { status: "passed", commands, evidence: verificationEvidence }, 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`; } -function nestedBlockerSignature(goal: UltragoalItem): string | null { +function nestedBlockerSignature(goal: UlwLoopItem): string | null { const blocker = Reflect.get(goal, "blocker"); const signature = isRecord(blocker) ? blocker["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) .length; } -export function clearGoalBlockerFields(goal: UltragoalItem): void { +export function clearGoalBlockerFields(goal: UlwLoopItem): void { for (const key of BLOCKER_FIELD_KEYS) Reflect.deleteProperty(goal, key); } diff --git a/packages/omo-codex/plugin/components/ultragoal/src/review-blockers.ts b/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts similarity index 60% rename from packages/omo-codex/plugin/components/ultragoal/src/review-blockers.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts index 08fbc18c4..100307d16 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/review-blockers.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/review-blockers.ts @@ -3,20 +3,20 @@ import { readCodexGoalSnapshotInput, reconcileCodexGoalSnapshot } from "./codex-goal-snapshot.js"; import { codexGoalMode, compatibleCodexObjectives, expectedCodexObjective, isFinalRunCompletionCandidate } from "./goal-status.js"; import { seedDefaultSuccessCriteria } from "./plan-crud.js"; -import { appendLedger, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js"; -import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan } from "./types.js"; -import { iso, UltragoalError } from "./types.js"; +import { appendLedger, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js"; +import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } 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 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(" "); -function ultragoalError(message: string, code: string): never { - throw new UltragoalError(message, code); +function ulwLoopError(message: string, code: string): never { + throw new UlwLoopError(message, code); } -function nextGoalId(plan: UltragoalPlan): string { +function nextGoalId(plan: UlwLoopPlan): string { const max = plan.goals.reduce((current, goal) => { const digits = /^G(\d+)/u.exec(goal.id)?.[1]; 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")}`; } -function appendBlockerGoal(plan: UltragoalPlan, args: RecordFinalReviewBlockersArgs, now: string): UltragoalItem { +function appendBlockerGoal(plan: UlwLoopPlan, args: RecordFinalReviewBlockersArgs, now: string): UlwLoopItem { const index = plan.goals.length; - const goal: UltragoalItem = { + const goal: UlwLoopItem = { id: nextGoalId(plan), title: args.title, objective: args.objective, @@ -44,17 +44,17 @@ export async function recordFinalReviewBlockers( repoRoot: string, args: RecordFinalReviewBlockersArgs, ): Promise { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); 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.status !== "in_progress") ultragoalError(`${goal.id} is ${goal.status}.`, "ultragoal_goal_not_in_progress"); - if (!isFinalRunCompletionCandidate(plan, goal)) ultragoalError(`${goal.id} is not final.`, "ultragoal_not_final_story"); + if (goal === undefined) ulwLoopError(`Unknown ulw-loop id: ${args.goalId}`, "ulw_loop_goal_not_found"); + if (goal.status !== "in_progress") ulwLoopError(`${goal.id} is ${goal.status}.`, "ulw_loop_goal_not_in_progress"); + if (!isFinalRunCompletionCandidate(plan, goal)) ulwLoopError(`${goal.id} is not final.`, "ulw_loop_not_final_story"); const snapshot = await readCodexGoalSnapshotInput(args.codexGoalJson, repoRoot); const aggregate = codexGoalMode(plan) === "aggregate"; 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(); for (const field of BLOCKER_FIELDS) Reflect.deleteProperty(goal, field); @@ -67,9 +67,9 @@ export async function recordFinalReviewBlockers( plan.updatedAt = now; 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 addedEntry: UltragoalLedgerEntry = { 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 blockedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_review_blocked", goalId: goal.id, status: goal.status, evidence: args.evidence, codexGoal }; + const addedEntry: UlwLoopLedgerEntry = { at: now, kind: "goal_added", goalId: newGoal.id, status: newGoal.status, evidence: args.evidence, message: newGoal.title }; + 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"); const ledgerEntries = [blockedEntry, addedEntry, summaryEntry]; await writePlan(repoRoot, plan); diff --git a/packages/omo-codex/plugin/components/ultragoal/src/steering.ts b/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts similarity index 73% rename from packages/omo-codex/plugin/components/ultragoal/src/steering.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/steering.ts index 81c151cc3..88e1fe1f5 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/steering.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/steering.ts @@ -1,21 +1,21 @@ // biome-ignore-all format: compact steering module must stay below the 240 pure-LOC budget -import { isUltragoalDone } from "./goal-status.js"; -import { appendLedger, readSteeringLedgerEntries, readUltragoalPlan, withUltragoalMutationLock, writePlan } from "./plan-io.js"; +import { isUlwLoopDone } from "./goal-status.js"; +import { appendLedger, readSteeringLedgerEntries, readUlwLoopPlan, withUlwLoopMutationLock, writePlan } from "./plan-io.js"; import type { - SteerUltragoalResult, - UltragoalItem, - UltragoalLedgerEntry, - UltragoalPlan, - UltragoalSteeringAudit, - UltragoalSteeringChildGoal, - UltragoalSteeringMutationKind, - UltragoalSteeringProposal, - UltragoalSteeringSource, - UltragoalSuccessCriterionUserModel, + SteerUlwLoopResult, + UlwLoopItem, + UlwLoopLedgerEntry, + UlwLoopPlan, + UlwLoopSteeringAudit, + UlwLoopSteeringChildGoal, + UlwLoopSteeringMutationKind, + UlwLoopSteeringProposal, + UlwLoopSteeringSource, + UlwLoopSuccessCriterionUserModel, } 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 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]; @@ -24,9 +24,9 @@ const text = (value: object, key: string): string | undefined => { const candidate = read(value, key); 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 isSource = (value: unknown): value is UltragoalSteeringSource => 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 isKind = (value: unknown): value is UlwLoopSteeringMutationKind => typeof value === "string" && ULW_LOOP_STEERING_MUTATION_KINDS.some((kind) => kind === value); +const isSource = (value: unknown): value is UlwLoopSteeringSource => typeof value === "string" && SOURCES.some((source) => source === 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 candidate = read(value, key); 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); -function child(value: unknown): UltragoalSteeringChildGoal | null { +function child(value: unknown): UlwLoopSteeringChildGoal | null { if (!isPlain(value)) return null; const title = text(value, "title"); const objective = text(value, "objective"); @@ -60,7 +60,7 @@ function childValues(proposal: object): unknown[] { 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 direct = texts(proposal, "pendingOrder"); 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); } -function auditFor(proposal: unknown, reasons: string[]): UltragoalSteeringAudit { +function auditFor(proposal: unknown, reasons: string[]): UlwLoopSteeringAudit { const object = isPlain(proposal) ? proposal : undefined; const kindRaw = object === undefined ? undefined : read(object, "kind"); const sourceRaw = object === undefined ? undefined : read(object, "source"); const evidence = object === undefined ? "" : (text(object, "evidence") ?? ""); 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; const criterionId = text(object, "criterionId"); const directiveText = text(object, "directiveText"); @@ -101,7 +101,7 @@ function auditFor(proposal: unknown, reasons: string[]): UltragoalSteeringAudit return audit; } -export function validateUltragoalSteeringProposal(plan: UltragoalPlan, proposal: unknown): UltragoalSteeringAudit { +export function validateUlwLoopSteeringProposal(plan: UlwLoopPlan, proposal: unknown): UlwLoopSteeringAudit { const reasons: string[] = []; if (!isPlain(proposal)) reasons.push("proposal must be an object"); 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 (hasProtected(proposal)) reasons.push("protected payload"); 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); 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); } -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]); 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`); @@ -134,7 +134,7 @@ function validateKind(plan: UltragoalPlan, proposal: object, kind: UltragoalStee 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 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"); @@ -142,7 +142,7 @@ function validateOrder(plan: UltragoalPlan, proposal: object, reasons: string[]) 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 criterionId = text(proposal, "criterionId"); 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"); } -function nextId(plan: UltragoalPlan, offset: number): string { +function nextId(plan: UlwLoopPlan, offset: number): string { const max = plan.goals.reduce((current, item) => { const digits = /^G(\d+)$/u.exec(item.id)?.[1]; 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")}`; } -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 }; } -export function applySteeringMutation(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, audit: UltragoalSteeringAudit): UltragoalPlan { +export function applySteeringMutation(plan: UlwLoopPlan, proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit): UlwLoopPlan { const next = structuredClone(plan); if (!audit.invariant.accepted) return next; 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 === "reorder_pending") { 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 === "split_subgoal" || proposal.kind === "mark_blocked_superseded") splitOrBlock(next, proposal, now); @@ -180,7 +180,7 @@ export function applySteeringMutation(plan: UltragoalPlan, proposal: UltragoalSt 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]); if (target === undefined) return; target.title = revised(proposal, "revisedTitle", "title") ?? target.title; @@ -190,7 +190,7 @@ function reviseWording(plan: UltragoalPlan, proposal: UltragoalSteeringProposal, 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]); if (target === undefined) return; 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; } -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 index = target?.successCriteria.findIndex((item) => item.id === proposal.criterionId) ?? -1; const current = target?.successCriteria[index]; @@ -220,12 +220,12 @@ function reviseCriterion(plan: UltragoalPlan, proposal: UltragoalSteeringProposa 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")); } -export function parseUltragoalSteeringDirective(text: string): UltragoalSteeringProposal | null { - const match = /(?:^|\s)(?:OMO_ULTRAGOAL_STEER|omo\.ultragoal\.steer|omo ultragoal steer):\s*([\s\S]+)$/u.exec(text); +export function parseUlwLoopSteeringDirective(text: string): UlwLoopSteeringProposal | null { + 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; try { 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 { - return withUltragoalMutationLock(repoRoot, async () => { - const plan = await readUltragoalPlan(repoRoot); +export async function steerUlwLoop(repoRoot: string, proposal: UlwLoopSteeringProposal): Promise { + return withUlwLoopMutationLock(repoRoot, async () => { + const plan = await readUlwLoopPlan(repoRoot); 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)); 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 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) await writePlan(repoRoot, next); 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 { - 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 }; +function ledgerEntry(proposal: UlwLoopSteeringProposal, audit: UlwLoopSteeringAudit, at: string): UlwLoopLedgerEntry { + 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]; if (goalId !== undefined) entry.goalId = goalId; if (proposal.criterionId !== undefined) entry.criterionId = proposal.criterionId; diff --git a/packages/omo-codex/plugin/components/ultragoal/src/types.ts b/packages/omo-codex/plugin/components/ulw-loop/src/types.ts similarity index 58% rename from packages/omo-codex/plugin/components/ultragoal/src/types.ts rename to packages/omo-codex/plugin/components/ulw-loop/src/types.ts index d065e672f..aa1f11ed2 100644 --- a/packages/omo-codex/plugin/components/ultragoal/src/types.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/src/types.ts @@ -1,9 +1,9 @@ -export const ULTRAGOAL_DIR = ".omo/ultragoal"; -export const ULTRAGOAL_BRIEF = "brief.md"; -export const ULTRAGOAL_GOALS = "goals.json"; -export const ULTRAGOAL_LEDGER = "ledger.jsonl"; +export const ULW_LOOP_DIR = ".omo/ulw-loop"; +export const ULW_LOOP_BRIEF = "brief.md"; +export const ULW_LOOP_GOALS = "goals.json"; +export const ULW_LOOP_LEDGER = "ledger.jsonl"; -export type UltragoalStatus = +export type UlwLoopStatus = | "pending" | "in_progress" | "complete" @@ -12,11 +12,11 @@ export type UltragoalStatus = | "review_blocked" | "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", "split_subgoal", "reorder_pending", @@ -25,22 +25,22 @@ export const ULTRAGOAL_STEERING_MUTATION_KINDS = [ "annotate_ledger", "mark_blocked_superseded", ] 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", "edge", "regression", "adversarial", ] 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 type UltragoalCriterionStatus = (typeof ULTRAGOAL_CRITERION_STATUSES)[number]; +export const ULW_LOOP_CRITERION_STATUSES = ["pending", "pass", "fail", "blocked"] as const satisfies readonly string[]; +export type UlwLoopCriterionStatus = (typeof ULW_LOOP_CRITERION_STATUSES)[number]; -export const ULTRAGOAL_LEDGER_EVENT_KINDS = [ +export const ULW_LOOP_LEDGER_EVENT_KINDS = [ "plan_created", "goal_started", "goal_resumed", @@ -61,20 +61,20 @@ export const ULTRAGOAL_LEDGER_EVENT_KINDS = [ "criterion_blocked", "criteria_revised", ] 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 scenario: string; - readonly userModel: UltragoalSuccessCriterionUserModel; + readonly userModel: UlwLoopSuccessCriterionUserModel; readonly expectedEvidence: string; capturedEvidence: string | null; - status: UltragoalCriterionStatus; + status: UlwLoopCriterionStatus; capturedAt?: string; notes?: string; } -export interface UltragoalSteeringInvariantResult { +export interface UlwLoopSteeringInvariantResult { accepted: boolean; structuralInvariantAccepted: boolean; evidenceBackedNecessity: boolean; @@ -83,21 +83,21 @@ export interface UltragoalSteeringInvariantResult { reasons?: string[]; } -export interface UltragoalSteeringChildGoal { +export interface UlwLoopSteeringChildGoal { title: string; objective: string; } -export interface UltragoalSteeringAfterPayload { +export interface UlwLoopSteeringAfterPayload { title?: string; objective?: string; pendingGoalIds?: string[]; - children?: UltragoalSteeringChildGoal[]; + children?: UlwLoopSteeringChildGoal[]; } -export interface UltragoalSteeringProposal { - kind: UltragoalSteeringMutationKind; - source: UltragoalSteeringSource; +export interface UlwLoopSteeringProposal { + kind: UlwLoopSteeringMutationKind; + source: UlwLoopSteeringSource; targetGoalId?: string; targetGoalIds?: string[]; criterionId?: string; @@ -105,48 +105,48 @@ export interface UltragoalSteeringProposal { rationale: string; title?: string; objective?: string; - childGoals?: UltragoalSteeringChildGoal[]; + childGoals?: UlwLoopSteeringChildGoal[]; revisedTitle?: string; revisedObjective?: string; pendingOrder?: string[]; blockedReason?: string; - after?: UltragoalSteeringAfterPayload; + after?: UlwLoopSteeringAfterPayload; directiveText?: string; promptSignature?: string; idempotencyKey?: string; now?: Date; } -export interface UltragoalSteeringAudit { - kind: UltragoalSteeringMutationKind; - source: UltragoalSteeringSource; +export interface UlwLoopSteeringAudit { + kind: UlwLoopSteeringMutationKind; + source: UlwLoopSteeringSource; targetGoalIds: string[]; criterionId?: string; before?: unknown; after?: unknown; evidence: string; rationale: string; - invariant: UltragoalSteeringInvariantResult; + invariant: UlwLoopSteeringInvariantResult; directiveText?: string; promptSignature?: string; idempotencyKey?: string; deduped?: boolean; } -export interface SteerUltragoalResult { - plan: UltragoalPlan; +export interface SteerUlwLoopResult { + plan: UlwLoopPlan; accepted: boolean; - audit: UltragoalSteeringAudit; + audit: UlwLoopSteeringAudit; rejectedReasons: string[]; deduped: boolean; } -export interface UltragoalItem { +export interface UlwLoopItem { id: string; title: string; objective: string; - status: UltragoalStatus; - successCriteria: UltragoalSuccessCriterion[]; + status: UlwLoopStatus; + successCriteria: UlwLoopSuccessCriterion[]; attempt: number; createdAt: string; updatedAt: string; @@ -156,7 +156,7 @@ export interface UltragoalItem { reviewBlockedAt?: string; evidence?: string; failureReason?: string; - steeringStatus?: UltragoalSteeringStatus; + steeringStatus?: UlwLoopSteeringStatus; supersededBy?: string[]; supersedes?: string[]; blockedReason?: string; @@ -168,54 +168,54 @@ export interface UltragoalItem { steeringRationale?: string; } -export interface UltragoalAggregateCompletion { +export interface UlwLoopAggregateCompletion { status: "complete"; completedAt: string; evidence: string; codexGoal?: unknown; } -export interface UltragoalPlan { +export interface UlwLoopPlan { version: 1; createdAt: string; updatedAt: string; briefPath: string; goalsPath: string; ledgerPath: string; - codexGoalMode?: UltragoalCodexGoalMode; + codexGoalMode?: UlwLoopCodexGoalMode; codexObjective?: string; codexObjectiveAliases?: string[]; - aggregateCompletion?: UltragoalAggregateCompletion; + aggregateCompletion?: UlwLoopAggregateCompletion; activeGoalId?: string; - goals: UltragoalItem[]; + goals: UlwLoopItem[]; } -export interface UltragoalLedgerEntry { +export interface UlwLoopLedgerEntry { at: string; - kind: UltragoalLedgerEventKind; + kind: UlwLoopLedgerEventKind; goalId?: string; criterionId?: string; - status?: UltragoalStatus; - criterionStatus?: UltragoalCriterionStatus; + status?: UlwLoopStatus; + criterionStatus?: UlwLoopCriterionStatus; message?: string; codexGoal?: unknown; evidence?: string; capturedEvidence?: string; - qualityGate?: UltragoalQualityGate; - steering?: UltragoalSteeringAudit; + qualityGate?: UlwLoopQualityGate; + steering?: UlwLoopSteeringAudit; before?: unknown; after?: unknown; - mutationKind?: UltragoalSteeringMutationKind; + mutationKind?: UlwLoopSteeringMutationKind; idempotencyKey?: string; blockerSignature?: string; blockerOccurrenceCount?: number; requiredExternalDecision?: string; } -export interface CreateUltragoalOptions { +export interface CreateUlwLoopOptions { brief: string; goals?: Array<{ title?: string; objective: string }>; - codexGoalMode?: UltragoalCodexGoalMode; + codexGoalMode?: UlwLoopCodexGoalMode; now?: Date; force?: boolean; } @@ -227,7 +227,7 @@ export interface StartNextOptions { export interface CheckpointOptions { goalId: string; - status: Extract | "blocked"; + status: Extract | "blocked"; evidence?: string; codexGoal?: unknown; qualityGate?: unknown; @@ -235,36 +235,36 @@ export interface CheckpointOptions { now?: Date; } -export interface AddUltragoalGoalOptions { +export interface AddUlwLoopGoalOptions { title: string; objective: string; evidence?: string; now?: Date; } -export interface RecordFinalReviewBlockersOptions extends AddUltragoalGoalOptions { +export interface RecordFinalReviewBlockersOptions extends AddUlwLoopGoalOptions { goalId: string; codexGoal?: unknown; } -export interface UltragoalQualityGate { +export interface UlwLoopQualityGate { aiSlopCleaner: { status: "passed"; evidence: string }; verification: { status: "passed"; commands: string[]; evidence: string }; codeReview: { recommendation: "APPROVE"; architectStatus: "CLEAR"; evidence: string }; } -export interface UltragoalErrorOptions { +export interface UlwLoopErrorOptions { readonly cause?: unknown; readonly details?: Record; } -export class UltragoalError extends Error { +export class UlwLoopError extends Error { readonly code: string; readonly details?: Record; - constructor(message: string, code: string, opts?: UltragoalErrorOptions) { + constructor(message: string, code: string, opts?: UlwLoopErrorOptions) { super(message, opts?.cause === undefined ? undefined : { cause: opts.cause }); - this.name = "UltragoalError"; + this.name = "UlwLoopError"; this.code = code; if (opts?.details !== undefined) { this.details = opts.details; diff --git a/packages/omo-codex/plugin/components/ultragoal/test/checkpoint.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts similarity index 55% rename from packages/omo-codex/plugin/components/ultragoal/test/checkpoint.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts index 8bd8031a5..e58d9b592 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/checkpoint.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts @@ -4,52 +4,52 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { checkpointUltragoal } from "../src/checkpoint.js"; -import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; -import { ultragoalBriefPath, ultragoalDir, ultragoalLedgerPath } from "../src/paths.js"; +import { checkpointUlwLoop } from "../src/checkpoint.js"; +import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; +import { ulwLoopBriefPath, ulwLoopDir, ulwLoopLedgerPath } from "../src/paths.js"; import { writePlan } from "../src/plan-io.js"; -import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; 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 }; } -function goal(overrides: Partial = {}): UltragoalItem { +function goal(overrides: Partial = {}): 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 }; } -function plan(goals: UltragoalItem[], overrides: Partial = {}): UltragoalPlan { - 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 }; +function plan(goals: UlwLoopItem[], overrides: Partial = {}): UlwLoopPlan { + 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); const activeGoalId = goals.find((candidate) => candidate.status === "in_progress")?.id; if (result.activeGoalId === undefined && activeGoalId !== undefined) result.activeGoalId = activeGoalId; return result; } -async function samplePlan(overrides: Partial = {}): Promise { - const fixture: UltragoalPlan = JSON.parse(await readFile(new URL("./fixtures/sample-plan.json", import.meta.url), "utf8")); +async function samplePlan(overrides: Partial = {}): Promise { + 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); } -async function repoWith(seed: UltragoalPlan): Promise { +async function repoWith(seed: UlwLoopPlan): Promise { const repo = await mkdtemp(join(tmpdir(), "ug-checkpoint-")); - await mkdir(ultragoalDir(repo), { recursive: true }); + await mkdir(ulwLoopDir(repo), { recursive: true }); await writePlan(repo, seed); 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 } }); } -async function lastLedger(repo: string): Promise { - const last = (await readFile(ultragoalLedgerPath(repo), "utf8")).trim().split(/\r?\n/).at(-1); +async function lastLedger(repo: string): Promise { + const last = (await readFile(ulwLoopLedgerPath(repo), "utf8")).trim().split(/\r?\n/).at(-1); if (last === undefined) throw new Error("expected ledger entry"); - const entry: UltragoalLedgerEntry = JSON.parse(last); + const entry: UlwLoopLedgerEntry = JSON.parse(last); return entry; } @@ -57,80 +57,80 @@ async function expectCode(action: () => Promise, code: string): Promise try { await action(); } catch (error) { - expect(error).toBeInstanceOf(UltragoalError); - if (!(error instanceof UltragoalError)) throw error; + expect(error).toBeInstanceOf(UlwLoopError); + if (!(error instanceof UlwLoopError)) throw error; expect(error.code).toBe(code); return; } - throw new Error("Expected UltragoalError"); + throw new Error("Expected UlwLoopError"); } -function passGoal(id: string, overrides: Partial = {}): UltragoalItem { +function passGoal(id: string, overrides: Partial = {}): UlwLoopItem { return goal({ id, successCriteria: [criterion("C001", "pass"), criterion("C002", "pass"), criterion("C003", "pass")], ...overrides }); } -describe("checkpointUltragoal status=complete criteria gate", () => { - it("THROWS ultragoal_criteria_not_all_pass when any criterion is pending", async () => { +describe("checkpointUlwLoop status=complete criteria gate", () => { + 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")] })] })); - 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 () => { - 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")] })])); - 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 () => { 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 () => { 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((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 () => { 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 () => { 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 () => { 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 () => { 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 () => { 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.plan.aggregateCompletion?.status).toBe("complete"); }); - it("ACCEPTS complete when task-scoped completed Codex objective maps to the ultragoal brief", async () => { - const taskObjective = "Fix ultragoal objective mismatch and install local ulw"; + it("ACCEPTS complete when task-scoped completed Codex objective maps to the ulw-loop brief", async () => { + const taskObjective = "Fix ulw-loop objective mismatch and install local ulw"; 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", status: "complete", 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 () => { 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( - checkpointUltragoal(repo, { + checkpointUlwLoop(repo, { goalId: "G001", status: "complete", 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 () => { 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.failedAt).toMatch(/^\d{4}-\d{2}-\d{2}T/u); expect((await lastLedger(repo)).kind).toBe("goal_failed"); @@ -169,27 +169,27 @@ describe("checkpointUltragoal status=failed", () => { it("classifies external authorization blocker signatures", async () => { 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"); }); 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 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.nonRetriable).toBe(true); }); it("skips the criteria gate for failed status", async () => { 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 () => { 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.blockedReason).toContain("ghcr.io"); 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 () => { 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 () => { 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 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); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/cli-commands.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts similarity index 68% rename from packages/omo-codex/plugin/components/ultragoal/test/cli-commands.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts index 6a6418868..4decc9927 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/cli-commands.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/cli-commands.test.ts @@ -3,8 +3,8 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { ultragoalCommand } from "../src/cli-commands.ts"; -import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; +import { ulwLoopCommand } from "../src/cli-commands.ts"; +import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; let testDir: string; let out: string[]; @@ -38,12 +38,12 @@ function stdoutJson(): Record { return JSON.parse(out.join("")); } 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> { resetOutput(); - expect(await ultragoalCommand(["create-goals", "--brief", brief, "--json"])).toBe(0); + expect(await ulwLoopCommand(["create-goals", "--brief", brief, "--json"])).toBe(0); const parsed = stdoutJson(); resetOutput(); return parsed; @@ -51,7 +51,7 @@ async function createPlan(brief = "- Goal A\n- Goal B"): Promise { expect( - await ultragoalCommand([ + await ulwLoopCommand([ "record-evidence", "--goal-id", goalId, @@ -66,41 +66,41 @@ async function passCriterion(goalId: string, criterionId: string): Promise resetOutput(); } -describe("ultragoalCommand help", () => { +describe("ulwLoopCommand help", () => { it("prints usage when no subcommand", async () => { - expect(await ultragoalCommand([])).toBe(0); - expect(out.join("")).toContain("omo ultragoal"); + expect(await ulwLoopCommand([])).toBe(0); + 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 () => { - 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); const parsed = stdoutJson(); expect(parsed).toMatchObject({ ok: true }); 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/ultragoal/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/brief.md"), "utf8")).toContain("Goal A"); + expect(await readFile(join(testDir, ".omo/ulw-loop/goals.json"), "utf8")).toContain("successCriteria"); + 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 () => { await createPlan(); - expect(await ultragoalCommand(["status"])).toBe(0); + expect(await ulwLoopCommand(["status"])).toBe(0); 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 () => { await createPlan(); - expect(await ultragoalCommand(["complete-goals", "--json"])).toBe(0); + expect(await ulwLoopCommand(["complete-goals", "--json"])).toBe(0); expect(stdoutJson()).toMatchObject({ ok: true, 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 () => { await createPlan(); expect( - await ultragoalCommand([ + await ulwLoopCommand([ "record-evidence", "--goal-id", "G001-goal-a", @@ -137,7 +137,7 @@ describe("ultragoalCommand record-evidence", () => { await createPlan(); expect( - await ultragoalCommand([ + await ulwLoopCommand([ "record-evidence", "--goal-id", "G404", @@ -149,22 +149,22 @@ describe("ultragoalCommand record-evidence", () => { "x", ]), ).toBe(1); - expect(err.join("")).toContain("[ultragoal]"); + expect(err.join("")).toContain("[ulw-loop]"); }); it("returns 1 + error on missing flags", async () => { 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); expect(err.join("")).toContain("Missing --goal-id"); }); }); -describe("ultragoalCommand criteria", () => { +describe("ulwLoopCommand criteria", () => { it("lists criteria for a goal", async () => { 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("happy"); }); @@ -172,18 +172,18 @@ describe("ultragoalCommand criteria", () => { it("supports --json output", async () => { 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()).toHaveProperty("criteria.0.id", "C001"); }); }); -describe("ultragoalCommand checkpoint", () => { +describe("ulwLoopCommand checkpoint", () => { it("REJECTS status=complete when criteria pending", async () => { await createPlan(); expect( - await ultragoalCommand([ + await ulwLoopCommand([ "checkpoint", "--goal-id", "G001-goal-a", @@ -205,7 +205,7 @@ describe("ultragoalCommand checkpoint", () => { await passCriterion("G001-goal-a", "C003"); expect( - await ultragoalCommand([ + await ulwLoopCommand([ "checkpoint", "--goal-id", "G001-goal-a", @@ -222,12 +222,12 @@ describe("ultragoalCommand checkpoint", () => { }); }); -describe("ultragoalCommand steer", () => { +describe("ulwLoopCommand steer", () => { it("dispatches to the steering engine", async () => { await createPlan(); expect( - await ultragoalCommand([ + await ulwLoopCommand([ "steer", "--kind", "add_subgoal", @@ -250,25 +250,25 @@ describe("ultragoalCommand steer", () => { }); }); -describe("ultragoalCommand add-goal", () => { +describe("ulwLoopCommand add-goal", () => { it("appends a pending goal", async () => { 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" } }); }); }); -describe("ultragoalCommand unknown", () => { +describe("ulwLoopCommand unknown", () => { it("returns 1 + prints help on unknown subcommand", async () => { - expect(await ultragoalCommand(["wat"])).toBe(1); - expect(out.join("")).toContain("omo ultragoal"); + expect(await ulwLoopCommand(["wat"])).toBe(1); + expect(out.join("")).toContain("omo ulw-loop"); }); }); -describe("ultragoalCommand error handling", () => { - it("returns 1 + prints [ultragoal] prefix on UltragoalError", async () => { - expect(await ultragoalCommand(["status"])).toBe(1); - expect(err.join("")).toContain("[ultragoal]"); +describe("ulwLoopCommand error handling", () => { + it("returns 1 + prints [ulw-loop] prefix on UlwLoopError", async () => { + expect(await ulwLoopCommand(["status"])).toBe(1); + expect(err.join("")).toContain("[ulw-loop]"); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/cli-helpers.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/cli-helpers.test.ts similarity index 78% rename from packages/omo-codex/plugin/components/ultragoal/test/cli-helpers.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/cli-helpers.test.ts index 80e2193e5..0f515f53a 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/cli-helpers.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/cli-helpers.test.ts @@ -13,13 +13,13 @@ import { readRepeated, readValue, } from "../src/cli-arg-parser.js"; -import { normalizeCodexGoalMode, printStatus, ULTRAGOAL_HELP } from "../src/cli-output.js"; -import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import { normalizeCodexGoalMode, printStatus, ULW_LOOP_HELP } from "../src/cli-output.js"; +import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; -function criterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function criterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path returns 200", @@ -31,7 +31,7 @@ function criterion(overrides: Partial = {}): Ultragoa }; } -function goal(overrides: Partial = {}): UltragoalItem { +function goal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Auth endpoint", @@ -49,14 +49,14 @@ function goal(overrides: Partial = {}): UltragoalItem { }; } -function plan(overrides: Partial = {}): UltragoalPlan { +function plan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", activeGoalId: "G001", goals: [goal()], ...overrides, @@ -159,7 +159,7 @@ describe("parseRecordEvidenceArgs", () => { it("throws when goal-id missing", () => { expect(() => parseRecordEvidenceArgs(["record-evidence", "--criterion-id", "C001", "--status", "pass", "--evidence", "x"]), - ).toThrow(UltragoalError); + ).toThrow(UlwLoopError); }); it("throws when status is not pass|fail|blocked", () => { @@ -175,7 +175,7 @@ describe("parseRecordEvidenceArgs", () => { "--evidence", "x", ]), - ).toThrow(UltragoalError); + ).toThrow(UlwLoopError); }); it("includes optional --notes when present", () => { @@ -197,24 +197,24 @@ describe("parseRecordEvidenceArgs", () => { }); }); -describe("ULTRAGOAL_HELP", () => { - it("mentions omo ultragoal + every subcommand", () => { - expect(ULTRAGOAL_HELP).toContain("omo ultragoal"); - expect(ULTRAGOAL_HELP).toContain("create-goals"); - expect(ULTRAGOAL_HELP).toContain("complete-goals"); - expect(ULTRAGOAL_HELP).toContain("status"); - expect(ULTRAGOAL_HELP).toContain("checkpoint"); - expect(ULTRAGOAL_HELP).toContain("steer"); - expect(ULTRAGOAL_HELP).toContain("record-evidence"); - expect(ULTRAGOAL_HELP).toContain("criteria"); - expect(ULTRAGOAL_HELP).toContain("add-goal"); - expect(ULTRAGOAL_HELP).toContain("record-review-blockers"); +describe("ULW_LOOP_HELP", () => { + it("mentions omo ulw-loop + every subcommand", () => { + expect(ULW_LOOP_HELP).toContain("omo ulw-loop"); + expect(ULW_LOOP_HELP).toContain("create-goals"); + expect(ULW_LOOP_HELP).toContain("complete-goals"); + expect(ULW_LOOP_HELP).toContain("status"); + expect(ULW_LOOP_HELP).toContain("checkpoint"); + expect(ULW_LOOP_HELP).toContain("steer"); + expect(ULW_LOOP_HELP).toContain("record-evidence"); + expect(ULW_LOOP_HELP).toContain("criteria"); + expect(ULW_LOOP_HELP).toContain("add-goal"); + expect(ULW_LOOP_HELP).toContain("record-review-blockers"); }); it("never mentions the legacy typo", () => { 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"); }); - it("throws UltragoalError when invalid", () => { - expect(() => normalizeCodexGoalMode("per-story")).toThrow(UltragoalError); + it("throws UlwLoopError when invalid", () => { + expect(() => normalizeCodexGoalMode("per-story")).toThrow(UlwLoopError); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/cli-steering.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering.test.ts similarity index 91% rename from packages/omo-codex/plugin/components/ultragoal/test/cli-steering.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/cli-steering.test.ts index 0070ae354..16566f061 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/cli-steering.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/cli-steering.test.ts @@ -11,24 +11,24 @@ import { parseSteeringSource, printSteerResult, } from "../src/cli-steering.js"; -import type { SteerUltragoalResult, UltragoalPlan } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { SteerUlwLoopResult, UlwLoopPlan } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; -function plan(): UltragoalPlan { +function plan(): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals: [], }; } -function steerResult(overrides: Partial = {}): SteerUltragoalResult { +function steerResult(overrides: Partial = {}): SteerUlwLoopResult { return { plan: plan(), accepted: true, @@ -73,11 +73,11 @@ describe("parseSteeringKind", () => { }); it("throws when --kind missing", () => { - expect(() => parseSteeringKind([])).toThrow(UltragoalError); + expect(() => parseSteeringKind([])).toThrow(UlwLoopError); }); 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", "y", ]), - ).rejects.toThrow(UltragoalError); + ).rejects.toThrow(UlwLoopError); }); it("throws when --evidence missing", async () => { await expect( 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", "y", ]), - ).rejects.toThrow(UltragoalError); + ).rejects.toThrow(UlwLoopError); }); it("throws when goal-id missing", async () => { @@ -231,7 +231,7 @@ describe("parseSteeringProposal revise_criterion", () => { "--rationale", "y", ]), - ).rejects.toThrow(UltragoalError); + ).rejects.toThrow(UlwLoopError); }); it("throws when criterion-id missing", async () => { @@ -248,7 +248,7 @@ describe("parseSteeringProposal revise_criterion", () => { "--rationale", "y", ]), - ).rejects.toThrow(UltragoalError); + ).rejects.toThrow(UlwLoopError); }); }); @@ -387,7 +387,7 @@ describe("normalizeSteeringProposal", () => { it("rejects empty evidence after trim", () => { expect(() => 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", () => { const output = captureStdout(() => printSteerResult(steerResult(), false)); - expect(output).toContain("ultragoal steer: accepted add_subgoal"); - expect(output).toContain("ultragoal status"); + expect(output).toContain("ulw-loop steer: accepted add_subgoal"); + expect(output).toContain("ulw-loop status"); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/codex-goal-instruction.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts similarity index 82% rename from packages/omo-codex/plugin/components/ultragoal/test/codex-goal-instruction.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts index 4dcb9d711..d7b41baea 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/codex-goal-instruction.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; import { buildCodexGoalInstruction } from "../src/codex-goal-instruction.js"; -import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; -import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; +import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; +import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; -function makeCriterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function makeCriterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path", @@ -18,7 +18,7 @@ function makeCriterion(overrides: Partial = {}): Ultr }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Goal one", @@ -32,24 +32,24 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(overrides: Partial = {}): UltragoalPlan { +function makePlan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals: [], ...overrides, }; } 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() }); 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", () => { @@ -58,7 +58,7 @@ describe("buildCodexGoalInstruction aggregate mode", () => { goal: makeGoal(), }); expect(json).toEqual({ - objective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, + objective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, status: "active", }); expect(text).toContain("objective and status only"); @@ -146,8 +146,8 @@ describe("buildCodexGoalInstruction rebrand audit", () => { 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() }); - expect(text).toContain(".omo/ultragoal"); + expect(text).toContain(".omo/ulw-loop"); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/codex-goal-snapshot.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts similarity index 98% rename from packages/omo-codex/plugin/components/ultragoal/test/codex-goal-snapshot.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts index a1e0d29d5..b70f1ffaf 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/codex-goal-snapshot.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-snapshot.test.ts @@ -92,7 +92,7 @@ describe("readCodexGoalSnapshotInput", () => { // then 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 () => { diff --git a/packages/omo-codex/plugin/components/ultragoal/test/codex-hook.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/codex-hook.test.ts similarity index 71% rename from packages/omo-codex/plugin/components/ultragoal/test/codex-hook.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/codex-hook.test.ts index fa524dfdd..ca3620c62 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/codex-hook.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/codex-hook.test.ts @@ -6,34 +6,34 @@ import { describe, expect, it } from "vitest"; import { applyPreToolUseGoalBudgetGuard, - applyUserPromptUltragoalSteering, + applyUserPromptUlwLoopSteering, type PreToolUsePayload, parseUserPromptSubmitPayload, runPreToolUseGoalBudgetGuardCli, - runUltragoalHookCli, + runUlwLoopHookCli, type UserPromptSubmitPayload, } 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 type { UltragoalPlan } from "../src/types.js"; +import type { UlwLoopPlan } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; async function bootstrapPlanRepo(): Promise { const repoRoot = await mkdtemp(join(tmpdir(), "ug-hook-")); - await mkdir(ultragoalDir(repoRoot), { recursive: true }); + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); await writePlan(repoRoot, samplePlan()); return repoRoot; } -function samplePlan(): UltragoalPlan { +function samplePlan(): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals: [ { id: "G001", @@ -70,7 +70,7 @@ function preToolPayload(toolName: string, toolInput: unknown): PreToolUsePayload function payloadWithRuntimeEvent(hookEventName: string): UserPromptSubmitPayload { 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", ); 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 parsed = parseUserPromptSubmitPayload(raw); 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", () => { @@ -109,12 +109,12 @@ describe("parseUserPromptSubmitPayload", () => { }); }); -describe("applyUserPromptUltragoalSteering - OMO directive patterns", () => { - it("processes OMO_ULTRAGOAL_STEER: prompt and returns audit text on success", async () => { +describe("applyUserPromptUlwLoopSteering - OMO directive patterns", () => { + it("processes OMO_ULW_LOOP_STEER: prompt and returns audit text on success", async () => { const repoRoot = await bootstrapPlanRepo(); - const out = await applyUserPromptUltragoalSteering( + const out = await applyUserPromptUlwLoopSteering( 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, ), ); @@ -122,22 +122,22 @@ describe("applyUserPromptUltragoalSteering - OMO directive patterns", () => { 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 out = await applyUserPromptUltragoalSteering( + const out = await applyUserPromptUlwLoopSteering( 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, ), ); expect(out).toContain("accepted"); }); - it("processes omo ultragoal steer: pattern", async () => { + it("processes omo ulw-loop steer: pattern", async () => { const repoRoot = await bootstrapPlanRepo(); - const out = await applyUserPromptUltragoalSteering( + const out = await applyUserPromptUlwLoopSteering( 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, ), ); @@ -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 () => { - 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( - await applyUserPromptUltragoalSteering( - payload('OMX_ULTRAGOAL_STEER: {"kind":"annotate_ledger","evidence":"x","rationale":"y"}', "/tmp"), + await applyUserPromptUlwLoopSteering( + payload('OMX_ULW_LOOP_STEER: {"kind":"annotate_ledger","evidence":"x","rationale":"y"}', "/tmp"), ), ).toBe(""); }); 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 () => { const repoRoot = await mkdtemp(join(tmpdir(), "ug-nohook-")); - const out = await applyUserPromptUltragoalSteering( + const out = await applyUserPromptUlwLoopSteering( 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, ), ); @@ -176,30 +176,30 @@ describe("applyUserPromptUltragoalSteering - error swallowing", () => { }); 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(""); }); }); -describe("runUltragoalHookCli (stdin/stdout integration)", () => { +describe("runUlwLoopHookCli (stdin/stdout integration)", () => { it("reads stdin, applies steering, writes audit to stdout", async () => { const repoRoot = await bootstrapPlanRepo(); const stdin = Readable.from([ JSON.stringify( 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, ), ), ]); const capture = captureStdout(); - await runUltragoalHookCli(stdin, capture.stdout); + await runUlwLoopHookCli(stdin, capture.stdout); expect(capture.read().length).toBeGreaterThan(0); }); it("writes nothing when stdin is empty", async () => { const capture = captureStdout(); - await runUltragoalHookCli(Readable.from([""]), capture.stdout); + await runUlwLoopHookCli(Readable.from([""]), capture.stdout); expect(capture.read()).toBe(""); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/evidence-criteria-gate.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/evidence-criteria-gate.test.ts similarity index 71% rename from packages/omo-codex/plugin/components/ultragoal/test/evidence-criteria-gate.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/evidence-criteria-gate.test.ts index ee7452f18..e02de24b4 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/evidence-criteria-gate.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/evidence-criteria-gate.test.ts @@ -1,12 +1,12 @@ import { describe, expect, it } from "vitest"; import { requireAllCriteriaPass } from "../src/evidence.js"; -import type { UltragoalItem, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; -function makeCriterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function makeCriterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path login returns 200", @@ -18,7 +18,7 @@ function makeCriterion(overrides: Partial = {}): Ultr }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Auth endpoint", @@ -50,7 +50,7 @@ describe("requireAllCriteriaPass", () => { expect(() => requireAllCriteriaPass(goal)).not.toThrow(); }); - it("throws UltragoalError when any criterion pending", () => { + it("throws UlwLoopError when any criterion pending", () => { // given const goal = makeGoal({ successCriteria: [ @@ -61,7 +61,7 @@ describe("requireAllCriteriaPass", () => { }); // when / then - expect(() => requireAllCriteriaPass(goal)).toThrow(UltragoalError); + expect(() => requireAllCriteriaPass(goal)).toThrow(UlwLoopError); }); it("throws when any fail/blocked too", () => { @@ -70,11 +70,11 @@ describe("requireAllCriteriaPass", () => { const goal2 = makeGoal({ successCriteria: [makeCriterion({ id: "C001", status: "blocked" })] }); // when / then - expect(() => requireAllCriteriaPass(goal1)).toThrow(UltragoalError); - expect(() => requireAllCriteriaPass(goal2)).toThrow(UltragoalError); + expect(() => requireAllCriteriaPass(goal1)).toThrow(UlwLoopError); + expect(() => requireAllCriteriaPass(goal2)).toThrow(UlwLoopError); }); - it("UltragoalError includes details.goalId + details.unresolved", () => { + it("UlwLoopError includes details.goalId + details.unresolved", () => { // given const goal = makeGoal({ id: "G001", @@ -90,9 +90,9 @@ describe("requireAllCriteriaPass", () => { requireAllCriteriaPass(goal); expect.fail("expected throw"); } catch (error) { - expect(error).toBeInstanceOf(UltragoalError); - if (!(error instanceof UltragoalError)) throw error; - expect(error.code).toBe("ultragoal_criteria_not_all_pass"); + expect(error).toBeInstanceOf(UlwLoopError); + if (!(error instanceof UlwLoopError)) throw error; + expect(error.code).toBe("ulw_loop_criteria_not_all_pass"); expect(error.details?.["goalId"]).toBe("G001"); expect(Array.isArray(error.details?.["unresolved"])).toBe(true); } diff --git a/packages/omo-codex/plugin/components/ultragoal/test/evidence.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/evidence.test.ts similarity index 83% rename from packages/omo-codex/plugin/components/ultragoal/test/evidence.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/evidence.test.ts index 77243e2d5..061000f67 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/evidence.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/evidence.test.ts @@ -9,34 +9,34 @@ import { recordEvidence, unresolvedCriteriaOf, } from "../src/evidence.js"; -import { ultragoalDir } from "../src/paths.js"; -import { readUltragoalPlan, writePlan } from "../src/plan-io.js"; -import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import { ulwLoopDir } from "../src/paths.js"; +import { readUlwLoopPlan, writePlan } from "../src/plan-io.js"; +import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; -async function bootstrapRepo(plan: UltragoalPlan): Promise { +async function bootstrapRepo(plan: UlwLoopPlan): Promise { const repo = await mkdtemp(join(tmpdir(), "ug-evidence-")); - await mkdir(ultragoalDir(repo), { recursive: true }); + await mkdir(ulwLoopDir(repo), { recursive: true }); await writePlan(repo, plan); return repo; } -async function readLastLedgerEntry(repo: string): Promise { - const lines = (await readFile(join(repo, ".omo/ultragoal/ledger.jsonl"), "utf8")).trim().split("\n"); +async function readLastLedgerEntry(repo: string): Promise { + const lines = (await readFile(join(repo, ".omo/ulw-loop/ledger.jsonl"), "utf8")).trim().split("\n"); const last = lines.at(-1); if (last === undefined) throw new Error("expected ledger entry"); return JSON.parse(last); } -function firstGoal(plan: UltragoalPlan): UltragoalItem { +function firstGoal(plan: UlwLoopPlan): UlwLoopItem { const goal = plan.goals.at(0); if (goal === undefined) throw new Error("expected goal"); return goal; } -function makeCriterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function makeCriterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path login returns 200", @@ -48,7 +48,7 @@ function makeCriterion(overrides: Partial = {}): Ultr }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Auth endpoint", @@ -66,16 +66,16 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(overrides: Partial = {}): UltragoalPlan { +function makePlan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", 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: [], goals: [makeGoal()], ...overrides, @@ -114,7 +114,7 @@ describe("recordEvidence (status=pass)", () => { 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"); }); }); @@ -157,7 +157,7 @@ describe("recordEvidence error cases", () => { await expect( 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 () => { @@ -165,7 +165,7 @@ describe("recordEvidence error cases", () => { await expect( recordEvidence(repo, { goalId: "G001", criterionId: "CUNKNOWN", status: "pass", evidence: "x" }), - ).rejects.toBeInstanceOf(UltragoalError); + ).rejects.toBeInstanceOf(UlwLoopError); }); it("throws when evidence is empty/whitespace", async () => { @@ -173,7 +173,7 @@ describe("recordEvidence error cases", () => { await expect( recordEvidence(repo, { goalId: "G001", criterionId: "C001", status: "pass", evidence: " " }), - ).rejects.toBeInstanceOf(UltragoalError); + ).rejects.toBeInstanceOf(UlwLoopError); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/.gitkeep b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/.gitkeep similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/.gitkeep rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/.gitkeep diff --git a/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/codex-goal-snapshot.json b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/codex-goal-snapshot.json new file mode 100644 index 000000000..ba0932eb9 --- /dev/null +++ b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/codex-goal-snapshot.json @@ -0,0 +1 @@ +{ "goal": { "objective": "Complete the durable ulw-loop plan", "status": "active" } } diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-brief.md b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-brief.md similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-brief.md rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-brief.md diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-plan.json b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-plan.json similarity index 97% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-plan.json rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-plan.json index f49feee34..61378785a 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-plan.json +++ b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-plan.json @@ -2,7 +2,7 @@ "version": 1, "createdAt": "2026-05-23T00:00:00.000Z", "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": [], "goals": [ { diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-quality-gate.json b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/sample-quality-gate.json rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/sample-quality-gate.json diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/steering-proposal.json b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/steering-proposal.json similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/steering-proposal.json rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/steering-proposal.json diff --git a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/user-prompt-submit.json b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/user-prompt-submit.json similarity index 55% rename from packages/omo-codex/plugin/components/ultragoal/test/fixtures/user-prompt-submit.json rename to packages/omo-codex/plugin/components/ulw-loop/test/fixtures/user-prompt-submit.json index 0f6e079e3..e10a28fe1 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/fixtures/user-prompt-submit.json +++ b/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/user-prompt-submit.json @@ -3,7 +3,7 @@ "hook_event_name": "UserPromptSubmit", "model": "gpt-5.5", "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", "transcript_path": "/tmp/transcript.log", "turn_id": "t1" diff --git a/packages/omo-codex/plugin/components/ultragoal/test/goal-status.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/goal-status.test.ts similarity index 84% rename from packages/omo-codex/plugin/components/ultragoal/test/goal-status.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/goal-status.test.ts index 9b79b8bb4..04670c7c6 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/goal-status.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/goal-status.test.ts @@ -8,14 +8,14 @@ import { firstUnresolvedCriterion, hasAllCriteriaPass, isFinalRunCompletionCandidate, - isUltragoalDone, - ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, + isUlwLoopDone, + 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"; -function makeCriterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function makeCriterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path", @@ -27,7 +27,7 @@ function makeCriterion(overrides: Partial = {}): Ultr }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Goal one", @@ -41,20 +41,20 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(overrides: Partial = {}): UltragoalPlan { +function makePlan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals: [], ...overrides, }; } -describe("isUltragoalDone", () => { +describe("isUlwLoopDone", () => { it("returns true when all goals complete", () => { // given const plan = makePlan({ @@ -62,7 +62,7 @@ describe("isUltragoalDone", () => { }); // when - const done = isUltragoalDone(plan); + const done = isUlwLoopDone(plan); // then expect(done).toBe(true); @@ -73,7 +73,7 @@ describe("isUltragoalDone", () => { const plan = makePlan({ goals: [makeGoal({ status: "complete" }), makeGoal({ id: "G002", status: "pending" })] }); // when - const done = isUltragoalDone(plan); + const done = isUlwLoopDone(plan); // then expect(done).toBe(false); @@ -91,7 +91,7 @@ describe("isUltragoalDone", () => { const plan = makePlan({ goals: [superseded, replacement] }); // when - const done = isUltragoalDone(plan); + const done = isUlwLoopDone(plan); // then expect(done).toBe(true); @@ -155,7 +155,7 @@ describe("expectedCodexObjective", () => { 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 const goal = makeGoal({ objective: "story objective" }); const plan = makePlan({ codexGoalMode: "aggregate" }); @@ -164,7 +164,7 @@ describe("expectedCodexObjective", () => { const objective = expectedCodexObjective(plan, goal); // then - expect(objective).toBe(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE); + expect(objective).toBe(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE); }); it("per_story mode returns goal.objective", () => { @@ -189,12 +189,12 @@ describe("aggregateCodexObjective", () => { expect(objective).toBe("aggregate objective"); }); - it("falls back to ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE", () => { + it("falls back to ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE", () => { // when const objective = aggregateCodexObjective(makePlan()); // 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", () => { - it("references the .omo/ultragoal path and excludes the legacy workspace", () => { +describe("ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE", () => { + it("references the .omo/ulw-loop path and excludes the legacy workspace", () => { const legacyWorkspace = [".", "om", "x"].join(""); - expect(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE).toContain(".omo/ultragoal"); - expect(ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE).not.toContain(legacyWorkspace); + expect(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE).toContain(".omo/ulw-loop"); + expect(ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE).not.toContain(legacyWorkspace); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/package-smoke.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts similarity index 78% rename from packages/omo-codex/plugin/components/ultragoal/test/package-smoke.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts index 072853d05..a8350f62b 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/package-smoke.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts @@ -55,7 +55,7 @@ describe("hooks/hooks.json", () => { 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"); 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 () => { - 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); }); - it("#given Codex skill hinting #when ultragoal skill metadata is inspected #then ulw-loop is the primary mention name", async () => { - const text = await readText("skills/ultragoal/SKILL.md"); + 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/ulw-loop/SKILL.md"); 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("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 () => { - const text = await readText("skills/ultragoal/agents/openai.yaml"); + it("#given Codex dollar hinting #when querying ulw-loop #then ulw-loop surfaces the ulw-loop alias", async () => { + const text = await readText("skills/ulw-loop/agents/openai.yaml"); 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("Use $ulw-loop"); }); - it("#given Codex dollar hinting #when querying ultragoal #then ultragoal remains discoverable as an alias", async () => { - const text = await readText("skills/ultragoal/agents/openai.yaml"); + it("#given Codex dollar hinting #when querying ulw-loop #then ulw-loop remains discoverable as an alias", async () => { + const text = await readText("skills/ulw-loop/agents/openai.yaml"); expect(text).toContain("search_terms:"); - expect(text).toContain('- "ultragoal"'); + expect(text).toContain('- "ulw-loop"'); }); 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"); }); 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()).toContain("record-evidence"); }); 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("ULTRAGOAL_CLI"); - expect(text).toContain("components/ultragoal/dist/cli.js"); + expect(text).toContain("ULW_LOOP_CLI"); + 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 () => { - 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("ULTRAGOAL_NODE"); - expect(text).toContain(".omo/ultragoal/bootstrap-notepad.md"); + expect(text).toContain("ULW_LOOP_NODE"); + expect(text).toContain(".omo/ulw-loop/bootstrap-notepad.md"); expect(text).not.toContain("ls -1"); }); it("uses the .omo workspace path", async () => { - const text = await readText("skills/ultragoal/SKILL.md"); - expect(text).toContain(".omo/ultragoal"); + const text = await readText("skills/ulw-loop/SKILL.md"); + expect(text).toContain(".omo/ulw-loop"); }); }); diff --git a/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts new file mode 100644 index 000000000..abc00321c --- /dev/null +++ b/packages/omo-codex/plugin/components/ulw-loop/test/paths.test.ts @@ -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"); + }); +}); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/plan-crud.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/plan-crud.test.ts similarity index 68% rename from packages/omo-codex/plugin/components/ultragoal/test/plan-crud.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/plan-crud.test.ts index 4d3d366cf..abc80dcf2 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/plan-crud.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/plan-crud.test.ts @@ -3,18 +3,18 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { ultragoalBriefPath, ultragoalGoalsPath, ultragoalLedgerPath } from "../src/paths.js"; +import { ulwLoopBriefPath, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js"; import { - addUltragoalGoal, - createUltragoalPlan, + addUlwLoopGoal, + createUlwLoopPlan, deriveGoalCandidates, seedDefaultSuccessCriteria, - startNextUltragoal, - summarizeUltragoalPlan, + startNextUlwLoop, + summarizeUlwLoopPlan, } from "../src/plan-crud.js"; import { writePlan } from "../src/plan-io.js"; -import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; @@ -27,20 +27,20 @@ async function readBriefFixture(): Promise { } async function ledgerKinds(repoRoot: string): Promise { - const raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8"); + const raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8"); return raw .split(/\r?\n/) .filter(Boolean) .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"); if (base === undefined) throw new Error("expected seeded criterion"); return { ...base, status }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Build auth service", @@ -54,20 +54,20 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(goals: UltragoalItem[]): UltragoalPlan { +function makePlan(goals: UlwLoopItem[]): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", codexGoalMode: "aggregate", goals, }; } -function scheduled(result: Awaited>) { +function scheduled(result: Awaited>) { if ("done" in result) throw new Error("expected scheduled goal"); return result; } @@ -93,20 +93,20 @@ describe("seedDefaultSuccessCriteria", () => { }); }); -describe("createUltragoalPlan", () => { - it("creates .omo/ultragoal/{brief.md, goals.json, ledger.jsonl} in repoRoot", async () => { +describe("createUlwLoopPlan", () => { + it("creates .omo/ulw-loop/{brief.md, goals.json, ledger.jsonl} in repoRoot", async () => { const repoRoot = await makeRepo(); 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(ultragoalGoalsPath(repoRoot), "utf8")).toContain("G001-build-the-jwt-auth-endpoint"); + expect(await readFile(ulwLoopBriefPath(repoRoot), "utf8")).toBe(brief.endsWith("\n") ? brief : `${brief}\n`); + expect(await readFile(ulwLoopGoalsPath(repoRoot), "utf8")).toContain("G001-build-the-jwt-auth-endpoint"); expect(await ledgerKinds(repoRoot)).toEqual(["plan_created"]); }); 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.every((goal) => goal.successCriteria.length >= 3)).toBe(true); @@ -114,17 +114,17 @@ describe("createUltragoalPlan", () => { it("refuses overwrite of an existing plan without --force", async () => { 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(createUltragoalPlan(repoRoot, { brief: "second" })).rejects.toThrow("Refusing to overwrite"); + await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow(UlwLoopError); + await expect(createUlwLoopPlan(repoRoot, { brief: "second" })).rejects.toThrow("Refusing to overwrite"); }); 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.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 () => { 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(goal.id).toBe("G002-add-rate-limit"); @@ -164,20 +164,20 @@ describe("addUltragoalGoal", () => { it("appends a ledger entry for goal_added", async () => { 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"]); }); }); -describe("startNextUltragoal", () => { +describe("startNextUlwLoop", () => { it("picks the first pending goal", async () => { 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.status).toBe("in_progress"); @@ -186,11 +186,11 @@ describe("startNextUltragoal", () => { it("resumes the in_progress goal when one exists", async () => { 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" }); 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.resumed).toBe(true); @@ -199,10 +199,10 @@ describe("startNextUltragoal", () => { it("with retryFailed picks first failed (non-blocked) goal", async () => { const repoRoot = await makeRepo(); 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])); - 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.attempt).toBe(1); @@ -211,16 +211,16 @@ describe("startNextUltragoal", () => { it("returns { done: true } when no eligible goals remain", async () => { 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" })])); - const result = await startNextUltragoal(repoRoot, {}); + const result = await startNextUlwLoop(repoRoot, {}); expect(result).toMatchObject({ done: true }); }); }); -describe("summarizeUltragoalPlan", () => { +describe("summarizeUlwLoopPlan", () => { it("counts goals by status", () => { const plan = makePlan([ makeGoal({ id: "G001", status: "pending" }), @@ -232,7 +232,7 @@ describe("summarizeUltragoalPlan", () => { makeGoal({ id: "G007", status: "needs_user_decision", steeringStatus: "superseded" }), ]); - expect(summarizeUltragoalPlan(plan)).toMatchObject({ + expect(summarizeUlwLoopPlan(plan)).toMatchObject({ total: 7, pending: 1, in_progress: 1, @@ -251,6 +251,6 @@ describe("summarizeUltragoalPlan", () => { 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 }); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/plan-io.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts similarity index 69% rename from packages/omo-codex/plugin/components/ultragoal/test/plan-io.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts index 9791e5d9c..fe4a07775 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/plan-io.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/plan-io.test.ts @@ -2,22 +2,22 @@ import { copyFile, mkdir, mkdtemp, readdir, readFile, writeFile } from "node:fs/ import { tmpdir } from "node:os"; import { join } from "node:path"; import { beforeEach, describe, expect, it } from "vitest"; -import { ultragoalDir, ultragoalGoalsPath, ultragoalLedgerPath } from "../src/paths.js"; +import { ulwLoopDir, ulwLoopGoalsPath, ulwLoopLedgerPath } from "../src/paths.js"; import { appendLedger, readSteeringLedgerEntries, - readUltragoalPlan, - withUltragoalMutationLock, + readUlwLoopPlan, + withUlwLoopMutationLock, writePlan, } from "../src/plan-io.js"; -import type { UltragoalItem, UltragoalLedgerEntry, UltragoalPlan } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopLedgerEntry, UlwLoopPlan } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; 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 { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Build auth service", @@ -31,14 +31,14 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(overrides: Partial = {}): UltragoalPlan { +function makePlan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", codexGoalMode: "aggregate", codexObjective: STABLE_OBJECTIVE, codexObjectiveAliases: [], @@ -47,7 +47,7 @@ function makePlan(overrides: Partial = {}): UltragoalPlan { }; } -function entry(kind: UltragoalLedgerEntry["kind"], goalId = "G001"): UltragoalLedgerEntry { +function entry(kind: UlwLoopLedgerEntry["kind"], goalId = "G001"): UlwLoopLedgerEntry { return { at: NOW, kind, goalId }; } @@ -55,17 +55,17 @@ async function makeRepo(): Promise { return mkdtemp(join(tmpdir(), "ug-io-")); } -async function writeRawPlan(repoRoot: string, plan: UltragoalPlan): Promise { - await mkdir(ultragoalDir(repoRoot), { recursive: true }); - await writeFile(ultragoalGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8"); +async function writeRawPlan(repoRoot: string, plan: UlwLoopPlan): Promise { + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); + await writeFile(ulwLoopGoalsPath(repoRoot), `${JSON.stringify(plan, null, 2)}\n`, "utf8"); } async function readLedgerLines(repoRoot: string): Promise { - const raw = await readFile(ultragoalLedgerPath(repoRoot), "utf8"); + const raw = await readFile(ulwLoopLedgerPath(repoRoot), "utf8"); return raw.split(/\r?\n/).filter(Boolean); } -describe("readUltragoalPlan", () => { +describe("readUlwLoopPlan", () => { let repoRoot = ""; beforeEach(async () => { @@ -73,19 +73,19 @@ describe("readUltragoalPlan", () => { repoRoot = await makeRepo(); }); - it("throws UltragoalError when goals.json is missing", async () => { + it("throws UlwLoopError when goals.json is missing", async () => { // when/then - await expect(readUltragoalPlan(repoRoot)).rejects.toThrow(UltragoalError); - await expect(readUltragoalPlan(repoRoot)).rejects.toThrow("omo ultragoal create-goals"); + await expect(readUlwLoopPlan(repoRoot)).rejects.toThrow(UlwLoopError); + await expect(readUlwLoopPlan(repoRoot)).rejects.toThrow("omo ulw-loop create-goals"); }); it("returns parsed plan when fixture is present", async () => { // given - await mkdir(ultragoalDir(repoRoot), { recursive: true }); - await copyFile(join(process.cwd(), "test", "fixtures", "sample-plan.json"), ultragoalGoalsPath(repoRoot)); + await mkdir(ulwLoopDir(repoRoot), { recursive: true }); + await copyFile(join(process.cwd(), "test", "fixtures", "sample-plan.json"), ulwLoopGoalsPath(repoRoot)); // when - const plan = await readUltragoalPlan(repoRoot); + const plan = await readUlwLoopPlan(repoRoot); // then 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 () => { // 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 })); // when - const plan = await readUltragoalPlan(repoRoot); + const plan = await readUlwLoopPlan(repoRoot); // then expect(plan.codexObjective).toBe(STABLE_OBJECTIVE); 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] }); const lines = await readLedgerLines(repoRoot); expect(lines).toHaveLength(1); @@ -125,9 +125,9 @@ describe("writePlan", () => { await writePlan(repoRoot, makePlan()); // 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((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 () => { @@ -139,7 +139,7 @@ describe("writePlan", () => { await writePlan(repoRoot, makePlan({ codexObjective: "second" })); // then - expect(JSON.parse(await readFile(ultragoalGoalsPath(repoRoot), "utf8"))).toMatchObject({ + expect(JSON.parse(await readFile(ulwLoopGoalsPath(repoRoot), "utf8"))).toMatchObject({ codexObjective: "second", }); }); @@ -166,7 +166,7 @@ describe("appendLedger", () => { await appendLedger(repoRoot, entry("goal_completed")); // then - expect(await readFile(ultragoalLedgerPath(repoRoot), "utf8")).toContain("goal_completed"); + expect(await readFile(ulwLoopLedgerPath(repoRoot), "utf8")).toContain("goal_completed"); }); it("preserves prior entries", async () => { @@ -209,7 +209,7 @@ describe("readSteeringLedgerEntries", () => { }); }); -describe("withUltragoalMutationLock", () => { +describe("withUlwLoopMutationLock", () => { it("serializes concurrent invocations", async () => { // given const repoRoot = await makeRepo(); @@ -221,7 +221,7 @@ describe("withUltragoalMutationLock", () => { // when await Promise.all( [1, 2, 3].map((_) => - withUltragoalMutationLock(repoRoot, async () => { + withUlwLoopMutationLock(repoRoot, async () => { active += 1; maxActive = Math.max(maxActive, active); const current = Number(await readFile(counterPath, "utf8")); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/quality-gate.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts similarity index 78% rename from packages/omo-codex/plugin/components/ultragoal/test/quality-gate.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts index afb975739..9dd0aebc0 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/quality-gate.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts @@ -9,8 +9,8 @@ import { sameBlockerOccurrences, validateQualityGate, } from "../src/quality-gate.js"; -import type { UltragoalItem, UltragoalPlan } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopPlan } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; const VALID_GATE = { @@ -20,7 +20,7 @@ const VALID_GATE = { criteriaCoverage: { totalCriteria: 2, passCount: 2, adversarialClassesCovered: ["malformed_input"] }, } as const; -interface GoalWithBlocker extends UltragoalItem { +interface GoalWithBlocker extends UlwLoopItem { blocker?: { readonly signature: string }; blockerEvidence?: string; blockerOccurrences?: number; @@ -31,17 +31,17 @@ function makeGate(overrides: Record = {}): Record = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Goal one", @@ -55,14 +55,14 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(goals: UltragoalItem[]): UltragoalPlan { +function makePlan(goals: UlwLoopItem[]): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals, }; } @@ -81,39 +81,39 @@ describe("validateQualityGate", () => { expect(gate).toMatchObject({ criteriaCoverage: { totalCriteria: 9, passCount: 9 } }); }); - it("throws UltragoalError when aiSlopCleaner missing", () => { + it("throws UlwLoopError when aiSlopCleaner missing", () => { // when const error = getQualityGateError(makeGate({ aiSlopCleaner: undefined })); // 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 const error = getQualityGateError(makeGate({ verification: undefined })); // 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 const error = getQualityGateError(makeGate({ codeReview: undefined })); // 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 const error = getQualityGateError(makeGate({ criteriaCoverage: undefined })); // 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 const error = getQualityGateError( makeGate({ criteriaCoverage: { totalCriteria: 3, passCount: 2, adversarialClassesCovered: [] } }), @@ -123,7 +123,7 @@ describe("validateQualityGate", () => { 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 const error = getQualityGateError( makeGate({ codeReview: { ...VALID_GATE.codeReview, recommendation: "COMMENT" } }), @@ -133,7 +133,7 @@ describe("validateQualityGate", () => { expect(error.message).toContain("recommendation"); }); - it("throws UltragoalError when architectStatus is not CLEAR", () => { + it("throws UlwLoopError when architectStatus is not CLEAR", () => { // when const error = getQualityGateError( makeGate({ codeReview: { ...VALID_GATE.codeReview, architectStatus: "WATCH" } }), diff --git a/packages/omo-codex/plugin/components/ultragoal/test/review-blockers.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/review-blockers.test.ts similarity index 67% rename from packages/omo-codex/plugin/components/ultragoal/test/review-blockers.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/review-blockers.test.ts index 95d47da94..025133713 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/review-blockers.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/review-blockers.test.ts @@ -3,16 +3,16 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; -import { ultragoalDir, ultragoalLedgerPath } from "../src/paths.js"; +import { ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE } from "../src/goal-status.js"; +import { ulwLoopDir, ulwLoopLedgerPath } from "../src/paths.js"; import { writePlan } from "../src/plan-io.js"; import { recordFinalReviewBlockers } from "../src/review-blockers.js"; -import type { UltragoalItem, UltragoalPlan, UltragoalSuccessCriterion } from "../src/types.js"; -import { UltragoalError } from "../src/types.js"; +import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/types.js"; +import { UlwLoopError } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; 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 = { @@ -23,7 +23,7 @@ const validArgs = { codexGoalJson: VALID_SNAPSHOT_JSON, }; -function makeCriterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function makeCriterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "happy path", @@ -35,11 +35,11 @@ function makeCriterion(overrides: Partial = {}): Ultr }; } -function makeGoal(overrides: Partial = {}): UltragoalItem { +function makeGoal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Build durable plan", - objective: "Complete one ultragoal story", + objective: "Complete one ulw-loop story", status: "pending", successCriteria: [makeCriterion()], attempt: 1, @@ -49,49 +49,49 @@ function makeGoal(overrides: Partial = {}): UltragoalItem { }; } -function makePlan(overrides: Partial = {}): UltragoalPlan { +function makePlan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", codexGoalMode: "aggregate", - codexObjective: ULTRAGOAL_AGGREGATE_CODEX_OBJECTIVE, + codexObjective: ULW_LOOP_AGGREGATE_CODEX_OBJECTIVE, goals: [makeGoal({ status: "in_progress" })], ...overrides, }; } -async function bootstrapRepo(plan: UltragoalPlan): Promise { +async function bootstrapRepo(plan: UlwLoopPlan): Promise { 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); return repo; } async function ledgerKinds(repo: string): Promise { - const raw = await readFile(ultragoalLedgerPath(repo), "utf8"); + const raw = await readFile(ulwLoopLedgerPath(repo), "utf8"); return raw .split(/\r?\n/) .filter(Boolean) .map((line) => JSON.parse(line).kind); } -async function expectUltragoalCode(action: () => Promise, code: string): Promise { +async function expectUlwLoopCode(action: () => Promise, code: string): Promise { try { await action(); } catch (error) { - expect(error).toBeInstanceOf(UltragoalError); - if (!(error instanceof UltragoalError)) throw error; + expect(error).toBeInstanceOf(UlwLoopError); + if (!(error instanceof UlwLoopError)) throw error; expect(error.code).toBe(code); return; } - throw new Error("Expected UltragoalError"); + throw new Error("Expected UlwLoopError"); } -function finalPlan(): UltragoalPlan { +function finalPlan(): UlwLoopPlan { return makePlan({ activeGoalId: "G002", goals: [ @@ -129,42 +129,42 @@ describe("recordFinalReviewBlockers happy path", () => { }); 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()); - await expectUltragoalCode( + await expectUlwLoopCode( () => 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( makePlan({ 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( makePlan({ goals: [makeGoal({ id: "G001", status: "in_progress" }), makeGoal({ id: "G002", status: "pending" })], }), ); - await expectUltragoalCode( + await expectUlwLoopCode( () => 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 codexGoalJson = JSON.stringify({ goal: { objective: "wrong", status: "active" } }); - await expectUltragoalCode( + await expectUlwLoopCode( () => recordFinalReviewBlockers(repo, { ...validArgs, codexGoalJson }), - "ultragoal_codex_snapshot_mismatch", + "ulw_loop_codex_snapshot_mismatch", ); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/steering.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts similarity index 71% rename from packages/omo-codex/plugin/components/ultragoal/test/steering.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts index 5a66a6f01..aa50dbea4 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/steering.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/steering.test.ts @@ -2,20 +2,20 @@ import { mkdtemp, readFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import { ultragoalGoalsPath } from "../src/paths.js"; -import { readSteeringLedgerEntries, readUltragoalPlan, writePlan } from "../src/plan-io.js"; +import { ulwLoopGoalsPath } from "../src/paths.js"; +import { readSteeringLedgerEntries, readUlwLoopPlan, writePlan } from "../src/plan-io.js"; import { applySteeringMutation, - parseUltragoalSteeringDirective, - steerUltragoal, - validateUltragoalSteeringProposal, + parseUlwLoopSteeringDirective, + steerUlwLoop, + validateUlwLoopSteeringProposal, } from "../src/steering.js"; import type { - UltragoalItem, - UltragoalPlan, - UltragoalSteeringProposal, - UltragoalSuccessCriterion, - UltragoalSuccessCriterionUserModel, + UlwLoopItem, + UlwLoopPlan, + UlwLoopSteeringProposal, + UlwLoopSuccessCriterion, + UlwLoopSuccessCriterionUserModel, } from "../src/types.js"; const NOW = "2026-05-23T00:00:00.000Z"; @@ -24,11 +24,11 @@ type CriterionSteeringFields = { readonly goalId?: string; readonly scenario?: string; readonly expectedEvidence?: string; - readonly userModel?: UltragoalSuccessCriterionUserModel; + readonly userModel?: UlwLoopSuccessCriterionUserModel; }; -type SteeringInput = UltragoalSteeringProposal & CriterionSteeringFields; +type SteeringInput = UlwLoopSteeringProposal & CriterionSteeringFields; -function criterion(overrides: Partial = {}): UltragoalSuccessCriterion { +function criterion(overrides: Partial = {}): UlwLoopSuccessCriterion { return { id: "C001", scenario: "old scenario", @@ -40,7 +40,7 @@ function criterion(overrides: Partial = {}): Ultragoa }; } -function goal(overrides: Partial = {}): UltragoalItem { +function goal(overrides: Partial = {}): UlwLoopItem { return { id: "G001", title: "Build auth service", @@ -54,14 +54,14 @@ function goal(overrides: Partial = {}): UltragoalItem { }; } -function plan(overrides: Partial = {}): UltragoalPlan { +function plan(overrides: Partial = {}): UlwLoopPlan { return { version: 1, createdAt: NOW, updatedAt: NOW, - briefPath: ".omo/ultragoal/brief.md", - goalsPath: ".omo/ultragoal/goals.json", - ledgerPath: ".omo/ultragoal/ledger.jsonl", + briefPath: ".omo/ulw-loop/brief.md", + goalsPath: ".omo/ulw-loop/goals.json", + ledgerPath: ".omo/ulw-loop/ledger.jsonl", goals: [ goal(), goal({ id: "G002", title: "Rate limit", objective: "Throttle login" }), @@ -83,18 +83,18 @@ function steering(overrides: Partial = {}): SteeringInput { }; } -async function repoWithPlan(seed: UltragoalPlan = plan()): Promise { +async function repoWithPlan(seed: UlwLoopPlan = plan()): Promise { const repoRoot = await mkdtemp(join(tmpdir(), "ug-steer-")); await writePlan(repoRoot, seed); return repoRoot; } -describe("validateUltragoalSteeringProposal", () => { +describe("validateUlwLoopSteeringProposal", () => { it("accepts valid add_subgoal", async () => { const proposal: unknown = JSON.parse( 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([ @@ -104,26 +104,23 @@ describe("validateUltragoalSteeringProposal", () => { ["protected payload mutations", { after: { codexObjective: "replace", qualityGate: { status: "passed" } } }], ["weakened completion text", { objective: "skip tests and mark complete faster" }], ])("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.rejectedReasons.length).toBeGreaterThan(0); }); it("rejects when plan already 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", () => { - const audit = validateUltragoalSteeringProposal( - plan(), - steering({ kind: "split_subgoal", targetGoalId: "G001" }), - ); + const audit = validateUlwLoopSteeringProposal(plan(), steering({ kind: "split_subgoal", targetGoalId: "G001" })); expect(audit.invariant.accepted).toBe(false); }); it("rejects reorder_pending with unknown goal id", () => { - const audit = validateUltragoalSteeringProposal( + const audit = validateUlwLoopSteeringProposal( plan(), steering({ kind: "reorder_pending", pendingOrder: ["missing"] }), ); @@ -134,7 +131,7 @@ describe("validateUltragoalSteeringProposal", () => { ["new scenario", { scenario: "new precise scenario" }], ["new expectedEvidence", { expectedEvidence: "specific command output" }], ])("accepts valid revise_criterion with %s", (_name, update) => { - const audit = validateUltragoalSteeringProposal( + const audit = validateUlwLoopSteeringProposal( plan(), steering({ kind: "revise_criterion", goalId: "G001", criterionId: "C001", ...update }), ); @@ -146,16 +143,16 @@ describe("validateUltragoalSteeringProposal", () => { ["unknown criterionId", { goalId: "G001", criterionId: "missing", scenario: "new" }], ["no updates", { goalId: "G001", criterionId: "C001" }], ])("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); }); }); -describe("steerUltragoal", () => { +describe("steerUlwLoop", () => { it("add_subgoal: appends goal + ledger entry", async () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal(repoRoot, steering({ idempotencyKey: "add" })); - const persisted = await readUltragoalPlan(repoRoot); + const result = await steerUlwLoop(repoRoot, steering({ idempotencyKey: "add" })); + const persisted = await readUlwLoopPlan(repoRoot); expect(result.accepted).toBe(true); expect(persisted.goals.at(-1)).toMatchObject({ id: "G004", title: "Investigate auth blocker" }); expect((await readSteeringLedgerEntries(repoRoot)).at(-1)).toMatchObject({ @@ -166,7 +163,7 @@ describe("steerUltragoal", () => { it("split_subgoal: creates children + supersedes parent", async () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, steering({ kind: "split_subgoal", @@ -180,7 +177,7 @@ describe("steerUltragoal", () => { it("reorder_pending: changes goal order", async () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, steering({ kind: "reorder_pending", pendingOrder: ["G002", "G001"] }), ); @@ -189,7 +186,7 @@ describe("steerUltragoal", () => { it("revise_pending_wording: updates title/objective", async () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, steering({ kind: "revise_pending_wording", @@ -207,14 +204,14 @@ describe("steerUltragoal", () => { it("annotate_ledger: ledger-only, no plan mutation", async () => { const seed = plan(); 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(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 () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, steering({ kind: "mark_blocked_superseded", @@ -228,7 +225,7 @@ describe("steerUltragoal", () => { it("mark_blocked_superseded without children: blocks goal", async () => { const repoRoot = await repoWithPlan(); - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, 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) => { const repoRoot = await repoWithPlan(); const criterionId = status === "pending" ? "C001" : "C002"; - const result = await steerUltragoal( + const result = await steerUlwLoop( repoRoot, steering({ kind: "revise_criterion", @@ -261,7 +258,7 @@ describe("steerUltragoal", () => { }); it("revise_criterion: updates the targeted criterion in plan", () => { - const audit = validateUltragoalSteeringProposal( + const audit = validateUlwLoopSteeringProposal( plan(), 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 () => { const repoRoot = await repoWithPlan(); - await steerUltragoal(repoRoot, steering({ idempotencyKey: "same-key" })); - const second = await steerUltragoal(repoRoot, steering({ idempotencyKey: "same-key" })); + await steerUlwLoop(repoRoot, steering({ idempotencyKey: "same-key" })); + const second = await steerUlwLoop(repoRoot, steering({ idempotencyKey: "same-key" })); expect(second.deduped).toBe(true); - expect((await readUltragoalPlan(repoRoot)).goals).toHaveLength(4); + expect((await readUlwLoopPlan(repoRoot)).goals).toHaveLength(4); }); }); -describe("parseUltragoalSteeringDirective", () => { - it.each(["OMO_ULTRAGOAL_STEER", "omo.ultragoal.steer", "omo ultragoal steer"])("parses %s pattern", (marker) => { - expect(parseUltragoalSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toMatchObject({ +describe("parseUlwLoopSteeringDirective", () => { + it.each(["OMO_ULW_LOOP_STEER", "omo.ulw-loop.steer", "omo ulw-loop steer"])("parses %s pattern", (marker) => { + expect(parseUlwLoopSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toMatchObject({ kind: "add_subgoal", }); }); 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", () => { - expect(parseUltragoalSteeringDirective("OMO_ULTRAGOAL_STEER: {bad json")).toBeNull(); + expect(parseUlwLoopSteeringDirective("OMO_ULW_LOOP_STEER: {bad json")).toBeNull(); }); it("returns null for deprecated markers", () => { - const marker = ["OM", "X_ULTRAGOAL_STEER"].join(""); - expect(parseUltragoalSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toBeNull(); + const marker = ["OM", "X_ULW_LOOP_STEER"].join(""); + expect(parseUlwLoopSteeringDirective(`${marker}: ${JSON.stringify(steering())}`)).toBeNull(); }); }); diff --git a/packages/omo-codex/plugin/components/ultragoal/test/types.test.ts b/packages/omo-codex/plugin/components/ulw-loop/test/types.test.ts similarity index 55% rename from packages/omo-codex/plugin/components/ultragoal/test/types.test.ts rename to packages/omo-codex/plugin/components/ulw-loop/test/types.test.ts index e4d7ddfa0..794f17ee6 100644 --- a/packages/omo-codex/plugin/components/ultragoal/test/types.test.ts +++ b/packages/omo-codex/plugin/components/ulw-loop/test/types.test.ts @@ -2,56 +2,56 @@ import { describe, expect, it } from "vitest"; import { iso, - ULTRAGOAL_BRIEF, - ULTRAGOAL_CRITERION_STATUSES, - ULTRAGOAL_DIR, - ULTRAGOAL_GOALS, - ULTRAGOAL_LEDGER, - ULTRAGOAL_STEERING_MUTATION_KINDS, - ULTRAGOAL_SUCCESS_CRITERION_USER_MODELS, - UltragoalError, + ULW_LOOP_BRIEF, + ULW_LOOP_CRITERION_STATUSES, + ULW_LOOP_DIR, + ULW_LOOP_GOALS, + ULW_LOOP_LEDGER, + ULW_LOOP_STEERING_MUTATION_KINDS, + ULW_LOOP_SUCCESS_CRITERION_USER_MODELS, + UlwLoopError, } from "../src/types.ts"; -describe("ultragoal domain constants", () => { +describe("ulw-loop domain constants", () => { describe("when checking workspace paths", () => { - it("then ULTRAGOAL_DIR points to the omo workspace", () => { - expect(ULTRAGOAL_DIR).toBe(".omo/ultragoal"); + it("then ULW_LOOP_DIR points to the omo workspace", () => { + expect(ULW_LOOP_DIR).toBe(".omo/ulw-loop"); }); it("then artifact filenames are stable", () => { - expect(ULTRAGOAL_BRIEF).toBe("brief.md"); - expect(ULTRAGOAL_GOALS).toBe("goals.json"); - expect(ULTRAGOAL_LEDGER).toBe("ledger.jsonl"); + expect(ULW_LOOP_BRIEF).toBe("brief.md"); + expect(ULW_LOOP_GOALS).toBe("goals.json"); + expect(ULW_LOOP_LEDGER).toBe("ledger.jsonl"); }); }); describe("when checking steering mutation kinds", () => { 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", () => { - expect(ULTRAGOAL_STEERING_MUTATION_KINDS).toHaveLength(7); + expect(ULW_LOOP_STEERING_MUTATION_KINDS).toHaveLength(7); }); }); describe("when checking criterion user models", () => { 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", () => { 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", () => { 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.code).toBe("TEST_CODE"); @@ -60,7 +60,7 @@ describe("UltragoalError", () => { it("then accepts optional cause + details", () => { 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.details).toEqual({ goalId: "G001" }); diff --git a/packages/omo-codex/plugin/components/ultragoal/tsconfig.build.json b/packages/omo-codex/plugin/components/ulw-loop/tsconfig.build.json similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/tsconfig.build.json rename to packages/omo-codex/plugin/components/ulw-loop/tsconfig.build.json diff --git a/packages/omo-codex/plugin/components/ultragoal/tsconfig.json b/packages/omo-codex/plugin/components/ulw-loop/tsconfig.json similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/tsconfig.json rename to packages/omo-codex/plugin/components/ulw-loop/tsconfig.json diff --git a/packages/omo-codex/plugin/components/ultragoal/vitest.config.ts b/packages/omo-codex/plugin/components/ulw-loop/vitest.config.ts similarity index 100% rename from packages/omo-codex/plugin/components/ultragoal/vitest.config.ts rename to packages/omo-codex/plugin/components/ulw-loop/vitest.config.ts diff --git a/packages/omo-codex/plugin/hooks/hooks.json b/packages/omo-codex/plugin/hooks/hooks.json index c1d8bffb1..8ddcde0e7 100644 --- a/packages/omo-codex/plugin/hooks/hooks.json +++ b/packages/omo-codex/plugin/hooks/hooks.json @@ -45,9 +45,9 @@ "hooks": [ { "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, - "statusMessage": "checking OMO ultragoal steering" + "statusMessage": "checking OMO ulw-loop steering" } ] } @@ -58,7 +58,7 @@ "hooks": [ { "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, "statusMessage": "enforcing OMO unlimited goal budget" } diff --git a/packages/omo-codex/plugin/package.json b/packages/omo-codex/plugin/package.json index c9cbc8d62..52e641fcd 100644 --- a/packages/omo-codex/plugin/package.json +++ b/packages/omo-codex/plugin/package.json @@ -11,7 +11,7 @@ "components/lsp", "components/telemetry", "components/start-work-continuation", - "components/ultragoal", + "components/ulw-loop", "components/ultrawork" ], "scripts": { diff --git a/packages/omo-codex/plugin/scripts/sync-skills.mjs b/packages/omo-codex/plugin/scripts/sync-skills.mjs index 6ba2923a2..79bb154af 100644 --- a/packages/omo-codex/plugin/scripts/sync-skills.mjs +++ b/packages/omo-codex/plugin/scripts/sync-skills.mjs @@ -10,7 +10,7 @@ const skillSources = [ ["comment-checker", "components/comment-checker/skills/comment-checker"], ["lsp", "components/lsp/skills/lsp"], ["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*\(/; diff --git a/packages/omo-codex/plugin/skills/ultragoal/SKILL.md b/packages/omo-codex/plugin/skills/ultragoal/SKILL.md deleted file mode 100644 index 86918797d..000000000 --- a/packages/omo-codex/plugin/skills/ultragoal/SKILL.md +++ /dev/null @@ -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-`, 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 "" --json -omo ultragoal create-goals --brief-file --json -cat | 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: for - verify by `. -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-`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :` 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 --criterion-id --status pass --evidence " | " --json` - - FAIL: `omo ultragoal record-evidence --goal-id --criterion-id --status fail --evidence " | " --notes "" --json` - - BLOCKED: `omo ultragoal record-evidence --goal-id --criterion-id --status blocked --evidence "" --notes "" --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 --json`. -2. Call `get_goal` for a fresh snapshot. -3. Run `omo ultragoal checkpoint --goal-id --status complete --evidence "" --codex-goal-json --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 --title "<...>" --objective "<...>" --evidence "" --codex-goal-json --json`. -7. If clean, checkpoint final completion: -```sh -omo ultragoal checkpoint --goal-id --status complete --evidence "" --codex-goal-json --quality-gate-json --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 [] --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. diff --git a/packages/omo-codex/plugin/skills/ultragoal/.gitkeep b/packages/omo-codex/plugin/skills/ulw-loop/.gitkeep similarity index 100% rename from packages/omo-codex/plugin/skills/ultragoal/.gitkeep rename to packages/omo-codex/plugin/skills/ulw-loop/.gitkeep diff --git a/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md b/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md new file mode 100644 index 000000000..7e83e5ca6 --- /dev/null +++ b/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md @@ -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-`, 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 "" --json +omo ulw-loop create-goals --brief-file --json +cat | 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/-.`), 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: for - verify by `. +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-`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :` 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 --criterion-id --status pass --evidence " | " --json` + - FAIL: `omo ulw-loop record-evidence --goal-id --criterion-id --status fail --evidence " | " --notes "" --json` + - BLOCKED: `omo ulw-loop record-evidence --goal-id --criterion-id --status blocked --evidence "" --notes "" --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 --json`. +2. Call `get_goal` for a fresh snapshot. +3. Run `omo ulw-loop checkpoint --goal-id --status complete --evidence "" --codex-goal-json --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 --title "<...>" --objective "<...>" --evidence "" --codex-goal-json --json`. +7. If clean, checkpoint final completion: +```sh +omo ulw-loop checkpoint --goal-id --status complete --evidence "" --codex-goal-json --quality-gate-json --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 [] --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. diff --git a/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/agents/openai.yaml b/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml similarity index 93% rename from packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/agents/openai.yaml rename to packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml index f6855ddbb..a3c91a292 100644 --- a/packages/omo-codex/plugin/components/ultragoal/skills/ultragoal/agents/openai.yaml +++ b/packages/omo-codex/plugin/skills/ulw-loop/agents/openai.yaml @@ -2,5 +2,5 @@ interface: display_name: "ulw loop" short_description: "Goal-like ultrawork loop for systematic decomposition" search_terms: - - "ultragoal" + - "ulw-loop" default_prompt: "Use $ulw-loop to break this work into a systematic ultrawork loop with evidence-backed checkpoints." diff --git a/packages/omo-codex/plugin/test/aggregate.test.mjs b/packages/omo-codex/plugin/test/aggregate.test.mjs index e94d4a762..303fc68e9 100644 --- a/packages/omo-codex/plugin/test/aggregate.test.mjs +++ b/packages/omo-codex/plugin/test/aggregate.test.mjs @@ -47,7 +47,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i "components/rules/dist/cli.js", "components/start-work-continuation/dist/cli.js", "components/telemetry/dist/cli.js", - "components/ultragoal/dist/cli.js", + "components/ulw-loop/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) { 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 const hooks = await readJson("hooks/hooks.json"); 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; // 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.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^create_goal$"]); }); @@ -128,8 +128,8 @@ test("#given component directories #when scanned #then only intentional resource "rules", "start-work-continuation", "telemetry", - "ultragoal", "ultrawork", + "ulw-loop", ]); for (const name of componentNames) { const expectedManifest = expectedComponentManifests.get(name); diff --git a/packages/omo-codex/plugin/test/sync-skills.test.mjs b/packages/omo-codex/plugin/test/sync-skills.test.mjs index f924ec0df..1f4b297d7 100644 --- a/packages/omo-codex/plugin/test/sync-skills.test.mjs +++ b/packages/omo-codex/plugin/test/sync-skills.test.mjs @@ -20,7 +20,7 @@ const expectedSkills = [ "review-work", "rules", "start-work", - "ultragoal", + "ulw-loop", ]; 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 - const skillRoot = join(root, "skills", "ultragoal"); + const skillRoot = join(root, "skills", "ulw-loop"); // when 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, /Goal-like loop that uses ultrawork mode to decompose work into systematic, evidence-bound steps\./); 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, /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 - const skillRoot = join(root, "skills", "ultragoal"); + const skillRoot = join(root, "skills", "ulw-loop"); // when const interfaceMetadata = await readFile(join(skillRoot, "agents", "openai.yaml"), "utf8"); // then 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 () => { diff --git a/src/cli/install-codex/link-cached-plugin-agents.test.ts b/src/cli/install-codex/link-cached-plugin-agents.test.ts index 62cf859d5..eabac8790 100644 --- a/src/cli/install-codex/link-cached-plugin-agents.test.ts +++ b/src/cli/install-codex/link-cached-plugin-agents.test.ts @@ -12,7 +12,7 @@ async function makeFixture(): Promise<{ codexHome: string; pluginRoot: string }> const codexHome = join(root, "codex") const pluginRoot = join(root, "plugin") 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( join(pluginRoot, "components", "ultrawork", "agents", "explorer.toml"), 'name = "explorer"\n', @@ -22,7 +22,7 @@ async function makeFixture(): Promise<{ codexHome: string; pluginRoot: string }> 'name = "librarian"\n', ) await writeFile( - join(pluginRoot, "components", "ultragoal", "agents", "planner.toml"), + join(pluginRoot, "components", "ulw-loop", "agents", "planner.toml"), 'name = "planner"\n', ) return { codexHome, pluginRoot } @@ -160,7 +160,7 @@ describe("linkCachedPluginAgents", () => { // then const targets = linked.map((entry) => entry.target).sort() 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 () => {