gsd-pi 2.11.0 → 2.13.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 (165) hide show
  1. package/dist/cli.js +18 -1
  2. package/dist/onboarding.js +3 -0
  3. package/dist/resource-loader.d.ts +2 -0
  4. package/dist/resource-loader.js +36 -1
  5. package/dist/resources/extensions/bg-shell/index.ts +51 -7
  6. package/dist/resources/extensions/gsd/auto-worktree.ts +509 -0
  7. package/dist/resources/extensions/gsd/auto.ts +381 -13
  8. package/dist/resources/extensions/gsd/commands.ts +9 -3
  9. package/dist/resources/extensions/gsd/doctor.ts +254 -3
  10. package/dist/resources/extensions/gsd/git-self-heal.ts +198 -0
  11. package/dist/resources/extensions/gsd/git-service.ts +11 -0
  12. package/dist/resources/extensions/gsd/guided-flow.ts +81 -9
  13. package/dist/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  14. package/dist/resources/extensions/gsd/preferences.ts +209 -1
  15. package/dist/resources/extensions/gsd/prompt-loader.ts +28 -1
  16. package/dist/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  17. package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  18. package/dist/resources/extensions/gsd/prompts/discuss.md +10 -8
  19. package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
  20. package/dist/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  21. package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  22. package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  23. package/dist/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  24. package/dist/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  25. package/dist/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  26. package/dist/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  27. package/dist/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  28. package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  29. package/dist/resources/extensions/gsd/prompts/queue.md +3 -1
  30. package/dist/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  31. package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
  32. package/dist/resources/extensions/gsd/prompts/system.md +32 -29
  33. package/dist/resources/extensions/gsd/templates/context.md +1 -1
  34. package/dist/resources/extensions/gsd/templates/state.md +3 -3
  35. package/dist/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  36. package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  37. package/dist/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  38. package/dist/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  39. package/dist/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  40. package/dist/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  41. package/dist/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  42. package/dist/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  43. package/dist/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  44. package/dist/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  45. package/dist/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  46. package/dist/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  47. package/dist/resources/extensions/gsd/types.ts +109 -0
  48. package/dist/resources/extensions/gsd/worktree-manager.ts +6 -4
  49. package/dist/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  50. package/dist/resources/extensions/search-the-web/native-search.ts +15 -10
  51. package/dist/resources/extensions/search-the-web/provider.ts +19 -2
  52. package/dist/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  53. package/dist/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  54. package/dist/resources/extensions/search-the-web/tool-search.ts +62 -3
  55. package/dist/wizard.js +1 -0
  56. package/package.json +1 -1
  57. package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
  58. package/packages/pi-agent-core/dist/agent-loop.js +169 -55
  59. package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
  60. package/packages/pi-agent-core/dist/agent.d.ts +13 -1
  61. package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
  62. package/packages/pi-agent-core/dist/agent.js +16 -0
  63. package/packages/pi-agent-core/dist/agent.js.map +1 -1
  64. package/packages/pi-agent-core/dist/types.d.ts +91 -1
  65. package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
  66. package/packages/pi-agent-core/dist/types.js.map +1 -1
  67. package/packages/pi-agent-core/src/agent-loop.ts +273 -63
  68. package/packages/pi-agent-core/src/agent.ts +24 -0
  69. package/packages/pi-agent-core/src/types.ts +98 -0
  70. package/packages/pi-ai/dist/env-api-keys.js +1 -0
  71. package/packages/pi-ai/dist/env-api-keys.js.map +1 -1
  72. package/packages/pi-ai/dist/models.generated.d.ts +314 -0
  73. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  74. package/packages/pi-ai/dist/models.generated.js +236 -0
  75. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  76. package/packages/pi-ai/dist/types.d.ts +1 -1
  77. package/packages/pi-ai/dist/types.d.ts.map +1 -1
  78. package/packages/pi-ai/dist/types.js.map +1 -1
  79. package/packages/pi-ai/src/env-api-keys.ts +1 -0
  80. package/packages/pi-ai/src/models.generated.ts +236 -0
  81. package/packages/pi-ai/src/types.ts +2 -1
  82. package/packages/pi-coding-agent/dist/cli/args.d.ts.map +1 -1
  83. package/packages/pi-coding-agent/dist/cli/args.js +2 -1
  84. package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
  85. package/packages/pi-coding-agent/dist/core/agent-session.d.ts +10 -0
  86. package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
  87. package/packages/pi-coding-agent/dist/core/agent-session.js +69 -8
  88. package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
  89. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +4 -1
  90. package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
  91. package/packages/pi-coding-agent/dist/core/extensions/runner.js +2 -1
  92. package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
  93. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +5 -0
  94. package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
  95. package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
  96. package/packages/pi-coding-agent/dist/core/model-resolver.d.ts.map +1 -1
  97. package/packages/pi-coding-agent/dist/core/model-resolver.js +1 -0
  98. package/packages/pi-coding-agent/dist/core/model-resolver.js.map +1 -1
  99. package/packages/pi-coding-agent/dist/core/sdk.js +3 -3
  100. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  101. package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
  102. package/packages/pi-coding-agent/dist/core/system-prompt.js +6 -0
  103. package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
  104. package/packages/pi-coding-agent/src/cli/args.ts +2 -1
  105. package/packages/pi-coding-agent/src/core/agent-session.ts +76 -7
  106. package/packages/pi-coding-agent/src/core/extensions/runner.ts +2 -1
  107. package/packages/pi-coding-agent/src/core/extensions/types.ts +2 -0
  108. package/packages/pi-coding-agent/src/core/model-resolver.ts +1 -0
  109. package/packages/pi-coding-agent/src/core/sdk.ts +3 -3
  110. package/packages/pi-coding-agent/src/core/system-prompt.ts +9 -0
  111. package/packages/pi-tui/dist/components/editor.d.ts +11 -0
  112. package/packages/pi-tui/dist/components/editor.d.ts.map +1 -1
  113. package/packages/pi-tui/dist/components/editor.js +64 -6
  114. package/packages/pi-tui/dist/components/editor.js.map +1 -1
  115. package/packages/pi-tui/src/components/editor.ts +71 -6
  116. package/src/resources/extensions/bg-shell/index.ts +51 -7
  117. package/src/resources/extensions/gsd/auto-worktree.ts +509 -0
  118. package/src/resources/extensions/gsd/auto.ts +381 -13
  119. package/src/resources/extensions/gsd/commands.ts +9 -3
  120. package/src/resources/extensions/gsd/doctor.ts +254 -3
  121. package/src/resources/extensions/gsd/git-self-heal.ts +198 -0
  122. package/src/resources/extensions/gsd/git-service.ts +11 -0
  123. package/src/resources/extensions/gsd/guided-flow.ts +81 -9
  124. package/src/resources/extensions/gsd/post-unit-hooks.ts +449 -0
  125. package/src/resources/extensions/gsd/preferences.ts +209 -1
  126. package/src/resources/extensions/gsd/prompt-loader.ts +28 -1
  127. package/src/resources/extensions/gsd/prompts/complete-milestone.md +1 -1
  128. package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -3
  129. package/src/resources/extensions/gsd/prompts/discuss.md +10 -8
  130. package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
  131. package/src/resources/extensions/gsd/prompts/guided-complete-slice.md +3 -1
  132. package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +3 -1
  133. package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +3 -1
  134. package/src/resources/extensions/gsd/prompts/guided-execute-task.md +3 -1
  135. package/src/resources/extensions/gsd/prompts/guided-plan-milestone.md +4 -2
  136. package/src/resources/extensions/gsd/prompts/guided-plan-slice.md +3 -1
  137. package/src/resources/extensions/gsd/prompts/guided-research-slice.md +3 -1
  138. package/src/resources/extensions/gsd/prompts/plan-milestone.md +9 -12
  139. package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -3
  140. package/src/resources/extensions/gsd/prompts/queue.md +3 -1
  141. package/src/resources/extensions/gsd/prompts/research-milestone.md +1 -1
  142. package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
  143. package/src/resources/extensions/gsd/prompts/system.md +32 -29
  144. package/src/resources/extensions/gsd/templates/context.md +1 -1
  145. package/src/resources/extensions/gsd/templates/state.md +3 -3
  146. package/src/resources/extensions/gsd/tests/auto-worktree-merge.test.ts +282 -0
  147. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +259 -0
  148. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +147 -0
  149. package/src/resources/extensions/gsd/tests/doctor-git.test.ts +246 -0
  150. package/src/resources/extensions/gsd/tests/doctor.test.ts +115 -1
  151. package/src/resources/extensions/gsd/tests/git-self-heal.test.ts +234 -0
  152. package/src/resources/extensions/gsd/tests/isolation-resolver.test.ts +107 -0
  153. package/src/resources/extensions/gsd/tests/post-unit-hooks.test.ts +297 -0
  154. package/src/resources/extensions/gsd/tests/preferences-git.test.ts +88 -0
  155. package/src/resources/extensions/gsd/tests/preferences-hooks.test.ts +226 -0
  156. package/src/resources/extensions/gsd/tests/regex-hardening.test.ts +12 -0
  157. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +315 -0
  158. package/src/resources/extensions/gsd/types.ts +109 -0
  159. package/src/resources/extensions/gsd/worktree-manager.ts +6 -4
  160. package/src/resources/extensions/search-the-web/command-search-provider.ts +8 -4
  161. package/src/resources/extensions/search-the-web/native-search.ts +15 -10
  162. package/src/resources/extensions/search-the-web/provider.ts +19 -2
  163. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +62 -0
  164. package/src/resources/extensions/search-the-web/tool-llm-context.ts +62 -3
  165. package/src/resources/extensions/search-the-web/tool-search.ts +62 -3
