@opengsd/gsd-core 1.17.0 → 1.19.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 (369) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +2 -1
  3. package/.opencode/plugins/gsd-core.js +2 -3
  4. package/agents/gsd-advisor-researcher.compact.md +1 -1
  5. package/agents/gsd-advisor-researcher.md +1 -1
  6. package/agents/gsd-assumptions-analyzer.compact.md +1 -1
  7. package/agents/gsd-assumptions-analyzer.md +1 -1
  8. package/agents/gsd-code-fixer.compact.md +3 -3
  9. package/agents/gsd-code-fixer.md +5 -4
  10. package/agents/gsd-code-reviewer.compact.md +1 -1
  11. package/agents/gsd-code-reviewer.md +1 -1
  12. package/agents/gsd-codebase-mapper.compact.md +1 -1
  13. package/agents/gsd-codebase-mapper.md +1 -1
  14. package/agents/gsd-debugger.md +1 -1
  15. package/agents/gsd-doc-verifier.compact.md +4 -4
  16. package/agents/gsd-doc-verifier.md +4 -3
  17. package/agents/gsd-doc-writer.compact.md +23 -5
  18. package/agents/gsd-doc-writer.md +19 -6
  19. package/agents/gsd-eval-auditor.compact.md +1 -1
  20. package/agents/gsd-eval-auditor.md +1 -1
  21. package/agents/gsd-executor.md +7 -3
  22. package/agents/gsd-integration-checker.compact.md +1 -1
  23. package/agents/gsd-integration-checker.md +1 -1
  24. package/agents/gsd-nyquist-auditor.compact.md +1 -1
  25. package/agents/gsd-nyquist-auditor.md +1 -1
  26. package/agents/gsd-phase-researcher.md +1 -1
  27. package/agents/gsd-plan-checker.md +2 -1
  28. package/agents/gsd-planner.md +5 -4
  29. package/agents/gsd-project-researcher.compact.md +1 -1
  30. package/agents/gsd-project-researcher.md +1 -1
  31. package/agents/gsd-research-synthesizer.compact.md +5 -5
  32. package/agents/gsd-research-synthesizer.md +5 -5
  33. package/agents/gsd-roadmapper.compact.md +1 -1
  34. package/agents/gsd-roadmapper.md +1 -1
  35. package/agents/gsd-security-auditor.compact.md +1 -1
  36. package/agents/gsd-security-auditor.md +1 -1
  37. package/agents/gsd-ui-auditor.compact.md +1 -1
  38. package/agents/gsd-ui-auditor.md +1 -1
  39. package/agents/gsd-ui-checker.compact.md +1 -1
  40. package/agents/gsd-ui-checker.md +1 -1
  41. package/agents/gsd-ui-researcher.compact.md +13 -1
  42. package/agents/gsd-ui-researcher.md +28 -1
  43. package/agents/gsd-verifier.md +2 -2
  44. package/bin/install.js +460 -105
  45. package/commands/gsd/code-review.md +8 -0
  46. package/commands/gsd/pause-work.md +1 -1
  47. package/gsd-core/bin/gsd-tools.cjs +124 -18
  48. package/gsd-core/bin/lib/agent-install-check.cjs +29 -3
  49. package/gsd-core/bin/lib/args-terminator.cjs +114 -0
  50. package/gsd-core/bin/lib/broken-windows.cjs +59 -1
  51. package/gsd-core/bin/lib/capability-ledger.cjs +25 -0
  52. package/gsd-core/bin/lib/capability-lock.cjs +41 -8
  53. package/gsd-core/bin/lib/capability-registry.cjs +162 -74
  54. package/gsd-core/bin/lib/capability-source.cjs +3 -1
  55. package/gsd-core/bin/lib/capability-state.cjs +20 -11
  56. package/gsd-core/bin/lib/capability-validator.cjs +23 -4
  57. package/gsd-core/bin/lib/commands.cjs +5 -1
  58. package/gsd-core/bin/lib/config-loader.cjs +13 -3
  59. package/gsd-core/bin/lib/config.cjs +256 -58
  60. package/gsd-core/bin/lib/core-utils.cjs +34 -0
  61. package/gsd-core/bin/lib/decision-coverage-support.cjs +93 -0
  62. package/gsd-core/bin/lib/gate-context-drift.cjs +8 -3
  63. package/gsd-core/bin/lib/gate-decision-coverage-plan.cjs +21 -1
  64. package/gsd-core/bin/lib/gate-decision-coverage-verify.cjs +19 -1
  65. package/gsd-core/bin/lib/gate-evaluation-scope.cjs +134 -13
  66. package/gsd-core/bin/lib/gate-predicate-evaluator.cjs +23 -3
  67. package/gsd-core/bin/lib/gate-predicate.cjs +8 -12
  68. package/gsd-core/bin/lib/gate-tdd-review-checkpoint.cjs +114 -18
  69. package/gsd-core/bin/lib/gate-ui-safety.cjs +23 -5
  70. package/gsd-core/bin/lib/git-info-exclude.cjs +57 -0
  71. package/gsd-core/bin/lib/health-diagnostic-rules/phase-structure.cjs +18 -3
  72. package/gsd-core/bin/lib/health-diagnostic-types.cjs +4 -0
  73. package/gsd-core/bin/lib/health-diagnostic.cjs +52 -1
  74. package/gsd-core/bin/lib/host-integration.cjs +13 -6
  75. package/gsd-core/bin/lib/init.cjs +76 -5
  76. package/gsd-core/bin/lib/install-effort-resolver.cjs +2 -2
  77. package/gsd-core/bin/lib/install-engine.cjs +246 -32
  78. package/gsd-core/bin/lib/install-model-override-resolver.cjs +4 -3
  79. package/gsd-core/bin/lib/install-profiles.cjs +14 -7
  80. package/gsd-core/bin/lib/installer-migration-report.cjs +2 -0
  81. package/gsd-core/bin/lib/markdown-line-diff.cjs +147 -0
  82. package/gsd-core/bin/lib/markdown-sectionizer.cjs +113 -0
  83. package/gsd-core/bin/lib/milestone-lock.cjs +8 -1
  84. package/gsd-core/bin/lib/milestone-release-map.cjs +244 -0
  85. package/gsd-core/bin/lib/milestone-release-tags.cjs +440 -0
  86. package/gsd-core/bin/lib/milestone.cjs +43 -1
  87. package/gsd-core/bin/lib/model-catalog.cjs +8 -0
  88. package/gsd-core/bin/lib/normalize-test-command.cjs +67 -0
  89. package/gsd-core/bin/lib/phase-id.cjs +21 -4
  90. package/gsd-core/bin/lib/phase-lifecycle.cjs +37 -4
  91. package/gsd-core/bin/lib/phase.cjs +235 -37
  92. package/gsd-core/bin/lib/planning-snapshot.cjs +3 -1
  93. package/gsd-core/bin/lib/planning-workspace.cjs +18 -0
  94. package/gsd-core/bin/lib/pristine-baseline.cjs +11 -3
  95. package/gsd-core/bin/lib/read-only-git-env.cjs +58 -0
  96. package/gsd-core/bin/lib/report-parser.cjs +57 -1
  97. package/gsd-core/bin/lib/review-lane-descriptor.cjs +35 -2
  98. package/gsd-core/bin/lib/review-lane-invocation.cjs +40 -0
  99. package/gsd-core/bin/lib/review-lane-runner.cjs +33 -4
  100. package/gsd-core/bin/lib/review-reviewer-selection.cjs +20 -1
  101. package/gsd-core/bin/lib/roadmap-parser.cjs +12 -0
  102. package/gsd-core/bin/lib/roadmap-upgrade.cjs +39 -2
  103. package/gsd-core/bin/lib/roadmap.cjs +89 -30
  104. package/gsd-core/bin/lib/runtime-artifact-conversion.cjs +428 -16
  105. package/gsd-core/bin/lib/runtime-artifact-layout.cjs +8 -4
  106. package/gsd-core/bin/lib/runtime-homes.cjs +18 -0
  107. package/gsd-core/bin/lib/runtime-hooks-surface.cjs +60 -16
  108. package/gsd-core/bin/lib/shell-command-projection.cjs +123 -16
  109. package/gsd-core/bin/lib/smart-entry.cjs +37 -16
  110. package/gsd-core/bin/lib/state-contract.cjs +29 -9
  111. package/gsd-core/bin/lib/state-document.cjs +47 -5
  112. package/gsd-core/bin/lib/state-transition.cjs +150 -34
  113. package/gsd-core/bin/lib/state.cjs +140 -88
  114. package/gsd-core/bin/lib/surface.cjs +9 -0
  115. package/gsd-core/bin/lib/task-command-router.cjs +7 -28
  116. package/gsd-core/bin/lib/task-content-resolution.cjs +46 -1
  117. package/gsd-core/bin/lib/tdd-red-evidence.cjs +5 -0
  118. package/gsd-core/bin/lib/tdd-test-files.cjs +42 -0
  119. package/gsd-core/bin/lib/verification.cjs +96 -9
  120. package/gsd-core/bin/lib/verify.cjs +38 -1
  121. package/gsd-core/bin/lib/worktree-safety.cjs +22 -1
  122. package/gsd-core/bin/shared/config-defaults.manifest.json +1 -0
  123. package/gsd-core/bin/shared/config-schema.manifest.json +1 -0
  124. package/gsd-core/references/agent-contracts.md +1 -1
  125. package/gsd-core/references/agent-skills-bootstrap.md +26 -7
  126. package/gsd-core/references/git-integration.md +14 -3
  127. package/gsd-core/references/loop-hook-dispatch.md +13 -6
  128. package/gsd-core/references/planner-reversibility.md +18 -4
  129. package/gsd-core/references/planner-verify-command-grounding.md +18 -0
  130. package/gsd-core/references/planning-config.md +2 -0
  131. package/gsd-core/references/tdd.md +5 -4
  132. package/gsd-core/references/verifier-phase-gates.md +8 -1
  133. package/gsd-core/templates/DEBUG.md +16 -0
  134. package/gsd-core/templates/config.json +2 -1
  135. package/gsd-core/templates/verification-report.md +1 -1
  136. package/gsd-core/workflows/add-backlog.md +10 -2
  137. package/gsd-core/workflows/add-phase.md +5 -1
  138. package/gsd-core/workflows/add-tests.md +5 -1
  139. package/gsd-core/workflows/add-todo.md +5 -1
  140. package/gsd-core/workflows/ai-integration-phase.md +5 -1
  141. package/gsd-core/workflows/audit-fix.md +2 -1
  142. package/gsd-core/workflows/audit-milestone.md +21 -9
  143. package/gsd-core/workflows/autonomous/templates/minimal-context.md +41 -0
  144. package/gsd-core/workflows/autonomous.md +52 -64
  145. package/gsd-core/workflows/check-todos.md +5 -1
  146. package/gsd-core/workflows/code-review/steps/structural-pre-pass.md +10 -1
  147. package/gsd-core/workflows/code-review-fix.md +5 -1
  148. package/gsd-core/workflows/code-review.md +66 -8
  149. package/gsd-core/workflows/complete-milestone/detail/elaboration.md +5 -1
  150. package/gsd-core/workflows/complete-milestone/steps/project-md-evolution-review.md +145 -0
  151. package/gsd-core/workflows/complete-milestone/steps/release-map.md +80 -0
  152. package/gsd-core/workflows/complete-milestone.md +64 -157
  153. package/gsd-core/workflows/debug.md +5 -1
  154. package/gsd-core/workflows/diagnose-issues.md +34 -6
  155. package/gsd-core/workflows/discuss-phase/modes/chain.md +5 -1
  156. package/gsd-core/workflows/discuss-phase/modes/power.md +1 -1
  157. package/gsd-core/workflows/discuss-phase-assumptions.md +44 -5
  158. package/gsd-core/workflows/discuss-phase-power.md +19 -2
  159. package/gsd-core/workflows/discuss-phase.md +11 -11
  160. package/gsd-core/workflows/docs-update/detail/elaboration.md +7 -6
  161. package/gsd-core/workflows/docs-update.md +27 -5
  162. package/gsd-core/workflows/edit-phase.md +5 -1
  163. package/gsd-core/workflows/eval-review.md +11 -3
  164. package/gsd-core/workflows/execute-phase/steps/codebase-drift-gate.md +5 -1
  165. package/gsd-core/workflows/execute-phase/steps/per-plan-worktree-gate.md +13 -7
  166. package/gsd-core/workflows/execute-phase/steps/post-merge-gate.md +19 -8
  167. package/gsd-core/workflows/execute-phase/steps/regression-gate-run.md +21 -3
  168. package/gsd-core/workflows/execute-phase/steps/regression-gate.md +18 -1
  169. package/gsd-core/workflows/execute-phase/steps/verify-phase-goal.md +7 -3
  170. package/gsd-core/workflows/execute-phase/steps/wave-post-gate-hooks.md +11 -2
  171. package/gsd-core/workflows/execute-phase.md +46 -11
  172. package/gsd-core/workflows/execute-plan.md +8 -4
  173. package/gsd-core/workflows/extract-learnings.md +6 -2
  174. package/gsd-core/workflows/forensics.md +5 -1
  175. package/gsd-core/workflows/health.md +2 -1
  176. package/gsd-core/workflows/import.md +6 -2
  177. package/gsd-core/workflows/insert-phase.md +5 -1
  178. package/gsd-core/workflows/manager.md +10 -2
  179. package/gsd-core/workflows/map-codebase.md +5 -1
  180. package/gsd-core/workflows/milestone-summary.md +10 -2
  181. package/gsd-core/workflows/new-milestone.md +13 -5
  182. package/gsd-core/workflows/new-project.md +9 -5
  183. package/gsd-core/workflows/next.md +8 -4
  184. package/gsd-core/workflows/pause-work.md +3 -3
  185. package/gsd-core/workflows/plan-phase/steps/auto-advance-check.md +70 -0
  186. package/gsd-core/workflows/plan-phase/steps/chunked-planning-mode.md +7 -3
  187. package/gsd-core/workflows/plan-phase/steps/closed-phase-gate.md +6 -2
  188. package/gsd-core/workflows/plan-phase.md +54 -86
  189. package/gsd-core/workflows/plan-review-convergence.md +17 -5
  190. package/gsd-core/workflows/plant-seed.md +8 -4
  191. package/gsd-core/workflows/progress.md +6 -2
  192. package/gsd-core/workflows/quick/steps/plan-checker-loop.md +1 -1
  193. package/gsd-core/workflows/quick/steps/worktree-pre-dispatch-commit.md +8 -1
  194. package/gsd-core/workflows/quick-batch/steps/plan-checker-loop.md +2 -1
  195. package/gsd-core/workflows/quick-batch.md +5 -1
  196. package/gsd-core/workflows/quick.md +23 -8
  197. package/gsd-core/workflows/remove-phase.md +5 -1
  198. package/gsd-core/workflows/resume-project.md +5 -1
  199. package/gsd-core/workflows/review.md +22 -15
  200. package/gsd-core/workflows/scan.md +5 -1
  201. package/gsd-core/workflows/section-manifest.json +5 -0
  202. package/gsd-core/workflows/secure-phase.md +6 -2
  203. package/gsd-core/workflows/settings.md +4 -3
  204. package/gsd-core/workflows/ship.md +16 -14
  205. package/gsd-core/workflows/spec-phase.md +3 -6
  206. package/gsd-core/workflows/thread.md +10 -2
  207. package/gsd-core/workflows/transition.md +5 -1
  208. package/gsd-core/workflows/ui-phase.md +5 -1
  209. package/gsd-core/workflows/ui-review.md +6 -2
  210. package/gsd-core/workflows/ultraplan-phase.md +5 -1
  211. package/gsd-core/workflows/update.md +5 -1
  212. package/gsd-core/workflows/validate-phase.md +6 -2
  213. package/gsd-core/workflows/verify-work/detail/elaboration.md +1 -1
  214. package/gsd-core/workflows/verify-work/steps/mvp-uat-framing.md +5 -1
  215. package/gsd-core/workflows/verify-work.md +15 -6
  216. package/hooks/dist/gsd-agent-isolation-guard.js +14 -2
  217. package/hooks/dist/gsd-cursor-subagent-start.js +8 -3
  218. package/hooks/dist/gsd-path-export.js +95 -0
  219. package/hooks/dist/gsd-secret-read-guard.js +244 -53
  220. package/hooks/dist/gsd-statusline.js +35 -2
  221. package/hooks/dist/gsd-validate-commit.sh +27 -5
  222. package/hooks/dist/gsd-windsurf-pre-write.js +28 -1
  223. package/hooks/dist/gsd-workflow-guard.js +1 -1
  224. package/hooks/dist/gsd-worktree-path-guard.js +28 -1
  225. package/hooks/dist/lib/isolation-sentinel.js +44 -0
  226. package/hooks/dist/managed-hooks-registry.cjs +1 -0
  227. package/hooks/gsd-agent-isolation-guard.js +14 -2
  228. package/hooks/gsd-cursor-subagent-start.js +8 -3
  229. package/hooks/gsd-path-export.js +95 -0
  230. package/hooks/gsd-secret-read-guard.js +244 -53
  231. package/hooks/gsd-statusline.js +35 -2
  232. package/hooks/gsd-validate-commit.sh +27 -5
  233. package/hooks/gsd-windsurf-pre-write.js +28 -1
  234. package/hooks/gsd-workflow-guard.js +1 -1
  235. package/hooks/gsd-worktree-path-guard.js +28 -1
  236. package/hooks/hooks.json +1 -0
  237. package/hooks/lib/isolation-sentinel.js +44 -0
  238. package/hooks/managed-hooks-registry.cjs +1 -0
  239. package/package.json +13 -6
  240. package/pi/gsd.cjs +1047 -10
  241. package/pi/package-agents/gsd-advisor-researcher.md +113 -0
  242. package/pi/package-agents/gsd-ai-researcher.md +111 -0
  243. package/pi/package-agents/gsd-assumptions-analyzer.md +110 -0
  244. package/pi/package-agents/gsd-code-fixer.md +769 -0
  245. package/pi/package-agents/gsd-code-reviewer.md +402 -0
  246. package/pi/package-agents/gsd-codebase-mapper.md +850 -0
  247. package/pi/package-agents/gsd-debug-session-manager.md +416 -0
  248. package/pi/package-agents/gsd-debugger.md +1274 -0
  249. package/pi/package-agents/gsd-doc-classifier.md +269 -0
  250. package/pi/package-agents/gsd-doc-synthesizer.md +259 -0
  251. package/pi/package-agents/gsd-doc-verifier.md +212 -0
  252. package/pi/package-agents/gsd-doc-writer.md +626 -0
  253. package/pi/package-agents/gsd-dom-verifier.md +164 -0
  254. package/pi/package-agents/gsd-domain-researcher.md +144 -0
  255. package/pi/package-agents/gsd-eval-auditor.md +185 -0
  256. package/pi/package-agents/gsd-eval-planner.md +149 -0
  257. package/pi/package-agents/gsd-executor.md +894 -0
  258. package/pi/package-agents/gsd-framework-selector.md +159 -0
  259. package/pi/package-agents/gsd-integration-checker.md +476 -0
  260. package/pi/package-agents/gsd-intel-updater.md +338 -0
  261. package/pi/package-agents/gsd-mempalace-curator.md +49 -0
  262. package/pi/package-agents/gsd-nyquist-auditor.md +200 -0
  263. package/pi/package-agents/gsd-pattern-mapper.md +340 -0
  264. package/pi/package-agents/gsd-phase-researcher.md +897 -0
  265. package/pi/package-agents/gsd-plan-checker.md +1077 -0
  266. package/pi/package-agents/gsd-planner.md +1003 -0
  267. package/pi/package-agents/gsd-project-researcher.md +611 -0
  268. package/pi/package-agents/gsd-research-synthesizer.md +259 -0
  269. package/pi/package-agents/gsd-roadmapper.md +779 -0
  270. package/pi/package-agents/gsd-security-auditor.md +171 -0
  271. package/pi/package-agents/gsd-ui-auditor.md +581 -0
  272. package/pi/package-agents/gsd-ui-checker.md +417 -0
  273. package/pi/package-agents/gsd-ui-researcher.md +469 -0
  274. package/pi/package-agents/gsd-user-profiler.md +174 -0
  275. package/pi/package-agents/gsd-verifier.md +963 -0
  276. package/pi/package-agents/manifest.json +39 -0
  277. package/pi/package-skills/gsd-add-tests/SKILL.md +39 -0
  278. package/pi/package-skills/gsd-ai-integration-phase/SKILL.md +36 -0
  279. package/pi/package-skills/gsd-audit-fix/SKILL.md +34 -0
  280. package/pi/package-skills/gsd-audit-milestone/SKILL.md +40 -0
  281. package/pi/package-skills/gsd-audit-uat/SKILL.md +26 -0
  282. package/pi/package-skills/gsd-autonomous/SKILL.md +52 -0
  283. package/pi/package-skills/gsd-capture/SKILL.md +68 -0
  284. package/pi/package-skills/gsd-cleanup/SKILL.md +25 -0
  285. package/pi/package-skills/gsd-code-review/SKILL.md +71 -0
  286. package/pi/package-skills/gsd-complete-milestone/SKILL.md +147 -0
  287. package/pi/package-skills/gsd-config/SKILL.md +60 -0
  288. package/pi/package-skills/gsd-debug/SKILL.md +64 -0
  289. package/pi/package-skills/gsd-discuss-phase/SKILL.md +74 -0
  290. package/pi/package-skills/gsd-docs-update/SKILL.md +50 -0
  291. package/pi/package-skills/gsd-eval-review/SKILL.md +35 -0
  292. package/pi/package-skills/gsd-execute-phase/SKILL.md +62 -0
  293. package/pi/package-skills/gsd-explore/SKILL.md +26 -0
  294. package/pi/package-skills/gsd-extract-learnings/SKILL.md +25 -0
  295. package/pi/package-skills/gsd-fast/SKILL.md +33 -0
  296. package/pi/package-skills/gsd-forensics/SKILL.md +59 -0
  297. package/pi/package-skills/gsd-graphify/SKILL.md +210 -0
  298. package/pi/package-skills/gsd-health/SKILL.md +36 -0
  299. package/pi/package-skills/gsd-help/SKILL.md +37 -0
  300. package/pi/package-skills/gsd-import/SKILL.md +46 -0
  301. package/pi/package-skills/gsd-inbox/SKILL.md +42 -0
  302. package/pi/package-skills/gsd-ingest-docs/SKILL.md +43 -0
  303. package/pi/package-skills/gsd-manager/SKILL.md +46 -0
  304. package/pi/package-skills/gsd-map-codebase/SKILL.md +85 -0
  305. package/pi/package-skills/gsd-mempalace-capture/SKILL.md +111 -0
  306. package/pi/package-skills/gsd-mempalace-recall/SKILL.md +111 -0
  307. package/pi/package-skills/gsd-milestone-summary/SKILL.md +54 -0
  308. package/pi/package-skills/gsd-mvp-phase/SKILL.md +45 -0
  309. package/pi/package-skills/gsd-new-milestone/SKILL.md +49 -0
  310. package/pi/package-skills/gsd-new-project/SKILL.md +49 -0
  311. package/pi/package-skills/gsd-next/SKILL.md +34 -0
  312. package/pi/package-skills/gsd-ns-context/SKILL.md +31 -0
  313. package/pi/package-skills/gsd-ns-ideate/SKILL.md +30 -0
  314. package/pi/package-skills/gsd-ns-manage/SKILL.md +42 -0
  315. package/pi/package-skills/gsd-ns-project/SKILL.md +34 -0
  316. package/pi/package-skills/gsd-ns-review/SKILL.md +35 -0
  317. package/pi/package-skills/gsd-ns-workflow/SKILL.md +44 -0
  318. package/pi/package-skills/gsd-onboard/SKILL.md +46 -0
  319. package/pi/package-skills/gsd-pause-work/SKILL.md +48 -0
  320. package/pi/package-skills/gsd-phase/SKILL.md +61 -0
  321. package/pi/package-skills/gsd-plan-phase/SKILL.md +62 -0
  322. package/pi/package-skills/gsd-plan-review-convergence/SKILL.md +63 -0
  323. package/pi/package-skills/gsd-pr-branch/SKILL.md +31 -0
  324. package/pi/package-skills/gsd-profile-user/SKILL.md +48 -0
  325. package/pi/package-skills/gsd-progress/SKILL.md +51 -0
  326. package/pi/package-skills/gsd-quick/SKILL.md +179 -0
  327. package/pi/package-skills/gsd-quick-batch/SKILL.md +114 -0
  328. package/pi/package-skills/gsd-resume-work/SKILL.md +31 -0
  329. package/pi/package-skills/gsd-review/SKILL.md +50 -0
  330. package/pi/package-skills/gsd-review-backlog/SKILL.md +65 -0
  331. package/pi/package-skills/gsd-secure-phase/SKILL.md +37 -0
  332. package/pi/package-skills/gsd-settings/SKILL.md +30 -0
  333. package/pi/package-skills/gsd-ship/SKILL.md +27 -0
  334. package/pi/package-skills/gsd-sketch/SKILL.md +56 -0
  335. package/pi/package-skills/gsd-spec-phase/SKILL.md +63 -0
  336. package/pi/package-skills/gsd-spike/SKILL.md +53 -0
  337. package/pi/package-skills/gsd-stats/SKILL.md +22 -0
  338. package/pi/package-skills/gsd-surface/SKILL.md +177 -0
  339. package/pi/package-skills/gsd-thread/SKILL.md +29 -0
  340. package/pi/package-skills/gsd-ui-phase/SKILL.md +35 -0
  341. package/pi/package-skills/gsd-ui-review/SKILL.md +35 -0
  342. package/pi/package-skills/gsd-ultraplan-phase/SKILL.md +38 -0
  343. package/pi/package-skills/gsd-undo/SKILL.md +38 -0
  344. package/pi/package-skills/gsd-update/SKILL.md +51 -0
  345. package/pi/package-skills/gsd-validate-phase/SKILL.md +37 -0
  346. package/pi/package-skills/gsd-verify-work/SKILL.md +41 -0
  347. package/pi/package-skills/gsd-workspace/SKILL.md +57 -0
  348. package/pi/package-skills/gsd-workstreams/SKILL.md +73 -0
  349. package/scripts/audit-workflow-script-paths.cjs +1 -1
  350. package/scripts/build-hooks.js +4 -0
  351. package/scripts/command-contract-helpers.cjs +1 -1
  352. package/scripts/diagnose-windows-spawn-watcher.cjs +317 -0
  353. package/scripts/docs-guard-registry.cjs +22 -7
  354. package/scripts/gen-health-docs.cjs +13 -1
  355. package/scripts/gen-pi-package-skills.cjs +217 -0
  356. package/scripts/lib/macos-conformance-tier.generated.cjs +19 -0
  357. package/scripts/lib/platform-conformance-tier.generated.cjs +41 -0
  358. package/scripts/lint-docs-guard-registration.exempt-baseline.cjs +3 -3
  359. package/scripts/lint-phase-arg-assignment.cjs +2 -2
  360. package/scripts/lint-retired-runtime-name.cjs +5 -0
  361. package/scripts/lint-workflow-shellcheck-baseline.json +16 -16
  362. package/scripts/npm-audit-baseline.cjs +41 -7
  363. package/scripts/release-tarball-smoke.cjs +1 -0
  364. package/scripts/reviewed-or-maintainer-policy.cjs +181 -0
  365. package/scripts/select-docs-guards.cjs +1 -1
  366. package/scripts/self-test-report-policy.cjs +350 -0
  367. package/skills/gsd-code-review/SKILL.md +8 -0
  368. package/skills/gsd-pause-work/SKILL.md +1 -1
  369. package/vscode/package.json +1 -1
