oh-my-opencode 5.0.0-beta.47 → 5.0.0-beta.49

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 (129) hide show
  1. package/README.ja.md +35 -27
  2. package/README.ko.md +35 -27
  3. package/README.md +19 -34
  4. package/README.ru.md +33 -28
  5. package/README.zh-cn.md +31 -27
  6. package/dist/cli/index.js +19 -15
  7. package/dist/cli-node/index.js +19 -15
  8. package/dist/features/team-mode/tools/messaging-fallback-wake.d.ts +2 -0
  9. package/dist/features/team-mode/tools/messaging-live-delivery-recipient.d.ts +2 -0
  10. package/dist/features/team-mode/tools/messaging-runtime.d.ts +6 -0
  11. package/dist/index.js +30 -18
  12. package/dist/tui.js +4 -0
  13. package/package.json +14 -14
  14. package/packages/lsp-daemon/dist/cli.js +6 -3
  15. package/packages/lsp-daemon/dist/client.js +6 -3
  16. package/packages/lsp-daemon/dist/ensure-daemon.d.ts +6 -1
  17. package/packages/lsp-daemon/dist/ensure-daemon.js +9 -2
  18. package/packages/lsp-daemon/dist/index.js +6 -3
  19. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +2 -2
  20. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  25. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  29. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +6 -3
  30. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  31. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  33. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  34. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  35. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  36. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  37. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  38. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +11 -10
  39. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  40. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  42. package/packages/omo-codex/plugin/components/ulw-execute-continuation/AGENTS.md +3 -3
  43. package/packages/omo-codex/plugin/components/ulw-execute-continuation/README.md +3 -3
  44. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +4 -4
  45. package/packages/omo-codex/plugin/components/ulw-execute-continuation/dist/cli.js +1 -1
  46. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -13
  47. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  48. package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/codex-hook.ts +1 -1
  49. package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/types.ts +1 -1
  50. package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/cli.test.ts +3 -7
  51. package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/codex-hook.test.ts +16 -0
  52. package/packages/omo-codex/plugin/components/ulw-loop/README.md +2 -2
  53. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +11 -10
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +8 -15
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -2
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  57. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
  58. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +88 -67
  59. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +3 -4
  60. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +1 -1
  61. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +13 -16
  63. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +2 -0
  64. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +47 -26
  65. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +2 -2
  66. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +1 -0
  67. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +14 -4
  68. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +17 -4
  69. package/packages/omo-codex/plugin/components/ulw-loop/package.json +2 -2
  70. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  71. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +8 -14
  72. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -2
  73. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
  74. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +5 -1
  75. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +3 -4
  76. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +1 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +4 -1
  78. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +21 -16
  79. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +52 -25
  80. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +2 -2
  81. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +16 -4
  82. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +11 -12
  83. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-fields-messages.test.ts +98 -0
  84. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-lazycodex-surface.test.ts +93 -0
  85. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +9 -4
  86. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +10 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +70 -7
  88. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +18 -0
  96. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  98. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  99. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  100. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  109. package/packages/omo-codex/plugin/package-lock.json +12 -12
  110. package/packages/omo-codex/plugin/package.json +1 -1
  111. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +61 -33
  112. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +4 -2
  113. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +4 -2
  114. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -2
  115. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +13 -3
  116. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +11 -10
  117. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +11 -6
  118. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +24 -0
  119. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  120. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +4 -2
  121. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -2
  122. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +8 -4
  123. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +62 -0
  124. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +3 -3
  125. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +23 -1
  126. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +36 -16
  127. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  128. package/packages/prompts-core/prompts/ultrawork/codex.md +11 -10
  129. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +0 -17
@@ -7,7 +7,20 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit --with-ultrawork",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Loop Steering"
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "PostToolUse": [
16
+ {
17
+ "matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-tool-use-spawn",
22
+ "timeout": 5,
23
+ "statusMessage": "(OmO 5.0.0-beta.49) Recording Spawn Admission"
11
24
  }
12
25
  ]
13
26
  }
@@ -20,7 +33,7 @@
20
33
  "type": "command",
21
34
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
22
35
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.47) Enforcing Unlimited Ulw-Loop Budget"
36
+ "statusMessage": "(OmO 5.0.0-beta.49) Enforcing Unlimited Ulw-Loop Budget"
24
37
  }
25
38
  ]