@@ -120,17 +120,37 @@ Templates showing the expected format for each artifact type are in:
120
120
 
121
121
  ## Execution Heuristics
122
122
 
123
- ### Tool-routing hierarchy
123
+ ### Tool rules
124
124
 
125
- Use the lightest sufficient tool first.
125
+ **File reading:** Use `read` for inspecting files. Never use `cat`, `head`, `tail`, or `sed -n` to view file contents. Use `read` with `offset`/`limit` for slicing. `bash` is for searching (`rg`, `grep`, `find`) and running commands — not for displaying file contents.
126
126
 
127
- - Broad unfamiliar subsystem mapping -> `subagent` with `scout`
128
- - Library, package, or framework truth -> `resolve_library` then `get_library_docs`
129
- - Current external facts -> `search-the-web` + `fetch_page`, or `search_and_read` for one-call extraction
130
- - Long-running processes (servers, watchers, persistent daemons) -> `bg_shell` with `start` + `wait_for_ready`
131
- - Background process status -> `bg_shell` with `digest` (not `output`). Token budget: `digest` (~30 tokens) < `highlights` (~100) < `output` (~2000).
132
- - One-shot commands where you want the result delivered back (builds, tests, installs) -> `async_bash`; result is pushed to you automatically when the command exits.
133
- - Secrets -> `secure_env_collect`
127
+ **File editing:** Always `read` a file before using `edit`. The `edit` tool requires exact text match — you need the real content, not a guess. Use `write` only for new files or complete rewrites.
128
+
129
+ **Code navigation:** Use `lsp` for go-to-definition, find-references, and type info. Falls back gracefully if no server is available. Never `grep` for a symbol definition when `lsp` can resolve it semantically.
130
+
131
+ **Codebase exploration:** Use `subagent` with `scout` for broad unfamiliar subsystem mapping. Use `rg` for text search across files. Use `lsp` for structural navigation. Never read files one-by-one to "explore" — search first, then read what's relevant.
132
+
133
+ **Documentation lookup:** Use `resolve_library` → `get_library_docs` for library/framework questions. Start with `tokens=5000`. Never guess at API signatures from memory when docs are available.
134
+
135
+ **External facts:** Use `search-the-web` + `fetch_page`, or `search_and_read` for one-call extraction. Use `freshness` for recency. Never state current facts from training data without verification.
136
+
137
+ **Background processes:** Use `bg_shell` with `start` + `wait_for_ready` for servers, watchers, and daemons. Never poll with `sleep`/retry loops — `wait_for_ready` exists for this. For status checks, use `digest` (~30 tokens), not `output` (~2000 tokens). Use `highlights` (~100 tokens) when you need significant lines only. Use `output` only when actively debugging.
138
+
139
+ **One-shot commands:** Use `async_bash` for builds, tests, and installs. The result is pushed to you when the command exits — no polling needed. Use `await_job` to block on a specific job.
140
+
141
+ **Secrets:** Use `secure_env_collect`. Never ask the user to edit `.env` files or paste secrets.
142
+
143
+ **Browser verification:** Verify frontend work against a running app. Discovery: `browser_find`/`browser_snapshot_refs`. Action: refs/selectors → `browser_batch` for obvious sequences. Verification: `browser_assert` for explicit pass/fail. Diagnostics: `browser_diff` for ambiguous outcomes → console/network logs when assertions fail → full page inspection as last resort. Debug in order: failing assertion → diff → diagnostics → element state → broader inspection. Retry only with a new hypothesis.
144
+
145
+ ### Anti-patterns — never do these
146
+
147
+ - Never use `cat` to read a file you might edit — `read` gives you the exact text `edit` needs.
148
+ - Never `grep` for a function definition when `lsp` go-to-definition is available.
149
+ - Never poll a server with `sleep 1 && curl` loops — use `bg_shell` `wait_for_ready`.
150
+ - Never use `bg_shell` `output` for a status check — use `digest`.
151
+ - Never read files one-by-one to understand a subsystem — use `rg` or `scout` first.
152
+ - Never guess at library APIs from training data — use `get_library_docs`.
153
+ - Never ask the user to run a command, set a variable, or check something you can check yourself.
134
154
 
