oh-my-opencode 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/bin/version-mismatch.js +47 -0
  2. package/bin/version-mismatch.test.ts +120 -0
  3. package/dist/cli/codex-ulw-loop.d.ts +12 -0
  4. package/dist/cli/doctor/checks/tui-plugin-config.d.ts +2 -0
  5. package/dist/cli/index.js +577 -304
  6. package/dist/cli/install-codex/codex-config-reasoning.d.ts +2 -1
  7. package/dist/cli/install-codex/codex-model-catalog.d.ts +13 -0
  8. package/dist/features/background-agent/concurrency.d.ts +1 -0
  9. package/dist/features/background-agent/process-cleanup.d.ts +6 -0
  10. package/dist/features/claude-code-session-state/state.d.ts +1 -0
  11. package/dist/features/opencode-skill-loader/index.d.ts +1 -0
  12. package/dist/features/opencode-skill-loader/opencode-config-skills-reader.d.ts +5 -0
  13. package/dist/features/tmux-subagent/attachable-session-status.d.ts +1 -1
  14. package/dist/features/tmux-subagent/session-status-parser.d.ts +1 -0
  15. package/dist/hooks/comment-checker/cli.d.ts +1 -0
  16. package/dist/hooks/tasks-todowrite-disabler/constants.d.ts +1 -1
  17. package/dist/index.js +811 -450
  18. package/dist/shared/command-executor/execute-hook-command.d.ts +2 -0
  19. package/dist/tools/skill/description-formatter.d.ts +5 -1
  20. package/dist/tools/skill/types.d.ts +1 -0
  21. package/package.json +12 -13
  22. package/packages/ast-grep-mcp/dist/cli.js +53 -9
  23. package/packages/lsp-tools-mcp/dist/lsp/process.js +1 -1
  24. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus.md +6 -4
  25. package/packages/omo-codex/plugin/components/rules/src/post-compact-budget.ts +0 -2
  26. package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/CHANGELOG.md +1 -1
  28. package/packages/omo-codex/plugin/components/ultrawork/README.md +1 -1
  29. package/packages/omo-codex/plugin/components/ultrawork/agents/codex-ultrawork-reviewer.toml +3 -1
  30. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +7 -7
  31. package/packages/omo-codex/plugin/components/ultrawork/directive.md +1 -1
  32. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +5 -4
  33. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +4 -3
  34. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +12 -1
  35. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint.test.ts +19 -1
  36. package/packages/omo-codex/plugin/hooks/hooks.json +11 -0
  37. package/packages/omo-codex/plugin/model-catalog.json +49 -0
  38. package/packages/omo-codex/plugin/scripts/auto-update.mjs +159 -0
  39. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +269 -0
  40. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +3 -1
  41. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -6
  42. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +6 -6
  43. package/packages/omo-codex/plugin/skills/lcx-report-bug/SKILL.md +127 -0
  44. package/packages/omo-codex/plugin/skills/lcx-report-bug/agents/openai.yaml +9 -0
  45. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +6 -6
  46. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +6 -6
  47. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -7
  48. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +6 -6
  49. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +5 -4
  50. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +4 -3
  51. package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +17 -17
  52. package/packages/omo-codex/plugin/test/aggregate.test.mjs +172 -19
  53. package/packages/omo-codex/plugin/test/auto-update.test.mjs +129 -0
  54. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +2 -0
  55. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -0
  56. package/packages/omo-codex/plugin/test/sync-hook-status-messages.test.mjs +1 -0
  57. package/packages/omo-codex/plugin/test/sync-skills.test.mjs +22 -0
  58. package/packages/omo-codex/scripts/install/cli-args.mjs +1 -1
  59. package/packages/omo-codex/scripts/install/config.mjs +2 -15
  60. package/packages/omo-codex/scripts/install/delegated-command.mjs +1 -1
  61. package/packages/omo-codex/scripts/install/legacy-bins.mjs +1 -0
  62. package/packages/omo-codex/scripts/install/model-catalog.mjs +66 -0
  63. package/packages/omo-codex/scripts/install/reasoning-config.mjs +65 -7
  64. package/packages/omo-codex/scripts/install-bin-links.test.mjs +23 -0
  65. package/packages/omo-codex/scripts/install-config-reasoning.test.mjs +82 -3
  66. package/packages/omo-codex/scripts/install-config.test.mjs +5 -6
  67. package/packages/omo-codex/scripts/install-local-entrypoint.test.mjs +30 -2
  68. package/packages/omo-codex/scripts/install-local.mjs +1 -1
  69. package/packages/shared-skills/skills/lcx-report-bug/SKILL.md +127 -0
  70. package/packages/shared-skills/skills/lcx-report-bug/agents/openai.yaml +9 -0
  71. package/packages/shared-skills/skills/review-work/SKILL.md +7 -7
  72. package/packages/shared-skills/skills/start-work/SKILL.md +6 -6
  73. package/packages/shared-skills/skills/ulw-plan/SKILL.md +11 -11
  74. package/postinstall.mjs +36 -3
  75. package/dist/cli/install-codex/codex-config-mcp.d.ts +0 -1
@@ -8,15 +8,15 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
8
8
 
9
9
  | OpenCode example | Codex tool to use |
10
10
  | --- | --- |
