oh-my-opencode 4.16.2 → 4.16.3

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 (133) hide show
  1. package/.agents/skills/pre-publish-review/SKILL.md +3 -0
  2. package/README.md +2 -2
  3. package/dist/agents/hephaestus/agent.d.ts +1 -1
  4. package/dist/agents/hephaestus/gpt-5-6.d.ts +2 -0
  5. package/dist/agents/momus-gpt-5-6.d.ts +1 -0
  6. package/dist/agents/types.d.ts +2 -0
  7. package/dist/cli/index.js +800 -536
  8. package/dist/cli-node/index.js +800 -536
  9. package/dist/features/background-agent/error-classifier.d.ts +5 -0
  10. package/dist/features/builtin-commands/templates/refactor-sections/intro-and-analysis.d.ts +1 -1
  11. package/dist/features/builtin-commands/templates/remove-ai-slops.d.ts +1 -1
  12. package/dist/index.js +416 -37
  13. package/dist/shared/live-server-route.d.ts +1 -1
  14. package/dist/skills/review-work/SKILL.md +1 -1
  15. package/dist/skills/ulw-plan/references/full-workflow.md +1 -1
  16. package/dist/skills/ulw-research/SKILL.md +2 -2
  17. package/dist/skills/visual-qa/SKILL.md +1 -1
  18. package/dist/tui.js +125 -13
  19. package/package.json +13 -13
  20. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +398 -246
  22. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  28. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  31. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  32. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  33. package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +93 -0
  34. package/packages/omo-codex/plugin/components/rules/dist/cli.js +43 -24
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/rules/src/codex-hook.ts +2 -1
  38. package/packages/omo-codex/plugin/components/rules/src/dynamic-target-fingerprints.ts +15 -6
  39. package/packages/omo-codex/plugin/components/rules/src/rules-engine-factory.ts +7 -2
  40. package/packages/omo-codex/plugin/components/rules/src/static-injection.ts +2 -2
  41. package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +32 -0
  42. package/packages/omo-codex/plugin/components/rules/test/hephaestus-model-variant.test.ts +106 -0
  43. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  44. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/teammode/AGENTS.md +12 -9
  46. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  47. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/teammode/skills/teammode/SKILL.md +136 -76
  49. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-guide.mjs +94 -23
  50. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-state.mjs +94 -14
  51. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team-transport.mjs +55 -0
  52. package/packages/omo-codex/plugin/components/teammode/skills/teammode/scripts/team.mjs +72 -18
  53. package/packages/omo-codex/plugin/components/teammode/test/thread-title-hook.test.ts +23 -0
  54. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/ultrawork/agents/explorer.toml +24 -41
  57. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-clone-fidelity-reviewer.toml +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-code-reviewer.toml +1 -1
  59. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-executor.toml +2 -2
  60. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-gate-reviewer.toml +1 -1
  61. package/packages/omo-codex/plugin/components/ultrawork/agents/lazycodex-qa-executor.toml +1 -1
  62. package/packages/omo-codex/plugin/components/ultrawork/agents/librarian.toml +21 -49
  63. package/packages/omo-codex/plugin/components/ultrawork/agents/metis.toml +17 -29
  64. package/packages/omo-codex/plugin/components/ultrawork/agents/momus.toml +20 -50
  65. package/packages/omo-codex/plugin/components/ultrawork/agents/plan.toml +18 -39
  66. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  67. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  68. package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +1 -1
  69. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +2 -2
  70. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  71. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  72. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  85. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  93. package/packages/omo-codex/plugin/package-lock.json +13 -13
  94. package/packages/omo-codex/plugin/package.json +1 -1
  95. package/packages/omo-codex/plugin/scripts/auto-update.mjs +9 -2
  96. package/packages/omo-codex/plugin/scripts/entry-guard.mjs +26 -0
  97. package/packages/omo-codex/plugin/scripts/materialize-shared-upstreams.mjs +3 -1
  98. package/packages/omo-codex/plugin/scripts/migrate-codex-config/multi-agent-v2-guard.mjs +39 -4
  99. package/packages/omo-codex/plugin/scripts/migrate-codex-config/subagent-limit-guard.mjs +24 -4
  100. package/packages/omo-codex/plugin/scripts/migrate-codex-config.mjs +3 -3
  101. package/packages/omo-codex/plugin/scripts/migrate-omo-sot.mjs +2 -2
  102. package/packages/omo-codex/plugin/scripts/sync-hook-status-messages.mjs +2 -2
  103. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +6 -4
  104. package/packages/omo-codex/plugin/scripts/sync-version.mjs +4 -2
  105. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +1 -1
  106. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +1 -1
  107. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +1 -1
  108. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +1 -1
  109. package/packages/omo-codex/plugin/skills/start-work/SKILL.md +1 -1
  110. package/packages/omo-codex/plugin/skills/teammode/SKILL.md +136 -76
  111. package/packages/omo-codex/plugin/skills/teammode/scripts/team-guide.mjs +94 -23
  112. package/packages/omo-codex/plugin/skills/teammode/scripts/team-state.mjs +94 -14
  113. package/packages/omo-codex/plugin/skills/teammode/scripts/team-transport.mjs +55 -0
  114. package/packages/omo-codex/plugin/skills/teammode/scripts/team.mjs +72 -18
  115. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  116. package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +1 -1
  117. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +2 -2
  118. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +2 -2
  119. package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +47 -1
  120. package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +4 -2
  121. package/packages/omo-codex/plugin/test/auto-update-restart-notice.test.mjs +33 -0
  122. package/packages/omo-codex/plugin/test/migrate-codex-config.test.mjs +146 -9
  123. package/packages/omo-codex/plugin/test/multi-agent-v2-regression.test.mjs +71 -0
  124. package/packages/omo-codex/plugin/test/subagent-limit-migration.test.mjs +32 -0
  125. package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +9 -2
  126. package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +345 -0
  127. package/packages/omo-codex/scripts/install-dist/install-local.mjs +436 -284
  128. package/packages/omo-codex/scripts/install-generated-bundle.test.mjs +111 -1
  129. package/packages/shared-skills/skills/review-work/SKILL.md +1 -1
  130. package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +1 -1
  131. package/packages/shared-skills/skills/ulw-research/SKILL.md +2 -2
  132. package/packages/shared-skills/skills/visual-qa/SKILL.md +1 -1
  133. /package/packages/omo-codex/plugin/components/rules/bundled-rules/{hephaestus.md → hephaestus/gpt-5.5.md} +0 -0
