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
@@ -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
+ });
@@ -0,0 +1,93 @@
1
+ import { expect, it } from "vitest";
2
+ import { validateQualityGate } from "../src/quality-gate.js";
3
+
4
+ const base = {
5
+ manualQa: {
6
+ by: "main-session",
7
+ status: "passed",
8
+ evidence: "qa",
9
+ surfaceEvidence: [
10
+ { id: "s", criterionRef: "C1", surface: "cli", invocation: "true", verdict: "passed", artifactRefs: ["a"] },
11
+ ],
12
+ adversarialCases: [
13
+ {
14
+ id: "x",
15
+ criterionRef: "C1",
16
+ scenario: "x",
17
+ expectedBehavior: "x",
18
+ verdict: "not_applicable",
19
+ reason: "x",
20
+ artifactRefs: ["a"],
21
+ },
22
+ ],
23
+ artifactRefs: [{ id: "a", kind: "cli-transcript", description: "a", path: "qa.txt" }],
24
+ },
25
+ gateReview: {
26
+ by: "category:unspecified-high",
27
+ recommendation: "APPROVE",
28
+ reportPath: "gate.md",
29
+ evidence: "gate",
30
+ blockers: [],
31
+ },
32
+ iteration: { fullRerun: true, status: "passed", rerunCommands: ["true"], evidence: "rerun" },
33
+ criteriaCoverage: {
34
+ totalCriteria: 1,
35
+ passCount: 1,
36
+ originalIntent: "i",
37
+ desiredOutcome: "d",
38
+ userOutcomeReview: "u",
39
+ adversarialClassesCovered: ["x"],
40
+ },
41
+ };
42
+ it("accepts lazycodex self-review gate without codeReview", () =>
43
+ expect(() => validateQualityGate(base)).not.toThrow());
44
+ it("accepts wrapped and optional codeReview forms", () => {
45
+ expect(() => validateQualityGate({ qualityGate: base })).not.toThrow();
46
+ expect(() =>
47
+ validateQualityGate({
48
+ ...base,
49
+ codeReview: {
50
+ by: "lazycodex-code-reviewer",
51
+ recommendation: "APPROVE",
52
+ codeQualityStatus: "CLEAR",
53
+ reportPath: "code.md",
54
+ evidence: "review",
55
+ blockers: [],
56
+ },
57
+ }),
58
+ ).not.toThrow();
59
+ });
60
+ it("#given main-session codeReview #when validated #then preserves the accepted author", () => {
61
+ const gate = validateQualityGate({
62
+ ...base,
63
+ codeReview: {
64
+ by: "main-session",
65
+ recommendation: "APPROVE",
66
+ codeQualityStatus: "CLEAR",
67
+ reportPath: "code.md",
68
+ evidence: "review",
69
+ blockers: [],
70
+ },
71
+ });
72
+ expect(gate.surface).toBe("lazycodex");
73
+ if (gate.surface === "lazycodex") expect(gate.codeReview?.by).toBe("main-session");
74
+ });
75
+ it("#given random codeReview author #when validated #then rejects the lane", () => {
76
+ expect(() =>
77
+ validateQualityGate({
78
+ ...base,
79
+ codeReview: {
80
+ by: "random",
81
+ recommendation: "APPROVE",
82
+ codeQualityStatus: "CLEAR",
83
+ reportPath: "code.md",
84
+ evidence: "review",
85
+ blockers: [],
86
+ },
87
+ }),
88
+ ).toThrow(/codeReview\.by/);
89
+ });
90
+ it("rejects an unknown gate acceptor", () =>
91
+ expect(() => validateQualityGate({ ...base, gateReview: { ...base.gateReview, by: "random-agent" } })).toThrow(
92
+ /gateReview\.by/,
93
+ ));
@@ -132,10 +132,15 @@ describe("validateQualityGate reviewer roles", () => {
132
132
  expect(error.message).toContain("no codeReview lane");
133
133
  });
134
134
 
