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
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts
4
- import { existsSync as existsSync3, statSync } from "node:fs";
4
+ import { existsSync as existsSync4, statSync } from "node:fs";
5
5
  import { readFile as readFile5 } from "node:fs/promises";
6
6
  import { resolve as resolve3 } from "node:path";
7
7
 
@@ -648,6 +648,53 @@ function adversarialVerdict(row, field) {
648
648
  invalid(`${field} must be passed or not_applicable with a reason.`, field);
649
649
  }
650
650
 
651
+ // packages/omo-codex/plugin/components/ulw-loop/src/surface.ts
652
+ import { existsSync as existsSync3, readFileSync } from "node:fs";
653
+ import { dirname, join as join2 } from "node:path";
654
+ import { fileURLToPath } from "node:url";
655
+ var REVIEWER_ROLES_BY_SURFACE = {
656
+ lazycodex: {
657
+ codeReview: "lazycodex-code-reviewer",
658
+ manualQa: "lazycodex-qa-executor",
659
+ gateReview: "lazycodex-gate-reviewer"
660
+ },
661
+ "omo-senpi": {
662
+ codeReview: "omo-senpi-code-reviewer",
663
+ manualQa: "omo-senpi-qa-executor",
664
+ gateReview: "omo-senpi-gate-reviewer"
665
+ }
666
+ };
667
+ var GATE_REVIEWER_AGENT_NAMES = new Set(Object.values(REVIEWER_ROLES_BY_SURFACE).map((roles) => roles.gateReview));
668
+ function reviewerRolesFor(surface) {
669
+ return REVIEWER_ROLES_BY_SURFACE[surface];
670
+ }
671
+ var SURFACE_MARKER_FILENAME = "surface.json";
672
+ var SURFACE_ENV_KEY = "OMO_AGENT_TOOLKIT_SURFACE";
673
+ function parseSurface(value) {
674
+ return value === "lazycodex" || value === "omo-senpi" ? value : null;
675
+ }
676
+ function resolveToolkitSurface(options) {
677
+ const env = options?.env ?? process.env;
678
+ const fromEnv = parseSurface(env[SURFACE_ENV_KEY]);
679
+ if (fromEnv !== null)
680
+ return fromEnv;
681
+ const entryDir = options?.entryDir ?? dirname(fileURLToPath(import.meta.url));
682
+ const markerPath = join2(entryDir, SURFACE_MARKER_FILENAME);
683
+ return readMarkerSurface(markerPath) ?? "lazycodex";
684
+ }
685
+ function readMarkerSurface(markerPath) {
686
+ try {
687
+ if (!existsSync3(markerPath))
688
+ return null;
689
+ const parsed = JSON.parse(readFileSync(markerPath, "utf8"));
690
+ return parseSurface(parsed["surface"]);
691
+ } catch (error) {
692
+ if (error instanceof Error)
693
+ return null;
694
+ throw error;
695
+ }
696
+ }
697
+
651
698
  // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate-blockers.ts
652
699
  var BLOCKER_FIELD_KEYS = "blocker blockerSignature blockerEvidence blockerOccurrences blockedAt".split(" ");
653
700
  var URL_PATTERN = /https?:\/\/\S+/g;
@@ -691,11 +738,6 @@ function clearGoalBlockerFields(goal) {
691
738
  }
692
739
 
693
740
  // packages/omo-codex/plugin/components/ulw-loop/src/quality-gate.ts
694
- var REVIEWER_ROLES = {
695
- codeReview: "lazycodex-code-reviewer",
696
- manualQa: "lazycodex-qa-executor",
697
- gateReview: "lazycodex-gate-reviewer"
698
- };
699
741
  function reviewerRoleField(value, expected, field) {
700
742
  const actual = textField(value, field);
701
743
  if (actual !== expected)
@@ -729,14 +771,14 @@ function artifactCompatible(surface, kind) {
729
771
  }
730
772
  }
731
773
  function checkFile(path, field, opts) {
732
- if (opts === undefined)
774
+ if (opts?.repoRoot === undefined || opts.fs === undefined)
733
775
  return;
734
776
  const absolute = resolve2(opts.repoRoot, path);
735
777
  if (!opts.fs.existsSync(absolute))
736
778
  invalid(`${field} must point to an existing artifact.`, field);
737
779
  if (opts.fs.statSync(absolute).size <= 0)
738
780
  invalid(`${field} must point to a non-empty artifact.`, field);
739
- if (opts.currentAttemptDir !== undefined) {
781
+ if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
740
782
  const attemptRoot = resolve2(opts.repoRoot, opts.currentAttemptDir);
741
783
  if (!isWithinAttemptDir(absolute, attemptRoot))
742
784
  invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
@@ -775,6 +817,7 @@ function referencedArtifacts(value, field, byId) {
775
817
  });
776
818
  }
