@opengsd/gsd-core 1.9.1 → 1.11.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 (426) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -3
  3. package/.opencode/plugins/gsd-core.js +8 -1
  4. package/agents/gsd-code-fixer.md +27 -3
  5. package/agents/gsd-debug-session-manager.md +11 -0
  6. package/agents/gsd-debugger.md +12 -246
  7. package/agents/gsd-doc-synthesizer.md +2 -4
  8. package/agents/gsd-executor.md +12 -10
  9. package/agents/gsd-integration-checker.md +3 -0
  10. package/agents/gsd-mempalace-curator.md +5 -2
  11. package/agents/gsd-phase-researcher.md +20 -1
  12. package/agents/gsd-plan-checker.md +46 -0
  13. package/agents/gsd-planner.md +49 -54
  14. package/agents/gsd-roadmapper.md +21 -3
  15. package/agents/gsd-user-profiler.md +3 -0
  16. package/agents/gsd-verifier.md +26 -73
  17. package/bin/install.js +1272 -1238
  18. package/bin/lib/ui-safety-gate.cjs +2 -0
  19. package/commands/gsd/code-review.md +1 -1
  20. package/commands/gsd/execute-phase.md +1 -1
  21. package/commands/gsd/map-codebase.md +1 -1
  22. package/commands/gsd/mempalace-capture.md +2 -2
  23. package/commands/gsd/mempalace-recall.md +1 -1
  24. package/commands/gsd/new-milestone.md +2 -2
  25. package/commands/gsd/plan-phase.md +1 -1
  26. package/commands/gsd/quick.md +1 -1
  27. package/commands/gsd/review-backlog.md +2 -1
  28. package/commands/gsd/verify-work.md +1 -1
  29. package/gsd-core/bin/gsd-tools.cjs +1009 -115
  30. package/gsd-core/bin/lib/active-workstream-store.cjs +153 -12
  31. package/gsd-core/bin/lib/agent-install-check.cjs +268 -38
  32. package/gsd-core/bin/lib/api-coverage.cjs +123 -5
  33. package/gsd-core/bin/lib/artifacts.cjs +3 -0
  34. package/gsd-core/bin/lib/assumption-delta.cjs +2 -4
  35. package/gsd-core/bin/lib/audit-command-router.cjs +9 -2
  36. package/gsd-core/bin/lib/audit.cjs +926 -202
  37. package/gsd-core/bin/lib/broken-windows.cjs +36 -6
  38. package/gsd-core/bin/lib/capability-consent.cjs +149 -15
  39. package/gsd-core/bin/lib/capability-lifecycle.cjs +45 -0
  40. package/gsd-core/bin/lib/capability-registry.cjs +608 -148
  41. package/gsd-core/bin/lib/capability-source.cjs +92 -0
  42. package/gsd-core/bin/lib/capability-trust.cjs +444 -25
  43. package/gsd-core/bin/lib/capability-validator.cjs +507 -24
  44. package/gsd-core/bin/lib/capability-writer.cjs +3 -2
  45. package/gsd-core/bin/lib/check-command-router.cjs +114 -38
  46. package/gsd-core/bin/lib/claude-orchestration.cjs +56 -3
  47. package/gsd-core/bin/lib/codex-agent-toml.cjs +329 -0
  48. package/gsd-core/bin/lib/command-aliases.cjs +94 -0
  49. package/gsd-core/bin/lib/command-roster.cjs +44 -1
  50. package/gsd-core/bin/lib/commands.cjs +665 -99
  51. package/gsd-core/bin/lib/commonjs-marker.cjs +142 -0
  52. package/gsd-core/bin/lib/complexity-trigger.cjs +1172 -0
  53. package/gsd-core/bin/lib/config-loader.cjs +76 -0
  54. package/gsd-core/bin/lib/config.cjs +22 -2
  55. package/gsd-core/bin/lib/context-composer.cjs +278 -0
  56. package/gsd-core/bin/lib/context-predicates.cjs +506 -0
  57. package/gsd-core/bin/lib/core-utils.cjs +217 -40
  58. package/gsd-core/bin/lib/decisions.cjs +23 -0
  59. package/gsd-core/bin/lib/docs.cjs +3 -2
  60. package/gsd-core/bin/lib/external-job.cjs +19 -4
  61. package/gsd-core/bin/lib/fallow-runner.cjs +20 -44
  62. package/gsd-core/bin/lib/frontmatter.cjs +239 -32
  63. package/gsd-core/bin/lib/gap-checker.cjs +68 -7
  64. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +57 -6
  65. package/gsd-core/bin/lib/git-base-branch.cjs +160 -15
  66. package/gsd-core/bin/lib/graphify.cjs +142 -27
  67. package/gsd-core/bin/lib/gsd2-import.cjs +37 -5
  68. package/gsd-core/bin/lib/health-diagnostic-rules/agent-install.cjs +101 -0
  69. package/gsd-core/bin/lib/health-diagnostic-rules/config-validation.cjs +348 -0
  70. package/gsd-core/bin/lib/health-diagnostic-rules/consistency.cjs +145 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/install-surface-shadowing.cjs +98 -0
  72. package/gsd-core/bin/lib/health-diagnostic-rules/milestone-archive-hygiene.cjs +100 -0
  73. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +222 -0
  74. package/gsd-core/bin/lib/health-diagnostic-rules/roadmap-disk-consistency.cjs +265 -0
  75. package/gsd-core/bin/lib/health-diagnostic-rules/root-existence.cjs +161 -0
  76. package/gsd-core/bin/lib/health-diagnostic-rules/state-consistency.cjs +303 -0
  77. package/gsd-core/bin/lib/health-diagnostic-rules/worktree-health.cjs +173 -0
  78. package/gsd-core/bin/lib/health-diagnostic-types.cjs +68 -0
  79. package/gsd-core/bin/lib/health-diagnostic.cjs +431 -0
  80. package/gsd-core/bin/lib/host-integration.cjs +13 -1
  81. package/gsd-core/bin/lib/host-runtime-detection.cjs +134 -0
  82. package/gsd-core/bin/lib/init-command-router.cjs +83 -8
  83. package/gsd-core/bin/lib/init.cjs +1325 -169
  84. package/gsd-core/bin/lib/install-effort-resolver.cjs +73 -30
  85. package/gsd-core/bin/lib/install-engine.cjs +805 -264
  86. package/gsd-core/bin/lib/install-fs-adapter.cjs +262 -0
  87. package/gsd-core/bin/lib/install-model-override-resolver.cjs +203 -0
  88. package/gsd-core/bin/lib/install-profiles.cjs +160 -57
  89. package/gsd-core/bin/lib/install-scope.cjs +270 -0
  90. package/gsd-core/bin/lib/install-shadow-report.cjs +385 -0
  91. package/gsd-core/bin/lib/installed-surface-resolver.cjs +381 -0
  92. package/gsd-core/bin/lib/installer-migration-authoring.cjs +3 -1
  93. package/gsd-core/bin/lib/installer-migration-report.cjs +4 -0
  94. package/gsd-core/bin/lib/installer-migrations/007-retire-config-root-commonjs-marker.cjs +149 -0
  95. package/gsd-core/bin/lib/installer-migrations/008-cursor-retire-commands-surface.cjs +55 -0
  96. package/gsd-core/bin/lib/installer-migrations/009-pi-retire-reserved-hooks-dir.cjs +199 -0
  97. package/gsd-core/bin/lib/installer-migrations.cjs +206 -13
  98. package/gsd-core/bin/lib/io.cjs +38 -3
  99. package/gsd-core/bin/lib/markdown-sectionizer.cjs +8 -1
  100. package/gsd-core/bin/lib/markdown-table.cjs +133 -20
  101. package/gsd-core/bin/lib/mcp-catalog.cjs +518 -0
  102. package/gsd-core/bin/lib/mcp-server.cjs +135 -3
  103. package/gsd-core/bin/lib/milestone-lock.cjs +248 -0
  104. package/gsd-core/bin/lib/milestone.cjs +821 -109
  105. package/gsd-core/bin/lib/model-catalog.cjs +59 -1
  106. package/gsd-core/bin/lib/model-resolver.cjs +183 -40
  107. package/gsd-core/bin/lib/normalize-test-command.cjs +1 -1
  108. package/gsd-core/bin/lib/pattern.cjs +122 -0
  109. package/gsd-core/bin/lib/phase-estimation.cjs +1 -1
  110. package/gsd-core/bin/lib/phase-id.cjs +507 -36
  111. package/gsd-core/bin/lib/phase-lifecycle.cjs +28 -3
  112. package/gsd-core/bin/lib/phase-locator.cjs +258 -58
  113. package/gsd-core/bin/lib/phase.cjs +891 -156
  114. package/gsd-core/bin/lib/plan-dependency-graph.cjs +303 -0
  115. package/gsd-core/bin/lib/plan-drift-guard.cjs +120 -0
  116. package/gsd-core/bin/lib/plan-scan.cjs +86 -2
  117. package/gsd-core/bin/lib/planning-scope.cjs +31 -0
  118. package/gsd-core/bin/lib/planning-snapshot.cjs +890 -0
  119. package/gsd-core/bin/lib/planning-workspace.cjs +60 -6
  120. package/gsd-core/bin/lib/probe-core.cjs +1 -1
  121. package/gsd-core/bin/lib/profile-output.cjs +1 -1
  122. package/gsd-core/bin/lib/prompt-budget.cjs +128 -165
  123. package/gsd-core/bin/lib/refactor-trigger-command-router.cjs +740 -0
  124. package/gsd-core/bin/lib/retired-artifact-cleanup.cjs +85 -0
  125. package/gsd-core/bin/lib/review-lane-descriptor.cjs +108 -0
  126. package/gsd-core/bin/lib/review-lane-invocation.cjs +30 -0
  127. package/gsd-core/bin/lib/review-lane-runner.cjs +447 -68
  128. package/gsd-core/bin/lib/review-reviewer-selection.cjs +13 -18
  129. package/gsd-core/bin/lib/roadmap-command-router.cjs +76 -9
  130. package/gsd-core/bin/lib/roadmap-parser.cjs +1035 -194
  131. package/gsd-core/bin/lib/roadmap-upgrade.cjs +37 -10
  132. package/gsd-core/bin/lib/roadmap.cjs +405 -84
  133. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +795 -100
  134. package/gsd-core/bin/lib/runtime-artifact-install-plan.cjs +14 -2
  135. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +440 -57
  136. package/gsd-core/bin/lib/runtime-config-adapter-registry.cjs +3 -2
  137. package/gsd-core/bin/lib/runtime-homes.cjs +220 -41
  138. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +220 -44
  139. package/gsd-core/bin/lib/runtime-name-policy.cjs +3 -1
  140. package/gsd-core/bin/lib/runtime-slash.cjs +27 -9
  141. package/gsd-core/bin/lib/section-manifest.cjs +209 -0
  142. package/gsd-core/bin/lib/security.cjs +104 -5
  143. package/gsd-core/bin/lib/shell-command-projection.cjs +388 -30
  144. package/gsd-core/bin/lib/smart-entry.cjs +154 -22
  145. package/gsd-core/bin/lib/state-command-router.cjs +5 -1
  146. package/gsd-core/bin/lib/state-document.cjs +152 -8
  147. package/gsd-core/bin/lib/state-transition.cjs +424 -105
  148. package/gsd-core/bin/lib/state.cjs +1927 -401
  149. package/gsd-core/bin/lib/surface.cjs +35 -10
  150. package/gsd-core/bin/lib/text-lines.cjs +80 -0
  151. package/gsd-core/bin/lib/token-scanner.cjs +76 -0
  152. package/gsd-core/bin/lib/uat-predicate.cjs +20 -4
  153. package/gsd-core/bin/lib/uat.cjs +706 -64
  154. package/gsd-core/bin/lib/ui-frontend-evidence.cjs +157 -0
  155. package/gsd-core/bin/lib/ui-safety-gate.cjs +14 -5
  156. package/gsd-core/bin/lib/unusable-input.cjs +33 -0
  157. package/gsd-core/bin/lib/update-context.cjs +8 -2
  158. package/gsd-core/bin/lib/user-artifact-staging.cjs +705 -0
  159. package/gsd-core/bin/lib/validate.cjs +20 -6
  160. package/gsd-core/bin/lib/vendor/README.md +37 -0
  161. package/gsd-core/bin/lib/vendor/re2js.cjs +6480 -0
  162. package/gsd-core/bin/lib/vendor/re2js.d.cts +938 -0
  163. package/gsd-core/bin/lib/verification-command-router.cjs +2 -1
  164. package/gsd-core/bin/lib/verification.cjs +287 -20
  165. package/gsd-core/bin/lib/verify.cjs +368 -880
  166. package/gsd-core/bin/lib/workflow-fragments.cjs +557 -0
  167. package/gsd-core/bin/lib/workstream-inventory-builder.cjs +203 -19
  168. package/gsd-core/bin/lib/workstream-inventory.cjs +576 -31
  169. package/gsd-core/bin/lib/workstream.cjs +8 -2
  170. package/gsd-core/bin/lib/worktree-base-ref.cjs +50 -6
  171. package/gsd-core/bin/lib/worktree-safety.cjs +450 -125
  172. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  173. package/gsd-core/bin/shared/config-schema.manifest.json +9 -1
  174. package/gsd-core/references/agent-contracts.md +43 -26
  175. package/gsd-core/references/artifact-types.md +10 -3
  176. package/gsd-core/references/autonomous-ui-design-contract.md +42 -0
  177. package/gsd-core/references/checkpoints.md +2 -2
  178. package/gsd-core/references/context-budget.md +1 -1
  179. package/gsd-core/references/debugger-techniques.md +255 -0
  180. package/gsd-core/references/dispatch-isolation-gate.md +138 -0
  181. package/gsd-core/references/doc-conflict-engine.md +1 -1
  182. package/gsd-core/references/execute-mvp-tdd.md +3 -3
  183. package/gsd-core/references/execute-phase-between-wave-reset.md +6 -2
  184. package/gsd-core/references/execute-phase-context-guard.md +1 -1
  185. package/gsd-core/references/execute-phase-response-language.md +1 -1
  186. package/gsd-core/references/execute-phase-wave-guard.md +6 -2
  187. package/gsd-core/references/gate-prompts.md +1 -1
  188. package/gsd-core/references/git-planning-commit.md +2 -1
  189. package/gsd-core/references/loop-hook-dispatch.md +39 -2
  190. package/gsd-core/references/model-profiles.md +12 -4
  191. package/gsd-core/references/mvp-concepts.md +9 -9
  192. package/gsd-core/references/planner-guidance.md +3 -9
  193. package/gsd-core/references/planner-preconditions.md +1 -1
  194. package/gsd-core/references/planner-reviews.md +1 -1
  195. package/gsd-core/references/planning-config.md +8 -6
  196. package/gsd-core/references/research-documentation-lookup.md +5 -3
  197. package/gsd-core/references/revision-loop.md +1 -1
  198. package/gsd-core/references/specless-probe-fallback.md +8 -7
  199. package/gsd-core/references/universal-anti-patterns.md +3 -3
  200. package/gsd-core/references/verifier-phase-gates.md +192 -0
  201. package/gsd-core/references/verifier-wiring-patterns.md +100 -0
  202. package/gsd-core/references/verify-mvp-mode.md +1 -1
  203. package/gsd-core/references/workstream-flag.md +22 -6
  204. package/gsd-core/references/worktree-branch-check.md +2 -2
  205. package/gsd-core/templates/discussion-log.md +1 -1
  206. package/gsd-core/templates/phase-prompt.md +2 -4
  207. package/gsd-core/templates/state.md +4 -4
  208. package/gsd-core/templates/summary-complex.md +2 -0
  209. package/gsd-core/templates/summary-minimal.md +2 -0
  210. package/gsd-core/templates/summary-standard.md +2 -0
  211. package/gsd-core/templates/summary.md +2 -0
  212. package/gsd-core/templates/verification-report.md +9 -1
  213. package/gsd-core/workflows/ai-integration-phase.md +9 -11
  214. package/gsd-core/workflows/audit-milestone.md +3 -0
  215. package/gsd-core/workflows/autonomous/steps/converge-banner.md +1 -0
  216. package/gsd-core/workflows/autonomous/steps/converge-dispatch-bg.md +11 -0
  217. package/gsd-core/workflows/autonomous/steps/converge-dispatch-inline.md +7 -0
  218. package/gsd-core/workflows/autonomous/steps/converge-fail-fast.md +21 -0
  219. package/gsd-core/workflows/autonomous/steps/converge-loop.md +7 -0
  220. package/gsd-core/workflows/autonomous.md +33 -70
  221. package/gsd-core/workflows/cleanup.md +62 -3
  222. package/gsd-core/workflows/code-review/steps/dispatch-fix.md +39 -0
  223. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +93 -0
  224. package/gsd-core/workflows/code-review-fix.md +37 -10
  225. package/gsd-core/workflows/code-review.md +74 -166
  226. package/gsd-core/workflows/complete-milestone/steps/git-tag.md +29 -0
  227. package/gsd-core/workflows/complete-milestone.md +160 -95
  228. package/gsd-core/workflows/debug.md +16 -17
  229. package/gsd-core/workflows/diagnose-issues.md +56 -8
  230. package/gsd-core/workflows/discuss-phase/modes/chain.md +2 -1
  231. package/gsd-core/workflows/discuss-phase/modes/default.md +1 -1
  232. package/gsd-core/workflows/discuss-phase-assumptions/steps/auto-advance-dispatch.md +15 -0
  233. package/gsd-core/workflows/discuss-phase-assumptions.md +7 -17
  234. package/gsd-core/workflows/docs-update/steps/dispatch-monorepo-packages.md +51 -0
  235. package/gsd-core/workflows/docs-update.md +8 -51
  236. package/gsd-core/workflows/edit-phase.md +26 -1
  237. package/gsd-core/workflows/eval-review.md +3 -5
  238. package/gsd-core/workflows/execute-phase/steps/executor-isolation-dispatch.md +64 -7
  239. package/gsd-core/workflows/execute-phase/steps/gap-closure-artifacts.md +50 -0
  240. package/gsd-core/workflows/execute-phase/steps/partial-wave.md +31 -0
  241. package/gsd-core/workflows/execute-phase/steps/per-plan-executor-routing.md +77 -0
  242. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +21 -0
  243. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +42 -0
  244. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +43 -37
  245. package/gsd-core/workflows/execute-phase.md +103 -187
  246. package/gsd-core/workflows/execute-plan.md +36 -4
  247. package/gsd-core/workflows/explore.md +131 -4
  248. package/gsd-core/workflows/fast.md +10 -2
  249. package/gsd-core/workflows/health.md +73 -4
  250. package/gsd-core/workflows/help/modes/full.md +6 -1
  251. package/gsd-core/workflows/import.md +4 -4
  252. package/gsd-core/workflows/ingest-docs.md +7 -6
  253. package/gsd-core/workflows/mvp-phase.md +6 -3
  254. package/gsd-core/workflows/new-milestone/steps/project-md-milestone-write.md +16 -0
  255. package/gsd-core/workflows/new-milestone/steps/reset-phase-safety.md +19 -0
  256. package/gsd-core/workflows/new-milestone.md +35 -47
  257. package/gsd-core/workflows/new-project/steps/auto-mode-config.md +176 -0
  258. package/gsd-core/workflows/new-project/steps/auto-mode-detection.md +32 -0
  259. package/gsd-core/workflows/new-project/steps/codebase-map-offer.md +18 -0
  260. package/gsd-core/workflows/new-project.md +27 -240
  261. package/gsd-core/workflows/next.md +12 -0
  262. package/gsd-core/workflows/plan-phase/steps/adr-ingest-express-path.md +15 -0
  263. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +110 -0
  264. package/gsd-core/workflows/plan-phase/steps/prd-express-gate.md +8 -0
  265. package/gsd-core/workflows/plan-phase/steps/research-only-early-exit.md +17 -0
  266. package/gsd-core/workflows/plan-phase/steps/research-only-modifiers.md +16 -0
  267. package/gsd-core/workflows/plan-phase/steps/reviews-prerequisite.md +17 -0
  268. package/gsd-core/workflows/plan-phase/steps/stall-detection-helpers.md +149 -0
  269. package/gsd-core/workflows/plan-phase.md +89 -209
  270. package/gsd-core/workflows/plan-review-convergence.md +50 -2
  271. package/gsd-core/workflows/progress/steps/forensic-audit.md +125 -0
  272. package/gsd-core/workflows/progress/steps/mvp-display.md +18 -0
  273. package/gsd-core/workflows/progress.md +45 -159
  274. package/gsd-core/workflows/quick/steps/discussion-phase.md +124 -0
  275. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +111 -0
  276. package/gsd-core/workflows/quick/steps/quick-verification.md +67 -0
  277. package/gsd-core/workflows/quick/steps/research-phase.md +72 -0
  278. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +37 -0
  279. package/gsd-core/workflows/quick.md +55 -405
  280. package/gsd-core/workflows/resume-project.md +3 -0
  281. package/gsd-core/workflows/review/steps/reviewer-instances-note-1.md +4 -0
  282. package/gsd-core/workflows/review/steps/reviewer-instances-note-2.md +3 -0
  283. package/gsd-core/workflows/review.md +41 -13
  284. package/gsd-core/workflows/section-manifest.json +219 -0
  285. package/gsd-core/workflows/secure-phase.md +1 -1
  286. package/gsd-core/workflows/session-report.md +2 -1
  287. package/gsd-core/workflows/settings.md +66 -2
  288. package/gsd-core/workflows/ship.md +104 -44
  289. package/gsd-core/workflows/sketch.md +1 -1
  290. package/gsd-core/workflows/spec-phase.md +41 -20
  291. package/gsd-core/workflows/spike-wrap-up.md +20 -5
  292. package/gsd-core/workflows/spike.md +50 -16
  293. package/gsd-core/workflows/sync-skills.md +106 -13
  294. package/gsd-core/workflows/transition/steps/workstream-collision-check.md +17 -0
  295. package/gsd-core/workflows/transition.md +53 -31
  296. package/gsd-core/workflows/ui-phase.md +13 -12
  297. package/gsd-core/workflows/ui-review.md +2 -2
  298. package/gsd-core/workflows/update/steps/channel-banner.md +7 -0
  299. package/gsd-core/workflows/update.md +19 -8
  300. package/gsd-core/workflows/validate-phase.md +1 -1
  301. package/gsd-core/workflows/verify-work/steps/automated-ui-verification.md +36 -0
  302. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +21 -0
  303. package/gsd-core/workflows/verify-work.md +17 -65
  304. package/hooks/dist/gsd-agent-isolation-guard.js +517 -0
  305. package/hooks/dist/gsd-check-update-worker.js +64 -12
  306. package/hooks/dist/gsd-check-update.js +19 -1
  307. package/hooks/dist/gsd-cursor-pre-tool.js +0 -3
  308. package/hooks/dist/gsd-cursor-subagent-start.js +607 -26
  309. package/hooks/dist/gsd-cursor-subagent-stop.js +3 -2
  310. package/hooks/dist/gsd-prompt-guard.js +21 -20
  311. package/hooks/dist/gsd-read-injection-scanner.js +45 -24
  312. package/hooks/dist/gsd-statusline.js +90 -6
  313. package/hooks/dist/gsd-update-banner.js +22 -1
  314. package/hooks/dist/gsd-workflow-guard.js +134 -36
  315. package/hooks/dist/gsd-worktree-path-guard.js +2 -1
  316. package/hooks/dist/gsd-write-guard.js +359 -0
  317. package/hooks/dist/lib/git-cmd.js +92 -59
  318. package/hooks/dist/lib/injection-patterns.js +45 -0
  319. package/hooks/dist/lib/isolation-deny-reason.js +39 -0
  320. package/hooks/dist/lib/isolation-sentinel.js +277 -0
  321. package/hooks/dist/managed-hooks-registry.cjs +2 -0
  322. package/hooks/gsd-agent-isolation-guard.js +517 -0
  323. package/hooks/gsd-check-update-worker.js +64 -12
  324. package/hooks/gsd-check-update.js +19 -1
  325. package/hooks/gsd-cursor-pre-tool.js +0 -3
  326. package/hooks/gsd-cursor-subagent-start.js +607 -26
  327. package/hooks/gsd-cursor-subagent-stop.js +3 -2
  328. package/hooks/gsd-prompt-guard.js +21 -20
  329. package/hooks/gsd-read-injection-scanner.js +45 -24
  330. package/hooks/gsd-statusline.js +90 -6
  331. package/hooks/gsd-update-banner.js +22 -1
  332. package/hooks/gsd-workflow-guard.js +134 -36
  333. package/hooks/gsd-worktree-path-guard.js +2 -1
  334. package/hooks/gsd-write-guard.js +359 -0
  335. package/hooks/hooks.json +12 -0
  336. package/hooks/lib/git-cmd.js +92 -59
  337. package/hooks/lib/injection-patterns.js +45 -0
  338. package/hooks/lib/isolation-deny-reason.js +39 -0
  339. package/hooks/lib/isolation-sentinel.js +277 -0
  340. package/hooks/managed-hooks-registry.cjs +2 -0
  341. package/package.json +31 -10
  342. package/pi/gsd.cjs +71 -12
  343. package/scripts/baselines/planning-prompt-drift-baseline.json +4 -0
  344. package/scripts/baselines/planning-snapshot-bypass-baseline.json +12 -0
  345. package/scripts/baselines/unreachable-guard-drift-baseline.json +4 -0
  346. package/scripts/build-hooks.js +9 -0
  347. package/scripts/changeset/lint.cjs +68 -6
  348. package/scripts/changeset/serialize.cjs +5 -1
  349. package/scripts/check-alias-drift.cjs +7 -43
  350. package/scripts/check-contract-drift.cjs +297 -0
  351. package/scripts/ci-test-scope.cjs +19 -2
  352. package/scripts/command-contract-helpers.cjs +903 -1
  353. package/scripts/gen-adr-index.cjs +728 -38
  354. package/scripts/gen-capability-matrix.cjs +1 -1
  355. package/scripts/gen-capability-registry.cjs +3 -15
  356. package/scripts/gen-context-index.cjs +439 -0
  357. package/scripts/gen-health-docs.cjs +390 -0
  358. package/scripts/gen-inventory-manifest.cjs +150 -4
  359. package/scripts/gen-loop-host-contract.cjs +4 -24
  360. package/scripts/gen-prompt-budget-parity-corpus.cjs +645 -0
  361. package/scripts/gen-registry.cjs +3 -14
  362. package/scripts/gen-section-manifest.cjs +638 -0
  363. package/scripts/generate-package-identity.cjs +4 -2
  364. package/scripts/lib/alias-drift-families.cjs +46 -0
  365. package/scripts/lib/drift-scan.cjs +278 -0
  366. package/scripts/lint-allow-test-rule-refs.allowlist.json +15 -54
  367. package/scripts/lint-allow-test-rule-refs.effective-ceiling.json +4 -0
  368. package/scripts/lint-allow-test-rule-refs.unverified-ceiling.json +3 -0
  369. package/scripts/lint-canary-version-leak.cjs +73 -0
  370. package/scripts/lint-command-contract.cjs +96 -13
  371. package/scripts/lint-compiled-artifact-sync.cjs +6 -1
  372. package/scripts/lint-completion-predicate-drift.cjs +933 -0
  373. package/scripts/lint-completion-ratio-drift.cjs +214 -0
  374. package/scripts/lint-default-flip-documentation.cjs +193 -0
  375. package/scripts/lint-docs-command-form.cjs +195 -0
  376. package/scripts/lint-docs-required.cjs +9 -1
  377. package/scripts/lint-emitted-drift-ack.cjs +215 -20
  378. package/scripts/lint-eslint-glob-coverage.allowlist.json +34 -0
  379. package/scripts/lint-eslint-glob-coverage.cjs +340 -0
  380. package/scripts/lint-example-parser-parity.cjs +395 -0
  381. package/scripts/lint-frontmatter-scalar-broad-grep.cjs +237 -0
  382. package/scripts/lint-health-diagnostic-rule-table.cjs +404 -0
  383. package/scripts/lint-hooks-runtime-build-seam.cjs +262 -0
  384. package/scripts/lint-milestone-window-drift.cjs +468 -0
  385. package/scripts/lint-phase-enumeration-drift.cjs +479 -0
  386. package/scripts/lint-plan-count-drift.cjs +318 -0
  387. package/scripts/lint-planning-artifact-writer-drift.cjs +398 -0
  388. package/scripts/lint-planning-prompt-drift.cjs +434 -0
  389. package/scripts/lint-planning-snapshot-bypass-drift.cjs +544 -0
  390. package/scripts/lint-regression-test-names.cjs +15 -13
  391. package/scripts/lint-removed-but-needed.cjs +320 -0
  392. package/scripts/lint-state-field-drift.cjs +805 -0
  393. package/scripts/lint-state-write-path-drift.cjs +1045 -0
  394. package/scripts/lint-test-file-count.allowlist.json +40 -3
  395. package/scripts/lint-unreachable-guard-drift.cjs +843 -0
  396. package/scripts/lint-vendored-deps.cjs +124 -0
  397. package/scripts/mutation-matrix.cjs +13 -0
  398. package/scripts/pr-changed-files.cjs +63 -0
  399. package/scripts/pr-template-policy.cjs +14 -4
  400. package/scripts/prompt-injection-scan.sh +52 -6
  401. package/scripts/require-issue-link-policy.cjs +192 -0
  402. package/scripts/state-write-path-drift-baseline.json +19 -0
  403. package/scripts/sync-runtime-launcher.cjs +2 -4
  404. package/skills/gsd-autonomous/SKILL.md +0 -1
  405. package/skills/gsd-code-review/SKILL.md +1 -1
  406. package/skills/gsd-execute-phase/SKILL.md +1 -2
  407. package/skills/gsd-map-codebase/SKILL.md +1 -1
  408. package/skills/gsd-mempalace-capture/SKILL.md +2 -2
  409. package/skills/gsd-mempalace-recall/SKILL.md +1 -1
  410. package/skills/gsd-new-milestone/SKILL.md +2 -2
  411. package/skills/gsd-next/SKILL.md +0 -1
  412. package/skills/gsd-plan-phase/SKILL.md +1 -2
  413. package/skills/gsd-progress/SKILL.md +0 -1
  414. package/skills/gsd-quick/SKILL.md +1 -1
  415. package/skills/gsd-review-backlog/SKILL.md +2 -1
  416. package/skills/gsd-stats/SKILL.md +0 -1
  417. package/skills/gsd-verify-work/SKILL.md +1 -1
  418. package/vscode/package.json +1 -1
  419. package/gsd-core/workflows/discovery-phase.md +0 -298
  420. package/gsd-core/workflows/plan-milestone-gaps.md +0 -281
  421. package/gsd-core/workflows/verify-phase.md +0 -577
  422. package/scripts/affected-tests-lib.cjs +0 -554
  423. package/scripts/gen-emitted-baseline.cjs +0 -145
  424. package/scripts/lint-allow-test-rule-refs.cjs +0 -162
  425. package/scripts/run-affected-tests.cjs +0 -7
  426. package/scripts/run-tests.cjs +0 -1050
