gsd-pi 2.80.0-dev.2be7caf18 → 2.80.0-dev.3065f7bfa
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/README.md +1 -0
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/gsd/auto/phases.js +178 -108
- package/dist/resources/extensions/gsd/auto/resolve.js +17 -0
- package/dist/resources/extensions/gsd/auto/run-unit.js +20 -30
- package/dist/resources/extensions/gsd/auto/session.js +8 -0
- 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-recovery.js +43 -1
- package/dist/resources/extensions/gsd/auto-start.js +197 -6
- package/dist/resources/extensions/gsd/auto-supervisor.js +8 -1
- package/dist/resources/extensions/gsd/auto-timeout-recovery.js +2 -2
- package/dist/resources/extensions/gsd/auto-worktree.js +111 -1
- package/dist/resources/extensions/gsd/auto.js +79 -24
- package/dist/resources/extensions/gsd/bootstrap/agent-end-recovery.js +42 -2
- 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 +238 -28
- 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/context-budget.js +37 -2
- package/dist/resources/extensions/gsd/db/unit-dispatches.js +39 -0
- package/dist/resources/extensions/gsd/db-base-schema.js +4 -2
- package/dist/resources/extensions/gsd/db-migration-steps.js +6 -0
- package/dist/resources/extensions/gsd/ecosystem/gsd-extension-api.js +2 -0
- package/dist/resources/extensions/gsd/git-service.js +36 -4
- package/dist/resources/extensions/gsd/gsd-db.js +46 -13
- package/dist/resources/extensions/gsd/guided-flow.js +66 -32
- package/dist/resources/extensions/gsd/memory-store.js +69 -12
- 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 +22 -0
- package/dist/resources/extensions/gsd/prompt-loader.js +28 -2
- package/dist/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- 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/tools/memory-tools.js +1 -0
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +1 -1
- package/dist/resources/extensions/gsd/unit-runtime.js +11 -0
- package/dist/resources/extensions/gsd/workflow-protocol.js +131 -0
- package/dist/resources/extensions/gsd/worktree-resolver.js +68 -21
- 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 +15 -15
- 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 +15 -15
- 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/package.json +3 -3
- package/packages/mcp-server/src/workflow-tools.test.ts +9 -2
- package/packages/native/tsconfig.tsbuildinfo +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-abort-order.test.js +32 -0
- package/packages/pi-coding-agent/dist/core/agent-session-abort-order.test.js.map +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 +41 -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-abort-order.test.ts +36 -0
- 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 +45 -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/gsd/auto/loop-deps.ts +2 -2
- package/src/resources/extensions/gsd/auto/phases.ts +251 -172
- package/src/resources/extensions/gsd/auto/resolve.ts +23 -1
- package/src/resources/extensions/gsd/auto/run-unit.ts +25 -30
- package/src/resources/extensions/gsd/auto/session.ts +8 -0
- 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-recovery.ts +54 -0
- package/src/resources/extensions/gsd/auto-start.ts +230 -9
- package/src/resources/extensions/gsd/auto-supervisor.ts +7 -0
- package/src/resources/extensions/gsd/auto-timeout-recovery.ts +2 -2
- package/src/resources/extensions/gsd/auto-worktree.ts +123 -0
- package/src/resources/extensions/gsd/auto.ts +87 -19
- package/src/resources/extensions/gsd/bootstrap/agent-end-recovery.ts +49 -1
- 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 +283 -28
- 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/context-budget.ts +44 -2
- package/src/resources/extensions/gsd/db/unit-dispatches.ts +41 -0
- package/src/resources/extensions/gsd/db-base-schema.ts +4 -2
- package/src/resources/extensions/gsd/db-migration-steps.ts +8 -0
- package/src/resources/extensions/gsd/ecosystem/gsd-extension-api.ts +3 -0
- package/src/resources/extensions/gsd/git-service.ts +46 -8
- package/src/resources/extensions/gsd/gsd-db.ts +50 -13
- package/src/resources/extensions/gsd/guided-flow.ts +84 -39
- package/src/resources/extensions/gsd/memory-store.ts +77 -12
- 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 +23 -0
- package/src/resources/extensions/gsd/prompt-loader.ts +27 -2
- package/src/resources/extensions/gsd/prompts/complete-milestone.md +16 -13
- 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 +417 -10
- package/src/resources/extensions/gsd/tests/auto-phases-lifecycle.test.ts +56 -13
- package/src/resources/extensions/gsd/tests/auto-recovery.test.ts +14 -1
- 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/compaction-snapshot.test.ts +3 -1
- 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-budget.test.ts +10 -1
- 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/dispatch-rule-coverage.test.ts +313 -0
- 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/integration/git-service.test.ts +54 -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 +239 -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/memory-decay-factor.test.ts +90 -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 +45 -5
- package/src/resources/extensions/gsd/tests/prompt-duplication-cuts.test.ts +230 -0
- package/src/resources/extensions/gsd/tests/prompt-step-ordering.test.ts +19 -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/schema-v27-v28-sequence.test.ts +156 -0
- package/src/resources/extensions/gsd/tests/select-resumable-milestone.test.ts +96 -0
- package/src/resources/extensions/gsd/tests/session-switch-abort-misclassification.test.ts +106 -0
- package/src/resources/extensions/gsd/tests/signal-handlers.test.ts +27 -0
- package/src/resources/extensions/gsd/tests/stalled-tool-recovery.test.ts +49 -1
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +38 -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 +30 -0
- package/src/resources/extensions/gsd/tests/unit-runtime.test.ts +30 -0
- package/src/resources/extensions/gsd/tests/unstructured-continue-context-injection.test.ts +5 -4
- package/src/resources/extensions/gsd/tests/workflow-protocol-excerpt.test.ts +99 -0
- package/src/resources/extensions/gsd/tests/workflow-tool-executors.test.ts +3 -0
- 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 +167 -4
- package/src/resources/extensions/gsd/tools/memory-tools.ts +1 -0
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +1 -1
- package/src/resources/extensions/gsd/unit-runtime.ts +11 -0
- package/src/resources/extensions/gsd/workflow-protocol.ts +160 -0
- package/src/resources/extensions/gsd/worktree-resolver.ts +85 -19
- package/src/resources/extensions/gsd/tests/phases-merge-error-stops-auto.test.ts +0 -97
- /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{3EDDd9ULaybSupPoA_vf- → O2WvnXROue6l7nAp_4dhC}/_ssgManifest.js +0 -0
package/README.md
CHANGED
|
@@ -259,6 +259,7 @@ Full documentation is in the [`docs/`](./docs/) directory:
|
|
|
259
259
|
|
|
260
260
|
- **[Architecture](./docs/dev/architecture.md)** — system design and dispatch pipeline
|
|
261
261
|
- **[CI/CD Pipeline](./docs/dev/ci-cd-pipeline.md)** — three-stage promotion pipeline (Dev → Test → Prod)
|
|
262
|
+
- **[E2E Testing](./tests/e2e/README.md)** — real-process CLI/runtime coverage and CI runner expectations
|
|
262
263
|
- **[Pipeline Simplification (ADR-003)](./docs/dev/ADR-003-pipeline-simplification.md)** — merged research into planning, mechanical completion
|
|
263
264
|
- **[VS Code Extension](./vscode-extension/README.md)** — chat participant, sidebar dashboard, RPC integration
|
|
264
265
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
89938c18ffd18359
|
|
@@ -23,12 +23,12 @@ import { existsSync, cpSync } from "node:fs";
|
|
|
23
23
|
import { logWarning, logError, _resetLogs, drainLogs, drainAndSummarize, formatForNotification, hasAnyIssues, } from "../workflow-logger.js";
|
|
24
24
|
import { gsdRoot } from "../paths.js";
|
|
25
25
|
import { atomicWriteSync } from "../atomic-write.js";
|
|
26
|
-
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps } from "../auto-recovery.js";
|
|
26
|
+
import { verifyExpectedArtifact, diagnoseExpectedArtifact, buildLoopRemediationSteps, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
27
27
|
import { writeUnitRuntimeRecord } from "../unit-runtime.js";
|
|
28
28
|
import { withTimeout, FINALIZE_PRE_TIMEOUT_MS, FINALIZE_POST_TIMEOUT_MS } from "./finalize-timeout.js";
|
|
29
29
|
import { getEligibleSlices } from "../slice-parallel-eligibility.js";
|
|
30
30
|
import { startSliceParallel } from "../slice-parallel-orchestrator.js";
|
|
31
|
-
import { isDbAvailable, getMilestoneSlices
|
|
31
|
+
import { isDbAvailable, getMilestoneSlices } from "../gsd-db.js";
|
|
32
32
|
import { ensurePlanV2Graph, isEmptyPlanV2GraphResult, isMissingFinalizedContextResult } from "../uok/plan-v2.js";
|
|
33
33
|
import { resolveUokFlags } from "../uok/flags.js";
|
|
34
34
|
import { UokGateRunner } from "../uok/gate-runner.js";
|
|
@@ -42,12 +42,10 @@ import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit,
|
|
|
42
42
|
function isSamePathLocal(a, b) {
|
|
43
43
|
return normalizeWorktreePathForCompare(a) === normalizeWorktreePathForCompare(b);
|
|
44
44
|
}
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
return true;
|
|
50
|
-
return refreshOpenDatabaseFromDisk();
|
|
45
|
+
export function shouldDegradeEmptyWorktreeToProjectRoot(worktreeClassification, projectRootClassification) {
|
|
46
|
+
return (worktreeClassification.kind === "greenfield" &&
|
|
47
|
+
projectRootClassification.kind !== "greenfield" &&
|
|
48
|
+
projectRootClassification.kind !== "invalid-repo");
|
|
51
49
|
}
|
|
52
50
|
// ─── Session timeout auto-resume state ────────────────────────────────────────
|
|
53
51
|
let consecutiveSessionTimeouts = 0;
|
|
@@ -136,6 +134,74 @@ async function closeoutAndStop(ctx, pi, s, deps, reason) {
|
|
|
136
134
|
}
|
|
137
135
|
await deps.stopAuto(ctx, pi, reason);
|
|
138
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
|
+
}
|
|
139
205
|
async function emitCancelledUnitEnd(ic, unitType, unitId, unitStartSeq, errorContext) {
|
|
140
206
|
ic.deps.emitJournalEvent({
|
|
141
207
|
ts: new Date().toISOString(),
|
|
@@ -477,28 +543,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
477
543
|
s.unitLifetimeDispatches.clear();
|
|
478
544
|
loopState.recentUnits.length = 0;
|
|
479
545
|
loopState.stuckRecoveryAttempts = 0;
|
|
480
|
-
// Worktree lifecycle on milestone transition — merge current, enter next
|
|
481
|
-
// #2909: preflight
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
catch (mergeErr) {
|
|
487
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
488
|
-
// Real code conflicts — stop the loop instead of retrying forever (#2330)
|
|
489
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
490
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
491
|
-
return { action: "break", reason: "merge-conflict" };
|
|
492
|
-
}
|
|
493
|
-
// Non-conflict merge errors — stop auto to avoid advancing with unmerged work
|
|
494
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
495
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
496
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
497
|
-
return { action: "break", reason: "merge-failed" };
|
|
498
|
-
}
|
|
499
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
500
|
-
if (preflightTransition.stashPushed) {
|
|
501
|
-
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;
|
|
502
552
|
}
|
|
503
553
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
504
554
|
deps.invalidateAllCaches();
|
|
@@ -552,30 +602,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
552
602
|
}
|
|
553
603
|
const incomplete = state.registry.filter((m) => m.status !== "complete" && m.status !== "parked");
|
|
554
604
|
if (incomplete.length === 0 && state.registry.length > 0) {
|
|
555
|
-
// All milestones complete — merge milestone branch before stopping
|
|
605
|
+
// All milestones complete — merge milestone branch before stopping.
|
|
556
606
|
if (s.currentMilestoneId) {
|
|
557
|
-
// #2909: preflight
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
562
|
-
s.milestoneMergedInPhases = true;
|
|
563
|
-
}
|
|
564
|
-
catch (mergeErr) {
|
|
565
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
566
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
567
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
568
|
-
return { action: "break", reason: "merge-conflict" };
|
|
569
|
-
}
|
|
570
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
571
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
572
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
573
|
-
return { action: "break", reason: "merge-failed" };
|
|
574
|
-
}
|
|
575
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
576
|
-
if (preflightAllComplete.stashPushed) {
|
|
577
|
-
deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightAllComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
578
|
-
}
|
|
607
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
608
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
609
|
+
if (stop)
|
|
610
|
+
return stop;
|
|
579
611
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
580
612
|
}
|
|
581
613
|
deps.sendDesktopNotification("GSD", "All milestones complete!", "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
@@ -638,30 +670,12 @@ export async function runPreDispatch(ic, loopState) {
|
|
|
638
670
|
}
|
|
639
671
|
// Terminal: complete
|
|
640
672
|
if (state.phase === "complete") {
|
|
641
|
-
// Milestone merge on complete (before closeout so branch state is clean)
|
|
673
|
+
// Milestone merge on complete (before closeout so branch state is clean).
|
|
642
674
|
if (s.currentMilestoneId) {
|
|
643
|
-
// #2909: preflight
|
|
644
|
-
const
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
// Prevent stopAuto from attempting the same merge (#2645)
|
|
648
|
-
s.milestoneMergedInPhases = true;
|
|
649
|
-
}
|
|
650
|
-
catch (mergeErr) {
|
|
651
|
-
if (mergeErr instanceof MergeConflictError) {
|
|
652
|
-
ctx.ui.notify(`Merge conflict: ${mergeErr.conflictedFiles.join(", ")}. Resolve conflicts manually and run /gsd auto to resume.`, "error");
|
|
653
|
-
await deps.stopAuto(ctx, pi, `Merge conflict on milestone ${s.currentMilestoneId}`);
|
|
654
|
-
return { action: "break", reason: "merge-conflict" };
|
|
655
|
-
}
|
|
656
|
-
logError("engine", "Milestone merge failed with non-conflict error", { milestone: s.currentMilestoneId, error: String(mergeErr) });
|
|
657
|
-
ctx.ui.notify(`Merge failed: ${mergeErr instanceof Error ? mergeErr.message : String(mergeErr)}. Resolve and run /gsd auto to resume.`, "error");
|
|
658
|
-
await deps.stopAuto(ctx, pi, `Merge error on milestone ${s.currentMilestoneId}: ${String(mergeErr)}`);
|
|
659
|
-
return { action: "break", reason: "merge-failed" };
|
|
660
|
-
}
|
|
661
|
-
// #2909: postflight — restore stashed changes after successful merge
|
|
662
|
-
if (preflightComplete.stashPushed) {
|
|
663
|
-
deps.postflightPopStash(s.originalBasePath || s.basePath, s.currentMilestoneId, preflightComplete.stashMarker, ctx.ui.notify.bind(ctx.ui));
|
|
664
|
-
}
|
|
675
|
+
// #2909 / #5538-followup: preflight stash + always-on postflight pop.
|
|
676
|
+
const stop = await _runMilestoneMergeWithStashRestore(ic, s.currentMilestoneId);
|
|
677
|
+
if (stop)
|
|
678
|
+
return stop;
|
|
665
679
|
// PR creation (auto_pr) is handled inside mergeMilestoneToMain (#2302)
|
|
666
680
|
}
|
|
667
681
|
deps.sendDesktopNotification("GSD", `Milestone ${mid} complete!`, "success", "milestone", basename(s.originalBasePath || s.basePath));
|
|
@@ -754,6 +768,48 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
754
768
|
let unitId = dispatchResult.unitId;
|
|
755
769
|
let prompt = dispatchResult.prompt;
|
|
756
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
|
+
}
|
|
757
813
|
// ── Sliding-window stuck detection with graduated recovery ──
|
|
758
814
|
const derivedKey = `${unitType}/${unitId}`;
|
|
759
815
|
// Always record this dispatch in the sliding window so detectStuck() has
|
|
@@ -795,11 +851,18 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
795
851
|
level: 1,
|
|
796
852
|
action: "artifact-found",
|
|
797
853
|
});
|
|
798
|
-
|
|
799
|
-
if (!
|
|
800
|
-
ctx.ui.notify(
|
|
854
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
855
|
+
if (!recoveryDb.ok) {
|
|
856
|
+
ctx.ui.notify(recoveryDb.fatal
|
|
857
|
+
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
858
|
+
: `${recoveryDb.message} Keeping stuck state for retry.`, "warning");
|
|
859
|
+
if (recoveryDb.fatal) {
|
|
860
|
+
await deps.pauseAuto(ctx, pi);
|
|
861
|
+
return { action: "break", reason: recoveryDb.reason };
|
|
862
|
+
}
|
|
801
863
|
return { action: "continue" };
|
|
802
864
|
}
|
|
865
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk. Invalidating caches.`, "info");
|
|
803
866
|
deps.invalidateAllCaches();
|
|
804
867
|
loopState.recentUnits.length = 0;
|
|
805
868
|
loopState.stuckRecoveryAttempts = 0;
|
|
@@ -818,13 +881,20 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
818
881
|
level: 2,
|
|
819
882
|
action: "artifact-found",
|
|
820
883
|
});
|
|
821
|
-
|
|
822
|
-
if (
|
|
884
|
+
const recoveryDb = refreshRecoveryDbForArtifact(unitType, unitId);
|
|
885
|
+
if (recoveryDb.ok) {
|
|
886
|
+
ctx.ui.notify(`Stuck recovery: artifact for ${unitType} ${unitId} found on disk after cache invalidation. Continuing.`, "info");
|
|
823
887
|
loopState.recentUnits.length = 0;
|
|
824
888
|
loopState.stuckRecoveryAttempts = 0;
|
|
825
889
|
return { action: "continue" };
|
|
826
890
|
}
|
|
827
|
-
ctx.ui.notify(
|
|
891
|
+
ctx.ui.notify(recoveryDb.fatal
|
|
892
|
+
? `${recoveryDb.message} Pausing auto-mode for manual recovery.`
|
|
893
|
+
: `${recoveryDb.message} Stopping for manual recovery.`, "warning");
|
|
894
|
+
if (recoveryDb.fatal) {
|
|
895
|
+
await deps.pauseAuto(ctx, pi);
|
|
896
|
+
return { action: "break", reason: recoveryDb.reason };
|
|
897
|
+
}
|
|
828
898
|
}
|
|
829
899
|
debugLog("autoLoop", {
|
|
830
900
|
phase: "stuck-detected",
|
|
@@ -856,32 +926,6 @@ export async function runDispatch(ic, preData, loopState) {
|
|
|
856
926
|
}
|
|
857
927
|
}
|
|
858
928
|
}
|
|
859
|
-
// Pre-dispatch hooks
|
|
860
|
-
const preDispatchResult = deps.runPreDispatchHooks(unitType, unitId, prompt, s.basePath);
|
|
861
|
-
if (preDispatchResult.firedHooks.length > 0) {
|
|
862
|
-
ctx.ui.notify(`Pre-dispatch hook${preDispatchResult.firedHooks.length > 1 ? "s" : ""}: ${preDispatchResult.firedHooks.join(", ")}`, "info");
|
|
863
|
-
deps.emitJournalEvent({ ts: new Date().toISOString(), flowId: ic.flowId, seq: ic.nextSeq(), eventType: "pre-dispatch-hook", data: { firedHooks: preDispatchResult.firedHooks, action: preDispatchResult.action } });
|
|
864
|
-
}
|
|
865
|
-
if (preDispatchResult.action === "skip") {
|
|
866
|
-
ctx.ui.notify(`Skipping ${unitType} ${unitId} (pre-dispatch hook).`, "info");
|
|
867
|
-
await new Promise((r) => setImmediate(r));
|
|
868
|
-
return { action: "continue" };
|
|
869
|
-
}
|
|
870
|
-
if (preDispatchResult.action === "replace") {
|
|
871
|
-
prompt = preDispatchResult.prompt ?? prompt;
|
|
872
|
-
if (preDispatchResult.unitType)
|
|
873
|
-
unitType = preDispatchResult.unitType;
|
|
874
|
-
}
|
|
875
|
-
else if (preDispatchResult.prompt) {
|
|
876
|
-
prompt = preDispatchResult.prompt;
|
|
877
|
-
}
|
|
878
|
-
const guardBasePath = _resolveDispatchGuardBasePath(s);
|
|
879
|
-
const priorSliceBlocker = deps.getPriorSliceCompletionBlocker(guardBasePath, deps.getMainBranch(guardBasePath), unitType, unitId);
|
|
880
|
-
if (priorSliceBlocker) {
|
|
881
|
-
await deps.stopAuto(ctx, pi, priorSliceBlocker);
|
|
882
|
-
debugLog("autoLoop", { phase: "exit", reason: "prior-slice-blocker" });
|
|
883
|
-
return { action: "break", reason: "prior-slice-blocker" };
|
|
884
|
-
}
|
|
885
929
|
return {
|
|
886
930
|
action: "next",
|
|
887
931
|
data: {
|
|
@@ -1111,6 +1155,25 @@ export async function runUnitPhase(ic, iterData, loopState, sidecarItem) {
|
|
|
1111
1155
|
}
|
|
1112
1156
|
}
|
|
1113
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") {
|
|
1114
1177
|
debugLog("runUnitPhase", { phase: "worktree-health-greenfield", basePath: s.basePath, classification: projectClassification });
|
|
1115
1178
|
ctx.ui.notify(`Warning: ${s.basePath} has no project content yet — proceeding as greenfield project`, "warning");
|
|
1116
1179
|
}
|
|
@@ -1672,6 +1735,13 @@ export async function runFinalize(ic, iterData, loopState, sidecarItem) {
|
|
|
1672
1735
|
}
|
|
1673
1736
|
// Both pre and post verification completed without timeout — reset counter
|
|
1674
1737
|
loopState.consecutiveFinalizeTimeouts = 0;
|
|
1738
|
+
if (preUnitSnapshot) {
|
|
1739
|
+
writeUnitRuntimeRecord(s.basePath, preUnitSnapshot.type, preUnitSnapshot.id, preUnitSnapshot.startedAt, {
|
|
1740
|
+
phase: "finalized",
|
|
1741
|
+
lastProgressAt: Date.now(),
|
|
1742
|
+
lastProgressKind: "finalize-success",
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1675
1745
|
s.currentUnit = null;
|
|
1676
1746
|
clearCurrentPhase();
|
|
1677
1747
|
// Surface accumulated workflow-logger issues for this unit to the user.
|
|
@@ -17,6 +17,7 @@ import { bumpTurnGeneration } from "./turn-epoch.js";
|
|
|
17
17
|
// scoped pendingResolve + pendingAgentEndQueue pattern.
|
|
18
18
|
let _currentResolve = null;
|
|
19
19
|
let _sessionSwitchInFlight = false;
|
|
20
|
+
let _pendingSwitchCancellation = null;
|
|
20
21
|
// ─── Setters (needed for cross-module mutation) ─────────────────────────────
|
|
21
22
|
export function _setCurrentResolve(fn) {
|
|
22
23
|
_currentResolve = fn;
|
|
@@ -27,6 +28,11 @@ export function _setSessionSwitchInFlight(v) {
|
|
|
27
28
|
export function _clearCurrentResolve() {
|
|
28
29
|
_currentResolve = null;
|
|
29
30
|
}
|
|
31
|
+
export function _consumePendingSwitchCancellation() {
|
|
32
|
+
const pending = _pendingSwitchCancellation;
|
|
33
|
+
_pendingSwitchCancellation = null;
|
|
34
|
+
return pending;
|
|
35
|
+
}
|
|
30
36
|
// ─── resolveAgentEnd ─────────────────────────────────────────────────────────
|
|
31
37
|
/**
|
|
32
38
|
* Called from the agent_end event handler in index.ts to resolve the
|
|
@@ -90,8 +96,18 @@ export function resolveAgentEndCancelled(errorContext) {
|
|
|
90
96
|
debugLog("resolveAgentEndCancelled", { status: "resolving-cancelled" });
|
|
91
97
|
const r = _currentResolve;
|
|
92
98
|
_currentResolve = null;
|
|
99
|
+
_pendingSwitchCancellation = null;
|
|
93
100
|
r({ status: "cancelled", ...(errorContext ? { errorContext } : {}) });
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (_sessionSwitchInFlight) {
|
|
104
|
+
bumpTurnGeneration(`cancelled-during-switch:${errorContext?.category ?? "unknown"}`);
|
|
105
|
+
_pendingSwitchCancellation = errorContext ? { errorContext } : {};
|
|
106
|
+
debugLog("resolveAgentEndCancelled", { status: "queued-during-switch" });
|
|
107
|
+
return false;
|
|
94
108
|
}
|
|
109
|
+
debugLog("resolveAgentEndCancelled", { status: "no-pending-resolve" });
|
|
110
|
+
return false;
|
|
95
111
|
}
|
|
96
112
|
// ─── resetPendingResolve (test helper) ───────────────────────────────────────
|
|
97
113
|
/**
|
|
@@ -101,6 +117,7 @@ export function resolveAgentEndCancelled(errorContext) {
|
|
|
101
117
|
export function _resetPendingResolve() {
|
|
102
118
|
_currentResolve = null;
|
|
103
119
|
_sessionSwitchInFlight = false;
|
|
120
|
+
_pendingSwitchCancellation = null;
|
|
104
121
|
}
|
|
105
122
|
export function _hasPendingResolveForTest() {
|
|
106
123
|
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, _setCurrentResolve, _setSessionSwitchInFlight } from "./resolve.js";
|
|
3
|
+
import { _clearCurrentResolve, _consumePendingSwitchCancellation, _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,43 +19,21 @@ 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) {
|
|
60
38
|
_setSessionSwitchInFlight(false);
|
|
61
39
|
}
|
|
@@ -71,6 +49,7 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
71
49
|
catch (sessionErr) {
|
|
72
50
|
if (sessionTimeoutHandle)
|
|
73
51
|
clearTimeout(sessionTimeoutHandle);
|
|
52
|
+
_consumePendingSwitchCancellation();
|
|
74
53
|
const msg = sessionErr instanceof Error ? sessionErr.message : String(sessionErr);
|
|
75
54
|
debugLog("runUnit", {
|
|
76
55
|
phase: "session-error",
|
|
@@ -83,15 +62,18 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
83
62
|
if (sessionTimeoutHandle)
|
|
84
63
|
clearTimeout(sessionTimeoutHandle);
|
|
85
64
|
if (sessionResult.cancelled) {
|
|
65
|
+
_consumePendingSwitchCancellation();
|
|
86
66
|
debugLog("runUnit-session-timeout", { unitType, unitId });
|
|
87
67
|
return { status: "cancelled", errorContext: { message: "Session creation timed out", category: "timeout", isTransient: true } };
|
|
88
68
|
}
|
|
89
69
|
if (!s.active) {
|
|
70
|
+
_consumePendingSwitchCancellation();
|
|
90
71
|
return { status: "cancelled" };
|
|
91
72
|
}
|
|
92
73
|
if (s.currentUnitModel && typeof pi.setModel === "function") {
|
|
93
74
|
const restored = await pi.setModel(s.currentUnitModel, { persist: false });
|
|
94
75
|
if (!restored) {
|
|
76
|
+
_consumePendingSwitchCancellation();
|
|
95
77
|
const message = `Failed to restore configured model ${s.currentUnitModel.provider}/${s.currentUnitModel.id} after session creation`;
|
|
96
78
|
ctx.ui.notify(`${message}. Cancelling unit before dispatch.`, "warning");
|
|
97
79
|
return {
|
|
@@ -111,6 +93,14 @@ export async function runUnit(ctx, pi, s, unitType, unitId, prompt) {
|
|
|
111
93
|
const unitPromise = new Promise((resolve) => {
|
|
112
94
|
_setCurrentResolve(resolve);
|
|
113
95
|
});
|
|
96
|
+
const pendingSwitchCancellation = _consumePendingSwitchCancellation();
|
|
97
|
+
if (pendingSwitchCancellation) {
|
|
98
|
+
_clearCurrentResolve();
|
|
99
|
+
return {
|
|
100
|
+
status: "cancelled",
|
|
101
|
+
...(pendingSwitchCancellation.errorContext ? { errorContext: pendingSwitchCancellation.errorContext } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
114
104
|
// ── Provider request-readiness pre-check (#4555) ──
|
|
115
105
|
// Verify the provider can accept requests before dispatching. If the token
|
|
116
106
|
// has expired since bootstrap, return cancelled immediately so the unit is
|
|
@@ -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;
|
|
@@ -11,7 +11,6 @@ import { buildResearchSlicePrompt, buildResearchMilestonePrompt, buildPlanSliceP
|
|
|
11
11
|
import { loadEffectiveGSDPreferences } from "./preferences.js";
|
|
12
12
|
import { pauseAuto } from "./auto.js";
|
|
13
13
|
import { resolveCanonicalMilestoneRoot } from "./worktree-manager.js";
|
|
14
|
-
import { logWarning } from "./workflow-logger.js";
|
|
15
14
|
import { getWorkflowTransportSupportError, getRequiredWorkflowToolsForAutoUnit, } from "./workflow-mcp.js";
|
|
16
15
|
export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
17
16
|
const state = await deriveState(base);
|
|
@@ -231,36 +230,10 @@ export async function dispatchDirectPhase(ctx, pi, phase, base) {
|
|
|
231
230
|
return;
|
|
232
231
|
}
|
|
233
232
|
ctx.ui.notify(`Dispatching ${unitType} for ${unitId}...`, "info");
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
if (process.cwd() !== dispatchBase) {
|
|
240
|
-
process.chdir(dispatchBase);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
catch (err) {
|
|
244
|
-
const msg = `Failed to chdir before direct-dispatch newSession (basePath: ${dispatchBase}): ${err instanceof Error ? err.message : String(err)}`;
|
|
245
|
-
logWarning("engine", msg, { file: "auto-direct-dispatch.ts", basePath: dispatchBase, error: err instanceof Error ? err.message : String(err) });
|
|
246
|
-
ctx.ui.notify(`${msg}. Cancelling dispatch to avoid running in the wrong directory.`, "error");
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
const result = await ctx.newSession();
|
|
250
|
-
if (result.cancelled) {
|
|
251
|
-
ctx.ui.notify("Session creation cancelled.", "warning");
|
|
252
|
-
return;
|
|
253
|
-
}
|
|
254
|
-
pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
|
|
255
|
-
}
|
|
256
|
-
finally {
|
|
257
|
-
try {
|
|
258
|
-
if (process.cwd() !== originalCwd) {
|
|
259
|
-
process.chdir(originalCwd);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
catch (err) {
|
|
263
|
-
logWarning("engine", `Failed to restore cwd after direct dispatch: ${err instanceof Error ? err.message : String(err)}`, { file: "auto-direct-dispatch.ts", basePath: originalCwd });
|
|
264
|
-
}
|
|
233
|
+
const result = await ctx.newSession({ workspaceRoot: dispatchBase });
|
|
234
|
+
if (result.cancelled) {
|
|
235
|
+
ctx.ui.notify("Session creation cancelled.", "warning");
|
|
236
|
+
return;
|
|
265
237
|
}
|
|
238
|
+
pi.sendMessage({ customType: "gsd-dispatch", content: prompt, display: false }, { triggerTurn: true });
|
|
266
239
|
}
|
|
@@ -791,6 +791,22 @@ export const DISPATCH_RULES = [
|
|
|
791
791
|
const unitId = `${mid}/${sid}`;
|
|
792
792
|
let priorPreExecFailure;
|
|
793
793
|
if (session?.lastPreExecFailure?.unitId === unitId) {
|
|
794
|
+
// Circuit breaker: stop re-dispatching after 2 failed retries. The
|
|
795
|
+
// planner has had multiple attempts with injected failure context and
|
|
796
|
+
// still cannot produce a valid plan — human review is required.
|
|
797
|
+
const MAX_PRE_EXEC_RETRIES = 2;
|
|
798
|
+
const retryCount = session.preExecRetryCount?.get(unitId) ?? 0;
|
|
799
|
+
if (retryCount >= MAX_PRE_EXEC_RETRIES) {
|
|
800
|
+
const findings = session.lastPreExecFailure.blockingFindings.join("; ");
|
|
801
|
+
session.lastPreExecFailure = null;
|
|
802
|
+
session.preExecRetryCount?.delete(unitId);
|
|
803
|
+
return {
|
|
804
|
+
action: "stop",
|
|
805
|
+
reason: `Pre-execution checks failed ${retryCount} times for ${unitId} — manual intervention required. Blocking findings: ${findings}. Fix the plan manually, then run /gsd auto to resume.`,
|
|
806
|
+
level: "error",
|
|
807
|
+
matchedRule: "planning → plan-slice",
|
|
808
|
+
};
|
|
809
|
+
}
|
|
794
810
|
priorPreExecFailure = {
|
|
795
811
|
blockingFindings: session.lastPreExecFailure.blockingFindings,
|
|
796
812
|
verdictExcerpt: session.lastPreExecFailure.verdictExcerpt,
|