oh-my-opencode 5.0.0-beta.30 → 5.0.0-beta.31

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 (134) hide show
  1. package/dist/cli/index.js +75 -26
  2. package/dist/cli-node/index.js +75 -26
  3. package/dist/config/schema/background-task.d.ts +1 -0
  4. package/dist/config/schema/oh-my-opencode-config.d.ts +1 -0
  5. package/dist/features/background-agent/constants.d.ts +0 -7
  6. package/dist/features/background-agent/manager.d.ts +2 -1
  7. package/dist/features/background-agent/parent-wake-dispatched-tracker.d.ts +0 -1
  8. package/dist/features/background-agent/subagent-spawn-limits.d.ts +8 -0
  9. package/dist/features/task-toast-manager/manager.d.ts +0 -24
  10. package/dist/hooks/anthropic-context-window-limit-recovery/message-storage-directory.d.ts +0 -3
  11. package/dist/hooks/anthropic-context-window-limit-recovery/state.d.ts +1 -0
  12. package/dist/hooks/anthropic-context-window-limit-recovery/storage/empty-messages.d.ts +0 -1
  13. package/dist/hooks/claude-code-hooks/session-hook-state.d.ts +1 -0
  14. package/dist/hooks/claude-code-hooks/stop.d.ts +2 -0
  15. package/dist/hooks/claude-code-hooks/todo.d.ts +0 -1
  16. package/dist/hooks/claude-code-hooks/transcript.d.ts +2 -0
  17. package/dist/hooks/fsync-skip-warning/index.d.ts +4 -0
  18. package/dist/hooks/hashline-edit-diff-enhancer/hook.d.ts +1 -0
  19. package/dist/hooks/hashline-edit-diff-enhancer/pending-captures.d.ts +13 -0
  20. package/dist/hooks/keyword-detector/hook.d.ts +7 -0
  21. package/dist/hooks/keyword-detector/ultrawork/source-detector.d.ts +0 -1
  22. package/dist/hooks/model-fallback/hook.d.ts +1 -1
  23. package/dist/index.js +439 -218
  24. package/dist/oh-my-opencode.schema.json +5 -0
  25. package/dist/plugin-dispose.d.ts +3 -0
  26. package/dist/shared/agent-tool-restrictions.d.ts +0 -1
  27. package/dist/tools/delegate-task/sync-session-cleanup.d.ts +3 -0
  28. package/dist/tools/delegate-task/sync-session-poller.d.ts +1 -0
  29. package/dist/tools/delegate-task/types.d.ts +1 -0
  30. package/dist/tui.js +56 -3
  31. package/package.json +15 -15
  32. package/packages/git-bash-mcp/dist/cli.js +1 -0
  33. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +97 -0
  34. package/packages/lsp-core/src/lsp/client-wrapper.ts +21 -6
  35. package/packages/lsp-core/src/lsp/constants.ts +5 -0
  36. package/packages/lsp-core/src/lsp/errors.ts +15 -0
  37. package/packages/lsp-core/src/lsp/manager-lifecycle.test.ts +262 -0
  38. package/packages/lsp-core/src/lsp/manager.ts +106 -13
  39. package/packages/lsp-core/src/lsp/workspace-markers.ts +18 -1
  40. package/packages/lsp-core/src/mcp.ts +3 -0
  41. package/packages/lsp-daemon/dist/cli.js +114 -18
  42. package/packages/lsp-daemon/dist/client.js +114 -18
  43. package/packages/lsp-daemon/dist/index.js +114 -18
  44. package/packages/lsp-daemon/dist/proxy.js +3 -0
  45. package/packages/lsp-tools-mcp/dist/cli.js +114 -18
  46. package/packages/lsp-tools-mcp/dist/lsp/manager.js +93 -12
  47. package/packages/lsp-tools-mcp/dist/mcp.js +114 -18
  48. package/packages/lsp-tools-mcp/dist/tools.js +113 -18
  49. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  50. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  51. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +31 -2
  53. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +31 -2
  54. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  55. package/packages/omo-codex/plugin/components/codegraph/src/mcp-unavailable.ts +12 -0
  56. package/packages/omo-codex/plugin/components/codegraph/test/serve-unavailable-idle.test.ts +47 -0
  57. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  58. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  59. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  60. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  61. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  62. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  63. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  64. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +113 -18
  65. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  66. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  67. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  68. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  69. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  70. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  71. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  72. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  73. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  74. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  75. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +2 -2
  76. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  77. package/packages/omo-codex/plugin/components/ulw-loop/AGENTS.md +1 -0
  78. package/packages/omo-codex/plugin/components/ulw-loop/dist/checkpoint.js +2 -0
  79. package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +100 -54
  80. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.d.ts +2 -0
  81. package/packages/omo-codex/plugin/components/ulw-loop/dist/codex-goal-instruction.js +9 -6
  82. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.d.ts +4 -2
  83. package/packages/omo-codex/plugin/components/ulw-loop/dist/quality-gate.js +7 -10
  84. package/packages/omo-codex/plugin/components/ulw-loop/dist/spawn-guard.js +3 -2
  85. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.d.ts +15 -0
  86. package/packages/omo-codex/plugin/components/ulw-loop/dist/surface.js +50 -0
  87. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  88. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  89. package/packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts +2 -0
  90. package/packages/omo-codex/plugin/components/ulw-loop/src/codex-goal-instruction.ts +16 -5
  91. package/packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts +11 -14
  92. package/packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts +3 -2
  93. package/packages/omo-codex/plugin/components/ulw-loop/src/surface.ts +73 -0
  94. package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-final.test.ts +44 -1
  95. package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +25 -0
  96. package/packages/omo-codex/plugin/components/ulw-loop/test/fixtures/quality-gate-builder.ts +7 -3
  97. package/packages/omo-codex/plugin/components/ulw-loop/test/quality-gate-roles.test.ts +45 -2
  98. package/packages/omo-codex/plugin/components/ulw-loop/test/spawn-guard.test.ts +20 -0
  99. package/packages/omo-codex/plugin/components/ulw-loop/test/surface.test.ts +74 -0
  100. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  101. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  102. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  103. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  104. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  105. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  106. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  107. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  108. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  109. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  110. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  111. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  112. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  113. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  114. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  115. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  116. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  117. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  118. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-ulw-execute-continuation.json +1 -1
  119. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  120. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  121. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  122. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  123. package/packages/omo-codex/plugin/package-lock.json +13 -13
  124. package/packages/omo-codex/plugin/package.json +1 -1
  125. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  126. package/dist/hooks/ralph-loop/completion-promise-detector-test-input.d.ts +0 -11
  127. package/dist/hooks/ralph-loop/loop-session-recovery.d.ts +0 -7
  128. package/dist/tools/call-omo-agent/message-storage-directory.d.ts +0 -1
  129. package/dist/tools/delegate-task/model-string-parser.d.ts +0 -1
  130. package/packages/lsp-tools-mcp/dist/lsp/cleanup-errors.d.ts +0 -1
  131. package/packages/lsp-tools-mcp/dist/lsp/constants.d.ts +0 -1
  132. package/packages/lsp-tools-mcp/dist/lsp/language-mappings.d.ts +0 -1
  133. package/packages/lsp-tools-mcp/dist/lsp/process-signal-cleanup.d.ts +0 -1
  134. package/packages/lsp-tools-mcp/dist/missing-dependency-result.d.ts +0 -1