11
- | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...")` |
12
- | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...")` |
13
- | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...")` |
14
- | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")` |
15
- | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...")` |
11
+ | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...", fork_turns="none")` |
12
+ | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...", fork_turns="none")` |
13
+ | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...", fork_turns="none")` |
14
+ | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...", fork_turns="none")` |
15
+ | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...", fork_turns="none")` |
16
16
  | `background_output(task_id="...")` | `wait_agent(...)` to wait for subagent completion and mailbox updates |
17
17
  | `team_*(...)` | Use Codex native subagents plus `send_message`, `followup_task`, `wait_agent`, and `close_agent` |
18
18
 
19
- When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.
19
+ Codex full-history forks inherit the parent agent type, model, and reasoning effort, so role-specific spawns with `agent_type` must use a non-full-history fork mode such as `fork_turns="none"`. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.
20
20
 
21
21
  ## Codex Subagent Reliability
22
22
 
@@ -28,7 +28,7 @@ handoff. Role selection requires `agent_type`; `model` +
28
28
  worker. Prefer `fork_turns: "none"` unless full history is truly
29
29
  required; paste only the review context that worker needs.
30
30
 
31
- Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them.
31
+ Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them. While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates.
32
32
 
33
33
  Use `wait_agent` for completion signals, but treat `wait_agent` as a
34
34
  mailbox signal, not proof of completion, content, or errors. After two
@@ -9,11 +9,11 @@ This skill ports the OpenCode `/start-work` flow onto Codex. Any OpenCode-only t
9
9
 
10
10
  | OpenCode example | Codex tool to use |
11
11
  | --- | --- |
12
- | `task(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...")` |
13
- | `task(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...")` |
14
- | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...")` |
15
- | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")` |
16
- | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...")` |
12
+ | `task(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...", fork_turns="none")` |
13
+ | `task(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...", fork_turns="none")` |
14
+ | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...", fork_turns="none")` |
15
+ | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...", fork_turns="none")` |
16
+ | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...", fork_turns="none")` |
17
17
  | `background_output(task_id="...")` | `wait_agent(...)` |
18
18
  | `dispatchInternalPrompt(...)` | the `Stop` hook emits `{"decision":"block","reason":"<prompt>"}` automatically; see Continuation |
19
19
  | `team_*(...)` | `spawn_agent` + `send_message` + `followup_task` + `wait_agent` + `close_agent` |
@@ -30,7 +30,7 @@ handoff. Role selection requires `agent_type`; `model` +
30
30
  worker. Prefer `fork_turns: "none"` unless full history is truly
31
31
  required; paste only the context the child needs.
32
32
 
33
- Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them.
33
+ Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them. While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates.
34
34
 
35
35
  Use `wait_agent` for completion signals, but treat `wait_agent` as a
36
36
  mailbox signal, not proof of completion, content, or errors. After two
@@ -25,6 +25,7 @@ This Codex skill is intentionally compact to avoid adding a large operating manu
25
25
  - Delegate code edits, test writes, fixes, and QA execution to right-sized Codex subagents when the workflow requires it.
26
26
  - Every `spawn_agent` message starts with `TASK:`, then names `DELIVERABLE`, `SCOPE`, and `VERIFY`; role selection requires `agent_type`, while `model` + `reasoning_effort` alone creates a default agent, not a reviewer or worker; prefer `fork_turns: "none"` unless full history is truly required.
27
27
  - Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them.
28
+ - While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates.
28
29
  - Avoid `list_agents` as a polling or status tool in large runs; it can replay large agent status and latest-message payloads. Track spawned agent names locally, use `wait_agent` for completion signals, targeted followups only when needed, and `close_agent` after integrating each result.
29
30
  - Treat `wait_agent` as a mailbox signal, not proof of completion, content, or errors. After two waits with no substantive result, send one targeted followup, then record inconclusive and respawn a smaller `fork_turns: "none"` task if the child stays silent or ack-only.
30
31
 
@@ -34,10 +35,10 @@ The full workflow may mention OpenCode-style orchestration examples. In Codex, t
34
35
 
35
36
  | Workflow intent | Codex tool |
36
37
  | --- | --- |
37
- | Plan agent | `spawn_agent(agent_type="plan", ...)` |
38
- | Search/read-only worker | `spawn_agent(agent_type="explorer", ...)` |
39
- | Implementation or QA worker | `spawn_agent(agent_type="worker", ...)` |
40
- | Final verification reviewer | `spawn_agent(agent_type="codex-ultrawork-reviewer", ...)` |
38
+ | Plan agent | `spawn_agent(agent_type="plan", fork_turns="none", ...)` |
39
+ | Search/read-only worker | `spawn_agent(agent_type="explorer", fork_turns="none", ...)` |
40
+ | Implementation or QA worker | `spawn_agent(agent_type="worker", fork_turns="none", ...)` |
41
+ | Final verification reviewer | `spawn_agent(agent_type="codex-ultrawork-reviewer", fork_turns="none", ...)` |
41
42
  | Wait for background result | `wait_agent(...)` |
42
43
  | Clean up finished worker | `close_agent(...)` |
43
44
 
@@ -33,9 +33,9 @@ Size each worker to the task — never spend `xhigh` on a one-liner, never send
33
33
  | Task shape | agent_type | model | reasoning_effort |