26
39
  },
@@ -31,7 +44,7 @@
31
44
  "type": "command",
32
45
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
33
46
  "timeout": 5,
34
- "statusMessage": "(OmO 5.0.0-beta.47) Guarding Ulw-Loop Spawns"
47
+ "statusMessage": "(OmO 5.0.0-beta.49) Guarding Ulw-Loop Spawns"
35
48
  }
36
49
  ]
37
50
  }
@@ -43,7 +56,7 @@
43
56
  "type": "command",
44
57
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
45
58
  "timeout": 10,
46
- "statusMessage": "(OmO 5.0.0-beta.47) Checking Ulw-Loop Resume"
59
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Loop Resume"
47
60
  }
48
61
  ]
49
62
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.47",
3
+ "version": "5.0.0-beta.49",
4
4
  "description": "Codex plugin: durable repo-native multi-goal orchestration with embedded success criteria and observable evidence audit.",
5
5
  "type": "module",
6
6
  "packageManager": "npm@11.12.1",
@@ -38,7 +38,7 @@
38
38
  "CHANGELOG.md"
39
39
  ],
40
40
  "scripts": {
41
- "build": "tsc -p tsconfig.build.json",
41
+ "build": "tsc -p tsconfig.build.json && bun build src/cli.ts --target node --format esm --outfile dist/cli.js",
42
42
  "test": "vitest --run",
43
43
  "test:watch": "vitest",
44
44
  "typecheck": "tsc --noEmit",
@@ -198,7 +198,7 @@ omo-agent-toolkit ulw-loop checkpoint --goal-id <g> --status complete --evidence
198
198
  omo-agent-toolkit ulw-loop complete-goals
199
199
  ```
200
200
 
201
- The lazycodex gate uses all five sections shown in the sample below, including `codeReview`.
201
+ The lazycodex gate requires `manualQa`, `gateReview`, `iteration`, and `criteriaCoverage`; `codeReview` is optional. Self-review defaults to `main-session`, while gate review may use `main-session` or an approved `category:*` acceptor. Spawn reviewer lanes only when strict review is explicitly requested.
202
202
 
203
203
  ## Final Quality Gate
204
204
  Trigger only for the final aggregate goal after every criterion in every goal is `pass`.
@@ -31,7 +31,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
31
31
  },
32
32
  ];
33
33
  const manualQa = {
34
- by: surface === "omo-senpi" ? "main-session" : "lazycodex-qa-executor",
34
+ by: "main-session",
35
35
  status: "passed",
36
36
  evidence: "<replace:manual QA evidence>",
37
37
  surfaceEvidence: [
@@ -68,7 +68,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
68
68
  const common = {
69
69
  manualQa,
70
70
  gateReview: {
71
- by: surface === "omo-senpi" ? "category:deep" : "lazycodex-gate-reviewer",
71
+ by: surface === "omo-senpi" ? "category:deep" : "main-session",
72
72
  recommendation: "APPROVE",
73
73
  reportPath: artifactPath(base, "gate-review.md"),
74
74
  evidence: "<replace:gate review evidence>",
@@ -90,18 +90,7 @@ function gateTemplate(surface: UlwLoopToolkitSurface, base: string): Record<stri
90
90
  adversarialClassesCovered: ["<replace:adversarial class>"],
91
91
  },
92
92
  };
93
- if (surface === "omo-senpi") return common;
94
- return {
95
- codeReview: {
96
- by: "lazycodex-code-reviewer",
97
- recommendation: "APPROVE",
98
- codeQualityStatus: "CLEAR",
99
- reportPath: artifactPath(base, "code-review.md"),
100
- evidence: "<replace:code review evidence>",
101
- blockers: [],
102
- },
103
- ...common,
104
- };
93
+ return common;
105
94
  }
106
95
 
107
96
  export async function checkpointTemplate(
@@ -121,6 +110,11 @@ export async function checkpointTemplate(
121
110
  "Fill every <replace:...> value with plausible non-empty evidence and use real, non-empty artifact files.",
122
111
  'Passing codex-goal-json example: {"goal":{"objective":"<plan codexObjective verbatim>","status":"complete"}}.',
123
112
  'Passing quality-gate-json example requires gateReview {"by":"category:deep","recommendation":"APPROVE","evidence":"review passed","reportPath":"<attemptDir>/gate-review.md","blockers":[],"notes":[]}, manualQa.artifactRefs objects, iteration, and criteriaCoverage.',
113
+ ...(resolveToolkitSurface() === "lazycodex"
114
+ ? [
115
+ "Self-review defaults: manualQa.by and gateReview.by are main-session. Alternatives: manualQa.by accepts lazycodex-qa-executor; gateReview.by accepts lazycodex-gate-reviewer, category:deep, category:unspecified-high, or category:unspecified-low. Optional codeReview.by accepts lazycodex-code-reviewer or main-session.",
116
+ ]
117
+ : []),
124
118
  ...(hasAttempt ? [] : ["This plan is evidence-layout v1; artifacts go under .omo/evidence/."]),
125
119
  ].join(" ");
126
120
  return {
@@ -126,7 +126,7 @@ function buildLedger(
126
126
  codexGoal: unknown,
127
127
  aggregateCompletion: UlwLoopAggregateCompletion | undefined,
128
128
  ): UlwLoopLedgerEntry {
129
- const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview.codeQualityStatus === "WATCH";
129
+ const watch = qualityGate?.surface === "lazycodex" && qualityGate.codeReview?.codeQualityStatus === "WATCH";
130
130
  const entry: UlwLoopLedgerEntry = {
131
131
  at: now,
132
132
  kind: ledgerKind(args.status, goal, aggregateCompletion),
@@ -134,7 +134,7 @@ function buildLedger(
134
134
  status: goal.status,
135
135
  evidence:
136
136
  watch && qualityGate.surface === "lazycodex"
137
- ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview.evidence}`
137
+ ? `${args.evidence} | codeQuality=WATCH: ${qualityGate.codeReview?.evidence ?? ""}`
138
138
  : args.evidence,
139
139
  };