@@ -0,0 +1,50 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ export const REVIEWER_ROLES_BY_SURFACE = {
5
+ lazycodex: {
6
+ codeReview: "lazycodex-code-reviewer",
7
+ manualQa: "lazycodex-qa-executor",
8
+ gateReview: "lazycodex-gate-reviewer",
9
+ },
10
+ "omo-senpi": {
11
+ codeReview: "omo-senpi-code-reviewer",
12
+ manualQa: "omo-senpi-qa-executor",
13
+ gateReview: "omo-senpi-gate-reviewer",
14
+ },
15
+ };
16
+ export const GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
17
+ export function reviewerRolesFor(surface) {
18
+ return REVIEWER_ROLES_BY_SURFACE[surface];
19
+ }
20
+ export const SURFACE_MARKER_FILENAME = "surface.json";
21
+ const SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
22
+ function parseSurface(value) {
23
+ return value === "lazycodex" || value === "omo-senpi" ? value : null;
24
+ }
25
+ // Resolution order: explicit env override (tests, doctor probes) -> staged surface.json marker
26
+ // sitting next to the running bundle -> lazycodex default. The marker is distribution-baked, so a
27
+ // malformed marker falls back to the default instead of crashing checkpoint validation; the env
28
+ // override remains available to force the intended surface on a damaged install.
29
+ export function resolveToolkitSurface(options) {
30
+ const env = options?.env ?? process.env;
31
+ const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
32
+ if (fromEnv !== null)
33
+ return fromEnv;
34
+ const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
35
+ const markerPath = join(entryDir, SURFACE_MARKER_FILENAME);
36
+ return readMarkerSurface(markerPath) ?? "lazycodex";
37
+ }
38
+ function readMarkerSurface(markerPath) {
39
+ try {
40
+ if (!existsSync(markerPath))
41
+ return null;
42
+ const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
43
+ return parseSurface(parsed["surface"]);
44
+ }
45
+ catch (error) {
46
+ if (error instanceof Error)
47
+ return null;
48
+ throw error;
49
+ }
50
+ }
@@ -7,7 +7,7 @@
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.30) Checking Ulw-Loop Steering"
10
+ "statusMessage": "(OmO 5.0.0-beta.31) Checking Ulw-Loop Steering"
11
11
  }