34
34
  |---|---|---|---|
35
35
  | Trivial / mechanical (rename, move, obvious one-liner, config edit) | `worker` | `gpt-5.4-mini` | `low` |
36
- | Pure implementation against a clear spec (new function, endpoint, test from a named pattern) | `worker` | `gpt-5.3-codex` | `high` |
36
+ | Pure implementation against a clear spec (new function, endpoint, test from a named pattern) | `worker` | `gpt-5.4` | `high` |
37
37
  | Deep debugging / race / perf / subtle cross-module reasoning | `worker` | `gpt-5.5` | `xhigh` |
38
- | QA execution (drive a channel, capture evidence) | `worker` | `gpt-5.3-codex` | `high` |
38
+ | QA execution (drive a channel, capture evidence) | `worker` | `gpt-5.4` | `high` |
39
39
  | Read-only codebase search | `explorer` | role default | role default |
40
40
  | External library / docs research | `librarian` | role default | role default |
41
41
  | Final verification audit | `codex-ultrawork-reviewer` | role default | role default |
@@ -48,6 +48,7 @@ Codex subagent reliability:
48
48
  - Start every `spawn_agent` message with `TASK: <imperative assignment>`, then name `DELIVERABLE`, `SCOPE`, and `VERIFY`. State that it is an executable assignment, not a context handoff.
49
49
  - Prefer `fork_turns: "none"` unless full history is truly required; paste only the context the child needs. Full-history forks can make the child continue old parent context instead of the delegated task.
50
50
  - Plan and reviewer agents may run for a long time; spawn them in the background, keep doing independent root work, and poll with short wait_agent cycles. Never use a single long blocking wait for them.
51
+ - While any child is active, keep the parent visibly alive with brief status updates that include active subagent count, agent names, last heartbeat, and whether the parent is waiting for mailbox updates.
51
52
  - Do not use `list_agents` as a polling or status tool in long or high-context runs; it can replay large agent status and latest-message payloads. Track spawned agent names locally, use `wait_agent` for completion signals, targeted followups only when needed, and `close_agent` after integrating each result.
52
53
  - Treat `wait_agent` as a mailbox signal, not proof of completion, content, or errors. After two waits with no substantive result, send one targeted followup: `TASK STILL ACTIVE: return <deliverable> or BLOCKED: <reason>`. If still silent or ack-only, record inconclusive, do not count it as pass/review approval, close if safe, and respawn a smaller `fork_turns: "none"` task with the missing deliverable.
53
54
 
@@ -147,7 +148,7 @@ Loop per goal. Cap at 5 cycles per goal. Cap identical same-criterion failures a
147
148
  2. Register atomic todos: `path: <action> for <criterion> - verify by <check>`.
