gsd-pi 2.33.1-dev.ee47f1b → 2.34.0-dev.bbb5216

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 (135) hide show
  1. package/dist/bundled-resource-path.d.ts +8 -0
  2. package/dist/bundled-resource-path.js +14 -0
  3. package/dist/headless-query.js +6 -6
  4. package/dist/resources/extensions/gsd/auto/session.js +27 -32
  5. package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
  6. package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
  7. package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
  8. package/dist/resources/extensions/gsd/auto-loop.js +956 -0
  9. package/dist/resources/extensions/gsd/auto-observability.js +4 -2
  10. package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
  11. package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
  12. package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
  13. package/dist/resources/extensions/gsd/auto-start.js +330 -309
  14. package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
  15. package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
  16. package/dist/resources/extensions/gsd/auto-timers.js +3 -4
  17. package/dist/resources/extensions/gsd/auto-verification.js +35 -73
  18. package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
  19. package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
  20. package/dist/resources/extensions/gsd/auto.js +283 -1013
  21. package/dist/resources/extensions/gsd/captures.js +10 -4
  22. package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
  23. package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  24. package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
  25. package/dist/resources/extensions/gsd/git-service.js +1 -1
  26. package/dist/resources/extensions/gsd/gsd-db.js +296 -151
  27. package/dist/resources/extensions/gsd/index.js +92 -228
  28. package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
  29. package/dist/resources/extensions/gsd/progress-score.js +61 -156
  30. package/dist/resources/extensions/gsd/quick.js +98 -122
  31. package/dist/resources/extensions/gsd/session-lock.js +13 -0
  32. package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
  33. package/dist/resources/extensions/gsd/undo.js +43 -48
  34. package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
  35. package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
  36. package/dist/resources/extensions/gsd/verification-gate.js +6 -35
  37. package/dist/resources/extensions/gsd/worktree-command.js +30 -24
  38. package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
  39. package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
  40. package/dist/resources/extensions/gsd/worktree.js +7 -44
  41. package/dist/tool-bootstrap.js +59 -11
  42. package/dist/worktree-cli.js +7 -7
  43. package/package.json +1 -1
  44. package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
  45. package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
  46. package/packages/pi-ai/dist/models.generated.js +735 -2588
  47. package/packages/pi-ai/dist/models.generated.js.map +1 -1
  48. package/packages/pi-ai/src/models.generated.ts +1039 -2892
  49. package/packages/pi-coding-agent/package.json +1 -1
  50. package/pkg/package.json +1 -1
  51. package/src/resources/extensions/gsd/auto/session.ts +47 -30
  52. package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
  53. package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
  54. package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
  55. package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
  56. package/src/resources/extensions/gsd/auto-observability.ts +4 -2
  57. package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
  58. package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
  59. package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
  60. package/src/resources/extensions/gsd/auto-start.ts +440 -354
  61. package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
  62. package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
  63. package/src/resources/extensions/gsd/auto-timers.ts +3 -4
  64. package/src/resources/extensions/gsd/auto-verification.ts +76 -90
  65. package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
  66. package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
  67. package/src/resources/extensions/gsd/auto.ts +515 -1199
  68. package/src/resources/extensions/gsd/captures.ts +10 -4
  69. package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
  70. package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
  71. package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
  72. package/src/resources/extensions/gsd/git-service.ts +8 -1
  73. package/src/resources/extensions/gsd/gitignore.ts +4 -2
  74. package/src/resources/extensions/gsd/gsd-db.ts +375 -180
  75. package/src/resources/extensions/gsd/index.ts +104 -263
  76. package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
  77. package/src/resources/extensions/gsd/progress-score.ts +65 -200
  78. package/src/resources/extensions/gsd/quick.ts +121 -125
  79. package/src/resources/extensions/gsd/session-lock.ts +11 -0
  80. package/src/resources/extensions/gsd/templates/preferences.md +1 -0
  81. package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
  82. package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
  83. package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
  84. package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
  85. package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
  86. package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
  87. package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
  88. package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
  89. package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
  90. package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
  91. package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
  92. package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
  93. package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
  94. package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
  95. package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
  96. package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
  97. package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
  98. package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
  99. package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
  100. package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
  101. package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
  102. package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
  103. package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
  104. package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
  105. package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
  106. package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
  107. package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
  108. package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
  109. package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
  110. package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
  111. package/src/resources/extensions/gsd/types.ts +90 -81
  112. package/src/resources/extensions/gsd/undo.ts +42 -46
  113. package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
  114. package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
  115. package/src/resources/extensions/gsd/verification-gate.ts +6 -39
  116. package/src/resources/extensions/gsd/worktree-command.ts +36 -24
  117. package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
  118. package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
  119. package/src/resources/extensions/gsd/worktree.ts +7 -44
  120. package/dist/resources/extensions/gsd/auto-constants.js +0 -5
  121. package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
  122. package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
  123. package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
  124. package/src/resources/extensions/gsd/auto-constants.ts +0 -6
  125. package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
  126. package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
  127. package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
  128. package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
  129. package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
  130. package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
  131. package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
  132. package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
  133. package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
  134. package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
  135. package/src/resources/extensions/gsd/tests/session-lock.test.ts +0 -434
