gsd-pi 2.24.0 → 2.26.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 (206) hide show
  1. package/README.md +13 -3
  2. package/dist/headless.js +24 -4
  3. package/dist/models-resolver.d.ts +0 -11
  4. package/dist/models-resolver.js +0 -15
  5. package/dist/resource-loader.d.ts +0 -1
  6. package/dist/resource-loader.js +0 -9
  7. package/dist/resources/GSD-WORKFLOW.md +12 -9
  8. package/dist/resources/extensions/async-jobs/index.ts +9 -1
  9. package/dist/resources/extensions/bg-shell/index.ts +3 -2
  10. package/dist/resources/extensions/bg-shell/overlay.ts +18 -17
  11. package/dist/resources/extensions/get-secrets-from-user.ts +5 -23
  12. package/dist/resources/extensions/gsd/activity-log.ts +5 -3
  13. package/dist/resources/extensions/gsd/auto-prompts.ts +14 -0
  14. package/dist/resources/extensions/gsd/auto-recovery.ts +7 -4
  15. package/dist/resources/extensions/gsd/auto-worktree.ts +132 -3
  16. package/dist/resources/extensions/gsd/auto.ts +265 -48
  17. package/dist/resources/extensions/gsd/cache.ts +3 -1
  18. package/dist/resources/extensions/gsd/doctor-proactive.ts +7 -6
  19. package/dist/resources/extensions/gsd/doctor.ts +26 -1
  20. package/dist/resources/extensions/gsd/files.ts +13 -2
  21. package/dist/resources/extensions/gsd/git-service.ts +74 -14
  22. package/dist/resources/extensions/gsd/gsd-db.ts +78 -1
  23. package/dist/resources/extensions/gsd/guided-flow.ts +54 -22
  24. package/dist/resources/extensions/gsd/index.ts +62 -8
  25. package/dist/resources/extensions/gsd/memory-extractor.ts +352 -0
  26. package/dist/resources/extensions/gsd/memory-store.ts +441 -0
  27. package/dist/resources/extensions/gsd/migrate/command.ts +2 -2
  28. package/dist/resources/extensions/gsd/migrate/writer.ts +39 -0
  29. package/dist/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  30. package/dist/resources/extensions/gsd/preferences.ts +2 -1
  31. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  33. package/dist/resources/extensions/gsd/prompts/discuss.md +5 -5
  34. package/dist/resources/extensions/gsd/prompts/execute-task.md +1 -1
  35. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  36. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  37. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  38. package/dist/resources/extensions/gsd/prompts/queue.md +3 -3
  39. package/dist/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  40. package/dist/resources/extensions/gsd/roadmap-slices.ts +45 -1
  41. package/dist/resources/extensions/gsd/state.ts +17 -6
  42. package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  43. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  44. package/dist/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  45. package/dist/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  46. package/dist/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  47. package/dist/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  48. package/dist/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  49. package/dist/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  50. package/dist/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  51. package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  52. package/dist/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  53. package/dist/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  54. package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  55. package/dist/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  56. package/dist/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  57. package/dist/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  58. package/dist/resources/extensions/gsd/triage-ui.ts +1 -1
  59. package/dist/resources/extensions/gsd/types.ts +2 -0
  60. package/dist/resources/extensions/gsd/visualizer-data.ts +291 -10
  61. package/dist/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  62. package/dist/resources/extensions/gsd/visualizer-views.ts +462 -48
  63. package/dist/resources/extensions/gsd/worktree.ts +9 -2
  64. package/dist/resources/extensions/search-the-web/native-search.ts +19 -5
  65. package/dist/resources/extensions/shared/path-display.ts +19 -0
  66. package/package.json +1 -6
  67. package/packages/pi-agent-core/dist/agent-loop.js +2 -0
  68. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  69. package/packages/pi-agent-core/src/agent-loop.ts +2 -0
  70. package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
  71. package/packages/pi-ai/dist/providers/anthropic.js +64 -0
  72. package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
  73. package/packages/pi-ai/dist/providers/mistral.js +3 -0
  74. package/packages/pi-ai/dist/providers/mistral.js.map +1 -1
  75. package/packages/pi-ai/dist/types.d.ts +23 -1
  76. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  77. package/packages/pi-ai/dist/types.js.map +1 -1
  78. package/packages/pi-ai/src/providers/anthropic.ts +65 -1
  79. package/packages/pi-ai/src/providers/mistral.ts +3 -0
  80. package/packages/pi-ai/src/types.ts +19 -1
  81. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -0
  82. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/core/agent-session.js +32 -0
  84. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/keybindings.js +1 -1
  86. package/packages/pi-coding-agent/dist/core/keybindings.js.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -1
  88. package/packages/pi-coding-agent/dist/core/lsp/client.js +12 -1
  89. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +7 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/sdk.d.ts +2 -2
  94. package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/sdk.js +8 -3
  96. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +3 -0
  98. package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/settings-manager.js +8 -0
  100. package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  103. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  104. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  105. package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -1
  106. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  107. package/packages/pi-coding-agent/dist/index.d.ts +2 -1
  108. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  109. package/packages/pi-coding-agent/dist/index.js +5 -1
  110. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  111. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +41 -3
  112. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
  113. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +301 -62
  114. package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
  115. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js +17 -0
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/tool-execution.js.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +5 -0
  119. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +135 -30
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  122. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts +8 -0
  123. package/packages/pi-coding-agent/dist/tests/path-display.test.d.ts.map +1 -0
  124. package/packages/pi-coding-agent/dist/tests/path-display.test.js +60 -0
  125. package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -0
  126. package/packages/pi-coding-agent/dist/utils/clipboard-image.d.ts.map +1 -1
  127. package/packages/pi-coding-agent/dist/utils/clipboard-image.js +32 -6
  128. package/packages/pi-coding-agent/dist/utils/clipboard-image.js.map +1 -1
  129. package/packages/pi-coding-agent/dist/utils/path-display.d.ts +34 -0
  130. package/packages/pi-coding-agent/dist/utils/path-display.d.ts.map +1 -0
  131. package/packages/pi-coding-agent/dist/utils/path-display.js +36 -0
  132. package/packages/pi-coding-agent/dist/utils/path-display.js.map +1 -0
  133. package/packages/pi-coding-agent/src/core/agent-session.ts +36 -0
  134. package/packages/pi-coding-agent/src/core/keybindings.ts +1 -1
  135. package/packages/pi-coding-agent/src/core/lsp/client.ts +11 -1
  136. package/packages/pi-coding-agent/src/core/lsp/index.ts +7 -0
  137. package/packages/pi-coding-agent/src/core/sdk.ts +17 -1
  138. package/packages/pi-coding-agent/src/core/settings-manager.ts +11 -0
  139. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  140. package/packages/pi-coding-agent/src/core/system-prompt.ts +2 -1
  141. package/packages/pi-coding-agent/src/index.ts +15 -0
  142. package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +347 -62
  143. package/packages/pi-coding-agent/src/modes/interactive/components/tool-execution.ts +18 -0
  144. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +124 -4
  145. package/packages/pi-coding-agent/src/tests/path-display.test.ts +85 -0
  146. package/packages/pi-coding-agent/src/utils/clipboard-image.ts +33 -6
  147. package/packages/pi-coding-agent/src/utils/path-display.ts +36 -0
  148. package/src/resources/GSD-WORKFLOW.md +12 -9
  149. package/src/resources/extensions/async-jobs/index.ts +9 -1
  150. package/src/resources/extensions/bg-shell/index.ts +3 -2
  151. package/src/resources/extensions/bg-shell/overlay.ts +18 -17
  152. package/src/resources/extensions/get-secrets-from-user.ts +5 -23
  153. package/src/resources/extensions/gsd/activity-log.ts +5 -3
  154. package/src/resources/extensions/gsd/auto-prompts.ts +14 -0
  155. package/src/resources/extensions/gsd/auto-recovery.ts +7 -4
  156. package/src/resources/extensions/gsd/auto-worktree.ts +132 -3
  157. package/src/resources/extensions/gsd/auto.ts +265 -48
  158. package/src/resources/extensions/gsd/cache.ts +3 -1
  159. package/src/resources/extensions/gsd/doctor-proactive.ts +7 -6
  160. package/src/resources/extensions/gsd/doctor.ts +26 -1
  161. package/src/resources/extensions/gsd/files.ts +13 -2
  162. package/src/resources/extensions/gsd/git-service.ts +74 -14
  163. package/src/resources/extensions/gsd/gsd-db.ts +78 -1
  164. package/src/resources/extensions/gsd/guided-flow.ts +54 -22
  165. package/src/resources/extensions/gsd/index.ts +62 -8
  166. package/src/resources/extensions/gsd/memory-extractor.ts +352 -0
  167. package/src/resources/extensions/gsd/memory-store.ts +441 -0
  168. package/src/resources/extensions/gsd/migrate/command.ts +2 -2
  169. package/src/resources/extensions/gsd/migrate/writer.ts +39 -0
  170. package/src/resources/extensions/gsd/parallel-orchestrator.ts +122 -4
  171. package/src/resources/extensions/gsd/preferences.ts +2 -1
  172. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
  173. package/src/resources/extensions/gsd/prompts/discuss-headless.md +4 -4
  174. package/src/resources/extensions/gsd/prompts/discuss.md +5 -5
  175. package/src/resources/extensions/gsd/prompts/execute-task.md +1 -1
  176. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +1 -1
  177. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +1 -1
  178. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
  179. package/src/resources/extensions/gsd/prompts/queue.md +3 -3
  180. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +1 -1
  181. package/src/resources/extensions/gsd/roadmap-slices.ts +45 -1
  182. package/src/resources/extensions/gsd/state.ts +17 -6
  183. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
  184. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  185. package/src/resources/extensions/gsd/tests/derive-state.test.ts +70 -0
  186. package/src/resources/extensions/gsd/tests/doctor-proactive.test.ts +23 -3
  187. package/src/resources/extensions/gsd/tests/git-service.test.ts +70 -4
  188. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +2 -2
  189. package/src/resources/extensions/gsd/tests/md-importer.test.ts +2 -3
  190. package/src/resources/extensions/gsd/tests/memory-extractor.test.ts +180 -0
  191. package/src/resources/extensions/gsd/tests/memory-store.test.ts +345 -0
  192. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +13 -7
  193. package/src/resources/extensions/gsd/tests/parallel-worker-monitoring.test.ts +171 -0
  194. package/src/resources/extensions/gsd/tests/smart-entry-draft.test.ts +1 -1
  195. package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +8 -4
  196. package/src/resources/extensions/gsd/tests/visualizer-data.test.ts +147 -2
  197. package/src/resources/extensions/gsd/tests/visualizer-overlay.test.ts +88 -10
  198. package/src/resources/extensions/gsd/tests/visualizer-views.test.ts +314 -87
  199. package/src/resources/extensions/gsd/triage-ui.ts +1 -1
  200. package/src/resources/extensions/gsd/types.ts +2 -0
  201. package/src/resources/extensions/gsd/visualizer-data.ts +291 -10
  202. package/src/resources/extensions/gsd/visualizer-overlay.ts +237 -28
  203. package/src/resources/extensions/gsd/visualizer-views.ts +462 -48
  204. package/src/resources/extensions/gsd/worktree.ts +9 -2
  205. package/src/resources/extensions/search-the-web/native-search.ts +19 -5
  206. package/src/resources/extensions/shared/path-display.ts +19 -0