148
149
  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: when the task touches EXISTING behavior, PIN it FIRST — write a characterization test that asserts the current observable behavior and PASSES on the unchanged code, so any later regression fails loudly. Then RED (the new 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. The baseline-pin scenario must be as rigorous and specific as the new-behavior scenario: exact inputs, exact observable, exact assertion. Serialize only on a NAMED dependency.
149
150
  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.
150
- 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.
151
+ 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.4`, `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.
151
152
  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.
152
153
  7. CLEAN (PAIRED, NEVER SKIP): tear down every runtime artifact step 5 spawned BEFORE recording — server PIDs (`kill`, verify `kill -0` fails), `tmux` sessions (`tmux kill-session -t ulw-qa-<criterion>`; confirm `tmux ls`), browser / Playwright contexts (`.close()`), containers (`docker rm -f`), bound ports (`lsof -i :<port>` empty), temp sockets / files / dirs (`rm -rf` the `mktemp` paths), QA-only env vars, AND `close_agent` on every finished worker. Register each teardown as its own todo the moment the QA spawns the resource (scripts, tmux assets, browsers / agent-browser sessions, PIDs, ports) so none is forgotten. 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.
153
154
  8. RECORD exactly one result:
@@ -9,15 +9,15 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
9
9
 
10
10
  | OpenCode example | Codex tool to use |
11
11
  | --- | --- |
12
- | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...")` |
13
- | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...")` |
14
- | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...")` |
15
- | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...")` |
16
- | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...")` |
12
+ | `call_omo_agent(subagent_type="explore", ...)` | `spawn_agent(agent_type="explorer", task_name="...", message="...", fork_turns="none")` |
13
+ | `call_omo_agent(subagent_type="librarian", ...)` | `spawn_agent(agent_type="librarian", task_name="...", message="...", fork_turns="none")` |
14
+ | `task(subagent_type="plan", ...)` | `spawn_agent(agent_type="plan", task_name="...", message="...", fork_turns="none")` |
15
+ | `task(subagent_type="oracle", ...)` for final verification | `spawn_agent(agent_type="codex-ultrawork-reviewer", task_name="...", message="...", fork_turns="none")` |
16
+ | `task(category="...", ...)` for implementation or QA | `spawn_agent(agent_type="worker", task_name="...", message="...", fork_turns="none")` |
17
17
  | `background_output(task_id="...")` | `wait_agent(...)` to wait for subagent completion and mailbox updates |
18
18
  | `team_*(...)` | Use Codex native subagents plus `send_message`, `followup_task`, `wait_agent`, and `close_agent` |
19
19
 
20
- When translating `load_skills=[...]`, include the requested skill names in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.
20
+ Codex full-history forks inherit the parent agent type, model, and reasoning effort, so role-specific spawns with `agent_type` must use a non-full-history fork mode such as `fork_turns="none"`. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. If a code block below conflicts with this section, this section wins.
21
21
 
22
22
  <identity>
23
23
  You are Prometheus - Strategic Planning Consultant.
@@ -26,7 +26,7 @@ Named after the Titan who brought fire to humanity, you bring foresight and stru
26
26
  **YOU ARE A PLANNER. NOT AN IMPLEMENTER. NOT A CODE WRITER.**
27
27
 
28
28
  When user says "do X", "fix X", "build X" - interpret as "create a work plan for X". No exceptions.
29
- Your only outputs: questions, research, work plans (`plans/<slug>.md`), drafts (`.omo/drafts/*.md`).
29
+ Your only outputs: questions, research, work plans (`.omo/plans/<slug>.md`), drafts (`.omo/drafts/*.md`).
30
30
  </identity>
31
31
 
32
32
  <mission>
@@ -68,7 +68,7 @@ This is your north star quality metric.
68
68
  - Spawning read-only subagents for research
69
69
 
70
70
  ### Allowed (plan artifacts only)
71
- - Writing/editing files in `plans/<slug>.md`
71
+ - Writing/editing files in `.omo/plans/<slug>.md`
72
72
  - Writing/editing files in `.omo/drafts/*.md`
73
73
 
74
74
  ### Forbidden (mutating, plan-executing)
@@ -185,7 +185,7 @@ ANY NO -> Ask the specific unclear question.
185
185
  Spawn the metis agent to analyze the planning session for contradictions, ambiguity, missing constraints, and execution risks:
186
186
 
187
187
  ```
188
- spawn_agent(agent_type="metis", task_name="gap-analysis",
188
+ spawn_agent(agent_type="metis", task_name="gap-analysis", fork_turns="none",
189
189
  message="Review this planning session. Goal: {summary}. Discussed: {key points}. Understanding: {interpretation}. Research: {findings}. Identify: contradictions, ambiguity, missing constraints, execution risks, scope creep areas, missing acceptance criteria.")
190
190
  ```
191
191
 
@@ -233,7 +233,7 @@ Self-review checklist:
233
233
  **Defaults Applied**: [default]: [assumption]
234
234
  **Decisions Needed**: [question requiring user input] (if any)
235
235
 
236
- Plan saved to: plans/{slug}.md
236
+ Plan saved to: .omo/plans/{slug}.md
237
237
  ```
238
238
 
239
239
  If "Decisions Needed" exists, wait for user response and update plan.
@@ -253,8 +253,8 @@ Only activated when user selects "High Accuracy Review".
253
253
  Spawn the momus agent with the plan file path:
254
254
 
255
255
  ```
256
- spawn_agent(agent_type="momus", task_name="plan-review",
257
- message="Review this plan: plans/{slug}.md")
256
+ spawn_agent(agent_type="momus", task_name="plan-review", fork_turns="none",
257
+ message="Review this plan: .omo/plans/{slug}.md")
258
258
  ```
259
259
 
260
260
  Handle the three-verdict response:
@@ -270,13 +270,13 @@ Handle the three-verdict response:
270
270
 
271
271
  After plan is complete (direct or Momus-approved):
272
272
  1. Delete draft: remove `.omo/drafts/{name}.md`
273
- 2. Guide user: "Plan saved to `plans/{slug}.md`. Spawn a worker agent to begin execution."
273
+ 2. Guide user: "Plan saved to `.omo/plans/{slug}.md`. Spawn a worker agent to begin execution."
274
274
  </phases>
275
275
 
276
276
  <plan_template>
277
277
  ## Plan Structure
278
278
 
279
- Generate to: `plans/{slug}.md`
279
+ Generate to: `.omo/plans/{slug}.md`
280
280
 
281
281
  **Single Plan Mandate**: No matter how large the task, EVERYTHING goes into ONE plan. Never split into "Phase 1, Phase 2". 50+ TODOs is fine.
282
282
 
@@ -308,7 +308,7 @@ Generate to: `plans/{slug}.md`
308
308
  > ZERO HUMAN INTERVENTION - all verification is agent-executed.
309
309
  - Test decision: [TDD / tests-after / none] + framework
310
310
  - QA policy: Every task has agent-executed scenarios
311
- - Evidence: evidence/task-{N}-{slug}.{ext}
311
+ - Evidence: .omo/evidence/task-{N}-{slug}.{ext}
312
312
 
313
313
  ## Execution Strategy
314
314
  ### Parallel Execution Waves
@@ -346,13 +346,13 @@ Wave 2: [dependent tasks]
346
346
  Tool: [bash / curl / tmux / playwright]
347
347
  Steps: [exact actions with specific data]
348
348
  Expected: [concrete, binary pass/fail]
349
- Evidence: evidence/task-{N}-{slug}.{ext}
349
+ Evidence: .omo/evidence/task-{N}-{slug}.{ext}
350
350
 
351
351
  Scenario: [Failure/edge case]
352
352
  Tool: [same]
353
353
  Steps: [trigger error condition]
354
354
  Expected: [graceful failure with correct error message/code]
355
- Evidence: evidence/task-{N}-{slug}-error.{ext}
355
+ Evidence: .omo/evidence/task-{N}-{slug}-error.{ext}
356
356
  ```
357
357
 
358
358
  **Commit**: YES/NO | Message: `type(scope): desc` | Files: [paths]
@@ -5,17 +5,30 @@ import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
8
+ const mcpPackageManifestPaths = ["../../lsp-tools-mcp/package.json", "../../ast-grep-mcp/package.json", "../../git-bash-mcp/package.json"];
9
+ const mcpPackageManifestExists = await Promise.all(mcpPackageManifestPaths.map(exists));
8
10
 
9
11
  async function readJson(relativePath) {
10
12
  return JSON.parse(await readFile(join(root, relativePath), "utf8"));
11
13
  }
12
14
 
15
+ async function exists(relativePath) {
16
+ try {
17
+ await stat(join(root, relativePath));
18
+ return true;
19
+ } catch (error) {
20
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return false;
21
+ throw error;
22
+ }
23
+ }
24
+
13
25
  async function readComponentHookManifests() {
14
26
  const components = await readdir(join(root, "components"), { withFileTypes: true });
15
27
  const manifests = [];
16
28
  for (const entry of components) {
17
29
  if (!entry.isDirectory()) continue;
18
30
  const source = join("components", entry.name, "hooks", "hooks.json");
31
+ if (!(await exists(source))) continue;
19
32
  manifests.push({ source, hooks: await readJson(source) });
20
33
  }
21
34
  return manifests.sort((left, right) => left.source.localeCompare(right.source));
@@ -57,6 +70,18 @@ function findSpawnAgentTypes(content) {
57
70
  return [...agentTypes].sort();
58
71
  }
59
72
 
73
+ function findRoleSpecificSpawnsWithoutForkTurnsNone(content) {
74
+ const missingForkTurns = [];
75
+ const regex = /spawn_agent\(agent_type="([^"]+)"[^)]*\)/g;
76
+ for (const match of content.matchAll(regex)) {
77
+ const call = match[0];
78
+ if (!call.includes('fork_turns="none"')) {
79
+ missingForkTurns.push(call);
80
+ }
81
+ }
82
+ return missingForkTurns;
83
+ }
84
+
60
85
  test("#given aggregate plugin manifest #when inspected #then it owns the omo namespace", async () => {
61
86
  // given
62
87
  const manifest = await readJson(".codex-plugin/plugin.json");
@@ -99,6 +124,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i
99
124
  "components/telemetry/dist/cli.js",
100
125
  "components/ulw-loop/dist/cli.js",
101
126
  "components/ultrawork/dist/cli.js",
127
+ "scripts/auto-update.mjs",
102
128
  ];
103
129
 
104
130
  // then
@@ -106,6 +132,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i
106
132
  assert.match(text, new RegExp(marker.replaceAll("/", "\\/")));
107
133
  }
108
134
  assert.doesNotMatch(text, /codex-(comment-checker|lsp|rules|telemetry|ulw-loop|ultrawork)@/);
135
+ assert.equal(await exists("scripts/migrate-codex-config.mjs"), true);
109
136
  });
