@opengsd/gsd-core 1.7.0 → 1.9.0

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 (261) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.opencode/plugins/gsd-core.js +45 -1
  4. package/README.md +2 -0
  5. package/agents/gsd-code-fixer.md +1 -1
  6. package/agents/gsd-codebase-mapper.md +1 -1
  7. package/agents/gsd-debug-session-manager.md +78 -4
  8. package/agents/gsd-debugger.md +87 -29
  9. package/agents/gsd-executor.md +49 -9
  10. package/agents/gsd-intel-updater.md +3 -3
  11. package/agents/gsd-phase-researcher.md +4 -2
  12. package/agents/gsd-plan-checker.md +20 -0
  13. package/agents/gsd-planner.md +44 -59
  14. package/agents/gsd-project-researcher.md +2 -2
  15. package/agents/gsd-ui-auditor.md +0 -40
  16. package/agents/gsd-verifier.md +2 -2
  17. package/bin/install.js +1338 -135
  18. package/commands/gsd/ai-integration-phase.md +1 -1
  19. package/commands/gsd/mempalace-capture.md +9 -5
  20. package/commands/gsd/new-milestone.md +1 -1
  21. package/commands/gsd/plan-phase.md +5 -3
  22. package/commands/gsd/plan-review-convergence.md +7 -2
  23. package/gsd-core/bin/gsd-tools.cjs +2690 -2472
  24. package/gsd-core/bin/lib/adapter-imperative.cjs +8 -1
  25. package/gsd-core/bin/lib/agent-command-router.cjs +20 -5
  26. package/gsd-core/bin/lib/api-coverage.cjs +360 -53
  27. package/gsd-core/bin/lib/audit.cjs +8 -8
  28. package/gsd-core/bin/lib/broken-windows.cjs +716 -0
  29. package/gsd-core/bin/lib/capability-command-router.cjs +733 -0
  30. package/gsd-core/bin/lib/capability-consent.cjs +40 -1
  31. package/gsd-core/bin/lib/capability-lifecycle.cjs +58 -0
  32. package/gsd-core/bin/lib/capability-loader.cjs +23 -1
  33. package/gsd-core/bin/lib/capability-registry.cjs +1450 -160
  34. package/gsd-core/bin/lib/capability-trust.cjs +468 -33
  35. package/gsd-core/bin/lib/capability-validator.cjs +882 -6
  36. package/gsd-core/bin/lib/capability-writer.cjs +6 -1
  37. package/gsd-core/bin/lib/check-command-router.cjs +140 -27
  38. package/gsd-core/bin/lib/cjs-command-router-adapter.cjs +15 -0
  39. package/gsd-core/bin/lib/claude-orchestration-command-router.cjs +209 -31
  40. package/gsd-core/bin/lib/claude-orchestration.cjs +203 -25
  41. package/gsd-core/bin/lib/command-aliases.cjs +14 -0
  42. package/gsd-core/bin/lib/commands.cjs +326 -21
  43. package/gsd-core/bin/lib/config-loader.cjs +214 -30
  44. package/gsd-core/bin/lib/config.cjs +158 -22
  45. package/gsd-core/bin/lib/core-utils.cjs +6 -1
  46. package/gsd-core/bin/lib/decisions.cjs +32 -8
  47. package/gsd-core/bin/lib/docs.cjs +6 -0
  48. package/gsd-core/bin/lib/estimate-cli.cjs +336 -0
  49. package/gsd-core/bin/lib/external-descriptor-trust.cjs +14 -2
  50. package/gsd-core/bin/lib/frontmatter.cjs +125 -15
  51. package/gsd-core/bin/lib/gap-checker.cjs +17 -2
  52. package/gsd-core/bin/lib/host-integration.cjs +215 -8
  53. package/gsd-core/bin/lib/init.cjs +155 -66
  54. package/gsd-core/bin/lib/install-engine.cjs +299 -23
  55. package/gsd-core/bin/lib/install-profiles.cjs +239 -1
  56. package/gsd-core/bin/lib/installer-migrations/005-opencode-baseline-commands-dir.cjs +146 -0
  57. package/gsd-core/bin/lib/installer-migrations/006-pi-extension-cjs-to-js.cjs +91 -0
  58. package/gsd-core/bin/lib/installer-migrations.cjs +44 -5
  59. package/gsd-core/bin/lib/markdown-sectionizer.cjs +107 -0
  60. package/gsd-core/bin/lib/milestone.cjs +248 -14
  61. package/gsd-core/bin/lib/model-catalog.cjs +69 -4
  62. package/gsd-core/bin/lib/model-resolver.cjs +189 -7
  63. package/gsd-core/bin/lib/observability/logger.cjs +7 -2
  64. package/gsd-core/bin/lib/onboard-projection.cjs +11 -8
  65. package/gsd-core/bin/lib/phase-command-router.cjs +10 -1
  66. package/gsd-core/bin/lib/phase-estimation.cjs +398 -0
  67. package/gsd-core/bin/lib/phase-id.cjs +304 -9
  68. package/gsd-core/bin/lib/phase.cjs +258 -17
  69. package/gsd-core/bin/lib/plan-drift-guard.cjs +1 -1
  70. package/gsd-core/bin/lib/plan-scan.cjs +70 -2
  71. package/gsd-core/bin/lib/planning-workspace.cjs +9 -2
  72. package/gsd-core/bin/lib/profile-output.cjs +34 -8
  73. package/gsd-core/bin/lib/review-lane-descriptor.cjs +927 -0
  74. package/gsd-core/bin/lib/review-lane-invocation.cjs +348 -0
  75. package/gsd-core/bin/lib/review-lane-runner.cjs +594 -0
  76. package/gsd-core/bin/lib/review-reviewer-selection.cjs +114 -32
  77. package/gsd-core/bin/lib/roadmap-parser.cjs +61 -10
  78. package/gsd-core/bin/lib/roadmap.cjs +23 -7
  79. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +38 -5
  80. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +23 -9
  81. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +156 -0
  82. package/gsd-core/bin/lib/runtime-name-policy.cjs +15 -2
  83. package/gsd-core/bin/lib/smart-entry.cjs +70 -5
  84. package/gsd-core/bin/lib/state-document.cjs +171 -24
  85. package/gsd-core/bin/lib/state-transition.cjs +50 -11
  86. package/gsd-core/bin/lib/state.cjs +206 -32
  87. package/gsd-core/bin/lib/surface.cjs +51 -9
  88. package/gsd-core/bin/lib/uat-predicate.cjs +6 -4
  89. package/gsd-core/bin/lib/uat.cjs +428 -11
  90. package/gsd-core/bin/lib/ui-consideration-probe.cjs +2 -2
  91. package/gsd-core/bin/lib/unusable-input.cjs +216 -0
  92. package/gsd-core/bin/lib/validate.cjs +44 -8
  93. package/gsd-core/bin/lib/verification.cjs +163 -31
  94. package/gsd-core/bin/lib/verify.cjs +348 -42
  95. package/gsd-core/bin/lib/worktree-safety.cjs +360 -15
  96. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  97. package/gsd-core/bin/shared/config-schema.manifest.json +4 -15
  98. package/gsd-core/bin/shared/model-catalog.json +5 -0
  99. package/gsd-core/bin/shared/runtime-aliases.manifest.json +5 -0
  100. package/gsd-core/references/api-coverage.md +37 -7
  101. package/gsd-core/references/checkpoints.md +1 -1
  102. package/gsd-core/references/common-bug-patterns.md +13 -0
  103. package/gsd-core/references/context-budget.md +40 -0
  104. package/gsd-core/references/debugger-bug-taxonomy.md +111 -0
  105. package/gsd-core/references/debugger-fix-acceptance.md +157 -0
  106. package/gsd-core/references/debugger-philosophy.md +1 -0
  107. package/gsd-core/references/debugger-prevention.md +98 -0
  108. package/gsd-core/references/debugger-rca-branching.md +98 -0
  109. package/gsd-core/references/debugger-repro-hardening.md +130 -0
  110. package/gsd-core/references/debugger-sbfl.md +110 -0
  111. package/gsd-core/references/debugger-semantic-recall.md +81 -0
  112. package/gsd-core/references/execute-phase-quota-recovery.md +55 -0
  113. package/gsd-core/references/execute-phase-requirement-revert.md +8 -0
  114. package/gsd-core/references/execute-phase-response-language.md +7 -0
  115. package/gsd-core/references/gate-prompts.md +6 -3
  116. package/gsd-core/references/model-profile-resolution.md +64 -13
  117. package/gsd-core/references/offer-next.md +88 -0
  118. package/gsd-core/references/planner-antipatterns.md +6 -0
  119. package/gsd-core/references/planner-mvp-mode.md +12 -13
  120. package/gsd-core/references/planner-preconditions.md +156 -0
  121. package/gsd-core/references/planner-reversibility.md +132 -0
  122. package/gsd-core/references/planning-config.md +2 -1
  123. package/gsd-core/references/reviewer-instances.md +28 -19
  124. package/gsd-core/references/runtime-aware-dispatch.md +42 -0
  125. package/gsd-core/references/skeleton-template.md +1 -1
  126. package/gsd-core/references/thinking-models-planning.md +3 -1
  127. package/gsd-core/references/ui-consideration-probe.md +2 -2
  128. package/gsd-core/references/worktree-branch-check.md +4 -4
  129. package/gsd-core/templates/DEBUG.md +5 -3
  130. package/gsd-core/templates/summary-minimal.md +4 -0
  131. package/gsd-core/templates/summary-standard.md +4 -0
  132. package/gsd-core/templates/summary.md +7 -0
  133. package/gsd-core/workflows/add-phase.md +2 -0
  134. package/gsd-core/workflows/add-tests.md +3 -1
  135. package/gsd-core/workflows/add-todo.md +32 -1
  136. package/gsd-core/workflows/ai-integration-phase.md +8 -6
  137. package/gsd-core/workflows/audit-fix.md +6 -2
  138. package/gsd-core/workflows/audit-milestone.md +8 -0
  139. package/gsd-core/workflows/autonomous.md +19 -15
  140. package/gsd-core/workflows/check-todos.md +5 -3
  141. package/gsd-core/workflows/cleanup.md +7 -1
  142. package/gsd-core/workflows/code-review-fix.md +14 -6
  143. package/gsd-core/workflows/code-review.md +93 -24
  144. package/gsd-core/workflows/complete-milestone.md +3 -0
  145. package/gsd-core/workflows/debug.md +35 -7
  146. package/gsd-core/workflows/diagnose-issues.md +5 -1
  147. package/gsd-core/workflows/discovery-phase.md +7 -0
  148. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -4
  149. package/gsd-core/workflows/discuss-phase/modes/auto.md +0 -6
  150. package/gsd-core/workflows/discuss-phase/templates/context.md +16 -2
  151. package/gsd-core/workflows/discuss-phase-assumptions.md +18 -9
  152. package/gsd-core/workflows/discuss-phase.md +2 -2
  153. package/gsd-core/workflows/do.md +7 -1
  154. package/gsd-core/workflows/docs-update.md +9 -0
  155. package/gsd-core/workflows/eval-review.md +4 -1
  156. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +4 -0
  157. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +160 -0
  158. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +4 -4
  159. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +2 -2
  160. package/gsd-core/workflows/execute-phase.md +110 -149
  161. package/gsd-core/workflows/execute-plan.md +20 -8
  162. package/gsd-core/workflows/explore.md +4 -0
  163. package/gsd-core/workflows/extract-learnings.md +21 -0
  164. package/gsd-core/workflows/graduation.md +3 -0
  165. package/gsd-core/workflows/health.md +7 -1
  166. package/gsd-core/workflows/help/modes/full.md +9 -5
  167. package/gsd-core/workflows/import.md +11 -2
  168. package/gsd-core/workflows/inbox.md +7 -0
  169. package/gsd-core/workflows/ingest-docs.md +19 -10
  170. package/gsd-core/workflows/manager.md +3 -1
  171. package/gsd-core/workflows/map-codebase.md +17 -10
  172. package/gsd-core/workflows/mvp-phase.md +3 -0
  173. package/gsd-core/workflows/new-milestone.md +79 -23
  174. package/gsd-core/workflows/new-project.md +28 -19
  175. package/gsd-core/workflows/new-workspace.md +3 -1
  176. package/gsd-core/workflows/next.md +5 -2
  177. package/gsd-core/workflows/onboard.md +3 -0
  178. package/gsd-core/workflows/plan-phase.md +56 -51
  179. package/gsd-core/workflows/plan-review-convergence.md +61 -12
  180. package/gsd-core/workflows/plant-seed.md +3 -0
  181. package/gsd-core/workflows/profile-user.md +7 -1
  182. package/gsd-core/workflows/progress.md +31 -3
  183. package/gsd-core/workflows/quick.md +33 -10
  184. package/gsd-core/workflows/remove-workspace.md +3 -0
  185. package/gsd-core/workflows/review.md +172 -585
  186. package/gsd-core/workflows/scan.md +10 -2
  187. package/gsd-core/workflows/secure-phase.md +13 -2
  188. package/gsd-core/workflows/settings-integrations.md +3 -0
  189. package/gsd-core/workflows/settings.md +3 -0
  190. package/gsd-core/workflows/ship.md +88 -11
  191. package/gsd-core/workflows/sketch.md +3 -0
  192. package/gsd-core/workflows/smart-entry.md +4 -1
  193. package/gsd-core/workflows/spike.md +7 -1
  194. package/gsd-core/workflows/ui-phase.md +11 -2
  195. package/gsd-core/workflows/ui-review.md +11 -1
  196. package/gsd-core/workflows/undo.md +7 -0
  197. package/gsd-core/workflows/update.md +106 -5
  198. package/gsd-core/workflows/validate-phase.md +13 -2
  199. package/gsd-core/workflows/verify-phase.md +2 -2
  200. package/gsd-core/workflows/verify-work.md +15 -4
  201. package/hooks/dist/gsd-context-monitor.js +27 -9
  202. package/hooks/dist/gsd-cursor-session-start.js +6 -2
  203. package/hooks/dist/gsd-cursor-stop.js +6 -2
  204. package/hooks/dist/gsd-cursor-subagent-start.js +6 -2
  205. package/hooks/dist/gsd-graphify-update.sh +9 -0
  206. package/hooks/dist/gsd-phase-boundary.sh +14 -2
  207. package/hooks/dist/gsd-prompt-guard.js +101 -2
  208. package/hooks/dist/gsd-read-guard.js +100 -2
  209. package/hooks/dist/gsd-read-injection-scanner.js +109 -2
  210. package/hooks/dist/gsd-statusline.js +97 -9
  211. package/hooks/dist/gsd-workflow-guard.js +110 -6
  212. package/hooks/dist/gsd-worktree-path-guard.js +132 -8
  213. package/hooks/dist/lib/cursor-workspace.js +74 -0
  214. package/hooks/gsd-context-monitor.js +27 -9
  215. package/hooks/gsd-cursor-session-start.js +6 -2
  216. package/hooks/gsd-cursor-stop.js +6 -2
  217. package/hooks/gsd-cursor-subagent-start.js +6 -2
  218. package/hooks/gsd-graphify-update.sh +9 -0
  219. package/hooks/gsd-phase-boundary.sh +14 -2
  220. package/hooks/gsd-prompt-guard.js +101 -2
  221. package/hooks/gsd-read-guard.js +100 -2
  222. package/hooks/gsd-read-injection-scanner.js +109 -2
  223. package/hooks/gsd-statusline.js +97 -9
  224. package/hooks/gsd-workflow-guard.js +110 -6
  225. package/hooks/gsd-worktree-path-guard.js +132 -8
  226. package/hooks/lib/cursor-workspace.js +74 -0
  227. package/package.json +10 -8
  228. package/pi/gsd.cjs +34 -3
  229. package/scripts/changeset/lint.cjs +1 -0
  230. package/scripts/changeset/parse.cjs +26 -0
  231. package/scripts/check-coverage-gate.cjs +51 -0
  232. package/scripts/check-glossary-refs.cjs +244 -0
  233. package/scripts/ci-rebase-check.cjs +48 -4
  234. package/scripts/ci-test-scope.cjs +67 -17
  235. package/scripts/gen-adr-index.cjs +528 -0
  236. package/scripts/gen-capability-matrix.cjs +26 -2
  237. package/scripts/gen-capability-registry.cjs +132 -34
  238. package/scripts/gen-emitted-baseline.cjs +145 -0
  239. package/scripts/gen-test-timings.cjs +201 -0
  240. package/scripts/lint-compiled-artifact-sync.cjs +146 -0
  241. package/scripts/lint-emitted-drift-ack.cjs +149 -0
  242. package/scripts/lint-fix-has-regression-test.cjs +131 -0
  243. package/scripts/lint-portable-timeout.cjs +140 -0
  244. package/scripts/lint-resolution-provenance.cjs +9 -0
  245. package/scripts/lint-test-file-count.allowlist.json +1 -0
  246. package/scripts/mutation-matrix.cjs +4 -0
  247. package/scripts/prompt-injection-scan.sh +6 -0
  248. package/scripts/registry-schema.cjs +57 -8
  249. package/scripts/release-notes/conventional-title.cjs +19 -1
  250. package/scripts/release-notes/format-github-release-notes.cjs +7 -3
  251. package/scripts/release-tarball-smoke.cjs +18 -11
  252. package/scripts/run-tests.cjs +420 -58
  253. package/scripts/workflow-size.cjs +16 -8
  254. package/skills/gsd-ai-integration-phase/SKILL.md +1 -1
  255. package/skills/gsd-mempalace-capture/SKILL.md +9 -5
  256. package/skills/gsd-new-milestone/SKILL.md +1 -1
  257. package/skills/gsd-plan-phase/SKILL.md +5 -3
  258. package/skills/gsd-plan-review-convergence/SKILL.md +7 -2
  259. package/vscode/package.json +1 -1
  260. package/scripts/gen-golden-install-parity-zcode.cjs +0 -77
  261. package/scripts/update-size-baseline.cjs +0 -68