135
155
  ### Ask vs infer
136
156
 
@@ -150,32 +170,15 @@ Verify according to task type: bug fix → rerun repro, script fix → rerun com
150
170
 
151
171
  For non-trivial work, verify both the feature and the failure/diagnostic surface. If a command fails, loop: inspect error, fix, rerun until it passes or a real blocker requires user input.
152
172
 
173
+ Work is not done when the code compiles. Work is done when the verification passes.
174
+
153
175
  ### Agent-First Observability
154
176
 
155
177
  For relevant work: add health/status surfaces, persist failure state (last error, phase, timestamp, retry count), verify both happy path and at least one diagnostic signal. Never log secrets. Remove noisy one-off instrumentation before finishing unless it provides durable diagnostic value.
156
178
 
157
179
  ### Root-cause-first debugging
158
180
 
159
- Fix the root cause, not symptoms. When applying a temporary mitigation, label it clearly and preserve the path to the real fix.
160
-
161
- ## Situational Playbooks
162
-
163
- ### Background processes
164
-
165
- Use `bg_shell` for persistent processes — servers, watchers, anything that keeps running. Set `type:'server'` + `ready_port` for dev servers, `group:'name'` for related processes. Use `wait_for_ready` instead of polling. Use `digest` for status checks, `highlights` for significant output, `output` only when debugging. Use `send_and_wait` for interactive CLIs. Kill processes when done.
166
-
167
- Use `async_bash` for one-shot commands (builds, tests, installs) where you want the output delivered back automatically. Result arrives as a follow-up message when the command exits — no polling needed. Use `await_job` to explicitly wait for a specific job, `cancel_job` to stop one, `/jobs` to see what's running.
168
-
169
- ### Web behavior
170
-
171
- Verify frontend work with browser tools against a running app. Operating order: `browser_find`/`browser_snapshot_refs` for discovery → refs/selectors for targeting → `browser_batch` for obvious sequences → `browser_assert` for verification → `browser_diff` for ambiguous outcomes → console/network logs when assertions fail → full page inspection as last resort.
172
-
173
- Debug browser failures in order: failing assertion → `browser_diff` → console/network diagnostics → element/accessibility state → broader inspection. Retry only with a new hypothesis.
174
-
175
- ### Libraries and current facts
176
-
177
- - Libraries: `resolve_library` → `get_library_docs` with specific topic query. Start with `tokens=5000`.
178
- - Current facts: `search-the-web` to evaluate the landscape and pick URLs, or `search_and_read` when you know what you're looking for. Use `freshness` for recency, `domain` to scope to a specific site.
181
+ Fix the root cause, not symptoms. When applying a temporary mitigation, label it clearly and preserve the path to the real fix. Never add a guard or try/catch to suppress an error you haven't diagnosed.
179
182
 
