@opengsd/gsd-core 1.13.0 → 1.15.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 (441) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.ja-JP.md +3 -3
  4. package/README.ko-KR.md +3 -3
  5. package/README.pt-BR.md +3 -3
  6. package/README.zh-CN.md +3 -3
  7. package/agents/gsd-advisor-researcher.compact.md +85 -0
  8. package/agents/gsd-ai-researcher.compact.md +96 -0
  9. package/agents/gsd-assumptions-analyzer.compact.md +81 -0
  10. package/agents/gsd-code-fixer.compact.md +459 -0
  11. package/agents/gsd-code-fixer.md +9 -8
  12. package/agents/gsd-code-reviewer.compact.md +269 -0
  13. package/agents/gsd-code-reviewer.md +15 -3
  14. package/agents/gsd-codebase-mapper.compact.md +760 -0
  15. package/agents/gsd-debug-session-manager.compact.md +360 -0
  16. package/agents/gsd-debug-session-manager.md +17 -2
  17. package/agents/gsd-debugger.md +2 -2
  18. package/agents/gsd-doc-classifier.compact.md +192 -0
  19. package/agents/gsd-doc-synthesizer.compact.md +200 -0
  20. package/agents/gsd-doc-verifier.compact.md +143 -0
  21. package/agents/gsd-doc-writer.compact.md +440 -0
  22. package/agents/gsd-dom-verifier.compact.md +138 -0
  23. package/agents/gsd-domain-researcher.compact.md +141 -0
  24. package/agents/gsd-eval-auditor.compact.md +160 -0
  25. package/agents/gsd-eval-auditor.md +1 -1
  26. package/agents/gsd-eval-planner.compact.md +137 -0
  27. package/agents/gsd-executor.md +13 -8
  28. package/agents/gsd-framework-selector.compact.md +82 -0
  29. package/agents/gsd-integration-checker.compact.md +245 -0
  30. package/agents/gsd-intel-updater.compact.md +226 -0
  31. package/agents/gsd-intel-updater.md +1 -1
  32. package/agents/gsd-mempalace-curator.compact.md +45 -0
  33. package/agents/gsd-nyquist-auditor.compact.md +179 -0
  34. package/agents/gsd-pattern-mapper.compact.md +275 -0
  35. package/agents/gsd-phase-researcher.md +19 -11
  36. package/agents/gsd-plan-checker.md +8 -7
  37. package/agents/gsd-planner.md +12 -8
  38. package/agents/gsd-project-researcher.compact.md +587 -0
  39. package/agents/gsd-project-researcher.md +1 -1
  40. package/agents/gsd-research-synthesizer.compact.md +212 -0
  41. package/agents/gsd-research-synthesizer.md +1 -1
  42. package/agents/gsd-roadmapper.compact.md +454 -0
  43. package/agents/gsd-roadmapper.md +13 -0
  44. package/agents/gsd-security-auditor.compact.md +162 -0
  45. package/agents/gsd-ui-auditor.compact.md +404 -0
  46. package/agents/gsd-ui-auditor.md +155 -17
  47. package/agents/gsd-ui-checker.compact.md +277 -0
  48. package/agents/gsd-ui-researcher.compact.md +282 -0
  49. package/agents/gsd-ui-researcher.md +1 -1
  50. package/agents/gsd-user-profiler.compact.md +108 -0
  51. package/agents/gsd-verifier.md +10 -9
  52. package/bin/install.js +848 -163
  53. package/commands/gsd/autonomous.md +2 -2
  54. package/commands/gsd/capture.md +1 -1
  55. package/commands/gsd/cleanup.md +1 -0
  56. package/commands/gsd/code-review.md +2 -1
  57. package/commands/gsd/complete-milestone.md +1 -0
  58. package/commands/gsd/config.md +1 -0
  59. package/commands/gsd/debug.md +1 -0
  60. package/commands/gsd/graphify.md +1 -0
  61. package/commands/gsd/health.md +1 -0
  62. package/commands/gsd/mempalace-capture.md +8 -3
  63. package/commands/gsd/mempalace-recall.md +1 -0
  64. package/commands/gsd/new-milestone.md +1 -0
  65. package/commands/gsd/new-project.md +1 -0
  66. package/commands/gsd/next.md +1 -0
  67. package/commands/gsd/pause-work.md +1 -0
  68. package/commands/gsd/phase.md +1 -0
  69. package/commands/gsd/plan-review-convergence.md +6 -6
  70. package/commands/gsd/pr-branch.md +1 -0
  71. package/commands/gsd/progress.md +1 -1
  72. package/commands/gsd/quick-batch.md +1 -1
  73. package/commands/gsd/resume-work.md +1 -0
  74. package/commands/gsd/review-backlog.md +1 -0
  75. package/commands/gsd/review.md +2 -3
  76. package/commands/gsd/settings.md +2 -1
  77. package/commands/gsd/stats.md +1 -0
  78. package/commands/gsd/thread.md +1 -0
  79. package/commands/gsd/workspace.md +1 -0
  80. package/commands/gsd/workstreams.md +1 -0
  81. package/gsd-core/bin/check-latest-version.cjs +8 -3
  82. package/gsd-core/bin/gsd-tools.cjs +672 -146
  83. package/gsd-core/bin/lib/adr-parser.cjs +4 -2
  84. package/gsd-core/bin/lib/artifacts.cjs +2 -1
  85. package/gsd-core/bin/lib/audit.cjs +119 -34
  86. package/gsd-core/bin/lib/broken-windows.cjs +168 -49
  87. package/gsd-core/bin/lib/capability-lifecycle.cjs +10 -6
  88. package/gsd-core/bin/lib/capability-loader.cjs +135 -1
  89. package/gsd-core/bin/lib/capability-registry.cjs +96 -189
  90. package/gsd-core/bin/lib/capability-source.cjs +19 -2
  91. package/gsd-core/bin/lib/capability-validator.cjs +14 -2
  92. package/gsd-core/bin/lib/check-command-router.cjs +213 -49
  93. package/gsd-core/bin/lib/code-review-depth.cjs +2 -2
  94. package/gsd-core/bin/lib/codex-agent-toml.cjs +21 -25
  95. package/gsd-core/bin/lib/commands.cjs +823 -112
  96. package/gsd-core/bin/lib/config-loader.cjs +66 -4
  97. package/gsd-core/bin/lib/config.cjs +186 -45
  98. package/gsd-core/bin/lib/coverage.cjs +1 -1
  99. package/gsd-core/bin/lib/decisions.cjs +164 -45
  100. package/gsd-core/bin/lib/external-descriptor-trust.cjs +29 -14
  101. package/gsd-core/bin/lib/frontmatter.cjs +13 -0
  102. package/gsd-core/bin/lib/graphify.cjs +10 -2
  103. package/gsd-core/bin/lib/gsd2-import.cjs +1 -2
  104. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +12 -1
  105. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +1 -1
  106. package/gsd-core/bin/lib/host-runtime-detection.cjs +9 -0
  107. package/gsd-core/bin/lib/init.cjs +614 -86
  108. package/gsd-core/bin/lib/install-engine.cjs +29 -3
  109. package/gsd-core/bin/lib/install-profiles.cjs +14 -0
  110. package/gsd-core/bin/lib/installer-migrations.cjs +41 -5
  111. package/gsd-core/bin/lib/loop-resolver.cjs +50 -31
  112. package/gsd-core/bin/lib/mcp-catalog.cjs +2 -2
  113. package/gsd-core/bin/lib/milestone.cjs +37 -13
  114. package/gsd-core/bin/lib/model-resolver.cjs +253 -53
  115. package/gsd-core/bin/lib/phase-command-router.cjs +16 -2
  116. package/gsd-core/bin/lib/phase-id-card.cjs +32 -0
  117. package/gsd-core/bin/lib/phase-id-display.cjs +78 -0
  118. package/gsd-core/bin/lib/phase-id.cjs +268 -27
  119. package/gsd-core/bin/lib/phase-lifecycle.cjs +61 -0
  120. package/gsd-core/bin/lib/phase-locator.cjs +29 -10
  121. package/gsd-core/bin/lib/phase.cjs +393 -88
  122. package/gsd-core/bin/lib/plan-document.cjs +49 -1
  123. package/gsd-core/bin/lib/planning-document.cjs +459 -0
  124. package/gsd-core/bin/lib/planning-inspect.cjs +52 -19
  125. package/gsd-core/bin/lib/planning-snapshot.cjs +61 -12
  126. package/gsd-core/bin/lib/planning-workspace.cjs +57 -3
  127. package/gsd-core/bin/lib/pr-branch-patterns.cjs +57 -0
  128. package/gsd-core/bin/lib/pristine-baseline.cjs +182 -0
  129. package/gsd-core/bin/lib/probe-core.cjs +7 -1
  130. package/gsd-core/bin/lib/prohibition-enforcement.cjs +91 -4
  131. package/gsd-core/bin/lib/project-root.cjs +41 -2
  132. package/gsd-core/bin/lib/quick-batch.cjs +1 -1
  133. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +61 -2
  134. package/gsd-core/bin/lib/research-store.cjs +11 -12
  135. package/gsd-core/bin/lib/review-lane-descriptor.cjs +10 -30
  136. package/gsd-core/bin/lib/review-lane-invocation.cjs +23 -0
  137. package/gsd-core/bin/lib/review-reviewer-selection.cjs +2 -2
  138. package/gsd-core/bin/lib/reviewer-step-dispatch.cjs +337 -0
  139. package/gsd-core/bin/lib/roadmap-command-router.cjs +12 -4
  140. package/gsd-core/bin/lib/roadmap-parser.cjs +219 -18
  141. package/gsd-core/bin/lib/roadmap-upgrade.cjs +1539 -13
  142. package/gsd-core/bin/lib/roadmap.cjs +356 -42
  143. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +310 -41
  144. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +15 -4
  145. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +13 -5
  146. package/gsd-core/bin/lib/runtime-homes.cjs +4 -0
  147. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +408 -37
  148. package/gsd-core/bin/lib/runtime-name-policy.cjs +111 -1
  149. package/gsd-core/bin/lib/security.cjs +126 -7
  150. package/gsd-core/bin/lib/shell-command-projection.cjs +10 -6
  151. package/gsd-core/bin/lib/state-document.cjs +130 -28
  152. package/gsd-core/bin/lib/state-md-schema.cjs +21 -14
  153. package/gsd-core/bin/lib/state-transition.cjs +181 -30
  154. package/gsd-core/bin/lib/state.cjs +265 -27
  155. package/gsd-core/bin/lib/surface.cjs +77 -3
  156. package/gsd-core/bin/lib/task-command-router.cjs +12 -6
  157. package/gsd-core/bin/lib/tdd-red-evidence.cjs +78 -5
  158. package/gsd-core/bin/lib/uat-predicate.cjs +47 -4
  159. package/gsd-core/bin/lib/uat.cjs +9 -1
  160. package/gsd-core/bin/lib/ui-consideration-probe.cjs +15 -2
  161. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +100 -9
  162. package/gsd-core/bin/lib/undo-commit-selection.cjs +131 -0
  163. package/gsd-core/bin/lib/update-context.cjs +30 -24
  164. package/gsd-core/bin/lib/vendor/js-yaml.cjs +11 -3
  165. package/gsd-core/bin/lib/verification.cjs +315 -30
  166. package/gsd-core/bin/lib/verify-command-grounding.cjs +47 -3
  167. package/gsd-core/bin/lib/verify.cjs +320 -48
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +1 -0
  169. package/gsd-core/bin/lib/worktree-base-ref.cjs +482 -73
  170. package/gsd-core/bin/lib/worktree-safety.cjs +797 -58
  171. package/gsd-core/bin/shared/config-defaults.manifest.json +4 -0
  172. package/gsd-core/bin/shared/config-schema.manifest.json +6 -0
  173. package/gsd-core/bin/verify-reapply-patches.cjs +439 -80
  174. package/gsd-core/references/checkpoints.md +5 -3
  175. package/gsd-core/references/compact-content-gate.md +66 -0
  176. package/gsd-core/references/edge-probe-fixtures/01-round-half-even/expected-coverage.json +28 -3
  177. package/gsd-core/references/edge-probe-fixtures/02-merge-intervals/expected-coverage.json +37 -4
  178. package/gsd-core/references/edge-probe-fixtures/03-truncate-graphemes/expected-coverage.json +28 -3
  179. package/gsd-core/references/edge-probe-fixtures/04-money-rounding/expected-coverage.json +28 -3
  180. package/gsd-core/references/edge-probe-fixtures/05-list-dedupe/expected-coverage.json +37 -4
  181. package/gsd-core/references/edge-probe-fixtures/06-resolved-mixed/expected-coverage.json +37 -4
  182. package/gsd-core/references/edge-probe.md +195 -21
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +7 -6
  184. package/gsd-core/references/execute-phase-wave-guard.md +22 -11
  185. package/gsd-core/references/gsd-run-resolver.md +1 -1
  186. package/gsd-core/references/loop-hook-dispatch.md +18 -0
  187. package/gsd-core/references/model-profiles.md +13 -4
  188. package/gsd-core/references/phase-argument-parsing.md +9 -7
  189. package/gsd-core/references/phase-id-convention.md +28 -0
  190. package/gsd-core/references/planner-gap-closure.md +2 -0
  191. package/gsd-core/references/planner-load-graph-context.md +24 -13
  192. package/gsd-core/references/planner-verify-command-grounding.md +14 -0
  193. package/gsd-core/references/planning-config.md +14 -2
  194. package/gsd-core/references/tdd.md +30 -4
  195. package/gsd-core/references/thinking-models-planning.md +18 -2
  196. package/gsd-core/references/ui-consideration-probe.md +10 -5
  197. package/gsd-core/references/verification-patterns.md +17 -4
  198. package/gsd-core/references/verify-command-path-resolvability.md +10 -2
  199. package/gsd-core/references/worktree-path-safety.md +433 -2
  200. package/gsd-core/templates/README.md +7 -1
  201. package/gsd-core/templates/state.md +6 -3
  202. package/gsd-core/templates/summary.compact.md +212 -0
  203. package/gsd-core/templates/user-setup.compact.md +199 -0
  204. package/gsd-core/templates/user-setup.md +0 -9
  205. package/gsd-core/templates/verification-report.md +1 -1
  206. package/gsd-core/workflows/_runtime-launcher.snippet.sh +1 -1
  207. package/gsd-core/workflows/add-backlog.md +1 -1
  208. package/gsd-core/workflows/add-phase.md +1 -1
  209. package/gsd-core/workflows/add-tests.md +2 -2
  210. package/gsd-core/workflows/add-todo.md +6 -5
  211. package/gsd-core/workflows/ai-integration-phase.md +11 -3
  212. package/gsd-core/workflows/audit-fix.md +1 -1
  213. package/gsd-core/workflows/audit-milestone.md +1 -1
  214. package/gsd-core/workflows/audit-uat.md +1 -1
  215. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +9 -18
  216. package/gsd-core/workflows/autonomous.md +29 -16
  217. package/gsd-core/workflows/check-todos.md +6 -4
  218. package/gsd-core/workflows/cleanup.md +5 -3
  219. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +4 -3
  220. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +8 -1
  221. package/gsd-core/workflows/code-review-fix.md +108 -22
  222. package/gsd-core/workflows/code-review.md +216 -73
  223. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +274 -0
  224. package/gsd-core/workflows/complete-milestone.md +41 -264
  225. package/gsd-core/workflows/debug.md +3 -3
  226. package/gsd-core/workflows/diagnose-issues.md +1 -1
  227. package/gsd-core/workflows/discuss-phase/modes/advisor.md +1 -1
  228. package/gsd-core/workflows/discuss-phase/modes/chain.md +1 -1
  229. package/gsd-core/workflows/discuss-phase-assumptions.md +1 -1
  230. package/gsd-core/workflows/discuss-phase.md +1 -1
  231. package/gsd-core/workflows/do.md +2 -2
  232. package/gsd-core/workflows/docs-update/detail/elaboration.md +179 -0
  233. package/gsd-core/workflows/docs-update.md +17 -158
  234. package/gsd-core/workflows/edit-phase.md +1 -1
  235. package/gsd-core/workflows/eval-review.md +10 -3
  236. package/gsd-core/workflows/execute-phase/detail/elaboration.md +124 -0
  237. package/gsd-core/workflows/execute-phase/steps/code-review-disposition.md +1017 -0
  238. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +19 -4
  239. package/gsd-core/workflows/execute-phase/steps/completion-reconciliation.md +56 -0
  240. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +43 -4
  241. package/gsd-core/workflows/execute-phase/steps/executor-progress-policy.md +43 -0
  242. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +1 -1
  243. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +1 -1
  244. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +1 -1
  245. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +1 -1
  246. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +46 -9
  247. package/gsd-core/workflows/execute-phase/steps/protected-branch.md +1 -1
  248. package/gsd-core/workflows/execute-phase/steps/ready-wave-gate.md +37 -0
  249. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +1 -1
  250. package/gsd-core/workflows/execute-phase/steps/sequential-root-pin.md +35 -0
  251. package/gsd-core/workflows/execute-phase/steps/stale-reverification.md +24 -0
  252. package/gsd-core/workflows/execute-phase/steps/tdd-applicability-resolution.md +1 -1
  253. package/gsd-core/workflows/execute-phase/steps/threat-id-gate.md +28 -0
  254. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +1 -1
  255. package/gsd-core/workflows/execute-phase/steps/worktree-base-check.md +25 -0
  256. package/gsd-core/workflows/execute-phase.md +83 -172
  257. package/gsd-core/workflows/execute-plan.md +24 -10
  258. package/gsd-core/workflows/explore.md +3 -3
  259. package/gsd-core/workflows/extract-learnings.md +2 -1
  260. package/gsd-core/workflows/fast.md +1 -1
  261. package/gsd-core/workflows/forensics.md +1 -1
  262. package/gsd-core/workflows/graduation.md +1 -1
  263. package/gsd-core/workflows/health.md +2 -2
  264. package/gsd-core/workflows/help/modes/full.compact.md +398 -0
  265. package/gsd-core/workflows/help/modes/full.md +5 -5
  266. package/gsd-core/workflows/help/modes/topic.md +15 -5
  267. package/gsd-core/workflows/help.md +1 -1
  268. package/gsd-core/workflows/import.md +2 -2
  269. package/gsd-core/workflows/inbox.md +2 -2
  270. package/gsd-core/workflows/ingest-docs.md +3 -3
  271. package/gsd-core/workflows/insert-phase.md +1 -1
  272. package/gsd-core/workflows/list-seeds.md +1 -1
  273. package/gsd-core/workflows/list-workspaces.md +1 -1
  274. package/gsd-core/workflows/manager.md +2 -2
  275. package/gsd-core/workflows/map-codebase.md +52 -5
  276. package/gsd-core/workflows/milestone-summary.md +1 -1
  277. package/gsd-core/workflows/mvp-phase.md +1 -1
  278. package/gsd-core/workflows/new-milestone.md +56 -14
  279. package/gsd-core/workflows/new-project/detail/elaboration.md +216 -0
  280. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +3 -3
  281. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +1 -1
  282. package/gsd-core/workflows/new-project.md +39 -209
  283. package/gsd-core/workflows/new-workspace.md +2 -2
  284. package/gsd-core/workflows/next.md +1 -1
  285. package/gsd-core/workflows/note.md +1 -1
  286. package/gsd-core/workflows/onboard.md +1 -1
  287. package/gsd-core/workflows/pause-work.md +1 -1
  288. package/gsd-core/workflows/plan-phase/detail/elaboration.md +209 -0
  289. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +17 -5
  290. package/gsd-core/workflows/plan-phase/steps/prd-express-path.md +1 -1
  291. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +23 -5
  292. package/gsd-core/workflows/plan-phase.md +45 -187
  293. package/gsd-core/workflows/plan-review-convergence.md +21 -5
  294. package/gsd-core/workflows/plant-seed.md +62 -20
  295. package/gsd-core/workflows/pr-branch.md +132 -20
  296. package/gsd-core/workflows/profile-user.md +2 -2
  297. package/gsd-core/workflows/progress.md +1 -1
  298. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +25 -0
  299. package/gsd-core/workflows/quick/steps/quick-verification.md +1 -1
  300. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +1 -1
  301. package/gsd-core/workflows/quick-batch/steps/batch-init.md +1 -1
  302. package/gsd-core/workflows/quick-batch/steps/completion.md +1 -1
  303. package/gsd-core/workflows/quick-batch/steps/merge-wave.md +1 -1
  304. package/gsd-core/workflows/quick-batch/steps/planner-wave.md +1 -1
  305. package/gsd-core/workflows/quick-batch/steps/research-phase.md +1 -1
  306. package/gsd-core/workflows/quick-batch/steps/resume-mode.md +1 -1
  307. package/gsd-core/workflows/quick-batch/steps/verification-wave.md +1 -1
  308. package/gsd-core/workflows/quick-batch/steps/worktree-dispatch.md +1 -1
  309. package/gsd-core/workflows/quick-batch.md +1 -1
  310. package/gsd-core/workflows/quick.md +29 -10
  311. package/gsd-core/workflows/reapply-patches.md +86 -6
  312. package/gsd-core/workflows/remove-phase.md +1 -1
  313. package/gsd-core/workflows/remove-workspace.md +2 -2
  314. package/gsd-core/workflows/resume-project.md +1 -1
  315. package/gsd-core/workflows/review.md +31 -16
  316. package/gsd-core/workflows/scan.md +1 -1
  317. package/gsd-core/workflows/secure-phase.md +3 -2
  318. package/gsd-core/workflows/settings-advanced.md +30 -10
  319. package/gsd-core/workflows/settings-integrations.md +2 -3
  320. package/gsd-core/workflows/settings.md +22 -9
  321. package/gsd-core/workflows/ship.md +3 -2
  322. package/gsd-core/workflows/sketch-wrap-up.md +1 -1
  323. package/gsd-core/workflows/sketch.md +1 -1
  324. package/gsd-core/workflows/smart-entry.md +2 -2
  325. package/gsd-core/workflows/spec-phase.md +15 -5
  326. package/gsd-core/workflows/spike-wrap-up.md +1 -1
  327. package/gsd-core/workflows/spike.md +1 -1
  328. package/gsd-core/workflows/stats.md +1 -1
  329. package/gsd-core/workflows/sync-skills.md +5 -5
  330. package/gsd-core/workflows/thread.md +1 -1
  331. package/gsd-core/workflows/transition.md +1 -1
  332. package/gsd-core/workflows/ui-phase.md +44 -8
  333. package/gsd-core/workflows/ui-review.md +18 -4
  334. package/gsd-core/workflows/ultraplan-phase.md +1 -1
  335. package/gsd-core/workflows/undo.md +339 -20
  336. package/gsd-core/workflows/update.md +14 -12
  337. package/gsd-core/workflows/validate-phase.md +3 -2
  338. package/gsd-core/workflows/verify-work/detail/elaboration.md +230 -0
  339. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +1 -1
  340. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +1 -1
  341. package/gsd-core/workflows/verify-work.md +101 -196
  342. package/hooks/dist/gsd-agent-isolation-guard.js +66 -16
  343. package/hooks/dist/gsd-context-monitor.js +88 -15
  344. package/hooks/dist/gsd-cursor-subagent-start.js +34 -14
  345. package/hooks/dist/gsd-secret-read-guard.js +71 -19
  346. package/hooks/dist/gsd-statusline.js +81 -20
  347. package/hooks/dist/gsd-validate-commit.sh +97 -8
  348. package/hooks/dist/gsd-worktree-path-guard.js +25 -14
  349. package/hooks/dist/gsd-write-guard.js +46 -1
  350. package/hooks/dist/lib/dispatch-identity.js +187 -0
  351. package/hooks/dist/lib/filename-classification.js +64 -0
  352. package/hooks/dist/lib/isolation-deny-reason.js +53 -1
  353. package/hooks/dist/lib/isolation-sentinel.js +58 -19
  354. package/hooks/gsd-agent-isolation-guard.js +66 -16
  355. package/hooks/gsd-context-monitor.js +88 -15
  356. package/hooks/gsd-cursor-subagent-start.js +34 -14
  357. package/hooks/gsd-secret-read-guard.js +71 -19
  358. package/hooks/gsd-statusline.js +81 -20
  359. package/hooks/gsd-validate-commit.sh +97 -8
  360. package/hooks/gsd-worktree-path-guard.js +25 -14
  361. package/hooks/gsd-write-guard.js +46 -1
  362. package/hooks/lib/dispatch-identity.js +187 -0
  363. package/hooks/lib/filename-classification.js +64 -0
  364. package/hooks/lib/isolation-deny-reason.js +53 -1
  365. package/hooks/lib/isolation-sentinel.js +58 -19
  366. package/package.json +11 -6
  367. package/scripts/benchmark-compact-content-variants.cjs +298 -0
  368. package/scripts/benchmark-compact-content.cjs +368 -0
  369. package/scripts/build-hooks.js +15 -6
  370. package/scripts/check-contract-drift.cjs +131 -12
  371. package/scripts/check-env.cjs +36 -8
  372. package/scripts/check-glossary-refs.cjs +25 -21
  373. package/scripts/ci-next-health.cjs +271 -0
  374. package/scripts/ci-prepare-test-scope.cjs +7 -7
  375. package/scripts/ci-test-scope.cjs +126 -20
  376. package/scripts/ci-timeout-report.cjs +1 -1
  377. package/scripts/command-contract-helpers.cjs +3 -0
  378. package/scripts/diff-touches-shipped-paths.cjs +1 -1
  379. package/scripts/docs-guard-registry.cjs +35 -2
  380. package/scripts/gen-adr-index.cjs +8 -2
  381. package/scripts/gen-inventory-manifest.cjs +12 -0
  382. package/scripts/gen-loop-host-contract.cjs +69 -0
  383. package/scripts/gen-platform-conformance-tier.cjs +557 -0
  384. package/scripts/lib/drift-scan.cjs +1 -1
  385. package/scripts/lib/macos-conformance-tier.generated.cjs +224 -0
  386. package/scripts/lib/ndjson-reporter.cjs +3 -2
  387. package/scripts/lib/npm-version-check-diagnosis.cjs +59 -0
  388. package/scripts/lib/platform-conformance-tier.generated.cjs +287 -0
  389. package/scripts/lib/suite-detection.cjs +32 -0
  390. package/scripts/lint-allowed-tools-parity.cjs +221 -0
  391. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +47 -3
  392. package/scripts/lint-phase-arg-assignment.cjs +257 -0
  393. package/scripts/lint-phase-id-drift.cjs +623 -13
  394. package/scripts/lint-pr-branch-pattern-drift.cjs +148 -0
  395. package/scripts/lint-response-language-coverage.cjs +9 -3
  396. package/scripts/lint-retired-runtime-name.cjs +619 -0
  397. package/scripts/lint-source-test-name-collision.cjs +1 -1
  398. package/scripts/lint-state-write-path-drift.cjs +93 -0
  399. package/scripts/lint-test-file-count.allowlist.json +29 -9
  400. package/scripts/lint-vendored-deps.cjs +128 -17
  401. package/scripts/lint-workflow-shellcheck-baseline.json +100 -0
  402. package/scripts/prompt-injection-scan.sh +18 -0
  403. package/scripts/release-tarball-smoke.cjs +194 -1
  404. package/scripts/workflow-size.cjs +139 -0
  405. package/skills/gsd-autonomous/SKILL.md +2 -2
  406. package/skills/gsd-capture/SKILL.md +1 -1
  407. package/skills/gsd-cleanup/SKILL.md +1 -0
  408. package/skills/gsd-code-review/SKILL.md +2 -1
  409. package/skills/gsd-complete-milestone/SKILL.md +1 -0
  410. package/skills/gsd-config/SKILL.md +1 -0
  411. package/skills/gsd-debug/SKILL.md +1 -0
  412. package/skills/gsd-graphify/SKILL.md +1 -0
  413. package/skills/gsd-health/SKILL.md +1 -0
  414. package/skills/gsd-mempalace-capture/SKILL.md +8 -3
  415. package/skills/gsd-mempalace-recall/SKILL.md +1 -0
  416. package/skills/gsd-new-milestone/SKILL.md +1 -0
  417. package/skills/gsd-new-project/SKILL.md +1 -0
  418. package/skills/gsd-next/SKILL.md +1 -0
  419. package/skills/gsd-pause-work/SKILL.md +1 -0
  420. package/skills/gsd-phase/SKILL.md +1 -0
  421. package/skills/gsd-plan-review-convergence/SKILL.md +5 -5
  422. package/skills/gsd-pr-branch/SKILL.md +1 -0
  423. package/skills/gsd-progress/SKILL.md +1 -1
  424. package/skills/gsd-quick-batch/SKILL.md +1 -1
  425. package/skills/gsd-resume-work/SKILL.md +1 -0
  426. package/skills/gsd-review/SKILL.md +2 -3
  427. package/skills/gsd-review-backlog/SKILL.md +1 -0
  428. package/skills/gsd-settings/SKILL.md +2 -1
  429. package/skills/gsd-stats/SKILL.md +1 -0
  430. package/skills/gsd-thread/SKILL.md +1 -0
  431. package/skills/gsd-workspace/SKILL.md +1 -0
  432. package/skills/gsd-workstreams/SKILL.md +1 -0
  433. package/vscode/package.json +1 -1
  434. package/gsd-core/templates/claude-md.md +0 -145
  435. package/gsd-core/templates/codebase/concerns.md +0 -310
  436. package/gsd-core/templates/codebase/conventions.md +0 -307
  437. package/gsd-core/templates/codebase/integrations.md +0 -280
  438. package/gsd-core/templates/codebase/structure.md +0 -285
  439. package/gsd-core/templates/codebase/testing.md +0 -480
  440. package/gsd-core/templates/debug-subagent-prompt.md +0 -91
  441. package/gsd-core/templates/discovery.md +0 -146
