gsd-pi 2.82.0-dev.3709f22a5 → 2.82.0-dev.57fd453e4

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 (183) hide show
  1. package/README.md +2 -2
  2. package/dist/resources/.managed-resources-content-hash +1 -1
  3. package/dist/resources/extensions/claude-code-cli/stream-adapter.js +1 -1
  4. package/dist/resources/extensions/gsd/auto/loop.js +14 -1
  5. package/dist/resources/extensions/gsd/auto/phases.js +53 -29
  6. package/dist/resources/extensions/gsd/auto/session.js +4 -0
  7. package/dist/resources/extensions/gsd/auto/workflow-kernel.js +3 -0
  8. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +1 -0
  9. package/dist/resources/extensions/gsd/auto-dispatch.js +12 -18
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +13 -6
  11. package/dist/resources/extensions/gsd/auto-recovery.js +40 -13
  12. package/dist/resources/extensions/gsd/auto-start.js +3 -3
  13. package/dist/resources/extensions/gsd/auto-verification.js +17 -4
  14. package/dist/resources/extensions/gsd/auto-worktree.js +65 -9
  15. package/dist/resources/extensions/gsd/auto.js +14 -8
  16. package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +6 -1
  17. package/dist/resources/extensions/gsd/bootstrap/db-tools.js +1 -1
  18. package/dist/resources/extensions/gsd/bootstrap/write-gate.js +1 -1
  19. package/dist/resources/extensions/gsd/commands-prefs-wizard.js +7 -2
  20. package/dist/resources/extensions/gsd/crash-recovery.js +16 -4
  21. package/dist/resources/extensions/gsd/db/milestone-leases.js +24 -0
  22. package/dist/resources/extensions/gsd/forensics.js +3 -3
  23. package/dist/resources/extensions/gsd/git-service.js +6 -2
  24. package/dist/resources/extensions/gsd/gsd-db.js +20 -6
  25. package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -3
  26. package/dist/resources/extensions/gsd/guided-flow.js +8 -5
  27. package/dist/resources/extensions/gsd/markdown-renderer.js +10 -8
  28. package/dist/resources/extensions/gsd/paths.js +4 -0
  29. package/dist/resources/extensions/gsd/queue-reorder-ui.js +30 -13
  30. package/dist/resources/extensions/gsd/state.js +1 -1
  31. package/dist/resources/extensions/gsd/status-guards.js +7 -0
  32. package/dist/resources/extensions/gsd/templates/plan.md +1 -0
  33. package/dist/resources/extensions/gsd/templates/task-plan.md +6 -0
  34. package/dist/resources/extensions/gsd/tools/plan-slice.js +3 -5
  35. package/dist/resources/extensions/gsd/workflow-mcp.js +17 -1
  36. package/dist/resources/extensions/gsd/worktree-manager.js +1 -1
  37. package/dist/resources/extensions/ttsr/ttsr-manager.js +3 -1
  38. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  39. package/dist/web/standalone/.next/BUILD_ID +1 -1
  40. package/dist/web/standalone/.next/app-path-routes-manifest.json +14 -14
  41. package/dist/web/standalone/.next/build-manifest.json +3 -3
  42. package/dist/web/standalone/.next/prerender-manifest.json +3 -3
  43. package/dist/web/standalone/.next/react-loadable-manifest.json +3 -3
  44. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  45. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  46. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  47. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  48. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  49. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  50. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  51. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  52. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  53. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  54. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  55. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  56. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  57. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  58. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  59. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  60. package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
  61. package/dist/web/standalone/.next/server/app/index.html +1 -1
  62. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  63. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  64. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  65. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  66. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  67. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  68. package/dist/web/standalone/.next/server/app-paths-manifest.json +14 -14
  69. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  70. package/dist/web/standalone/.next/server/middleware-react-loadable-manifest.js +1 -1
  71. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  72. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  73. package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
  74. package/dist/web/standalone/.next/static/chunks/8359.65b24fac92188a6b.js +10 -0
  75. package/dist/web/standalone/.next/static/chunks/9441.ff70bb53f6835771.js +1 -0
  76. package/dist/web/standalone/.next/static/chunks/{webpack-9a4db269f9ed63ad.js → webpack-855d616060cb6e59.js} +1 -1
  77. package/package.json +1 -1
  78. package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
  79. package/packages/pi-ai/dist/providers/google-gemini-cli.js +5 -0
  80. package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
  81. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts +2 -0
  82. package/packages/pi-ai/dist/providers/google-gemini-cli.test.d.ts.map +1 -0
  83. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js +41 -0
  84. package/packages/pi-ai/dist/providers/google-gemini-cli.test.js.map +1 -0
  85. package/packages/pi-ai/src/providers/google-gemini-cli.test.ts +49 -0
  86. package/packages/pi-ai/src/providers/google-gemini-cli.ts +7 -0
  87. package/packages/pi-ai/tsconfig.tsbuildinfo +1 -1
  88. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js +44 -3
  89. package/packages/pi-coding-agent/dist/core/chat-controller-ordering.test.js.map +1 -1
  90. package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
  91. package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
  92. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
  93. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +71 -97
  94. package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
  95. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js +12 -0
  96. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode-ordering.test.js.map +1 -1
  97. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  98. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +19 -8
  99. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  100. package/packages/pi-coding-agent/src/core/chat-controller-ordering.test.ts +53 -3
  101. package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
  102. package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +75 -102
  103. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode-ordering.test.ts +14 -0
  104. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +23 -8
  105. package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
  106. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts +2 -0
  107. package/packages/pi-tui/dist/__tests__/terminal.test.d.ts.map +1 -0
  108. package/packages/pi-tui/dist/__tests__/terminal.test.js +103 -0
  109. package/packages/pi-tui/dist/__tests__/terminal.test.js.map +1 -0
  110. package/packages/pi-tui/dist/terminal.d.ts +2 -0
  111. package/packages/pi-tui/dist/terminal.d.ts.map +1 -1
  112. package/packages/pi-tui/dist/terminal.js +12 -0
  113. package/packages/pi-tui/dist/terminal.js.map +1 -1
  114. package/packages/pi-tui/src/__tests__/terminal.test.ts +121 -0
  115. package/packages/pi-tui/src/terminal.ts +11 -0
  116. package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
  117. package/src/resources/extensions/claude-code-cli/stream-adapter.ts +1 -1
  118. package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +9 -0
  119. package/src/resources/extensions/gsd/auto/loop.ts +14 -1
  120. package/src/resources/extensions/gsd/auto/phases.ts +60 -36
  121. package/src/resources/extensions/gsd/auto/session.ts +4 -0
  122. package/src/resources/extensions/gsd/auto/workflow-kernel.ts +5 -1
  123. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +1 -0
  124. package/src/resources/extensions/gsd/auto-dispatch.ts +12 -18
  125. package/src/resources/extensions/gsd/auto-post-unit.ts +14 -6
  126. package/src/resources/extensions/gsd/auto-recovery.ts +45 -11
  127. package/src/resources/extensions/gsd/auto-start.ts +2 -3
  128. package/src/resources/extensions/gsd/auto-verification.ts +22 -2
  129. package/src/resources/extensions/gsd/auto-worktree.ts +74 -9
  130. package/src/resources/extensions/gsd/auto.ts +13 -8
  131. package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +9 -1
  132. package/src/resources/extensions/gsd/bootstrap/db-tools.ts +1 -1
  133. package/src/resources/extensions/gsd/bootstrap/write-gate.ts +1 -1
  134. package/src/resources/extensions/gsd/commands-prefs-wizard.ts +8 -3
  135. package/src/resources/extensions/gsd/crash-recovery.ts +16 -2
  136. package/src/resources/extensions/gsd/db/milestone-leases.ts +26 -0
  137. package/src/resources/extensions/gsd/forensics.ts +3 -3
  138. package/src/resources/extensions/gsd/git-service.ts +6 -3
  139. package/src/resources/extensions/gsd/gsd-db.ts +18 -6
  140. package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -3
  141. package/src/resources/extensions/gsd/guided-flow.ts +8 -5
  142. package/src/resources/extensions/gsd/markdown-renderer.ts +10 -8
  143. package/src/resources/extensions/gsd/paths.ts +5 -0
  144. package/src/resources/extensions/gsd/queue-reorder-ui.ts +31 -13
  145. package/src/resources/extensions/gsd/state.ts +1 -1
  146. package/src/resources/extensions/gsd/status-guards.ts +8 -0
  147. package/src/resources/extensions/gsd/templates/plan.md +1 -0
  148. package/src/resources/extensions/gsd/templates/task-plan.md +6 -0
  149. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +2 -0
  150. package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +139 -1
  151. package/src/resources/extensions/gsd/tests/auto-post-unit-step-message.test.ts +6 -5
  152. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +76 -5
  153. package/src/resources/extensions/gsd/tests/checkout-branch-stash-guard.test.ts +87 -0
  154. package/src/resources/extensions/gsd/tests/crash-recovery-via-db.test.ts +43 -0
  155. package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +2 -0
  156. package/src/resources/extensions/gsd/tests/dispatch-complete-milestone-guard.test.ts +27 -0
  157. package/src/resources/extensions/gsd/tests/gsd-db.test.ts +11 -0
  158. package/src/resources/extensions/gsd/tests/gsdroot-worktree-detection.test.ts +5 -2
  159. package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +9 -0
  160. package/src/resources/extensions/gsd/tests/journal-integration.test.ts +46 -0
  161. package/src/resources/extensions/gsd/tests/merge-db-cycle.test.ts +179 -0
  162. package/src/resources/extensions/gsd/tests/pipeline-variant-dispatch.test.ts +2 -1
  163. package/src/resources/extensions/gsd/tests/plan-slice.test.ts +26 -1
  164. package/src/resources/extensions/gsd/tests/post-unit-state-rebuild.test.ts +84 -0
  165. package/src/resources/extensions/gsd/tests/prefs-wizard-coverage.test.ts +59 -0
  166. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +18 -1
  167. package/src/resources/extensions/gsd/tests/quality-gates.test.ts +6 -0
  168. package/src/resources/extensions/gsd/tests/queue-reorder-ui.test.ts +54 -0
  169. package/src/resources/extensions/gsd/tests/remediation-completion-guard.test.ts +43 -0
  170. package/src/resources/extensions/gsd/tests/run-uat-replay-cap.test.ts +2 -3
  171. package/src/resources/extensions/gsd/tests/status-guards.test.ts +13 -1
  172. package/src/resources/extensions/gsd/tests/unit-context-manifest.test.ts +17 -0
  173. package/src/resources/extensions/gsd/tests/validate-milestone-stuck-guard.test.ts +29 -2
  174. package/src/resources/extensions/gsd/tests/workflow-kernel.test.ts +7 -0
  175. package/src/resources/extensions/gsd/tests/workflow-mcp.test.ts +18 -0
  176. package/src/resources/extensions/gsd/tools/plan-slice.ts +3 -4
  177. package/src/resources/extensions/gsd/workflow-mcp.ts +18 -1
  178. package/src/resources/extensions/gsd/worktree-manager.ts +1 -1
  179. package/src/resources/extensions/ttsr/ttsr-manager.ts +5 -1
  180. package/dist/web/standalone/.next/static/chunks/8359.7eb3bb8f8ecf4c01.js +0 -10
  181. package/dist/web/standalone/.next/static/chunks/9441.1081da1125d1764f.js +0 -1
  182. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → ky6ieNHfZXB_oHPklwTJb}/_buildManifest.js +0 -0
  183. /package/dist/web/standalone/.next/static/{kkGf3_VaPFkiDNV_D7Dtl → ky6ieNHfZXB_oHPklwTJb}/_ssgManifest.js +0 -0