@@ -18,7 +18,14 @@
18
18
  * 5. "main" (last-resort default)
19
19
  *
20
20
  * Every git subprocess is bounded with a timeout (≤ 30 s); on timeout/error
21
- * the resolver degrades gracefully to the next tier — it never throws.
21
+ * the resolver degrades gracefully to the next tier — it never throws. Tier 5
22
+ * is reachable two ways that `resolveBaseBranch()` alone cannot tell apart: a
23
+ * repository that genuinely has no candidate branch (every git query on tiers
24
+ * 2-4 completed and cleanly answered "nothing"), or a total resolution
25
+ * failure (some query timed out / could not run). `resolveBaseBranchDiagnostics()`
26
+ * distinguishes the two via `verified`; `cmdGitBaseBranch` surfaces the
27
+ * unverified case as a stderr diagnostic without changing its stdout contract
28
+ * (#3057 B4).
22
29
  *
23
30
  * Pure/testable: all I/O is injectable via the `deps` argument so unit
24
31
  * tests can run without touching the real filesystem or spawning real git.
@@ -31,8 +38,11 @@ exports.readConfigBaseBranch = readConfigBaseBranch;
31
38
  exports.trySymbolicRef = trySymbolicRef;
32
39
  exports.tryRemoteShow = tryRemoteShow;
33
40
  exports.tryLocalBranch = tryLocalBranch;
41
+ exports.resolveBaseBranchDiagnostics = resolveBaseBranchDiagnostics;
34
42
  exports.resolveBaseBranch = resolveBaseBranch;
35
43
  exports.gitWorktreeInfoInternal = gitWorktreeInfoInternal;
44
+ exports.phaseStartCommit = phaseStartCommit;
45
+ exports.changedFilesSince = changedFilesSince;
36
46
  exports.cmdGitBaseBranch = cmdGitBaseBranch;
37
47
  const node_fs_1 = __importDefault(require("node:fs"));
38
48
  const node_path_1 = __importDefault(require("node:path"));
@@ -115,7 +125,8 @@ function tryRemoteShow(cwd, execGit) {
115
125
  const branch = m[1];
116
126
  // git emits "(unknown)" when the remote is offline but the local cache
117
127
  // resolved it; treat that as non-authoritative and fall through.
118
- if (!branch || branch === '(unknown)')
128
+ // No `!branch ||` guard: m[1] comes from the `(\S+)` capture group above, so it is never empty.
129
+ if (branch === '(unknown)')
119
130
  return null;
120
131
  return branch;
121
132
  }
@@ -153,41 +164,70 @@ function tryLocalBranch(cwd, execGit) {
153
164
  }
154
165
  }