@@ -51,11 +51,11 @@ Use these templates exactly:
51
51
  5. Write `{{roadmapPath}}` (using Roadmap template) — decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice.
52
52
  6. Seed `.gsd/DECISIONS.md` (using Decisions template)
53
53
  7. Update `.gsd/STATE.md`
54
- 8. Commit: `docs({{milestoneId}}): context, requirements, and roadmap`
54
+ 8. {{commitInstruction}}
55
55
  9. Say exactly: "Milestone {{milestoneId}} ready."
56
56
 
57
57
  **For multi-milestone**, write in this order:
58
- 1. Create all milestone directories: `mkdir -p .gsd/milestones/{M###}/slices` for each
58
+ 1. For each milestone, call `gsd_generate_milestone_id` to get its ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones/<ID>/slices` for each.
59
59
  2. Write `.gsd/PROJECT.md` — full vision across ALL milestones (using Project template)
60
60
  3. Write `.gsd/REQUIREMENTS.md` — full capability contract (using Requirements template)
61
61
  4. Seed `.gsd/DECISIONS.md` (using Decisions template)
@@ -71,7 +71,7 @@ Use these templates exactly:
71
71
  ```
72
72
  Each context file should be rich enough that a future agent — with no memory of this conversation — can understand the intent, constraints, dependencies, what the milestone unlocks, and what "done" looks like.