@@ -149,6 +149,16 @@ function ensureExitHandler(gsdDir: string): void {
149
149
  export function acquireSessionLock(basePath: string): SessionLockResult {
150
150
  const lp = lockPath(basePath);
151
151
 
152
+ // Re-entrant acquire on the same path: release our current OS lock first so
153
+ // proper-lockfile clears its update timer before we acquire a fresh lock.
154
+ if (_releaseFunction && _lockedPath === basePath) {
155
+ try { _releaseFunction(); } catch { /* may already be released */ }
156
+ _releaseFunction = null;
157
+ _lockedPath = null;
158
+ _lockPid = 0;
159
+ _lockCompromised = false;
160
+ }
161
+
152
162
  // Ensure the directory exists
153
163
  mkdirSync(dirname(lp), { recursive: true });
154
164
 
@@ -234,6 +244,7 @@ export function acquireSessionLock(basePath: string): SessionLockResult {
234
244
  _releaseFunction = release;
235
245
  _lockedPath = basePath;
236
246
  _lockPid = process.pid;
247
+ _lockCompromised = false;
237
248
 
238
249
  // Safety net — uses centralized handler to avoid double-registration
239
250
  ensureExitHandler(gsdDir);
@@ -30,6 +30,7 @@ token_profile:
30
30
  phases:
31
31
  skip_research:
32
32
  skip_reassess:
33
+ reassess_after_slice:
33
34
  skip_slice_research:
34
35
  dynamic_routing:
35
36
  enabled:
@@ -1,14 +1,9 @@
1
1
  /**
2
- * agent-end-retry.test.ts — Verifies the deferred agent_end retry mechanism (#1072).
2
+ * agent-end-retry.test.ts — Regression checks for the post-#1419 agent_end model.
3
3
  *
4
- * When handleAgentEnd is already running and a second agent_end event fires
5
- * (e.g. a hook/triage/quick-task unit dispatched inside handleAgentEnd completes
6
- * before it returns), the reentrancy guard must not silently drop the event.
7
- * Instead, it should queue a retry via pendingAgentEndRetry so the completed
8
- * unit's agent_end is processed after the current handler finishes.
9
- *
10
- * Without this, auto-mode can stall permanently in the "summarizing" phase
11
- * with no unit running and no watchdog set.
4
+ * The old recursive handleAgentEnd retry path is gone. The loop now keeps
5
+ * pendingResolve + pendingAgentEndQueue on AutoSession, and handleAgentEnd is
6
+ * only a thin compatibility wrapper around resolveAgentEnd().
12
7
  */
13
8
 
14
9
  import test from "node:test";
@@ -29,79 +24,57 @@ function getSessionTsSource(): string {
29
24
  return readFileSync(SESSION_TS_PATH, "utf-8");
30
25
  }
31
26
 
32
- // ── AutoSession must declare pendingAgentEndRetry ────────────────────────────
33
-
34
- test("AutoSession declares pendingAgentEndRetry field", () => {
27
+ test("AutoSession declares pending agent_end queue state", () => {
35
28
  const source = getSessionTsSource();
36
29
  assert.ok(
37
- source.includes("pendingAgentEndRetry"),
38
- "AutoSession (auto/session.ts) must declare pendingAgentEndRetry field for deferred retry",
30
+ source.includes("pendingResolve"),
31
+ "AutoSession must declare pendingResolve for the in-flight unit promise",
32
+ );
33
+ assert.ok(
34
+ source.includes("pendingAgentEndQueue"),
35
+ "AutoSession must declare pendingAgentEndQueue for between-iteration agent_end events",
39
36
  );
40
37
  });
41
38
 
42
- test("AutoSession resets pendingAgentEndRetry in reset()", () => {
39
+ test("AutoSession reset clears pending agent_end queue state", () => {
43
40
  const source = getSessionTsSource();
44
- // Find the reset() method — it's declared as "reset(): void {"
45
41
  const resetIdx = source.indexOf("reset(): void");
46
42
  assert.ok(resetIdx > -1, "AutoSession must have a reset() method");
47
- const resetBlock = source.slice(resetIdx, resetIdx + 3000);
43
+ const resetBlock = source.slice(resetIdx, resetIdx + 4000);
48
44
  assert.ok(
49
- resetBlock.includes("pendingAgentEndRetry"),
50
- "reset() must clear pendingAgentEndRetry",
45
+ resetBlock.includes("this.pendingResolve = null"),
46
+ "reset() must clear pendingResolve",
51
47
  );
52
- });
53
-
54
- // ── handleAgentEnd reentrancy guard must queue retry ─────────────────────────
55
-
56
- test("handleAgentEnd sets pendingAgentEndRetry when reentrant", () => {
57
- const source = getAutoTsSource();
58
- // Find the handleAgentEnd function
59
- const fnIdx = source.indexOf("export async function handleAgentEnd");
60
- assert.ok(fnIdx > -1, "handleAgentEnd must exist in auto.ts");
61
-
62
- // The reentrancy guard section (within ~500 chars of the function start)
63
- const guardBlock = source.slice(fnIdx, fnIdx + 800);
64
48
  assert.ok(
65
- guardBlock.includes("s.handlingAgentEnd"),
66
- "handleAgentEnd must check s.handlingAgentEnd",
49
+ resetBlock.includes("this.pendingAgentEndQueue = []"),
50
+ "reset() must clear pendingAgentEndQueue",
67
51
  );
52
+ });
53
+
54
+ test("legacy pendingAgentEndRetry state is gone", () => {
55
+ const source = getSessionTsSource();
68
56
  assert.ok(
69
- guardBlock.includes("pendingAgentEndRetry = true"),
70
- "reentrancy guard must set pendingAgentEndRetry = true instead of silently dropping (#1072)",
57
+ !source.includes("pendingAgentEndRetry"),
58
+ "AutoSession should no longer use legacy pendingAgentEndRetry state",
71
59
  );
72
60
  });
73
61
 
74
- // ── finally block must process pendingAgentEndRetry ──────────────────────────
75
-
76
- test("handleAgentEnd finally block retries if pendingAgentEndRetry is set", () => {
62
+ test("handleAgentEnd is a thin compatibility wrapper", () => {
77
63
  const source = getAutoTsSource();
78
64
  const fnIdx = source.indexOf("export async function handleAgentEnd");
79
- assert.ok(fnIdx > -1, "handleAgentEnd must exist");
80
-
81
- // Find the finally block within handleAgentEnd (search for the closing pattern)
65
+ assert.ok(fnIdx > -1, "handleAgentEnd must exist in auto.ts");
82
66
  const fnBlock = source.slice(fnIdx, source.indexOf("\n// ─── ", fnIdx + 100));
67
+
83
68
  assert.ok(
84
- fnBlock.includes("pendingAgentEndRetry"),
85
- "handleAgentEnd finally block must check pendingAgentEndRetry",
86
- );
87
- assert.ok(
88
- fnBlock.includes("setImmediate"),
89
- "deferred retry must use setImmediate to avoid stack overflow (#1072)",
69
+ fnBlock.includes("resolveAgentEnd("),
70
+ "handleAgentEnd must delegate to resolveAgentEnd",
90
71
  );
91
72
  assert.ok(
92
- fnBlock.includes("handleAgentEnd(ctx, pi)"),
93
- "deferred retry must call handleAgentEnd recursively (#1072)",
73
+ !fnBlock.includes("pendingAgentEndRetry"),
74
+ "handleAgentEnd must not use legacy retry state",
94
75
  );
95
- });
96
-
97
- // ── Regression: reentrancy guard must NOT silently return ─────────────────────
98
-
99
- test("reentrancy guard references issue #1072", () => {
100
- const source = getAutoTsSource();
101
- const fnIdx = source.indexOf("export async function handleAgentEnd");
102
- const guardBlock = source.slice(fnIdx, fnIdx + 800);
103
76
  assert.ok(
104
- guardBlock.includes("1072"),
105
- "reentrancy guard comment must reference #1072 for traceability",
77
+ !fnBlock.includes("dispatchNextUnit"),
78
+ "handleAgentEnd must not dispatch recursively",
106
79
  );
107
80
  });
@@ -12,7 +12,15 @@
12
12
 
13
13
  import test from "node:test";
14
14
  import assert from "node:assert/strict";
15
- import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, realpathSync, readFileSync } from "node:fs";
15
+ import {
16
+ mkdtempSync,
17
+ mkdirSync,
18
+ rmSync,
19
+ writeFileSync,
20
+ existsSync,
21
+ realpathSync,
22
+ readFileSync,
23
+ } from "node:fs";
16
24
  import { join, dirname } from "node:path";
17
25
  import { tmpdir } from "node:os";
18
26
  import { execSync } from "node:child_process";
@@ -28,11 +36,17 @@ import {
28
36
  const __dirname = dirname(fileURLToPath(import.meta.url));
29
37
 
30
38
  function run(command: string, cwd: string): string {
31
- return execSync(command, { cwd, stdio: ["ignore", "pipe", "pipe"], encoding: "utf-8" }).trim();
39
+ return execSync(command, {
40
+ cwd,
41
+ stdio: ["ignore", "pipe", "pipe"],
42
+ encoding: "utf-8",
43
+ }).trim();
32
44
  }
33
45
 
34
46
  function createTempRepo(): string {
35
- const dir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-all-complete-test-")));
47
+ const dir = realpathSync(
48
+ mkdtempSync(join(tmpdir(), "gsd-all-complete-test-")),
49
+ );
36
50
  run("git init", dir);
37
51
  run("git config user.email test@test.com", dir);
38
52
  run("git config user.name Test", dir);
@@ -63,41 +77,54 @@ function createMilestoneArtifacts(dir: string, mid: string): void {
63
77
 
64
78
  // ─── Source-level: verify the merge code exists in the "all complete" path ────
65
79
 
66
- test("auto.ts 'all milestones complete' path merges before stopping (#962)", () => {
67
- const autoSrc = readFileSync(join(__dirname, "..", "auto.ts"), "utf-8");
80
+ test("auto-loop 'all milestones complete' path merges before stopping (#962)", () => {
81
+ const loopSrc = readFileSync(join(__dirname, "..", "auto-loop.ts"), "utf-8");
82
+ const resolverSrc = readFileSync(
83
+ join(__dirname, "..", "worktree-resolver.ts"),
84
+ "utf-8",
85
+ );
68
86
 
69
87
  // Find the "incomplete.length === 0" block
70
- const incompleteIdx = autoSrc.indexOf("incomplete.length === 0");
71
- assert.ok(incompleteIdx > -1, "auto.ts should have 'incomplete.length === 0' check");
88
+ const incompleteIdx = loopSrc.indexOf("incomplete.length === 0");
89
+ assert.ok(
90
+ incompleteIdx > -1,
91
+ "auto-loop.ts should have 'incomplete.length === 0' check",
92
+ );
72
93
 
73
94
  // The merge call must appear BETWEEN the incomplete check and the stopAuto call.
74
- // After the #1308 refactor, the merge is delegated to tryMergeMilestone.
75
- const blockAfterIncomplete = autoSrc.slice(incompleteIdx, incompleteIdx + 3000);
95
+ const blockAfterIncomplete = loopSrc.slice(
96
+ incompleteIdx,
97
+ incompleteIdx + 3000,
98
+ );
76
99
 
77
100
  assert.ok(
78
- blockAfterIncomplete.includes("tryMergeMilestone"),
79
- "auto.ts should call tryMergeMilestone in the 'all milestones complete' path",
101
+ blockAfterIncomplete.includes("deps.resolver.mergeAndExit"),
102
+ "auto-loop.ts should call resolver.mergeAndExit in the 'all milestones complete' path",
80
103
  );
81
104
 
82
105
  // The merge should come before stopAuto in this block
83
- const mergePos = blockAfterIncomplete.indexOf("tryMergeMilestone");
106
+ const mergePos = blockAfterIncomplete.indexOf("deps.resolver.mergeAndExit");
84
107
  const stopPos = blockAfterIncomplete.indexOf("stopAuto");
85
108
  assert.ok(
86
109
  mergePos < stopPos,
87
- "tryMergeMilestone should be called before stopAuto in the 'all complete' path",
110
+ "resolver.mergeAndExit should be called before stopAuto in the 'all complete' path",
88
111
  );
89
112
 
90
- // Verify tryMergeMilestone handles both worktree and branch isolation
91
- const helperIdx = autoSrc.indexOf("function tryMergeMilestone");
92
- assert.ok(helperIdx > -1, "tryMergeMilestone helper should exist");
93
- const helperBlock = autoSrc.slice(helperIdx, helperIdx + 2000);
113
+ const helperIdx = resolverSrc.indexOf("mergeAndExit(milestoneId");
114
+ assert.ok(
115
+ helperIdx > -1,
116
+ "WorktreeResolver.mergeAndExit helper should exist",
117
+ );
118
+ const helperBlock = resolverSrc.slice(helperIdx, helperIdx + 2600);
94
119
  assert.ok(
95
- helperBlock.includes("isInAutoWorktree"),
96
- "tryMergeMilestone should check isInAutoWorktree for worktree mode",
120
+ helperBlock.includes('mode === "worktree"') ||
121
+ helperBlock.includes('mode: "worktree"'),
122
+ "WorktreeResolver.mergeAndExit should handle worktree mode",
97
123
  );
98
124
  assert.ok(
99
- helperBlock.includes("getIsolationMode") || helperBlock.includes("isolationMode"),
100
- "tryMergeMilestone should check isolation mode for branch mode",
125
+ helperBlock.includes('mode === "branch"') ||
126
+ helperBlock.includes('mode: "branch"'),
127
+ "WorktreeResolver.mergeAndExit should handle branch mode",
101
128
  );
102
129
  });
103
130
 
@@ -124,23 +151,38 @@ test("single milestone worktree is merged to main when all complete (#962)", ()
124
151
  run('git commit -m "feat(M001): add feature"', wt);
125
152
 
126
153
  // Simulate the fix: merge before stopping (what the "all complete" path now does)
127
- const roadmapPath = join(tempDir, ".gsd", "milestones", "M001", "M001-ROADMAP.md");
154
+ const roadmapPath = join(
155
+ tempDir,
156
+ ".gsd",
157
+ "milestones",
158
+ "M001",
159
+ "M001-ROADMAP.md",
160
+ );
128
161
  const roadmapContent = readFileSync(roadmapPath, "utf-8");
129
162
  const mergeResult = mergeMilestoneToMain(tempDir, "M001", roadmapContent);
130
163
 
131
164
  // Verify work is on main
132
- assert.ok(existsSync(join(tempDir, "feature.ts")), "feature.ts should be on main after merge");
165
+ assert.ok(
166
+ existsSync(join(tempDir, "feature.ts")),
167
+ "feature.ts should be on main after merge",
168
+ );
133
169
  assert.equal(process.cwd(), tempDir, "cwd restored to project root");
134
170
  assert.ok(!isInAutoWorktree(tempDir), "no longer in auto-worktree");
135
171
  assert.equal(getAutoWorktreeOriginalBase(), null, "originalBase cleared");
136
172
 
137
173
  // Verify milestone branch was cleaned up
138
174
  const branches = run("git branch", tempDir);
139
- assert.ok(!branches.includes("milestone/M001"), "milestone branch should be deleted");
175
+ assert.ok(
176
+ !branches.includes("milestone/M001"),
177
+ "milestone branch should be deleted",
178
+ );
140
179
 
141
180
  // Verify squash commit on main
142
181
  const log = run("git log --oneline -3", tempDir);
143
- assert.ok(log.includes("M001"), "squash commit on main should reference M001");
182
+ assert.ok(
183
+ log.includes("M001"),
184
+ "squash commit on main should reference M001",
185
+ );
144
186
 
145
187
  assert.ok(mergeResult.commitMessage.length > 0, "commit message returned");
146
188
  } finally {
@@ -171,7 +213,10 @@ test("last milestone worktree is merged when it's the final one (#962)", () => {
171
213
  writeFileSync(join(wt1, "m001-work.ts"), "export const m001 = true;\n");
172
214
  run("git add .", wt1);
173
215
  run('git commit -m "feat(M001): m001 work"', wt1);
174
- const roadmap1 = readFileSync(join(tempDir, ".gsd", "milestones", "M001", "M001-ROADMAP.md"), "utf-8");
216
+ const roadmap1 = readFileSync(
217
+ join(tempDir, ".gsd", "milestones", "M001", "M001-ROADMAP.md"),
218
+ "utf-8",
219
+ );
175
220
  mergeMilestoneToMain(tempDir, "M001", roadmap1);
176
221
 
177
222
  // Now complete M002 (the LAST milestone — this is the #962 scenario)
@@ -179,7 +224,10 @@ test("last milestone worktree is merged when it's the final one (#962)", () => {
179
224
  writeFileSync(join(wt2, "m002-work.ts"), "export const m002 = true;\n");
180
225
  run("git add .", wt2);
181
226
  run('git commit -m "feat(M002): m002 work"', wt2);
182
- const roadmap2 = readFileSync(join(tempDir, ".gsd", "milestones", "M002", "M002-ROADMAP.md"), "utf-8");
227
+ const roadmap2 = readFileSync(
228
+ join(tempDir, ".gsd", "milestones", "M002", "M002-ROADMAP.md"),
229
+ "utf-8",
230
+ );
183
231
  mergeMilestoneToMain(tempDir, "M002", roadmap2);
184
232
 
185
233
  // Both features should now be on main
@@ -5,7 +5,7 @@ import {
5
5
  getBudgetAlertLevel,
6
6
  getBudgetEnforcementAction,
7
7
  getNewBudgetAlertLevel,
8
- } from "../auto-budget.js";
8
+ } from "../auto.js";
9
9
 
10
10
  test("getBudgetAlertLevel returns the expected threshold bucket", () => {
11
11
  assert.equal(getBudgetAlertLevel(0.10), 0);
@@ -1,10 +1,25 @@
1
1
  import test from "node:test";
2
2
  import assert from "node:assert/strict";
3
3
  import { mkdirSync, mkdtempSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
4
+ import { createRequire } from "node:module";
4
5
  import { join } from "node:path";
5
6
  import { tmpdir } from "node:os";
6
7
 
7
8
  import { writeLock, readCrashLock, clearLock, isLockProcessAlive } from "../crash-recovery.ts";
9
+ import { acquireSessionLock, releaseSessionLock } from "../session-lock.ts";
10
+
11
+ const require = createRequire(import.meta.url);
12
+
13
+ function hasProperLockfile(): boolean {
14
+ try {
15
+ require("proper-lockfile");
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ const properLockfileAvailable = hasProperLockfile();
8
23
 
9
24
  // ─── writeLock creates auto.lock in .gsd/ ────────────────────────────────
10
25
 
@@ -95,6 +110,28 @@ test("clearLock is safe when no lock file exists", () => {
95
110
  rmSync(dir, { recursive: true, force: true });
96
111
  });
97
112
 
113
+ test("bootstrap cleanup releases session lock artifacts", () => {
114
+ const dir = mkdtempSync(join(tmpdir(), "gsd-lock-test-"));
115
+ mkdirSync(join(dir, ".gsd"), { recursive: true });
116
+
117
+ try {
118
+ const result = acquireSessionLock(dir);
119
+ assert.equal(result.acquired, true, "session lock should be acquired");
120
+ assert.ok(existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should exist while lock is held");
121
+ if (properLockfileAvailable) {
122
+ assert.ok(existsSync(join(dir, ".gsd.lock")), ".gsd.lock should exist while lock is held");
123
+ }
124
+
125
+ releaseSessionLock(dir);
126
+ clearLock(dir);
127
+
128
+ assert.ok(!existsSync(join(dir, ".gsd", "auto.lock")), "auto.lock should be removed by bootstrap cleanup");
129
+ assert.ok(!existsSync(join(dir, ".gsd.lock")), ".gsd.lock should be removed by bootstrap cleanup");
130
+ } finally {
131
+ rmSync(dir, { recursive: true, force: true });
132
+ }
133
+ });
134
+
98
135
  // ─── isLockProcessAlive detects live vs dead PIDs ────────────────────────
99
136
 
100
137
  test("isLockProcessAlive returns false for dead PID", () => {