777
819
  function validateQualityGate(input, opts) {
820
+ const reviewerRoles = reviewerRolesFor(opts?.reviewerSurface ?? "lazycodex");
778
821
  const gate = section(input, "qualityGate");
779
822
  const codeReview = section(gate["codeReview"], "codeReview");
780
823
  const manualQa = section(gate["manualQa"], "manualQa");
@@ -795,7 +838,7 @@ function validateQualityGate(input, opts) {
795
838
  checkFile(gateReportPath, "gateReview.reportPath", opts);
796
839
  return {
797
840
  codeReview: {
798
- by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
841
+ by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
799
842
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
800
843
  codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
801
844
  reportPath: codeReportPath,
@@ -803,7 +846,7 @@ function validateQualityGate(input, opts) {
803
846
  blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers")
804
847
  },
805
848
  manualQa: {
806
- by: reviewerRoleField(manualQa["by"], REVIEWER_ROLES.manualQa, "manualQa.by"),
849
+ by: reviewerRoleField(manualQa["by"], reviewerRoles.manualQa, "manualQa.by"),
807
850
  status: literal(manualQa["status"], "passed", "manualQa.status"),
808
851
  evidence: textField(manualQa["evidence"], "manualQa.evidence"),
809
852
  surfaceEvidence,
@@ -811,7 +854,7 @@ function validateQualityGate(input, opts) {
811
854
  artifactRefs
812
855
  },
813
856
  gateReview: {
814
- by: reviewerRoleField(gateReview["by"], REVIEWER_ROLES.gateReview, "gateReview.by"),
857
+ by: reviewerRoleField(gateReview["by"], reviewerRoles.gateReview, "gateReview.by"),
815
858
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
816
859
  reportPath: gateReportPath,
817
860
  evidence: textField(gateReview["evidence"], "gateReview.evidence"),
@@ -1084,7 +1127,7 @@ function fail(message, code = "ULW_LOOP_VALIDATION_BATCH_INVALID") {
1084
1127
  }
1085
1128
 
1086
1129
  // packages/omo-codex/plugin/components/ulw-loop/src/checkpoint.ts
1087
- var QUALITY_GATE_FS = { existsSync: existsSync3, statSync };
1130
+ var QUALITY_GATE_FS = { existsSync: existsSync4, statSync };
1088
1131
  function ulwLoopFail2(message, code) {
1089
1132
  throw new UlwLoopError(message, code);
1090
1133
  }
@@ -1110,7 +1153,7 @@ async function readJsonInput2(raw, repoRoot) {
1110
1153
  throw error;
1111
1154
  }
1112
1155
  const path = resolve3(repoRoot, trimmed);
1113
- if (!existsSync3(path))
1156
+ if (!existsSync4(path))
1114
1157
  return ulwLoopFail2("Quality gate JSON is neither valid JSON nor a readable path.", "ulw_loop_json_input_invalid");
1115
1158
  try {
1116
1159
  return JSON.parse(await readFile5(path, "utf8"));
@@ -1222,6 +1265,7 @@ async function checkpointUlwLoop(repoRoot, args, scope) {
1222
1265
  qualityGate = validateQualityGate(await readJsonInput2(args.qualityGateJson, repoRoot), {
1223
1266
  repoRoot,
1224
1267
  fs: QUALITY_GATE_FS,
1268
+ reviewerSurface: resolveToolkitSurface(),
1225
1269
  ...plan.evidenceLayoutVersion === 2 ? { currentAttemptDir: ulwLoopAttemptEvidenceDir(goal.id, goal.attempt, scope) } : {}
1226
1270
  });
1227
1271
  requireBatchGate(plan, goal, qualityGate);
@@ -1345,12 +1389,13 @@ function buildCodexGoalInstruction(args) {
1345
1389
  const mode = codexGoalMode(args.plan);
1346
1390
  const createGoal = buildCreateGoalPayload(args.plan, args.goal);
1347
1391
  const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
1348
- return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal };
1392
+ const surface = args.surface ?? resolveToolkitSurface();
1393
+ return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal, surface), json: createGoal };
1349
1394
  }
1350
1395
  function buildCreateGoalPayload(plan, goal) {
1351
1396
  return { objective: expectedCodexObjective(plan, goal) };
1352
1397
  }
1353
- function buildText(mode, plan, goal, createGoal, isFinal) {
1398
+ function buildText(mode, plan, goal, createGoal, isFinal, surface) {
1354
1399
  return joinLines([
1355
1400
  mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
1356
1401
  `Mode: ${mode}`,
@@ -1367,7 +1412,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
1367
1412
  "- Goals are unlimited. Do not add numeric limits.",
1368
1413
  ...modeConstraintLines(mode, isFinal),
1369
1414
  ...evidenceLayoutLines(plan),
1370
- finalSection(plan, goal, isFinal, mode === "aggregate"),
1415
+ finalSection(plan, goal, isFinal, mode === "aggregate", surface),
1371
1416
  ...checkpointLines(plan, mode),
1372
1417
  "",
1373
1418
  "create_goal payload:",
@@ -1419,7 +1464,8 @@ function evidenceLayoutLines(plan) {
1419
1464
  "- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo-agent-toolkit ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory."
1420
1465
  ];
1421
1466
  }
1422
- function finalSection(plan, goal, isFinal, aggregate) {
1467
+ function finalSection(plan, goal, isFinal, aggregate, surface) {
1468
+ const roles = reviewerRolesFor(surface);
1423
1469
  if (!isFinal)
1424
1470
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
1425
1471
  const option = sessionOption(plan);
@@ -1429,8 +1475,8 @@ function finalSection(plan, goal, isFinal, aggregate) {
1429
1475
  "Final story — run mandatory quality gate before update_goal:",
1430
1476
  "- Run targeted verification for changed behavior.",
1431
1477
  "- Confirm every manualQa artifact path exists and has non-zero size.",
1432
- '- 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).',
1433
- "- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
1478
+ `- 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).`,
1479
+ `- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
1434
1480
  "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
1435
1481
  "- 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.",
1436
1482
  "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
@@ -1454,7 +1500,7 @@ function joinLines(lines) {
1454
1500
  }
1455
1501
 
1456
1502
  // packages/omo-codex/plugin/components/ulw-loop/src/plan-crud.ts
1457
- import { existsSync as existsSync4 } from "node:fs";
1503
+ import { existsSync as existsSync5 } from "node:fs";
1458
1504
  import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
1459
1505
 
1460
1506
  // packages/omo-codex/plugin/components/ulw-loop/src/plan-goal-factory.ts
@@ -1564,7 +1610,7 @@ function clearGoalBlockerFields2(goal) {
1564
1610
  }
1565
1611
  async function createUlwLoopPlan(repoRoot, args, scope) {
1566
1612
  return withUlwLoopMutationLock(repoRoot, scope, async () => {
1567
- if (!args.force && existsSync4(ulwLoopGoalsPath(repoRoot, scope))) {
1613
+ if (!args.force && existsSync5(ulwLoopGoalsPath(repoRoot, scope))) {
1568
1614
  const existing = await readUlwLoopPlan(repoRoot, scope);
1569
1615
  if (isUlwLoopDone(existing))
1570
1616
  throw completedPlanExistsError(scope);
@@ -2802,11 +2848,11 @@ function commandScope(argv) {
2802
2848
  }
2803
2849
 
2804
2850
  // packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-directive.ts
2805
- import { readFileSync as readFileSync2 } from "node:fs";
2851
+ import { readFileSync as readFileSync3 } from "node:fs";
2806
2852
 
2807
2853
  // packages/omo-codex/plugin/components/ulw-loop/src/ultrawork-skill-pointer.ts
2808
- import { existsSync as existsSync5, readFileSync } from "node:fs";
2809
- import { fileURLToPath } from "node:url";
2854
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "node:fs";
2855
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
2810
2856
  var ULTRAWORK_SKILL_POINTER_TEMPLATE = `<ultrawork-mode>
2811
2857
  ULTRAWORK MODE IS ACTIVE FOR THIS TASK.
2812
2858
 
@@ -2837,16 +2883,16 @@ Do not start the requested work until all three steps are complete.
2837
2883
  `;
2838
2884
  var ULTRAWORK_SKILL_PATH_PLACEHOLDER = "{{ULTRAWORK_SKILL_PATH}}";
2839
2885
  var ULTRAWORK_SKILL_FILE_URL = new URL("../../../skills/ultrawork/SKILL.md", import.meta.url);
2840
- var ULTRAWORK_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
2886
+ var ULTRAWORK_DIRECTIVE = readFileSync2(new URL("../directive.md", import.meta.url), "utf8");
2841
2887
  function resolveUltraworkSkillFilePath() {
2842
- return fileURLToPath(ULTRAWORK_SKILL_FILE_URL);
2888
+ return fileURLToPath2(ULTRAWORK_SKILL_FILE_URL);
2843
2889
  }
2844
2890
  function buildUltraworkSkillPointer(skillFilePath) {
2845
2891
  return ULTRAWORK_SKILL_POINTER_TEMPLATE.replace(ULTRAWORK_SKILL_PATH_PLACEHOLDER, skillFilePath);
2846
2892
  }
2847
2893
  function buildUltraworkAdditionalContext(options = {}) {
2848
2894
  const skillFilePath = options.skillFilePath === undefined ? resolveUltraworkSkillFilePath() : options.skillFilePath;
2849
- if (skillFilePath !== null && existsSync5(skillFilePath)) {
2895
+ if (skillFilePath !== null && existsSync6(skillFilePath)) {
2850
2896
  return buildUltraworkSkillPointer(skillFilePath);
2851
2897
  }
2852
2898
  return ULTRAWORK_DIRECTIVE;
@@ -2900,7 +2946,7 @@ function hasUltraworkDirectiveAlreadyInTranscript(transcriptPath) {
2900
2946
  return false;
2901
2947
  }
2902
2948
  function readTranscriptTail(transcriptPath) {
2903
- const rawTranscript = readFileSync2(transcriptPath);
2949
+ const rawTranscript = readFileSync3(transcriptPath);
2904
2950
  return rawTranscript.subarray(Math.max(0, rawTranscript.byteLength - TRANSCRIPT_SEARCH_BYTES)).toString("utf8");
2905
2951
  }
2906
2952
  function isUltraworkPrompt(prompt) {
@@ -3093,16 +3139,16 @@ function readAll(stdin) {
3093
3139
  }
3094
3140
 
3095
3141
  // packages/omo-codex/plugin/components/ulw-loop/src/spawn-guard.ts
3096
- import { existsSync as existsSync6, readdirSync, readFileSync as readFileSync3, statSync as statSync2, writeFileSync } from "node:fs";
3097
- import { join as join2 } from "node:path";
3142
+ import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync as statSync2, writeFileSync } from "node:fs";
3143
+ import { join as join3 } from "node:path";
3098
3144
  var SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
3099
3145
  var DEFAULT_FANOUT_LIMIT = 60;
3100
- var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
3146
+ var GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
3101
3147
  function applySpawnGuards(payload) {
3102
3148
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
3103
3149
  return "";
3104
3150
  const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
3105
- const plan = readPlan(join2(stateDir, "goals.json"));
3151
+ const plan = readPlan(join3(stateDir, "goals.json"));
3106
3152
  if (plan === null)
3107
3153
  return "";
3108
3154
  const fanOutDenial = consumeFanOutBudget(stateDir);
@@ -3130,7 +3176,7 @@ async function runSpawnGuardCli(stdin, stdout) {
3130
3176
  }
3131
3177
  }
3132
3178
  function consumeFanOutBudget(stateDir) {
3133
- const counterPath = join2(stateDir, "spawn-count.json");
3179
+ const counterPath = join3(stateDir, "spawn-count.json");
3134
3180
  const count = readCount(counterPath) + 1;
3135
3181
  writeFileSync(counterPath, JSON.stringify({ count }));
3136
3182
  const limit = fanOutLimit();
@@ -3151,15 +3197,15 @@ function missingGateArtifact(payload, plan) {
3151
3197
  const attemptDir = ulwLoopAttemptEvidenceDir(goal3.id, goal3.attempt, scope);
3152
3198
  for (const name of [`${goal3.id}-code-review.md`, `${goal3.id}-manual-qa.md`]) {
3153
3199
  const relative2 = `${attemptDir}/${name}`;
3154
- if (!isNonEmptyFile(join2(payload.cwd, relative2)))
3200
+ if (!isNonEmptyFile(join3(payload.cwd, relative2)))
3155
3201
  return relative2;
3156
3202
  }
3157
3203
  return null;
3158
3204
  }
3159
3205
  const flatReport = `.omo/evidence/${goal3.id}-code-review.md`;
3160
- if (!isNonEmptyFile(join2(payload.cwd, flatReport)))
3206
+ if (!isNonEmptyFile(join3(payload.cwd, flatReport)))
3161
3207
  return flatReport;
3162
- if (!hasOtherEvidenceFile(join2(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
3208
+ if (!hasOtherEvidenceFile(join3(payload.cwd, ".omo", "evidence"), `${goal3.id}-code-review.md`))
3163
3209
  return `.omo/evidence/<any manual-QA artifact besides ${goal3.id}-code-review.md>`;
3164
3210
  return null;
3165
3211
  }
@@ -3169,7 +3215,7 @@ function isGateReviewerSpawn(toolInput) {
3169
3215
  const record = toolInput;
3170
3216
  const agentType = record["agent_type"];
3171
3217
  if (typeof agentType === "string")
3172
- return agentType === "lazycodex-gate-reviewer";
3218
+ return GATE_REVIEWER_AGENT_NAMES.has(agentType);
3173
3219
  const message = record["message"];
3174
3220
  return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
3175
3221
  }
@@ -3193,7 +3239,7 @@ function fanOutLimit() {
3193
3239
  }
3194
3240
  function isNonEmptyFile(path) {
3195
3241
  try {
3196
- return existsSync6(path) && statSync2(path).size > 0;
3242
+ return existsSync7(path) && statSync2(path).size > 0;
3197
3243
  } catch (error) {
3198
3244
  if (error instanceof Error)
3199
3245
  return false;
@@ -3202,7 +3248,7 @@ function isNonEmptyFile(path) {
3202
3248
  }
3203
3249
  function hasOtherEvidenceFile(evidenceDir, excludedName) {
3204
3250
  try {
3205
- return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join2(evidenceDir, name)));
3251
+ return readdirSync(evidenceDir).some((name) => name !== excludedName && isNonEmptyFile(join3(evidenceDir, name)));
3206
3252
  } catch (error) {
3207
3253
  if (error instanceof Error)
3208
3254
  return false;
@@ -3211,7 +3257,7 @@ function hasOtherEvidenceFile(evidenceDir, excludedName) {
3211
3257
  }
3212
3258
  function readCount(counterPath) {
3213
3259
  try {
3214
- const parsed = JSON.parse(readFileSync3(counterPath, "utf8"));
3260
+ const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
3215
3261
  return typeof parsed["count"] === "number" && parsed["count"] >= 0 ? parsed["count"] : 0;
3216
3262
  } catch (error) {
3217
3263
  if (error instanceof Error)
@@ -3221,7 +3267,7 @@ function readCount(counterPath) {
3221
3267
  }
3222
3268
  function readPlan(goalsPath) {
3223
3269
  try {
3224
- return JSON.parse(readFileSync3(goalsPath, "utf8"));
3270
+ return JSON.parse(readFileSync4(goalsPath, "utf8"));
3225
3271
  } catch (error) {
3226
3272
  if (error instanceof Error)
3227
3273
  return null;
@@ -3230,8 +3276,8 @@ function readPlan(goalsPath) {
3230
3276
  }
3231
3277
 
3232
3278
  // packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts
3233
- import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "node:fs";
3234
- import { isAbsolute as isAbsolute2, join as join3, resolve as resolve4, sep as sep2 } from "node:path";
3279
+ import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync2 } from "node:fs";
3280
+ import { isAbsolute as isAbsolute2, join as join4, resolve as resolve4, sep as sep2 } from "node:path";
3235
3281
  var RESUME_CAP = 2;
3236
3282
  var CONTEXT_PRESSURE_MARKERS2 = [
3237
3283
  "context compacted",
@@ -3251,7 +3297,7 @@ function runStopResumeHook(input) {
3251
3297
  if (boulderContinuationWillFire(payload.cwd, payload.session_id))
3252
3298
  return "";
3253
3299
  const stateDir = ulwLoopDir(payload.cwd, { sessionId: payload.session_id });
3254
- const plan = readPlan2(join3(stateDir, "goals.json"));
3300
+ const plan = readPlan2(join4(stateDir, "goals.json"));
3255
3301
  if (plan === null || plan.aggregateCompletion?.status === "complete")
3256
3302
  return "";
3257
3303
  const goal3 = resumableGoal(plan);
@@ -3288,7 +3334,7 @@ function isResumableStatus(status2) {
3288
3334
  return status2 === "pending" || status2 === "in_progress";
3289
3335
  }
3290
3336
  function consumeResumeBudget(stateDir, goalId) {
3291
- const ledgerLineCount = countLedgerLines(join3(stateDir, "ledger.jsonl"));
3337
+ const ledgerLineCount = countLedgerLines(join4(stateDir, "ledger.jsonl"));
3292
3338
  const counterPath = resolve4(stateDir, `auto-resume-${goalId}.json`);
3293
3339
  const stuckPath = resolve4(stateDir, `auto-resume-${goalId}.stuck`);
3294
3340
  if (!isInsideDir(stateDir, counterPath) || !isInsideDir(stateDir, stuckPath))
@@ -3321,7 +3367,7 @@ function renderResumeDirective(plan, goal3, sessionId) {
3321
3367
  }
3322
3368
  function readPlan2(goalsPath) {
3323
3369
  try {
3324
- return JSON.parse(readFileSync4(goalsPath, "utf8"));
3370
+ return JSON.parse(readFileSync5(goalsPath, "utf8"));
3325
3371
  } catch (error) {
3326
3372
  if (error instanceof Error)
3327
3373
  return null;
@@ -3330,7 +3376,7 @@ function readPlan2(goalsPath) {
3330
3376
  }
3331
3377
  function countLedgerLines(ledgerPath) {
3332
3378
  try {
3333
- return readFileSync4(ledgerPath, "utf8").split(`
3379
+ return readFileSync5(ledgerPath, "utf8").split(`
3334
3380
  `).filter(Boolean).length;
3335
3381
  } catch (error) {
3336
3382
  if (error instanceof Error)
@@ -3340,9 +3386,9 @@ function countLedgerLines(ledgerPath) {
3340
3386
  }
3341
3387
  function readCounter(counterPath) {
3342
3388
  try {
3343
- if (!existsSync7(counterPath))
3389
+ if (!existsSync8(counterPath))
3344
3390
  return null;
3345
- const parsed = JSON.parse(readFileSync4(counterPath, "utf8"));
3391
+ const parsed = JSON.parse(readFileSync5(counterPath, "utf8"));
3346
3392
  if (typeof parsed["count"] !== "number" || typeof parsed["ledgerLineCount"] !== "number")
3347
3393
  return null;
3348
3394
  return { count: parsed["count"], ledgerLineCount: parsed["ledgerLineCount"] };
@@ -3354,7 +3400,7 @@ function readCounter(counterPath) {
3354
3400
  }
3355
3401
  function boulderContinuationWillFire(cwd, sessionId) {
3356
3402
  try {
3357
- const raw = JSON.parse(readFileSync4(join3(cwd, ".omo", "boulder.json"), "utf8"));
3403
+ const raw = JSON.parse(readFileSync5(join4(cwd, ".omo", "boulder.json"), "utf8"));
3358
3404
  const works = raw["works"];
3359
3405
  const entries = typeof works === "object" && works !== null ? Object.values(works) : [raw];
3360
3406
  return entries.some((work) => {
@@ -3373,7 +3419,7 @@ function boulderContinuationWillFire(cwd, sessionId) {
3373
3419
  }
3374
3420
  function transcriptShowsContextPressure(transcriptPath) {
3375
3421
  try {
3376
- const transcript = readFileSync4(transcriptPath, "utf8").toLowerCase();
3422
+ const transcript = readFileSync5(transcriptPath, "utf8").toLowerCase();
3377
3423
  return CONTEXT_PRESSURE_MARKERS2.some((marker) => transcript.includes(marker));
3378
3424
  } catch (error) {
3379
3425
  if (error instanceof Error)
@@ -3385,12 +3431,12 @@ function boulderPlanHasChecklist(cwd, entry) {
3385
3431
  const activePlan = entry["active_plan"];
3386
3432
  if (typeof activePlan !== "string" || activePlan.trim().length === 0)
3387
3433
  return false;
3388
- const planPath = isAbsolute2(activePlan) ? activePlan : join3(cwd, activePlan);
3434
+ const planPath = isAbsolute2(activePlan) ? activePlan : join4(cwd, activePlan);
3389
3435
  const worktree = entry["worktree_path"];
3390
- const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join3(isAbsolute2(worktree) ? worktree : join3(cwd, worktree), activePlan), planPath] : [planPath];
3436
+ const candidates = typeof worktree === "string" && worktree.trim().length > 0 && !isAbsolute2(activePlan) ? [join4(isAbsolute2(worktree) ? worktree : join4(cwd, worktree), activePlan), planPath] : [planPath];
3391
3437
  for (const candidate of candidates) {
3392
3438
  try {
3393
- return readFileSync4(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
3439
+ return readFileSync5(candidate, "utf8").split(/\r?\n/).some((line) => line.startsWith("- [ ] ") || line.startsWith("- [x] ") || line.startsWith("- [X] "));
3394
3440
  } catch (error) {
3395
3441
  if (!(error instanceof Error))
3396
3442
  throw error;
@@ -1,3 +1,4 @@
1
+ import { type UlwLoopToolkitSurface } from "./surface.js";
1
2
  import type { UlwLoopItem, UlwLoopPlan } from "./types.js";
2
3
  export interface CodexCreateGoalPayload {
3
4
  readonly objective: string;
@@ -10,4 +11,5 @@ export declare function buildCodexGoalInstruction(args: {
10
11
  readonly plan: UlwLoopPlan;
11
12
  readonly goal: UlwLoopItem;
12
13
  readonly isFinal?: boolean;
14
+ readonly surface?: UlwLoopToolkitSurface;
13
15
  }): UlwLoopGoalInstruction;
@@ -1,15 +1,17 @@
1
1
  // biome-ignore-all format: long agent-facing instruction strings are intentionally stable.
2
2
  import { codexGoalMode, expectedCodexObjective, isEssentialCriterion, isFinalRunCompletionCandidate, } from "./goal-status.js";
3
+ import { resolveToolkitSurface, reviewerRolesFor } from "./surface.js";
3
4
  export function buildCodexGoalInstruction(args) {
4
5
  const mode = codexGoalMode(args.plan);
5
6
  const createGoal = buildCreateGoalPayload(args.plan, args.goal);
6
7
  const isFinal = args.isFinal ?? isFinalRunCompletionCandidate(args.plan, args.goal);
7
- return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal), json: createGoal };
8
+ const surface = args.surface ?? resolveToolkitSurface();
9
+ return { text: buildText(mode, args.plan, args.goal, createGoal, isFinal, surface), json: createGoal };
8
10
  }
9
11
  function buildCreateGoalPayload(plan, goal) {
10
12
  return { objective: expectedCodexObjective(plan, goal) };
11
13
  }
12
- function buildText(mode, plan, goal, createGoal, isFinal) {
14
+ function buildText(mode, plan, goal, createGoal, isFinal, surface) {
13
15
  return joinLines([
14
16
  mode === "aggregate" ? "UlwLoop aggregate-goal handoff" : "UlwLoop active-goal handoff",
15
17
  `Mode: ${mode}`,
@@ -26,7 +28,7 @@ function buildText(mode, plan, goal, createGoal, isFinal) {
26
28
  "- Goals are unlimited. Do not add numeric limits.",
27
29
  ...modeConstraintLines(mode, isFinal),
28
30
  ...evidenceLayoutLines(plan),
29
- finalSection(plan, goal, isFinal, mode === "aggregate"),
31
+ finalSection(plan, goal, isFinal, mode === "aggregate", surface),
30
32
  ...checkpointLines(plan, mode),
31
33
  "",
32
34
  "create_goal payload:",
@@ -80,7 +82,8 @@ function evidenceLayoutLines(plan) {
80
82
  "- Evidence layout v2: write every artifact for the active goal (QA matrix, review reports, receipts) under the current attempt directory — read currentAttemptDir from `omo-agent-toolkit ulw-loop status --json` (.omo/evidence/ulw/<session>/<goalId>/a<attempt>). The final checkpoint rejects quality-gate artifacts outside that directory.",
81
83
  ];
82
84
  }
83
- function finalSection(plan, goal, isFinal, aggregate) {
85
+ function finalSection(plan, goal, isFinal, aggregate, surface) {
86
+ const roles = reviewerRolesFor(surface);
84
87
  if (!isFinal)
85
88
  return "- This is not the final ulw-loop story; do not run the final reviewer/manual-QA/gate-review quality gate yet.";
86
89
  const option = sessionOption(plan);
@@ -90,8 +93,8 @@ function finalSection(plan, goal, isFinal, aggregate) {
90
93
  "Final story — run mandatory quality gate before update_goal:",
91
94
  "- Run targeted verification for changed behavior.",
92
95
  "- Confirm every manualQa artifact path exists and has non-zero size.",
93
- "- 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).",
94
- "- Only then spawn lazycodex-gate-reviewer (same fork settings), passing those artifact paths.",
96
+ `- 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).`,
97
+ `- Only then spawn ${roles.gateReview} (same fork settings), passing those artifact paths.`,
95
98
  "- Require clean codeReview, manualQa, gateReview, iteration, and criteriaCoverage. criteriaCoverage must summarize originalIntent, desiredOutcome, and userOutcomeReview; counts alone are not approval.",
96
99
  "- 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.",
97
100
  "- If codeQualityStatus is WATCH, include the WATCH notes verbatim in your final user-facing message.",
@@ -1,3 +1,4 @@
1
+ import { type UlwLoopToolkitSurface } from "./surface.js";
1
2
  import type { UlwLoopQualityGate } from "./types.js";
2
3
  export { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, normalizeBlockerEvidence, sameBlockerOccurrences, } from "./quality-gate-blockers.js";
3
4
  export interface QualityGateFs {
@@ -7,8 +8,9 @@ export interface QualityGateFs {
7
8
  };
8
9
  }
9
10
  export interface ValidateQualityGateOptions {
10
- readonly repoRoot: string;
11
- readonly fs: QualityGateFs;
11
+ readonly repoRoot?: string;
12
+ readonly fs?: QualityGateFs;
12
13
  readonly currentAttemptDir?: string;
14
+ readonly reviewerSurface?: UlwLoopToolkitSurface;
13
15
  }
14
16
  export declare function validateQualityGate(input: unknown, opts?: ValidateQualityGateOptions): UlwLoopQualityGate;
@@ -2,11 +2,7 @@ import { resolve } from "node:path";
2
2
  import { isWithinAttemptDir } from "./paths.js";
3
3
  import { emptyBlockers, invalid, literal, numberField, section, stringArray, textField, } from "./quality-gate-fields.js";
4
4
  import { adversarialVerdict, codeQualityStatusField, passedVerdict } from "./quality-gate-verdicts.js";
5
- const REVIEWER_ROLES = {
6
- codeReview: "lazycodex-code-reviewer",
7
- manualQa: "lazycodex-qa-executor",
8
- gateReview: "lazycodex-gate-reviewer",
9
- };
5
+ import { reviewerRolesFor } from "./surface.js";
10
6
  export { classifyExternalAuthorizationBlocker, clearGoalBlockerFields, normalizeBlockerEvidence, sameBlockerOccurrences, } from "./quality-gate-blockers.js";
11
7
  function reviewerRoleField(value, expected, field) {
12
8
  const actual = textField(value, field);
@@ -51,14 +47,14 @@ function artifactCompatible(surface, kind) {
51
47
  }
52
48
  }
53
49
  function checkFile(path, field, opts) {
54
- if (opts === undefined)
50
+ if (opts?.repoRoot === undefined || opts.fs === undefined)
55
51
  return;
56
52
  const absolute = resolve(opts.repoRoot, path);
57
53
  if (!opts.fs.existsSync(absolute))
58
54
  invalid(`${field} must point to an existing artifact.`, field);
59
55
  if (opts.fs.statSync(absolute).size <= 0)
60
56
  invalid(`${field} must point to a non-empty artifact.`, field);
61
- if (opts.currentAttemptDir !== undefined) {
57
+ if (opts.currentAttemptDir !== undefined && opts.repoRoot !== undefined) {
62
58
  const attemptRoot = resolve(opts.repoRoot, opts.currentAttemptDir);
63
59
  if (!isWithinAttemptDir(absolute, attemptRoot))
64
60
  invalid(`${field} (${path}) must point to an artifact from the current attempt (${opts.currentAttemptDir}).`, field);
@@ -97,6 +93,7 @@ function referencedArtifacts(value, field, byId) {
97
93
  });
98
94
  }
99
95
  export function validateQualityGate(input, opts) {
96
+ const reviewerRoles = reviewerRolesFor(opts?.reviewerSurface ?? "lazycodex");
100
97
  const gate = section(input, "qualityGate");
101
98
  const codeReview = section(gate["codeReview"], "codeReview");
102
99
  const manualQa = section(gate["manualQa"], "manualQa");
@@ -117,7 +114,7 @@ export function validateQualityGate(input, opts) {
117
114
  checkFile(gateReportPath, "gateReview.reportPath", opts);
118
115
  return {
119
116
  codeReview: {
120
- by: reviewerRoleField(codeReview["by"], REVIEWER_ROLES.codeReview, "codeReview.by"),
117
+ by: reviewerRoleField(codeReview["by"], reviewerRoles.codeReview, "codeReview.by"),
121
118
  recommendation: literal(codeReview["recommendation"], "APPROVE", "codeReview.recommendation"),
122
119
  codeQualityStatus: codeQualityStatusField(codeReview["codeQualityStatus"], "codeReview.codeQualityStatus"),
123
120
  reportPath: codeReportPath,
@@ -125,7 +122,7 @@ export function validateQualityGate(input, opts) {
125
122
  blockers: emptyBlockers(codeReview["blockers"], "codeReview.blockers"),
126
123
  },
127
124
  manualQa: {
128
- by: reviewerRoleField(manualQa["by"], REVIEWER_ROLES.manualQa, "manualQa.by"),
125
+ by: reviewerRoleField(manualQa["by"], reviewerRoles.manualQa, "manualQa.by"),
129
126
  status: literal(manualQa["status"], "passed", "manualQa.status"),
130
127
  evidence: textField(manualQa["evidence"], "manualQa.evidence"),
131
128
  surfaceEvidence,
@@ -133,7 +130,7 @@ export function validateQualityGate(input, opts) {
133
130
  artifactRefs,
134
131
  },
135
132
  gateReview: {
136
- by: reviewerRoleField(gateReview["by"], REVIEWER_ROLES.gateReview, "gateReview.by"),
133
+ by: reviewerRoleField(gateReview["by"], reviewerRoles.gateReview, "gateReview.by"),
137
134
  recommendation: literal(gateReview["recommendation"], "APPROVE", "gateReview.recommendation"),
138
135
  reportPath: gateReportPath,
139
136
  evidence: textField(gateReview["evidence"], "gateReview.evidence"),
@@ -3,12 +3,13 @@ import { join } from "node:path";
3
3
  import { parsePreToolUsePayload } from "./codex-hook.js";
4
4
  import { isFinalRunCompletionCandidate } from "./goal-status.js";
5
5
  import { ulwLoopAttemptEvidenceDir, ulwLoopDir } from "./paths.js";
6
+ import { GATE_REVIEWER_AGENT_NAMES } from "./surface.js";
6
7
  // spawn_agent = v1; collaborationspawn_agent = the delimiter-free flattened v2
7
8
  // hook token from codex-rs hook_names.rs; collaboration.spawn_agent = the
8
9
  // dotted token observed live in the task-1 probe (hook-tool-tokens.txt).
9
10
  const SPAWN_TOOL_TOKENS = new Set(["spawn_agent", "collaborationspawn_agent", "collaboration.spawn_agent"]);
10
11
  const DEFAULT_FANOUT_LIMIT = 60;
11
- const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|final gate review/i;
12
+ const GATE_MESSAGE_PATTERN = /lazycodex-gate-reviewer|omo-senpi-gate-reviewer|final gate review/i;
12
13
  export function applySpawnGuards(payload) {
13
14
  if (payload.hook_event_name !== "PreToolUse" || !SPAWN_TOOL_TOKENS.has(payload.tool_name))
14
15
  return "";
@@ -84,7 +85,7 @@ function isGateReviewerSpawn(toolInput) {
84
85
  const record = toolInput;
85
86
  const agentType = record["agent_type"];
86
87
  if (typeof agentType === "string")
87
- return agentType === "lazycodex-gate-reviewer";
88
+ return GATE_REVIEWER_AGENT_NAMES.has(agentType);
88
89
  const message = record["message"];
89
90
  return typeof message === "string" && GATE_MESSAGE_PATTERN.test(message);
90
91
  }
@@ -0,0 +1,15 @@
1
+ export type UlwLoopToolkitSurface = "lazycodex" | "omo-senpi";
2
+ export interface UlwLoopReviewerRoles {
3
+ readonly codeReview: string;
4
+ readonly manualQa: string;
5
+ readonly gateReview: string;
6
+ }
7
+ export declare const REVIEWER_ROLES_BY_SURFACE: Readonly<Record<UlwLoopToolkitSurface, UlwLoopReviewerRoles>>;
8
+ export declare const GATE_REVIEWER_AGENT_NAMES: ReadonlySet<string>;
9
+ export declare function reviewerRolesFor(surface: UlwLoopToolkitSurface): UlwLoopReviewerRoles;
10
+ export declare const SURFACE_MARKER_FILENAME = "surface.json";
11
+ export interface ResolveToolkitSurfaceOptions {
12
+ readonly env?: Readonly<Record<string, string | undefined>>;
13
+ readonly entryDir?: string;
14
+ }
15
+ export declare function resolveToolkitSurface(options?: ResolveToolkitSurfaceOptions): UlwLoopToolkitSurface;