gsd-pi 2.80.0-dev.cf9433f56 → 2.80.0-dev.d4fc28e6b
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.js +0 -19
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +29 -0
- package/dist/resources/extensions/gsd/auto/loop.js +71 -8
- package/dist/resources/extensions/gsd/auto/phases.js +150 -94
- package/dist/resources/extensions/gsd/auto/resolve.js +12 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +10 -30
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- package/dist/resources/extensions/gsd/auto/workflow-dispatch-claim.js +33 -1
- package/dist/resources/extensions/gsd/auto/workflow-worker-heartbeat.js +9 -1
- package/dist/resources/extensions/gsd/auto-direct-dispatch.js +5 -32
- package/dist/resources/extensions/gsd/auto-dispatch.js +16 -0
- package/dist/resources/extensions/gsd/auto-post-unit.js +17 -4
- package/dist/resources/extensions/gsd/auto-prompts.js +90 -15
- package/dist/resources/extensions/gsd/auto-start.js +197 -6
- package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +18 -22
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +86 -19
- package/dist/resources/extensions/gsd/bootstrap/db-tools.js +49 -36
- package/dist/resources/extensions/gsd/bootstrap/dynamic-tools.js +15 -5
- package/dist/resources/extensions/gsd/bootstrap/exec-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/journal-tools.js +7 -1
- package/dist/resources/extensions/gsd/bootstrap/memory-tools.js +9 -3
- package/dist/resources/extensions/gsd/bootstrap/query-tools.js +8 -2
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +298 -54
- package/dist/resources/extensions/gsd/bootstrap/system-context.js +82 -23
- package/dist/resources/extensions/gsd/clean-root-preflight.js +24 -6
- package/dist/resources/extensions/gsd/commands-handlers.js +23 -9
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +53 -0
- package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
- package/dist/resources/extensions/gsd/guided-flow.js +47 -28
- package/dist/resources/extensions/gsd/native-git-bridge.js +32 -8
- package/dist/resources/extensions/gsd/orphan-stash-audit.js +101 -0
- package/dist/resources/extensions/gsd/parallel-orchestrator.js +13 -3
- package/dist/resources/extensions/gsd/pre-execution-checks.js +15 -0
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/dist/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +35 -4
- 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 +12 -12
- package/dist/web/standalone/.next/build-manifest.json +2 -2
- package/dist/web/standalone/.next/prerender-manifest.json +3 -3
- package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/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 +12 -12
- package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
- package/dist/web/standalone/.next/server/pages/404.html +1 -1
- package/dist/web/standalone/.next/server/pages/500.html +1 -1
- package/dist/web/standalone/.next/server/server-reference-manifest.json +1 -1
- package/dist/welcome-screen.d.ts +2 -0
- package/dist/welcome-screen.js +9 -7
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent-loop.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent-loop.js +4 -1
- package/packages/pi-agent-core/dist/agent-loop.js.map +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +5 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +2 -0
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/index.d.ts +1 -0
- package/packages/pi-agent-core/dist/index.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/index.js +2 -0
- package/packages/pi-agent-core/dist/index.js.map +1 -1
- package/packages/pi-agent-core/dist/token-audit.d.ts +47 -0
- package/packages/pi-agent-core/dist/token-audit.d.ts.map +1 -0
- package/packages/pi-agent-core/dist/token-audit.js +221 -0
- package/packages/pi-agent-core/dist/token-audit.js.map +1 -0
- package/packages/pi-agent-core/dist/types.d.ts +9 -0
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent-loop.test.ts +128 -0
- package/packages/pi-agent-core/src/agent-loop.ts +4 -1
- package/packages/pi-agent-core/src/agent.ts +8 -0
- package/packages/pi-agent-core/src/index.ts +2 -0
- package/packages/pi-agent-core/src/token-audit.test.ts +189 -0
- package/packages/pi-agent-core/src/token-audit.ts +287 -0
- package/packages/pi-agent-core/src/types.ts +14 -0
- package/packages/pi-agent-core/tsconfig.tsbuildinfo +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js +18 -0
- package/packages/pi-coding-agent/dist/core/agent-session-tool-refresh.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +12 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +36 -7
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +8 -0
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/extensions/runner.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.js +3 -6
- package/packages/pi-coding-agent/dist/core/extensions/runner.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js +3 -3
- package/packages/pi-coding-agent/dist/core/extensions/runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts +32 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/types.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js +2 -0
- package/packages/pi-coding-agent/dist/core/hooks-runner.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.d.ts.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js +46 -0
- package/packages/pi-coding-agent/dist/core/sdk-tool-filter.test.js.map +1 -0
- package/packages/pi-coding-agent/dist/core/sdk.d.ts +10 -2
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +74 -2
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +22 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts +6 -7
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +2 -3
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/src/core/agent-session-tool-refresh.test.ts +25 -0
- package/packages/pi-coding-agent/src/core/agent-session.ts +40 -7
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +10 -0
- package/packages/pi-coding-agent/src/core/extensions/runner.test.ts +3 -3
- package/packages/pi-coding-agent/src/core/extensions/runner.ts +5 -5
- package/packages/pi-coding-agent/src/core/extensions/types.ts +35 -1
- package/packages/pi-coding-agent/src/core/hooks-runner.test.ts +2 -0
- package/packages/pi-coding-agent/src/core/sdk-tool-filter.test.ts +60 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +85 -3
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +28 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -10
- package/packages/pi-coding-agent/tsconfig.tsbuildinfo +1 -1
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +30 -0
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +26 -0
- package/src/resources/extensions/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/loop.ts +84 -8
- package/src/resources/extensions/gsd/auto/phases.ts +218 -154
- package/src/resources/extensions/gsd/auto/resolve.ts +19 -0
- package/src/resources/extensions/gsd/auto/run-unit.ts +10 -29
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- package/src/resources/extensions/gsd/auto/workflow-dispatch-claim.ts +63 -1
- package/src/resources/extensions/gsd/auto/workflow-worker-heartbeat.ts +14 -1
- package/src/resources/extensions/gsd/auto-direct-dispatch.ts +8 -34
- package/src/resources/extensions/gsd/auto-dispatch.ts +16 -0
- package/src/resources/extensions/gsd/auto-post-unit.ts +18 -4
- package/src/resources/extensions/gsd/auto-prompts.ts +95 -14
- package/src/resources/extensions/gsd/auto-start.ts +230 -9
- package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
- package/src/resources/extensions/gsd/auto.ts +18 -18
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +100 -18
- package/src/resources/extensions/gsd/bootstrap/db-tools.ts +50 -36
- package/src/resources/extensions/gsd/bootstrap/dynamic-tools.ts +16 -5
- package/src/resources/extensions/gsd/bootstrap/exec-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/journal-tools.ts +8 -1
- package/src/resources/extensions/gsd/bootstrap/memory-tools.ts +10 -3
- package/src/resources/extensions/gsd/bootstrap/query-tools.ts +9 -2
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +347 -54
- package/src/resources/extensions/gsd/bootstrap/system-context.ts +90 -22
- package/src/resources/extensions/gsd/clean-root-preflight.ts +32 -7
- package/src/resources/extensions/gsd/commands-handlers.ts +34 -15
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +66 -0
- package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
- package/src/resources/extensions/gsd/guided-flow.ts +52 -35
- package/src/resources/extensions/gsd/native-git-bridge.ts +39 -6
- package/src/resources/extensions/gsd/orphan-stash-audit.ts +117 -0
- package/src/resources/extensions/gsd/parallel-orchestrator.ts +13 -3
- package/src/resources/extensions/gsd/pre-execution-checks.ts +16 -0
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +2 -0
- package/src/resources/extensions/gsd/prompts/complete-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/execute-task.md +4 -2
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/replan-slice.md +2 -2
- package/src/resources/extensions/gsd/tests/artifact-retry-cap.test.ts +2 -2
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +361 -10
- package/src/resources/extensions/gsd/tests/auto-wrapup-inflight-guard.test.ts +168 -6
- package/src/resources/extensions/gsd/tests/clean-root-preflight.test.ts +15 -6
- package/src/resources/extensions/gsd/tests/complete-milestone-excerpt.test.ts +31 -0
- package/src/resources/extensions/gsd/tests/complete-slice-composer.test.ts +3 -2
- package/src/resources/extensions/gsd/tests/context-store.test.ts +7 -1
- package/src/resources/extensions/gsd/tests/custom-engine-loop-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/execute-task-rendering.test.ts +5 -2
- package/src/resources/extensions/gsd/tests/fast-forward-reused-milestone-branch.test.ts +219 -0
- package/src/resources/extensions/gsd/tests/finalize-survivor-branch.test.ts +132 -0
- package/src/resources/extensions/gsd/tests/isolation-none-branch-guard.test.ts +6 -3
- package/src/resources/extensions/gsd/tests/journal-integration.test.ts +5 -1
- package/src/resources/extensions/gsd/tests/journal-query-tool.test.ts +32 -0
- package/src/resources/extensions/gsd/tests/knowledge.test.ts +47 -0
- package/src/resources/extensions/gsd/tests/merge-conflict-stops-loop.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/milestone-merge-stash-restore.test.ts +242 -0
- package/src/resources/extensions/gsd/tests/native-git-bridge-exec-fallback.test.ts +34 -2
- package/src/resources/extensions/gsd/tests/originalbase-path-comparison.test.ts +3 -0
- package/src/resources/extensions/gsd/tests/orphan-merge-bootstrap.test.ts +133 -0
- package/src/resources/extensions/gsd/tests/orphan-stash-audit.test.ts +201 -0
- package/src/resources/extensions/gsd/tests/parallel-orchestrator-fast-forward.test.ts +113 -0
- package/src/resources/extensions/gsd/tests/pre-execution-checks.test.ts +7 -5
- package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/query-tools-db-open.test.ts +3 -3
- package/src/resources/extensions/gsd/tests/restore-tools-after-discuss.test.ts +38 -17
- package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/session-start-footer.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +166 -0
- package/src/resources/extensions/gsd/tests/state-corruption-2945.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/system-context-memory.test.ts +112 -0
- package/src/resources/extensions/gsd/tests/system-context-message-routing.test.ts +7 -9
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +291 -0
- package/src/resources/extensions/gsd/tests/unit-dispatches.test.ts +50 -1
- package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/workflow-dispatch-claim.test.ts +142 -0
- package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/workflow-worker-heartbeat.test.ts +32 -1
- package/src/resources/extensions/gsd/tests/worktree-journal-events.test.ts +1 -0
- package/src/resources/extensions/gsd/tests/worktree-path-injection.test.ts +22 -19
- package/src/resources/extensions/gsd/tests/worktree-project-root-degrade.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/worktree-resolver.test.ts +104 -3
- package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +49 -4
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{-5nHJWzSdG-WkPMul_khA → cWaxzf-sdbSSbbwYu8q7a}/_ssgManifest.js +0 -0
package/dist/cli.js
CHANGED
|
@@ -760,25 +760,6 @@ if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
|
760
760
|
: 'stdout is';
|
|
761
761
|
printNonTtyErrorAndExit(missing, true);
|
|
762
762
|
}
|
|
763
|
-
// Welcome screen — shown on every fresh interactive session before TUI takes over.
|
|
764
|
-
// Skip when the first-run banner was already printed in loader.ts (prevents double banner).
|
|
765
|
-
if (!process.env.GSD_FIRST_RUN_BANNER) {
|
|
766
|
-
const { printWelcomeScreen } = await import('./welcome-screen.js');
|
|
767
|
-
let remoteChannel;
|
|
768
|
-
try {
|
|
769
|
-
const { resolveRemoteConfig } = await import('./resources/extensions/remote-questions/config.js');
|
|
770
|
-
const rc = resolveRemoteConfig();
|
|
771
|
-
if (rc)
|
|
772
|
-
remoteChannel = rc.channel;
|
|
773
|
-
}
|
|
774
|
-
catch { /* non-fatal */ }
|
|
775
|
-
printWelcomeScreen({
|
|
776
|
-
version: process.env.GSD_VERSION || '0.0.0',
|
|
777
|
-
modelName: settingsManager.getDefaultModel() || undefined,
|
|
778
|
-
provider: settingsManager.getDefaultProvider() || undefined,
|
|
779
|
-
remoteChannel,
|
|
780
|
-
});
|
|
781
|
-
}
|
|
782
763
|
const interactiveMode = new InteractiveMode(session);
|
|
783
764
|
markStartup('InteractiveMode');
|
|
784
765
|
printStartupTimings();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
83a1a81197451245
|
|
@@ -261,6 +261,26 @@ function makeErrorMessage(model, errorMsg) {
|
|
|
261
261
|
timestamp: Date.now(),
|
|
262
262
|
};
|
|
263
263
|
}
|
|
264
|
+
export function isClaudeCodeAbortErrorMessage(message) {
|
|
265
|
+
if (!message)
|
|
266
|
+
return false;
|
|
267
|
+
return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
|
|
268
|
+
}
|
|
269
|
+
function isBareClaudeCodeAbortErrorMessage(message) {
|
|
270
|
+
if (!message)
|
|
271
|
+
return false;
|
|
272
|
+
const normalized = message.trim().replace(/\s+/g, " ").toLowerCase();
|
|
273
|
+
return normalized === "claude code process aborted by user"
|
|
274
|
+
|| normalized === "request aborted by user"
|
|
275
|
+
|| normalized === "process aborted by user";
|
|
276
|
+
}
|
|
277
|
+
export function resolveClaudeCodeAbortedMessageText(errorMsg, lastTextContent) {
|
|
278
|
+
const trimmedError = errorMsg.trim();
|
|
279
|
+
if (trimmedError && !isBareClaudeCodeAbortErrorMessage(trimmedError)) {
|
|
280
|
+
return trimmedError;
|
|
281
|
+
}
|
|
282
|
+
return lastTextContent;
|
|
283
|
+
}
|
|
264
284
|
/**
|
|
265
285
|
* Generator exhaustion without a terminal result means the SDK stream was
|
|
266
286
|
* interrupted mid-turn. Surface it as an error so downstream recovery logic
|
|
@@ -1512,6 +1532,15 @@ async function pumpSdkMessages(model, context, options, stream) {
|
|
|
1512
1532
|
}
|
|
1513
1533
|
catch (err) {
|
|
1514
1534
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1535
|
+
if (options?.signal?.aborted || isClaudeCodeAbortErrorMessage(errorMsg)) {
|
|
1536
|
+
const abortedText = resolveClaudeCodeAbortedMessageText(errorMsg, lastTextContent);
|
|
1537
|
+
stream.push({
|
|
1538
|
+
type: "error",
|
|
1539
|
+
reason: "aborted",
|
|
1540
|
+
error: makeAbortedMessage(modelId, abortedText),
|
|
1541
|
+
});
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1515
1544
|
stream.push({
|
|
1516
1545
|
type: "error",
|
|
1517
1546
|
reason: "error",
|
|
@@ -16,7 +16,7 @@ import { ModelPolicyDispatchBlockedError } from "../auto-model-selection.js";
|
|
|
16
16
|
import { resolveEngine } from "../engine-resolver.js";
|
|
17
17
|
import { logWarning } from "../workflow-logger.js";
|
|
18
18
|
import { recordDispatchClaim, markRunning as markDispatchRunning, markCompleted as markDispatchCompleted, markFailed as markDispatchFailed, getRecentForUnit as getRecentDispatchesForUnit, getRecentUnitKeysForProjectRoot, } from "../db/unit-dispatches.js";
|
|
19
|
-
import { refreshMilestoneLease } from "../db/milestone-leases.js";
|
|
19
|
+
import { claimMilestoneLease, refreshMilestoneLease } from "../db/milestone-leases.js";
|
|
20
20
|
import { heartbeatAutoWorker } from "../db/auto-workers.js";
|
|
21
21
|
import { getRuntimeKv, setRuntimeKv } from "../db/runtime-kv.js";
|
|
22
22
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
@@ -27,7 +27,7 @@ import { hydrateCustomVerifyRetryCounts, saveCustomVerifyRetryCounts, } from "./
|
|
|
27
27
|
import { settleDispatchCompleted, settleDispatchFailed, } from "./workflow-dispatch-ledger.js";
|
|
28
28
|
import { emitOpenUnitEndForUnit } from "../crash-recovery.js";
|
|
29
29
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
30
|
-
import { openDispatchClaim } from "./workflow-dispatch-claim.js";
|
|
30
|
+
import { ensureDispatchLease, openDispatchClaim } from "./workflow-dispatch-claim.js";
|
|
31
31
|
import { completeWorkflowIteration } from "./workflow-iteration-completion.js";
|
|
32
32
|
import { createWorkflowJournalReporter } from "./workflow-journal-reporter.js";
|
|
33
33
|
import { createWorkflowPhaseReporter } from "./workflow-phase-reporter.js";
|
|
@@ -105,6 +105,18 @@ function logDispatchClaimFailed(err) {
|
|
|
105
105
|
error: err instanceof Error ? err.message : String(err),
|
|
106
106
|
});
|
|
107
107
|
}
|
|
108
|
+
function logDispatchLeaseRecovered(details) {
|
|
109
|
+
debugLog("autoLoop", {
|
|
110
|
+
phase: details.recovered ? "dispatch-lease-recovered" : "dispatch-lease-acquired",
|
|
111
|
+
...details,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
function logDispatchLeaseRecoveryFailed(details) {
|
|
115
|
+
debugLog("autoLoop", {
|
|
116
|
+
phase: "dispatch-lease-recovery-failed",
|
|
117
|
+
...details,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
108
120
|
function logCustomVerifyRetryLoadFailure(err) {
|
|
109
121
|
debugLog("autoLoop", {
|
|
110
122
|
phase: "load-custom-verify-retries-failed",
|
|
@@ -186,6 +198,10 @@ export async function autoLoop(ctx, pi, s, deps, options) {
|
|
|
186
198
|
phase: "heartbeat-failed",
|
|
187
199
|
error: err instanceof Error ? err.message : String(err),
|
|
188
200
|
}),
|
|
201
|
+
logLeaseRefreshMiss: details => debugLog("autoLoop", {
|
|
202
|
+
phase: "lease-refresh-missed",
|
|
203
|
+
...details,
|
|
204
|
+
}),
|
|
189
205
|
});
|
|
190
206
|
// ── Journal: per-iteration flow grouping ──
|
|
191
207
|
const flowId = randomUUID();
|
|
@@ -581,23 +597,70 @@ export async function autoLoop(ctx, pi, s, deps, options) {
|
|
|
581
597
|
await enforceMinRequestInterval(s, prefs);
|
|
582
598
|
// Phase B: claim a unit_dispatches row before invoking the unit. The
|
|
583
599
|
// partial unique index idx_unit_dispatches_active_per_unit prevents
|
|
584
|
-
// a second worker from claiming the same unit concurrently.
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
const
|
|
600
|
+
// a second worker from claiming the same unit concurrently. When this
|
|
601
|
+
// process has a worker identity, make the milestone lease explicit before
|
|
602
|
+
// claiming so a step-mode handoff cannot leave us running with a stale
|
|
603
|
+
// in-memory token and no backing lease row.
|
|
604
|
+
const leaseBeforeClaim = ensureDispatchLease(s, iterData.mid, {
|
|
605
|
+
claimMilestoneLease,
|
|
606
|
+
logLeaseRecovered: logDispatchLeaseRecovered,
|
|
607
|
+
logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
|
|
608
|
+
});
|
|
609
|
+
if (leaseBeforeClaim.kind === "blocked" || leaseBeforeClaim.kind === "failed") {
|
|
610
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} before dispatching ${iterData.unitType} ${iterData.unitId}: ${leaseBeforeClaim.reason}`;
|
|
611
|
+
ctx.ui.notify(msg, "error");
|
|
612
|
+
finishTurn("stopped", "execution", msg);
|
|
613
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
614
|
+
break;
|
|
615
|
+
}
|
|
616
|
+
let dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
|
|
589
617
|
getRecentDispatchesForUnit,
|
|
590
618
|
recordDispatchClaim,
|
|
591
619
|
markDispatchRunning,
|
|
592
620
|
logClaimRejected: logDispatchClaimRejected,
|
|
593
621
|
logClaimFailed: logDispatchClaimFailed,
|
|
594
622
|
});
|
|
595
|
-
|
|
623
|
+
let dispatchDecision = decideDispatchClaim(dispatchClaim.kind === "opened"
|
|
596
624
|
? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
|
|
597
625
|
: dispatchClaim.kind === "skip"
|
|
598
626
|
? { kind: "skip", reason: dispatchClaim.reason }
|
|
599
627
|
: { kind: "degraded" });
|
|
628
|
+
if (dispatchDecision.action === "skip" && dispatchDecision.reason === "stale-lease") {
|
|
629
|
+
const leaseRecovery = ensureDispatchLease(s, iterData.mid, {
|
|
630
|
+
claimMilestoneLease,
|
|
631
|
+
logLeaseRecovered: logDispatchLeaseRecovered,
|
|
632
|
+
logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
|
|
633
|
+
}, { forceReclaim: true });
|
|
634
|
+
if (leaseRecovery.kind === "ready") {
|
|
635
|
+
dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
|
|
636
|
+
getRecentDispatchesForUnit,
|
|
637
|
+
recordDispatchClaim,
|
|
638
|
+
markDispatchRunning,
|
|
639
|
+
logClaimRejected: logDispatchClaimRejected,
|
|
640
|
+
logClaimFailed: logDispatchClaimFailed,
|
|
641
|
+
});
|
|
642
|
+
dispatchDecision = decideDispatchClaim(dispatchClaim.kind === "opened"
|
|
643
|
+
? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
|
|
644
|
+
: dispatchClaim.kind === "skip"
|
|
645
|
+
? { kind: "skip", reason: dispatchClaim.reason }
|
|
646
|
+
: { kind: "degraded" });
|
|
647
|
+
}
|
|
648
|
+
else {
|
|
649
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}: ${leaseRecovery.reason}`;
|
|
650
|
+
ctx.ui.notify(msg, "error");
|
|
651
|
+
finishTurn("stopped", "execution", msg);
|
|
652
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
600
656
|
if (dispatchDecision.action === "skip") {
|
|
657
|
+
if (dispatchDecision.reason === "stale-lease") {
|
|
658
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}; dispatch claim still failed after recovery.`;
|
|
659
|
+
ctx.ui.notify(msg, "error");
|
|
660
|
+
finishTurn("stopped", "execution", msg);
|
|
661
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
601
664
|
finishTurn("skipped", "execution", dispatchDecision.reason);
|
|
602
665
|
continue;
|
|
603
666
|
}
|
|
@@ -42,6 +42,11 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
|
|
|
42
42
|
function isSamePathLocal(a, b) {
|
|
43
43
|
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
44
44
|
}
|
|
45
|
+
export function shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification) {
|
|
46
|
+
return (worktreeClassification.kind === "greenfield" &&
|
|
47
|
+
projectRootClassification.kind !== "greenfield" &&
|
|
48
|
+
projectRootClassification.kind !== "invalid-repo");
|
|
49
|
+
}
|
|
45
50
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
46
51
|
let consecutiveSessionTimeouts = 0;
|
|
47
52
|
const MAX_SESSION_TIMEOUT_AUTO_RESUMES = 3;
|
|
@@ -129,6 +134,74 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
129
134
|
}
|
|
130
135
|
await deps.stopAuto(ctx, pi, reason);
|
|
131
136
|
}
|
|
137
|
+
async function stopOnPostflightRecoveryNeeded(ic, result, milestoneId) {
|
|
138
|
+
if (!result.needsManualRecovery)
|
|
139
|
+
return null;
|
|
140
|
+
const { ctx, pi, deps } = ic;
|
|
141
|
+
const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
|
|
142
|
+
ctx.ui.notify(`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`, "error");
|
|
143
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
144
|
+
return { action: "break", reason: "postflight-stash-restore-failed" };
|
|
145
|
+
}
|
|
146
|
+
async function restorePreflightStashOrStop(ic, preflight, milestoneId) {
|
|
147
|
+
if (!preflight.stashPushed)
|
|
148
|
+
return null;
|
|
149
|
+
const { ctx, s, deps } = ic;
|
|
150
|
+
const result = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
151
|
+
return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Run a milestone merge surrounded by preflight stash + always-on postflight
|
|
155
|
+
* pop. The previous code popped the stash only after a successful merge, which
|
|
156
|
+
* leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
|
|
157
|
+
* leaving the user's pre-merge working tree silently stashed away after a
|
|
158
|
+
* merge-conflict or other merge error. This helper restores the stash on
|
|
159
|
+
* every exit path, then surfaces the merge or stash failure (in priority
|
|
160
|
+
* order) as the loop's stop reason.
|
|
161
|
+
*
|
|
162
|
+
* Returns a `break` action when auto-mode must stop, or `null` when the merge
|
|
163
|
+
* succeeded and the stash (if any) was restored cleanly.
|
|
164
|
+
*/
|
|
165
|
+
export async function _runMilestoneMergeWithStashRestore(ic, milestoneId) {
|
|
166
|
+
const { ctx, pi, s, deps } = ic;
|
|
167
|
+
const preflight = deps.preflightCleanRoot(s.originalBasePath || s.basePath, milestoneId, ctx.ui.notify.bind(ctx.ui));
|
|
168
|
+
let mergeError = null;
|
|
169
|
+
try {
|
|
170
|
+
deps.resolver.mergeAndExit(milestoneId, ctx.ui);
|
|
171
|
+
s.milestoneMergedInPhases = true;
|
|
172
|
+
}
|
|
173
|
+
catch (mergeErr) {
|
|
174
|
+
mergeError = mergeErr;
|
|
175
|
+
}
|
|
176
|
+
// Always attempt to restore the stashed working tree, even on merge error.
|
|
177
|
+
// postflightPopStash itself does not throw; failures surface via the
|
|
178
|
+
// PostflightResult.needsManualRecovery flag.
|
|
179
|
+
let stashResult = null;
|
|
180
|
+
if (preflight.stashPushed) {
|
|
181
|
+
stashResult = deps.postflightPopStash(s.originalBasePath || s.basePath, milestoneId, preflight.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
182
|
+
}
|
|
183
|
+
// Merge failure takes priority over stash recovery — the merge is the
|
|
184
|
+
// authoritative gate. If the stash also needed manual recovery, the user
|
|
185
|
+
// already saw the postflightPopStash notify above.
|
|
186
|
+
if (mergeError) {
|
|
187
|
+
if (mergeError instanceof MergeConflictError) {
|
|
188
|
+
ctx.ui.notify(`Merge conflict: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
189
|
+
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${milestoneId}`);
|
|
190
|
+
return { action: "break", reason: "merge-conflict" };
|
|
191
|
+
}
|
|
192
|
+
logError("engine", "Milestone merge failed with non-conflict error", {
|
|
193
|
+
milestone: milestoneId,
|
|
194
|
+
error: String(mergeError),
|
|
195
|
+
});
|
|
196
|
+
ctx.ui.notify(`Merge failed: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`, "error");
|
|
197
|
+
await deps.stopAuto(ctx, pi, `Merge error on milestone ${milestoneId}: ${String(mergeError)}`);
|
|
198
|
+
return { action: "break", reason: "merge-failed" };
|
|
199
|
+
}
|
|
200
|
+
if (stashResult) {
|
|
201
|
+
return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
132
205
|
async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
|
|
133
206
|
ic.deps.emitJournalEvent({
|
|
134
207
|
ts: new Date().toISOString(),
|
|
@@ -470,28 +543,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
470
543
|
s.unitLifetimeDispatches.clear();
|
|
471
544
|
loopState.recentUnits.length = 0;
|
|
472
545
|
loopState.stuckRecoveryAttempts = 0;
|
|
473
|
-
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
474
|
-
// #2909: preflight
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
catch (mergeErr) {
|
|
480
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
481
|
-
// Real code conflicts — stop the loop instead of retrying forever (#2330)
|
|
482
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
483
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
484
|
-
return { action: "break", reason: "merge-conflict" };
|
|
485
|
-
}
|
|
486
|
-
// Non-conflict merge errors — stop auto to avoid advancing with unmerged work
|
|
487
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
488
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
489
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
490
|
-
return { action: "break", reason: "merge-failed" };
|
|
491
|
-
}
|
|
492
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
493
|
-
if (preflightTransition.stashPushed) {
|
|
494
|
-
deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightTransition.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
546
|
+
// Worktree lifecycle on milestone transition — merge current, enter next.
|
|
547
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
548
|
+
{
|
|
549
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
550
|
+
if (stop)
|
|
551
|
+
return stop;
|
|
495
552
|
}
|
|
496
553
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
497
554
|
deps.invalidateAllCaches();
|
|
@@ -545,30 +602,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
545
602
|
}
|
|
546
603
|
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
547
604
|
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
548
|
-
// All milestones complete — merge milestone branch before stopping
|
|
605
|
+
// All milestones complete — merge milestone branch before stopping.
|
|
549
606
|
if (s.currentMilestoneId) {
|
|
550
|
-
// #2909: preflight
|
|
551
|
-
const
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
555
|
-
s.milestoneMergedInPhases = true;
|
|
556
|
-
}
|
|
557
|
-
catch (mergeErr) {
|
|
558
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
559
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
560
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
561
|
-
return { action: "break", reason: "merge-conflict" };
|
|
562
|
-
}
|
|
563
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
564
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
565
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
566
|
-
return { action: "break", reason: "merge-failed" };
|
|
567
|
-
}
|
|
568
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
569
|
-
if (preflightAllComplete.stashPushed) {
|
|
570
|
-
deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightAllComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
571
|
-
}
|
|
607
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
608
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
609
|
+
if (stop)
|
|
610
|
+
return stop;
|
|
572
611
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
573
612
|
}
|
|
574
613
|
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
@@ -631,30 +670,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
631
670
|
}
|
|
632
671
|
// Terminal: complete
|
|
633
672
|
if (state.phase === "complete") {
|
|
634
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
673
|
+
// Milestone merge on complete (before closeout so branch state is clean).
|
|
635
674
|
if (s.currentMilestoneId) {
|
|
636
|
-
// #2909: preflight
|
|
637
|
-
const
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
641
|
-
s.milestoneMergedInPhases = true;
|
|
642
|
-
}
|
|
643
|
-
catch (mergeErr) {
|
|
644
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
645
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
646
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
647
|
-
return { action: "break", reason: "merge-conflict" };
|
|
648
|
-
}
|
|
649
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
650
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
651
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
652
|
-
return { action: "break", reason: "merge-failed" };
|
|
653
|
-
}
|
|
654
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
655
|
-
if (preflightComplete.stashPushed) {
|
|
656
|
-
deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
657
|
-
}
|
|
675
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
676
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
677
|
+
if (stop)
|
|
678
|
+
return stop;
|
|
658
679
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
659
680
|
}
|
|
660
681
|
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
@@ -747,6 +768,48 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
747
768
|
let unitId = dispatchResult.unitId;
|
|
748
769
|
let prompt = dispatchResult.prompt;
|
|
749
770
|
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
771
|
+
// Resolve hooks and prior-slice gating before health/stuck accounting so
|
|
772
|
+
// those checks run against the final dispatch unit.
|
|
773
|
+
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
774
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
775
|
+
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
776
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
777
|
+
}
|
|
778
|
+
if (preDispatchResult.action === "skip") {
|
|
779
|
+
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
780
|
+
await new Promise((r) => setImmediate(r));
|
|
781
|
+
return { action: "continue" };
|
|
782
|
+
}
|
|
783
|
+
if (preDispatchResult.action === "replace") {
|
|
784
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
785
|
+
if (preDispatchResult.unitType)
|
|
786
|
+
unitType = preDispatchResult.unitType;
|
|
787
|
+
}
|
|
788
|
+
else if (preDispatchResult.prompt) {
|
|
789
|
+
prompt = preDispatchResult.prompt;
|
|
790
|
+
}
|
|
791
|
+
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
792
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
|
|
793
|
+
if (priorSliceBlocker) {
|
|
794
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
795
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
796
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
797
|
+
}
|
|
798
|
+
// Execute-task needs a real writable checkout. The same health check also
|
|
799
|
+
// exists in runUnitPhase, but the stuck-window detector runs before that
|
|
800
|
+
// phase. Check here too so repeated derivations of a broken worktree stop
|
|
801
|
+
// with the actionable worktree error instead of the generic stuck-loop error.
|
|
802
|
+
if (s.basePath && unitType === "execute-task") {
|
|
803
|
+
const gitMarker = join(s.basePath, ".git");
|
|
804
|
+
const hasGit = deps.existsSync(gitMarker);
|
|
805
|
+
if (!hasGit) {
|
|
806
|
+
const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
|
|
807
|
+
debugLog("autoLoop", { phase: "dispatch-worktree-health-fail", basePath: s.basePath, hasGit });
|
|
808
|
+
ctx.ui.notify(msg, "error");
|
|
809
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
810
|
+
return { action: "break", reason: "worktree-invalid" };
|
|
811
|
+
}
|
|
812
|
+
}
|
|
750
813
|
// ── Sliding-window stuck detection with graduated recovery ──
|
|
751
814
|
const derivedKey = `${unitType}/${unitId}`;
|
|
752
815
|
// Always record this dispatch in the sliding window so detectStuck() has
|
|
@@ -863,32 +926,6 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
863
926
|
}
|
|
864
927
|
}
|
|
865
928
|
}
|
|
866
|
-
// Pre-dispatch hooks
|
|
867
|
-
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
868
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
869
|
-
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
870
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
871
|
-
}
|
|
872
|
-
if (preDispatchResult.action === "skip") {
|
|
873
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
874
|
-
await new Promise((r) => setImmediate(r));
|
|
875
|
-
return { action: "continue" };
|
|
876
|
-
}
|
|
877
|
-
if (preDispatchResult.action === "replace") {
|
|
878
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
879
|
-
if (preDispatchResult.unitType)
|
|
880
|
-
unitType = preDispatchResult.unitType;
|
|
881
|
-
}
|
|
882
|
-
else if (preDispatchResult.prompt) {
|
|
883
|
-
prompt = preDispatchResult.prompt;
|
|
884
|
-
}
|
|
885
|
-
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
886
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
|
|
887
|
-
if (priorSliceBlocker) {
|
|
888
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
889
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
890
|
-
return { action: "break", reason: "prior-slice-blocker" };
|
|
891
|
-
}
|
|
892
929
|
return {
|
|
893
930
|
action: "next",
|
|
894
931
|
data: {
|
|
@@ -1118,6 +1155,25 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1118
1155
|
}
|
|
1119
1156
|
}
|
|
1120
1157
|
else if (projectClassification.kind === "greenfield") {
|
|
1158
|
+
const projectRoot = s.canonicalProjectRoot;
|
|
1159
|
+
if (!isSamePathLocal(s.basePath, projectRoot)) {
|
|
1160
|
+
const projectRootClassification = classifyProject(projectRoot);
|
|
1161
|
+
if (shouldDegradeEmptyWorktreeToProjectRoot(projectClassification, projectRootClassification)) {
|
|
1162
|
+
debugLog("runUnitPhase", {
|
|
1163
|
+
phase: "worktree-health-degrade-to-project-root",
|
|
1164
|
+
worktreePath: s.basePath,
|
|
1165
|
+
projectRoot,
|
|
1166
|
+
worktreeClassification: projectClassification,
|
|
1167
|
+
projectRootClassification,
|
|
1168
|
+
});
|
|
1169
|
+
ctx.ui.notify(`Warning: ${s.basePath} has no project content, but ${projectRoot} does. Continuing in project root because the milestone worktree cannot represent untracked project files.`, "warning");
|
|
1170
|
+
s.basePath = projectRoot;
|
|
1171
|
+
s.isolationDegraded = true;
|
|
1172
|
+
projectClassification = projectRootClassification;
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
if (projectClassification.kind === "greenfield") {
|
|
1121
1177
|
debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
|
|
1122
1178
|
ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
|
|
1123
1179
|
}
|
|
@@ -18,6 +18,8 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
|
|
|
18
18
|
let _currentResolve = null;
|
|
19
19
|
let _sessionSwitchInFlight = false;
|
|
20
20
|
let _pendingSwitchCancellation = null;
|
|
21
|
+
let _sessionSwitchAbortGraceUntil = 0;
|
|
22
|
+
const DEFAULT_SESSION_SWITCH_ABORT_GRACE_MS = 2_000;
|
|
21
23
|
// ─── Setters (needed for cross-module mutation) ─────────────────────────────
|
|
22
24
|
export function _setCurrentResolve(fn) {
|
|
23
25
|
_currentResolve = fn;
|
|
@@ -25,6 +27,15 @@ export function _setCurrentResolve(fn) {
|
|
|
25
27
|
export function _setSessionSwitchInFlight(v) {
|
|
26
28
|
_sessionSwitchInFlight = v;
|
|
27
29
|
}
|
|
30
|
+
export function _markSessionSwitchAbortGraceWindow(durationMs = DEFAULT_SESSION_SWITCH_ABORT_GRACE_MS) {
|
|
31
|
+
_sessionSwitchAbortGraceUntil = Math.max(_sessionSwitchAbortGraceUntil, Date.now() + durationMs);
|
|
32
|
+
}
|
|
33
|
+
export function _clearSessionSwitchAbortGraceWindow() {
|
|
34
|
+
_sessionSwitchAbortGraceUntil = 0;
|
|
35
|
+
}
|
|
36
|
+
export function isSessionSwitchAbortGraceActive(now = Date.now()) {
|
|
37
|
+
return now < _sessionSwitchAbortGraceUntil;
|
|
38
|
+
}
|
|
28
39
|
export function _clearCurrentResolve() {
|
|
29
40
|
_currentResolve = null;
|
|
30
41
|
}
|
|
@@ -118,6 +129,7 @@ export function _resetPendingResolve() {
|
|
|
118
129
|
_currentResolve = null;
|
|
119
130
|
_sessionSwitchInFlight = false;
|
|
120
131
|
_pendingSwitchCancellation = null;
|
|
132
|
+
_sessionSwitchAbortGraceUntil = 0;
|
|
121
133
|
}
|
|
122
134
|
export function _hasPendingResolveForTest() {
|
|
123
135
|
return _currentResolve !== null;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// GSD-2 + src/resources/extensions/gsd/auto/run-unit.ts - Runs one GSD auto-mode unit from session creation through agent completion.
|
|
2
2
|
import { NEW_SESSION_TIMEOUT_MS } from "./session.js";
|
|
3
|
-
import { _clearCurrentResolve, _consumePendingSwitchCancellation, _setCurrentResolve, _setSessionSwitchInFlight, } from "./resolve.js";
|
|
3
|
+
import { _clearCurrentResolve, _consumePendingSwitchCancellation, _markSessionSwitchAbortGraceWindow, _setCurrentResolve, _setSessionSwitchInFlight, } from "./resolve.js";
|
|
4
4
|
import { getCurrentTurnGeneration, runWithTurnGeneration, } from "./turn-epoch.js";
|
|
5
5
|
import { debugLog } from "../debug-logger.js";
|
|
6
6
|
import { logWarning } from "../workflow-logger.js";
|
|
@@ -19,44 +19,23 @@ let sessionSwitchGeneration = 0;
|
|
|
19
19
|
*/
|
|
20
20
|
export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
21
21
|
debugLog("runUnit", { phase: "start", unitType, unitId });
|
|
22
|
-
// Ensure cwd matches basePath BEFORE newSession() captures it. The new
|
|
23
|
-
// session reads process.cwd() during construction to anchor its tool
|
|
24
|
-
// runtime and system prompt; if cwd has drifted (async_bash, background
|
|
25
|
-
// jobs, prior unit cleanup), the session would otherwise be rooted to
|
|
26
|
-
// the wrong directory. Must be synchronous — no awaits between chdir
|
|
27
|
-
// and newSession (#1389, #4762 follow-up).
|
|
28
|
-
try {
|
|
29
|
-
if (process.cwd() !== s.basePath) {
|
|
30
|
-
process.chdir(s.basePath);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch (e) {
|
|
34
|
-
const msg = `Failed to chdir to basePath before newSession (basePath: ${s.basePath}): ${String(e)}`;
|
|
35
|
-
logWarning("engine", msg, { basePath: s.basePath, error: String(e) });
|
|
36
|
-
return {
|
|
37
|
-
status: "cancelled",
|
|
38
|
-
errorContext: {
|
|
39
|
-
message: msg,
|
|
40
|
-
category: "session-failed",
|
|
41
|
-
isTransient: true,
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
22
|
// ── Session creation with timeout ──
|
|
46
23
|
debugLog("runUnit", { phase: "session-create", unitType, unitId });
|
|
47
24
|
let sessionResult;
|
|
48
25
|
let sessionTimeoutHandle;
|
|
49
26
|
const mySessionSwitchGeneration = ++sessionSwitchGeneration;
|
|
50
|
-
// #3731: Cancellation controller for newSession(). When
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// it from capturing the (now-root) process.cwd() and rebuilding the tool
|
|
54
|
-
// runtime with the wrong cwd.
|
|
27
|
+
// #3731: Cancellation controller for newSession(). When session creation
|
|
28
|
+
// times out, abort before a late session switch can rebuild the tool runtime
|
|
29
|
+
// against a stale workspace root.
|
|
55
30
|
const sessionAbortController = new AbortController();
|
|
56
31
|
_setSessionSwitchInFlight(true);
|
|
57
32
|
try {
|
|
58
|
-
const sessionPromise = s.cmdCtx.newSession({
|
|
33
|
+
const sessionPromise = s.cmdCtx.newSession({
|
|
34
|
+
abortSignal: sessionAbortController.signal,
|
|
35
|
+
workspaceRoot: s.basePath,
|
|
36
|
+
}).finally(() => {
|
|
59
37
|
if (sessionSwitchGeneration === mySessionSwitchGeneration) {
|
|
38
|
+
_markSessionSwitchAbortGraceWindow();
|
|
60
39
|
_setSessionSwitchInFlight(false);
|
|
61
40
|
}
|
|
62
41
|
});
|
|
@@ -111,6 +90,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
111
90
|
// ── Create the agent_end promise (per-unit one-shot) ──
|
|
112
91
|
// This happens after newSession completes so session-switch agent_end events
|
|
113
92
|
// from the previous session cannot resolve the new unit.
|
|
93
|
+
_markSessionSwitchAbortGraceWindow();
|
|
114
94
|
_setSessionSwitchInFlight(false);
|
|
115
95
|
const unitPromise = new Promise((resolve) => {
|
|
116
96
|
_setCurrentResolve(resolve);
|
|
@@ -106,6 +106,13 @@ export class AutoSession {
|
|
|
106
106
|
* stale context bleeding into unrelated slices.
|
|
107
107
|
*/
|
|
108
108
|
lastPreExecFailure = null;
|
|
109
|
+
/**
|
|
110
|
+
* Tracks how many consecutive times each slice unit has failed pre-execution
|
|
111
|
+
* checks. Keyed by unitId (e.g. "M001/S01"). Used to break the infinite
|
|
112
|
+
* plan-slice → pre-exec fail → re-dispatch loop when the planner cannot fix
|
|
113
|
+
* the issues after MAX_PRE_EXEC_RETRIES re-attempts.
|
|
114
|
+
*/
|
|
115
|
+
preExecRetryCount = new Map();
|
|
109
116
|
// ── Tool invocation errors (#2883) ──────────────────────────────────
|
|
110
117
|
/** Set when a GSD tool execution ends with isError due to malformed/truncated
|
|
111
118
|
* JSON arguments. Checked by postUnitPreVerification to break retry loops. */
|
|
@@ -258,6 +265,7 @@ export class AutoSession {
|
|
|
258
265
|
this.rewriteAttemptCount = 0;
|
|
259
266
|
this.consecutiveCompleteBootstraps = 0;
|
|
260
267
|
this.lastPreExecFailure = null;
|
|
268
|
+
this.preExecRetryCount.clear();
|
|
261
269
|
this.lastToolInvocationError = null;
|
|
262
270
|
this.lastUnitAgentEndMessages = null;
|
|
263
271
|
this.lastGitActionFailure = null;
|