gsd-pi 2.8.0 → 2.8.2

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 (143) hide show
  1. package/dist/loader.js +5 -0
  2. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts +2 -0
  3. package/node_modules/@gsd/pi-coding-agent/dist/config.d.ts.map +1 -1
  4. package/node_modules/@gsd/pi-coding-agent/dist/config.js +4 -0
  5. package/node_modules/@gsd/pi-coding-agent/dist/config.js.map +1 -1
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js +2 -2
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js +97 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js +112 -3
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js +32 -22
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts +3 -1
  33. package/node_modules/@gsd/pi-coding-agent/dist/index.d.ts.map +1 -1
  34. package/node_modules/@gsd/pi-coding-agent/dist/index.js +4 -1
  35. package/node_modules/@gsd/pi-coding-agent/dist/index.js.map +1 -1
  36. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  37. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  38. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  39. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  41. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js +11 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/utils/shell.js.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/src/config.ts +5 -0
  44. package/node_modules/@gsd/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  45. package/node_modules/@gsd/pi-coding-agent/src/core/bash-executor.ts +2 -2
  46. package/node_modules/@gsd/pi-coding-agent/src/core/blob-store.ts +106 -0
  47. package/node_modules/@gsd/pi-coding-agent/src/core/session-manager.ts +119 -3
  48. package/node_modules/@gsd/pi-coding-agent/src/core/tools/bash.ts +35 -22
  49. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  50. package/node_modules/@gsd/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  51. package/node_modules/@gsd/pi-coding-agent/src/index.ts +4 -1
  52. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  53. package/node_modules/@gsd/pi-coding-agent/src/utils/shell.ts +11 -0
  54. package/package.json +6 -1
  55. package/packages/pi-coding-agent/dist/config.d.ts +2 -0
  56. package/packages/pi-coding-agent/dist/config.d.ts.map +1 -1
  57. package/packages/pi-coding-agent/dist/config.js +4 -0
  58. package/packages/pi-coding-agent/dist/config.js.map +1 -1
  59. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts +52 -0
  60. package/packages/pi-coding-agent/dist/core/artifact-manager.d.ts.map +1 -0
  61. package/packages/pi-coding-agent/dist/core/artifact-manager.js +117 -0
  62. package/packages/pi-coding-agent/dist/core/artifact-manager.js.map +1 -0
  63. package/packages/pi-coding-agent/dist/core/bash-executor.js +2 -2
  64. package/packages/pi-coding-agent/dist/core/bash-executor.js.map +1 -1
  65. package/packages/pi-coding-agent/dist/core/blob-store.d.ts +31 -0
  66. package/packages/pi-coding-agent/dist/core/blob-store.d.ts.map +1 -0
  67. package/packages/pi-coding-agent/dist/core/blob-store.js +97 -0
  68. package/packages/pi-coding-agent/dist/core/blob-store.js.map +1 -0
  69. package/packages/pi-coding-agent/dist/core/session-manager.d.ts +1 -0
  70. package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
  71. package/packages/pi-coding-agent/dist/core/session-manager.js +112 -3
  72. package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
  73. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts +4 -0
  74. package/packages/pi-coding-agent/dist/core/tools/bash.d.ts.map +1 -1
  75. package/packages/pi-coding-agent/dist/core/tools/bash.js +32 -22
  76. package/packages/pi-coding-agent/dist/core/tools/bash.js.map +1 -1
  77. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts +1 -1
  78. package/packages/pi-coding-agent/dist/core/tools/path-utils.d.ts.map +1 -1
  79. package/packages/pi-coding-agent/dist/core/tools/path-utils.js +13 -2
  80. package/packages/pi-coding-agent/dist/core/tools/path-utils.js.map +1 -1
  81. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts +2 -0
  82. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js +57 -0
  84. package/packages/pi-coding-agent/dist/core/tools/path-utils.test.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/index.d.ts +3 -1
  86. package/packages/pi-coding-agent/dist/index.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/index.js +4 -1
  88. package/packages/pi-coding-agent/dist/index.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  90. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +7 -5
  91. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/utils/shell.d.ts +7 -0
  93. package/packages/pi-coding-agent/dist/utils/shell.d.ts.map +1 -1
  94. package/packages/pi-coding-agent/dist/utils/shell.js +11 -0
  95. package/packages/pi-coding-agent/dist/utils/shell.js.map +1 -1
  96. package/packages/pi-coding-agent/src/config.ts +5 -0
  97. package/packages/pi-coding-agent/src/core/artifact-manager.ts +125 -0
  98. package/packages/pi-coding-agent/src/core/bash-executor.ts +2 -2
  99. package/packages/pi-coding-agent/src/core/blob-store.ts +106 -0
  100. package/packages/pi-coding-agent/src/core/session-manager.ts +119 -3
  101. package/packages/pi-coding-agent/src/core/tools/bash.ts +35 -22
  102. package/packages/pi-coding-agent/src/core/tools/path-utils.test.ts +66 -0
  103. package/packages/pi-coding-agent/src/core/tools/path-utils.ts +14 -2
  104. package/packages/pi-coding-agent/src/index.ts +4 -1
  105. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +6 -4
  106. package/packages/pi-coding-agent/src/utils/shell.ts +11 -0
  107. package/src/resources/extensions/bg-shell/index.ts +2 -1
  108. package/src/resources/extensions/browser-tools/lifecycle.ts +6 -1
  109. package/src/resources/extensions/gsd/auto.ts +92 -49
  110. package/src/resources/extensions/gsd/dispatch-guard.ts +65 -0
  111. package/src/resources/extensions/gsd/docs/preferences-reference.md +76 -0
  112. package/src/resources/extensions/gsd/exit-command.ts +18 -0
  113. package/src/resources/extensions/gsd/files.ts +9 -40
  114. package/src/resources/extensions/gsd/git-service.ts +62 -17
  115. package/src/resources/extensions/gsd/gitignore.ts +28 -0
  116. package/src/resources/extensions/gsd/guided-flow.ts +49 -11
  117. package/src/resources/extensions/gsd/index.ts +111 -16
  118. package/src/resources/extensions/gsd/preferences.ts +8 -0
  119. package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -2
  120. package/src/resources/extensions/gsd/prompts/complete-slice.md +3 -3
  121. package/src/resources/extensions/gsd/prompts/discuss.md +27 -2
  122. package/src/resources/extensions/gsd/prompts/execute-task.md +2 -2
  123. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -3
  124. package/src/resources/extensions/gsd/prompts/plan-milestone.md +1 -1
  125. package/src/resources/extensions/gsd/prompts/plan-slice.md +2 -2
  126. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +3 -3
  127. package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
  128. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  129. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  130. package/src/resources/extensions/gsd/prompts/run-uat.md +4 -4
  131. package/src/resources/extensions/gsd/roadmap-slices.ts +50 -0
  132. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +102 -0
  133. package/src/resources/extensions/gsd/tests/exit-command.test.ts +50 -0
  134. package/src/resources/extensions/gsd/tests/git-service.test.ts +116 -39
  135. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +5 -5
  136. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +2 -1
  137. package/src/resources/extensions/gsd/tests/roadmap-slices.test.ts +59 -0
  138. package/src/resources/extensions/gsd/tests/run-uat.test.ts +2 -4
  139. package/src/resources/extensions/gsd/tests/write-gate.test.ts +122 -0
  140. package/src/resources/extensions/ttsr/index.ts +163 -0
  141. package/src/resources/extensions/ttsr/rule-loader.ts +121 -0
  142. package/src/resources/extensions/ttsr/ttsr-interrupt.md +6 -0
  143. package/src/resources/extensions/ttsr/ttsr-manager.ts +344 -0
