gsd-antigravity-kit 2.0.0 → 2.1.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 (258) hide show
  1. package/.agent/skills/gsd/SKILL.md +26 -4
  2. package/.agent/skills/gsd/VERSION +1 -1
  3. package/.agent/skills/gsd/assets/templates/AI-SPEC.md +246 -0
  4. package/.agent/skills/gsd/assets/templates/DEBUG.md +7 -2
  5. package/.agent/skills/gsd/assets/templates/config.json +56 -48
  6. package/.agent/skills/gsd/assets/templates/research.md +40 -0
  7. package/.agent/skills/gsd/assets/templates/spec.md +307 -0
  8. package/.agent/skills/gsd/assets/templates/state.md +8 -0
  9. package/.agent/skills/gsd/bin/gsd-tools.cjs +212 -11
  10. package/.agent/skills/gsd/bin/help-manifest.json +8 -2
  11. package/.agent/skills/gsd/bin/hooks/gsd-check-update-worker.js +108 -0
  12. package/.agent/skills/gsd/bin/hooks/gsd-check-update.js +14 -89
  13. package/.agent/skills/gsd/bin/hooks/gsd-context-monitor.js +34 -5
  14. package/.agent/skills/gsd/bin/hooks/gsd-phase-boundary.sh +1 -0
  15. package/.agent/skills/gsd/bin/hooks/gsd-prompt-guard.js +1 -1
  16. package/.agent/skills/gsd/bin/hooks/gsd-read-guard.js +6 -1
  17. package/.agent/skills/gsd/bin/hooks/gsd-session-state.sh +1 -0
  18. package/.agent/skills/gsd/bin/hooks/gsd-statusline.js +150 -16
  19. package/.agent/skills/gsd/bin/hooks/gsd-validate-commit.sh +1 -0
  20. package/.agent/skills/gsd/bin/hooks/gsd-workflow-guard.js +1 -1
  21. package/.agent/skills/gsd/bin/lib/audit.cjs +757 -0
  22. package/.agent/skills/gsd/bin/lib/commands.cjs +17 -7
  23. package/.agent/skills/gsd/bin/lib/config.cjs +66 -20
  24. package/.agent/skills/gsd/bin/lib/core.cjs +212 -12
  25. package/.agent/skills/gsd/bin/lib/frontmatter.cjs +6 -8
  26. package/.agent/skills/gsd/bin/lib/graphify.cjs +494 -0
  27. package/.agent/skills/gsd/bin/lib/gsd2-import.cjs +511 -0
  28. package/.agent/skills/gsd/bin/lib/init.cjs +371 -18
  29. package/.agent/skills/gsd/bin/lib/intel.cjs +9 -30
  30. package/.agent/skills/gsd/bin/lib/milestone.cjs +18 -17
  31. package/.agent/skills/gsd/bin/lib/model-profiles.cjs +1 -0
  32. package/.agent/skills/gsd/bin/lib/phase.cjs +225 -98
  33. package/.agent/skills/gsd/bin/lib/profile-output.cjs +17 -5
  34. package/.agent/skills/gsd/bin/lib/roadmap.cjs +12 -5
  35. package/.agent/skills/gsd/bin/lib/state.cjs +394 -129
  36. package/.agent/skills/gsd/bin/lib/template.cjs +8 -4
  37. package/.agent/skills/gsd/bin/lib/uat.cjs +2 -1
  38. package/.agent/skills/gsd/bin/lib/verify.cjs +111 -42
  39. package/.agent/skills/gsd/migration_report.md +2 -2
  40. package/.agent/skills/gsd/references/agents/gsd-advisor-researcher.md +23 -0
  41. package/.agent/skills/gsd/references/agents/gsd-ai-researcher.md +133 -0
  42. package/.agent/skills/gsd/references/agents/gsd-code-fixer.md +11 -10
  43. package/.agent/skills/gsd/references/agents/gsd-code-reviewer.md +2 -2
  44. package/.agent/skills/gsd/references/agents/gsd-codebase-mapper.md +13 -2
  45. package/.agent/skills/gsd/references/agents/gsd-debug-session-manager.md +314 -0
  46. package/.agent/skills/gsd/references/agents/gsd-debugger.md +147 -76
  47. package/.agent/skills/gsd/references/agents/gsd-doc-verifier.md +1 -1
  48. package/.agent/skills/gsd/references/agents/gsd-doc-writer.md +615 -602
  49. package/.agent/skills/gsd/references/agents/gsd-domain-researcher.md +153 -0
  50. package/.agent/skills/gsd/references/agents/gsd-eval-auditor.md +175 -0
  51. package/.agent/skills/gsd/references/agents/gsd-eval-planner.md +154 -0
  52. package/.agent/skills/gsd/references/agents/gsd-executor.md +108 -38
  53. package/.agent/skills/gsd/references/agents/gsd-framework-selector.md +160 -0
  54. package/.agent/skills/gsd/references/agents/gsd-integration-checker.md +454 -443
  55. package/.agent/skills/gsd/references/agents/gsd-intel-updater.md +40 -20
  56. package/.agent/skills/gsd/references/agents/gsd-nyquist-auditor.md +187 -176
  57. package/.agent/skills/gsd/references/agents/gsd-pattern-mapper.md +335 -0
  58. package/.agent/skills/gsd/references/agents/gsd-phase-researcher.md +112 -13
  59. package/.agent/skills/gsd/references/agents/gsd-plan-checker.md +104 -10
  60. package/.agent/skills/gsd/references/agents/gsd-planner.md +125 -167
  61. package/.agent/skills/gsd/references/agents/gsd-project-researcher.md +25 -2
  62. package/.agent/skills/gsd/references/agents/gsd-research-synthesizer.md +3 -3
  63. package/.agent/skills/gsd/references/agents/gsd-roadmapper.md +12 -1
  64. package/.agent/skills/gsd/references/agents/gsd-security-auditor.md +139 -128
  65. package/.agent/skills/gsd/references/agents/gsd-ui-auditor.md +3 -3
  66. package/.agent/skills/gsd/references/agents/gsd-ui-checker.md +11 -2
  67. package/.agent/skills/gsd/references/agents/gsd-ui-researcher.md +27 -4
  68. package/.agent/skills/gsd/references/agents/gsd-verifier.md +13 -19
  69. package/.agent/skills/gsd/references/commands/atomic/add-todo.md +2 -2
  70. package/.agent/skills/gsd/references/commands/atomic/check-todos.md +2 -2
  71. package/.agent/skills/gsd/references/commands/atomic/cleanup.md +2 -2
  72. package/.agent/skills/gsd/references/commands/atomic/do.md +2 -2
  73. package/.agent/skills/gsd/references/commands/atomic/help.md +2 -2
  74. package/.agent/skills/gsd/references/commands/atomic/join-discord.md +2 -2
  75. package/.agent/skills/gsd/references/commands/atomic/note.md +2 -2
  76. package/.agent/skills/gsd/references/commands/atomic/session-report.md +2 -2
  77. package/.agent/skills/gsd/references/commands/atomic/ship.md +2 -2
  78. package/.agent/skills/gsd/references/commands/atomic/stats.md +2 -2
  79. package/.agent/skills/gsd/references/commands/atomic/thread.md +141 -41
  80. package/.agent/skills/gsd/references/commands/atomic/undo.md +2 -2
  81. package/.agent/skills/gsd/references/commands/milestone/add-backlog.md +15 -12
  82. package/.agent/skills/gsd/references/commands/milestone/audit-milestone.md +2 -2
  83. package/.agent/skills/gsd/references/commands/milestone/complete-milestone.md +2 -2
  84. package/.agent/skills/gsd/references/commands/milestone/milestone-summary.md +2 -2
  85. package/.agent/skills/gsd/references/commands/milestone/new-milestone.md +2 -2
  86. package/.agent/skills/gsd/references/commands/milestone/plan-milestone-gaps.md +2 -2
  87. package/.agent/skills/gsd/references/commands/milestone/plant-seed.md +2 -2
  88. package/.agent/skills/gsd/references/commands/milestone/review-backlog.md +4 -4
  89. package/.agent/skills/gsd/references/commands/misc/ai-integration-phase.md +38 -0
  90. package/.agent/skills/gsd/references/commands/misc/audit-fix.md +2 -2
  91. package/.agent/skills/gsd/references/commands/misc/audit-uat.md +2 -2
  92. package/.agent/skills/gsd/references/commands/misc/eval-review.md +34 -0
  93. package/.agent/skills/gsd/references/commands/misc/extract_learnings.md +24 -0
  94. package/.agent/skills/gsd/references/commands/misc/from-gsd2.md +49 -0
  95. package/.agent/skills/gsd/references/commands/misc/graphify.md +203 -0
  96. package/.agent/skills/gsd/references/commands/misc/inbox.md +40 -0
  97. package/.agent/skills/gsd/references/commands/misc/next.md +5 -3
  98. package/.agent/skills/gsd/references/commands/misc/progress.md +4 -3
  99. package/.agent/skills/gsd/references/commands/misc/sketch-wrap-up.md +33 -0
  100. package/.agent/skills/gsd/references/commands/misc/sketch.md +47 -0
  101. package/.agent/skills/gsd/references/commands/misc/spec-phase.md +64 -0
  102. package/.agent/skills/gsd/references/commands/misc/spike-wrap-up.md +33 -0
  103. package/.agent/skills/gsd/references/commands/misc/spike.md +43 -0
  104. package/.agent/skills/gsd/references/commands/misc/verify-work.md +2 -2
  105. package/.agent/skills/gsd/references/commands/phase/add-phase.md +2 -2
  106. package/.agent/skills/gsd/references/commands/phase/add-tests.md +2 -2
  107. package/.agent/skills/gsd/references/commands/phase/discuss-phase.md +5 -5
  108. package/.agent/skills/gsd/references/commands/phase/execute-phase.md +4 -4
  109. package/.agent/skills/gsd/references/commands/phase/insert-phase.md +2 -2
  110. package/.agent/skills/gsd/references/commands/phase/list-phase-assumptions.md +2 -2
  111. package/.agent/skills/gsd/references/commands/phase/plan-phase.md +3 -3
  112. package/.agent/skills/gsd/references/commands/phase/remove-phase.md +2 -2
  113. package/.agent/skills/gsd/references/commands/phase/research-phase.md +5 -5
  114. package/.agent/skills/gsd/references/commands/phase/secure-phase.md +2 -2
  115. package/.agent/skills/gsd/references/commands/phase/ui-phase.md +2 -2
  116. package/.agent/skills/gsd/references/commands/phase/ui-review.md +2 -2
  117. package/.agent/skills/gsd/references/commands/phase/validate-phase.md +2 -2
  118. package/.agent/skills/gsd/references/commands/phase/workstreams.md +9 -9
  119. package/.agent/skills/gsd/references/commands/project/analyze-dependencies.md +2 -2
  120. package/.agent/skills/gsd/references/commands/project/explore.md +2 -2
  121. package/.agent/skills/gsd/references/commands/project/import.md +2 -2
  122. package/.agent/skills/gsd/references/commands/project/intel.md +10 -10
  123. package/.agent/skills/gsd/references/commands/project/list-workspaces.md +2 -2
  124. package/.agent/skills/gsd/references/commands/project/map-codebase.md +2 -2
  125. package/.agent/skills/gsd/references/commands/project/new-project.md +2 -2
  126. package/.agent/skills/gsd/references/commands/project/new-workspace.md +2 -2
  127. package/.agent/skills/gsd/references/commands/project/remove-workspace.md +2 -2
  128. package/.agent/skills/gsd/references/commands/project/scan.md +2 -2
  129. package/.agent/skills/gsd/references/commands/system/autonomous.md +4 -3
  130. package/.agent/skills/gsd/references/commands/system/code-review-fix.md +3 -3
  131. package/.agent/skills/gsd/references/commands/system/code-review.md +3 -3
  132. package/.agent/skills/gsd/references/commands/system/debug.md +177 -100
  133. package/.agent/skills/gsd/references/commands/system/docs-update.md +2 -2
  134. package/.agent/skills/gsd/references/commands/system/fast.md +2 -2
  135. package/.agent/skills/gsd/references/commands/system/forensics.md +2 -2
  136. package/.agent/skills/gsd/references/commands/system/gsd-tools.md +153 -6
  137. package/.agent/skills/gsd/references/commands/system/health.md +2 -2
  138. package/.agent/skills/gsd/references/commands/system/manager.md +3 -3
  139. package/.agent/skills/gsd/references/commands/system/pause-work.md +2 -2
  140. package/.agent/skills/gsd/references/commands/system/pr-branch.md +2 -2
  141. package/.agent/skills/gsd/references/commands/system/profile-user.md +2 -2
  142. package/.agent/skills/gsd/references/commands/system/quick.md +127 -3
  143. package/.agent/skills/gsd/references/commands/system/reapply-patches.md +45 -6
  144. package/.agent/skills/gsd/references/commands/system/resume-work.md +2 -2
  145. package/.agent/skills/gsd/references/commands/system/review.md +6 -4
  146. package/.agent/skills/gsd/references/commands/system/set-profile.md +3 -3
  147. package/.agent/skills/gsd/references/commands/system/settings.md +2 -2
  148. package/.agent/skills/gsd/references/commands/system/update.md +2 -2
  149. package/.agent/skills/gsd/references/docs/ai-evals.md +156 -0
  150. package/.agent/skills/gsd/references/docs/ai-frameworks.md +186 -0
  151. package/.agent/skills/gsd/references/docs/artifact-types.md +18 -0
  152. package/.agent/skills/gsd/references/docs/autonomous-smart-discuss.md +277 -0
  153. package/.agent/skills/gsd/references/docs/checkpoints.md +30 -0
  154. package/.agent/skills/gsd/references/docs/common-bug-patterns.md +49 -49
  155. package/.agent/skills/gsd/references/docs/continuation-format.md +11 -7
  156. package/.agent/skills/gsd/references/docs/debugger-philosophy.md +76 -0
  157. package/.agent/skills/gsd/references/docs/decimal-phase-calculation.md +64 -64
  158. package/.agent/skills/gsd/references/docs/executor-examples.md +110 -0
  159. package/.agent/skills/gsd/references/docs/git-integration.md +4 -4
  160. package/.agent/skills/gsd/references/docs/git-planning-commit.md +40 -38
  161. package/.agent/skills/gsd/references/docs/ios-scaffold.md +123 -0
  162. package/.agent/skills/gsd/references/docs/mandatory-initial-read.md +2 -0
  163. package/.agent/skills/gsd/references/docs/phase-argument-parsing.md +61 -61
  164. package/.agent/skills/gsd/references/docs/planner-antipatterns.md +89 -0
  165. package/.agent/skills/gsd/references/docs/planner-revision.md +87 -87
  166. package/.agent/skills/gsd/references/docs/planner-source-audit.md +73 -0
  167. package/.agent/skills/gsd/references/docs/planning-config.md +33 -8
  168. package/.agent/skills/gsd/references/docs/project-skills-discovery.md +19 -0
  169. package/.agent/skills/gsd/references/docs/sketch-interactivity.md +41 -0
  170. package/.agent/skills/gsd/references/docs/sketch-theme-system.md +94 -0
  171. package/.agent/skills/gsd/references/docs/sketch-tooling.md +45 -0
  172. package/.agent/skills/gsd/references/docs/sketch-variant-patterns.md +81 -0
  173. package/.agent/skills/gsd/references/docs/tdd.md +67 -0
  174. package/.agent/skills/gsd/references/docs/universal-anti-patterns.md +5 -0
  175. package/.agent/skills/gsd/references/docs/workstream-flag.md +11 -11
  176. package/.agent/skills/gsd/references/mapping.md +1 -1
  177. package/.agent/skills/gsd/references/workflows/add-phase.md +112 -112
  178. package/.agent/skills/gsd/references/workflows/add-tests.md +6 -3
  179. package/.agent/skills/gsd/references/workflows/add-todo.md +5 -3
  180. package/.agent/skills/gsd/references/workflows/ai-integration-phase.md +284 -0
  181. package/.agent/skills/gsd/references/workflows/audit-fix.md +157 -157
  182. package/.agent/skills/gsd/references/workflows/audit-milestone.md +340 -340
  183. package/.agent/skills/gsd/references/workflows/audit-uat.md +109 -109
  184. package/.agent/skills/gsd/references/workflows/autonomous.md +20 -288
  185. package/.agent/skills/gsd/references/workflows/check-todos.md +4 -2
  186. package/.agent/skills/gsd/references/workflows/cleanup.md +3 -1
  187. package/.agent/skills/gsd/references/workflows/code-review-fix.md +497 -497
  188. package/.agent/skills/gsd/references/workflows/code-review.md +515 -515
  189. package/.agent/skills/gsd/references/workflows/complete-milestone.md +97 -24
  190. package/.agent/skills/gsd/references/workflows/diagnose-issues.md +238 -238
  191. package/.agent/skills/gsd/references/workflows/discovery-phase.md +2 -0
  192. package/.agent/skills/gsd/references/workflows/discuss-phase-assumptions.md +11 -11
  193. package/.agent/skills/gsd/references/workflows/discuss-phase.md +143 -19
  194. package/.agent/skills/gsd/references/workflows/do.md +8 -2
  195. package/.agent/skills/gsd/references/workflows/docs-update.md +5 -3
  196. package/.agent/skills/gsd/references/workflows/eval-review.md +155 -0
  197. package/.agent/skills/gsd/references/workflows/execute-phase.md +338 -54
  198. package/.agent/skills/gsd/references/workflows/execute-plan.md +80 -104
  199. package/.agent/skills/gsd/references/workflows/explore.md +3 -1
  200. package/.agent/skills/gsd/references/workflows/extract_learnings.md +232 -0
  201. package/.agent/skills/gsd/references/workflows/forensics.md +3 -3
  202. package/.agent/skills/gsd/references/workflows/health.md +2 -2
  203. package/.agent/skills/gsd/references/workflows/help.md +59 -1
  204. package/.agent/skills/gsd/references/workflows/import.md +3 -1
  205. package/.agent/skills/gsd/references/workflows/inbox.md +387 -384
  206. package/.agent/skills/gsd/references/workflows/insert-phase.md +130 -130
  207. package/.agent/skills/gsd/references/workflows/list-workspaces.md +56 -56
  208. package/.agent/skills/gsd/references/workflows/manager.md +5 -3
  209. package/.agent/skills/gsd/references/workflows/map-codebase.md +19 -5
  210. package/.agent/skills/gsd/references/workflows/milestone-summary.md +6 -6
  211. package/.agent/skills/gsd/references/workflows/new-milestone.md +63 -9
  212. package/.agent/skills/gsd/references/workflows/new-project.md +126 -22
  213. package/.agent/skills/gsd/references/workflows/new-workspace.md +6 -4
  214. package/.agent/skills/gsd/references/workflows/next.md +220 -153
  215. package/.agent/skills/gsd/references/workflows/note.md +2 -0
  216. package/.agent/skills/gsd/references/workflows/pause-work.md +11 -7
  217. package/.agent/skills/gsd/references/workflows/plan-milestone-gaps.md +273 -273
  218. package/.agent/skills/gsd/references/workflows/plan-phase.md +281 -62
  219. package/.agent/skills/gsd/references/workflows/plant-seed.md +4 -1
  220. package/.agent/skills/gsd/references/workflows/pr-branch.md +41 -13
  221. package/.agent/skills/gsd/references/workflows/profile-user.md +15 -13
  222. package/.agent/skills/gsd/references/workflows/progress.md +133 -21
  223. package/.agent/skills/gsd/references/workflows/quick.md +67 -27
  224. package/.agent/skills/gsd/references/workflows/remove-phase.md +155 -155
  225. package/.agent/skills/gsd/references/workflows/remove-workspace.md +4 -2
  226. package/.agent/skills/gsd/references/workflows/research-phase.md +3 -3
  227. package/.agent/skills/gsd/references/workflows/resume-project.md +3 -3
  228. package/.agent/skills/gsd/references/workflows/review.md +71 -8
  229. package/.agent/skills/gsd/references/workflows/scan.md +102 -102
  230. package/.agent/skills/gsd/references/workflows/secure-phase.md +7 -5
  231. package/.agent/skills/gsd/references/workflows/settings.md +24 -7
  232. package/.agent/skills/gsd/references/workflows/ship.md +71 -6
  233. package/.agent/skills/gsd/references/workflows/sketch-wrap-up.md +283 -0
  234. package/.agent/skills/gsd/references/workflows/sketch.md +263 -0
  235. package/.agent/skills/gsd/references/workflows/spec-phase.md +262 -0
  236. package/.agent/skills/gsd/references/workflows/spike-wrap-up.md +273 -0
  237. package/.agent/skills/gsd/references/workflows/spike.md +270 -0
  238. package/.agent/skills/gsd/references/workflows/stats.md +60 -60
  239. package/.agent/skills/gsd/references/workflows/transition.md +671 -671
  240. package/.agent/skills/gsd/references/workflows/ui-phase.md +33 -12
  241. package/.agent/skills/gsd/references/workflows/ui-review.md +6 -4
  242. package/.agent/skills/gsd/references/workflows/undo.md +3 -1
  243. package/.agent/skills/gsd/references/workflows/update.md +113 -2
  244. package/.agent/skills/gsd/references/workflows/validate-phase.md +7 -5
  245. package/.agent/skills/gsd/references/workflows/verify-phase.md +93 -10
  246. package/.agent/skills/gsd/references/workflows/verify-work.md +50 -10
  247. package/.agent/skills/gsd-converter/references/mapping.md +1 -1
  248. package/.agent/skills/gsd-converter/scripts/convert.py +36 -17
  249. package/.agent/skills/gsd-converter/scripts/regression_test.py +68 -33
  250. package/README.md +3 -2
  251. package/package.json +4 -2
  252. package/.agent/skills/release-manager/SKILL.md +0 -162
  253. package/.agent/skills/release-manager/bin/LICENSE +0 -21
  254. package/.agent/skills/release-manager/bin/gh.exe +0 -0
  255. package/.agent/skills/release-manager/references/update_kb_from_fixes.md +0 -29
  256. package/.agent/skills/release-manager/scripts/release.ps1 +0 -222
  257. package/.agent/skills/selectpaste-update/SKILL.md +0 -46
  258. package/.agent/skills/selectpaste-update/scripts/sync-commands.py +0 -317