110
137
 
111
138
  test("#given aggregate hook commands #when inspected #then every command exposes a Codex status message", async () => {
@@ -172,6 +199,24 @@ test("#given aggregate OMO plugin is enabled #when hooks are inspected #then she
172
199
  assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^Bash$", "^create_goal$"]);
173
200
  });
174
201
 
202
+ test("#given aggregate SessionStart hooks #when inspected #then LazyCodex auto-update is registered", async () => {
203
+ // given
204
+ const hooks = await readJson("hooks/hooks.json");
205
+ const text = JSON.stringify(hooks);
206
+
207
+ // when
208
+ const sessionStartCommands = collectCommandHooks(hooks, "hooks/hooks.json")
209
+ .filter(({ eventName }) => eventName === "SessionStart")
210
+ .map(({ handler }) => handler.command);
211
+ const autoUpdateGroup = hooks.hooks.SessionStart.find((group) => JSON.stringify(group).includes("scripts/auto-update.mjs"));
212
+
213
+ // then
214
+ assert.equal(autoUpdateGroup?.matcher, "^startup$");
215
+ assert.match(text, /scripts\/auto-update\.mjs/);
216
+ assert.match(text, /Checking Auto Update/);
217
+ assert(sessionStartCommands.some((command) => command.includes("scripts/auto-update.mjs")));
218
+ });
219
+
175
220
  test("#given aggregate MCP config #when inspected #then code MCPs reference package runtimes without package names", async () => {
176
221
  // given
177
222
  const packageJson = await readJson("package.json");
@@ -208,25 +253,29 @@ test("#given aggregate MCP config #when inspected #then code MCPs reference pack
208
253
  assert.deepEqual(componentLocalMcpSources, []);
209
254
  });
210
255
 
211
- test("#given package-level MCP CLIs #when package metadata is inspected #then bin names use the omo prefix", async () => {
212
- // given
213
- const lspPackageJson = await readJson("../../lsp-tools-mcp/package.json");
214
- const astGrepPackageJson = await readJson("../../ast-grep-mcp/package.json");
215
- const gitBashPackageJson = await readJson("../../git-bash-mcp/package.json");
216
-
217
- // when
218
- const binNames = [
219
- ...Object.keys(lspPackageJson.bin ?? {}),
220
- ...Object.keys(astGrepPackageJson.bin ?? {}),
221
- ...Object.keys(gitBashPackageJson.bin ?? {}),
222
- ].sort();
256
+ test(
257
+ "#given package-level MCP CLIs #when package metadata is inspected #then bin names use the omo prefix",
258
+ { skip: mcpPackageManifestExists.some((exists) => !exists) },
259
+ async () => {
260
+ // given
261
+ const [lspPackageJson, astGrepPackageJson, gitBashPackageJson] = await Promise.all(
262
+ mcpPackageManifestPaths.map((path) => readJson(path)),
263
+ );
223
264
 
224
- // then
225
- assert.deepEqual(binNames, ["omo-ast-grep", "omo-git-bash", "omo-lsp"]);
226
- for (const name of binNames) {
227
- assert.match(name, /^omo-/);
228
- }
229
- });
265
+ // when
266
+ const binNames = [
267
+ ...Object.keys(lspPackageJson.bin ?? {}),
268
+ ...Object.keys(astGrepPackageJson.bin ?? {}),
269
+ ...Object.keys(gitBashPackageJson.bin ?? {}),
270
+ ].sort();
271
+
272
+ // then
273
+ assert.deepEqual(binNames, ["omo-ast-grep", "omo-git-bash", "omo-lsp"]);
274
+ for (const name of binNames) {
275
+ assert.match(name, /^omo-/);
276
+ }
277
+ },
278
+ );
230
279
 
231
280
  test("#given aggregate plugin build script #when inspected #then hook status and telemetry sync run before workspace builds", async () => {
232
281
  // given
@@ -261,7 +310,13 @@ test("#given component directories #when scanned #then only intentional resource
261
310
  const expectedComponentManifests = new Map([["rules", { hooks: "./hooks/hooks.json" }]]);
262
311
 
263
312
  // when
264
- const componentNames = components.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
313
+ const componentNames = [];
314
+ for (const entry of components) {
315
+ if (!entry.isDirectory()) continue;
316
+ if (!(await exists(join("components", entry.name, "package.json")))) continue;
317
+ componentNames.push(entry.name);
318
+ }
319
+ componentNames.sort();
265
320
 
266
321
  // then
267
322
  assert.deepEqual(componentNames, [
@@ -315,6 +370,65 @@ test("#given bundled Codex agents #when components/ultrawork/agents directory is
315
370
  }
316
371
  });
317
372
 
373
+ test("#given planner agent prompt #when inspected #then generated artifacts stay under .omo", async () => {
374
+ const prompt = await readFile(join(root, "components", "ultrawork", "agents", "plan.toml"), "utf8");
375
+
376
+ assert.match(prompt, /\.omo\/plans\/<slug>\.md/);
377
+ assert.match(prompt, /\.omo\/evidence\/task-<N>-<slug>\.<ext>/);
378
+ assert.doesNotMatch(prompt, /(?<!\.omo\/)plans\/<slug>\.md/);
379
+ assert.doesNotMatch(prompt, /(?<!\.omo\/)evidence\/task-/);
380
+ });
381
+
382
+ test("#given reviewer agent prompt #when inspected #then default model is ChatGPT-account compatible", async () => {
383
+ const prompt = await readFile(
384
+ join(root, "components", "ultrawork", "agents", "codex-ultrawork-reviewer.toml"),
385
+ "utf8",
386
+ );
387
+
388
+ assert.match(prompt, /^model\s*=\s*"gpt-5\.5"$/m);
389
+ assert.match(prompt, /^model_reasoning_effort\s*=\s*"xhigh"$/m);
390
+ assert.doesNotMatch(prompt, /^model\s*=\s*"gpt-5\.2"$/m);
391
+ assert.match(prompt, /ChatGPT account/);
392
+ });
393
+
394
+ test("#given bundled model catalog #when inspected #then default verifier and worker roles are pinned", async () => {
395
+ const catalog = JSON.parse(await readFile(join(root, "model-catalog.json"), "utf8"));
396
+
397
+ assert.equal(catalog.current.model, "gpt-5.5");
398
+ assert.equal(catalog.current.model_context_window, 400000);
399
+ assert.equal(catalog.current.model_reasoning_effort, "high");
400
+ assert.equal(catalog.current.plan_mode_reasoning_effort, "xhigh");
401
+ assert.deepEqual(catalog.roles.default, catalog.current);
402
+ assert.deepEqual(catalog.roles.verifier, {
403
+ model: "gpt-5.5",
404
+ model_reasoning_effort: "xhigh",
405
+ });
406
+ assert.deepEqual(catalog.roles.worker, {
407
+ model: "gpt-5.4",
408
+ model_reasoning_effort: "high",
409
+ });
410
+ });
411
+
412
+ test("#given Codex-facing orchestration surfaces #when inspected #then retired ChatGPT-account model names are not recommended", async () => {
413
+ const promptFiles = [
414
+ join(root, "skills", "ulw-loop", "references", "full-workflow.md"),
415
+ join(root, "components", "ulw-loop", "skills", "ulw-loop", "references", "full-workflow.md"),
416
+ join(root, "components", "ultrawork", "README.md"),
417
+ join(root, "components", "ultrawork", "CHANGELOG.md"),
418
+ join(root, "components", "rules", "src", "post-compact-budget.ts"),
419
+ ];
420
+
421
+ const staleReferences = [];
422
+ for (const promptPath of promptFiles) {
423
+ const content = await readFile(promptPath, "utf8");
424
+ if (/gpt-5\.(?:2|3-codex)/i.test(content)) {
425
+ staleReferences.push(`${basename(dirname(promptPath))}/${basename(promptPath)}`);
426
+ }
427
+ }
428
+
429
+ assert.deepEqual(staleReferences, []);
430
+ });
431
+
318
432
  test("#given synced skills with Codex compatibility guidance #when a bundled agent_type is referenced #then a matching TOML is bundled", async () => {
319
433
  const skillsDir = join(root, "skills");
320
434
  const skillEntries = await readdir(skillsDir, { withFileTypes: true });
@@ -343,3 +457,42 @@ test("#given synced skills with Codex compatibility guidance #when a bundled age
343
457
  assert.equal(basename(tomlPath), `${agentType}.toml`);
344
458
  }
345
459
  });
460
+
461
+ test('#given synced skills and bundled rules #when role-specific agents are spawned #then they set fork_turns="none"', async () => {
462
+ const skillsDir = join(root, "skills");
463
+ const skillEntries = await readdir(skillsDir, { withFileTypes: true });
464
+ const promptFiles = skillEntries
465
+ .filter((entry) => entry.isDirectory())
466
+ .map((entry) => join(skillsDir, entry.name, "SKILL.md"));
467
+ promptFiles.push(join(root, "components", "rules", "bundled-rules", "hephaestus.md"));
468
+
469
+ const missingForkTurns = [];
470
+ for (const promptPath of promptFiles) {
471
+ const content = await readFile(promptPath, "utf8");
472
+ for (const call of findRoleSpecificSpawnsWithoutForkTurnsNone(content)) {
473
+ missingForkTurns.push(`${basename(dirname(promptPath))}/${basename(promptPath)}: ${call}`);
474
+ }
475
+ }
476
+
477
+ assert.deepEqual(missingForkTurns, []);
478
+ });
479
+
480
+ test("#given long-running orchestration prompts #when waiting on child agents #then parent liveness is surfaced", async () => {
481
+ const promptFiles = [
482
+ join(root, "skills", "ulw-loop", "SKILL.md"),
483
+ join(root, "skills", "ulw-loop", "references", "full-workflow.md"),
484
+ join(root, "skills", "review-work", "SKILL.md"),
485
+ join(root, "skills", "start-work", "SKILL.md"),
486
+ join(root, "components", "rules", "bundled-rules", "hephaestus.md"),
487
+ ];
488
+
489
+ const missingLivenessGuidance = [];
490
+ for (const promptPath of promptFiles) {
491
+ const content = await readFile(promptPath, "utf8");
492
+ if (!content.includes("active subagent count") || !content.includes("last heartbeat")) {
493
+ missingLivenessGuidance.push(`${basename(dirname(promptPath))}/${basename(promptPath)}`);
494
+ }
495
+ }
496
+
497
+ assert.deepEqual(missingLivenessGuidance, []);
498
+ });
@@ -0,0 +1,129 @@
1
+ import assert from "node:assert/strict";
2
+ import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import test from "node:test";
6
+
7
+ import { resolveAutoUpdatePlan, runAutoUpdateCheck } from "../scripts/auto-update.mjs";
8
+
9
+ test("#given auto update is disabled #when resolving plan #then no command is scheduled", () => {
10
+ const plan = resolveAutoUpdatePlan({
11
+ env: { LAZYCODEX_AUTO_UPDATE_DISABLED: "1" },
12
+ now: 1_000,
13
+ lastCheckedAt: 0,
14
+ });
15
+
16
+ assert.equal(plan.shouldRun, false);
17
+ assert.equal(plan.reason, "disabled");
18
+ });
19
+
20
+ test("#given stale state #when resolving plan #then installer update command is scheduled", () => {
21
+ const plan = resolveAutoUpdatePlan({
22
+ env: {},
23
+ now: 90_000_000,
24
+ lastCheckedAt: 0,
25
+ });
26
+
27
+ assert.equal(plan.shouldRun, true);
28
+ assert.deepEqual(plan.command, "npx");
29
+ assert.deepEqual(plan.args, ["--yes", "lazycodex-ai@latest", "install", "--no-tui", "--skip-auth"]);
30
+ });
31
+
32
+ test("#given recent state #when resolving plan #then update is throttled", () => {
33
+ const plan = resolveAutoUpdatePlan({
34
+ env: {},
35
+ now: 90_000_000,
36
+ lastCheckedAt: 89_999_000,
37
+ });
38
+
39
+ assert.equal(plan.shouldRun, false);
40
+ assert.equal(plan.reason, "throttled");
41
+ });
42
+
43
+ test("#given test command override #when running check #then records state and launches command", async () => {
44
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-"));
45
+ const logPath = join(root, "spawn.log");
46
+ const statePath = join(root, "state.json");
47
+ const codexHome = join(root, "codex-home");
48
+
49
+ const result = await runAutoUpdateCheck({
50
+ env: {
51
+ CODEX_HOME: codexHome,
52
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
53
+ LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
54
+ LAZYCODEX_AUTO_UPDATE_INTERVAL_MS: "0",
55
+ LAZYCODEX_AUTO_UPDATE_COMMAND: process.execPath,
56
+ LAZYCODEX_AUTO_UPDATE_ARGS_JSON: JSON.stringify(["-e", `require("node:fs").writeFileSync(${JSON.stringify(logPath)}, "ok")`]),
57
+ LAZYCODEX_AUTO_UPDATE_WAIT: "1",
58
+ },
59
+ now: 123_456,
60
+ });
61
+
62
+ assert.equal(result.started, true);
63
+ assert.equal(JSON.parse(await readFile(statePath, "utf8")).lastCheckedAt, 123_456);
64
+ assert.equal(await readFile(logPath, "utf8"), "ok");
65
+ assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model = "gpt-5\.5"/);
66
+ });
67
+
68
+ test("#given active lock #when running check #then skips concurrent update", async () => {
69
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-lock-"));
70
+ const statePath = join(root, "state.json");
71
+ const lockPath = join(root, "state.json.lock");
72
+ const codexHome = join(root, "codex-home");
73
+ await writeFile(lockPath, "locked\n");
74
+
75
+ const result = await runAutoUpdateCheck({
76
+ env: {
77
+ CODEX_HOME: codexHome,
78
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
79
+ LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
80
+ LAZYCODEX_AUTO_UPDATE_LOCK_PATH: lockPath,
81
+ LAZYCODEX_AUTO_UPDATE_INTERVAL_MS: "0",
82
+ LAZYCODEX_AUTO_UPDATE_LOCK_STALE_MS: "600000",
83
+ },
84
+ now: 123_456,
85
+ });
86
+
87
+ assert.equal(result.started, false);
88
+ assert.equal(result.reason, "locked");
89
+ assert.match(await readFile(join(codexHome, "config.toml"), "utf8"), /model_context_window = 400000/);
90
+ });
91
+
92
+ test("#given throttled updater and stale Codex config #when running check #then config migration still runs", async () => {
93
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-auto-update-migration-"));
94
+ const statePath = join(root, "state.json");
95
+ const codexHome = join(root, "codex-home");
96
+ await writeFile(statePath, JSON.stringify({ lastCheckedAt: 99_999 }, null, 2));
97
+ await mkdir(codexHome, { recursive: true });
98
+ await writeFile(
99
+ join(codexHome, "config.toml"),
100
+ [
101
+ 'model = "gpt-5.2"',
102
+ "model_context_window = 272000",
103
+ 'model_reasoning_effort = "low"',
104
+ 'plan_mode_reasoning_effort = "medium"',
105
+ "",
106
+ "[features]",
107
+ "plugins = true",
108
+ "",
109
+ ].join("\n"),
110
+ );
111
+
112
+ const result = await runAutoUpdateCheck({
113
+ env: {
114
+ CODEX_HOME: codexHome,
115
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
116
+ LAZYCODEX_AUTO_UPDATE_STATE_PATH: statePath,
117
+ },
118
+ now: 100_000,
119
+ });
120
+
121
+ const content = await readFile(join(codexHome, "config.toml"), "utf8");
122
+ assert.equal(result.started, false);
123
+ assert.equal(result.reason, "throttled");
124
+ assert.match(content, /model = "gpt-5\.5"/);
125
+ assert.match(content, /model_context_window = 400000/);
126
+ assert.match(content, /model_reasoning_effort = "high"/);
127
+ assert.match(content, /plan_mode_reasoning_effort = "xhigh"/);
128
+ assert.doesNotMatch(content, /gpt-5\.2/);
129
+ });
@@ -15,6 +15,7 @@ const root = dirname(dirname(fileURLToPath(import.meta.url)));
15
15
  const AGGREGATE_EXPECTED_LABELS = new Map([
16
16
  ["hooks/hooks.json:SessionStart:0:0", "Loading Project Rules"],
17
17
  ["hooks/hooks.json:SessionStart:1:0", "Recording Session Telemetry"],
18
+ ["hooks/hooks.json:SessionStart:2:0", "Checking Auto Update"],
18
19
  ["hooks/hooks.json:UserPromptSubmit:0:0", "Loading Project Rules"],
19
20
  ["hooks/hooks.json:UserPromptSubmit:1:0", "Checking Ultrawork Trigger"],
20
21
  ["hooks/hooks.json:UserPromptSubmit:2:0", "Checking Ulw-Loop Steering"],
@@ -74,6 +75,7 @@ async function readComponentVersions() {
74
75
  const versions = new Map();
75
76
  for (const entry of components) {
76
77
  if (!entry.isDirectory()) continue;
78
+ if (!(await exists(join("components", entry.name, "package.json")))) continue;
77
79
  const packageJson = await readJson(join("components", entry.name, "package.json"));
78
80
  versions.set(entry.name, packageJson.version);
79
81
  }