@tuan_son.dinh/gsd 2.6.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 (227) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +453 -0
  3. package/dist/app-paths.d.ts +4 -0
  4. package/dist/app-paths.js +6 -0
  5. package/dist/cli.d.ts +1 -0
  6. package/dist/cli.js +269 -0
  7. package/dist/loader.d.ts +2 -0
  8. package/dist/loader.js +70 -0
  9. package/dist/logo.d.ts +16 -0
  10. package/dist/logo.js +25 -0
  11. package/dist/onboarding.d.ts +43 -0
  12. package/dist/onboarding.js +418 -0
  13. package/dist/pi-migration.d.ts +14 -0
  14. package/dist/pi-migration.js +57 -0
  15. package/dist/resource-loader.d.ts +22 -0
  16. package/dist/resource-loader.js +60 -0
  17. package/dist/tool-bootstrap.d.ts +4 -0
  18. package/dist/tool-bootstrap.js +74 -0
  19. package/dist/wizard.d.ts +7 -0
  20. package/dist/wizard.js +25 -0
  21. package/package.json +60 -0
  22. package/patches/@mariozechner+pi-coding-agent+0.57.1.patch +108 -0
  23. package/patches/@mariozechner+pi-tui+0.57.1.patch +47 -0
  24. package/pkg/dist/modes/interactive/theme/dark.json +85 -0
  25. package/pkg/dist/modes/interactive/theme/light.json +84 -0
  26. package/pkg/dist/modes/interactive/theme/theme-schema.json +335 -0
  27. package/pkg/dist/modes/interactive/theme/theme.d.ts +78 -0
  28. package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -0
  29. package/pkg/dist/modes/interactive/theme/theme.js +949 -0
  30. package/pkg/dist/modes/interactive/theme/theme.js.map +1 -0
  31. package/pkg/package.json +8 -0
  32. package/scripts/postinstall.js +127 -0
  33. package/src/resources/GSD-WORKFLOW.md +661 -0
  34. package/src/resources/agents/researcher.md +29 -0
  35. package/src/resources/agents/scout.md +56 -0
  36. package/src/resources/agents/worker.md +31 -0
  37. package/src/resources/extensions/ask-user-questions.ts +249 -0
  38. package/src/resources/extensions/bg-shell/index.ts +2808 -0
  39. package/src/resources/extensions/browser-tools/BROWSER-TOOLS-V2-PROPOSAL.md +1277 -0
  40. package/src/resources/extensions/browser-tools/core.js +1057 -0
  41. package/src/resources/extensions/browser-tools/index.ts +4989 -0
  42. package/src/resources/extensions/browser-tools/package.json +20 -0
  43. package/src/resources/extensions/context7/index.ts +428 -0
  44. package/src/resources/extensions/context7/package.json +11 -0
  45. package/src/resources/extensions/get-secrets-from-user.ts +352 -0
  46. package/src/resources/extensions/google-search/index.ts +323 -0
  47. package/src/resources/extensions/google-search/package.json +9 -0
  48. package/src/resources/extensions/gsd/activity-log.ts +69 -0
  49. package/src/resources/extensions/gsd/auto.ts +2744 -0
  50. package/src/resources/extensions/gsd/commands.ts +313 -0
  51. package/src/resources/extensions/gsd/crash-recovery.ts +85 -0
  52. package/src/resources/extensions/gsd/dashboard-overlay.ts +521 -0
  53. package/src/resources/extensions/gsd/docs/preferences-reference.md +176 -0
  54. package/src/resources/extensions/gsd/doctor.ts +690 -0
  55. package/src/resources/extensions/gsd/files.ts +732 -0
  56. package/src/resources/extensions/gsd/git-service.ts +597 -0
  57. package/src/resources/extensions/gsd/gitignore.ts +168 -0
  58. package/src/resources/extensions/gsd/guided-flow.ts +817 -0
  59. package/src/resources/extensions/gsd/index.ts +558 -0
  60. package/src/resources/extensions/gsd/metrics.ts +374 -0
  61. package/src/resources/extensions/gsd/migrate/command.ts +218 -0
  62. package/src/resources/extensions/gsd/migrate/index.ts +42 -0
  63. package/src/resources/extensions/gsd/migrate/parser.ts +323 -0
  64. package/src/resources/extensions/gsd/migrate/parsers.ts +624 -0
  65. package/src/resources/extensions/gsd/migrate/preview.ts +48 -0
  66. package/src/resources/extensions/gsd/migrate/transformer.ts +346 -0
  67. package/src/resources/extensions/gsd/migrate/types.ts +370 -0
  68. package/src/resources/extensions/gsd/migrate/validator.ts +55 -0
  69. package/src/resources/extensions/gsd/migrate/writer.ts +539 -0
  70. package/src/resources/extensions/gsd/observability-validator.ts +408 -0
  71. package/src/resources/extensions/gsd/package.json +11 -0
  72. package/src/resources/extensions/gsd/paths.ts +308 -0
  73. package/src/resources/extensions/gsd/preferences.ts +757 -0
  74. package/src/resources/extensions/gsd/prompt-loader.ts +50 -0
  75. package/src/resources/extensions/gsd/prompts/complete-milestone.md +25 -0
  76. package/src/resources/extensions/gsd/prompts/complete-slice.md +29 -0
  77. package/src/resources/extensions/gsd/prompts/discuss.md +189 -0
  78. package/src/resources/extensions/gsd/prompts/doctor-heal.md +29 -0
  79. package/src/resources/extensions/gsd/prompts/execute-task.md +61 -0
  80. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +1 -0
  81. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -0
  82. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +59 -0
  83. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +1 -0
  84. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +23 -0
  85. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +1 -0
  86. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +11 -0
  87. package/src/resources/extensions/gsd/prompts/guided-resume-task.md +1 -0
  88. package/src/resources/extensions/gsd/prompts/plan-milestone.md +65 -0
  89. package/src/resources/extensions/gsd/prompts/plan-slice.md +51 -0
  90. package/src/resources/extensions/gsd/prompts/queue.md +85 -0
  91. package/src/resources/extensions/gsd/prompts/reassess-roadmap.md +48 -0
  92. package/src/resources/extensions/gsd/prompts/replan-slice.md +39 -0
  93. package/src/resources/extensions/gsd/prompts/research-milestone.md +37 -0
  94. package/src/resources/extensions/gsd/prompts/research-slice.md +28 -0
  95. package/src/resources/extensions/gsd/prompts/review-migration.md +66 -0
  96. package/src/resources/extensions/gsd/prompts/run-uat.md +109 -0
  97. package/src/resources/extensions/gsd/prompts/system.md +187 -0
  98. package/src/resources/extensions/gsd/prompts/worktree-merge.md +123 -0
  99. package/src/resources/extensions/gsd/session-forensics.ts +487 -0
  100. package/src/resources/extensions/gsd/skill-discovery.ts +137 -0
  101. package/src/resources/extensions/gsd/state.ts +460 -0
  102. package/src/resources/extensions/gsd/templates/context.md +76 -0
  103. package/src/resources/extensions/gsd/templates/decisions.md +8 -0
  104. package/src/resources/extensions/gsd/templates/milestone-summary.md +73 -0
  105. package/src/resources/extensions/gsd/templates/plan.md +131 -0
  106. package/src/resources/extensions/gsd/templates/preferences.md +24 -0
  107. package/src/resources/extensions/gsd/templates/project.md +31 -0
  108. package/src/resources/extensions/gsd/templates/reassessment.md +28 -0
  109. package/src/resources/extensions/gsd/templates/requirements.md +81 -0
  110. package/src/resources/extensions/gsd/templates/research.md +46 -0
  111. package/src/resources/extensions/gsd/templates/roadmap.md +118 -0
  112. package/src/resources/extensions/gsd/templates/slice-context.md +58 -0
  113. package/src/resources/extensions/gsd/templates/slice-summary.md +99 -0
  114. package/src/resources/extensions/gsd/templates/state.md +19 -0
  115. package/src/resources/extensions/gsd/templates/task-plan.md +52 -0
  116. package/src/resources/extensions/gsd/templates/task-summary.md +57 -0
  117. package/src/resources/extensions/gsd/templates/uat.md +54 -0
  118. package/src/resources/extensions/gsd/tests/activity-log-prune.test.ts +327 -0
  119. package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +56 -0
  120. package/src/resources/extensions/gsd/tests/auto-supervisor.test.mjs +53 -0
  121. package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +225 -0
  122. package/src/resources/extensions/gsd/tests/cost-projection.test.ts +160 -0
  123. package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +341 -0
  124. package/src/resources/extensions/gsd/tests/derive-state.test.ts +689 -0
  125. package/src/resources/extensions/gsd/tests/discuss-prompt.test.ts +38 -0
  126. package/src/resources/extensions/gsd/tests/doctor.test.ts +505 -0
  127. package/src/resources/extensions/gsd/tests/git-service.test.ts +1313 -0
  128. package/src/resources/extensions/gsd/tests/idle-recovery.test.ts +308 -0
  129. package/src/resources/extensions/gsd/tests/metrics-io.test.ts +201 -0
  130. package/src/resources/extensions/gsd/tests/metrics.test.ts +217 -0
  131. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +390 -0
  132. package/src/resources/extensions/gsd/tests/migrate-parser.test.ts +786 -0
  133. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +657 -0
  134. package/src/resources/extensions/gsd/tests/migrate-validator-parsers.test.ts +443 -0
  135. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +318 -0
  136. package/src/resources/extensions/gsd/tests/migrate-writer.test.ts +420 -0
  137. package/src/resources/extensions/gsd/tests/must-have-parser.test.ts +309 -0
  138. package/src/resources/extensions/gsd/tests/parsers.test.ts +1351 -0
  139. package/src/resources/extensions/gsd/tests/plan-milestone.test.ts +163 -0
  140. package/src/resources/extensions/gsd/tests/plan-quality-validator.test.ts +386 -0
  141. package/src/resources/extensions/gsd/tests/reassess-prompt.test.ts +171 -0
  142. package/src/resources/extensions/gsd/tests/remote-questions.test.ts +155 -0
  143. package/src/resources/extensions/gsd/tests/remote-status.test.ts +99 -0
  144. package/src/resources/extensions/gsd/tests/replan-slice.test.ts +521 -0
  145. package/src/resources/extensions/gsd/tests/requirements.test.ts +125 -0
  146. package/src/resources/extensions/gsd/tests/resolve-ts-hooks.mjs +34 -0
  147. package/src/resources/extensions/gsd/tests/resolve-ts.mjs +11 -0
  148. package/src/resources/extensions/gsd/tests/run-uat.test.ts +348 -0
  149. package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +247 -0
  150. package/src/resources/extensions/gsd/tests/workflow-config.test.mjs +53 -0
  151. package/src/resources/extensions/gsd/tests/workspace-index.test.ts +94 -0
  152. package/src/resources/extensions/gsd/tests/worktree-integration.test.ts +253 -0
  153. package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +160 -0
  154. package/src/resources/extensions/gsd/tests/worktree.test.ts +264 -0
  155. package/src/resources/extensions/gsd/types.ts +159 -0
  156. package/src/resources/extensions/gsd/unit-runtime.ts +184 -0
  157. package/src/resources/extensions/gsd/workspace-index.ts +203 -0
  158. package/src/resources/extensions/gsd/worktree-command.ts +845 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +392 -0
  160. package/src/resources/extensions/gsd/worktree.ts +183 -0
  161. package/src/resources/extensions/mac-tools/index.ts +852 -0
  162. package/src/resources/extensions/mac-tools/swift-cli/Package.swift +22 -0
  163. package/src/resources/extensions/mac-tools/swift-cli/Sources/main.swift +1318 -0
  164. package/src/resources/extensions/mcporter/index.ts +429 -0
  165. package/src/resources/extensions/remote-questions/config.ts +81 -0
  166. package/src/resources/extensions/remote-questions/discord-adapter.ts +128 -0
  167. package/src/resources/extensions/remote-questions/format.ts +163 -0
  168. package/src/resources/extensions/remote-questions/manager.ts +192 -0
  169. package/src/resources/extensions/remote-questions/remote-command.ts +307 -0
  170. package/src/resources/extensions/remote-questions/slack-adapter.ts +92 -0
  171. package/src/resources/extensions/remote-questions/status.ts +31 -0
  172. package/src/resources/extensions/remote-questions/store.ts +77 -0
  173. package/src/resources/extensions/remote-questions/types.ts +75 -0
  174. package/src/resources/extensions/search-the-web/cache.ts +78 -0
  175. package/src/resources/extensions/search-the-web/command-search-provider.ts +95 -0
  176. package/src/resources/extensions/search-the-web/format.ts +258 -0
  177. package/src/resources/extensions/search-the-web/http.ts +238 -0
  178. package/src/resources/extensions/search-the-web/index.ts +65 -0
  179. package/src/resources/extensions/search-the-web/native-search.ts +157 -0
  180. package/src/resources/extensions/search-the-web/provider.ts +118 -0
  181. package/src/resources/extensions/search-the-web/tavily.ts +116 -0
  182. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +519 -0
  183. package/src/resources/extensions/search-the-web/tool-llm-context.ts +561 -0
  184. package/src/resources/extensions/search-the-web/tool-search.ts +576 -0
  185. package/src/resources/extensions/search-the-web/url-utils.ts +91 -0
  186. package/src/resources/extensions/shared/confirm-ui.ts +126 -0
  187. package/src/resources/extensions/shared/interview-ui.ts +613 -0
  188. package/src/resources/extensions/shared/next-action-ui.ts +197 -0
  189. package/src/resources/extensions/shared/progress-widget.ts +282 -0
  190. package/src/resources/extensions/shared/terminal.ts +23 -0
  191. package/src/resources/extensions/shared/thinking-widget.ts +107 -0
  192. package/src/resources/extensions/shared/ui.ts +400 -0
  193. package/src/resources/extensions/shared/wizard-ui.ts +551 -0
  194. package/src/resources/extensions/slash-commands/audit.ts +88 -0
  195. package/src/resources/extensions/slash-commands/clear.ts +10 -0
  196. package/src/resources/extensions/slash-commands/create-extension.ts +297 -0
  197. package/src/resources/extensions/slash-commands/create-slash-command.ts +234 -0
  198. package/src/resources/extensions/slash-commands/index.ts +12 -0
  199. package/src/resources/extensions/subagent/agents.ts +126 -0
  200. package/src/resources/extensions/subagent/index.ts +1020 -0
  201. package/src/resources/extensions/voice/index.ts +195 -0
  202. package/src/resources/extensions/voice/speech-recognizer.swift +154 -0
  203. package/src/resources/skills/debug-like-expert/SKILL.md +231 -0
  204. package/src/resources/skills/debug-like-expert/references/debugging-mindset.md +253 -0
  205. package/src/resources/skills/debug-like-expert/references/hypothesis-testing.md +373 -0
  206. package/src/resources/skills/debug-like-expert/references/investigation-techniques.md +337 -0
  207. package/src/resources/skills/debug-like-expert/references/verification-patterns.md +425 -0
  208. package/src/resources/skills/debug-like-expert/references/when-to-research.md +361 -0
  209. package/src/resources/skills/frontend-design/SKILL.md +45 -0
  210. package/src/resources/skills/swiftui/SKILL.md +208 -0
  211. package/src/resources/skills/swiftui/references/animations.md +921 -0
  212. package/src/resources/skills/swiftui/references/architecture.md +1561 -0
  213. package/src/resources/skills/swiftui/references/layout-system.md +1186 -0
  214. package/src/resources/skills/swiftui/references/navigation.md +1492 -0
  215. package/src/resources/skills/swiftui/references/networking-async.md +214 -0
  216. package/src/resources/skills/swiftui/references/performance.md +1706 -0
  217. package/src/resources/skills/swiftui/references/platform-integration.md +204 -0
  218. package/src/resources/skills/swiftui/references/state-management.md +1443 -0
  219. package/src/resources/skills/swiftui/references/swiftdata.md +297 -0
  220. package/src/resources/skills/swiftui/references/testing-debugging.md +247 -0
  221. package/src/resources/skills/swiftui/references/uikit-appkit-interop.md +218 -0
  222. package/src/resources/skills/swiftui/workflows/add-feature.md +191 -0
  223. package/src/resources/skills/swiftui/workflows/build-new-app.md +311 -0
  224. package/src/resources/skills/swiftui/workflows/debug-swiftui.md +192 -0
  225. package/src/resources/skills/swiftui/workflows/optimize-performance.md +197 -0
  226. package/src/resources/skills/swiftui/workflows/ship-app.md +203 -0
  227. package/src/resources/skills/swiftui/workflows/write-tests.md +235 -0
