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
|
@@ -418,6 +418,27 @@ function makeErrorMessage(model: string, errorMsg: string): AssistantMessage {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
export function isClaudeCodeAbortErrorMessage(message: string | undefined | null): boolean {
|
|
422
|
+
if (!message) return false;
|
|
423
|
+
return /\b(?:claude code process aborted by user|request aborted by user|process aborted by user)\b/i.test(message);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function isBareClaudeCodeAbortErrorMessage(message: string | undefined | null): boolean {
|
|
427
|
+
if (!message) return false;
|
|
428
|
+
const normalized = message.trim().replace(/\s+/g, " ").toLowerCase();
|
|
429
|
+
return normalized === "claude code process aborted by user"
|
|
430
|
+
|| normalized === "request aborted by user"
|
|
431
|
+
|| normalized === "process aborted by user";
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function resolveClaudeCodeAbortedMessageText(errorMsg: string, lastTextContent: string): string {
|
|
435
|
+
const trimmedError = errorMsg.trim();
|
|
436
|
+
if (trimmedError && !isBareClaudeCodeAbortErrorMessage(trimmedError)) {
|
|
437
|
+
return trimmedError;
|
|
438
|
+
}
|
|
439
|
+
return lastTextContent;
|
|
440
|
+
}
|
|
441
|
+
|
|
421
442
|
/**
|
|
422
443
|
* Generator exhaustion without a terminal result means the SDK stream was
|
|
423
444
|
* interrupted mid-turn. Surface it as an error so downstream recovery logic
|
|
@@ -1840,6 +1861,15 @@ async function pumpSdkMessages(
|
|
|
1840
1861
|
stream.push({ type: "error", reason: "error", error: fallback });
|
|
1841
1862
|
} catch (err) {
|
|
1842
1863
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
1864
|
+
if (options?.signal?.aborted || isClaudeCodeAbortErrorMessage(errorMsg)) {
|
|
1865
|
+
const abortedText = resolveClaudeCodeAbortedMessageText(errorMsg, lastTextContent);
|
|
1866
|
+
stream.push({
|
|
1867
|
+
type: "error",
|
|
1868
|
+
reason: "aborted",
|
|
1869
|
+
error: makeAbortedMessage(modelId, abortedText),
|
|
1870
|
+
});
|
|
1871
|
+
return;
|
|
1872
|
+
}
|
|
1843
1873
|
stream.push({
|
|
1844
1874
|
type: "error",
|
|
1845
1875
|
reason: "error",
|
|
@@ -5,6 +5,8 @@ import { join, resolve } from "node:path";
|
|
|
5
5
|
import { tmpdir } from "node:os";
|
|
6
6
|
import {
|
|
7
7
|
makeStreamExhaustedErrorMessage,
|
|
8
|
+
isClaudeCodeAbortErrorMessage,
|
|
9
|
+
resolveClaudeCodeAbortedMessageText,
|
|
8
10
|
getResultErrorMessage,
|
|
9
11
|
makeAbortedMessage,
|
|
10
12
|
mergePendingToolCalls,
|
|
@@ -1212,6 +1214,12 @@ describe("stream-adapter — MCP elicitation bridge", () => {
|
|
|
1212
1214
|
// ---------------------------------------------------------------------------
|
|
1213
1215
|
|
|
1214
1216
|
describe("stream-adapter — abort classification (F2)", () => {
|
|
1217
|
+
test("recognizes Claude Code SDK abort exceptions", () => {
|
|
1218
|
+
assert.equal(isClaudeCodeAbortErrorMessage("Claude Code process aborted by user"), true);
|
|
1219
|
+
assert.equal(isClaudeCodeAbortErrorMessage("Request aborted by user"), true);
|
|
1220
|
+
assert.equal(isClaudeCodeAbortErrorMessage("rate limit exceeded"), false);
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1215
1223
|
test("makeAbortedMessage sets stopReason to 'aborted', not 'error'", () => {
|
|
1216
1224
|
const message = makeAbortedMessage("claude-sonnet-4-6", "");
|
|
1217
1225
|
assert.equal(message.stopReason, "aborted");
|
|
@@ -1229,6 +1237,24 @@ describe("stream-adapter — abort classification (F2)", () => {
|
|
|
1229
1237
|
assert.notEqual(aborted.stopReason, exhausted.stopReason);
|
|
1230
1238
|
assert.equal(exhausted.errorMessage, "stream_exhausted_without_result");
|
|
1231
1239
|
});
|
|
1240
|
+
|
|
1241
|
+
test("abort catch preserves SDK diagnostic text instead of partial output", () => {
|
|
1242
|
+
const text = resolveClaudeCodeAbortedMessageText(
|
|
1243
|
+
"Request aborted by user\nAPI Error: 529 overloaded",
|
|
1244
|
+
"partial mid-stream text",
|
|
1245
|
+
);
|
|
1246
|
+
|
|
1247
|
+
assert.equal(text, "Request aborted by user\nAPI Error: 529 overloaded");
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
test("abort catch falls back to partial output for bare abort markers", () => {
|
|
1251
|
+
const text = resolveClaudeCodeAbortedMessageText(
|
|
1252
|
+
"Request aborted by user",
|
|
1253
|
+
"partial mid-stream text",
|
|
1254
|
+
);
|
|
1255
|
+
|
|
1256
|
+
assert.equal(text, "partial mid-stream text");
|
|
1257
|
+
});
|
|
1232
1258
|
});
|
|
1233
1259
|
|
|
1234
1260
|
// ---------------------------------------------------------------------------
|
|
@@ -23,7 +23,7 @@ import type { CmuxLogLevel } from "../../shared/cmux-events.js";
|
|
|
23
23
|
import type { JournalEntry } from "../journal.js";
|
|
24
24
|
import type { MergeReconcileResult } from "../auto-recovery.js";
|
|
25
25
|
import type { UokTurnObserver } from "../uok/contracts.js";
|
|
26
|
-
import type { PreflightResult } from "../clean-root-preflight.js";
|
|
26
|
+
import type { PostflightResult, PreflightResult } from "../clean-root-preflight.js";
|
|
27
27
|
|
|
28
28
|
type PauseAutoFn = (
|
|
29
29
|
ctx?: ExtensionContext,
|
|
@@ -141,7 +141,7 @@ export interface LoopDeps {
|
|
|
141
141
|
milestoneId: string,
|
|
142
142
|
stashMarker: string | undefined,
|
|
143
143
|
notify: (message: string, level: "info" | "warning" | "error") => void,
|
|
144
|
-
) =>
|
|
144
|
+
) => PostflightResult;
|
|
145
145
|
|
|
146
146
|
// Budget/context/secrets
|
|
147
147
|
getLedger: () => unknown;
|
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
getRecentForUnit as getRecentDispatchesForUnit,
|
|
39
39
|
getRecentUnitKeysForProjectRoot,
|
|
40
40
|
} from "../db/unit-dispatches.js";
|
|
41
|
-
import { refreshMilestoneLease } from "../db/milestone-leases.js";
|
|
41
|
+
import { claimMilestoneLease, refreshMilestoneLease } from "../db/milestone-leases.js";
|
|
42
42
|
import { heartbeatAutoWorker } from "../db/auto-workers.js";
|
|
43
43
|
import { getRuntimeKv, setRuntimeKv } from "../db/runtime-kv.js";
|
|
44
44
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
@@ -70,7 +70,7 @@ import {
|
|
|
70
70
|
} from "./workflow-dispatch-ledger.js";
|
|
71
71
|
import { emitOpenUnitEndForUnit } from "../crash-recovery.js";
|
|
72
72
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
73
|
-
import { openDispatchClaim } from "./workflow-dispatch-claim.js";
|
|
73
|
+
import { ensureDispatchLease, openDispatchClaim } from "./workflow-dispatch-claim.js";
|
|
74
74
|
import { completeWorkflowIteration } from "./workflow-iteration-completion.js";
|
|
75
75
|
import { createWorkflowJournalReporter } from "./workflow-journal-reporter.js";
|
|
76
76
|
import { createWorkflowPhaseReporter } from "./workflow-phase-reporter.js";
|
|
@@ -165,6 +165,29 @@ function logDispatchClaimFailed(err: unknown): void {
|
|
|
165
165
|
});
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
function logDispatchLeaseRecovered(details: {
|
|
169
|
+
milestoneId: string;
|
|
170
|
+
workerId: string;
|
|
171
|
+
token: number;
|
|
172
|
+
recovered: boolean;
|
|
173
|
+
}): void {
|
|
174
|
+
debugLog("autoLoop", {
|
|
175
|
+
phase: details.recovered ? "dispatch-lease-recovered" : "dispatch-lease-acquired",
|
|
176
|
+
...details,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function logDispatchLeaseRecoveryFailed(details: {
|
|
181
|
+
milestoneId?: string;
|
|
182
|
+
workerId?: string;
|
|
183
|
+
reason: string;
|
|
184
|
+
}): void {
|
|
185
|
+
debugLog("autoLoop", {
|
|
186
|
+
phase: "dispatch-lease-recovery-failed",
|
|
187
|
+
...details,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
168
191
|
function logCustomVerifyRetryLoadFailure(err: unknown): void {
|
|
169
192
|
debugLog("autoLoop", {
|
|
170
193
|
phase: "load-custom-verify-retries-failed",
|
|
@@ -274,6 +297,10 @@ export async function autoLoop(
|
|
|
274
297
|
phase: "heartbeat-failed",
|
|
275
298
|
error: err instanceof Error ? err.message : String(err),
|
|
276
299
|
}),
|
|
300
|
+
logLeaseRefreshMiss: details => debugLog("autoLoop", {
|
|
301
|
+
phase: "lease-refresh-missed",
|
|
302
|
+
...details,
|
|
303
|
+
}),
|
|
277
304
|
});
|
|
278
305
|
|
|
279
306
|
// ── Journal: per-iteration flow grouping ──
|
|
@@ -703,25 +730,74 @@ export async function autoLoop(
|
|
|
703
730
|
|
|
704
731
|
// Phase B: claim a unit_dispatches row before invoking the unit. The
|
|
705
732
|
// partial unique index idx_unit_dispatches_active_per_unit prevents
|
|
706
|
-
// a second worker from claiming the same unit concurrently.
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
//
|
|
710
|
-
const
|
|
733
|
+
// a second worker from claiming the same unit concurrently. When this
|
|
734
|
+
// process has a worker identity, make the milestone lease explicit before
|
|
735
|
+
// claiming so a step-mode handoff cannot leave us running with a stale
|
|
736
|
+
// in-memory token and no backing lease row.
|
|
737
|
+
const leaseBeforeClaim = ensureDispatchLease(s, iterData.mid, {
|
|
738
|
+
claimMilestoneLease,
|
|
739
|
+
logLeaseRecovered: logDispatchLeaseRecovered,
|
|
740
|
+
logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
|
|
741
|
+
});
|
|
742
|
+
if (leaseBeforeClaim.kind === "blocked" || leaseBeforeClaim.kind === "failed") {
|
|
743
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} before dispatching ${iterData.unitType} ${iterData.unitId}: ${leaseBeforeClaim.reason}`;
|
|
744
|
+
ctx.ui.notify(msg, "error");
|
|
745
|
+
finishTurn("stopped", "execution", msg);
|
|
746
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
747
|
+
break;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
let dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
|
|
711
751
|
getRecentDispatchesForUnit,
|
|
712
752
|
recordDispatchClaim,
|
|
713
753
|
markDispatchRunning,
|
|
714
754
|
logClaimRejected: logDispatchClaimRejected,
|
|
715
755
|
logClaimFailed: logDispatchClaimFailed,
|
|
716
756
|
});
|
|
717
|
-
|
|
757
|
+
let dispatchDecision = decideDispatchClaim(
|
|
718
758
|
dispatchClaim.kind === "opened"
|
|
719
759
|
? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
|
|
720
760
|
: dispatchClaim.kind === "skip"
|
|
721
761
|
? { kind: "skip", reason: dispatchClaim.reason }
|
|
722
762
|
: { kind: "degraded" },
|
|
723
763
|
);
|
|
764
|
+
if (dispatchDecision.action === "skip" && dispatchDecision.reason === "stale-lease") {
|
|
765
|
+
const leaseRecovery = ensureDispatchLease(s, iterData.mid, {
|
|
766
|
+
claimMilestoneLease,
|
|
767
|
+
logLeaseRecovered: logDispatchLeaseRecovered,
|
|
768
|
+
logLeaseRecoveryFailed: logDispatchLeaseRecoveryFailed,
|
|
769
|
+
}, { forceReclaim: true });
|
|
770
|
+
if (leaseRecovery.kind === "ready") {
|
|
771
|
+
dispatchClaim = openDispatchClaim(s, flowId, turnId, iterData, {
|
|
772
|
+
getRecentDispatchesForUnit,
|
|
773
|
+
recordDispatchClaim,
|
|
774
|
+
markDispatchRunning,
|
|
775
|
+
logClaimRejected: logDispatchClaimRejected,
|
|
776
|
+
logClaimFailed: logDispatchClaimFailed,
|
|
777
|
+
});
|
|
778
|
+
dispatchDecision = decideDispatchClaim(
|
|
779
|
+
dispatchClaim.kind === "opened"
|
|
780
|
+
? { kind: "opened", dispatchId: dispatchClaim.dispatchId }
|
|
781
|
+
: dispatchClaim.kind === "skip"
|
|
782
|
+
? { kind: "skip", reason: dispatchClaim.reason }
|
|
783
|
+
: { kind: "degraded" },
|
|
784
|
+
);
|
|
785
|
+
} else {
|
|
786
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}: ${leaseRecovery.reason}`;
|
|
787
|
+
ctx.ui.notify(msg, "error");
|
|
788
|
+
finishTurn("stopped", "execution", msg);
|
|
789
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
790
|
+
break;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
724
793
|
if (dispatchDecision.action === "skip") {
|
|
794
|
+
if (dispatchDecision.reason === "stale-lease") {
|
|
795
|
+
const msg = `Lost milestone lease for ${iterData.mid ?? "unknown"} while claiming ${iterData.unitType} ${iterData.unitId}; dispatch claim still failed after recovery.`;
|
|
796
|
+
ctx.ui.notify(msg, "error");
|
|
797
|
+
finishTurn("stopped", "execution", msg);
|
|
798
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
725
801
|
finishTurn("skipped", "execution", dispatchDecision.reason);
|
|
726
802
|
continue;
|
|
727
803
|
}
|
|
@@ -57,6 +57,7 @@ import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
|
57
57
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
58
58
|
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
59
59
|
import type { MinimalModelRegistry } from "../context-budget.js";
|
|
60
|
+
import type { PostflightResult, PreflightResult } from "../clean-root-preflight.js";
|
|
60
61
|
import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
|
|
61
62
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
62
63
|
import { UokGateRunner } from "../uok/gate-runner.js";
|
|
@@ -76,6 +77,17 @@ function isSamePathLocal(a: string, b: string): boolean {
|
|
|
76
77
|
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
77
78
|
}
|
|
78
79
|
|
|
80
|
+
export function shouldDegradeEmptyWorktreeToProjectRoot(
|
|
81
|
+
worktreeClassification: ReturnType<typeof classifyProject>,
|
|
82
|
+
projectRootClassification: ReturnType<typeof classifyProject>,
|
|
83
|
+
): boolean {
|
|
84
|
+
return (
|
|
85
|
+
worktreeClassification.kind === "greenfield" &&
|
|
86
|
+
projectRootClassification.kind !== "greenfield" &&
|
|
87
|
+
projectRootClassification.kind !== "invalid-repo"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
80
92
|
|
|
81
93
|
let consecutiveSessionTimeouts = 0;
|
|
@@ -209,6 +221,117 @@ async function closeoutAndStop(
|
|
|
209
221
|
await deps.stopAuto(ctx, pi, reason);
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
async function stopOnPostflightRecoveryNeeded(
|
|
225
|
+
ic: IterationContext,
|
|
226
|
+
result: PostflightResult,
|
|
227
|
+
milestoneId: string,
|
|
228
|
+
): Promise<{ action: "break"; reason: string } | null> {
|
|
229
|
+
if (!result.needsManualRecovery) return null;
|
|
230
|
+
const { ctx, pi, deps } = ic;
|
|
231
|
+
const reason = `Post-merge stash restore failed for milestone ${milestoneId}`;
|
|
232
|
+
ctx.ui.notify(
|
|
233
|
+
`${reason}. Resolve the working tree before resuming auto-mode. ${result.message}`,
|
|
234
|
+
"error",
|
|
235
|
+
);
|
|
236
|
+
await deps.stopAuto(ctx, pi, reason);
|
|
237
|
+
return { action: "break", reason: "postflight-stash-restore-failed" };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function restorePreflightStashOrStop(
|
|
241
|
+
ic: IterationContext,
|
|
242
|
+
preflight: PreflightResult,
|
|
243
|
+
milestoneId: string,
|
|
244
|
+
): Promise<{ action: "break"; reason: string } | null> {
|
|
245
|
+
if (!preflight.stashPushed) return null;
|
|
246
|
+
const { ctx, s, deps } = ic;
|
|
247
|
+
const result = deps.postflightPopStash(
|
|
248
|
+
s.originalBasePath || s.basePath,
|
|
249
|
+
milestoneId,
|
|
250
|
+
preflight.stashMarker,
|
|
251
|
+
ctx.ui.notify.bind(ctx.ui),
|
|
252
|
+
);
|
|
253
|
+
return stopOnPostflightRecoveryNeeded(ic, result, milestoneId);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Run a milestone merge surrounded by preflight stash + always-on postflight
|
|
258
|
+
* pop. The previous code popped the stash only after a successful merge, which
|
|
259
|
+
* leaked `gsd-preflight-stash:M00x:*` entries whenever `mergeAndExit` threw —
|
|
260
|
+
* leaving the user's pre-merge working tree silently stashed away after a
|
|
261
|
+
* merge-conflict or other merge error. This helper restores the stash on
|
|
262
|
+
* every exit path, then surfaces the merge or stash failure (in priority
|
|
263
|
+
* order) as the loop's stop reason.
|
|
264
|
+
*
|
|
265
|
+
* Returns a `break` action when auto-mode must stop, or `null` when the merge
|
|
266
|
+
* succeeded and the stash (if any) was restored cleanly.
|
|
267
|
+
*/
|
|
268
|
+
export async function _runMilestoneMergeWithStashRestore(
|
|
269
|
+
ic: IterationContext,
|
|
270
|
+
milestoneId: string,
|
|
271
|
+
): Promise<{ action: "break"; reason: string } | null> {
|
|
272
|
+
const { ctx, pi, s, deps } = ic;
|
|
273
|
+
|
|
274
|
+
const preflight = deps.preflightCleanRoot(
|
|
275
|
+
s.originalBasePath || s.basePath,
|
|
276
|
+
milestoneId,
|
|
277
|
+
ctx.ui.notify.bind(ctx.ui),
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
let mergeError: unknown = null;
|
|
281
|
+
try {
|
|
282
|
+
deps.resolver.mergeAndExit(milestoneId, ctx.ui);
|
|
283
|
+
s.milestoneMergedInPhases = true;
|
|
284
|
+
} catch (mergeErr) {
|
|
285
|
+
mergeError = mergeErr;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Always attempt to restore the stashed working tree, even on merge error.
|
|
289
|
+
// postflightPopStash itself does not throw; failures surface via the
|
|
290
|
+
// PostflightResult.needsManualRecovery flag.
|
|
291
|
+
let stashResult: PostflightResult | null = null;
|
|
292
|
+
if (preflight.stashPushed) {
|
|
293
|
+
stashResult = deps.postflightPopStash(
|
|
294
|
+
s.originalBasePath || s.basePath,
|
|
295
|
+
milestoneId,
|
|
296
|
+
preflight.stashMarker,
|
|
297
|
+
ctx.ui.notify.bind(ctx.ui),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Merge failure takes priority over stash recovery — the merge is the
|
|
302
|
+
// authoritative gate. If the stash also needed manual recovery, the user
|
|
303
|
+
// already saw the postflightPopStash notify above.
|
|
304
|
+
if (mergeError) {
|
|
305
|
+
if (mergeError instanceof MergeConflictError) {
|
|
306
|
+
ctx.ui.notify(
|
|
307
|
+
`Merge conflict: ${mergeError.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
308
|
+
"error",
|
|
309
|
+
);
|
|
310
|
+
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${milestoneId}`);
|
|
311
|
+
return { action: "break", reason: "merge-conflict" };
|
|
312
|
+
}
|
|
313
|
+
logError("engine", "Milestone merge failed with non-conflict error", {
|
|
314
|
+
milestone: milestoneId,
|
|
315
|
+
error: String(mergeError),
|
|
316
|
+
});
|
|
317
|
+
ctx.ui.notify(
|
|
318
|
+
`Merge failed: ${mergeError instanceof Error ? mergeError.message : String(mergeError)}. Resolve and run /gsd auto to resume.`,
|
|
319
|
+
"error",
|
|
320
|
+
);
|
|
321
|
+
await deps.stopAuto(
|
|
322
|
+
ctx,
|
|
323
|
+
pi,
|
|
324
|
+
`Merge error on milestone ${milestoneId}: ${String(mergeError)}`,
|
|
325
|
+
);
|
|
326
|
+
return { action: "break", reason: "merge-failed" };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (stashResult) {
|
|
330
|
+
return stopOnPostflightRecoveryNeeded(ic, stashResult, milestoneId);
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
212
335
|
async function emitCancelledUnitEnd(
|
|
213
336
|
ic: IterationContext,
|
|
214
337
|
unitType: string,
|
|
@@ -645,42 +768,11 @@ export async function runPreDispatch(
|
|
|
645
768
|
loopState.recentUnits.length = 0;
|
|
646
769
|
loopState.stuckRecoveryAttempts = 0;
|
|
647
770
|
|
|
648
|
-
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
649
|
-
// #2909: preflight
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
654
|
-
);
|
|
655
|
-
try {
|
|
656
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId!, ctx.ui);
|
|
657
|
-
} catch (mergeErr) {
|
|
658
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
659
|
-
// Real code conflicts — stop the loop instead of retrying forever (#2330)
|
|
660
|
-
ctx.ui.notify(
|
|
661
|
-
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
662
|
-
"error",
|
|
663
|
-
);
|
|
664
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
665
|
-
return { action: "break", reason: "merge-conflict" };
|
|
666
|
-
}
|
|
667
|
-
// Non-conflict merge errors — stop auto to avoid advancing with unmerged work
|
|
668
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
|
|
669
|
-
ctx.ui.notify(
|
|
670
|
-
`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
|
|
671
|
-
"error",
|
|
672
|
-
);
|
|
673
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
674
|
-
return { action: "break", reason: "merge-failed" };
|
|
675
|
-
}
|
|
676
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
677
|
-
if (preflightTransition.stashPushed) {
|
|
678
|
-
deps.postflightPopStash(
|
|
679
|
-
s.originalBasePath || s.basePath,
|
|
680
|
-
s.currentMilestoneId!,
|
|
681
|
-
preflightTransition.stashMarker,
|
|
682
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
683
|
-
);
|
|
771
|
+
// Worktree lifecycle on milestone transition — merge current, enter next.
|
|
772
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
773
|
+
{
|
|
774
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId!);
|
|
775
|
+
if (stop) return stop;
|
|
684
776
|
}
|
|
685
777
|
|
|
686
778
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
@@ -758,45 +850,11 @@ export async function runPreDispatch(
|
|
|
758
850
|
m.status !== "complete" && m.status !== "parked",
|
|
759
851
|
);
|
|
760
852
|
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
761
|
-
// All milestones complete — merge milestone branch before stopping
|
|
853
|
+
// All milestones complete — merge milestone branch before stopping.
|
|
762
854
|
if (s.currentMilestoneId) {
|
|
763
|
-
// #2909: preflight
|
|
764
|
-
const
|
|
765
|
-
|
|
766
|
-
s.currentMilestoneId,
|
|
767
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
768
|
-
);
|
|
769
|
-
try {
|
|
770
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
771
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
772
|
-
s.milestoneMergedInPhases = true;
|
|
773
|
-
} catch (mergeErr) {
|
|
774
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
775
|
-
ctx.ui.notify(
|
|
776
|
-
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
777
|
-
"error",
|
|
778
|
-
);
|
|
779
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
780
|
-
return { action: "break", reason: "merge-conflict" };
|
|
781
|
-
}
|
|
782
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
|
|
783
|
-
ctx.ui.notify(
|
|
784
|
-
`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
|
|
785
|
-
"error",
|
|
786
|
-
);
|
|
787
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
788
|
-
return { action: "break", reason: "merge-failed" };
|
|
789
|
-
}
|
|
790
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
791
|
-
if (preflightAllComplete.stashPushed) {
|
|
792
|
-
deps.postflightPopStash(
|
|
793
|
-
s.originalBasePath || s.basePath,
|
|
794
|
-
s.currentMilestoneId,
|
|
795
|
-
preflightAllComplete.stashMarker,
|
|
796
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
797
|
-
);
|
|
798
|
-
}
|
|
799
|
-
|
|
855
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
856
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
857
|
+
if (stop) return stop;
|
|
800
858
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
801
859
|
}
|
|
802
860
|
deps.sendDesktopNotification(
|
|
@@ -887,45 +945,11 @@ export async function runPreDispatch(
|
|
|
887
945
|
|
|
888
946
|
// Terminal: complete
|
|
889
947
|
if (state.phase === "complete") {
|
|
890
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
948
|
+
// Milestone merge on complete (before closeout so branch state is clean).
|
|
891
949
|
if (s.currentMilestoneId) {
|
|
892
|
-
// #2909: preflight
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
s.currentMilestoneId,
|
|
896
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
897
|
-
);
|
|
898
|
-
try {
|
|
899
|
-
deps.resolver.mergeAndExit(s.currentMilestoneId, ctx.ui);
|
|
900
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
901
|
-
s.milestoneMergedInPhases = true;
|
|
902
|
-
} catch (mergeErr) {
|
|
903
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
904
|
-
ctx.ui.notify(
|
|
905
|
-
`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`,
|
|
906
|
-
"error",
|
|
907
|
-
);
|
|
908
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
909
|
-
return { action: "break", reason: "merge-conflict" };
|
|
910
|
-
}
|
|
911
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId!, error: String(mergeErr) });
|
|
912
|
-
ctx.ui.notify(
|
|
913
|
-
`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`,
|
|
914
|
-
"error",
|
|
915
|
-
);
|
|
916
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
917
|
-
return { action: "break", reason: "merge-failed" };
|
|
918
|
-
}
|
|
919
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
920
|
-
if (preflightComplete.stashPushed) {
|
|
921
|
-
deps.postflightPopStash(
|
|
922
|
-
s.originalBasePath || s.basePath,
|
|
923
|
-
s.currentMilestoneId,
|
|
924
|
-
preflightComplete.stashMarker,
|
|
925
|
-
ctx.ui.notify.bind(ctx.ui),
|
|
926
|
-
);
|
|
927
|
-
}
|
|
928
|
-
|
|
950
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
951
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
952
|
+
if (stop) return stop;
|
|
929
953
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
930
954
|
}
|
|
931
955
|
deps.sendDesktopNotification(
|
|
@@ -1048,6 +1072,65 @@ export async function runDispatch(
|
|
|
1048
1072
|
let prompt = dispatchResult.prompt;
|
|
1049
1073
|
const pauseAfterUatDispatch = dispatchResult.pauseAfterDispatch ?? false;
|
|
1050
1074
|
|
|
1075
|
+
// Resolve hooks and prior-slice gating before health/stuck accounting so
|
|
1076
|
+
// those checks run against the final dispatch unit.
|
|
1077
|
+
const preDispatchResult = deps.runPreDispatchHooks(
|
|
1078
|
+
unitType,
|
|
1079
|
+
unitId,
|
|
1080
|
+
prompt,
|
|
1081
|
+
s.basePath,
|
|
1082
|
+
);
|
|
1083
|
+
if (preDispatchResult.firedHooks.length > 0) {
|
|
1084
|
+
ctx.ui.notify(
|
|
1085
|
+
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1086
|
+
"info",
|
|
1087
|
+
);
|
|
1088
|
+
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
1089
|
+
}
|
|
1090
|
+
if (preDispatchResult.action === "skip") {
|
|
1091
|
+
ctx.ui.notify(
|
|
1092
|
+
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1093
|
+
"info",
|
|
1094
|
+
);
|
|
1095
|
+
await new Promise((r) => setImmediate(r));
|
|
1096
|
+
return { action: "continue" };
|
|
1097
|
+
}
|
|
1098
|
+
if (preDispatchResult.action === "replace") {
|
|
1099
|
+
prompt = preDispatchResult.prompt ?? prompt;
|
|
1100
|
+
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1101
|
+
} else if (preDispatchResult.prompt) {
|
|
1102
|
+
prompt = preDispatchResult.prompt;
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
1106
|
+
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
|
|
1107
|
+
guardBasePath,
|
|
1108
|
+
deps.getMainBranch(guardBasePath),
|
|
1109
|
+
unitType,
|
|
1110
|
+
unitId,
|
|
1111
|
+
);
|
|
1112
|
+
if (priorSliceBlocker) {
|
|
1113
|
+
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1114
|
+
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1115
|
+
return { action: "break", reason: "prior-slice-blocker" };
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
// Execute-task needs a real writable checkout. The same health check also
|
|
1119
|
+
// exists in runUnitPhase, but the stuck-window detector runs before that
|
|
1120
|
+
// phase. Check here too so repeated derivations of a broken worktree stop
|
|
1121
|
+
// with the actionable worktree error instead of the generic stuck-loop error.
|
|
1122
|
+
if (s.basePath && unitType === "execute-task") {
|
|
1123
|
+
const gitMarker = join(s.basePath, ".git");
|
|
1124
|
+
const hasGit = deps.existsSync(gitMarker);
|
|
1125
|
+
if (!hasGit) {
|
|
1126
|
+
const msg = `Worktree health check failed: ${s.basePath} has no .git — refusing to dispatch ${unitType} ${unitId}`;
|
|
1127
|
+
debugLog("autoLoop", { phase: "dispatch-worktree-health-fail", basePath: s.basePath, hasGit });
|
|
1128
|
+
ctx.ui.notify(msg, "error");
|
|
1129
|
+
await deps.stopAuto(ctx, pi, msg);
|
|
1130
|
+
return { action: "break", reason: "worktree-invalid" };
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1051
1134
|
// ── Sliding-window stuck detection with graduated recovery ──
|
|
1052
1135
|
const derivedKey = `${unitType}/${unitId}`;
|
|
1053
1136
|
|
|
@@ -1189,48 +1272,6 @@ export async function runDispatch(
|
|
|
1189
1272
|
}
|
|
1190
1273
|
}
|
|
1191
1274
|
|
|
1192
|
-
// Pre-dispatch hooks
|
|
1193
|
-
const preDispatchResult = deps.runPreDispatchHooks(
|
|
1194
|
-
unitType,
|
|
1195
|
-
unitId,
|
|
1196
|
-
prompt,
|
|
1197
|
-
s.basePath,
|
|
1198
|
-
);
|
|
1199
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
1200
|
-
ctx.ui.notify(
|
|
1201
|
-
`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`,
|
|
1202
|
-
"info",
|
|
1203
|
-
);
|
|
1204
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
1205
|
-
}
|
|
1206
|
-
if (preDispatchResult.action === "skip") {
|
|
1207
|
-
ctx.ui.notify(
|
|
1208
|
-
`Skipping ${unitType} ${unitId} (pre-dispatch hook).`,
|
|
1209
|
-
"info",
|
|
1210
|
-
);
|
|
1211
|
-
await new Promise((r) => setImmediate(r));
|
|
1212
|
-
return { action: "continue" };
|
|
1213
|
-
}
|
|
1214
|
-
if (preDispatchResult.action === "replace") {
|
|
1215
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
1216
|
-
if (preDispatchResult.unitType) unitType = preDispatchResult.unitType;
|
|
1217
|
-
} else if (preDispatchResult.prompt) {
|
|
1218
|
-
prompt = preDispatchResult.prompt;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
1222
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(
|
|
1223
|
-
guardBasePath,
|
|
1224
|
-
deps.getMainBranch(guardBasePath),
|
|
1225
|
-
unitType,
|
|
1226
|
-
unitId,
|
|
1227
|
-
);
|
|
1228
|
-
if (priorSliceBlocker) {
|
|
1229
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
1230
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
1231
|
-
return { action: "break", reason: "prior-slice-blocker" };
|
|
1232
|
-
}
|
|
1233
|
-
|
|
1234
1275
|
return {
|
|
1235
1276
|
action: "next",
|
|
1236
1277
|
data: {
|
|
@@ -1523,6 +1564,29 @@ export async function runUnitPhase(
|
|
|
1523
1564
|
return { action: "break", reason: "worktree-invalid" };
|
|
1524
1565
|
}
|
|
1525
1566
|
} else if (projectClassification.kind === "greenfield") {
|
|
1567
|
+
const projectRoot = s.canonicalProjectRoot;
|
|
1568
|
+
if (!isSamePathLocal(s.basePath, projectRoot)) {
|
|
1569
|
+
const projectRootClassification = classifyProject(projectRoot);
|
|
1570
|
+
if (shouldDegradeEmptyWorktreeToProjectRoot(projectClassification, projectRootClassification)) {
|
|
1571
|
+
debugLog("runUnitPhase", {
|
|
1572
|
+
phase: "worktree-health-degrade-to-project-root",
|
|
1573
|
+
worktreePath: s.basePath,
|
|
1574
|
+
projectRoot,
|
|
1575
|
+
worktreeClassification: projectClassification,
|
|
1576
|
+
projectRootClassification,
|
|
1577
|
+
});
|
|
1578
|
+
ctx.ui.notify(
|
|
1579
|
+
`Warning: ${s.basePath} has no project content, but ${projectRoot} does. Continuing in project root because the milestone worktree cannot represent untracked project files.`,
|
|
1580
|
+
"warning",
|
|
1581
|
+
);
|
|
1582
|
+
s.basePath = projectRoot;
|
|
1583
|
+
s.isolationDegraded = true;
|
|
1584
|
+
projectClassification = projectRootClassification;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (projectClassification.kind === "greenfield") {
|
|
1526
1590
|
debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
|
|
1527
1591
|
ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
|
|
1528
1592
|
} else if (projectClassification.kind === "untyped-existing") {
|