lazycodex-ai 5.0.0-beta.48 → 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 (118) 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/package.json +1 -1
  9. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +2 -2
  10. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  11. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  12. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  13. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  14. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  15. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  16. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  17. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  19. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  20. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  21. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  22. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  23. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  24. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  25. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  26. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  27. package/packages/omo-codex/plugin/components/ultrawork/dist/cli.js +11 -10
  28. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  29. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/ultrawork/src/directive-content.ts +1 -1
  31. package/packages/omo-codex/plugin/components/ulw-execute-continuation/AGENTS.md +3 -3
  32. package/packages/omo-codex/plugin/components/ulw-execute-continuation/README.md +3 -3
  33. package/packages/omo-codex/plugin/components/ulw-execute-continuation/directive.md +4 -4
  34. package/packages/omo-codex/plugin/components/ulw-execute-continuation/dist/cli.js +1 -1
  35. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -13
  36. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/codex-hook.ts +1 -1
  38. package/packages/omo-codex/plugin/components/ulw-execute-continuation/src/types.ts +1 -1
  39. package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/cli.test.ts +3 -7
  40. package/packages/omo-codex/plugin/components/ulw-execute-continuation/test/codex-hook.test.ts +16 -0
  41. package/packages/omo-codex/plugin/components/ulw-loop/README.md +2 -2
  42. package/packages/omo-codex/plugin/components/ulw-loop/directive.md +11 -10
  43. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint-template.js +8 -15
  44. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -2
  45. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.d.ts +1 -1
  46. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli-output.js +1 -1
  47. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +88 -67
  48. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +3 -4
  49. package/packages/omo-codex/plugin/components/ulw-loop/dist/domain-types.d.ts +1 -1
  50. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate-fields.js +1 -1
  51. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +13 -16
  52. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.d.ts +2 -0
  53. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +47 -26
  54. package/packages/omo-codex/plugin/components/ulw-loop/dist/status-next-actions.js +2 -2
  55. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +1 -0
  56. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +14 -4
  57. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +17 -4
  58. package/packages/omo-codex/plugin/components/ulw-loop/package.json +2 -2
  59. package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint-template.ts +8 -14
  61. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -2
  62. package/packages/omo-codex/plugin/components/ulw-loop/src/cli-output.ts +1 -1
  63. package/packages/omo-codex/plugin/components/ulw-loop/src/cli.ts +5 -1
  64. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +3 -4
  65. package/packages/omo-codex/plugin/components/ulw-loop/src/domain-types.ts +1 -1
  66. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-fields.ts +4 -1
  67. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +21 -16
  68. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +52 -25
  69. package/packages/omo-codex/plugin/components/ulw-loop/src/status-next-actions.ts +2 -2
  70. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +16 -4
  71. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +11 -12
  72. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-fields-messages.test.ts +98 -0
  73. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-lazycodex-surface.test.ts +93 -0
  74. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +9 -4
  75. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate.test.ts +10 -0
  76. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +70 -7
  77. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +18 -0
  85. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  86. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  87. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  88. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  89. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  90. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  91. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  92. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  93. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  94. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  95. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  96. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  97. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  98. package/packages/omo-codex/plugin/package-lock.json +12 -12
  99. package/packages/omo-codex/plugin/package.json +1 -1
  100. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +61 -33
  101. package/packages/omo-codex/plugin/skills/init-deep/SKILL.md +4 -2
  102. package/packages/omo-codex/plugin/skills/refactor/SKILL.md +4 -2
  103. package/packages/omo-codex/plugin/skills/remove-ai-slops/SKILL.md +4 -2
  104. package/packages/omo-codex/plugin/skills/review-work/SKILL.md +13 -3
  105. package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +11 -10
  106. package/packages/omo-codex/plugin/skills/ulw-execute/SKILL.md +11 -6
  107. package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +24 -0
  108. package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -1
  109. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +4 -2
  110. package/packages/omo-codex/plugin/skills/visual-qa/SKILL.md +4 -2
  111. package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +8 -4
  112. package/packages/omo-codex/plugin/test/component-hook-contract-cases.mjs +62 -0
  113. package/packages/omo-codex/plugin/test/hook-status-message.test.mjs +3 -3
  114. package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +23 -1
  115. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +36 -16
  116. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  117. package/packages/prompts-core/prompts/ultrawork/codex.md +11 -10
  118. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +0 -17
@@ -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
  import { parsePreToolUsePayload } from "./codex-hook.js";
5
5
  import { isFinalRunCompletionCandidate } from "./goal-status.js";