@@ -76,6 +76,6 @@ If this milestone requires any external API keys or secrets:
76
76
 
77
77
  If this milestone does not require any external API keys or secrets, skip this step entirely — do not create an empty manifest.
78
78
 
79
- **You MUST write the file `{{outputAbsPath}}` before finishing.**
79
+ **You MUST write the file `{{outputPath}}` before finishing.**
80
80
 
81
81
  When done, say: "Milestone {{milestoneId}} planned."
@@ -37,7 +37,7 @@ Then:
37
37
  - a matching task plan file with description, steps, must-haves, verification, inputs, and expected output
38
38
  - Observability Impact section **only if the task touches runtime boundaries, async flows, or error paths** — omit it otherwise
39
39
  6. Write `{{outputPath}}`
40
- 7. Write individual task plans in `{{sliceAbsPath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc.
40
+ 7. Write individual task plans in `{{slicePath}}/tasks/`: `T01-PLAN.md`, `T02-PLAN.md`, etc.
41
41
  8. **Self-audit the plan.** Walk through each check — if any fail, fix the plan files before moving on:
42
42
  - **Completion semantics:** If every task were completed exactly as written, the slice goal/demo should actually be true.
43
43
  - **Requirement coverage:** Every must-have in the slice maps to at least one task. No must-have is orphaned. If `REQUIREMENTS.md` exists, every Active requirement this slice owns maps to at least one task.
@@ -52,6 +52,6 @@ Then:
52
52
 
53
53
  The slice directory and tasks/ subdirectory already exist. Do NOT mkdir. You are on the slice branch; all work stays here.
54
54
 
55
- **You MUST write the file `{{outputAbsPath}}` before finishing.**
55
+ **You MUST write the file `{{outputPath}}` before finishing.**
56
56
 
57
57
  When done, say: "Slice {{sliceId}} planned."
@@ -34,15 +34,15 @@ If all criteria have at least one remaining owning slice, the coverage check pas
34
34
 
35
35
  **If the roadmap is still good:**
36
36
 
37
- Write `{{assessmentAbsPath}}` with a brief confirmation that roadmap coverage still holds after {{completedSliceId}}. If requirements exist, explicitly note whether requirement coverage remains sound.
37
+ Write `{{assessmentPath}}` with a brief confirmation that roadmap coverage still holds after {{completedSliceId}}. If requirements exist, explicitly note whether requirement coverage remains sound.
38
38
 
39
39
  **If changes are needed:**
40
40
 
41
41
  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.
42
- 2. Write `{{assessmentAbsPath}}` explaining what changed and why — keep it brief and concrete.
42
+ 2. Write `{{assessmentPath}}` explaining what changed and why — keep it brief and concrete.
43
43
  3. If `.gsd/REQUIREMENTS.md` exists and requirement ownership or status changed, update it.
44
44
  4. Commit: `docs({{milestoneId}}): reassess roadmap after {{completedSliceId}}`
45
45
 
46
- **You MUST write the file `{{assessmentAbsPath}}` before finishing.**
46
+ **You MUST write the file `{{assessmentPath}}` before finishing.**
47
47
 
48
48
  When done, say: "Roadmap reassessed."
@@ -20,7 +20,7 @@ All relevant context has been preloaded below — the roadmap, current slice pla
20
20
 
21
21
  1. Read the blocker task summary carefully. Understand exactly what was discovered and why it blocks the current plan.
22
22
  2. Analyze the remaining `[ ]` tasks in the slice plan. Determine which are still valid, which need modification, and which should be replaced.
23
- 3. Write `{{replanAbsPath}}` documenting:
23
+ 3. Write `{{replanPath}}` documenting:
24
24
  - What blocker was discovered and in which task
25
25
  - What changed in the plan and why
26
26
  - Which incomplete tasks were modified, added, or removed
@@ -34,6 +34,6 @@ All relevant context has been preloaded below — the roadmap, current slice pla
34
34
  6. Do not commit manually — the system auto-commits your changes after this unit completes.
35
35
  7. Update `.gsd/STATE.md`
36
36
 
37
- **You MUST write `{{replanAbsPath}}` and the updated slice plan before finishing.**
37
+ **You MUST write `{{replanPath}}` and the updated slice plan before finishing.**
38
38
 
39
39
  When done, say: "Slice {{sliceId}} replanned."
@@ -32,6 +32,6 @@ Then research the codebase and relevant technologies. Narrate key findings and s
32
32
 
33
33
  **Research is advisory, not auto-binding.** Surface candidate requirements clearly instead of silently expanding scope.
34
34
 
35
- **You MUST write the file `{{outputAbsPath}}` before finishing.**
35
+ **You MUST write the file `{{outputPath}}` before finishing.**
36
36
 
37
37
  When done, say: "Milestone {{milestoneId}} researched."
@@ -23,6 +23,6 @@ Then research what this slice needs. Narrate key findings and surprises as you g
23
23
 
24
24
  The slice directory already exists at `{{slicePath}}/`. Do NOT mkdir — just write the file.
25
25
 
26
- **You MUST write the file `{{outputAbsPath}}` before finishing.**
26
+ **You MUST write the file `{{outputPath}}` before finishing.**
27
27
 
28
28
  When done, say: "Slice {{sliceId}} researched."
@@ -14,7 +14,7 @@ If a `GSD Skill Preferences` block is present in system context, use it to decid
14
14
 
15
15
  **UAT file:** `{{uatPath}}`
16
16
  **UAT type:** `{{uatType}}`
17
- **Result file to write:** `{{uatResultAbsPath}}` (relative: `{{uatResultPath}}`)
17
+ **Result file to write:** `{{uatResultPath}}`
18
18
 
19
19
  ### If UAT type is `artifact-driven`
20
20
 
@@ -37,7 +37,7 @@ After running all checks, compute the **overall verdict**:
37
37
  - `FAIL` — one or more checks failed
38
38
  - `PARTIAL` — some checks passed, some failed or were skipped
39
39
 
40
- Write `{{uatResultAbsPath}}` with:
40
+ Write `{{uatResultPath}}` with:
41
41
 
42
42
  ```markdown