@@ -18,8 +18,11 @@ exports.shortSha = shortSha;
18
18
  exports.readBaseRefFromSettings = readBaseRefFromSettings;
19
19
  exports.applyWorktreeBaseRef = applyWorktreeBaseRef;
20
20
  exports.resolveEffectiveBaseRef = resolveEffectiveBaseRef;
21
+ exports.findWorktreeCreateHook = findWorktreeCreateHook;
21
22
  exports.cmdWorktreeBaseCheck = cmdWorktreeBaseCheck;
22
23
  exports.cmdWorktreeSetBaseRef = cmdWorktreeSetBaseRef;
24
+ exports.classifyGitHead = classifyGitHead;
25
+ exports.observeHarnessForkFromHead = observeHarnessForkFromHead;
23
26
  exports.evaluateWorktreeBaseDegrade = evaluateWorktreeBaseDegrade;
24
27
  const node_fs_1 = __importDefault(require("node:fs"));
25
28
  const node_path_1 = __importDefault(require("node:path"));
@@ -93,14 +96,56 @@ function parseJsonc(text) {
93
96
  }
94
97
  }
95
98
  // ─── Message constants (verbatim — downstream docs/tests depend on these) ─────
99
+ // The fork side of the comparison is either an inferred ref (`origin/HEAD`,
100
+ // `origin/next`, …) or — when the caller supplies `observedForkBase` — the
101
+ // literal label below, meaning "the base a worktree this host created was
102
+ // measured to have" (#4588). Messages read the label to phrase the remedy.
103
+ const FORK_REF_OBSERVED = 'observed';
104
+ function describeForkRef(forkRef) {
105
+ return forkRef === FORK_REF_OBSERVED ? 'the observed fork base' : String(forkRef);
106
+ }
107
+ // An observation is a fixed measurement of one past dispatch: pushing cannot change it,
108
+ // so the remedy for an observed mismatch is a fresh dispatch (a new observation), never
109
+ // "push until the observation matches". The inferred fork base (origin/HEAD) does move
110
+ // with a push, so that remedy stays for the inferred case.
96
111
  function buildMsgDiverged(headSha, forkRef, forkSha) {
97
- return `⚠ Worktree base mismatch: HEAD (${shortSha(headSha)}) differs from ${forkRef} (${shortSha(forkSha)}). Running this phase sequentially on the main working tree. Parallel worktrees return once HEAD is merged/pushed so ${forkRef} matches it. (worktree.baseRef:"head" applies only where GSD itself creates the worktree — the runtime harness does not read it; #48, #3659.)`;
112
+ const fork = describeForkRef(forkRef);
113
+ const remedy = forkRef === FORK_REF_OBSERVED
114
+ ? 'Parallel worktrees return once a fresh dispatch is observed to fork from HEAD, or once HEAD is merged/pushed so the default fork base matches it'
115
+ : `Parallel worktrees return once HEAD is merged/pushed so ${fork} matches it`;
116
+ return `⚠ Worktree base mismatch: HEAD (${shortSha(headSha)}) differs from ${fork} (${shortSha(forkSha)}). Running this phase sequentially on the main working tree. ${remedy}, or set worktree.baseRef:"head" to fork worktrees from HEAD instead (honored by GSD-created worktrees and by the Claude Code harness; #683, #4588).`;
98
117
  }
