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
|
@@ -723,7 +723,7 @@ export async function resolveClaudePermissionMode(
|
|
|
723
723
|
);
|
|
724
724
|
return "bypassPermissions";
|
|
725
725
|
}
|
|
726
|
-
return "
|
|
726
|
+
return "bypassPermissions";
|
|
727
727
|
}
|
|
728
728
|
|
|
729
729
|
// NOTE: These helpers intentionally mirror @gsd/pi-ai anthropic-shared
|
|
@@ -783,21 +783,25 @@ export function buildSdkOptions(
|
|
|
783
783
|
): Record<string, unknown> {
|
|
784
784
|
const { reasoning, ...sdkExtraOptions } = extraOptions;
|
|
785
785
|
const mcpServers = buildWorkflowMcpServers();
|
|
786
|
-
const permissionMode = overrides?.permissionMode ?? "
|
|
787
|
-
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
// every `
|
|
792
|
-
//
|
|
786
|
+
const permissionMode = overrides?.permissionMode ?? "bypassPermissions";
|
|
787
|
+
// Globally unblock all tools. Users reported that the `acceptEdits` default
|
|
788
|
+
// plus a narrow allowlist silently declined most Bash/Agent/WebFetch calls
|
|
789
|
+
// when `extensionUIContext` wasn't threaded through. Default to
|
|
790
|
+
// bypassPermissions and an empty disallow list so every tool — including
|
|
791
|
+
// `AskUserQuestion` and every `mcp__*` workflow tool — is auto-approved.
|
|
792
|
+
// Opt back into gated mode with GSD_CLAUDE_CODE_PERMISSION_MODE=acceptEdits.
|
|
793
|
+
const disallowedTools: string[] = [];
|
|
793
794
|
const allowedTools = [
|
|
794
795
|
"Read",
|
|
795
796
|
"Write",
|
|
796
797
|
"Edit",
|
|
797
798
|
"Glob",
|
|
798
799
|
"Grep",
|
|
799
|
-
"Bash
|
|
800
|
-
"
|
|
800
|
+
"Bash",
|
|
801
|
+
"Agent",
|
|
802
|
+
"WebFetch",
|
|
803
|
+
"WebSearch",
|
|
804
|
+
"AskUserQuestion",
|
|
801
805
|
...(mcpServers ? Object.keys(mcpServers).map((serverName) => `mcp__${serverName}__*`) : []),
|
|
802
806
|
];
|
|
803
807
|
const supportsAdaptive = modelSupportsAdaptiveThinking(modelId);
|
|
@@ -761,15 +761,18 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
761
761
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
762
762
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
763
763
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, "/tmp/project");
|
|
764
|
-
assert.deepEqual(options.disallowedTools, [
|
|
764
|
+
assert.deepEqual(options.disallowedTools, []);
|
|
765
765
|
assert.deepEqual(options.allowedTools, [
|
|
766
766
|
"Read",
|
|
767
767
|
"Write",
|
|
768
768
|
"Edit",
|
|
769
769
|
"Glob",
|
|
770
770
|
"Grep",
|
|
771
|
-
"Bash
|
|
772
|
-
"
|
|
771
|
+
"Bash",
|
|
772
|
+
"Agent",
|
|
773
|
+
"WebFetch",
|
|
774
|
+
"WebSearch",
|
|
775
|
+
"AskUserQuestion",
|
|
773
776
|
"mcp__gsd-workflow__*",
|
|
774
777
|
]);
|
|
775
778
|
} finally {
|
|
@@ -781,7 +784,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
781
784
|
}
|
|
782
785
|
});
|
|
783
786
|
|
|
784
|
-
test("buildSdkOptions
|
|
787
|
+
test("buildSdkOptions auto-approves every tool for custom workflow MCP server names", () => {
|
|
785
788
|
const prev = {
|
|
786
789
|
GSD_WORKFLOW_MCP_COMMAND: process.env.GSD_WORKFLOW_MCP_COMMAND,
|
|
787
790
|
GSD_WORKFLOW_MCP_NAME: process.env.GSD_WORKFLOW_MCP_NAME,
|
|
@@ -799,15 +802,18 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
799
802
|
const options = buildSdkOptions("claude-sonnet-4-20250514", "test");
|
|
800
803
|
const mcpServers = options.mcpServers as Record<string, any>;
|
|
801
804
|
assert.ok(mcpServers?.["custom-workflow"], "expected custom workflow server config");
|
|
802
|
-
assert.deepEqual(options.disallowedTools, [
|
|
805
|
+
assert.deepEqual(options.disallowedTools, []);
|
|
803
806
|
assert.deepEqual(options.allowedTools, [
|
|
804
807
|
"Read",
|
|
805
808
|
"Write",
|
|
806
809
|
"Edit",
|
|
807
810
|
"Glob",
|
|
808
811
|
"Grep",
|
|
809
|
-
"Bash
|
|
810
|
-
"
|
|
812
|
+
"Bash",
|
|
813
|
+
"Agent",
|
|
814
|
+
"WebFetch",
|
|
815
|
+
"WebSearch",
|
|
816
|
+
"AskUserQuestion",
|
|
811
817
|
"mcp__custom-workflow__*",
|
|
812
818
|
]);
|
|
813
819
|
} finally {
|
|
@@ -845,9 +851,9 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
845
851
|
const mcpServers = (options as any).mcpServers;
|
|
846
852
|
if (mcpServers) {
|
|
847
853
|
assert.ok(mcpServers["gsd-workflow"], "if present, must be gsd-workflow");
|
|
848
|
-
assert.deepEqual((options as any).disallowedTools, [
|
|
854
|
+
assert.deepEqual((options as any).disallowedTools, []);
|
|
849
855
|
} else {
|
|
850
|
-
assert.deepEqual((options as any).disallowedTools, [
|
|
856
|
+
assert.deepEqual((options as any).disallowedTools, []);
|
|
851
857
|
}
|
|
852
858
|
rmSync(emptyDir, { recursive: true, force: true });
|
|
853
859
|
} finally {
|
|
@@ -894,7 +900,7 @@ describe("stream-adapter — session persistence (#2859)", () => {
|
|
|
894
900
|
assert.equal(srv.env.GSD_CLI_PATH, "/tmp/gsd");
|
|
895
901
|
assert.equal(srv.env.GSD_PERSIST_WRITE_GATE_STATE, "1");
|
|
896
902
|
assert.equal(srv.env.GSD_WORKFLOW_PROJECT_ROOT, resolvedRepoDir);
|
|
897
|
-
assert.deepEqual(options.disallowedTools, [
|
|
903
|
+
assert.deepEqual(options.disallowedTools, []);
|
|
898
904
|
} finally {
|
|
899
905
|
process.chdir(originalCwd);
|
|
900
906
|
rmSync(repoDir, { recursive: true, force: true });
|
|
@@ -1282,14 +1288,14 @@ describe("stream-adapter — permission mode (F10)", () => {
|
|
|
1282
1288
|
}
|
|
1283
1289
|
}
|
|
1284
1290
|
|
|
1285
|
-
test("buildSdkOptions defaults to
|
|
1291
|
+
test("buildSdkOptions defaults to bypassPermissions (globally unblocks all tools)", () => {
|
|
1286
1292
|
clearWorkflowMcpEnv();
|
|
1287
1293
|
const opts = buildSdkOptions("claude-sonnet-4-6", "test");
|
|
1288
|
-
assert.equal(opts.permissionMode, "
|
|
1294
|
+
assert.equal(opts.permissionMode, "bypassPermissions");
|
|
1289
1295
|
assert.equal(
|
|
1290
1296
|
opts.allowDangerouslySkipPermissions,
|
|
1291
|
-
|
|
1292
|
-
"allowDangerouslySkipPermissions must be
|
|
1297
|
+
true,
|
|
1298
|
+
"allowDangerouslySkipPermissions must be true when permissionMode is bypassPermissions",
|
|
1293
1299
|
);
|
|
1294
1300
|
});
|
|
1295
1301
|
|
|
@@ -1304,9 +1310,9 @@ describe("stream-adapter — permission mode (F10)", () => {
|
|
|
1304
1310
|
);
|
|
1305
1311
|
});
|
|
1306
1312
|
|
|
1307
|
-
test("resolveClaudePermissionMode defaults to
|
|
1313
|
+
test("resolveClaudePermissionMode defaults to bypassPermissions when no env var is set (globally unblocks all tools)", async () => {
|
|
1308
1314
|
const mode = await resolveClaudePermissionMode({});
|
|
1309
|
-
assert.equal(mode, "
|
|
1315
|
+
assert.equal(mode, "bypassPermissions");
|
|
1310
1316
|
});
|
|
1311
1317
|
|
|
1312
1318
|
test("resolveClaudePermissionMode honours the GSD_CLAUDE_CODE_PERMISSION_MODE env override", async () => {
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abandon-milestone detection for rewrite-docs overrides (#3490).
|
|
3
|
+
*
|
|
4
|
+
* Isolated from auto-post-unit.ts so behavioral tests can import this module
|
|
5
|
+
* without pulling in the full post-unit handler graph (which transitively
|
|
6
|
+
* loads model-router, workflow engine, etc.).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { Override } from "./files.js";
|
|
10
|
+
|
|
11
|
+
// Detect when a rewrite-docs override is about abandoning THE CURRENT
|
|
12
|
+
// MILESTONE — not just any override containing an abandon verb. Naively
|
|
13
|
+
// matching `/\b(abandon|cancel|drop|...)\b/` against override text produces
|
|
14
|
+
// false positives on scope-change prose ("cancel the standup reminder",
|
|
15
|
+
// "drop the dependency on X", "scrap the v1 design for the landing page").
|
|
16
|
+
//
|
|
17
|
+
// To qualify as an abandon-milestone signal, an override must contain both:
|
|
18
|
+
// 1. An abandon-family verb (abandon|descope|cancel|shelve|drop|scrap)
|
|
19
|
+
// 2. A milestone reference — either the literal word "milestone" or the
|
|
20
|
+
// current milestone ID — in the same override text.
|
|
21
|
+
|
|
22
|
+
// Verb variants cover both US and UK inflections:
|
|
23
|
+
// cancel / canceled / canceling / cancelled / cancelling / cancels
|
|
24
|
+
// travel-style "l"-doubling also applies to shelve/drop/scrap.
|
|
25
|
+
// "descope" also accepts "de-scope" and "de scope" (hyphen / space forms).
|
|
26
|
+
const ABANDON_VERB_RE = /\b(abandon(?:ed|ing|s)?|de[-\s]?scope(?:d|s|ing)?|cancel(?:led|ling|ed|ing|s)?|shelve(?:d|s)?|shelving|drop(?:ped|ping|s)?|scrap(?:ped|ping|s)?)\b/i;
|
|
27
|
+
|
|
28
|
+
export interface AbandonDecision {
|
|
29
|
+
shouldPark: boolean;
|
|
30
|
+
reason: string;
|
|
31
|
+
matched: string[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Decide whether a set of active overrides indicates the current milestone
|
|
36
|
+
* should be parked. Pure function — no I/O, no imports beyond types.
|
|
37
|
+
*/
|
|
38
|
+
export function detectAbandonMilestone(
|
|
39
|
+
overrides: Override[],
|
|
40
|
+
currentMilestoneId: string | null | undefined,
|
|
41
|
+
): AbandonDecision {
|
|
42
|
+
if (!currentMilestoneId) {
|
|
43
|
+
return { shouldPark: false, reason: "", matched: [] };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const escapedId = currentMilestoneId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
47
|
+
const milestoneRefRe = new RegExp(`\\b(?:milestone|${escapedId})\\b`, "i");
|
|
48
|
+
|
|
49
|
+
const matched = overrides
|
|
50
|
+
.filter(o => ABANDON_VERB_RE.test(o.change) && milestoneRefRe.test(o.change))
|
|
51
|
+
.map(o => o.change);
|
|
52
|
+
|
|
53
|
+
if (matched.length === 0) {
|
|
54
|
+
return { shouldPark: false, reason: "", matched: [] };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
shouldPark: true,
|
|
59
|
+
reason: matched.join("; "),
|
|
60
|
+
matched,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { UnitResult, AgentEndEvent, ErrorContext } from "./types.js";
|
|
12
12
|
import type { AutoSession } from "./session.js";
|
|
13
13
|
import { debugLog } from "../debug-logger.js";
|
|
14
|
+
import { bumpTurnGeneration } from "./turn-epoch.js";
|
|
14
15
|
|
|
15
16
|
// ─── Per-unit one-shot promise state ────────────────────────────────────────
|
|
16
17
|
//
|
|
@@ -68,6 +69,24 @@ export function isSessionSwitchInFlight(): boolean {
|
|
|
68
69
|
return _sessionSwitchInFlight;
|
|
69
70
|
}
|
|
70
71
|
|
|
72
|
+
// ─── bumpAndResolveSynthetic ────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Bump the turn epoch and synthetically resolve the pending unit promise —
|
|
76
|
+
* the exact sequence timeout recovery must perform when it advances past a
|
|
77
|
+
* timed-out unit. Using this helper enforces the invariant "bump iff we are
|
|
78
|
+
* actually superseding the turn" so a future caller cannot resolve without
|
|
79
|
+
* bumping (orphaned writes leak) or bump without resolving (next turn starts
|
|
80
|
+
* already stale).
|
|
81
|
+
*
|
|
82
|
+
* NOT to be used for steering retries that keep the same turn alive — those
|
|
83
|
+
* do not supersede the turn and must not bump.
|
|
84
|
+
*/
|
|
85
|
+
export function bumpAndResolveSynthetic(reason: string): void {
|
|
86
|
+
bumpTurnGeneration(reason);
|
|
87
|
+
resolveAgentEnd({ messages: [], _synthetic: reason } as unknown as AgentEndEvent);
|
|
88
|
+
}
|
|
89
|
+
|
|
71
90
|
// ─── resolveAgentEndCancelled ─────────────────────────────────────────────────
|
|
72
91
|
|
|
73
92
|
/**
|
|
@@ -79,6 +98,12 @@ export function isSessionSwitchInFlight(): boolean {
|
|
|
79
98
|
*/
|
|
80
99
|
export function resolveAgentEndCancelled(errorContext?: ErrorContext): void {
|
|
81
100
|
if (_currentResolve) {
|
|
101
|
+
// Cancellation supersedes the in-flight turn the same way timeout
|
|
102
|
+
// recovery does — bump the turn epoch so any lingering writes from the
|
|
103
|
+
// cancelled turn drop themselves.
|
|
104
|
+
bumpTurnGeneration(
|
|
105
|
+
`cancelled:${errorContext?.category ?? "unknown"}`,
|
|
106
|
+
);
|
|
82
107
|
debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
|
|
83
108
|
const r = _currentResolve;
|
|
84
109
|
_currentResolve = null;
|
|
@@ -97,6 +122,10 @@ export function _resetPendingResolve(): void {
|
|
|
97
122
|
_sessionSwitchInFlight = false;
|
|
98
123
|
}
|
|
99
124
|
|
|
125
|
+
export function _hasPendingResolveForTest(): boolean {
|
|
126
|
+
return _currentResolve !== null;
|
|
127
|
+
}
|
|
128
|
+
|
|
100
129
|
/**
|
|
101
130
|
* No-op for backward compatibility with tests that previously set the
|
|
102
131
|
* active session. The module no longer holds a session reference.
|
|
@@ -9,7 +9,11 @@ import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
|
9
9
|
import type { AutoSession } from "./session.js";
|
|
10
10
|
import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
|
|
11
11
|
import type { UnitResult } from "./types.js";
|
|
12
|
-
import { _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
|
|
12
|
+
import { _clearCurrentResolve, _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
|
|
13
|
+
import {
|
|
14
|
+
getCurrentTurnGeneration,
|
|
15
|
+
runWithTurnGeneration,
|
|
16
|
+
} from "./turn-epoch.js";
|
|
13
17
|
import { debugLog } from "../debug-logger.js";
|
|
14
18
|
import { logWarning, logError } from "../workflow-logger.js";
|
|
15
19
|
import { resolveAutoSupervisorConfig } from "../preferences.js";
|
|
@@ -144,6 +148,7 @@ export async function runUnit(
|
|
|
144
148
|
}
|
|
145
149
|
|
|
146
150
|
if (!ready) {
|
|
151
|
+
_clearCurrentResolve();
|
|
147
152
|
return {
|
|
148
153
|
status: "cancelled",
|
|
149
154
|
errorContext: {
|
|
@@ -156,6 +161,13 @@ export async function runUnit(
|
|
|
156
161
|
}
|
|
157
162
|
}
|
|
158
163
|
|
|
164
|
+
// ── Capture turn generation for stale-write detection ──
|
|
165
|
+
// Any write site reached via the sendMessage → tool-call → await chain
|
|
166
|
+
// below sees this generation via AsyncLocalStorage. If a timeout recovery
|
|
167
|
+
// or cancellation bumps the generation while this turn is in flight, those
|
|
168
|
+
// writes see themselves as stale and self-drop.
|
|
169
|
+
const capturedTurnGen = getCurrentTurnGeneration();
|
|
170
|
+
|
|
159
171
|
// ── Send the prompt ──
|
|
160
172
|
debugLog("runUnit", { phase: "send-message", unitType, unitId });
|
|
161
173
|
|
|
@@ -179,7 +191,9 @@ export async function runUnit(
|
|
|
179
191
|
resolve({ status: "cancelled", errorContext: { message: "Unit hard timeout — supervision may have failed", category: "timeout", isTransient: true } });
|
|
180
192
|
}, UNIT_HARD_TIMEOUT_MS);
|
|
181
193
|
});
|
|
182
|
-
const result = await
|
|
194
|
+
const result = await runWithTurnGeneration(capturedTurnGen, () =>
|
|
195
|
+
Promise.race([unitPromise, timeoutResult]),
|
|
196
|
+
);
|
|
183
197
|
if (unitTimeoutHandle) clearTimeout(unitTimeoutHandle);
|
|
184
198
|
debugLog("runUnit", {
|
|
185
199
|
phase: "agent-end-received",
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* auto/turn-epoch.ts — Turn generation counter + AsyncLocalStorage-backed
|
|
3
|
+
* capture for stale-turn write dropping.
|
|
4
|
+
*
|
|
5
|
+
* Problem: when auto-timeout-recovery synthetically resolves a timed-out
|
|
6
|
+
* unit so the loop can advance, the original LLM turn keeps running in the
|
|
7
|
+
* background. Its subsequent writes (journal events, audit events, tool
|
|
8
|
+
* calls that flow through closeout) then race the replacement unit's
|
|
9
|
+
* writes. DB-level guards (complete-task/complete-slice) block double
|
|
10
|
+
* state transitions, but journal/audit/closeout side-effects still fire
|
|
11
|
+
* with fresh identifiers and pollute forensics.
|
|
12
|
+
*
|
|
13
|
+
* Containment: every time we decide a turn is done (timeout recovery,
|
|
14
|
+
* explicit cancellation), bump a module-level generation counter.
|
|
15
|
+
* Turn-aware call sites wrap their body in `runWithTurnGeneration`, which
|
|
16
|
+
* captures the generation into AsyncLocalStorage. Write sites deep in the
|
|
17
|
+
* stack call `isStaleWrite` — if the captured generation is older than
|
|
18
|
+
* current, the turn has been superseded and the write is dropped.
|
|
19
|
+
*
|
|
20
|
+
* Failure mode: if AsyncLocalStorage context is lost across some exotic
|
|
21
|
+
* async boundary (e.g. a native-side worker callback), the write site sees
|
|
22
|
+
* `no-store` and falls through to current behavior — the write proceeds
|
|
23
|
+
* normally. That is a safe default; the correctness regression is only
|
|
24
|
+
* "noisier forensics under rare boundary loss," not duplicated state.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
28
|
+
|
|
29
|
+
import { debugLog } from "../debug-logger.js";
|
|
30
|
+
|
|
31
|
+
let _currentGeneration = 0;
|
|
32
|
+
|
|
33
|
+
const turnContext = new AsyncLocalStorage<{ capturedGen: number }>();
|
|
34
|
+
|
|
35
|
+
/** Current turn generation. Mutated only by bumpTurnGeneration. */
|
|
36
|
+
export function getCurrentTurnGeneration(): number {
|
|
37
|
+
return _currentGeneration;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Bump the turn generation and return the new value. Every caller should
|
|
42
|
+
* pass a short `reason` string so forensics can reconstruct why a given
|
|
43
|
+
* turn was marked stale.
|
|
44
|
+
*/
|
|
45
|
+
export function bumpTurnGeneration(reason: string): number {
|
|
46
|
+
_currentGeneration += 1;
|
|
47
|
+
debugLog("turnEpoch.bump", { reason, newGeneration: _currentGeneration });
|
|
48
|
+
return _currentGeneration;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run fn() with `capturedGen` attached to AsyncLocalStorage so that any
|
|
53
|
+
* write site reached from within fn() can check for staleness without
|
|
54
|
+
* parameter threading.
|
|
55
|
+
*/
|
|
56
|
+
export function runWithTurnGeneration<T>(capturedGen: number, fn: () => T): T {
|
|
57
|
+
return turnContext.run({ capturedGen }, fn);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* True when the current async context was started at a turn generation
|
|
62
|
+
* older than the current one — meaning the turn has been superseded by
|
|
63
|
+
* recovery/cancellation since it began.
|
|
64
|
+
*
|
|
65
|
+
* Returns false when there is no captured generation (e.g. the write is
|
|
66
|
+
* happening outside any wrapped turn). That is the safe default: writes
|
|
67
|
+
* proceed as they did before this epoch was introduced.
|
|
68
|
+
*/
|
|
69
|
+
export function isStaleWrite(component?: string): boolean {
|
|
70
|
+
const store = turnContext.getStore();
|
|
71
|
+
if (!store) return false;
|
|
72
|
+
const captured = store.capturedGen;
|
|
73
|
+
const current = _currentGeneration;
|
|
74
|
+
if (captured < current) {
|
|
75
|
+
debugLog("turnEpoch.stale", {
|
|
76
|
+
component: component ?? "unknown",
|
|
77
|
+
captured,
|
|
78
|
+
current,
|
|
79
|
+
});
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Snapshot of both the captured turn generation and the current one.
|
|
87
|
+
* Used by closeoutUnit to persist an orphan-marker entry instead of
|
|
88
|
+
* silently skipping the full closeout on a stale turn.
|
|
89
|
+
*/
|
|
90
|
+
export function describeTurnEpoch(): {
|
|
91
|
+
captured: number | null;
|
|
92
|
+
current: number;
|
|
93
|
+
stale: boolean;
|
|
94
|
+
} {
|
|
95
|
+
const store = turnContext.getStore();
|
|
96
|
+
const captured = store?.capturedGen ?? null;
|
|
97
|
+
const current = _currentGeneration;
|
|
98
|
+
return {
|
|
99
|
+
captured,
|
|
100
|
+
current,
|
|
101
|
+
stale: captured !== null && captured < current,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Test helper — resets module state so tests start from a known baseline. */
|
|
106
|
+
export function _resetTurnEpoch(): void {
|
|
107
|
+
_currentGeneration = 0;
|
|
108
|
+
}
|
|
@@ -421,6 +421,26 @@ export const DISPATCH_RULES: DispatchRule[] = [
|
|
|
421
421
|
};
|
|
422
422
|
},
|
|
423
423
|
},
|
|
424
|
+
{
|
|
425
|
+
name: "planning (require_slice_discussion) → pause for discussion (#3454)",
|
|
426
|
+
match: async ({ state, mid, basePath, prefs }) => {
|
|
427
|
+
if (state.phase !== "planning") return null;
|
|
428
|
+
if (!prefs?.phases?.require_slice_discussion) return null;
|
|
429
|
+
if (!state.activeSlice) return null;
|
|
430
|
+
// Only pause if the slice has no context file yet (discussion not done).
|
|
431
|
+
// resolveSliceFile returns null when the file does not exist on disk,
|
|
432
|
+
// but cachedReaddir could return a stale hit — verify with existsSync
|
|
433
|
+
// so the guard is defence-in-depth and the contract is explicit at the
|
|
434
|
+
// call site.
|
|
435
|
+
const sliceContextFile = resolveSliceFile(basePath, mid, state.activeSlice.id, "CONTEXT");
|
|
436
|
+
if (sliceContextFile && existsSync(sliceContextFile)) return null; // discussion already done, proceed
|
|
437
|
+
return {
|
|
438
|
+
action: "stop" as const,
|
|
439
|
+
reason: `Slice ${state.activeSlice.id} requires discussion before planning (require_slice_discussion is enabled). Run /gsd discuss to discuss this slice, then /gsd auto to resume.`,
|
|
440
|
+
level: "info" as const,
|
|
441
|
+
};
|
|
442
|
+
},
|
|
443
|
+
},
|
|
424
444
|
{
|
|
425
445
|
// Keep this rule before the single-slice research rule so the multi-slice
|
|
426
446
|
// path wins whenever 2+ slices are ready.
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
export { autoLoop, runUokKernelLoop, runLegacyAutoLoop } from "./auto/loop.js";
|
|
11
11
|
export { isInfrastructureError, INFRA_ERROR_CODES } from "./auto/infra-errors.js";
|
|
12
|
-
export { resolveAgentEnd, resolveAgentEndCancelled, isSessionSwitchInFlight, _resetPendingResolve, _setActiveSession } from "./auto/resolve.js";
|
|
12
|
+
export { resolveAgentEnd, resolveAgentEndCancelled, isSessionSwitchInFlight, _hasPendingResolveForTest, _resetPendingResolve, _setActiveSession } from "./auto/resolve.js";
|
|
13
13
|
export { detectStuck } from "./auto/detect-stuck.js";
|
|
14
14
|
export { runUnit } from "./auto/run-unit.js";
|
|
15
15
|
export type { LoopDeps } from "./auto/loop-deps.js";
|
|
@@ -71,6 +71,7 @@ import { resolveUokFlags } from "./uok/flags.js";
|
|
|
71
71
|
import { UokGateRunner } from "./uok/gate-runner.js";
|
|
72
72
|
import { writeTurnGitTransaction } from "./uok/gitops.js";
|
|
73
73
|
import { isClosedStatus } from "./status-guards.js";
|
|
74
|
+
import { detectAbandonMilestone } from "./abandon-detect.js";
|
|
74
75
|
|
|
75
76
|
/** Maximum verification retry attempts before escalating to blocker placeholder (#2653). */
|
|
76
77
|
const MAX_VERIFICATION_RETRIES = 3;
|
|
@@ -569,6 +570,35 @@ export async function postUnitPreVerification(pctx: PostUnitContext, opts?: PreV
|
|
|
569
570
|
// Rewrite-docs completion
|
|
570
571
|
if (s.currentUnit.type === "rewrite-docs") {
|
|
571
572
|
await runSafely("postUnit", "rewrite-docs-resolve", async () => {
|
|
573
|
+
// Detect abandon/descope overrides BEFORE resolving them (#3490).
|
|
574
|
+
// If an override is about abandoning the milestone, park it so the
|
|
575
|
+
// state engine skips it. Without this, rewrite-docs only edits
|
|
576
|
+
// markdown but the DB still has the milestone as active.
|
|
577
|
+
try {
|
|
578
|
+
const { loadActiveOverrides } = await import("./files.js");
|
|
579
|
+
const overrides = await loadActiveOverrides(s.basePath);
|
|
580
|
+
const decision = detectAbandonMilestone(overrides, s.currentMilestoneId);
|
|
581
|
+
if (decision.shouldPark && s.currentMilestoneId) {
|
|
582
|
+
const { parkMilestone } = await import("./milestone-actions.js");
|
|
583
|
+
const parked = parkMilestone(s.basePath, s.currentMilestoneId, decision.reason);
|
|
584
|
+
if (parked) {
|
|
585
|
+
ctx.ui.notify(`Milestone ${s.currentMilestoneId} parked: "${decision.reason}"`, "info");
|
|
586
|
+
} else {
|
|
587
|
+
// Park refused: milestone directory missing, milestone already
|
|
588
|
+
// completed (SUMMARY present), or PARKED.md already exists.
|
|
589
|
+
// resolveAllOverrides below will still consume the override —
|
|
590
|
+
// surface this loudly so the user notices state drift rather
|
|
591
|
+
// than silently losing the abandon directive.
|
|
592
|
+
const msg = `Abandon detected for ${s.currentMilestoneId} but park refused (milestone is completed, already parked, or missing). Override will be resolved anyway — verify state is correct.`;
|
|
593
|
+
logError("engine", msg);
|
|
594
|
+
ctx.ui.notify(msg, "warning");
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
} catch (err) {
|
|
598
|
+
logError("engine", `abandon-detect failed: ${(err as Error).message}`);
|
|
599
|
+
ctx.ui.notify(`Abandon detection failed — check logs. Overrides will still be resolved.`, "warning");
|
|
600
|
+
}
|
|
601
|
+
|
|
572
602
|
await resolveAllOverrides(s.basePath);
|
|
573
603
|
// Reset both disk and in-memory counters. Disk counter is authoritative
|
|
574
604
|
// (survives restarts); in-memory is kept in sync for the current session.
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
} from "./auto-recovery.js";
|
|
19
19
|
import { existsSync } from "node:fs";
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { bumpAndResolveSynthetic } from "./auto/resolve.js";
|
|
22
22
|
|
|
23
23
|
export interface RecoveryContext {
|
|
24
24
|
basePath: string;
|
|
@@ -35,6 +35,13 @@ export async function recoverTimedOutUnit(
|
|
|
35
35
|
reason: "idle" | "hard",
|
|
36
36
|
rctx: RecoveryContext,
|
|
37
37
|
): Promise<"recovered" | "paused"> {
|
|
38
|
+
// Note on turn epoch: the bump is intentionally NOT unconditional at
|
|
39
|
+
// function entry. Two branches below (the "steering retry" paths) keep
|
|
40
|
+
// the same LLM turn alive and let it try again — they must NOT bump,
|
|
41
|
+
// otherwise the retry's legitimate writes get marked stale and drop.
|
|
42
|
+
// Each advance branch calls `bumpAndResolveSynthetic` to bump+resolve
|
|
43
|
+
// atomically. Search for that helper to find all supersede sites.
|
|
44
|
+
|
|
38
45
|
const { basePath, verbose, currentUnitStartedAt, unitRecoveryCount } = rctx;
|
|
39
46
|
|
|
40
47
|
const runtime = readUnitRuntimeRecord(basePath, unitType, unitId);
|
|
@@ -74,7 +81,7 @@ export async function recoverTimedOutUnit(
|
|
|
74
81
|
"info",
|
|
75
82
|
);
|
|
76
83
|
unitRecoveryCount.delete(recoveryKey);
|
|
77
|
-
|
|
84
|
+
bumpAndResolveSynthetic(`timeout-recovery:${reason}:${unitType}/${unitId}`);
|
|
78
85
|
return "recovered";
|
|
79
86
|
}
|
|
80
87
|
|
|
@@ -145,7 +152,7 @@ export async function recoverTimedOutUnit(
|
|
|
145
152
|
"warning",
|
|
146
153
|
);
|
|
147
154
|
unitRecoveryCount.delete(recoveryKey);
|
|
148
|
-
|
|
155
|
+
bumpAndResolveSynthetic(`timeout-recovery:${reason}:${unitType}/${unitId}`);
|
|
149
156
|
return "recovered";
|
|
150
157
|
}
|
|
151
158
|
|
|
@@ -179,7 +186,7 @@ export async function recoverTimedOutUnit(
|
|
|
179
186
|
"info",
|
|
180
187
|
);
|
|
181
188
|
unitRecoveryCount.delete(recoveryKey);
|
|
182
|
-
|
|
189
|
+
bumpAndResolveSynthetic(`timeout-recovery:${reason}:${unitType}/${unitId}`);
|
|
183
190
|
return "recovered";
|
|
184
191
|
}
|
|
185
192
|
|
|
@@ -265,7 +272,7 @@ export async function recoverTimedOutUnit(
|
|
|
265
272
|
"warning",
|
|
266
273
|
);
|
|
267
274
|
unitRecoveryCount.delete(recoveryKey);
|
|
268
|
-
|
|
275
|
+
bumpAndResolveSynthetic(`timeout-recovery:${reason}:${unitType}/${unitId}`);
|
|
269
276
|
return "recovered";
|
|
270
277
|
}
|
|
271
278
|
|
|
@@ -45,9 +45,20 @@ export async function closeoutUnit(
|
|
|
45
45
|
const { buildMemoryLLMCall, extractMemoriesFromUnit } = await import('./memory-extractor.js');
|
|
46
46
|
const llmCallFn = buildMemoryLLMCall(ctx);
|
|
47
47
|
if (llmCallFn) {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// Awaited: a fire-and-forget here lets memory-extractor writes land in
|
|
49
|
+
// .gsd/ after closeoutUnit returns but before the milestone merge
|
|
50
|
+
// runs, which made the working tree appear dirty to `git merge
|
|
51
|
+
// --squash` (root cause class of #4704). Completion latency is now
|
|
52
|
+
// bounded by the extractor's LLM call, which is the acceptable price
|
|
53
|
+
// for not racing the merge boundary.
|
|
54
|
+
try {
|
|
55
|
+
await extractMemoriesFromUnit(activityFile, unitType, unitId, llmCallFn);
|
|
56
|
+
} catch (err) {
|
|
57
|
+
logWarning(
|
|
58
|
+
"engine",
|
|
59
|
+
`memory extraction failed for ${unitType}/${unitId}: ${(err as Error).message}`,
|
|
60
|
+
);
|
|
61
|
+
}
|
|
51
62
|
}
|
|
52
63
|
} catch (err) { /* non-fatal */
|
|
53
64
|
logWarning("engine", `operation failed: ${err instanceof Error ? err.message : String(err)}`);
|