@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
@@ -1264,10 +1264,10 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
1264
1264
  const bash = resolveBashExecutable(opts);
1265
1265
  if (isShellHook) {
1266
1266
  // An unresolved bash must still surface as an interpreterCandidates entry
1267
- // (the literal token, same as the portableHooks runner below) so
1267
+ // (the sentinel token, same as the portableHooks runner below) so
1268
1268
  // validateConfiguredEntrypoints reports 'unresolved-interpreter' instead
1269
1269
  // of silently skipping the check because the field is absent.
1270
- return [{ ...target, interpreterCandidates: [bash === null ? 'bash' : bash] }];
1270
+ return [{ ...target, interpreterCandidates: [bash === null ? UNRESOLVED_BASH_SENTINEL : bash] }];
1271
1271
  }
1272
1272
  // #4249: check the SAME stable alias buildNodeRunnerChainToken bakes as its
1273
1273
  // first candidate (normalizeNodePath rewrites a version-manager shim like
@@ -1287,7 +1287,7 @@ function configuredEntrypointsForHook(configDir, hookName, opts) {
1287
1287
  runtime,
1288
1288
  configPath,
1289
1289
  scriptPath: node_path_1.default.join(configDir, 'hooks', NODE_RUNNER_RESOLVER_HOOK),
1290
- interpreterCandidates: bash === null ? ['bash'] : [bash],
1290
+ interpreterCandidates: bash === null ? [UNRESOLVED_BASH_SENTINEL] : [bash],
1291
1291
  platform,
1292
1292
  };
1293
1293
  return [runner, { ...target, interpreterCandidates: nodeCandidates }];
@@ -1298,6 +1298,15 @@ function recordConfiguredHookCommand(command, configDir, hookName, opts) {
1298
1298
  }
1299
1299
  return command;
1300
1300
  }