@@ -85,7 +85,7 @@ Runs in parallel; ALL must APPROVE; surface results and wait for the user's expl
85
85
  - UNCLEAR: run the high-accuracy review AUTOMATICALLY before presenting (unless Classify=Trivial), then present a brief that LEADS with the derived approach and the adopted defaults; still wait for the user's explicit okay.
86
86
 
87
87
  ### High-accuracy review (dual review)
88
- The high-accuracy review is DUAL and both passes must return OKAY before handoff: (1) the native `momus` reviewer subagent, and (2) an independent Codex CLI review on gpt-5.5 at xhigh reasoning, run in a disposable isolated workspace and `CODEX_HOME` with the harness's normal approval and sandbox policy. Do not add flags that disable approvals or sandboxing. One round = exactly ONE `momus` + ONE independent review, dispatched together against the COMPLETE plan file (todos + TL;DR filled) - never a second `momus` while a round is in flight. After both verdicts return, fix every cited issue and resubmit both fresh until each approves. CLEAR: runs when the user opts in or `review_required: true`. UNCLEAR: runs automatically unless Classify=Trivial.
88
+ The high-accuracy review is DUAL and both passes must return OKAY before handoff: (1) the native `momus` reviewer subagent, and (2) an independent Codex CLI review on gpt-5.6-sol at xhigh reasoning, run in a disposable isolated workspace and `CODEX_HOME` with the harness's normal approval and sandbox policy. Do not add flags that disable approvals or sandboxing. Momus runs at Ultra and may take substantially longer than other agents. One round = exactly ONE `momus` + ONE independent review, dispatched together against the COMPLETE plan file (todos + TL;DR filled). Keep Momus in flight and wait for its terminal result: elapsed time alone never justifies cancelling, duplicating, replacing, or treating it as failed. After both verdicts return, fix every cited issue and resubmit both fresh until each approves. CLEAR: runs when the user opts in or `review_required: true`. UNCLEAR: runs automatically unless Classify=Trivial.
89
89
 
90
90
  The draft must record the native Momus session/result, the independent Codex CLI review command/result, and the fix/retry summary. Do not say "high-accuracy review completed" unless both receipts exist and both final verdicts are unconditional approval.
91
91
 
@@ -15,7 +15,7 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
15
15
  | `task(subagent_type="oracle", ...)` for final verification | `multi_agent_v1.spawn_agent({"message":"TASK: act as a rigorous reviewer. ...","agent_type":"lazycodex-gate-reviewer","fork_context":false})` |
16
16
  | `task(category="...", ...)` for implementation or QA | `multi_agent_v1.spawn_agent({"message":"TASK: act as an implementation or QA worker. ...","fork_context":false})` |
17
17
  | `background_output(task_id="...")` | `multi_agent_v1.wait_agent(...)` for mailbox signals |
18
- | `team_*(...)` | Use Codex native subagents via `multi_agent_v1.spawn_agent`, `multi_agent_v1.send_input`, `multi_agent_v1.wait_agent`, and `multi_agent_v1.close_agent` |
18
+ | `team_*(...)` | Use Codex native subagents via `multi_agent_v1.spawn_agent` and `multi_agent_v1.wait_agent`; use `multi_agent_v1.send_input` and `multi_agent_v1.close_agent` only when exposed in the active tools list |
19
19
 
20
20
  Role-specific behavior must be described in a self-contained `message`. Use `fork_context: false` to start the child with only the initial prompt (no parent history); use `fork_context: true` only when full parent history is truly required. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. OMO installs these selectable agent roles into `~/.codex/agents/`: `explorer`, `librarian`, `plan`, `momus`, `metis`, `lazycodex-code-reviewer`, `lazycodex-qa-executor`, and `lazycodex-gate-reviewer` - pass the matching name as `agent_type` so the child gets that role's model and instructions. If the spawn tool exposes no `agent_type` parameter, omit it and describe the role inside `message`. If a code block below conflicts with this section, this section wins.
21
21
 
@@ -69,7 +69,7 @@ Observation candidates and claim candidates travel back from workers as message
69
69
 
70
70
  ## Run the swarm as a cooperating team
71
71
 