12
12
  ]
13
13
  }
@@ -20,7 +20,7 @@
20
20
  "type": "command",
21
21
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
22
22
  "timeout": 5,
23
- "statusMessage": "(OmO 5.0.0-beta.30) Enforcing Unlimited Ulw-Loop Budget"
23
+ "statusMessage": "(OmO 5.0.0-beta.31) Enforcing Unlimited Ulw-Loop Budget"
24
24
  }
25
25
  ]
26
26
  },
@@ -31,7 +31,7 @@
31
31
  "type": "command",
32
32
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use-spawn",
33
33
  "timeout": 5,
34
- "statusMessage": "(OmO 5.0.0-beta.30) Guarding Ulw-Loop Spawns"
34
+ "statusMessage": "(OmO 5.0.0-beta.31) Guarding Ulw-Loop Spawns"
35
35
  }
36
36
  ]
37
37
  }
@@ -43,7 +43,7 @@
43
43
  "type": "command",
44
44
  "command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook stop",
45
45
  "timeout": 10,
46
- "statusMessage": "(OmO 5.0.0-beta.30) Checking Ulw-Loop Resume"
46
+ "statusMessage": "(OmO 5.0.0-beta.31) Checking Ulw-Loop Resume"
47
47
  }
48
48
  ]
49
49
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@code-yeongyu/codex-ulw-loop",
3
- "version": "5.0.0-beta.30",
3
+ "version": "5.0.0-beta.31",
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",
@@ -28,6 +28,7 @@ import {
28
28
  sameBlockerOccurrences,
29
29
  validateQualityGate,
30
30
  } from "./quality-gate.js";
