@opengsd/gsd-pi 1.4.0-dev.5fc22c6f → 1.4.0-dev.c18009cd
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/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +58 -4
- package/dist/resources/extensions/gsd/artifact-verification.js +17 -5
- package/dist/resources/extensions/gsd/auto/closeout.js +45 -42
- package/dist/resources/extensions/gsd/auto/finalize.js +9 -0
- package/dist/resources/extensions/gsd/auto/orchestrator.js +1 -1
- package/dist/resources/extensions/gsd/auto/run-unit.js +5 -2
- package/dist/resources/extensions/gsd/auto/schedule-wakeup.js +3 -0
- package/dist/resources/extensions/gsd/auto.js +10 -3
- package/dist/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.js +58 -9
- package/dist/resources/extensions/gsd/doctor-engine-checks.js +174 -16
- package/dist/resources/extensions/gsd/gsd-db.js +52 -0
- package/dist/resources/extensions/gsd/migrate-external.js +13 -4
- package/dist/resources/extensions/gsd/milestone-merge-transaction.js +3 -3
- package/dist/resources/extensions/gsd/repo-identity.js +16 -0
- package/dist/resources/extensions/gsd/root-write-leak-guard.js +4 -2
- package/dist/resources/extensions/gsd/unit-registry.js +27 -2
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +67 -5
- package/dist/tsconfig.extensions.tsbuildinfo +1 -1
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +6 -6
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +6 -6
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/integrations/hermes/CONTEXT.md +47 -0
- package/integrations/hermes/README.md +21 -0
- package/integrations/hermes/docs/issue-1162-grilling.md +197 -0
- package/integrations/hermes/docs/setup.md +4 -0
- package/integrations/hermes/open_gsd_hermes/commands.py +141 -0
- package/integrations/hermes/open_gsd_hermes/config.py +2 -0
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +594 -31
- package/integrations/hermes/open_gsd_hermes/notifications.py +10 -1
- package/integrations/hermes/plugin.yaml +4 -0
- package/integrations/hermes/tests/test_config.py +6 -0
- package/integrations/hermes/tests/test_gsd_client_cache.py +3 -0
- package/integrations/hermes/tests/test_gsd_client_execute.py +35 -0
- package/integrations/hermes/tests/test_gsd_client_process_cwd.py +22 -0
- package/integrations/hermes/tests/test_gsd_client_timeout.py +98 -0
- package/integrations/hermes/tests/test_milestone_client.py +447 -0
- package/integrations/hermes/tests/test_new_milestone_command.py +313 -0
- package/integrations/hermes/tests/test_supervisor_fsm.py +40 -0
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/session-manager.js +1 -1
- package/packages/daemon/dist/session-manager.js.map +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.d.ts.map +1 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js +4 -1
- package/packages/gsd-agent-core/dist/session/agent-session-navigation.js.map +1 -1
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/server.d.ts +18 -5
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +46 -5
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/dist/session-manager.js +1 -1
- package/packages/mcp-server/dist/session-manager.js.map +1 -1
- package/packages/mcp-server/package.json +4 -4
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/session-manager-types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager-types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +3 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +64 -3
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +212 -39
- package/src/resources/extensions/gsd/artifact-verification.ts +16 -4
- package/src/resources/extensions/gsd/auto/closeout.ts +54 -56
- package/src/resources/extensions/gsd/auto/finalize.ts +8 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -0
- package/src/resources/extensions/gsd/auto/orchestrator.ts +1 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +5 -2
- package/src/resources/extensions/gsd/auto/schedule-wakeup.ts +8 -0
- package/src/resources/extensions/gsd/auto.ts +9 -3
- package/src/resources/extensions/gsd/bootstrap/schedule-wakeup-tool.ts +80 -12
- package/src/resources/extensions/gsd/doctor-engine-checks.ts +230 -21
- package/src/resources/extensions/gsd/gsd-db.ts +72 -0
- package/src/resources/extensions/gsd/migrate-external.ts +14 -4
- package/src/resources/extensions/gsd/milestone-merge-transaction.ts +4 -4
- package/src/resources/extensions/gsd/repo-identity.ts +17 -0
- package/src/resources/extensions/gsd/root-write-leak-guard.ts +3 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +131 -5
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +43 -1
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +54 -0
- package/src/resources/extensions/gsd/tests/doctor-scope-db-unavailable.test.ts +335 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/migrate-external-worktree.test.ts +116 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +152 -22
- package/src/resources/extensions/gsd/tests/milestone-merge-transaction.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +50 -6
- package/src/resources/extensions/gsd/tests/root-write-leak-guard.test.ts +31 -9
- package/src/resources/extensions/gsd/tests/schedule-wakeup-tool.test.ts +167 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/unit-registry.test.ts +56 -0
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +4 -4
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +2 -2
- package/src/resources/extensions/gsd/unit-registry.ts +34 -2
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +135 -11
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{kIEFsCryyGy6RzifULbJl → L2UMqXELiDH3oz-7yguXp}/_ssgManifest.js +0 -0
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
insertMilestone,
|
|
8
8
|
insertSlice,
|
|
9
9
|
insertTask,
|
|
10
|
+
getTask,
|
|
10
11
|
openDatabase,
|
|
11
12
|
} from "../gsd-db.ts";
|
|
12
13
|
import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
@@ -335,6 +336,340 @@ test("checkEngineHealth reports artifact rows whose files are missing on disk",
|
|
|
335
336
|
assert.equal(missing.fixable, false);
|
|
336
337
|
});
|
|
337
338
|
|
|
339
|
+
test("checkEngineHealth resolves escaped .gsd artifact rows against the project .gsd directory", async (t) => {
|
|
340
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-escaped-artifact-"));
|
|
341
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
342
|
+
|
|
343
|
+
const gsdDir = join(base, ".gsd");
|
|
344
|
+
const artifactPath = "phases/01-m001/01-01-ASSESSMENT.md";
|
|
345
|
+
mkdirSync(join(gsdDir, "phases", "01-m001"), { recursive: true });
|
|
346
|
+
writeFileSync(join(gsdDir, artifactPath), "# Assessment\n", "utf-8");
|
|
347
|
+
|
|
348
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
349
|
+
insertArtifact({
|
|
350
|
+
path: `../../../Documents/Projects/project/.gsd/${artifactPath}`,
|
|
351
|
+
artifact_type: "ASSESSMENT",
|
|
352
|
+
milestone_id: "M001",
|
|
353
|
+
slice_id: "S01",
|
|
354
|
+
task_id: null,
|
|
355
|
+
full_content: "# Assessment\n",
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
const issues: any[] = [];
|
|
359
|
+
await checkEngineHealth(base, issues, []);
|
|
360
|
+
|
|
361
|
+
assert.equal(
|
|
362
|
+
issues.some((issue) => issue.code === "artifact_file_missing"),
|
|
363
|
+
false,
|
|
364
|
+
"escaped .gsd paths should resolve to the project .gsd artifact",
|
|
365
|
+
);
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test("checkEngineHealth reports escaped missing artifact rows with .gsd-relative paths", async (t) => {
|
|
369
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-escaped-missing-artifact-"));
|
|
370
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
371
|
+
|
|
372
|
+
const gsdDir = join(base, ".gsd");
|
|
373
|
+
mkdirSync(gsdDir, { recursive: true });
|
|
374
|
+
|
|
375
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
376
|
+
insertArtifact({
|
|
377
|
+
path: "../../../Documents/Projects/project/.gsd/phases/01-m001/01-01-PLAN.md",
|
|
378
|
+
artifact_type: "PLAN",
|
|
379
|
+
milestone_id: "M001",
|
|
380
|
+
slice_id: "S01",
|
|
381
|
+
task_id: null,
|
|
382
|
+
full_content: "# Plan\n",
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
const issues: any[] = [];
|
|
386
|
+
await checkEngineHealth(base, issues, []);
|
|
387
|
+
|
|
388
|
+
const issue = issues.find((candidate) => candidate.code === "artifact_file_missing");
|
|
389
|
+
assert.ok(issue, "missing escaped artifact row should still be reported");
|
|
390
|
+
assert.equal(issue.file, "phases/01-m001/01-01-PLAN.md");
|
|
391
|
+
assert.doesNotMatch(issue.message, /\.\.\//);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("checkEngineHealth repair prunes stale phases artifact rows with present milestones files", async (t) => {
|
|
395
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-stale-phase-artifact-"));
|
|
396
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
397
|
+
|
|
398
|
+
const gsdDir = join(base, ".gsd");
|
|
399
|
+
const stalePath = "phases/01-m001/01-01-PLAN.md";
|
|
400
|
+
const replacementPath = "milestones/M001/slices/S01/S01-PLAN.md";
|
|
401
|
+
mkdirSync(join(gsdDir, "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
402
|
+
writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
|
|
403
|
+
|
|
404
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
405
|
+
insertArtifact({
|
|
406
|
+
path: stalePath,
|
|
407
|
+
artifact_type: "PLAN",
|
|
408
|
+
milestone_id: "M001",
|
|
409
|
+
slice_id: "S01",
|
|
410
|
+
task_id: null,
|
|
411
|
+
full_content: "# stale plan\n",
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const issues: any[] = [];
|
|
415
|
+
const fixes: string[] = [];
|
|
416
|
+
await checkEngineHealth(base, issues, fixes, { repair: true });
|
|
417
|
+
|
|
418
|
+
assert.equal(
|
|
419
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath),
|
|
420
|
+
false,
|
|
421
|
+
"repair should not report stale rows it pruned",
|
|
422
|
+
);
|
|
423
|
+
assert.ok(fixes.includes(`pruned stale flat-phase artifact row ${stalePath}`));
|
|
424
|
+
|
|
425
|
+
const rows = _getAdapter()!
|
|
426
|
+
.prepare("SELECT path FROM artifacts ORDER BY path")
|
|
427
|
+
.all() as Array<{ path: string }>;
|
|
428
|
+
assert.deepEqual(rows.map((row) => row.path), []);
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
test("checkEngineHealth repair prunes stale phases task rows against tasks/<T>-<TYPE>.md replacements", async (t) => {
|
|
432
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-stale-task-phase-artifact-"));
|
|
433
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
434
|
+
|
|
435
|
+
const gsdDir = join(base, ".gsd");
|
|
436
|
+
const stalePath = "phases/01-m001/01-01-T01-SUMMARY.md";
|
|
437
|
+
// Canonical legacy task layout has no per-task subdirectory: the SUMMARY lives
|
|
438
|
+
// at tasks/<T>-<TYPE>.md, not tasks/<T>/<T>-<TYPE>.md. If the expected-path
|
|
439
|
+
// builder adds an extra tasks/<T>/ segment, task-scoped stale rows never match
|
|
440
|
+
// their on-disk replacement and leak unpruned.
|
|
441
|
+
const replacementPath = "milestones/M001/slices/S01/tasks/T01-SUMMARY.md";
|
|
442
|
+
mkdirSync(join(gsdDir, "milestones", "M001", "slices", "S01", "tasks"), { recursive: true });
|
|
443
|
+
writeFileSync(join(gsdDir, replacementPath), "# Summary\n", "utf-8");
|
|
444
|
+
|
|
445
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
446
|
+
insertArtifact({
|
|
447
|
+
path: stalePath,
|
|
448
|
+
artifact_type: "SUMMARY",
|
|
449
|
+
milestone_id: "M001",
|
|
450
|
+
slice_id: "S01",
|
|
451
|
+
task_id: "T01",
|
|
452
|
+
full_content: "# stale summary\n",
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
const issues: any[] = [];
|
|
456
|
+
const fixes: string[] = [];
|
|
457
|
+
await checkEngineHealth(base, issues, fixes, { repair: true });
|
|
458
|
+
|
|
459
|
+
assert.equal(
|
|
460
|
+
issues.some((issue) => issue.code === "artifact_file_missing" && issue.file === stalePath),
|
|
461
|
+
false,
|
|
462
|
+
"task-scoped stale rows should prune against the tasks/<T>-<TYPE>.md replacement",
|
|
463
|
+
);
|
|
464
|
+
assert.ok(fixes.includes(`pruned stale flat-phase artifact row ${stalePath}`));
|
|
465
|
+
|
|
466
|
+
const rows = _getAdapter()!
|
|
467
|
+
.prepare("SELECT path FROM artifacts ORDER BY path")
|
|
468
|
+
.all() as Array<{ path: string }>;
|
|
469
|
+
assert.deepEqual(rows.map((row) => row.path), []);
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
test("checkEngineHealth repair prunes stale phases rows stored as escaped ../ paths", async (t) => {
|
|
473
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-prune-escaped-phase-artifact-"));
|
|
474
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
475
|
+
|
|
476
|
+
const gsdDir = join(base, ".gsd");
|
|
477
|
+
// Leaked row: the DB stores an escaped, absolute-ish path rather than a clean
|
|
478
|
+
// `phases/…` value. The prune must gate on the .gsd-relative path so the row
|
|
479
|
+
// this repair targets is not skipped just because the raw value starts with `../`.
|
|
480
|
+
const stalePath = "../../../Documents/Projects/project/.gsd/phases/01-m001/01-01-PLAN.md";
|
|
481
|
+
const replacementPath = "milestones/M001/slices/S01/S01-PLAN.md";
|
|
482
|
+
mkdirSync(join(gsdDir, "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
483
|
+
writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
|
|
484
|
+
|
|
485
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
486
|
+
insertArtifact({
|
|
487
|
+
path: stalePath,
|
|
488
|
+
artifact_type: "PLAN",
|
|
489
|
+
milestone_id: "M001",
|
|
490
|
+
slice_id: "S01",
|
|
491
|
+
task_id: null,
|
|
492
|
+
full_content: "# stale plan\n",
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
const issues: any[] = [];
|
|
496
|
+
const fixes: string[] = [];
|
|
497
|
+
await checkEngineHealth(base, issues, fixes, { repair: true });
|
|
498
|
+
|
|
499
|
+
assert.equal(
|
|
500
|
+
issues.some((issue) => issue.code === "artifact_file_missing"),
|
|
501
|
+
false,
|
|
502
|
+
"escaped phases rows should prune against the milestones replacement, not be reported missing",
|
|
503
|
+
);
|
|
504
|
+
assert.ok(
|
|
505
|
+
fixes.some((fix) => fix.startsWith("pruned stale flat-phase artifact row") && fix.includes("phases/01-m001/01-01-PLAN.md")),
|
|
506
|
+
"repair should record a prune for the escaped stale phases row",
|
|
507
|
+
);
|
|
508
|
+
|
|
509
|
+
const rows = _getAdapter()!
|
|
510
|
+
.prepare("SELECT path FROM artifacts ORDER BY path")
|
|
511
|
+
.all() as Array<{ path: string }>;
|
|
512
|
+
assert.deepEqual(rows.map((row) => row.path), [], "escaped stale phases row should be deleted from the DB");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("checkEngineHealth marks escaped phases rows fixable when a milestones replacement exists", async (t) => {
|
|
516
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-escaped-phase-fixable-"));
|
|
517
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
518
|
+
|
|
519
|
+
const gsdDir = join(base, ".gsd");
|
|
520
|
+
const replacementPath = "milestones/M001/slices/S01/S01-PLAN.md";
|
|
521
|
+
mkdirSync(join(gsdDir, "milestones", "M001", "slices", "S01"), { recursive: true });
|
|
522
|
+
writeFileSync(join(gsdDir, replacementPath), "# Plan\n", "utf-8");
|
|
523
|
+
|
|
524
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
525
|
+
insertArtifact({
|
|
526
|
+
path: "../../../Documents/Projects/project/.gsd/phases/01-m001/01-01-PLAN.md",
|
|
527
|
+
artifact_type: "PLAN",
|
|
528
|
+
milestone_id: "M001",
|
|
529
|
+
slice_id: "S01",
|
|
530
|
+
task_id: null,
|
|
531
|
+
full_content: "# stale plan\n",
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
const issues: any[] = [];
|
|
535
|
+
await checkEngineHealth(base, issues, []);
|
|
536
|
+
|
|
537
|
+
const issue = issues.find((candidate) => candidate.code === "artifact_file_missing");
|
|
538
|
+
assert.ok(issue, "escaped stale row should still be reported when repair is off");
|
|
539
|
+
assert.equal(issue.file, "phases/01-m001/01-01-PLAN.md");
|
|
540
|
+
assert.equal(issue.fixable, true, "escaped phases rows with a milestones replacement must be marked fixable");
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
function taskSummary(id: string, verificationResult = "passed"): string {
|
|
544
|
+
return [
|
|
545
|
+
"---",
|
|
546
|
+
`id: ${id}`,
|
|
547
|
+
"parent: S01",
|
|
548
|
+
"milestone: M001",
|
|
549
|
+
"key_files:",
|
|
550
|
+
" - src/example.ts",
|
|
551
|
+
"key_decisions:",
|
|
552
|
+
" - Keep recovery conservative",
|
|
553
|
+
"duration: 5m",
|
|
554
|
+
`verification_result: ${verificationResult}`,
|
|
555
|
+
"completed_at: 2026-01-01T00:00:00.000Z",
|
|
556
|
+
"blocker_discovered: false",
|
|
557
|
+
"---",
|
|
558
|
+
"",
|
|
559
|
+
`# ${id}: Done`,
|
|
560
|
+
"",
|
|
561
|
+
"**Recovered task completion.**",
|
|
562
|
+
"",
|
|
563
|
+
"## What Happened",
|
|
564
|
+
"",
|
|
565
|
+
"The task finished and wrote its summary.",
|
|
566
|
+
"",
|
|
567
|
+
"## Deviations",
|
|
568
|
+
"",
|
|
569
|
+
"None.",
|
|
570
|
+
"",
|
|
571
|
+
"## Known Issues",
|
|
572
|
+
"",
|
|
573
|
+
"None.",
|
|
574
|
+
"",
|
|
575
|
+
].join("\n");
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
test("checkEngineHealth marks valid task artifact DB divergence fixable and repairs it", async (t) => {
|
|
579
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-artifact-db-repair-"));
|
|
580
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
581
|
+
|
|
582
|
+
const gsdDir = join(base, ".gsd");
|
|
583
|
+
const tasksDir = join(gsdDir, "milestones", "M001", "slices", "S01", "tasks");
|
|
584
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
585
|
+
|
|
586
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
587
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active" });
|
|
588
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", risk: "low", depends: [], sequence: 1 });
|
|
589
|
+
insertTask({ id: "T01", milestoneId: "M001", sliceId: "S01", title: "Task", status: "pending", sequence: 1 });
|
|
590
|
+
|
|
591
|
+
const relSummaryPath = "milestones/M001/slices/S01/tasks/T01-SUMMARY.md";
|
|
592
|
+
const summary = taskSummary("T01");
|
|
593
|
+
writeFileSync(join(gsdDir, relSummaryPath), summary, "utf-8");
|
|
594
|
+
insertArtifact({
|
|
595
|
+
path: relSummaryPath,
|
|
596
|
+
artifact_type: "SUMMARY",
|
|
597
|
+
milestone_id: "M001",
|
|
598
|
+
slice_id: "S01",
|
|
599
|
+
task_id: "T01",
|
|
600
|
+
full_content: summary,
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const detectIssues: any[] = [];
|
|
604
|
+
await checkEngineHealth(base, detectIssues, []);
|
|
605
|
+
|
|
606
|
+
const divergence = detectIssues.find((issue) => issue.code === "artifact_db_status_divergence" && issue.unitId === "M001/S01/T01");
|
|
607
|
+
assert.ok(divergence, "doctor should report the artifact/DB divergence");
|
|
608
|
+
assert.equal(divergence.fixable, true);
|
|
609
|
+
|
|
610
|
+
const repairIssues: any[] = [];
|
|
611
|
+
const fixes: string[] = [];
|
|
612
|
+
await checkEngineHealth(base, repairIssues, fixes, { repair: true });
|
|
613
|
+
|
|
614
|
+
assert.ok(
|
|
615
|
+
fixes.includes("repaired task completion from SUMMARY artifact for M001/S01/T01"),
|
|
616
|
+
"repair mode should report the task completion repair",
|
|
617
|
+
);
|
|
618
|
+
assert.equal(
|
|
619
|
+
repairIssues.some((issue) => issue.code === "artifact_db_status_divergence" && issue.unitId === "M001/S01/T01"),
|
|
620
|
+
false,
|
|
621
|
+
"repaired divergence should not be reported in the same doctor run",
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
const task = getTask("M001", "S01", "T01");
|
|
625
|
+
assert.equal(task?.status, "complete");
|
|
626
|
+
assert.equal(task?.completed_at, "2026-01-01T00:00:00.000Z");
|
|
627
|
+
assert.equal(task?.verification_result, "passed");
|
|
628
|
+
assert.match(task?.full_summary_md ?? "", /# T01: Done/);
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
test("checkEngineHealth keeps failed and negated-pass summaries non-fixable", async (t) => {
|
|
632
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-artifact-db-failure-"));
|
|
633
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
634
|
+
|
|
635
|
+
const gsdDir = join(base, ".gsd");
|
|
636
|
+
const tasksDir = join(gsdDir, "milestones", "M001", "slices", "S01", "tasks");
|
|
637
|
+
mkdirSync(tasksDir, { recursive: true });
|
|
638
|
+
|
|
639
|
+
openDatabase(join(gsdDir, "gsd.db"));
|
|
640
|
+
insertMilestone({ id: "M001", title: "Foundation", status: "active" });
|
|
641
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Slice", status: "active", risk: "low", depends: [], sequence: 1 });
|
|
642
|
+
const nonPassingResults = [
|
|
643
|
+
["T02", "failed"],
|
|
644
|
+
["T03", "not passed"],
|
|
645
|
+
["T04", "not passing"],
|
|
646
|
+
] as const;
|
|
647
|
+
for (const [taskId, verificationResult] of nonPassingResults) {
|
|
648
|
+
insertTask({ id: taskId, milestoneId: "M001", sliceId: "S01", title: "Task", status: "pending", sequence: 1 });
|
|
649
|
+
|
|
650
|
+
const relSummaryPath = `milestones/M001/slices/S01/tasks/${taskId}-SUMMARY.md`;
|
|
651
|
+
const summary = taskSummary(taskId, verificationResult);
|
|
652
|
+
writeFileSync(join(gsdDir, relSummaryPath), summary, "utf-8");
|
|
653
|
+
insertArtifact({
|
|
654
|
+
path: relSummaryPath,
|
|
655
|
+
artifact_type: "SUMMARY",
|
|
656
|
+
milestone_id: "M001",
|
|
657
|
+
slice_id: "S01",
|
|
658
|
+
task_id: taskId,
|
|
659
|
+
full_content: summary,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
const issues: any[] = [];
|
|
664
|
+
await checkEngineHealth(base, issues, []);
|
|
665
|
+
|
|
666
|
+
for (const [taskId] of nonPassingResults) {
|
|
667
|
+
const divergence = issues.find((issue) => issue.code === "artifact_db_status_divergence" && issue.unitId === `M001/S01/${taskId}`);
|
|
668
|
+
assert.ok(divergence, "doctor should still report non-passing summary drift");
|
|
669
|
+
assert.equal(divergence.fixable, false);
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
338
673
|
test("checkEngineHealth reports missing CONTEXT and RESEARCH artifacts as user-content warnings", async (t) => {
|
|
339
674
|
const base = mkdtempSync(join(tmpdir(), "gsd-doctor-user-content-artifact-"));
|
|
340
675
|
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
@@ -95,7 +95,7 @@ function makeDeps(
|
|
|
95
95
|
isInAutoWorktree: () => true,
|
|
96
96
|
shouldUseWorktreeIsolation: () => true,
|
|
97
97
|
getIsolationMode: () => "worktree",
|
|
98
|
-
|
|
98
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
99
99
|
syncWorktreeStateBack: () => ({ synced: [] }),
|
|
100
100
|
teardownAutoWorktree: () => undefined,
|
|
101
101
|
createAutoWorktree: () => "",
|
|
@@ -185,7 +185,7 @@ describe("WorktreeResolver.mergeAndExit re-throws MergeConflictError (#2330)", (
|
|
|
185
185
|
resolveMilestoneFile: (_base: string, _mid: string, type: string) =>
|
|
186
186
|
type === "ROADMAP" ? roadmapPath : null,
|
|
187
187
|
readFileSync: () => "# M001\n",
|
|
188
|
-
|
|
188
|
+
mergeMilestone: () => {
|
|
189
189
|
throw new MergeConflictError(conflicted, "squash", "worktree/M001", "main");
|
|
190
190
|
},
|
|
191
191
|
});
|
|
@@ -216,7 +216,7 @@ describe("WorktreeResolver.mergeAndExit re-throws MergeConflictError (#2330)", (
|
|
|
216
216
|
resolveMilestoneFile: (_base: string, _mid: string, type: string) =>
|
|
217
217
|
type === "ROADMAP" ? roadmapPath : null,
|
|
218
218
|
readFileSync: () => "# M001\n",
|
|
219
|
-
|
|
219
|
+
mergeMilestone: () => {
|
|
220
220
|
throw new FakePermError("EACCES: permission denied");
|
|
221
221
|
},
|
|
222
222
|
});
|
|
@@ -242,7 +242,7 @@ describe("WorktreeResolver.mergeAndExit re-throws MergeConflictError (#2330)", (
|
|
|
242
242
|
resolveMilestoneFile: (_base: string, _mid: string, type: string) =>
|
|
243
243
|
type === "ROADMAP" ? roadmapPath : null,
|
|
244
244
|
readFileSync: () => "# M001\n",
|
|
245
|
-
|
|
245
|
+
mergeMilestone: () => ({ pushed: false, codeFilesChanged: true }),
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
const resolver = makeResolver(makeSession(baseDir), deps);
|
|
@@ -16,6 +16,7 @@ import { tmpdir } from "node:os";
|
|
|
16
16
|
import { execSync } from "node:child_process";
|
|
17
17
|
|
|
18
18
|
import { migrateToExternalState, recoverFailedMigration } from "../migrate-external.ts";
|
|
19
|
+
import { externalGsdRoot } from "../repo-identity.ts";
|
|
19
20
|
|
|
20
21
|
function run(command: string, cwd: string): string {
|
|
21
22
|
return execSync(command, {
|
|
@@ -119,6 +120,121 @@ describe("migrate-external worktree guard (#2970)", () => {
|
|
|
119
120
|
});
|
|
120
121
|
});
|
|
121
122
|
|
|
123
|
+
describe("migrateToExternalState rename fallback (#1179)", () => {
|
|
124
|
+
test("falls back to copy/delete when rename fails with EACCES", () => {
|
|
125
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-migrate-eacces-")));
|
|
126
|
+
const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-state-eacces-")));
|
|
127
|
+
const previousStateDir = process.env.GSD_STATE_DIR;
|
|
128
|
+
const previousProjectId = process.env.GSD_PROJECT_ID;
|
|
129
|
+
const originalRenameSync = fs.renameSync;
|
|
130
|
+
const localGsd = join(base, ".gsd");
|
|
131
|
+
const migratingPath = join(base, ".gsd.migrating");
|
|
132
|
+
let renameAttempts = 0;
|
|
133
|
+
|
|
134
|
+
process.env.GSD_STATE_DIR = stateDir;
|
|
135
|
+
process.env.GSD_PROJECT_ID = "rename-eacces";
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
run("git init -b main", base);
|
|
139
|
+
mkdirSync(localGsd, { recursive: true });
|
|
140
|
+
writeFileSync(join(localGsd, "PREFERENCES.md"), "# prefs\n", "utf-8");
|
|
141
|
+
|
|
142
|
+
fs.renameSync = ((src, dst) => {
|
|
143
|
+
if (String(src) === localGsd && String(dst) === migratingPath) {
|
|
144
|
+
renameAttempts += 1;
|
|
145
|
+
const error = new Error("simulated WSL/DrvFs rename lock") as NodeJS.ErrnoException;
|
|
146
|
+
error.code = "EACCES";
|
|
147
|
+
throw error;
|
|
148
|
+
}
|
|
149
|
+
return originalRenameSync(src, dst);
|
|
150
|
+
}) as typeof fs.renameSync;
|
|
151
|
+
syncBuiltinESMExports();
|
|
152
|
+
|
|
153
|
+
const result = migrateToExternalState(base);
|
|
154
|
+
|
|
155
|
+
assert.equal(result.migrated, true, "EACCES rename failure should use copy/delete fallback");
|
|
156
|
+
assert.equal(result.error, undefined);
|
|
157
|
+
assert.equal(renameAttempts, 1, "should exercise the EACCES rename fallback once");
|
|
158
|
+
assert.equal(readFileSync(join(stateDir, "projects", "rename-eacces", "PREFERENCES.md"), "utf-8"), "# prefs\n");
|
|
159
|
+
assert.equal(readFileSync(join(base, ".gsd", "PREFERENCES.md"), "utf-8"), "# prefs\n");
|
|
160
|
+
assert.ok(!existsSync(migratingPath), "backup staging dir must be removed after successful migration");
|
|
161
|
+
} finally {
|
|
162
|
+
fs.renameSync = originalRenameSync;
|
|
163
|
+
syncBuiltinESMExports();
|
|
164
|
+
if (previousStateDir === undefined) {
|
|
165
|
+
delete process.env.GSD_STATE_DIR;
|
|
166
|
+
} else {
|
|
167
|
+
process.env.GSD_STATE_DIR = previousStateDir;
|
|
168
|
+
}
|
|
169
|
+
if (previousProjectId === undefined) {
|
|
170
|
+
delete process.env.GSD_PROJECT_ID;
|
|
171
|
+
} else {
|
|
172
|
+
process.env.GSD_PROJECT_ID = previousProjectId;
|
|
173
|
+
}
|
|
174
|
+
rmSync(base, { recursive: true, force: true });
|
|
175
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("migrateToExternalState already-migrated external state guard (#1178)", () => {
|
|
181
|
+
test("skips migration when external state already exists for a real local .gsd directory", () => {
|
|
182
|
+
const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-migrate-already-external-")));
|
|
183
|
+
const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "gsd-state-already-external-")));
|
|
184
|
+
const previousStateDir = process.env.GSD_STATE_DIR;
|
|
185
|
+
const previousProjectId = process.env.GSD_PROJECT_ID;
|
|
186
|
+
process.env.GSD_STATE_DIR = stateDir;
|
|
187
|
+
|
|
188
|
+
try {
|
|
189
|
+
run("git init -b main", base);
|
|
190
|
+
run('git config user.name "Test"', base);
|
|
191
|
+
run('git config user.email "test@example.com"', base);
|
|
192
|
+
writeFileSync(join(base, "README.md"), "# Test\n", "utf-8");
|
|
193
|
+
run("git add README.md", base);
|
|
194
|
+
run('git commit -m "init"', base);
|
|
195
|
+
|
|
196
|
+
const externalPath = externalGsdRoot(base);
|
|
197
|
+
const identity = externalPath.split(/[\\/]/).pop();
|
|
198
|
+
assert.ok(identity, "external state path must include an identity segment");
|
|
199
|
+
mkdirSync(externalPath, { recursive: true });
|
|
200
|
+
writeFileSync(join(base, ".gsd-id"), `${identity}\n`, "utf-8");
|
|
201
|
+
writeFileSync(join(externalPath, "gsd.db"), "newer external state\n", "utf-8");
|
|
202
|
+
|
|
203
|
+
mkdirSync(join(base, ".gsd"), { recursive: true });
|
|
204
|
+
writeFileSync(join(base, ".gsd", "gsd.db"), "stale local state\n", "utf-8");
|
|
205
|
+
|
|
206
|
+
const result = migrateToExternalState(base);
|
|
207
|
+
|
|
208
|
+
assert.equal(result.migrated, false, "already-migrated project must not re-run migration");
|
|
209
|
+
assert.match(result.error ?? "", /external state already exists/i);
|
|
210
|
+
assert.equal(
|
|
211
|
+
readFileSync(join(externalPath, "gsd.db"), "utf-8"),
|
|
212
|
+
"newer external state\n",
|
|
213
|
+
"newer external gsd.db must not be overwritten by stale local state",
|
|
214
|
+
);
|
|
215
|
+
assert.equal(
|
|
216
|
+
readFileSync(join(base, ".gsd", "gsd.db"), "utf-8"),
|
|
217
|
+
"stale local state\n",
|
|
218
|
+
"local real .gsd directory should be left for explicit recovery",
|
|
219
|
+
);
|
|
220
|
+
assert.ok(!existsSync(join(base, ".gsd.migrating")), "migration staging dir must not be created");
|
|
221
|
+
} finally {
|
|
222
|
+
if (previousStateDir === undefined) {
|
|
223
|
+
delete process.env.GSD_STATE_DIR;
|
|
224
|
+
} else {
|
|
225
|
+
process.env.GSD_STATE_DIR = previousStateDir;
|
|
226
|
+
}
|
|
227
|
+
if (previousProjectId === undefined) {
|
|
228
|
+
delete process.env.GSD_PROJECT_ID;
|
|
229
|
+
} else {
|
|
230
|
+
process.env.GSD_PROJECT_ID = previousProjectId;
|
|
231
|
+
}
|
|
232
|
+
rmSync(base, { recursive: true, force: true });
|
|
233
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
122
238
|
describe("migrateToExternalState copy failure rollback (#1047)", () => {
|
|
123
239
|
test("restores .gsd when a source entry fails to copy", () => {
|
|
124
240
|
const base = realpathSync(mkdtempSync(join(tmpdir(), "gsd-migrate-copy-fail-")));
|