@@ -313,11 +313,19 @@ function cmdCommit(cwd, message, files, raw, amend, noVerify) {
313
313
  }
314
314
 
315
315
  // Stage files
316
- const filesToStage = files && files.length > 0 ? files : ['.planning/'];
316
+ const explicitFiles = files && files.length > 0;
317
+ const filesToStage = explicitFiles ? files : ['.planning/'];
317
318
  for (const file of filesToStage) {
318
319
  const fullPath = path.join(cwd, file);
319
320
  if (!fs.existsSync(fullPath)) {
320
- // File was deleted/moved — stage the deletion
321
+ if (explicitFiles) {
322
+ // Caller passed an explicit --files list: missing files are skipped.
323
+ // Staging a deletion here would silently remove tracked planning files
324
+ // (e.g. STATE.md, ROADMAP.md) when they are temporarily absent (#2014).
325
+ continue;
326
+ }
327
+ // Default mode (staging all of .planning/): stage the deletion so
328
+ // removed planning files are not left dangling in the index.
321
329
  execGit(cwd, ['rm', '--cached', '--ignore-unmatch', file]);
322
330
  } else {
323
331
  execGit(cwd, ['add', file]);
@@ -823,8 +831,9 @@ function cmdStats(cwd, format, raw) {
823
831
  const headingPattern = /#{2,4}\s*Phase\s+(\d+[A-Z]?(?:\.\d+)*)\s*:\s*([^\n]+)/gi;
824
832
  let match;
825
833
  while ((match = headingPattern.exec(roadmapContent)) !== null) {
826
- phasesByNumber.set(match[1], {
827
- number: match[1],
834
+ const key = normalizePhaseName(match[1]);
835
+ phasesByNumber.set(key, {
836
+ number: key,
828
837
  name: match[2].replace(/\(INSERTED\)/i, '').trim(),
829
838
  plans: 0,
830
839
  summaries: 0,
@@ -854,9 +863,10 @@ function cmdStats(cwd, format, raw) {
854
863
 
855
864
  const status = determinePhaseStatus(plans, summaries, path.join(phasesDir, dir), 'Not Started');
856
865
 
857
- const existing = phasesByNumber.get(phaseNum);
858
- phasesByNumber.set(phaseNum, {
859
- number: phaseNum,
866
+ const normalizedNum = normalizePhaseName(phaseNum);
867
+ const existing = phasesByNumber.get(normalizedNum);
868
+ phasesByNumber.set(normalizedNum, {
869
+ number: normalizedNum,
860
870
  name: existing?.name || phaseName,
861
871
  plans: (existing?.plans || 0) + plans,
862
872
  summaries: (existing?.summaries || 0) + summaries,
@@ -4,7 +4,7 @@
4
4
 
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { output, error, planningRoot, CONFIG_DEFAULTS } = require('./core.cjs');
7
+ const { output, error, planningDir, withPlanningLock, CONFIG_DEFAULTS, atomicWriteFileSync } = require('./core.cjs');
8
8
  const {
9
9
  VALID_PROFILES,
10
10
  getAgentToModelMapForProfile,
@@ -15,19 +15,28 @@ const VALID_CONFIG_KEYS = new Set([
15
15
  'mode', 'granularity', 'parallelization', 'commit_docs', 'model_profile',
16
16
  'search_gitignored', 'brave_search', 'firecrawl', 'exa_search',
17
17
  'workflow.research', 'workflow.plan_check', 'workflow.verifier',
18
- 'workflow.nyquist_validation', 'workflow.ui_phase', 'workflow.ui_safety_gate',
18
+ 'workflow.nyquist_validation', 'workflow.ai_integration_phase', 'workflow.ui_phase', 'workflow.ui_safety_gate',
19
19
  'workflow.auto_advance', 'workflow.node_repair', 'workflow.node_repair_budget',
20
+ 'workflow.tdd_mode',
20
21
  'workflow.text_mode',
21
22
  'workflow.research_before_questions',
22
23
  'workflow.discuss_mode',
23
24
  'workflow.skip_discuss',
25
+ 'workflow.auto_prune_state',
24
26
  'workflow._auto_chain_active',
25
27
  'workflow.use_worktrees',
26
28
  'workflow.code_review',
27
29
  'workflow.code_review_depth',
30
+ 'workflow.code_review_command',
31
+ 'workflow.pattern_mapper',
32
+ 'workflow.plan_bounce',
33
+ 'workflow.plan_bounce_script',
34
+ 'workflow.plan_bounce_passes',
28
35
  'git.branching_strategy', 'git.base_branch', 'git.phase_branch_template', 'git.milestone_branch_template', 'git.quick_branch_template',
29
36
  'planning.commit_docs', 'planning.search_gitignored',
37
+ 'workflow.cross_ai_execution', 'workflow.cross_ai_command', 'workflow.cross_ai_timeout',
30
38
  'workflow.subagent_timeout',
39
+ 'workflow.inline_plan_threshold',
31
40
  'hooks.context_warnings',
32
41
  'features.thinking_partner',
33
42
  'context',
@@ -36,6 +45,10 @@ const VALID_CONFIG_KEYS = new Set([
36
45
  'project_code', 'phase_naming',
37
46
  'manager.flags.discuss', 'manager.flags.plan', 'manager.flags.execute',
38
47
  'response_language',
48
+ 'intel.enabled',
49
+ 'graphify.enabled',
50
+ 'graphify.build_timeout',
51
+ 'antigravity_md_path',
39
52
  ]);
40
53
 
41
54
  /**
@@ -47,6 +60,8 @@ function isValidConfigKey(keyPath) {
47
60
  if (VALID_CONFIG_KEYS.has(keyPath)) return true;
48
61
  // Allow agent_skills.<agent-type> with any agent type string
49
62
  if (/^agent_skills\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
63
+ // Allow review.models.<cli-name> for per-CLI model selection in /gsd-review
64
+ if (/^review\.models\.[a-zA-Z0-9_-]+$/.test(keyPath)) return true;
50
65
  // Allow features.<feature_name> — dynamic namespace for feature flags.
51
66
  // Intentionally open-ended so new flags (e.g., features.global_learnings) work
52
67
  // without updating VALID_CONFIG_KEYS each time.
@@ -61,9 +76,11 @@ const CONFIG_KEY_SUGGESTIONS = {
61
76
  'hooks.research_questions': 'workflow.research_before_questions',
62
77
  'workflow.research_questions': 'workflow.research_before_questions',
63
78
  'workflow.codereview': 'workflow.code_review',
79
+ 'workflow.review_command': 'workflow.code_review_command',
64
80
  'workflow.review': 'workflow.code_review',
65
81
  'workflow.code_review_level': 'workflow.code_review_depth',
66
82
  'workflow.review_depth': 'workflow.code_review_depth',
83
+ 'review.model': 'review.models.<cli-name>',
67
84
  };
68
85
 
69
86
  function validateKnownConfigKeyPath(keyPath) {
@@ -143,12 +160,20 @@ function buildNewProjectConfig(userChoices) {
143
160
  node_repair_budget: 2,
144
161
  ui_phase: true,
145
162
  ui_safety_gate: true,
163
+ ai_integration_phase: true,
164
+ tdd_mode: false,
146
165
  text_mode: false,
147
166
  research_before_questions: false,
148
167
  discuss_mode: 'discuss',
149
168
  skip_discuss: false,
150
169
  code_review: true,
151
170
  code_review_depth: 'standard',
171
+ code_review_command: null,
172
+ pattern_mapper: true,
173
+ plan_bounce: false,
174
+ plan_bounce_script: null,
175
+ plan_bounce_passes: 2,
176
+ auto_prune_state: false,
152
177
  },
153
178
  hooks: {
154
179
  context_warnings: true,
@@ -156,6 +181,7 @@ function buildNewProjectConfig(userChoices) {
156
181
  project_code: null,
157
182
  phase_naming: 'sequential',
158
183
  agent_skills: {},
184
+ antigravity_md_path: './ANTIGRAVITY.md',
159
185
  };
160
186
 
161
187
  // Three-level deep merge: hardcoded <- userDefaults <- choices
@@ -196,7 +222,7 @@ function buildNewProjectConfig(userChoices) {
196
222
  * Idempotent: if config.json already exists, returns { created: false }.
197
223
  */
198
224
  function cmdConfigNewProject(cwd, choicesJson, raw) {
199
- const planningBase = planningRoot(cwd);
225
+ const planningBase = planningDir(cwd);
200
226
  const configPath = path.join(planningBase, 'config.json');
201
227
 
202
228
  // Idempotent: don't overwrite existing config
@@ -227,7 +253,7 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
227
253
  const config = buildNewProjectConfig(userChoices);
228
254
 
229
255
  try {
230
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
256
+ atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
231
257
  output({ created: true, path: '.planning/config.json' }, raw, 'created');
232
258
  } catch (err) {
233
259
  error('Failed to write config.json: ' + err.message);
@@ -241,7 +267,7 @@ function cmdConfigNewProject(cwd, choicesJson, raw) {
241
267
  * the happy path. But note that `error()` will still `exit(1)` out of the process.
242
268
  */
243
269
  function ensureConfigFile(cwd) {
244
- const planningBase = planningRoot(cwd);
270
+ const planningBase = planningDir(cwd);
245
271
  const configPath = path.join(planningBase, 'config.json');
246
272
 
247
273
  // Ensure .planning directory exists
@@ -261,7 +287,7 @@ function ensureConfigFile(cwd) {
261
287
  const config = buildNewProjectConfig({});
262
288
 
263
289
  try {
264
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
290
+ atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
265
291
  return { created: true, path: '.planning/config.json' };
266
292
  } catch (err) {
267
293
  error('Failed to create config.json: ' + err.message);
@@ -291,38 +317,40 @@ function cmdConfigEnsureSection(cwd, raw) {
291
317
  * the happy path. But note that `error()` will still `exit(1)` out of the process.
292
318
  */
293
319
  function setConfigValue(cwd, keyPath, parsedValue) {
294
- const configPath = path.join(planningRoot(cwd), 'config.json');
320
+ const configPath = path.join(planningDir(cwd), 'config.json');
295
321
 
322
+ return withPlanningLock(cwd, () => {
296
323
  // Load existing config or start with empty object
297
324
  let config = {};
298
325
  try {
299
- if (fs.existsSync(configPath)) {
326
+ if (fs.existsSync(configPath)) {
300
327
  config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
301
- }
328
+ }
302
329
  } catch (err) {
303
- error('Failed to read config.json: ' + err.message);
330
+ error('Failed to read config.json: ' + err.message);
304
331
  }
305
332
 
306
333
  // Set nested value using dot notation (e.g., "workflow.research")
307
334
  const keys = keyPath.split('.');
308
335
  let current = config;
309
336
  for (let i = 0; i < keys.length - 1; i++) {
310
- const key = keys[i];
311
- if (current[key] === undefined || typeof current[key] !== 'object') {
337
+ const key = keys[i];
338
+ if (current[key] === undefined || typeof current[key] !== 'object') {
312
339
  current[key] = {};
313
- }
314
- current = current[key];
340
+ }
341
+ current = current[key];
315
342
  }
316
343
  const previousValue = current[keys[keys.length - 1]]; // Capture previous value before overwriting
317
344
  current[keys[keys.length - 1]] = parsedValue;
318
345
 
319
346
  // Write back
320
347
  try {
321
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
322
- return { updated: true, key: keyPath, value: parsedValue, previousValue };
348
+ atomicWriteFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
349
+ return { updated: true, key: keyPath, value: parsedValue, previousValue };
323
350
  } catch (err) {
324
- error('Failed to write config.json: ' + err.message);
351
+ error('Failed to write config.json: ' + err.message);
325
352
  }
353
+ });
326
354
  }
327
355
 
328
356
  /**
@@ -361,17 +389,21 @@ function cmdConfigSet(cwd, keyPath, value, raw) {
361
389
  output(setConfigValueResult, raw, `${keyPath}=${parsedValue}`);
362
390
  }
363
391
 
364
- function cmdConfigGet(cwd, keyPath, raw) {
365
- const configPath = path.join(planningRoot(cwd), 'config.json');
392
+ function cmdConfigGet(cwd, keyPath, raw, defaultValue) {
393
+ const configPath = path.join(planningDir(cwd), 'config.json');
394
+ const hasDefault = defaultValue !== undefined;
366
395
 
367
396
  if (!keyPath) {
368
- error('Usage: config-get <key.path>');
397
+ error('Usage: config-get <key.path> [--default <value>]');
369
398
  }
370
399
 
371
400
  let config = {};
372
401
  try {
373
402
  if (fs.existsSync(configPath)) {
374
403
  config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
404
+ } else if (hasDefault) {
405
+ output(defaultValue, raw, String(defaultValue));
406
+ return;
375
407
  } else {
376
408
  error('No config.json found at ' + configPath);
377
409
  }
@@ -385,12 +417,14 @@ function cmdConfigGet(cwd, keyPath, raw) {
385
417
  let current = config;
386
418
  for (const key of keys) {
387
419
  if (current === undefined || current === null || typeof current !== 'object') {
420
+ if (hasDefault) { output(defaultValue, raw, String(defaultValue)); return; }
388
421
  error(`Key not found: ${keyPath}`);
389
422
  }
390
423
  current = current[key];
391
424
  }
392
425
 
393
426
  if (current === undefined) {
427
+ if (hasDefault) { output(defaultValue, raw, String(defaultValue)); return; }
394
428
  error(`Key not found: ${keyPath}`);
395
429
  }
396
430
 
@@ -461,6 +495,17 @@ function getCmdConfigSetModelProfileResultMessage(
461
495
  return paragraphs.join('\n\n');
462
496
  }
463
497
 
498
+ /**
499
+ * Print the resolved config.json path (workstream-aware). Used by settings.md
500
+ * so the workflow writes/reads the correct file when a workstream is active (#2282).
501
+ */
502
+ function cmdConfigPath(cwd) {
503
+ // Always emit as plain text — a file path is used via shell substitution,
504
+ // never consumed as JSON. Passing raw=true forces plain-text output.
505
+ const configPath = path.join(planningDir(cwd), 'config.json');
506
+ output(configPath, true, configPath);
507
+ }
508
+
464
509
  module.exports = {
465
510
  VALID_CONFIG_KEYS,
466
511
  cmdConfigEnsureSection,
@@ -468,4 +513,5 @@ module.exports = {
468
513
  cmdConfigGet,
469
514
  cmdConfigSetModelProfile,
470
515
  cmdConfigNewProject,
516
+ cmdConfigPath,
471
517
  };
@@ -27,6 +27,16 @@ const WORKSTREAM_SESSION_ENV_KEYS = [
27
27
  let cachedControllingTtyToken = null;
28
28
  let didProbeControllingTtyToken = false;
29
29
 
30
+ // Track all .planning/.lock files held by this process so they can be removed
31
+ // on exit. process.on('exit') fires even on process.exit(1), unlike try/finally
32
+ // which is skipped when error() calls process.exit(1) inside a locked region (#1916).
33
+ const _heldPlanningLocks = new Set();
34
+ process.on('exit', () => {
35
+ for (const lockPath of _heldPlanningLocks) {
36
+ try { fs.unlinkSync(lockPath); } catch { /* already gone */ }
37
+ }
38
+ });
39
+
30
40
  // ─── Path helpers ────────────────────────────────────────────────────────────
31
41
 
32
42
  /** Normalize a relative path to always use forward slashes (cross-platform). */
@@ -149,14 +159,25 @@ function findProjectRoot(startDir) {
149
159
  * @param {number} opts.maxAgeMs - max age in ms before removal (default: 5 min)
150
160
  * @param {boolean} opts.dirsOnly - if true, only remove directories (default: false)
151
161
  */
162
+ /**
163
+ * Dedicated GSD temp directory: path.join(os.tmpdir(), 'gsd').
164
+ * Created on first use. Keeps GSD temp files isolated from the system
165
+ * temp directory so reap scans only GSD files (#1975).
166
+ */
167
+ const GSD_TEMP_DIR = path.join(require('os').tmpdir(), 'gsd');
168
+
169
+ function ensureGsdTempDir() {
170
+ fs.mkdirSync(GSD_TEMP_DIR, { recursive: true });
171
+ }
172
+
152
173
  function reapStaleTempFiles(prefix = 'gsd-', { maxAgeMs = 5 * 60 * 1000, dirsOnly = false } = {}) {
153
174
  try {
154
- const tmpDir = require('os').tmpdir();
175
+ ensureGsdTempDir();
155
176
  const now = Date.now();
156
- const entries = fs.readdirSync(tmpDir);
177
+ const entries = fs.readdirSync(GSD_TEMP_DIR);
157
178
  for (const entry of entries) {
158
179
  if (!entry.startsWith(prefix)) continue;
159
- const fullPath = path.join(tmpDir, entry);
180
+ const fullPath = path.join(GSD_TEMP_DIR, entry);
160
181
  try {
161
182
  const stat = fs.statSync(fullPath);
162
183
  if (now - stat.mtimeMs > maxAgeMs) {
@@ -185,7 +206,8 @@ function output(result, raw, rawValue) {
185
206
  // Write to tmpfile and output the path prefixed with @file: so callers can detect it.
186
207
  if (json.length > 50000) {
187
208
  reapStaleTempFiles();
188
- const tmpPath = path.join(require('os').tmpdir(), `gsd-${Date.now()}.json`);
209
+ ensureGsdTempDir();
210
+ const tmpPath = path.join(GSD_TEMP_DIR, `gsd-${Date.now()}.json`);
189
211
  fs.writeFileSync(tmpPath, json, 'utf-8');
190
212
  data = '@file:' + tmpPath;
191
213
  } else {
@@ -284,6 +306,7 @@ const CONFIG_DEFAULTS = {
284
306
  plan_checker: true,
285
307
  verifier: true,
286
308
  nyquist_validation: true,
309
+ ai_integration_phase: true,
287
310
  parallelization: true,
288
311
  brave_search: false,
289
312
  firecrawl: false,
@@ -357,7 +380,7 @@ function loadConfig(cwd) {
357
380
  // Section containers that hold nested sub-keys
358
381
  'git', 'workflow', 'planning', 'hooks', 'features',
359
382
  // Internal keys loadConfig reads but config-set doesn't expose
360
- 'model_overrides', 'agent_skills', 'context_window', 'resolve_model_ids',
383
+ 'model_overrides', 'agent_skills', 'context_window', 'resolve_model_ids', 'antigravity_md_path',
361
384
  // Deprecated keys (still accepted for migration, not in config-set)
362
385
  'depth', 'multiRepo',
363
386
  ]);
@@ -407,7 +430,11 @@ function loadConfig(cwd) {
407
430
  brave_search: get('brave_search') ?? defaults.brave_search,
408
431
  firecrawl: get('firecrawl') ?? defaults.firecrawl,
409
432
  exa_search: get('exa_search') ?? defaults.exa_search,
433
+ tdd_mode: get('tdd_mode', { section: 'workflow', field: 'tdd_mode' }) ?? false,
410
434
  text_mode: get('text_mode', { section: 'workflow', field: 'text_mode' }) ?? defaults.text_mode,
435
+ auto_advance: get('auto_advance', { section: 'workflow', field: 'auto_advance' }) ?? false,
436
+ _auto_chain_active: get('_auto_chain_active', { section: 'workflow', field: '_auto_chain_active' }) ?? false,
437
+ mode: get('mode') ?? 'interactive',
411
438
  sub_repos: get('sub_repos', { section: 'planning', field: 'sub_repos' }) ?? defaults.sub_repos,
412
439
  resolve_model_ids: get('resolve_model_ids') ?? defaults.resolve_model_ids,
413
440
  context_window: get('context_window') ?? defaults.context_window,
@@ -418,6 +445,7 @@ function loadConfig(cwd) {
418
445
  agent_skills: parsed.agent_skills || {},
419
446
  manager: parsed.manager || {},
420
447
  response_language: get('response_language') || null,
448
+ antigravity_md_path: get('antigravity_md_path') || null,
421
449
  };
422
450
  } catch {
423
451
  // Fall back to ~/.gsd/defaults.json only for truly pre-project contexts (#1683)
@@ -455,7 +483,11 @@ function loadConfig(cwd) {
455
483
 
456
484
  // ─── Git utilities ────────────────────────────────────────────────────────────
457
485
 
486
+ const _gitIgnoredCache = new Map();
487
+
458
488
  function isGitIgnored(cwd, targetPath) {
489
+ const key = cwd + '::' + targetPath;
490
+ if (_gitIgnoredCache.has(key)) return _gitIgnoredCache.get(key);
459
491
  try {
460
492
  // --no-index checks .gitignore rules regardless of whether the file is tracked.
461
493
  // Without it, git check-ignore returns "not ignored" for tracked files even when
@@ -467,8 +499,10 @@ function isGitIgnored(cwd, targetPath) {
467
499
  cwd,
468
500
  stdio: 'pipe',
469
501
  });
502
+ _gitIgnoredCache.set(key, true);
470
503
  return true;
471
504
  } catch {
505
+ _gitIgnoredCache.set(key, false);
472
506
  return false;
473
507
  }
474
508
  }
@@ -630,6 +664,98 @@ function resolveWorktreeRoot(cwd) {
630
664
  return cwd;
631
665
  }
632
666
 
667
+ /**
668
+ * Parse `git worktree list --porcelain` output into an array of
669
+ * { path, branch } objects. Entries with a detached HEAD (no branch line)
670
+ * are skipped because we cannot safely reason about their merge status.
671
+ *
672
+ * @param {string} porcelain - raw output from git worktree list --porcelain
673
+ * @returns {{ path: string, branch: string }[]}
674
+ */
675
+ function parseWorktreePorcelain(porcelain) {
676
+ const entries = [];
677
+ let current = null;
678
+ for (const line of porcelain.split('\n')) {
679
+ if (line.startsWith('worktree ')) {
680
+ current = { path: line.slice('worktree '.length).trim(), branch: null };
681
+ } else if (line.startsWith('branch refs/heads/') && current) {
682
+ current.branch = line.slice('branch refs/heads/'.length).trim();
683
+ } else if (line === '' && current) {
684
+ if (current.branch) entries.push(current);
685
+ current = null;
686
+ }
687
+ }
688
+ // flush last entry if file doesn't end with blank line
689
+ if (current && current.branch) entries.push(current);
690
+ return entries;
691
+ }
692
+
693
+ /**
694
+ * Remove linked git worktrees whose branch has already been merged into the
695
+ * current HEAD of the main worktree. Also runs `git worktree prune` to clear
696
+ * any stale references left by manually-deleted worktree directories.
697
+ *
698
+ * Safe guards:
699
+ * - Never removes the main worktree (first entry in --porcelain output).
700
+ * - Never removes the worktree at process.cwd().
701
+ * - Never removes a worktree whose branch has unmerged commits.
702
+ * - Skips detached-HEAD worktrees (no branch name).
703
+ *
704
+ * @param {string} repoRoot - absolute path to the main (or any) worktree of
705
+ * the repository; used as `cwd` for git commands.
706
+ * @returns {string[]} list of worktree paths that were removed
707
+ */
708
+ function pruneOrphanedWorktrees(repoRoot) {
709
+ const pruned = [];
710
+ const cwd = process.cwd();
711
+
712
+ try {
713
+ // 1. Get all worktrees in porcelain format
714
+ const listResult = execGit(repoRoot, ['worktree', 'list', '--porcelain']);
715
+ if (listResult.exitCode !== 0) return pruned;
716
+
717
+ const worktrees = parseWorktreePorcelain(listResult.stdout);
718
+ if (worktrees.length === 0) {
719
+ execGit(repoRoot, ['worktree', 'prune']);
720
+ return pruned;
721
+ }
722
+
723
+ // 2. First entry is the main worktree — never touch it
724
+ const mainWorktreePath = worktrees[0].path;
725
+
726
+ // 3. Check each non-main worktree
727
+ for (let i = 1; i < worktrees.length; i++) {
728
+ const { path: wtPath, branch } = worktrees[i];
729
+
730
+ // Never remove the worktree for the current process directory
731
+ if (wtPath === cwd || cwd.startsWith(wtPath + path.sep)) continue;
732
+
733
+ // Check if the branch is fully merged into HEAD (main)
734
+ // git merge-base --is-ancestor <branch> HEAD exits 0 when merged
735
+ const ancestorCheck = execGit(repoRoot, [
736
+ 'merge-base', '--is-ancestor', branch, 'HEAD',
737
+ ]);
738
+
739
+ if (ancestorCheck.exitCode !== 0) {
740
+ // Not yet merged — leave it alone
741
+ continue;
742
+ }
743
+
744
+ // Remove the worktree and delete the branch
745
+ const removeResult = execGit(repoRoot, ['worktree', 'remove', '--force', wtPath]);
746
+ if (removeResult.exitCode === 0) {
747
+ execGit(repoRoot, ['branch', '-D', branch]);
748
+ pruned.push(wtPath);
749
+ }
750
+ }
751
+ } catch { /* never crash the caller */ }
752
+
753
+ // 4. Always run prune to clear stale references (e.g. manually-deleted dirs)
754
+ execGit(repoRoot, ['worktree', 'prune']);
755
+
756
+ return pruned;
757
+ }
758
+
633
759
  /**
634
760
  * Acquire a file-based lock for .planning/ writes.
635
761
  * Prevents concurrent worktrees from corrupting shared planning files.
@@ -653,10 +779,15 @@ function withPlanningLock(cwd, fn) {
653
779
  acquired: new Date().toISOString(),
654
780
  }), { flag: 'wx' });
655
781
 
782
+ // Register for exit-time cleanup so process.exit(1) inside a locked region
783
+ // cannot leave a stale lock file (#1916).
784
+ _heldPlanningLocks.add(lockPath);
785
+
656
786
  // Lock acquired — run the function
657
787
  try {
658
788
  return fn();
659
789
  } finally {
790
+ _heldPlanningLocks.delete(lockPath);
660
791
  try { fs.unlinkSync(lockPath); } catch { /* already released */ }
661
792
  }
662
793
  } catch (err) {
@@ -725,19 +856,23 @@ function planningRoot(cwd) {
725
856
  }
726
857
 
727
858
  /**
728
- * Get common .planning file paths, workstream-aware.
729
- * Scoped paths (state, roadmap, phases, requirements) resolve to the active workstream.
730
- * Shared paths (project, config) always resolve to the root .planning/.
859
+ * Get common .planning file paths, project-and-workstream-aware.
860
+ *
861
+ * All paths route through planningDir(cwd, ws), which honors the GSD_PROJECT
862
+ * env var and active workstream. This matches loadConfig() above (line 256),
863
+ * which has always read config.json via planningDir(cwd). Previously project
864
+ * and config were resolved against the unrouted .planning/ root, which broke
865
+ * `gsd-tools config-get` in multi-project layouts (the CRUD writers and the
866
+ * reader pointed at different files).
731
867
  */
732
868
  function planningPaths(cwd, ws) {
733
869
  const base = planningDir(cwd, ws);
734
- const root = path.join(cwd, '.planning');
735
870
  return {
736
871
  planning: base,
737
872
  state: path.join(base, 'STATE.md'),
738
873
  roadmap: path.join(base, 'ROADMAP.md'),
739
- project: path.join(root, 'PROJECT.md'),
740
- config: path.join(root, 'config.json'),
874
+ project: path.join(base, 'PROJECT.md'),
875
+ config: path.join(base, 'config.json'),
741
876
  phases: path.join(base, 'phases'),
742
877
  requirements: path.join(base, 'REQUIREMENTS.md'),
743
878
  };
@@ -934,7 +1069,10 @@ function normalizePhaseName(phase) {
934
1069
  const match = stripped.match(/^(\d+)([A-Z])?((?:\.\d+)*)/i);
935
1070
  if (match) {
936
1071
  const padded = match[1].padStart(2, '0');
937
- const letter = match[2] ? match[2].toUpperCase() : '';
1072
+ // Preserve original case of letter suffix (#1962).
1073
+ // Uppercasing causes directory/roadmap mismatches on case-sensitive filesystems
1074
+ // (e.g., "16c" in ROADMAP.md → directory "16C-name" → progress can't match).
1075
+ const letter = match[2] || '';
938
1076
  const decimal = match[3] || '';
939
1077
  return padded + letter + decimal;
940
1078
  }
@@ -1540,6 +1678,64 @@ function readSubdirectories(dirPath, sort = false) {
1540
1678
  }
1541
1679
  }
1542
1680
 
1681
+ // ─── Atomic file writes ───────────────────────────────────────────────────────
1682
+
1683
+ /**
1684
+ * Write a file atomically using write-to-temp-then-rename.
1685
+ *
1686
+ * On POSIX systems, `fs.renameSync` is atomic when the source and destination
1687
+ * are on the same filesystem. This prevents a process killed mid-write from
1688
+ * leaving a truncated file that is unparseable on next read.
1689
+ *
1690
+ * The temp file is placed alongside the target so it is guaranteed to be on
1691
+ * the same filesystem (required for rename atomicity). The PID is embedded in
1692
+ * the temp file name so concurrent writers use distinct paths.
1693
+ *
1694
+ * If `renameSync` fails (e.g. cross-device move), the function falls back to a
1695
+ * direct `writeFileSync` so callers always get a best-effort write.
1696
+ *
1697
+ * @param {string} filePath Absolute path to write.
1698
+ * @param {string|Buffer} content File content.
1699
+ * @param {string} [encoding='utf-8'] Encoding passed to writeFileSync.
1700
+ */
1701
+ function atomicWriteFileSync(filePath, content, encoding = 'utf-8') {
1702
+ const tmpPath = filePath + '.tmp.' + process.pid;
1703
+ try {
1704
+ fs.writeFileSync(tmpPath, content, encoding);
1705
+ fs.renameSync(tmpPath, filePath);
1706
+ } catch (renameErr) {
1707
+ // Clean up the temp file if rename failed, then fall back to direct write.
1708
+ try { fs.unlinkSync(tmpPath); } catch { /* already gone or never created */ }
1709
+ fs.writeFileSync(filePath, content, encoding);
1710
+ }
1711
+ }
1712
+
1713
+ /**
1714
+ * Format a Date as a fuzzy relative time string (e.g. "5 minutes ago").
1715
+ * @param {Date} date
1716
+ * @returns {string}
1717
+ */
1718
+ function timeAgo(date) {
1719
+ const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
1720
+ if (seconds < 5) return 'just now';
1721
+ if (seconds < 60) return `${seconds} seconds ago`;
1722
+ const minutes = Math.floor(seconds / 60);
1723
+ if (minutes === 1) return '1 minute ago';
1724
+ if (minutes < 60) return `${minutes} minutes ago`;
1725
+ const hours = Math.floor(minutes / 60);
1726
+ if (hours === 1) return '1 hour ago';
1727
+ if (hours < 24) return `${hours} hours ago`;
1728
+ const days = Math.floor(hours / 24);
1729
+ if (days === 1) return '1 day ago';
1730
+ if (days < 30) return `${days} days ago`;
1731
+ const months = Math.floor(days / 30);
1732
+ if (months === 1) return '1 month ago';
1733
+ if (months < 12) return `${months} months ago`;
1734
+ const years = Math.floor(days / 365);
1735
+ if (years === 1) return '1 year ago';
1736
+ return `${years} years ago`;
1737
+ }
1738
+
1543
1739
  module.exports = {
1544
1740
  parseIncludeFlag,
1545
1741
  discoverPhaseArtifacts,
@@ -1576,6 +1772,7 @@ module.exports = {
1576
1772
  findProjectRoot,
1577
1773
  detectSubRepos,
1578
1774
  reapStaleTempFiles,
1775
+ GSD_TEMP_DIR,
1579
1776
  MODEL_ALIAS_MAP,
1580
1777
  CONFIG_DEFAULTS,
1581
1778
  planningDir,
@@ -1589,4 +1786,7 @@ module.exports = {
1589
1786
  readSubdirectories,
1590
1787
  getAgentsDir,
1591
1788
  checkAgentsInstalled,
1789
+ atomicWriteFileSync,
1790
+ timeAgo,
1791
+ pruneOrphanedWorktrees,
1592
1792
  };