73
73
  8. Update `.gsd/STATE.md`
74
- 9. Commit: `docs: project plan — N milestones`
74
+ 9. {{multiMilestoneCommitInstruction}}
75
75
  10. Say exactly: "Milestone {{milestoneId}} ready."
76
76
 
77
77
  ## Critical Rules
@@ -82,5 +82,5 @@ Use these templates exactly:
82
82
  - **Investigate before writing** — always scout the codebase first
83
83
  - **Use depends_on frontmatter** for multi-milestone sequences (the state machine reads this field to determine execution order)
84
84
  - **Anti-reduction rule** — if the spec describes a big vision, plan the big vision. Do not ask "what's the minimum viable version?" or reduce scope. Phase complex/risky work into later milestones — do not cut it.
85
- - **Naming convention** — directories use bare IDs (`M001/`, `S01/`), files use ID-SUFFIX format (`M001-CONTEXT.md`, `M001-ROADMAP.md`)
85
+ - **Naming convention** — always use `gsd_generate_milestone_id` to get milestone IDs. Directories use bare IDs (e.g. `M001/` or `M001-r5jzab/`), files use ID-SUFFIX format (e.g. `M001-CONTEXT.md` or `M001-r5jzab-CONTEXT.md`). Never invent milestone IDs manually.
86
86
  - **End with "Milestone {{milestoneId}} ready."** — this triggers auto-start detection
@@ -201,9 +201,9 @@ When writing context.md, preserve the user's exact terminology, emphasis, and sp
201
201
  5. Write `{{roadmapPath}}` — use the **Roadmap** output template below. Decompose into demoable vertical slices with checkboxes, risk, depends, demo sentences, proof strategy, verification classes, milestone definition of done, requirement coverage, and a boundary map. If the milestone crosses multiple runtime boundaries, include an explicit final integration slice that proves the assembled system works end-to-end in a real environment.
202
202
  6. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below. Append rows for any architectural or pattern decisions made during discussion.
203
203
  7. Update `.gsd/STATE.md`
204
- 8. Commit: `docs({{milestoneId}}): context, requirements, and roadmap`
204
+ 8. {{commitInstruction}}
205
205
 
206
- After writing the files and committing, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
206
+ After writing the files, say exactly: "Milestone {{milestoneId}} ready." — nothing else. Auto-mode will start automatically.
207
207
 
208
208
  ### Multi-Milestone
209
209
 
@@ -211,7 +211,7 @@ Once the user confirms the milestone split:
211
211
 
212
212
  #### Phase 1: Shared artifacts
213
213
 
