gsd-pi 2.77.0-dev.c9c8932c6 → 2.77.0-dev.eaa4973bc
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-web-branch.d.ts +1 -0
- package/dist/cli-web-branch.js +3 -0
- package/dist/cli.js +24 -2
- package/dist/provider-migrations.d.ts +18 -0
- package/dist/provider-migrations.js +14 -0
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +14 -10
- package/dist/resources/extensions/gsd/abandon-detect.js +44 -0
- package/dist/resources/extensions/gsd/auto/resolve.js +24 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -2
- package/dist/resources/extensions/gsd/auto/turn-epoch.js +95 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +24 -0
- package/dist/resources/extensions/gsd/auto-loop.js +1 -1
- package/dist/resources/extensions/gsd/auto-post-unit.js +31 -0
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +11 -5
- package/dist/resources/extensions/gsd/auto-unit-closeout.js +11 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +58 -8
- package/dist/resources/extensions/gsd/auto.js +4 -0
- package/dist/resources/extensions/gsd/bootstrap/provider-error-resume.js +4 -2
- package/dist/resources/extensions/gsd/commands/handlers/workflow.js +31 -4
- package/dist/resources/extensions/gsd/file-lock.js +49 -9
- package/dist/resources/extensions/gsd/git-service.js +1 -0
- package/dist/resources/extensions/gsd/gitignore.js +1 -0
- package/dist/resources/extensions/gsd/guided-flow-queue.js +4 -1
- package/dist/resources/extensions/gsd/journal.js +17 -2
- package/dist/resources/extensions/gsd/milestone-actions.js +15 -0
- package/dist/resources/extensions/gsd/reports.js +5 -4
- package/dist/resources/extensions/gsd/tools/complete-slice.js +21 -0
- package/dist/resources/extensions/gsd/tools/complete-task.js +31 -0
- package/dist/resources/extensions/gsd/uok/audit.js +18 -2
- package/dist/resources/extensions/gsd/workflow-logger.js +10 -2
- package/dist/resources/extensions/gsd/worktree-manager.js +1 -0
- package/dist/resources/extensions/mcp-client/auth.js +10 -1
- package/dist/resources/extensions/mcp-client/index.js +118 -9
- package/dist/resources/skills/create-skill/SKILL.md +2 -2
- package/dist/resources/skills/create-skill/references/gsd-skill-ecosystem.md +4 -4
- package/dist/resources/skills/create-skill/workflows/audit-skill.md +4 -4
- package/dist/resources/skills/create-skill/workflows/create-new-skill.md +5 -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 +13 -13
- 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/react-loadable-manifest.json +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.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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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 +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/notifications/route.js +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 +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 +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 +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 +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 +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 +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 +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 +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/skill-health/route.js +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 +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 +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 +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 +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 +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 +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/1926.js +1 -0
- package/dist/web/standalone/.next/server/chunks/6897.js +1 -1
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/middleware-manifest.json +5 -5
- package/dist/web/standalone/.next/server/middleware-react-loadable-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/web/standalone/.next/static/chunks/2826.e9f5195e91f9cad2.js +11 -0
- package/dist/web/standalone/.next/static/chunks/{webpack-1832629448831fdc.js → webpack-2e68521d7c82f7c2.js} +1 -1
- package/package.json +3 -3
- package/packages/mcp-server/README.md +3 -3
- package/packages/mcp-server/dist/env-writer.d.ts +1 -0
- package/packages/mcp-server/dist/env-writer.d.ts.map +1 -1
- package/packages/mcp-server/dist/env-writer.js +74 -6
- package/packages/mcp-server/dist/env-writer.js.map +1 -1
- package/packages/mcp-server/dist/server.d.ts.map +1 -1
- package/packages/mcp-server/dist/server.js +25 -2
- package/packages/mcp-server/dist/server.js.map +1 -1
- package/packages/mcp-server/src/env-writer.test.ts +79 -1
- package/packages/mcp-server/src/env-writer.ts +76 -6
- package/packages/mcp-server/src/server.ts +29 -2
- package/packages/mcp-server/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js +21 -16
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.js +130 -0
- package/packages/pi-coding-agent/dist/core/compaction-orchestrator.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +4 -1
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.js +19 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js +15 -6
- package/packages/pi-coding-agent/dist/modes/interactive/components/__tests__/provider-display-name.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +14 -5
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts +7 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js +31 -9
- package/packages/pi-coding-agent/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/packages/pi-coding-agent/src/core/compaction-orchestrator.test.ts +154 -0
- package/packages/pi-coding-agent/src/core/compaction-orchestrator.ts +24 -17
- package/packages/pi-coding-agent/src/core/sdk.test.ts +25 -1
- package/packages/pi-coding-agent/src/core/sdk.ts +10 -3
- package/packages/pi-coding-agent/src/modes/interactive/components/__tests__/provider-display-name.test.ts +17 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +14 -5
- package/packages/pi-coding-agent/src/modes/interactive/components/model-selector.ts +45 -11
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js +19 -0
- package/packages/pi-tui/dist/__tests__/stdin-buffer.test.js.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.d.ts +7 -0
- package/packages/pi-tui/dist/stdin-buffer.d.ts.map +1 -1
- package/packages/pi-tui/dist/stdin-buffer.js +20 -0
- package/packages/pi-tui/dist/stdin-buffer.js.map +1 -1
- package/packages/pi-tui/src/__tests__/stdin-buffer.test.ts +25 -0
- package/packages/pi-tui/src/stdin-buffer.ts +26 -0
- package/packages/pi-tui/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +14 -10
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +22 -16
- package/src/resources/extensions/gsd/abandon-detect.ts +62 -0
- package/src/resources/extensions/gsd/auto/resolve.ts +29 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +16 -2
- package/src/resources/extensions/gsd/auto/turn-epoch.ts +108 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +20 -0
- package/src/resources/extensions/gsd/auto-loop.ts +1 -1
- package/src/resources/extensions/gsd/auto-post-unit.ts +30 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +12 -5
- package/src/resources/extensions/gsd/auto-unit-closeout.ts +14 -3
- package/src/resources/extensions/gsd/auto-worktree.ts +60 -5
- package/src/resources/extensions/gsd/auto.ts +5 -0
- package/src/resources/extensions/gsd/bootstrap/provider-error-resume.ts +6 -2
- package/src/resources/extensions/gsd/commands/handlers/workflow.ts +27 -8
- package/src/resources/extensions/gsd/file-lock.ts +84 -11
- package/src/resources/extensions/gsd/git-service.ts +1 -0
- package/src/resources/extensions/gsd/gitignore.ts +1 -0
- package/src/resources/extensions/gsd/guided-flow-queue.ts +4 -1
- package/src/resources/extensions/gsd/journal.ts +27 -2
- package/src/resources/extensions/gsd/milestone-actions.ts +18 -0
- package/src/resources/extensions/gsd/reports.ts +5 -4
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +11 -2
- package/src/resources/extensions/gsd/tests/auto-mode-guards.test.ts +79 -0
- package/src/resources/extensions/gsd/tests/file-lock.test.ts +86 -12
- package/src/resources/extensions/gsd/tests/integration/auto-worktree-milestone-merge.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/integration/git-service.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/mcp-client-security.test.ts +76 -0
- package/src/resources/extensions/gsd/tests/provider-errors.test.ts +39 -25
- package/src/resources/extensions/gsd/tests/queue-auto-guard.test.ts +181 -0
- package/src/resources/extensions/gsd/tests/quick-auto-guard.test.ts +13 -7
- package/src/resources/extensions/gsd/tests/require-slice-discussion-dispatch.test.ts +170 -0
- package/src/resources/extensions/gsd/tests/rewrite-docs-abandon-detect.test.ts +195 -0
- package/src/resources/extensions/gsd/tests/stuck-detection-coverage.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/triage-resolution.test.ts +50 -2
- package/src/resources/extensions/gsd/tests/turn-epoch.test.ts +162 -0
- package/src/resources/extensions/gsd/tools/complete-slice.ts +38 -0
- package/src/resources/extensions/gsd/tools/complete-task.ts +49 -0
- package/src/resources/extensions/gsd/uok/audit.ts +20 -2
- package/src/resources/extensions/gsd/workflow-logger.ts +22 -3
- package/src/resources/extensions/gsd/worktree-manager.ts +1 -0
- package/src/resources/extensions/mcp-client/auth.ts +12 -1
- package/src/resources/extensions/mcp-client/index.ts +129 -10
- package/src/resources/skills/create-skill/SKILL.md +2 -2
- package/src/resources/skills/create-skill/references/gsd-skill-ecosystem.md +4 -4
- package/src/resources/skills/create-skill/workflows/audit-skill.md +4 -4
- package/src/resources/skills/create-skill/workflows/create-new-skill.md +5 -5
- package/dist/web/standalone/.next/server/chunks/7461.js +0 -1
- package/dist/web/standalone/.next/static/chunks/2826.d445fb428ef41fa1.js +0 -9
- /package/dist/web/standalone/.next/static/{H51SzzS_EEgdBsDVhrLBv → 5wbu35_C2_MQ3Jj1lEVDx}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{H51SzzS_EEgdBsDVhrLBv → 5wbu35_C2_MQ3Jj1lEVDx}/_ssgManifest.js +0 -0
|
@@ -26,6 +26,9 @@ import {
|
|
|
26
26
|
isDbAvailable,
|
|
27
27
|
getMilestone,
|
|
28
28
|
getMilestoneSlices,
|
|
29
|
+
closeDatabase,
|
|
30
|
+
openDatabase,
|
|
31
|
+
getDbPath,
|
|
29
32
|
} from "./gsd-db.js";
|
|
30
33
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
31
34
|
import { execFileSync } from "node:child_process";
|
|
@@ -1668,22 +1671,51 @@ export function mergeMilestoneToMain(
|
|
|
1668
1671
|
const milestonesDir = join(gsdRoot(originalBasePath_), "milestones");
|
|
1669
1672
|
const shelterDir = join(gsdRoot(originalBasePath_), ".milestone-shelter");
|
|
1670
1673
|
const shelteredDirs: string[] = [];
|
|
1674
|
+
let shelterRestored = false;
|
|
1671
1675
|
|
|
1672
1676
|
// Helper: restore sheltered milestone directories (#2505).
|
|
1673
1677
|
// Called on both success and error paths to ensure queued CONTEXT files
|
|
1674
|
-
// are never permanently lost.
|
|
1678
|
+
// are never permanently lost. Idempotent — the error path may fire after
|
|
1679
|
+
// the success path has already restored and removed the shelter dir; a
|
|
1680
|
+
// second call is a no-op instead of logging a misleading "shelter restore
|
|
1681
|
+
// failed: ENOENT" error for shelter sources that were cleaned up legitimately.
|
|
1675
1682
|
const restoreShelter = (): void => {
|
|
1683
|
+
if (shelterRestored) return;
|
|
1684
|
+
shelterRestored = true;
|
|
1676
1685
|
if (shelteredDirs.length === 0) return;
|
|
1686
|
+
let restoreFailed = false;
|
|
1677
1687
|
for (const dirName of shelteredDirs) {
|
|
1688
|
+
const src = join(shelterDir, dirName);
|
|
1689
|
+
// If the shelter source is missing the restore cannot proceed for this
|
|
1690
|
+
// entry. Distinguish "legitimately missing" (shelter dir removed by a
|
|
1691
|
+
// prior successful restore or never copied) from a surprising ENOENT
|
|
1692
|
+
// inside an otherwise-populated shelter.
|
|
1693
|
+
if (!existsSync(src)) {
|
|
1694
|
+
logWarning(
|
|
1695
|
+
"worktree",
|
|
1696
|
+
`shelter source missing for ${dirName}; skipping restore (shelter already cleaned or entry never staged)`,
|
|
1697
|
+
);
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1678
1700
|
try {
|
|
1679
1701
|
mkdirSync(milestonesDir, { recursive: true });
|
|
1680
|
-
cpSync(
|
|
1702
|
+
cpSync(src, join(milestonesDir, dirName), { recursive: true, force: true });
|
|
1681
1703
|
} catch (err) { /* best-effort */
|
|
1682
|
-
|
|
1704
|
+
restoreFailed = true;
|
|
1705
|
+
logError("worktree", `shelter restore failed (${dirName}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1683
1706
|
}
|
|
1684
1707
|
}
|
|
1685
|
-
|
|
1686
|
-
|
|
1708
|
+
// Preserve the shelter if any per-entry restore failed — it is the only
|
|
1709
|
+
// surviving copy of the queued milestone dirs (sources were deleted during
|
|
1710
|
+
// shelter). Deleting it here would permanently lose those files (#2505).
|
|
1711
|
+
if (restoreFailed) {
|
|
1712
|
+
logWarning("worktree", `shelter retained at ${shelterDir} — manual recovery required for unrestored entries`);
|
|
1713
|
+
return;
|
|
1714
|
+
}
|
|
1715
|
+
if (existsSync(shelterDir)) {
|
|
1716
|
+
try { rmSync(shelterDir, { recursive: true, force: true }); } catch (err) { /* best-effort */
|
|
1717
|
+
logWarning("worktree", `shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1718
|
+
}
|
|
1687
1719
|
}
|
|
1688
1720
|
};
|
|
1689
1721
|
|
|
@@ -1720,6 +1752,21 @@ export function mergeMilestoneToMain(
|
|
|
1720
1752
|
// entries (e.g. a gitignored `.gsd` symlink under ADR-002) (#4573).
|
|
1721
1753
|
// Queued CONTEXT files under `.gsd/milestones/*` are already sheltered
|
|
1722
1754
|
// in step 7 above, so they won't be swept into the stash.
|
|
1755
|
+
// On Windows, SQLite holds mandatory file locks on the gsd.db WAL/SHM
|
|
1756
|
+
// sidecars while the connection is open. `git stash --include-untracked`
|
|
1757
|
+
// walks those files and fails with EBUSY (#4704). Close the DB before
|
|
1758
|
+
// stashing so Windows releases the handles; reopen after. No-op on
|
|
1759
|
+
// POSIX, where advisory locks don't block git.
|
|
1760
|
+
const needsDbCycle = process.platform === "win32" && isDbAvailable();
|
|
1761
|
+
const dbPathToReopen = needsDbCycle ? getDbPath() : null;
|
|
1762
|
+
if (needsDbCycle) {
|
|
1763
|
+
try {
|
|
1764
|
+
closeDatabase();
|
|
1765
|
+
} catch (err) {
|
|
1766
|
+
logWarning("worktree", `pre-stash db close failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1723
1770
|
let stashed = false;
|
|
1724
1771
|
try {
|
|
1725
1772
|
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
@@ -1741,6 +1788,14 @@ export function mergeMilestoneToMain(
|
|
|
1741
1788
|
logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1742
1789
|
}
|
|
1743
1790
|
|
|
1791
|
+
if (needsDbCycle && dbPathToReopen) {
|
|
1792
|
+
try {
|
|
1793
|
+
openDatabase(dbPathToReopen);
|
|
1794
|
+
} catch (err) {
|
|
1795
|
+
logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1744
1799
|
// 7b. Clean up stale merge state before attempting squash merge (#2912).
|
|
1745
1800
|
// A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
|
|
1746
1801
|
// or interrupted operation) causes `git merge --squash` to refuse with
|
|
@@ -485,6 +485,11 @@ export function isAutoActive(): boolean {
|
|
|
485
485
|
return s.active;
|
|
486
486
|
}
|
|
487
487
|
|
|
488
|
+
/** Test-only seam for validating auto-mode guards (#4704). Do not use in production code. */
|
|
489
|
+
export function _setAutoActiveForTest(active: boolean): void {
|
|
490
|
+
s.active = active;
|
|
491
|
+
}
|
|
492
|
+
|
|
488
493
|
export function isAutoPaused(): boolean {
|
|
489
494
|
return s.paused;
|
|
490
495
|
}
|
|
@@ -12,6 +12,8 @@ type AutoResumeSnapshot = Pick<AutoDashboardData, "active" | "paused" | "stepMod
|
|
|
12
12
|
|
|
13
13
|
export interface ProviderErrorResumeDeps {
|
|
14
14
|
getSnapshot(): AutoResumeSnapshot;
|
|
15
|
+
resetTransientRetryState(): void;
|
|
16
|
+
resetSessionTimeoutState(): void;
|
|
15
17
|
startAuto(
|
|
16
18
|
ctx: ExtensionCommandContext,
|
|
17
19
|
pi: ExtensionAPI,
|
|
@@ -23,6 +25,8 @@ export interface ProviderErrorResumeDeps {
|
|
|
23
25
|
|
|
24
26
|
const defaultDeps: ProviderErrorResumeDeps = {
|
|
25
27
|
getSnapshot: () => getAutoDashboardData(),
|
|
28
|
+
resetTransientRetryState,
|
|
29
|
+
resetSessionTimeoutState,
|
|
26
30
|
startAuto,
|
|
27
31
|
};
|
|
28
32
|
|
|
@@ -47,8 +51,8 @@ export async function resumeAutoAfterProviderDelay(
|
|
|
47
51
|
// Reset retry counters before restarting — without this, counters
|
|
48
52
|
// accumulate across pause/resume cycles and permanently lock out
|
|
49
53
|
// auto-resume after their respective MAX thresholds.
|
|
50
|
-
resetTransientRetryState();
|
|
51
|
-
resetSessionTimeoutState();
|
|
54
|
+
deps.resetTransientRetryState();
|
|
55
|
+
deps.resetSessionTimeoutState();
|
|
52
56
|
|
|
53
57
|
await deps.startAuto(
|
|
54
58
|
ctx as ExtensionCommandContext,
|
|
@@ -43,6 +43,25 @@ import {
|
|
|
43
43
|
validateFetchedContent,
|
|
44
44
|
} from "../../workflow-install.js";
|
|
45
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Refuses interactive commands that mutate durable .gsd/ planning state while
|
|
48
|
+
* auto-mode holds the worktree. Returns true if the command was blocked and
|
|
49
|
+
* the caller should return immediately; false if it is safe to proceed.
|
|
50
|
+
*
|
|
51
|
+
* Auto-mode's squash merge performs a pre-merge dirty-tree check; concurrent
|
|
52
|
+
* writes by interactive commands between that check and the merge itself
|
|
53
|
+
* cause __dirty_working_tree__ failures (#4704).
|
|
54
|
+
*/
|
|
55
|
+
function requireNotAutoActive(commandName: string, ctx: ExtensionCommandContext): boolean {
|
|
56
|
+
if (!isAutoActive()) return false;
|
|
57
|
+
ctx.ui.notify(
|
|
58
|
+
`${commandName} cannot run while auto-mode is active.\n` +
|
|
59
|
+
`Stop auto-mode first with /gsd stop, then run ${commandName}.`,
|
|
60
|
+
"error",
|
|
61
|
+
);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
|
|
46
65
|
// ─── Custom Workflow Subcommands ─────────────────────────────────────────
|
|
47
66
|
|
|
48
67
|
const RESERVED_SUBCOMMANDS = new Set([
|
|
@@ -473,12 +492,14 @@ async function handleCustomWorkflow(
|
|
|
473
492
|
export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionCommandContext, pi: ExtensionAPI): Promise<boolean> {
|
|
474
493
|
// ── /gsd do — natural language routing (must be early to route to other commands) ──
|
|
475
494
|
if (trimmed === "do" || trimmed.startsWith("do ")) {
|
|
495
|
+
if (requireNotAutoActive("/gsd do", ctx)) return true;
|
|
476
496
|
const { handleDo } = await import("../../commands-do.js");
|
|
477
497
|
await handleDo(trimmed.replace(/^do\s*/, "").trim(), ctx, pi);
|
|
478
498
|
return true;
|
|
479
499
|
}
|
|
480
500
|
// ── Backlog management ──
|
|
481
501
|
if (trimmed === "backlog" || trimmed.startsWith("backlog ")) {
|
|
502
|
+
if (requireNotAutoActive("/gsd backlog", ctx)) return true;
|
|
482
503
|
const { handleBacklog } = await import("../../commands-backlog.js");
|
|
483
504
|
await handleBacklog(trimmed.replace(/^backlog\s*/, "").trim(), ctx, pi);
|
|
484
505
|
return true;
|
|
@@ -490,26 +511,22 @@ export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionComma
|
|
|
490
511
|
}
|
|
491
512
|
|
|
492
513
|
if (trimmed === "queue") {
|
|
514
|
+
if (requireNotAutoActive("/gsd queue", ctx)) return true;
|
|
493
515
|
await showQueue(ctx, pi, projectRoot());
|
|
494
516
|
return true;
|
|
495
517
|
}
|
|
496
518
|
if (trimmed === "discuss") {
|
|
519
|
+
if (requireNotAutoActive("/gsd discuss", ctx)) return true;
|
|
497
520
|
await showDiscuss(ctx, pi, projectRoot());
|
|
498
521
|
return true;
|
|
499
522
|
}
|
|
500
523
|
if (trimmed === "quick" || trimmed.startsWith("quick ")) {
|
|
501
|
-
if (
|
|
502
|
-
ctx.ui.notify(
|
|
503
|
-
"/gsd quick cannot run while auto-mode is active.\n" +
|
|
504
|
-
"Stop auto-mode first with /gsd stop, then run /gsd quick.",
|
|
505
|
-
"error",
|
|
506
|
-
);
|
|
507
|
-
return true;
|
|
508
|
-
}
|
|
524
|
+
if (requireNotAutoActive("/gsd quick", ctx)) return true;
|
|
509
525
|
await handleQuick(trimmed.replace(/^quick\s*/, "").trim(), ctx, pi);
|
|
510
526
|
return true;
|
|
511
527
|
}
|
|
512
528
|
if (trimmed === "new-milestone") {
|
|
529
|
+
if (requireNotAutoActive("/gsd new-milestone", ctx)) return true;
|
|
513
530
|
const basePath = projectRoot();
|
|
514
531
|
const headlessContextPath = join(gsdRoot(basePath), "runtime", "headless-context.md");
|
|
515
532
|
if (existsSync(headlessContextPath)) {
|
|
@@ -531,6 +548,7 @@ export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionComma
|
|
|
531
548
|
return true;
|
|
532
549
|
}
|
|
533
550
|
if (trimmed === "park" || trimmed.startsWith("park ")) {
|
|
551
|
+
if (requireNotAutoActive("/gsd park", ctx)) return true;
|
|
534
552
|
const basePath = projectRoot();
|
|
535
553
|
const arg = trimmed.replace(/^park\s*/, "").trim();
|
|
536
554
|
let targetId = arg;
|
|
@@ -556,6 +574,7 @@ export async function handleWorkflowCommand(trimmed: string, ctx: ExtensionComma
|
|
|
556
574
|
return true;
|
|
557
575
|
}
|
|
558
576
|
if (trimmed === "unpark" || trimmed.startsWith("unpark ")) {
|
|
577
|
+
if (requireNotAutoActive("/gsd unpark", ctx)) return true;
|
|
559
578
|
const basePath = projectRoot();
|
|
560
579
|
const arg = trimmed.replace(/^unpark\s*/, "").trim();
|
|
561
580
|
let targetId = arg;
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
2
4
|
|
|
3
|
-
|
|
5
|
+
// The file-lock module is loaded in both CJS builds and ESM sources. Under ESM
|
|
6
|
+
// the bare `require` identifier is not defined, so we always go through
|
|
7
|
+
// createRequire. We try the current module's resolution context first and fall
|
|
8
|
+
// back to the installed gsd-pi package if we are running from a consumer
|
|
9
|
+
// project that does not hoist proper-lockfile.
|
|
10
|
+
const localRequire = createRequire(import.meta.url);
|
|
11
|
+
|
|
12
|
+
function _require(name: string): any {
|
|
4
13
|
try {
|
|
5
|
-
return
|
|
14
|
+
return localRequire(name);
|
|
6
15
|
} catch {
|
|
7
16
|
try {
|
|
8
|
-
const gsdPiRequire =
|
|
9
|
-
|
|
17
|
+
const gsdPiRequire = createRequire(
|
|
18
|
+
join(process.cwd(), "node_modules", "gsd-pi", "index.js"),
|
|
10
19
|
);
|
|
11
20
|
return gsdPiRequire(name);
|
|
12
21
|
} catch {
|
|
@@ -15,43 +24,107 @@ function _require(name: string) {
|
|
|
15
24
|
}
|
|
16
25
|
}
|
|
17
26
|
|
|
18
|
-
export
|
|
27
|
+
export type OnLocked = "fail" | "skip";
|
|
28
|
+
|
|
29
|
+
export interface FileLockOptions {
|
|
30
|
+
/**
|
|
31
|
+
* Behavior when the lock cannot be acquired after retries (ELOCKED).
|
|
32
|
+
* - "fail" (default): rethrow the ELOCKED error so the caller can react.
|
|
33
|
+
* - "skip": run fn() unlocked. Only choose this for best-effort writes
|
|
34
|
+
* that genuinely tolerate contention (e.g. high-frequency audit appends
|
|
35
|
+
* where dropping one entry is acceptable). Silent unlocked execution was
|
|
36
|
+
* the legacy behavior and is a correctness hazard for shared state.
|
|
37
|
+
*/
|
|
38
|
+
onLocked?: OnLocked;
|
|
39
|
+
/** proper-lockfile retries (default 5). */
|
|
40
|
+
retries?: number;
|
|
41
|
+
/** proper-lockfile stale threshold in ms (default 10000). */
|
|
42
|
+
stale?: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const DEFAULT_RETRIES = 5;
|
|
46
|
+
const DEFAULT_STALE_MS = 10000;
|
|
47
|
+
const SYNC_RETRY_DELAY_MS = 50;
|
|
48
|
+
|
|
49
|
+
// Block the thread for `ms` milliseconds without spinning the CPU.
|
|
50
|
+
// Used by the sync lock retry loop, since proper-lockfile's lockSync does not
|
|
51
|
+
// accept a `retries` option (only the async `lock` does).
|
|
52
|
+
function sleepSync(ms: number): void {
|
|
53
|
+
if (ms <= 0) return;
|
|
54
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function acquireLockSyncWithRetry(
|
|
58
|
+
lockfile: any,
|
|
59
|
+
filePath: string,
|
|
60
|
+
retries: number,
|
|
61
|
+
stale: number,
|
|
62
|
+
): () => void {
|
|
63
|
+
let lastErr: any;
|
|
64
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
65
|
+
try {
|
|
66
|
+
return lockfile.lockSync(filePath, { stale });
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
lastErr = err;
|
|
69
|
+
if (err?.code !== "ELOCKED") throw err;
|
|
70
|
+
if (attempt < retries) sleepSync(SYNC_RETRY_DELAY_MS);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw lastErr;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function withFileLockSync<T>(
|
|
77
|
+
filePath: string,
|
|
78
|
+
fn: () => T,
|
|
79
|
+
opts: FileLockOptions = {},
|
|
80
|
+
): T {
|
|
19
81
|
const lockfile = _require("proper-lockfile");
|
|
20
82
|
if (!lockfile) return fn();
|
|
21
83
|
|
|
22
84
|
if (!existsSync(filePath)) return fn();
|
|
23
85
|
|
|
86
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
87
|
+
const stale = opts.stale ?? DEFAULT_STALE_MS;
|
|
88
|
+
const onLocked: OnLocked = opts.onLocked ?? "fail";
|
|
89
|
+
|
|
24
90
|
try {
|
|
25
|
-
const release = lockfile
|
|
91
|
+
const release = acquireLockSyncWithRetry(lockfile, filePath, retries, stale);
|
|
26
92
|
try {
|
|
27
93
|
return fn();
|
|
28
94
|
} finally {
|
|
29
95
|
release();
|
|
30
96
|
}
|
|
31
97
|
} catch (err: any) {
|
|
32
|
-
if (err
|
|
33
|
-
// Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
|
|
98
|
+
if (err?.code === "ELOCKED" && onLocked === "skip") {
|
|
34
99
|
return fn();
|
|
35
100
|
}
|
|
36
101
|
throw err;
|
|
37
102
|
}
|
|
38
103
|
}
|
|
39
104
|
|
|
40
|
-
export async function withFileLock<T>(
|
|
105
|
+
export async function withFileLock<T>(
|
|
106
|
+
filePath: string,
|
|
107
|
+
fn: () => Promise<T> | T,
|
|
108
|
+
opts: FileLockOptions = {},
|
|
109
|
+
): Promise<T> {
|
|
41
110
|
const lockfile = _require("proper-lockfile");
|
|
42
111
|
if (!lockfile) return await fn();
|
|
43
112
|
|
|
44
113
|
if (!existsSync(filePath)) return await fn();
|
|
45
114
|
|
|
115
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
116
|
+
const stale = opts.stale ?? DEFAULT_STALE_MS;
|
|
117
|
+
const onLocked: OnLocked = opts.onLocked ?? "fail";
|
|
118
|
+
|
|
46
119
|
try {
|
|
47
|
-
const release = await lockfile.lock(filePath, { retries
|
|
120
|
+
const release = await lockfile.lock(filePath, { retries, stale });
|
|
48
121
|
try {
|
|
49
122
|
return await fn();
|
|
50
123
|
} finally {
|
|
51
124
|
await release();
|
|
52
125
|
}
|
|
53
126
|
} catch (err: any) {
|
|
54
|
-
if (err
|
|
127
|
+
if (err?.code === "ELOCKED" && onLocked === "skip") {
|
|
55
128
|
return await fn();
|
|
56
129
|
}
|
|
57
130
|
throw err;
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
resolveGsdRootFile, relGsdRootFile, relSliceFile,
|
|
19
19
|
} from "./paths.js";
|
|
20
20
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
21
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
21
22
|
import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
|
|
22
23
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
23
24
|
import { loadQueueOrder, sortByQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
@@ -435,5 +436,7 @@ function syncProjectMdSequence(
|
|
|
435
436
|
const separatorLine = lines[tableStart + 1];
|
|
436
437
|
const newTable = [headerLine, separatorLine, ...newRows];
|
|
437
438
|
lines.splice(tableStart, tableEnd - tableStart, ...newTable);
|
|
438
|
-
|
|
439
|
+
// Atomic write: tmp+rename avoids a torn PROJECT.md appearing dirty in
|
|
440
|
+
// another worktree's working tree during a concurrent /gsd auto merge.
|
|
441
|
+
atomicWriteSync(projectPath, lines.join("\n"), "utf-8");
|
|
439
442
|
}
|
|
@@ -12,8 +12,18 @@
|
|
|
12
12
|
* - Silent failure: journal writes never throw — absence of events is the failure signal
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
appendFileSync,
|
|
17
|
+
closeSync,
|
|
18
|
+
existsSync,
|
|
19
|
+
mkdirSync,
|
|
20
|
+
openSync,
|
|
21
|
+
readdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
} from "node:fs";
|
|
16
24
|
import { join } from "node:path";
|
|
25
|
+
import { isStaleWrite } from "./auto/turn-epoch.js";
|
|
26
|
+
import { withFileLockSync } from "./file-lock.js";
|
|
17
27
|
import { gsdRoot } from "./paths.js";
|
|
18
28
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
19
29
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
@@ -84,12 +94,27 @@ export interface JournalQueryFilters {
|
|
|
84
94
|
* Never throws — all errors are silently caught.
|
|
85
95
|
*/
|
|
86
96
|
export function emitJournalEvent(basePath: string, entry: JournalEntry): void {
|
|
97
|
+
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
98
|
+
// See auto/turn-epoch.ts for the full rationale.
|
|
99
|
+
if (isStaleWrite("journal")) return;
|
|
87
100
|
try {
|
|
88
101
|
const journalDir = join(gsdRoot(basePath), "journal");
|
|
89
102
|
mkdirSync(journalDir, { recursive: true });
|
|
90
103
|
const dateStr = entry.ts.slice(0, 10);
|
|
91
104
|
const filePath = join(journalDir, `${dateStr}.jsonl`);
|
|
92
|
-
|
|
105
|
+
// Ensure file exists so proper-lockfile can acquire a lock against it.
|
|
106
|
+
if (!existsSync(filePath)) closeSync(openSync(filePath, "a"));
|
|
107
|
+
// onLocked: "skip" — journal writes are best-effort. POSIX O_APPEND
|
|
108
|
+
// atomicity still protects small entries; the lock mainly serializes
|
|
109
|
+
// larger writes and gives cross-process exclusivity on platforms where
|
|
110
|
+
// O_APPEND semantics are weaker (Windows).
|
|
111
|
+
withFileLockSync(
|
|
112
|
+
filePath,
|
|
113
|
+
() => {
|
|
114
|
+
appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
115
|
+
},
|
|
116
|
+
{ onLocked: "skip" },
|
|
117
|
+
);
|
|
93
118
|
} catch {
|
|
94
119
|
// Silent failure — journal must never break auto-mode
|
|
95
120
|
}
|
|
@@ -23,6 +23,21 @@ import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
|
23
23
|
import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
24
24
|
import { removeWorktree } from "./worktree-manager.js";
|
|
25
25
|
import { logWarning } from "./workflow-logger.js";
|
|
26
|
+
import { isAutoActive } from "./auto.js";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Writer-side assert for mutations that race with auto-mode's squash merge (#4704).
|
|
30
|
+
* Auto-mode is confirmed not to call parkMilestone/discardMilestone/unparkMilestone
|
|
31
|
+
* internally — these throws only surface invariant violations from new or forgotten
|
|
32
|
+
* call sites, which is the correct failure mode to catch loudly.
|
|
33
|
+
*/
|
|
34
|
+
function assertNotAutoActive(action: string): void {
|
|
35
|
+
if (isAutoActive()) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`${action} cannot run while auto-mode is active. Stop auto-mode first with /gsd stop.`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
26
41
|
|
|
27
42
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
28
43
|
|
|
@@ -32,6 +47,7 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
32
47
|
* Returns true if successfully parked, false if milestone not found or already parked.
|
|
33
48
|
*/
|
|
34
49
|
export function parkMilestone(basePath: string, milestoneId: string, reason: string): boolean {
|
|
50
|
+
assertNotAutoActive("park milestone");
|
|
35
51
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
36
52
|
if (!mDir || !existsSync(mDir)) return false;
|
|
37
53
|
|
|
@@ -74,6 +90,7 @@ export function parkMilestone(basePath: string, milestoneId: string, reason: str
|
|
|
74
90
|
* Returns true if successfully unparked, false if milestone not found or not parked.
|
|
75
91
|
*/
|
|
76
92
|
export function unparkMilestone(basePath: string, milestoneId: string): boolean {
|
|
93
|
+
assertNotAutoActive("unpark milestone");
|
|
77
94
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
78
95
|
if (!mDir || !existsSync(mDir)) return false;
|
|
79
96
|
|
|
@@ -108,6 +125,7 @@ export function unparkMilestone(basePath: string, milestoneId: string): boolean
|
|
|
108
125
|
* Returns true if successfully discarded, false if milestone not found.
|
|
109
126
|
*/
|
|
110
127
|
export function discardMilestone(basePath: string, milestoneId: string): boolean {
|
|
128
|
+
assertNotAutoActive("discard milestone");
|
|
111
129
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
112
130
|
if (!mDir || !existsSync(mDir)) return false;
|
|
113
131
|
|
|
@@ -14,8 +14,9 @@
|
|
|
14
14
|
* Manual: /gsd export --html
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
18
18
|
import { join, basename } from 'node:path';
|
|
19
|
+
import { atomicWriteSync } from './atomic-write.js';
|
|
19
20
|
import { gsdRoot } from './paths.js';
|
|
20
21
|
import { formatCost, formatTokenCount } from './metrics.js';
|
|
21
22
|
import { formatDateShort, formatDuration } from '../shared/format-utils.js';
|
|
@@ -83,7 +84,7 @@ export function loadReportsIndex(basePath: string): ReportsIndex | null {
|
|
|
83
84
|
function saveReportsIndex(basePath: string, index: ReportsIndex): void {
|
|
84
85
|
const dir = reportsDir(basePath);
|
|
85
86
|
mkdirSync(dir, { recursive: true });
|
|
86
|
-
|
|
87
|
+
atomicWriteSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
|
|
87
88
|
}
|
|
88
89
|
|
|
89
90
|
// ─── Write a report snapshot ──────────────────────────────────────────────────
|
|
@@ -121,7 +122,7 @@ export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
|
|
|
121
122
|
const filename = `${prefix}-${timestamp}.html`;
|
|
122
123
|
const filePath = join(dir, filename);
|
|
123
124
|
|
|
124
|
-
|
|
125
|
+
atomicWriteSync(filePath, args.html, 'utf-8');
|
|
125
126
|
|
|
126
127
|
// Load or init registry
|
|
127
128
|
const existing = loadReportsIndex(args.basePath);
|
|
@@ -170,7 +171,7 @@ export function writeReportSnapshot(args: WriteReportSnapshotArgs): string {
|
|
|
170
171
|
|
|
171
172
|
export function regenerateHtmlIndex(basePath: string, index: ReportsIndex): void {
|
|
172
173
|
const html = buildIndexHtml(index);
|
|
173
|
-
|
|
174
|
+
atomicWriteSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
|
|
174
175
|
}
|
|
175
176
|
|
|
176
177
|
function buildIndexHtml(index: ReportsIndex): string {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
autoLoop,
|
|
11
11
|
detectStuck,
|
|
12
12
|
_resetPendingResolve,
|
|
13
|
+
_hasPendingResolveForTest,
|
|
13
14
|
_setActiveSession,
|
|
14
15
|
isSessionSwitchInFlight,
|
|
15
16
|
type UnitResult,
|
|
@@ -379,6 +380,7 @@ test("runUnit cancels before dispatch when provider is not request-ready (#4555)
|
|
|
379
380
|
/Provider anthropic is not request-ready/,
|
|
380
381
|
);
|
|
381
382
|
assert.equal(pi.calls.length, 0, "sendMessage must not be called when provider is not ready");
|
|
383
|
+
assert.equal(_hasPendingResolveForTest(), false, "provider cancellation must clear the pending resolver");
|
|
382
384
|
});
|
|
383
385
|
|
|
384
386
|
test("runUnit cancels before dispatch using currentUnitModel provider when set (#4555)", async () => {
|
|
@@ -1609,9 +1611,16 @@ test("auto-timeout-recovery.ts calls resolveAgentEnd instead of dispatchNextUnit
|
|
|
1609
1611
|
!src.includes("await dispatchNextUnit"),
|
|
1610
1612
|
"auto-timeout-recovery.ts must not call dispatchNextUnit",
|
|
1611
1613
|
);
|
|
1614
|
+
// After PR #4716, advance branches go through bumpAndResolveSynthetic()
|
|
1615
|
+
// (which bumps the turn epoch and calls resolveAgentEnd atomically).
|
|
1616
|
+
// Either direct resolveAgentEnd() or the helper satisfies the invariant:
|
|
1617
|
+
// the loop must be re-iterated on timeout recovery.
|
|
1618
|
+
const reIteratesLoop =
|
|
1619
|
+
src.includes("resolveAgentEnd(") ||
|
|
1620
|
+
src.includes("bumpAndResolveSynthetic(");
|
|
1612
1621
|
assert.ok(
|
|
1613
|
-
|
|
1614
|
-
"auto-timeout-recovery.ts must call resolveAgentEnd to re-iterate the loop on timeout recovery",
|
|
1622
|
+
reIteratesLoop,
|
|
1623
|
+
"auto-timeout-recovery.ts must call resolveAgentEnd (directly or via bumpAndResolveSynthetic) to re-iterate the loop on timeout recovery",
|
|
1615
1624
|
);
|
|
1616
1625
|
});
|
|
1617
1626
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for auto-mode guards (#4704 Tier 2 / #4712).
|
|
3
|
+
*
|
|
4
|
+
* Validates the defense-in-depth writer asserts in milestone-actions.ts —
|
|
5
|
+
* parkMilestone, unparkMilestone, and discardMilestone must refuse to run
|
|
6
|
+
* while auto-mode is active, regardless of the calling dispatch path.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, test, afterEach } from 'node:test';
|
|
9
|
+
import assert from 'node:assert/strict';
|
|
10
|
+
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
|
|
14
|
+
import { parkMilestone, unparkMilestone, discardMilestone } from '../milestone-actions.ts';
|
|
15
|
+
import { _setAutoActiveForTest } from '../auto.ts';
|
|
16
|
+
|
|
17
|
+
function createFixture(): string {
|
|
18
|
+
const base = mkdtempSync(join(tmpdir(), 'gsd-guard-test-'));
|
|
19
|
+
const mDir = join(base, '.gsd', 'milestones', 'M001');
|
|
20
|
+
mkdirSync(mDir, { recursive: true });
|
|
21
|
+
writeFileSync(join(mDir, 'M001-ROADMAP.md'), '# M001\n', 'utf-8');
|
|
22
|
+
return base;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('auto-mode guards (milestone-actions)', () => {
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
_setAutoActiveForTest(false);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('parkMilestone throws when auto-mode is active', () => {
|
|
31
|
+
const base = createFixture();
|
|
32
|
+
try {
|
|
33
|
+
_setAutoActiveForTest(true);
|
|
34
|
+
assert.throws(
|
|
35
|
+
() => parkMilestone(base, 'M001', 'test'),
|
|
36
|
+
/auto-mode is active/,
|
|
37
|
+
);
|
|
38
|
+
} finally {
|
|
39
|
+
rmSync(base, { recursive: true, force: true });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('unparkMilestone throws when auto-mode is active', () => {
|
|
44
|
+
const base = createFixture();
|
|
45
|
+
try {
|
|
46
|
+
_setAutoActiveForTest(true);
|
|
47
|
+
assert.throws(
|
|
48
|
+
() => unparkMilestone(base, 'M001'),
|
|
49
|
+
/auto-mode is active/,
|
|
50
|
+
);
|
|
51
|
+
} finally {
|
|
52
|
+
rmSync(base, { recursive: true, force: true });
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test('discardMilestone throws when auto-mode is active', () => {
|
|
57
|
+
const base = createFixture();
|
|
58
|
+
try {
|
|
59
|
+
_setAutoActiveForTest(true);
|
|
60
|
+
assert.throws(
|
|
61
|
+
() => discardMilestone(base, 'M001'),
|
|
62
|
+
/auto-mode is active/,
|
|
63
|
+
);
|
|
64
|
+
} finally {
|
|
65
|
+
rmSync(base, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('parkMilestone proceeds normally when auto-mode is inactive', () => {
|
|
70
|
+
const base = createFixture();
|
|
71
|
+
try {
|
|
72
|
+
_setAutoActiveForTest(false);
|
|
73
|
+
const result = parkMilestone(base, 'M001', 'baseline');
|
|
74
|
+
assert.ok(result, 'park succeeds when auto is inactive');
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(base, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|