99
118
  const MSG_UNKNOWN = `⚠ Cannot determine the worktree fork base (origin/HEAD unresolved). Running this phase sequentially on the main working tree to avoid a base mismatch. Parallel worktrees return once origin/HEAD resolves and matches HEAD. See #683, #3659.`;
100
- function buildMsgBaserefHeadIgnored(headSha, forkRef, forkSha) {
101
- return `⚠ Worktree base mismatch: worktree.baseRef:"head" is set, but the runtime harness does not honor it for isolated dispatch — the fork base stays ${forkRef} (${shortSha(forkSha)}) while HEAD is ${shortSha(headSha)} (#48; upstream claude-code#44965). Running this phase sequentially on the main working tree. Parallel worktrees return once HEAD is merged/pushed so ${forkRef} matches it, or on runtimes where GSD itself manages worktree creation. See #3659.`;
119
+ // Mode-neutral on purpose: the observation can come from a harness-created OR a
120
+ // GSD-created worktree, and the message must not attribute the miss to "the harness"
121
+ // when GSD's own `git worktree add` was the creator (P4.6 review, 2026-09-14).
122
+ function buildMsgBaserefHeadIgnored(headSha, forkSha) {
123
+ return `⚠ Worktree base mismatch: worktree.baseRef:"head" is set, but a worktree created for this dispatch was observed to fork from ${shortSha(forkSha)} while HEAD is ${shortSha(headSha)} — the worktree was not forked from HEAD despite the setting. Running this phase sequentially on the main working tree. Parallel worktrees return once a fresh dispatch is observed to fork from HEAD, or once HEAD is merged/pushed so the default fork base matches it. See #3659, #4588.`;
124
+ }
125
+ // Names the hook and its file, never "the harness": the user configured the hook, so the
126
+ // actionable remedy is theirs. An unparseable layer is phrased as "cannot be ruled out",
127
+ // because the check does not know a hook is there — it only cannot prove one is not.
128
+ // It deliberately promises nothing about pushing: neither HEAD nor the inferred fork base
129
+ // says where a hook forks, so the only measured way back to a trusted verdict is an
130
+ // observation (--observed-fork-base) or removing the cause (#4588 round review).
131
+ function buildMsgBaserefHeadHookBypass(headSha, forkRef, forkSha, finding) {
132
+ const fork = describeForkRef(forkRef);
133
+ const cause = finding.kind === 'hook'
134
+ ? `a Claude Code WorktreeCreate hook is configured in ${finding.file}`
135
+ : `${finding.file} could not be parsed, so a Claude Code WorktreeCreate hook in it cannot be ruled out`;
136
+ const remove = finding.kind === 'hook'
137
+ ? 'remove the hook'
138
+ : `fix ${finding.file} so it parses`;
139
+ return `⚠ Worktree base mismatch: worktree.baseRef:"head" is set, but ${cause}. A WorktreeCreate hook creates Claude Code's agent worktrees itself and Claude Code does not apply worktree.baseRef to them, so the setting is not trusted. Without it the check can only compare HEAD (${shortSha(headSha)}) against ${fork} (${shortSha(forkSha)}), and they differ. Running this phase sequentially on the main working tree. Neither ref says where the hook forks: for a measured verdict, pass the commit a hook-created worktree starts at as --observed-fork-base, or ${remove}. See #4588.`;
102
140
  }
103
- const MSG_HEAD_UNRESOLVABLE = `⚠ Cannot determine the worktree base (git rev-parse HEAD did not return a definitive answer). Running this phase sequentially on the main working tree to avoid an unverified base mismatch. Note: worktree.baseRef:"head" silences this check only where GSD itself creates the worktree (orchestrator-managed runtimes) — in harness mode it never applied (#48, #3659). Retry; if it persists, check for a stalled filesystem mount or a stale git index lock (.git/index.lock). See #683, #3050.`;
141
+ // A commit sha as `git rev-parse HEAD` prints it: 40 hex (SHA-1) or 64 hex (SHA-256).
142
+ // Abbreviated shas are refused rather than prefix-matched — the comparison below is
143
+ // exact, and an abbreviation that can never equal the full HEAD would silently always
144
+ // degrade (P4.6 review, 2026-09-14). Case is folded because the comparison is exact.
145
+ const FULL_SHA_RE = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
146
+ const MSG_OBSERVED_FORK_BASE_INVALID = 'observedForkBase must be a full 40- or 64-hex commit sha (git rev-parse HEAD inside the worktree, before any commit)';
147
+ const MSG_HEAD_UNRESOLVABLE = `⚠ Cannot determine the worktree base (git rev-parse HEAD did not return a definitive answer). Running this phase sequentially on the main working tree to avoid an unverified base mismatch. Retry; if it persists, check for a stalled filesystem mount or a stale git index lock (.git/index.lock). See #683, #3050.`;
148
+ const MSG_NO_GIT_REPOSITORY = `⚠ No worktree base exists here (git resolved no HEAD — the root is not a git repository, or the repository has no commits), so a harness worktree cannot be created. Running this dispatch sequentially on the main working tree instead — no isolation flag is required. See #4734.`;
104
149
  /**
105
150
  * Returns true when an execGit result indicates the subprocess was killed by
106
151
  * a timeout. A timeout means the command genuinely could not complete — it
@@ -221,6 +266,69 @@ function resolveEffectiveBaseRef(claudeDir, deps, userClaudeDir) {
221
266
  }
222
267
  return null;
223
268
  }
269
+ /**
270
+ * Looks for a Claude Code `WorktreeCreate` hook in the settings layers that
271
+ * resolveEffectiveBaseRef reads (#4588). Such a hook replaces the harness's own worktree
272
+ * creation: the agent worktree is whatever directory the hook emits, and Claude Code does
273
+ * not consult `worktree.baseRef` on that path. So on a host that configures one, `"head"`
274
+ * says nothing about where a harness-created worktree forks from.
275
+ *
276
+ * Claude Code merges hooks across layers, so every layer is checked — not only the one
277
+ * that supplied `baseRef`. Layers, in the same order and with the same user/global
278
+ * de-duplication as resolveEffectiveBaseRef:
279
+ * 1. <claudeDir>/settings.local.json
280
+ * 2. <claudeDir>/settings.json
281
+ * 3. <userClaudeDir>/settings.json (only when provided and a different directory)
282
+ *
283
+ * Returns the first finding in that order, or null:
284
+ * - kind 'hook' — `hooks.WorktreeCreate` is present and not an empty list.
285
+ * - kind 'unparseable' — the file exists but is not valid JSON/JSONC. Fails closed: a hook
286
+ * in it cannot be ruled out, and a false degrade costs a sequential
287
+ * wave where false trust costs every executor halting at exit 42.
288
+ * A layer deps.readFile reports as null (absent or unreadable) is skipped, exactly as in
289
+ * resolveEffectiveBaseRef, and so is a whitespace-only file, which cannot declare a hook.
290
+ *
291
+ * Settings files are the only hook sources readable from here. Claude Code also takes
292
+ * hooks from managed policy settings, a --settings file, plugins, agent frontmatter and
293
+ * SDK registrations; those stay invisible to this check, and the spawn-time exit-42 guard
294
+ * remains the backstop for them.
295
+ */
296
+ function findWorktreeCreateHook(claudeDir, deps, userClaudeDir) {
297
+ const readFile = deps?.readFile ?? ((p) => {
298
+ try {
299
+ return node_fs_1.default.readFileSync(p, 'utf8');
300
+ }
301
+ catch {
302
+ return null;
303
+ }
304
+ });
305
+ const layers = [node_path_1.default.join(claudeDir, 'settings.local.json'), node_path_1.default.join(claudeDir, 'settings.json')];
306
+ if (userClaudeDir && node_path_1.default.resolve(userClaudeDir) !== node_path_1.default.resolve(claudeDir)) {
307
+ layers.push(node_path_1.default.join(userClaudeDir, 'settings.json'));
308
+ }
309
+ for (const file of layers) {
310
+ const contents = readFile(file);
311
+ if (contents == null || contents.trim() === '')
312
+ continue;
313
+ let parsed;
314
+ try {
315
+ parsed = parseJsonc(contents);
316
+ }
317
+ catch {
318
+ return { file, kind: 'unparseable' };
319
+ }
320
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed))
321
+ continue;
322
+ const hooks = parsed.hooks;
323
+ if (hooks === null || typeof hooks !== 'object' || Array.isArray(hooks))
324
+ continue;
325
+ const entry = hooks.WorktreeCreate;
326
+ if (entry == null || (Array.isArray(entry) && entry.length === 0))
327
+ continue;
328
+ return { file, kind: 'hook' };
329
+ }
330
+ return null;
331
+ }
224
332
  /**
225
333
  * CLI command: check current worktree base-ref degradation status.
226
334
  *
@@ -245,16 +353,38 @@ function cmdWorktreeBaseCheck(cwd, args, deps) {
245
353
  }
246
354
  isolationMode = value;
247
355
  }
356
+ // --observed-fork-base <sha> threads a measured fork base through to the
357
+ // evaluation (#4588): what `git rev-parse HEAD` returned inside a worktree
358
+ // this host created, before any commit. Same fail-closed shape as --mode —
359
+ // a malformed or missing value throws rather than silently falling back to
360
+ // the inference the flag exists to replace.
361
+ let observedForkBase = null;
362
+ const observedIdx = args.indexOf('--observed-fork-base');
363
+ if (observedIdx !== -1) {
364
+ const value = args[observedIdx + 1];
365
+ if (typeof value !== 'string' || !FULL_SHA_RE.test(value.trim().toLowerCase())) {
366
+ throw new Error(`worktree base-check: --observed-fork-base: ${MSG_OBSERVED_FORK_BASE_INVALID}, got ${JSON.stringify(value ?? null)}`);
367
+ }
368
+ observedForkBase = value.trim().toLowerCase();
369
+ }
248
370
  const claudeDir = node_path_1.default.join(cwd, '.claude');
249
371
  const userClaudeDir = Object.prototype.hasOwnProperty.call(deps ?? {}, 'userClaudeDir')
250
372
  ? deps.userClaudeDir
251
373
  : (0, runtime_homes_cjs_1.getGlobalConfigDir)('claude');
252
374
  const effectiveBaseRef = resolveEffectiveBaseRef(claudeDir, deps?.readFile ? { readFile: deps.readFile } : undefined, userClaudeDir);
375
+ // The WorktreeCreate-hook interlock (#4588) only matters where the evaluation would
376
+ // otherwise trust "head" without comparing: harness-created worktrees and no
377
+ // observation. Skip the settings reads everywhere else.
378
+ const worktreeCreateHook = effectiveBaseRef === 'head' && isolationMode === 'harness-worktree' && observedForkBase === null
379
+ ? findWorktreeCreateHook(claudeDir, deps?.readFile ? { readFile: deps.readFile } : undefined, userClaudeDir)
380
+ : null;
253
381
  const result = evaluateWorktreeBaseDegrade({
254
382
  cwd,
255
383
  effectiveBaseRef,
256
384
  execGit: deps?.execGit,
257
385
  isolationMode,
386
+ observedForkBase,
387
+ worktreeCreateHook,
258
388
  });
259
389
  // Default emit goes through fs.writeSync(1, …), NOT process.stdout.write:
260
390
  // the CLI's --pick capture intercepts writeSync, and command substitution
@@ -342,10 +472,181 @@ function cmdWorktreeSetBaseRef(cwd, _args, deps) {
342
472
  write(JSON.stringify(output, null, 2) + '\n');
343
473
  return output;
344
474
  }
475
+ /**
476
+ * One classification of `git rev-parse HEAD`, shared by every surface that
477
+ * must tell git's definitive "no repository here" answer apart from ambiguous
478
+ * or failed resolutions (#4734 — before this, evaluateWorktreeBaseDegrade was
479
+ * the only owner and the isolation guard's fallback simply had no check).
480
+ * Exit 128 covers BOTH "not a git repository" and "a repository with no
481
+ * commits" (`ambiguous argument 'HEAD'`); stderr text is localized, so the
482
+ * exit code is the stable contract — and in neither case can a harness
483
+ * worktree be created, which is the only question every consumer asks.
484
+ *
485
+ * - `present`: exit 0 with a non-empty sha (`headSha` carries it, trimmed).
486
+ * - `definitive-absence`: exit 128 — git completed and definitively answered.
487
+ * - `ambiguous-absence`: exit 0 with empty stdout — git completed without a
488
+ * definitive answer (#3057 B8).
489
+ * - `indeterminate`: timeout or any other non-success — never evidence of
490
+ * anything; consumers must fail closed (#3050).
491
+ */
492
+ function classifyGitHead(deps) {
493
+ const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
494
+ const cwdOpts = deps?.cwd ? { cwd: deps.cwd } : {};
495
+ const headResult = execGit(['rev-parse', 'HEAD'], cwdOpts);
496
+ if (isExecGitTimeout(headResult)) {
497
+ return { status: 'indeterminate', headSha: null };
498
+ }
499
+ const headStdout = headResult.stdout ? headResult.stdout.trim() : '';
500
+ if (headResult.exitCode === 128) {
501
+ return { status: 'definitive-absence', headSha: null };
502
+ }
503
+ if (headResult.exitCode === 0 && !headStdout) {
504
+ return { status: 'ambiguous-absence', headSha: null };
505
+ }
506
+ if (headResult.exitCode !== 0) {
507
+ return { status: 'indeterminate', headSha: null };
508
+ }
509
+ return { status: 'present', headSha: headStdout };
510
+ }
511
+ /**
512
+ * #4588 (decision A2) — observe, from documented git metadata, whether the
513
+ * harness forks its worktrees from the orchestrator HEAD.
514
+ *
515
+ * Substrate: the harness's own prior worktrees. TWO LEGS answer the question —
516
+ * a cache read, then a live probe on a miss — and they are not the same kind
517
+ * of evidence.
518
+ *
519
+ * The LIVE PROBE looks for a linked worktree under
520
+ * `<repo>/.claude/worktrees/agent-*` that is still CLEAN (no tracked
521
+ * modifications; untracked review notes are fine) and whose HEAD equals the
522
+ * current orchestrator HEAD. That is POSITIVE evidence of fork-from-HEAD: an
523
+ * origin/HEAD fork would ordinarily have landed on an older commit once the
524
+ * orchestrator advanced. It is the only configuration that counts — every
525
+ * other observation (dirty worktree, different HEAD, no worktrees, git
526
+ * failure) is inconclusive and fails closed to the caller's existing flow.
527
+ *
528
+ * The CACHE leg looks at no worktree at all. It replays this function's OWN
529
+ * earlier conclusion for the same orchestrator HEAD, so a hit inherits whatever
530
+ * that earlier probe was worth and re-examines nothing — not the worktree, not
531
+ * even whether one still exists.
532
+ *
533
+ * EVIDENCE, NOT PROOF, AND THE LIVE PROBE FAILS IN TWO INDEPENDENT WAYS
534
+ * (#4921). What it reads is a worktree's PRESENT state — is it clean, where is
535
+ * its HEAD — and none of `worktree list --porcelain`, `status --porcelain` or
536
+ * `rev-parse HEAD` carries provenance.
537
+ *
538
+ * 1. It cannot say WHICH CREATOR. Where the harness is the only creator that
539
+ * is a distinction without a difference; where a `WorktreeCreate` hook is
540
+ * configured it is not, because a worktree the plain harness left behind
541
+ * BEFORE the hook existed — with HEAD unmoved since — is
542
+ * indistinguishable from one the hook made.
543
+ * 2. It cannot say WHAT IT WAS FORKED FROM either. A worktree created from an
544
+ * older base and since `git checkout --detach`ed onto the orchestrator
545
+ * HEAD is clean, sits at HEAD, and satisfies the probe identically. Note
546
+ * the shape of this one precisely: the worktree's own reflog DOES retain
547
+ * that original checkout, so the information is not lost — the probe just
548
+ * does not consult it, and #4868 did not design it to. Inherited from
549
+ * #4868 rather than introduced here, and accepted there for the no-hook
550
+ * case; stated so the strength of the signal is not overread.
551
+ *
552
+ * Both are reasons the observation is inadmissible once a hook is in the
553
+ * creation path, and the cache leg is a third, since it re-examines nothing at
554
+ * all. So the caller withholds it entirely under that interlock rather than
555
+ * re-keying it; see `hookWithheldHeadTrust` in evaluateWorktreeBaseDegrade.
556
+ *
557
+ * The verdict is cached at `<cwd>/.gsd/harness-fork-probe.json` keyed by the
558
+ * orchestrator HEAD (the decision's keying): trusted only while the
559
+ * orchestrator HEAD is unchanged; any HEAD move re-probes. Cache I/O failures
560
+ * are swallowed — the probe re-runs instead (#3659 fail-closed posture).
561
+ */
562
+ function observeHarnessForkFromHead(deps) {
563
+ const execGit = deps.execGit ?? shell_command_projection_cjs_1.execGit;
564
+ const cwd = deps.cwd ?? '.';
565
+ const cacheFile = node_path_1.default.join(cwd, '.gsd', 'harness-fork-probe.json');
566
+ const stateRead = deps.stateRead ?? ((file) => {
567
+ try {
568
+ return node_fs_1.default.readFileSync(file, 'utf8');
569
+ }
570
+ catch {
571
+ return null;
572
+ }
573
+ });
574
+ const stateWrite = deps.stateWrite ?? ((file, content) => {
575
+ try {
576
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(file), { recursive: true });
577
+ node_fs_1.default.writeFileSync(file, content, 'utf8');
578
+ }
579
+ catch {
580
+ // A cache write must never break the check that produced the verdict.
581
+ }
582
+ });
583
+ // 1. Cache — trusted only while the orchestrator HEAD is unchanged.
584
+ try {
585
+ const raw = stateRead(cacheFile);
586
+ if (raw) {
587
+ const cached = JSON.parse(raw);
588
+ if (cached.headSha === deps.headSha && cached.verdict === 'fork-from-head-confirmed') {
589
+ return { confirmed: true, source: 'cache', worktreePath: cached.worktreePath ?? null, worktreeHead: cached.worktreeHead ?? null };
590
+ }
591
+ }
592
+ }
593
+ catch {
594
+ // Corrupt cache — re-probe (fail open INTO the probe, which itself fails closed).
595
+ }
596
+ // 2. Probe: list linked worktrees, keep the harness's own, require a clean
597
+ // one whose HEAD equals the orchestrator HEAD. ANY failure — timeout,
598
+ // non-zero exit, or a throwing execGit — is an inconclusive observation,
599
+ // never a crash: the caller falls through to its existing flow (#4588).
600
+ try {
601
+ const list = execGit(['worktree', 'list', '--porcelain'], { cwd });
602
+ if (isExecGitTimeout(list) || list.exitCode !== 0) {
603
+ return { confirmed: false, source: 'none', worktreePath: null, worktreeHead: null };
604
+ }
605
+ const candidates = String(list.stdout || '')
606
+ .split('\n\n')
607
+ .map((block) => block.split('\n').find((l) => l.startsWith('worktree '))?.slice('worktree '.length).trim())
608
+ .filter((p) => !!p && p.includes('/.claude/worktrees/agent-'));
609
+ for (const wtPath of candidates) {
610
+ const status = execGit(['-C', wtPath, 'status', '--porcelain'], { cwd });
611
+ if (isExecGitTimeout(status) || status.exitCode !== 0)
612
+ continue;
613
+ const trackedDirty = String(status.stdout || '')
614
+ .split('\n')
615
+ .some((l) => l.trim() !== '' && !l.startsWith('?? '));
616
+ if (trackedDirty)
617
+ continue;
618
+ const wtHeadResult = execGit(['-C', wtPath, 'rev-parse', 'HEAD'], { cwd });
619
+ if (isExecGitTimeout(wtHeadResult) || wtHeadResult.exitCode !== 0)
620
+ continue;
621
+ const wtHead = wtHeadResult.stdout ? wtHeadResult.stdout.trim() : '';
622
+ if (wtHead && wtHead === deps.headSha) {
623
+ try {
624
+ stateWrite(cacheFile, `${JSON.stringify({
625
+ headSha: deps.headSha,
626
+ worktreePath: wtPath,
627
+ worktreeHead: wtHead,
628
+ verdict: 'fork-from-head-confirmed',
629
+ probedAt: new Date().toISOString(),
630
+ })}\n`);
631
+ }
632
+ catch {
633
+ // Cache write is best-effort; the verdict stands for this dispatch.
634
+ }
635
+ return { confirmed: true, source: 'probe', worktreePath: wtPath, worktreeHead: wtHead };
636
+ }
637
+ }
638
+ }
639
+ catch {
640
+ // A throwing execGit (unexpected stub shape, seam surprise) is an
641
+ // inconclusive observation, not a crash — the #3659 comparison governs.
642
+ }
643
+ return { confirmed: false, source: 'none', worktreePath: null, worktreeHead: null };
644
+ }
345
645
  /**
346
646
  * Evaluates whether the current worktree HEAD has diverged from the fork base
347
- * (origin/HEAD) that the Claude Code harness would use when creating a 'fresh'
348
- * parallel worktree.
647
+ * a 'fresh' parallel worktree would be created from — `origin/HEAD` when the
648
+ * fork base is inferred, or the base a worktree was actually observed to have
649
+ * when the caller supplies one (#4588).
349
650
  *
350
651
  * Returns a structured result with shouldDegrade, reason, and a user-visible
351
652
  * message when degradation is warranted.
@@ -354,81 +655,171 @@ function evaluateWorktreeBaseDegrade(deps) {
354
655
  const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
355
656
  const cwd = deps?.cwd;
356
657
  const cwdOpts = cwd ? { cwd } : {};
357
- // a. baseRef 'head' suppresses ONLY where GSD controls the fork start-point
358
- // (#3659). The former unconditional suppress trusted the harness to honor the
359
- // setting; #48 verified 5/5 that the Agent-isolation dispatch path never
360
- // routes through project settings (upstream claude-code#44965), so in
361
- // harness mode the fork base is always origin/HEAD and 'head' must fall
362
- // through to the same comparison the fresh path runs. In
363
- // orchestrator-worktree mode GSD itself runs `git worktree add <path>
364
- // <start-point>` with the orchestrator HEAD — 'head' is honored by
365
- // construction there and the suppress is correct. Any non-"head" value
366
- // (including "fresh" and absent/null) has fresh/origin-HEAD semantics and is
367
- // evaluated against origin/HEAD as before. (Reference: #683, #48, #3659.)
368
- const headIgnoredByHarness = deps?.effectiveBaseRef === 'head';
369
- if (headIgnoredByHarness && (deps?.isolationMode ?? 'harness-worktree') === 'orchestrator-worktree') {
658
+ const baseRefHead = deps?.effectiveBaseRef === 'head';
659
+ const observedRaw = deps?.observedForkBase;
660
+ // Only a string or an explicit absence is a legal observation. A number, object or
661
+ // boolean is a programmer error and must not be read as "no observation" — the same
662
+ // TypeError shape applyWorktreeBaseRef uses for a non-object (P4.6 review, round 2).
663
+ if (observedRaw != null && typeof observedRaw !== 'string') {
664
+ throw new TypeError(`evaluateWorktreeBaseDegrade: ${MSG_OBSERVED_FORK_BASE_INVALID}, got ${typeof observedRaw}`);
665
+ }
666
+ const observedTrimmed = typeof observedRaw === 'string' ? observedRaw.trim().toLowerCase() : '';
667
+ if (observedTrimmed && !FULL_SHA_RE.test(observedTrimmed)) {
668
+ throw new TypeError(`evaluateWorktreeBaseDegrade: ${MSG_OBSERVED_FORK_BASE_INVALID}, got ${JSON.stringify(observedRaw)}`);
669
+ }
670
+ const observedForkBase = observedTrimmed || null;
671
+ // a. baseRef 'head' with no observation: the fork base IS the orchestrator
672
+ // HEAD, in both isolation modes. orchestrator-worktree: GSD runs
673
+ // `git worktree add <path> <start-point>` with the orchestrator HEAD, so it
674
+ // holds by construction (#3659). harness-worktree: the harness honors the
675
+ // setting — measured on current Claude Code from the project-local,
676
+ // project-shared and user/global layers on macOS, Windows and Linux (#4588).
677
+ // The former harness-mode fall-through rested on #48's finding that the
678
+ // harness did not read the setting; that was true of the harness at the time
679
+ // and was fixed upstream (claude-code#54940), but the check inferred the
680
+ // fork base from the setting's value alone and so could not notice. It is
681
+ // not replaced with a version cutover: a host that does not honor `head`
682
+ // forks from somewhere else, and the spawn-time `worktree_branch_check`
683
+ // guard halts that executor at exit 42 before it commits — the
684
+ // observation-based check that already exists. A caller holding that
685
+ // observation passes it as `observedForkBase` and lands in c/d below, where
686
+ // a mismatch under `head` degrades with `baseref-head-ignored-by-harness`.
687
+ // Any non-"head" value (including "fresh" and absent/null) keeps
688
+ // fresh/origin-HEAD semantics and is evaluated against origin/HEAD as
689
+ // before — with the setting absent the harness does fork from origin/HEAD,
690
+ // so that degrade is a correct reading, not this bug. Measured on Claude
691
+ // Code only: Cursor also declares `harness-worktree` and is unmeasured, so
692
+ // there the trust rests on the exit-42 backstop alone until someone reads a
693
+ // worktree's HEAD on that host. (#683, #48, #3659, #4588.)
694
+ //
695
+ // The one exception is a Claude Code `WorktreeCreate` hook under harness-worktree
696
+ // (#4588): the hook creates the agent worktree from whatever directory it emits and the
697
+ // harness does not apply `worktree.baseRef` on that path, so the measurement above does
698
+ // not cover it. The short-circuit is withheld and the origin/HEAD inference below runs,
699
+ // as for a host without the setting; a mismatch degrades with
700
+ // `baseref-head-bypassed-by-hook`, and the ONLY thing that restores a trusted verdict
701
+ // there is an explicit `--observed-fork-base` measurement of this dispatch. b2's
702
+ // prior-worktree observation deliberately does NOT restore it — see
703
+ // `hookWithheldHeadTrust` below (#4868, #4881, #4921). orchestrator-worktree is
704
+ // unaffected — GSD runs `git worktree add` itself and no Claude Code hook is in that
705
+ // path.
706
+ const hookFinding = deps?.worktreeCreateHook ?? null;
707
+ const hookBypassesBaseRef = hookFinding !== null && (deps?.isolationMode ?? 'harness-worktree') === 'harness-worktree';
708
+ // The interlock's fail-closed half, scoped to exactly the case branch a. declined to
709
+ // trust: `"head"` is set AND a hook (or an unparseable layer) is in the harness's
710
+ // worktree-creation path. It is deliberately NOT `hookBypassesBaseRef` alone — with no
711
+ // `"head"` setting, b2 is #4868's own arm and this PR does not re-scope it.
712
+ const hookWithheldHeadTrust = baseRefHead && hookBypassesBaseRef;
713
+ if (baseRefHead && observedForkBase === null && !hookBypassesBaseRef) {
370
714
  return { shouldDegrade: false, reason: 'baseref-head', message: null, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: null };
371
715
  }
372
- // b. Resolve HEAD sha.
373
- const headResult = execGit(['rev-parse', 'HEAD'], cwdOpts);
374
- // A TIMEOUT means the command never completed — it is not evidence of "not a
375
- // git repository" and must fail closed (distinct from the clean-exit-128
376
- // "no-head" case below, which genuinely completed and reported no HEAD).
377
- if (isExecGitTimeout(headResult)) {
716
+ // b. Resolve HEAD sha — through the single classification owner (#4734).
717
+ const head = classifyGitHead({ execGit, cwd });
718
+ if (head.status === 'indeterminate') {
719
+ // A timeout, git missing (exit 127), or any other non-128 non-success is
720
+ // NOT a definitive answer from git — fail closed (#3050).
378
721
  return { shouldDegrade: true, reason: 'head-unresolvable', message: MSG_HEAD_UNRESOLVABLE, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: null };
379
722
  }
380
- const headStdout = headResult.stdout ? headResult.stdout.trim() : '';
381
- // exit 128 is git's definitive "not a git repository" answer — it completed
382
- // and genuinely reported no HEAD. Only this specific, confirmed outcome
383
- // stays a benign non-degrade; every other non-success outcome below is
384
- // NOT a definitive answer from git and must fail closed (#3050).
385
- if (headResult.exitCode === 128) {
386
- return { shouldDegrade: false, reason: 'no-head', message: null, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: true };
387
- }
388
- // Exit 0 with empty stdout is pinned as benign no-degrade by an existing
389
- // regression guard (tests/worktree-base-ref.test.cjs — "git rev-parse HEAD
390
- // returns empty stdout"). Left unchanged deliberately; flagged in the
391
- // #3050 review for a product-intent call rather than silently flipped.
392
- // Unlike the exit-128 case above, git did NOT give a definitive "no HEAD"
393
- // answer here — `headAbsenceVerified:false` names that gap explicitly
394
- // instead of leaving it folded into an identical-looking 'no-head' reason
395
- // (#3057 B8; the product question of whether this SHOULD degrade is
396
- // unchanged and still open).
397
- if (headResult.exitCode === 0 && !headStdout) {
723
+ if (head.status === 'definitive-absence') {
724
+ // Exit 128 is git's definitive "no resolvable HEAD here" answer — not a
725
+ // git repository, or a repository with no commits. In either case a
726
+ // harness worktree can NEVER be created, so demanding worktree isolation
727
+ // blocked every dispatch from e.g. a multi-repo workspace root (#4734).
728
+ // This wires the verdict `headAbsenceVerified` (#3057 B8) was added to
729
+ // enable; the maintainer brief on #4734 answers the previously-open
730
+ // product question: the definitive case degrades.
731
+ return { shouldDegrade: true, reason: 'no-head', message: MSG_NO_GIT_REPOSITORY, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: true };
732
+ }
733
+ if (head.status === 'ambiguous-absence') {
734
+ // Exit 0 with empty stdout is pinned as benign no-degrade by an existing
735
+ // regression guard (tests/worktree-base-ref.test.cjs — "git rev-parse HEAD
736
+ // returns empty stdout"). Unlike the exit-128 case above, git did NOT
737
+ // give a definitive "no HEAD" answer here — `headAbsenceVerified:false`
738
+ // names that gap explicitly (#3057 B8; the product question of whether
739
+ // this SHOULD degrade is unchanged and still open).
398
740
  return { shouldDegrade: false, reason: 'no-head', message: null, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: false };
399
741
  }
400
- if (headResult.exitCode !== 0) {
401
- // Any other non-success outcome (e.g. exit 127 — git missing — or any
402
- // other non-zero, non-128 exit) is not a definitive "not a repo" answer.
403
- // Fail closed instead of silently treating it as benign.
404
- // (`!headStdout` was previously OR'd in here but is unreachable: the
405
- // exitCode===0 && !headStdout case is already handled above, and every
406
- // other branch here has exitCode!==0 already true — #3050 review.)
407
- return { shouldDegrade: true, reason: 'head-unresolvable', message: MSG_HEAD_UNRESOLVABLE, headSha: null, forkRef: null, forkSha: null, headAbsenceVerified: null };
742
+ const headSha = head.headSha;
743
+ // b2. #4868 (#4588 decision A2): OBSERVED fork-from-HEAD confirmation. A
744
+ // clean prior harness worktree sitting exactly at the orchestrator HEAD is
745
+ // positive evidence the harness forks from HEAD — in harness mode that
746
+ // supersedes the origin/HEAD comparison for this dispatch (the stale
747
+ // origin/HEAD the comparison would degrade on is not where the harness
748
+ // forks). Fail-closed: every non-confirming observation falls through to
749
+ // the comparison below. Since #4881 this step is reached only when branch a
750
+ // did NOT trust the setting: the setting is absent or not "head", or a
751
+ // WorktreeCreate hook withheld the trust — and in that second case the
752
+ // observation is NOT consulted at all (`hookWithheldHeadTrust`), because it
753
+ // cannot be attributed to the hook. The evidence is a worktree sitting at
754
+ // HEAD; nothing on disk records WHICH creator left it there, so a clean
755
+ // worktree the plain harness created BEFORE the hook was configured, with
756
+ // HEAD unmoved since, is indistinguishable from one the hook created. Keying
757
+ // the cache by hook configuration does not close that: the cache is only one
758
+ // of the two legs, and a cache miss falls through to the live probe, which
759
+ // re-finds the same stale worktree and re-confirms. Under a hook the only
760
+ // admissible positive signal is an explicit `--observed-fork-base`
761
+ // measurement of THIS dispatch, which lands in c/d below; absent one the
762
+ // inferred comparison runs and a mismatch degrades with
763
+ // `baseref-head-bypassed-by-hook`, leaving the spawn-time exit-42 guard as
764
+ // the backstop. That keeps the interlock fail-closed end to end.
765
+ // This step is skipped for a second, unrelated reason when the caller
766
+ // supplies `observedForkBase`: an explicit measurement of this dispatch's
767
+ // fork base outranks an inference from a prior worktree, and the two must
768
+ // not disagree silently. The mode gate excludes orchestrator-worktree (no
769
+ // harness, no hook, nothing to observe), and probeStateRead/Write default to
770
+ // the .gsd cache file under cwd. With no `"head"` setting the #4868 arm is
771
+ // unchanged, hook or not — that trust predates this PR and is not re-scoped
772
+ // here (#4921 round 1).
773
+ if (observedForkBase === null && !hookWithheldHeadTrust && (deps?.isolationMode ?? 'harness-worktree') === 'harness-worktree') {
774
+ const observed = observeHarnessForkFromHead({
775
+ execGit,
776
+ cwd: deps?.cwd,
777
+ headSha,
778
+ stateRead: deps?.probeStateRead,
779
+ stateWrite: deps?.probeStateWrite,
780
+ });
781
+ if (observed.confirmed) {
782
+ return {
783
+ shouldDegrade: false,
784
+ reason: 'fork-from-head-observed',
785
+ message: null,
786
+ headSha,
787
+ forkRef: null,
788
+ forkSha: null,
789
+ headAbsenceVerified: null,
790
+ };
791
+ }
408
792
  }
409
- const headSha = headStdout;
410
- // c. Resolve fork base (what the harness forks 'fresh' worktrees from = origin/HEAD).
793
+ // c. Resolve fork base. An observation wins outright: it is what a worktree
794
+ // this host created actually forked from, so there is nothing to infer
795
+ // (#4588). Otherwise infer origin/HEAD — what a 'fresh' worktree forks from.
411
796
  let forkRef = null;
412
797
  let forkSha = null;
413
- // Try direct origin/HEAD rev-parse first.
414
- const directResult = execGit(['rev-parse', '--verify', '--quiet', 'origin/HEAD'], cwdOpts);
415
- const directStdout = directResult.stdout ? directResult.stdout.trim() : '';
416
- if (directResult.exitCode === 0 && directStdout) {
417
- forkRef = 'origin/HEAD';
418
- forkSha = directStdout;
798
+ if (observedForkBase !== null) {
799
+ forkRef = FORK_REF_OBSERVED;
800
+ forkSha = observedForkBase;
419
801
  }
420
802
  else {
421
- // Fall back via symbolic-ref → refs/remotes/origin/HEAD
422
- const symResult = execGit(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], cwdOpts);
423
- const symStdout = symResult.stdout ? symResult.stdout.trim() : '';
424
- if (symResult.exitCode === 0 && symStdout) {
425
- const ref = symStdout;
426
- const symShaResult = execGit(['rev-parse', '--verify', '--quiet', ref], cwdOpts);
427
- const symShaStdout = symShaResult.stdout ? symShaResult.stdout.trim() : '';
428
- if (symShaResult.exitCode === 0 && symShaStdout) {
429
- // Strip leading 'refs/remotes/' to get e.g. 'origin/next'
430
- forkRef = ref.replace(/^refs\/remotes\//, '');
431
- forkSha = symShaStdout;
803
+ // Try direct origin/HEAD rev-parse first.
804
+ const directResult = execGit(['rev-parse', '--verify', '--quiet', 'origin/HEAD'], cwdOpts);
805
+ const directStdout = directResult.stdout ? directResult.stdout.trim() : '';
806
+ if (directResult.exitCode === 0 && directStdout) {
807
+ forkRef = 'origin/HEAD';
808
+ forkSha = directStdout;
809
+ }
810
+ else {
811
+ // Fall back via symbolic-ref → refs/remotes/origin/HEAD
812
+ const symResult = execGit(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], cwdOpts);
813
+ const symStdout = symResult.stdout ? symResult.stdout.trim() : '';
814
+ if (symResult.exitCode === 0 && symStdout) {
815
+ const ref = symStdout;
816
+ const symShaResult = execGit(['rev-parse', '--verify', '--quiet', ref], cwdOpts);
817
+ const symShaStdout = symShaResult.stdout ? symShaResult.stdout.trim() : '';
818
+ if (symShaResult.exitCode === 0 && symShaStdout) {
819
+ // Strip leading 'refs/remotes/' to get e.g. 'origin/next'
820
+ forkRef = ref.replace(/^refs\/remotes\//, '');
821
+ forkSha = symShaStdout;
822
+ }
432
823
  }
433
824
  }
434
825
  }
@@ -437,10 +828,28 @@ function evaluateWorktreeBaseDegrade(deps) {
437
828
  return { shouldDegrade: true, reason: 'fork-ref-unknown', message: MSG_UNKNOWN, headSha, forkRef: null, forkSha: null, headAbsenceVerified: null };
438
829
  }
439
830
  if (forkSha === headSha) {
440
- return { shouldDegrade: false, reason: 'head-matches-fork', message: null, headSha, forkRef, forkSha, headAbsenceVerified: null };
831
+ const reason = forkRef === FORK_REF_OBSERVED ? 'observed-fork-matches-head' : 'head-matches-fork';
832
+ return { shouldDegrade: false, reason, message: null, headSha, forkRef, forkSha, headAbsenceVerified: null };
833
+ }
834
+ if (baseRefHead && observedForkBase === null && hookFinding !== null) {
835
+ // Reachable only through the hook interlock in a.: "head" was not trusted because a
836
+ // WorktreeCreate hook (or an unparseable settings layer) is in the harness's path, b2
837
+ // was withheld there (`hookWithheldHeadTrust` — a prior worktree cannot be attributed
838
+ // to the hook), and HEAD differs from the inferred fork base (#4588, #4881, #4921).
839
+ // So this is now the unconditional harness-mode verdict for a hook host with "head"
840
+ // set, a diverged HEAD and no `--observed-fork-base`. The inferred comparison is the one
841
+ // this check made in harness mode before #4588 — it is not a measurement of the hook, so
842
+ // a match above (head-matches-fork) does not prove the hook forks from HEAD either; the
843
+ // spawn-time exit-42 guard stays the backstop for that case, and it halts even in a
844
+ // hook-emitted directory that is not a git worktree (its branch check fails first).
845
+ const message = buildMsgBaserefHeadHookBypass(headSha, forkRef, forkSha, hookFinding);
846
+ return { shouldDegrade: true, reason: 'baseref-head-bypassed-by-hook', message, headSha, forkRef, forkSha, headAbsenceVerified: null };
441
847
  }
442
- if (headIgnoredByHarness) {
443
- const message = buildMsgBaserefHeadIgnored(headSha, forkRef, forkSha);
848
+ if (baseRefHead) {
849
+ // Reachable only with an observation (a. returned otherwise): the setting
850
+ // asked for HEAD and the measured fork base is something else — the
851
+ // existing degrade-and-warn, now reporting a measurement (#4588).
852
+ const message = buildMsgBaserefHeadIgnored(headSha, forkSha);
444
853
  return { shouldDegrade: true, reason: 'baseref-head-ignored-by-harness', message, headSha, forkRef, forkSha, headAbsenceVerified: null };
445
854
  }
446
855
  const message = buildMsgDiverged(headSha, forkRef, forkSha);