@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,94 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { tmpdir } from "node:os";
4
+
5
+ import { getSuggestedNextCommands, indexWorkspace, listDoctorScopeSuggestions } from "../workspace-index.ts";
6
+
7
+ let passed = 0;
8
+ let failed = 0;
9
+
10
+ function assert(condition: boolean, message: string): void {
11
+ if (condition) passed++;
12
+ else {
13
+ failed++;
14
+ console.error(` FAIL: ${message}`);
15
+ }
16
+ }
17
+
18
+ function assertEq<T>(actual: T, expected: T, message: string): void {
19
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
20
+ else {
21
+ failed++;
22
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
23
+ }
24
+ }
25
+
26
+ const base = mkdtempSync(join(tmpdir(), "gsd-workspace-index-test-"));
27
+ const gsd = join(base, ".gsd");
28
+ const mDir = join(gsd, "milestones", "M001");
29
+ const sDir = join(mDir, "slices", "S01");
30
+ const tDir = join(sDir, "tasks");
31
+ mkdirSync(tDir, { recursive: true });
32
+
33
+ writeFileSync(join(mDir, "M001-ROADMAP.md"), `# M001: Demo Milestone
34
+
35
+ ## Slices
36
+ - [ ] **S01: Demo Slice** \`risk:low\` \`depends:[]\`
37
+ > After this: demo works
38
+ `);
39
+
40
+ writeFileSync(join(sDir, "S01-PLAN.md"), `# S01: Demo Slice
41
+
42
+ **Goal:** Demo
43
+ **Demo:** Demo
44
+
45
+ ## Must-Haves
46
+ - done
47
+
48
+ ## Tasks
49
+ - [ ] **T01: Implement thing** \`est:10m\`
50
+ Task is in progress.
51
+ `);
52
+
53
+ writeFileSync(join(tDir, "T01-PLAN.md"), `# T01: Implement thing
54
+
55
+ ## Steps
56
+ - do it
57
+ `);
58
+
59
+ async function main(): Promise<void> {
60
+ console.log("\n=== workspace index ===");
61
+ {
62
+ const index = await indexWorkspace(base);
63
+ assertEq(index.active.milestoneId, "M001", "active milestone indexed");
64
+ assertEq(index.active.sliceId, "S01", "active slice indexed");
65
+ assertEq(index.active.taskId, "T01", "active task indexed");
66
+ assert(index.scopes.some(scope => scope.scope === "M001/S01"), "slice scope listed");
67
+ assert(index.scopes.some(scope => scope.scope === "M001/S01/T01"), "task scope listed");
68
+ }
69
+
70
+ console.log("\n=== doctor scope suggestions ===");
71
+ {
72
+ const suggestions = await listDoctorScopeSuggestions(base);
73
+ assertEq(suggestions[0].value, "M001/S01", "active slice suggested first");
74
+ assert(suggestions.some(item => item.value === "M001/S01/T01"), "task scope suggested");
75
+ }
76
+
77
+ console.log("\n=== next command suggestions ===");
78
+ {
79
+ const commands = await getSuggestedNextCommands(base);
80
+ assert(commands.includes("/gsd auto"), "suggests auto during execution");
81
+ assert(commands.includes("/gsd doctor M001/S01"), "suggests scoped doctor");
82
+ assert(commands.includes("/gsd status"), "suggests status");
83
+ }
84
+
85
+ rmSync(base, { recursive: true, force: true });
86
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
87
+ if (failed > 0) process.exit(1);
88
+ console.log("All tests passed ✓");
89
+ }
90
+
91
+ main().catch((error) => {
92
+ console.error(error);
93
+ process.exit(1);
94
+ });
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Worktree Integration Tests
3
+ *
4
+ * Tests the full lifecycle of GSD operations inside a worktree:
5
+ * - Branch namespacing (gsd/<wt>/<M>/<S> instead of gsd/<M>/<S>)
6
+ * - getMainBranch returns worktree/<name> inside a worktree
7
+ * - switchToMain goes to worktree/<name>, not main
8
+ * - mergeSliceToMain merges into worktree/<name>
9
+ * - Parallel worktrees don't conflict on branch names
10
+ * - State derivation works correctly inside worktrees
11
+ */
12
+
13
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { tmpdir } from "node:os";
16
+ import { execSync } from "node:child_process";
17
+
18
+ import {
19
+ createWorktree,
20
+ listWorktrees,
21
+ removeWorktree,
22
+ worktreePath,
23
+ worktreeBranchName,
24
+ } from "../worktree-manager.ts";
25
+
26
+ import {
27
+ detectWorktreeName,
28
+ ensureSliceBranch,
29
+ getActiveSliceBranch,
30
+ getCurrentBranch,
31
+ getMainBranch,
32
+ getSliceBranchName,
33
+ isOnSliceBranch,
34
+ mergeSliceToMain,
35
+ switchToMain,
36
+ autoCommitCurrentBranch,
37
+ } from "../worktree.ts";
38
+
39
+ import { deriveState } from "../state.ts";
40
+
41
+ let passed = 0;
42
+ let failed = 0;
43
+
44
+ function assert(condition: boolean, message: string): void {
45
+ if (condition) passed++;
46
+ else {
47
+ failed++;
48
+ console.error(` FAIL: ${message}`);
49
+ }
50
+ }
51
+
52
+ function assertEq<T>(actual: T, expected: T, message: string): void {
53
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
54
+ else {
55
+ failed++;
56
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
57
+ }
58
+ }
59
+
60
+ function run(command: string, cwd: string): string {
61
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
62
+ }
63
+
64
+ // ─── Test repo setup ──────────────────────────────────────────────────────────
65
+
66
+ const base = mkdtempSync(join(tmpdir(), "gsd-wt-integration-"));
67
+ run("git init -b main", base);
68
+ run("git config user.name 'Pi Test'", base);
69
+ run("git config user.email 'pi@example.com'", base);
70
+
71
+ // Create a project with one milestone and two slices
72
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
73
+ mkdirSync(join(base, ".gsd", "milestones", "M001", "slices", "S02", "tasks"), { recursive: true });
74
+ writeFileSync(join(base, "README.md"), "# Test Project\n", "utf-8");
75
+ writeFileSync(
76
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
77
+ [
78
+ "# M001: Demo",
79
+ "",
80
+ "## Slices",
81
+ "- [ ] **S01: First** `risk:low` `depends:[]`",
82
+ " > After this: part one works",
83
+ "- [ ] **S02: Second** `risk:low` `depends:[]`",
84
+ " > After this: part two works",
85
+ ].join("\n") + "\n",
86
+ "utf-8",
87
+ );
88
+ writeFileSync(
89
+ join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md"),
90
+ "# S01: First\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",
91
+ "utf-8",
92
+ );
93
+ writeFileSync(
94
+ join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
95
+ "# S02: Second\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",
96
+ "utf-8",
97
+ );
98
+ run("git add .", base);
99
+ run("git commit -m 'chore: init'", base);
100
+
101
+ async function main(): Promise<void> {
102
+ // ── Verify main tree baseline ──────────────────────────────────────────────
103
+
104
+ console.log("\n=== Main tree baseline ===");
105
+ assertEq(getMainBranch(base), "main", "main tree getMainBranch returns main");
106
+ assertEq(detectWorktreeName(base), null, "main tree not detected as worktree");
107
+
108
+ // ── Create worktree and verify detection ───────────────────────────────────
109
+
110
+ console.log("\n=== Create worktree ===");
111
+ const wt = createWorktree(base, "alpha");
112
+ assert(existsSync(wt.path), "worktree created on disk");
113
+ assertEq(wt.branch, "worktree/alpha", "worktree branch name");
114
+
115
+ console.log("\n=== Worktree detection ===");
116
+ assertEq(detectWorktreeName(wt.path), "alpha", "detectWorktreeName inside worktree");
117
+ assertEq(getMainBranch(wt.path), "worktree/alpha", "getMainBranch returns worktree branch inside worktree");
118
+
119
+ // ── Verify current branch inside worktree ──────────────────────────────────
120
+
121
+ console.log("\n=== Worktree initial branch ===");
122
+ assertEq(getCurrentBranch(wt.path), "worktree/alpha", "worktree starts on its own branch");
123
+
124
+ // ── ensureSliceBranch inside worktree ──────────────────────────────────────
125
+
126
+ console.log("\n=== ensureSliceBranch in worktree ===");
127
+ const created = ensureSliceBranch(wt.path, "M001", "S01");
128
+ assert(created, "slice branch created");
129
+ assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "worktree-namespaced slice branch");
130
+ assert(isOnSliceBranch(wt.path), "isOnSliceBranch returns true");
131
+ assertEq(getActiveSliceBranch(wt.path), "gsd/alpha/M001/S01", "getActiveSliceBranch returns namespaced branch");
132
+
133
+ // ── Verify branch name helper ──────────────────────────────────────────────
134
+
135
+ console.log("\n=== getSliceBranchName with worktree ===");
136
+ assertEq(getSliceBranchName("M001", "S01", "alpha"), "gsd/alpha/M001/S01", "explicit worktree param");
137
+ assertEq(getSliceBranchName("M001", "S01"), "gsd/M001/S01", "no worktree param = plain branch");
138
+
139
+ // ── Do work on slice branch, then merge to worktree branch ─────────────────
140
+
141
+ console.log("\n=== Work and merge slice in worktree ===");
142
+ writeFileSync(join(wt.path, "feature.txt"), "new feature\n", "utf-8");
143
+ run("git add .", wt.path);
144
+ run("git commit -m 'feat: add feature'", wt.path);
145
+
146
+ // switchToMain should go to worktree/alpha, NOT main
147
+ switchToMain(wt.path);
148
+ assertEq(getCurrentBranch(wt.path), "worktree/alpha", "switchToMain goes to worktree branch, not main");
149
+
150
+ // mergeSliceToMain should merge into worktree/alpha
151
+ const merge = mergeSliceToMain(wt.path, "M001", "S01", "First");
152
+ assertEq(merge.branch, "gsd/alpha/M001/S01", "merged the namespaced branch");
153
+ assert(merge.deletedBranch, "slice branch deleted after merge");
154
+ assertEq(getCurrentBranch(wt.path), "worktree/alpha", "still on worktree branch after merge");
155
+ assert(readFileSync(join(wt.path, "feature.txt"), "utf-8").includes("new feature"), "merge brought feature to worktree branch");
156
+
157
+ // Verify slice branch is gone
158
+ const branches = run("git branch", base);
159
+ assert(!branches.includes("gsd/alpha/M001/S01"), "slice branch cleaned up");
160
+
161
+ // ── Second slice in same worktree ──────────────────────────────────────────
162
+
163
+ console.log("\n=== Second slice in worktree ===");
164
+ const created2 = ensureSliceBranch(wt.path, "M001", "S02");
165
+ assert(created2, "S02 branch created");
166
+ assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S02", "on S02 namespaced branch");
167
+
168
+ writeFileSync(join(wt.path, "feature2.txt"), "second feature\n", "utf-8");
169
+ run("git add .", wt.path);
170
+ run("git commit -m 'feat: add feature 2'", wt.path);
171
+
172
+ switchToMain(wt.path);
173
+ const merge2 = mergeSliceToMain(wt.path, "M001", "S02", "Second");
174
+ assertEq(merge2.branch, "gsd/alpha/M001/S02", "S02 merge correct");
175
+ assertEq(getCurrentBranch(wt.path), "worktree/alpha", "back on worktree branch");
176
+
177
+ // ── Main tree can still do its own slice work independently ────────────────
178
+
179
+ console.log("\n=== Main tree independent slice work ===");
180
+ assertEq(getCurrentBranch(base), "main", "main tree still on main");
181
+ const mainCreated = ensureSliceBranch(base, "M001", "S01");
182
+ assert(mainCreated, "main tree can create S01 branch (no conflict with worktree)");
183
+ assertEq(getCurrentBranch(base), "gsd/M001/S01", "main tree on plain branch name");
184
+
185
+ writeFileSync(join(base, "main-feature.txt"), "main work\n", "utf-8");
186
+ run("git add .", base);
187
+ run("git commit -m 'feat: main work'", base);
188
+
189
+ switchToMain(base);
190
+ assertEq(getCurrentBranch(base), "main", "main tree switchToMain goes to main");
191
+ const mainMerge = mergeSliceToMain(base, "M001", "S01", "First");
192
+ assertEq(mainMerge.branch, "gsd/M001/S01", "main tree merge uses plain branch");
193
+
194
+ // ── Parallel worktrees don't conflict ──────────────────────────────────────
195
+
196
+ console.log("\n=== Parallel worktrees ===");
197
+ const wt2 = createWorktree(base, "beta");
198
+ assertEq(getMainBranch(wt2.path), "worktree/beta", "second worktree has its own base branch");
199
+
200
+ // Both worktrees can create S01 branches without conflict
201
+ const betaCreated = ensureSliceBranch(wt2.path, "M001", "S01");
202
+ assert(betaCreated, "beta worktree can create S01");
203
+ assertEq(getCurrentBranch(wt2.path), "gsd/beta/M001/S01", "beta has its own namespaced branch");
204
+
205
+ // Alpha worktree can re-create S01 too (it was already merged+deleted earlier)
206
+ const alphaReCreated = ensureSliceBranch(wt.path, "M001", "S01");
207
+ assert(alphaReCreated, "alpha worktree can re-create S01");
208
+ assertEq(getCurrentBranch(wt.path), "gsd/alpha/M001/S01", "alpha re-created S01");
209
+
210
+ // Both exist simultaneously
211
+ const allBranches = run("git branch", base);
212
+ assert(allBranches.includes("gsd/alpha/M001/S01"), "alpha S01 branch exists");
213
+ assert(allBranches.includes("gsd/beta/M001/S01"), "beta S01 branch exists");
214
+
215
+ // ── State derivation in worktree ───────────────────────────────────────────
216
+
217
+ console.log("\n=== State derivation in worktree ===");
218
+ // Switch alpha back to its base so deriveState sees milestone files
219
+ switchToMain(wt.path);
220
+ const state = await deriveState(wt.path);
221
+ assert(state.activeMilestone !== null, "worktree has active milestone");
222
+ assertEq(state.activeMilestone?.id, "M001", "correct milestone");
223
+
224
+ // ── autoCommitCurrentBranch in worktree ────────────────────────────────────
225
+
226
+ console.log("\n=== autoCommitCurrentBranch in worktree ===");
227
+ ensureSliceBranch(wt2.path, "M001", "S01"); // re-checkout if needed
228
+ writeFileSync(join(wt2.path, "dirty.txt"), "uncommitted\n", "utf-8");
229
+ const commitMsg = autoCommitCurrentBranch(wt2.path, "execute-task", "M001/S01/T01");
230
+ assert(commitMsg !== null, "auto-commit works in worktree");
231
+ assertEq(run("git status --short", wt2.path), "", "worktree clean after auto-commit");
232
+
233
+ // ── Cleanup ────────────────────────────────────────────────────────────────
234
+
235
+ console.log("\n=== Cleanup ===");
236
+ // Switch worktrees back to their base branches before removal
237
+ switchToMain(wt.path);
238
+ switchToMain(wt2.path);
239
+ removeWorktree(base, "alpha", { deleteBranch: true });
240
+ removeWorktree(base, "beta", { deleteBranch: true });
241
+ assertEq(listWorktrees(base).length, 0, "all worktrees removed");
242
+
243
+ rmSync(base, { recursive: true, force: true });
244
+
245
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
246
+ if (failed > 0) process.exit(1);
247
+ console.log("All tests passed ✓");
248
+ }
249
+
250
+ main().catch((error) => {
251
+ console.error(error);
252
+ process.exit(1);
253
+ });
@@ -0,0 +1,160 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } 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
+ createWorktree,
8
+ listWorktrees,
9
+ removeWorktree,
10
+ diffWorktreeGSD,
11
+ getWorktreeGSDDiff,
12
+ getWorktreeLog,
13
+ worktreeBranchName,
14
+ worktreePath,
15
+ } from "../worktree-manager.ts";
16
+
17
+ let passed = 0;
18
+ let failed = 0;
19
+
20
+ function assert(condition: boolean, message: string): void {
21
+ if (condition) passed++;
22
+ else {
23
+ failed++;
24
+ console.error(` FAIL: ${message}`);
25
+ }
26
+ }
27
+
28
+ function assertEq<T>(actual: T, expected: T, message: string): void {
29
+ if (JSON.stringify(actual) === JSON.stringify(expected)) passed++;
30
+ else {
31
+ failed++;
32
+ console.error(` FAIL: ${message} — expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`);
33
+ }
34
+ }
35
+
36
+ function run(command: string, cwd: string): string {
37
+ return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
38
+ }
39
+
40
+ // Set up a test repo
41
+ const base = mkdtempSync(join(tmpdir(), "gsd-worktree-mgr-test-"));
42
+ run("git init -b main", base);
43
+ run("git config user.name 'Pi Test'", base);
44
+ run("git config user.email 'pi@example.com'", base);
45
+
46
+ // Create initial project structure
47
+ mkdirSync(join(base, ".gsd", "milestones", "M001"), { recursive: true });
48
+ writeFileSync(join(base, "README.md"), "# Test Project\n", "utf-8");
49
+ writeFileSync(
50
+ join(base, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
51
+ "# M001: Demo\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n > After this: it works\n",
52
+ "utf-8",
53
+ );
54
+ run("git add .", base);
55
+ run("git commit -m 'chore: init'", base);
56
+
57
+ async function main(): Promise<void> {
58
+ console.log("\n=== worktreeBranchName ===");
59
+ assertEq(worktreeBranchName("feature-x"), "worktree/feature-x", "branch name format");
60
+
61
+ console.log("\n=== createWorktree ===");
62
+ const info = createWorktree(base, "feature-x");
63
+ assert(info.name === "feature-x", "name matches");
64
+ assert(info.branch === "worktree/feature-x", "branch matches");
65
+ assert(info.exists, "worktree exists");
66
+ assert(existsSync(info.path), "worktree path exists on disk");
67
+ assert(existsSync(join(info.path, "README.md")), "README.md copied to worktree");
68
+ assert(existsSync(join(info.path, ".gsd", "milestones", "M001", "M001-ROADMAP.md")), ".gsd files copied");
69
+
70
+ // Branch was created
71
+ const branches = run("git branch", base);
72
+ assert(branches.includes("worktree/feature-x"), "branch was created");
73
+
74
+ console.log("\n=== createWorktree — duplicate ===");
75
+ let duplicateError = "";
76
+ try {
77
+ createWorktree(base, "feature-x");
78
+ } catch (e) {
79
+ duplicateError = (e as Error).message;
80
+ }
81
+ assert(duplicateError.includes("already exists"), "duplicate creation fails");
82
+
83
+ console.log("\n=== createWorktree — invalid name ===");
84
+ let invalidError = "";
85
+ try {
86
+ createWorktree(base, "bad name!");
87
+ } catch (e) {
88
+ invalidError = (e as Error).message;
89
+ }
90
+ assert(invalidError.includes("Invalid worktree name"), "invalid name rejected");
91
+
92
+ console.log("\n=== listWorktrees ===");
93
+ const list = listWorktrees(base);
94
+ assertEq(list.length, 1, "one worktree listed");
95
+ assertEq(list[0]!.name, "feature-x", "correct name");
96
+ assertEq(list[0]!.branch, "worktree/feature-x", "correct branch");
97
+ assert(list[0]!.exists, "exists flag is true");
98
+
99
+ console.log("\n=== make changes in worktree ===");
100
+ const wtPath = worktreePath(base, "feature-x");
101
+ // Add a new GSD artifact in the worktree
102
+ mkdirSync(join(wtPath, ".gsd", "milestones", "M002"), { recursive: true });
103
+ writeFileSync(
104
+ join(wtPath, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
105
+ "# M002: New Feature\n\n## Slices\n- [ ] **S01: Setup** `risk:low` `depends:[]`\n > After this: new feature ready\n",
106
+ "utf-8",
107
+ );
108
+ // Modify an existing artifact
109
+ writeFileSync(
110
+ join(wtPath, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
111
+ "# M001: Demo (updated)\n\n## Slices\n- [x] **S01: First** `risk:low` `depends:[]`\n > Done\n",
112
+ "utf-8",
113
+ );
114
+ run("git add .", wtPath);
115
+ run("git commit -m 'feat: add M002 and update M001'", wtPath);
116
+
117
+ console.log("\n=== diffWorktreeGSD ===");
118
+ const diff = diffWorktreeGSD(base, "feature-x");
119
+ assert(diff.added.length > 0, "has added files");
120
+ assert(diff.added.some(f => f.includes("M002")), "M002 roadmap is in added");
121
+ assert(diff.modified.length > 0, "has modified files");
122
+ assert(diff.modified.some(f => f.includes("M001")), "M001 roadmap is in modified");
123
+ assertEq(diff.removed.length, 0, "no removed files");
124
+
125
+ console.log("\n=== getWorktreeGSDDiff ===");
126
+ const fullDiff = getWorktreeGSDDiff(base, "feature-x");
127
+ assert(fullDiff.includes("M002"), "full diff mentions M002");
128
+ assert(fullDiff.includes("updated"), "full diff mentions update");
129
+
130
+ console.log("\n=== getWorktreeLog ===");
131
+ const log = getWorktreeLog(base, "feature-x");
132
+ assert(log.includes("add M002"), "log shows commit message");
133
+
134
+ console.log("\n=== removeWorktree ===");
135
+ removeWorktree(base, "feature-x", { deleteBranch: true });
136
+ assert(!existsSync(wtPath), "worktree directory removed");
137
+ const branchesAfter = run("git branch", base);
138
+ assert(!branchesAfter.includes("worktree/feature-x"), "branch deleted");
139
+
140
+ console.log("\n=== listWorktrees after removal ===");
141
+ const listAfter = listWorktrees(base);
142
+ assertEq(listAfter.length, 0, "no worktrees after removal");
143
+
144
+ console.log("\n=== removeWorktree — already gone ===");
145
+ // Should not throw
146
+ removeWorktree(base, "feature-x", { deleteBranch: true });
147
+ passed++;
148
+
149
+ // Cleanup
150
+ rmSync(base, { recursive: true, force: true });
151
+
152
+ console.log(`\nResults: ${passed} passed, ${failed} failed`);
153
+ if (failed > 0) process.exit(1);
154
+ console.log("All tests passed ✓");
155
+ }
156
+
157
+ main().catch((error) => {
158
+ console.error(error);
159
+ process.exit(1);
160
+ });