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
|
@@ -9,7 +9,7 @@ import { existsSync, cpSync, readFileSync, readdirSync, mkdirSync, realpathSync,
|
|
|
9
9
|
import { isAbsolute, join, sep as pathSep } from "node:path";
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { GSDError, GSD_IO_ERROR, GSD_GIT_ERROR } from "./errors.js";
|
|
12
|
-
import { reconcileWorktreeDb, isDbAvailable, getMilestone, getMilestoneSlices, } from "./gsd-db.js";
|
|
12
|
+
import { reconcileWorktreeDb, isDbAvailable, getMilestone, getMilestoneSlices, closeDatabase, openDatabase, getDbPath, } from "./gsd-db.js";
|
|
13
13
|
import { atomicWriteSync } from "./atomic-write.js";
|
|
14
14
|
import { execFileSync } from "node:child_process";
|
|
15
15
|
import { safeCopy, safeCopyRecursive } from "./safe-fs.js";
|
|
@@ -1433,26 +1433,53 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1433
1433
|
const milestonesDir = join(gsdRoot(originalBasePath_), "milestones");
|
|
1434
1434
|
const shelterDir = join(gsdRoot(originalBasePath_), ".milestone-shelter");
|
|
1435
1435
|
const shelteredDirs = [];
|
|
1436
|
+
let shelterRestored = false;
|
|
1436
1437
|
// Helper: restore sheltered milestone directories (#2505).
|
|
1437
1438
|
// Called on both success and error paths to ensure queued CONTEXT files
|
|
1438
|
-
// are never permanently lost.
|
|
1439
|
+
// are never permanently lost. Idempotent — the error path may fire after
|
|
1440
|
+
// the success path has already restored and removed the shelter dir; a
|
|
1441
|
+
// second call is a no-op instead of logging a misleading "shelter restore
|
|
1442
|
+
// failed: ENOENT" error for shelter sources that were cleaned up legitimately.
|
|
1439
1443
|
const restoreShelter = () => {
|
|
1444
|
+
if (shelterRestored)
|
|
1445
|
+
return;
|
|
1446
|
+
shelterRestored = true;
|
|
1440
1447
|
if (shelteredDirs.length === 0)
|
|
1441
1448
|
return;
|
|
1449
|
+
let restoreFailed = false;
|
|
1442
1450
|
for (const dirName of shelteredDirs) {
|
|
1451
|
+
const src = join(shelterDir, dirName);
|
|
1452
|
+
// If the shelter source is missing the restore cannot proceed for this
|
|
1453
|
+
// entry. Distinguish "legitimately missing" (shelter dir removed by a
|
|
1454
|
+
// prior successful restore or never copied) from a surprising ENOENT
|
|
1455
|
+
// inside an otherwise-populated shelter.
|
|
1456
|
+
if (!existsSync(src)) {
|
|
1457
|
+
logWarning("worktree", `shelter source missing for ${dirName}; skipping restore (shelter already cleaned or entry never staged)`);
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1443
1460
|
try {
|
|
1444
1461
|
mkdirSync(milestonesDir, { recursive: true });
|
|
1445
|
-
cpSync(
|
|
1462
|
+
cpSync(src, join(milestonesDir, dirName), { recursive: true, force: true });
|
|
1446
1463
|
}
|
|
1447
1464
|
catch (err) { /* best-effort */
|
|
1448
|
-
|
|
1465
|
+
restoreFailed = true;
|
|
1466
|
+
logError("worktree", `shelter restore failed (${dirName}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1449
1467
|
}
|
|
1450
1468
|
}
|
|
1451
|
-
|
|
1452
|
-
|
|
1469
|
+
// Preserve the shelter if any per-entry restore failed — it is the only
|
|
1470
|
+
// surviving copy of the queued milestone dirs (sources were deleted during
|
|
1471
|
+
// shelter). Deleting it here would permanently lose those files (#2505).
|
|
1472
|
+
if (restoreFailed) {
|
|
1473
|
+
logWarning("worktree", `shelter retained at ${shelterDir} — manual recovery required for unrestored entries`);
|
|
1474
|
+
return;
|
|
1453
1475
|
}
|
|
1454
|
-
|
|
1455
|
-
|
|
1476
|
+
if (existsSync(shelterDir)) {
|
|
1477
|
+
try {
|
|
1478
|
+
rmSync(shelterDir, { recursive: true, force: true });
|
|
1479
|
+
}
|
|
1480
|
+
catch (err) { /* best-effort */
|
|
1481
|
+
logWarning("worktree", `shelter cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1482
|
+
}
|
|
1456
1483
|
}
|
|
1457
1484
|
};
|
|
1458
1485
|
try {
|
|
@@ -1491,6 +1518,21 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1491
1518
|
// entries (e.g. a gitignored `.gsd` symlink under ADR-002) (#4573).
|
|
1492
1519
|
// Queued CONTEXT files under `.gsd/milestones/*` are already sheltered
|
|
1493
1520
|
// in step 7 above, so they won't be swept into the stash.
|
|
1521
|
+
// On Windows, SQLite holds mandatory file locks on the gsd.db WAL/SHM
|
|
1522
|
+
// sidecars while the connection is open. `git stash --include-untracked`
|
|
1523
|
+
// walks those files and fails with EBUSY (#4704). Close the DB before
|
|
1524
|
+
// stashing so Windows releases the handles; reopen after. No-op on
|
|
1525
|
+
// POSIX, where advisory locks don't block git.
|
|
1526
|
+
const needsDbCycle = process.platform === "win32" && isDbAvailable();
|
|
1527
|
+
const dbPathToReopen = needsDbCycle ? getDbPath() : null;
|
|
1528
|
+
if (needsDbCycle) {
|
|
1529
|
+
try {
|
|
1530
|
+
closeDatabase();
|
|
1531
|
+
}
|
|
1532
|
+
catch (err) {
|
|
1533
|
+
logWarning("worktree", `pre-stash db close failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1494
1536
|
let stashed = false;
|
|
1495
1537
|
try {
|
|
1496
1538
|
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
@@ -1508,6 +1550,14 @@ export function mergeMilestoneToMain(originalBasePath_, milestoneId, roadmapCont
|
|
|
1508
1550
|
// report the dirty tree if it fails.
|
|
1509
1551
|
logWarning("worktree", `git stash failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1510
1552
|
}
|
|
1553
|
+
if (needsDbCycle && dbPathToReopen) {
|
|
1554
|
+
try {
|
|
1555
|
+
openDatabase(dbPathToReopen);
|
|
1556
|
+
}
|
|
1557
|
+
catch (err) {
|
|
1558
|
+
logWarning("worktree", `post-stash db reopen failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1511
1561
|
// 7b. Clean up stale merge state before attempting squash merge (#2912).
|
|
1512
1562
|
// A leftover MERGE_HEAD (from a previous failed merge, libgit2 native path,
|
|
1513
1563
|
// or interrupted operation) causes `git merge --squash` to refuse with
|
|
@@ -274,6 +274,10 @@ export function getAutoDashboardData() {
|
|
|
274
274
|
export function isAutoActive() {
|
|
275
275
|
return s.active;
|
|
276
276
|
}
|
|
277
|
+
/** Test-only seam for validating auto-mode guards (#4704). Do not use in production code. */
|
|
278
|
+
export function _setAutoActiveForTest(active) {
|
|
279
|
+
s.active = active;
|
|
280
|
+
}
|
|
277
281
|
export function isAutoPaused() {
|
|
278
282
|
return s.paused;
|
|
279
283
|
}
|
|
@@ -3,6 +3,8 @@ import { resetTransientRetryState } from "./agent-end-recovery.js";
|
|
|
3
3
|
import { resetSessionTimeoutState } from "../auto/phases.js";
|
|
4
4
|
const defaultDeps = {
|
|
5
5
|
getSnapshot: () => getAutoDashboardData(),
|
|
6
|
+
resetTransientRetryState,
|
|
7
|
+
resetSessionTimeoutState,
|
|
6
8
|
startAuto,
|
|
7
9
|
};
|
|
8
10
|
export async function resumeAutoAfterProviderDelay(pi, ctx, deps = defaultDeps) {
|
|
@@ -18,8 +20,8 @@ export async function resumeAutoAfterProviderDelay(pi, ctx, deps = defaultDeps)
|
|
|
18
20
|
// Reset retry counters before restarting — without this, counters
|
|
19
21
|
// accumulate across pause/resume cycles and permanently lock out
|
|
20
22
|
// auto-resume after their respective MAX thresholds.
|
|
21
|
-
resetTransientRetryState();
|
|
22
|
-
resetSessionTimeoutState();
|
|
23
|
+
deps.resetTransientRetryState();
|
|
24
|
+
deps.resetSessionTimeoutState();
|
|
23
25
|
await deps.startAuto(ctx, pi, snapshot.basePath, false, { step: snapshot.stepMode });
|
|
24
26
|
return "resumed";
|
|
25
27
|
}
|
|
@@ -17,6 +17,22 @@ import { validateDefinition } from "../../definition-loader.js";
|
|
|
17
17
|
import { formatPluginInfo, listPluginsFormatted, resolvePlugin, } from "../../workflow-plugins.js";
|
|
18
18
|
import { dispatchOneshot } from "../../workflow-dispatch.js";
|
|
19
19
|
import { fetchWorkflowSource, globalInstallDir, inferPluginName, installPlugin, previewContent, projectInstallDir, resolveSourceUrl, uninstallPlugin, validateFetchedContent, } from "../../workflow-install.js";
|
|
20
|
+
/**
|
|
21
|
+
* Refuses interactive commands that mutate durable .gsd/ planning state while
|
|
22
|
+
* auto-mode holds the worktree. Returns true if the command was blocked and
|
|
23
|
+
* the caller should return immediately; false if it is safe to proceed.
|
|
24
|
+
*
|
|
25
|
+
* Auto-mode's squash merge performs a pre-merge dirty-tree check; concurrent
|
|
26
|
+
* writes by interactive commands between that check and the merge itself
|
|
27
|
+
* cause __dirty_working_tree__ failures (#4704).
|
|
28
|
+
*/
|
|
29
|
+
function requireNotAutoActive(commandName, ctx) {
|
|
30
|
+
if (!isAutoActive())
|
|
31
|
+
return false;
|
|
32
|
+
ctx.ui.notify(`${commandName} cannot run while auto-mode is active.\n` +
|
|
33
|
+
`Stop auto-mode first with /gsd stop, then run ${commandName}.`, "error");
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
20
36
|
// ─── Custom Workflow Subcommands ─────────────────────────────────────────
|
|
21
37
|
const RESERVED_SUBCOMMANDS = new Set([
|
|
22
38
|
"new", "run", "list", "validate", "pause", "resume",
|
|
@@ -394,12 +410,16 @@ async function handleCustomWorkflow(sub, ctx, pi) {
|
|
|
394
410
|
export async function handleWorkflowCommand(trimmed, ctx, pi) {
|
|
395
411
|
// ── /gsd do — natural language routing (must be early to route to other commands) ──
|
|
396
412
|
if (trimmed === "do" || trimmed.startsWith("do ")) {
|
|
413
|
+
if (requireNotAutoActive("/gsd do", ctx))
|
|
414
|
+
return true;
|
|
397
415
|
const { handleDo } = await import("../../commands-do.js");
|
|
398
416
|
await handleDo(trimmed.replace(/^do\s*/, "").trim(), ctx, pi);
|
|
399
417
|
return true;
|
|
400
418
|
}
|
|
401
419
|
// ── Backlog management ──
|
|
402
420
|
if (trimmed === "backlog" || trimmed.startsWith("backlog ")) {
|
|
421
|
+
if (requireNotAutoActive("/gsd backlog", ctx))
|
|
422
|
+
return true;
|
|
403
423
|
const { handleBacklog } = await import("../../commands-backlog.js");
|
|
404
424
|
await handleBacklog(trimmed.replace(/^backlog\s*/, "").trim(), ctx, pi);
|
|
405
425
|
return true;
|
|
@@ -410,23 +430,26 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
|
|
|
410
430
|
return handleCustomWorkflow(sub, ctx, pi);
|
|
411
431
|
}
|
|
412
432
|
if (trimmed === "queue") {
|
|
433
|
+
if (requireNotAutoActive("/gsd queue", ctx))
|
|
434
|
+
return true;
|
|
413
435
|
await showQueue(ctx, pi, projectRoot());
|
|
414
436
|
return true;
|
|
415
437
|
}
|
|
416
438
|
if (trimmed === "discuss") {
|
|
439
|
+
if (requireNotAutoActive("/gsd discuss", ctx))
|
|
440
|
+
return true;
|
|
417
441
|
await showDiscuss(ctx, pi, projectRoot());
|
|
418
442
|
return true;
|
|
419
443
|
}
|
|
420
444
|
if (trimmed === "quick" || trimmed.startsWith("quick ")) {
|
|
421
|
-
if (
|
|
422
|
-
ctx.ui.notify("/gsd quick cannot run while auto-mode is active.\n" +
|
|
423
|
-
"Stop auto-mode first with /gsd stop, then run /gsd quick.", "error");
|
|
445
|
+
if (requireNotAutoActive("/gsd quick", ctx))
|
|
424
446
|
return true;
|
|
425
|
-
}
|
|
426
447
|
await handleQuick(trimmed.replace(/^quick\s*/, "").trim(), ctx, pi);
|
|
427
448
|
return true;
|
|
428
449
|
}
|
|
429
450
|
if (trimmed === "new-milestone") {
|
|
451
|
+
if (requireNotAutoActive("/gsd new-milestone", ctx))
|
|
452
|
+
return true;
|
|
430
453
|
const basePath = projectRoot();
|
|
431
454
|
const headlessContextPath = join(gsdRoot(basePath), "runtime", "headless-context.md");
|
|
432
455
|
if (existsSync(headlessContextPath)) {
|
|
@@ -452,6 +475,8 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
|
|
|
452
475
|
return true;
|
|
453
476
|
}
|
|
454
477
|
if (trimmed === "park" || trimmed.startsWith("park ")) {
|
|
478
|
+
if (requireNotAutoActive("/gsd park", ctx))
|
|
479
|
+
return true;
|
|
455
480
|
const basePath = projectRoot();
|
|
456
481
|
const arg = trimmed.replace(/^park\s*/, "").trim();
|
|
457
482
|
let targetId = arg;
|
|
@@ -474,6 +499,8 @@ export async function handleWorkflowCommand(trimmed, ctx, pi) {
|
|
|
474
499
|
return true;
|
|
475
500
|
}
|
|
476
501
|
if (trimmed === "unpark" || trimmed.startsWith("unpark ")) {
|
|
502
|
+
if (requireNotAutoActive("/gsd unpark", ctx))
|
|
503
|
+
return true;
|
|
477
504
|
const basePath = projectRoot();
|
|
478
505
|
const arg = trimmed.replace(/^unpark\s*/, "").trim();
|
|
479
506
|
let targetId = arg;
|
|
@@ -1,11 +1,19 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// The file-lock module is loaded in both CJS builds and ESM sources. Under ESM
|
|
5
|
+
// the bare `require` identifier is not defined, so we always go through
|
|
6
|
+
// createRequire. We try the current module's resolution context first and fall
|
|
7
|
+
// back to the installed gsd-pi package if we are running from a consumer
|
|
8
|
+
// project that does not hoist proper-lockfile.
|
|
9
|
+
const localRequire = createRequire(import.meta.url);
|
|
2
10
|
function _require(name) {
|
|
3
11
|
try {
|
|
4
|
-
return
|
|
12
|
+
return localRequire(name);
|
|
5
13
|
}
|
|
6
14
|
catch {
|
|
7
15
|
try {
|
|
8
|
-
const gsdPiRequire =
|
|
16
|
+
const gsdPiRequire = createRequire(join(process.cwd(), "node_modules", "gsd-pi", "index.js"));
|
|
9
17
|
return gsdPiRequire(name);
|
|
10
18
|
}
|
|
11
19
|
catch {
|
|
@@ -13,14 +21,44 @@ function _require(name) {
|
|
|
13
21
|
}
|
|
14
22
|
}
|
|
15
23
|
}
|
|
16
|
-
|
|
24
|
+
const DEFAULT_RETRIES = 5;
|
|
25
|
+
const DEFAULT_STALE_MS = 10000;
|
|
26
|
+
const SYNC_RETRY_DELAY_MS = 50;
|
|
27
|
+
// Block the thread for `ms` milliseconds without spinning the CPU.
|
|
28
|
+
// Used by the sync lock retry loop, since proper-lockfile's lockSync does not
|
|
29
|
+
// accept a `retries` option (only the async `lock` does).
|
|
30
|
+
function sleepSync(ms) {
|
|
31
|
+
if (ms <= 0)
|
|
32
|
+
return;
|
|
33
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
34
|
+
}
|
|
35
|
+
function acquireLockSyncWithRetry(lockfile, filePath, retries, stale) {
|
|
36
|
+
let lastErr;
|
|
37
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
38
|
+
try {
|
|
39
|
+
return lockfile.lockSync(filePath, { stale });
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
lastErr = err;
|
|
43
|
+
if (err?.code !== "ELOCKED")
|
|
44
|
+
throw err;
|
|
45
|
+
if (attempt < retries)
|
|
46
|
+
sleepSync(SYNC_RETRY_DELAY_MS);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
throw lastErr;
|
|
50
|
+
}
|
|
51
|
+
export function withFileLockSync(filePath, fn, opts = {}) {
|
|
17
52
|
const lockfile = _require("proper-lockfile");
|
|
18
53
|
if (!lockfile)
|
|
19
54
|
return fn();
|
|
20
55
|
if (!existsSync(filePath))
|
|
21
56
|
return fn();
|
|
57
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
58
|
+
const stale = opts.stale ?? DEFAULT_STALE_MS;
|
|
59
|
+
const onLocked = opts.onLocked ?? "fail";
|
|
22
60
|
try {
|
|
23
|
-
const release = lockfile
|
|
61
|
+
const release = acquireLockSyncWithRetry(lockfile, filePath, retries, stale);
|
|
24
62
|
try {
|
|
25
63
|
return fn();
|
|
26
64
|
}
|
|
@@ -29,21 +67,23 @@ export function withFileLockSync(filePath, fn) {
|
|
|
29
67
|
}
|
|
30
68
|
}
|
|
31
69
|
catch (err) {
|
|
32
|
-
if (err
|
|
33
|
-
// Could not get lock after retries, let's fallback to un-locked instead of crashing the whole state machine
|
|
70
|
+
if (err?.code === "ELOCKED" && onLocked === "skip") {
|
|
34
71
|
return fn();
|
|
35
72
|
}
|
|
36
73
|
throw err;
|
|
37
74
|
}
|
|
38
75
|
}
|
|
39
|
-
export async function withFileLock(filePath, fn) {
|
|
76
|
+
export async function withFileLock(filePath, fn, opts = {}) {
|
|
40
77
|
const lockfile = _require("proper-lockfile");
|
|
41
78
|
if (!lockfile)
|
|
42
79
|
return await fn();
|
|
43
80
|
if (!existsSync(filePath))
|
|
44
81
|
return await fn();
|
|
82
|
+
const retries = opts.retries ?? DEFAULT_RETRIES;
|
|
83
|
+
const stale = opts.stale ?? DEFAULT_STALE_MS;
|
|
84
|
+
const onLocked = opts.onLocked ?? "fail";
|
|
45
85
|
try {
|
|
46
|
-
const release = await lockfile.lock(filePath, { retries
|
|
86
|
+
const release = await lockfile.lock(filePath, { retries, stale });
|
|
47
87
|
try {
|
|
48
88
|
return await fn();
|
|
49
89
|
}
|
|
@@ -52,7 +92,7 @@ export async function withFileLock(filePath, fn) {
|
|
|
52
92
|
}
|
|
53
93
|
}
|
|
54
94
|
catch (err) {
|
|
55
|
-
if (err
|
|
95
|
+
if (err?.code === "ELOCKED" && onLocked === "skip") {
|
|
56
96
|
return await fn();
|
|
57
97
|
}
|
|
58
98
|
throw err;
|
|
@@ -13,6 +13,7 @@ import { deriveState } from "./state.js";
|
|
|
13
13
|
import { invalidateAllCaches } from "./cache.js";
|
|
14
14
|
import { gsdRoot, resolveMilestoneFile, resolveGsdRootFile, relGsdRootFile, } from "./paths.js";
|
|
15
15
|
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
16
|
+
import { atomicWriteSync } from "./atomic-write.js";
|
|
16
17
|
import { nativeAddPaths, nativeCommit } from "./native-git-bridge.js";
|
|
17
18
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
18
19
|
import { saveQueueOrder } from "./queue-order.js";
|
|
@@ -361,5 +362,7 @@ function syncProjectMdSequence(basePath, registry, newOrder) {
|
|
|
361
362
|
const separatorLine = lines[tableStart + 1];
|
|
362
363
|
const newTable = [headerLine, separatorLine, ...newRows];
|
|
363
364
|
lines.splice(tableStart, tableEnd - tableStart, ...newTable);
|
|
364
|
-
|
|
365
|
+
// Atomic write: tmp+rename avoids a torn PROJECT.md appearing dirty in
|
|
366
|
+
// another worktree's working tree during a concurrent /gsd auto merge.
|
|
367
|
+
atomicWriteSync(projectPath, lines.join("\n"), "utf-8");
|
|
365
368
|
}
|
|
@@ -11,8 +11,10 @@
|
|
|
11
11
|
* - queryJournal() enables programmatic filtering by flowId, eventType, unitId, time range
|
|
12
12
|
* - Silent failure: journal writes never throw — absence of events is the failure signal
|
|
13
13
|
*/
|
|
14
|
-
import { appendFileSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
|
|
14
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
+
import { isStaleWrite } from "./auto/turn-epoch.js";
|
|
17
|
+
import { withFileLockSync } from "./file-lock.js";
|
|
16
18
|
import { gsdRoot } from "./paths.js";
|
|
17
19
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
18
20
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
@@ -26,12 +28,25 @@ import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
|
26
28
|
* Never throws — all errors are silently caught.
|
|
27
29
|
*/
|
|
28
30
|
export function emitJournalEvent(basePath, entry) {
|
|
31
|
+
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
32
|
+
// See auto/turn-epoch.ts for the full rationale.
|
|
33
|
+
if (isStaleWrite("journal"))
|
|
34
|
+
return;
|
|
29
35
|
try {
|
|
30
36
|
const journalDir = join(gsdRoot(basePath), "journal");
|
|
31
37
|
mkdirSync(journalDir, { recursive: true });
|
|
32
38
|
const dateStr = entry.ts.slice(0, 10);
|
|
33
39
|
const filePath = join(journalDir, `${dateStr}.jsonl`);
|
|
34
|
-
|
|
40
|
+
// Ensure file exists so proper-lockfile can acquire a lock against it.
|
|
41
|
+
if (!existsSync(filePath))
|
|
42
|
+
closeSync(openSync(filePath, "a"));
|
|
43
|
+
// onLocked: "skip" — journal writes are best-effort. POSIX O_APPEND
|
|
44
|
+
// atomicity still protects small entries; the lock mainly serializes
|
|
45
|
+
// larger writes and gives cross-process exclusivity on platforms where
|
|
46
|
+
// O_APPEND semantics are weaker (Windows).
|
|
47
|
+
withFileLockSync(filePath, () => {
|
|
48
|
+
appendFileSync(filePath, JSON.stringify(entry) + "\n");
|
|
49
|
+
}, { onLocked: "skip" });
|
|
35
50
|
}
|
|
36
51
|
catch {
|
|
37
52
|
// Silent failure — journal must never break auto-mode
|
|
@@ -18,6 +18,18 @@ import { loadQueueOrder, saveQueueOrder } from "./queue-order.js";
|
|
|
18
18
|
import { deleteMilestone, getMilestone, isDbAvailable, updateMilestoneStatus } from "./gsd-db.js";
|
|
19
19
|
import { removeWorktree } from "./worktree-manager.js";
|
|
20
20
|
import { logWarning } from "./workflow-logger.js";
|
|
21
|
+
import { isAutoActive } from "./auto.js";
|
|
22
|
+
/**
|
|
23
|
+
* Writer-side assert for mutations that race with auto-mode's squash merge (#4704).
|
|
24
|
+
* Auto-mode is confirmed not to call parkMilestone/discardMilestone/unparkMilestone
|
|
25
|
+
* internally — these throws only surface invariant violations from new or forgotten
|
|
26
|
+
* call sites, which is the correct failure mode to catch loudly.
|
|
27
|
+
*/
|
|
28
|
+
function assertNotAutoActive(action) {
|
|
29
|
+
if (isAutoActive()) {
|
|
30
|
+
throw new Error(`${action} cannot run while auto-mode is active. Stop auto-mode first with /gsd stop.`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
21
33
|
// ─── Park ──────────────────────────────────────────────────────────────────
|
|
22
34
|
/**
|
|
23
35
|
* Park a milestone — creates a PARKED.md marker file with reason and timestamp.
|
|
@@ -25,6 +37,7 @@ import { logWarning } from "./workflow-logger.js";
|
|
|
25
37
|
* Returns true if successfully parked, false if milestone not found or already parked.
|
|
26
38
|
*/
|
|
27
39
|
export function parkMilestone(basePath, milestoneId, reason) {
|
|
40
|
+
assertNotAutoActive("park milestone");
|
|
28
41
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
29
42
|
if (!mDir || !existsSync(mDir))
|
|
30
43
|
return false;
|
|
@@ -65,6 +78,7 @@ export function parkMilestone(basePath, milestoneId, reason) {
|
|
|
65
78
|
* Returns true if successfully unparked, false if milestone not found or not parked.
|
|
66
79
|
*/
|
|
67
80
|
export function unparkMilestone(basePath, milestoneId) {
|
|
81
|
+
assertNotAutoActive("unpark milestone");
|
|
68
82
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
69
83
|
if (!mDir || !existsSync(mDir))
|
|
70
84
|
return false;
|
|
@@ -97,6 +111,7 @@ export function unparkMilestone(basePath, milestoneId) {
|
|
|
97
111
|
* Returns true if successfully discarded, false if milestone not found.
|
|
98
112
|
*/
|
|
99
113
|
export function discardMilestone(basePath, milestoneId) {
|
|
114
|
+
assertNotAutoActive("discard milestone");
|
|
100
115
|
const mDir = resolveMilestonePath(basePath, milestoneId);
|
|
101
116
|
if (!mDir || !existsSync(mDir))
|
|
102
117
|
return false;
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
* Auto-triggered: after each milestone completion (when auto_report: true).
|
|
14
14
|
* Manual: /gsd export --html
|
|
15
15
|
*/
|
|
16
|
-
import {
|
|
16
|
+
import { readFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
17
17
|
import { join } from 'node:path';
|
|
18
|
+
import { atomicWriteSync } from './atomic-write.js';
|
|
18
19
|
import { gsdRoot } from './paths.js';
|
|
19
20
|
import { formatCost, formatTokenCount } from './metrics.js';
|
|
20
21
|
import { formatDateShort, formatDuration } from '../shared/format-utils.js';
|
|
@@ -43,7 +44,7 @@ export function loadReportsIndex(basePath) {
|
|
|
43
44
|
function saveReportsIndex(basePath, index) {
|
|
44
45
|
const dir = reportsDir(basePath);
|
|
45
46
|
mkdirSync(dir, { recursive: true });
|
|
46
|
-
|
|
47
|
+
atomicWriteSync(reportsIndexPath(basePath), JSON.stringify(index, null, 2) + '\n', 'utf-8');
|
|
47
48
|
}
|
|
48
49
|
/**
|
|
49
50
|
* Write a report snapshot to .gsd/reports/, update reports.json, regenerate index.html.
|
|
@@ -56,7 +57,7 @@ export function writeReportSnapshot(args) {
|
|
|
56
57
|
const prefix = args.milestoneId === 'final' ? 'final' : args.milestoneId;
|
|
57
58
|
const filename = `${prefix}-${timestamp}.html`;
|
|
58
59
|
const filePath = join(dir, filename);
|
|
59
|
-
|
|
60
|
+
atomicWriteSync(filePath, args.html, 'utf-8');
|
|
60
61
|
// Load or init registry
|
|
61
62
|
const existing = loadReportsIndex(args.basePath);
|
|
62
63
|
const index = existing ?? {
|
|
@@ -97,7 +98,7 @@ export function writeReportSnapshot(args) {
|
|
|
97
98
|
// ─── HTML Index Generator ─────────────────────────────────────────────────────
|
|
98
99
|
export function regenerateHtmlIndex(basePath, index) {
|
|
99
100
|
const html = buildIndexHtml(index);
|
|
100
|
-
|
|
101
|
+
atomicWriteSync(reportsHtmlIndexPath(basePath), html, 'utf-8');
|
|
101
102
|
}
|
|
102
103
|
function buildIndexHtml(index) {
|
|
103
104
|
const { projectName, projectPath, gsdVersion, entries } = index;
|
|
@@ -16,6 +16,7 @@ import { checkOwnership, sliceUnitKey } from "../unit-ownership.js";
|
|
|
16
16
|
import { saveFile, clearParseCache } from "../files.js";
|
|
17
17
|
import { invalidateStateCache } from "../state.js";
|
|
18
18
|
import { renderRoadmapCheckboxes } from "../markdown-renderer.js";
|
|
19
|
+
import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
19
20
|
import { renderAllProjections } from "../workflow-projections.js";
|
|
20
21
|
import { writeManifest } from "../workflow-manifest.js";
|
|
21
22
|
import { appendEvent } from "../workflow-events.js";
|
|
@@ -228,6 +229,10 @@ export async function handleCompleteSlice(params, basePath) {
|
|
|
228
229
|
}
|
|
229
230
|
const slice = getSlice(params.milestoneId, params.sliceId);
|
|
230
231
|
if (slice && isClosedStatus(slice.status)) {
|
|
232
|
+
if (isStaleWrite("complete-slice")) {
|
|
233
|
+
guardError = "__stale_duplicate__";
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
231
236
|
guardError = `slice ${params.sliceId} is already complete — use gsd_slice_reopen first if you need to redo it`;
|
|
232
237
|
return;
|
|
233
238
|
}
|
|
@@ -248,6 +253,22 @@ export async function handleCompleteSlice(params, basePath) {
|
|
|
248
253
|
insertSlice({ id: params.sliceId, milestoneId: params.milestoneId, title: params.sliceId });
|
|
249
254
|
updateSliceStatus(params.milestoneId, params.sliceId, "complete", completedAt);
|
|
250
255
|
});
|
|
256
|
+
if (guardError === "__stale_duplicate__") {
|
|
257
|
+
// Stale duplicate from a turn superseded by timeout recovery. Return a
|
|
258
|
+
// non-mutating success so the orphaned LLM tool call unwinds quietly.
|
|
259
|
+
const sliceDir = resolveSlicePath(basePath, params.milestoneId, params.sliceId);
|
|
260
|
+
const staleSummaryPath = sliceDir
|
|
261
|
+
? join(sliceDir, `${params.sliceId}-SUMMARY.md`)
|
|
262
|
+
: join(basePath, ".gsd", "milestones", params.milestoneId, "slices", params.sliceId, `${params.sliceId}-SUMMARY.md`);
|
|
263
|
+
return {
|
|
264
|
+
sliceId: params.sliceId,
|
|
265
|
+
milestoneId: params.milestoneId,
|
|
266
|
+
summaryPath: staleSummaryPath,
|
|
267
|
+
uatPath: staleSummaryPath.replace(/-SUMMARY\.md$/, "-UAT.md"),
|
|
268
|
+
duplicate: true,
|
|
269
|
+
stale: true,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
251
272
|
if (guardError) {
|
|
252
273
|
return { error: guardError };
|
|
253
274
|
}
|
|
@@ -21,6 +21,7 @@ import { writeManifest } from "../workflow-manifest.js";
|
|
|
21
21
|
import { appendEvent } from "../workflow-events.js";
|
|
22
22
|
import { logWarning, logError } from "../workflow-logger.js";
|
|
23
23
|
import { loadEffectiveGSDPreferences } from "../preferences.js";
|
|
24
|
+
import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
24
25
|
import { buildEscalationArtifact, writeEscalationArtifact } from "../escalation.js";
|
|
25
26
|
/**
|
|
26
27
|
* Map an execute-task-owned gate id to the CompleteTaskParams field whose
|
|
@@ -167,6 +168,18 @@ export async function handleCompleteTask(params, basePath) {
|
|
|
167
168
|
}
|
|
168
169
|
const existingTask = getTask(params.milestoneId, params.sliceId, params.taskId);
|
|
169
170
|
if (existingTask && isClosedStatus(existingTask.status)) {
|
|
171
|
+
// Stale-turn path: a timed-out turn that was superseded by recovery
|
|
172
|
+
// can still reach this code when its LLM call eventually returns and
|
|
173
|
+
// invokes gsd_complete_task. Returning an error would produce noisy
|
|
174
|
+
// "already complete — use reopen first" logs in the orphaned turn.
|
|
175
|
+
// Instead, signal the duplicate via a non-mutating success shape that
|
|
176
|
+
// callers can detect via `duplicate: true` / `stale: true`.
|
|
177
|
+
if (isStaleWrite("complete-task")) {
|
|
178
|
+
// Sentinel handled below — outside the transaction — so we don't
|
|
179
|
+
// render SUMMARY.md or flip plan checkboxes for a stale duplicate.
|
|
180
|
+
guardError = "__stale_duplicate__";
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
170
183
|
guardError = `task ${params.taskId} is already complete — use gsd_task_reopen first if you need to redo it`;
|
|
171
184
|
return;
|
|
172
185
|
}
|
|
@@ -201,6 +214,24 @@ export async function handleCompleteTask(params, basePath) {
|
|
|
201
214
|
});
|
|
202
215
|
}
|
|
203
216
|
});
|
|
217
|
+
if (guardError === "__stale_duplicate__") {
|
|
218
|
+
// Orphaned-turn duplicate: the task is already complete from the
|
|
219
|
+
// superseded turn's earlier (real) call. Return a non-mutating success
|
|
220
|
+
// so the stale LLM tool call unwinds cleanly. summaryPath is synthesized
|
|
221
|
+
// from the existing on-disk layout; no file is written.
|
|
222
|
+
const tasksDir = resolveTasksDir(basePath, params.milestoneId, params.sliceId);
|
|
223
|
+
const staleSummaryPath = tasksDir
|
|
224
|
+
? join(tasksDir, `${params.taskId}-SUMMARY.md`)
|
|
225
|
+
: join(basePath, ".gsd", "milestones", params.milestoneId, "slices", params.sliceId, "tasks", `${params.taskId}-SUMMARY.md`);
|
|
226
|
+
return {
|
|
227
|
+
taskId: params.taskId,
|
|
228
|
+
sliceId: params.sliceId,
|
|
229
|
+
milestoneId: params.milestoneId,
|
|
230
|
+
summaryPath: staleSummaryPath,
|
|
231
|
+
duplicate: true,
|
|
232
|
+
stale: true,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
204
235
|
if (guardError) {
|
|
205
236
|
return { error: guardError };
|
|
206
237
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { appendFileSync, mkdirSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { isStaleWrite } from "../auto/turn-epoch.js";
|
|
5
|
+
import { withFileLockSync } from "../file-lock.js";
|
|
4
6
|
import { gsdRoot } from "../paths.js";
|
|
5
7
|
import { isDbAvailable, insertAuditEvent } from "../gsd-db.js";
|
|
6
8
|
function auditLogPath(basePath) {
|
|
@@ -22,9 +24,23 @@ export function buildAuditEnvelope(args) {
|
|
|
22
24
|
};
|
|
23
25
|
}
|
|
24
26
|
export function emitUokAuditEvent(basePath, event) {
|
|
27
|
+
// Drop writes from a turn superseded by timeout recovery / cancellation.
|
|
28
|
+
if (isStaleWrite("uok-audit"))
|
|
29
|
+
return;
|
|
25
30
|
try {
|
|
26
31
|
ensureAuditDir(basePath);
|
|
27
|
-
|
|
32
|
+
const path = auditLogPath(basePath);
|
|
33
|
+
// proper-lockfile requires the target file to exist before locking.
|
|
34
|
+
// Touch it via open(O_APPEND|O_CREAT) so the first writer wins the race
|
|
35
|
+
// atomically at the kernel level.
|
|
36
|
+
if (!existsSync(path))
|
|
37
|
+
closeSync(openSync(path, "a"));
|
|
38
|
+
// onLocked: "skip" — audit writes are best-effort; under heavy contention
|
|
39
|
+
// POSIX O_APPEND atomicity still protects small line writes, so skipping
|
|
40
|
+
// the lock rather than stalling orchestration is the correct tradeoff.
|
|
41
|
+
withFileLockSync(path, () => {
|
|
42
|
+
appendFileSync(path, `${JSON.stringify(event)}\n`, "utf-8");
|
|
43
|
+
}, { onLocked: "skip" });
|
|
28
44
|
}
|
|
29
45
|
catch {
|
|
30
46
|
// Best-effort: audit writes must never break orchestration.
|
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
// a process. The auto-loop must call _resetLogs() (or drainAndSummarize()) at
|
|
16
16
|
// the start of each unit to prevent log bleed between units running in the same
|
|
17
17
|
// Node process.
|
|
18
|
-
import { appendFileSync,
|
|
18
|
+
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, } from "node:fs";
|
|
19
19
|
import { join } from "node:path";
|
|
20
|
+
import { withFileLockSync } from "./file-lock.js";
|
|
20
21
|
import { appendNotification } from "./notification-store.js";
|
|
21
22
|
import { buildAuditEnvelope, emitUokAuditEvent } from "./uok/audit.js";
|
|
22
23
|
import { isUnifiedAuditEnabled } from "./uok/audit-toggle.js";
|
|
@@ -231,8 +232,15 @@ function _push(severity, component, message, context) {
|
|
|
231
232
|
try {
|
|
232
233
|
const auditDir = join(_auditBasePath, ".gsd");
|
|
233
234
|
mkdirSync(auditDir, { recursive: true });
|
|
235
|
+
const auditPath = join(auditDir, "audit-log.jsonl");
|
|
234
236
|
const sanitized = _sanitizeForAudit(entry);
|
|
235
|
-
|
|
237
|
+
// Ensure file exists so proper-lockfile can acquire a lock against it.
|
|
238
|
+
if (!existsSync(auditPath))
|
|
239
|
+
closeSync(openSync(auditPath, "a"));
|
|
240
|
+
// onLocked: "skip" — never block error logging on lock contention.
|
|
241
|
+
withFileLockSync(auditPath, () => {
|
|
242
|
+
appendFileSync(auditPath, JSON.stringify(sanitized) + "\n", "utf-8");
|
|
243
|
+
}, { onLocked: "skip" });
|
|
236
244
|
}
|
|
237
245
|
catch (auditErr) {
|
|
238
246
|
// Best-effort — never let audit write failures bubble up
|