1301
+ // #5100: the two null-runner branches of buildHookCommand share this. No
1302
+ // command was ever built, so track() (which needs one) cannot record the
1303
+ // hook — push the entry directly instead, so the gate sees it rather than
1304
+ // the hook going silently unregistered.
1305
+ function recordUnresolvedBashEntrypoint(configDir, hookName, opts) {
1306
+ if (opts.configuredEntrypoints) {
1307
+ opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
1308
+ }
1309
+ }
1301
1310
  function buildHookCommand(configDir, hookName, opts) {
1302
1311
  if (!opts)
1303
1312
  opts = {};
@@ -1323,15 +1332,9 @@ function buildHookCommand(configDir, hookName, opts) {
1323
1332
  const runner = resolveBashRunner(opts);
1324
1333
  if (runner === null) {
1325
1334
  // #4249 (antigravity review): this early return skips `track()` below,
1326
- // so an unresolved bash on win32 (no Git Bash found) previously left
1327
- // this hook silently unregistered with nothing for
1328
- // validateConfiguredEntrypoints to reject — configuredEntrypointsForHook's
1329
- // own 'unresolved bash must still surface' comment describes intent this
1330
- // return never reached. Push the entry directly (no `command`, since
1331
- // none was ever built) so the gate actually sees it.
1332
- if (opts.configuredEntrypoints) {
1333
- opts.configuredEntrypoints.push(...configuredEntrypointsForHook(configDir, hookName, opts));
1334
- }
1335
+ // which needs a command that was never built — record the entry
1336
+ // directly so the gate can reject it.
1337
+ recordUnresolvedBashEntrypoint(configDir, hookName, opts);
1335
1338
  return null;
1336
1339
  }
1337
1340
  if (opts.portableHooks) {
@@ -1374,9 +1377,13 @@ function buildHookCommand(configDir, hookName, opts) {
1374
1377
  configDir,
1375
1378
  homeDir: node_os_1.default.homedir(),
1376
1379
  });
1377
- // Absolute Git-Bash discovery on win32 when available (#580); `bash` on
1378
- // PATH otherwise — the same assumption .sh hooks already make.
1379
- const resolverRunner = resolveBashRunner(opts) || 'bash';
1380
+ // #5100: same contract as the .sh early return. A bare `bash` is how
1381
+ // win32 reaches WSL's System32 launcher, and the gate used to accept it.
1382
+ const resolverRunner = resolveBashRunner(opts);
1383
+ if (resolverRunner === null) {
1384
+ recordUnresolvedBashEntrypoint(configDir, hookName, opts);
1385
+ return null;
1386
+ }
1380
1387
  return track(shellCmdProjection.projectShellCommandText({
1381
1388
  runnerToken: resolverRunner,
1382
1389
  argTokens: [
@@ -2158,6 +2165,9 @@ const SETTINGS_JSON_HOOK_ROWS = deepFreeze([
2158
2165
  // Guard: only register if the hook file was actually installed (#1754, #1817).
2159
2166
  { file: 'gsd-check-update.js', event: 'SessionStart', command: { opts: 'updateCheckCommand' },
2160
2167
  configuredMessage: 'Configured update check hook', skipLabel: 'update check hook' },
2168
+ // #5465: persists the gsd-core bin directory on PATH through $CLAUDE_ENV_FILE (SessionStart-only).
2169
+ { file: 'gsd-path-export.js', event: 'SessionStart', timeout: 5, command: { build: 'js' },
2170
+ configuredMessage: 'Configured PATH persistence hook', skipLabel: 'PATH persistence hook' },
2161
2171
  { file: 'gsd-context-monitor.js', event: 'post', matcher: 'Bash|Edit|Write|MultiEdit|Agent|Task', timeout: 10,
2162
2172
  command: { opts: 'contextMonitorCommand' },
2163
2173
  configuredMessage: 'Configured context window monitor hook', skipLabel: 'context monitor hook',
@@ -2710,6 +2720,30 @@ function referencesHook(h, hookName) {
2710
2720
  (Array.isArray(args) && args.some(a => typeof a === 'string' && a.includes(hookName))) ||
2711
2721
  (typeof url === 'string' && url.includes(hookName));
2712
2722
  }
2723
+ // #5100: the writer emits the bare `bash` token when the Git Bash policy
2724
+ // finds nothing, and the gate resolves that token through the same policy —
2725
+ // never a PATH scan. One shared definition so the two sides cannot drift.
2726
+ const UNRESOLVED_BASH_SENTINEL = 'bash';
2727
+ const WIN32_BARE_BASH_TOKENS = [UNRESOLVED_BASH_SENTINEL, 'bash.exe'];
2728
+ function isWin32BareBashToken(candidate, platform) {
2729
+ if ((platform || process.platform) !== 'win32')
2730
+ return false;
2731
+ // Equality already rejects any path: a string holding a separator can never
2732
+ // equal a bare token, so no explicit separator guard is needed here.
2733
+ return WIN32_BARE_BASH_TOKENS.includes(candidate.trim().toLowerCase());
2734
+ }
2735
+ // #5100: the literal `bash` sentinel means "Git Bash policy found nothing"
2736
+ // (#4249). Resolve that token through resolveBashExecutable, never a PATH
2737
+ // scan — WSL's System32\bash.exe is on PATH and cannot see Windows paths.
2738
+ function win32BareBashResolves(candidate, platform, deps) {
2739
+ if (!isWin32BareBashToken(candidate, platform))
2740
+ return null;
2741
+ return resolveBashExecutable({
2742
+ platform: 'win32',
2743
+ env: deps.env,
2744
+ existsSync: deps.existsSync,
2745
+ }) !== null;
2746
+ }
2713
2747
  function validateConfiguredEntrypoints(entries, deps = {}) {
2714
2748
  const statSync = deps.statSync ?? node_fs_1.default.statSync;
2715
2749
  const accessSync = deps.accessSync ?? node_fs_1.default.accessSync;
@@ -2766,7 +2800,12 @@ function validateConfiguredEntrypoints(entries, deps = {}) {
2766
2800
  invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'script', path: entry.scriptPath, reason: 'not-executable' });
2767
2801
  }
2768
2802
  }
2769
- if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate => resolve(candidate, { platform: entry.platform, requireExecutable: true }) !== null)) {
2803
+ if (entry.interpreterCandidates && !entry.interpreterCandidates.some(candidate => {
2804
+ const bareBash = win32BareBashResolves(candidate, entry.platform, deps);
2805
+ if (bareBash !== null)
2806
+ return bareBash;
2807
+ return resolve(candidate, { platform: entry.platform, requireExecutable: true }) !== null;
2808
+ })) {
2770
2809
  invalid.push({ runtime: entry.runtime, configPath: entry.configPath, role: 'interpreter', path: entry.interpreterCandidates.join(' | '), reason: 'unresolved-interpreter' });
2771
2810
  }
2772
2811
  }
@@ -2856,6 +2895,11 @@ module.exports = {
2856
2895
  resolveBashExecutable,
2857
2896
  resolveBashRunner,
2858
2897
  NODE_RUNNER_RESOLVER_HOOK,
2898
+ // #5100: the literal the writer records for an unresolved Git Bash and the
2899
+ // gate resolves through the Git Bash policy. Exported so a consumer that
2900
+ // needs to recognise that entry imports it rather than respelling it;
2901
+ // bin/install.js keys on the entry having no command instead.
2902
+ UNRESOLVED_BASH_SENTINEL,
2859
2903
  // Atomic write seam (shared with bin/install.js so all writes participate
2860
2904
  // in install.js's _cleanTmpFiles() scoped temp-cleanup).
2861
2905
  atomicWriteFileSync,
@@ -65,7 +65,9 @@ const node_fs_1 = __importDefault(require("node:fs"));
65
65
  const node_child_process_1 = __importDefault(require("node:child_process"));
66
66
  const pattern_cjs_1 = require("./pattern.cjs");
67
67
  const frontmatter_fence_cjs_1 = require("./frontmatter-fence.cjs");
68
+ const markdown_line_diff_cjs_1 = require("./markdown-line-diff.cjs");
68
69
  const runtime_name_policy_cjs_1 = require("./runtime-name-policy.cjs");
70
+ const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
69
71
  /**
70
72
  * Convert a filesystem path to POSIX form (forward slashes) by translating the
71
73
  * platform-native separator. Single seam for native→POSIX conversion.
@@ -249,6 +251,8 @@ const MANAGED_HOOK_BASENAMES_BY_SURFACE = {
249
251
  'gsd-worktree-path-guard.js',
250
252
  // #4221: secret-file read guard (Read|Grep|Bash).
251
253
  'gsd-secret-read-guard.js',
254
+ // #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
255
+ 'gsd-path-export.js',
252
256
  ]),
253
257
  'codex-toml': new Set([
254
258
  'gsd-check-update.js',
@@ -279,6 +283,8 @@ const MANAGED_HOOK_COMMAND_BASENAMES_BY_SURFACE = {
279
283
  'gsd-worktree-path-guard.js',
280
284
  // #4221: secret-file read guard (Read|Grep|Bash).
281
285
  'gsd-secret-read-guard.js',
286
+ // #5465: SessionStart PATH persistence through $CLAUDE_ENV_FILE.
287
+ 'gsd-path-export.js',
282
288
  ]),
283
289
  'codex-toml': new Set([
284
290
  'gsd-check-update.js',
@@ -613,8 +619,20 @@ function execGit(args, opts = {}) {
613
619
  // Non-interactive defaults: a hung credential prompt or terminal-input
614
620
  // probe must surface as a timeout, not block the tool forever. Callers
615
621
  // can override via opts.env.
622
+ //
623
+ // #5048: readOnlyGitEnv() as well. execGit is the shared seam behind every
624
+ // git call the tool makes, including the read-only ones (`status`,
625
+ // `diff --cached`, `rev-parse`, `log`, …). By default git refreshes the index
626
+ // and takes an *optional* `.git/index.lock` to write the refreshed copy back,
627
+ // so a read can lose a race against a real `git add` / `git commit` and fail
628
+ // it with `Unable to create '.git/index.lock': File exists`. The owner is
629
+ // src/read-only-git-env.cts, shared with every other read-only seam, so this
630
+ // call site cannot drift from them. The variable disables only those optional
631
+ // index operations; a caller that genuinely needs one (a writing command, or a
632
+ // status whose freshness it wants persisted) can still set it back through
633
+ // opts.env, which is spread last on purpose.
616
634
  const env = {
617
- ...process.env,
635
+ ...(0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
618
636
  GIT_TERMINAL_PROMPT: '0',
619
637
  GCM_INTERACTIVE: 'never',
620
638
  ...(opts.env || {}),
@@ -1053,16 +1071,40 @@ function leadingFrontmatterLineCount(text) {
1053
1071
  // The lines before the closing fence, plus the closing fence line itself.
1054
1072
  return text.slice(0, fence.closingStart).split('\n').length;
1055
1073
  }
1056
- function _normalizeMd(content) {
1074
+ /** A leading BOM is not part of the first line's content when aligning two documents. */
1075
+ function withoutBom(line) {
1076
+ return line !== undefined && line.charCodeAt(0) === 0xFEFF ? line.slice(1) : (line ?? '');
1077
+ }
1078
+ function _normalizeMd(content, prior) {
1057
1079
  if (!content || typeof content !== 'string')
1058
1080
  return content;
1059
1081
  let text = content.replace(/\r\n/g, '\n');
1060
1082
  const lines = text.split('\n');
1083
+ // #5165: when the document being replaced is known, only the junctions the
1084
+ // write touched are normalized; every other region stays byte-identical.
1085
+ let scope = null;
1086
+ let priorLines = [];
1087
+ if (typeof prior === 'string' && prior !== '') {
1088
+ priorLines = prior.replace(/\r\n/g, '\n').split('\n');
1089
+ // A BOM on either first line must not make an unchanged line look rewritten.
1090
+ const alignPrior = [withoutBom(priorLines[0]), ...priorLines.slice(1)];
1091
+ const alignNext = [withoutBom(lines[0]), ...lines.slice(1)];
1092
+ const match = (0, markdown_line_diff_cjs_1.matchLines)(alignPrior, alignNext);
1093
+ if (match)
1094
+ scope = (0, markdown_line_diff_cjs_1.writeScope)(match, priorLines.length);
1095
+ }
1061
1096
  // The frontmatter block is YAML, not markdown: its lines are published exactly as written.
1062
1097
  // A `# comment` is not a heading, and a blank line inserted into a multi-line quoted scalar
1063
1098
  // or removed from a block scalar changes the value (found while implementing #5105).
1064
1099
  const frontmatterLines = leadingFrontmatterLineCount(text);
1065
1100
  const result = [];
1101
+ // A separating blank the rules add. In scoped mode (#5165) two rules that fire on
1102
+ // the same junction add ONE blank: the whole-document pass collapses them in its
1103
+ // final blank-run regex, which scoped mode must not apply to runs the write kept.
1104
+ const blank = () => {
1105
+ if (scope === null || result[result.length - 1] !== '')
1106
+ result.push('');
1107
+ };
1066
1108
  const fenceRegex = /^```/;
1067
1109
  const insideFence = new Array(lines.length);
1068
1110
  let fenceOpen = false;
@@ -1095,11 +1137,23 @@ function _normalizeMd(content) {
1095
1137
  const prevTrimmed = prev.trimEnd();
1096
1138
  const trimmed = line.trimEnd();
1097
1139
  const isFenceLine = fenceRegex.test(trimmed);
1098
- if (/^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---')
1099
- result.push('');
1100
- if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine)) {
1140
+ // A heading or fence line the write authored brings its own separation, even
1141
+ // where it replaced a prose line in place (#5165). One that replaced a heading
1142
+ // or fence (a rename, a renumber) inherits the spacing around it instead.
1143
+ const isStructural = (text) => /^#{1,6}\s/.test(text.trimEnd()) || fenceRegex.test(text.trimEnd());
1144
+ const replacedPrior = scope !== null ? scope.replacedPriorIndex(i) : -1;
1145
+ const authoredStructure = scope !== null && scope.lineWritten(i) && isStructural(line)
1146
+ && !(replacedPrior >= 0 && isStructural(priorLines[replacedPrior] ?? ''));
1147
+ const keptBefore = scope !== null && !authoredStructure && i > 0 && scope.junctionKept(i - 1);
1148
+ const keptAfter = scope !== null && !authoredStructure && i < lines.length - 1 && scope.junctionKept(i);
1149
+ // A blank-line run is collapsed only where the write made it (#5165).
1150
+ if (scope !== null && line === '' && result.length > 0 && result[result.length - 1] === '' && !keptBefore)
1151
+ continue;
1152
+ if (/^#{1,6}\s/.test(trimmed) && i > 0 && prevTrimmed !== '' && prevTrimmed !== '---' && !keptBefore)
1153
+ blank();
1154
+ if (isFenceLine && i > 0 && prevTrimmed !== '' && !insideFence[i] && (i === 0 || !insideFence[i - 1] || isFenceLine) && !keptBefore) {
1101
1155
  if (i === 0 || !insideFence[i - 1])
1102
- result.push('');
1156
+ blank();
1103
1157
  }
1104
1158
  // No "separate a list from a preceding paragraph" rule (#3854, #4725).
1105
1159
  // This pass re-normalizes the ENTIRE document on every .md write, so an
@@ -1111,30 +1165,32 @@ function _normalizeMd(content) {
1111
1165
  // from the after-heading rule below. The after-a-bullet rule at the end
1112
1166
  // of this loop is a different transition (list→prose) and is unaffected.
1113
1167
  result.push(line);
1114
- if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '')
1115
- result.push('');
1116
- if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '')
1117
- result.push('');
1118
- if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1) {
1168
+ if (/^#{1,6}\s/.test(trimmed) && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
1169
+ blank();
1170
+ if (/^```\s*$/.test(trimmed) && i > 0 && insideFence[i - 1] && i < lines.length - 1 && (lines[i + 1] ?? '').trimEnd() !== '' && !keptAfter)
1171
+ blank();
1172
+ if (/^(\s*[-*+]\s|\s*\d+\.\s)/.test(line) && i < lines.length - 1 && !keptAfter) {
1119
1173
  const next = lines[i + 1];
1120
1174
  if (next !== undefined && next.trimEnd() !== '' && !/^(\s*[-*+]\s|\s*\d+\.\s)/.test(next) && !/^\s/.test(next))
1121
- result.push('');
1175
+ blank();
1122
1176
  }
1123
1177
  }
1124
1178
  // The frontmatter lines are `result`'s first entries, unchanged; the blank-run collapse
1125
1179
  // applies from the line ending that closes the block onward.
1126
1180
  const head = result.slice(0, frontmatterLines).join('\n');
1127
1181
  text = result.join('\n');
1128
- text = head + text.slice(head.length).replace(/\n{3,}/g, '\n\n');
1182
+ text = scope === null ? head + text.slice(head.length).replace(/\n{3,}/g, '\n\n') : text;
1129
1183
  text = text.replace(/\n*$/, '\n');
1130
1184
  return text;
1131
1185
  }
1132
1186
  function normalizeContent(filePath, content, opts = {}) {
1133
1187
  const encoding = opts.encoding ?? 'utf-8';
1188
+ if (opts.verbatim === true)
1189
+ return { content: content ?? '', encoding };
1134
1190
  const isMd = node_path_1.default.extname(filePath).toLowerCase() === '.md';
1135
1191
  let normalized;
1136
1192
  if (isMd) {
1137
- normalized = _normalizeMd(content);
1193
+ normalized = _normalizeMd(content, opts.prior);
1138
1194
  }
1139
1195
  else {
1140
1196
  normalized = (content ?? '').replace(/\r\n/g, '\n').replace(/\n*$/, '\n');
@@ -1156,7 +1212,7 @@ function normalizeContent(filePath, content, opts = {}) {
1156
1212
  * do not simplify this back to a direct string comparison.
1157
1213
  */
1158
1214
  function contentChangedAfterNormalize(filePath, before, after) {
1159
- return normalizeContent(filePath, after).content !== normalizeContent(filePath, before).content;
1215
+ return normalizeContent(filePath, after, { prior: before }).content !== normalizeContent(filePath, before, { prior: before }).content;
1160
1216
  }
1161
1217
  // Rename errnos that are transient on Windows: a concurrent reader (or an AV
1162
1218
  // scanner / indexer) holding the target open makes renameSync fail briefly.
@@ -1208,8 +1264,59 @@ function retryRenameSync(fromPath, toPath) {
1208
1264
  if (err !== null)
1209
1265
  throw err;
1210
1266
  }
1267
+ /**
1268
+ * Largest on-disk Markdown document read back as the `prior` of a scoped write.
1269
+ * A larger file is normalized whole (the pre-#5165 behavior) rather than read and
1270
+ * diffed on every write.
1271
+ */
1272
+ const MAX_PRIOR_BYTES = 2 * 1024 * 1024;
1273
+ /**
1274
+ * The Markdown document a write replaces, or undefined when there is none worth
1275
+ * diffing against: no file, not a regular file (a FIFO or device reports size 0
1276
+ * and would block or never end), unreadable, or larger than MAX_PRIOR_BYTES. The
1277
+ * bound is enforced on the bytes actually read from one open descriptor, so a
1278
+ * file that grows after the check cannot exceed it.
1279
+ */
1280
+ function readPriorMarkdown(filePath, encoding) {
1281
+ let fd;
1282
+ try {
1283
+ // O_NONBLOCK keeps opening a FIFO from waiting for a writer; it is absent on Windows.
1284
+ fd = node_fs_1.default.openSync(filePath, node_fs_1.default.constants.O_RDONLY | (node_fs_1.default.constants.O_NONBLOCK ?? 0));
1285
+ const stat = node_fs_1.default.fstatSync(fd);
1286
+ if (!stat.isFile() || stat.size > MAX_PRIOR_BYTES)
1287
+ return undefined;
1288
+ // One byte of headroom: reading it means the file grew past what was measured.
1289
+ const buffer = Buffer.alloc(stat.size + 1);
1290
+ let length = 0;
1291
+ while (length < buffer.length) {
1292
+ const read = node_fs_1.default.readSync(fd, buffer, length, buffer.length - length, null);
1293
+ if (read === 0)
1294
+ break;
1295
+ length += read;
1296
+ }
1297
+ if (length > stat.size)
1298
+ return undefined;
1299
+ return buffer.toString(encoding, 0, length);
1300
+ }
1301
+ catch {
1302
+ return undefined; // no prior document (new file or unreadable): normalize it whole
1303
+ }
1304
+ finally {
1305
+ if (fd !== undefined) {
1306
+ try {
1307
+ node_fs_1.default.closeSync(fd);
1308
+ }
1309
+ catch { /* already closed */ }
1310
+ }
1311
+ }
1312
+ }
1211
1313
  function platformWriteSync(filePath, content, opts = {}) {
1212
- const { content: normalized, encoding } = normalizeContent(filePath, content, opts);
1314
+ // #5165: a Markdown write is scoped to what it changed relative to the file it replaces.
1315
+ let prior = opts.prior;
1316
+ if (prior === undefined && opts.verbatim !== true && node_path_1.default.extname(filePath).toLowerCase() === '.md') {
1317
+ prior = readPriorMarkdown(filePath, opts.encoding ?? 'utf-8');
1318
+ }
1319
+ const { content: normalized, encoding } = normalizeContent(filePath, content, { ...opts, prior });
1213
1320
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(filePath), { recursive: true });
1214
1321
  const tmpPath = filePath + '.tmp.' + process.pid;
1215
1322
  // Step 1: write the sibling tmp file. If THIS fails, nothing was published, so a
@@ -43,6 +43,7 @@ const node_fs_1 = __importDefault(require("node:fs"));
43
43
  const node_path_1 = __importDefault(require("node:path"));
44
44
  const node_child_process_1 = require("node:child_process");
45
45
  const markdown_sectionizer_cjs_1 = require("./markdown-sectionizer.cjs");
46
+ const read_only_git_env_cjs_1 = require("./read-only-git-env.cjs");
46
47
  const runtime_slash_cjs_1 = require("./runtime-slash.cjs");
47
48
  // #3696: the calendar-validity predicate moved to the STATE.md document module
48
49
  // so `state validate` can assert the same `last_activity` invariant this reader
@@ -222,6 +223,18 @@ function readGitSignals(cwd) {
222
223
  windowsHide: true,
223
224
  stdio: ['pipe', 'pipe', 'pipe'],
224
225
  timeout: 10_000,
226
+ // #5048: readGitSignals runs `git status`, which by default refreshes
227
+ // the index and takes an *optional* `.git/index.lock` to write the
228
+ // refreshed copy back. That lock is the one a real `git add` / `git
229
+ // commit` needs, so a routine "which phase am I in" probe can fail
230
+ // someone else's commit with `Unable to create '.git/index.lock': File
231
+ // exists`. GIT_OPTIONAL_LOCKS=0 turns off exactly that optional write.
232
+ // The `rev-parse` / `log` calls sharing this helper never open the
233
+ // index, so the variable is inert for them. The block itself is owned by
234
+ // src/read-only-git-env.cts and shared with every other read-only seam;
235
+ // parity across every read-only surface is asserted in
236
+ // tests/git-optional-locks-parity.test.cjs.
237
+ env: (0, read_only_git_env_cjs_1.readOnlyGitEnv)(),
225
238
  });
226
239
  }
227
240
  catch {
@@ -675,6 +688,28 @@ function actionsFor(situation, s, runtime = 'claude') {
675
688
  }
676
689
  }
677
690
  // ─── Summary line ─────────────────────────────────────────────────────────────
691
+ function phaseSummary(s) {
692
+ const phase = s.current_phase;
693
+ // Preserve the milestone ordinal introduced by #5228 when it is known.
694
+ const at = phase === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, phase) === 0);
695
+ if (at >= 0)
696
+ return `Phase ${at + 1} of ${s.milestone_phases.length}`;
697
+ const total = s.total_phases;
698
+ const legacy = `Phase ${phase ?? '?'} of ${total ?? '?'}`;
699
+ const roadmapTotal = s.roadmap_total_phases;
700
+ // Keep the recorded pair when roadmap counts are unavailable, or when the
701
+ // pair can describe a single milestone without changing its existing text.
702
+ if (phase === null || total === null || roadmapTotal === null || roadmapTotal <= 0
703
+ || (total === roadmapTotal && comparePhaseNum(phase, total) <= 0)) {
704
+ return legacy;
705
+ }
706
+ // When the recorded pair cannot safely describe this roadmap, label its
707
+ // counts separately rather than printing a mixed-scope ratio.
708
+ const roadmapDone = s.roadmap_completed_phases;
709
+ return roadmapDone === null
710
+ ? `Phase ${phase} (${roadmapTotal} roadmap phases)`
711
+ : `Phase ${phase} (${roadmapDone} of ${roadmapTotal} roadmap phases complete)`;
712
+ }
678
713
  function buildSummary(situation, s) {
679
714
  switch (situation) {
680
715
  case 'no-project':
@@ -688,7 +723,7 @@ function buildSummary(situation, s) {
688
723
  case 'needs-first-phase':
689
724
  return 'Project initialized — plan your first phase';
690
725
  case 'planning':
691
- return `${phasePosition(s)} — needs a plan`;
726
+ return `${phaseSummary(s)} — needs a plan`;
692
727
  case 'executing':
693
728
  return progressLine('executing', s);
694
729
  case 'verify-pending':
@@ -702,24 +737,10 @@ function buildSummary(situation, s) {
702
737
  return 'Unsure of state — showing progress';
703
738
  }
704
739
  }
705
- /**
706
- * "Phase N of M" for the current phase. N is the phase's 1-based position in
707
- * the current milestone's ordered phase list (#5228), so a milestone that
708
- * continues the numbering of an earlier one (phases 9-17) reads "Phase 1 of 9",
709
- * not "Phase 9 of 9". When the phase is not in that list (or the roadmap lists
710
- * none) the STATE.md figures are printed as recorded.
711
- */
712
- function phasePosition(s) {
713
- const current = s.current_phase;
714
- const at = current === null ? -1 : s.milestone_phases.findIndex((id) => comparePhaseNum(id, current) === 0);
715
- if (at >= 0)
716
- return `Phase ${at + 1} of ${s.milestone_phases.length}`;
717
- return `Phase ${s.current_phase ?? '?'} of ${s.total_phases ?? '?'}`;
718
- }
719
740
  function progressLine(tail, s) {
720
741
  const parts = [];
721
742
  if (s.current_phase !== null && s.total_phases !== null) {
722
- parts.push(phasePosition(s));
743
+ parts.push(phaseSummary(s));
723
744
  }
724
745
  else if (s.current_phase !== null) {
725
746
  parts.push(`Phase ${s.current_phase}`);
@@ -129,6 +129,7 @@ function owners() {
129
129
  locateProgressTable: phaseLifecycleMod.locateProgressTable,
130
130
  isSentinelPhaseId: phaseIdMod.isSentinelPhaseId,
131
131
  parsePhaseFromProse: phaseIdMod.parsePhaseFromProse,
132
+ resolvePhaseIdConvention: planningWorkspaceMod.resolvePhaseIdConvention,
132
133
  getMilestoneInfo: roadmapParserMod.getMilestoneInfo,
133
134
  collectSection: markdownSectionizerMod.collectSection,
134
135
  stateFieldValue: stateDocumentMod.stateFieldValue,
@@ -154,16 +155,31 @@ function buildMilestone(cwd) {
154
155
  }
155
156
  // ─── phases: Progress-table (primary) enumerator ───────────────────────────
156
157
  const PHASE_CELL_RE = /^(\d+(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
157
- function phasesFromProgressTable(table) {
158
+ // #5626: under `milestone-prefixed` the phase id is the whole `M-NN` (`3-01`), so the number capture takes
159
+ // one optional `-NN` segment. Chosen by the convention value alone: every other convention keeps the
160
+ // dotted-only capture above, and the `\d+-\d+` shape stays an ordinary leading integer there.
161
+ const MILESTONE_PREFIXED = 'milestone-prefixed';
162
+ const PHASE_CELL_MILESTONE_RE = /^(\d+(?:-\d+)?(?:\.\d+)*)\s*[.:)–—-]?\s*(.*)$/;
163
+ /** The project's `phase_id_convention` (null when unset or unreadable), through its one owner. */
164
+ function phaseIdConventionOf(cwd) {
165
+ try {
166
+ return owners().resolvePhaseIdConvention(cwd);
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ }
172
+ function phasesFromProgressTable(table, convention) {
158
173
  const { isSentinelPhaseId } = owners();
159
174
  const phases = [];
175
+ const cellRe = convention === MILESTONE_PREFIXED ? PHASE_CELL_MILESTONE_RE : PHASE_CELL_RE;
160
176
  for (const row of table.rows) {
161
177
  const cell = (row['Phase'] ?? '').trim();
162
178
  if (!/^\d/.test(cell))
163
179
  continue; // stray note row, no leading digit
164
180
  if (isSentinelPhaseId(cell))
165
181
  continue; // phase 0 / 999.x sentinels
166
- const m = PHASE_CELL_RE.exec(cell);
182
+ const m = cellRe.exec(cell);
167
183
  const number = m ? m[1] : cell;
168
184
  const name = m && m[2].trim().length > 0 ? m[2].trim() : null;
169
185
  phases.push({ number, name, status: (0, phase_status_cjs_1.parseRoadmapStatusCell)(row['Status']) });
@@ -173,6 +189,8 @@ function phasesFromProgressTable(table) {
173
189
  // ─── phases: `## Phases` checkbox fallback ─────────────────────────────────
174
190
  // `- [x] **Phase 1: Foundation**` / `- [ ] **Phase 2.1: Hardening**`.
175
191
  const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
192
+ // #5626: the same bullet under `milestone-prefixed`: the number capture takes one optional `-NN` segment.
193
+ const PHASE_BULLET_MILESTONE_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?:-\d+)?(?:\.\d+)*)[ \t]*:?[ \t]*([^*\n]*)\*\*/gm;
176
194
  /**
177
195
  * STATE.md's current phase, via the same frontmatter-then-body chain
178
196
  * `cmdStateCompletePhase` uses (`stateFieldValue` owns the #1760 fallback
@@ -180,7 +198,7 @@ const PHASE_BULLET_RE = /^[ \t]*[-*][ \t]+\[([ xX])\][ \t]+\*\*Phase[ \t]+(\d+(?
180
198
  * phase as unknown so every unchecked bullet is PENDING rather than
181
199
  * IN_PROGRESS by a guess.
182
200
  */
183
- function currentPhaseFromState(statePath) {
201
+ function currentPhaseFromState(statePath, convention) {
184
202
  const { platformReadSync, extractFrontmatter, stripFrontmatter, stateFieldValue, parsePhaseFromProse } = owners();
185
203
  try {
186
204
  const raw = platformReadSync(statePath);
@@ -191,7 +209,7 @@ function currentPhaseFromState(statePath) {
191
209
  const { value } = stateFieldValue(fm, body, 'current_phase', 'Current Phase');
192
210
  if (!value)
193
211
  return null;
194
- return parsePhaseFromProse(value).phase;
212
+ return parsePhaseFromProse(value, convention).phase;
195
213
  }
196
214
  catch {
197
215
  return null;
@@ -207,15 +225,16 @@ function currentPhaseFromState(statePath) {
207
225
  * `**Phase N` anchor inside `PHASE_BULLET_RE` is the second layer of that
208
226
  * defence — a plan bullet's text never starts with `**Phase`.
209
227
  */
210
- function phasesFromChecklist(roadmapContent, cwd, statePath) {
228
+ function phasesFromChecklist(roadmapContent, cwd, statePath, convention) {
211
229
  const { collectSection, isSentinelPhaseId } = owners();
212
230
  const section = collectSection(roadmapContent, (h) => /^phases$/i.test(h.text.trim()));
213
231
  if (!section)
214
232
  return [];
215
- const currentPhase = currentPhaseFromState(statePath);
233
+ const currentPhase = currentPhaseFromState(statePath, convention);
216
234
  const phases = [];
217
235
  let match;
218
- const pattern = new RegExp(PHASE_BULLET_RE.source, PHASE_BULLET_RE.flags);
236
+ const bulletRe = convention === MILESTONE_PREFIXED ? PHASE_BULLET_MILESTONE_RE : PHASE_BULLET_RE;
237
+ const pattern = new RegExp(bulletRe.source, bulletRe.flags);
219
238
  while ((match = pattern.exec(section.body)) !== null) {
220
239
  const checked = match[1] === 'x' || match[1] === 'X';
221
240
  const number = match[2];
@@ -237,10 +256,11 @@ function buildPhases(cwd, paths) {
237
256
  return [];
238
257
  if (content.charCodeAt(0) === 0xfeff)
239
258
  content = content.slice(1); // strip leading BOM
259
+ const convention = phaseIdConventionOf(cwd);
240
260
  const table = locateProgressTable(content);
241
261
  if (table)
242
- return phasesFromProgressTable(table);
243
- return phasesFromChecklist(content, cwd, paths.state);
262
+ return phasesFromProgressTable(table, convention);
263
+ return phasesFromChecklist(content, cwd, paths.state, convention);
244
264
  }
245
265
  catch {
246
266
  return [];
@@ -14,6 +14,7 @@ exports.isUnfilledFieldValue = isUnfilledFieldValue;
14
14
  exports.leadingCalendarDate = leadingCalendarDate;
15
15
  exports.isRealCalendarDate = isRealCalendarDate;
16
16
  exports.stateFieldContinuation = stateFieldContinuation;
17
+ exports.fieldContinuationEnd = fieldContinuationEnd;
17
18
  exports.stateExtractField = stateExtractField;
18
19
  exports.stateFieldValue = stateFieldValue;
19
20
  exports.stateCurrentPositionSlice = stateCurrentPositionSlice;
@@ -359,6 +360,8 @@ const STATE_SIBLING_FIELD_LINE_RE = /^\*{0,2}[A-Za-z][A-Za-z0-9 _-]*\*{0,2}:{1,2
359
360
  * cannot wrap), or is followed by end-of-file, a blank line, Markdown
360
361
  * structure, or a sibling field.
361
362
  */
363
+ /** The line terminators the STATE.md readers (JS `m`-flag `^`/`$`) treat as a line boundary. */
364
+ const LINE_SEPARATOR_RE = /[\n\u2028\u2029]/;
362
365
  function stateFieldContinuation(content, fieldName) {
363
366
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);
364
367
  // Same two single-line grammars stateExtractField uses, in the same order, so
@@ -375,10 +378,21 @@ function stateFieldContinuation(content, fieldName) {
375
378
  // before testing for the newline.
376
379
  const afterValue = match.index + match[0].length;
377
380
  const rest = content.slice(afterValue).replace(/^\r/, '');
378
- if (!rest.startsWith('\n'))
381
+ // #5460: U+2028 / U+2029 are line starts to the `m`-flag readers (and stop `(.+)`), so they end
382
+ // the value's line exactly as `\n` does.
383
+ if (!/^[\n\u2028\u2029]/.test(rest))
379
384
  return null; // end of file: nothing follows
380
- const lines = rest.slice(1).split('\n').map((line) => line.replace(/\r$/, ''));
381
- const continuation = [];
385
+ const lines = rest.slice(1).split(LINE_SEPARATOR_RE).map((line) => line.replace(/\r$/, ''));
386
+ const continuation = lines.slice(0, continuationLineCount(lines)).map((line) => line.trim());
387
+ return continuation.length ? continuation.join(' ') : null;
388
+ }
389
+ /**
390
+ * How many of the lines directly below a single-line field belong to its value: the scan
391
+ * `stateFieldContinuation` reports on, shared so a writer that replaces the value removes exactly
392
+ * the lines a validator would call its continuation (#4945). `lines` are CR-free.
393
+ */
394
+ function continuationLineCount(lines) {
395
+ let count = 0;
382
396
  for (let i = 0; i < lines.length; i++) {
383
397
  const line = lines[i];
384
398
  if (!line.trim())
@@ -391,9 +405,37 @@ function stateFieldContinuation(content, fieldName) {
391
405
  // title, so stop before consuming it rather than after.
392
406
  if (i + 1 < lines.length && SETEXT_UNDERLINE_RE.test(lines[i + 1]))
393
407
  break;
394
- continuation.push(line.trim());
408
+ count++;
395
409
  }
396
- return continuation.length ? continuation.join(' ') : null;
410
+ return count;
411
+ }
412
+ /**
413
+ * The index just past a single-line field's value AND the continuation lines that wrap it
414
+ * (#4945). `valueEnd` is where the field's own line ends (before its line terminator). Replacing
415
+ * `content.slice(valueEnd, result)` with nothing removes every continuation line and the
416
+ * terminator in front of each, so the terminator after the last one stays. Returns `valueEnd`
417
+ * when nothing continues the value.
418
+ */
419
+ function fieldContinuationEnd(content, valueEnd) {
420
+ const tail = content.slice(valueEnd);
421
+ // #5460: the readers' `m`-flag `^` also starts a line after U+2028 / U+2029, so a continuation
422
+ // scan that split on `\n` alone swallowed `...tail<U+2028>Stopped at: x` as ONE continuation line
423
+ // and deleted the sibling field with it. Each separator is one UTF-16 unit, like `\n`, so the
424
+ // removal arithmetic below is unchanged.
425
+ const first = /^(?:\r?\n|[\u2028\u2029])/.exec(tail);
426
+ if (!first)
427
+ return valueEnd;
428
+ const rawLines = tail.slice(first[0].length).split(LINE_SEPARATOR_RE);
429
+ const count = continuationLineCount(rawLines.map((line) => line.replace(/\r$/, '')));
430
+ if (count === 0)
431
+ return valueEnd;
432
+ let removed = first[0].length + (count - 1);
433
+ for (let i = 0; i < count; i++)
434
+ removed += rawLines[i].length;
435
+ // The terminator after the last continuation line stays; on a CRLF document that includes its CR.
436
+ if (rawLines[count - 1].endsWith('\r'))
437
+ removed -= 1;
438
+ return valueEnd + removed;
397
439
  }
398
440
  function stateExtractField(content, fieldName) {
399
441
  const escaped = (0, pattern_cjs_1.escapeRegex)(fieldName);