@opengsd/gsd-pi 1.6.0 → 1.7.0-dev.30c4ec2f
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/README.md +6 -10
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +19 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +20 -0
- package/dist/resources/extensions/gsd/auto.js +12 -0
- package/dist/resources/extensions/gsd/browser-daemon-auto-prep.js +68 -2
- package/dist/resources/extensions/gsd/doctor-history.js +65 -0
- package/dist/resources/extensions/gsd/doctor-state-checks.js +509 -0
- package/dist/resources/extensions/gsd/doctor-workspace-checks.js +91 -0
- package/dist/resources/extensions/gsd/doctor.js +14 -653
- package/dist/resources/extensions/gsd/md-importer.js +7 -2
- package/dist/resources/extensions/gsd/migrate/planning-writer.js +8 -2
- package/dist/resources/extensions/gsd/migration-auto-check.js +4 -0
- package/dist/resources/extensions/gsd/reopen-reason.js +98 -0
- package/dist/resources/extensions/gsd/session-status-io.js +31 -0
- package/dist/resources/extensions/gsd/state-reconciliation/drift/project-md.js +54 -14
- package/dist/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.js +48 -5
- package/dist/resources/extensions/gsd/tools/reopen-task.js +15 -0
- package/dist/resources/extensions/shared/gsd-browser-cli.js +22 -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 +10 -10
- package/dist/web/standalone/.next/build-manifest.json +3 -3
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/required-server-files.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page.js +3 -3
- package/dist/web/standalone/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
- 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/page.js +2 -2
- package/dist/web/standalone/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +3 -3
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
- 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 +3 -3
- 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/api/boot/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/boot/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/browse-directories/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/dev-mode/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/experimental/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/experimental/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/notifications/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/preferences/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/remote-questions/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/remote-questions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/session/events/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/input/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route.js +2 -2
- package/dist/web/standalone/.next/server/app/api/terminal/resize/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js +4 -4
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/upload/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/update/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +4 -4
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +4 -4
- 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 +3 -3
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/page.js +2 -2
- package/dist/web/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +10 -10
- package/dist/web/standalone/.next/server/chunks/2842.js +4 -4
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware.js +3 -3
- 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/dist/web/standalone/.next/static/chunks/app/_not-found/page-a6fb1847f67f167c.js +1 -0
- package/dist/web/standalone/.next/static/chunks/app/{layout-b35cbfff38aaf4cf.js → layout-4ae2d68984392bbf.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/app/page-72a856634ad14c10.js +1 -0
- package/dist/web/standalone/.next/static/chunks/{main-app-590a74400e35f685.js → main-app-90d1d8d5e5d2dc6b.js} +1 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-1115d46ac61b9823.js +1 -0
- package/dist/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +2 -2
- package/dist/web/standalone/node_modules/node-pty/build/Release/pty.node +0 -0
- package/dist/web/standalone/node_modules/node-pty/build/pty.target.mk +14 -14
- package/dist/web/standalone/server.js +1 -1
- package/integrations/hermes/open_gsd_hermes/gsd_client.py +1 -1
- package/integrations/hermes/pyproject.toml +1 -1
- package/package.json +6 -6
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts +1 -1
- package/packages/daemon/dist/cloud-cli.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-cli.js +42 -2
- package/packages/daemon/dist/cloud-cli.js.map +1 -1
- package/packages/daemon/dist/cloud-config.d.ts +1 -0
- package/packages/daemon/dist/cloud-config.d.ts.map +1 -1
- package/packages/daemon/dist/cloud-config.js +26 -8
- package/packages/daemon/dist/cloud-config.js.map +1 -1
- package/packages/daemon/dist/cloud-config.test.js +19 -1
- package/packages/daemon/dist/cloud-config.test.js.map +1 -1
- package/packages/daemon/dist/device-flow.d.ts +24 -0
- package/packages/daemon/dist/device-flow.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.js +128 -0
- package/packages/daemon/dist/device-flow.js.map +1 -0
- package/packages/daemon/dist/device-flow.test.d.ts +2 -0
- package/packages/daemon/dist/device-flow.test.d.ts.map +1 -0
- package/packages/daemon/dist/device-flow.test.js +172 -0
- package/packages/daemon/dist/device-flow.test.js.map +1 -0
- package/packages/daemon/package.json +5 -5
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/gsd-cloud/README.md +48 -0
- package/packages/gsd-cloud/bin/gsd-cloud.js +28 -0
- package/packages/gsd-cloud/package.json +40 -0
- 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/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/browser-tools/tests/gsd-browser-launch-config.test.mjs +19 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +22 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +20 -0
- package/src/resources/extensions/gsd/auto.ts +12 -0
- package/src/resources/extensions/gsd/browser-daemon-auto-prep.ts +77 -1
- package/src/resources/extensions/gsd/doctor-history.ts +81 -0
- package/src/resources/extensions/gsd/doctor-state-checks.ts +527 -0
- package/src/resources/extensions/gsd/doctor-workspace-checks.ts +97 -0
- package/src/resources/extensions/gsd/doctor.ts +14 -673
- package/src/resources/extensions/gsd/md-importer.ts +7 -2
- package/src/resources/extensions/gsd/migrate/planning-writer.ts +9 -3
- package/src/resources/extensions/gsd/migration-auto-check.ts +3 -0
- package/src/resources/extensions/gsd/reopen-reason.ts +109 -0
- package/src/resources/extensions/gsd/session-status-io.ts +34 -0
- package/src/resources/extensions/gsd/state-reconciliation/drift/project-md.ts +67 -20
- package/src/resources/extensions/gsd/state-reconciliation/drift/sketch-flag.ts +47 -5
- package/src/resources/extensions/gsd/tests/browser-daemon-auto-prep.test.ts +26 -0
- package/src/resources/extensions/gsd/tests/db-authority-regression.test.ts +76 -1
- package/src/resources/extensions/gsd/tests/doctor-history-public-api.test.ts +25 -0
- package/src/resources/extensions/gsd/tests/migrate-hierarchy.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/parsers-legacy-importers.test.ts +4 -0
- package/src/resources/extensions/gsd/tests/planning-writer.test.ts +18 -1
- package/src/resources/extensions/gsd/tests/progressive-planning.test.ts +18 -3
- package/src/resources/extensions/gsd/tests/reopen-reason.test.ts +117 -0
- package/src/resources/extensions/gsd/tests/state-reconciliation-drift.test.ts +177 -25
- package/src/resources/extensions/gsd/tests/worker-resume-wait.test.ts +73 -0
- package/src/resources/extensions/gsd/tools/reopen-task.ts +15 -0
- package/src/resources/extensions/shared/gsd-browser-cli.ts +35 -7
- package/dist/web/standalone/.next/static/chunks/app/_not-found/page-49f565245e1e4afe.js +0 -1
- package/dist/web/standalone/.next/static/chunks/app/page-bc4c723c6a19c6f0.js +0 -1
- package/dist/web/standalone/.next/static/chunks/next/dist/client/components/builtin/global-error-ec3eaffc9785ba48.js +0 -1
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{6xYMOzxVhsQgryneJ5KRc → ue2JDvA1wboxPBPeblD5N}/_ssgManifest.js +0 -0
|
@@ -420,6 +420,59 @@ test("migration auto-check still reports real drift with scratch dirs excluded f
|
|
|
420
420
|
}
|
|
421
421
|
});
|
|
422
422
|
|
|
423
|
+
test("migration auto-check excludes sketch slice stub tasks from the markdown scan (#1286)", () => {
|
|
424
|
+
const base = makeBase();
|
|
425
|
+
try {
|
|
426
|
+
// S01 is a sketch (`[sketch]` badge): its PLAN is a stub carrying a
|
|
427
|
+
// placeholder <tasks> block until refinement. migrateHierarchyToDb skips
|
|
428
|
+
// seeding those stub tasks, so the markdown scan must skip them too — else
|
|
429
|
+
// the stub inflates the markdown count and reports false drift against a DB
|
|
430
|
+
// that (correctly) holds zero tasks for the sketch slice.
|
|
431
|
+
const roadmap = `# M001: Test Milestone
|
|
432
|
+
|
|
433
|
+
**Vision:** Sketch slices must not inflate markdown task counts.
|
|
434
|
+
|
|
435
|
+
## Slices
|
|
436
|
+
|
|
437
|
+
- [ ] **S01: Sketch Slice** \`[sketch]\` \`risk:low\` \`depends:[]\`
|
|
438
|
+
> After this: Sketch decomposed.
|
|
439
|
+
|
|
440
|
+
- [ ] **S02: Real Slice** \`risk:low\` \`depends:[]\`
|
|
441
|
+
> After this: Real work done.
|
|
442
|
+
`;
|
|
443
|
+
const sketchPlan = `# S01: Sketch Slice
|
|
444
|
+
|
|
445
|
+
**Goal:** Placeholder.
|
|
446
|
+
|
|
447
|
+
## Tasks
|
|
448
|
+
|
|
449
|
+
- [ ] **T01: Plan 01**
|
|
450
|
+
Stub placeholder task.
|
|
451
|
+
`;
|
|
452
|
+
const realPlan = `# S02: Real Slice
|
|
453
|
+
|
|
454
|
+
**Goal:** Real work.
|
|
455
|
+
|
|
456
|
+
## Tasks
|
|
457
|
+
|
|
458
|
+
- [ ] **T01: Real Task**
|
|
459
|
+
Real work.
|
|
460
|
+
`;
|
|
461
|
+
const milestoneDir = join(base, ".gsd", "milestones", "M001");
|
|
462
|
+
mkdirSync(join(milestoneDir, "slices", "S01"), { recursive: true });
|
|
463
|
+
mkdirSync(join(milestoneDir, "slices", "S02"), { recursive: true });
|
|
464
|
+
writeFileSync(join(milestoneDir, "M001-ROADMAP.md"), roadmap);
|
|
465
|
+
writeFileSync(join(milestoneDir, "slices", "S01", "S01-PLAN.md"), sketchPlan);
|
|
466
|
+
writeFileSync(join(milestoneDir, "slices", "S02", "S02-PLAN.md"), realPlan);
|
|
467
|
+
|
|
468
|
+
// Both slices count, but only the refined slice contributes a task; the
|
|
469
|
+
// sketch stub task is excluded, matching migrateHierarchyToDb.
|
|
470
|
+
assert.deepEqual(countMarkdownHierarchy(base), { milestones: 1, slices: 2, tasks: 1 });
|
|
471
|
+
} finally {
|
|
472
|
+
cleanup(base);
|
|
473
|
+
}
|
|
474
|
+
});
|
|
475
|
+
|
|
423
476
|
test("migration auto-check still compares a roadmapless milestone that HAS a DB row", async () => {
|
|
424
477
|
const base = makeBase();
|
|
425
478
|
try {
|
|
@@ -47,6 +47,9 @@ const ALLOWED_IMPORTERS = new Set([
|
|
|
47
47
|
"gsd/migration-auto-check.ts",
|
|
48
48
|
// drift detection: compares markdown projection against DB by design
|
|
49
49
|
"gsd/state-reconciliation/drift/roadmap.ts",
|
|
50
|
+
// drift detection: reads PLAN.md tasks to distinguish a real plan from a
|
|
51
|
+
// stub before clearing a stale is_sketch flag (#1287)
|
|
52
|
+
"gsd/state-reconciliation/drift/sketch-flag.ts",
|
|
50
53
|
// stale-render detection + render verification helpers over rendered output
|
|
51
54
|
"gsd/markdown-renderer.ts",
|
|
52
55
|
// pre-migration fallback: deriveState must work before the DB exists
|
|
@@ -58,6 +61,7 @@ const ALLOWED_IMPORTERS = new Set([
|
|
|
58
61
|
"gsd/artifact-verification.ts",
|
|
59
62
|
// diagnostics-only surfaces: report on projections, make no dispatch decisions
|
|
60
63
|
"gsd/doctor.ts",
|
|
64
|
+
"gsd/doctor-state-checks.ts",
|
|
61
65
|
// diagnostics-only: compares PLAN.md task checkboxes against DB status
|
|
62
66
|
"gsd/doctor-engine-checks.ts",
|
|
63
67
|
// display/telemetry-only surfaces
|
|
@@ -8,7 +8,7 @@ import { tmpdir } from "node:os";
|
|
|
8
8
|
import { randomUUID } from "node:crypto";
|
|
9
9
|
|
|
10
10
|
import { writePlanningDirectory } from "../migrate/planning-writer.ts";
|
|
11
|
-
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask } from "../gsd-db.ts";
|
|
11
|
+
import { openDatabase, closeDatabase, insertMilestone, insertSlice, insertTask, updateTaskStatus } from "../gsd-db.ts";
|
|
12
12
|
|
|
13
13
|
const tmpDirs: string[] = [];
|
|
14
14
|
function makeTmp(): string {
|
|
@@ -65,6 +65,23 @@ test("writePlanningDirectory emits phase plan file with XML structure", async ()
|
|
|
65
65
|
assert.match(plan, /<tasks>/);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
test("writePlanningDirectory renders completed task checkboxes as [x] (#1276)", async () => {
|
|
69
|
+
const base = makeTmp();
|
|
70
|
+
updateTaskStatus("M001", "S01", "T01", "complete");
|
|
71
|
+
await writePlanningDirectory(base, "flat-phases");
|
|
72
|
+
|
|
73
|
+
const phaseDir = readdirSync(join(base, ".planning", "phases"), { withFileTypes: true })
|
|
74
|
+
.filter((d) => d.isDirectory())
|
|
75
|
+
.map((d) => d.name)[0]!;
|
|
76
|
+
const planFile = readdirSync(join(base, ".planning", "phases", phaseDir))
|
|
77
|
+
.filter((f) => f.endsWith("PLAN.md"))[0]!;
|
|
78
|
+
const plan = readFileSync(join(base, ".planning", "phases", phaseDir, planFile), "utf-8");
|
|
79
|
+
assert.match(plan, /- \[x\] \*\*T01\*\*/, "completed task should project as [x], not [ ]");
|
|
80
|
+
|
|
81
|
+
const roadmap = readFileSync(join(base, ".planning", "ROADMAP.md"), "utf-8");
|
|
82
|
+
assert.match(roadmap, /- \[x\] 01 —/, "phase with all tasks complete should be [x] in ROADMAP");
|
|
83
|
+
});
|
|
84
|
+
|
|
68
85
|
test("writePlanningDirectory is idempotent: writing twice produces stable content", async () => {
|
|
69
86
|
const base = makeTmp();
|
|
70
87
|
await writePlanningDirectory(base, "flat-phases");
|
|
@@ -76,6 +76,21 @@ function writeS01Artifacts(base: string): void {
|
|
|
76
76
|
writeFileSync(join(base, ".gsd", "milestones", "M001", "slices", "S01", "S01-SUMMARY.md"), "# S01 Summary\n");
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
// A *real* decomposed PLAN (>= 1 genuine task). #1287: the stale-sketch-flag
|
|
80
|
+
// detector requires a real plan — not a bare stub — before clearing is_sketch.
|
|
81
|
+
function realPlanContent(sliceId: string): string {
|
|
82
|
+
return [
|
|
83
|
+
`# ${sliceId}: Feature`,
|
|
84
|
+
"",
|
|
85
|
+
"**Goal:** Ship the feature",
|
|
86
|
+
"",
|
|
87
|
+
"## Tasks",
|
|
88
|
+
"",
|
|
89
|
+
"- [ ] **T01: Build the feature** `est:1h`",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
79
94
|
function cleanup(base: string, originalCwd: string): void {
|
|
80
95
|
try { closeDatabase(); } catch { /* noop */ }
|
|
81
96
|
process.chdir(originalCwd);
|
|
@@ -185,7 +200,7 @@ test("ADR-011: existing PLAN + stale sketch flag heals via reconcileBeforeDispat
|
|
|
185
200
|
writePreferences(base, "phases:\n progressive_planning: true");
|
|
186
201
|
writeFileSync(
|
|
187
202
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
188
|
-
"
|
|
203
|
+
realPlanContent("S02"),
|
|
189
204
|
);
|
|
190
205
|
process.chdir(base);
|
|
191
206
|
|
|
@@ -252,7 +267,7 @@ test("ADR-011: reconcileBeforeDispatch auto-heals stale sketch flag when PLAN ex
|
|
|
252
267
|
// Simulate plan-slice completion where PLAN exists but is_sketch was not flipped.
|
|
253
268
|
writeFileSync(
|
|
254
269
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
255
|
-
"
|
|
270
|
+
realPlanContent("S02"),
|
|
256
271
|
);
|
|
257
272
|
process.chdir(base);
|
|
258
273
|
|
|
@@ -282,7 +297,7 @@ test("ADR-011: deriveState stays pure across worktree/canonical-root; healing be
|
|
|
282
297
|
writePreferences(base, "phases:\n skip_research: false");
|
|
283
298
|
writeFileSync(
|
|
284
299
|
join(base, ".gsd", "milestones", "M001", "slices", "S02", "S02-PLAN.md"),
|
|
285
|
-
"
|
|
300
|
+
realPlanContent("S02"),
|
|
286
301
|
);
|
|
287
302
|
const worktreePath = join(base, "worker");
|
|
288
303
|
mkdirSync(worktreePath, { recursive: true });
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// GSD — reopen-reason injection tests (#1272)
|
|
2
|
+
// Copyright (c) 2026 Jeremy McSpadden <jeremy@fluxlabs.net>
|
|
3
|
+
|
|
4
|
+
import test from 'node:test';
|
|
5
|
+
import assert from 'node:assert/strict';
|
|
6
|
+
import { mkdtempSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { tmpdir } from 'node:os';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
openDatabase,
|
|
12
|
+
closeDatabase,
|
|
13
|
+
insertMilestone,
|
|
14
|
+
insertSlice,
|
|
15
|
+
insertTask,
|
|
16
|
+
} from '../gsd-db.ts';
|
|
17
|
+
import { handleReopenTask } from '../tools/reopen-task.ts';
|
|
18
|
+
import {
|
|
19
|
+
reopenReasonArtifactPath,
|
|
20
|
+
writeReopenReason,
|
|
21
|
+
claimReopenReasonForInjection,
|
|
22
|
+
} from '../reopen-reason.ts';
|
|
23
|
+
|
|
24
|
+
function makeTmpBase(): string {
|
|
25
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-reopen-reason-'));
|
|
26
|
+
mkdirSync(join(base, '.gsd', 'milestones', 'M001', 'slices', 'S01', 'tasks'), { recursive: true });
|
|
27
|
+
return base;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function cleanup(base: string): void {
|
|
31
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
32
|
+
try { rmSync(base, { recursive: true, force: true }); } catch { /* noop */ }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function seedCompleteTask(): void {
|
|
36
|
+
insertMilestone({ id: 'M001', title: 'Test Milestone', status: 'active' });
|
|
37
|
+
insertSlice({ id: 'S01', milestoneId: 'M001', title: 'Test Slice', status: 'in_progress' });
|
|
38
|
+
insertTask({ id: 'T01', sliceId: 'S01', milestoneId: 'M001', title: 'Task One', status: 'complete' });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
test('writeReopenReason then claim: injects the reason, then is one-shot', () => {
|
|
42
|
+
const base = makeTmpBase();
|
|
43
|
+
try {
|
|
44
|
+
const reason = 'T01 shifted the descendants count to 12. Fix: update the assertion to 12.';
|
|
45
|
+
writeReopenReason(base, 'M001', 'S01', 'T01', reason);
|
|
46
|
+
|
|
47
|
+
const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
|
|
48
|
+
assert.ok(existsSync(path), 'artifact should be written');
|
|
49
|
+
|
|
50
|
+
const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
51
|
+
assert.ok(claimed, 'first claim should return an injection block');
|
|
52
|
+
assert.match(claimed!.injectionBlock, /Reopened — Reason/);
|
|
53
|
+
assert.match(claimed!.injectionBlock, /descendants count to 12/);
|
|
54
|
+
assert.ok(!existsSync(path), 'artifact should be deleted after claim (one-shot)');
|
|
55
|
+
|
|
56
|
+
const second = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
57
|
+
assert.equal(second, null, 'second claim should return null');
|
|
58
|
+
} finally {
|
|
59
|
+
cleanup(base);
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('claimReopenReasonForInjection: returns null when nothing pending', () => {
|
|
64
|
+
const base = makeTmpBase();
|
|
65
|
+
try {
|
|
66
|
+
assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
|
|
67
|
+
} finally {
|
|
68
|
+
cleanup(base);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test('writeReopenReason: empty/whitespace reason is a no-op', () => {
|
|
73
|
+
const base = makeTmpBase();
|
|
74
|
+
try {
|
|
75
|
+
writeReopenReason(base, 'M001', 'S01', 'T01', ' ');
|
|
76
|
+
const path = reopenReasonArtifactPath(base, 'M001', 'S01', 'T01')!;
|
|
77
|
+
assert.ok(!existsSync(path), 'no artifact should be written for an empty reason');
|
|
78
|
+
} finally {
|
|
79
|
+
cleanup(base);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('handleReopenTask: persists a claimable reopen reason when reason is provided', async () => {
|
|
84
|
+
const base = makeTmpBase();
|
|
85
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
86
|
+
try {
|
|
87
|
+
seedCompleteTask();
|
|
88
|
+
|
|
89
|
+
const result = await handleReopenTask({
|
|
90
|
+
milestoneId: 'M001',
|
|
91
|
+
sliceId: 'S01',
|
|
92
|
+
taskId: 'T01',
|
|
93
|
+
reason: 'Full suite caught NavNodeTest regression — update count assertion to 12.',
|
|
94
|
+
}, base);
|
|
95
|
+
assert.ok(!('error' in result), `unexpected error: ${'error' in result ? result.error : ''}`);
|
|
96
|
+
|
|
97
|
+
const claimed = claimReopenReasonForInjection(base, 'M001', 'S01', 'T01');
|
|
98
|
+
assert.ok(claimed, 'reopen reason should be claimable after handleReopenTask');
|
|
99
|
+
assert.match(claimed!.injectionBlock, /NavNodeTest regression/);
|
|
100
|
+
} finally {
|
|
101
|
+
cleanup(base);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test('handleReopenTask: no reason provided leaves nothing to claim', async () => {
|
|
106
|
+
const base = makeTmpBase();
|
|
107
|
+
openDatabase(join(base, '.gsd', 'gsd.db'));
|
|
108
|
+
try {
|
|
109
|
+
seedCompleteTask();
|
|
110
|
+
|
|
111
|
+
await handleReopenTask({ milestoneId: 'M001', sliceId: 'S01', taskId: 'T01' }, base);
|
|
112
|
+
|
|
113
|
+
assert.equal(claimReopenReasonForInjection(base, 'M001', 'S01', 'T01'), null);
|
|
114
|
+
} finally {
|
|
115
|
+
cleanup(base);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
@@ -107,11 +107,11 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
|
|
|
107
107
|
sketchScope: "limited",
|
|
108
108
|
});
|
|
109
109
|
|
|
110
|
-
// Simulate the post-crash scenario: PLAN.md
|
|
111
|
-
// is_sketch flag is still 1.
|
|
110
|
+
// Simulate the post-crash scenario: a *real* PLAN.md (a decomposed task)
|
|
111
|
+
// exists on disk but the is_sketch flag is still 1.
|
|
112
112
|
writeFileSync(
|
|
113
113
|
join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
|
|
114
|
-
"
|
|
114
|
+
makeStalePlanContent("S02", [{ id: "T01", title: "Build the feature", done: false }]),
|
|
115
115
|
);
|
|
116
116
|
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "pre: flagged as sketch");
|
|
117
117
|
|
|
@@ -131,6 +131,120 @@ test("ADR-017 (#5700): sketch-flag drift detected and repaired end-to-end", asyn
|
|
|
131
131
|
}
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
test("#1287: stub/placeholder PLAN does NOT clear the sketch flag", async (t) => {
|
|
135
|
+
const base = makeFixtureBase();
|
|
136
|
+
t.after(() => cleanup(base));
|
|
137
|
+
|
|
138
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
139
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
140
|
+
insertSlice({
|
|
141
|
+
id: "S02",
|
|
142
|
+
milestoneId: "M001",
|
|
143
|
+
title: "Feature",
|
|
144
|
+
status: "pending",
|
|
145
|
+
risk: "medium",
|
|
146
|
+
depends: [],
|
|
147
|
+
demo: "S02 demo.",
|
|
148
|
+
sequence: 1,
|
|
149
|
+
isSketch: true,
|
|
150
|
+
sketchScope: "limited",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
|
|
154
|
+
|
|
155
|
+
// A bare stub PLAN (no decomposed tasks) must not clear the flag.
|
|
156
|
+
writeFileSync(planPath, "# S02 Plan\n");
|
|
157
|
+
clearRendererCaches();
|
|
158
|
+
let state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
159
|
+
let result = await reconcileBeforeDispatch(base, {
|
|
160
|
+
invalidateStateCache: () => {},
|
|
161
|
+
deriveState: async () => state,
|
|
162
|
+
});
|
|
163
|
+
assert.equal(result.ok, true);
|
|
164
|
+
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "stub PLAN: flag stays set");
|
|
165
|
+
assert.equal(result.repaired.length, 0, "stub PLAN: no repair");
|
|
166
|
+
|
|
167
|
+
// A projection round-trip stub whose only task is a synthetic "Plan NN"
|
|
168
|
+
// placeholder (migrate/transformer.buildTaskTitle) must also not clear it.
|
|
169
|
+
writeFileSync(
|
|
170
|
+
planPath,
|
|
171
|
+
makeStalePlanContent("S02", [{ id: "T01", title: "Plan 01", done: false }]),
|
|
172
|
+
);
|
|
173
|
+
clearRendererCaches();
|
|
174
|
+
state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
175
|
+
result = await reconcileBeforeDispatch(base, {
|
|
176
|
+
invalidateStateCache: () => {},
|
|
177
|
+
deriveState: async () => state,
|
|
178
|
+
});
|
|
179
|
+
assert.equal(result.ok, true);
|
|
180
|
+
assert.equal(getSlice("M001", "S02")?.is_sketch, 1, "placeholder task: flag stays set");
|
|
181
|
+
assert.equal(result.repaired.length, 0, "placeholder task: no repair");
|
|
182
|
+
|
|
183
|
+
// buildTaskTitle also emits `${phase} ${plan}` (e.g. "00 01") when the plan
|
|
184
|
+
// frontmatter carries phase/plan. This projected placeholder must not clear
|
|
185
|
+
// the flag either.
|
|
186
|
+
writeFileSync(
|
|
187
|
+
planPath,
|
|
188
|
+
makeStalePlanContent("S02", [{ id: "T01", title: "00 01", done: false }]),
|
|
189
|
+
);
|
|
190
|
+
clearRendererCaches();
|
|
191
|
+
state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
192
|
+
result = await reconcileBeforeDispatch(base, {
|
|
193
|
+
invalidateStateCache: () => {},
|
|
194
|
+
deriveState: async () => state,
|
|
195
|
+
});
|
|
196
|
+
assert.equal(result.ok, true);
|
|
197
|
+
assert.equal(
|
|
198
|
+
getSlice("M001", "S02")?.is_sketch,
|
|
199
|
+
1,
|
|
200
|
+
"phase/plan placeholder task: flag stays set",
|
|
201
|
+
);
|
|
202
|
+
assert.equal(result.repaired.length, 0, "phase/plan placeholder task: no repair");
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("#1288: real tasks shaped like `word + number` still clear the sketch flag", async (t) => {
|
|
206
|
+
const base = makeFixtureBase();
|
|
207
|
+
t.after(() => cleanup(base));
|
|
208
|
+
|
|
209
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
210
|
+
insertMilestone({ id: "M001", title: "Test", status: "active" });
|
|
211
|
+
insertSlice({
|
|
212
|
+
id: "S02",
|
|
213
|
+
milestoneId: "M001",
|
|
214
|
+
title: "Feature",
|
|
215
|
+
status: "pending",
|
|
216
|
+
risk: "medium",
|
|
217
|
+
depends: [],
|
|
218
|
+
demo: "S02 demo.",
|
|
219
|
+
sequence: 1,
|
|
220
|
+
isSketch: true,
|
|
221
|
+
sketchScope: "limited",
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const planPath = join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md");
|
|
225
|
+
|
|
226
|
+
// `Step 1` / `RFC 1234` match the loose `word + number` shape but are genuine
|
|
227
|
+
// decomposed tasks. buildTaskTitle only emits `${phase} ${plan}` with a
|
|
228
|
+
// digit-led phase, so these must NOT be read as placeholders (#1288): a real
|
|
229
|
+
// plan-slice like this must still clear the stale is_sketch flag.
|
|
230
|
+
writeFileSync(
|
|
231
|
+
planPath,
|
|
232
|
+
makeStalePlanContent("S02", [
|
|
233
|
+
{ id: "T01", title: "Step 1", done: false },
|
|
234
|
+
{ id: "T02", title: "RFC 1234", done: false },
|
|
235
|
+
]),
|
|
236
|
+
);
|
|
237
|
+
clearRendererCaches();
|
|
238
|
+
const state = makeState({ activeMilestone: { id: "M001", title: "Test" } });
|
|
239
|
+
const result = await reconcileBeforeDispatch(base, {
|
|
240
|
+
invalidateStateCache: () => {},
|
|
241
|
+
deriveState: async () => state,
|
|
242
|
+
});
|
|
243
|
+
assert.equal(result.ok, true);
|
|
244
|
+
assert.equal(getSlice("M001", "S02")?.is_sketch, 0, "real task titles: flag cleared");
|
|
245
|
+
assert.equal(result.repaired.length, 1, "real task titles: repaired once");
|
|
246
|
+
});
|
|
247
|
+
|
|
134
248
|
test("ADR-017 (#5700): repair failure throws ReconciliationFailedError with shape", async () => {
|
|
135
249
|
const seenDrift: DriftRecord = { kind: "stale-sketch-flag", mid: "M001", sid: "S02" };
|
|
136
250
|
const handler: DriftHandler = {
|
|
@@ -1032,7 +1146,7 @@ test("ADR-017 (#5703): live worker lock is not cleared", async (t) => {
|
|
|
1032
1146
|
|
|
1033
1147
|
// ─── #5704: unregistered-milestone drift ────────────────────────────────────
|
|
1034
1148
|
|
|
1035
|
-
test("ADR-017 (#5704): unregistered-milestone drift
|
|
1149
|
+
test("ADR-017 (#5704/#1281): unregistered-milestone drift pauses with a hint instead of hard-escalating", async (t) => {
|
|
1036
1150
|
const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-"));
|
|
1037
1151
|
const milestoneDir = join(base, ".gsd", "phases", "42-test");
|
|
1038
1152
|
mkdirSync(milestoneDir, { recursive: true });
|
|
@@ -1059,29 +1173,67 @@ test("ADR-017 (#5704): unregistered-milestone drift fails closed without importi
|
|
|
1059
1173
|
// Pre-condition: filesystem has the milestone, DB does NOT.
|
|
1060
1174
|
assert.equal(getMilestone("M042"), null, "pre: DB has no row for M042");
|
|
1061
1175
|
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
return true;
|
|
1080
|
-
},
|
|
1081
|
-
);
|
|
1176
|
+
// #1281: the handler exposes a `blocker`, so reconciliation returns a
|
|
1177
|
+
// non-fatal pause-with-hint (ok:true + blocker) rather than throwing.
|
|
1178
|
+
const result = await reconcileBeforeDispatch(base, {
|
|
1179
|
+
invalidateStateCache: () => {},
|
|
1180
|
+
deriveState: async () => makeState(),
|
|
1181
|
+
});
|
|
1182
|
+
assert.equal(result.ok, true);
|
|
1183
|
+
const blocker = result.blockers.find((b) => /M042/.test(b));
|
|
1184
|
+
assert.ok(blocker, "expected an M042 unregistered-milestone blocker");
|
|
1185
|
+
assert.match(blocker!, /markdown projection/);
|
|
1186
|
+
// Hint leads with targeted, non-destructive actions (rename/discard)...
|
|
1187
|
+
assert.match(blocker!, /Rename/);
|
|
1188
|
+
assert.match(blocker!, /Discard/);
|
|
1189
|
+
// ...and reframes recover as a destructive last resort, not the fix (#826).
|
|
1190
|
+
assert.match(blocker!, /\/gsd recover/);
|
|
1191
|
+
assert.match(blocker!, /last resort/i);
|
|
1192
|
+
// Runtime never imports markdown into the DB.
|
|
1082
1193
|
assert.equal(getMilestone("M042"), null, "post: DB still has no row for M042");
|
|
1083
1194
|
});
|
|
1084
1195
|
|
|
1196
|
+
test("#1281: descriptive flat-phase dir registered under a suffixed id → no false-positive drift", async (t) => {
|
|
1197
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-1281-suffix-"));
|
|
1198
|
+
// Descriptive slug the flat-phase extractor cannot recover the suffix from →
|
|
1199
|
+
// it derives the bare `M007`, which has no DB row.
|
|
1200
|
+
const milestoneDir = join(base, ".gsd", "phases", "07-v40fmq-m007-v40fmq-navigation-footer-system");
|
|
1201
|
+
mkdirSync(milestoneDir, { recursive: true });
|
|
1202
|
+
writeFileSync(
|
|
1203
|
+
join(milestoneDir, "M007-v40fmq-ROADMAP.md"),
|
|
1204
|
+
[
|
|
1205
|
+
"# M007: Navigation Footer System",
|
|
1206
|
+
"",
|
|
1207
|
+
"**Vision:** Verify no false-positive unregistered-milestone drift",
|
|
1208
|
+
"",
|
|
1209
|
+
"## Slices",
|
|
1210
|
+
"",
|
|
1211
|
+
"- [ ] **S01: Foundation** `risk:medium` `depends:[]`",
|
|
1212
|
+
"",
|
|
1213
|
+
].join("\n"),
|
|
1214
|
+
);
|
|
1215
|
+
t.after(() => {
|
|
1216
|
+
try { closeDatabase(); } catch { /* noop */ }
|
|
1217
|
+
rmSync(base, { recursive: true, force: true });
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
openDatabase(join(base, ".gsd", "gsd.db"));
|
|
1221
|
+
// The milestone IS registered — under the unique_milestone_ids suffixed id.
|
|
1222
|
+
insertMilestone({ id: "M007-v40fmq", title: "Navigation Footer System", status: "complete" });
|
|
1223
|
+
|
|
1224
|
+
const result = await reconcileBeforeDispatch(base, {
|
|
1225
|
+
invalidateStateCache: () => {},
|
|
1226
|
+
deriveState: async () => makeState(),
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
assert.equal(result.ok, true);
|
|
1230
|
+
assert.equal(
|
|
1231
|
+
result.blockers.some((b) => /unregistered/i.test(b) || /M007/.test(b)),
|
|
1232
|
+
false,
|
|
1233
|
+
"bare M007 derived from the slug must resolve to the registered M007-v40fmq row",
|
|
1234
|
+
);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1085
1237
|
test("ADR-017 (#5704): registered milestone (DB row present) → no drift", async (t) => {
|
|
1086
1238
|
const base = mkdtempSync(join(tmpdir(), "gsd-adr017-projmd-clean-"));
|
|
1087
1239
|
const milestoneDir = join(base, ".gsd", "phases", "01-test");
|
|
@@ -1977,7 +2129,7 @@ test("deriveState is pure: stale sketch healed only via reconcileBeforeDispatch"
|
|
|
1977
2129
|
});
|
|
1978
2130
|
writeFileSync(
|
|
1979
2131
|
join(base, ".gsd", "phases", "01-test", "01-02-PLAN.md"),
|
|
1980
|
-
"
|
|
2132
|
+
makeStalePlanContent("S02", [{ id: "T01", title: "Build the feature", done: false }]),
|
|
1981
2133
|
);
|
|
1982
2134
|
|
|
1983
2135
|
const { deriveState } = await import("../state.ts");
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests: awaitWorkerResume — headless worker pause survives without a resumer.
|
|
3
|
+
*
|
|
4
|
+
* Regression for #1273: a worker `pause` used to call a terminal pauseAuto()
|
|
5
|
+
* that exited the process with BLOCKED (10). On the headless path there is no
|
|
6
|
+
* resumer to send the matching `resume`, so the worker was stranded after its
|
|
7
|
+
* first unit. awaitWorkerResume lets the worker wait for the coordinator to lift
|
|
8
|
+
* the pause and, when no resumer responds, report "timeout" so the caller can
|
|
9
|
+
* degrade to in-process serialization instead of halting forever.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { describe, it } from "node:test";
|
|
14
|
+
import { mkdtempSync, mkdirSync, rmSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { tmpdir } from "node:os";
|
|
17
|
+
|
|
18
|
+
import { awaitWorkerResume, sendSignal } from "../session-status-io.ts";
|
|
19
|
+
|
|
20
|
+
function makeBase(): { base: string; cleanup: () => void } {
|
|
21
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-worker-resume-"));
|
|
22
|
+
mkdirSync(join(base, ".gsd", "parallel"), { recursive: true });
|
|
23
|
+
return { base, cleanup: () => rmSync(base, { recursive: true, force: true }) };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("awaitWorkerResume", () => {
|
|
27
|
+
it("returns 'resume' when the coordinator sends resume", async () => {
|
|
28
|
+
const { base, cleanup } = makeBase();
|
|
29
|
+
try {
|
|
30
|
+
sendSignal(base, "M001", "resume");
|
|
31
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
|
|
32
|
+
assert.equal(outcome, "resume");
|
|
33
|
+
} finally {
|
|
34
|
+
cleanup();
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("returns 'stop' when the coordinator sends stop", async () => {
|
|
39
|
+
const { base, cleanup } = makeBase();
|
|
40
|
+
try {
|
|
41
|
+
sendSignal(base, "M001", "stop");
|
|
42
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 500, pollMs: 20 });
|
|
43
|
+
assert.equal(outcome, "stop");
|
|
44
|
+
} finally {
|
|
45
|
+
cleanup();
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("returns 'timeout' when no resumer lifts the pause (headless, #1273)", async () => {
|
|
50
|
+
const { base, cleanup } = makeBase();
|
|
51
|
+
try {
|
|
52
|
+
const outcome = await awaitWorkerResume(base, "M001", { timeoutMs: 100, pollMs: 20 });
|
|
53
|
+
assert.equal(outcome, "timeout");
|
|
54
|
+
} finally {
|
|
55
|
+
cleanup();
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("ignores a repeated pause and still resumes when resume arrives", async () => {
|
|
60
|
+
const { base, cleanup } = makeBase();
|
|
61
|
+
try {
|
|
62
|
+
// A duplicate pause must not be treated as a resume, nor reset the wait.
|
|
63
|
+
sendSignal(base, "M001", "pause");
|
|
64
|
+
const waiting = awaitWorkerResume(base, "M001", { timeoutMs: 1000, pollMs: 20 });
|
|
65
|
+
// Deliver the resume shortly after the pause is consumed and ignored.
|
|
66
|
+
setTimeout(() => sendSignal(base, "M001", "resume"), 60);
|
|
67
|
+
const outcome = await waiting;
|
|
68
|
+
assert.equal(outcome, "resume");
|
|
69
|
+
} finally {
|
|
70
|
+
cleanup();
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -23,6 +23,7 @@ import { flushWorkflowProjections } from "../projection-flush.js";
|
|
|
23
23
|
import { writeManifest } from "../workflow-manifest.js";
|
|
24
24
|
import { appendEvent } from "../workflow-events.js";
|
|
25
25
|
import { logWarning } from "../workflow-logger.js";
|
|
26
|
+
import { writeReopenReason } from "../reopen-reason.js";
|
|
26
27
|
import { existsSync, unlinkSync } from "node:fs";
|
|
27
28
|
import { join, basename, dirname } from "node:path";
|
|
28
29
|
import { resolveTasksDir, resolveSlicePath, clearPathCache } from "../paths.js";
|
|
@@ -125,6 +126,20 @@ export async function handleReopenTask(
|
|
|
125
126
|
}
|
|
126
127
|
clearPathCache();
|
|
127
128
|
|
|
129
|
+
// ── Persist the reopen reason for the next execute-task dispatch (#1272) ──
|
|
130
|
+
// Without this, the re-dispatched executor receives the original task plan
|
|
131
|
+
// and verify command with zero signal that it was reopened — it re-runs the
|
|
132
|
+
// (still-green) scoped verify and re-completes the task, never touching the
|
|
133
|
+
// regression the gate actually caught. buildExecuteTaskPrompt claims this
|
|
134
|
+
// artifact and prepends the diagnosis to the next dispatch.
|
|
135
|
+
if (params.reason && params.reason.trim() !== "") {
|
|
136
|
+
try {
|
|
137
|
+
writeReopenReason(basePath, params.milestoneId, params.sliceId, params.taskId, params.reason);
|
|
138
|
+
} catch (reasonErr) {
|
|
139
|
+
logWarning("tool", `reopen-task reason persistence warning: ${(reasonErr as Error).message}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
128
143
|
// ── Post-mutation hook ───────────────────────────────────────────────────
|
|
129
144
|
try {
|
|
130
145
|
await flushWorkflowProjections(basePath, { milestoneId: params.milestoneId });
|