72
- Saturation research defaults to teammode, not isolated fire-and-forget workers: a lead one worker surfaces almost always reshapes what another should search next. When your harness gives you real cooperating members — Codex: the `teammode` skill (`codex_app` threads); OpenCode: `team_mode` — run this swarm as a team. Fall back to the background-worker swarm below only when team mode is unavailable, or the axes are genuinely independent with no cross-pollination expected.
72
+ Saturation research defaults to teammode, not isolated fire-and-forget workers: a lead one worker surfaces almost always reshapes what another should search next. When your harness gives you real cooperating members — Codex: the `teammode` skill (MultiAgentV2 agents, or `codex_app` threads as its fallback); OpenCode: `team_mode` — run this swarm as a team. Fall back to the background-worker swarm below only when team mode is unavailable, or the axes are genuinely independent with no cross-pollination expected.
73
73
 
74
74
  - **One member per axis — by part, ownership, or perspective, never a job title.** Each Phase 0 axis is one member owning one concrete slice: a codebase part, a source territory, or a question lens. No two members share an angle. "Backend researcher" or "the web person" gives no real boundary and invites overlap — name what the member owns.
75
75
  - **Many teammates by default.** Prefer a larger roster, usually 5-8 teammates, whenever the axes can be made distinct. Add at least one skeptic or red-team perspective for hyperdebate/ultradebate: cross-critique claims, evidence quality, synthesis structure, and visual-report choices before they reach the final deliverable.
@@ -15,7 +15,7 @@ This skill may include examples copied from the OpenCode harness. In Codex, do n
15
15
  | `task(subagent_type="oracle", ...)` for final verification | `multi_agent_v1.spawn_agent({"message":"TASK: act as a rigorous reviewer. ...","agent_type":"lazycodex-gate-reviewer","fork_context":false})` |
16
16
  | `task(category="...", ...)` for implementation or QA | `multi_agent_v1.spawn_agent({"message":"TASK: act as an implementation or QA worker. ...","fork_context":false})` |
17
17
  | `background_output(task_id="...")` | `multi_agent_v1.wait_agent(...)` for mailbox signals |
18
- | `team_*(...)` | Use Codex native subagents via `multi_agent_v1.spawn_agent`, `multi_agent_v1.send_input`, `multi_agent_v1.wait_agent`, and `multi_agent_v1.close_agent` |
18
+ | `team_*(...)` | Use Codex native subagents via `multi_agent_v1.spawn_agent` and `multi_agent_v1.wait_agent`; use `multi_agent_v1.send_input` and `multi_agent_v1.close_agent` only when exposed in the active tools list |
19
19
 
20
20
  Role-specific behavior must be described in a self-contained `message`. Use `fork_context: false` to start the child with only the initial prompt (no parent history); use `fork_context: true` only when full parent history is truly required. Include any required conversation context, files, diffs, constraints, and requested skill names directly in the spawned agent's `message`. OMO installs these selectable agent roles into `~/.codex/agents/`: `explorer`, `librarian`, `plan`, `momus`, `metis`, `lazycodex-code-reviewer`, `lazycodex-qa-executor`, and `lazycodex-gate-reviewer` - pass the matching name as `agent_type` so the child gets that role's model and instructions. If the spawn tool exposes no `agent_type` parameter, omit it and describe the role inside `message`. If a code block below conflicts with this section, this section wins.
21
21
 
@@ -271,7 +271,7 @@ If any page fails, you are not done: fix it, re-capture the full set, re-dispatc
271
271
 
272
272
  Run this step IN ADDITION to Steps 1-4 when the original user task has a concrete visual target: "clone this site", "move this Figma design to code", "rebuild this screen", "make it look exactly like X", or "build this Imagen/Stitch/generated mockup and overview". For these tasks the normal dual-oracle is necessary but NOT sufficient. After it returns, run the following TWO additional MANDATORY verifications and LOOP until BOTH pass.
273
273
 
274
- 1. Pixel-perfect design-compare subagent (visual oracle). Dispatch a focused, read-only design-compare reviewer (recommend `gpt-5.5` with medium reasoning). It must crop/zoom BOTH the reference (target / Figma export / source-site screenshot / generated page snapshot) and the ACTUAL screenshot into matching regions and read them **pixel-by-pixel** - header, nav, each card, spacing, type ramp, color tokens - not at a glance. It must also compare the overview text or annotations against the rendered content and DOM text. Anchor every claim with the bundled tool:
274
+ 1. Pixel-perfect design-compare subagent (visual oracle). Dispatch a focused, read-only design-compare reviewer (recommend `gpt-5.6-sol` with xhigh reasoning). It must crop/zoom BOTH the reference (target / Figma export / source-site screenshot / generated page snapshot) and the ACTUAL screenshot into matching regions and read them **pixel-by-pixel** - header, nav, each card, spacing, type ramp, color tokens - not at a glance. It must also compare the overview text or annotations against the rendered content and DOM text. Anchor every claim with the bundled tool:
275
275
 