214
- 1. `mkdir -p .gsd/milestones/{{milestoneId}}/slices` for each milestone
214
+ 1. For each milestone, call `gsd_generate_milestone_id` to get its ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones/<ID>/slices`.
215
215
  2. Write `.gsd/PROJECT.md` — use the **Project** output template below.
216
216
  3. Write `.gsd/REQUIREMENTS.md` — use the **Requirements** output template below. Capture Active, Deferred, Out of Scope, and any already Validated requirements. Later milestones may have provisional ownership where slice plans do not exist yet.
217
217
  4. Seed `.gsd/DECISIONS.md` — use the **Decisions** output template below.
@@ -271,8 +271,8 @@ For single-milestone projects, do NOT write this file — it is only for multi-m
271
271
  #### Phase 4: Finalize
272
272
 
273
273
  7. Update `.gsd/STATE.md`
274
- 8. Commit: `docs: project plan — N milestones` (replace N with the actual milestone count)
274
+ 8. {{multiMilestoneCommitInstruction}}
275
275
 
276
- After writing the files and committing, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
276
+ After writing the files, say exactly: "Milestone M001 ready." — nothing else. Auto-mode will start automatically.
277
277
 
278
278
  {{inlinedTemplates}}
@@ -63,7 +63,7 @@ Then:
63
63
  14. Read the template at `~/.gsd/agent/extensions/gsd/templates/task-summary.md`
64
64
  15. Write `{{taskSummaryPath}}`
65
65
  16. Mark {{taskId}} done in `{{planPath}}` (change `[ ]` to `[x]`)
66
- 17. Do not commit manually — the system auto-commits your changes after this unit completes.
66
+ 17. Do not run git commands — the system reads your task summary after completion and creates a meaningful commit from it (type inferred from title, message from your one-liner, key files from frontmatter). Write a clear, specific one-liner in the summary — it becomes the commit message.
67
67
  18. Update `.gsd/STATE.md`
68
68
 
69
69
  All work stays in your working directory: `{{workingDirectory}}`.
@@ -104,5 +104,5 @@ Once the user confirms depth:
104
104
  1. Use the **Context** output template below
105
105
  2. `mkdir -p` the milestone directory if needed
106
106
  3. Write `{{milestoneId}}-CONTEXT.md` — preserve the user's exact terminology, emphasis, and framing. Do not paraphrase nuance into generic summaries. The context file is downstream agents' only window into this conversation.
107
- 4. Commit: `git add {{milestoneId}}-CONTEXT.md && git commit -m "docs({{milestoneId}}): milestone context from discuss"`
107
+ 4. {{commitInstruction}}
108
108
  5. Say exactly: `"{{milestoneId}} context written."` — nothing else.
@@ -55,7 +55,7 @@ Once the user is ready to wrap up:
55
55
  - **Constraints** — anything the user flagged as a hard constraint
56
56
  - **Integration Points** — what this slice consumes and produces
57
57
  - **Open Questions** — anything still unresolved, with current thinking
58
- 4. Commit: `git -C {{projectRoot}} add {{contextPath}} && git -C {{projectRoot}} commit -m "docs({{milestoneId}}/{{sliceId}}): slice context from discuss"`
58
+ 4. {{commitInstruction}}
59
59
  5. Say exactly: `"{{sliceId}} context written."` — nothing else.
60
60
 
61
61
  {{inlinedTemplates}}
@@ -59,7 +59,7 @@ Then:
59
59
  - **Scope sanity:** Target 2–5 steps and 3–8 files per task. 10+ steps or 12+ files — must split. Each task must be completable in a single fresh context window.
60
60
  - **Feature completeness:** Every task produces real, user-facing progress — not just internal scaffolding.
61
61
  9. If planning produced structural decisions, append them to `.gsd/DECISIONS.md`
62
- 10. Commit: `docs({{sliceId}}): add slice plan`
62
+ 10. {{commitInstruction}}
63
63
  11. Update `.gsd/STATE.md`
64
64
 
65
65
  The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. All work stays in your working directory: `{{workingDirectory}}`.
@@ -79,9 +79,9 @@ Determine where the new milestones should go in the overall sequence. Consider d
79
79
 
80
80
  ## Output Phase
81
81
 
82
- Once the user is satisfied, in a single pass for **each** new milestone (starting from {{nextId}}):
82
+ Once the user is satisfied, in a single pass for **each** new milestone:
83
83
 
84
- 1. `mkdir -p .gsd/milestones/<ID>/slices`
84
+ 1. Call `gsd_generate_milestone_id` to get the milestone ID — never invent milestone IDs manually. Then `mkdir -p .gsd/milestones/<ID>/slices`.
85
85
  2. Write `.gsd/milestones/<ID>/<ID>-CONTEXT.md` — use the **Context** output template below. Capture intent, scope, risks, constraints, integration points, and relevant requirements. Mark the status as "Queued — pending auto-mode execution." **If this milestone depends on other milestones, add YAML frontmatter with `depends_on`:**
86
86
  ```yaml
87
87
  ---
@@ -96,7 +96,7 @@ Then, after all milestone directories and context files are written:
96
96
  4. If `.gsd/REQUIREMENTS.md` exists and the queued work introduces new in-scope capabilities or promotes Deferred items, update it.
97
97
  5. If discussion produced decisions relevant to existing work, append to `.gsd/DECISIONS.md`.
98
98
  6. Append to `.gsd/QUEUE.md`.
99
- 7. Commit: `docs: queue <milestone list>`
99
+ 7. {{commitInstruction}}
100
100
 
101
101
  **Do NOT write roadmaps for queued milestones.**
102
102
  **Do NOT update `.gsd/STATE.md`.**
@@ -57,7 +57,7 @@ Write `{{assessmentPath}}` with a brief confirmation that roadmap coverage still
57
57
  1. Rewrite the remaining (unchecked) slices in `{{roadmapPath}}`. Keep completed slices exactly as they are (`[x]`). Update the boundary map for changed slices. Update the proof strategy if risks changed. Update requirement coverage if ownership or scope changed.
58
58
  2. Write `{{assessmentPath}}` explaining what changed and why — keep it brief and concrete.
59
59
  3. If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it.
60
- 4. Commit: `docs({{milestoneId}}): reassess roadmap after {{completedSliceId}}`
60
+ 4. {{commitInstruction}}
61
61
 
62
62
  **You MUST write the file `{{assessmentPath}}` before finishing.**
63
63
 
@@ -53,7 +53,12 @@ function extractSlicesSection(content: string): string {
53
53
  export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
54
54
  const slicesSection = extractSlicesSection(content);
55
55
  const slices: RoadmapSliceEntry[] = [];
56
- if (!slicesSection) return slices;
56
+ if (!slicesSection) {
57
+ // Fallback: detect prose-style slice headers (## Slice S01: Title)
58
+ // when the LLM writes freeform prose instead of the ## Slices checklist.
59
+ // This prevents a permanent "No slice eligible" block (#807).
60
+ return parseProseSliceHeaders(content);
61
+ }
57
62
 
58
63
  const checkboxItems = slicesSection.split("\n");
59
64
  let currentSlice: RoadmapSliceEntry | null = null;
@@ -88,3 +93,42 @@ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
88
93
  if (currentSlice) slices.push(currentSlice);
89
94
  return slices;
90
95
  }
