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
@@ -11,10 +11,6 @@ import {
11
11
  diagnoseExpectedArtifact,
12
12
  buildLoopRemediationSteps,
13
13
  selfHealRuntimeRecords,
14
- completedKeysPath,
15
- persistCompletedKey,
16
- removePersistedKey,
17
- loadPersistedKeys,
18
14
  } from "../auto-recovery.ts";
19
15
  import { parseRoadmap, clearParseCache } from "../files.ts";
20
16
  import { invalidateAllCaches } from "../cache.ts";
@@ -201,143 +197,6 @@ test("buildLoopRemediationSteps returns null for unknown type", () => {
201
197
  }
202
198
  });
203
199
 
204
- // ─── Completed-unit key persistence ───────────────────────────────────────
205
-
206
- test("completedKeysPath returns path inside .gsd", () => {
207
- const path = completedKeysPath("/project");
208
- assert.ok(path.includes(".gsd"));
209
- assert.ok(path.includes("completed-units.json"));
210
- });
211
-
212
- test("persistCompletedKey and loadPersistedKeys round-trip", () => {
213
- const base = makeTmpBase();
214
- try {
215
- persistCompletedKey(base, "execute-task/M001/S01/T01");
216
- persistCompletedKey(base, "plan-slice/M001/S02");
217
-
218
- const keys = new Set<string>();
219
- loadPersistedKeys(base, keys);
220
-
221
- assert.ok(keys.has("execute-task/M001/S01/T01"));
222
- assert.ok(keys.has("plan-slice/M001/S02"));
223
- assert.equal(keys.size, 2);
224
- } finally {
225
- cleanup(base);
226
- }
227
- });
228
-
229
- test("persistCompletedKey is idempotent", () => {
230
- const base = makeTmpBase();
231
- try {
232
- persistCompletedKey(base, "execute-task/M001/S01/T01");
233
- persistCompletedKey(base, "execute-task/M001/S01/T01");
234
-
235
- const keys = new Set<string>();
236
- loadPersistedKeys(base, keys);
237
- assert.equal(keys.size, 1);
238
- } finally {
239
- cleanup(base);
240
- }
241
- });
242
-
243
- test("removePersistedKey removes a key", () => {
244
- const base = makeTmpBase();
245
- try {
246
- persistCompletedKey(base, "a");
247
- persistCompletedKey(base, "b");
248
- removePersistedKey(base, "a");
249
-
250
- const keys = new Set<string>();
251
- loadPersistedKeys(base, keys);
252
- assert.ok(!keys.has("a"));
253
- assert.ok(keys.has("b"));
254
- } finally {
255
- cleanup(base);
256
- }
257
- });
258
-
259
- test("loadPersistedKeys handles missing file gracefully", () => {
260
- const base = makeTmpBase();
261
- try {
262
- const keys = new Set<string>();
263
- assert.doesNotThrow(() => loadPersistedKeys(base, keys));
264
- assert.equal(keys.size, 0);
265
- } finally {
266
- cleanup(base);
267
- }
268
- });
269
-
270
- test("removePersistedKey is safe when file doesn't exist", () => {
271
- const base = makeTmpBase();
272
- try {
273
- assert.doesNotThrow(() => removePersistedKey(base, "nonexistent"));
274
- } finally {
275
- cleanup(base);
276
- }
277
- });
278
-
279
- // ─── Dual-load across worktree boundary (#769) ───────────────────────────
280
-
281
- test("loadPersistedKeys unions keys from project root and worktree", () => {
282
- // Simulate two separate .gsd directories (project root + worktree)
283
- // each with a different set of completed keys. Loading from both
284
- // into the same Set should produce the union.
285
- const projectRoot = makeTmpBase();
286
- const worktree = makeTmpBase();
287
- try {
288
- // Persist different keys in each location
289
- persistCompletedKey(projectRoot, "execute-task/M001/S01/T01");
290
- persistCompletedKey(projectRoot, "plan-slice/M001/S02");
291
-
292
- persistCompletedKey(worktree, "execute-task/M001/S01/T02");
293
- persistCompletedKey(worktree, "plan-slice/M001/S02"); // overlap
294
-
295
- // Load from both into the same set (mimicking startup dual-load)
296
- const keys = new Set<string>();
297
- loadPersistedKeys(projectRoot, keys);
298
- loadPersistedKeys(worktree, keys);
299
-
300
- assert.ok(keys.has("execute-task/M001/S01/T01"), "key from project root");
301
- assert.ok(keys.has("plan-slice/M001/S02"), "shared key");
302
- assert.ok(keys.has("execute-task/M001/S01/T02"), "key from worktree");
303
- assert.equal(keys.size, 3, "union should deduplicate overlapping keys");
304
- } finally {
305
- cleanup(projectRoot);
306
- cleanup(worktree);
307
- }
308
- });
309
-
310
- test("completed-units.json set-union merge produces correct result", () => {
311
- // Verify that a manual set-union merge correctly merges two JSON arrays
312
- // of completed-unit keys.
313
- const projectRoot = makeTmpBase();
314
- const worktree = makeTmpBase();
315
- try {
316
- // Write keys to both locations
317
- const prKeysFile = join(projectRoot, ".gsd", "completed-units.json");
318
- const wtKeysFile = join(worktree, ".gsd", "completed-units.json");
319
-
320
- writeFileSync(prKeysFile, JSON.stringify(["a", "b"]));
321
- writeFileSync(wtKeysFile, JSON.stringify(["b", "c", "d"]));
322
-
323
- // Perform a set-union merge of two JSON key arrays
324
- const srcKeys: string[] = JSON.parse(readFileSync(wtKeysFile, "utf8"));
325
- let dstKeys: string[] = [];
326
- if (existsSync(prKeysFile)) {
327
- dstKeys = JSON.parse(readFileSync(prKeysFile, "utf8"));
328
- }
329
- const merged = [...new Set([...dstKeys, ...srcKeys])];
330
- writeFileSync(prKeysFile, JSON.stringify(merged, null, 2));
331
-
332
- // Verify the merged result
333
- const result: string[] = JSON.parse(readFileSync(prKeysFile, "utf8"));
334
- assert.deepStrictEqual(result.sort(), ["a", "b", "c", "d"]);
335
- } finally {
336
- cleanup(projectRoot);
337
- cleanup(worktree);
338
- }
339
- });
340
-
341
200
  // ─── verifyExpectedArtifact: parse cache collision regression ─────────────