140
140
  if (codexGoal !== undefined) entry.codexGoal = codexGoal;
@@ -16,7 +16,7 @@ export const ULW_LOOP_HELP = `Usage:
16
16
  omo-agent-toolkit ulw-loop add-goal --title "..." --objective "..." [--json]
17
17
  omo-agent-toolkit ulw-loop record-review-blockers --goal-id <id> --title "..." --objective "..." --evidence "..." --codex-goal-json <...> [--json]
18
18
 
19
- Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly.
19
+ Every state subcommand needs a session scope: [--session-id <id>] or the session env (OMO_ULW_LOOP_SESSION_ID / CODEX_SESSION_ID / CODEX_THREAD_ID / PI_SESSION_ID); state lives under .omo/ulw-loop/<id>/ and the unscoped root is never used implicitly. status --json exposes the currentAttemptDir; put all quality-gate artifacts under it.
20
20
  Every subcommand accepts --help | -h to print its own usage line.`;
21
21
 
22
22
  export function subcommandHelp(subcommand: string): string {
@@ -2,7 +2,7 @@
2
2
  import { isUlwLoopSubcommand, ulwLoopCommand } from "./cli-commands.js";
3
3
  import { ULW_LOOP_HELP } from "./cli-output.js";
4
4
  import { runPreToolUseGoalBudgetGuardCli, runUlwLoopHookCli } from "./codex-hook.js";
5
- import { runSpawnGuardCli } from "./spawn-guard.js";
5
+ import { runSpawnAdmissionRecorderCli, runSpawnGuardCli } from "./spawn-guard.js";
6
6
  import { runStopResumeHookCli } from "./stop-resume-hook.js";
7
7
 
8
8
  async function main(): Promise<number> {
@@ -29,6 +29,10 @@ async function main(): Promise<number> {
29
29
  await runStopResumeHookCli(process.stdin, process.stdout);
30
30
  return 0;
31
31
  }
32
+ if (sub === "post-tool-use-spawn") {
33
+ await runSpawnAdmissionRecorderCli(process.stdin, process.stdout);
34
+ return 0;
35
+ }
32
36
  if (sub === "pre-tool-use-spawn") {
33
37
  await runSpawnGuardCli(process.stdin, process.stdout);
34
38
  return 0;
@@ -131,12 +131,11 @@ function finalSection(
131
131
  const blockerCommand = `omo-agent-toolkit ulw-loop record-review-blockers${option} --goal-id ${goal.id} --title "Resolve final code-review blockers" --objective "<blocker-resolution objective>" --evidence "<review findings>" --codex-goal-json "<active get_goal JSON or path>"`;
132
132
  const checkpointCommand = `omo-agent-toolkit ulw-loop checkpoint${option} --goal-id ${goal.id} --status complete --evidence "<targeted verification/manualQa/gateReview evidence>" --codex-goal-json "<fresh complete get_goal JSON or path>" --quality-gate-json "<quality gate JSON or path>"`;
133
133
  return joinLines([
134
- "Final story — run mandatory quality gate before update_goal:",
134
+ "Final story — self-review and manual QA are the default; use the quality gate before update_goal:",
135
135
  "- Run targeted verification for changed behavior.",
136
136
  "- Confirm every manualQa artifact path exists and has non-zero size.",
137
- `- First spawn ${roles.codeReview} and ${roles.manualQa} in parallel (fork_context: false on the v1 surface; fork_turns: "none" on v2). Include the original brief, goal objectives, desired user-visible outcome, diff, and evidence; wait for BOTH to return and confirm their report artifacts exist on disk (code-review report + manualQa matrix).`,
138
- `- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
139
- "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
137
+ `- Run manual QA yourself and write its artifact under currentAttemptDir. Only if the user explicitly demands strict, rigorous, or high-accuracy review, spawn ${roles.gateReview}, optionally also ${roles.codeReview} and ${roles.manualQa}; otherwise set manualQa.by and gateReview.by to "main-session".`,
138
+ "- Require passed manualQa, approved gateReview, passed iteration, and complete criteriaCoverage; include codeReview only when strict review was requested. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
140
139
  "- On a reviewer REJECT, fix only the cited blockers, rerun the affected verification/Manual-QA, and re-review the delta at most TWICE; if blockers remain, record them and surface to the user.",
141
140
  "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
142
141
  "- If any reviewer is blocked/inconclusive or the quality gate is not clean, do not call update_goal. Record blocker work first:",
@@ -141,7 +141,7 @@ interface UlwLoopQualityGateCommon {
141
141
 
142
142
  export interface UlwLoopQualityGateLazycodex extends UlwLoopQualityGateCommon {
143
143
  readonly surface: "lazycodex";
144
- readonly codeReview: {
144
+ readonly codeReview?: {
145
145
  readonly by: string;
146
146
  readonly recommendation: "APPROVE";
147
147
  readonly codeQualityStatus: "CLEAR" | "WATCH";
@@ -61,7 +61,10 @@ export function isRecord(value: unknown): value is Record<string, unknown> {
61
61
  }
62
62
  export function section(value: unknown, field: string): Record<string, unknown> {
63
63
  if (isRecord(value)) return value;
64
- invalid(`Final quality gate is missing ${field} evidence.`, field);
64
+ invalid(
65
+ `Final quality gate is missing ${field} evidence (accepted input: a bare object with manualQa/gateReview/iteration/criteriaCoverage and optional codeReview, or the same object under a top-level "qualityGate" key).`,
66
+ field,
67
+ );
65
68
  return {};
66
69
  }
67
70
  export function textField(value: unknown, field: string): string {
@@ -20,7 +20,12 @@ import {
20
20
  withQualityGateCollector,
21
21
  } from "./quality-gate-fields.js";
22
22
  import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
23
- import { GATE_SECTION_BY_ACCEPTOR, type UlwLoopToolkitSurface } from "./surface.js";
23
+ import {
24
+ GATE_SECTION_BY_ACCEPTOR,
25
+ OPTIONAL_GATE_SECTIONS_BY_SURFACE,
26
+ REQUIRED_GATE_SECTIONS_BY_SURFACE,
27
+ type UlwLoopToolkitSurface,
28
+ } from "./surface.js";
24
29
  import type { UlwLoopManualQaArtifactRef, UlwLoopQualityGate } from "./types.js";
25
30
 
26
31
  export {
@@ -42,20 +47,10 @@ export interface ValidateQualityGateOptions {
42
47
  readonly reviewerSurface?: UlwLoopToolkitSurface;
43
48
  }
44
49
 
45
- function reviewerRoleField(value: unknown, expected: string, field: string): string {
46
- if (typeof value !== "string" || value.trim() === "") {
47
- textField(value, field);
48
- return expected;
49
- }
50
- const actual = textField(value, field);
51
- if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
52
- return expected;
53
- }
54
-
55
50
  function reviewerAcceptorField(
56
51
  value: unknown,
57
52
  surface: UlwLoopToolkitSurface,
58
- sectionName: "manualQa" | "gateReview",
53
+ sectionName: "codeReview" | "manualQa" | "gateReview",
59
54
  ): string {
60
55
  const field = `${sectionName}.by`;
61
56
  const accepted = GATE_SECTION_BY_ACCEPTOR[surface][sectionName];
@@ -75,19 +70,28 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
75
70
 
76
71
  function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
77
72
  const surface = opts?.reviewerSurface ?? "lazycodex";
78
- const gate = section(input, "qualityGate");
73
+ const raw = input as Record<string, unknown>;
74
+ const gate = section(
75
+ raw && typeof raw === "object" && "qualityGate" in raw ? raw["qualityGate"] : input,
76
+ "qualityGate",
77
+ );
78
+ for (const name of REQUIRED_GATE_SECTIONS_BY_SURFACE[surface]) section(gate[name], name);
79
+ for (const name of Object.keys(gate)) {
80
+ if (name === "codeReview" && !OPTIONAL_GATE_SECTIONS_BY_SURFACE[surface].includes(name as never))
81
+ invalid("omo-senpi gate has no codeReview lane.", name);
82
+ }
79
83
  if (surface === "omo-senpi" && gate["codeReview"] !== undefined)
80
84
  invalid("omo-senpi gate has no codeReview lane.", "codeReview");
81
85
  const manualQa = section(gate["manualQa"], "manualQa");
82
86
  const gateReview = section(gate["gateReview"], "gateReview");
83
87
  const iteration = section(gate["iteration"], "iteration");
84
88
  const coverage = section(gate["criteriaCoverage"], "criteriaCoverage");
85
- const codeReview = surface === "lazycodex" ? section(gate["codeReview"], "codeReview") : {};
89
+ const codeReview = gate["codeReview"] !== undefined ? section(gate["codeReview"], "codeReview") : undefined;
86
90
  const manualQaBy = reviewerAcceptorField(manualQa["by"], surface, "manualQa");
87
91
  const gateReviewBy = reviewerAcceptorField(gateReview["by"], surface, "gateReview");
88
92
  const manualQaEvidence = textField(manualQa["evidence"], "manualQa.evidence");
89
93
  const gateReviewEvidence = textField(gateReview["evidence"], "gateReview.evidence");
90
- if (surface === "lazycodex") reviewerRoleField(codeReview?.["by"], "lazycodex-code-reviewer", "codeReview.by");
94
+
91
95
  const totalCriteria = numberField(coverage["totalCriteria"], "criteriaCoverage.totalCriteria");
92
96
  const passCount = numberField(coverage["passCount"], "criteriaCoverage.passCount");
93
97
  if (!isPoisoned("criteriaCoverage.passCount") && passCount < totalCriteria)
@@ -133,13 +137,14 @@ function validateQualityGateUncollected(input: unknown, opts?: ValidateQualityGa
133
137
  },
134
138
  };
135
139
  if (surface === "omo-senpi") return { surface, ...common };
140
+ if (codeReview === undefined) return { surface, ...common };
136
141
  const codeReportPath = textField(codeReview["reportPath"], "codeReview.reportPath");
137
142
  checkFile(codeReportPath, "codeReview.reportPath", opts);
138
143
  return {
139
144
  surface,
140
145
  ...common,
141
146
  codeReview: {
142
- by: "lazycodex-code-reviewer",
147
+ by: reviewerAcceptorField(codeReview["by"], surface, "codeReview"),
143
148
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
144
149
  codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
145
150
  reportPath: codeReportPath,
@@ -1,5 +1,5 @@
1
1
  import { randomBytes } from "node:crypto";
2
- import { existsSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
 
5
5
  import type { PreToolUsePayload } from "./codex-hook.js";
@@ -19,7 +19,7 @@ import type { UlwLoopPlan } from "./types.js";
19
19
  // hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
20
20
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
21
21
  const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
22
- const DEFAULT_FANOUT_LIMIT = 60;
22
+ export const DEFAULT_FANOUT_LIMIT = 24;
23
23
  const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
24
24
  const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
25
25
  const REVIEW_AGENT_TYPES = [
@@ -35,6 +35,11 @@ export interface SpawnGuardOptions {
35
35
 
36
36
  export function applySpawnGuards(payload: PreToolUsePayload, options: SpawnGuardOptions = {}): string {
37
37
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
38
+ const breaker = readAdmissionBreaker(payload.session_id);
39
+ if (breaker !== null)
40
+ return deny(
41
+ `Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`,
42
+ );
38
43
  const scope = { sessionId: payload.session_id } as const;
39
44
  const stateDir = ulwLoopDir(payload.cwd, scope);
40
45
  const plan = readPlan(join(stateDir, "goals.json"));
@@ -59,7 +64,7 @@ function evaluateGuards(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir:
59
64
  if (fanOutPeek !== null) return deny(fanOutPeek);
60
65
  const missingArtifact = missingGateArtifact(payload, plan);
61
66
  if (missingArtifact !== null)
62
- return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
67
+ return deny(`record manual QA first; gate audits its artifacts: missing ${missingArtifact}`);
63
68
  const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
64
69
  if (reviewDenial !== null) return deny(reviewDenial);
65
70
  const fanOutDenial = consumeFanOutBudget(stateDir);
@@ -67,6 +72,34 @@ function evaluateGuards(payload: PreToolUsePayload, plan: UlwLoopPlan, stateDir:
67
72
  return "";
68
73
  }
69
74
 
75
+ export async function runSpawnAdmissionRecorderCli(
76
+ stdin: NodeJS.ReadableStream,
77
+ stdout: NodeJS.WritableStream,
78
+ ): Promise<void> {
79
+ const chunks: Buffer[] = [];
80
+ for await (const chunk of stdin) chunks.push(Buffer.from(chunk));
81
+ try {
82
+ const payload = JSON.parse(Buffer.concat(chunks).toString("utf8")) as Record<string, unknown>;
83
+ const response =
84
+ typeof payload["tool_response"] === "string"
85
+ ? payload["tool_response"]
86
+ : JSON.stringify(payload["tool_response"] ?? "");
87
+ if (!/too many active cells|AgentLimitReached|max_threads|max_concurrent_threads_per_session/i.test(response))
88
+ return;
89
+ const dataDir = process.env["PLUGIN_DATA"];
90
+ if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string") return;
91
+ const markerDir = join(dataDir, "spawn-breaker");
92
+ mkdirSync(markerDir, { recursive: true });
93
+ atomicWriteJson(join(markerDir, `${payload["session_id"]}.json`), {
94
+ reason: response,
95
+ at: new Date().toISOString(),
96
+ });
97
+ } catch {
98
+ /* malformed hook input is ignored */
99
+ }
100
+ void stdout;
101
+ }
102
+
70
103
  export async function runSpawnGuardCli(stdin: NodeJS.ReadableStream, stdout: NodeJS.WritableStream): Promise<void> {
71
104
  try {
72
105
  const chunks: Buffer[] = [];
@@ -128,9 +161,7 @@ function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): str
128
161
  if (goal === undefined || goal.status === "complete") return null;
129
162
  if (!goal.successCriteria.every((criterion) => criterion.status === "pass")) return null;
130
163
  const scope = { sessionId: payload.session_id } as const;
131
- const surface = resolveToolkitSurface();
132
- const requiredArtifacts =
133
- surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
164
+ const requiredArtifacts = [`${goal.id}-manual-qa.md`];
134
165
  if (plan.evidenceLayoutVersion === 2) {
135
166
  const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
136
167
  for (const name of requiredArtifacts) {
@@ -139,16 +170,8 @@ function missingGateArtifact(payload: PreToolUsePayload, plan: UlwLoopPlan): str
139
170
  }
140
171
  return null;
141
172
  }
142
- const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
143
- if (surface !== "omo-senpi" && !isNonEmptyFile(join(payload.cwd, flatReport))) return flatReport;
144
- if (surface === "omo-senpi") {
145
- const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
146
- return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
147
- }
148
- // v1 manual-QA approximation: any other non-empty evidence file counts.
149
- if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
150
- return `.omo/evidence/${goal.id}-manual-qa.md`;
151
- return null;
173
+ const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
174
+ return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
152
175
  }
153
176
 
154
177
  function isGateReviewerSpawn(toolInput: unknown): boolean {
@@ -216,6 +239,19 @@ function deny(reason: string): string {
216
239
  })}\n`;
217
240
  }
218
241
 
242
+ function readAdmissionBreaker(sessionId: string): string | null {
243
+ const dataDir = process.env["PLUGIN_DATA"];
244
+ if (typeof dataDir !== "string") return null;
245
+ try {
246
+ const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8")) as {
247
+ reason?: unknown;
248
+ };
249
+ return typeof value.reason === "string" ? value.reason : "capacity limit";
250
+ } catch {
251
+ return null;
252
+ }
253
+ }
254
+
219
255
  function fanOutLimit(): number {
220
256
  const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
221
257
  if (raw === undefined) return DEFAULT_FANOUT_LIMIT;
@@ -239,15 +275,6 @@ function isNonEmptyFile(path: string): boolean {
239
275
  }
240
276
  }
241
277
 
242
- function hasOtherEvidenceFile(evidenceDir: string, excludedName: string): boolean {
243
- try {
244
- return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
245
- } catch (error) {
246
- if (error instanceof Error) return false;
247
- throw error;
248
- }
249
- }
250
-
251
278
  function readCount(counterPath: string): number {
252
279
  try {
253
280
  const parsed = JSON.parse(readFileSync(counterPath, "utf8")) as Record<string, unknown>;
@@ -28,9 +28,9 @@ function activeGoalActions(plan: UlwLoopPlan, active: UlwLoopItem): readonly str
28
28
  ];
29
29
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
30
30
  return [
31
- `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
31
+ `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate. Use status --json's currentAttemptDir for all quality-gate artifacts.`,
32
32
  ];
33
33
  return [
34
- `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`.`,
34
+ `${active.id} passes every criterion: close it with \`omo-agent-toolkit ulw-loop checkpoint --goal-id ${active.id} --status complete --evidence "<proof>" --codex-goal-json '<get_goal json>'\`. Put quality-gate artifacts under the currentAttemptDir shown by status --json.`,
35
35
  ];
36
36
  }
@@ -36,17 +36,29 @@ export type UlwLoopGateSection = "codeReview" | "manualQa" | "gateReview" | "ite
36
36
 
37
37
  export const REQUIRED_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>> =
38
38
  {
39
- lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
39
+ lazycodex: ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
40
40
  "omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
41
41
  };
42
42
 
43
+ export const OPTIONAL_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>> =
44
+ {
45
+ lazycodex: ["codeReview"],
46
+ "omo-senpi": [],
47
+ };
48
+
43
49
  export const GATE_SECTION_BY_ACCEPTOR: Readonly<
44
50
  Record<UlwLoopToolkitSurface, Readonly<Partial<Record<UlwLoopGateSection, readonly string[]>>>>
45
51
  > = {
46
52
  lazycodex: {
47
- codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
48
- manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
49
- gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview],
53
+ codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview, "main-session"],
54
+ manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa, "main-session"],
55
+ gateReview: [
56
+ REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview,
57
+ "category:deep",
58
+ "category:unspecified-high",
59
+ "category:unspecified-low",
60
+ "main-session",
61
+ ],
50
62
  },
51
63
  "omo-senpi": {
52
64
  manualQa: ["main-session"],
@@ -47,7 +47,7 @@ describe("checkpointUlwLoop final story surface resolution", () => {
47
47
  expect(result.plan.aggregateCompletion?.status).toBe("complete");
48
48
  });
49
49
 
50
- it("#given the same codeReview-free gate #when the surface is lazycodex #then the final story is rejected", async () => {
50
+ it("#given the same codeReview-free gate #when the surface is lazycodex #then the final story completes", async () => {
51
51
  const repo = await repoWith(
52
52
  plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
53
53
  );
@@ -59,17 +59,16 @@ describe("checkpointUlwLoop final story surface resolution", () => {
59
59
  requiredSection(gate, "manualQa")["by"] = "main-session";
60
60
  requiredSection(gate, "gateReview")["by"] = "category:deep";
61
61
 
62
- await expectCode(
63
- () =>
64
- checkpointUlwLoop(repo, {
65
- goalId: "G002",
66
- status: "complete",
67
- evidence: "final work complete and validation passed",
68
- codexGoalJson: snapshot("complete"),
69
- qualityGateJson: JSON.stringify(gate),
70
- }),
71
- "ULW_LOOP_QUALITY_GATE_INVALID",
72
- );
62
+ const result = await checkpointUlwLoop(repo, {
63
+ goalId: "G002",
64
+ status: "complete",
65
+ evidence: "final work complete and validation passed",
66
+ codexGoalJson: snapshot("complete"),
67
+ qualityGateJson: JSON.stringify(gate),
68
+ });
69
+
70
+ expect(result.aggregateCompletion?.status).toBe("complete");
71
+ expect(result.plan.aggregateCompletion?.status).toBe("complete");
73
72
  });
74
73
 
75
74
  it("#given the omo-senpi surface #when the gate includes codeReview #then the final story is rejected", async () => {
@@ -0,0 +1,98 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { ULW_LOOP_HELP } from "../src/cli-output.js";
4
+ import { validateQualityGate } from "../src/quality-gate.js";
5
+ import { statusNextActions } from "../src/status-next-actions.js";
6
+ import type { UlwLoopPlan } from "../src/types.js";
7
+
8
+ const NOW = "2026-05-23T00:00:00.000Z";
9
+
10
+ const GATE_WITHOUT_MANUAL_QA = {
11
+ codeReview: {
12
+ by: "lazycodex-code-reviewer",
13
+ recommendation: "APPROVE",
14
+ codeQualityStatus: "CLEAR",
15
+ reportPath: "code-review.md",
16
+ evidence: "Reviewed the diff; no blocking issues remain.",
17
+ blockers: [],
18
+ },
19
+ gateReview: {
20
+ by: "lazycodex-gate-reviewer",
21
+ recommendation: "APPROVE",
22
+ reportPath: "gate-review.md",
23
+ evidence: "Gate is approved.",
24
+ blockers: [],
25
+ },
26
+ iteration: {
27
+ fullRerun: true,
28
+ status: "passed",
29
+ rerunCommands: ["true"],
30
+ evidence: "Rerun passed.",
31
+ },
32
+ criteriaCoverage: {
33
+ totalCriteria: 1,
34
+ passCount: 1,
35
+ originalIntent: "Ship a complete gate.",
36
+ desiredOutcome: "The gate names accepted input forms.",
37
+ userOutcomeReview: "Outcome matches the request.",
38
+ adversarialClassesCovered: ["malformed_input"],
39
+ },
40
+ };
41
+
42
+ function thrownMessage(run: () => unknown): string {
43
+ try {
44
+ run();
45
+ } catch (error) {
46
+ return error instanceof Error ? error.message : String(error);
47
+ }
48
+ return "";
49
+ }
50
+
51
+ describe("quality-gate field and status messages", () => {
52
+ it("missing-manualQa error contains accepted input and names bare-object and top-level qualityGate forms", () => {
53
+ const message = thrownMessage(() => validateQualityGate(GATE_WITHOUT_MANUAL_QA));
54
+
55
+ expect(message).toContain("accepted input");
56
+ expect(message).toContain("bare object");
57
+ expect(message).toContain('top-level "qualityGate" key');
58
+ });
59
+
60
+ it("status next-action text mentions currentAttemptDir", () => {
61
+ const plan: UlwLoopPlan = {
62
+ version: 1,
63
+ createdAt: NOW,
64
+ updatedAt: NOW,
65
+ briefPath: ".omo/ulw-loop/brief.md",
66
+ goalsPath: ".omo/ulw-loop/goals.json",
67
+ ledgerPath: ".omo/ulw-loop/ledger.jsonl",
68
+ activeGoalId: "G001",
69
+ goals: [
70
+ {
71
+ id: "G001",
72
+ title: "Goal one",
73
+ objective: "Complete goal one",
74
+ status: "in_progress",
75
+ successCriteria: [
76
+ {
77
+ id: "C001",
78
+ scenario: "works",
79
+ userModel: "happy",
80
+ expectedEvidence: "proof",
81
+ capturedEvidence: "passed",
82
+ status: "pass",
83
+ },
84
+ ],
85
+ attempt: 1,
86
+ createdAt: NOW,
87
+ updatedAt: NOW,
88
+ },
89
+ ],
90
+ };
91
+
92
+ expect(statusNextActions(plan).join("\n")).toContain("currentAttemptDir");
93
+ });
94
+
95
+ it("CLI help mentions currentAttemptDir", () => {
96
+ expect(ULW_LOOP_HELP).toContain("currentAttemptDir");
97
+ });
98
+ });