96
+
97
+ /**
98
+ * Fallback parser for prose-style roadmaps where the LLM wrote
99
+ * `## Slice S01: Title` headers instead of the machine-readable
100
+ * `## Slices` checklist. Extracts slice IDs and titles so auto-mode
101
+ * can at least identify slices and plan them.
102
+ *
103
+ * Also handles `## S01: Title` and `## S01 — Title` variants.
104
+ */
105
+ function parseProseSliceHeaders(content: string): RoadmapSliceEntry[] {
106
+ const slices: RoadmapSliceEntry[] = [];
107
+ const headerPattern = /^##\s+(?:Slice\s+)?(S\d+)[:\s—–-]+\s*(.+)/gm;
108
+ let match: RegExpExecArray | null;
109
+
110
+ while ((match = headerPattern.exec(content)) !== null) {
111
+ const id = match[1]!;
112
+ const title = match[2]!.trim();
113
+
114
+ // Try to extract depends from prose: "Depends on: S01" or "**Depends on:** S01, S02"
115
+ const afterHeader = content.slice(match.index + match[0].length);
116
+ const nextHeader = afterHeader.search(/^##\s/m);
117
+ const section = nextHeader !== -1 ? afterHeader.slice(0, nextHeader) : afterHeader.slice(0, 500);
118
+
119
+ const depsMatch = section.match(/\*{0,2}Depends\s+on:?\*{0,2}\s*(.+)/i);
120
+ let depends: string[] = [];
121
+ if (depsMatch) {
122
+ const rawDeps = depsMatch[1]!.replace(/none/i, "").trim();
123
+ if (rawDeps) {
124
+ depends = expandDependencies(
125
+ rawDeps.split(/[,;]/).map(s => s.trim().replace(/[^A-Za-z0-9]/g, "")).filter(Boolean)
126
+ );
127
+ }
128
+ }
129
+
130
+ slices.push({ id, title, risk: "medium" as RiskLevel, depends, done: false, demo: "" });
131
+ }
132
+
133
+ return slices;
134
+ }
@@ -62,7 +62,11 @@ export function isValidationTerminal(validationContent: string): boolean {
62
62
  if (!match) return false;
63
63
  const verdict = match[1].match(/verdict:\s*(\S+)/);
64
64
  if (!verdict) return false;
65
- return verdict[1] === 'pass' || verdict[1] === 'needs-attention';
65
+ // 'pass' and 'needs-attention' are always terminal.
66
+ // 'needs-remediation' is treated as terminal to prevent infinite loops
67
+ // when no remediation slices exist in the roadmap (#832). The validation
68
+ // report is preserved on disk for manual review.
69
+ return verdict[1] === 'pass' || verdict[1] === 'needs-attention' || verdict[1] === 'needs-remediation';
66
70
  }
67
71
 
68
72
  // ─── State Derivation ──────────────────────────────────────────────────────
@@ -290,19 +294,26 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
290
294
 
291
295
  if (complete) {
292
296
  // All slices done — check validation and summary state
297
+ const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
293
298
  const validationFile = resolveMilestoneFile(basePath, mid, "VALIDATION");
294
299
  const validationContent = validationFile ? await cachedLoadFile(validationFile) : null;
295
300
  const validationTerminal = validationContent ? isValidationTerminal(validationContent) : false;
296
- const summaryFile = resolveMilestoneFile(basePath, mid, "SUMMARY");
297
301
 
298
- if (!validationTerminal && !activeMilestoneFound) {
299
- // No terminal validation yet validating-milestone
302
+ if (summaryFile) {
303
+ // Summary exists milestone is complete regardless of validation state.
304
+ // The summary is the terminal artifact (#864).
305
+ registry.push({ id: mid, title, status: 'complete' });
306
+ } else if (!validationTerminal && !activeMilestoneFound) {
307
+ // No summary and no terminal validation → validating-milestone
300
308
  activeMilestone = { id: mid, title };
301
309
  activeRoadmap = roadmap;
302
310
  activeMilestoneFound = true;
303
311
  registry.push({ id: mid, title, status: 'active' });
304
- } else if (!summaryFile && !activeMilestoneFound) {
305
- // Validated but no summary written yet completing-milestone
312
+ } else if (!validationTerminal && activeMilestoneFound) {
313
+ // No summary and no terminal validation, but another milestone is already active
314
+ registry.push({ id: mid, title, status: 'pending' });
315
+ } else if (!activeMilestoneFound) {
316
+ // Terminal validation but no summary → completing-milestone
306
317
  activeMilestone = { id: mid, title };
307
318
  activeRoadmap = roadmap;
308
319
  activeMilestoneFound = true;
@@ -17,6 +17,8 @@ import {
17
17
  loadPersistedKeys,
18
18
  } from "../auto-recovery.ts";
19
19
  import { parseRoadmap, clearParseCache } from "../files.ts";
20
+ import { invalidateAllCaches } from "../cache.ts";
21
+ import { deriveState, invalidateStateCache } from "../state.ts";
20
22
 
21
23
  function makeTmpBase(): string {
22
24
  const base = join(tmpdir(), `gsd-test-${randomUUID()}`);
@@ -584,3 +586,55 @@ test("selfHealRuntimeRecords clears stale record when artifact exists at worktre
584
586
  cleanup(mainBase);
585
587
  }
586
588
  });
589
+
590
+ // ─── #793: invalidateAllCaches unblocks skip-loop ─────────────────────────
591
+ // When the skip-loop breaker fires, it must call invalidateAllCaches() (not
592
+ // just invalidateStateCache()) to clear path/parse caches that deriveState
593
+ // depends on. Without this, even after cache invalidation, deriveState reads
594
+ // stale directory listings and returns the same unit, looping forever.
595
+ test("#793: invalidateAllCaches clears all caches so deriveState sees fresh disk state", async () => {
596
+ const base = makeTmpBase();
597
+ try {
598
+ const mid = "M001";
599
+ const sid = "S01";
600
+ const planDir = join(base, ".gsd", "milestones", mid, "slices", sid);
601
+ const tasksDir = join(planDir, "tasks");
602
+ mkdirSync(tasksDir, { recursive: true });
603
+ mkdirSync(join(base, ".gsd", "milestones", mid), { recursive: true });
604
+
605
+ writeFileSync(
606
+ join(base, ".gsd", "milestones", mid, `${mid}-ROADMAP.md`),
607
+ `# M001: Test Milestone\n\n**Vision:** test.\n\n## Slices\n\n- [ ] **${sid}: Slice One** \`risk:low\` \`depends:[]\`\n > After this: done.\n`,
608
+ );
609
+ const planUnchecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [ ] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
610
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planUnchecked);
611
+ writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01: Task One\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
612
+ writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02: Task Two\n\n**Goal:** t\n\n## Steps\n- step\n\n## Verification\n- v\n");
613
+
614
+ // Warm all caches
615
+ const state1 = await deriveState(base);
616
+ assert.equal(state1.activeTask?.id, "T01", "initial: T01 is active");
617
+
618
+ // Simulate task completion on disk (what the LLM does)
619
+ const planChecked = `# ${sid}: Slice One\n\n**Goal:** test.\n\n## Tasks\n\n- [x] **T01: Task One** \`est:10m\`\n- [ ] **T02: Task Two** \`est:10m\`\n`;
620
+ writeFileSync(join(planDir, `${sid}-PLAN.md`), planChecked);
621
+ writeFileSync(join(tasksDir, "T01-SUMMARY.md"), "---\nid: T01\n---\n# Summary\n");
622
+
623
+ // invalidateStateCache alone: _stateCache cleared but path/parse caches warm
624
+ invalidateStateCache();
625
+
626
+ // invalidateAllCaches: all caches cleared — deriveState must re-read disk
627
+ invalidateAllCaches();
628
+ const state2 = await deriveState(base);
629
+
630
+ // After full invalidation, T01 should be complete and T02 should be next
631
+ assert.notEqual(state2.activeTask?.id, "T01", "#793: T01 not re-dispatched after full invalidation");
632
+
633
+ // Verify the caches are truly cleared by calling clearParseCache and clearPathCache
634
+ // do not throw (they should be no-ops after invalidateAllCaches already cleared them)
635
+ clearParseCache(); // no-op, but should not throw
636
+ assert.ok(true, "clearParseCache after invalidateAllCaches is safe");
637
+ } finally {
638
+ cleanup(base);
639
+ }
640
+ });
@@ -153,6 +153,64 @@ async function main(): Promise<void> {
153
153
  // After teardown, originalBase should be null
154
154
  assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
155
155
 
156
+ // ─── #778: reconcile plan checkboxes on re-attach ─────────────────
157
+ console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
158
+ {
159
+ // Simulate: T01 [x] was committed to milestone branch, T02 [x] was
160
+ // written to project root by syncStateToProjectRoot() but the
161
+ // auto-commit crashed before it fired. On restart the worktree is
162
+ // re-created from the milestone branch HEAD (T02 still [ ]).
163
+ // reconcilePlanCheckboxes should forward-apply T02 [x] from the root.
164
+
165
+ const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
166
+ const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
167
+ const { mkdirSync: mkdir, writeFileSync: write, readFileSync: read } = await import("node:fs");
168
+
169
+ // Plan on integration branch (project root): T01 [x], T02 [x]
170
+ mkdir(planDir, { recursive: true });
171
+ write(
172
+ join(tempDir, planRelPath),
173
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
174
+ );
175
+
176
+ // Write integration-branch plan to git so milestone branch starts from it
177
+ run(`git add .`, tempDir);
178
+ run(`git commit -m "add plan with T01 and T02 checked" --allow-empty`, tempDir);
179
+
180
+ // Create milestone branch with only T01 [x] (simulating crash before T02 commit)
181
+ const milestoneBranch = "milestone/M004";
182
+ run(`git checkout -b ${milestoneBranch}`, tempDir);
183
+ mkdir(planDir, { recursive: true });
184
+ write(
185
+ join(tempDir, planRelPath),
186
+ "# S01 Plan\n- [x] **T01:** task one\n- [ ] **T02:** task two\n- [ ] **T03:** task three\n",
187
+ );
188
+ run(`git add .`, tempDir);
189
+ run(`git commit -m "milestone: only T01 checked"`, tempDir);
190
+ run(`git checkout main`, tempDir);
191
+
192
+ // Restore project root plan (T01+T02 [x]) — simulates syncStateToProjectRoot
193
+ write(
194
+ join(tempDir, planRelPath),
195
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
196
+ );
197
+
198
+ // Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
199
+ const wtPath = createAutoWorktree(tempDir, "M004");
200
+
201
+ try {
202
+ const wtPlanPath = join(wtPath, planRelPath);
203
+ assertTrue(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
204
+
205
+ const wtPlan = read(wtPlanPath, "utf-8");
206
+ assertTrue(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
207
+ assertTrue(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
208
+ assertTrue(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
209
+ } finally {
210
+ teardownAutoWorktree(tempDir, "M004");
211
+ }
212
+ }
213
+
156
214
  } finally {
157
215
  // Always restore cwd and clean up
158
216
  process.chdir(savedCwd);
@@ -700,6 +700,76 @@ slice: S01
700
700
  }
701
701
  }
702
702
 
703
+ // ─── Test: completed M001 (summary, no validation) skipped for active M003 (#864) ────
704
+ console.log('\n=== completed milestone with summary but no validation is not active (#864) ===');
705
+ {
706
+ const base = createFixtureBase();
707
+ try {
708
+ // M001: all slices done, has summary, no validation
709
+ writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Done.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
710
+ writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: First Milestone\n\n**Completed.**');
711
+ // M003: incomplete, should be active
712
+ writeRoadmap(base, 'M003', `# M003: Active Milestone\n\n**Vision:** Do stuff.\n\n## Slices\n\n- [ ] **S01: Work slice** \`risk:low\` \`depends:[]\`\n > Needs work.\n`);
713
+
714
+ const state = await deriveState(base);
715
+ assertEq(state.activeMilestone?.id, 'M003', 'active milestone is M003, not completed M001');
716
+ const m001Entry = state.registry.find(e => e.id === 'M001');
717
+ assertEq(m001Entry?.status, 'complete', 'M001 is marked complete despite no validation');
718
+ } finally {
719
+ cleanup(base);
720
+ }
721
+ }
722
+
723
+ // ─── Test: completed M001 with summary AND validation is complete (#864) ────
724
+ console.log('\n=== completed milestone with summary and validation is complete ===');
725
+ {
726
+ const base = createFixtureBase();
727
+ try {
728
+ writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Done.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
729
+ writeMilestoneSummary(base, 'M001', '---\nid: M001\n---\n\n# M001: First Milestone\n\n**Completed.**');
730
+ writeMilestoneValidation(base, 'M001', 'pass');
731
+ writeRoadmap(base, 'M003', `# M003: Active Milestone\n\n**Vision:** Do stuff.\n\n## Slices\n\n- [ ] **S01: Work slice** \`risk:low\` \`depends:[]\`\n > Needs work.\n`);
732
+
733
+ const state = await deriveState(base);
734
+ assertEq(state.activeMilestone?.id, 'M003', 'active milestone is M003');
735
+ const m001Entry = state.registry.find(e => e.id === 'M001');
736
+ assertEq(m001Entry?.status, 'complete', 'M001 with both summary and validation is complete');
737
+ } finally {
738
+ cleanup(base);
739
+ }
740
+ }
741
+
742
+ // ─── Test: all slices done, no summary, no validation → needs validation (#864) ────
743
+ console.log('\n=== all slices done, no summary, no validation → validating-milestone ===');
744
+ {
745
+ const base = createFixtureBase();
746
+ try {
747
+ writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Validate me.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
748
+ // No summary, no validation — this should be active for validation
749
+
750
+ const state = await deriveState(base);
751
+ assertEq(state.activeMilestone?.id, 'M001', 'M001 is active for validation');
752
+ } finally {
753
+ cleanup(base);
754
+ }
755
+ }
756
+
757
+ // ─── Test: all slices done, validation pass, no summary → needs completion (#864) ────
758
+ console.log('\n=== all slices done, validation pass, no summary → completing-milestone ===');
759
+ {
760
+ const base = createFixtureBase();
761
+ try {
762
+ writeRoadmap(base, 'M001', `# M001: First Milestone\n\n**Vision:** Complete me.\n\n## Slices\n\n- [x] **S01: Done slice** \`risk:low\` \`depends:[]\`\n > Completed.\n`);
763
+ writeMilestoneValidation(base, 'M001', 'pass');
764
+ // No summary — validated but not yet completed
765
+
766
+ const state = await deriveState(base);
767
+ assertEq(state.activeMilestone?.id, 'M001', 'M001 is active for completion');
768
+ } finally {
769
+ cleanup(base);
770
+ }
771
+ }
772
+
703
773
  report();
704
774
  }
705
775
 
@@ -188,7 +188,7 @@ async function main(): Promise<void> {
188
188
  cleanups.push(dir);
189
189
  mkdirSync(join(dir, ".gsd"), { recursive: true });
190
190
 
191
- const result = preDispatchHealthGate(dir);
191
+ const result = await preDispatchHealthGate(dir);
192
192
  assertTrue(result.proceed, "gate passes on clean state");
193
193
  assertEq(result.issues.length, 0, "no issues on clean state");
194
194
  }
@@ -206,7 +206,7 @@ async function main(): Promise<void> {
206
206
  unitStartedAt: "2026-03-10T00:01:00Z", completedUnits: 3,
207
207
  }));
208
208
 
209
- const result = preDispatchHealthGate(dir);
209
+ const result = await preDispatchHealthGate(dir);
210
210
  assertTrue(result.proceed, "gate passes after auto-clearing stale lock");
211
211
  assertTrue(result.fixesApplied.some(f => f.includes("cleared stale auto.lock")), "reports lock cleared");
212
212
  assertTrue(!existsSync(join(dir, ".gsd", "auto.lock")), "lock file removed");
@@ -222,7 +222,7 @@ async function main(): Promise<void> {
222
222
  const headHash = run("git rev-parse HEAD", dir);
223
223
  writeFileSync(join(dir, ".git", "MERGE_HEAD"), headHash + "\n");
224
224
 
225
- const result = preDispatchHealthGate(dir);
225
+ const result = await preDispatchHealthGate(dir);
226
226
  assertTrue(result.proceed, "gate passes after auto-healing merge state");
227
227
  assertTrue(result.fixesApplied.some(f => f.includes("cleaned merge state")), "reports merge state cleaned");
228
228
  assertTrue(!existsSync(join(dir, ".git", "MERGE_HEAD")), "MERGE_HEAD removed");
@@ -231,6 +231,26 @@ async function main(): Promise<void> {
231
231
  console.log(" (skipped on Windows)");
232
232
  }
233
233
 
234
+ console.log("\n=== health gate: STATE.md missing — auto-healed ===");
235
+ {
236
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "doc-proactive-")));
237
+ cleanups.push(dir);
238
+ // Minimal .gsd structure: milestones dir exists but no STATE.md
239
+ mkdirSync(join(dir, ".gsd", "milestones"), { recursive: true });
240
+
241
+ const stateFile = join(dir, ".gsd", "STATE.md");
242
+ assertTrue(!existsSync(stateFile), "STATE.md does not exist before gate");
243
+
244
+ const result = await preDispatchHealthGate(dir);
245
+ assertTrue(result.proceed, "gate passes after rebuilding STATE.md");
246
+ assertTrue(
247
+ result.fixesApplied.some(f => f.includes("rebuilt missing STATE.md")),
248
+ "reports STATE.md rebuilt",
249
+ );
250
+ assertTrue(existsSync(stateFile), "STATE.md created by auto-heal");
251
+ assertTrue(result.issues.length === 0, "no blocking issues after heal");
252
+ }
253
+
234
254
  } finally {
235
255
  resetProactiveHealing();
236
256
  for (const dir of cleanups) {
@@ -5,6 +5,7 @@ import { execSync } from "node:child_process";
5
5
 
6
6
  import {
7
7
  inferCommitType,
8
+ buildTaskCommitMessage,
8
9
  GitServiceImpl,
9
10
  RUNTIME_EXCLUSION_PATHS,
10
11
  VALID_BRANCH_NAME,
@@ -14,6 +15,7 @@ import {
14
15
  type GitPreferences,
15
16
  type CommitOptions,
16
17
  type PreMergeCheckResult,
18
+ type TaskCommitContext,
17
19
  } from "../git-service.ts";
18
20
  import { createTestContext } from './test-helpers.ts';
19
21
 
@@ -188,6 +190,58 @@ async function main(): Promise<void> {
188
190
  "'prefix' does not match 'fix' — word boundary prevents partial match"
189
191
  );
190
192
 
193
+ // ─── inferCommitType with oneLiner ──────────────────────────────────────
194
+
195
+ console.log("\n=== inferCommitType with oneLiner ===");
196
+
197
+ assertEq(
198
+ inferCommitType("implement dashboard", "Fixed rendering bug in sidebar"),
199
+ "fix",
200
+ "one-liner with 'fixed' overrides generic title → fix"
201
+ );
202
+
203
+ assertEq(
204
+ inferCommitType("add search", "Optimized query performance with caching"),
205
+ "perf",
206
+ "one-liner with 'performance' and 'caching' → perf"
207
+ );
208
+
209
+ // ─── buildTaskCommitMessage ─────────────────────────────────────────────
210
+
211
+ console.log("\n=== buildTaskCommitMessage ===");
212
+
213
+ {
214
+ const msg = buildTaskCommitMessage({
215
+ taskId: "S01/T02",
216
+ taskTitle: "implement user authentication",
217
+ oneLiner: "Added JWT-based auth with refresh token rotation",
218
+ keyFiles: ["src/auth.ts", "src/middleware/jwt.ts"],
219
+ });
220
+ assertTrue(msg.startsWith("feat(S01/T02):"), "message starts with type(scope)");
221
+ assertTrue(msg.includes("JWT-based auth"), "message includes one-liner content");
222
+ assertTrue(msg.includes("- src/auth.ts"), "message body includes key files");
223
+ assertTrue(msg.includes("- src/middleware/jwt.ts"), "message body includes second key file");
224
+ }
225
+
226
+ {
227
+ const msg = buildTaskCommitMessage({
228
+ taskId: "S02/T01",
229
+ taskTitle: "fix login redirect bug",
230
+ });
231
+ assertTrue(msg.startsWith("fix(S02/T01):"), "infers fix type from title");
232
+ assertTrue(msg.includes("fix login redirect bug"), "uses task title when no one-liner");
233
+ assertTrue(!msg.includes("\n"), "no body when no key files");
234
+ }
235
+
236
+ {
237
+ const msg = buildTaskCommitMessage({
238
+ taskId: "S01/T03",
239
+ taskTitle: "add tests",
240
+ oneLiner: "Unit tests for auth module with coverage",
241
+ });
242
+ assertTrue(msg.startsWith("test(S01/T03):"), "infers test type");
243
+ }
244
+
191
245
  // ─── RUNTIME_EXCLUSION_PATHS ───────────────────────────────────────────
192
246
 
193
247
  console.log("\n=== RUNTIME_EXCLUSION_PATHS ===");
@@ -430,13 +484,25 @@ async function main(): Promise<void> {
430
484
  const svc = new GitServiceImpl(repo);
431
485
 
432
486
  createFile(repo, "src/new-feature.ts", "export const x = 1;");
433
- const msg = svc.autoCommit("task", "T01");
434
487
 
435
- assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns correct message format");
488
+ // Without task context, autoCommit uses generic chore message
489
+ const msg = svc.autoCommit("task", "T01");
490
+ assertEq(msg, "chore(T01): auto-commit after task", "autoCommit returns generic format without task context");
436
491
 
437
- // Verify the commit exists
438
492
  const log = run("git log --oneline -1", repo);
439
- assertTrue(log.includes("chore(T01): auto-commit after task"), "commit message is in git log");
493
+ assertTrue(log.includes("chore(T01): auto-commit after task"), "generic commit message is in git log");
494
+
495
+ // With task context, autoCommit uses meaningful message
496
+ createFile(repo, "src/auth.ts", "export function login() {}");
497
+ const msg2 = svc.autoCommit("task", "S01/T02", [], {
498
+ taskId: "S01/T02",
499
+ taskTitle: "implement user authentication endpoint",
500
+ oneLiner: "Added JWT-based auth with refresh token rotation",
501
+ keyFiles: ["src/auth.ts"],
502
+ });
503
+ assertTrue(msg2 !== null, "autoCommit with task context returns a message");
504
+ assertTrue(msg2!.startsWith("feat(S01/T02):"), "meaningful commit uses feat type and scope");
505
+ assertTrue(msg2!.includes("JWT-based auth"), "meaningful commit includes one-liner content");
440
506
 
441
507
  rmSync(repo, { recursive: true, force: true });
442
508
  }
@@ -65,8 +65,8 @@ console.log('\n=== gsd-db: fresh DB schema init (memory) ===');
65
65
 
66
66
  // Check schema_version table
67
67
  const adapter = _getAdapter()!;
68
- const version = adapter.prepare('SELECT version FROM schema_version').get();
69
- assertEq(version?.['version'], 2, 'schema version should be 2');
68
+ const version = adapter.prepare('SELECT MAX(version) as version FROM schema_version').get();
69
+ assertEq(version?.['version'], 3, 'schema version should be 3');
70
70
 
71
71
  // Check tables exist by querying them
72
72
  const dRows = adapter.prepare('SELECT count(*) as cnt FROM decisions').get();
@@ -350,12 +350,11 @@ console.log('=== md-importer: missing file handling ===');
350
350
  console.log('=== md-importer: schema v1→v2 migration ===');
351
351
 
352
352
  {
353
- // This test verifies that opening a v1 DB auto-migrates to v2
354
- // (The actual migration is tested via the gsd-db.test.ts schema version assertion = 2)
353
+ // This test verifies that opening a fresh DB auto-migrates to current schema version
355
354
  openDatabase(':memory:');
356
355
  const adapter = _getAdapter();
357
356
  const version = adapter?.prepare('SELECT MAX(version) as v FROM schema_version').get();
358
- assertEq(version?.v, 2, 'new DB should be at schema version 2');
357
+ assertEq(version?.v, 3, 'new DB should be at schema version 3');
359
358
 
360
359
  // Artifacts table should exist
361
360
  const tableCheck = adapter?.prepare("SELECT count(*) as c FROM sqlite_master WHERE type='table' AND name='artifacts'").get();