342
201
 
343
202
  test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () => {
@@ -528,9 +387,9 @@ test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", ()
528
387
 
529
388
  // ─── selfHealRuntimeRecords — worktree base path (#769) ──────────────────
530
389
 
531
- test("selfHealRuntimeRecords clears stale record when artifact exists at worktree base (#769)", async () => {
532
- // Simulate worktree layout: the runtime record AND the artifact both live
533
- // under the worktree's .gsd/, not the main project root.
390
+ test("selfHealRuntimeRecords clears stale dispatched records (#769)", async () => {
391
+ // selfHealRuntimeRecords now only clears stale dispatched records (>1h).
392
+ // No completedKeySet parameter deriveState is sole authority.
534
393
  const worktreeBase = makeTmpBase();
535
394
  const mainBase = makeTmpBase();
536
395
  try {
@@ -541,10 +400,6 @@ test("selfHealRuntimeRecords clears stale record when artifact exists at worktre
541
400
  phase: "dispatched",
542
401
  });
543
402
 
544
- // Write the UAT result artifact in the worktree .gsd/milestones/
545
- const uatPath = join(worktreeBase, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT-RESULT.md");
546
- writeFileSync(uatPath, "---\nresult: pass\n---\n# UAT Result\nAll tests passed.\n");
547
-
548
403
  // Verify the runtime record exists before heal
549
404
  const before = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
550
405
  assert.ok(before, "runtime record should exist before heal");
@@ -555,32 +410,23 @@ test("selfHealRuntimeRecords clears stale record when artifact exists at worktre
555
410
  ui: { notify: (msg: string) => { notifications.push(msg); } },
556
411
  } as any;
557
412
 
558
- // Call selfHeal with worktreeBase — this is the fix: using the worktree path
559
- // so both the runtime record and artifact are found
560
- const completedKeys = new Set<string>();
561
- await selfHealRuntimeRecords(worktreeBase, mockCtx, completedKeys);
413
+ // Call selfHeal with worktreeBase — should clear the stale record
414
+ await selfHealRuntimeRecords(worktreeBase, mockCtx);
562
415
 
563
416
  // The stale record should be cleared
564
417
  const after = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
565
418
  assert.equal(after, null, "runtime record should be cleared after heal");
566
-
567
- // The completion key should be persisted
568
- assert.ok(completedKeys.has("run-uat/M001/S01"), "completion key should be added");
569
419
  assert.ok(notifications.some(n => n.includes("Self-heal")), "should emit self-heal notification");
570
420
 
571
- // Now verify that calling with mainBase does NOT find/clear anything (the old bug)
572
- // Write a stale record at mainBase but NO artifact there
421
+ // Write a stale record at mainBase
573
422
  writeUnitRuntimeRecord(mainBase, "run-uat", "M001/S01", Date.now() - 7200_000, {
574
423
  phase: "dispatched",
575
424
  });
576
- const mainKeys = new Set<string>();
577
- await selfHealRuntimeRecords(mainBase, mockCtx, mainKeys);
425
+ await selfHealRuntimeRecords(mainBase, mockCtx);
578
426
 
579
- // The record at mainBase should be cleared by the stale timeout (>1h),
580
- // but the completion key should NOT be set (artifact doesn't exist at mainBase)
427
+ // The record at mainBase should also be cleared by the stale timeout (>1h)
581
428
  const afterMain = readUnitRuntimeRecord(mainBase, "run-uat", "M001/S01");
582
429
  assert.equal(afterMain, null, "stale record at main base should be cleared by timeout");
583
- assert.ok(!mainKeys.has("run-uat/M001/S01"), "completion key should NOT be set when artifact is missing");
584
430
  } finally {
585
431
  cleanup(worktreeBase);
586
432
  cleanup(mainBase);
@@ -2,11 +2,10 @@
2
2
  * Integration tests for the secrets collection gate in startAuto().
3
3
  *
4
4
  * Exercises getManifestStatus() → collectSecretsFromManifest() composition
5
- * end-to-end using real filesystem state. Proves the gate paths:
5
+ * end-to-end using real filesystem state. Proves the three gate paths:
6
6
  * 1. No manifest exists — gate skips silently
7
- * 2. Pending keys exist — gate triggers collection (direct call)
7
+ * 2. Pending keys exist — gate triggers collection
8
8
  * 3. No pending keys — gate skips silently
9
- * 4. Pending keys in auto-mode — session pauses instead of blocking (#1146)
10
9
  *
11
10
  * Uses temp directories with real .gsd/milestones/M001/ structure, mirroring
12
11
  * the pattern from manifest-status.test.ts.
@@ -19,7 +18,6 @@ import { join } from 'node:path';
19
18
  import { tmpdir } from 'node:os';
20
19
  import { getManifestStatus } from '../files.ts';
21
20
  import { collectSecretsFromManifest } from '../../get-secrets-from-user.ts';
22
- import { AutoSession } from '../auto/session.ts';
23
21
 
24
22
  function makeTempDir(prefix: string): string {
25
23
  const dir = join(tmpdir(), `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2)}`);
@@ -148,110 +146,6 @@ test('secrets gate: pending keys exist — gate triggers collection, manifest up
148
146
 
149
147
  // ─── Scenario 3: No pending keys — all collected or in env ──────────────────
150
148
 
151
- // ─── Scenario 4: Pending keys pause AutoSession instead of blocking (#1146) ──
152
-
153
- test('secrets gate: pending keys set pausedForSecrets on AutoSession', async () => {
154
- const tmp = makeTempDir('gate-pause-session');
155
- try {
156
- // Ensure pending keys are NOT in env
157
- delete process.env.GSD_PAUSE_TEST_KEY_A;
158
- delete process.env.GSD_PAUSE_TEST_KEY_B;
159
-
160
- writeManifest(tmp, `# Secrets Manifest
161
-
162
- **Milestone:** M001
163
- **Generated:** 2025-06-20T10:00:00Z
164
-
165
- ### GSD_PAUSE_TEST_KEY_A
166
-
167
- **Service:** ServiceA
168
- **Status:** pending
169
- **Destination:** dotenv
170
-
171
- 1. Get key A from dashboard
172
-
173
- ### GSD_PAUSE_TEST_KEY_B
174
-
175
- **Service:** ServiceB
176
- **Status:** pending
177
- **Destination:** dotenv
178
-
179
- 1. Get key B from dashboard
180
- `);
181
-
182
- // Verify manifest has pending keys
183
- const status = await getManifestStatus(tmp, 'M001');
184
- assert.notStrictEqual(status, null, 'manifest should exist');
185
- assert.deepStrictEqual(status!.pending, ['GSD_PAUSE_TEST_KEY_A', 'GSD_PAUSE_TEST_KEY_B']);
186
-
187
- // Simulate what auto-start.ts now does: set pause flags on session
188
- const session = new AutoSession();
189
- session.active = true;
190
- session.currentMilestoneId = 'M001';
191
-
192
- // The new gate logic: if pending keys exist, pause instead of collecting
193
- if (status!.pending.length > 0) {
194
- session.paused = true;
195
- session.pausedForSecrets = true;
196
- }
197
-
198
- assert.strictEqual(session.paused, true, 'session should be paused');
199
- assert.strictEqual(session.pausedForSecrets, true, 'pausedForSecrets flag should be set');
200
-
201
- // Verify reset() clears pausedForSecrets
202
- session.reset();
203
- assert.strictEqual(session.pausedForSecrets, false, 'reset() should clear pausedForSecrets');
204
- } finally {
205
- delete process.env.GSD_PAUSE_TEST_KEY_A;
206
- delete process.env.GSD_PAUSE_TEST_KEY_B;
207
- rmSync(tmp, { recursive: true, force: true });
208
- }
209
- });
210
-
211
- test('secrets gate: no pending keys do not set pausedForSecrets', async () => {
212
- const tmp = makeTempDir('gate-no-pause');
213
- const savedKey = process.env.GSD_NO_PAUSE_TEST_KEY;
214
- try {
215
- process.env.GSD_NO_PAUSE_TEST_KEY = 'already-set';
216
-
217
- writeManifest(tmp, `# Secrets Manifest
218
-
219
- **Milestone:** M001
220
- **Generated:** 2025-06-20T10:00:00Z
221
-
222
- ### GSD_NO_PAUSE_TEST_KEY
223
-
224
- **Service:** ServiceX
225
- **Status:** pending
226
- **Destination:** dotenv
227
-
228
- 1. Already in env
229
- `);
230
-
231
- const status = await getManifestStatus(tmp, 'M001');
232
- assert.notStrictEqual(status, null, 'manifest should exist');
233
- assert.deepStrictEqual(status!.pending, [], 'no pending keys — already in env');
234
-
235
- // Simulate gate logic — no pending keys, no pause
236
- const session = new AutoSession();
237
- session.active = true;
238
-
239
- if (status!.pending.length > 0) {
240
- session.paused = true;
241
- session.pausedForSecrets = true;
242
- }
243
-
244
- assert.strictEqual(session.paused, false, 'session should NOT be paused');
245
- assert.strictEqual(session.pausedForSecrets, false, 'pausedForSecrets should NOT be set');
246
- } finally {
247
- delete process.env.GSD_NO_PAUSE_TEST_KEY;
248
- if (savedKey !== undefined) process.env.GSD_NO_PAUSE_TEST_KEY = savedKey;
249
- rmSync(tmp, { recursive: true, force: true });
250
- }
251
- });
252
-
253
- // ─── Scenario 3: No pending keys — all collected or in env ──────────────────
254
-
255
149
  test('secrets gate: no pending keys — getManifestStatus shows pending.length === 0', async () => {
256
150
  const tmp = makeTempDir('gate-no-pending');
257
151
  const savedKey = process.env.GSD_GATE_TEST_ENVKEY;
@@ -145,8 +145,7 @@ test("AutoSession.reset() references every instance property", () => {
145
145
  assert.ok(resetMatch, "AutoSession.reset() method not found");
146
146
  const resetBody = resetMatch![1]!;
147
147
 
148
- // completedKeySet is intentionally not cleared (documented in reset())
149
- const intentionallySkipped = new Set(["completedKeySet"]);
148
+ const intentionallySkipped = new Set<string>([]);
150
149
 
151
150
  const missingFromReset: string[] = [];
152
151
  for (const prop of properties) {
@@ -182,7 +181,6 @@ test("AutoSession.toJSON() includes key diagnostic properties", () => {
182
181
  "basePath",
183
182
  "currentMilestoneId",
184
183
  "currentUnit",
185
- "dispatching",
186
184
  ];
187
185
 
188
186
  const missing = requiredDiagnostics.filter(prop => !toJSONBody.includes(prop));
@@ -32,9 +32,6 @@ function createTempRepo(): string {
32
32
  run("git config user.email test@test.com", dir);
33
33
  run("git config user.name Test", dir);
34
34
  writeFileSync(join(dir, "README.md"), "# test\n");
35
- // Mirror production: GSD runtime dirs are gitignored so autoCommitDirtyState
36
- // doesn't pick up the worktrees directory as dirty state (#1127 fix).
37
- writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n");
38
35
  mkdirSync(join(dir, ".gsd"), { recursive: true });
39
36
  writeFileSync(join(dir, ".gsd", "STATE.md"), "# State\n");
40
37
  run("git add .", dir);
@@ -153,6 +153,64 @@ async function main(): Promise<void> {
153
153
  // After teardown, originalBase should be null
154
154
  assertEq(getAutoWorktreeOriginalBase(), null, "no split-brain: originalBase cleared");
155
155
 
156
+ // ─── #778: reconcile plan checkboxes on re-attach ─────────────────
157
+ console.log("\n=== #778: reconcile plan checkboxes on re-attach ===");
158
+ {
159
+ // Simulate: T01 [x] was committed to milestone branch, T02 [x] was
160
+ // written to project root by syncStateToProjectRoot() but the
161
+ // auto-commit crashed before it fired. On restart the worktree is
162
+ // re-created from the milestone branch HEAD (T02 still [ ]).
163
+ // reconcilePlanCheckboxes should forward-apply T02 [x] from the root.
164
+
165
+ const planRelPath = join(".gsd", "milestones", "M004", "slices", "S01", "S01-PLAN.md");
166
+ const planDir = join(tempDir, ".gsd", "milestones", "M004", "slices", "S01");
167
+ const { mkdirSync: mkdir, writeFileSync: write, readFileSync: read } = await import("node:fs");
168
+
169
+ // Plan on integration branch (project root): T01 [x], T02 [x]
170
+ mkdir(planDir, { recursive: true });
171
+ write(
172
+ join(tempDir, planRelPath),
173
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
174
+ );
175
+
176
+ // Write integration-branch plan to git so milestone branch starts from it
177
+ run(`git add .`, tempDir);
178
+ run(`git commit -m "add plan with T01 and T02 checked" --allow-empty`, tempDir);
179
+
180
+ // Create milestone branch with only T01 [x] (simulating crash before T02 commit)
181
+ const milestoneBranch = "milestone/M004";
182
+ run(`git checkout -b ${milestoneBranch}`, tempDir);
183
+ mkdir(planDir, { recursive: true });
184
+ write(
185
+ join(tempDir, planRelPath),
186
+ "# S01 Plan\n- [x] **T01:** task one\n- [ ] **T02:** task two\n- [ ] **T03:** task three\n",
187
+ );
188
+ run(`git add .`, tempDir);
189
+ run(`git commit -m "milestone: only T01 checked"`, tempDir);
190
+ run(`git checkout main`, tempDir);
191
+
192
+ // Restore project root plan (T01+T02 [x]) — simulates syncStateToProjectRoot
193
+ write(
194
+ join(tempDir, planRelPath),
195
+ "# S01 Plan\n- [x] **T01:** task one\n- [x] **T02:** task two\n- [ ] **T03:** task three\n",
196
+ );
197
+
198
+ // Create worktree re-attached to existing milestone branch (T02 still [ ] in branch)
199
+ const wtPath = createAutoWorktree(tempDir, "M004");
200
+
201
+ try {
202
+ const wtPlanPath = join(wtPath, planRelPath);
203
+ assertTrue(existsSync(wtPlanPath), "plan file exists in worktree after re-attach");
204
+
205
+ const wtPlan = read(wtPlanPath, "utf-8");
206
+ assertTrue(wtPlan.includes("- [x] **T02:"), "T02 should be [x] after reconciliation (was [ ] on branch)");
207
+ assertTrue(wtPlan.includes("- [x] **T01:"), "T01 stays [x]");
208
+ assertTrue(wtPlan.includes("- [ ] **T03:"), "T03 stays [ ] (not in root either)");
209
+ } finally {
210
+ teardownAutoWorktree(tempDir, "M004");
211
+ }
212
+ }
213
+
156
214
  } finally {
157
215
  // Always restore cwd and clean up
158
216
  process.chdir(savedCwd);
@@ -71,58 +71,3 @@ test("dispatch guard works without git repo", () => {
71
71
  rmSync(repo, { recursive: true, force: true });
72
72
  }
73
73
  });
74
-
75
- test("dispatch guard skips parked milestones — they do not block later milestones", () => {
76
- const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-parked-"));
77
- try {
78
- // M004 is parked with incomplete slices
79
- mkdirSync(join(repo, ".gsd", "milestones", "M004"), { recursive: true });
80
- writeFileSync(join(repo, ".gsd", "milestones", "M004", "M004-ROADMAP.md"),
81
- "# M004: Parked Milestone\n\n## Slices\n- [ ] **S01: Unfinished** `risk:high` `depends:[]`\n");
82
- writeFileSync(join(repo, ".gsd", "milestones", "M004", "M004-PARKED.md"),
83
- "---\nparked_at: 2026-03-18T09:00:00.000Z\nreason: \"Parked via /gsd park\"\n---\n\n# M004 — Parked\n");
84
-
85
- // M010 is the target milestone
86
- mkdirSync(join(repo, ".gsd", "milestones", "M010"), { recursive: true });
87
- writeFileSync(join(repo, ".gsd", "milestones", "M010", "M010-ROADMAP.md"),
88
- "# M010: Active Milestone\n\n## Slices\n- [ ] **S01: First** `risk:high` `depends:[]`\n");
89
-
90
- // M004's incomplete S01 should NOT block M010/S01 because M004 is parked
91
- assert.equal(
92
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M010/S01"),
93
- null,
94
- );
95
- } finally {
96
- rmSync(repo, { recursive: true, force: true });
97
- }
98
- });
99
-
100
- test("dispatch guard still blocks on non-parked incomplete milestones", () => {
101
- const repo = mkdtempSync(join(tmpdir(), "gsd-dispatch-guard-mixed-"));
102
- try {
103
- // M003 is parked — should be skipped
104
- mkdirSync(join(repo, ".gsd", "milestones", "M003"), { recursive: true });
105
- writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-ROADMAP.md"),
106
- "# M003: Parked\n\n## Slices\n- [ ] **S01: Unfinished** `risk:high` `depends:[]`\n");
107
- writeFileSync(join(repo, ".gsd", "milestones", "M003", "M003-PARKED.md"),
108
- "---\nparked_at: 2026-03-18T09:00:00.000Z\nreason: \"Parked\"\n---\n");
109
-
110
- // M005 is NOT parked and has incomplete slices — should block
111
- mkdirSync(join(repo, ".gsd", "milestones", "M005"), { recursive: true });
112
- writeFileSync(join(repo, ".gsd", "milestones", "M005", "M005-ROADMAP.md"),
113
- "# M005: Active Incomplete\n\n## Slices\n- [ ] **S01: Pending** `risk:low` `depends:[]`\n");
114
-
115
- // M010 is the target
116
- mkdirSync(join(repo, ".gsd", "milestones", "M010"), { recursive: true });
117
- writeFileSync(join(repo, ".gsd", "milestones", "M010", "M010-ROADMAP.md"),
118
- "# M010: Target\n\n## Slices\n- [ ] **S01: First** `risk:low` `depends:[]`\n");
119
-
120
- // M005/S01 should block M010/S01 (M003 is parked, so skipped)
121
- assert.equal(
122
- getPriorSliceCompletionBlocker(repo, "main", "plan-slice", "M010/S01"),
123
- "Cannot dispatch plan-slice M010/S01: earlier slice M005/S01 is not complete.",
124
- );
125
- } finally {
126
- rmSync(repo, { recursive: true, force: true });
127
- }
128
- });
@@ -159,4 +159,26 @@ describe('headless query', () => {
159
159
  assert.equal(snap.state.activeMilestone!.id, 'M001')
160
160
  assert.equal(snap.next.action, 'dispatch')
161
161
  })
162
+
163
+ it('reports all milestones complete with a clean stop reason', async () => {
164
+ writeRoadmap(base, 'M001', `# M001: Test Milestone
165
+
166
+ ## Slices
167
+
168
+ - [x] **S01: First Slice** \`risk:low\` \`depends:[]\`
169
+ > Done.
170
+ `)
171
+ writeFileSync(
172
+ join(base, '.gsd', 'milestones', 'M001', 'M001-SUMMARY.md'),
173
+ '# M001 Summary\n\nComplete.',
174
+ )
175
+
176
+ const result = await handleQuery(base)
177
+ const snap = result.data as QuerySnapshot
178
+
179
+ assert.equal(result.exitCode, 0)
180
+ assert.equal(snap.state.phase, 'complete')
181
+ assert.equal(snap.next.action, 'stop')
182
+ assert.equal(snap.next.reason, 'All milestones complete.')
183
+ })
162
184
  })
@@ -38,9 +38,6 @@ function createTempRepo(): string {
38
38
  run("git config user.email test@test.com", dir);
39
39
  run("git config user.name Test", dir);
40
40
  writeFileSync(join(dir, "README.md"), "# test\n");
41
- // Mirror production: .gsd/worktrees/ is gitignored so autoCommitDirtyState
42
- // doesn't pick up the worktrees directory as dirty state (#1127 fix).
43
- writeFileSync(join(dir, ".gitignore"), ".gsd/worktrees/\n");
44
41
  run("git add .", dir);
45
42
  run("git commit -m init", dir);
46
43
  run("git branch -M main", dir);
@@ -125,23 +122,23 @@ test("worktree swap on milestone transition: merge old, create new", () => {
125
122
 
126
123
  // ─── Verify the transition code path exists in auto.ts ──────────────────────
127
124
 
128
- test("auto.ts milestone transition block contains worktree lifecycle", () => {
125
+ test("auto-loop.ts milestone transition block contains worktree lifecycle", () => {
129
126
  const autoSrc = readFileSync(
130
- join(__dirname, "..", "auto.ts"),
127
+ join(__dirname, "..", "auto-loop.ts"),
131
128
  "utf-8",
132
129
  );
133
130
 
134
- // The fix adds worktree merge + create inside the milestone transition block
131
+ // The resolver handles worktree merge + enter inside the milestone transition block
135
132
  assert.ok(
136
133
  autoSrc.includes("Worktree lifecycle on milestone transition"),
137
- "auto.ts should contain the worktree lifecycle comment marker",
134
+ "auto-loop.ts should contain the worktree lifecycle comment marker",
138
135
  );
139
136
  assert.ok(
140
- autoSrc.includes("mergeMilestoneToMain") && autoSrc.includes("mid !== s.currentMilestoneId"),
141
- "auto.ts should call mergeMilestoneToMain during milestone transition",
137
+ autoSrc.includes("resolver.mergeAndExit") && autoSrc.includes("mid !== s.currentMilestoneId"),
138
+ "auto-loop.ts should call resolver.mergeAndExit during milestone transition",
142
139
  );
143
140
  assert.ok(
144
- autoSrc.includes("createAutoWorktree") && autoSrc.includes("Created auto-worktree for"),
145
- "auto.ts should create new worktree for incoming milestone",
141
+ autoSrc.includes("resolver.enterMilestone"),
142
+ "auto-loop.ts should call resolver.enterMilestone for incoming milestone",
146
143
  );
147
144
  });
@@ -277,13 +277,11 @@ test("index.ts tracks consecutive transient errors for escalating backoff", () =
277
277
  test("index.ts resets consecutive transient error counter on success", () => {
278
278
  const indexSource = readFileSync(join(__dirname, "..", "index.ts"), "utf-8");
279
279
 
280
- // After successful unit completion, the counter must be reset
281
- const marker = "successful unit completion";
282
- const successSection = indexSource.indexOf(marker);
283
- assert.ok(successSection > -1, "must have success section that clears network retries");
284
- const nearbyCode = indexSource.slice(Math.max(0, successSection - 100), successSection + 200);
280
+ // After successful unit completion, the counter must be reset.
281
+ // Use a regex across the success block so CRLF checkouts on Windows do not
282
+ // push the reset line outside a fixed substring window.
285
283
  assert.ok(
286
- nearbyCode.includes("consecutiveTransientErrors = 0"),
284
+ /consecutiveTransientErrors\s*=\s*0\s*;[\s\S]{0,250}successful unit completion/.test(indexSource),
287
285
  "consecutive transient error counter must be reset on successful unit completion (#1166)",
288
286
  );
289
287
  });
@@ -334,7 +334,7 @@ async function main(): Promise<void> {
334
334
  ].join('\n'),
335
335
  );
336
336
 
337
- // human-experience UAT should not dispatch
337
+ // human-experience UAT still dispatches, but auto-mode later pauses for manual review
338
338
  writeSliceFile(base, 'M001', 'S01', 'UAT', makeUatContent('human-experience'));
339
339
 
340
340
  const state = {
@@ -351,8 +351,8 @@ async function main(): Promise<void> {
351
351
  const result = await checkNeedsRunUat(base, 'M001', state as any, { uat_dispatch: true } as any);
352
352
  assertEq(
353
353
  result,
354
- null,
355
- 'human-experience UAT is skipped auto-mode only dispatches artifact-driven UATs',
354
+ { sliceId: 'S01', uatType: 'human-experience' },
355
+ 'human-experience UAT dispatches so auto-mode can pause for manual review',
356
356
  );
357
357
  } finally {
358
358
  cleanup(base);