@opengsd/gsd-core 1.12.0 → 1.13.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 (286) 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 +12 -0
  4. package/agents/gsd-executor.md +63 -35
  5. package/agents/gsd-plan-checker.md +76 -57
  6. package/agents/gsd-planner.md +14 -0
  7. package/agents/gsd-ui-checker.md +19 -3
  8. package/agents/gsd-ui-researcher.md +29 -0
  9. package/agents/gsd-verifier.md +23 -1
  10. package/bin/install.js +239 -67
  11. package/commands/gsd/execute-phase.md +1 -1
  12. package/commands/gsd/ns-workflow.md +2 -1
  13. package/commands/gsd/phase.md +1 -1
  14. package/commands/gsd/quick-batch.md +105 -0
  15. package/commands/gsd/surface.md +18 -8
  16. package/gsd-core/bin/gsd-tools.cjs +195 -50
  17. package/gsd-core/bin/lib/capability-activation.cjs +27 -0
  18. package/gsd-core/bin/lib/capability-registry.cjs +514 -114
  19. package/gsd-core/bin/lib/capability-state.cjs +7 -1
  20. package/gsd-core/bin/lib/capability-validator.cjs +120 -4
  21. package/gsd-core/bin/lib/capability-writer.cjs +14 -4
  22. package/gsd-core/bin/lib/check-command-router.cjs +85 -2
  23. package/gsd-core/bin/lib/claude-orchestration.cjs +10 -25
  24. package/gsd-core/bin/lib/clusters.cjs +1 -0
  25. package/gsd-core/bin/lib/command-aliases.cjs +16 -0
  26. package/gsd-core/bin/lib/commands.cjs +337 -13
  27. package/gsd-core/bin/lib/config-loader.cjs +3 -0
  28. package/gsd-core/bin/lib/core-utils.cjs +34 -7
  29. package/gsd-core/bin/lib/decisions.cjs +213 -1
  30. package/gsd-core/bin/lib/edge-probe.cjs +14 -1
  31. package/gsd-core/bin/lib/file-overlap-partitioner.cjs +74 -0
  32. package/gsd-core/bin/lib/frontmatter.cjs +137 -23
  33. package/gsd-core/bin/lib/gap-checker.cjs +22 -13
  34. package/gsd-core/bin/lib/git-base-branch.cjs +10 -2
  35. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +8 -2
  36. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +54 -11
  37. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +75 -22
  38. package/gsd-core/bin/lib/host-integration.cjs +57 -5
  39. package/gsd-core/bin/lib/init-command-router.cjs +14 -0
  40. package/gsd-core/bin/lib/init.cjs +132 -15
  41. package/gsd-core/bin/lib/install-engine.cjs +184 -12
  42. package/gsd-core/bin/lib/install-model-override-resolver.cjs +45 -0
  43. package/gsd-core/bin/lib/install-profiles.cjs +22 -14
  44. package/gsd-core/bin/lib/installer-migration-report.cjs +1 -0
  45. package/gsd-core/bin/lib/io.cjs +35 -0
  46. package/gsd-core/bin/lib/loop-resolver.cjs +14 -8
  47. package/gsd-core/bin/lib/markdown-table.cjs +123 -0
  48. package/gsd-core/bin/lib/milestone.cjs +22 -2
  49. package/gsd-core/bin/lib/phase-command-router.cjs +13 -6
  50. package/gsd-core/bin/lib/phase-id.cjs +251 -9
  51. package/gsd-core/bin/lib/phase.cjs +774 -35
  52. package/gsd-core/bin/lib/plan-document.cjs +10 -0
  53. package/gsd-core/bin/lib/planning-snapshot.cjs +147 -20
  54. package/gsd-core/bin/lib/planning-workspace.cjs +103 -28
  55. package/gsd-core/bin/lib/quick-batch-command-router.cjs +285 -0
  56. package/gsd-core/bin/lib/quick-batch-dispatch.cjs +250 -0
  57. package/gsd-core/bin/lib/quick-batch.cjs +840 -0
  58. package/gsd-core/bin/lib/review-lane-descriptor.cjs +53 -5
  59. package/gsd-core/bin/lib/review-lane-invocation.cjs +73 -1
  60. package/gsd-core/bin/lib/review-lane-runner.cjs +136 -10
  61. package/gsd-core/bin/lib/roadmap-parser.cjs +499 -26
  62. package/gsd-core/bin/lib/roadmap.cjs +187 -58
  63. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +233 -33
  64. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +16 -17
  65. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +286 -108
  66. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +215 -43
  67. package/gsd-core/bin/lib/shell-command-projection.cjs +4 -0
  68. package/gsd-core/bin/lib/smart-entry.cjs +7 -9
  69. package/gsd-core/bin/lib/state-document.cjs +30 -5
  70. package/gsd-core/bin/lib/state-md-schema.cjs +23 -13
  71. package/gsd-core/bin/lib/state-transition.cjs +333 -44
  72. package/gsd-core/bin/lib/state.cjs +684 -125
  73. package/gsd-core/bin/lib/surface.cjs +23 -8
  74. package/gsd-core/bin/lib/tdd-red-evidence.cjs +133 -0
  75. package/gsd-core/bin/lib/uat.cjs +1419 -515
  76. package/gsd-core/bin/lib/update-context.cjs +6 -2
  77. package/gsd-core/bin/lib/validate.cjs +230 -12
  78. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  79. package/gsd-core/bin/lib/verification.cjs +273 -12
  80. package/gsd-core/bin/lib/verify-command-router.cjs +1 -0
  81. package/gsd-core/bin/lib/verify.cjs +346 -16
  82. package/gsd-core/bin/lib/workstream-inventory.cjs +20 -2
  83. package/gsd-core/bin/lib/worktree-safety.cjs +8 -0
  84. package/gsd-core/bin/shared/config-schema.manifest.json +8 -0
  85. package/gsd-core/bin/verify-reapply-patches.cjs +70 -3
  86. package/gsd-core/references/agent-contracts.md +3 -3
  87. package/gsd-core/references/edge-probe.md +17 -13
  88. package/gsd-core/references/execute-mvp-tdd.md +18 -16
  89. package/gsd-core/references/execute-phase-response-language.md +6 -0
  90. package/gsd-core/references/executor-examples.md +42 -0
  91. package/gsd-core/references/few-shot-examples/plan-checker.md +15 -15
  92. package/gsd-core/references/mvp-concepts.md +2 -2
  93. package/gsd-core/references/plan-checker-examples.md +41 -0
  94. package/gsd-core/references/planner-antipatterns.md +25 -0
  95. package/gsd-core/references/planner-chunked.md +5 -1
  96. package/gsd-core/references/planner-coupling.md +42 -0
  97. package/gsd-core/references/planner-quick-batch.md +71 -0
  98. package/gsd-core/references/planner-reviews.md +47 -0
  99. package/gsd-core/references/planner-revision.md +75 -2
  100. package/gsd-core/references/planning-config.md +2 -1
  101. package/gsd-core/references/response-language-directive.md +9 -0
  102. package/gsd-core/references/revision-loop.md +118 -11
  103. package/gsd-core/references/tdd.md +14 -9
  104. package/gsd-core/references/verifier-evidence-gate.md +160 -0
  105. package/gsd-core/templates/phase-prompt.md +4 -0
  106. package/gsd-core/templates/verification-report.md +5 -0
  107. package/gsd-core/workflows/add-backlog.md +2 -0
  108. package/gsd-core/workflows/add-phase.md +2 -0
  109. package/gsd-core/workflows/add-tests.md +1 -1
  110. package/gsd-core/workflows/add-todo.md +1 -1
  111. package/gsd-core/workflows/ai-integration-phase.md +1 -1
  112. package/gsd-core/workflows/analyze-dependencies.md +2 -0
  113. package/gsd-core/workflows/audit-fix.md +2 -0
  114. package/gsd-core/workflows/audit-milestone.md +2 -0
  115. package/gsd-core/workflows/audit-uat.md +2 -0
  116. package/gsd-core/workflows/autonomous.md +2 -0
  117. package/gsd-core/workflows/check-todos.md +1 -1
  118. package/gsd-core/workflows/cleanup.md +1 -1
  119. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +15 -13
  120. package/gsd-core/workflows/code-review-fix.md +2 -0
  121. package/gsd-core/workflows/code-review.md +73 -31
  122. package/gsd-core/workflows/complete-milestone.md +13 -4
  123. package/gsd-core/workflows/debug.md +1 -1
  124. package/gsd-core/workflows/diagnose-issues.md +5 -1
  125. package/gsd-core/workflows/discuss-phase/modes/advisor.md +2 -0
  126. package/gsd-core/workflows/discuss-phase/modes/all.md +2 -0
  127. package/gsd-core/workflows/discuss-phase/modes/analyze.md +2 -0
  128. package/gsd-core/workflows/discuss-phase/modes/auto.md +2 -0
  129. package/gsd-core/workflows/discuss-phase/modes/batch.md +2 -0
  130. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -0
  131. package/gsd-core/workflows/discuss-phase/modes/default.md +2 -0
  132. package/gsd-core/workflows/discuss-phase/modes/power.md +2 -0
  133. package/gsd-core/workflows/discuss-phase/modes/text.md +2 -0
  134. package/gsd-core/workflows/discuss-phase/templates/context.md +2 -0
  135. package/gsd-core/workflows/discuss-phase/templates/discussion-log.md +2 -0
  136. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  137. package/gsd-core/workflows/discuss-phase-power.md +2 -0
  138. package/gsd-core/workflows/discuss-phase.md +1 -1
  139. package/gsd-core/workflows/do.md +43 -13
  140. package/gsd-core/workflows/docs-update.md +1 -1
  141. package/gsd-core/workflows/edit-phase.md +2 -0
  142. package/gsd-core/workflows/eval-review.md +1 -1
  143. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +2 -0
  144. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +17 -1
  145. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +8 -2
  146. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +2 -0
  147. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +25 -0
  148. package/gsd-core/workflows/execute-phase/steps/worktree-recovery-policy.md +2 -0
  149. package/gsd-core/workflows/execute-phase.md +32 -14
  150. package/gsd-core/workflows/execute-plan.md +8 -8
  151. package/gsd-core/workflows/explore.md +2 -0
  152. package/gsd-core/workflows/extract-learnings.md +2 -0
  153. package/gsd-core/workflows/fast.md +6 -0
  154. package/gsd-core/workflows/forensics.md +2 -0
  155. package/gsd-core/workflows/graduation.md +1 -1
  156. package/gsd-core/workflows/health.md +1 -1
  157. package/gsd-core/workflows/help/modes/brief.md +2 -0
  158. package/gsd-core/workflows/help/modes/default.md +2 -0
  159. package/gsd-core/workflows/help/modes/full.md +12 -0
  160. package/gsd-core/workflows/help/modes/topic.md +2 -0
  161. package/gsd-core/workflows/help.md +2 -0
  162. package/gsd-core/workflows/import.md +3 -3
  163. package/gsd-core/workflows/inbox.md +1 -1
  164. package/gsd-core/workflows/ingest-docs.md +1 -1
  165. package/gsd-core/workflows/insert-phase.md +2 -0
  166. package/gsd-core/workflows/list-phase-assumptions.md +2 -0
  167. package/gsd-core/workflows/list-seeds.md +2 -0
  168. package/gsd-core/workflows/list-workspaces.md +2 -0
  169. package/gsd-core/workflows/manager.md +3 -3
  170. package/gsd-core/workflows/map-codebase.md +2 -0
  171. package/gsd-core/workflows/milestone-summary.md +2 -0
  172. package/gsd-core/workflows/mvp-phase.md +1 -1
  173. package/gsd-core/workflows/new-milestone.md +1 -1
  174. package/gsd-core/workflows/new-project.md +5 -3
  175. package/gsd-core/workflows/new-workspace.md +1 -1
  176. package/gsd-core/workflows/next.md +2 -0
  177. package/gsd-core/workflows/node-repair.md +2 -0
  178. package/gsd-core/workflows/note.md +2 -0
  179. package/gsd-core/workflows/onboard.md +1 -1
  180. package/gsd-core/workflows/pause-work.md +19 -4
  181. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +100 -18
  182. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +2 -0
  183. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +9 -0
  184. package/gsd-core/workflows/plan-phase.md +130 -12
  185. package/gsd-core/workflows/plan-review-convergence.md +102 -10
  186. package/gsd-core/workflows/plant-seed.md +1 -1
  187. package/gsd-core/workflows/pr-branch.md +11 -3
  188. package/gsd-core/workflows/profile-user.md +1 -1
  189. package/gsd-core/workflows/progress/steps/forensic-audit.md +1 -1
  190. package/gsd-core/workflows/progress.md +25 -3
  191. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +37 -2
  192. package/gsd-core/workflows/quick/steps/research-phase.md +3 -3
  193. package/gsd-core/workflows/quick-batch/steps/batch-init.md +55 -0
  194. package/gsd-core/workflows/quick-batch/steps/completion.md +65 -0
  195. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +100 -0
  196. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +147 -0
  197. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +158 -0
  198. package/gsd-core/workflows/quick-batch/steps/research-phase.md +95 -0
  199. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +49 -0
  200. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +73 -0
  201. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +169 -0
  202. package/gsd-core/workflows/quick-batch.md +203 -0
  203. package/gsd-core/workflows/quick.md +13 -3
  204. package/gsd-core/workflows/reapply-patches.md +2 -0
  205. package/gsd-core/workflows/remove-phase.md +2 -0
  206. package/gsd-core/workflows/remove-workspace.md +1 -1
  207. package/gsd-core/workflows/resume-project.md +6 -2
  208. package/gsd-core/workflows/review.md +215 -10
  209. package/gsd-core/workflows/scan.md +2 -0
  210. package/gsd-core/workflows/section-manifest.json +12 -0
  211. package/gsd-core/workflows/secure-phase.md +1 -1
  212. package/gsd-core/workflows/session-report.md +2 -0
  213. package/gsd-core/workflows/settings-advanced.md +2 -0
  214. package/gsd-core/workflows/settings-integrations.md +9 -8
  215. package/gsd-core/workflows/settings.md +1 -1
  216. package/gsd-core/workflows/ship.md +10 -10
  217. package/gsd-core/workflows/sketch-wrap-up.md +2 -0
  218. package/gsd-core/workflows/sketch.md +1 -1
  219. package/gsd-core/workflows/smart-entry.md +1 -1
  220. package/gsd-core/workflows/spec-phase.md +24 -19
  221. package/gsd-core/workflows/spike-wrap-up.md +2 -0
  222. package/gsd-core/workflows/spike.md +1 -1
  223. package/gsd-core/workflows/stats.md +2 -0
  224. package/gsd-core/workflows/sync-skills.md +12 -4
  225. package/gsd-core/workflows/thread.md +2 -0
  226. package/gsd-core/workflows/transition.md +2 -0
  227. package/gsd-core/workflows/ui-phase.md +26 -5
  228. package/gsd-core/workflows/ui-review.md +1 -1
  229. package/gsd-core/workflows/ultraplan-phase.md +2 -0
  230. package/gsd-core/workflows/undo.md +1 -1
  231. package/gsd-core/workflows/update.md +41 -38
  232. package/gsd-core/workflows/validate-phase.md +1 -1
  233. package/gsd-core/workflows/verify-work.md +49 -3
  234. package/hooks/dist/gsd-check-update-worker.js +19 -2
  235. package/hooks/dist/gsd-context-monitor.js +283 -12
  236. package/hooks/dist/gsd-node-runner.sh +1 -0
  237. package/hooks/dist/gsd-prompt-guard.js +30 -5
  238. package/hooks/dist/gsd-read-guard.js +2 -0
  239. package/hooks/dist/gsd-read-injection-scanner.js +5 -5
  240. package/hooks/dist/gsd-secret-read-guard.js +1079 -0
  241. package/hooks/dist/gsd-statusline.js +7 -3
  242. package/hooks/dist/gsd-validate-commit.sh +444 -7
  243. package/hooks/dist/gsd-workflow-guard.js +2 -1
  244. package/hooks/dist/lib/git-cmd.js +210 -1
  245. package/hooks/dist/lib/injection-patterns.js +36 -6
  246. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  247. package/hooks/gsd-check-update-worker.js +19 -2
  248. package/hooks/gsd-context-monitor.js +283 -12
  249. package/hooks/gsd-node-runner.sh +1 -0
  250. package/hooks/gsd-prompt-guard.js +30 -5
  251. package/hooks/gsd-read-guard.js +2 -0
  252. package/hooks/gsd-read-injection-scanner.js +5 -5
  253. package/hooks/gsd-secret-read-guard.js +1079 -0
  254. package/hooks/gsd-statusline.js +7 -3
  255. package/hooks/gsd-validate-commit.sh +444 -7
  256. package/hooks/gsd-workflow-guard.js +2 -1
  257. package/hooks/hooks.json +6 -0
  258. package/hooks/lib/git-cmd.js +210 -1
  259. package/hooks/lib/injection-patterns.js +36 -6
  260. package/hooks/managed-hooks-registry.cjs +1 -0
  261. package/package.json +5 -5
  262. package/scripts/build-hooks.js +11 -4
  263. package/scripts/ci-test-scope.cjs +7 -0
  264. package/scripts/docs-guard-registry.cjs +10 -0
  265. package/scripts/gen-loop-host-contract.cjs +67 -15
  266. package/scripts/lib/shellcheck-fetch.cjs +247 -0
  267. package/scripts/lint-allow-test-rule-refs.allowlist.json +0 -6
  268. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +1 -1
  269. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +1 -1
  270. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +5 -0
  271. package/scripts/lint-phase-enumeration-drift.cjs +24 -6
  272. package/scripts/lint-phase-id-drift.cjs +133 -8
  273. package/scripts/lint-portable-grep.cjs +176 -0
  274. package/scripts/lint-response-language-coverage.cjs +524 -0
  275. package/scripts/lint-test-file-count.allowlist.json +3 -1
  276. package/scripts/lint-workflow-shellcheck-baseline.json +1027 -0
  277. package/scripts/lint-workflow-shellcheck.cjs +614 -0
  278. package/scripts/npm-audit-baseline.cjs +376 -0
  279. package/scripts/prompt-injection-scan.sh +8 -0
  280. package/scripts/require-issue-link-policy.cjs +16 -1
  281. package/skills/gsd-execute-phase/SKILL.md +1 -1
  282. package/skills/gsd-ns-workflow/SKILL.md +1 -0
  283. package/skills/gsd-phase/SKILL.md +1 -1
  284. package/skills/gsd-quick-batch/SKILL.md +105 -0
  285. package/skills/gsd-surface/SKILL.md +18 -8
  286. package/vscode/package.json +1 -1