@@ -10,7 +10,7 @@ import { GATE_REVIEWER_AGENT_NAMES, REVIEWER_ROLES_BY_SURFACE, resolveToolkitSur
10
10
  // hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
11
11
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
12
12
  const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
13
- const DEFAULT_FANOUT_LIMIT = 60;
13
+ export const DEFAULT_FANOUT_LIMIT = 24;
14
14
  const DEFAULT_REVIEW_SPAWN_LIMIT = 3;
15
15
  const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
16
16
  const REVIEW_AGENT_TYPES = [
@@ -22,6 +22,9 @@ const REVIEW_AGENT_TYPE_SET = new Set(REVIEW_AGENT_TYPES);
22
22
  export function applySpawnGuards(payload, options = {}) {
23
23
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
24
24
  return "";
25
+ const breaker = readAdmissionBreaker(payload.session_id);
26
+ if (breaker !== null)
27
+ return deny(`Subagent admission failed earlier in this session (${breaker}). Do not spawn more workers or reviewers; report the capacity block and wait for the user.`);
25
28
  const scope = { sessionId: payload.session_id };
26
29
  const stateDir = ulwLoopDir(payload.cwd, scope);
27
30
  const plan = readPlan(join(stateDir, "goals.json"));
@@ -43,7 +46,7 @@ function evaluateGuards(payload, plan, stateDir) {
43
46
  return deny(fanOutPeek);
44
47
  const missingArtifact = missingGateArtifact(payload, plan);
45
48
  if (missingArtifact !== null)
46
- return deny(`spawn code-review + QA first; gate audits their artifacts: missing ${missingArtifact}`);
49
+ return deny(`record manual QA first; gate audits its artifacts: missing ${missingArtifact}`);
47
50
  const reviewDenial = consumeReviewSpawnBudget(payload, plan, stateDir);
48
51
  if (reviewDenial !== null)
49
52
  return deny(reviewDenial);
@@ -52,6 +55,32 @@ function evaluateGuards(payload, plan, stateDir) {
52
55
  return deny(fanOutDenial);
53
56
  return "";
54
57
  }
58
+ export async function runSpawnAdmissionRecorderCli(stdin, stdout) {
59
+ const chunks = [];
60
+ for await (const chunk of stdin)
61
+ chunks.push(Buffer.from(chunk));
62
+ try {
63
+ const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
64
+ const response = typeof payload["tool_response"] === "string"
65
+ ? payload["tool_response"]
66
+ : JSON.stringify(payload["tool_response"] ?? "");
67
+ if (!/too many active cells|AgentLimitReached|max_threads|max_concurrent_threads_per_session/i.test(response))
68
+ return;
69
+ const dataDir = process.env["PLUGIN_DATA"];
70
+ if (typeof dataDir !== "string" || typeof payload["session_id"] !== "string")
71
+ return;
72
+ const markerDir = join(dataDir, "spawn-breaker");
73
+ mkdirSync(markerDir, { recursive: true });
74
+ atomicWriteJson(join(markerDir, `${payload["session_id"]}.json`), {
75
+ reason: response,
76
+ at: new Date().toISOString(),
77
+ });
78
+ }
79
+ catch {
80
+ /* malformed hook input is ignored */
81
+ }
82
+ void stdout;
83
+ }
55
84
  export async function runSpawnGuardCli(stdin, stdout) {
56
85
  try {
57
86
  const chunks = [];
@@ -120,8 +149,7 @@ function missingGateArtifact(payload, plan) {
120
149
  if (!goal.successCriteria.every((criterion) => criterion.status === "pass"))
121
150
  return null;
122
151
  const scope = { sessionId: payload.session_id };
123
- const surface = resolveToolkitSurface();
124
- const requiredArtifacts = surface === "omo-senpi" ? [`${goal.id}-manual-qa.md`] : [`${goal.id}-code-review.md`, `${goal.id}-manual-qa.md`];
152
+ const requiredArtifacts = [`${goal.id}-manual-qa.md`];
125
153
  if (plan.evidenceLayoutVersion === 2) {
126
154
  const attemptDir = ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope);
127
155
  for (const name of requiredArtifacts) {
@@ -131,17 +159,8 @@ function missingGateArtifact(payload, plan) {
131
159
  }
132
160
  return null;
133
161
  }
134
- const flatReport = `.omo/evidence/${goal.id}-code-review.md`;
135
- if (surface !== "omo-senpi" && !isNonEmptyFile(join(payload.cwd, flatReport)))
136
- return flatReport;
137
- if (surface === "omo-senpi") {
138
- const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
139
- return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
140
- }
141
- // v1 manual-QA approximation: any other non-empty evidence file counts.
142
- if (!hasOtherEvidenceFile(join(payload.cwd, ".omo", "evidence"), `${goal.id}-code-review.md`))
143
- return `.omo/evidence/${goal.id}-manual-qa.md`;
144
- return null;
162
+ const manualQa = `.omo/evidence/${goal.id}-manual-qa.md`;
163
+ return isNonEmptyFile(join(payload.cwd, manualQa)) ? null : manualQa;
145
164
  }
146
165
  function isGateReviewerSpawn(toolInput) {
147
166
  const agentType = reviewAgentType(toolInput);
@@ -212,6 +231,18 @@ function deny(reason) {
212
231
  },
213
232
  })}\n`;
214
233
  }
234
+ function readAdmissionBreaker(sessionId) {
235
+ const dataDir = process.env["PLUGIN_DATA"];
236
+ if (typeof dataDir !== "string")
237
+ return null;
238
+ try {
239
+ const value = JSON.parse(readFileSync(join(dataDir, "spawn-breaker", `${sessionId}.json`), "utf8"));
240
+ return typeof value.reason === "string" ? value.reason : "capacity limit";
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
215
246
  function fanOutLimit() {
216
247
  const raw = process.env["OMO_SPAWN_FANOUT_LIMIT"];
217
248
  if (raw === undefined)
@@ -236,16 +267,6 @@ function isNonEmptyFile(path) {
236
267
  throw error;
237
268
  }
238
269
  }
239
- function hasOtherEvidenceFile(evidenceDir, excludedName) {
240
- try {
241
- return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join(evidenceDir, name)));
242
- }
243
- catch (error) {
244
- if (error instanceof Error)
245
- return false;
246
- throw error;
247
- }
248
- }
249
270
  function readCount(counterPath) {
250
271
  try {
251
272
  const parsed = JSON.parse(readFileSync(counterPath, "utf8"));
@@ -25,9 +25,9 @@ function activeGoalActions(plan, active) {
25
25
  ];
26
26
  if (hasAllCriteriaPass(active) && isFinalRunCompletionCandidate(plan, active))
27
27
  return [
28
- `${active.id} passes every criterion and is the final story: update_goal complete, then checkpoint --print-template to build the final quality gate.`,
28
+ `${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.`,
29
29
  ];
30
30
  return [
31
- `${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>'\`.`,
31
+ `${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.`,
32
32
  ];
33
33
  }
@@ -8,6 +8,7 @@ export declare const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSu
8
8
  export declare const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string>;
9
9
  export type UlwLoopGateSection = "codeReview" | "manualQa" | "gateReview" | "iteration" | "criteriaCoverage";
10
10
  export declare const REQUIRED_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>>;
11
+ export declare const OPTIONAL_GATE_SECTIONS_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, readonly UlwLoopGateSection[]>>;
11
12
  export declare const GATE_SECTION_BY_ACCEPTOR: Readonly<Record<UlwLoopToolkitSurface, Readonly<Partial<Record<UlwLoopGateSection, readonly string[]>>>>>;
12
13
  export declare function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles;
13
14
  export declare const SURFACE_MARKER_FILENAME = "surface.json";
@@ -15,14 +15,24 @@ export const REVIEWER_ROLES_BY_SURFACE = {
15
15
  };
16
16
  export const GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
17
17
  export const REQUIRED_GATE_SECTIONS_BY_SURFACE = {
18
- lazycodex: ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"],
18
+ lazycodex: ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
19
19
  "omo-senpi": ["manualQa", "gateReview", "iteration", "criteriaCoverage"],
20
20
  };
21
+ export const OPTIONAL_GATE_SECTIONS_BY_SURFACE = {
22
+ lazycodex: ["codeReview"],
23
+ "omo-senpi": [],
24
+ };
21
25
  export const GATE_SECTION_BY_ACCEPTOR = {
22
26
  lazycodex: {
23
- codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview],
24
- manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa],
25
- gateReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview],
27
+ codeReview: [REVIEWER_ROLES_BY_SURFACE.lazycodex.codeReview, "main-session"],
28
+ manualQa: [REVIEWER_ROLES_BY_SURFACE.lazycodex.manualQa, "main-session"],
29
+ gateReview: [
30
+ REVIEWER_ROLES_BY_SURFACE.lazycodex.gateReview,
31
+ "category:deep",
32
+ "category:unspecified-high",
33
+ "category:unspecified-low",
34
+ "main-session",
35
+ ],
26
36
  },
27
37
  "omo-senpi": {
28
38
  manualQa: ["main-session"],
@@ -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.48) 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.48) 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.48) 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.48) 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.48",
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
  }