@@ -0,0 +1,87 @@
1
+ import { describe, test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { execFileSync } from "node:child_process";
4
+ import { mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
5
+ import { join } from "node:path";
6
+ import { tmpdir } from "node:os";
7
+
8
+ import { checkoutBranchWithStashGuard } from "../auto-worktree.ts";
9
+
10
+ function git(args: string[], cwd: string): string {
11
+ return execFileSync("git", args, {
12
+ cwd,
13
+ stdio: ["ignore", "pipe", "pipe"],
14
+ encoding: "utf-8",
15
+ });
16
+ }
17
+
18
+ function createRepo(t: { after: (fn: () => void) => void }): string {
19
+ const dir = realpathSync(mkdtempSync(join(tmpdir(), "checkout-stash-guard-")));
20
+ t.after(() => rmSync(dir, { recursive: true, force: true }));
21
+ git(["init"], dir);
22
+ git(["config", "user.email", "test@example.com"], dir);
23
+ git(["config", "user.name", "Test User"], dir);
24
+ writeFileSync(join(dir, "note.txt"), "base\n");
25
+ git(["add", "note.txt"], dir);
26
+ git(["commit", "-m", "init"], dir);
27
+ git(["branch", "-M", "main"], dir);
28
+ return dir;
29
+ }
30
+
31
+ describe("checkoutBranchWithStashGuard", () => {
32
+ test("restores dirty working tree after successful checkout", (t) => {
33
+ const repo = createRepo(t);
34
+ git(["checkout", "-b", "milestone/M001"], repo);
35
+ git(["checkout", "main"], repo);
36
+
37
+ writeFileSync(join(repo, "note.txt"), "dirty\n");
38
+
39
+ checkoutBranchWithStashGuard(repo, "milestone/M001", "test-success");
40
+
41
+ const branch = git(["branch", "--show-current"], repo).trim();
42
+ assert.equal(branch, "milestone/M001");
43
+ const content = git(["show", "HEAD:note.txt"], repo).trim();
44
+ assert.equal(content, "base");
45
+ const wtContent = readFileSync(join(repo, "note.txt"), "utf8");
46
+ assert.equal(wtContent, "dirty\n");
47
+ const status = git(["status", "--porcelain"], repo);
48
+ assert.match(status, /note\.txt/);
49
+ });
50
+
51
+ test("restores dirty working tree when checkout throws", (t) => {
52
+ const repo = createRepo(t);
53
+ writeFileSync(join(repo, "note.txt"), "dirty\n");
54
+
55
+ assert.throws(
56
+ () => checkoutBranchWithStashGuard(repo, "milestone/DOES-NOT-EXIST", "test-failure"),
57
+ );
58
+
59
+ const status = git(["status", "--porcelain"], repo);
60
+ assert.match(status, /note\.txt/);
61
+ const stashList = git(["stash", "list"], repo).trim();
62
+ assert.equal(stashList, "");
63
+ });
64
+
65
+ test("surfaces distinct error when checkout succeeds but stash pop conflicts", (t) => {
66
+ const repo = createRepo(t);
67
+ // Branch B has a divergent version of note.txt so popping a stash made
68
+ // against main will conflict after the checkout to B.
69
+ git(["checkout", "-b", "milestone/B"], repo);
70
+ writeFileSync(join(repo, "note.txt"), "B-version\n");
71
+ git(["add", "note.txt"], repo);
72
+ git(["commit", "-m", "B"], repo);
73
+ git(["checkout", "main"], repo);
74
+
75
+ writeFileSync(join(repo, "note.txt"), "local\n");
76
+
77
+ assert.throws(
78
+ () => checkoutBranchWithStashGuard(repo, "milestone/B", "test-pop-failure"),
79
+ /checkout to 'milestone\/B' succeeded but stash restore failed/,
80
+ );
81
+
82
+ const branch = git(["branch", "--show-current"], repo).trim();
83
+ assert.equal(branch, "milestone/B");
84
+ const stashList = git(["stash", "list"], repo).trim();
85
+ assert.match(stashList, /gsd: checkout stash/);
86
+ });
87
+ });
@@ -225,6 +225,25 @@ test("clearLock removes the session_file row for the active worker", (t) => {
225
225
  "session_file row deleted by clearLock");
226
226
  });
227
227
 
228
+ test("clearLock marks stale worker crashed when no current-process worker matches", (t) => {
229
+ const base = makeBase();
230
+ t.after(() => cleanup(base));
231
+ openDatabase(join(base, ".gsd", "gsd.db"));
232
+ const projectRoot = normalizeRealPath(base);
233
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
234
+
235
+ setRuntimeKv("worker", workerId, "session_file", "/tmp/stale-session.jsonl");
236
+ setWorkerPid(workerId, 99999);
237
+ expireWorker(workerId);
238
+ assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
239
+
240
+ clearLock(base);
241
+
242
+ assert.equal(getAutoWorker(workerId)?.status, "crashed");
243
+ assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
244
+ assert.equal(readCrashLock(base), null);
245
+ });
246
+
228
247
  test("clearStaleWorkerLock crashes stale worker and cancels latest active dispatch", (t) => {
229
248
  const base = makeBase();
230
249
  t.after(() => cleanup(base));
@@ -264,3 +283,27 @@ test("clearStaleWorkerLock crashes stale worker and cancels latest active dispat
264
283
  assert.equal(getRuntimeKv("worker", workerId, "session_file"), null);
265
284
  assert.equal(readCrashLock(base), null);
266
285
  });
286
+
287
+ test("clearLock marks stale worker crashed and releases held milestone lease", (t) => {
288
+ const base = makeBase();
289
+ t.after(() => cleanup(base));
290
+ openDatabase(join(base, ".gsd", "gsd.db"));
291
+ insertMilestone({ id: "M001", title: "T", status: "active" });
292
+ const projectRoot = normalizeRealPath(base);
293
+ const workerId = registerAutoWorker({ projectRootRealpath: projectRoot });
294
+ const lease = claimMilestoneLease(workerId, "M001");
295
+ assert.equal(lease.ok, true);
296
+ if (!lease.ok) return;
297
+
298
+ setWorkerPid(workerId, 99999);
299
+ expireWorker(workerId);
300
+ assert.ok(readCrashLock(base), "stale worker is detected before clearLock");
301
+
302
+ clearLock(base);
303
+
304
+ assert.equal(getAutoWorker(workerId)?.status, "crashed");
305
+ const leaseRow = _getAdapter()!.prepare(
306
+ `SELECT status FROM milestone_leases WHERE milestone_id = :m`,
307
+ ).get({ ":m": "M001" }) as { status: string } | undefined;
308
+ assert.equal(leaseRow?.status, "released");
309
+ });
@@ -98,6 +98,8 @@ function makeMockPi() {
98
98
  sendMessage: (...args: unknown[]) => {
99
99
  calls.push(args);
100
100
  },
101
+ getThinkingLevel: () => "off",
102
+ setThinkingLevel: () => {},
101
103
  calls,
102
104
  } as any;
103
105
  }
@@ -10,6 +10,7 @@ import assert from "node:assert/strict";
10
10
  import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
11
11
  import { join } from "node:path";
12
12
  import { tmpdir } from "node:os";
13
+ import { execFileSync } from "node:child_process";
13
14
 
14
15
  import { DISPATCH_RULES, type DispatchContext } from "../auto-dispatch.ts";
15
16
  import { closeDatabase, insertMilestone, openDatabase } from "../gsd-db.ts";
@@ -23,6 +24,14 @@ function makeBase(): string {
23
24
  return base;
24
25
  }
25
26
 
27
+ function initGitRepo(base: string): void {
28
+ execFileSync("git", ["init"], { cwd: base, stdio: "ignore" });
29
+ execFileSync("git", ["config", "user.email", "test@test.com"], { cwd: base, stdio: "ignore" });
30
+ execFileSync("git", ["config", "user.name", "Test"], { cwd: base, stdio: "ignore" });
31
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
32
+ execFileSync("git", ["commit", "-m", "initial"], { cwd: base, stdio: "ignore" });
33
+ }
34
+
26
35
  function buildDispatchCtx(basePath: string): DispatchContext {
27
36
  return {
28
37
  basePath,
@@ -76,6 +85,24 @@ describe("completing-milestone dispatch guard (#4324)", () => {
76
85
  assert.equal(result?.unitType, "complete-milestone");
77
86
  assert.equal(result?.unitId, "M001");
78
87
  });
88
+
89
+ test("dispatches complete-milestone when only .gsd/ files exist in git history (#5097)", async () => {
90
+ base = makeBase();
91
+ rmSync(join(base, "implementation.txt"), { force: true });
92
+ initGitRepo(base);
93
+ writeFileSync(join(base, ".gsd", "milestones", "M001", "M001-SUMMARY.md"), "# Milestone Summary\n");
94
+ execFileSync("git", ["add", "."], { cwd: base, stdio: "ignore" });
95
+ execFileSync("git", ["commit", "-m", "chore: planning artifacts only"], { cwd: base, stdio: "ignore" });
96
+
97
+ openDatabase(join(base, ".gsd", "gsd.db"));
98
+ insertMilestone({ id: "M001", title: "Milestone One", status: "active" });
99
+
100
+ const result = await rule.match(buildDispatchCtx(base));
101
+
102
+ assert.equal(result?.action, "dispatch");
103
+ assert.equal(result?.unitType, "complete-milestone");
104
+ assert.equal(result?.unitId, "M001");
105
+ });
79
106
  });
80
107
 
81
108
  describe("complete phase dispatch guard (#5683)", () => {
@@ -37,6 +37,7 @@ import {
37
37
  checkpointDatabase,
38
38
  refreshOpenDatabaseFromDisk,
39
39
  tryCreateMemoriesFts,
40
+ _isLikelyWslDrvFsPathForTest,
40
41
  } from '../gsd-db.ts';
41
42
  import { _resetLogs, peekLogs, setStderrLoggingEnabled } from '../workflow-logger.ts';
42
43
 
@@ -345,6 +346,16 @@ describe('gsd-db', () => {
345
346
  });
346
347
  });
347
348
 
349
+ test('gsd-db: detects WSL DrvFs mount paths for conservative pragmas', () => {
350
+ withPlatform('linux', () => {
351
+ assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), true);
352
+ assert.equal(_isLikelyWslDrvFsPathForTest('/tmp/gsd.db'), false);
353
+ });
354
+ withPlatform('darwin', () => {
355
+ assert.equal(_isLikelyWslDrvFsPathForTest('/mnt/d/code/project/.gsd/gsd.db'), false);
356
+ });
357
+ });
358
+
348
359
  test('gsd-db: transaction rollback on error', () => {
349
360
  openDatabase(':memory:');
350
361
 
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * gsdRoot() must return the canonical project .gsd directory when basePath
5
5
  * is inside a .gsd/worktrees/<name>/ structure. Worktree-local .gsd folders
6
- * are legacy projection roots only; runtime state is DB-authoritative at the
6
+ * are projection roots; runtime/control state stays DB-authoritative at the
7
7
  * project .gsd.
8
8
  *
9
9
  * The bug: when a git worktree lives at /project/.gsd/worktrees/M008/,
@@ -21,7 +21,7 @@ import { mkdtempSync, realpathSync } from "node:fs";
21
21
  import { tmpdir } from "node:os";
22
22
  import { spawnSync } from "node:child_process";
23
23
 
24
- import { gsdRoot, resolveGsdPathContract, _clearGsdRootCache } from "../paths.ts";
24
+ import { gsdProjectionRoot, gsdRoot, resolveGsdPathContract, _clearGsdRootCache } from "../paths.ts";
25
25
 
26
26
  describe("gsdRoot() worktree detection (#2594)", () => {
27
27
  let projectRoot: string;
@@ -76,6 +76,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
76
76
  `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
77
77
  );
78
78
  assert.equal(resolveGsdPathContract(worktreeBase).worktreeGsd, worktreeGsd);
79
+ assert.equal(gsdProjectionRoot(worktreeBase), worktreeGsd);
79
80
  });
80
81
 
81
82
  test("returns project .gsd when worktree .gsd does not exist yet", () => {
@@ -89,6 +90,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
89
90
  projectGsd,
90
91
  `Expected canonical project .gsd (${projectGsd}), got ${result}.`,
91
92
  );
93
+ assert.equal(gsdProjectionRoot(worktreeBase), join(worktreeBase, ".gsd"));
92
94
  });
93
95
 
94
96
  test("returns project .gsd when basePath is a real git worktree inside .gsd/worktrees/", () => {
@@ -116,6 +118,7 @@ describe("gsdRoot() worktree detection (#2594)", () => {
116
118
  projectGsd,
117
119
  `Expected canonical project .gsd (${projectGsd}), got ${gsdResult}`,
118
120
  );
121
+ assert.equal(gsdProjectionRoot(worktreeBase), join(worktreeBase, ".gsd"));
119
122
 
120
123
  // Cleanup worktree
121
124
  spawnSync("git", ["worktree", "remove", "--force", worktreeBase], {
@@ -1168,6 +1168,15 @@ describe('git-service', async () => {
1168
1168
  rmSync(repo, { recursive: true, force: true });
1169
1169
  });
1170
1170
 
1171
+ test('Integration branch: rejects milestone branches', () => {
1172
+ const repo = initBranchTestRepo();
1173
+
1174
+ writeIntegrationBranch(repo, "M001", "milestone/M001");
1175
+ assert.deepStrictEqual(readIntegrationBranch(repo, "M001"), null, "milestone branches are not recorded as integration branch");
1176
+
1177
+ rmSync(repo, { recursive: true, force: true });
1178
+ });
1179
+
1171
1180
  // ─── writeIntegrationBranch: still records legitimate branches ────────
1172
1181
 
1173
1182
  test('Integration branch: records non-ephemeral gsd branches', () => {
@@ -22,6 +22,7 @@ import type { IterationContext, LoopState, PreDispatchData, IterationData } from
22
22
  import type { SessionLockStatus } from "../session-lock.js";
23
23
  import { runDispatch, runUnitPhase, runPreDispatch, runFinalize } from "../auto/phases.js";
24
24
  import { readUnitRuntimeRecord } from "../unit-runtime.js";
25
+ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
25
26
  import {
26
27
  closeDatabase,
27
28
  insertMilestone,
@@ -160,6 +161,8 @@ function makeIC(
160
161
  pi: {
161
162
  sendMessage: () => {},
162
163
  setModel: async () => true,
164
+ getThinkingLevel: () => "off",
165
+ setThinkingLevel: () => {},
163
166
  } as any,
164
167
  s: makeSession(),
165
168
  deps,
@@ -868,6 +871,49 @@ test("runUnitPhase increments unitDispatchCount for repeated artifact-missing re
868
871
  assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01"), 2);
869
872
  });
870
873
 
874
+ test("runUnitPhase pre-dispatch model validation failures do not emit unit-start or dispatch runtime state", async (t) => {
875
+ const capture = createEventCapture();
876
+ const base = mkdtempSync(join(tmpdir(), `gsd-pre-dispatch-block-${randomUUID()}`));
877
+ t.after(() => rmSync(base, { recursive: true, force: true }));
878
+
879
+ const deps = makeMockDeps(capture, {
880
+ selectAndApplyModel: async () => {
881
+ throw new ModelPolicyDispatchBlockedError("execute-task", "M001/S01/T01", []);
882
+ },
883
+ });
884
+ const ic = makeIC(deps, {
885
+ s: {
886
+ ...makeSession(),
887
+ basePath: base,
888
+ } as any,
889
+ });
890
+ const iterData: IterationData = {
891
+ unitType: "execute-task",
892
+ unitId: "M001/S01/T01",
893
+ prompt: "do stuff",
894
+ finalPrompt: "do stuff",
895
+ pauseAfterUatDispatch: false,
896
+ state: { phase: "executing", activeMilestone: { id: "M001" }, activeSlice: { id: "S01" }, registry: [], blockers: [] } as any,
897
+ mid: "M001",
898
+ midTitle: "Test",
899
+ isRetry: false,
900
+ previousTier: undefined,
901
+ };
902
+ const loopState: LoopState = { recentUnits: [{ key: "execute-task/M001/S01/T01" }], stuckRecoveryAttempts: 0, consecutiveFinalizeTimeouts: 0 };
903
+
904
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
905
+ await assert.rejects(() => runUnitPhase(ic, iterData, loopState), ModelPolicyDispatchBlockedError);
906
+
907
+ const startEvents = capture.events.filter(e => e.eventType === "unit-start");
908
+ assert.equal(startEvents.length, 0, "pre-dispatch validation failures must not emit unit-start");
909
+ assert.equal(ic.s.unitDispatchCount.get("execute-task/M001/S01/T01") ?? 0, 0, "dispatch count must not increment on pre-dispatch validation failure");
910
+ assert.equal(
911
+ readUnitRuntimeRecord(base, "execute-task", "M001/S01/T01"),
912
+ null,
913
+ "pre-dispatch validation failures must not persist a dispatched runtime record",
914
+ );
915
+ });
916
+
871
917
  test("all events from a mock iteration have monotonically increasing seq and same flowId", async () => {
872
918
  const capture = createEventCapture();
873
919
  const { resolveAgentEnd, _resetPendingResolve } = await import("../auto/resolve.js");
@@ -0,0 +1,179 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import {
4
+ chmodSync,
5
+ existsSync,
6
+ mkdtempSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ realpathSync,
10
+ rmSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { tmpdir } from "node:os";
14
+ import { delimiter, join } from "node:path";
15
+ import { execFileSync } from "node:child_process";
16
+
17
+ import { mergeMilestoneToMain } from "../auto-worktree.ts";
18
+ import { closeDatabase, openDatabase } from "../gsd-db.ts";
19
+ import { GIT_NO_PROMPT_ENV } from "../git-constants.js";
20
+ import { _clearGsdRootCache } from "../paths.ts";
21
+ import { _resetServiceCache } from "../worktree.ts";
22
+ import { worktreePath } from "../worktree-manager.ts";
23
+
24
+ function git(args: string[], cwd: string): string {
25
+ return execFileSync("git", args, {
26
+ cwd,
27
+ stdio: ["ignore", "pipe", "pipe"],
28
+ encoding: "utf-8",
29
+ }).trim();
30
+ }
31
+
32
+ function withPlatform<T>(platform: NodeJS.Platform, fn: () => T): T {
33
+ const descriptor = Object.getOwnPropertyDescriptor(process, "platform");
34
+ Object.defineProperty(process, "platform", { value: platform });
35
+ try {
36
+ return fn();
37
+ } finally {
38
+ if (descriptor) {
39
+ Object.defineProperty(process, "platform", descriptor);
40
+ }
41
+ }
42
+ }
43
+
44
+ function realGitPath(): string {
45
+ const gitExecPath = execFileSync("git", ["--exec-path"], {
46
+ encoding: "utf-8",
47
+ }).trim();
48
+ return join(gitExecPath, process.platform === "win32" ? "git.exe" : "git");
49
+ }
50
+
51
+ function installGitShim(bin: string, probePath: string): void {
52
+ const shim = join(bin, "git-proxy.cjs");
53
+ writeFileSync(
54
+ shim,
55
+ `
56
+ const { appendFileSync, existsSync } = require("node:fs");
57
+ const { join } = require("node:path");
58
+ const { spawnSync } = require("node:child_process");
59
+
60
+ const realGit = ${JSON.stringify(realGitPath())};
61
+ const probePath = ${JSON.stringify(probePath)};
62
+ const args = process.argv.slice(2);
63
+
64
+ if (args[0] === "merge" && args[1] === "--squash") {
65
+ const sidecars = [
66
+ join(process.cwd(), ".gsd", "gsd.db-wal"),
67
+ join(process.cwd(), ".gsd", "gsd.db-shm"),
68
+ ];
69
+ const locked = sidecars.find((path) => existsSync(path));
70
+ if (locked) {
71
+ appendFileSync(probePath, "blocked:" + locked + "\\n");
72
+ console.error("error: local changes would be overwritten by merge");
73
+ console.error("\\t" + locked);
74
+ process.exit(1);
75
+ }
76
+ appendFileSync(probePath, "clean\\n");
77
+ }
78
+
79
+ const result = spawnSync(realGit, args, { stdio: "inherit", env: process.env });
80
+ process.exit(result.status ?? 1);
81
+ `,
82
+ "utf-8",
83
+ );
84
+
85
+ if (process.platform === "win32") {
86
+ writeFileSync(join(bin, "git.cmd"), `@echo off\r\nnode "%~dp0git-proxy.cjs" %*\r\n`, "utf-8");
87
+ } else {
88
+ const executable = join(bin, "git");
89
+ writeFileSync(executable, `#!/bin/sh\nexec node "${shim}" "$@"\n`, "utf-8");
90
+ chmodSync(executable, 0o755);
91
+ }
92
+ }
93
+
94
+ function createRepo(root: string): { repo: string; worktree: string } {
95
+ const repo = join(root, "repo");
96
+ mkdirSync(repo, { recursive: true });
97
+ git(["init"], repo);
98
+ git(["config", "user.email", "test@test.com"], repo);
99
+ git(["config", "user.name", "Test"], repo);
100
+ writeFileSync(join(repo, ".gitignore"), ".gsd/\n", "utf-8");
101
+ writeFileSync(join(repo, "README.md"), "# test\n", "utf-8");
102
+ git(["add", "."], repo);
103
+ git(["commit", "-m", "init"], repo);
104
+ git(["branch", "-M", "main"], repo);
105
+
106
+ git(["checkout", "-b", "milestone/M001"], repo);
107
+ writeFileSync(join(repo, "feature.txt"), "milestone change\n", "utf-8");
108
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
109
+ writeFileSync(join(repo, ".gsd", "gsd.db-shm"), "milestone placeholder\n", "utf-8");
110
+ git(["add", "feature.txt"], repo);
111
+ git(["add", "-f", ".gsd/gsd.db-shm"], repo);
112
+ git(["commit", "-m", "feat: milestone change"], repo);
113
+ git(["checkout", "main"], repo);
114
+
115
+ const wt = worktreePath(repo, "M001");
116
+ mkdirSync(join(repo, ".gsd", "worktrees"), { recursive: true });
117
+ git(["worktree", "add", wt, "milestone/M001"], repo);
118
+ return { repo, worktree: wt };
119
+ }
120
+
121
+ test("mergeMilestoneToMain keeps the Windows DB cycle closed through squash merge", () => {
122
+ const savedCwd = process.cwd();
123
+ const originalPath = process.env.PATH ?? "";
124
+ const gitEnv = GIT_NO_PROMPT_ENV as NodeJS.ProcessEnv;
125
+ const originalGitEnvPath = gitEnv.PATH;
126
+ const originalHome = process.env.HOME;
127
+ const originalGsdHome = process.env.GSD_HOME;
128
+
129
+ const root = realpathSync(mkdtempSync(join(tmpdir(), "gsd-db-cycle-")));
130
+ const fakeHome = join(root, "home");
131
+ const bin = join(root, "bin");
132
+ const probePath = join(root, "merge-probe.txt");
133
+ mkdirSync(fakeHome, { recursive: true });
134
+ mkdirSync(bin, { recursive: true });
135
+ installGitShim(bin, probePath);
136
+
137
+ try {
138
+ process.env.HOME = fakeHome;
139
+ process.env.GSD_HOME = join(fakeHome, ".gsd");
140
+ _clearGsdRootCache();
141
+ _resetServiceCache();
142
+
143
+ const { repo, worktree } = createRepo(root);
144
+ mkdirSync(join(repo, ".gsd"), { recursive: true });
145
+
146
+ withPlatform("win32", () => {
147
+ assert.equal(openDatabase(join(repo, ".gsd", "gsd.db")), true);
148
+ assert.equal(existsSync(join(repo, ".gsd", "gsd.db-shm")), true);
149
+
150
+ process.env.PATH = `${bin}${delimiter}${originalPath}`;
151
+ gitEnv.PATH = process.env.PATH;
152
+ process.chdir(worktree);
153
+
154
+ const result = mergeMilestoneToMain(repo, "M001", "# M001: Windows DB cycle\n");
155
+ assert.equal(result.codeFilesChanged, true);
156
+ });
157
+
158
+ assert.equal(git(["show", "HEAD:feature.txt"], repo), "milestone change");
159
+ assert.equal(readFileSync(probePath, "utf-8"), "clean\n");
160
+ } finally {
161
+ closeDatabase();
162
+ process.chdir(savedCwd);
163
+ process.env.PATH = originalPath;
164
+ gitEnv.PATH = originalGitEnvPath;
165
+ if (originalHome === undefined) {
166
+ delete process.env.HOME;
167
+ } else {
168
+ process.env.HOME = originalHome;
169
+ }
170
+ if (originalGsdHome === undefined) {
171
+ delete process.env.GSD_HOME;
172
+ } else {
173
+ process.env.GSD_HOME = originalGsdHome;
174
+ }
175
+ _clearGsdRootCache();
176
+ _resetServiceCache();
177
+ if (existsSync(root)) rmSync(root, { recursive: true, force: true });
178
+ }
179
+ });
@@ -243,7 +243,8 @@ test("#4781 phase 2: validate-milestone rule writes pass-through VALIDATION for
243
243
  const content = readFileSync(validationPath, "utf-8");
244
244
  assert.match(content, /verdict: pass/);
245
245
  assert.match(content, /skip_validation: true/);
246
- assert.match(content, /trivial-scope pipeline variant \(#4781\)/);
246
+ assert.match(content, /trivial-scope pipeline variant/);
247
+ assert.doesNotMatch(content, /#[0-9]{3,}/, "validation output must not include tracker-style refs");
247
248
  });
248
249
 
249
250
  test("#4781 phase 2: validate-milestone skip path does not persist gates without a real slice", async (t) => {
@@ -2,7 +2,7 @@
2
2
 
3
3
  import test from 'node:test';
4
4
  import assert from 'node:assert/strict';
5
- import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync } from 'node:fs';
5
+ import { mkdtempSync, mkdirSync, rmSync, readFileSync, existsSync, writeFileSync, realpathSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { tmpdir } from 'node:os';
8
8
 
@@ -105,6 +105,31 @@ test('handlePlanSlice writes slice/task planning state and renders plan artifact
105
105
  }
106
106
  });
107
107
 
108
+ test('handlePlanSlice renders plan artifacts under worktree-local .gsd while using project DB', async () => {
109
+ const base = makeTmpBase();
110
+ const worktree = join(base, '.gsd', 'worktrees', 'M001');
111
+ mkdirSync(join(worktree, '.gsd'), { recursive: true });
112
+ mkdirSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools'), { recursive: true });
113
+ writeFileSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-milestone.ts'), '// fixture\n', 'utf-8');
114
+ writeFileSync(join(worktree, 'src', 'resources', 'extensions', 'gsd', 'tools', 'plan-task.ts'), '// fixture\n', 'utf-8');
115
+ openDatabase(join(base, '.gsd', 'gsd.db'));
116
+
117
+ try {
118
+ seedParentSlice();
119
+
120
+ const result = await handlePlanSlice(validParams(), worktree);
121
+ assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
122
+
123
+ const worktreePlan = join(worktree, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md');
124
+ const projectPlan = join(base, '.gsd', 'milestones', 'M001', 'slices', 'S02', 'S02-PLAN.md');
125
+ assert.ok(existsSync(worktreePlan), 'slice plan should be rendered to worktree-local .gsd');
126
+ assert.ok(!existsSync(projectPlan), 'slice plan should not be rendered to project .gsd');
127
+ assert.equal(result.planPath, realpathSync(worktreePlan));
128
+ } finally {
129
+ cleanup(base);
130
+ }
131
+ });
132
+
108
133
  test('handlePlanSlice advances DB-derived state out of planning immediately', async () => {
109
134
  const base = makeTmpBase();
110
135
  openDatabase(join(base, '.gsd', 'gsd.db'));