@opengsd/gsd-pi 1.2.0-dev.4c756166 → 1.2.0-dev.955e4da0
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/bg-shell/utilities.js +2 -2
- package/dist/resources/extensions/claude-code-cli/models.js +9 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +8 -2
- package/dist/resources/extensions/gsd/auto/orchestrator.js +33 -4
- package/dist/resources/extensions/gsd/auto/phases.js +6 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +8 -6
- package/dist/resources/extensions/gsd/auto-start.js +8 -13
- package/dist/resources/extensions/gsd/auto-worktree-repair.js +10 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +13 -270
- package/dist/resources/extensions/gsd/auto.js +4 -7
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +9 -6
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +32 -3
- package/dist/resources/extensions/gsd/bootstrap/write-gate.js +26 -4
- package/dist/resources/extensions/gsd/captures.js +5 -13
- package/dist/resources/extensions/gsd/closeout-recovery.js +3 -2
- package/dist/resources/extensions/gsd/commands/catalog.js +6 -62
- package/dist/resources/extensions/gsd/db/engine.js +755 -0
- package/dist/resources/extensions/gsd/db/queries.js +372 -0
- package/dist/resources/extensions/gsd/db/sql-constants.js +11 -0
- package/dist/resources/extensions/gsd/db/writers/cascades.js +194 -0
- package/dist/resources/extensions/gsd/db/writers/import-restore.js +182 -0
- package/dist/resources/extensions/gsd/db/writers/memory.js +149 -0
- package/dist/resources/extensions/gsd/db/writers/reconcile.js +458 -0
- package/dist/resources/extensions/gsd/db/writers/status.js +70 -0
- package/dist/resources/extensions/gsd/doctor-environment.js +8 -10
- package/dist/resources/extensions/gsd/doctor-git-checks.js +4 -3
- package/dist/resources/extensions/gsd/doctor-runtime-checks.js +9 -2
- package/dist/resources/extensions/gsd/git-service.js +1 -0
- package/dist/resources/extensions/gsd/gitignore.js +3 -0
- package/dist/resources/extensions/gsd/gsd-db.js +171 -2048
- package/dist/resources/extensions/gsd/guided-flow.js +34 -3
- package/dist/resources/extensions/gsd/migrate/safety.js +17 -9
- package/dist/resources/extensions/gsd/migration-auto-check.js +24 -3
- package/dist/resources/extensions/gsd/model-cost-table.js +1 -0
- package/dist/resources/extensions/gsd/model-router.js +3 -0
- package/dist/resources/extensions/gsd/parallel-merge.js +14 -11
- package/dist/resources/extensions/gsd/parallel-monitor-overlay.js +7 -5
- package/dist/resources/extensions/gsd/paths.js +10 -24
- package/dist/resources/extensions/gsd/preferences.js +14 -0
- package/dist/resources/extensions/gsd/recovery-classification.js +12 -1
- package/dist/resources/extensions/gsd/safety/evidence-collector.js +37 -4
- package/dist/resources/extensions/gsd/safety/evidence-cross-ref.js +7 -2
- package/dist/resources/extensions/gsd/safety/file-change-validator.js +10 -0
- package/dist/resources/extensions/gsd/state-transition-matrix.js +38 -0
- package/dist/resources/extensions/gsd/status-guards.js +56 -8
- package/dist/resources/extensions/gsd/tools/complete-slice.js +24 -43
- package/dist/resources/extensions/gsd/tools/exec-tool.js +5 -5
- package/dist/resources/extensions/gsd/tools/reopen-milestone.js +11 -29
- package/dist/resources/extensions/gsd/tools/reopen-slice.js +14 -33
- package/dist/resources/extensions/gsd/tools/skip-slice.js +18 -36
- package/dist/resources/extensions/gsd/undo.js +8 -7
- package/dist/resources/extensions/gsd/worktree-git-recovery.js +287 -0
- package/dist/resources/extensions/gsd/worktree-lifecycle.js +9 -1
- package/dist/resources/extensions/gsd/worktree-manager.js +45 -28
- package/dist/resources/extensions/gsd/worktree-placement.js +59 -0
- package/dist/resources/extensions/gsd/worktree-reentry.js +12 -8
- package/dist/resources/extensions/gsd/worktree-root.js +17 -6
- package/dist/resources/extensions/gsd/worktree-safety.js +8 -5
- package/dist/resources/extensions/gsd/worktree-session-state.js +12 -10
- package/dist/resources/skills/gsd-browser/SKILL.md +1 -1
- 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 +13 -13
- 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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +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 +13 -13
- package/dist/web/standalone/.next/server/chunks/{5047.js → 5942.js} +2 -2
- package/dist/web/standalone/.next/server/chunks/8357.js +1 -1
- 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/dist/worktree-status-banner.js +7 -3
- package/package.json +1 -1
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/dist/workflow-tools.d.ts.map +1 -1
- package/packages/mcp-server/dist/workflow-tools.js +30 -21
- package/packages/mcp-server/dist/workflow-tools.js.map +1 -1
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +266 -35
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +235 -46
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.js +3 -1
- package/packages/pi-coding-agent/dist/core/capability-patches.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/bg-shell/utilities.ts +2 -2
- package/src/resources/extensions/claude-code-cli/models.ts +9 -0
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +6 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +28 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +1 -1
- package/src/resources/extensions/gsd/auto/orchestrator.ts +39 -5
- package/src/resources/extensions/gsd/auto/phases.ts +10 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +12 -5
- package/src/resources/extensions/gsd/auto-start.ts +8 -14
- package/src/resources/extensions/gsd/auto-worktree-repair.ts +13 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +20 -280
- package/src/resources/extensions/gsd/auto.ts +12 -9
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +10 -6
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +32 -3
- package/src/resources/extensions/gsd/bootstrap/write-gate.ts +25 -3
- package/src/resources/extensions/gsd/captures.ts +5 -14
- package/src/resources/extensions/gsd/closeout-recovery.ts +2 -1
- package/src/resources/extensions/gsd/commands/catalog.ts +6 -68
- package/src/resources/extensions/gsd/db/engine.ts +809 -0
- package/src/resources/extensions/gsd/db/queries.ts +453 -0
- package/src/resources/extensions/gsd/db/sql-constants.ts +12 -0
- package/src/resources/extensions/gsd/db/writers/cascades.ts +237 -0
- package/src/resources/extensions/gsd/db/writers/import-restore.ts +310 -0
- package/src/resources/extensions/gsd/db/writers/memory.ts +220 -0
- package/src/resources/extensions/gsd/db/writers/reconcile.ts +500 -0
- package/src/resources/extensions/gsd/db/writers/status.ts +88 -0
- package/src/resources/extensions/gsd/doctor-environment.ts +8 -11
- package/src/resources/extensions/gsd/doctor-git-checks.ts +3 -3
- package/src/resources/extensions/gsd/doctor-runtime-checks.ts +10 -3
- package/src/resources/extensions/gsd/git-service.ts +1 -0
- package/src/resources/extensions/gsd/gitignore.ts +3 -0
- package/src/resources/extensions/gsd/gsd-db.ts +173 -2373
- package/src/resources/extensions/gsd/guided-flow.ts +34 -3
- package/src/resources/extensions/gsd/migrate/safety.ts +15 -7
- package/src/resources/extensions/gsd/migration-auto-check.ts +28 -3
- package/src/resources/extensions/gsd/model-cost-table.ts +1 -0
- package/src/resources/extensions/gsd/model-router.ts +3 -0
- package/src/resources/extensions/gsd/parallel-merge.ts +12 -9
- package/src/resources/extensions/gsd/parallel-monitor-overlay.ts +6 -5
- package/src/resources/extensions/gsd/paths.ts +9 -22
- package/src/resources/extensions/gsd/preferences.ts +18 -0
- package/src/resources/extensions/gsd/recovery-classification.ts +14 -1
- package/src/resources/extensions/gsd/safety/evidence-collector.ts +36 -4
- package/src/resources/extensions/gsd/safety/evidence-cross-ref.ts +7 -2
- package/src/resources/extensions/gsd/safety/file-change-validator.ts +14 -0
- package/src/resources/extensions/gsd/state-transition-matrix.ts +42 -0
- package/src/resources/extensions/gsd/status-guards.ts +59 -8
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +123 -0
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/auto-post-unit-evidence-crossref-4909.test.ts +46 -0
- package/src/resources/extensions/gsd/tests/auto-worktree-registry.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-worktree-repair.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/clear-stale-autostart.test.ts +22 -0
- package/src/resources/extensions/gsd/tests/evidence-xref-gsd-exec.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/file-change-validator.test.ts +33 -1
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/integration/auto-worktree.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/migration-auto-check.test.ts +85 -1
- package/src/resources/extensions/gsd/tests/recovery-classification-illegal-transition.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/register-hooks-depth-verification.test.ts +91 -1
- package/src/resources/extensions/gsd/tests/safety-harness-false-positives.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/session-switch-clears-pending-autostart.test.ts +108 -0
- package/src/resources/extensions/gsd/tests/single-writer-invariant.test.ts +43 -6
- package/src/resources/extensions/gsd/tests/state-transition-matrix.test.ts +36 -0
- package/src/resources/extensions/gsd/tests/status-guards.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/worktree-lifecycle.test.ts +41 -4
- package/src/resources/extensions/gsd/tests/worktree-manager.test.ts +22 -1
- package/src/resources/extensions/gsd/tests/worktree-placement.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/worktree-reentry.test.ts +1 -1
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +3 -1
- package/src/resources/extensions/gsd/tests/worktree-symlink-removal.test.ts +12 -6
- package/src/resources/extensions/gsd/tests/worktree-teardown-safety.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/write-gate.test.ts +42 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +23 -58
- package/src/resources/extensions/gsd/tools/exec-tool.ts +5 -5
- package/src/resources/extensions/gsd/tools/reopen-milestone.ts +11 -38
- package/src/resources/extensions/gsd/tools/reopen-slice.ts +14 -42
- package/src/resources/extensions/gsd/tools/skip-slice.ts +18 -44
- package/src/resources/extensions/gsd/undo.ts +9 -8
- package/src/resources/extensions/gsd/worktree-git-recovery.ts +308 -0
- package/src/resources/extensions/gsd/worktree-lifecycle.ts +10 -1
- package/src/resources/extensions/gsd/worktree-manager.ts +47 -28
- package/src/resources/extensions/gsd/worktree-placement.ts +63 -0
- package/src/resources/extensions/gsd/worktree-reentry.ts +10 -7
- package/src/resources/extensions/gsd/worktree-root.ts +17 -6
- package/src/resources/extensions/gsd/worktree-safety.ts +8 -5
- package/src/resources/extensions/gsd/worktree-session-state.ts +12 -10
- package/src/resources/skills/gsd-browser/SKILL.md +1 -1
- /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{DUFWcMFRH3iXh7d2fbrOF → C24pqUd-aru-l0Dp0gLZP}/_ssgManifest.js +0 -0
|
@@ -5517,6 +5517,129 @@ test("dispatch Worktree Safety wins before stuck detection for execute-task with
|
|
|
5517
5517
|
);
|
|
5518
5518
|
});
|
|
5519
5519
|
|
|
5520
|
+
test("dispatch Worktree Safety honors degraded branch fallback instead of demanding the canonical worktree root", async (t) => {
|
|
5521
|
+
_resetPendingResolve();
|
|
5522
|
+
|
|
5523
|
+
const ctx = makeMockCtx();
|
|
5524
|
+
const pi = makeMockPi();
|
|
5525
|
+
const notifications: string[] = [];
|
|
5526
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
5527
|
+
|
|
5528
|
+
// Worktree creation failed and the lifecycle fell back to the milestone
|
|
5529
|
+
// branch in the project root. The safety gate must validate against that
|
|
5530
|
+
// effective branch mode, not the configured worktree mode.
|
|
5531
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-degraded-"));
|
|
5532
|
+
t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
|
|
5533
|
+
|
|
5534
|
+
const s = makeLoopSession({
|
|
5535
|
+
basePath: projectRoot,
|
|
5536
|
+
originalBasePath: projectRoot,
|
|
5537
|
+
canonicalProjectRoot: projectRoot,
|
|
5538
|
+
isolationDegraded: true,
|
|
5539
|
+
});
|
|
5540
|
+
const deps = makeMockDeps({
|
|
5541
|
+
getIsolationMode: () => "worktree",
|
|
5542
|
+
});
|
|
5543
|
+
const result = await runDispatch(
|
|
5544
|
+
{
|
|
5545
|
+
ctx,
|
|
5546
|
+
pi,
|
|
5547
|
+
s,
|
|
5548
|
+
deps,
|
|
5549
|
+
prefs: undefined,
|
|
5550
|
+
iteration: 1,
|
|
5551
|
+
flowId: "test-flow",
|
|
5552
|
+
nextSeq: () => 1,
|
|
5553
|
+
},
|
|
5554
|
+
{
|
|
5555
|
+
state: {
|
|
5556
|
+
phase: "executing",
|
|
5557
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
5558
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
5559
|
+
activeTask: { id: "T01" },
|
|
5560
|
+
registry: [{ id: "M001", status: "active" }],
|
|
5561
|
+
blockers: [],
|
|
5562
|
+
} as any,
|
|
5563
|
+
mid: "M001",
|
|
5564
|
+
midTitle: "Test",
|
|
5565
|
+
},
|
|
5566
|
+
{
|
|
5567
|
+
recentUnits: [],
|
|
5568
|
+
stuckRecoveryAttempts: 0,
|
|
5569
|
+
consecutiveFinalizeTimeouts: 0,
|
|
5570
|
+
},
|
|
5571
|
+
);
|
|
5572
|
+
|
|
5573
|
+
assert.equal(result.action, "next", "dispatch must proceed under degraded branch isolation");
|
|
5574
|
+
assert.ok(
|
|
5575
|
+
!notifications.some((n) => n.includes("Worktree Safety failed")),
|
|
5576
|
+
"degraded branch fallback must not trip a false invalid-root",
|
|
5577
|
+
);
|
|
5578
|
+
assert.ok(!deps.callLog.includes("stopAuto"), "auto-mode must not stop on the degraded fallback");
|
|
5579
|
+
});
|
|
5580
|
+
|
|
5581
|
+
test("dispatch Worktree Safety honors stranded branch recovery instead of demanding the canonical worktree root", async (t) => {
|
|
5582
|
+
_resetPendingResolve();
|
|
5583
|
+
|
|
5584
|
+
const ctx = makeMockCtx();
|
|
5585
|
+
const pi = makeMockPi();
|
|
5586
|
+
const notifications: string[] = [];
|
|
5587
|
+
ctx.ui.notify = (msg: string) => { notifications.push(msg); };
|
|
5588
|
+
|
|
5589
|
+
// Bootstrap adopted stranded work by checking out the milestone branch in
|
|
5590
|
+
// the project root (strandedRecoveryIsolationMode = "branch"). Isolation is
|
|
5591
|
+
// NOT degraded — the adoption is intentional. The safety gate must validate
|
|
5592
|
+
// against the effective branch mode, not the configured worktree mode.
|
|
5593
|
+
const projectRoot = mkdtempSync(join(tmpdir(), "gsd-wt-safety-stranded-"));
|
|
5594
|
+
t.after(() => rmSync(projectRoot, { recursive: true, force: true }));
|
|
5595
|
+
|
|
5596
|
+
const s = makeLoopSession({
|
|
5597
|
+
basePath: projectRoot,
|
|
5598
|
+
originalBasePath: projectRoot,
|
|
5599
|
+
canonicalProjectRoot: projectRoot,
|
|
5600
|
+
strandedRecoveryIsolationMode: "branch",
|
|
5601
|
+
});
|
|
5602
|
+
const deps = makeMockDeps({
|
|
5603
|
+
getIsolationMode: () => "worktree",
|
|
5604
|
+
});
|
|
5605
|
+
const result = await runDispatch(
|
|
5606
|
+
{
|
|
5607
|
+
ctx,
|
|
5608
|
+
pi,
|
|
5609
|
+
s,
|
|
5610
|
+
deps,
|
|
5611
|
+
prefs: undefined,
|
|
5612
|
+
iteration: 1,
|
|
5613
|
+
flowId: "test-flow",
|
|
5614
|
+
nextSeq: () => 1,
|
|
5615
|
+
},
|
|
5616
|
+
{
|
|
5617
|
+
state: {
|
|
5618
|
+
phase: "executing",
|
|
5619
|
+
activeMilestone: { id: "M001", title: "Test", status: "active" },
|
|
5620
|
+
activeSlice: { id: "S01", title: "Slice 1" },
|
|
5621
|
+
activeTask: { id: "T01" },
|
|
5622
|
+
registry: [{ id: "M001", status: "active" }],
|
|
5623
|
+
blockers: [],
|
|
5624
|
+
} as any,
|
|
5625
|
+
mid: "M001",
|
|
5626
|
+
midTitle: "Test",
|
|
5627
|
+
},
|
|
5628
|
+
{
|
|
5629
|
+
recentUnits: [],
|
|
5630
|
+
stuckRecoveryAttempts: 0,
|
|
5631
|
+
consecutiveFinalizeTimeouts: 0,
|
|
5632
|
+
},
|
|
5633
|
+
);
|
|
5634
|
+
|
|
5635
|
+
assert.equal(result.action, "next", "dispatch must proceed under stranded branch recovery");
|
|
5636
|
+
assert.ok(
|
|
5637
|
+
!notifications.some((n) => n.includes("Worktree Safety failed")),
|
|
5638
|
+
"stranded branch recovery must not trip a false invalid-root",
|
|
5639
|
+
);
|
|
5640
|
+
assert.ok(!deps.callLog.includes("stopAuto"), "auto-mode must not stop on stranded branch recovery");
|
|
5641
|
+
});
|
|
5642
|
+
|
|
5520
5643
|
test("runDispatch runs stuck detection while artifact verification retry is pending (#5719)", async (t) => {
|
|
5521
5644
|
_resetPendingResolve();
|
|
5522
5645
|
|
|
@@ -306,7 +306,9 @@ test("pauseAuto records the expected worktree path when paused from project root
|
|
|
306
306
|
|
|
307
307
|
const meta = readPausedSessionMetadata(base);
|
|
308
308
|
assert.ok(meta);
|
|
309
|
-
|
|
309
|
+
// No worktree exists yet, so the recorded path is the canonical
|
|
310
|
+
// .gsd-worktrees/ creation location (worktree-placement seam).
|
|
311
|
+
assert.equal(meta.worktreePath, join(base, ".gsd-worktrees", "M001"));
|
|
310
312
|
} finally {
|
|
311
313
|
autoSession.reset();
|
|
312
314
|
try {
|
|
@@ -88,3 +88,49 @@ test("detects session execution tools supported by the evidence collector", () =
|
|
|
88
88
|
|
|
89
89
|
assert.equal(_hasExecutionToolCallsInSessionForTest(entries), true);
|
|
90
90
|
});
|
|
91
|
+
|
|
92
|
+
test("detects execution tool calls in bare agent-end messages (no session-entry wrapper)", () => {
|
|
93
|
+
// The auto loop passes opts.agentEndMessages as bare {role, content}
|
|
94
|
+
// messages — not {type: "message", message} session-manager entries.
|
|
95
|
+
const entries = [
|
|
96
|
+
{
|
|
97
|
+
role: "assistant",
|
|
98
|
+
content: [
|
|
99
|
+
{
|
|
100
|
+
type: "toolCall",
|
|
101
|
+
name: "Bash",
|
|
102
|
+
arguments: { command: "test -s index.html && grep -q localStorage index.html" },
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
assert.equal(_hasExecutionToolCallsInSessionForTest(entries), true);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("does not suppress for bare agent-end messages without execution tools", () => {
|
|
112
|
+
const entries = [
|
|
113
|
+
{
|
|
114
|
+
role: "assistant",
|
|
115
|
+
content: [
|
|
116
|
+
{ type: "text", text: "Task complete." },
|
|
117
|
+
{ type: "toolCall", name: "Write", arguments: { file_path: "index.html" } },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
assert.equal(_hasExecutionToolCallsInSessionForTest(entries), false);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("ignores bare user messages with toolCall-shaped content", () => {
|
|
126
|
+
const entries = [
|
|
127
|
+
{
|
|
128
|
+
role: "user",
|
|
129
|
+
content: [
|
|
130
|
+
{ type: "toolCall", name: "bash", arguments: { command: "echo hi" } },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
assert.equal(_hasExecutionToolCallsInSessionForTest(entries), false);
|
|
136
|
+
});
|
|
@@ -184,7 +184,7 @@ describe("auto-worktree workspace registry", () => {
|
|
|
184
184
|
git(["commit", "-m", "add milestone"], tempDir);
|
|
185
185
|
|
|
186
186
|
createAutoWorktree(tempDir, "M003");
|
|
187
|
-
const wtDir = join(tempDir, ".gsd
|
|
187
|
+
const wtDir = join(tempDir, ".gsd-worktrees", "M003");
|
|
188
188
|
writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
|
|
189
189
|
git(["add", "feature.txt"], wtDir);
|
|
190
190
|
git(["commit", "-m", "feat: implement M003"], wtDir);
|
|
@@ -216,7 +216,7 @@ describe("auto-worktree workspace registry", () => {
|
|
|
216
216
|
git(["commit", "-m", "add milestone"], tempDir);
|
|
217
217
|
|
|
218
218
|
createAutoWorktree(tempDir, "M004");
|
|
219
|
-
const wtDir = join(tempDir, ".gsd
|
|
219
|
+
const wtDir = join(tempDir, ".gsd-worktrees", "M004");
|
|
220
220
|
writeFileSync(join(wtDir, "feature.txt"), "implemented\n");
|
|
221
221
|
git(["add", "feature.txt"], wtDir);
|
|
222
222
|
git(["commit", "-m", "feat: implement M004"], wtDir);
|
|
@@ -35,7 +35,8 @@ test("repair target accepts a missing expected milestone worktree", () => {
|
|
|
35
35
|
|
|
36
36
|
assert.equal(result.ok, true);
|
|
37
37
|
if (result.ok) {
|
|
38
|
-
|
|
38
|
+
// No worktree exists yet, so the expected path is the canonical container.
|
|
39
|
+
assert.equal(result.expectedPath, join(base, ".gsd-worktrees", "M001"));
|
|
39
40
|
}
|
|
40
41
|
} finally {
|
|
41
42
|
cleanup(base);
|
|
@@ -192,7 +193,8 @@ test("paused metadata path resolves to the expected worktree while paused at pro
|
|
|
192
193
|
baseIsAutoWorktree: false,
|
|
193
194
|
});
|
|
194
195
|
|
|
195
|
-
|
|
196
|
+
// No worktree exists yet, so resolution lands at the canonical container.
|
|
197
|
+
assert.equal(result, join(base, ".gsd-worktrees", "M001"));
|
|
196
198
|
} finally {
|
|
197
199
|
cleanup(base);
|
|
198
200
|
}
|
|
@@ -73,4 +73,26 @@ describe("clear stale pending auto-start (#3667)", () => {
|
|
|
73
73
|
"pending auto-start gate must clear stale map entries for completed discussions",
|
|
74
74
|
);
|
|
75
75
|
});
|
|
76
|
+
|
|
77
|
+
test("guided-flow does not treat a live discuss turn as a stale pending entry", () => {
|
|
78
|
+
const source = readFileSync(join(__dirname, "..", "guided-flow.ts"), "utf-8");
|
|
79
|
+
assert.ok(
|
|
80
|
+
source.includes("!isAgentTurnInFlight(ctx)"),
|
|
81
|
+
"stale-entry deletion must be gated on no agent turn being in flight — a dispatched " +
|
|
82
|
+
"discuss turn can think for over 30s before writing its first artifact, and deleting " +
|
|
83
|
+
"its entry re-dispatches the workflow (duplicate interview + duplicate completion message)",
|
|
84
|
+
);
|
|
85
|
+
assert.ok(
|
|
86
|
+
source.includes('const milestoneHasDraft = !!resolveMilestoneFile(basePath, entry.milestoneId, "CONTEXT-DRAFT");'),
|
|
87
|
+
"stale-entry check must treat an existing CONTEXT-DRAFT as proof of an in-progress interview",
|
|
88
|
+
);
|
|
89
|
+
assert.ok(
|
|
90
|
+
source.includes("!milestoneHasDraft"),
|
|
91
|
+
"stale-entry deletion must require the CONTEXT-DRAFT to be absent",
|
|
92
|
+
);
|
|
93
|
+
assert.ok(
|
|
94
|
+
source.includes("ctx.hasPendingMessages"),
|
|
95
|
+
"in-flight detection must also cover dispatched-but-not-yet-started queued messages",
|
|
96
|
+
);
|
|
97
|
+
});
|
|
76
98
|
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
// Project/App: gsd-pi
|
|
2
|
+
// File Purpose: Regression tests for evidence cross-referencing of gsd_exec /
|
|
3
|
+
// gsd_uat_exec tool calls. Mirrors the live false-positive where an
|
|
4
|
+
// execute-task agent ran its verification commands through gsd_exec (script
|
|
5
|
+
// body in the `script` argument) and the cross-referencer reported
|
|
6
|
+
// "No bash tool call found" despite successful execution.
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
resetEvidence,
|
|
16
|
+
getEvidence,
|
|
17
|
+
recordToolCall,
|
|
18
|
+
recordToolResult,
|
|
19
|
+
isExecutionToolName,
|
|
20
|
+
type BashEvidence,
|
|
21
|
+
} from "../safety/evidence-collector.ts";
|
|
22
|
+
import { crossReferenceEvidence } from "../safety/evidence-cross-ref.ts";
|
|
23
|
+
|
|
24
|
+
function gsdExecResult(exitCode: number, id = "4858202d-2ed7-4a0a-9ef7-4e159e65da83"): unknown {
|
|
25
|
+
return {
|
|
26
|
+
content: [{
|
|
27
|
+
type: "text",
|
|
28
|
+
text: JSON.stringify({
|
|
29
|
+
operation: "gsd_exec",
|
|
30
|
+
id,
|
|
31
|
+
runtime: "bash",
|
|
32
|
+
exit_code: exitCode,
|
|
33
|
+
signal: null,
|
|
34
|
+
timed_out: false,
|
|
35
|
+
duration_ms: 272,
|
|
36
|
+
stdout_bytes: 592,
|
|
37
|
+
stderr_bytes: 0,
|
|
38
|
+
meta_path: `/tmp/does-not-exist/.gsd/exec/${id}.meta.json`,
|
|
39
|
+
}),
|
|
40
|
+
}],
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
test("evidence-xref: verification run through gsd_exec script matches the claimed command", () => {
|
|
45
|
+
resetEvidence();
|
|
46
|
+
|
|
47
|
+
// The live false positive: agent runs `node --test tests/verify-s01.test.js`
|
|
48
|
+
// inside a gsd_exec script with a cd prefix and exit-code echo suffix.
|
|
49
|
+
recordToolCall("tc-exec-1", "gsd_exec", {
|
|
50
|
+
script: 'cd /work/.gsd/worktrees/M001 && node --test tests/verify-s01.test.js; echo "EXIT=$?"',
|
|
51
|
+
purpose: "T02: run node --test contract checks against T01 index.html",
|
|
52
|
+
});
|
|
53
|
+
recordToolResult("tc-exec-1", "gsd_exec", gsdExecResult(0), false);
|
|
54
|
+
|
|
55
|
+
const mismatches = crossReferenceEvidence(
|
|
56
|
+
[{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
|
|
57
|
+
getEvidence(),
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
assert.deepEqual(mismatches, [], "gsd_exec-executed verification must not be flagged as missing");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("evidence-xref: multi-line gsd_exec script matches claims for each embedded command", () => {
|
|
64
|
+
resetEvidence();
|
|
65
|
+
|
|
66
|
+
recordToolCall("tc-exec-2", "gsd_exec", {
|
|
67
|
+
script: [
|
|
68
|
+
"cd /work/.gsd/worktrees/M001",
|
|
69
|
+
"sed -i '' \"s/'todos'/'tasks-v1'/\" index.html",
|
|
70
|
+
"node --test tests/verify-s01.test.js > /dev/null 2>&1",
|
|
71
|
+
'echo "BROKEN_EXIT=$?"',
|
|
72
|
+
].join("\n"),
|
|
73
|
+
purpose: "T02: deliberate contract break must fail, then restore",
|
|
74
|
+
});
|
|
75
|
+
recordToolResult("tc-exec-2", "gsd_exec", gsdExecResult(0), false);
|
|
76
|
+
|
|
77
|
+
const mismatches = crossReferenceEvidence(
|
|
78
|
+
[{ command: "node --test tests/verify-s01.test.js > /dev/null 2>&1", exitCode: 0, verdict: "passed" }],
|
|
79
|
+
getEvidence(),
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
assert.deepEqual(mismatches, [], "command embedded in a multi-line script must match");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("evidence-xref: claimed pass with failing gsd_exec exit_code is still an error", () => {
|
|
86
|
+
resetEvidence();
|
|
87
|
+
|
|
88
|
+
recordToolCall("tc-exec-3", "gsd_exec", {
|
|
89
|
+
script: "node --test tests/verify-s01.test.js",
|
|
90
|
+
purpose: "verification",
|
|
91
|
+
});
|
|
92
|
+
// gsd_exec reports failures via the JSON envelope's exit_code (and isError).
|
|
93
|
+
recordToolResult("tc-exec-3", "gsd_exec", gsdExecResult(1), true);
|
|
94
|
+
|
|
95
|
+
const mismatches = crossReferenceEvidence(
|
|
96
|
+
[{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
|
|
97
|
+
getEvidence(),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
assert.equal(mismatches.length, 1);
|
|
101
|
+
assert.equal(mismatches[0].severity, "error");
|
|
102
|
+
assert.match(mismatches[0].reason, /Claimed exitCode=0 but actual exitCode=1/);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("evidence-collector: gsd_uat_exec and MCP-namespaced variants are execution tools", () => {
|
|
106
|
+
assert.equal(isExecutionToolName("gsd_uat_exec"), true);
|
|
107
|
+
assert.equal(isExecutionToolName("mcp__gsd-workflow__gsd_uat_exec"), true);
|
|
108
|
+
assert.equal(isExecutionToolName("mcp__gsd-workflow__gsd_exec"), true);
|
|
109
|
+
|
|
110
|
+
resetEvidence();
|
|
111
|
+
recordToolCall("tc-uat-1", "gsd_uat_exec", { script: "curl -fsS http://localhost:3000/health" });
|
|
112
|
+
const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
|
|
113
|
+
assert.equal(bash.length, 1, "gsd_uat_exec must record bash evidence");
|
|
114
|
+
assert.equal(bash[0].command, "curl -fsS http://localhost:3000/health");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("evidence-xref: blank-command evidence does not satisfy arbitrary claims", () => {
|
|
118
|
+
// Before script extraction existed, gsd_exec calls were recorded with
|
|
119
|
+
// command: "" — and `"x".includes("")` made them match every claim,
|
|
120
|
+
// masking genuine fabrications. Blank entries must never match.
|
|
121
|
+
const mismatches = crossReferenceEvidence(
|
|
122
|
+
[{ command: "node --test tests/verify-s01.test.js", exitCode: 0, verdict: "passed" }],
|
|
123
|
+
[{
|
|
124
|
+
kind: "bash",
|
|
125
|
+
toolCallId: "tc-blank",
|
|
126
|
+
command: "",
|
|
127
|
+
exitCode: 0,
|
|
128
|
+
outputSnippet: "",
|
|
129
|
+
timestamp: 1,
|
|
130
|
+
}],
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
assert.equal(mismatches.length, 1);
|
|
134
|
+
assert.equal(mismatches[0].severity, "warning");
|
|
135
|
+
assert.match(mismatches[0].reason, /No bash tool call found/);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("evidence-collector: exit code falls back to .gsd/exec meta.json when result text omits it", (t) => {
|
|
139
|
+
const dir = mkdtempSync(join(tmpdir(), "gsd-exec-meta-"));
|
|
140
|
+
t.after(() => rmSync(dir, { recursive: true, force: true }));
|
|
141
|
+
|
|
142
|
+
const metaPath = join(dir, "run-1.meta.json");
|
|
143
|
+
writeFileSync(metaPath, JSON.stringify({ id: "run-1", exit_code: 7 }));
|
|
144
|
+
|
|
145
|
+
resetEvidence();
|
|
146
|
+
recordToolCall("tc-meta-1", "gsd_exec", { script: "exit 7" });
|
|
147
|
+
// Truncated result: meta_path survives but exit_code was cut off.
|
|
148
|
+
recordToolResult(
|
|
149
|
+
"tc-meta-1",
|
|
150
|
+
"gsd_exec",
|
|
151
|
+
{ content: [{ type: "text", text: `{"operation":"gsd_exec","meta_path":${JSON.stringify(metaPath)}` }] },
|
|
152
|
+
false,
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
const bash = getEvidence().filter((e): e is BashEvidence => e.kind === "bash");
|
|
156
|
+
assert.equal(bash[0].exitCode, 7, "exit code must be recovered from meta.json");
|
|
157
|
+
});
|
|
@@ -5,7 +5,7 @@ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
|
-
import { validateFileChanges } from "../safety/file-change-validator.ts";
|
|
8
|
+
import { validateFileChanges, effectiveFileChangeAllowlist } from "../safety/file-change-validator.ts";
|
|
9
9
|
|
|
10
10
|
function git(cwd: string, ...args: string[]): string {
|
|
11
11
|
return execFileSync("git", args, {
|
|
@@ -106,3 +106,35 @@ test("validateFileChanges ignores inline descriptions in expected output paths",
|
|
|
106
106
|
"described expected output should not trigger unexpected-file warnings",
|
|
107
107
|
);
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
test("effectiveFileChangeAllowlist includes .gitignore when GSD manages it", () => {
|
|
111
|
+
assert.deepEqual(effectiveFileChangeAllowlist([], undefined), [".gitignore"]);
|
|
112
|
+
assert.deepEqual(effectiveFileChangeAllowlist(["docs/**"], true), ["docs/**", ".gitignore"]);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("effectiveFileChangeAllowlist keeps .gitignore auditable when management is disabled", () => {
|
|
116
|
+
assert.deepEqual(effectiveFileChangeAllowlist(["docs/**"], false), ["docs/**"]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("GSD-managed .gitignore edit swept into a task commit is not flagged", (t) => {
|
|
120
|
+
const base = mkdtempSync(join(tmpdir(), "gsd-file-change-validator-"));
|
|
121
|
+
t.after(() => rmSync(base, { recursive: true, force: true }));
|
|
122
|
+
|
|
123
|
+
git(base, "init");
|
|
124
|
+
git(base, "config", "user.email", "test@example.com");
|
|
125
|
+
git(base, "config", "user.name", "Test User");
|
|
126
|
+
writeFileSync(join(base, "index.html"), "<main></main>\n");
|
|
127
|
+
writeFileSync(join(base, ".gitignore"), "# ── GSD baseline (auto-generated) ──\n.gsd\n");
|
|
128
|
+
git(base, "add", ".");
|
|
129
|
+
git(base, "commit", "-m", "task commit with swept gitignore");
|
|
130
|
+
|
|
131
|
+
const audit = validateFileChanges(
|
|
132
|
+
base,
|
|
133
|
+
["index.html"],
|
|
134
|
+
[],
|
|
135
|
+
effectiveFileChangeAllowlist([], undefined),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
assert.ok(audit, "audit should be produced");
|
|
139
|
+
assert.deepEqual(audit.unexpectedFiles, [], ".gitignore must not be flagged when GSD manages it");
|
|
140
|
+
});
|
package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts
CHANGED
|
@@ -746,7 +746,7 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
|
|
|
746
746
|
});
|
|
747
747
|
|
|
748
748
|
test("#2156: mergeMilestoneToMain removes external-state worktrees using the milestone branch name", () => {
|
|
749
|
-
const { repo
|
|
749
|
+
const { repo } = freshRepoWithExternalGsd();
|
|
750
750
|
const wtPath = createAutoWorktree(repo, "M215");
|
|
751
751
|
|
|
752
752
|
addSliceToMilestone(repo, wtPath, "M215", "S01", "External cleanup", [
|
|
@@ -754,9 +754,10 @@ describe("auto-worktree-milestone-merge", { timeout: 300_000 }, () => {
|
|
|
754
754
|
]);
|
|
755
755
|
|
|
756
756
|
const realWtPath = realpathSync(wtPath);
|
|
757
|
-
assert.
|
|
758
|
-
realWtPath
|
|
759
|
-
|
|
757
|
+
assert.equal(
|
|
758
|
+
realWtPath,
|
|
759
|
+
join(repo, ".gsd-worktrees", "M215"),
|
|
760
|
+
`worktree should use canonical path under project root, got ${realWtPath}`,
|
|
760
761
|
);
|
|
761
762
|
|
|
762
763
|
// Recreate the exact divergence from #1852: local .gsd/ is replaced with a
|
|
@@ -157,7 +157,7 @@ describe("auto-worktree lifecycle", () => {
|
|
|
157
157
|
try {
|
|
158
158
|
const wtPath = createAutoWorktree(tempDir, "M001");
|
|
159
159
|
const realWtPath = realpathSync(wtPath);
|
|
160
|
-
assert.
|
|
160
|
+
assert.equal(realWtPath, join(tempDir, ".gsd-worktrees", "M001"), "worktree uses canonical path under project root, not through the .gsd symlink");
|
|
161
161
|
|
|
162
162
|
_resetAutoWorktreeOriginalBaseForTests();
|
|
163
163
|
process.chdir(realWtPath);
|
|
@@ -293,11 +293,12 @@ describe('git-service', async () => {
|
|
|
293
293
|
|
|
294
294
|
assert.deepStrictEqual(
|
|
295
295
|
RUNTIME_EXCLUSION_PATHS.length,
|
|
296
|
-
|
|
297
|
-
"exactly
|
|
296
|
+
17,
|
|
297
|
+
"exactly 17 runtime exclusion paths"
|
|
298
298
|
);
|
|
299
299
|
|
|
300
300
|
const expectedPaths = [
|
|
301
|
+
".gsd-worktrees/",
|
|
301
302
|
".gsd/activity/",
|
|
302
303
|
".gsd/audit/",
|
|
303
304
|
".gsd/forensics/",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
|
-
import { copyFileSync, mkdtempSync, renameSync, rmSync } from "node:fs";
|
|
3
|
+
import { copyFileSync, mkdirSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import test from "node:test";
|
|
@@ -288,6 +288,90 @@ test("migration auto-check refreshes a stale open DB handle before comparing", a
|
|
|
288
288
|
}
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
+
function writeScratchMilestoneDir(base: string, milestoneId: string, file?: string): void {
|
|
292
|
+
const dir = join(base, ".gsd", "milestones", milestoneId);
|
|
293
|
+
mkdirSync(dir, { recursive: true });
|
|
294
|
+
if (file) writeFileSync(join(dir, file), `# ${milestoneId} discussion context\n`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
test("migration auto-check ignores discussion-scratch milestone dirs (CONTEXT only, no DB row)", async () => {
|
|
298
|
+
const base = makeBase();
|
|
299
|
+
try {
|
|
300
|
+
await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01
|
|
301
|
+
assert.equal(await ensureDbOpen(base), true);
|
|
302
|
+
insertMilestone({ id: "M001", title: "Legacy Milestone", status: "active" });
|
|
303
|
+
insertSlice({ id: "S01", milestoneId: "M001", title: "Legacy Slice", status: "pending", risk: "medium", depends: [], demo: "Legacy slice demo", sequence: 1 });
|
|
304
|
+
insertTask({ id: "T01", sliceId: "S01", milestoneId: "M001", title: "Legacy Task", status: "pending" });
|
|
305
|
+
|
|
306
|
+
// Mid-discussion artifacts: dirs with no ROADMAP and no DB row. The queued
|
|
307
|
+
// DB row is only inserted at discussion handoff, so these are expected to
|
|
308
|
+
// be DB-less — not drift, and recover must not be recommended (it would
|
|
309
|
+
// import them as ghost active milestones).
|
|
310
|
+
writeScratchMilestoneDir(base, "M002", "M002-CONTEXT.md");
|
|
311
|
+
writeScratchMilestoneDir(base, "M003", "M003-CONTEXT-DRAFT.md");
|
|
312
|
+
writeScratchMilestoneDir(base, "M004"); // empty dir
|
|
313
|
+
|
|
314
|
+
const result = await checkMarkdownHierarchyAgainstDb(base);
|
|
315
|
+
assert.equal(result.action, "none");
|
|
316
|
+
assert.equal(result.reason, "in-sync");
|
|
317
|
+
assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
|
|
318
|
+
} finally {
|
|
319
|
+
cleanup(base);
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("migration auto-check stays quiet mid-first-discussion (scratch dir over empty DB)", async () => {
|
|
324
|
+
const base = makeBase();
|
|
325
|
+
try {
|
|
326
|
+
await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
|
|
327
|
+
assert.equal(await ensureDbOpen(base), true);
|
|
328
|
+
writeScratchMilestoneDir(base, "M001", "M001-CONTEXT.md");
|
|
329
|
+
|
|
330
|
+
const result = await checkMarkdownHierarchyAgainstDb(base);
|
|
331
|
+
assert.equal(result.action, "none");
|
|
332
|
+
assert.equal(result.reason, "no-markdown");
|
|
333
|
+
} finally {
|
|
334
|
+
cleanup(base);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("migration auto-check still reports real drift with scratch dirs excluded from counts", async () => {
|
|
339
|
+
const base = makeBase();
|
|
340
|
+
try {
|
|
341
|
+
await writeGSDDirectory(projectFixture(), base); // markdown: M001 / S01 / T01, DB empty
|
|
342
|
+
assert.equal(await ensureDbOpen(base), true);
|
|
343
|
+
writeScratchMilestoneDir(base, "M002", "M002-CONTEXT.md");
|
|
344
|
+
|
|
345
|
+
const result = await checkMarkdownHierarchyAgainstDb(base);
|
|
346
|
+
assert.equal(result.action, "recovery-required");
|
|
347
|
+
assert.equal(result.reason, "db-empty");
|
|
348
|
+
assert.equal(result.recoveryCommand, "/gsd recover --confirm");
|
|
349
|
+
// The scratch dir must not inflate the reported markdown count.
|
|
350
|
+
assert.deepEqual(result.markdown, { milestones: 1, slices: 1, tasks: 1 });
|
|
351
|
+
} finally {
|
|
352
|
+
cleanup(base);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("migration auto-check still compares a roadmapless milestone that HAS a DB row", async () => {
|
|
357
|
+
const base = makeBase();
|
|
358
|
+
try {
|
|
359
|
+
await writeGSDDirectory({ projectContent: "# P\n", decisionsContent: "", requirements: [], milestones: [] }, base);
|
|
360
|
+
assert.equal(await ensureDbOpen(base), true);
|
|
361
|
+
// Post-handoff queued milestone: CONTEXT-only dir WITH a DB row. It must
|
|
362
|
+
// stay in the comparison (both sides have it → in-sync).
|
|
363
|
+
insertMilestone({ id: "M001", title: "M001", status: "queued" });
|
|
364
|
+
writeScratchMilestoneDir(base, "M001", "M001-CONTEXT.md");
|
|
365
|
+
|
|
366
|
+
const result = await checkMarkdownHierarchyAgainstDb(base);
|
|
367
|
+
assert.equal(result.action, "none");
|
|
368
|
+
assert.equal(result.reason, "in-sync");
|
|
369
|
+
assert.deepEqual(result.markdown, { milestones: 1, slices: 0, tasks: 0 });
|
|
370
|
+
} finally {
|
|
371
|
+
cleanup(base);
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
291
375
|
test("rebuildMarkdownProjectionsFromDb realigns markdown when DB holds extra rows", async () => {
|
|
292
376
|
const base = makeBase();
|
|
293
377
|
try {
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// GSD — recovery-classification: illegal-transition kind (ADR-030)
|
|
2
|
+
|
|
3
|
+
import test from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
|
|
6
|
+
import { classifyFailure } from "../recovery-classification.ts";
|
|
7
|
+
import { IllegalPhaseTransitionError } from "../state-transition-matrix.ts";
|
|
8
|
+
|
|
9
|
+
test("classifyFailure recognizes IllegalPhaseTransitionError by class and escalates", () => {
|
|
10
|
+
const classification = classifyFailure({
|
|
11
|
+
error: new IllegalPhaseTransitionError("executing", "complete"),
|
|
12
|
+
unitType: "execute-task",
|
|
13
|
+
unitId: "T-1",
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
assert.equal(classification.failureKind, "illegal-transition");
|
|
17
|
+
assert.equal(classification.action, "escalate");
|
|
18
|
+
assert.equal(classification.exitReason, "illegal-transition");
|
|
19
|
+
assert.match(classification.reason, /Illegal phase transition/);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("classifyFailure routes an explicit illegal-transition failureKind to the same case", () => {
|
|
23
|
+
const classification = classifyFailure({
|
|
24
|
+
error: new Error("derived edge rejected"),
|
|
25
|
+
failureKind: "illegal-transition",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
assert.equal(classification.failureKind, "illegal-transition");
|
|
29
|
+
assert.equal(classification.action, "escalate");
|
|
30
|
+
});
|