155
166
  /**
156
- * Resolve the default/base branch for the repository at `cwd`.
167
+ * Resolve the default/base branch for the repository at `cwd`, along with
168
+ * whether the tier-5 last-resort default (if reached) was verified.
157
169
  *
158
170
  * Consults the full precedence ladder and always returns a non-empty string.
159
171
  * Never throws.
160
172
  */
161
- function resolveBaseBranch(cwd, deps) {
162
- const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
173
+ function resolveBaseBranchDiagnostics(cwd, deps) {
174
+ const rawExecGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
175
+ // A genuine execGit failure (timeout, or the call could not even spawn —
176
+ // e.g. git missing, surfaced as exitCode 127 with `error` set) is distinct
177
+ // from git completing and cleanly reporting a negative answer (non-zero
178
+ // exit with no useful output, or exit 0 with empty stdout). Only the former
179
+ // means a tier's answer was never actually obtained. Wrapping execGit here
180
+ // observes every tier's calls uniformly without changing trySymbolicRef /
181
+ // tryRemoteShow / tryLocalBranch's own return contracts.
182
+ let anyGitFailure = false;
183
+ const execGit = (args, opts) => {
184
+ const r = rawExecGit(args, opts);
185
+ if (r.timedOut || r.error)
186
+ anyGitFailure = true;
187
+ return r;
188
+ };
163
189
  // Derive .planning dir relative to cwd (mirrors planningDir() in planning-workspace.cjs)
164
190
  const planningDir = node_path_1.default.join(cwd, '.planning');
165
191
  // 1. Config override
166
192
  const configured = readConfigBaseBranch(planningDir, deps);
167
193
  if (configured)
168
- return configured;
194
+ return { branch: configured, verified: true };
169
195
  // 2. symbolic-ref (fast, no network)
170
196
  const symref = trySymbolicRef(cwd, execGit);
171
197
  if (symref)
172
- return symref;
198
+ return { branch: symref, verified: true };
173
199
  // 3. git remote show origin (authoritative when origin/HEAD unset)
174
200
  const remoteShow = tryRemoteShow(cwd, execGit);
175
201
  if (remoteShow)
176
- return remoteShow;
202
+ return { branch: remoteShow, verified: true };
177
203
  // 4. Local branch existence
178
204
  const local = tryLocalBranch(cwd, execGit);
179
205
  if (local)
180
- return local;
181
- // 5. Last-resort default
182
- return 'main';
206
+ return { branch: local, verified: true };
207
+ // 5. Last-resort default. `verified:false` when at least one tier-2/3/4
208
+ // execGit call timed out or failed to run — the default was never actually
209
+ // checked against this repository, it is just what's left after git could
210
+ // not answer (#3057 B4).
211
+ return { branch: 'main', verified: !anyGitFailure };
212
+ }
213
+ /**
214
+ * Resolve the default/base branch for the repository at `cwd`.
215
+ *
216
+ * Consults the full precedence ladder and always returns a non-empty string.
217
+ * Never throws. See {@link resolveBaseBranchDiagnostics} for a caller that
218
+ * needs to distinguish a verified answer from an unverified fallback.
219
+ */
220
+ function resolveBaseBranch(cwd, deps) {
221
+ return resolveBaseBranchDiagnostics(cwd, deps).branch;
183
222
  }
184
223
  /**
185
224
  * Detect whether `cwd` sits inside a git worktree, and if so, return the
186
225
  * absolute path of the worktree root.
187
226
  */
188
- function gitWorktreeInfoInternal(cwd) {
227
+ function gitWorktreeInfoInternal(cwd, deps) {
228
+ const execGit = deps?.execGit ?? shell_command_projection_cjs_1.execGit;
189
229
  try {
190
- const insideResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 });
230
+ const insideResult = execGit(['rev-parse', '--is-inside-work-tree'], { cwd, timeout: 5000 });
191
231
  if (insideResult.exitCode !== 0) {
192
232
  return { inside: false, worktreeRoot: null };
193
233
  }
@@ -195,7 +235,7 @@ function gitWorktreeInfoInternal(cwd) {
195
235
  if (insideStdout !== 'true') {
196
236
  return { inside: false, worktreeRoot: null };
197
237
  }
198
- const rootResult = (0, shell_command_projection_cjs_1.execGit)(['rev-parse', '--show-toplevel'], { cwd, timeout: 5000 });
238
+ const rootResult = execGit(['rev-parse', '--show-toplevel'], { cwd, timeout: 5000 });
199
239
  if (rootResult.exitCode !== 0) {
200
240
  return { inside: true, worktreeRoot: null };
201
241
  }
@@ -206,6 +246,106 @@ function gitWorktreeInfoInternal(cwd) {
206
246
  return { inside: false, worktreeRoot: null };
207
247
  }
208
248
  }
249
+ // ─── Adapter 3: phase-start anchor + touched-file listing (issue #1953) ───────
250
+ /**
251
+ * Resolve the commit that ADDED `<phaseDir>/*-PLAN.md` — the anchor commit
252
+ * marking when the phase began (see `.gsd/phase/feat-1953-complexity-triggered-
253
+ * refactor/42-router-contract.md`, "Touched-file anchor"). `phaseDir` is a
254
+ * project-relative path (backslashes are normalized unconditionally before
255
+ * building the pathspec, never via `path.sep` — matches the repo's
256
+ * cross-platform path-normalization convention).
257
+ *
258
+ * Bounded (`timeout: 15_000`), degrades to `null` on any failure or when no
259
+ * such commit exists (a phase never planned through git, a shallow clone).
260
+ * Never throws.
261
+ */
262
+ function phaseStartCommit(cwd, phaseDir, execGit) {
263
+ const git = execGit ?? shell_command_projection_cjs_1.execGit;
264
+ try {
265
+ const normalizedPhaseDir = phaseDir.replace(/\\/g, '/');
266
+ const pathspec = `${normalizedPhaseDir}/*-PLAN.md`;
267
+ const r = git(['log', '--format=%H', '--diff-filter=A', '-1', '--', pathspec], { cwd, timeout: 15_000 });
268
+ if (r.exitCode !== 0 || !r.stdout)
269
+ return null;
270
+ const sha = r.stdout.trim();
271
+ return sha || null;
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ /**
278
+ * Reject characters/sequences that have no legitimate use in a git revision
279
+ * expression reaching this module (a ref name, SHA, or `<ref>~N` / `<ref>^N`
280
+ * / `<ref>@{...}` navigation) but that a hostile `--since` value could use to
281
+ * confuse either the shell-out or a downstream reader: whitespace, ASCII
282
+ * control characters, and the option-shaped `?`, `*`, `[`, `\` characters
283
+ * that `git check-ref-format` also disallows in ref *names*. A leading `-`
284
+ * is rejected outright — that is the actual option-injection vector `--end-
285
+ * of-options` (below) already neutralizes, so this is belt-and-suspenders
286
+ * for older git. `..` is rejected because `sinceRef` is a single revision
287
+ * that this function itself turns into a range (`sinceRef..HEAD`); a
288
+ * `sinceRef` that already contains `..` can only produce a malformed or
289
+ * misleading range. A trailing `.lock` is rejected per `check-ref-format`.
290
+ *
291
+ * Deliberately NOT rejected: `~`, `^`, `:`, `@`, `{`, `}` — `check-ref-
292
+ * format` disallows these in a bare ref *name*, but this value is a git
293
+ * *revision expression*, and rejecting them would break entirely ordinary
294
+ * user input such as `HEAD~1`, `HEAD^`, or `main@{yesterday}`. None of
295
+ * these characters can reintroduce option parsing once `--end-of-options`
296
+ * is in effect, so allowing them costs nothing security-wise.
297
+ */
298
+ function isSafeRevisionRef(ref) {
299
+ if (ref === '')
300
+ return false;
301
+ if (ref.startsWith('-'))
302
+ return false;
303
+ if (/[\x00-\x1f\x7f ?*[\\]/.test(ref))
304
+ return false;
305
+ if (ref.includes('..'))
306
+ return false;
307
+ if (ref.endsWith('.lock'))
308
+ return false;
309
+ return true;
310
+ }
311
+ /**
312
+ * List files changed between `sinceRef` and `HEAD`, NUL-delimited and
313
+ * quotepath-safe. Load-bearing details (see the router contract):
314
+ * - `-z` and `-c core.quotepath=false` avoid git's lossy quote-and-escape
315
+ * round-trip for non-ASCII paths;
316
+ * - splitting on `NUL` (never `\n`) tolerates a filename containing a real
317
+ * newline (git permits it);
318
+ * - `sinceRef` is validated by `isSafeRevisionRef` AND the revision-range
319
+ * argument is preceded by `--end-of-options`. A trailing `--` alone does
320
+ * NOT stop git from option-parsing an argument that appears BEFORE it —
321
+ * it only stops PATHSPEC interpretation of arguments AFTER it — so
322
+ * `--since '--output=/tmp/pwn'` would otherwise become the argument
323
+ * `--output=/tmp/pwn..HEAD`, which git accepts as an option and uses to
324
+ * redirect diff output to an attacker-chosen path. `--end-of-options`
325
+ * (git >= 2.24) is the correct fix: everything after it is parsed as a
326
+ * revision or path, never as an option, regardless of leading `-`.
327
+ *
328
+ * Bounded (`timeout: 15_000`), degrades to `null` when `sinceRef` fails
329
+ * validation or the underlying git call fails (non-zero exit, timeout, or
330
+ * spawn error) — never throws. An empty result set (no files changed
331
+ * between the two revisions) is a valid, non-null answer: `[]`.
332
+ */
333
+ function changedFilesSince(cwd, sinceRef, execGit) {
334
+ if (!isSafeRevisionRef(sinceRef))
335
+ return null;
336
+ const git = execGit ?? shell_command_projection_cjs_1.execGit;
337
+ try {
338
+ const r = git(['-c', 'core.quotepath=false', 'diff', '--name-only', '-z', '--end-of-options', `${sinceRef}..HEAD`, '--'], { cwd, timeout: 15_000 });
339
+ if (r.exitCode !== 0)
340
+ return null;
341
+ if (!r.stdout)
342
+ return [];
343
+ return r.stdout.split('\0').filter((f) => f.length > 0);
344
+ }
345
+ catch {
346
+ return null;
347
+ }
348
+ }
209
349
  // ─── CLI entry point ──────────────────────────────────────────────────────────
210
350
  /**
211
351
  * CLI command: `gsd-tools git base-branch`
@@ -213,7 +353,12 @@ function gitWorktreeInfoInternal(cwd) {
213
353
  * Called by workflows via `gsd_run query git.base-branch`.
214
354
  */
215
355
  function cmdGitBaseBranch(cwd, _args, deps) {
216
- const branch = resolveBaseBranch(cwd, deps);
356
+ const { branch, verified } = resolveBaseBranchDiagnostics(cwd, deps);
357
+ if (!verified) {
358
+ const writeDiagnostic = deps?.writeDiagnostic ?? ((s) => process.stderr.write(s));
359
+ writeDiagnostic(`⚠ git-base-branch: defaulted to 'main' WITHOUT verifying against this repository — ` +
360
+ `a git query timed out or could not run. See #3057.\n`);
361
+ }
217
362
  const write = deps?.write ?? ((s) => process.stdout.write(s));
218
363
  write(branch + '\n');
219
364
  return branch;
@@ -16,6 +16,15 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
16
16
  // eslint-disable-next-line @typescript-eslint/no-require-imports
17
17
  const capabilityStateMod = require("./capability-state.cjs");
18
18
  const { isCapabilityActive } = capabilityStateMod;
19
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- io.cjs is an export= CommonJS module
20
+ const ioMod = require("./io.cjs");
21
+ const { serializeForOutput } = ioMod;
22
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- prompt-budget.cjs is an export= CommonJS module
23
+ const promptBudget = require("./prompt-budget.cjs");
24
+ // The repo's single token scale (phase-estimation.cts documents the rule: a
25
+ // ratio between two measurement methods measures the methods, not the miss).
26
+ // This module previously carried a private copy of the same chars/4 formula.
27
+ const { estimateTokens } = promptBudget;
19
28
  /**
20
29
  * Return the standard disabled response object.
21
30
  */
@@ -52,8 +61,12 @@ function execGraphify(cwd, args, options = {}) {
52
61
  reason: GRAPHIFY_REASON.ENOENT,
53
62
  };
54
63
  }
55
- // Timeout — seam exposes signal; spawnSync sets SIGTERM when killed by timeout.
56
- if (result.signal === 'SIGTERM') {
64
+ // Timeout — result.timedOut is derived by the shared isSpawnTimeout predicate
65
+ // (shell-command-projection.cts), keyed on error.code === 'ETIMEDOUT' rather
66
+ // than signal === 'SIGTERM': Windows does not reliably report SIGTERM on a
67
+ // timeout kill, and an externally-delivered SIGTERM (error is null) is not
68
+ // a timeout at all.
69
+ if (result.timedOut) {
57
70
  return {
58
71
  exitCode: 124,
59
72
  stdout: result.stdout,
@@ -104,15 +117,28 @@ function checkGraphifyVersion() {
104
117
  }
105
118
  }
106
119
  // Strategy 2: fall back to python3 importlib.metadata
120
+ let pyPackageConfirmed = false;
107
121
  if (!versionStr) {
108
122
  const pyResult = (0, shell_command_projection_cjs_1.execTool)('python3', [
109
123
  '-c',
110
124
  'from importlib.metadata import version; print(version("graphifyy"))',
111
125
  ], { timeout: 5000 });
112
126
  if (!pyResult.error && pyResult.exitCode === 0 && pyResult.stdout) {
113
- versionStr = pyResult.stdout;
127
+ versionStr = pyResult.stdout.trim();
128
+ pyPackageConfirmed = true; // importlib.metadata confirmed the package
114
129
  }
115
130
  }
131
+ else {
132
+ // #3020: verify the `graphify` binary on PATH is actually the graphifyy
133
+ // package — a foreign binary that happens to print a version-like string
134
+ // must not silently report compatible. If importlib.metadata cannot confirm
135
+ // the package, emit an identity warning even if the version looks right.
136
+ const pyVerify = (0, shell_command_projection_cjs_1.execTool)('python3', [
137
+ '-c',
138
+ 'from importlib.metadata import version; print(version("graphifyy"))',
139
+ ], { timeout: 5000 });
140
+ pyPackageConfirmed = !pyVerify.error && pyVerify.exitCode === 0 && !!pyVerify.stdout;
141
+ }
116
142
  if (!versionStr) {
117
143
  return { version: null, compatible: null, warning: 'Could not determine graphify version' };
118
144
  }
@@ -120,9 +146,19 @@ function checkGraphifyVersion() {
120
146
  if (parts.length < 2 || parts.some(isNaN)) {
121
147
  return { version: versionStr, compatible: null, warning: 'Could not parse version: ' + versionStr };
122
148
  }
123
- const compatible = parts[0] === 0 && parts[1] >= 4;
124
- const warning = compatible ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
125
- return { version: versionStr, compatible, warning };
149
+ const versionInRange = parts[0] === 0 && parts[1] >= 4;
150
+ // #3020: if the `graphify` binary answered --version but the Python package
151
+ // graphifyy could not be confirmed, the tool identity is unverified — emit
152
+ // a warning naming the mismatch regardless of version-range compatibility.
153
+ if (!pyPackageConfirmed) {
154
+ return {
155
+ version: versionStr,
156
+ compatible: false,
157
+ warning: 'graphify version ' + versionStr + ' detected but the graphifyy Python package could not be confirmed — the `graphify` binary on PATH may be a different tool. Verify with: pip show graphifyy',
158
+ };
159
+ }
160
+ const warning = versionInRange ? null : 'graphify version ' + versionStr + ' is outside tested range >=0.4.0,<1.0';
161
+ return { version: versionStr, compatible: versionInRange, warning };
126
162
  }
127
163
  /**
128
164
  * Safely read and parse a JSON file. Returns null on missing file or parse error.
@@ -195,41 +231,118 @@ function seedAndExpand(graph, term, maxHops = 2) {
195
231
  const resultNodes = [...visitedNodes].map(id => nodeMap[id]).filter((n) => Boolean(n));
196
232
  return { nodes: resultNodes, edges: collectedEdges, seeds: new Set(seeds.map(n => n.id)) };
197
233
  }
234
+ /**
235
+ * The single definition of `graphifyQuery`'s wire shape.
236
+ *
237
+ * Both the emitter (`graphifyQuery`'s return) and the budget estimator go
238
+ * through here, so `budget_estimate` measures the object the caller actually
239
+ * receives rather than a private approximation of it (#2738).
240
+ */
241
+ function buildQueryResponse(term, core) {
242
+ return {
243
+ term,
244
+ nodes: core.nodes,
245
+ edges: core.edges,
246
+ total_nodes: core.nodes.length,
247
+ total_edges: core.edges.length,
248
+ trimmed: core.trimmed,
249
+ // Budget outcome (#2738) — only present when a budget was requested
250
+ ...(core.budget ? { budget_met: core.budget.met, budget_estimate: core.budget.estimate } : {}),
251
+ };
252
+ }
198
253
  /**
199
254
  * Apply token budget by dropping edges by confidence tier (D-04, D-05, D-06).
200
- * Token estimation: Math.ceil(JSON.stringify(obj).length / 4).
201
255
  * Drop order: AMBIGUOUS -> INFERRED -> EXTRACTED.
256
+ *
257
+ * The estimate measures the response **as emitted** — `serializeForOutput()`
258
+ * over the same object `graphifyQuery` returns, pretty-printed and including
259
+ * the wrapper keys. Measuring a compact `{nodes, edges}` instead understates
260
+ * the payload the caller receives, which makes `budget_met` a confident claim
261
+ * about a payload nobody is handed (#2738).
262
+ *
263
+ * `term` participates in the emitted bytes, so it is threaded through; the
264
+ * default keeps direct unit calls on the same wire shape, minus those bytes.
202
265
  */
203
- function applyBudget(result, budgetTokens) {
204
- if (!budgetTokens)
266
+ function applyBudget(result, budgetTokens, term = '') {
267
+ // == null (not truthiness): --budget 0 is a valid parsed budget the router
268
+ // forwards, and treating it as "no budget" silently returns the unbounded
269
+ // result — the same silent-non-application defect class as #974/#2738.
270
+ //
271
+ // Number.isFinite additionally keeps NaN out of the comparisons below, where
272
+ // every `estimate <= NaN` is false: the loop would strip all three tiers and
273
+ // return a seeds-only payload indistinguishable from a legitimate aggressive
274
+ // trim. The CLI cannot reach that state — graphify-command-router rejects a
275
+ // non-numeric --budget before this is called — but graphifyQuery and
276
+ // applyBudget are module-level entry points a future caller could reach
277
+ // without that validation. Infinity routes here too, and deliberately: an
278
+ // unbounded budget is not a budget.
279
+ if (budgetTokens == null || !Number.isFinite(budgetTokens))
205
280
  return result;
206
281
  const CONFIDENCE_ORDER = ['AMBIGUOUS', 'INFERRED', 'EXTRACTED'];
207
282
  let edges = [...result.edges];
208
283
  let omitted = 0;
209
- const estimateTokens = (obj) => Math.ceil(JSON.stringify(obj).length / 4);
284
+ // Nodes that survive a given edge set: edge-reachable, plus seeds (always kept)
285
+ const survivingNodes = (edgeSet) => {
286
+ const reachableNodes = new Set();
287
+ for (const edge of edgeSet) {
288
+ reachableNodes.add(edge.source);
289
+ reachableNodes.add(edge.target);
290
+ }
291
+ return result.nodes.filter(n => reachableNodes.has(n.id) || (result.seeds && result.seeds.has(n.id)));
292
+ };
293
+ const trimmedLabel = (dropped, unreachable) => dropped > 0 ? `[${dropped} edges omitted, ${unreachable} nodes unreachable]` : null;
294
+ /**
295
+ * Tokens of the response as `output()` will emit it.
296
+ *
297
+ * Self-referential by construction: `budget_estimate` is itself one of the
298
+ * emitted fields, so its own digit width counts toward the total. Resolved by
299
+ * iterating to a fixed point — the sequence is non-decreasing (a wider number,
300
+ * and `false` over `true`, can only add characters), so it settles in a couple
301
+ * of passes. The cap is a guard rather than an expectation, and it exits on the
302
+ * larger value: over-reporting is the safe direction for a budget signal;
303
+ * under-reporting is the defect this fixes.
304
+ */
305
+ const wireEstimate = (candidateNodes, candidateEdges, trimmed) => {
306
+ let est = 0;
307
+ for (let i = 0; i < 8; i++) {
308
+ const next = estimateTokens(serializeForOutput(buildQueryResponse(term, {
309
+ nodes: candidateNodes,
310
+ edges: candidateEdges,
311
+ trimmed,
312
+ budget: { met: est <= budgetTokens, estimate: est },
313
+ })));
314
+ if (next === est)
315
+ break;
316
+ est = next;
317
+ }
318
+ return est;
319
+ };
320
+ // Estimate against the post-pruning node set after each tier removal, so a
321
+ // removal that already fits (once orphaned nodes are excluded) stops the loop
322
+ // instead of dropping the next, higher-confidence tier too (#2738).
323
+ let nodes = survivingNodes(edges);
324
+ let estimate = wireEstimate(nodes, edges, trimmedLabel(omitted, result.nodes.length - nodes.length));
210
325
  for (const tier of CONFIDENCE_ORDER) {
211
- if (estimateTokens({ nodes: result.nodes, edges }) <= budgetTokens)
326
+ if (estimate <= budgetTokens)
212
327
  break;
213
328
  const before = edges.length;
214
329
  // Check both confidence and confidence_score field names (Open Question 1)
215
330
  edges = edges.filter(e => (e.confidence || e.confidence_score) !== tier);
216
331
  omitted += before - edges.length;
332
+ nodes = survivingNodes(edges);
333
+ estimate = wireEstimate(nodes, edges, trimmedLabel(omitted, result.nodes.length - nodes.length));
217
334
  }
218
- // Find unreachable nodes after edge removal
219
- const reachableNodes = new Set();
220
- for (const edge of edges) {
221
- reachableNodes.add(edge.source);
222
- reachableNodes.add(edge.target);
223
- }
224
- // Always keep seed nodes
225
- const nodes = result.nodes.filter(n => reachableNodes.has(n.id) || (result.seeds && result.seeds.has(n.id)));
226
335
  const unreachable = result.nodes.length - nodes.length;
227
336
  return {
228
337
  nodes,
229
338
  edges,
230
- trimmed: omitted > 0 ? `[${omitted} edges omitted, ${unreachable} nodes unreachable]` : null,
339
+ trimmed: trimmedLabel(omitted, unreachable),
231
340
  total_nodes: nodes.length,
232
341
  total_edges: edges.length,
342
+ // Seeds are retained unconditionally, so the seed set is a floor the
343
+ // reduction cannot go below — report the outcome instead of hiding a miss (#2738)
344
+ budget_met: estimate <= budgetTokens,
345
+ budget_estimate: estimate,
233
346
  };
234
347
  }
235
348
  // ─── Public API ──────────────────────────────────────────────────────────────
@@ -303,17 +416,19 @@ function graphifyQuery(cwd, term, options = {}) {
303
416
  return { error: 'Failed to parse graph.json' };
304
417
  }
305
418
  let result = seedAndExpand(graph, term);
306
- if (options.budget) {
307
- result = applyBudget(result, options.budget);
419
+ if (options.budget != null) {
420
+ result = applyBudget(result, options.budget, term);
308
421
  }
309
- return {
310
- term,
422
+ // Same builder the estimator measured, so budget_estimate describes exactly
423
+ // these bytes (#2738).
424
+ return buildQueryResponse(term, {
311
425
  nodes: result.nodes,
312
426
  edges: result.edges,
313
- total_nodes: result.nodes.length,
314
- total_edges: result.edges.length,
315
427
  trimmed: 'trimmed' in result ? (result.trimmed || null) : null,
316
- };
428
+ budget: 'budget_met' in result
429
+ ? { met: result.budget_met, estimate: result.budget_estimate }
430
+ : undefined,
431
+ });
317
432
  }
318
433
  /**
319
434
  * Return status information about the knowledge graph (STAT-01, STAT-02).
@@ -27,9 +27,16 @@ const node_path_1 = __importDefault(require("node:path"));
27
27
  const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs");
28
28
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
29
29
  const clock_cjs_1 = require("./clock.cjs");
30
+ const phase_lifecycle_cjs_1 = require("./phase-lifecycle.cjs");
31
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- core-utils.cjs is an export= CommonJS module
32
+ const coreUtilsMod = require("./core-utils.cjs");
30
33
  // eslint-disable-next-line @typescript-eslint/no-require-imports
31
34
  const ioMod = require("./io.cjs");
35
+ // eslint-disable-next-line @typescript-eslint/no-require-imports -- frontmatter.cjs is an export= CommonJS module
36
+ const frontmatterMod = require("./frontmatter.cjs");
32
37
  const { output } = ioMod;
38
+ const { transliterateForSlug } = coreUtilsMod;
39
+ const { stripFrontmatter } = frontmatterMod;
33
40
  // ─── Utilities ──────────────────────────────────────────────────────────────
34
41
  function readOptional(filePath) {
35
42
  try {
@@ -43,7 +50,11 @@ function zeroPad(n, width = 2) {
43
50
  return String(n).padStart(width, '0');
44
51
  }
45
52
  function slugify(title) {
46
- return title.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
53
+ // #2848: transliterate Cyrillic to ASCII before the filter so a non-Latin
54
+ // title does not collapse to an empty slug. The shared primitive keeps this
55
+ // in sync with generateSlugInternal. slugify's DISTINCT contract is preserved:
56
+ // single leading/trailing hyphen strip (/^-|-$/), and NO 60-char truncation.
57
+ return transliterateForSlug(title).replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
47
58
  }
48
59
  // ─── GSD-2 Parser ───────────────────────────────────────────────────────────
49
60
  /**
@@ -116,6 +127,12 @@ function parseTaskMustHaves(content) {
116
127
  /**
117
128
  * Read all task plan files from a GSD-2 tasks/ directory.
118
129
  */
130
+ // #3183 (ADR-3180 Decision 4(a) — bucket B, out of scope for the
131
+ // scanPhasePlans migration): this reads a FOREIGN GSD-2 legacy project's own
132
+ // `tasks/` directory convention (`T##-PLAN.md`) during a one-time import —
133
+ // it is not this project's `.planning/phases/<phase>/` layout at all, has no
134
+ // nested-plans/superseded-status concept, and scanPhasePlans's grammar
135
+ // (which is scoped to GSD's OWN phase directories) does not apply here.
119
136
  function readTasksDir(tasksDir) {
120
137
  if (!node_fs_1.default.existsSync(tasksDir))
121
138
  return [];
@@ -218,9 +235,22 @@ function buildPlanMd(task, phasePrefix, planPrefix, phaseSlug, milestoneTitle) {
218
235
  */
219
236
  function buildSummaryMd(task, phasePrefix, planPrefix) {
220
237
  const raw = task.summary || '';
221
- // Strip GSD-2 frontmatter block (--- ... ---) if present
222
- const bodyMatch = raw.match(/^---[\s\S]*?---\n+([\s\S]*)$/);
223
- const body = bodyMatch ? bodyMatch[1].trim() : raw.trim();
238
+ // Strip the GSD-2 frontmatter block via the canonical primitive (#2703). The
239
+ // previous local regex required a bare `\n` after the closing `---`, so a
240
+ // CRLF-authored summary never matched, fell through to the untouched-raw
241
+ // branch, and had its frontmatter emitted a second time inside the body of
242
+ // the document this function then wrapped in a fresh v1 block.
243
+ //
244
+ // `extractFrontmatter` — which the issue names — returns only the parsed
245
+ // object and never the body, so it cannot serve this call site;
246
+ // `stripFrontmatter` is the same module's canonical body primitive.
247
+ //
248
+ // `once` is load-bearing. A GSD-2 summary is an arbitrary user-authored
249
+ // document, not a GSD artefact with a known frontmatter-doubling failure
250
+ // mode, so a body opening with a thematic-break-delimited section
251
+ // (`---` / heading / `---`) is far likelier than a corrupt second header —
252
+ // and the default greedy loop would delete it without a trace.
253
+ const body = stripFrontmatter(raw, { once: true }).trim();
224
254
  return [
225
255
  '---',
226
256
  `phase: "${phasePrefix}"`,
@@ -271,7 +301,9 @@ function buildStateMd(phaseMap) {
271
301
  const currentEntry = phaseMap.find(p => !p.slice.done);
272
302
  const totalPhases = phaseMap.length;
273
303
  const donePhases = phaseMap.filter(p => p.slice.done).length;
274
- const pct = totalPhases > 0 ? Math.round((donePhases / totalPhases) * 100) : 0;
304
+ // ADR-3180 D7: one owner for completion percent. clampPercent's 100 ceiling is
305
+ // unreachable here (donePhases is a subset of totalPhases) — the value is unchanged.
306
+ const pct = (0, phase_lifecycle_cjs_1.clampPercent)(donePhases, totalPhases);
275
307
  const currentPhaseNum = currentEntry ? zeroPad(currentEntry.phaseNum) : zeroPad(totalPhases);
276
308
  const currentSlug = currentEntry ? slugify(currentEntry.slice.title) : 'complete';
277
309
  const status = currentEntry ? 'Ready to plan' : 'All phases complete';