@@ -0,0 +1,594 @@
1
+ "use strict";
2
+ /**
3
+ * Reviewer Lane Runner (ADR-2782 Phase 5b, #2799).
4
+ *
5
+ * Executes an invocation plan from `review-lane-invocation.cts`: probes the lane, spawns the binary
6
+ * or calls the HTTP endpoint, applies the empty-output policy, and dispatches the three first-party
7
+ * handlers D6 names. This is the module that replaces ~640 lines of hand-authored per-CLI bash.
8
+ *
9
+ * THREE RUNTIME DEPENDENCIES DISAPPEAR HERE, and that is a correctness win rather than a tidy-up:
10
+ * - `jq` — five legs needed it on PATH; it is absent on stock Windows/Git-Bash (#2589). Parsing is
11
+ * `JSON.parse` now.
12
+ * - `curl` — the three OpenAI-compatible legs shelled out to it. Node's `fetch` replaces it, and
13
+ * the raw response body (where an OpenAI-compatible server puts its error JSON on a 4xx/5xx
14
+ * while curl still exits 0) is no longer discarded by a pipe.
15
+ * - external `timeout` / `gtimeout` — the Antigravity leg probed for one because
16
+ * `--print-timeout` cannot fire before a session exists, and STOCK MACOS SHIPS NEITHER, so on a
17
+ * stock Mac that leg ran unbounded. `spawnSync`'s native `timeout` is always available, so D7's
18
+ * "skip the probe where no bounding mechanism exists" carve-out is no longer needed.
19
+ *
20
+ * EVERY subprocess call in this file passes `timeout` + `killSignal` + `maxBuffer`. This repo has a
21
+ * named `DEFECT.UNBOUNDED-SUBPROCESS` class (`CONTEXT.md:772`) and an unbounded sync spawn is not a
22
+ * slow test, it is a hung one: `--test-force-exit` cannot interrupt a synchronous call, so a frozen
23
+ * spawn hangs the whole CI chunk to its 10-minute kill with `# fail 0` and no `not ok` (#2099).
24
+ *
25
+ * TOTAL. No function here throws for a lane-level failure; every one returns a typed outcome. A
26
+ * reviewer lane that cannot run must produce a diagnosable artifact, because the ambiguity between
27
+ * "failed" and "ran cleanly with nothing to report" IS the defect this epic closes (#2494/#2605).
28
+ */
29
+ Object.defineProperty(exports, "__esModule", { value: true });
30
+ exports.checkEgressHost = checkEgressHost;
31
+ exports.probeLane = probeLane;
32
+ exports.writeReviewOrStub = writeReviewOrStub;
33
+ exports.handleOpencodeOutput = handleOpencodeOutput;
34
+ exports.antigravityWatermark = antigravityWatermark;
35
+ exports.antigravityTranscriptFallback = antigravityTranscriptFallback;
36
+ exports.antigravityPrompt = antigravityPrompt;
37
+ exports.antigravityArgv = antigravityArgv;
38
+ exports.antigravityDiagnostic = antigravityDiagnostic;
39
+ exports.stampBlindReview = stampBlindReview;
40
+ exports.runOpenAiCompatible = runOpenAiCompatible;
41
+ exports.runLane = runLane;
42
+ const review_lane_invocation_cjs_1 = require("./review-lane-invocation.cjs");
43
+ /**
44
+ * Compare a lane's re-resolved egress destination against the one the user consented to.
45
+ *
46
+ * ADR-2782 D5 puts this on the invocation path deliberately. `hostConfigKey` names a key in
47
+ * `.planning/config.json`, which is the ONE consent-bound value living outside the SHA-pinned
48
+ * bundle: user- and CI-editable at any time, with no re-install and no integrity check. Without
49
+ * this check a lane consented against `http://localhost:8080` could be silently redirected to a
50
+ * remote host by an ordinary pull request touching a JSON file, and every subsequent review would
51
+ * egress plans, requirements, research and decisions to the new destination with no re-prompt.
52
+ *
53
+ * ABSENCE IS NOT A MISMATCH, and this is the part that must not be "hardened" later by someone who
54
+ * reads only the rule name:
55
+ * - No consent record at all ⇒ ALLOW. First-party lanes (`ollama`, `lm_studio`, `llama_cpp`) ship
56
+ * inside the SHA-pinned distribution and are never consent-gated, so blocking on a missing
57
+ * record would break every existing local-model user on upgrade.
58
+ * - A consent record predating this feature ⇒ ALLOW. Those records were written before a host was
59
+ * recorded; treating the absence as a change would force spurious re-consent across every
60
+ * installed capability, which ADR-2782 D4 rule 5 explicitly forbids.
61
+ *
62
+ * Hosts are compared NORMALIZED, so a trailing slash or an explicit `:80` is not a "destination
63
+ * change". A warning that fires on cosmetic edits is a warning users learn to dismiss.
64
+ */
65
+ function checkEgressHost(consentedHost, currentHost) {
66
+ const consented = typeof consentedHost === 'string' ? (0, review_lane_invocation_cjs_1.normalizeHost)(consentedHost) : '';
67
+ if (!consented)
68
+ return { allowed: true };
69
+ const current = (0, review_lane_invocation_cjs_1.normalizeHost)(currentHost);
70
+ if (consented === current)
71
+ return { allowed: true, consentedHost: consented, currentHost: current };
72
+ return { allowed: false, consentedHost: consented, currentHost: current };
73
+ }
74
+ /* ------------------------------------------------------------------ *
75
+ * Probe (D7) — every probe bounded, and only for a SELECTED lane
76
+ * ------------------------------------------------------------------ */
77
+ async function probeLane(plan, deps) {
78
+ // Prerequisites first: a missing `jq`-style helper is a clearer failure than whatever the tool
79
+ // does without it, and reporting it costs nothing.
80
+ for (const bin of plan.requiresBinaries) {
81
+ if (!deps.hasBinary(bin)) {
82
+ return {
83
+ available: false,
84
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_REQUIRED_BINARY,
85
+ detail: `lane '${plan.slug}' requires '${bin}' on PATH`,
86
+ };
87
+ }
88
+ }
89
+ const probe = plan.probe;
90
+ if (!probe || typeof probe !== 'object') {
91
+ return { available: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED, detail: 'lane declares no probe' };
92
+ }
93
+ switch (probe.kind) {
94
+ case 'command-exists':
95
+ return deps.hasBinary(probe.binary)
96
+ ? { available: true }
97
+ : {
98
+ available: false,
99
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_BINARY,
100
+ detail: `'${probe.binary}' not found on PATH`,
101
+ };
102
+ case 'command-capability': {
103
+ // Existence alone is structurally insufficient here: `kimi` is claimed by BOTH the Kimi Code
104
+ // CLI and the legacy Python kimi-cli, and an existence-only probe registers the wrong tool.
105
+ if (!deps.hasBinary(probe.binary)) {
106
+ return {
107
+ available: false,
108
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.MISSING_BINARY,
109
+ detail: `'${probe.binary}' not found on PATH`,
110
+ };
111
+ }
112
+ const out = deps.spawn(probe.binary, ['--help'], { timeoutMs: probe.timeoutMs });
113
+ if (out.errorCode === 'ETIMEDOUT') {
114
+ return {
115
+ available: false,
116
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_TIMEOUT,
117
+ detail: `'${probe.binary} --help' exceeded ${probe.timeoutMs}ms`,
118
+ };
119
+ }
120
+ const help = `${out.stdout}\n${out.stderr}`;
121
+ return help.includes(probe.needle)
122
+ ? { available: true }
123
+ : {
124
+ available: false,
125
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED,
126
+ detail: `'${probe.binary}' is on PATH but its --help lacks '${probe.needle}' — this is a different tool sharing the name`,
127
+ };
128
+ }
129
+ case 'http-reachable': {
130
+ // Only a STRING config value names a host. `String(unknown)` would turn an object into the
131
+ // literal '[object Object]' and probe that as a URL — a nonsense destination reported as
132
+ // "unreachable" rather than as the misconfiguration it is.
133
+ const configured = deps.configGet(probe.hostConfigKey);
134
+ const base = (0, review_lane_invocation_cjs_1.normalizeHost)((typeof configured === 'string' ? configured : '') ||
135
+ (plan.transport === 'openai-http' ? plan.host : ''));
136
+ if (!base) {
137
+ return { available: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.HOST_UNREACHABLE, detail: 'no host resolved' };
138
+ }
139
+ const r = await deps.httpJson(`${base}${probe.path}`, { method: 'GET', timeoutMs: probe.timeoutMs });
140
+ return r.ok
141
+ ? { available: true }
142
+ : {
143
+ available: false,
144
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.HOST_UNREACHABLE,
145
+ detail: `${base}${probe.path} unreachable: ${r.error ?? `HTTP ${r.status}`}`,
146
+ };
147
+ }
148
+ default:
149
+ return {
150
+ available: false,
151
+ reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.PROBE_FAILED,
152
+ detail: `unknown probe kind '${String(probe.kind)}'`,
153
+ };
154
+ }
155
+ }
156
+ /* ------------------------------------------------------------------ *
157
+ * Empty-output policy (#2494 / #2605 / #2794)
158
+ * ------------------------------------------------------------------ */
159
+ /**
160
+ * Write the review, or a diagnostic stub when the lane produced nothing usable.
161
+ *
162
+ * The stub is not cosmetic. Before #2494/#2605 a failed lane left a zero-byte file, `write_reviews`
163
+ * rendered it as "a reviewer that ran cleanly with nothing to report", and the lane vanished from
164
+ * the cross-AI consensus while `present_results` reported success — a review blind in one eye. The
165
+ * stub keeps its "failed or returned empty output" header precisely so it can never be mistaken for
166
+ * a real review.
167
+ *
168
+ * `extraDiagnostics` carries the raw HTTP response body for the OpenAI-compatible lanes: an error
169
+ * from such a server arrives with HTTP 4xx/5xx and the JSON in the BODY, so stderr alone is empty
170
+ * and the body is the only evidence.
171
+ */
172
+ function writeReviewOrStub(plan, content, deps, extraDiagnostics) {
173
+ if (!(0, review_lane_invocation_cjs_1.isEmptyReview)(content)) {
174
+ deps.writeFile(plan.reviewPath, content.endsWith('\n') ? content : `${content}\n`);
175
+ return { stubbed: false };
176
+ }
177
+ const stderr = deps.exists(plan.errPath) ? deps.readFile(plan.errPath) : '';
178
+ const parts = [`${plan.slug} review failed or returned empty output. stderr:`, stderr];
179
+ if (extraDiagnostics)
180
+ parts.push('Raw response body:', extraDiagnostics);
181
+ deps.writeFile(plan.reviewPath, `${parts.join('\n')}\n`);
182
+ return { stubbed: true };
183
+ }
184
+ /* ------------------------------------------------------------------ *
185
+ * Handlers (D6) — named first-party code, never conditionals in data
186
+ * ------------------------------------------------------------------ */
187
+ /**
188
+ * `opencode` — reconstruct the review from the assistant `text` parts of a `--format json` stream.
189
+ *
190
+ * `--format json` is the PRIMARY invocation, not a fallback: OpenCode's default `build` agent is an
191
+ * agentic coder, and on a large review prompt it may run a few `read` tool calls then end its turn
192
+ * with ZERO output tokens, so `--format default` yields empty stdout and the reviewer is silently
193
+ * lost (#1936). When no assistant text was emitted, surface the stop reason and output-token count
194
+ * so the failure is diagnosable rather than a generic empty stub.
195
+ *
196
+ * The stream is JSONL-ish: one JSON value per line. A line that does not parse is SKIPPED rather
197
+ * than failing the lane — a partial stream still carries usable review text, and losing the whole
198
+ * review to one malformed line would be strictly worse than the bug this handler exists to fix.
199
+ */
200
+ function handleOpencodeOutput(rawStdout) {
201
+ const texts = [];
202
+ let stopReason = '?';
203
+ let outputTokens = '?';
204
+ for (const line of String(rawStdout ?? '').split(/\r?\n/)) {
205
+ const trimmed = line.trim();
206
+ if (!trimmed)
207
+ continue;
208
+ let evt;
209
+ try {
210
+ evt = JSON.parse(trimmed);
211
+ }
212
+ catch {
213
+ continue;
214
+ }
215
+ if (evt === null || typeof evt !== 'object')
216
+ continue;
217
+ const e = evt;
218
+ const part = (e.part ?? {});
219
+ if (e.type === 'text' && typeof part.text === 'string') {
220
+ // An EMPTY string is kept, not skipped. The shipped jq was `.part.text // empty`, and `//`
221
+ // only substitutes for `false`/`null` — an empty string is truthy in jq, so it survived and
222
+ // contributed a blank line. Dropping it here would quietly close up blank lines between
223
+ // parts, which is a real (if cosmetic) divergence from the behaviour being ported.
224
+ texts.push(part.text);
225
+ }
226
+ else if (e.type === 'step_finish') {
227
+ if (typeof part.reason === 'string')
228
+ stopReason = part.reason;
229
+ const tokens = (part.tokens ?? {});
230
+ if (typeof tokens.output === 'number')
231
+ outputTokens = String(tokens.output);
232
+ }
233
+ }
234
+ return {
235
+ review: texts.join('\n'),
236
+ diagnostic: `stop reason=${stopReason}, output tokens=${outputTokens}`,
237
+ };
238
+ }
239
+ /**
240
+ * `antigravity` — three layers, a two-level timeout, and a stale-response watermark.
241
+ *
242
+ * Layer 1 is stdout, which works on macOS/Linux/WSL. On native Windows `agy -p` silently produces
243
+ * no stdout despite the API call succeeding (an upstream `text_drip.go` non-TTY flush bug), so
244
+ * layer 2 reads the transcript `agy` always persists to disk. Layer 3 is a diagnostic stub.
245
+ *
246
+ * THE WATERMARK IS THE SUBTLE PART. Without it, layer 2 reads the last `PLANNER_RESPONSE` in the
247
+ * transcript regardless of when it was written — including one from a PREVIOUS invocation in the
248
+ * same workspace, silently presenting a stale review as this run's. So the transcript line count is
249
+ * snapshotted BEFORE the spawn and only lines appended after it are considered. If the conversation
250
+ * id changed, `agy` started a fresh session and every line is new (skip 0).
251
+ *
252
+ * `takeWatermark` must therefore be called before `runAntigravity`; the plan's outer timeout is the
253
+ * external wall-clock cap that `--print-timeout` cannot provide (it cannot fire before a session
254
+ * exists — a process can stall pre-session and outlive its own native timeout, #2073 mode 3).
255
+ *
256
+ * KNOWN LIMIT — the watermark is sequential, not concurrent. `last_conversations.json` is keyed by
257
+ * WORKSPACE, so two `/gsd:review` runs against the same repo at the same time resolve the same
258
+ * conversation id and share one transcript. The fallback then takes "the latest DONE
259
+ * PLANNER_RESPONSE after the watermark" with nothing to tell the two runs apart, so one run could
260
+ * read the other's response. This cannot be closed here: `agy` exposes no per-invocation id to
261
+ * filter on, and the transcript carries none. It is stated rather than silently tolerated because
262
+ * the guarantee this function advertises ("never stale") holds only for sequential use, and a
263
+ * future reader deserves to know which half is actually guaranteed.
264
+ */
265
+ function antigravityWatermark(workspace, deps) {
266
+ const cachePath = `${deps.homeDir}/.gemini/antigravity-cli/cache/last_conversations.json`;
267
+ if (!deps.exists(cachePath))
268
+ return { convId: '', lines: 0 };
269
+ let cache;
270
+ try {
271
+ cache = JSON.parse(deps.readFile(cachePath));
272
+ }
273
+ catch {
274
+ return { convId: '', lines: 0 };
275
+ }
276
+ const convId = resolveConvId(cache, workspace);
277
+ if (!convId)
278
+ return { convId: '', lines: 0 };
279
+ const tx = transcriptPath(deps.homeDir, convId);
280
+ if (!deps.exists(tx))
281
+ return { convId, lines: 0 };
282
+ try {
283
+ return { convId, lines: deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim()).length };
284
+ }
285
+ catch {
286
+ return { convId, lines: 0 };
287
+ }
288
+ }
289
+ /** Workspace lookup is case-insensitive — the leg's jq did `ascii_downcase` on both sides. */
290
+ function resolveConvId(cache, workspace) {
291
+ if (Object.prototype.hasOwnProperty.call(cache, workspace)) {
292
+ const direct = cache[workspace];
293
+ if (typeof direct === 'string' && direct)
294
+ return direct;
295
+ }
296
+ const target = workspace.toLowerCase();
297
+ for (const [k, v] of Object.entries(cache)) {
298
+ if (k.toLowerCase() === target && typeof v === 'string' && v)
299
+ return v;
300
+ }
301
+ return '';
302
+ }
303
+ function transcriptPath(homeDir, convId) {
304
+ return `${homeDir}/.gemini/antigravity-cli/brain/${convId}/.system_generated/logs/transcript.jsonl`;
305
+ }
306
+ /**
307
+ * Layer 2: the newest `PLANNER_RESPONSE` written AFTER the watermark, or `''`.
308
+ *
309
+ * Returning `''` rather than the newest entry overall is the whole point — an empty result lets
310
+ * layer 3 fire with an honest diagnostic, where a stale one would be indistinguishable from a
311
+ * successful review.
312
+ */
313
+ function antigravityTranscriptFallback(workspace, mark, deps) {
314
+ const cachePath = `${deps.homeDir}/.gemini/antigravity-cli/cache/last_conversations.json`;
315
+ if (!deps.exists(cachePath))
316
+ return '';
317
+ let cache;
318
+ try {
319
+ cache = JSON.parse(deps.readFile(cachePath));
320
+ }
321
+ catch {
322
+ return '';
323
+ }
324
+ const convId = resolveConvId(cache, workspace);
325
+ if (!convId)
326
+ return '';
327
+ const tx = transcriptPath(deps.homeDir, convId);
328
+ if (!deps.exists(tx))
329
+ return '';
330
+ let lines;
331
+ try {
332
+ lines = deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim());
333
+ }
334
+ catch {
335
+ return '';
336
+ }
337
+ // Same conv-id ⇒ only lines beyond the watermark are this run's. Different id ⇒ fresh session,
338
+ // so every line is new.
339
+ const skip = convId === mark.convId ? mark.lines : 0;
340
+ let latest = '';
341
+ for (const line of lines.slice(skip)) {
342
+ let entry;
343
+ try {
344
+ entry = JSON.parse(line);
345
+ }
346
+ catch {
347
+ continue;
348
+ }
349
+ if (entry.source === 'MODEL' &&
350
+ entry.status === 'DONE' &&
351
+ entry.type === 'PLANNER_RESPONSE' &&
352
+ typeof entry.content === 'string') {
353
+ latest = entry.content;
354
+ }
355
+ }
356
+ return latest;
357
+ }
358
+ /**
359
+ * Antigravity's prompt variant (#2176).
360
+ *
361
+ * Differs from the standard `argv-file-ref` text by one clause, and that clause is load-bearing:
362
+ * it REQUIRES the reviewer to self-report when it cannot read the repo. Without it a blind review
363
+ * is indistinguishable from a grounded one — the reviewer happily reviews the plan text in
364
+ * isolation and its verdict is counted at full weight in the consensus. `stampBlindReview` reads
365
+ * this self-report back out.
366
+ */
367
+ function antigravityPrompt(promptPath, repoRoot) {
368
+ return (`Read the file at ${promptPath} in full and carry out the review request it contains. ` +
369
+ `The repository under review is at ${repoRoot} — resolve every relative file path in the ` +
370
+ `review request against that absolute root and verify claims against those files. ` +
371
+ `If you cannot read files under ${repoRoot}, begin your output with the exact line ` +
372
+ `REVIEWED-WITHOUT-REPO-ACCESS before the review. Output only the resulting markdown review. ` +
373
+ `Do not edit any files.`);
374
+ }
375
+ /**
376
+ * Antigravity's argv adjustments — the two things data cannot express for this lane.
377
+ *
378
+ * 1. `--add-dir <repo>`, CAPABILITY-PROBED. Without it agy's permission context never receives the
379
+ * cwd repo: the agent anchors on its own `~/.gemini/antigravity-cli/scratch` dir and reviews the
380
+ * plan text in isolation, which is exactly what the Review Instructions forbid (#2176). It is
381
+ * probed rather than assumed because older agy builds reject the unknown flag outright, and a
382
+ * lane that fails to start is worse than one that runs on the prompt anchor alone.
383
+ * 2. The self-report prompt variant above, swapped in for the standard file-ref text.
384
+ *
385
+ * Both are argv shape, so they belong here rather than in the descriptor: expressing "add this flag
386
+ * only if the binary's --help mentions it" as data would need a conditional, which is precisely
387
+ * what the named-handler seam exists to absorb (ADR-2782 D6).
388
+ */
389
+ function antigravityArgv(argv, promptPath, repoRoot, deps) {
390
+ const standard = (0, review_lane_invocation_cjs_1.fileRefPrompt)(promptPath, repoRoot);
391
+ const out = argv.map((a) => (a === standard ? antigravityPrompt(promptPath, repoRoot) : a));
392
+ let supportsAddDir = false;
393
+ try {
394
+ const help = deps.spawn('agy', ['--help'], { timeoutMs: 5_000 });
395
+ supportsAddDir = `${help.stdout}\n${help.stderr}`.includes('--add-dir');
396
+ }
397
+ catch {
398
+ supportsAddDir = false;
399
+ }
400
+ if (!supportsAddDir)
401
+ return out;
402
+ // Insert before the trailing `-p <prompt>` pair so the prompt stays last, as the leg had it.
403
+ const pIdx = out.lastIndexOf('-p');
404
+ if (pIdx === -1)
405
+ return [...out, '--add-dir', repoRoot];
406
+ return [...out.slice(0, pIdx), '--add-dir', repoRoot, ...out.slice(pIdx)];
407
+ }
408
+ /**
409
+ * Layer 3's diagnostic: what `agy` itself logged, when both stdout and the transcript were empty.
410
+ *
411
+ * #2073 mode 2 is invisible without this. A pinned model that 404s server-side exits **0** with
412
+ * empty stdout AND an empty transcript — every signal the other two layers read says "clean run,
413
+ * nothing to report". The only evidence is in `agy`'s own log, so a bare generic stub would leave
414
+ * the user with a silently missing reviewer and nothing to diagnose.
415
+ *
416
+ * Mode 3 (a pre-session stall, which `--print-timeout` cannot bound because it cannot fire before a
417
+ * session exists) leaves no log line at all, so its tell is stated rather than searched for.
418
+ */
419
+ function antigravityDiagnostic(deps) {
420
+ const lines = [
421
+ 'Antigravity review failed or returned empty output.',
422
+ ];
423
+ const logPath = `${deps.homeDir}/.gemini/antigravity-cli/cli.log`;
424
+ if (deps.exists(logPath)) {
425
+ try {
426
+ const hits = deps
427
+ .readFile(logPath)
428
+ .split(/\r?\n/)
429
+ .filter((l) => /agent executor error|NOT_FOUND|Publisher model/i.test(l))
430
+ .slice(-3);
431
+ if (hits.length) {
432
+ lines.push("agy log hint (pinned model may be unavailable — run 'agy models' and set review.models.agy):", ...hits);
433
+ }
434
+ }
435
+ catch {
436
+ /* an unreadable log is not worth failing the lane over */
437
+ }
438
+ }
439
+ lines.push('If no agy run started, that is the pre-session-stall case: check whether a new ' +
440
+ '~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch.');
441
+ return lines.join('\n');
442
+ }
443
+ /**
444
+ * Stamp a machine-readable marker when the reviewer plainly ran without repo access (#2176).
445
+ *
446
+ * BOTH tells are ANCHORED, and that anchoring is load-bearing: a grounded review that merely QUOTES
447
+ * `REVIEWED-WITHOUT-REPO-ACCESS` — reviewing this very file, say — must never be mis-stamped. So
448
+ * the self-report is only honoured in the first five lines, and the scratch-dir tell only in a
449
+ * workspace-DECLARATION phrasing.
450
+ */
451
+ function stampBlindReview(review) {
452
+ if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review))
453
+ return review;
454
+ const head = review.split(/\r?\n/).slice(0, 5).join('\n');
455
+ const selfReported = head.includes('REVIEWED-WITHOUT-REPO-ACCESS');
456
+ const scratchTell = /(workspace|working) (directory|dir)[\s\S]{0,40}antigravity-cli\/scratch/i.test(review);
457
+ if (!selfReported && !scratchTell)
458
+ return review;
459
+ return ('> [reviewed-without-repo-access] This reviewer ran without visibility into the repo under ' +
460
+ 'review — down-weight its verdict in the Consensus Summary.\n\n' +
461
+ review);
462
+ }
463
+ /**
464
+ * `openai-compatible` — model discovery, the chat-completions round trip, and the served-model
465
+ * mismatch warning.
466
+ *
467
+ * The raw body is returned alongside the content because an OpenAI-compatible server reports errors
468
+ * with an HTTP 4xx/5xx and the JSON in the BODY. The bash piped the response straight into `jq`,
469
+ * which discarded exactly that evidence; the stub appends it now.
470
+ */
471
+ async function runOpenAiCompatible(plan, promptText, deps) {
472
+ let model = plan.model;
473
+ if (!model && plan.modelsUrl) {
474
+ const listed = await deps.httpJson(plan.modelsUrl, { method: 'GET', timeoutMs: 2_000 });
475
+ if (listed.ok) {
476
+ try {
477
+ const parsed = JSON.parse(listed.body);
478
+ const first = Array.isArray(parsed.data) ? parsed.data[0] : undefined;
479
+ if (first && typeof first.id === 'string' && first.id)
480
+ model = first.id;
481
+ }
482
+ catch {
483
+ /* fall through to the declared fallback */
484
+ }
485
+ }
486
+ }
487
+ if (!model)
488
+ model = plan.fallbackModel;
489
+ const body = JSON.stringify({ model, messages: [{ role: 'user', content: promptText }] });
490
+ const res = await deps.httpJson(plan.url, { method: 'POST', body, timeoutMs: plan.timeoutMs });
491
+ if (!res.ok && !res.body) {
492
+ return { review: '', rawBody: res.error ?? `HTTP ${res.status}` };
493
+ }
494
+ let review = '';
495
+ try {
496
+ const parsed = JSON.parse(res.body);
497
+ // A server that quietly serves a different model than requested produces a review the user
498
+ // will attribute to the wrong system. Warn rather than fail — the review is still real.
499
+ if (typeof parsed.model === 'string' && parsed.model && parsed.model !== model) {
500
+ deps.warn(`${plan.slug} served model '${parsed.model}' but '${model}' was requested. ` +
501
+ `Review may be from a different model.`);
502
+ }
503
+ const content = parsed.choices?.[0]?.message?.content;
504
+ if (typeof content === 'string')
505
+ review = content;
506
+ }
507
+ catch {
508
+ /* leave review empty — the raw body carries the diagnosis */
509
+ }
510
+ return { review, rawBody: res.body };
511
+ }
512
+ /* ------------------------------------------------------------------ *
513
+ * Orchestration
514
+ * ------------------------------------------------------------------ */
515
+ /**
516
+ * Run one resolved lane end to end.
517
+ *
518
+ * Order is deliberate: egress re-verification happens BEFORE the probe and before any spawn, so a
519
+ * lane whose destination changed never receives the plan text even once.
520
+ */
521
+ async function runLane(plan, deps, opts) {
522
+ const base = { slug: plan.slug, stubbed: false };
523
+ if (plan.transport === 'openai-http') {
524
+ const egress = checkEgressHost(opts.consentedHost, plan.host);
525
+ if (!egress.allowed) {
526
+ const detail = `lane '${plan.slug}' was consented to send plans to ${egress.consentedHost} but ` +
527
+ `${plan.hostConfigKey} now resolves to ${egress.currentHost}. Re-consent to allow the new ` +
528
+ `destination — this lane will not run against a host you did not approve.`;
529
+ deps.warn(detail);
530
+ return { ...base, ok: false, reason: review_lane_invocation_cjs_1.LANE_UNAVAILABLE.EGRESS_HOST_CHANGED, detail };
531
+ }
532
+ }
533
+ const probed = await probeLane(plan, deps);
534
+ if (!probed.available) {
535
+ // D4's carve-out: not finding a lane nobody asked for is normal; failing to run a lane somebody
536
+ // asked for is an ERROR. Both are visible; only the second is fatal to the run.
537
+ if (opts.explicitlyRequested)
538
+ deps.warn(`explicitly requested reviewer '${plan.slug}': ${probed.detail}`);
539
+ return { ...base, ok: false, reason: probed.reason, detail: probed.detail };
540
+ }
541
+ return plan.transport === 'spawn'
542
+ ? runSpawnLane(plan, deps, opts.repoRoot)
543
+ : runHttpLane(plan, deps);
544
+ }
545
+ function runSpawnLane(plan, deps, repoRoot) {
546
+ const input = plan.stdin && deps.exists(plan.stdin) ? deps.readFile(plan.stdin) : undefined;
547
+ const mark = plan.handler === 'antigravity' ? antigravityWatermark(repoRoot, deps) : { convId: '', lines: 0 };
548
+ const argv = plan.handler === 'antigravity'
549
+ ? antigravityArgv(plan.argv, plan.promptPath, repoRoot, deps)
550
+ : plan.argv;
551
+ const out = deps.spawn(plan.binary, argv, { input, timeoutMs: plan.timeoutMs });
552
+ deps.writeFile(plan.errPath, out.stderr ?? '');
553
+ // `file-arg` lanes write the review themselves and their stdout is deliberately discarded (#1698).
554
+ let review = plan.outputTarget.kind === 'file'
555
+ ? deps.exists(plan.outputTarget.path)
556
+ ? deps.readFile(plan.outputTarget.path)
557
+ : ''
558
+ : (out.stdout ?? '');
559
+ let extra;
560
+ if (plan.handler === 'opencode') {
561
+ const rebuilt = handleOpencodeOutput(review);
562
+ if (!(0, review_lane_invocation_cjs_1.isEmptyReview)(rebuilt.review)) {
563
+ review = rebuilt.review;
564
+ }
565
+ else {
566
+ review = '';
567
+ extra = `OpenCode review returned no assistant text (#1936: agent ended its turn with no final message).\nDiagnostic: ${rebuilt.diagnostic}`;
568
+ }
569
+ }
570
+ if (plan.handler === 'antigravity') {
571
+ // A non-zero exit (timeout kill, crash) discards partial output so the transcript fallback and
572
+ // the diagnostic stub can take over.
573
+ if (out.status !== 0)
574
+ review = '';
575
+ if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review))
576
+ review = antigravityTranscriptFallback(repoRoot, mark, deps);
577
+ review = stampBlindReview(review);
578
+ // Layer 3. `emptyOutput: 'handler-owned'` means the generic stub does not fire for this lane,
579
+ // so if nothing is written here the lane goes out empty — the #2073 failure itself.
580
+ if ((0, review_lane_invocation_cjs_1.isEmptyReview)(review)) {
581
+ deps.writeFile(plan.reviewPath, `${antigravityDiagnostic(deps)}\n`);
582
+ return { slug: plan.slug, ok: true, stubbed: true };
583
+ }
584
+ }
585
+ const { stubbed } = writeReviewOrStub(plan, review, deps, extra);
586
+ return { slug: plan.slug, ok: true, stubbed };
587
+ }
588
+ async function runHttpLane(plan, deps) {
589
+ const promptText = deps.exists(plan.promptPath) ? deps.readFile(plan.promptPath) : '';
590
+ const { review, rawBody } = await runOpenAiCompatible(plan, promptText, deps);
591
+ deps.writeFile(plan.errPath, '');
592
+ const { stubbed } = writeReviewOrStub(plan, review, deps, rawBody);
593
+ return { slug: plan.slug, ok: true, stubbed };
594
+ }