31
+ import { resolveToolkitSurface } from "./surface.js";
31
32
  import type {
32
33
  UlwLoopAggregateCompletion,
33
34
  UlwLoopItem,
@@ -230,6 +231,7 @@ export async function checkpointUlwLoop(
230
231
  qualityGate = validateQualityGate(await readJsonInput(args.qualityGateJson, repoRoot), {
231
232
  repoRoot,
232
233
  fs: QUALITY_GATE_FS,
234
+ reviewerSurface: resolveToolkitSurface(),
233
235
  ...(plan.evidenceLayoutVersion === 2
234
236
  ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) }
235
237
  : {}),
@@ -5,6 +5,7 @@ import {
5
5
  isEssentialCriterion,
6
6
  isFinalRunCompletionCandidate,
7
7
  } from "./goal-status.js";
8
+ import { resolveToolkitSurface, reviewerRolesFor, type UlwLoopToolkitSurface } from "./surface.js";
8
9
  import type { UlwLoopCodexGoalMode, UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "./types.js";
9
10
 
10
11
  export interface CodexCreateGoalPayload {
@@ -20,11 +21,13 @@ export function buildCodexGoalInstruction(args: {
20
21
  readonly plan: UlwLoopPlan;
21
22
  readonly goal: UlwLoopItem;
22
23
  readonly isFinal?: boolean;
24
+ readonly surface?: UlwLoopToolkitSurface;
23
25
  }): UlwLoopGoalInstruction {
24
26
  const mode = codexGoalMode(args.plan);
25
27
  const createGoal = buildCreateGoalPayload(args.plan, args.goal);
26
28
  const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
27
- return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal };
29
+ const surface = args.surface ?? resolveToolkitSurface();
30
+ return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal, surface), json: createGoal };
28
31
  }
29
32
 
30
33
  function buildCreateGoalPayload(plan: UlwLoopPlan, goal: UlwLoopItem): CodexCreateGoalPayload {
@@ -37,6 +40,7 @@ function buildText(
37
40
  goal: UlwLoopItem,
38
41
  createGoal: CodexCreateGoalPayload,
39
42
  isFinal: boolean,
43
+ surface: UlwLoopToolkitSurface,
40
44
  ): string {
41
45
  return joinLines([
42
46
  mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
@@ -54,7 +58,7 @@ function buildText(
54
58
  "- Goals are unlimited. Do not add numeric limits.",
55
59
  ...modeConstraintLines(mode, isFinal),
56
60
  ...evidenceLayoutLines(plan),
57
- finalSection(plan, goal, isFinal, mode === "aggregate"),
61
+ finalSection(plan, goal, isFinal, mode === "aggregate", surface),
58
62
  ...checkpointLines(plan, mode),
59
63
  "",
60
64
  "create_goal payload:",
@@ -112,7 +116,14 @@ function evidenceLayoutLines(plan: UlwLoopPlan): string[] {
112
116
  ];
113
117
  }
114
118
 
115
- function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, aggregate: boolean): string {
119
+ function finalSection(
120
+ plan: UlwLoopPlan,
121
+ goal: UlwLoopItem,
122
+ isFinal: boolean,
123
+ aggregate: boolean,
124
+ surface: UlwLoopToolkitSurface,
125
+ ): string {
126
+ const roles = reviewerRolesFor(surface);
116
127
  if (!isFinal)
117
128
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
118
129
  const option = sessionOption(plan);
@@ -122,8 +133,8 @@ function finalSection(plan: UlwLoopPlan, goal: UlwLoopItem, isFinal: boolean, ag
122
133
  "Final story — run mandatory quality gate before update_goal:",
123
134
  "- Run targeted verification for changed behavior.",
124
135
  "- Confirm every manualQa artifact path exists and has non-zero size.",
125
- "- First spawn lazycodex-code-reviewer and lazycodex-qa-executor 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).",
126
- "- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
136
+ `- 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).`,
137
+ `- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
127
138
  "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
128
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.",
129
140
  "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
@@ -10,6 +10,7 @@ import {
10
10
  textField,
11
11
  } from "./quality-gate-fields.js";
12
12
  import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
13
+ import { reviewerRolesFor, type UlwLoopToolkitSurface } from "./surface.js";
13
14
  import type {
14
15
  UlwLoopManualQaArtifactKind,
15
16
  UlwLoopManualQaArtifactRef,
@@ -17,12 +18,6 @@ import type {
17
18
  UlwLoopQualityGate,
18
19
  } from "./types.js";
19
20
 
20
- const REVIEWER_ROLES = {
21
- codeReview: "lazycodex-code-reviewer",
22
- manualQa: "lazycodex-qa-executor",
23
- gateReview: "lazycodex-gate-reviewer",
24
- } as const;
25
-
26
21
  export {
27
22
  classifyExternalAuthorizationBlocker,
28
23
  clearGoalBlockerFields,
@@ -36,12 +31,13 @@ export interface QualityGateFs {
36
31
  }
37
32
 
38
33
  export interface ValidateQualityGateOptions {
39
- readonly repoRoot: string;
40
- readonly fs: QualityGateFs;
34
+ readonly repoRoot?: string;
35
+ readonly fs?: QualityGateFs;
41
36
  readonly currentAttemptDir?: string;
37
+ readonly reviewerSurface?: UlwLoopToolkitSurface;
42
38
  }
43
39
 
44
- function reviewerRoleField<T extends string>(value: unknown, expected: T, field: string): T {
40
+ function reviewerRoleField(value: unknown, expected: string, field: string): string {
45
41
  const actual = textField(value, field);
46
42
  if (actual !== expected) invalid(`${field} must be ${expected}.`, field);
47
43
  return expected;
@@ -91,11 +87,11 @@ function artifactCompatible(surface: UlwLoopManualQaSurface, kind: UlwLoopManual
91
87
  }
92
88
 
93
89
  function checkFile(path: string, field: string, opts?: ValidateQualityGateOptions): void {
94
- if (opts === undefined) return;
90
+ if (opts?.repoRoot === undefined || opts.fs === undefined) return;
95
91
  const absolute = resolve(opts.repoRoot, path);
96
92
  if (!opts.fs.existsSync(absolute)) invalid(`${field} must point to an existing artifact.`, field);
97
93
  if (opts.fs.statSync(absolute).size <= 0) invalid(`${field} must point to a non-empty artifact.`, field);
98
- if (opts.currentAttemptDir !== undefined) {
94
+ if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
99
95
  const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
100
96
  if (!isWithinAttemptDir(absolute, attemptRoot))
101
97
  invalid(
@@ -143,6 +139,7 @@ function referencedArtifacts(
143
139
  }
144
140
 
145
141
  export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate {
142
+ const reviewerRoles = reviewerRolesFor(opts?.reviewerSurface ?? "lazycodex");
146
143
  const gate = section(input, "qualityGate");
147
144
  const codeReview = section(gate["codeReview"], "codeReview");
148
145
  const manualQa = section(gate["manualQa"], "manualQa");
@@ -163,7 +160,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
163
160
  checkFile(gateReportPath, "gateReview.reportPath", opts);
164
161
  return {
165
162
  codeReview: {
166
- by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
163
+ by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
167
164
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
168
165
  codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
169
166
  reportPath: codeReportPath,
@@ -171,7 +168,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
171
168
  blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
172
169
  },
173
170
  manualQa: {
174
- by: reviewerRoleField(manualQa["by"], REVIEWER_ROLES.manualQa, "manualQa.by"),
171
+ by: reviewerRoleField(manualQa["by"], reviewerRoles.manualQa, "manualQa.by"),
175
172
  status: literal(manualQa["status"], "passed", "manualQa.status"),
176
173
  evidence: textField(manualQa["evidence"], "manualQa.evidence"),
177
174
  surfaceEvidence,
@@ -179,7 +176,7 @@ export function validateQualityGate(input: unknown, opts?: ValidateQualityGateOp
179
176
  artifactRefs,
180
177
  },
181
178
  gateReview: {
182
- by: reviewerRoleField(gateReview["by"], REVIEWER_ROLES.gateReview, "gateReview.by"),
179
+ by: reviewerRoleField(gateReview["by"], reviewerRoles.gateReview, "gateReview.by"),
183
180
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
184
181
  reportPath: gateReportPath,
185
182
  evidence: textField(gateReview["evidence"], "gateReview.evidence"),
@@ -5,6 +5,7 @@ import type { PreToolUsePayload } from "./codex-hook.js";
5
5
  import { parsePreToolUsePayload } from "./codex-hook.js";
6
6
  import { isFinalRunCompletionCandidate } from "./goal-status.js";
7
7
  import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
8
+ import { GATE_REVIEWER_AGENT_NAMES } from "./surface.js";
8
9
  import type { UlwLoopPlan } from "./types.js";
9
10
 
10
11
  // spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
@@ -12,7 +13,7 @@ import type { UlwLoopPlan } from "./types.js";
12
13
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
13
14
  const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
14
15
  const DEFAULT_FANOUT_LIMIT = 60;
15
- const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
16
+ const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
16
17
 
17
18
  export function applySpawnGuards(payload: PreToolUsePayload): string {
18
19
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name)) return "";
@@ -77,7 +78,7 @@ function isGateReviewerSpawn(toolInput: unknown): boolean {
77
78
  if (typeof toolInput !== "object" || toolInput === null) return false;
78
79
  const record = toolInput as Record<string, unknown>;
79
80
  const agentType = record["agent_type"];
80
- if (typeof agentType === "string") return agentType === "lazycodex-gate-reviewer";
81
+ if (typeof agentType === "string") return GATE_REVIEWER_AGENT_NAMES.has(agentType);
81
82
  const message = record["message"];
82
83
  return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
83
84
  }
@@ -0,0 +1,73 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ // The agent toolkit ships on two surfaces: the LazyCodex (Codex CLI) plugin runs the component
6
+ // bundle directly, while omo-senpi stages the same bundle through stage-agent-toolkit.mjs. The
7
+ // reviewer identities the final quality gate enforces differ per surface, so the staging script
8
+ // bakes a `surface.json` marker next to the staged bundle and this module resolves it. Without a
9
+ // marker (the Codex layout) the surface stays "lazycodex".
10
+ export type UlwLoopToolkitSurface = "lazycodex" | "omo-senpi";
11
+
12
+ export interface UlwLoopReviewerRoles {
13
+ readonly codeReview: string;
14
+ readonly manualQa: string;
15
+ readonly gateReview: string;
16
+ }
17
+
18
+ export const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, UlwLoopReviewerRoles>> = {
19
+ lazycodex: {
20
+ codeReview: "lazycodex-code-reviewer",
21
+ manualQa: "lazycodex-qa-executor",
22
+ gateReview: "lazycodex-gate-reviewer",
23
+ },
24
+ "omo-senpi": {
25
+ codeReview: "omo-senpi-code-reviewer",
26
+ manualQa: "omo-senpi-qa-executor",
27
+ gateReview: "omo-senpi-gate-reviewer",
28
+ },
29
+ };
30
+
31
+ export const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string> = new Set(
32
+ Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview),
33
+ );
34
+
35
+ export function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles {
36
+ return REVIEWER_ROLES_BY_SURFACE[surface];
37
+ }
38
+
39
+ export const SURFACE_MARKER_FILENAME = "surface.json";
40
+ const SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
41
+
42
+ function parseSurface(value: unknown): UlwLoopToolkitSurface | null {
43
+ return value === "lazycodex" || value === "omo-senpi" ? value : null;
44
+ }
45
+
46
+ export interface ResolveToolkitSurfaceOptions {
47
+ readonly env?: Readonly<Record<string, string | undefined>>;
48
+ readonly entryDir?: string;
49
+ }
50
+
51
+ // Resolution order: explicit env override (tests, doctor probes) -> staged surface.json marker
52
+ // sitting next to the running bundle -> lazycodex default. The marker is distribution-baked, so a
53
+ // malformed marker falls back to the default instead of crashing checkpoint validation; the env
54
+ // override remains available to force the intended surface on a damaged install.
55
+ export function resolveToolkitSurface(options?: ResolveToolkitSurfaceOptions): UlwLoopToolkitSurface {
56
+ const env = options?.env ?? process.env;
57
+ const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
58
+ if (fromEnv !== null) return fromEnv;
59
+ const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
60
+ const markerPath = join(entryDir, SURFACE_MARKER_FILENAME);
61
+ return readMarkerSurface(markerPath) ?? "lazycodex";
62
+ }
63
+
64
+ function readMarkerSurface(markerPath: string): UlwLoopToolkitSurface | null {
65
+ try {
66
+ if (!existsSync(markerPath)) return null;
67
+ const parsed = JSON.parse(readFileSync(markerPath, "utf8")) as Record<string, unknown>;
68
+ return parseSurface(parsed["surface"]);
69
+ } catch (error) {
70
+ if (error instanceof Error) return null;
71
+ throw error;
72
+ }
73
+ }
@@ -1,5 +1,5 @@
1
1
  import { writeFile } from "node:fs/promises";
2
- import { describe, expect, it } from "vitest";
2
+ import { afterEach, describe, expect, it } from "vitest";
3
3
 
4
4
  import { checkpointUlwLoop } from "../src/checkpoint.js";
5
5
  import { ulwLoopBriefPath } from "../src/paths.js";
@@ -7,6 +7,49 @@ import { startNextUlwLoop } from "../src/plan-crud.js";
7
7
  import { criterion, expectCode, goal, passGoal, plan, repoWith, snapshot } from "./fixtures/checkpoint-builders.js";
8
8
  import { MISSING_ARTIFACT_PATH, qualityGateJson } from "./fixtures/quality-gate-builder.js";
9
9
 
10
+ describe("checkpointUlwLoop final story surface resolution", () => {
11
+ afterEach(() => {
12
+ delete process.env["OMO_AGENT_TOOLKIT_SURFACE"];
13
+ });
14
+
15
+ it("#given the omo-senpi surface #when the gate names omo-senpi reviewers #then the final story completes", async () => {
16
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
17
+ const repo = await repoWith(
18
+ plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
19
+ );
20
+
21
+ const result = await checkpointUlwLoop(repo, {
22
+ goalId: "G002",
23
+ status: "complete",
24
+ evidence: "final work complete and validation passed",
25
+ codexGoalJson: snapshot("complete"),
26
+ qualityGateJson: await qualityGateJson(repo, undefined, "G001-finished", "omo-senpi"),
27
+ });
28
+
29
+ expect(result.aggregateCompletion?.status).toBe("complete");
30
+ });
31
+
32
+ it("#given the omo-senpi surface #when the gate names lazycodex reviewers #then the final story is rejected", async () => {
33
+ process.env["OMO_AGENT_TOOLKIT_SURFACE"] = "omo-senpi";
34
+ const repo = await repoWith(
35
+ plan([passGoal("G001", { status: "complete" }), passGoal("G002")], { activeGoalId: "G002" }),
36
+ );
37
+ const gateJson = await qualityGateJson(repo);
38
+
39
+ await expectCode(
40
+ () =>
41
+ checkpointUlwLoop(repo, {
42
+ goalId: "G002",
43
+ status: "complete",
44
+ evidence: "final work complete and validation passed",
45
+ codexGoalJson: snapshot("complete"),
46
+ qualityGateJson: gateJson,
47
+ }),
48
+ "ULW_LOOP_QUALITY_GATE_INVALID",
49
+ );
50
+ });
51
+ });
52
+
10
53
  describe("checkpointUlwLoop final story", () => {
11
54
  it("requires quality-gate-json for the final goal complete", async () => {
12
55
  const repo = await repoWith(
@@ -6,6 +6,7 @@ import type { UlwLoopItem, UlwLoopPlan, UlwLoopSuccessCriterion } from "../src/t
6
6
 
7
7
  const NOW = "2026-05-23T00:00:00.000Z";
8
8
  const FINAL_REVIEW_ROLES = ["lazycodex-code-reviewer", "lazycodex-qa-executor", "lazycodex-gate-reviewer"] as const;
9
+ const SENPI_REVIEW_ROLES = ["omo-senpi-code-reviewer", "omo-senpi-qa-executor", "omo-senpi-gate-reviewer"] as const;
9
10
  const QUALITY_GATE_SECTIONS = ["codeReview", "manualQa", "gateReview", "iteration", "criteriaCoverage"] as const;
10
11
 
11
12
  function makeCriterion(overrides: Partial<UlwLoopSuccessCriterion> = {}): UlwLoopSuccessCriterion {
@@ -94,6 +95,30 @@ describe("buildCodexGoalInstruction aggregate mode", () => {
94
95
  expect(text).toContain("checkpoint");
95
96
  });
96
97
 
98
+ it("#given a final story on the omo-senpi surface #when rendering instructions #then names the omo-senpi reviewers only", () => {
99
+ const { text } = buildCodexGoalInstruction({
100
+ plan: makePlan({ codexGoalMode: "aggregate" }),
101
+ goal: makeGoal(),
102
+ isFinal: true,
103
+ surface: "omo-senpi",
104
+ });
105
+
106
+ expectTextToContainAll(text, SENPI_REVIEW_ROLES);
107
+ for (const role of FINAL_REVIEW_ROLES) expect(text).not.toContain(role);
108
+ });
109
+
110
+ it("#given a final story with the explicit lazycodex surface #when rendering instructions #then keeps the lazycodex reviewers", () => {
111
+ const { text } = buildCodexGoalInstruction({
112
+ plan: makePlan({ codexGoalMode: "aggregate" }),
113
+ goal: makeGoal(),
114
+ isFinal: true,
115
+ surface: "lazycodex",
116
+ });
117
+
118
+ expectTextToContainAll(text, FINAL_REVIEW_ROLES);
119
+ for (const role of SENPI_REVIEW_ROLES) expect(text).not.toContain(role);
120
+ });
121
+
97
122
  it("#given a scoped plan #when rendering final commands #then includes the session id option", () => {
98
123
  const { text } = buildCodexGoalInstruction({
99
124
  plan: makePlan({
@@ -1,6 +1,8 @@
1
1
  import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { join } from "node:path";
3
3
 
4
+ import { reviewerRolesFor, type UlwLoopToolkitSurface } from "../../src/surface.js";
5
+
4
6
  // v2 evidence layout: artifacts must live inside the goal's current attempt dir
5
7
  export function qaDirFor(goalId: string, attempt = 1, sessionId = "session"): string {
6
8
  return `.omo/evidence/ulw/${sessionId}/${goalId}/a${attempt}`;
@@ -25,13 +27,15 @@ export async function qualityGateJson(
25
27
  repoRoot: string,
26
28
  cliArtifactPath?: string,
27
29
  goalId = "G001-finished",
30
+ surface: UlwLoopToolkitSurface = "lazycodex",
28
31
  ): Promise<string> {
32
+ const roles = reviewerRolesFor(surface);
29
33
  const qaDir = qaDirFor(goalId);
30
34
  await writeQualityGateArtifacts(repoRoot, qaDir);
31
35
  const cliPath = cliArtifactPath ?? `${qaDir}/cli-pass.txt`;
32
36
  return JSON.stringify({
33
37
  codeReview: {
34
- by: "lazycodex-code-reviewer",
38
+ by: roles.codeReview,
35
39
  recommendation: "APPROVE",
36
40
  codeQualityStatus: "CLEAR",
37
41
  reportPath: `${qaDir}/code-review.md`,
@@ -39,7 +43,7 @@ export async function qualityGateJson(
39
43
  blockers: [],
40
44
  },
41
45
  manualQa: {
42
- by: "lazycodex-qa-executor",
46
+ by: roles.manualQa,
43
47
  status: "passed",
44
48
  evidence: "Ran CLI checkpoint validation with artifact-backed evidence.",
45
49
  surfaceEvidence: [
@@ -78,7 +82,7 @@ export async function qualityGateJson(
78
82
  ],
79
83
  },
80
84
  gateReview: {
81
- by: "lazycodex-gate-reviewer",
85
+ by: roles.gateReview,
82
86
  recommendation: "APPROVE",
83
87
  reportPath: `${qaDir}/gate-review.md`,
84
88
  evidence: "Verified all criteria and artifact evidence.",
@@ -75,9 +75,9 @@ function gateWith(overrides: Record<string, unknown>): Record<string, unknown> {
75
75
  return { ...BASE_GATE, ...overrides };
76
76
  }
77
77
 
78
- function qualityGateError(input: unknown): UlwLoopError {
78
+ function qualityGateError(input: unknown, surface?: "lazycodex" | "omo-senpi"): UlwLoopError {
79
79
  try {
80
- validateQualityGate(input);
80
+ validateQualityGate(input, surface === undefined ? undefined : { reviewerSurface: surface });
81
81
  } catch (error) {
82
82
  if (error instanceof UlwLoopError) return error;
83
83
  throw error;
@@ -85,6 +85,20 @@ function qualityGateError(input: unknown): UlwLoopError {
85
85
  throw new Error("Expected UlwLoopError");
86
86
  }
87
87
 
88
+ function senpiGate(): Record<string, unknown> {
89
+ return gateWith({
90
+ codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-code-reviewer" },
91
+ manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
92
+ gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
93
+ criteriaCoverage: {
94
+ ...BASE_GATE.criteriaCoverage,
95
+ originalIntent: "User wanted the senpi surface to accept honest reviewer attribution.",
96
+ desiredOutcome: "A gate naming the omo-senpi reviewers validates on the omo-senpi surface.",
97
+ userOutcomeReview: "The senpi-surface gate passed with truthful reviewer identities.",
98
+ },
99
+ });
100
+ }
101
+
88
102
  describe("validateQualityGate reviewer roles", () => {
89
103
  it("#given arbitrary reviewer role names #when validated #then each role field is rejected", () => {
90
104
  const cases = [
@@ -106,6 +120,35 @@ describe("validateQualityGate reviewer roles", () => {
106
120
  }
107
121
  });
108
122
 
123
+ it("#given omo-senpi reviewer identities #when validated on the omo-senpi surface #then the gate is accepted", () => {
124
+ expect(() => validateQualityGate(senpiGate(), { reviewerSurface: "omo-senpi" })).not.toThrow();
125
+ });
126
+
127
+ it("#given lazycodex identities #when validated on the omo-senpi surface #then each role names the omo-senpi identity", () => {
128
+ const error = qualityGateError(gateWith({}), "omo-senpi");
129
+ expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
130
+ expect(error.message).toContain("omo-senpi-code-reviewer");
131
+ });
132
+
133
+ it("#given omo-senpi identities #when validated on the default lazycodex surface #then the gate is rejected", () => {
134
+ const error = qualityGateError(senpiGate());
135
+ expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
136
+ expect(error.message).toContain("lazycodex-code-reviewer");
137
+ });
138
+
139
+ it("#given swapped omo-senpi reviewer roles #when validated on the omo-senpi surface #then section-specific roles are enforced", () => {
140
+ const swapped = gateWith({
141
+ codeReview: { ...BASE_GATE.codeReview, by: "omo-senpi-qa-executor" },
142
+ manualQa: { ...BASE_GATE.manualQa, by: "omo-senpi-qa-executor" },
143
+ gateReview: { ...BASE_GATE.gateReview, by: "omo-senpi-gate-reviewer" },
144
+ });
145
+
146
+ const error = qualityGateError(swapped, "omo-senpi");
147
+
148
+ expect(error.code).toBe("ULW_LOOP_QUALITY_GATE_INVALID");
149
+ expect(error.message).toContain("codeReview.by");
150
+ });
151
+
109
152
  it("#given swapped LazyCodex reviewer roles #when validated #then section-specific roles are enforced", () => {
110
153
  const cases = [
111
154
  {
@@ -146,6 +146,26 @@ describe("applySpawnGuards gate-artifact guard", () => {
146
146
  expect(parsed.permissionDecisionReason).toContain("g1-code-review.md");
147
147
  });
148
148
 
149
+ it("#given an omo-senpi gate spawn by agent_type without artifacts #when guarded #then denies naming the missing path", () => {
150
+ writeGoals();
151
+
152
+ const output = applySpawnGuards(
153
+ payload("collaborationspawn_agent", { agent_type: "omo-senpi-gate-reviewer", message: "audit the artifacts" }),
154
+ );
155
+
156
+ const parsed = deny(output);
157
+ expect(parsed.permissionDecision).toBe("deny");
158
+ expect(parsed.permissionDecisionReason).toContain("g1-code-review.md");
159
+ });
160
+
161
+ it("#given an omo-senpi gate reviewer named in the message #when guarded #then still denies", () => {
162
+ writeGoals();
163
+
164
+ const output = applySpawnGuards(payload("spawn_agent", { message: "spawn omo-senpi-gate-reviewer now" }));
165
+
166
+ expect(deny(output).permissionDecision).toBe("deny");
167
+ });
168
+
149
169
  it("#given a gate spawn identified by message only #when guarded #then still denies", () => {
150
170
  writeGoals();
151
171