180
183
  ## Communication
181
184
 
@@ -1,4 +1,4 @@
1
- # {{milestoneId}}: {{milestoneTitle}} — Context
1
+ # {{milestoneId}}: {{milestoneTitle}}
2
2
 
3
3
  **Gathered:** {{date}}
4
4
  **Status:** Ready for planning
@@ -1,8 +1,8 @@
1
1
  # GSD State
2
2
 
3
- **Active Milestone:** {{milestoneId}} {{milestoneTitle}}
4
- **Active Slice:** {{sliceId}} {{sliceTitle}}
5
- **Active Task:** {{taskId}} {{taskTitle}}
3
+ **Active Milestone:** {{milestoneId}}: {{milestoneTitle}}
4
+ **Active Slice:** {{sliceId}}: {{sliceTitle}}
5
+ **Active Task:** {{taskId}}: {{taskTitle}}
6
6
  **Phase:** {{phase}}
7
7
  **Slice Branch:** {{activeBranch}}
8
8
  **Active Workspace:** {{activeWorkspace}}
@@ -0,0 +1,282 @@
1
+ /**
2
+ * auto-worktree-merge.test.ts — Integration tests for mergeSliceToMilestone.
3
+ *
4
+ * Covers: --no-ff merge topology, rich commit messages, slice branch deletion,
5
+ * zero-commit error, real code conflicts, .gsd/ non-conflict in worktree mode.
6
+ * All tests use real git operations in temp repos.
7
+ */
8
+
9
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { execSync } from "node:child_process";
13
+
14
+ import {
15
+ createAutoWorktree,
16
+ teardownAutoWorktree,
17
+ mergeSliceToMilestone,
18
+ } from "../auto-worktree.ts";
19
+ import { MergeConflictError } from "../git-service.ts";
20
+ import { getSliceBranchName } from "../worktree.ts";
21
+
22
+ import { createTestContext } from "./test-helpers.ts";
23
+
24
+ const { assertEq, assertTrue, assertMatch, report } = createTestContext();
25
+
26
+ function run(cmd: string, cwd: string): string {
27
+ return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
28
+ }
29
+
30
+ function createTempRepo(): string {
31
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-merge-test-")));
32
+ run("git init", dir);
33
+ run("git config user.email test@test.com", dir);
34
+ run("git config user.name Test", dir);
35
+ writeFileSync(join(dir, "README.md"), "# test\n");
36
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
37
+ writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
38
+ run("git add .", dir);
39
+ run("git commit -m init", dir);
40
+ run("git branch -M main", dir);
41
+ return dir;
42
+ }
43
+
44
+ /** Create a slice branch in the worktree, add commits, return branch name. */
45
+ function setupSliceBranch(
46
+ wtPath: string,
47
+ milestoneId: string,
48
+ sliceId: string,
49
+ commits: Array<{ file: string; content: string; message: string }>,
50
+ ): string {
51
+ // Detect worktree name for branch naming
52
+ const normalizedPath = wtPath.replaceAll("\\", "/");
53
+ const marker = "/.gsd/worktrees/";
54
+ const idx = normalizedPath.indexOf(marker);
55
+ const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
56
+ const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
57
+
58
+ run(`git checkout -b ${sliceBranch}`, wtPath);
59
+ for (const c of commits) {
60
+ writeFileSync(join(wtPath, c.file), c.content);
61
+ run("git add .", wtPath);
62
+ run(`git commit -m "${c.message}"`, wtPath);
63
+ }
64
+ return sliceBranch;
65
+ }
66
+
67
+ async function main(): Promise<void> {
68
+ const savedCwd = process.cwd();
69
+ const tempDirs: string[] = [];
70
+
71
+ function freshRepo(): string {
72
+ const d = createTempRepo();
73
+ tempDirs.push(d);
74
+ return d;
75
+ }
76
+
77
+ try {
78
+ // ─── Test 1: Single slice --no-ff merge ────────────────────────────
79
+ console.log("\n=== single slice --no-ff merge ===");
80
+ {
81
+ const repo = freshRepo();
82
+ const wtPath = createAutoWorktree(repo, "M003");
83
+
84
+ const sliceBranch = setupSliceBranch(wtPath, "M003", "S01", [
85
+ { file: "a.ts", content: "const a = 1;\n", message: "add a.ts" },
86
+ { file: "b.ts", content: "const b = 2;\n", message: "add b.ts" },
87
+ { file: "c.ts", content: "const c = 3;\n", message: "add c.ts" },
88
+ ]);
89
+ run("git checkout milestone/M003", wtPath);
90
+
91
+ const result = mergeSliceToMilestone(repo, "M003", "S01", "Add core files");
92
+
93
+ // Verify we're back on milestone branch
94
+ const branch = run("git branch --show-current", wtPath);
95
+ assertEq(branch, "milestone/M003", "back on milestone branch after merge");
96
+
97
+ // Verify merge topology via git log --graph
98
+ const log = run("git log --oneline --graph", wtPath);
99
+ assertTrue(log.includes("* "), "merge commit visible in graph (asterisk with two parents)");
100
+ assertTrue(log.includes("add a.ts"), "slice commit 'add a.ts' visible");
101
+ assertTrue(log.includes("add b.ts"), "slice commit 'add b.ts' visible");
102
+ assertTrue(log.includes("add c.ts"), "slice commit 'add c.ts' visible");
103
+
104
+ // Verify commit message format
105
+ assertMatch(result.mergedCommitMessage, /feat\(M003\/S01\)/, "commit message has conventional format");
106
+ assertTrue(result.mergedCommitMessage.includes("Add core files"), "commit message includes slice title");
107
+
108
+ // Verify slice branch deleted
109
+ assertTrue(result.deletedBranch, "slice branch deleted");
110
+ const branches = run("git branch", wtPath);
111
+ assertTrue(!branches.includes(sliceBranch), "slice branch no longer in git branch list");
112
+
113
+ teardownAutoWorktree(repo, "M003");
114
+ }
115
+
116
+ // ─── Test 2: Two sequential slices ─────────────────────────────────
117
+ console.log("\n=== two sequential slices ===");
118
+ {
119
+ const repo = freshRepo();
120
+ const wtPath = createAutoWorktree(repo, "M003");
121
+
122
+ // Slice S01
123
+ setupSliceBranch(wtPath, "M003", "S01", [
124
+ { file: "s1.ts", content: "export const s1 = 1;\n", message: "s1 work" },
125
+ ]);
126
+ run("git checkout milestone/M003", wtPath);
127
+ mergeSliceToMilestone(repo, "M003", "S01", "First slice");
128
+
129
+ // Slice S02
130
+ setupSliceBranch(wtPath, "M003", "S02", [
131
+ { file: "s2.ts", content: "export const s2 = 2;\n", message: "s2 work" },
132
+ ]);
133
+ run("git checkout milestone/M003", wtPath);
134
+ mergeSliceToMilestone(repo, "M003", "S02", "Second slice");
135
+
136
+ // Verify two merge boundaries
137
+ const log = run("git log --oneline --graph", wtPath);
138
+ const mergeLines = log.split("\n").filter(l => l.includes("* "));
139
+ assertTrue(mergeLines.length >= 2, "two distinct merge commits in graph");
140
+ assertTrue(log.includes("s1 work"), "S01 commit visible");
141
+ assertTrue(log.includes("s2 work"), "S02 commit visible");
142
+
143
+ teardownAutoWorktree(repo, "M003");
144
+ }
145
+
146
+ // ─── Test 3: Zero commits throws ───────────────────────────────────
147
+ console.log("\n=== zero commits throws ===");
148
+ {
149
+ const repo = freshRepo();
150
+ const wtPath = createAutoWorktree(repo, "M003");
151
+
152
+ // Create slice branch with no commits ahead
153
+ const normalizedPath = wtPath.replaceAll("\\", "/");
154
+ const marker = "/.gsd/worktrees/";
155
+ const idx = normalizedPath.indexOf(marker);
156
+ const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
157
+ const sliceBranch = getSliceBranchName("M003", "S01", worktreeName);
158
+ run(`git checkout -b ${sliceBranch}`, wtPath);
159
+ // No commits — immediately try to merge
160
+ run(`git checkout milestone/M003`, wtPath);
161
+
162
+ let threw = false;
163
+ try {
164
+ mergeSliceToMilestone(repo, "M003", "S01", "Empty slice");
165
+ } catch (err) {
166
+ threw = true;
167
+ assertTrue(
168
+ err instanceof Error && err.message.includes("no commits ahead"),
169
+ "error message mentions no commits ahead",
170
+ );
171
+ }
172
+ assertTrue(threw, "mergeSliceToMilestone throws on zero commits");
173
+
174
+ teardownAutoWorktree(repo, "M003");
175
+ }
176
+
177
+ // ─── Test 4: Real code conflict throws MergeConflictError ──────────
178
+ console.log("\n=== real code conflict throws MergeConflictError ===");
179
+ {
180
+ const repo = freshRepo();
181
+ const wtPath = createAutoWorktree(repo, "M003");
182
+
183
+ // Add a file on milestone branch
184
+ writeFileSync(join(wtPath, "shared.ts"), "// version 1\n");
185
+ run("git add .", wtPath);
186
+ run('git commit -m "add shared.ts"', wtPath);
187
+
188
+ // Create slice branch, modify same file differently
189
+ const normalizedPath = wtPath.replaceAll("\\", "/");
190
+ const marker = "/.gsd/worktrees/";
191
+ const idx = normalizedPath.indexOf(marker);
192
+ const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
193
+ const sliceBranch = getSliceBranchName("M003", "S01", worktreeName);
194
+ run(`git checkout -b ${sliceBranch}`, wtPath);
195
+ writeFileSync(join(wtPath, "shared.ts"), "// slice version\nexport const x = 1;\n");
196
+ run("git add .", wtPath);
197
+ run('git commit -m "slice edit shared.ts"', wtPath);
198
+
199
+ // Modify same file on milestone branch
200
+ run("git checkout milestone/M003", wtPath);
201
+ writeFileSync(join(wtPath, "shared.ts"), "// milestone version\nexport const y = 2;\n");
202
+ run("git add .", wtPath);
203
+ run('git commit -m "milestone edit shared.ts"', wtPath);
204
+
205
+ // Go back to milestone branch for merge call
206
+ run("git checkout milestone/M003", wtPath);
207
+
208
+ let caught: MergeConflictError | null = null;
209
+ try {
210
+ mergeSliceToMilestone(repo, "M003", "S01", "Conflicting slice");
211
+ } catch (err) {
212
+ if (err instanceof MergeConflictError) {
213
+ caught = err;
214
+ } else {
215
+ throw err;
216
+ }
217
+ }
218
+
219
+ assertTrue(caught !== null, "MergeConflictError thrown on conflict");
220
+ if (caught) {
221
+ assertTrue(caught.conflictedFiles.includes("shared.ts"), "conflictedFiles includes shared.ts");
222
+ assertEq(caught.strategy, "merge", "strategy is merge");
223
+ assertTrue(caught.branch.includes("S01"), "branch includes S01");
224
+ }
225
+
226
+ // Clean up conflict state before teardown
227
+ run("git merge --abort || true", wtPath);
228
+ run("git checkout milestone/M003", wtPath);
229
+ teardownAutoWorktree(repo, "M003");
230
+ }
231
+
232
+ // ─── Test 5: .gsd/ changes don't conflict ─────────────────────────
233
+ console.log("\n=== .gsd/ changes don't conflict ===");
234
+ {
235
+ const repo = freshRepo();
236
+ const wtPath = createAutoWorktree(repo, "M003");
237
+
238
+ // The .gsd/ directory in worktrees is local — it's not shared via git
239
+ // between the main repo and the worktree. So modifications to .gsd/
240
+ // files in both branches shouldn't cause conflicts because .gsd/ is
241
+ // in the main repo's tree but the worktree has its own working copy.
242
+ //
243
+ // In the worktree, .gsd/ IS tracked (inherited from main). But since
244
+ // slice branches diverge from milestone branch, .gsd/ changes on both
245
+ // can conflict. The key insight: in real auto-mode, .gsd/ changes only
246
+ // happen on the milestone branch (planning artifacts), not on slice
247
+ // branches (which only have code changes). So we test that code-only
248
+ // slice commits merge cleanly even when milestone has .gsd/ changes.
249
+
250
+ // Add a .gsd/ change on milestone branch
251
+ writeFileSync(join(wtPath, ".gsd", "STATE.md"), "# Updated State\nactive: M003\n");
252
+ run("git add .", wtPath);
253
+ run('git commit -m "update .gsd/STATE.md on milestone"', wtPath);
254
+
255
+ // Create slice branch with code-only changes
256
+ setupSliceBranch(wtPath, "M003", "S01", [
257
+ { file: "feature.ts", content: "export const feature = true;\n", message: "add feature" },
258
+ ]);
259
+ run("git checkout milestone/M003", wtPath);
260
+
261
+ // Merge should succeed — no .gsd/ conflict since slice didn't touch .gsd/
262
+ const result = mergeSliceToMilestone(repo, "M003", "S01", "Feature slice");
263
+ assertTrue(result.branch.includes("S01"), ".gsd/ no-conflict merge succeeded");
264
+ assertTrue(result.deletedBranch, "slice branch deleted after .gsd/-safe merge");
265
+
266
+ // Verify feature file exists after merge
267
+ assertTrue(existsSync(join(wtPath, "feature.ts")), "feature.ts present after merge");
268
+
269
+ teardownAutoWorktree(repo, "M003");
270
+ }
271
+
272
+ } finally {
273
+ process.chdir(savedCwd);
274
+ for (const d of tempDirs) {
275
+ if (existsSync(d)) rmSync(d, { recursive: true, force: true });
276
+ }
277
+ }
278
+
279
+ report();
280
+ }
281
+
282
+ main();
@@ -0,0 +1,259 @@
1
+ /**
2
+ * auto-worktree-milestone-merge.test.ts — Integration tests for mergeMilestoneToMain.
3
+ *
4
+ * Covers: squash-merge topology (one commit on main), rich commit message with
5
+ * slice titles, worktree cleanup, nothing-to-commit edge case, auto-push with
6
+ * bare remote. All tests use real git operations in temp repos.
7
+ */
8
+
9
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, existsSync, realpathSync, readFileSync } from "node:fs";
10
+ import { join } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { execSync } from "node:child_process";
13
+
14
+ import {
15
+ createAutoWorktree,
16
+ mergeMilestoneToMain,
17
+ mergeSliceToMilestone,
18
+ getAutoWorktreeOriginalBase,
19
+ } from "../auto-worktree.ts";
20
+ import { getSliceBranchName } from "../worktree.ts";
21
+
22
+ import { createTestContext } from "./test-helpers.ts";
23
+
24
+ const { assertEq, assertTrue, assertMatch, report } = createTestContext();
25
+
26
+ function run(cmd: string, cwd: string): string {
27
+ return execSync(cmd, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
28
+ }
29
+
30
+ function createTempRepo(): string {
31
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-merge-test-")));
32
+ run("git init", dir);
33
+ run("git config user.email test@test.com", dir);
34
+ run("git config user.name Test", dir);
35
+ writeFileSync(join(dir, "README.md"), "# test\n");
36
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
37
+ writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
38
+ run("git add .", dir);
39
+ run("git commit -m init", dir);
40
+ run("git branch -M main", dir);
41
+ return dir;
42
+ }
43
+
44
+ /** Minimal roadmap content for mergeMilestoneToMain. */
45
+ function makeRoadmap(milestoneId: string, title: string, slices: Array<{ id: string; title: string }>): string {
46
+ const sliceLines = slices.map(s => `- [x] **${s.id}: ${s.title}**`).join("\n");
47
+ return `# ${milestoneId}: ${title}\n\n## Slices\n${sliceLines}\n`;
48
+ }
49
+
50
+ /** Set up a slice branch on the worktree, add commits, merge it --no-ff to milestone. */
51
+ function addSliceToMilestone(
52
+ repo: string,
53
+ wtPath: string,
54
+ milestoneId: string,
55
+ sliceId: string,
56
+ sliceTitle: string,
57
+ commits: Array<{ file: string; content: string; message: string }>,
58
+ ): void {
59
+ // Detect worktree name for branch naming
60
+ const normalizedPath = wtPath.replaceAll("\\", "/");
61
+ const marker = "/.gsd/worktrees/";
62
+ const idx = normalizedPath.indexOf(marker);
63
+ const worktreeName = idx !== -1 ? normalizedPath.slice(idx + marker.length).split("/")[0] : null;
64
+
65
+ const sliceBranch = getSliceBranchName(milestoneId, sliceId, worktreeName);
66
+
67
+ run(`git checkout -b ${sliceBranch}`, wtPath);
68
+ for (const c of commits) {
69
+ writeFileSync(join(wtPath, c.file), c.content);
70
+ run("git add .", wtPath);
71
+ run(`git commit -m "${c.message}"`, wtPath);
72
+ }
73
+ run(`git checkout milestone/${milestoneId}`, wtPath);
74
+ mergeSliceToMilestone(repo, milestoneId, sliceId, sliceTitle);
75
+ }
76
+
77
+ async function main(): Promise<void> {
78
+ const savedCwd = process.cwd();
79
+ const tempDirs: string[] = [];
80
+
81
+ function freshRepo(): string {
82
+ const d = createTempRepo();
83
+ tempDirs.push(d);
84
+ return d;
85
+ }
86
+
87
+ try {
88
+ // ─── Test 1: Basic squash merge — one commit on main ───────────────
89
+ console.log("\n=== basic squash merge — one commit on main ===");
90
+ {
91
+ const repo = freshRepo();
92
+ const wtPath = createAutoWorktree(repo, "M010");
93
+
94
+ // Add two slices with multiple commits each
95
+ addSliceToMilestone(repo, wtPath, "M010", "S01", "Auth module", [
96
+ { file: "auth.ts", content: "export const auth = true;\n", message: "add auth" },
97
+ { file: "auth-utils.ts", content: "export const hash = () => {};\n", message: "add auth utils" },
98
+ ]);
99
+ addSliceToMilestone(repo, wtPath, "M010", "S02", "User dashboard", [
100
+ { file: "dashboard.ts", content: "export const dash = true;\n", message: "add dashboard" },
101
+ { file: "widgets.ts", content: "export const widgets = [];\n", message: "add widgets" },
102
+ ]);
103
+
104
+ const roadmap = makeRoadmap("M010", "User management", [
105
+ { id: "S01", title: "Auth module" },
106
+ { id: "S02", title: "User dashboard" },
107
+ ]);
108
+
109
+ const mainLogBefore = run("git log --oneline main", repo);
110
+ const mainCommitCountBefore = mainLogBefore.split("\n").length;
111
+
112
+ const result = mergeMilestoneToMain(repo, "M010", roadmap);
113
+
114
+ // Exactly one new commit on main
115
+ const mainLog = run("git log --oneline main", repo);
116
+ const mainCommitCountAfter = mainLog.split("\n").length;
117
+ assertEq(mainCommitCountAfter, mainCommitCountBefore + 1, "exactly one new commit on main");
118
+
119
+ // Milestone branch deleted
120
+ const branches = run("git branch", repo);
121
+ assertTrue(!branches.includes("milestone/M010"), "milestone branch deleted");
122
+
123
+ // Worktree directory removed
124
+ const worktreeDir = join(repo, ".gsd", "worktrees", "M010");
125
+ assertTrue(!existsSync(worktreeDir), "worktree directory removed");
126
+
127
+ // Module state cleared
128
+ assertEq(getAutoWorktreeOriginalBase(), null, "originalBase cleared after merge");
129
+
130
+ // Files from both slices present on main
131
+ assertTrue(existsSync(join(repo, "auth.ts")), "auth.ts on main");
132
+ assertTrue(existsSync(join(repo, "dashboard.ts")), "dashboard.ts on main");
133
+ assertTrue(existsSync(join(repo, "widgets.ts")), "widgets.ts on main");
134
+
135
+ // Result shape
136
+ assertTrue(result.commitMessage.length > 0, "commitMessage returned");
137
+ assertTrue(typeof result.pushed === "boolean", "pushed is boolean");
138
+ }
139
+
140
+ // ─── Test 2: Rich commit message format ────────────────────────────
141
+ console.log("\n=== rich commit message format ===");
142
+ {
143
+ const repo = freshRepo();
144
+ const wtPath = createAutoWorktree(repo, "M020");
145
+
146
+ addSliceToMilestone(repo, wtPath, "M020", "S01", "Core API", [
147
+ { file: "api.ts", content: "export const api = true;\n", message: "add api" },
148
+ ]);
149
+ addSliceToMilestone(repo, wtPath, "M020", "S02", "Error handling", [
150
+ { file: "errors.ts", content: "export class AppError {}\n", message: "add errors" },
151
+ ]);
152
+ addSliceToMilestone(repo, wtPath, "M020", "S03", "Logging infra", [
153
+ { file: "logger.ts", content: "export const log = () => {};\n", message: "add logger" },
154
+ ]);
155
+
156
+ const roadmap = makeRoadmap("M020", "Backend foundation", [
157
+ { id: "S01", title: "Core API" },
158
+ { id: "S02", title: "Error handling" },
159
+ { id: "S03", title: "Logging infra" },
160
+ ]);
161
+
162
+ const result = mergeMilestoneToMain(repo, "M020", roadmap);
163
+
164
+ // Subject line: conventional commit format
165
+ assertMatch(result.commitMessage, /^feat\(M020\):/, "subject has conventional commit prefix");
166
+ assertTrue(result.commitMessage.includes("Backend foundation"), "subject includes milestone title");
167
+
168
+ // Body: slice listing
169
+ assertTrue(result.commitMessage.includes("- S01: Core API"), "body lists S01");
170
+ assertTrue(result.commitMessage.includes("- S02: Error handling"), "body lists S02");
171
+ assertTrue(result.commitMessage.includes("- S03: Logging infra"), "body lists S03");
172
+
173
+ // Branch metadata
174
+ assertTrue(result.commitMessage.includes("Branch: milestone/M020"), "body has branch metadata");
175
+
176
+ // Verify the actual git commit message matches
177
+ const gitMsg = run("git log -1 --format=%B main", repo).trim();
178
+ assertMatch(gitMsg, /^feat\(M020\):/, "git commit message starts with feat(M020):");
179
+ assertTrue(gitMsg.includes("- S01: Core API"), "git commit body has S01");
180
+ }
181
+
182
+ // ─── Test 3: Nothing to commit — no changes ────────────────────────
183
+ console.log("\n=== nothing to commit — no changes ===");
184
+ {
185
+ const repo = freshRepo();
186
+ const wtPath = createAutoWorktree(repo, "M030");
187
+
188
+ // Don't add any slices/changes — milestone branch is identical to main
189
+ const roadmap = makeRoadmap("M030", "Empty milestone", []);
190
+
191
+ // Should complete without throwing
192
+ let threw = false;
193
+ try {
194
+ const result = mergeMilestoneToMain(repo, "M030", roadmap);
195
+ assertTrue(typeof result.pushed === "boolean", "returns result even with nothing to commit");
196
+ } catch {
197
+ threw = true;
198
+ }
199
+ assertTrue(!threw, "does not throw on nothing-to-commit");
200
+
201
+ // Main log unchanged (only init commit)
202
+ const mainLog = run("git log --oneline main", repo);
203
+ assertEq(mainLog.split("\n").length, 1, "main still has only init commit");
204
+ }
205
+
206
+ // ─── Test 4: Auto-push — verify push mechanics work ──────────────
207
+ // Note: loadEffectiveGSDPreferences uses a module-level const for project
208
+ // prefs path (process.cwd() at import time), so temp repo prefs aren't
209
+ // discoverable. We verify the push mechanics work by testing that
210
+ // mergeMilestoneToMain successfully completes with a remote configured,
211
+ // then manually push to verify the remote is set up correctly.
212
+ console.log("\n=== auto-push with bare remote ===");
213
+ {
214
+ const repo = freshRepo();
215
+
216
+ // Set up bare remote
217
+ const bareDir = realpathSync(mkdtempSync(join(tmpdir(), "wt-ms-bare-")));
218
+ tempDirs.push(bareDir);
219
+ run("git init --bare", bareDir);
220
+ run(`git remote add origin ${bareDir}`, repo);
221
+ run("git push -u origin main", repo);
222
+
223
+ const wtPath = createAutoWorktree(repo, "M040");
224
+
225
+ addSliceToMilestone(repo, wtPath, "M040", "S01", "Push test", [
226
+ { file: "pushed.ts", content: "export const pushed = true;\n", message: "add pushed file" },
227
+ ]);
228
+
229
+ const roadmap = makeRoadmap("M040", "Push verification", [
230
+ { id: "S01", title: "Push test" },
231
+ ]);
232
+
233
+ const result = mergeMilestoneToMain(repo, "M040", roadmap);
234
+
235
+ // Verify merge succeeded (commit on main)
236
+ const mainLog = run("git log --oneline main", repo);
237
+ assertTrue(mainLog.includes("feat(M040)"), "milestone commit on main");
238
+
239
+ // Manually push to verify remote works
240
+ run("git push origin main", repo);
241
+ const remoteLog = run("git log --oneline main", bareDir);
242
+ assertTrue(remoteLog.includes("feat(M040)"), "milestone commit reachable on remote after manual push");
243
+
244
+ // result.pushed will be false since prefs aren't loadable in temp repos
245
+ // (module-level const limitation) — that's expected
246
+ assertEq(result.pushed, false, "pushed is false without discoverable prefs");
247
+ }
248
+
249
+ } finally {
250
+ process.chdir(savedCwd);
251
+ for (const d of tempDirs) {
252
+ if (existsSync(d)) rmSync(d, { recursive: true, force: true });
253
+ }
254
+ }
255
+
256
+ report();
257
+ }
258
+
259
+ main();