@@ -45,6 +45,9 @@
45
45
  * 4. `flags: string[]` — Antigravity is selected by BOTH `--antigravity` and
46
46
  * `--agy`, which a single-valued field cannot express. This also flattens
47
47
  * D8's uniqueness invariant across every lane's flags.
48
+ * 5. `NATIVE_TIMEOUT` — a lane whose CLI takes its own native inner timeout flag (today only
49
+ * antigravity's `--print-timeout`) declares where the resolved value goes; `resolveLanePlan`
50
+ * computes what it is from the same resolved outer `timeoutMs` (#3274).
48
51
  *
49
52
  * Phase 2 (#2795) implements the manifest validator against the amended
50
53
  * vocabulary, which is the point of amending rather than leaving it to be
@@ -69,7 +72,7 @@ exports.checkReviewerDocsParity = checkReviewerDocsParity;
69
72
  * and vanishes when it has nothing to contribute (no model configured, no effort channel, prompt on
70
73
  * stdin), which is what lets one template serve the configured and unconfigured cases.
71
74
  *
72
- * This is a closed four-member vocabulary with no expressions, no nesting and no conditionals — a
75
+ * This is a closed five-member vocabulary with no expressions, no nesting and no conditionals — a
73
76
  * placeholder set, deliberately not a template language. The moment it needs a conditional, the
74
77
  * lane wants a `handler` instead (D6).
75
78
  */
@@ -82,6 +85,9 @@ exports.ARGV_PLACEHOLDER = Object.freeze({
82
85
  OUTPUT: '{{output}}',
83
86
  /** The argv-borne prompt, or nothing unless `promptChannel` is `argv`/`argv-file-ref`. */
84
87
  PROMPT: '{{prompt}}',
88
+ /** A lane's own CLI-native inner timeout duration, derived from the resolved outer `timeoutMs`
89
+ * (never independently configured) — see `resolveLanePlan`'s expansion of this token. */
90
+ NATIVE_TIMEOUT: '{{nativeTimeout}}',
85
91
  });
86
92
  const SPAWN_STDIN_STDOUT = {
87
93
  promptChannel: 'stdin',
@@ -107,12 +113,15 @@ exports.REVIEWER_LANES = Object.freeze([
107
113
  effortChannel: 'none',
108
114
  },
109
115
  timeoutFloorMs: 900_000,
116
+ timeoutConfigKey: 'review.timeouts.gemini',
110
117
  emptyOutput: 'stub-with-stderr',
111
118
  reviewsSection: 'Gemini',
112
119
  evidenceClass: 'source-grounded',
113
120
  requiresBinaries: [],
114
121
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.gemini',
115
122
  modelConfigKey: 'review.models.gemini',
123
+ effortConfigKey: null,
124
+ defaultEffort: null,
116
125
  handler: null,
117
126
  },
118
127
  {
@@ -139,12 +148,15 @@ exports.REVIEWER_LANES = Object.freeze([
139
148
  env: { CLAUDE_CODE_DISABLE_CLAUDE_MDS: '1', CLAUDE_CODE_DISABLE_AUTO_MEMORY: '1' },
140
149
  },
141
150
  timeoutFloorMs: 1_200_000,
151
+ timeoutConfigKey: 'review.timeouts.claude',
142
152
  emptyOutput: 'stub-with-stderr',
143
153
  reviewsSection: 'Claude',
144
154
  evidenceClass: 'source-grounded',
145
155
  requiresBinaries: [],
146
156
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.claude',
147
157
  modelConfigKey: 'review.models.claude',
158
+ effortConfigKey: 'review.effort.claude',
159
+ defaultEffort: 'high',
148
160
  handler: null,
149
161
  },
150
162
  {
@@ -167,12 +179,15 @@ exports.REVIEWER_LANES = Object.freeze([
167
179
  effortChannel: 'argv',
168
180
  },
169
181
  timeoutFloorMs: 1_200_000,
182
+ timeoutConfigKey: 'review.timeouts.codex',
170
183
  emptyOutput: 'stub-with-stderr',
171
184
  reviewsSection: 'Codex',
172
185
  evidenceClass: 'source-grounded',
173
186
  requiresBinaries: [],
174
187
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.codex',
175
188
  modelConfigKey: 'review.models.codex',
189
+ effortConfigKey: 'review.effort.codex',
190
+ defaultEffort: 'high',
176
191
  handler: null,
177
192
  },
178
193
  {
@@ -192,6 +207,7 @@ exports.REVIEWER_LANES = Object.freeze([
192
207
  effortChannel: 'none',
193
208
  },
194
209
  timeoutFloorMs: 360_000,
210
+ timeoutConfigKey: null,
195
211
  emptyOutput: 'stub-with-stderr',
196
212
  reviewsSection: 'CodeRabbit',
197
213
  evidenceClass: 'diff-only',
@@ -199,6 +215,8 @@ exports.REVIEWER_LANES = Object.freeze([
199
215
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.coderabbit',
200
216
  // Accepts no model flag at all (review.md:367) — not merely "none configured".
201
217
  modelConfigKey: null,
218
+ effortConfigKey: null,
219
+ defaultEffort: null,
202
220
  handler: null,
203
221
  },
204
222
  {
@@ -217,6 +235,7 @@ exports.REVIEWER_LANES = Object.freeze([
217
235
  effortChannel: 'argv',
218
236
  },
219
237
  timeoutFloorMs: 660_000,
238
+ timeoutConfigKey: 'review.timeouts.opencode',
220
239
  emptyOutput: 'stub-with-stderr',
221
240
  reviewsSection: 'OpenCode',
222
241
  evidenceClass: 'source-grounded',
@@ -225,6 +244,8 @@ exports.REVIEWER_LANES = Object.freeze([
225
244
  requiresBinaries: [],
226
245
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.opencode',
227
246
  modelConfigKey: 'review.models.opencode',
247
+ effortConfigKey: 'review.effort.opencode',
248
+ defaultEffort: 'high',
228
249
  // Phase 5b (#2799): was `null`. The review is REBUILT from assistant `text` parts; a plain
229
250
  // stdout copy would write the raw JSON envelope as the review (#1936). See LaneHandler.
230
251
  handler: 'opencode',
@@ -242,12 +263,15 @@ exports.REVIEWER_LANES = Object.freeze([
242
263
  effortChannel: 'none',
243
264
  },
244
265
  timeoutFloorMs: 900_000,
266
+ timeoutConfigKey: null,
245
267
  emptyOutput: 'stub-with-stderr',
246
268
  reviewsSection: 'Qwen',
247
269
  evidenceClass: 'source-grounded',
248
270
  requiresBinaries: [],
249
271
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.qwen',
250
272
  modelConfigKey: null,
273
+ effortConfigKey: null,
274
+ defaultEffort: null,
251
275
  handler: null,
252
276
  },
253
277
  {
@@ -260,19 +284,23 @@ exports.REVIEWER_LANES = Object.freeze([
260
284
  probe: { kind: 'command-exists', binary: 'cursor-agent' },
261
285
  invoke: {
262
286
  binary: 'cursor-agent',
263
- args: ['-p', '--mode', 'ask', '--trust', '--output-format', 'text', '{{prompt}}'],
287
+ args: ['-p', '{{model}}', '--mode', 'ask', '--trust', '--output-format', 'text', '{{prompt}}'],
264
288
  promptChannel: 'argv-file-ref',
265
289
  outputChannel: 'stdout',
266
- modelArg: null,
290
+ modelArg: '--model',
267
291
  effortChannel: 'none',
268
292
  },
269
293
  timeoutFloorMs: 900_000,
294
+ timeoutConfigKey: null,
270
295
  emptyOutput: 'stub-with-stderr',
271
296
  reviewsSection: 'Cursor',
272
297
  evidenceClass: 'source-grounded',
273
298
  requiresBinaries: [],
274
299
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.cursor',
275
- modelConfigKey: null,
300
+ // #3653: cursor-agent exposes --model (204 selectable models); wired the same as codex.
301
+ modelConfigKey: 'review.models.cursor',
302
+ effortConfigKey: null,
303
+ defaultEffort: null,
276
304
  handler: null,
277
305
  },
278
306
  {
@@ -286,13 +314,19 @@ exports.REVIEWER_LANES = Object.freeze([
286
314
  probe: { kind: 'command-exists', binary: 'agy' },
287
315
  invoke: {
288
316
  binary: 'agy',
289
- args: ['--print-timeout', '540s', '{{model}}', '-p', '{{prompt}}'],
317
+ // `{{nativeTimeout}}` is the fifth ARGV_PLACEHOLDER member (#3274) — `resolveLanePlan`
318
+ // (review-lane-invocation.cts) expands it to a value DERIVED from this same lane's resolved
319
+ // outer `timeoutMs`, so the native `--print-timeout` and the outer wall-clock cap can never
320
+ // drift apart. No other shipped lane's `args` template contains this token, so the expansion
321
+ // is inert everywhere else.
322
+ args: ['--print-timeout', '{{nativeTimeout}}', '{{model}}', '-p', '{{prompt}}'],
290
323
  promptChannel: 'argv-file-ref',
291
324
  outputChannel: 'stdout',
292
325
  modelArg: '--model',
293
326
  effortChannel: 'none',
294
327
  },
295
328
  timeoutFloorMs: 600_000,
329
+ timeoutConfigKey: 'review.timeouts.antigravity',
296
330
  emptyOutput: 'handler-owned',
297
331
  reviewsSection: 'Antigravity',
298
332
  evidenceClass: 'source-grounded',
@@ -302,6 +336,8 @@ exports.REVIEWER_LANES = Object.freeze([
302
336
  // NOT `review.models.antigravity` — the shipped key is `review.models.agy` (review.md:291) and
303
337
  // Phase 4 federated it under that name. This lane is why the key is declared, not derived.
304
338
  modelConfigKey: 'review.models.agy',
339
+ effortConfigKey: null,
340
+ defaultEffort: null,
305
341
  handler: 'antigravity',
306
342
  },
307
343
  {
@@ -323,6 +359,7 @@ exports.REVIEWER_LANES = Object.freeze([
323
359
  effortChannel: 'none',
324
360
  },
325
361
  timeoutFloorMs: 120_000,
362
+ timeoutConfigKey: 'review.timeouts.ollama',
326
363
  emptyOutput: 'stub-with-stderr',
327
364
  reviewsSection: 'Ollama',
328
365
  evidenceClass: 'source-grounded',
@@ -331,6 +368,8 @@ exports.REVIEWER_LANES = Object.freeze([
331
368
  requiresBinaries: [],
332
369
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.ollama',
333
370
  modelConfigKey: 'review.models.ollama',
371
+ effortConfigKey: null,
372
+ defaultEffort: null,
334
373
  handler: 'openai-compatible',
335
374
  },
336
375
  {
@@ -352,12 +391,15 @@ exports.REVIEWER_LANES = Object.freeze([
352
391
  effortChannel: 'none',
353
392
  },
354
393
  timeoutFloorMs: 120_000,
394
+ timeoutConfigKey: 'review.timeouts.lm_studio',
355
395
  emptyOutput: 'stub-with-stderr',
356
396
  reviewsSection: 'LM Studio',
357
397
  evidenceClass: 'source-grounded',
358
398
  requiresBinaries: [],
359
399
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.lm_studio',
360
400
  modelConfigKey: 'review.models.lm_studio',
401
+ effortConfigKey: null,
402
+ defaultEffort: null,
361
403
  handler: 'openai-compatible',
362
404
  },
363
405
  {
@@ -379,12 +421,15 @@ exports.REVIEWER_LANES = Object.freeze([
379
421
  effortChannel: 'none',
380
422
  },
381
423
  timeoutFloorMs: 120_000,
424
+ timeoutConfigKey: 'review.timeouts.llama_cpp',
382
425
  emptyOutput: 'stub-with-stderr',
383
426
  reviewsSection: 'llama.cpp',
384
427
  evidenceClass: 'source-grounded',
385
428
  requiresBinaries: [],
386
429
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.llama_cpp',
387
430
  modelConfigKey: 'review.models.llama_cpp',
431
+ effortConfigKey: null,
432
+ defaultEffort: null,
388
433
  handler: 'openai-compatible',
389
434
  },
390
435
  {
@@ -424,12 +469,15 @@ exports.REVIEWER_LANES = Object.freeze([
424
469
  effortChannel: 'none',
425
470
  },
426
471
  timeoutFloorMs: 900_000,
472
+ timeoutConfigKey: 'review.timeouts.kimi-code',
427
473
  emptyOutput: 'stub-with-stderr',
428
474
  reviewsSection: 'Kimi Code',
429
475
  evidenceClass: 'source-grounded',
430
476
  requiresBinaries: [],
431
477
  promptBudgetKey: 'review.max_prompt_tokens_per_reviewer.kimi-code',
432
478
  modelConfigKey: 'review.models.kimi-code',
479
+ effortConfigKey: null,
480
+ defaultEffort: null,
433
481
  handler: null,
434
482
  },
435
483
  ].map((lane) => Object.freeze(lane)));
@@ -26,6 +26,9 @@ Object.defineProperty(exports, "__esModule", { value: true });
26
26
  exports.LANE_UNAVAILABLE = void 0;
27
27
  exports.configString = configString;
28
28
  exports.normalizeHost = normalizeHost;
29
+ exports.resolveLaneEffort = resolveLaneEffort;
30
+ exports.resolveTimeoutMs = resolveTimeoutMs;
31
+ exports.nativeTimeoutToken = nativeTimeoutToken;
29
32
  exports.isEmptyReview = isEmptyReview;
30
33
  exports.fileRefPrompt = fileRefPrompt;
31
34
  exports.resolveLanePlan = resolveLanePlan;
@@ -122,6 +125,73 @@ function normalizeHost(raw) {
122
125
  const pathPart = u.pathname.replace(/\/+$/, '');
123
126
  return `${scheme}//${host}${port ? `:${port}` : ''}${pathPart}`;
124
127
  }
128
+ /** Levels GSD's effort axis accepts (#3533). `inherit` selects the no-argument path. */
129
+ const EFFORT_LEVELS = new Set([
130
+ 'minimal', 'low', 'medium', 'high', 'xhigh', 'max', 'inherit',
131
+ ]);
132
+ /**
133
+ * Resolve one lane's reasoning effort from REVIEW configuration (#4255).
134
+ *
135
+ * Resolution order, highest first:
136
+ * 1. `lane.effortConfigKey` — the per-lane review effort the operator set
137
+ * 2. `lane.defaultEffort` — the lane's declared review default (`high` for prompt-fed,
138
+ * source-grounded lanes)
139
+ * 3. nothing — no effort argument is emitted and the reviewer CLI's own configuration decides
140
+ *
141
+ * A configured `'inherit'` selects (3) explicitly. An unrecognized level is REFUSED rather than
142
+ * passed to the host: it falls back to the lane default, because forwarding a typo would render an
143
+ * argument the CLI rejects and kill the lane outright.
144
+ *
145
+ * What this function deliberately does NOT do is consult any agent's execution settings. Before
146
+ * #4255 the level came from `gsd-plan-checker`'s installed frontmatter through a hardcoded agent
147
+ * id, so every lane ran at a fast structural verifier's `low` — and, because the rendered argument
148
+ * is a CLI config override, it silently beat the effort the operator had configured for that CLI
149
+ * itself. A value inherited from an unrelated agent is worse than no value at all, which is why
150
+ * (3) emits nothing rather than falling back to some other agent's number.
151
+ *
152
+ * `renderArgv` is injected (the host table and the ADR-2481 surface negotiation live in
153
+ * `model-catalog` / `commands`, above this module's layer) so this stays a pure function of its
154
+ * inputs and the golden lane table can assert it without a spawn.
155
+ */
156
+ function resolveLaneEffort(lane, configGet, renderArgv) {
157
+ const none = { argv: [], value: null, source: 'none' };
158
+ if (!lane || typeof lane !== 'object')
159
+ return none;
160
+ const configured = lane.effortConfigKey ? configString(configGet(lane.effortConfigKey)) : null;
161
+ const valid = configured !== null && EFFORT_LEVELS.has(configured) ? configured : null;
162
+ const level = valid ?? configString(lane.defaultEffort);
163
+ if (level === null || level === 'inherit')
164
+ return none;
165
+ const rendered = renderArgv(lane.slug, level);
166
+ const argv = (rendered.argv ?? []).filter((a) => typeof a === 'string' && a !== '');
167
+ if (argv.length === 0)
168
+ return none;
169
+ return {
170
+ argv,
171
+ value: configString(rendered.value) ?? level,
172
+ source: valid !== null ? 'config' : 'lane-default',
173
+ };
174
+ }
175
+ function resolveTimeoutMs(timeoutConfigKey, floorMs, configGet) {
176
+ const configuredSeconds = typeof timeoutConfigKey === 'string' ? configGet(timeoutConfigKey) : undefined;
177
+ return typeof configuredSeconds === 'number' && Number.isFinite(configuredSeconds) && configuredSeconds > 0
178
+ ? configuredSeconds * 1000
179
+ : floorMs;
180
+ }
181
+ /** Buffer (seconds) a lane's native inner timeout sits under its resolved outer wall-clock cap
182
+ * (#3274). Matches the shipped 600s outer / 540s native relationship exactly when unconfigured:
183
+ * floor(600000/1000) - 60 = 540. */
184
+ const NATIVE_TIMEOUT_BUFFER_SECONDS = 60;
185
+ /**
186
+ * Render the `{{nativeTimeout}}` argv placeholder from a lane's resolved outer timeout (#3274).
187
+ *
188
+ * Clamped to a 1-second floor so a very small configured (or, today, only-ever-default) outer
189
+ * timeout never produces a zero or negative duration string a CLI would reject or misinterpret.
190
+ */
191
+ function nativeTimeoutToken(timeoutMs) {
192
+ const seconds = Math.max(1, Math.floor(timeoutMs / 1000) - NATIVE_TIMEOUT_BUFFER_SECONDS);
193
+ return `${seconds}s`;
194
+ }
125
195
  /**
126
196
  * Classify a lane's output as a review or as empty.
127
197
  *
@@ -212,9 +282,10 @@ function resolveLanePlan(input) {
212
282
  return fail(exports.LANE_UNAVAILABLE.UNKNOWN_HANDLER, `lane '${slug}' names handler '${String(handler)}', which this GSD version does not provide`);
213
283
  }
214
284
  const { promptPath, reviewPath, errPath } = artifactPaths(input.runDir, slug);
215
- const timeoutMs = typeof lane.timeoutFloorMs === 'number' && Number.isFinite(lane.timeoutFloorMs) && lane.timeoutFloorMs > 0
285
+ const floorMs = typeof lane.timeoutFloorMs === 'number' && Number.isFinite(lane.timeoutFloorMs) && lane.timeoutFloorMs > 0
216
286
  ? lane.timeoutFloorMs
217
287
  : 900_000;
288
+ const timeoutMs = resolveTimeoutMs(lane.timeoutConfigKey, floorMs, input.configGet);
218
289
  const emptyOutput = lane.emptyOutput === 'handler-owned' ? 'handler-owned' : 'stub-with-stderr';
219
290
  // #3194: only an EXACT 'diff-only' declaration exempts a lane from evidence verification.
220
291
  // Anything else — including a missing or garbage value on a third-party overlay body —
@@ -319,6 +390,7 @@ function resolveLanePlan(input) {
319
390
  '{{effort}}': effortExpansion,
320
391
  '{{output}}': outputExpansion,
321
392
  '{{prompt}}': promptExpansion,
393
+ '{{nativeTimeout}}': [nativeTimeoutToken(timeoutMs)],
322
394
  };
323
395
  const template = Array.isArray(inv.args)
324
396
  ? inv.args.filter((a) => typeof a === 'string')
@@ -27,12 +27,13 @@
27
27
  * "failed" and "ran cleanly with nothing to report" IS the defect this epic closes (#2494/#2605).
28
28
  */
29
29
  Object.defineProperty(exports, "__esModule", { value: true });
30
- exports.MODEL_VALUE_MAX = exports.BANNER_SCAN_LINES = exports.UNRESOLVED_MODEL = exports.MODEL_SOURCE = void 0;
30
+ exports.ANTIGRAVITY_FAILURE_MODE = exports.MODEL_VALUE_MAX = exports.BANNER_SCAN_LINES = exports.UNRESOLVED_MODEL = exports.MODEL_SOURCE = void 0;
31
31
  exports.parseModelBanner = parseModelBanner;
32
32
  exports.parseTranscriptModel = parseTranscriptModel;
33
33
  exports.checkEgressHost = checkEgressHost;
34
34
  exports.probeLane = probeLane;
35
35
  exports.writeReviewOrStub = writeReviewOrStub;
36
+ exports.emptyOutputDiagnosis = emptyOutputDiagnosis;
36
37
  exports.handleOpencodeOutput = handleOpencodeOutput;
37
38
  exports.antigravityWatermark = antigravityWatermark;
38
39
  exports.antigravityTranscriptFallback = antigravityTranscriptFallback;
@@ -40,6 +41,7 @@ exports.antigravityModel = antigravityModel;
40
41
  exports.resolveSpawnModel = resolveSpawnModel;
41
42
  exports.antigravityPrompt = antigravityPrompt;
42
43
  exports.antigravityArgv = antigravityArgv;
44
+ exports.antigravityFailureMode = antigravityFailureMode;
43
45
  exports.antigravityDiagnostic = antigravityDiagnostic;
44
46
  exports.stampBlindReview = stampBlindReview;
45
47
  exports.stampUngroundedReview = stampUngroundedReview;
@@ -354,8 +356,15 @@ async function probeLane(plan, deps) {
354
356
  * `extraDiagnostics` carries the raw HTTP response body for the OpenAI-compatible lanes: an error
355
357
  * from such a server arrives with HTTP 4xx/5xx and the JSON in the BODY, so stderr alone is empty
356
358
  * and the body is the only evidence.
359
+ *
360
+ * `outcome` (#4255) carries the spawn's exit status so the stub can say WHICH empty it is. The
361
+ * header alone cannot: a crash, a timeout kill and a model that ended its turn without writing a
362
+ * final message all reach here as the same zero bytes, and the third is what a too-low reasoning
363
+ * effort produces on a large source-grounded prompt. A clean exit inside the timeout with no
364
+ * output is a stopped-short model, and the stub now says so — with the effort it ran at, which is
365
+ * the value the operator would change.
357
366
  */
358
- function writeReviewOrStub(plan, content, deps, extraDiagnostics) {
367
+ function writeReviewOrStub(plan, content, deps, extraDiagnostics, outcome) {
359
368
  if (!(0, review_lane_invocation_cjs_1.isEmptyReview)(content)) {
360
369
  deps.writeFile(plan.reviewPath, content.endsWith('\n') ? content : `${content}\n`);
361
370
  return { stubbed: false };
@@ -364,9 +373,53 @@ function writeReviewOrStub(plan, content, deps, extraDiagnostics) {
364
373
  const parts = [`${plan.slug} review failed or returned empty output. stderr:`, stderr];
365
374
  if (extraDiagnostics)
366
375
  parts.push('Raw response body:', extraDiagnostics);
376
+ parts.push(emptyOutputDiagnosis(plan, outcome));
367
377
  deps.writeFile(plan.reviewPath, `${parts.join('\n')}\n`);
368
378
  return { stubbed: true };
369
379
  }
380
+ /**
381
+ * One line naming the effort the lane ran at and how the process ended (#4255).
382
+ *
383
+ * Kept out of the header so the `failed or returned empty output` string every downstream reader
384
+ * greps for is untouched — this is an added line, not a reworded one.
385
+ */
386
+ function emptyOutputDiagnosis(plan, outcome) {
387
+ // `effort` lives on the spawn plan only. An HTTP lane reaches a server directly and has no
388
+ // reviewer CLI at all, so naming one there would be a lie about what ran (Codex review of
389
+ // #4255) — the two transports get different, accurate wording.
390
+ const spawned = plan.transport === 'spawn';
391
+ const level = spawned ? plan.effort : null;
392
+ const effort = level
393
+ ? `ran at effort=${level}`
394
+ : spawned
395
+ ? "ran with no effort argument, so the reviewer CLI's own configuration applied"
396
+ : 'is an HTTP lane and carries no reasoning-effort setting';
397
+ if (!outcome)
398
+ return `Diagnosis: ${plan.slug} ${effort}.`;
399
+ // Four endings, not two. `status` is null for BOTH a timeout kill and a process that never
400
+ // ran or died on a signal (ENOENT, SIGKILL) — reporting the latter as "status null" said
401
+ // nothing, and folding them together would attach the stopped-short hint to a crash.
402
+ const timedOut = outcome.errorCode === 'ETIMEDOUT';
403
+ const neverRan = !timedOut && outcome.status === null;
404
+ const cleanExit = outcome.status === 0;
405
+ const ending = timedOut
406
+ ? 'was killed by the outer timeout'
407
+ : neverRan
408
+ ? `did not exit normally (${outcome.errorCode ?? 'killed by a signal'})`
409
+ : cleanExit
410
+ ? 'exited cleanly inside the timeout'
411
+ : `exited with status ${String(outcome.status)}`;
412
+ // Hedged deliberately. A clean exit with no output is CONSISTENT with a model ending its turn
413
+ // without a final message — which is what too low an effort produces on a large prompt — but it
414
+ // is equally consistent with the CLI writing its output somewhere this lane did not read. The
415
+ // line points at the likeliest cause without asserting it.
416
+ const tail = cleanExit
417
+ ? ' — a clean exit that produced no output is most often a model ending its turn without'
418
+ + ' writing a final message rather than a crash; if this lane carries a reasoning effort,'
419
+ + ' raising it is the usual fix.'
420
+ : '.';
421
+ return `Diagnosis: ${plan.slug} ${effort} and ${ending}${tail}`;
422
+ }
370
423
  /* ------------------------------------------------------------------ *
371
424
  * Handlers (D6) — named first-party code, never conditionals in data
372
425
  * ------------------------------------------------------------------ */
@@ -677,9 +730,13 @@ function antigravityPrompt(promptPath, repoRoot) {
677
730
  * lane that fails to start is worse than one that runs on the prompt anchor alone.
678
731
  * 2. The self-report prompt variant above, swapped in for the standard file-ref text.
679
732
  *
680
- * Both are argv shape, so they belong here rather than in the descriptor: expressing "add this flag
681
- * only if the binary's --help mentions it" as data would need a conditional, which is precisely
682
- * what the named-handler seam exists to absorb (ADR-2782 D6).
733
+ * Both are argv shape, so they belong here rather than in the descriptor: expressing "add this
734
+ * flag only if the binary's --help mentions it" as data would need a conditional, which is
735
+ * precisely what the named-handler seam exists to absorb (ADR-2782 D6). The native
736
+ * `--print-timeout` VALUE (#3274) is NOT this handler's job — `resolveLanePlan`
737
+ * (review-lane-invocation.cts) resolves the `{{nativeTimeout}}` ARGV_PLACEHOLDER itself, exactly
738
+ * like `{{model}}`/`{{effort}}`/`{{output}}`/`{{prompt}}`, so `plan.argv` arrives here already fully
739
+ * resolved.
683
740
  */
684
741
  function antigravityArgv(argv, promptPath, repoRoot, deps) {
685
742
  const standard = (0, review_lane_invocation_cjs_1.fileRefPrompt)(promptPath, repoRoot);
@@ -710,8 +767,54 @@ function antigravityArgv(argv, promptPath, repoRoot, deps) {
710
767
  *
711
768
  * Mode 3 (a pre-session stall, which `--print-timeout` cannot bound because it cannot fire before a
712
769
  * session exists) leaves no log line at all, so its tell is stated rather than searched for.
770
+ *
771
+ * #3996 mode 4 (a headless tool-permission denial) exits 0 with empty stdout and a POPULATED
772
+ * transcript, and reports the cause only on stderr — so the stub carries the run's stderr
773
+ * (`evidence.stderr`, the same `plan.errPath` content the generic stub reads), and the mode-3
774
+ * tell is stated only when `antigravityFailureMode` rules a session out (a fresh conv-id or
775
+ * transcript growth past the watermark means one verifiably started). Asserting mode 3 over a
776
+ * transcript this code just read inverts who is better placed to know.
713
777
  */
714
- function antigravityDiagnostic(deps) {
778
+ /**
779
+ * What actually failed, as a typed fact rather than a guess baked into prose. #3996.
780
+ *
781
+ * The session-started question is decidable at diagnostic time with the same staleness rule the
782
+ * layer-2 fallback uses: re-resolve the workspace's CURRENT conv-id and compare against the
783
+ * pre-spawn watermark. A conv-id that changed, or a transcript that grew past the watermark
784
+ * line count, means THIS invocation demonstrably reached a session — a pre-launch stall is
785
+ * ruled out. An unchanged conv-id with no growth means nothing new happened, which is the
786
+ * #2073 mode-3 shape even when a PRIOR session's transcript still exists on disk (existence
787
+ * alone would mis-diagnose mode 3 as mode 4 in any workspace with history).
788
+ */
789
+ exports.ANTIGRAVITY_FAILURE_MODE = Object.freeze({
790
+ /** A session ran this invocation (fresh conv-id, or transcript growth) but no review was recovered. */
791
+ SESSION_STARTED: 'session_started',
792
+ /** No session this invocation — the #2073 mode-3 stall tell is the right signpost. */
793
+ PRE_SESSION_STALL: 'pre_session_stall',
794
+ });
795
+ function antigravityFailureMode(workspace, mark, deps) {
796
+ const convId = resolveWorkspaceConvId(workspace, deps);
797
+ if (!convId)
798
+ return exports.ANTIGRAVITY_FAILURE_MODE.PRE_SESSION_STALL;
799
+ // A different conv-id than the watermark saw = a fresh session this run, transcript or not.
800
+ if (convId !== mark.convId)
801
+ return exports.ANTIGRAVITY_FAILURE_MODE.SESSION_STARTED;
802
+ const tx = transcriptPath(deps.homeDir, convId);
803
+ if (!deps.exists(tx))
804
+ return exports.ANTIGRAVITY_FAILURE_MODE.PRE_SESSION_STALL;
805
+ try {
806
+ const lines = deps.readFile(tx).split(/\r?\n/).filter((l) => l.trim()).length;
807
+ return lines > mark.lines
808
+ ? exports.ANTIGRAVITY_FAILURE_MODE.SESSION_STARTED
809
+ : exports.ANTIGRAVITY_FAILURE_MODE.PRE_SESSION_STALL;
810
+ }
811
+ catch {
812
+ // #3118 fail-closed shape: the transcript indisputably exists but cannot be read — do not
813
+ // assert the stall case over a file this code cannot check.
814
+ return exports.ANTIGRAVITY_FAILURE_MODE.SESSION_STARTED;
815
+ }
816
+ }
817
+ function antigravityDiagnostic(deps, evidence = {}) {
715
818
  const lines = [
716
819
  'Antigravity review failed or returned empty output.',
717
820
  ];
@@ -731,8 +834,28 @@ function antigravityDiagnostic(deps) {
731
834
  /* an unreadable log is not worth failing the lane over */
732
835
  }
733
836
  }
734
- lines.push('If no agy run started, that is the pre-session-stall case: check whether a new ' +
735
- '~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch.');
837
+ // #3996: the stub carries agy's stderr, as the generic lane stub already does — mode 4 (a
838
+ // headless tool-permission denial) exits 0 with empty stdout and a populated transcript, and
839
+ // the stderr line is the only signal that names its cause.
840
+ const stderr = (evidence.stderr ?? '').trim();
841
+ if (stderr)
842
+ lines.push('stderr:', stderr);
843
+ // #3996: mode 3's tell is stated only when its precondition holds — the mode is computed from
844
+ // the watermark (#3996 mode 3 in a workspace with history is a no-growth transcript, not an
845
+ // absent one), never guessed from prose.
846
+ if (evidence.mode === exports.ANTIGRAVITY_FAILURE_MODE.SESSION_STARTED) {
847
+ lines.push('An agy session started this run but no review was recovered, so a pre-launch stall is ' +
848
+ 'ruled out.' +
849
+ (stderr
850
+ ? ' See stderr above; a headless run that was auto-denied a tool permission reports ' +
851
+ 'the cause and its fix there.'
852
+ : ' agy reported nothing on its error stream; inspect the transcript under ' +
853
+ '~/.gemini/antigravity-cli/brain/<conv-id>/ for where the session stopped.'));
854
+ }
855
+ else {
856
+ lines.push('If no agy run started, that is the pre-session-stall case: check whether a new ' +
857
+ '~/.gemini/antigravity-cli/brain/<conv-id>/ dir appeared within ~30s of launch.');
858
+ }
736
859
  return lines.join('\n');
737
860
  }
738
861
  /**
@@ -948,7 +1071,10 @@ function runSpawnLane(plan, deps, repoRoot) {
948
1071
  // pinned model that 404s server-side and exits 0 with empty output, so the model IS the
949
1072
  // diagnosis — dropping it here would throw away the one piece of evidence the stub exists
950
1073
  // to preserve.
951
- deps.writeFile(plan.reviewPath, `${antigravityDiagnostic(deps)}\n`);
1074
+ deps.writeFile(plan.reviewPath, `${antigravityDiagnostic(deps, {
1075
+ stderr: errContent,
1076
+ mode: antigravityFailureMode(repoRoot, mark, deps),
1077
+ })}\n`);
952
1078
  return { slug: plan.slug, ok: true, stubbed: true, model };
953
1079
  }
954
1080
  }
@@ -959,7 +1085,7 @@ function runSpawnLane(plan, deps, repoRoot) {
959
1085
  // folded in as a diff observation, and the citation check must not change that surface.
960
1086
  if (plan.evidenceClass !== 'diff-only')
961
1087
  review = stampUngroundedReview(review);
962
- const { stubbed } = writeReviewOrStub(plan, review, deps, extra);
1088
+ const { stubbed } = writeReviewOrStub(plan, review, deps, extra, out);
963
1089
  return { slug: plan.slug, ok: true, stubbed, model };
964
1090
  }
965
1091
  async function runHttpLane(plan, deps) {