43
43
  ---
@@ -68,7 +68,7 @@ date: <ISO 8601 timestamp>
68
68
 
69
69
  This UAT type requires human execution or live-runtime observation that you cannot perform mechanically. Your role is to surface it clearly for review.
70
70
 
71
- Write `{{uatResultAbsPath}}` with:
71
+ Write `{{uatResultPath}}` with:
72
72
 
73
73
  ```markdown
74
74
  ---
@@ -104,6 +104,6 @@ Once updated, run `/gsd auto` to resume auto-mode.
104
104
 
105
105
  ---
106
106
 
107
- **You MUST write `{{uatResultAbsPath}}` before finishing.**
107
+ **You MUST write `{{uatResultPath}}` before finishing.**
108
108
 
109
109
  When done, say: "UAT {{sliceId}} complete."
@@ -0,0 +1,50 @@
1
+ import type { RoadmapSliceEntry, RiskLevel } from "./types.ts";
2
+
3
+ function extractSlicesSection(content: string): string {
4
+ const headingMatch = /^## Slices\s*$/m.exec(content);
5
+ if (!headingMatch || headingMatch.index == null) return "";
6
+
7
+ const start = headingMatch.index + headingMatch[0].length;
8
+ const rest = content.slice(start).replace(/^\r?\n/, "");
9
+ const nextHeading = /^##\s+/m.exec(rest);
10
+ return (nextHeading ? rest.slice(0, nextHeading.index) : rest).trimEnd();
11
+ }
12
+
13
+ export function parseRoadmapSlices(content: string): RoadmapSliceEntry[] {
14
+ const slicesSection = extractSlicesSection(content);
15
+ const slices: RoadmapSliceEntry[] = [];
16
+ if (!slicesSection) return slices;
17
+
18
+ const checkboxItems = slicesSection.split("\n");
19
+ let currentSlice: RoadmapSliceEntry | null = null;
20
+
21
+ for (const line of checkboxItems) {
22
+ const cbMatch = line.match(/^\s*-\s+\[([ xX])\]\s+\*\*(\w+):\s+(.+?)\*\*\s*(.*)/);
23
+ if (cbMatch) {
24
+ if (currentSlice) slices.push(currentSlice);
25
+
26
+ const done = cbMatch[1].toLowerCase() === "x";
27
+ const id = cbMatch[2]!;
28
+ const title = cbMatch[3]!;
29
+ const rest = cbMatch[4] ?? "";
30
+
31
+ const riskMatch = rest.match(/`risk:(\w+)`/);
32
+ const risk = (riskMatch ? riskMatch[1] : "low") as RiskLevel;
33
+
34
+ const depsMatch = rest.match(/`depends:\[([^\]]*)\]`/);
35
+ const depends = depsMatch && depsMatch[1]!.trim()
36
+ ? depsMatch[1]!.split(",").map(s => s.trim())
37
+ : [];
38
+
39
+ currentSlice = { id, title, risk, depends, done, demo: "" };
40
+ continue;
41
+ }
42
+
43
+ if (currentSlice && line.trim().startsWith(">")) {
44
+ currentSlice.demo = line.trim().replace(/^>\s*/, "").replace(/^After this:\s*/i, "");
45
+ }
46
+ }
47
+
48
+ if (currentSlice) slices.push(currentSlice);
49
+ return slices;
50
+ }
@@ -0,0 +1,102 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { execSync } from "node:child_process";
3
+ import { join } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { getPriorSliceCompletionBlocker } from "../dispatch-guard.ts";
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function assertEq<T>(actual: T, expected: T, message: string): void {
11
+ if (JSON.stringify(actual) === JSON.stringify(expected)) {
12
+ passed += 1;
13
+ return;
14
+ }
15
+ failed += 1;
16
+ console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
17
+ }
18
+
19
+ function run(command: string, cwd: string): void {
20
+ execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"] });
21
+ }
22
+
23
+ const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-"));
24
+ try {
25
+ mkdirSync(join(repo, ".gsd", "milestones", "M002"), { recursive: true });
26
+ mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
27
+
28
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
29
+ "# M002: Previous",
30
+ "",
31
+ "## Slices",
32
+ "- [x] **S01: Done** `risk:low` `depends:[]`",
33
+ "- [ ] **S02: Pending** `risk:low` `depends:[S01]`",
34
+ "",
35
+ ].join("\n"));
36
+
37
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
38
+ "# M003: Current",
39
+ "",
40
+ "## Slices",
41
+ "- [ ] **S01: First** `risk:low` `depends:[]`",
42
+ "- [ ] **S02: Second** `risk:low` `depends:[S01]`",
43
+ "",
44
+ ].join("\n"));
45
+
46
+ run("git init -b main", repo);
47
+ run("git config user.email test@example.com", repo);
48
+ run("git config user.name Test", repo);
49
+ run("git add .", repo);
50
+ run("git commit -m init", repo);
51
+
52
+ assertEq(
53
+ getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M003/S01"),
54
+ "Cannot dispatch plan-slice M003/S01: earlier slice M002/S02 is not complete on main.",
55
+ "blocks first slice of next milestone when prior milestone is incomplete on main",
56
+ );
57
+
58
+ writeFileSync(join(repo, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), [
59
+ "# M002: Previous",
60
+ "",
61
+ "## Slices",
62
+ "- [x] **S01: Done** `risk:low` `depends:[]`",
63
+ "- [x] **S02: Done** `risk:low` `depends:[S01]`",
64
+ "",
65
+ ].join("\n"));
66
+ run("git add .", repo);
67
+ run("git commit -m complete-m002", repo);
68
+
69
+ assertEq(
70
+ getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
71
+ "Cannot dispatch execute-task M003/S02/T01: earlier slice M003/S01 is not complete on main.",
72
+ "blocks later slice in same milestone when an earlier slice is incomplete on main",
73
+ );
74
+
75
+ writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"), [
76
+ "# M003: Current",
77
+ "",
78
+ "## Slices",
79
+ "- [x] **S01: First** `risk:low` `depends:[]`",
80
+ "- [ ] **S02: Second** `risk:low` `depends:[S01]`",
81
+ "",
82
+ ].join("\n"));
83
+ run("git add .", repo);
84
+ run("git commit -m complete-m003-s01", repo);
85
+
86
+ assertEq(
87
+ getPriorSliceCompletionBlocker(repo, "main", "execute-task", "M003/S02/T01"),
88
+ null,
89
+ "allows dispatch when all earlier slices are complete on main",
90
+ );
91
+
92
+ assertEq(
93
+ getPriorSliceCompletionBlocker(repo, "main", "plan-milestone", "M003"),
94
+ null,
95
+ "does not affect non-slice dispatch types",
96
+ );
97
+ } finally {
98
+ rmSync(repo, { recursive: true, force: true });
99
+ }
100
+
101
+ console.log(`Passed: ${passed}, Failed: ${failed}`);
102
+ if (failed > 0) process.exit(1);
@@ -0,0 +1,50 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { registerExitCommand } from "../exit-command.ts";
5
+
6
+ test("/exit requests graceful shutdown instead of process.exit", async () => {
7
+ const commands = new Map<
8
+ string,
9
+ {
10
+ description?: string;
11
+ handler: (args: string, ctx: { shutdown: () => Promise<void> }) => Promise<void>;
12
+ }
13
+ >();
14
+
15
+ const pi = {
16
+ registerCommand(name: string, options: any) {
17
+ commands.set(name, options);
18
+ },
19
+ };
20
+
21
+ let stopAutoCalls = 0;
22
+ registerExitCommand(pi as any, {
23
+ async stopAuto() {
24
+ stopAutoCalls += 1;
25
+ },
26
+ });
27
+
28
+ const exit = commands.get("exit");
29
+ assert.ok(exit, "registerExitCommand should register /exit");
30
+ assert.equal(exit.description, "Exit GSD gracefully");
31
+
32
+ let shutdownCalls = 0;
33
+ const originalExit = process.exit;
34
+ process.exit = ((code?: number) => {
35
+ throw new Error(`process.exit should not be called: ${code ?? "undefined"}`);
36
+ }) as typeof process.exit;
37
+
38
+ try {
39
+ await exit.handler("", {
40
+ async shutdown() {
41
+ shutdownCalls += 1;
42
+ },
43
+ });
44
+ } finally {
45
+ process.exit = originalExit;
46
+ }
47
+
48
+ assert.equal(stopAutoCalls, 1, "handler should stop auto-mode exactly once before shutdown");
49
+ assert.equal(shutdownCalls, 1, "handler should request graceful shutdown exactly once");
50
+ });
@@ -1,4 +1,4 @@
1
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
1
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
2
2
  import { join, dirname } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  import { execSync } from "node:child_process";
@@ -210,8 +210,8 @@ async function main(): Promise<void> {
210
210
 
211
211
  assertEq(
212
212
  RUNTIME_EXCLUSION_PATHS.length,
213
- 6,
214
- "exactly 6 runtime exclusion paths"
213
+ 7,
214
+ "exactly 7 runtime exclusion paths"
215
215
  );
216
216
 
217
217
  const expectedPaths = [
@@ -220,6 +220,7 @@ async function main(): Promise<void> {
220
220
  ".gsd/worktrees/",
221
221
  ".gsd/auto.lock",
222
222
  ".gsd/metrics.json",
223
+ ".gsd/completed-units.json",
223
224
  ".gsd/STATE.md",
224
225
  ];
225
226
 
@@ -347,52 +348,76 @@ async function main(): Promise<void> {
347
348
  rmSync(repo, { recursive: true, force: true });
348
349
  }
349
350
 
350
- // ─── GitServiceImpl: smart staging fallback ────────────────────────────
351
+ // ─── GitServiceImpl: smart staging excludes tracked runtime files ──────
351
352
 
352
- console.log("\n=== GitServiceImpl: smart staging fallback ===");
353
+ console.log("\n=== GitServiceImpl: smart staging excludes tracked runtime files ===");
353
354
 
354
355
  {
355
- // We can't easily make the pathspec fail in a real repo, but we can test
356
- // the fallback behavior by verifying that if smart staging somehow fails,
357
- // everything gets staged. We do this by checking that a commit with both
358
- // runtime and real files works when pathspec would fail.
356
+ // Reproduces the real bug: .gsd/ runtime files that are already tracked
357
+ // (in the git index) must be excluded from staging even when .gsd/ is
358
+ // in .gitignore. The old pathspec-exclude approach failed silently in
359
+ // this case and fell back to `git add -A`, staging everything.
359
360
  //
360
- // To force the fallback: temporarily override RUNTIME_EXCLUSION_PATHS
361
- // with an invalid pathspec. Since we can't modify a readonly array,
362
- // we'll test the actual fallback by creating a custom subclass.
361
+ // The fix has three layers:
362
+ // 1. Auto-cleanup: git rm --cached removes tracked runtime files from index
363
+ // 2. Stage-then-unstage: git add -A + git reset HEAD replaces pathspec excludes
364
+ // 3. Pre-checkout discard: git checkout -- .gsd/ clears dirty runtime files
363
365
 
364
366
  const repo = initTempRepo();
367
+ const svc = new GitServiceImpl(repo);
365
368
 
366
- // Create a subclass that overrides smartStage to simulate failure + fallback
367
- class FallbackTestService extends GitServiceImpl {
368
- fallbackUsed = false;
369
- smartStageWithBadPathspec(): void {
370
- // Simulate: try bad pathspec, catch, fallback
371
- try {
372
- runGit(this.basePath, ["add", "-A", "--", ".", ":(exclude)__NONEXISTENT_PATHSPEC_SYNTAX_ERROR__["]);
373
- // If the above doesn't throw, git accepted it (some versions do).
374
- // That's fine — the point is testing the fallback path.
375
- throw new Error("force fallback for test");
376
- } catch {
377
- console.error("GitService: smart staging failed, falling back to git add -A");
378
- this.fallbackUsed = true;
379
- runGit(this.basePath, ["add", "-A"]);
380
- }
381
- }
382
- }
383
-
384
- const svc = new FallbackTestService(repo);
369
+ // Simulate a repo where .gsd/ files were previously force-added
370
+ createFile(repo, ".gsd/metrics.json", '{"version":1}');
371
+ createFile(repo, ".gsd/completed-units.json", '["unit1"]');
372
+ createFile(repo, ".gsd/activity/log.jsonl", '{"ts":1}');
385
373
  createFile(repo, "src/real.ts", "real code");
386
- createFile(repo, ".gsd/activity/log.jsonl", "log");
374
+ // Force-add .gsd/ files to simulate historical tracking
375
+ runGit(repo, ["add", "-f", ".gsd/metrics.json", ".gsd/completed-units.json", ".gsd/activity/log.jsonl", "src/real.ts"]);
376
+ runGit(repo, ["commit", "-F", "-"], { input: "init with tracked runtime files" });
377
+
378
+ // Add .gitignore with .gsd/ (matches real-world setup from ensureGitignore)
379
+ createFile(repo, ".gitignore", ".gsd/\n");
380
+ runGit(repo, ["add", ".gitignore"]);
381
+ runGit(repo, ["commit", "-F", "-"], { input: "add gitignore" });
382
+
383
+ // Verify runtime files are tracked (precondition)
384
+ const tracked = run("git ls-files .gsd/", repo);
385
+ assert(tracked.includes("metrics.json"), "precondition: metrics.json tracked");
386
+ assert(tracked.includes("completed-units.json"), "precondition: completed-units.json tracked");
387
+ assert(tracked.includes("activity/log.jsonl"), "precondition: activity log tracked");
388
+
389
+ // Now modify both runtime and real files
390
+ createFile(repo, ".gsd/metrics.json", '{"version":2}');
391
+ createFile(repo, ".gsd/completed-units.json", '["unit1","unit2"]');
392
+ createFile(repo, ".gsd/activity/log.jsonl", '{"ts":2}');
393
+ createFile(repo, "src/real.ts", "updated code");
394
+
395
+ // autoCommit should commit real.ts. The first call also runs auto-cleanup
396
+ // which removes runtime files from the index via a dedicated commit.
397
+ const msg = svc.autoCommit("execute-task", "M001/S01/T01");
398
+ assert(msg !== null, "autoCommit produces a commit");
399
+
400
+ const show = run("git show --stat HEAD", repo);
401
+ assert(show.includes("src/real.ts"), "real files are committed");
402
+
403
+ // After the commit, runtime files must no longer be in the git index.
404
+ // They remain on disk but are untracked (protected by .gitignore).
405
+ const trackedAfter = run("git ls-files .gsd/", repo);
406
+ assertEq(trackedAfter, "", "no .gsd/ runtime files remain in the index");
387
407
 
388
- // Call the fallback path manually
389
- svc.smartStageWithBadPathspec();
408
+ // Verify a second autoCommit with changed runtime files does NOT stage them
409
+ createFile(repo, ".gsd/metrics.json", '{"version":3}');
410
+ createFile(repo, ".gsd/completed-units.json", '["unit1","unit2","unit3"]');
411
+ createFile(repo, "src/real.ts", "third version");
390
412
 
391
- // Check that everything was staged (fallback stages all)
392
- const staged = run("git diff --cached --name-only", repo);
393
- assert(staged.includes("src/real.ts"), "fallback stages real files");
394
- assert(staged.includes(".gsd/activity/log.jsonl"), "fallback stages runtime files too (no exclusion)");
395
- assert(svc.fallbackUsed, "fallback path was actually used");
413
+ const msg2 = svc.autoCommit("execute-task", "M001/S01/T02");
414
+ assert(msg2 !== null, "second autoCommit produces a commit");
415
+
416
+ const show2 = run("git show --stat HEAD", repo);
417
+ assert(show2.includes("src/real.ts"), "real files committed in second commit");
418
+ assert(!show2.includes("metrics"), "metrics.json not in second commit");
419
+ assert(!show2.includes("completed-units"), "completed-units.json not in second commit");
420
+ assert(!show2.includes("activity"), "activity not in second commit");
396
421
 
397
422
  rmSync(repo, { recursive: true, force: true });
398
423
  }
@@ -1345,6 +1370,58 @@ async function main(): Promise<void> {
1345
1370
  assert(true, "PreMergeCheckResult type exported and usable");
1346
1371
  }
1347
1372
 
1373
+ // ─── untrackRuntimeFiles: removes tracked runtime files from index ───
1374
+
1375
+ console.log("\n=== untrackRuntimeFiles ===");
1376
+
1377
+ {
1378
+ const { untrackRuntimeFiles } = await import("../gitignore.ts");
1379
+ const repo = mkdtempSync(join(tmpdir(), "gsd-untrack-"));
1380
+ run("git init -b main", repo);
1381
+ run("git config user.email test@test.com", repo);
1382
+ run("git config user.name Test", repo);
1383
+
1384
+ // Create and track runtime files (simulates pre-.gitignore state)
1385
+ mkdirSync(join(repo, ".gsd", "activity"), { recursive: true });
1386
+ mkdirSync(join(repo, ".gsd", "runtime"), { recursive: true });
1387
+ writeFileSync(join(repo, ".gsd", "completed-units.json"), '["u1"]');
1388
+ writeFileSync(join(repo, ".gsd", "metrics.json"), '{}');
1389
+ writeFileSync(join(repo, ".gsd", "STATE.md"), "# State");
1390
+ writeFileSync(join(repo, ".gsd", "activity", "log.jsonl"), "{}");
1391
+ writeFileSync(join(repo, ".gsd", "runtime", "data.json"), "{}");
1392
+ writeFileSync(join(repo, "src.ts"), "code");
1393
+ run("git add -A", repo);
1394
+ run("git commit -m init", repo);
1395
+
1396
+ // Precondition: runtime files are tracked
1397
+ const trackedBefore = run("git ls-files .gsd/", repo);
1398
+ assert(trackedBefore.includes("completed-units.json"), "untrack: precondition — completed-units tracked");
1399
+ assert(trackedBefore.includes("metrics.json"), "untrack: precondition — metrics tracked");
1400
+
1401
+ // Run untrackRuntimeFiles
1402
+ untrackRuntimeFiles(repo);
1403
+
1404
+ // Runtime files should be removed from the index
1405
+ const trackedAfter = run("git ls-files .gsd/", repo);
1406
+ assertEq(trackedAfter, "", "untrack: all runtime files removed from index");
1407
+
1408
+ // Non-runtime files remain tracked
1409
+ const srcTracked = run("git ls-files src.ts", repo);
1410
+ assert(srcTracked.includes("src.ts"), "untrack: non-runtime files remain tracked");
1411
+
1412
+ // Files still exist on disk
1413
+ assert(existsSync(join(repo, ".gsd", "completed-units.json")),
1414
+ "untrack: completed-units.json still on disk");
1415
+ assert(existsSync(join(repo, ".gsd", "metrics.json")),
1416
+ "untrack: metrics.json still on disk");
1417
+
1418
+ // Idempotent — running again doesn't error
1419
+ untrackRuntimeFiles(repo);
1420
+ assert(true, "untrack: second call is idempotent (no error)");
1421
+
1422
+ rmSync(repo, { recursive: true, force: true });
1423
+ }
1424
+
1348
1425
  console.log(`\nResults: ${passed} passed, ${failed} failed`);
1349
1426
  if (failed > 0) process.exit(1);
1350
1427
  console.log("All tests passed ✓");
@@ -54,7 +54,7 @@ async function main(): Promise<void> {
54
54
  const testVars = {
55
55
  milestoneId: "M099",
56
56
  completedSliceId: "S03",
57
- assessmentAbsPath: ".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md",
57
+ assessmentPath: ".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md",
58
58
  roadmapPath: ".gsd/milestones/M099/M099-ROADMAP.md",
59
59
  inlinedContext: "--- test inlined context block ---",
60
60
  };
@@ -75,14 +75,14 @@ async function main(): Promise<void> {
75
75
  // Verify all test variables were substituted into the output
76
76
  assert(result.includes("M099"), "prompt contains milestoneId 'M099'");
77
77
  assert(result.includes("S03"), "prompt contains completedSliceId 'S03'");
78
- assert(result.includes(".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md"), "prompt contains assessmentAbsPath");
78
+ assert(result.includes(".gsd/milestones/M099/slices/S03/S03-ASSESSMENT.md"), "prompt contains assessmentPath");
79
79
  assert(result.includes(".gsd/milestones/M099/M099-ROADMAP.md"), "prompt contains roadmapPath");
80
80
  assert(result.includes("--- test inlined context block ---"), "prompt contains inlinedContext");
81
81
 
82
82
  // Verify no un-substituted variables remain
83
83
  assert(!result.includes("{{milestoneId}}"), "no un-substituted {{milestoneId}}");
84
84
  assert(!result.includes("{{completedSliceId}}"), "no un-substituted {{completedSliceId}}");
85
- assert(!result.includes("{{assessmentAbsPath}}"), "no un-substituted {{assessmentAbsPath}}");
85
+ assert(!result.includes("{{assessmentPath}}"), "no un-substituted {{assessmentPath}}");
86
86
  assert(!result.includes("{{roadmapPath}}"), "no un-substituted {{roadmapPath}}");
87
87
  assert(!result.includes("{{inlinedContext}}"), "no un-substituted {{inlinedContext}}");
88
88
  }
@@ -93,7 +93,7 @@ async function main(): Promise<void> {
93
93
  const prompt = loadPromptFromWorktree("reassess-roadmap", {
94
94
  milestoneId: "M001",
95
95
  completedSliceId: "S01",
96
- assessmentAbsPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
96
+ assessmentPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
97
97
  roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
98
98
  inlinedContext: "context",
99
99
  });
@@ -132,7 +132,7 @@ async function main(): Promise<void> {
132
132
  const prompt = loadPromptFromWorktree("reassess-roadmap", {
133
133
  milestoneId: "M001",
134
134
  completedSliceId: "S01",
135
- assessmentAbsPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
135
+ assessmentPath: ".gsd/milestones/M001/slices/S01/S01-ASSESSMENT.md",
136
136
  roadmapPath: ".gsd/milestones/M001/M001-ROADMAP.md",
137
137
  inlinedContext: "context",
138
138
  });
@@ -404,12 +404,13 @@ console.log('\n=== prompt: replan-slice contains preserve-completed-tasks instru
404
404
  slicePath: '.gsd/milestones/M001/slices/S01',
405
405
  planPath: '.gsd/milestones/M001/slices/S01/S01-PLAN.md',
406
406
  blockerTaskId: 'T01',
407
+ replanPath: '.gsd/milestones/M001/slices/S01/S01-REPLAN.md',
407
408
  inlinedContext: '',
408
409
  });
409
410
 
410
411
  assert(prompt.includes('Do NOT renumber or remove completed tasks'), 'prompt contains preserve-completed-tasks instruction');
411
412
  assert(prompt.includes('[x]'), 'prompt mentions [x] checkmarks');
412
- assert(prompt.includes('replanAbsPath') || prompt.includes('REPLAN'), 'prompt references replan output path');
413
+ assert(prompt.includes('REPLAN'), 'prompt references replan output path');
413
414
  assert(prompt.includes('blocker_discovered'), 'prompt mentions blocker_discovered');
414
415
  }
415
416
 
@@ -0,0 +1,59 @@
1
+ import { parseRoadmap } from "../files.ts";
2
+ import { parseRoadmapSlices } from "../roadmap-slices.ts";
3
+
4
+ let passed = 0;
5
+ let failed = 0;
6
+
7
+ function assert(condition: boolean, message: string): void {
8
+ if (condition) passed += 1;
9
+ else {
10
+ failed += 1;
11
+ console.error(`FAIL: ${message}`);
12
+ }
13
+ }
14
+
15
+ function assertEq<T>(actual: T, expected: T, message: string): void {
16
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed += 1;
17
+ else {
18
+ failed += 1;
19
+ console.error(`FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
20
+ }
21
+ }
22
+
23
+ const content = `# M003: Current
24
+
25
+ **Vision:** Build the thing.
26
+
27
+ ## Slices
28
+ - [x] **S01: First Slice** \`risk:low\` \`depends:[]\`
29
+ > After this: First demo works.
30
+ - [ ] **S02: Second Slice** \`risk:medium\` \`depends:[S01]\`
31
+ - [x] **S03: Third Slice** \`depends:[S01, S02]\`
32
+ > After this: Third demo works.
33
+
34
+ ## Boundary Map
35
+ ### S01 → S02
36
+ Produces:
37
+ foo.ts
38
+ `;
39
+
40
+ console.log("\n=== parseRoadmapSlices ===");
41
+ const slices = parseRoadmapSlices(content);
42
+ assertEq(slices.length, 3, "slice count");
43
+ assertEq(slices[0]?.id, "S01", "first id");
44
+ assertEq(slices[0]?.done, true, "first done");
45
+ assertEq(slices[0]?.demo, "First demo works.", "first demo");
46
+ assertEq(slices[1]?.depends, ["S01"], "second depends");
47
+ assertEq(slices[1]?.risk, "medium", "second risk");
48
+ assertEq(slices[2]?.risk, "low", "missing risk defaults to low");
49
+ assertEq(slices[2]?.depends, ["S01", "S02"], "third depends");
50
+
51
+ console.log("\n=== parseRoadmap integration ===");
52
+ const roadmap = parseRoadmap(content);
53
+ assertEq(roadmap.slices, slices, "parseRoadmap uses extracted slice parser");
54
+ assertEq(roadmap.title, "M003: Current", "roadmap title preserved");
55
+ assertEq(roadmap.vision, "Build the thing.", "roadmap vision preserved");
56
+ assert(roadmap.boundaryMap.length === 1, "boundary map still parsed");
57
+
58
+ console.log(`Passed: ${passed}, Failed: ${failed}`);
59
+ if (failed > 0) process.exit(1);