gsd-pi 2.23.0 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +12 -3
- package/dist/headless.d.ts +4 -0
- package/dist/headless.js +118 -10
- package/dist/help-text.js +22 -7
- package/dist/resource-loader.js +64 -9
- package/dist/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/dist/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/dist/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/dist/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/dist/resources/extensions/gsd/auto.ts +123 -41
- package/dist/resources/extensions/gsd/commands.ts +176 -10
- package/dist/resources/extensions/gsd/complexity.ts +1 -0
- package/dist/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/dist/resources/extensions/gsd/doctor.ts +56 -11
- package/dist/resources/extensions/gsd/exit-command.ts +2 -2
- package/dist/resources/extensions/gsd/gitignore.ts +1 -0
- package/dist/resources/extensions/gsd/guided-flow.ts +75 -0
- package/dist/resources/extensions/gsd/index.ts +34 -1
- package/dist/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/dist/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/dist/resources/extensions/gsd/preferences.ts +65 -1
- package/dist/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/dist/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/dist/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/dist/resources/extensions/gsd/session-status-io.ts +197 -0
- package/dist/resources/extensions/gsd/state.ts +72 -30
- package/dist/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/dist/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/dist/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- package/dist/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/dist/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/dist/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/dist/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/dist/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/dist/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/dist/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/dist/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/dist/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/dist/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/dist/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/dist/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/dist/resources/extensions/gsd/types.ts +15 -1
- package/dist/resources/extensions/subagent/index.ts +5 -0
- package/dist/resources/extensions/subagent/worker-registry.ts +99 -0
- package/dist/update-check.d.ts +9 -0
- package/dist/update-check.js +97 -0
- package/package.json +6 -1
- package/packages/pi-ai/dist/providers/anthropic.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic.js +16 -7
- package/packages/pi-ai/dist/providers/anthropic.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +21 -9
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +12 -4
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js +12 -4
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/src/providers/anthropic.ts +21 -8
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +16 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +32 -17
- package/packages/pi-ai/src/providers/openai-completions.ts +16 -4
- package/packages/pi-ai/src/providers/openai-responses.ts +16 -4
- package/packages/pi-coding-agent/dist/core/agent-session.js +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +1 -1
- package/packages/pi-coding-agent/src/core/settings-manager.ts +2 -2
- package/scripts/postinstall.js +7 -109
- package/src/resources/extensions/gsd/auto-dispatch.ts +51 -2
- package/src/resources/extensions/gsd/auto-prompts.ts +73 -0
- package/src/resources/extensions/gsd/auto-recovery.ts +41 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +15 -3
- package/src/resources/extensions/gsd/auto.ts +123 -41
- package/src/resources/extensions/gsd/commands.ts +176 -10
- package/src/resources/extensions/gsd/complexity.ts +1 -0
- package/src/resources/extensions/gsd/dashboard-overlay.ts +38 -0
- package/src/resources/extensions/gsd/doctor.ts +56 -11
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow.ts +75 -0
- package/src/resources/extensions/gsd/index.ts +34 -1
- package/src/resources/extensions/gsd/parallel-eligibility.ts +233 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +156 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +496 -0
- package/src/resources/extensions/gsd/preferences.ts +65 -1
- package/src/resources/extensions/gsd/prompts/discuss-headless.md +86 -0
- package/src/resources/extensions/gsd/prompts/research-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +40 -61
- package/src/resources/extensions/gsd/provider-error-pause.ts +29 -2
- package/src/resources/extensions/gsd/session-status-io.ts +197 -0
- package/src/resources/extensions/gsd/state.ts +72 -30
- package/src/resources/extensions/gsd/tests/agent-end-provider-error.test.ts +81 -0
- package/src/resources/extensions/gsd/tests/auto-budget-alerts.test.ts +20 -3
- package/src/resources/extensions/gsd/tests/auto-preflight.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +202 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-milestone-merge.test.ts +34 -0
- package/src/resources/extensions/gsd/tests/complete-milestone.test.ts +8 -1
- package/src/resources/extensions/gsd/tests/derive-state-db.test.ts +9 -15
- package/src/resources/extensions/gsd/tests/derive-state-deps.test.ts +9 -0
- package/src/resources/extensions/gsd/tests/derive-state-draft.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/derive-state.test.ts +14 -0
- package/src/resources/extensions/gsd/tests/integration-mixed-milestones.test.ts +8 -0
- package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +5 -5
- package/src/resources/extensions/gsd/tests/parallel-orchestration.test.ts +656 -0
- package/src/resources/extensions/gsd/tests/parallel-workers-multi-milestone-e2e.test.ts +354 -0
- package/src/resources/extensions/gsd/tests/queue-reorder-e2e.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/validate-milestone.test.ts +316 -0
- package/src/resources/extensions/gsd/tests/worker-registry.test.ts +148 -0
- package/src/resources/extensions/gsd/types.ts +15 -1
- package/src/resources/extensions/subagent/index.ts +5 -0
- package/src/resources/extensions/subagent/worker-registry.ts +99 -0
|
@@ -17,6 +17,7 @@ writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-PLAN.md"), `
|
|
|
17
17
|
writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "tasks", "T01-SUMMARY.md"), `---\nid: T01\nparent: S01\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# T01: Old Task\n\n**Done**\n\n## What Happened\nDone.\n\n## Diagnostics\n- log\n`);
|
|
18
18
|
writeFileSync(join(gsd, "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"), `---\nid: S01\nparent: M001\nmilestone: M001\nprovides: []\nrequires: []\naffects: []\nkey_files: []\nkey_decisions: []\npatterns_established: []\nobservability_surfaces: []\ndrill_down_paths: []\nduration: 5m\nverification_result: passed\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# S01: Old Slice\n\n**Done**\n\n## What Happened\nDone.\n\n## Verification\nDone.\n\n## Deviations\nNone\n\n## Known Limitations\nNone\n\n## Follow-ups\nNone\n\n## Files Created/Modified\n- \`x\` — x\n\n## Forward Intelligence\n\n### What the next slice should know\n- x\n\n### What's fragile\n- x\n\n### Authoritative diagnostics\n- x\n\n### What assumptions changed\n- x\n`);
|
|
19
19
|
|
|
20
|
+
writeFileSync(join(gsd, "milestones", "M001", "M001-VALIDATION.md"), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.\n`);
|
|
20
21
|
writeFileSync(join(gsd, "milestones", "M001", "M001-SUMMARY.md"), `---\nid: M001\nstatus: complete\ncompleted_at: 2026-03-09T00:00:00Z\n---\n\n# M001: Historical\n\nComplete.\n`);
|
|
21
22
|
|
|
22
23
|
writeFileSync(join(gsd, "milestones", "M009", "M009-ROADMAP.md"), `# M009: Active\n\n## Slices\n- [ ] **S01: Active Slice** \`risk:low\` \`depends:[]\`\n > After this: active works\n`);
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
verifyExpectedArtifact,
|
|
11
11
|
diagnoseExpectedArtifact,
|
|
12
12
|
buildLoopRemediationSteps,
|
|
13
|
+
selfHealRuntimeRecords,
|
|
13
14
|
completedKeysPath,
|
|
14
15
|
persistCompletedKey,
|
|
15
16
|
removePersistedKey,
|
|
@@ -273,6 +274,68 @@ test("removePersistedKey is safe when file doesn't exist", () => {
|
|
|
273
274
|
}
|
|
274
275
|
});
|
|
275
276
|
|
|
277
|
+
// ─── Dual-load across worktree boundary (#769) ───────────────────────────
|
|
278
|
+
|
|
279
|
+
test("loadPersistedKeys unions keys from project root and worktree", () => {
|
|
280
|
+
// Simulate two separate .gsd directories (project root + worktree)
|
|
281
|
+
// each with a different set of completed keys. Loading from both
|
|
282
|
+
// into the same Set should produce the union.
|
|
283
|
+
const projectRoot = makeTmpBase();
|
|
284
|
+
const worktree = makeTmpBase();
|
|
285
|
+
try {
|
|
286
|
+
// Persist different keys in each location
|
|
287
|
+
persistCompletedKey(projectRoot, "execute-task/M001/S01/T01");
|
|
288
|
+
persistCompletedKey(projectRoot, "plan-slice/M001/S02");
|
|
289
|
+
|
|
290
|
+
persistCompletedKey(worktree, "execute-task/M001/S01/T02");
|
|
291
|
+
persistCompletedKey(worktree, "plan-slice/M001/S02"); // overlap
|
|
292
|
+
|
|
293
|
+
// Load from both into the same set (mimicking startup dual-load)
|
|
294
|
+
const keys = new Set<string>();
|
|
295
|
+
loadPersistedKeys(projectRoot, keys);
|
|
296
|
+
loadPersistedKeys(worktree, keys);
|
|
297
|
+
|
|
298
|
+
assert.ok(keys.has("execute-task/M001/S01/T01"), "key from project root");
|
|
299
|
+
assert.ok(keys.has("plan-slice/M001/S02"), "shared key");
|
|
300
|
+
assert.ok(keys.has("execute-task/M001/S01/T02"), "key from worktree");
|
|
301
|
+
assert.equal(keys.size, 3, "union should deduplicate overlapping keys");
|
|
302
|
+
} finally {
|
|
303
|
+
cleanup(projectRoot);
|
|
304
|
+
cleanup(worktree);
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("completed-units.json set-union merge produces correct result", () => {
|
|
309
|
+
// Verify that a manual set-union merge (as done in syncStateToProjectRoot)
|
|
310
|
+
// correctly merges two JSON arrays of keys.
|
|
311
|
+
const projectRoot = makeTmpBase();
|
|
312
|
+
const worktree = makeTmpBase();
|
|
313
|
+
try {
|
|
314
|
+
// Write keys to both locations
|
|
315
|
+
const prKeysFile = join(projectRoot, ".gsd", "completed-units.json");
|
|
316
|
+
const wtKeysFile = join(worktree, ".gsd", "completed-units.json");
|
|
317
|
+
|
|
318
|
+
writeFileSync(prKeysFile, JSON.stringify(["a", "b"]));
|
|
319
|
+
writeFileSync(wtKeysFile, JSON.stringify(["b", "c", "d"]));
|
|
320
|
+
|
|
321
|
+
// Perform the same merge logic used in syncStateToProjectRoot
|
|
322
|
+
const srcKeys: string[] = JSON.parse(readFileSync(wtKeysFile, "utf8"));
|
|
323
|
+
let dstKeys: string[] = [];
|
|
324
|
+
if (existsSync(prKeysFile)) {
|
|
325
|
+
dstKeys = JSON.parse(readFileSync(prKeysFile, "utf8"));
|
|
326
|
+
}
|
|
327
|
+
const merged = [...new Set([...dstKeys, ...srcKeys])];
|
|
328
|
+
writeFileSync(prKeysFile, JSON.stringify(merged, null, 2));
|
|
329
|
+
|
|
330
|
+
// Verify the merged result
|
|
331
|
+
const result: string[] = JSON.parse(readFileSync(prKeysFile, "utf8"));
|
|
332
|
+
assert.deepStrictEqual(result.sort(), ["a", "b", "c", "d"]);
|
|
333
|
+
} finally {
|
|
334
|
+
cleanup(projectRoot);
|
|
335
|
+
cleanup(worktree);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
|
|
276
339
|
// ─── verifyExpectedArtifact: parse cache collision regression ─────────────
|
|
277
340
|
|
|
278
341
|
test("verifyExpectedArtifact detects roadmap [x] change despite parse cache", () => {
|
|
@@ -343,7 +406,8 @@ test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => {
|
|
|
343
406
|
const base = makeTmpBase();
|
|
344
407
|
try {
|
|
345
408
|
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
346
|
-
|
|
409
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
410
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
347
411
|
writeFileSync(join(sliceDir, "S01-PLAN.md"), [
|
|
348
412
|
"# S01: Test Slice",
|
|
349
413
|
"",
|
|
@@ -352,6 +416,8 @@ test("verifyExpectedArtifact accepts plan-slice with actual tasks", () => {
|
|
|
352
416
|
"- [ ] **T01: Implement feature** `est:2h`",
|
|
353
417
|
"- [ ] **T02: Write tests** `est:1h`",
|
|
354
418
|
].join("\n"));
|
|
419
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
|
|
420
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
|
|
355
421
|
assert.strictEqual(
|
|
356
422
|
verifyExpectedArtifact("plan-slice", "M001/S01", base),
|
|
357
423
|
true,
|
|
@@ -366,7 +432,8 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
|
|
|
366
432
|
const base = makeTmpBase();
|
|
367
433
|
try {
|
|
368
434
|
const sliceDir = join(base, ".gsd", "milestones", "M001", "slices", "S01");
|
|
369
|
-
|
|
435
|
+
const tasksDir = join(sliceDir, "tasks");
|
|
436
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
370
437
|
writeFileSync(join(sliceDir, "S01-PLAN.md"), [
|
|
371
438
|
"# S01: Test Slice",
|
|
372
439
|
"",
|
|
@@ -375,6 +442,8 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
|
|
|
375
442
|
"- [x] **T01: Implement feature** `est:2h`",
|
|
376
443
|
"- [ ] **T02: Write tests** `est:1h`",
|
|
377
444
|
].join("\n"));
|
|
445
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan");
|
|
446
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan");
|
|
378
447
|
assert.strictEqual(
|
|
379
448
|
verifyExpectedArtifact("plan-slice", "M001/S01", base),
|
|
380
449
|
true,
|
|
@@ -384,3 +453,134 @@ test("verifyExpectedArtifact accepts plan-slice with completed tasks", () => {
|
|
|
384
453
|
cleanup(base);
|
|
385
454
|
}
|
|
386
455
|
});
|
|
456
|
+
|
|
457
|
+
// ─── verifyExpectedArtifact: plan-slice task plan check (#739) ────────────
|
|
458
|
+
|
|
459
|
+
test("verifyExpectedArtifact plan-slice passes when all task plan files exist", () => {
|
|
460
|
+
const base = makeTmpBase();
|
|
461
|
+
try {
|
|
462
|
+
const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
463
|
+
const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
464
|
+
const planContent = [
|
|
465
|
+
"# S01: Test Slice",
|
|
466
|
+
"",
|
|
467
|
+
"## Tasks",
|
|
468
|
+
"",
|
|
469
|
+
"- [ ] **T01: First task** `est:1h`",
|
|
470
|
+
"- [ ] **T02: Second task** `est:2h`",
|
|
471
|
+
].join("\n");
|
|
472
|
+
writeFileSync(planPath, planContent);
|
|
473
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
|
|
474
|
+
writeFileSync(join(tasksDir, "T02-PLAN.md"), "# T02 Plan\n\nDo the other thing.");
|
|
475
|
+
|
|
476
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
477
|
+
assert.equal(result, true, "should pass when all task plan files exist");
|
|
478
|
+
} finally {
|
|
479
|
+
cleanup(base);
|
|
480
|
+
}
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("verifyExpectedArtifact plan-slice fails when a task plan file is missing (#739)", () => {
|
|
484
|
+
const base = makeTmpBase();
|
|
485
|
+
try {
|
|
486
|
+
const tasksDir = join(base, ".gsd", "milestones", "M001", "slices", "S01", "tasks");
|
|
487
|
+
const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
488
|
+
const planContent = [
|
|
489
|
+
"# S01: Test Slice",
|
|
490
|
+
"",
|
|
491
|
+
"## Tasks",
|
|
492
|
+
"",
|
|
493
|
+
"- [ ] **T01: First task** `est:1h`",
|
|
494
|
+
"- [ ] **T02: Second task** `est:2h`",
|
|
495
|
+
].join("\n");
|
|
496
|
+
writeFileSync(planPath, planContent);
|
|
497
|
+
// Only write T01-PLAN.md — T02 is missing
|
|
498
|
+
writeFileSync(join(tasksDir, "T01-PLAN.md"), "# T01 Plan\n\nDo the thing.");
|
|
499
|
+
|
|
500
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
501
|
+
assert.equal(result, false, "should fail when T02-PLAN.md is missing");
|
|
502
|
+
} finally {
|
|
503
|
+
cleanup(base);
|
|
504
|
+
}
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("verifyExpectedArtifact plan-slice fails for plan with no tasks (#699)", () => {
|
|
508
|
+
const base = makeTmpBase();
|
|
509
|
+
try {
|
|
510
|
+
const planPath = join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-PLAN.md");
|
|
511
|
+
const planContent = [
|
|
512
|
+
"# S01: Test Slice",
|
|
513
|
+
"",
|
|
514
|
+
"## Goal",
|
|
515
|
+
"",
|
|
516
|
+
"Just some documentation updates, no tasks.",
|
|
517
|
+
].join("\n");
|
|
518
|
+
writeFileSync(planPath, planContent);
|
|
519
|
+
|
|
520
|
+
const result = verifyExpectedArtifact("plan-slice", "M001/S01", base);
|
|
521
|
+
assert.equal(result, false, "should fail when plan has no task entries (empty scaffold, #699)");
|
|
522
|
+
} finally {
|
|
523
|
+
cleanup(base);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// ─── selfHealRuntimeRecords — worktree base path (#769) ──────────────────
|
|
528
|
+
|
|
529
|
+
test("selfHealRuntimeRecords clears stale record when artifact exists at worktree base (#769)", async () => {
|
|
530
|
+
// Simulate worktree layout: the runtime record AND the artifact both live
|
|
531
|
+
// under the worktree's .gsd/, not the main project root.
|
|
532
|
+
const worktreeBase = makeTmpBase();
|
|
533
|
+
const mainBase = makeTmpBase();
|
|
534
|
+
try {
|
|
535
|
+
const { writeUnitRuntimeRecord, readUnitRuntimeRecord } = await import("../unit-runtime.ts");
|
|
536
|
+
|
|
537
|
+
// Write a stale runtime record in the worktree .gsd/runtime/units/
|
|
538
|
+
writeUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01", Date.now() - 7200_000, {
|
|
539
|
+
phase: "dispatched",
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
// Write the UAT result artifact in the worktree .gsd/milestones/
|
|
543
|
+
const uatPath = join(worktreeBase, ".gsd", "milestones", "M001", "slices", "S01", "S01-UAT-RESULT.md");
|
|
544
|
+
writeFileSync(uatPath, "---\nresult: pass\n---\n# UAT Result\nAll tests passed.\n");
|
|
545
|
+
|
|
546
|
+
// Verify the runtime record exists before heal
|
|
547
|
+
const before = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
|
|
548
|
+
assert.ok(before, "runtime record should exist before heal");
|
|
549
|
+
|
|
550
|
+
// Mock ExtensionContext with minimal notify
|
|
551
|
+
const notifications: string[] = [];
|
|
552
|
+
const mockCtx = {
|
|
553
|
+
ui: { notify: (msg: string) => { notifications.push(msg); } },
|
|
554
|
+
} as any;
|
|
555
|
+
|
|
556
|
+
// Call selfHeal with worktreeBase — this is the fix: using the worktree path
|
|
557
|
+
// so both the runtime record and artifact are found
|
|
558
|
+
const completedKeys = new Set<string>();
|
|
559
|
+
await selfHealRuntimeRecords(worktreeBase, mockCtx, completedKeys);
|
|
560
|
+
|
|
561
|
+
// The stale record should be cleared
|
|
562
|
+
const after = readUnitRuntimeRecord(worktreeBase, "run-uat", "M001/S01");
|
|
563
|
+
assert.equal(after, null, "runtime record should be cleared after heal");
|
|
564
|
+
|
|
565
|
+
// The completion key should be persisted
|
|
566
|
+
assert.ok(completedKeys.has("run-uat/M001/S01"), "completion key should be added");
|
|
567
|
+
assert.ok(notifications.some(n => n.includes("Self-heal")), "should emit self-heal notification");
|
|
568
|
+
|
|
569
|
+
// Now verify that calling with mainBase does NOT find/clear anything (the old bug)
|
|
570
|
+
// Write a stale record at mainBase but NO artifact there
|
|
571
|
+
writeUnitRuntimeRecord(mainBase, "run-uat", "M001/S01", Date.now() - 7200_000, {
|
|
572
|
+
phase: "dispatched",
|
|
573
|
+
});
|
|
574
|
+
const mainKeys = new Set<string>();
|
|
575
|
+
await selfHealRuntimeRecords(mainBase, mockCtx, mainKeys);
|
|
576
|
+
|
|
577
|
+
// The record at mainBase should be cleared by the stale timeout (>1h),
|
|
578
|
+
// but the completion key should NOT be set (artifact doesn't exist at mainBase)
|
|
579
|
+
const afterMain = readUnitRuntimeRecord(mainBase, "run-uat", "M001/S01");
|
|
580
|
+
assert.equal(afterMain, null, "stale record at main base should be cleared by timeout");
|
|
581
|
+
assert.ok(!mainKeys.has("run-uat/M001/S01"), "completion key should NOT be set when artifact is missing");
|
|
582
|
+
} finally {
|
|
583
|
+
cleanup(worktreeBase);
|
|
584
|
+
cleanup(mainBase);
|
|
585
|
+
}
|
|
586
|
+
});
|
|
@@ -290,6 +290,40 @@ async function main(): Promise<void> {
|
|
|
290
290
|
assertTrue(existsSync(join(repo, "feature.ts")), "feature.ts merged to main");
|
|
291
291
|
}
|
|
292
292
|
|
|
293
|
+
// ─── Test 6: Skip checkout when main already current (#757) ───────
|
|
294
|
+
console.log("\n=== skip checkout when main already current (#757) ===");
|
|
295
|
+
{
|
|
296
|
+
const repo = freshRepo();
|
|
297
|
+
const wtPath = createAutoWorktree(repo, "M060");
|
|
298
|
+
|
|
299
|
+
addSliceToMilestone(repo, wtPath, "M060", "S01", "Skip checkout test", [
|
|
300
|
+
{ file: "skip-checkout.ts", content: "export const skip = true;\n", message: "add skip-checkout" },
|
|
301
|
+
]);
|
|
302
|
+
|
|
303
|
+
const roadmap = makeRoadmap("M060", "Skip checkout verification", [
|
|
304
|
+
{ id: "S01", title: "Skip checkout test" },
|
|
305
|
+
]);
|
|
306
|
+
|
|
307
|
+
// Verify main is already checked out at repo root (worktree default)
|
|
308
|
+
const branchAtRoot = run("git rev-parse --abbrev-ref HEAD", repo);
|
|
309
|
+
assertEq(branchAtRoot, "main", "main is already checked out at project root");
|
|
310
|
+
|
|
311
|
+
// mergeMilestoneToMain should succeed without attempting to checkout main
|
|
312
|
+
// (which would fail with "already used by worktree" error)
|
|
313
|
+
let threw = false;
|
|
314
|
+
try {
|
|
315
|
+
const result = mergeMilestoneToMain(repo, "M060", roadmap);
|
|
316
|
+
assertTrue(result.commitMessage.includes("feat(M060)"), "merge commit created");
|
|
317
|
+
} catch (err) {
|
|
318
|
+
threw = true;
|
|
319
|
+
console.error("Unexpected error:", err);
|
|
320
|
+
}
|
|
321
|
+
assertTrue(!threw, "does not fail when main is already checked out at project root");
|
|
322
|
+
|
|
323
|
+
// Verify the merge actually happened
|
|
324
|
+
assertTrue(existsSync(join(repo, "skip-checkout.ts")), "skip-checkout.ts merged to main");
|
|
325
|
+
}
|
|
326
|
+
|
|
293
327
|
} finally {
|
|
294
328
|
process.chdir(savedCwd);
|
|
295
329
|
for (const d of tempDirs) {
|
|
@@ -45,6 +45,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
|
|
|
45
45
|
writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function writeMilestoneValidation(base: string, mid: string, verdict: string = "pass"): void {
|
|
49
|
+
const dir = join(base, ".gsd", "milestones", mid);
|
|
50
|
+
mkdirSync(dir, { recursive: true });
|
|
51
|
+
writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: ${verdict}\nremediation_round: 0\n---\n\n# Validation\nValidated.`);
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
function cleanup(base: string): void {
|
|
49
55
|
rmSync(base, { recursive: true, force: true });
|
|
50
56
|
}
|
|
@@ -176,7 +182,8 @@ async function main(): Promise<void> {
|
|
|
176
182
|
const roadmap = parseRoadmap(roadmapContent!);
|
|
177
183
|
assertTrue(isMilestoneComplete(roadmap), "isMilestoneComplete returns true when all slices are [x]");
|
|
178
184
|
|
|
179
|
-
// Verify deriveState returns completing-milestone phase
|
|
185
|
+
// Verify deriveState returns completing-milestone phase (with validation already done)
|
|
186
|
+
writeMilestoneValidation(base, "M001");
|
|
180
187
|
const state = await deriveState(base);
|
|
181
188
|
assertEq(state.phase, "completing-milestone", "deriveState returns completing-milestone when all slices done, no summary");
|
|
182
189
|
assertEq(state.activeMilestone?.id, "M001", "active milestone is M001");
|
|
@@ -248,31 +248,24 @@ async function main(): Promise<void> {
|
|
|
248
248
|
}
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
-
// ─── Test 5: Requirements counting from DB content
|
|
252
|
-
console.log('\n=== derive-state-db: requirements from
|
|
251
|
+
// ─── Test 5: Requirements counting from disk (DB no longer used for content) ─
|
|
252
|
+
console.log('\n=== derive-state-db: requirements from disk content ===');
|
|
253
253
|
{
|
|
254
254
|
const base = createFixtureBase();
|
|
255
255
|
try {
|
|
256
256
|
// Write minimal milestone dir (needed for milestone discovery)
|
|
257
257
|
mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
openDatabase(':memory:');
|
|
261
|
-
insertArtifactRow('REQUIREMENTS.md', REQUIREMENTS_CONTENT, {
|
|
262
|
-
artifact_type: 'requirements',
|
|
263
|
-
});
|
|
258
|
+
// Write REQUIREMENTS.md to disk (DB content is no longer used by deriveState)
|
|
259
|
+
writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
|
|
264
260
|
|
|
265
261
|
invalidateStateCache();
|
|
266
262
|
const state = await deriveState(base);
|
|
267
263
|
|
|
268
|
-
// Requirements should come from
|
|
269
|
-
assertEq(state.requirements?.active, 2, 'req-from-
|
|
270
|
-
assertEq(state.requirements?.validated, 1, 'req-from-
|
|
271
|
-
assertEq(state.requirements?.total, 3, 'req-from-
|
|
272
|
-
|
|
273
|
-
closeDatabase();
|
|
264
|
+
// Requirements should come from disk
|
|
265
|
+
assertEq(state.requirements?.active, 2, 'req-from-disk: requirements.active = 2');
|
|
266
|
+
assertEq(state.requirements?.validated, 1, 'req-from-disk: requirements.validated = 1');
|
|
267
|
+
assertEq(state.requirements?.total, 3, 'req-from-disk: requirements.total = 3');
|
|
274
268
|
} finally {
|
|
275
|
-
closeDatabase();
|
|
276
269
|
cleanup(base);
|
|
277
270
|
}
|
|
278
271
|
}
|
|
@@ -310,6 +303,7 @@ async function main(): Promise<void> {
|
|
|
310
303
|
mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
|
|
311
304
|
mkdirSync(join(base, '.gsd', 'milestones', 'M002'), { recursive: true });
|
|
312
305
|
writeFile(base, 'milestones/M001/M001-ROADMAP.md', completedRoadmap);
|
|
306
|
+
writeFile(base, 'milestones/M001/M001-VALIDATION.md', `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
|
|
313
307
|
writeFile(base, 'milestones/M001/M001-SUMMARY.md', summaryContent);
|
|
314
308
|
writeFile(base, 'milestones/M002/M002-ROADMAP.md', activeRoadmap);
|
|
315
309
|
|
|
@@ -26,6 +26,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
|
|
|
26
26
|
writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
+
function writeMilestoneValidation(base: string, mid: string): void {
|
|
30
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
31
|
+
mkdirSync(dir, { recursive: true });
|
|
32
|
+
writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
36
|
* Creates M00x-CONTEXT.md with a valid YAML frontmatter block.
|
|
31
37
|
* frontmatter is the raw YAML lines between the --- delimiters.
|
|
@@ -120,6 +126,7 @@ async function main(): Promise<void> {
|
|
|
120
126
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
121
127
|
> After this: Done.
|
|
122
128
|
`);
|
|
129
|
+
writeMilestoneValidation(base, 'M001');
|
|
123
130
|
writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nFirst milestone is complete.');
|
|
124
131
|
|
|
125
132
|
// M002: depends on M001, now unblocked
|
|
@@ -252,6 +259,7 @@ async function main(): Promise<void> {
|
|
|
252
259
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
253
260
|
> After this: Done.
|
|
254
261
|
`);
|
|
262
|
+
writeMilestoneValidation(base, 'M002');
|
|
255
263
|
writeMilestoneSummary(base, 'M002', '# M002 Summary\n\nSecond milestone is complete.');
|
|
256
264
|
|
|
257
265
|
const state = await deriveState(base);
|
|
@@ -321,6 +329,7 @@ async function main(): Promise<void> {
|
|
|
321
329
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
322
330
|
> After this: Done.
|
|
323
331
|
`);
|
|
332
|
+
writeMilestoneValidation(base, 'M004-0zjrg0');
|
|
324
333
|
writeMilestoneSummary(base, 'M004-0zjrg0', '# M004-0zjrg0 Summary\n\nComplete.');
|
|
325
334
|
|
|
326
335
|
// M005-b0m2hl: depends on M004-0zjrg0 (lowercase hex suffix)
|
|
@@ -54,6 +54,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
|
|
|
54
54
|
writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
function writeMilestoneValidation(base: string, mid: string): void {
|
|
58
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
59
|
+
mkdirSync(dir, { recursive: true });
|
|
60
|
+
writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
|
|
61
|
+
}
|
|
62
|
+
|
|
57
63
|
function cleanup(base: string): void {
|
|
58
64
|
rmSync(base, { recursive: true, force: true });
|
|
59
65
|
}
|
|
@@ -143,6 +149,7 @@ async function main(): Promise<void> {
|
|
|
143
149
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
144
150
|
> After this: Done.
|
|
145
151
|
`);
|
|
152
|
+
writeMilestoneValidation(base, 'M001');
|
|
146
153
|
writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nFirst milestone complete.');
|
|
147
154
|
|
|
148
155
|
// M002: only CONTEXT-DRAFT.md
|
|
@@ -178,6 +185,7 @@ async function main(): Promise<void> {
|
|
|
178
185
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
179
186
|
> After this: Done.
|
|
180
187
|
`);
|
|
188
|
+
writeMilestoneValidation(base, 'M001');
|
|
181
189
|
writeMilestoneSummary(base, 'M001', '# M001 Summary\n\nComplete.');
|
|
182
190
|
|
|
183
191
|
// M002: draft only — should become active with needs-discussion
|
|
@@ -38,6 +38,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
|
|
|
38
38
|
writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function writeMilestoneValidation(base: string, mid: string, verdict: string = 'pass'): void {
|
|
42
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
43
|
+
mkdirSync(dir, { recursive: true });
|
|
44
|
+
writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: ${verdict}\nremediation_round: 0\n---\n\n# Validation\nValidated.`);
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
function writeRequirements(base: string, content: string): void {
|
|
42
48
|
writeFileSync(join(base, '.gsd', 'REQUIREMENTS.md'), content);
|
|
43
49
|
}
|
|
@@ -285,6 +291,7 @@ Continue from step 2.
|
|
|
285
291
|
> After this: Done.
|
|
286
292
|
`);
|
|
287
293
|
|
|
294
|
+
writeMilestoneValidation(base, 'M001');
|
|
288
295
|
writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone complete.`);
|
|
289
296
|
|
|
290
297
|
const state = await deriveState(base);
|
|
@@ -381,6 +388,7 @@ Continue from step 2.
|
|
|
381
388
|
> After this: Done.
|
|
382
389
|
`);
|
|
383
390
|
|
|
391
|
+
writeMilestoneValidation(base, 'M001');
|
|
384
392
|
writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
|
|
385
393
|
|
|
386
394
|
// M002: active (has incomplete slices)
|
|
@@ -486,6 +494,8 @@ Continue from step 2.
|
|
|
486
494
|
> After this: S02 complete.
|
|
487
495
|
`);
|
|
488
496
|
|
|
497
|
+
writeMilestoneValidation(base, 'M001');
|
|
498
|
+
|
|
489
499
|
const state = await deriveState(base);
|
|
490
500
|
|
|
491
501
|
assertEq(state.phase, 'completing-milestone', 'completing-ms: phase is completing-milestone');
|
|
@@ -521,6 +531,7 @@ Continue from step 2.
|
|
|
521
531
|
> After this: Done.
|
|
522
532
|
`);
|
|
523
533
|
|
|
534
|
+
writeMilestoneValidation(base, 'M001');
|
|
524
535
|
writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nMilestone is complete.`);
|
|
525
536
|
|
|
526
537
|
const state = await deriveState(base);
|
|
@@ -550,6 +561,7 @@ Continue from step 2.
|
|
|
550
561
|
- [x] **S01: Done** \`risk:low\` \`depends:[]\`
|
|
551
562
|
> After this: Done.
|
|
552
563
|
`);
|
|
564
|
+
writeMilestoneValidation(base, 'M001');
|
|
553
565
|
writeMilestoneSummary(base, 'M001', `# M001 Summary\n\nFirst milestone complete.`);
|
|
554
566
|
|
|
555
567
|
// M002: all slices done, no summary → completing-milestone
|
|
@@ -566,6 +578,8 @@ Continue from step 2.
|
|
|
566
578
|
> After this: Done.
|
|
567
579
|
`);
|
|
568
580
|
|
|
581
|
+
writeMilestoneValidation(base, 'M002');
|
|
582
|
+
|
|
569
583
|
// M003: has incomplete slices → pending (M002 is active)
|
|
570
584
|
writeRoadmap(base, 'M003', `# M003: Third Milestone
|
|
571
585
|
|
|
@@ -51,6 +51,12 @@ function writeMilestoneSummary(base: string, mid: string, content: string): void
|
|
|
51
51
|
writeFileSync(join(dir, `${mid}-SUMMARY.md`), content);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
function writeMilestoneValidation(base: string, mid: string): void {
|
|
55
|
+
const dir = join(base, '.gsd', 'milestones', mid);
|
|
56
|
+
mkdirSync(dir, { recursive: true });
|
|
57
|
+
writeFileSync(join(dir, `${mid}-VALIDATION.md`), `---\nverdict: pass\nremediation_round: 0\n---\n\n# Validation\nPassed.`);
|
|
58
|
+
}
|
|
59
|
+
|
|
54
60
|
function cleanup(base: string): void {
|
|
55
61
|
rmSync(base, { recursive: true, force: true });
|
|
56
62
|
}
|
|
@@ -166,6 +172,7 @@ async function main(): Promise<void> {
|
|
|
166
172
|
Did it.
|
|
167
173
|
`);
|
|
168
174
|
|
|
175
|
+
writeMilestoneValidation(base, 'M001');
|
|
169
176
|
writeMilestoneSummary(base, 'M001', `# M001: Legacy Feature Summary
|
|
170
177
|
|
|
171
178
|
**One-liner summary**
|
|
@@ -265,6 +272,7 @@ Everything worked.
|
|
|
265
272
|
Did it.
|
|
266
273
|
`);
|
|
267
274
|
|
|
275
|
+
writeMilestoneValidation(base, 'M001');
|
|
268
276
|
writeMilestoneSummary(base, 'M001', `# M001: Legacy Feature Summary
|
|
269
277
|
|
|
270
278
|
**One-liner summary**
|
|
@@ -263,12 +263,12 @@ async function main(): Promise<void> {
|
|
|
263
263
|
// No REQUIREMENTS.md since empty requirements
|
|
264
264
|
assertTrue(!existsSync(join(base, '.gsd', 'REQUIREMENTS.md')), 'complete: REQUIREMENTS.md NOT written (empty)');
|
|
265
265
|
|
|
266
|
-
// deriveState: all slices done, all tasks done — needs milestone summary
|
|
267
|
-
// Without
|
|
266
|
+
// deriveState: all slices done, all tasks done — needs validation then milestone summary
|
|
267
|
+
// Without VALIDATION file, it should be 'validating-milestone'
|
|
268
268
|
const state = await deriveState(base);
|
|
269
|
-
// All slices are done in roadmap.
|
|
270
|
-
// deriveState should return '
|
|
271
|
-
assertEq(state.phase, '
|
|
269
|
+
// All slices are done in roadmap. No VALIDATION or SUMMARY exists.
|
|
270
|
+
// deriveState should return 'validating-milestone' since validation gate precedes completion.
|
|
271
|
+
assertEq(state.phase, 'validating-milestone', 'complete: deriveState phase is validating-milestone');
|
|
272
272
|
assertTrue(state.activeMilestone !== null, 'complete: deriveState has activeMilestone');
|
|
273
273
|
assertEq(state.activeMilestone!.id, 'M001', 'complete: deriveState activeMilestone is M001');
|
|
274
274
|
|