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.
- package/dist/bundled-resource-path.d.ts +8 -0
- package/dist/bundled-resource-path.js +14 -0
- package/dist/headless-query.js +6 -6
- package/dist/resources/extensions/gsd/auto/session.js +27 -32
- package/dist/resources/extensions/gsd/auto-dashboard.js +29 -109
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +6 -1
- package/dist/resources/extensions/gsd/auto-dispatch.js +52 -81
- package/dist/resources/extensions/gsd/auto-loop.js +956 -0
- package/dist/resources/extensions/gsd/auto-observability.js +4 -2
- package/dist/resources/extensions/gsd/auto-post-unit.js +75 -185
- package/dist/resources/extensions/gsd/auto-prompts.js +133 -101
- package/dist/resources/extensions/gsd/auto-recovery.js +59 -97
- package/dist/resources/extensions/gsd/auto-start.js +330 -309
- package/dist/resources/extensions/gsd/auto-supervisor.js +5 -11
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +7 -7
- package/dist/resources/extensions/gsd/auto-timers.js +3 -4
- package/dist/resources/extensions/gsd/auto-verification.js +35 -73
- package/dist/resources/extensions/gsd/auto-worktree-sync.js +167 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +291 -126
- package/dist/resources/extensions/gsd/auto.js +283 -1013
- package/dist/resources/extensions/gsd/captures.js +10 -4
- package/dist/resources/extensions/gsd/dispatch-guard.js +7 -8
- package/dist/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/dist/resources/extensions/gsd/doctor-checks.js +3 -4
- package/dist/resources/extensions/gsd/git-service.js +1 -1
- package/dist/resources/extensions/gsd/gsd-db.js +296 -151
- package/dist/resources/extensions/gsd/index.js +92 -228
- package/dist/resources/extensions/gsd/post-unit-hooks.js +13 -13
- package/dist/resources/extensions/gsd/progress-score.js +61 -156
- package/dist/resources/extensions/gsd/quick.js +98 -122
- package/dist/resources/extensions/gsd/session-lock.js +13 -0
- package/dist/resources/extensions/gsd/templates/preferences.md +1 -0
- package/dist/resources/extensions/gsd/undo.js +43 -48
- package/dist/resources/extensions/gsd/unit-runtime.js +16 -15
- package/dist/resources/extensions/gsd/verification-evidence.js +0 -1
- package/dist/resources/extensions/gsd/verification-gate.js +6 -35
- package/dist/resources/extensions/gsd/worktree-command.js +30 -24
- package/dist/resources/extensions/gsd/worktree-manager.js +2 -3
- package/dist/resources/extensions/gsd/worktree-resolver.js +344 -0
- package/dist/resources/extensions/gsd/worktree.js +7 -44
- package/dist/tool-bootstrap.js +59 -11
- package/dist/worktree-cli.js +7 -7
- package/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +3630 -5483
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +735 -2588
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/src/models.generated.ts +1039 -2892
- package/packages/pi-coding-agent/package.json +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/extensions/gsd/auto/session.ts +47 -30
- package/src/resources/extensions/gsd/auto-dashboard.ts +28 -131
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +6 -1
- package/src/resources/extensions/gsd/auto-dispatch.ts +135 -91
- package/src/resources/extensions/gsd/auto-loop.ts +1665 -0
- package/src/resources/extensions/gsd/auto-observability.ts +4 -2
- package/src/resources/extensions/gsd/auto-post-unit.ts +85 -228
- package/src/resources/extensions/gsd/auto-prompts.ts +138 -109
- package/src/resources/extensions/gsd/auto-recovery.ts +124 -118
- package/src/resources/extensions/gsd/auto-start.ts +440 -354
- package/src/resources/extensions/gsd/auto-supervisor.ts +5 -12
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +8 -8
- package/src/resources/extensions/gsd/auto-timers.ts +3 -4
- package/src/resources/extensions/gsd/auto-verification.ts +76 -90
- package/src/resources/extensions/gsd/auto-worktree-sync.ts +204 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +389 -141
- package/src/resources/extensions/gsd/auto.ts +515 -1199
- package/src/resources/extensions/gsd/captures.ts +10 -4
- package/src/resources/extensions/gsd/dispatch-guard.ts +13 -9
- package/src/resources/extensions/gsd/docs/preferences-reference.md +25 -18
- package/src/resources/extensions/gsd/doctor-checks.ts +3 -4
- package/src/resources/extensions/gsd/git-service.ts +8 -1
- package/src/resources/extensions/gsd/gitignore.ts +4 -2
- package/src/resources/extensions/gsd/gsd-db.ts +375 -180
- package/src/resources/extensions/gsd/index.ts +104 -263
- package/src/resources/extensions/gsd/post-unit-hooks.ts +13 -13
- package/src/resources/extensions/gsd/progress-score.ts +65 -200
- package/src/resources/extensions/gsd/quick.ts +121 -125
- package/src/resources/extensions/gsd/session-lock.ts +11 -0
- package/src/resources/extensions/gsd/templates/preferences.md +1 -0
- package/src/resources/extensions/gsd/tests/agent-end-retry.test.ts +32 -59
- package/src/resources/extensions/gsd/tests/all-milestones-complete-merge.test.ts +75 -27
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/auto-lock-creation.test.ts +37 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +1458 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +8 -162
- package/src/resources/extensions/gsd/tests/auto-secrets-gate.test.ts +2 -108
- package/src/resources/extensions/gsd/tests/auto-session-encapsulation.test.ts +1 -3
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/auto-worktree.test.ts +58 -0
- package/src/resources/extensions/gsd/tests/dispatch-guard.test.ts +0 -55
- package/src/resources/extensions/gsd/tests/headless-query.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/milestone-transition-worktree.test.ts +8 -11
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +4 -6
- package/src/resources/extensions/gsd/tests/run-uat.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/session-lock-regression.test.ts +64 -0
- package/src/resources/extensions/gsd/tests/sidecar-queue.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/stale-worktree-cwd.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/token-profile.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/triage-dispatch.test.ts +6 -6
- package/src/resources/extensions/gsd/tests/undo.test.ts +6 -0
- package/src/resources/extensions/gsd/tests/verification-evidence.test.ts +24 -26
- package/src/resources/extensions/gsd/tests/verification-gate.test.ts +7 -201
- package/src/resources/extensions/gsd/tests/worktree-db-integration.test.ts +205 -0
- package/src/resources/extensions/gsd/tests/worktree-db.test.ts +442 -0
- package/src/resources/extensions/gsd/tests/worktree-e2e.test.ts +0 -3
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +705 -0
- package/src/resources/extensions/gsd/tests/worktree-sync-milestones.test.ts +57 -106
- package/src/resources/extensions/gsd/tests/worktree.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +43 -132
- package/src/resources/extensions/gsd/types.ts +90 -81
- package/src/resources/extensions/gsd/undo.ts +42 -46
- package/src/resources/extensions/gsd/unit-runtime.ts +14 -18
- package/src/resources/extensions/gsd/verification-evidence.ts +1 -3
- package/src/resources/extensions/gsd/verification-gate.ts +6 -39
- package/src/resources/extensions/gsd/worktree-command.ts +36 -24
- package/src/resources/extensions/gsd/worktree-manager.ts +2 -3
- package/src/resources/extensions/gsd/worktree-resolver.ts +485 -0
- package/src/resources/extensions/gsd/worktree.ts +7 -44
- package/dist/resources/extensions/gsd/auto-constants.js +0 -5
- package/dist/resources/extensions/gsd/auto-idempotency.js +0 -106
- package/dist/resources/extensions/gsd/auto-stuck-detection.js +0 -165
- package/dist/resources/extensions/gsd/mechanical-completion.js +0 -351
- package/src/resources/extensions/gsd/auto-constants.ts +0 -6
- package/src/resources/extensions/gsd/auto-idempotency.ts +0 -151
- package/src/resources/extensions/gsd/auto-stuck-detection.ts +0 -221
- package/src/resources/extensions/gsd/mechanical-completion.ts +0 -430
- package/src/resources/extensions/gsd/tests/auto-dispatch-loop.test.ts +0 -691
- package/src/resources/extensions/gsd/tests/auto-reentrancy-guard.test.ts +0 -127
- package/src/resources/extensions/gsd/tests/auto-skip-loop.test.ts +0 -123
- package/src/resources/extensions/gsd/tests/dispatch-stall-guard.test.ts +0 -126
- package/src/resources/extensions/gsd/tests/loop-regression.test.ts +0 -874
- package/src/resources/extensions/gsd/tests/mechanical-completion.test.ts +0 -356
- package/src/resources/extensions/gsd/tests/progress-score.test.ts +0 -206
- 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
|
|
532
|
-
//
|
|
533
|
-
//
|
|
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 —
|
|
559
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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("
|
|
141
|
-
"auto.ts should call
|
|
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("
|
|
145
|
-
"auto.ts should
|
|
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
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
355
|
-
'human-experience UAT
|
|
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);
|