135
- it("#given a four-section senpi gate #when validated on the default lazycodex surface #then codeReview is required", () => {
136
- const error = qualityGateError(senpiGate());
137
- expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
138
- expect(error.message).toContain("codeReview");
135
+ it("#given a four-section gate #when validated on the default lazycodex surface #then codeReview is optional", () => {
136
+ expect(() =>
137
+ validateQualityGate(
138
+ senpiGate({
139
+ manualQa: { ...BASE_GATE.manualQa, by: "main-session" },
140
+ gateReview: { ...BASE_GATE.gateReview, by: "category:deep" },
141
+ }),
142
+ ),
143
+ ).not.toThrow();
139
144
  });
140
145
 
141
146
  it("#given swapped omo-senpi reviewer roles #when validated on the omo-senpi surface #then section-specific roles are enforced", () => {
@@ -136,6 +136,16 @@ describe("validateQualityGate", () => {
136
136
  expect(gate.manualQa.artifactRefs).toHaveLength(5);
137
137
  });
138
138
 
139
+ it("#given missing manualQa section #when validated #then it explains accepted input forms", () => {
140
+ const input = makeGate();
141
+ delete input["manualQa"];
142
+
143
+ const error = getQualityGateError(input);
144
+
145
+ expect(error.message).toContain("accepted input");
146
+ expect(error.message).toContain('top-level "qualityGate" key');
147
+ });
148
+
139
149
  it("#given missing manualQa surface evidence #when validated #then it fails closed", () => {
140
150
  // given
141
151
  const input = makeGate({
@@ -2,18 +2,22 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { mkdtemp, rm } from "node:fs/promises";
3
3
  import { tmpdir } from "node:os";
4
4
  import { join } from "node:path";
5
+ import { Readable, Writable } from "node:stream";
5
6
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
7
 
7
8
  import type { PreToolUsePayload } from "../src/codex-hook.ts";
8
- import { applySpawnGuards } from "../src/spawn-guard.ts";
9
+ import { applySpawnGuards, DEFAULT_FANOUT_LIMIT, runSpawnAdmissionRecorderCli } from "../src/spawn-guard.ts";
9
10
 
10
11
  let workDir: string;
11
12
  let originalLimit: string | undefined;
12
13
  let originalReviewLimit: string | undefined;
13
14
  let originalToolkitSurface: string | undefined;
15
+ let originalPluginData: string | undefined;
14
16
 
15
17
  beforeEach(async () => {
16
18
  workDir = await mkdtemp(join(tmpdir(), "ulw-spawn-guard-"));
19
+ originalPluginData = process.env["PLUGIN_DATA"];
20
+ process.env["PLUGIN_DATA"] = join(workDir, "plugin-data");
17
21
  originalLimit = process.env["OMO_SPAWN_FANOUT_LIMIT"];
18
22
  originalReviewLimit = process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"];
19
23
  originalToolkitSurface = process.env["OMO_AGENT_TOOLKIT_SURFACE"];
@@ -30,6 +34,8 @@ afterEach(async () => {
30
34
  else process.env["OMO_ULW_LOOP_REVIEW_SPAWN_LIMIT"] = originalReviewLimit;
31
35
  if (originalToolkitSurface === undefined) delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
32
36
  else process.env["OMO_AGENT_TOOLKIT_SURFACE"] = originalToolkitSurface;
37
+ if (originalPluginData === undefined) delete process.env["PLUGIN_DATA"];
38
+ else process.env["PLUGIN_DATA"] = originalPluginData;
33
39
  await rm(workDir, { recursive: true, force: true });
34
40
  });
35
41
 
@@ -98,7 +104,66 @@ function deny(output: string): { permissionDecision: string; permissionDecisionR
98
104
  return JSON.parse(output).hookSpecificOutput;
99
105
  }
100
106
 
107
+ describe("spawn admission breaker", () => {
108
+ async function record(response: unknown): Promise<string> {
109
+ let output = "";
110
+ const stdout = new Writable({
111
+ write(chunk, _encoding, callback) {
112
+ output += chunk.toString();
113
+ callback();
114
+ },
115
+ });
116
+ await runSpawnAdmissionRecorderCli(
117
+ Readable.from([
118
+ JSON.stringify({
119
+ ...payload("spawn_agent", { message: "scan" }),
120
+ hook_event_name: "PostToolUse",
121
+ tool_response: response,
122
+ }),
123
+ ]),
124
+ stdout,
125
+ );
126
+ return output;
127
+ }
128
+
129
+ it.each(["too many active cells", "AgentLimitReached", "max_threads", "max_concurrent_threads_per_session"])(
130
+ "records %s silently and denies without a plan while isolating other sessions",
131
+ async (reason) => {
132
+ expect(await record(reason)).toBe("");
133
+ const marker = JSON.parse(readFileSync(join(workDir, "plugin-data", "spawn-breaker", "s1.json"), "utf8"));
134
+ expect(Object.keys(marker).sort()).toEqual(["at", "reason"]);
135
+ expect(marker.reason).toBe(reason);
136
+ expect(Number.isFinite(Date.parse(marker.at))).toBe(true);
137
+ expect(deny(applySpawnGuards(payload("spawn_agent", {}))).permissionDecision).toBe("deny");
138
+ expect(applySpawnGuards({ ...payload("spawn_agent", {}), session_id: "clean" })).toBe("");
139
+ },
140
+ );
141
+
142
+ it("does not record a successful admission", async () => {
143
+ expect(await record({ agent_id: "worker-1" })).toBe("");
144
+ expect(existsSync(join(workDir, "plugin-data", "spawn-breaker", "s1.json"))).toBe(false);
145
+ });
146
+
147
+ it("denies before artifact, quota, and state lock guards", async () => {
148
+ writeGoals();
149
+ writeFileSync(
150
+ join(sessionDir(), ".state.lock"),
151
+ JSON.stringify({ pid: process.pid, createdAt: new Date().toISOString(), token: "live" }),
152
+ );
153
+ await record("AgentLimitReached");
154
+ const output = deny(
155
+ applySpawnGuards(payload("spawn_agent", { agent_type: "lazycodex-gate-reviewer" }), { lockTimeoutMs: 0 }),
156
+ );
157
+ expect(output.permissionDecisionReason).toContain("AgentLimitReached");
158
+ expect(existsSync(join(sessionDir(), "spawn-count.json"))).toBe(false);
159
+ expect(existsSync(join(sessionDir(), "review-spawn-counts.json"))).toBe(false);
160
+ });
161
+ });
162
+
101
163
  describe("applySpawnGuards fan-out cap", () => {
164
+ it("defaults to 24 spawns", () => {
165
+ expect(DEFAULT_FANOUT_LIMIT).toBe(24);
166
+ });
102
167
  it("#given spawns under the limit #when guarded #then allows and counts", () => {
103
168
  writeGoals();
104
169
 
@@ -251,7 +316,7 @@ describe("applySpawnGuards gate-artifact guard", () => {
251
316
  );
252
317
 
253
318
  expect(output).not.toBe("");
254
- expect(deny(output).permissionDecisionReason).toContain("g1-code-review.md");
319
+ expect(deny(output).permissionDecisionReason).toContain("g1-manual-qa.md");
255
320
  expect(existsSync(join(sessionDir(), "review-spawn-counts.json"))).toBe(false);
256
321
  });
257
322
 
@@ -265,7 +330,7 @@ describe("applySpawnGuards gate-artifact guard", () => {
265
330
  const parsed = deny(output);
266
331
  expect(parsed.permissionDecision).toBe("deny");
267
332
  expect(parsed.permissionDecisionReason).toContain("missing");
268
- expect(parsed.permissionDecisionReason).toContain("g1-code-review.md");
333
+ expect(parsed.permissionDecisionReason).toContain("g1-manual-qa.md");
269
334
  });
270
335
 
271
336
  it("#given senpi main-session QA is present but code review is absent #when the gate reviewer spawns #then the self-check allows it", () => {
@@ -366,10 +431,9 @@ describe("applySpawnGuards gate-artifact guard", () => {
366
431
  expect(counters["lazycodex-code-reviewer:g1:a1"]).toBeUndefined();
367
432
  });
368
433
 
369
- it("#given v1 artifacts on disk #when the gate spawns #then allows", () => {
434
+ it("#given only v1 manual QA on disk #when the gate spawns #then allows", () => {
370
435
  writeGoals();
371
436
  mkdirSync(join(workDir, ".omo", "evidence"), { recursive: true });
372
- writeFileSync(join(workDir, ".omo", "evidence", "g1-code-review.md"), "report\n");
373
437
  writeFileSync(join(workDir, ".omo", "evidence", "g1-manual-qa.md"), "matrix\n");
374
438
 
375
439
  const output = applySpawnGuards(
@@ -388,14 +452,13 @@ describe("applySpawnGuards gate-artifact guard", () => {
388
452
  payload("spawn_agent", { agent_type: "lazycodex-gate-reviewer", message: "final gate review" }),
389
453
  );
390
454
 
391
- expect(deny(output).permissionDecisionReason).toContain(".omo/evidence/ulw/s1/g1/a1/g1-code-review.md");
455
+ expect(deny(output).permissionDecisionReason).toContain(".omo/evidence/ulw/s1/g1/a1/g1-manual-qa.md");
392
456
  });
393
457
 
394
458
  it("#given a v2 plan with attempt-dir artifacts #when the gate spawns #then allows", () => {
395
459
  writeGoals({ evidenceLayoutVersion: 2 });
396
460
  const attemptDir = join(workDir, ".omo", "evidence", "ulw", "s1", "g1", "a1");
397
461
  mkdirSync(attemptDir, { recursive: true });
398
- writeFileSync(join(attemptDir, "g1-code-review.md"), "report\n");
399
462
  writeFileSync(join(attemptDir, "g1-manual-qa.md"), "matrix\n");
400
463
 
401
464
  const output = applySpawnGuards(
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Resetting Git Bash MCP Reminder",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Resetting Git Bash MCP Reminder",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\git-bash\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-compact",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Resetting LSP Diagnostics Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Resetting LSP Diagnostics Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lsp\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook post-compact",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Resetting Project Rule Cache",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Resetting Project Rule Cache",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook post-compact"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/comment-checker/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 30,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Comments",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Comments",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\comment-checker\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lsp/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 60,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Checking LSP Diagnostics",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking LSP Diagnostics",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lsp\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/teammode/dist/cli.js\" hook post-tool-use",
10
10
  "timeout": 10,
11
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Thread Title Hygiene",
11
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Thread Title Hygiene",
12
12
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\teammode\\dist\\cli.js\" hook post-tool-use"
13
13
  }
14
14
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook post-tool-use",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Matching Project Rules",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Matching Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook post-tool-use"
12
12
  }
13
13
  ],
@@ -0,0 +1,18 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "^(spawn_agent|collaborationspawn_agent|collaboration\\.spawn_agent)$",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook post-tool-use-spawn",
10
+ "timeout": 5,
11
+ "statusMessage": "(OmO 5.0.0-beta.49) Recording Spawn Admission",
12
+ "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook post-tool-use-spawn"
13
+ }
14
+ ]
15
+ }
16
+ ]
17
+ }
18
+ }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook pre-tool-use",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Enforcing Unlimited Goal Budget",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Enforcing Unlimited Goal Budget",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook pre-tool-use"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "type": "command",
9
9
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook pre-tool-use-spawn",
10
10
  "timeout": 5,
11
- "statusMessage": "(OmO 5.0.0-beta.48) Guarding Ulw-Loop Spawns",
11
+ "statusMessage": "(OmO 5.0.0-beta.49) Guarding Ulw-Loop Spawns",
12
12
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook pre-tool-use-spawn"
13
13
  }
14
14
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook pre-tool-use",
9
9
  "timeout": 5,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Recommending Git Bash MCP",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Recommending Git Bash MCP",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\git-bash\\dist\\cli.js\" hook pre-tool-use"
12
12
  }
13
13
  ],
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/scripts/auto-update.mjs\" hook session-start",
9
9
  "timeout": 15,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Auto Update",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Auto Update",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\scripts\\auto-update.mjs\" hook session-start"
12
12
  }
13
13
  ],
@@ -8,7 +8,7 @@
8
8
  "command": "node \"${PLUGIN_ROOT}/components/bootstrap/dist/cli.js\" hook session-start",
9
9
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\bootstrap.ps1\"",
10
10
  "timeout": 30,
11
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Bootstrap Provisioning"
11
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Bootstrap Provisioning"
12
12
  }
13
13
  ]
14
14
  }
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/rules/dist/cli.js\" hook session-start",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Loading Project Rules",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Loading Project Rules",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\rules\\dist\\cli.js\" hook session-start"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/telemetry/dist/cli.js\" hook session-start",
9
9
  "timeout": 15,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Recording Session Telemetry",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Recording Session Telemetry",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\telemetry\\dist\\cli.js\" hook session-start"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-execute-continuation/dist/cli.js\" hook stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Ulw-Execute Continuation",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Execute Continuation",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-execute-continuation\\dist\\cli.js\" hook stop"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/ulw-loop/dist/cli.js\" hook stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Checking Ulw-Loop Resume",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Checking Ulw-Loop Resume",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\ulw-loop\\dist\\cli.js\" hook stop"
12
12
  }
13
13
  ]
@@ -7,7 +7,7 @@
7
7
  "type": "command",
8
8
  "command": "node \"${PLUGIN_ROOT}/components/lazycodex-executor-verify/dist/cli.js\" hook subagent-stop",
9
9
  "timeout": 10,
10
- "statusMessage": "(OmO 5.0.0-beta.48) Verifying LazyCodex Executor Evidence",
10
+ "statusMessage": "(OmO 5.0.0-beta.49) Verifying LazyCodex Executor Evidence",
11
11
  "commandWindows": "powershell -NoProfile -ExecutionPolicy Bypass -File \"${PLUGIN_ROOT}\\components\\bootstrap\\scripts\\node-dispatch.ps1\" \"${PLUGIN_ROOT}\\components\\lazycodex-executor-verify\\dist\\cli.js\" hook subagent-stop"
12
12
  }
13
13
  ],