@@ -0,0 +1,264 @@
1
+ import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+ import { execSync } from "node:child_process";
5
+
6
+ import {
7
+ autoCommitCurrentBranch,
8
+ detectWorktreeName,
9
+ ensureSliceBranch,
10
+ getActiveSliceBranch,
11
+ getCurrentBranch,
12
+ getSliceBranchName,
13
+ isOnSliceBranch,
14
+ mergeSliceToMain,
15
+ parseSliceBranch,
16
+ SLICE_BRANCH_RE,
17
+ switchToMain,
18
+ } from "../worktree.ts";
19
+ import { deriveState } from "../state.ts";
20
+ import { indexWorkspace } from "../workspace-index.ts";
21
+
22
+ let passed = 0;
23
+ let failed = 0;
24
+
25
+ function assert(condition: boolean, message: string): void {
26
+ if (condition) passed++;
27
+ else {
28
+ failed++;
29
+ console.error(` FAIL: ${message}`);
30
+ }
31
+ }
32
+
33
+ function assertEq<T>(actual: T, expected: T, message: string): void {
34
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
35
+ else {
36
+ failed++;
37
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
38
+ }
39
+ }
40
+
41
+ function run(command: string, cwd: string): string {
42
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
43
+ }
44
+
45
+ const base = mkdtempSync(join(tmpdir(), "gsd-branch-test-"));
46
+ run("git init -b main", base);
47
+ run("git config user.name 'Pi Test'", base);
48
+ run("git config user.email 'pi@example.com'", base);
49
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
50
+ writeFileSync(join(base, "README.md"), "hello\n", "utf-8");
51
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), `# M001: Demo\n\n## Slices\n- [ ] **S01: Slice One** \`risk:low\` \`depends:[]\`\n > After this: demo works\n`, "utf-8");
52
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"), `# S01: Slice One\n\n**Goal:** Demo\n**Demo:** Demo\n\n## Must-Haves\n- done\n\n## Tasks\n- [ ] **T01: Implement** \`est:10m\`\n do it\n`, "utf-8");
53
+ run("git add .", base);
54
+ run("git commit -m 'chore: init'", base);
55
+
56
+ async function main(): Promise<void> {
57
+ console.log("\n=== ensureSliceBranch ===");
58
+ const created = ensureSliceBranch(base, "M001", "S01");
59
+ assert(created, "branch created on first ensure");
60
+ assertEq(getCurrentBranch(base), "gsd/M001/S01", "switched to slice branch");
61
+
62
+ console.log("\n=== idempotent ensure ===");
63
+ const secondCreate = ensureSliceBranch(base, "M001", "S01");
64
+ assertEq(secondCreate, false, "branch not recreated on second ensure");
65
+ assertEq(getCurrentBranch(base), "gsd/M001/S01", "still on slice branch");
66
+
67
+ console.log("\n=== getActiveSliceBranch ===");
68
+ assertEq(getActiveSliceBranch(base), "gsd/M001/S01", "getActiveSliceBranch returns current slice branch");
69
+
70
+ console.log("\n=== state surfaces active branch ===");
71
+ const state = await deriveState(base);
72
+ assertEq(state.activeBranch, "gsd/M001/S01", "state exposes active branch");
73
+
74
+ console.log("\n=== workspace index surfaces branch ===");
75
+ const index = await indexWorkspace(base);
76
+ const slice = index.milestones[0]?.slices[0];
77
+ assertEq(slice?.branch, "gsd/M001/S01", "workspace index exposes branch");
78
+
79
+ console.log("\n=== autoCommitCurrentBranch ===");
80
+ // Clean — should return null
81
+ const cleanResult = autoCommitCurrentBranch(base, "execute-task", "M001/S01/T01");
82
+ assertEq(cleanResult, null, "returns null for clean repo");
83
+
84
+ // Make dirty
85
+ writeFileSync(join(base, "dirty.txt"), "uncommitted\n", "utf-8");
86
+ const dirtyResult = autoCommitCurrentBranch(base, "execute-task", "M001/S01/T01");
87
+ assert(dirtyResult !== null, "returns commit message for dirty repo");
88
+ assert(dirtyResult!.includes("M001/S01/T01"), "commit message includes unit id");
89
+ assertEq(run("git status --short", base), "", "repo is clean after auto-commit");
90
+
91
+ console.log("\n=== switchToMain ===");
92
+ switchToMain(base);
93
+ assertEq(getCurrentBranch(base), "main", "switched back to main");
94
+ assertEq(getActiveSliceBranch(base), null, "getActiveSliceBranch returns null on main");
95
+
96
+ console.log("\n=== mergeSliceToMain ===");
97
+ // Switch back to slice, make a change, switch to main, merge
98
+ ensureSliceBranch(base, "M001", "S01");
99
+ writeFileSync(join(base, "README.md"), "hello from slice\n", "utf-8");
100
+ run("git add README.md", base);
101
+ run("git commit -m 'feat: slice change'", base);
102
+ switchToMain(base);
103
+
104
+ const merge = mergeSliceToMain(base, "M001", "S01", "Slice One");
105
+ assertEq(merge.branch, "gsd/M001/S01", "merge reports branch");
106
+ assertEq(getCurrentBranch(base), "main", "still on main after merge");
107
+ assert(readFileSync(join(base, "README.md"), "utf-8").includes("slice"), "main got squashed content");
108
+ assert(merge.deletedBranch, "branch was deleted");
109
+
110
+ // Verify branch is actually gone
111
+ const branches = run("git branch", base);
112
+ assert(!branches.includes("gsd/M001/S01"), "slice branch no longer exists");
113
+
114
+ console.log("\n=== switchToMain auto-commits dirty files ===");
115
+ // Set up S02
116
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
117
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
118
+ "# M001: Demo", "", "## Slices",
119
+ "- [x] **S01: Slice One** `risk:low` `depends:[]`", " > Done",
120
+ "- [ ] **S02: Slice Two** `risk:low` `depends:[]`", " > Demo 2",
121
+ ].join("\n") + "\n", "utf-8");
122
+ run("git add .", base);
123
+ run("git commit -m 'chore: add S02'", base);
124
+
125
+ ensureSliceBranch(base, "M001", "S02");
126
+ writeFileSync(join(base, "feature.txt"), "new feature\n", "utf-8");
127
+ // Don't commit — switchToMain should auto-commit
128
+ switchToMain(base);
129
+ assertEq(getCurrentBranch(base), "main", "switched to main despite dirty files");
130
+
131
+ // Verify the commit happened on the slice branch
132
+ ensureSliceBranch(base, "M001", "S02");
133
+ assert(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "dirty file was committed on slice branch");
134
+ switchToMain(base);
135
+
136
+ // Now merge S02
137
+ const mergeS02 = mergeSliceToMain(base, "M001", "S02", "Slice Two");
138
+ assert(readFileSync(join(base, "feature.txt"), "utf-8").includes("new feature"), "main got feature from auto-committed branch");
139
+ assertEq(mergeS02.deletedBranch, true, "S02 branch deleted");
140
+
141
+ console.log("\n=== getSliceBranchName ===");
142
+ assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "branch name format correct");
143
+ assertEq(getSliceBranchName("M001", "S01", null), "gsd/M001/S01", "null worktree = plain branch");
144
+ assertEq(getSliceBranchName("M001", "S01", "my-wt"), "gsd/my-wt/M001/S01", "worktree-namespaced branch");
145
+
146
+ console.log("\n=== parseSliceBranch ===");
147
+ const plain = parseSliceBranch("gsd/M001/S01");
148
+ assert(plain !== null, "parses plain branch");
149
+ assertEq(plain!.worktreeName, null, "plain branch has no worktree name");
150
+ assertEq(plain!.milestoneId, "M001", "plain branch milestone");
151
+ assertEq(plain!.sliceId, "S01", "plain branch slice");
152
+
153
+ const namespaced = parseSliceBranch("gsd/feature-auth/M001/S01");
154
+ assert(namespaced !== null, "parses worktree-namespaced branch");
155
+ assertEq(namespaced!.worktreeName, "feature-auth", "worktree name extracted");
156
+ assertEq(namespaced!.milestoneId, "M001", "namespaced branch milestone");
157
+ assertEq(namespaced!.sliceId, "S01", "namespaced branch slice");
158
+
159
+ const invalid = parseSliceBranch("main");
160
+ assertEq(invalid, null, "non-slice branch returns null");
161
+
162
+ const worktreeBranch = parseSliceBranch("worktree/foo");
163
+ assertEq(worktreeBranch, null, "worktree/ prefix is not a slice branch");
164
+
165
+ console.log("\n=== SLICE_BRANCH_RE ===");
166
+ assert(SLICE_BRANCH_RE.test("gsd/M001/S01"), "regex matches plain branch");
167
+ assert(SLICE_BRANCH_RE.test("gsd/my-wt/M001/S01"), "regex matches worktree branch");
168
+ assert(!SLICE_BRANCH_RE.test("main"), "regex rejects main");
169
+ assert(!SLICE_BRANCH_RE.test("gsd/"), "regex rejects bare gsd/");
170
+ assert(!SLICE_BRANCH_RE.test("worktree/foo"), "regex rejects worktree/foo");
171
+
172
+ console.log("\n=== detectWorktreeName ===");
173
+ assertEq(detectWorktreeName("/projects/myapp"), null, "no worktree in plain path");
174
+ assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/feature-auth"), "feature-auth", "detects worktree name");
175
+ assertEq(detectWorktreeName("/projects/myapp/.gsd/worktrees/my-wt/subdir"), "my-wt", "detects worktree with subdir");
176
+
177
+ // ── Regression: slice branch from non-main working branch ───────────
178
+ // Reproduces the bug where planning artifacts committed to a working
179
+ // branch (e.g. "developer") are lost when the slice branch is created
180
+ // from "main" which doesn't have them.
181
+ console.log("\n=== ensureSliceBranch from non-main working branch ===");
182
+ const base2 = mkdtempSync(join(tmpdir(), "gsd-branch-base-test-"));
183
+ run("git init -b main", base2);
184
+ run("git config user.name 'Pi Test'", base2);
185
+ run("git config user.email 'pi@example.com'", base2);
186
+ writeFileSync(join(base2, "README.md"), "hello\n", "utf-8");
187
+ run("git add .", base2);
188
+ run("git commit -m 'chore: init'", base2);
189
+
190
+ // Create a "developer" branch with planning artifacts (like the real scenario)
191
+ run("git checkout -b developer", base2);
192
+ mkdirSync(join(base2, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
193
+ writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md"), "# M001 Context\nGoal: fix eslint\n", "utf-8");
194
+ writeFileSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
195
+ "# M001: ESLint Cleanup", "", "## Slices",
196
+ "- [ ] **S01: Config Fix** `risk:low` `depends:[]`", " > Fix config",
197
+ ].join("\n") + "\n", "utf-8");
198
+ run("git add .", base2);
199
+ run("git commit -m 'docs(M001): context and roadmap'", base2);
200
+
201
+ // Verify main does NOT have the artifacts
202
+ const mainRoadmap = run("git show main:.gsd/milestones/M001/M001-ROADMAP.md 2>&1 || echo MISSING", base2);
203
+ assert(mainRoadmap.includes("MISSING") || mainRoadmap.includes("does not exist"), "main branch lacks roadmap");
204
+
205
+ // Now create slice branch from developer — should inherit artifacts
206
+ assertEq(getCurrentBranch(base2), "developer", "on developer branch before ensure");
207
+ const created3 = ensureSliceBranch(base2, "M001", "S01");
208
+ assert(created3, "slice branch created from developer");
209
+ assertEq(getCurrentBranch(base2), "gsd/M001/S01", "switched to slice branch");
210
+
211
+ // The critical assertion: planning artifacts must exist on the slice branch
212
+ assert(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), "roadmap exists on slice branch");
213
+ assert(existsSync(join(base2, ".gsd", "milestones", "M001", "M001-CONTEXT.md")), "context exists on slice branch");
214
+
215
+ // Verify deriveState sees the correct phase (not pre-planning)
216
+ const state2 = await deriveState(base2);
217
+ assertEq(state2.phase, "planning", "deriveState sees planning phase on slice branch");
218
+ assert(state2.activeSlice !== null, "active slice found");
219
+ assertEq(state2.activeSlice!.id, "S01", "active slice is S01");
220
+
221
+ rmSync(base2, { recursive: true, force: true });
222
+
223
+ // ── Slice branch from another slice branch falls back to main ───────
224
+ console.log("\n=== ensureSliceBranch from slice branch falls back to main ===");
225
+ const base3 = mkdtempSync(join(tmpdir(), "gsd-branch-chain-test-"));
226
+ run("git init -b main", base3);
227
+ run("git config user.name 'Pi Test'", base3);
228
+ run("git config user.email 'pi@example.com'", base3);
229
+ mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
230
+ mkdirSync(join(base3, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
231
+ writeFileSync(join(base3, "README.md"), "hello\n", "utf-8");
232
+ writeFileSync(join(base3, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), [
233
+ "# M001: Demo", "", "## Slices",
234
+ "- [ ] **S01: First** `risk:low` `depends:[]`", " > first",
235
+ "- [ ] **S02: Second** `risk:low` `depends:[]`", " > second",
236
+ ].join("\n") + "\n", "utf-8");
237
+ run("git add .", base3);
238
+ run("git commit -m 'chore: init'", base3);
239
+
240
+ ensureSliceBranch(base3, "M001", "S01");
241
+ assertEq(getCurrentBranch(base3), "gsd/M001/S01", "on S01 slice branch");
242
+
243
+ // Creating S02 while on S01 should NOT chain from S01 — should use main
244
+ const created4 = ensureSliceBranch(base3, "M001", "S02");
245
+ assert(created4, "S02 branch created");
246
+ assertEq(getCurrentBranch(base3), "gsd/M001/S02", "switched to S02");
247
+
248
+ // S02 should be based on main, not on gsd/M001/S01
249
+ const s02Base = run("git merge-base main gsd/M001/S02", base3);
250
+ const mainHead = run("git rev-parse main", base3);
251
+ assertEq(s02Base, mainHead, "S02 is based on main, not on S01 slice branch");
252
+
253
+ rmSync(base3, { recursive: true, force: true });
254
+
255
+ rmSync(base, { recursive: true, force: true });
256
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
257
+ if (failed > 0) process.exit(1);
258
+ console.log("All tests passed ✓");
259
+ }
260
+
261
+ main().catch((error) => {
262
+ console.error(error);
263
+ process.exit(1);
264
+ });
@@ -0,0 +1,159 @@
1
+ // GSD Extension — Core Type Definitions
2
+ // Types consumed by state derivation, file parsing, and status display.
3
+ // Pure interfaces — no logic, no runtime dependencies.
4
+
5
+ // ─── Enums & Literal Unions ────────────────────────────────────────────────
6
+
7
+ export type RiskLevel = 'low' | 'medium' | 'high';
8
+ export type Phase = 'pre-planning' | 'discussing' | 'researching' | 'planning' | 'executing' | 'verifying' | 'summarizing' | 'advancing' | 'completing-milestone' | 'replanning-slice' | 'complete' | 'paused' | 'blocked';
9
+ export type ContinueStatus = 'in_progress' | 'interrupted' | 'compacted';
10
+
11
+ // ─── Roadmap (Milestone-level) ─────────────────────────────────────────────
12
+
13
+ export interface RoadmapSliceEntry {
14
+ id: string; // e.g. "S01"
15
+ title: string; // e.g. "Types + File I/O + Git Operations"
16
+ risk: RiskLevel;
17
+ depends: string[]; // e.g. ["S01", "S02"]
18
+ done: boolean;
19
+ demo: string; // the "After this:" sentence
20
+ }
21
+
22
+ export interface BoundaryMapEntry {
23
+ fromSlice: string; // e.g. "S01"
24
+ toSlice: string; // e.g. "S02" or "terminal"
25
+ produces: string; // raw text block of what this slice produces
26
+ consumes: string; // raw text block of what it consumes (or "nothing")
27
+ }
28
+
29
+ export interface Roadmap {
30
+ title: string; // e.g. "M001: GSD Extension — Hierarchical Planning with Auto Mode"
31
+ vision: string;
32
+ successCriteria: string[];
33
+ slices: RoadmapSliceEntry[];
34
+ boundaryMap: BoundaryMapEntry[];
35
+ }
36
+
37
+ // ─── Slice Plan ────────────────────────────────────────────────────────────
38
+
39
+ export interface TaskPlanEntry {
40
+ id: string; // e.g. "T01"
41
+ title: string; // e.g. "Core Type Definitions"
42
+ description: string;
43
+ done: boolean;
44
+ estimate: string; // e.g. "30m", "2h" — informational only
45
+ files?: string[]; // e.g. ["types.ts", "files.ts"] — extracted from "- Files:" subline
46
+ verify?: string; // e.g. "run tests" — extracted from "- Verify:" subline
47
+ }
48
+
49
+ export interface SlicePlan {
50
+ id: string; // e.g. "S01"
51
+ title: string; // from the H1
52
+ goal: string;
53
+ demo: string;
54
+ mustHaves: string[]; // top-level must-have bullet points
55
+ tasks: TaskPlanEntry[];
56
+ filesLikelyTouched: string[];
57
+ }
58
+
59
+ // ─── Summary (Task & Slice level) ──────────────────────────────────────────
60
+
61
+ export interface SummaryRequires {
62
+ slice: string;
63
+ provides: string;
64
+ }
65
+
66
+ export interface SummaryFrontmatter {
67
+ id: string;
68
+ parent: string;
69
+ milestone: string;
70
+ provides: string[];
71
+ requires: SummaryRequires[];
72
+ affects: string[];
73
+ key_files: string[];
74
+ key_decisions: string[];
75
+ patterns_established: string[];
76
+ drill_down_paths: string[];
77
+ observability_surfaces: string[];
78
+ duration: string;
79
+ verification_result: string;
80
+ completed_at: string;
81
+ blocker_discovered: boolean;
82
+ }
83
+
84
+ export interface FileModified {
85
+ path: string;
86
+ description: string;
87
+ }
88
+
89
+ export interface Summary {
90
+ frontmatter: SummaryFrontmatter;
91
+ title: string;
92
+ oneLiner: string;
93
+ whatHappened: string;
94
+ deviations: string;
95
+ filesModified: FileModified[];
96
+ }
97
+
98
+ // ─── Continue-Here ─────────────────────────────────────────────────────────
99
+
100
+ export interface ContinueFrontmatter {
101
+ milestone: string;
102
+ slice: string;
103
+ task: string;
104
+ step: number;
105
+ totalSteps: number;
106
+ status: ContinueStatus;
107
+ savedAt: string;
108
+ }
109
+
110
+ export interface Continue {
111
+ frontmatter: ContinueFrontmatter;
112
+ completedWork: string;
113
+ remainingWork: string;
114
+ decisions: string;
115
+ context: string;
116
+ nextAction: string;
117
+ }
118
+
119
+ // ─── GSD State (Derived Dashboard) ────────────────────────────────────────
120
+
121
+ export interface ActiveRef {
122
+ id: string;
123
+ title: string;
124
+ }
125
+
126
+ export interface MilestoneRegistryEntry {
127
+ id: string;
128
+ title: string;
129
+ status: 'complete' | 'active' | 'pending';
130
+ /** Milestone IDs that must be complete before this milestone becomes active. Populated from CONTEXT.md YAML frontmatter. */
131
+ dependsOn?: string[];
132
+ }
133
+
134
+ export interface RequirementCounts {
135
+ active: number;
136
+ validated: number;
137
+ deferred: number;
138
+ outOfScope: number;
139
+ blocked: number;
140
+ total: number;
141
+ }
142
+
143
+ export interface GSDState {
144
+ activeMilestone: ActiveRef | null;
145
+ activeSlice: ActiveRef | null;
146
+ activeTask: ActiveRef | null;
147
+ phase: Phase;
148
+ recentDecisions: string[];
149
+ blockers: string[];
150
+ nextAction: string;
151
+ activeBranch?: string;
152
+ registry: MilestoneRegistryEntry[];
153
+ requirements?: RequirementCounts;
154
+ progress?: {
155
+ milestones: { done: number; total: number };
156
+ slices?: { done: number; total: number };
157
+ tasks?: { done: number; total: number };
158
+ };
159
+ }
@@ -0,0 +1,184 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import {
4
+ gsdRoot,
5
+ relSliceFile,
6
+ relTaskFile,
7
+ resolveSliceFile,
8
+ resolveTaskFile,
9
+ } from "./paths.ts";
10
+ import { loadFile, parseTaskPlanMustHaves, countMustHavesMentionedInSummary } from "./files.ts";
11
+
12
+ export type UnitRuntimePhase =
13
+ | "dispatched"
14
+ | "wrapup-warning-sent"
15
+ | "timeout"
16
+ | "recovered"
17
+ | "finalized"
18
+ | "paused"
19
+ | "skipped";
20
+
21
+ export interface ExecuteTaskRecoveryStatus {
22
+ planPath: string;
23
+ summaryPath: string;
24
+ summaryExists: boolean;
25
+ taskChecked: boolean;
26
+ nextActionAdvanced: boolean;
27
+ mustHaveCount: number;
28
+ mustHavesMentionedInSummary: number;
29
+ }
30
+
31
+ export interface AutoUnitRuntimeRecord {
32
+ version: 1;
33
+ unitType: string;
34
+ unitId: string;
35
+ startedAt: number;
36
+ updatedAt: number;
37
+ phase: UnitRuntimePhase;
38
+ wrapupWarningSent: boolean;
39
+ timeoutAt: number | null;
40
+ lastProgressAt: number;
41
+ progressCount: number;
42
+ lastProgressKind: string;
43
+ recovery?: ExecuteTaskRecoveryStatus;
44
+ recoveryAttempts?: number;
45
+ lastRecoveryReason?: "idle" | "hard";
46
+ }
47
+
48
+ function runtimeDir(basePath: string): string {
49
+ return join(gsdRoot(basePath), "runtime", "units");
50
+ }
51
+
52
+ function runtimePath(basePath: string, unitType: string, unitId: string): string {
53
+ return join(runtimeDir(basePath), `${unitType}-${unitId.replace(/[\/]/g, "-")}.json`);
54
+ }
55
+
56
+ export function writeUnitRuntimeRecord(
57
+ basePath: string,
58
+ unitType: string,
59
+ unitId: string,
60
+ startedAt: number,
61
+ updates: Partial<AutoUnitRuntimeRecord> = {},
62
+ ): AutoUnitRuntimeRecord {
63
+ const dir = runtimeDir(basePath);
64
+ mkdirSync(dir, { recursive: true });
65
+ const path = runtimePath(basePath, unitType, unitId);
66
+ const prev = readUnitRuntimeRecord(basePath, unitType, unitId);
67
+ const next: AutoUnitRuntimeRecord = {
68
+ version: 1,
69
+ unitType,
70
+ unitId,
71
+ startedAt,
72
+ updatedAt: Date.now(),
73
+ phase: updates.phase ?? prev?.phase ?? "dispatched",
74
+ wrapupWarningSent: updates.wrapupWarningSent ?? prev?.wrapupWarningSent ?? false,
75
+ timeoutAt: updates.timeoutAt ?? prev?.timeoutAt ?? null,
76
+ lastProgressAt: updates.lastProgressAt ?? prev?.lastProgressAt ?? Date.now(),
77
+ progressCount: updates.progressCount ?? prev?.progressCount ?? 0,
78
+ lastProgressKind: updates.lastProgressKind ?? prev?.lastProgressKind ?? "dispatch",
79
+ recovery: updates.recovery ?? prev?.recovery,
80
+ recoveryAttempts: updates.recoveryAttempts ?? prev?.recoveryAttempts ?? 0,
81
+ lastRecoveryReason: updates.lastRecoveryReason ?? prev?.lastRecoveryReason,
82
+ };
83
+ writeFileSync(path, JSON.stringify(next, null, 2) + "\n", "utf-8");
84
+ return next;
85
+ }
86
+
87
+ export function readUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): AutoUnitRuntimeRecord | null {
88
+ const path = runtimePath(basePath, unitType, unitId);
89
+ if (!existsSync(path)) return null;
90
+ try {
91
+ return JSON.parse(readFileSync(path, "utf-8")) as AutoUnitRuntimeRecord;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ export function clearUnitRuntimeRecord(basePath: string, unitType: string, unitId: string): void {
98
+ const path = runtimePath(basePath, unitType, unitId);
99
+ if (existsSync(path)) unlinkSync(path);
100
+ }
101
+
102
+ /**
103
+ * Return all runtime records currently on disk for `basePath`.
104
+ * Returns an empty array if the runtime directory does not exist.
105
+ */
106
+ export function listUnitRuntimeRecords(basePath: string): AutoUnitRuntimeRecord[] {
107
+ const dir = runtimeDir(basePath);
108
+ if (!existsSync(dir)) return [];
109
+ const results: AutoUnitRuntimeRecord[] = [];
110
+ for (const file of readdirSync(dir)) {
111
+ if (!file.endsWith(".json")) continue;
112
+ try {
113
+ const raw = readFileSync(join(dir, file), "utf-8");
114
+ const record = JSON.parse(raw) as AutoUnitRuntimeRecord;
115
+ results.push(record);
116
+ } catch {
117
+ // Skip malformed files
118
+ }
119
+ }
120
+ return results;
121
+ }
122
+
123
+ export async function inspectExecuteTaskDurability(
124
+ basePath: string,
125
+ unitId: string,
126
+ ): Promise<ExecuteTaskRecoveryStatus | null> {
127
+ const [mid, sid, tid] = unitId.split("/");
128
+ if (!mid || !sid || !tid) return null;
129
+
130
+ const planAbs = resolveSliceFile(basePath, mid, sid, "PLAN");
131
+ const summaryAbs = resolveTaskFile(basePath, mid, sid, tid, "SUMMARY");
132
+ const stateAbs = join(gsdRoot(basePath), "STATE.md");
133
+
134
+ const planPath = relSliceFile(basePath, mid, sid, "PLAN");
135
+ const summaryPath = relTaskFile(basePath, mid, sid, tid, "SUMMARY");
136
+
137
+ const planContent = planAbs ? await loadFile(planAbs) : null;
138
+ const stateContent = existsSync(stateAbs) ? readFileSync(stateAbs, "utf-8") : "";
139
+ const summaryExists = !!(summaryAbs && existsSync(summaryAbs));
140
+
141
+ const escapedTid = tid.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
142
+ const taskChecked = !!planContent && new RegExp(`^- \\[[xX]\\] \\*\\*${escapedTid}:`, "m").test(planContent);
143
+ const nextActionAdvanced = !new RegExp(`Execute ${tid}\\b`).test(stateContent);
144
+
145
+ // Must-have coverage: load task plan and count mentions in summary
146
+ let mustHaveCount = 0;
147
+ let mustHavesMentionedInSummary = 0;
148
+
149
+ const taskPlanAbs = resolveTaskFile(basePath, mid, sid, tid, "PLAN");
150
+ if (taskPlanAbs) {
151
+ const taskPlanContent = await loadFile(taskPlanAbs);
152
+ if (taskPlanContent) {
153
+ const mustHaves = parseTaskPlanMustHaves(taskPlanContent);
154
+ mustHaveCount = mustHaves.length;
155
+ if (mustHaveCount > 0 && summaryExists && summaryAbs) {
156
+ const summaryContent = await loadFile(summaryAbs);
157
+ if (summaryContent) {
158
+ mustHavesMentionedInSummary = countMustHavesMentionedInSummary(mustHaves, summaryContent);
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ return {
165
+ planPath,
166
+ summaryPath,
167
+ summaryExists,
168
+ taskChecked,
169
+ nextActionAdvanced,
170
+ mustHaveCount,
171
+ mustHavesMentionedInSummary,
172
+ };
173
+ }
174
+
175
+ export function formatExecuteTaskRecoveryStatus(status: ExecuteTaskRecoveryStatus): string {
176
+ const missing = [] as string[];
177
+ if (!status.summaryExists) missing.push(`summary missing (${status.summaryPath})`);
178
+ if (!status.taskChecked) missing.push(`task checkbox unchecked in ${status.planPath}`);
179
+ if (!status.nextActionAdvanced) missing.push("state next action still points at the timed-out task");
180
+ if (status.mustHaveCount > 0 && status.mustHavesMentionedInSummary < status.mustHaveCount) {
181
+ missing.push(`must-have gap: ${status.mustHavesMentionedInSummary} of ${status.mustHaveCount} must-haves addressed in summary`);
182
+ }
183
+ return missing.length > 0 ? missing.join("; ") : "all durable task artifacts present";
184
+ }