276
276
  ```
277
277
  node "$SKILL_DIR/scripts/visual-qa.mjs" image-diff <reference.png> <actual.png>
@@ -16,9 +16,50 @@ const agentSchemaKeys = new Set([
16
16
  ]);
17
17
 
18
18
  const lazycodexAgentInvariants = new Map([
19
+ [
20
+ "explorer.toml",
21
+ {
22
+ model: "gpt-5.6-terra",
23
+ effort: "medium",
24
+ includes: [/Read-only/, /working tree/, /rg/],
25
+ },
26
+ ],
27
+ [
28
+ "librarian.toml",
29
+ {
30
+ model: "gpt-5.6-terra",
31
+ effort: "medium",
32
+ includes: [/Read-only/, /SHA-pinned GitHub permalink/, /external/],
33
+ },
34
+ ],
35
+ [
36
+ "metis.toml",
37
+ {
38
+ model: "gpt-5.6-sol",
39
+ effort: "high",
40
+ includes: [/pre-planning analyst/i, /contradictions/, /Read-only/],
41
+ },
42
+ ],
43
+ [
44
+ "momus.toml",
45
+ {
46
+ model: "gpt-5.6-sol",
47
+ effort: "ultra",
48
+ includes: [/plan reviewer/i, /OKAY, ITERATE, or REJECT/, /Read-only/],
49
+ },
50
+ ],
51
+ [
52
+ "plan.toml",
53
+ {
54
+ model: "gpt-5.6-sol",
55
+ effort: "xhigh",
56
+ includes: [/strategic planning consultant/i, /\.omo\/plans\/<slug>\.md/, /never implements/i],
57
+ },
58
+ ],
19
59
  [
20
60
  "lazycodex-executor.toml",
21
61
  {
62
+ model: "gpt-5.6-sol",
22
63
  effort: "high",
23
64
  includes: [/EVIDENCE_RECORDED: <path>/, /scenario/i, /artifact/i],
24
65
  },
@@ -26,6 +67,7 @@ const lazycodexAgentInvariants = new Map([
26
67
  [
27
68
  "lazycodex-clone-fidelity-reviewer.toml",
28
69
  {
70
+ model: "gpt-5.6-sol",
29
71
  effort: "xhigh",
30
72
  includes: [/recommendation/, /blockers/, /\.omo\/evidence\/<goal>-clone-fidelity\.md/],
31
73
  },
@@ -33,6 +75,7 @@ const lazycodexAgentInvariants = new Map([
33
75
  [
34
76
  "lazycodex-code-reviewer.toml",
35
77
  {
78
+ model: "gpt-5.6-sol",
36
79
  effort: "xhigh",
37
80
  includes: [/codeQualityStatus/, /recommendation/, /\.omo\/evidence\/<goal>-code-review\.md/],
38
81
  },
@@ -40,6 +83,7 @@ const lazycodexAgentInvariants = new Map([
40
83
  [
41
84
  "lazycodex-qa-executor.toml",
42
85
  {
86
+ model: "gpt-5.6-terra",
43
87
  effort: "medium",
44
88
  includes: [/not_applicable/, /surfaceEvidence/, /adversarialCases/],
45
89
  },
@@ -47,6 +91,7 @@ const lazycodexAgentInvariants = new Map([
47
91
  [
48
92
  "lazycodex-gate-reviewer.toml",
49
93
  {
94
+ model: "gpt-5.6-sol",
50
95
  effort: "xhigh",
51
96
  includes: [/APPROVE\/REJECT/, /blockers/, /\.omo\/evidence\/<goal>-gate-review\.md/],
52
97
  },
@@ -109,7 +154,8 @@ test("#given lazycodex agent prompts #when inspected #then each role pins model
109
154
  for (const [fileName, invariant] of lazycodexAgentInvariants) {
110
155
  const prompt = await readFile(join(agentsDir, fileName), "utf8");
111
156
 
112
- assert.match(prompt, /^model\s*=\s*"gpt-5\.5"$/m);
157
+ const escapedModel = invariant.model.replace(/\./g, "\\.");
158
+ assert.match(prompt, new RegExp(`^model\\s*=\\s*"${escapedModel}"$`, "m"));
113
159
  assert.match(prompt, new RegExp(`^model_reasoning_effort\\s*=\\s*"${invariant.effort}"$`, "m"));
114
160
  assert.doesNotMatch(prompt, /^tools\s*=/m);
115
161
  assert.doesNotMatch(prompt, /^blocking\s*=/m);
@@ -34,7 +34,8 @@ test('#given synced skills and bundled rules #when role-specific agents are spaw
34
34
  const promptFiles = skillEntries
35
35
  .filter((entry) => entry.isDirectory())
36
36
  .map((entry) => join(skillsDir, entry.name, "SKILL.md"));
37
- promptFiles.push(join(root, "components", "rules", "bundled-rules", "hephaestus.md"));
37
+ promptFiles.push(join(root, "components", "rules", "bundled-rules", "hephaestus", "gpt-5.5.md"));
38
+ promptFiles.push(join(root, "components", "rules", "bundled-rules", "hephaestus", "gpt-5.6.md"));
38
39
 
39
40
  const missingForkContext = [];
40
41
  for (const promptPath of promptFiles) {
@@ -53,7 +54,8 @@ test("#given long-running orchestration prompts #when waiting on child agents #t
53
54
  join(root, "skills", "ulw-loop", "references", "full-workflow.md"),
54
55
  join(root, "skills", "review-work", "SKILL.md"),
55
56
  join(root, "skills", "start-work", "SKILL.md"),
56
- join(root, "components", "rules", "bundled-rules", "hephaestus.md"),
57
+ join(root, "components", "rules", "bundled-rules", "hephaestus", "gpt-5.5.md"),
58
+ join(root, "components", "rules", "bundled-rules", "hephaestus", "gpt-5.6.md"),
57
59
  ];
58
60
 
59
61
  const missingLivenessGuidance = [];
@@ -224,3 +224,36 @@ test("#given completed pending update #when hook session-start runs as CLI #then
224
224
  assert.equal(repeat.status, 0);
225
225
  assert.equal(repeat.stdout, "");
226
226
  });
227
+
228
+ test("#given the script reached through a symlinked directory #when hook session-start runs as CLI #then the entry guard still fires", async () => {
229
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-restart-notice-symlink-"));
230
+ const env = autoUpdateEnv(root, {
231
+ LAZYCODEX_CURRENT_VERSION: "1.0.1",
232
+ LAZYCODEX_LATEST_VERSION: "1.0.1",
233
+ LAZYCODEX_CONFIG_MIGRATION_DISABLED: "1",
234
+ });
235
+ await writeFile(env.LAZYCODEX_AUTO_UPDATE_STATE_PATH, JSON.stringify({
236
+ lastCheckedAt: Date.now() - 1_000,
237
+ lastStatus: "success",
238
+ pendingNotice: { fromVersion: "1.0.0", toVersion: "1.0.1", startedAt: 1 },
239
+ }));
240
+ // Simulate a symlinked plugin cache dir: argv[1] spells the symlink, while
241
+ // import.meta.url resolves to the real path. Seen in the wild; the old
242
+ // `pathToFileURL(process.argv[1])` guard never matched and the hook was a
243
+ // silent no-op.
244
+ const { symlink } = await import("node:fs/promises");
245
+ const { dirname } = await import("node:path");
246
+ const linkDir = join(root, "linked-scripts");
247
+ await symlink(dirname(SCRIPT_PATH), linkDir, "dir");
248
+ const linkedScript = join(linkDir, "auto-update.mjs");
249
+
250
+ const result = spawnSync(process.execPath, [linkedScript, "hook", "session-start"], {
251
+ encoding: "utf8",
252
+ env: { ...process.env, ...env },
253
+ });
254
+
255
+ assert.equal(result.status, 0);
256
+ const lines = result.stdout.split("\n").filter((line) => line.length > 0);
257
+ assert.equal(lines.length, 1, `expected one SessionStart JSON line, got stdout=${JSON.stringify(result.stdout)} stderr=${JSON.stringify(result.stderr)}`);
258
+ assert.equal(JSON.parse(lines[0]).hookSpecificOutput.hookEventName, "SessionStart");
259
+ });
@@ -756,7 +756,7 @@ test("#given global config with forced multi_agent_v2 #when full migration runs
756
756
  test("#given enabled = true with an inline comment #when forcing disable #then flips to false and preserves the comment", () => {
757
757
  const config = ["[features.multi_agent_v2]", "enabled = true # tuned by me", ""].join("\n");
758
758
 
759
- const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null });
759
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: "v1" });
760
760
 
761
761
  assert.match(result, /^enabled = false # tuned by me$/m);
762
762
  assert.doesNotMatch(result, /enabled = true/);
@@ -767,7 +767,7 @@ test("#given enabled = true with an inline comment #when forcing disable #then f
767
767
  test("#given a section header with an inline comment #when forcing disable #then patches in place without duplicating the table", () => {
768
768
  const config = ["[features.multi_agent_v2] # pinned by me", "enabled = true", ""].join("\n");
769
769
 
770
- const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null });
770
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: "v1" });
771
771
 
772
772
  assert.equal((result.match(/\[features\.multi_agent_v2\]/g) ?? []).length, 1);
773
773
  assert.match(result, /enabled = false/);
@@ -779,19 +779,19 @@ test("#given an already-guarded commented config #when re-running #then output i
779
779
  const configA = ["[features.multi_agent_v2]", "enabled = true # tuned by me", ""].join("\n");
780
780
  const configB = ["[features.multi_agent_v2] # pinned by me", "enabled = true", ""].join("\n");
781
781
 
782
- const firstA = forceDisableMultiAgentV2(configA, { multiAgentVersion: null });
783
- const rerunA = forceDisableMultiAgentV2(firstA, { multiAgentVersion: null });
782
+ const firstA = forceDisableMultiAgentV2(configA, { multiAgentVersion: "v1" });
783
+ const rerunA = forceDisableMultiAgentV2(firstA, { multiAgentVersion: "v1" });
784
784
  assert.equal(rerunA, firstA);
785
785
 
786
- const firstB = forceDisableMultiAgentV2(configB, { multiAgentVersion: null });
787
- const rerunB = forceDisableMultiAgentV2(firstB, { multiAgentVersion: null });
786
+ const firstB = forceDisableMultiAgentV2(configB, { multiAgentVersion: "v1" });
787
+ const rerunB = forceDisableMultiAgentV2(firstB, { multiAgentVersion: "v1" });
788
788
  assert.equal(rerunB, firstB);
789
789
  });
790
790
 
791
791
  test("#given user-disabled with an inline comment #when forcing disable #then returns config unchanged", () => {
792
792
  const config = ["[features.multi_agent_v2]", "enabled = false # I turned this off myself", ""].join("\n");
793
793
 
794
- const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null });
794
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: "v1" });
795
795
 
796
796
  assert.equal(result, config);
797
797
  });
@@ -799,7 +799,7 @@ test("#given user-disabled with an inline comment #when forcing disable #then re
799
799
  test("#given [features] shorthand true with an inline comment #when forcing disable #then removes the shorthand and appends one disabled table", () => {
800
800
  const config = ["[features]", "plugins = true", "multi_agent_v2 = true # legacy", ""].join("\n");
801
801
 
802
- const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null });
802
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: "v1" });
803
803
 
804
804
  assert.doesNotMatch(result, /^\s*multi_agent_v2\s*=/m);
805
805
  assert.equal((result.match(/\[features\.multi_agent_v2\]/g) ?? []).length, 1);
@@ -816,7 +816,7 @@ test("#given a following section header with an inline comment #when inserting e
816
816
  "",
817
817
  ].join("\n");
818
818
 
819
- const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null });
819
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: "v1" });
820
820
 
821
821
  assert.match(result, /\[features\.multi_agent_v2\]\nenabled = false\n/);
822
822
  assert.match(result, /\[mcp_servers\.x\][^\n]*\nenabled = true/);
@@ -1034,6 +1034,41 @@ test("#given legacy shorthand and no session model on hook path #when full migra
1034
1034
  assert.equal("enabled" in parsed.features.multi_agent_v2, false);
1035
1035
  });
1036
1036
 
1037
+ test("#given SoT migration failure #when hook migration runs #then config.toml repair still happens", async () => {
1038
+ const { runAutoUpdateCheck } = await import("../scripts/auto-update.mjs");
1039
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-sot-isolation-"));
1040
+ const codexHome = join(root, "codex-home");
1041
+ await mkdir(codexHome, { recursive: true });
1042
+ const configPath = join(codexHome, "config.toml");
1043
+ await writeFile(
1044
+ configPath,
1045
+ ['model = "gpt-5.6-sol"', "", "[features.multi_agent_v2]", "enabled = false", ""].join("\n"),
1046
+ );
1047
+ await writeFile(
1048
+ join(codexHome, "models_cache.json"),
1049
+ JSON.stringify({ models: [{ slug: "gpt-5.6-sol", multi_agent_version: "v2" }] }),
1050
+ );
1051
+ // HOME pointing at a FILE makes migrateOmoSotConfig's mkdir(~/.omo) throw.
1052
+ const brokenHome = join(root, "not-a-dir");
1053
+ await writeFile(brokenHome, "");
1054
+
1055
+ await runAutoUpdateCheck({
1056
+ env: {
1057
+ CODEX_HOME: codexHome,
1058
+ HOME: brokenHome,
1059
+ USERPROFILE: brokenHome,
1060
+ LAZYCODEX_AUTO_UPDATE_DISABLED: "1",
1061
+ LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json"),
1062
+ LAZYCODEX_AUTO_UPDATE_STATE_PATH: join(root, "state.json"),
1063
+ },
1064
+ sessionModel: "gpt-5.6-sol",
1065
+ requireSessionModel: true,
1066
+ });
1067
+
1068
+ const content = await readFile(configPath, "utf8");
1069
+ assert.doesNotMatch(content, /^\s*enabled\s*=\s*false/m);
1070
+ });
1071
+
1037
1072
  test("#given config default gpt-5.5 #when full migration gets SessionStart gpt-5.6-terra #then clears disable using session model", async () => {
1038
1073
  const root = await mkdtemp(join(tmpdir(), "lazycodex-multi-agent-v2-session-model-"));
1039
1074
  const codexHome = join(root, "codex-home");
@@ -1078,6 +1113,47 @@ test("#given config default gpt-5.5 #when full migration gets SessionStart gpt-5
1078
1113
  assert.match(content, /max_concurrent_threads_per_session = 1000/);
1079
1114
  });
1080
1115
 
1116
+ test("#given no session model and no root model #when forcing disable #then leaves the enable state untouched", () => {
1117
+ const config = ["[features]", "plugins = true", "", "[features.multi_agent_v2]", "enabled = true", ""].join("\n");
1118
+
1119
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null, sessionModel: null });
1120
+
1121
+ assert.match(result, /enabled = true/);
1122
+ assert.doesNotMatch(result, /enabled = false/);
1123
+ assert.doesNotMatch(result, /openai\/codex#26753/);
1124
+ });
1125
+
1126
+ test("#given no session model and no root model #when config has no multi_agent_v2 section #then does not append a disable", () => {
1127
+ const config = ["[features]", "plugins = true", ""].join("\n");
1128
+
1129
+ const result = forceDisableMultiAgentV2(config, { multiAgentVersion: null, sessionModel: null });
1130
+
1131
+ assert.equal(result, config);
1132
+ assert.doesNotMatch(result, /\[features\.multi_agent_v2\]/);
1133
+ });
1134
+
1135
+ test("#given user-modified config without root model #when full non-hook migration runs #then writes no disable and no new agents.max_threads", async () => {
1136
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-multi-agent-v2-desktop-no-model-"));
1137
+ const codexHome = join(root, "codex-home");
1138
+ await mkdir(codexHome, { recursive: true });
1139
+ const configPath = join(codexHome, "config.toml");
1140
+ // Codex Desktop sessions select the model in the UI; config.toml has no root
1141
+ // `model`, so migration cannot prove the session is not a GPT-5.6
1142
+ // reserved-schema model (#6002).
1143
+ await writeFile(configPath, ['model_reasoning_effort = "high"', "", "[features]", "plugins = true", ""].join("\n"));
1144
+
1145
+ await migrateCodexConfig({
1146
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
1147
+ cwd: root,
1148
+ });
1149
+
1150
+ const content = await readFile(configPath, "utf8");
1151
+ assert.doesNotMatch(content, /^\s*enabled\s*=\s*false/m);
1152
+ assert.doesNotMatch(content, /openai\/codex#26753/);
1153
+ assert.doesNotMatch(content, /^\s*max_threads\s*=/m);
1154
+ assert.match(content, /max_concurrent_threads_per_session = 1000/);
1155
+ });
1156
+
1081
1157
  async function canCreateSymlink(type) {
1082
1158
  const root = await mkdtemp(join(tmpdir(), "lazycodex-symlink-capability-"));
1083
1159
  const target = join(root, "target");
@@ -1103,3 +1179,64 @@ async function canCreateSymlink(type) {
1103
1179
  return false;
1104
1180
  }
1105
1181
  }
1182
+
1183
+ test("#given model_catalog_json declares a v2 model as v1 #when full migration runs #then keeps the managed disable and max_threads", async () => {
1184
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-multi-agent-v2-catalog-override-"));
1185
+ const codexHome = join(root, "codex-home");
1186
+ await mkdir(codexHome, { recursive: true });
1187
+ const configPath = join(codexHome, "config.toml");
1188
+ const catalogPath = join(root, "custom-catalog.json");
1189
+ // Codex Desktop user forces gpt-5.6-sol to v1 via an explicit replacement catalog.
1190
+ await writeFile(configPath, [
1191
+ 'model = "gpt-5.6-sol"',
1192
+ `model_catalog_json = "${catalogPath}"`,
1193
+ "",
1194
+ "[agents]",
1195
+ "max_threads = 1000",
1196
+ "max_depth = 2",
1197
+ "",
1198
+ "[features.multi_agent_v2]",
1199
+ "enabled = false",
1200
+ "max_concurrent_threads_per_session = 1000",
1201
+ "",
1202
+ ].join("\n"));
1203
+ // models_cache.json STILL says v2 (stale), but the explicit catalog wins and says v1.
1204
+ await writeFile(join(codexHome, "models_cache.json"), JSON.stringify({ models: [{ slug: "gpt-5.6-sol", multi_agent_version: "v2" }] }));
1205
+ await writeFile(catalogPath, JSON.stringify({ models: [{ slug: "gpt-5.6-sol", multi_agent_version: "v1" }] }));
1206
+
1207
+ await migrateCodexConfig({
1208
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
1209
+ cwd: root,
1210
+ });
1211
+
1212
+ const content = await readFile(configPath, "utf8");
1213
+ assert.match(content, /enabled = false/, "explicit v1 catalog must keep the managed disable");
1214
+ assert.match(content, /max_threads = 1000/, "explicit v1 catalog must keep agents.max_threads");
1215
+ assert.match(content, /max_concurrent_threads_per_session = 1000/);
1216
+ assert.match(content, /max_depth = 2/);
1217
+ });
1218
+
1219
+ test("#given model_catalog_json declares a model as v2 #when full migration runs #then clears the managed disable", async () => {
1220
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-multi-agent-v2-catalog-v2-"));
1221
+ const codexHome = join(root, "codex-home");
1222
+ await mkdir(codexHome, { recursive: true });
1223
+ const configPath = join(codexHome, "config.toml");
1224
+ const catalogPath = join(root, "custom-catalog.json");
1225
+ await writeFile(configPath, [
1226
+ 'model = "custom-model"',
1227
+ `model_catalog_json = "${catalogPath}"`,
1228
+ "",
1229
+ "[features.multi_agent_v2]",
1230
+ "enabled = false",
1231
+ "",
1232
+ ].join("\n"));
1233
+ await writeFile(catalogPath, JSON.stringify({ models: [{ slug: "custom-model", multi_agent_version: "v2" }] }));
1234
+
1235
+ await migrateCodexConfig({
1236
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
1237
+ cwd: root,
1238
+ });
1239
+
1240
+ const content = await readFile(configPath, "utf8");
1241
+ assert.doesNotMatch(content, /^\s*enabled\s*=\s*false/m, "explicit v2 catalog must clear the disable");
1242
+ });
@@ -0,0 +1,71 @@
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 { migrateCodexConfig } from "../scripts/migrate-codex-config.mjs";
8
+
9
+ test("#given relative model_catalog_json declares gpt-5.6 model as v1 #when migrating #then resolves catalog from config directory", async () => {
10
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-v2-relative-catalog-"));
11
+ const codexHome = join(root, "codex-home");
12
+ await mkdir(codexHome, { recursive: true });
13
+ const configPath = join(codexHome, "config.toml");
14
+ await writeFile(
15
+ configPath,
16
+ [
17
+ 'model = "gpt-5.6-sol"',
18
+ 'model_catalog_json = "custom-catalog.json"',
19
+ "",
20
+ "[agents]",
21
+ "max_threads = 1000",
22
+ "",
23
+ "[features.multi_agent_v2]",
24
+ "enabled = false",
25
+ "max_concurrent_threads_per_session = 1000",
26
+ "",
27
+ ].join("\n"),
28
+ );
29
+ await writeFile(join(codexHome, "models_cache.json"), JSON.stringify({ models: [{ slug: "gpt-5.6-sol", multi_agent_version: "v2" }] }));
30
+ await writeFile(join(codexHome, "custom-catalog.json"), JSON.stringify({ models: [{ slug: "gpt-5.6-sol", multi_agent_version: "v1" }] }));
31
+
32
+ await migrateCodexConfig({
33
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
34
+ cwd: root,
35
+ });
36
+
37
+ const content = await readFile(configPath, "utf8");
38
+ assert.match(content, /enabled = false/);
39
+ assert.match(content, /max_threads = 1000/);
40
+ });
41
+
42
+ test("#given no SessionStart model and root gpt-5.6 model without catalog #when migrating #then clears stale V2 disable", async () => {
43
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-v2-root-gpt56-nocache-"));
44
+ const codexHome = join(root, "codex-home");
45
+ await mkdir(codexHome, { recursive: true });
46
+ const configPath = join(codexHome, "config.toml");
47
+ await writeFile(
48
+ configPath,
49
+ [
50
+ 'model = "gpt-5.6-terra"',
51
+ "",
52
+ "[agents]",
53
+ "max_threads = 1000",
54
+ "",
55
+ "[features.multi_agent_v2]",
56
+ "enabled = false",
57
+ "max_concurrent_threads_per_session = 1000",
58
+ "",
59
+ ].join("\n"),
60
+ );
61
+
62
+ await migrateCodexConfig({
63
+ env: { CODEX_HOME: codexHome, LAZYCODEX_MODEL_CATALOG_STATE_PATH: join(root, "model-state.json") },
64
+ cwd: root,
65
+ });
66
+
67
+ const content = await readFile(configPath, "utf8");
68
+ assert.doesNotMatch(content, /^\s*enabled\s*=\s*false/m);
69
+ assert.doesNotMatch(content, /^\s*max_threads\s*=/m);
70
+ assert.match(content, /max_concurrent_threads_per_session = 1000/);
71
+ });
@@ -75,3 +75,35 @@ test("#given gpt-5.6 session model with no models_cache #when migrating #then do
75
75
  assert.match(content, /max_depth = 4/);
76
76
  assert.match(content, /max_concurrent_threads_per_session = 1000/);
77
77
  });
78
+
79
+ test("#given config without any model #when migrating #then does not introduce agents.max_threads", async () => {
80
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-subagent-limit-no-model-"));
81
+ const configPath = join(root, "config.toml");
82
+ await writeFile(
83
+ configPath,
84
+ ['model_reasoning_effort = "high"', "", "[agents]", "max_depth = 4", ""].join("\n"),
85
+ );
86
+
87
+ await migrateConfigFile(configPath, { env: { CODEX_HOME: root } });
88
+
89
+ const content = await readFile(configPath, "utf8");
90
+ assert.doesNotMatch(content, /^\s*max_threads\s*=/m);
91
+ assert.match(content, /max_depth = 4/);
92
+ assert.match(content, /max_concurrent_threads_per_session = 1000/);
93
+ });
94
+
95
+ test("#given config without any model but an existing low cap #when migrating #then still raises the existing cap", async () => {
96
+ const root = await mkdtemp(join(tmpdir(), "lazycodex-subagent-limit-no-model-raise-"));
97
+ const configPath = join(root, "config.toml");
98
+ await writeFile(
99
+ configPath,
100
+ ['model_reasoning_effort = "high"', "", "[agents]", "max_threads = 6", "max_depth = 4", ""].join("\n"),
101
+ );
102
+
103
+ await migrateConfigFile(configPath, { env: { CODEX_HOME: root } });
104
+
105
+ const content = await readFile(configPath, "utf8");
106
+ assert.match(content, /max_threads = 1000/);
107
+ assert.doesNotMatch(content, /max_threads = 6/);
108
+ assert.match(content, /max_depth = 4/);
109
+ });
@@ -3,7 +3,7 @@ import { readdir, readFile } from "node:fs/promises";
3
3
  import { dirname, join } from "node:path";
4
4
  import test from "node:test";
5
5
  import { fileURLToPath } from "node:url";
6
- import { insertCodexCompatibilityGuidance } from "../scripts/sync-skills.mjs";
6
+ import { codexHarnessToolCompatibility, insertCodexCompatibilityGuidance } from "../scripts/sync-skills.mjs";
7
7
 
8
8
  const root = dirname(dirname(fileURLToPath(import.meta.url)));
9
9
 
@@ -207,7 +207,8 @@ test("#given synced aggregate Codex skills #when they describe background orches
207
207
  ["working progress message", /WORKING:/],
208
208
  ["blocked progress message", /BLOCKED:/],
209
209
  ["mailbox timeout framing", /timeout only means no new mailbox update arrived/],
210
- ["multi_agent_v1.wait_agent ref", /multi_agent_v1\.wait_agent/],
210
+ // Skills route by session tool surface: the namespaced V1 tool or the flat V2 tool both count.
211
+ ["wait-agent tool ref", /multi_agent_v1\.wait_agent|`wait_agent`/],
211
212
  ["explicit fallback conditions", /Fallback only when|Mark a file for retry only when/],
212
213
  ];
213
214
  const bannedPatterns = [
@@ -265,3 +266,9 @@ test("#given PR and review skills #when synced for Codex #then worktree lifecycl
265
266
  assert.match(reviewWork, /dedicated review worktree attached to that branch/);
266
267
  assert.match(reviewWork, /Never\s+checkout, test, or edit the review branch in the main worktree/);
267
268
  });
269
+
270
+ test("#given generated Codex compatibility guidance #when multi-agent lifecycle tools are mentioned #then optional tools are guarded by the active tools list", () => {
271
+ assert.match(codexHarnessToolCompatibility, /when exposed in the active tools list/, "send_input/close_agent must be marked optional (lazycodex#116)");
272
+ assert.match(codexHarnessToolCompatibility, /multi_agent_v1\.spawn_agent/);
273
+ assert.match(codexHarnessToolCompatibility, /multi_agent_v1\.wait_agent/);
274
+ });