@@ -145,10 +145,28 @@ function evaluateDecisionCoveragePlan(input) {
145
145
  if (decisions.length === 0) {
146
146
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { passed: true, skipped: true, reason: 'no trackable decisions', total: 0, covered: 0, uncovered: [], message: 'No trackable decisions in CONTEXT.md.' });
147
147
  }
148
+ // #4939: a phase-dir argument that was GIVEN but is not a directory is a caller
149
+ // error like #2770's empty context argument and #4794's non-file context path:
150
+ // no plan was scanned, so nothing was measured. Fail closed with total/covered
151
+ // null and `uncovered` omitted. An OMITTED phase dir is left to the scan below —
152
+ // the #4130 `--context <path>` alone tests pin that as a coverage gap.
153
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
154
+ if (phaseDirIssue) {
155
+ const phaseDirResult = { passed: false, skipped: false, reason: phaseDirIssue.reason, total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" ${phaseDirIssue.what}, so no plans were scanned. ${phaseDirIssue.hint}` };
156
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(true, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('block', true, phaseDirResult);
157
+ }
148
158
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
149
159
  if (planContents.kind === 'unreadable')
150
160
  return unreadableDecisionGate(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
151
- const sections = planContents.value.map(decision_coverage_support_cjs_1.extractPlanDesignatedSections);
161
+ // #4939: a phase directory that holds no PLAN file is the same caller error: zero plans
162
+ // loaded is not zero coverage.
163
+ if (phaseDir && planContents.value.length === 0) {
164
+ return (0, gate_verdict_cjs_1.gateVerdict)('block', true, { passed: false, skipped: false, reason: 'phase directory holds no plans', total: null, covered: null, message: `Decision coverage gate: the phase directory "${positionals[0]}" holds no PLAN files, so no plans were scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
165
+ }
166
+ const tracker = (0, decision_coverage_support_cjs_1.resolveTrackerBodies)(projectDir, planContents.value, input.deps);
167
+ if (tracker.kind === 'unreadable')
168
+ return unreadableDecisionGate(`${tracker.span}: ${tracker.reason}`);
169
+ const sections = planContents.value.map((c, i) => (0, decision_coverage_support_cjs_1.extractPlanDesignatedSections)(c) + (tracker.value.bodies[i] ? '\n' + tracker.value.bodies[i] : ''));
152
170
  const uncovered = [];
153
171
  let covered = 0;
154
172
  for (const decision of decisions) {
@@ -157,6 +175,8 @@ function evaluateDecisionCoveragePlan(input) {
157
175
  else
158
176
  uncovered.push({ id: decision.id, text: decision.text, category: decision.category });
159
177
  }
178
+ if (uncovered.length > 0 && tracker.value.missingResolver)
179
+ return unreadableDecisionGate(`${tracker.value.missingResolver.span}: ${tracker.value.missingResolver.reason}`);
160
180
  const passed = uncovered.length === 0;
161
181
  return (0, gate_verdict_cjs_1.gateVerdict)(passed ? 'pass' : 'block', !passed, {
162
182
  passed,
@@ -90,19 +90,35 @@ function evaluateDecisionCoverageVerify(input) {
90
90
  if (decisions.length === 0) {
91
91
  return (0, gate_verdict_cjs_1.gateVerdict)('skip', false, { skipped: true, blocking: false, reason: 'no trackable decisions', total: 0, honored: 0, not_honored: [], message: 'No trackable decisions in CONTEXT.md.' });
92
92
  }
93
+ // #4939: mirror the plan gate. A phase-dir argument that names no directory
94
+ // means no plan or summary was scanned, so no decision can be reported
95
+ // not-honored. Warn (verify stays non-blocking) rather than list every decision.
96
+ const phaseDirIssue = phaseDir ? (0, decision_coverage_support_cjs_1.phaseDirProblem)(phaseDir) : null;
97
+ if (phaseDirIssue) {
98
+ const phaseDirResult = { skipped: false, blocking: false, reason: phaseDirIssue.reason, total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" ${phaseDirIssue.what}, so no plans or summaries were scanned. ${phaseDirIssue.hint}` };
99
+ return phaseDirIssue.unreadable ? (0, gate_verdict_cjs_1.gateUnreadable)(false, phaseDirResult) : (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, phaseDirResult);
100
+ }
93
101
  // Every file the haystack is built from is typed evidence (#5170): one that exists but cannot be
94
102
  // read is `unreadable`, never an empty slice of the haystack that makes a decision look unhonored.
95
103
  const planContents = (0, decision_coverage_support_cjs_1.loadPlanContents)(phaseDir);
96
104
  if (planContents.kind === 'unreadable')
97
105
  return unreadableVerify(`${planContents.span ?? phaseDir}: ${planContents.reason}`);
106
+ const tracker = (0, decision_coverage_support_cjs_1.resolveTrackerBodies)(projectDir, planContents.value, input.deps);
107
+ if (tracker.kind === 'unreadable')
108
+ return unreadableVerify(`${tracker.span}: ${tracker.reason}`);
98
109
  const summaryParts = (0, decision_coverage_support_cjs_1.loadSummaryContents)(phaseDir);
99
110
  if (summaryParts.kind === 'unreadable')
100
111
  return unreadableVerify(`${summaryParts.span ?? phaseDir}: ${summaryParts.reason}`);
112
+ // #4939: mirror the plan gate. A phase directory with no PLAN and no SUMMARY file gave the
113
+ // verifier nothing to read, which is a caller error, not a phase whose decisions were all missed.
114
+ if (phaseDir && planContents.value.length === 0 && summaryParts.value.length === 0) {
115
+ return (0, gate_verdict_cjs_1.gateVerdict)('advisory', false, { skipped: false, blocking: false, reason: 'phase directory holds no plans', total: null, honored: null, not_honored: [], message: `Decision coverage verify (warning): the phase directory "${args[0]}" holds no PLAN or SUMMARY files, so nothing was scanned. Pass the phase DIRECTORY that holds the plans (e.g. .planning/phases/01-slug), not the phase number.` });
116
+ }
101
117
  const modifiedFiles = (0, decision_coverage_support_cjs_1.readModifiedFilesContent)(projectDir, summaryParts.value);
102
118
  if (modifiedFiles.kind === 'unreadable')
103
119
  return unreadableVerify(`${modifiedFiles.span ?? 'files_modified'}: ${modifiedFiles.reason}`);
104
120
  const haystack = [
105
- planContents.value.join('\n\n'),
121
+ planContents.value.map((c, i) => c + (tracker.value.bodies[i] ? '\n' + tracker.value.bodies[i] : '')).join('\n\n'),
106
122
  summaryParts.value.join('\n\n'),
107
123
  modifiedFiles.value,
108
124
  (0, decision_coverage_support_cjs_1.phaseCommitMessages)(projectDir, phaseDir),
@@ -115,6 +131,8 @@ function evaluateDecisionCoverageVerify(input) {
115
131
  else
116
132
  notHonored.push({ id: decision.id, text: decision.text, category: decision.category });
117
133
  }
134
+ if (notHonored.length > 0 && tracker.value.missingResolver)
135
+ return unreadableVerify(`${tracker.value.missingResolver.span}: ${tracker.value.missingResolver.reason}`);
118
136
  return (0, gate_verdict_cjs_1.gateVerdict)(notHonored.length === 0 ? 'pass' : 'advisory', false, {
119
137
  skipped: false,
120
138
  blocking: false,
@@ -35,9 +35,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.MAX_COMMITS_CEILING = exports.SCOPE_EXCLUSION_PATHSPECS = void 0;
37
37
  exports.extractTaskCommitRefs = extractTaskCommitRefs;
38
+ exports.qualifiedPlanSubjectPattern = qualifiedPlanSubjectPattern;
38
39
  exports.planSubjectPattern = planSubjectPattern;
39
40
  exports.isSafeRefArgument = isSafeRefArgument;
40
41
  exports.isSafeDateArgument = isSafeDateArgument;
42
+ exports.commitIsAncestor = commitIsAncestor;
41
43
  exports.resolveEvaluationScope = resolveEvaluationScope;
42
44
  exports.evaluateEvaluationScope = evaluateEvaluationScope;
43
45
  const node_fs_1 = __importDefault(require("node:fs"));
@@ -46,6 +48,9 @@ const shell_command_projection_cjs_1 = require("./shell-command-projection.cjs")
46
48
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
47
49
  const gate_phase_context_cjs_1 = require("./gate-phase-context.cjs");
48
50
  const pattern_cjs_1 = require("./pattern.cjs");
51
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
52
+ const planningWorkspaceMod = require("./planning-workspace.cjs");
53
+ const { readScopedConfigValue } = planningWorkspaceMod;
49
54
  const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
50
55
  // ─── Constants ────────────────────────────────────────────────────────────────
51
56
  /** Paths that are never part of a code-review or UI scope (planning artifacts, lockfiles). */
@@ -107,25 +112,124 @@ function paddedPattern(value) {
107
112
  const intPart = (m[1] ?? '').replace(/^0+(?=\d)/, '');
108
113
  return `0*${(0, pattern_cjs_1.escapeEre)(intPart)}${(0, pattern_cjs_1.escapeEre)(m[2] ?? '')}`;
109
114
  }
115
+ /**
116
+ * The `<phase>-<plan>` fragment of a plan subject pattern, or null when `id` is not that shape.
117
+ *
118
+ * Zero-padding tolerant in both halves: `03-01` yields `0*3-0*1`, so `feat(03-01):`, `test(3-1):`
119
+ * and `fix(03-01)!:` match while `feat(03-010):` does not (#4003, #4619, #4748).
120
+ */
121
+ function phasePlanPattern(id) {
122
+ const dash = id.indexOf('-');
123
+ const phasePart = dash > 0 ? id.slice(0, dash) : '';
124
+ const planPart = dash > 0 ? id.slice(dash + 1) : '';
125
+ if (/^[0-9A-Za-z.]+$/.test(phasePart) && /^[0-9A-Za-z.]+$/.test(planPart)) {
126
+ return `${paddedPattern(phasePart)}-${paddedPattern(planPart)}`;
127
+ }
128
+ return null;
129
+ }
130
+ /**
131
+ * The anchored subject pattern for a `<phase>-<plan>` ERE fragment, optionally behind one scope
132
+ * qualifier as the group `(<qualifier>-)?`.
133
+ *
134
+ * Every spelling of a plan id goes through this one builder: the bare id, the id carrying the
135
+ * configured project code (#5271), and the workstream-qualified id `<ws>-<phase>-<plan>` that #5177
136
+ * proposes. The qualifier is ERE-escaped, so a workstream name (a path segment, not a project-code
137
+ * token) is matched literally. The group is a real capture group, never `(?:…)`: the pattern goes
138
+ * to `git log --extended-regexp`, and POSIX ERE has no non-capturing group.
139
+ */
140
+ function qualifiedPlanSubjectPattern(fragment, qualifier = null) {
141
+ const group = qualifier === null ? '' : `(${(0, pattern_cjs_1.escapeEre)(qualifier)}-)?`;
142
+ return `^[a-z]+\\(${group}${fragment}\\)!?:`;
143
+ }
144
+ /**
145
+ * The `<CODE>-` prefix of `planId` when it is EXACTLY the project's configured `project_code`
146
+ * followed by a dash — the separator included — else null.
147
+ *
148
+ * The match is CASE-SENSITIVE and anchored to the configured value. The phase-id module's
149
+ * `stripProjectCodePrefix` grammar (case-insensitive, any code before a digit) recognises ANY
150
+ * letter prefix, which is deliberately wider than the project: `AUTH-01-02` and `setup-1-2` are
151
+ * ids this project never assigned a code to, and stripping their prefixes made them resolve
152
+ * another plan's commits (#5271). Only the code the project actually configures may be
153
+ * stripped, so an id that merely LOOKS coded keeps its literal-match guarantee.
154
+ */
155
+ function configuredCodePrefix(planId, projectCode) {
156
+ if (projectCode === null || !isUsableProjectCode(projectCode))
157
+ return null;
158
+ const prefix = `${projectCode}-`;
159
+ return planId.startsWith(prefix) ? prefix : null;
160
+ }
110
161
  /**
111
162
  * The anchored subject pattern for a plan id, or null when `planId` is empty, over-long or carries a
112
163
  * control / whitespace character.
113
164
  *
114
165
  * A `<phase>-<plan>` id (`03-01`) is zero-padding tolerant: `feat(03-01):`, `test(3-1):` and
115
- * `fix(03-01)!:` match, `feat(03-010):` does not. Any other id (a plan FILE NAME that does not follow
116
- * the numbering) is matched LITERALLY — every ERE metacharacter escaped — so `x.*` or `a[b]` can only
117
- * match a commit that names that exact id, and never widens the pattern.
166
+ * `fix(03-01)!:` match, `feat(03-010):` does not. An id carrying the CONFIGURED project code
167
+ * (`PRJ-01-01` with `project_code: "PRJ"`, #5271) matches either spelling of the SAME plan —
168
+ * `feat(PRJ-01-01):` and `feat(01-01):` — because the two init verbs hand out different phase
169
+ * tokens for one plan. The code is stripped ONLY when it is the configured one: an id like
170
+ * `AUTH-01-02` or `setup-1-2`, which merely looks coded, is matched LITERALLY — every ERE
171
+ * metacharacter escaped — so it can only match a commit naming that exact id and never widens the
172
+ * pattern to another plan's commits. Any other id (a plan FILE NAME that does not follow the
173
+ * numbering) is matched literally for the same reason.
174
+ *
175
+ * `projectCode` is the project's configured `project_code`; with none (no config, no `planningDir`),
176
+ * every id is matched literally, which is the pre-#5271 behaviour.
118
177
  */
119
- function planSubjectPattern(planId) {
178
+ function planSubjectPattern(planId, projectCode = null) {
120
179
  if (planId.length === 0 || planId.length > 200 || /[\s\x00-\x1f\x7f]/.test(planId))
121
180
  return null;
122
- const dash = planId.indexOf('-');
123
- const phasePart = dash > 0 ? planId.slice(0, dash) : '';
124
- const planPart = dash > 0 ? planId.slice(dash + 1) : '';
125
- if (/^[0-9A-Za-z.]+$/.test(phasePart) && /^[0-9A-Za-z.]+$/.test(planPart)) {
126
- return `^[a-z]+\\(${paddedPattern(phasePart)}-${paddedPattern(planPart)}\\)!?:`;
181
+ const direct = phasePlanPattern(planId);
182
+ if (direct)
183
+ return qualifiedPlanSubjectPattern(direct);
184
+ // #5271: `init.execute-phase` returns `phase_number: "PRJ-01"` where `init.plan-phase` returns
185
+ // `padded_phase: "01"`, and every gate builds its `--plan` from the former. Splitting `PRJ-01-01`
186
+ // at its FIRST dash leaves `01-01` as the plan half, which is not a plan number, so the id fell
187
+ // through to the literal pattern that no executor commit can ever carry — an empty scope at exit
188
+ // 0, which is the correct answer for a plan that has NOT started and the wrong one for one that
189
+ // has. Retry on the CONFIGURED project-code-stripped id, with the code OPTIONAL so the commit the
190
+ // executor actually wrote still matches.
191
+ //
192
+ // The `(PRJ-)?` group comes from `qualifiedPlanSubjectPattern`, and it rides ONLY on a remainder
193
+ // that could be a plan number, so an empty or non-numeric remainder keeps the whole id literal —
194
+ // never WIDER than the pre-#5271 pattern.
195
+ const prefix = configuredCodePrefix(planId, projectCode);
196
+ if (prefix !== null) {
197
+ const stripped = planId.slice(prefix.length);
198
+ const coded = phasePlanPattern(stripped);
199
+ // The only remainder `phasePlanPattern` cannot split is the N-segment milestone shape
200
+ // (`01-02-03`), so require that whole shape: a bare `1`, `01-` or a trailing dash would
201
+ // otherwise widen the optional group onto `feat(1):`.
202
+ const planShaped = coded !== null || /^\d[0-9A-Za-z.]*(-[0-9A-Za-z.]+){2,}$/.test(stripped);
203
+ if (!planShaped)
204
+ return qualifiedPlanSubjectPattern((0, pattern_cjs_1.escapeEre)(planId));
205
+ return qualifiedPlanSubjectPattern(coded ?? (0, pattern_cjs_1.escapeEre)(stripped), projectCode);
127
206
  }
128
- return `^[a-z]+\\(${(0, pattern_cjs_1.escapeEre)(planId)}\\)!?:`;
207
+ return qualifiedPlanSubjectPattern((0, pattern_cjs_1.escapeEre)(planId));
208
+ }
209
+ /**
210
+ * Whether a configured `project_code` can safely be stripped from a plan id and put in a pattern.
211
+ *
212
+ * `config-set` stores `project_code` unvalidated and docs/CONFIGURATION.md calls it "any short
213
+ * string", so this is a SAFETY check, not the phase-id module's uppercase grammar: a letter first,
214
+ * then letters, digits, `_`, `.` or `-`, at most 32 characters. That admits `gsd` and `AUTH-X`
215
+ * (which the uppercase grammar left in the #5271 symptom) while excluding whitespace, control
216
+ * characters and anything long enough to be a sentence. The value is ERE-escaped by the pattern
217
+ * builder, so no metacharacter it admits can change the pattern.
218
+ */
219
+ function isUsableProjectCode(code) {
220
+ return /^[A-Za-z][A-Za-z0-9_.-]{0,31}$/.test(code);
221
+ }
222
+ /**
223
+ * The project's configured `project_code`, or null when it is absent, non-string or unusable
224
+ * (`isUsableProjectCode`). An unusable value reads as ABSENT: the coded id keeps its LITERAL
225
+ * spelling, which is never WIDER than the pre-#5271 pattern, since a literal id can only match a
226
+ * subject naming that exact id.
227
+ */
228
+ function configuredProjectCode(projectDir) {
229
+ const { present, value } = readScopedConfigValue(projectDir, ['project_code']);
230
+ if (!present || typeof value !== 'string')
231
+ return null;
232
+ return isUsableProjectCode(value) ? value : null;
129
233
  }
130
234
  // ─── Git plumbing ─────────────────────────────────────────────────────────────
131
235
  class ScopeUnreadable extends Error {
@@ -189,6 +293,23 @@ function isSafeDateArgument(candidate) {
189
293
  function isAncestor(git, sha, of) {
190
294
  return gitOrNo(git, ['merge-base', '--is-ancestor', sha, of]) !== null;
191
295
  }
296
+ /**
297
+ * Whether commit `sha` is an ancestor of commit `of` (#5275): ancestry, never log position or commit
298
+ * date, so a history merged from worktree branches is judged by what it descends from. `null` means
299
+ * git could not answer (missing, hung, a bad object): the caller must not read that as "no".
300
+ */
301
+ function commitIsAncestor(projectDir, sha, of, execGit) {
302
+ if (!isSafeRefArgument(sha) || !isSafeRefArgument(of))
303
+ return null;
304
+ try {
305
+ return isAncestor(makeGit(projectDir, execGit ?? shell_command_projection_cjs_1.execGit), sha, of);
306
+ }
307
+ catch (error) {
308
+ if (error instanceof ScopeUnreadable)
309
+ return null;
310
+ throw error;
311
+ }
312
+ }
192
313
  // ─── The resolver ─────────────────────────────────────────────────────────────
193
314
  function emptyScope(unit) {
194
315
  return {
@@ -210,7 +331,7 @@ function resolveEvaluationScope(projectDir, unit, options = {}) {
210
331
  if (unit.kind === 'phase')
211
332
  resolvePhase(projectDir, unit, scope, git, ref, options, repoRoot);
212
333
  else
213
- resolveBySubject(unit, scope, git, ref, options, repoRoot);
334
+ resolveBySubject(projectDir, unit, scope, git, ref, options, repoRoot);
214
335
  return scope;
215
336
  }
216
337
  catch (error) {
@@ -388,7 +509,7 @@ function resolvePhase(projectDir, unit, scope, git, ref, options, repoRoot) {
388
509
  scope.commits = parseLog(git(['log', `${scope.rangeBase}..${ref}`, logFormat(options.includeBody === true), '-n', String(max)]), options.includeBody === true);
389
510
  finalizeFiles(scope, repoRoot, rangeFiles(git, scope.rangeBase, ref));
390
511
  }
391
- function resolveBySubject(unit, scope, git, ref, options, repoRoot) {
512
+ function resolveBySubject(projectDir, unit, scope, git, ref, options, repoRoot) {
392
513
  const withBody = options.includeBody === true;
393
514
  // Refuse rather than truncate: a silently dropped positive pathspec narrows the match.
394
515
  const pathspecs = options.pathspecs ?? [];
@@ -412,7 +533,7 @@ function resolveBySubject(unit, scope, git, ref, options, repoRoot) {
412
533
  const grepArgs = [];
413
534
  let anchored = null;
414
535
  if (unit.kind === 'plan') {
415
- const pattern = planSubjectPattern(unit.planId);
536
+ const pattern = planSubjectPattern(unit.planId, configuredProjectCode(projectDir));
416
537
  if (pattern === null)
417
538
  throw new ScopeUnreadable('invalid-plan-id');
418
539
  grepArgs.push('--extended-regexp', `--grep=${pattern}`);
@@ -26,6 +26,10 @@
26
26
  * This is a leaf pure module: no fs, no child_process, no config — the subprocess
27
27
  * seam is injected so the evaluator is trivially testable without spawning.
28
28
  */
29
+ var __importDefault = (this && this.__importDefault) || function (mod) {
30
+ return (mod && mod.__esModule) ? mod : { "default": mod };
31
+ };
32
+ const node_path_1 = __importDefault(require("node:path"));
29
33
  // ─── Public constants ─────────────────────────────────────────────────────────
30
34
  /** Default command timeout for `command-exit-zero` (30s). Matches execTool default. */
31
35
  const COMMAND_EXIT_ZERO_DEFAULT_TIMEOUT_MS = 30_000;
@@ -116,15 +120,31 @@ function evaluateArtifactFrontmatterEquals(predicate, ctx, deps) {
116
120
  throw new Error('artifact-frontmatter-equals predicate requires an "equals" key');
117
121
  }
118
122
  const targetDir = isNonEmptyString(ctx.phaseDir) ? ctx.phaseDir : ctx.cwd;
119
- const filePath = deps.findPhaseArtifact(targetDir, artifactSuffix);
120
- if (!filePath) {
123
+ // Plain `.sort()` (no locale) so the outcome never depends on directory listing order (#5313).
124
+ // Set: finders return realpaths, so a symlink to the same file is one artifact, not two.
125
+ const found = deps.findPhaseArtifact(targetDir, artifactSuffix);
126
+ // A pre-#5313 dep returned `string | null`; a silent null -> "not found" block or per-character spread would mislead.
127
+ if (!Array.isArray(found)) {
128
+ throw new Error('predicate deps "findPhaseArtifact" must return an array of paths');
129
+ }
130
+ const candidates = [...new Set(found)].sort();
131
+ if (candidates.length === 0) {
121
132
  return {
122
133
  block: true,
123
134
  message: `Artifact matching ${artifactSuffix} not found in ${targetDir}`,
124
135
  details: { kind: 'artifact-frontmatter-equals', artifactNotFound: true },
125
136
  };
126
137
  }
127
- const fm = deps.readFrontmatter(filePath);
138
+ // #5313: never pick one of several matches (listing order is OS-dependent); a throw would fail open via onError.
139
+ if (candidates.length > 1) {
140
+ const names = candidates.map((c) => node_path_1.default.relative(targetDir, c).split(node_path_1.default.sep).join('/')).join(', ');
141
+ return {
142
+ block: true,
143
+ message: `Multiple artifacts match ${artifactSuffix} in ${targetDir}: ${names}; remove or rename all but one`,
144
+ details: { kind: 'artifact-frontmatter-equals', ambiguous: true, candidates },
145
+ };
146
+ }
147
+ const fm = deps.readFrontmatter(candidates[0]);
128
148
  const actualValue = fm[field];
129
149
  // eslint-disable-next-line @typescript-eslint/no-base-to-string
130
150
  const expectedStr = typeof expectedValue === 'object' ? JSON.stringify(expectedValue) : String(expectedValue);
@@ -80,31 +80,27 @@ function buildPredicateDeps() {
80
80
  if (phaseDirStat.kind === 'unreadable')
81
81
  return unreadable(phaseDir, phaseDirStat.reason);
82
82
  if (phaseDirStat.kind === 'none')
83
- return null;
83
+ return [];
84
84
  if (artifactSuffix === '.' ||
85
85
  artifactSuffix === '..' ||
86
86
  artifactSuffix.includes('\0') ||
87
87
  node_path_1.default.basename(artifactSuffix) !== artifactSuffix ||
88
88
  node_path_1.default.win32.basename(artifactSuffix) !== artifactSuffix) {
89
- return null;
90
- }
91
- const directContained = (0, security_cjs_1.tryWithinRoot)(artifactSuffix, phaseDir);
92
- if (directContained !== null) {
93
- const direct = regularFileOrNull(directContained);
94
- if (direct !== null)
95
- return direct;
89
+ return [];
96
90
  }
91
+ // #5313: collect EVERY distinct match; the evaluator fails closed on >1 instead of trusting listing order.
92
+ const found = [];
97
93
  const planningContained = (0, security_cjs_1.tryWithinRoot)(node_path_1.default.join('.planning', artifactSuffix), phaseDir);
98
94
  if (planningContained !== null) {
99
95
  const planning = regularFileOrNull(planningContained);
100
96
  if (planning !== null)
101
- return planning;
97
+ found.push(planning);
102
98
  }
103
99
  const listing = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
104
100
  if (listing.kind === 'unreadable')
105
101
  return unreadable(phaseDir, listing.reason);
106
102
  if (listing.kind === 'none')
107
- return null;
103
+ return [];
108
104
  for (const f of listing.value) {
109
105
  if (f.endsWith('-' + artifactSuffix) || f === artifactSuffix) {
110
106
  const candidateContained = (0, security_cjs_1.tryWithinRoot)(f, phaseDir);
@@ -112,10 +108,10 @@ function buildPredicateDeps() {
112
108
  continue;
113
109
  const candidate = regularFileOrNull(candidateContained);
114
110
  if (candidate !== null)
115
- return candidate;
111
+ found.push(candidate);
116
112
  }
117
113
  }
118
- return null;
114
+ return found;
119
115
  },
120
116
  readFrontmatter(filePath) {
121
117
  const content = (0, shell_command_projection_cjs_1.platformReadSync)(filePath);
@@ -17,12 +17,19 @@
17
17
  * tolerant. A plan id that is not `<phase>-<plan>` (a plan named `.*-PLAN.md`) matches nothing,
18
18
  * and a git failure is "no commits" exactly as before.
19
19
  *
20
+ * Cross-plan GREEN (#4867): a gap closure is planned as a RED-only probe plan plus the plan that
21
+ * `depends_on` it and carries the fix. A `type: tdd` plan with its own RED but no GREEN is credited
22
+ * with a `feat(...)` commit under any plan that lists it in `depends_on`, directly or through a
23
+ * chain; the row carries `greenFrom` and the report names the supplier. Its own RED is still
24
+ * required, and a plan nothing depends on is scored on its own commits alone.
25
+ *
20
26
  * Argv after the verb: `<phase>` (a number; an unresolvable phase reports zero plans).
21
27
  */
22
28
  var __importDefault = (this && this.__importDefault) || function (mod) {
23
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
24
30
  };
25
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
+ exports.TDD_ROW_REASON = void 0;
26
33
  exports.evaluateTddReviewCheckpoint = evaluateTddReviewCheckpoint;
27
34
  const node_path_1 = __importDefault(require("node:path"));
28
35
  const gate_verdict_cjs_1 = require("./gate-verdict.cjs");
@@ -31,7 +38,10 @@ const gate_evidence_cjs_1 = require("./gate-evidence.cjs");
31
38
  const gate_evaluation_scope_cjs_1 = require("./gate-evaluation-scope.cjs");
32
39
  // eslint-disable-next-line @typescript-eslint/no-require-imports
33
40
  const frontmatterMod = require("./frontmatter.cjs");
34
- const { frontmatterKeyHasValue } = frontmatterMod;
41
+ const { frontmatterKeyHasValue, extractFrontmatter } = frontmatterMod;
42
+ exports.TDD_ROW_REASON = {
43
+ GREEN_BEFORE_RED: 'green_before_red',
44
+ };
35
45
  /**
36
46
  * True when the plan's frontmatter declares `type: tdd` (CRLF included, #2449). The block is the
37
47
  * one fence owner's, read as RAW text — not through the YAML parser — so a block the parser refuses
@@ -42,20 +52,69 @@ const { frontmatterKeyHasValue } = frontmatterMod;
42
52
  function isTddPlan(content) {
43
53
  return frontmatterKeyHasValue(content, 'type', 'tdd');
44
54
  }
55
+ /** A `depends_on` token as a plan id: `01.1-20`, `01.1-20-PLAN` and `01.1-20-PLAN.md` all name plan `01.1-20`. */
56
+ function toPlanId(token) {
57
+ return token.trim().replace(/-PLAN(?:\.md)?$/i, '');
58
+ }
45
59
  /**
46
- * The commit types (`test`, `feat`, `refactor`, …) among the plan's own commits — those that
47
- * touched at least one path (`pathspecs: ['.']`, as the old `git log -- .` lookup did). An
48
- * unresolvable scope (git unavailable, an id that is not `<phase>-<plan>`) is "no commits".
60
+ * The plan ids a plan lists in `depends_on` (#4867). A plan whose frontmatter does not parse, or has
61
+ * no `depends_on`, depends on nothing: the pairing below only ever ADDS credit, so an unreadable
62
+ * link can never turn a failing plan into a passing one.
49
63
  */
50
- function planCommitKinds(projectDir, planId) {
64
+ function dependsOnOf(content) {
65
+ // `extractFrontmatter` does not throw: an unparseable block is an empty result, so `depends_on` is absent.
66
+ const value = extractFrontmatter(content)['depends_on'];
67
+ const tokens = Array.isArray(value) ? value.map(String) : typeof value === 'string' ? value.split(',') : [];
68
+ return tokens.map(toPlanId).filter((id) => id !== '');
69
+ }
70
+ /** Every plan that depends on `planId`, directly or through a chain (#4867); a cycle is walked once. */
71
+ function transitiveDependents(planId, dependents) {
72
+ const seen = new Set();
73
+ const queue = [planId];
74
+ while (queue.length > 0) {
75
+ for (const next of dependents.get(queue.shift()) ?? []) {
76
+ if (next !== planId && !seen.has(next)) {
77
+ seen.add(next);
78
+ queue.push(next);
79
+ }
80
+ }
81
+ }
82
+ return [...seen].sort();
83
+ }
84
+ /**
85
+ * The plan's own commits (`test`, `feat`, `refactor`, …) — those that touched at least one path
86
+ * (`pathspecs: ['.']`, as the old `git log -- .` lookup did) — with the sha the ordering check needs.
87
+ * An unresolvable scope (git unavailable, an id that is not `<phase>-<plan>`) is "no commits".
88
+ */
89
+ function planCommits(projectDir, planId) {
51
90
  const scope = (0, gate_evaluation_scope_cjs_1.resolveEvaluationScope)(projectDir, { kind: 'plan', planId }, { pathspecs: ['.'], commitsOnly: true });
52
- const kinds = new Set();
91
+ const commits = [];
53
92
  for (const commit of scope.commits) {
54
93
  const kind = /^([a-z]+)\(/.exec(commit.subject)?.[1];
55
94
  if (kind)
56
- kinds.add(kind);
95
+ commits.push({ sha: commit.sha, kind });
96
+ }
97
+ return commits;
98
+ }
99
+ const shasOfKind = (commits, kind) => commits.filter((c) => c.kind === kind).map((c) => c.sha);
100
+ /**
101
+ * Whether some GREEN descends from some RED (#5275). Decided by ancestry only — never log position or
102
+ * commit date — so a history merged from worktree branches is judged by what each commit descends from.
103
+ * `true` as soon as one pair is in order; `null` when git could not answer for a pair and none was
104
+ * in order (an environment fault is not a violation, so the caller keeps the credit); else `false`.
105
+ */
106
+ function greenFollowsRed(projectDir, reds, greens) {
107
+ let unanswered = false;
108
+ for (const red of reds) {
109
+ for (const green of greens) {
110
+ const redIsAncestor = (0, gate_evaluation_scope_cjs_1.commitIsAncestor)(projectDir, red, green);
111
+ if (redIsAncestor === true)
112
+ return true;
113
+ if (redIsAncestor === null)
114
+ unanswered = true;
115
+ }
57
116
  }
58
- return kinds;
117
+ return unanswered ? null : false;
59
118
  }
60
119
  /**
61
120
  * The verdict when the review could not read a plan or the phase directory. Advisory policy is
@@ -91,6 +150,9 @@ function evaluateTddReviewCheckpoint(input) {
91
150
  const phaseDir = located.kind === 'found' ? located.value : '';
92
151
  // Find all PLAN.md files with type: tdd in frontmatter
93
152
  const tddPlanFiles = [];
153
+ // #4867: plan id -> the plans that list it in `depends_on`, over EVERY plan of the phase (a
154
+ // dependent GREEN plan is usually `type: execute`, not `tdd`).
155
+ const dependents = new Map();
94
156
  const unreadable = [];
95
157
  if (phaseDir) {
96
158
  const entries = (0, gate_evidence_cjs_1.readDirEvidence)(phaseDir);
@@ -110,8 +172,13 @@ function evaluateTddReviewCheckpoint(input) {
110
172
  const plan = (0, gate_evidence_cjs_1.readTextEvidence)(planPath);
111
173
  if (plan.kind === 'unreadable')
112
174
  unreadable.push({ source: planPath, reason: plan.reason });
113
- else if (plan.kind === 'found' && isTddPlan(plan.value))
114
- tddPlanFiles.push(planPath);
175
+ else if (plan.kind === 'found') {
176
+ if (isTddPlan(plan.value))
177
+ tddPlanFiles.push(planPath);
178
+ const planId = node_path_1.default.basename(planPath, '-PLAN.md');
179
+ for (const dep of dependsOnOf(plan.value))
180
+ dependents.set(dep, [...(dependents.get(dep) ?? []), planId]);
181
+ }
115
182
  }
116
183
  }
117
184
  }
@@ -134,17 +201,41 @@ function evaluateTddReviewCheckpoint(input) {
134
201
  const rows = [];
135
202
  for (const planPath of tddPlanFiles) {
136
203
  const planId = node_path_1.default.basename(planPath, '-PLAN.md');
137
- const kinds = planCommitKinds(projectDir, planId);
138
- const red = kinds.has('test');
139
- const green = kinds.has('feat');
140
- const refactor = kinds.has('refactor');
204
+ const commits = planCommits(projectDir, planId);
205
+ const reds = shasOfKind(commits, 'test');
206
+ const red = reds.length > 0;
207
+ const refactor = commits.some((c) => c.kind === 'refactor');
208
+ // #5275: with a RED present, a GREEN counts only when it descends from a RED. Without one there is
209
+ // nothing to order against and the plan is already missing RED, so a GREEN commit stays credited.
210
+ const inOrder = (greens) => greens.length > 0 && (!red || greenFollowsRed(projectDir, reds, greens) !== false);
211
+ const ownGreens = shasOfKind(commits, 'feat');
212
+ const ownOk = inOrder(ownGreens);
213
+ // #4867: a RED-only probe plan is satisfied by the GREEN of a plan that (transitively) declares
214
+ // `depends_on` it. Only a real `feat(...)` commit under such a plan counts, and it must follow the
215
+ // RED too (#5275); the plan's own RED is still required, and a plan nothing depends on is scored
216
+ // on its own commits alone.
217
+ const dependentGreens = ownOk
218
+ ? []
219
+ : transitiveDependents(planId, dependents).map((id) => ({ id, greens: shasOfKind(planCommits(projectDir, id), 'feat') }));
220
+ const greenFrom = dependentGreens.filter((d) => inOrder(d.greens)).map((d) => d.id);
221
+ const green = ownOk || greenFrom.length > 0;
222
+ const anyGreenCommit = ownGreens.length > 0 || dependentGreens.some((d) => d.greens.length > 0);
141
223
  const missing = [];
142
224
  if (!red)
143
225
  missing.push('RED');
144
- if (!green)
226
+ if (!anyGreenCommit)
145
227
  missing.push('GREEN');
146
- const status = missing.length === 0 ? 'Pass' : 'FAIL';
147
- rows.push({ planId, red, green, refactor, status, missing });
228
+ const status = missing.length === 0 && green ? 'Pass' : 'FAIL';
229
+ // Both commit kinds exist but no GREEN follows a RED: its own reason. A row that fails for a
230
+ // missing commit keeps the exact shape it always had (`missing` already says why).
231
+ const reason = status === 'FAIL' && missing.length === 0
232
+ ? exports.TDD_ROW_REASON.GREEN_BEFORE_RED
233
+ : undefined;
234
+ rows.push({
235
+ planId, red, green, refactor, status, missing,
236
+ ...(reason ? { reason } : {}),
237
+ ...(greenFrom.length > 0 ? { greenFrom } : {}),
238
+ });
148
239
  }
149
240
  const violations = rows.filter(r => r.status === 'FAIL').length;
150
241
  // Build review table
@@ -160,10 +251,15 @@ function evaluateTddReviewCheckpoint(input) {
160
251
  tableDivider,
161
252
  ...tableRows,
162
253
  ].join('\n');
254
+ for (const r of rows.filter(row => row.greenFrom)) {
255
+ table += `\n\nPlan ${r.planId} GREEN is supplied by dependent plan ${(r.greenFrom ?? []).join(', ')}.`;
256
+ }
163
257
  if (violations > 0) {
164
258
  table += '\n\n⚠ Gate violations are advisory — review before advancing.';
165
259
  for (const r of rows.filter(row => row.status === 'FAIL')) {
166
- table += `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
260
+ table += r.reason === exports.TDD_ROW_REASON.GREEN_BEFORE_RED
261
+ ? `\n Plan ${r.planId} GREEN precedes RED: no feat(...) commit descends from a test(...) commit.`
262
+ : `\n Plan ${r.planId} missing: ${r.missing.join(', ')} gate commit(s).`;
167
263
  table += `\n Expected commit pattern: test(${r.planId}): ... → feat(${r.planId}): ...`;
168
264
  }
169
265
  }