@opengsd/gsd-pi 1.1.1-dev.b2556262 → 1.2.0-dev.844675c9
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/project-sessions.js +4 -2
- package/dist/resources/.managed-resources-content-hash +1 -1
- package/dist/resources/extensions/claude-code-cli/stream-adapter.js +17 -9
- package/dist/resources/extensions/gsd/auto/contracts.js +8 -1
- package/dist/resources/extensions/gsd/auto/orchestrator.js +659 -57
- package/dist/resources/extensions/gsd/auto-prompts.js +110 -1
- package/dist/resources/extensions/gsd/auto-runtime-state.js +3 -0
- package/dist/resources/extensions/gsd/auto-tool-tracking.js +5 -0
- package/dist/resources/extensions/gsd/auto-unit-tool-scope.js +29 -0
- package/dist/resources/extensions/gsd/auto-worktree.js +24 -17
- package/dist/resources/extensions/gsd/auto.js +62 -464
- package/dist/resources/extensions/gsd/bootstrap/register-hooks.js +4 -1
- package/dist/resources/extensions/gsd/debug-logger.js +10 -0
- package/dist/resources/extensions/gsd/doctor-proactive.js +7 -2
- package/dist/resources/extensions/gsd/guided-flow.js +2 -2
- package/dist/resources/extensions/gsd/markdown-renderer.js +31 -32
- package/dist/resources/extensions/gsd/mcp-filter.js +6 -0
- package/dist/resources/extensions/gsd/native-git-bridge.js +45 -0
- package/dist/resources/extensions/gsd/prompts/discuss.md +6 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
- package/dist/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
- package/dist/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
- package/dist/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
- package/dist/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/dist/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/dist/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
- package/dist/resources/extensions/gsd/schemas/parsers.js +6 -1
- package/dist/resources/extensions/gsd/state-reconciliation/drift/artifact-db.js +21 -1
- package/dist/resources/extensions/gsd/tools/workflow-tool-executors.js +169 -20
- package/dist/resources/extensions/gsd/user-input-boundary.js +42 -4
- package/dist/tsconfig.extensions.tsbuildinfo +1 -0
- package/dist/web/standalone/.next/BUILD_ID +1 -1
- package/dist/web/standalone/.next/app-path-routes-manifest.json +8 -8
- 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/api/boot/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/input/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/resize/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/bridge-terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/captures/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/cleanup/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/doctor/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/export-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/files/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/forensics/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js +1 -1
- package/dist/web/standalone/.next/server/app/api/git/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/history/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/hooks/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/inspect/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/knowledge/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/live-state/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/mcp-connections/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/notifications/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/onboarding/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/projects/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/recovery/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/browser/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/command/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/events/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/session/manage/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/settings-data/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/shutdown/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/skill-health/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/steer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/switch-root/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/sessions/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/terminal/stream/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/undo/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/api/visualizer/route.js.nft.json +1 -1
- package/dist/web/standalone/.next/server/app/index.html +1 -1
- package/dist/web/standalone/.next/server/app/index.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/dist/web/standalone/.next/server/app-paths-manifest.json +8 -8
- package/dist/web/standalone/.next/server/chunks/5047.js +2 -0
- package/dist/web/standalone/.next/server/chunks/5124.js +1 -0
- package/dist/web/standalone/.next/server/chunks/8357.js +2 -2
- 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/web/standalone/node_modules/@gsd/native/package.json +1 -1
- package/dist/web/standalone/node_modules/node-pty/build/Makefile +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/container.js +26 -18
- package/dist/web/standalone/node_modules/postcss/lib/css-syntax-error.js +47 -14
- package/dist/web/standalone/node_modules/postcss/lib/declaration.js +4 -4
- package/dist/web/standalone/node_modules/postcss/lib/fromJSON.js +3 -3
- package/dist/web/standalone/node_modules/postcss/lib/input.js +54 -29
- package/dist/web/standalone/node_modules/postcss/lib/lazy-result.js +47 -37
- package/dist/web/standalone/node_modules/postcss/lib/map-generator.js +26 -9
- package/dist/web/standalone/node_modules/postcss/lib/no-work-result.js +57 -55
- package/dist/web/standalone/node_modules/postcss/lib/node.js +99 -31
- package/dist/web/standalone/node_modules/postcss/lib/parse.js +1 -1
- package/dist/web/standalone/node_modules/postcss/lib/parser.js +10 -9
- package/dist/web/standalone/node_modules/postcss/lib/postcss.js +12 -12
- package/dist/web/standalone/node_modules/postcss/lib/previous-map.js +30 -11
- package/dist/web/standalone/node_modules/postcss/lib/processor.js +7 -7
- package/dist/web/standalone/node_modules/postcss/lib/result.js +5 -5
- package/dist/web/standalone/node_modules/postcss/lib/rule.js +6 -6
- package/dist/web/standalone/node_modules/postcss/lib/stringifier.js +69 -28
- package/dist/web/standalone/node_modules/postcss/lib/tokenize.js +6 -2
- package/dist/web/standalone/node_modules/postcss/package.json +48 -48
- package/package.json +16 -11
- package/packages/cloud-mcp-gateway/package.json +2 -2
- package/packages/contracts/package.json +1 -1
- package/packages/daemon/package.json +4 -4
- package/packages/gsd-agent-core/package.json +5 -5
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js +1 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/transcript-design.js.map +1 -1
- package/packages/gsd-agent-modes/package.json +7 -7
- package/packages/mcp-server/package.json +3 -3
- package/packages/native/package.json +1 -1
- package/packages/pi-agent-core/package.json +1 -1
- package/packages/pi-ai/dist/models.generated.d.ts +0 -34
- package/packages/pi-ai/dist/models.generated.d.ts.map +1 -1
- package/packages/pi-ai/dist/models.generated.js +12 -46
- package/packages/pi-ai/dist/models.generated.js.map +1 -1
- package/packages/pi-ai/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/auth-storage.js +11 -3
- package/packages/pi-coding-agent/dist/core/auth-storage.js.map +1 -1
- package/packages/pi-coding-agent/package.json +7 -7
- package/packages/pi-tui/package.json +2 -2
- package/packages/rpc-client/package.json +2 -2
- package/pkg/package.json +1 -1
- package/scripts/install/deps.js +10 -0
- package/scripts/link-workspace-packages.cjs +7 -40
- package/src/resources/extensions/claude-code-cli/stream-adapter.ts +18 -8
- package/src/resources/extensions/claude-code-cli/tests/stream-adapter.test.ts +2 -2
- package/src/resources/extensions/gsd/auto/contracts.ts +8 -119
- package/src/resources/extensions/gsd/auto/orchestrator.ts +794 -58
- package/src/resources/extensions/gsd/auto-prompts.ts +114 -1
- package/src/resources/extensions/gsd/auto-runtime-state.ts +4 -0
- package/src/resources/extensions/gsd/auto-tool-tracking.ts +5 -0
- package/src/resources/extensions/gsd/auto-unit-tool-scope.ts +33 -0
- package/src/resources/extensions/gsd/auto-worktree.ts +24 -16
- package/src/resources/extensions/gsd/auto.ts +81 -500
- package/src/resources/extensions/gsd/bootstrap/register-hooks.ts +4 -0
- package/src/resources/extensions/gsd/debug-logger.ts +11 -0
- package/src/resources/extensions/gsd/doctor-proactive.ts +8 -2
- package/src/resources/extensions/gsd/guided-flow.ts +2 -2
- package/src/resources/extensions/gsd/markdown-renderer.ts +38 -19
- package/src/resources/extensions/gsd/mcp-filter.ts +7 -0
- package/src/resources/extensions/gsd/native-git-bridge.ts +48 -0
- package/src/resources/extensions/gsd/prompts/discuss.md +6 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-milestone.md +5 -7
- package/src/resources/extensions/gsd/prompts/guided-discuss-project.md +3 -5
- package/src/resources/extensions/gsd/prompts/guided-discuss-requirements.md +1 -2
- package/src/resources/extensions/gsd/prompts/guided-discuss-slice.md +5 -6
- package/src/resources/extensions/gsd/prompts/plan-slice.md +1 -1
- package/src/resources/extensions/gsd/prompts/research-milestone.md +2 -2
- package/src/resources/extensions/gsd/prompts/validate-milestone.md +5 -3
- package/src/resources/extensions/gsd/schemas/parsers.ts +6 -1
- package/src/resources/extensions/gsd/state-reconciliation/drift/artifact-db.ts +31 -10
- package/src/resources/extensions/gsd/tests/artifact-db-drift-memo.test.ts +66 -0
- package/src/resources/extensions/gsd/tests/auto-dispatch-baseline-harness.test.ts +53 -0
- package/src/resources/extensions/gsd/tests/auto-orchestrator.test.ts +590 -855
- package/src/resources/extensions/gsd/tests/auto-paused-ui-cleanup.test.ts +38 -10
- package/src/resources/extensions/gsd/tests/debug-logger.test.ts +15 -0
- package/src/resources/extensions/gsd/tests/execute-summary-save-empty-project.test.ts +64 -1
- package/src/resources/extensions/gsd/tests/integration/merge-strategy-regular.test.ts +157 -0
- package/src/resources/extensions/gsd/tests/markdown-renderer-parse-cache.test.ts +75 -0
- package/src/resources/extensions/gsd/tests/native-merge-regular.test.ts +139 -0
- package/src/resources/extensions/gsd/tests/orchestrator-legacy-parity.test.ts +127 -0
- package/src/resources/extensions/gsd/tests/parse-project-milestone-bridge.test.ts +77 -0
- package/src/resources/extensions/gsd/tests/plan-slice-prompt.test.ts +4 -2
- package/src/resources/extensions/gsd/tests/prompt-contracts.test.ts +29 -2
- package/src/resources/extensions/gsd/tests/research-milestone-composer.test.ts +65 -0
- package/src/resources/extensions/gsd/tests/start-auto-detached.test.ts +19 -5
- package/src/resources/extensions/gsd/tests/token-tool-gating.test.ts +38 -0
- package/src/resources/extensions/gsd/tests/user-input-boundary.test.ts +62 -0
- package/src/resources/extensions/gsd/tests/worktree-safety.test.ts +24 -0
- package/src/resources/extensions/gsd/tests/write-gate-planning-unit.test.ts +15 -3
- package/src/resources/extensions/gsd/tools/workflow-tool-executors.ts +183 -21
- package/src/resources/extensions/gsd/user-input-boundary.ts +37 -5
- package/dist/web/standalone/.next/server/chunks/678.js +0 -2
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts +0 -21
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js +0 -213
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/gsd-widget-prototype.js.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts +0 -28
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js +0 -249
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-density-prototype.js.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts +0 -19
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.d.ts.map +0 -1
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js +0 -797
- package/packages/gsd-agent-modes/dist/modes/interactive/components/__prototype__/transcript-design-prototype.js.map +0 -1
- package/scripts/ensure-workspace-builds.cjs +0 -129
- /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → Qbr81pQ-pbQXP4bq2VXLv}/_buildManifest.js +0 -0
- /package/dist/web/standalone/.next/static/{tJOKQbQRO-9MiFDO8DIDS → Qbr81pQ-pbQXP4bq2VXLv}/_ssgManifest.js +0 -0
|
@@ -1,8 +1,61 @@
|
|
|
1
1
|
// Project/App: gsd-pi
|
|
2
2
|
// File Purpose: Auto Orchestration module implementation and ADR-015 invariant pipeline owner.
|
|
3
|
+
//
|
|
4
|
+
// Phase 2 of #442 collapsed the nine single-implementation adapter seams
|
|
5
|
+
// (DispatchAdapter, RecoveryAdapter, StateReconciliationAdapter,
|
|
6
|
+
// ToolContractAdapter, WorktreeAdapter, HealthAdapter, UokGateAdapter,
|
|
7
|
+
// RuntimePersistenceAdapter, NotificationAdapter) into this class. The
|
|
8
|
+
// orchestrator now constructs from the concrete extension context and calls
|
|
9
|
+
// the real collaborators (state-reconciliation, doctor-proactive,
|
|
10
|
+
// auto-dispatch, recovery-classification, tool-contract, worktree-safety,
|
|
11
|
+
// uok/gate-runner, journal, session-lock, ctx.ui.notify) directly.
|
|
3
12
|
|
|
4
|
-
import type {
|
|
13
|
+
import type { ExtensionAPI, ExtensionContext } from "@gsd/pi-coding-agent";
|
|
14
|
+
|
|
15
|
+
import type { AutoAdvanceResult, AutoOrchestrationModule, AutoSessionContext, AutoStatus } from "./contracts.js";
|
|
16
|
+
import type { AutoSession, PendingOrchestrationDispatch } from "./session.js";
|
|
5
17
|
import type { GSDState } from "../types.js";
|
|
18
|
+
import type { MinimalModelRegistry } from "../context-budget.js";
|
|
19
|
+
|
|
20
|
+
import { debugCount, debugTime } from "../debug-logger.js";
|
|
21
|
+
import { reconcileBeforeDispatch } from "../state-reconciliation.js";
|
|
22
|
+
import { resolveDispatch } from "../auto-dispatch.js";
|
|
23
|
+
import { classifyFailure } from "../recovery-classification.js";
|
|
24
|
+
import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
25
|
+
import { invalidateAllCaches } from "../cache.js";
|
|
26
|
+
import { compileUnitToolContract } from "../tool-contract.js";
|
|
27
|
+
import { createWorktreeSafetyModule } from "../worktree-safety.js";
|
|
28
|
+
import { repairAutoWorktreeSafetyFailure } from "../auto-worktree-repair.js";
|
|
29
|
+
import { resolveManifest } from "../unit-context-manifest.js";
|
|
30
|
+
import {
|
|
31
|
+
preDispatchHealthGate,
|
|
32
|
+
recordHealthSnapshot,
|
|
33
|
+
} from "../doctor-proactive.js";
|
|
34
|
+
import { checkResourcesStale, autoWorktreeBranch, mergeMilestoneToMain } from "../auto-worktree.js";
|
|
35
|
+
import { getSessionLockStatus } from "../session-lock.js";
|
|
36
|
+
import { resolveUokFlags } from "../uok/flags.js";
|
|
37
|
+
import { emitJournalEvent as _emitJournalEvent } from "../journal.js";
|
|
38
|
+
import { loadEffectiveGSDPreferences, getIsolationMode } from "../preferences.js";
|
|
39
|
+
import { detectWorktreeName, resolveProjectRoot } from "../worktree.js";
|
|
40
|
+
import { GitServiceImpl } from "../git-service.js";
|
|
41
|
+
import { WorktreeStateProjection } from "../worktree-state-projection.js";
|
|
42
|
+
import { WorktreeLifecycle } from "../worktree-lifecycle.js";
|
|
43
|
+
import { createWorkspace, scopeMilestone } from "../workspace.js";
|
|
44
|
+
import { supportsStructuredQuestions } from "../workflow-mcp.js";
|
|
45
|
+
import { getToolBaselineSnapshot } from "../auto-model-selection.js";
|
|
46
|
+
import { deriveState } from "../state.js";
|
|
47
|
+
import { parseUnitId } from "../unit-id.js";
|
|
48
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
49
|
+
import {
|
|
50
|
+
isDbAvailable,
|
|
51
|
+
getSlice,
|
|
52
|
+
getTask,
|
|
53
|
+
refreshOpenDatabaseFromDisk,
|
|
54
|
+
} from "../gsd-db.js";
|
|
55
|
+
import { getErrorMessage } from "../error-utils.js";
|
|
56
|
+
import { logWarning } from "../workflow-logger.js";
|
|
57
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
58
|
+
import { join } from "node:path";
|
|
6
59
|
|
|
7
60
|
function now(): number {
|
|
8
61
|
return Date.now();
|
|
@@ -25,38 +78,662 @@ function noRemainingUnitsReason(stateSnapshot: GSDState): string {
|
|
|
25
78
|
return "no remaining units";
|
|
26
79
|
}
|
|
27
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Concrete construction context for the Auto Orchestrator.
|
|
83
|
+
*
|
|
84
|
+
* Phase 2 of #442 replaced the nine adapter interfaces with this bundle of the
|
|
85
|
+
* real values the wiring factory used to close over: the extension context and
|
|
86
|
+
* API, the dispatch/runtime base paths, and the shared {@link AutoSession}
|
|
87
|
+
* singleton.
|
|
88
|
+
*/
|
|
89
|
+
export interface OrchestratorContext {
|
|
90
|
+
ctx: ExtensionContext;
|
|
91
|
+
pi: ExtensionAPI;
|
|
92
|
+
dispatchBasePath: string;
|
|
93
|
+
runtimeBasePath: string;
|
|
94
|
+
session: AutoSession;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Result type of a single dispatch decision. */
|
|
98
|
+
export type DispatchDecision =
|
|
99
|
+
| { kind: "blocked"; reason: string; action: "pause" | "stop" }
|
|
100
|
+
| { kind: "skipped"; reason: string }
|
|
101
|
+
| { unitType: string; unitId: string; reason: string; preconditions: string[] }
|
|
102
|
+
| null;
|
|
103
|
+
|
|
104
|
+
/** Inputs to a dispatch decision. Caller-supplied fields override ctx-derived ones. */
|
|
105
|
+
export interface DispatchDecisionInput {
|
|
106
|
+
stateSnapshot: GSDState;
|
|
107
|
+
/** Optional live session context, forwarded to dispatch rules that need session-derived state. */
|
|
108
|
+
session?: AutoSession;
|
|
109
|
+
/** Mirrors `DispatchContext.structuredQuestionsAvailable` — "true"/"false" string per the dispatch contract. */
|
|
110
|
+
structuredQuestionsAvailable?: "true" | "false";
|
|
111
|
+
/** Session model context window in tokens, forwarded to the budget engine. */
|
|
112
|
+
sessionContextWindow?: number;
|
|
113
|
+
/** Session model provider, used for provider-specific effective context windows. */
|
|
114
|
+
sessionProvider?: string;
|
|
115
|
+
/** Model registry for executor-model lookups inside the budget engine. */
|
|
116
|
+
modelRegistry?: MinimalModelRegistry;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getAlreadyClosedDispatchReason(unitType: string, unitId: string): string | null {
|
|
120
|
+
if (!isDbAvailable()) return null;
|
|
121
|
+
refreshOpenDatabaseFromDisk();
|
|
122
|
+
const { milestone, slice, task } = parseUnitId(unitId);
|
|
123
|
+
if (unitType === "execute-task" && milestone && slice && task) {
|
|
124
|
+
const row = getTask(milestone, slice, task);
|
|
125
|
+
return row && isClosedStatus(row.status)
|
|
126
|
+
? `execute-task ${unitId} is already ${row.status}`
|
|
127
|
+
: null;
|
|
128
|
+
}
|
|
129
|
+
if (unitType === "complete-slice" && milestone && slice) {
|
|
130
|
+
const row = getSlice(milestone, slice);
|
|
131
|
+
return row && isClosedStatus(row.status)
|
|
132
|
+
? `complete-slice ${unitId} is already ${row.status}`
|
|
133
|
+
: null;
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function shouldAdoptActiveMilestone(
|
|
139
|
+
state: GSDState,
|
|
140
|
+
activeSession: AutoSession | undefined,
|
|
141
|
+
activeDispatchBasePath: string,
|
|
142
|
+
): boolean {
|
|
143
|
+
const activeMilestoneId = state.activeMilestone?.id;
|
|
144
|
+
const currentMilestoneId = activeSession?.currentMilestoneId;
|
|
145
|
+
if (!activeSession || !activeMilestoneId || !currentMilestoneId || activeMilestoneId === currentMilestoneId) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const scopedWorktreeMilestone =
|
|
150
|
+
(activeSession.basePath ? detectWorktreeName(activeSession.basePath) : null) ??
|
|
151
|
+
detectWorktreeName(activeDispatchBasePath);
|
|
152
|
+
if (scopedWorktreeMilestone && scopedWorktreeMilestone !== activeMilestoneId) {
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const currentMilestone = state.registry.find((milestone) => milestone.id === currentMilestoneId);
|
|
157
|
+
return !!currentMilestone && isClosedStatus(currentMilestone.status);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Pure dispatch-decision function — formerly `createWiredDispatchAdapter`'s
|
|
162
|
+
* `decideNextUnit`. Folded out of the closure so the orchestrator can call it
|
|
163
|
+
* directly and tests can drive the exact dispatch decision logic against real
|
|
164
|
+
* fixtures without re-introducing an adapter seam.
|
|
165
|
+
*
|
|
166
|
+
* Derives session-derived dispatch inputs the same way phases.ts:runDispatch
|
|
167
|
+
* does (#5789): prefers caller-supplied values when present so test harnesses
|
|
168
|
+
* and alternative wirings can inject deterministic snapshots; otherwise pulls
|
|
169
|
+
* from the captured pi/ctx references.
|
|
170
|
+
*/
|
|
171
|
+
export async function decideOrchestratorDispatch(
|
|
172
|
+
ctx: ExtensionContext,
|
|
173
|
+
pi: ExtensionAPI,
|
|
174
|
+
dispatchBasePath: string,
|
|
175
|
+
session: AutoSession | undefined,
|
|
176
|
+
input: DispatchDecisionInput,
|
|
177
|
+
): Promise<DispatchDecision> {
|
|
178
|
+
const state = input.stateSnapshot;
|
|
179
|
+
const active = state.activeMilestone;
|
|
180
|
+
if (!active) return null;
|
|
181
|
+
|
|
182
|
+
const activeSession = input.session ?? session;
|
|
183
|
+
const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
|
|
184
|
+
if (activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
|
|
185
|
+
activeSession.currentMilestoneId = active.id;
|
|
186
|
+
}
|
|
187
|
+
const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
|
|
188
|
+
|
|
189
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
190
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
191
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
192
|
+
// the captured pi/ctx references.
|
|
193
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
194
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
195
|
+
const modelRegistry = input.modelRegistry ?? (ctx.modelRegistry as MinimalModelRegistry | undefined);
|
|
196
|
+
const authMode =
|
|
197
|
+
sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
198
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
199
|
+
: undefined;
|
|
200
|
+
// Use baseline snapshot — same reason as phases.ts:runDispatch: the live
|
|
201
|
+
// active set may be narrowed by the prior unit before selectAndApplyModel
|
|
202
|
+
// restores it, causing false transport-preflight failures (#477 follow-up).
|
|
203
|
+
const activeTools = getToolBaselineSnapshot(pi);
|
|
204
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
205
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
206
|
+
// some transports.
|
|
207
|
+
const structuredQuestionsAvailable =
|
|
208
|
+
input.structuredQuestionsAvailable ??
|
|
209
|
+
(prefs?.planning_depth === "deep"
|
|
210
|
+
? "false"
|
|
211
|
+
: supportsStructuredQuestions(activeTools, {
|
|
212
|
+
authMode,
|
|
213
|
+
baseUrl: ctx.model?.baseUrl,
|
|
214
|
+
})
|
|
215
|
+
? "true"
|
|
216
|
+
: "false");
|
|
217
|
+
|
|
218
|
+
const pendingRetry = session?.pendingVerificationRetryDispatch;
|
|
219
|
+
if (session && pendingRetry) {
|
|
220
|
+
session.pendingVerificationRetryDispatch = null;
|
|
221
|
+
const alreadyClosedReason = getAlreadyClosedDispatchReason(
|
|
222
|
+
pendingRetry.unitType,
|
|
223
|
+
pendingRetry.unitId,
|
|
224
|
+
);
|
|
225
|
+
if (alreadyClosedReason) {
|
|
226
|
+
session.pendingOrchestrationDispatch = null;
|
|
227
|
+
session.pendingVerificationRetry = null;
|
|
228
|
+
return { kind: "skipped", reason: alreadyClosedReason };
|
|
229
|
+
}
|
|
230
|
+
session.pendingOrchestrationDispatch = pendingRetry;
|
|
231
|
+
return {
|
|
232
|
+
unitType: pendingRetry.unitType,
|
|
233
|
+
unitId: pendingRetry.unitId,
|
|
234
|
+
reason: "verification-retry",
|
|
235
|
+
preconditions: [],
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const action = await resolveDispatch({
|
|
240
|
+
basePath: activeDispatchBasePath,
|
|
241
|
+
mid: active.id,
|
|
242
|
+
midTitle: active.title,
|
|
243
|
+
state,
|
|
244
|
+
prefs,
|
|
245
|
+
session: activeSession,
|
|
246
|
+
structuredQuestionsAvailable,
|
|
247
|
+
sessionContextWindow,
|
|
248
|
+
sessionProvider,
|
|
249
|
+
modelRegistry,
|
|
250
|
+
activeTools,
|
|
251
|
+
sessionAuthMode: authMode,
|
|
252
|
+
sessionBaseUrl: ctx.model?.baseUrl,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
if (action.action === "stop") {
|
|
256
|
+
if (session) session.pendingOrchestrationDispatch = null;
|
|
257
|
+
return {
|
|
258
|
+
kind: "blocked",
|
|
259
|
+
reason: action.reason,
|
|
260
|
+
action: action.level === "warning" ? "pause" : "stop",
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
if (action.action !== "dispatch") {
|
|
264
|
+
if (session) session.pendingOrchestrationDispatch = null;
|
|
265
|
+
return {
|
|
266
|
+
kind: "skipped",
|
|
267
|
+
reason: action.matchedRule ?? "dispatch-skip",
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
const alreadyClosedReason = getAlreadyClosedDispatchReason(action.unitType, action.unitId);
|
|
271
|
+
if (alreadyClosedReason) {
|
|
272
|
+
if (session) {
|
|
273
|
+
session.pendingOrchestrationDispatch = null;
|
|
274
|
+
session.pendingVerificationRetry = null;
|
|
275
|
+
}
|
|
276
|
+
return { kind: "skipped", reason: alreadyClosedReason };
|
|
277
|
+
}
|
|
278
|
+
if (session) {
|
|
279
|
+
const pending: PendingOrchestrationDispatch = {
|
|
280
|
+
unitType: action.unitType,
|
|
281
|
+
unitId: action.unitId,
|
|
282
|
+
prompt: action.prompt,
|
|
283
|
+
pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
|
|
284
|
+
state,
|
|
285
|
+
mid: active.id,
|
|
286
|
+
midTitle: active.title,
|
|
287
|
+
};
|
|
288
|
+
session.pendingOrchestrationDispatch = pending;
|
|
289
|
+
}
|
|
290
|
+
return {
|
|
291
|
+
unitType: action.unitType,
|
|
292
|
+
unitId: action.unitId,
|
|
293
|
+
reason: action.matchedRule ?? "dispatch",
|
|
294
|
+
preconditions: [],
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
28
298
|
export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
29
299
|
private status: AutoStatus = {
|
|
30
300
|
phase: "idle",
|
|
31
301
|
transitionCount: 0,
|
|
32
302
|
};
|
|
33
|
-
private readonly
|
|
303
|
+
private readonly ctx: ExtensionContext;
|
|
304
|
+
private readonly pi: ExtensionAPI;
|
|
305
|
+
private readonly dispatchBasePath: string;
|
|
306
|
+
private readonly runtimeBasePath: string;
|
|
307
|
+
private readonly s: AutoSession;
|
|
308
|
+
private readonly flowId: string;
|
|
309
|
+
private seq = 0;
|
|
34
310
|
private lastAdvanceKey: string | null = null;
|
|
35
311
|
private lastFinalizedUnitKey: string | null = null;
|
|
36
312
|
private dispatchKeyWindow: string[] = [];
|
|
313
|
+
// #442: the unit key we last attempted graduated stuck-recovery for. Bounds
|
|
314
|
+
// recovery to one attempt per stuck episode per run (reset on start/resume/
|
|
315
|
+
// stop), mirroring the legacy Level-1-then-Level-2 escalation in phases.ts.
|
|
316
|
+
private lastStuckRecoveryKey: string | null = null;
|
|
317
|
+
|
|
318
|
+
public constructor(context: OrchestratorContext) {
|
|
319
|
+
this.ctx = context.ctx;
|
|
320
|
+
this.pi = context.pi;
|
|
321
|
+
this.dispatchBasePath = context.dispatchBasePath;
|
|
322
|
+
this.runtimeBasePath = context.runtimeBasePath;
|
|
323
|
+
this.s = context.session;
|
|
324
|
+
this.flowId = `auto-orchestrator-${Date.now()}`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ── Live base-path resolution (was the wiring factory's getLiveDispatchBasePath) ──
|
|
328
|
+
|
|
329
|
+
private getLiveDispatchBasePath(): string {
|
|
330
|
+
return resolveLiveOrchestratorBasePath({
|
|
331
|
+
capturedBasePath: this.dispatchBasePath,
|
|
332
|
+
runtimeBasePath: this.runtimeBasePath,
|
|
333
|
+
sessionBasePath: this.s.basePath,
|
|
334
|
+
originalBasePath: this.s.originalBasePath,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── RuntimePersistenceAdapter (folded) ───────────────────────────────────
|
|
339
|
+
|
|
340
|
+
private ensureLockOwnership(): void {
|
|
341
|
+
const status = getSessionLockStatus(this.runtimeBasePath);
|
|
342
|
+
if (!status.valid || status.failureReason === "pid-mismatch") {
|
|
343
|
+
throw new Error("session lock held by another process");
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Map an orchestrator lifecycle event name to its journal eventType and emit
|
|
349
|
+
* it. The name→eventType ternary is preserved byte-for-byte from the legacy
|
|
350
|
+
* wired RuntimePersistenceAdapter.journalTransition.
|
|
351
|
+
*/
|
|
352
|
+
private journalTransition(event: {
|
|
353
|
+
name: string;
|
|
354
|
+
reason?: string;
|
|
355
|
+
unitType?: string;
|
|
356
|
+
unitId?: string;
|
|
357
|
+
}): void {
|
|
358
|
+
const eventType = event.name === "start"
|
|
359
|
+
? "orchestrator-iteration-start"
|
|
360
|
+
: event.name === "resume"
|
|
361
|
+
? "orchestrator-iteration-start"
|
|
362
|
+
: event.name === "advance"
|
|
363
|
+
? "orchestrator-dispatch-match"
|
|
364
|
+
: event.name === "advance-blocked"
|
|
365
|
+
? "orchestrator-guard-block"
|
|
366
|
+
: event.name === "advance-stopped"
|
|
367
|
+
? "orchestrator-dispatch-stop"
|
|
368
|
+
: event.name === "advance-error"
|
|
369
|
+
? "orchestrator-iteration-end"
|
|
370
|
+
: event.name === "advance-paused" || event.name === "advance-retry"
|
|
371
|
+
? "orchestrator-guard-block"
|
|
372
|
+
: event.name === "stop"
|
|
373
|
+
? "orchestrator-terminal"
|
|
374
|
+
: "orchestrator-iteration-end";
|
|
375
|
+
|
|
376
|
+
_emitJournalEvent(this.runtimeBasePath, {
|
|
377
|
+
ts: new Date().toISOString(),
|
|
378
|
+
flowId: this.flowId,
|
|
379
|
+
seq: ++this.seq,
|
|
380
|
+
eventType,
|
|
381
|
+
data: {
|
|
382
|
+
source: "auto-orchestrator",
|
|
383
|
+
name: event.name,
|
|
384
|
+
reason: event.reason,
|
|
385
|
+
unitType: event.unitType,
|
|
386
|
+
unitId: event.unitId,
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── NotificationAdapter (folded) ─────────────────────────────────────────
|
|
392
|
+
|
|
393
|
+
private notifyLifecycle(event: { name: string; detail?: string }): void {
|
|
394
|
+
if (event.name === "error") {
|
|
395
|
+
this.ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// ── HealthAdapter (folded) ───────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
private checkResourcesStale(): string | null {
|
|
402
|
+
return checkResourcesStale(this.s.resourceVersionOnStart);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private async preAdvanceGate(): Promise<
|
|
406
|
+
| { kind: "pass"; fixesApplied?: readonly string[] }
|
|
407
|
+
| { kind: "fail"; reason: string; action?: "pause" | "stop" }
|
|
408
|
+
| { kind: "threw"; error: unknown }
|
|
409
|
+
> {
|
|
410
|
+
try {
|
|
411
|
+
const gate = await preDispatchHealthGate(this.getLiveDispatchBasePath());
|
|
412
|
+
if (gate.proceed) {
|
|
413
|
+
return {
|
|
414
|
+
kind: "pass",
|
|
415
|
+
fixesApplied: gate.fixesApplied,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
return {
|
|
419
|
+
kind: "fail",
|
|
420
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
421
|
+
action: gate.severity ?? "pause",
|
|
422
|
+
};
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return { kind: "threw", error };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private postAdvanceRecord(result: AutoAdvanceResult): void {
|
|
429
|
+
if (result.kind === "error") {
|
|
430
|
+
recordHealthSnapshot(1, 0, 0, [{
|
|
431
|
+
code: "orchestration-error",
|
|
432
|
+
message: result.reason ?? "orchestration error",
|
|
433
|
+
severity: "error",
|
|
434
|
+
unitId: "orchestration",
|
|
435
|
+
}], [], "orchestration");
|
|
436
|
+
} else if (result.kind === "blocked") {
|
|
437
|
+
recordHealthSnapshot(0, 1, 0, [{
|
|
438
|
+
code: "orchestration-blocked",
|
|
439
|
+
message: result.reason ?? "orchestration blocked",
|
|
440
|
+
severity: "warning",
|
|
441
|
+
unitId: "orchestration",
|
|
442
|
+
}], [], "orchestration");
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ── UokGateAdapter (folded) ──────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
private async emitUokGate(input: {
|
|
449
|
+
gateId: string;
|
|
450
|
+
gateType: "policy" | "execution";
|
|
451
|
+
outcome: "pass" | "fail" | "manual-attention";
|
|
452
|
+
failureClass: "none" | "policy" | "manual-attention";
|
|
453
|
+
rationale: string;
|
|
454
|
+
findings?: string;
|
|
455
|
+
milestoneId?: string;
|
|
456
|
+
}): Promise<void> {
|
|
457
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
458
|
+
const prefs = loadEffectiveGSDPreferences(activeBasePath)?.preferences;
|
|
459
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
460
|
+
if (!uokFlags.gates) return;
|
|
461
|
+
const milestoneId = input.milestoneId ?? this.s.currentMilestoneId ?? undefined;
|
|
462
|
+
try {
|
|
463
|
+
const { UokGateRunner } = await import("../uok/gate-runner.js");
|
|
464
|
+
const runner = new UokGateRunner();
|
|
465
|
+
runner.register({
|
|
466
|
+
id: input.gateId,
|
|
467
|
+
type: input.gateType,
|
|
468
|
+
execute: async () => ({
|
|
469
|
+
outcome: input.outcome,
|
|
470
|
+
failureClass: input.failureClass,
|
|
471
|
+
rationale: input.rationale,
|
|
472
|
+
findings: input.findings ?? "",
|
|
473
|
+
}),
|
|
474
|
+
});
|
|
475
|
+
await runner.run(input.gateId, {
|
|
476
|
+
basePath: activeBasePath,
|
|
477
|
+
traceId: `pre-dispatch:${this.flowId}`,
|
|
478
|
+
turnId: `orch-${this.seq}`,
|
|
479
|
+
milestoneId,
|
|
480
|
+
unitType: "pre-dispatch",
|
|
481
|
+
unitId: `orch-${this.seq}`,
|
|
482
|
+
});
|
|
483
|
+
} catch (err) {
|
|
484
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
485
|
+
file: "orchestrator.ts",
|
|
486
|
+
gateId: input.gateId,
|
|
487
|
+
gateType: input.gateType,
|
|
488
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ── StateReconciliationAdapter (folded) ──────────────────────────────────
|
|
494
|
+
|
|
495
|
+
private async reconcileBeforeDispatch(): Promise<
|
|
496
|
+
{ ok: true; reason: string; stateSnapshot?: GSDState }
|
|
497
|
+
| { ok: false; reason: string; stateSnapshot?: GSDState }
|
|
498
|
+
> {
|
|
499
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
500
|
+
const result = await reconcileBeforeDispatch(activeBasePath);
|
|
501
|
+
// Failure-path summaries written by gsd_summary_save create
|
|
502
|
+
// artifact-db-status-divergence blockers for tasks that are still
|
|
503
|
+
// pending (gsd_task_complete never ran). These tasks can still be
|
|
504
|
+
// dispatched and the drift self-heals once they complete successfully.
|
|
505
|
+
const hardBlockers = result.blockers.filter(
|
|
506
|
+
(b) =>
|
|
507
|
+
!b.includes("has SUMMARY artifact while DB status is") &&
|
|
508
|
+
!b.includes("has SUMMARY on disk while DB status is") &&
|
|
509
|
+
!b.includes("has task SUMMARY artifacts but no DB tasks"),
|
|
510
|
+
);
|
|
511
|
+
if (hardBlockers.length > 0) {
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
reason: hardBlockers[0],
|
|
515
|
+
stateSnapshot: result.stateSnapshot,
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
const repairedKinds = result.repaired.map((d) => d.kind);
|
|
519
|
+
return {
|
|
520
|
+
ok: true,
|
|
521
|
+
reason:
|
|
522
|
+
repairedKinds.length > 0
|
|
523
|
+
? `repaired: ${repairedKinds.join(", ")}`
|
|
524
|
+
: "clean",
|
|
525
|
+
stateSnapshot: result.stateSnapshot,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── DispatchAdapter (folded) ─────────────────────────────────────────────
|
|
37
530
|
|
|
38
|
-
|
|
39
|
-
this.
|
|
531
|
+
private decideNextUnit(input: DispatchDecisionInput): Promise<DispatchDecision> {
|
|
532
|
+
return decideOrchestratorDispatch(this.ctx, this.pi, this.dispatchBasePath, this.s, input);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// ── ToolContractAdapter (folded) ─────────────────────────────────────────
|
|
536
|
+
|
|
537
|
+
private compileUnitToolContract(unitType: string): { ok: true; reason: string } | { ok: false; reason: string } {
|
|
538
|
+
const result = compileUnitToolContract(unitType);
|
|
539
|
+
if (!result.ok) return { ok: false, reason: result.detail };
|
|
540
|
+
return { ok: true, reason: result.contract.validationRules.join(", ") };
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// ── WorktreeAdapter (folded) ─────────────────────────────────────────────
|
|
544
|
+
|
|
545
|
+
private getEffectiveUnitIsolationMode(basePath: string): ReturnType<typeof getIsolationMode> {
|
|
546
|
+
const configuredMode = getIsolationMode(basePath);
|
|
547
|
+
return configuredMode === "worktree" && this.s.isolationDegraded ? "branch" : configuredMode;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private buildLifecycle(): WorktreeLifecycle {
|
|
551
|
+
return new WorktreeLifecycle(this.s, {
|
|
552
|
+
gitServiceFactory: (basePath: string) => {
|
|
553
|
+
const gitConfig = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
554
|
+
return new GitServiceImpl(basePath, gitConfig);
|
|
555
|
+
},
|
|
556
|
+
worktreeProjection: new WorktreeStateProjection(),
|
|
557
|
+
mergeMilestoneToMain,
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private rebuildScope(rawPath: string, milestoneId: string | null): void {
|
|
562
|
+
if (!milestoneId) {
|
|
563
|
+
this.s.scope = null;
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
try {
|
|
567
|
+
const workspace = createWorkspace(rawPath);
|
|
568
|
+
this.s.scope = scopeMilestone(workspace, milestoneId);
|
|
569
|
+
} catch {
|
|
570
|
+
// Non-fatal — scope is additive. Existing readers still use basePath.
|
|
571
|
+
this.s.scope = null;
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
private async prepareWorktreeForUnit(
|
|
576
|
+
unitType: string,
|
|
577
|
+
unitId: string,
|
|
578
|
+
): Promise<{ ok: true; reason: string } | { ok: false; reason: string }> {
|
|
579
|
+
const isolationMode = this.getEffectiveUnitIsolationMode(this.runtimeBasePath);
|
|
580
|
+
const manifest = resolveManifest(unitType);
|
|
581
|
+
if (!manifest) {
|
|
582
|
+
return {
|
|
583
|
+
ok: false,
|
|
584
|
+
reason: `No Unit manifest is registered for ${unitType}`,
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
if (isolationMode !== "worktree") {
|
|
588
|
+
return { ok: true, reason: "not-required" };
|
|
589
|
+
}
|
|
590
|
+
const writeScope =
|
|
591
|
+
manifest.tools.mode === "all" || manifest.tools.mode === "docs"
|
|
592
|
+
? "source-writing"
|
|
593
|
+
: "planning-only";
|
|
594
|
+
const safety = createWorktreeSafetyModule();
|
|
595
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
596
|
+
const snapshot = await deriveState(activeBasePath);
|
|
597
|
+
const milestoneId = snapshot.activeMilestone?.id ?? null;
|
|
598
|
+
const expectedBranch = milestoneId ? autoWorktreeBranch(milestoneId) : null;
|
|
599
|
+
let result = safety.validateUnitRoot({
|
|
600
|
+
unitType,
|
|
601
|
+
unitId,
|
|
602
|
+
writeScope,
|
|
603
|
+
projectRoot: this.runtimeBasePath,
|
|
604
|
+
unitRoot: activeBasePath,
|
|
605
|
+
milestoneId,
|
|
606
|
+
isolationMode,
|
|
607
|
+
expectedBranch,
|
|
608
|
+
});
|
|
609
|
+
if (!result.ok) {
|
|
610
|
+
const repaired = await repairAutoWorktreeSafetyFailure({
|
|
611
|
+
safetyResult: result,
|
|
612
|
+
projectRoot: this.runtimeBasePath,
|
|
613
|
+
activeRoot: activeBasePath,
|
|
614
|
+
milestoneId,
|
|
615
|
+
enterMilestone: async (id) => {
|
|
616
|
+
this.buildLifecycle().adoptSessionRoot(this.runtimeBasePath, this.s.originalBasePath || this.runtimeBasePath);
|
|
617
|
+
const enterResult = this.buildLifecycle().enterMilestone(id, {
|
|
618
|
+
notify: this.ctx.ui.notify.bind(this.ctx.ui),
|
|
619
|
+
});
|
|
620
|
+
if (!enterResult.ok) return { ok: false, reason: enterResult.reason };
|
|
621
|
+
this.rebuildScope(this.s.basePath, this.s.currentMilestoneId);
|
|
622
|
+
return { ok: true };
|
|
623
|
+
},
|
|
624
|
+
revalidate: () => safety.validateUnitRoot({
|
|
625
|
+
unitType,
|
|
626
|
+
unitId,
|
|
627
|
+
writeScope,
|
|
628
|
+
projectRoot: this.runtimeBasePath,
|
|
629
|
+
unitRoot: this.getLiveDispatchBasePath(),
|
|
630
|
+
milestoneId,
|
|
631
|
+
isolationMode: this.getEffectiveUnitIsolationMode(this.runtimeBasePath),
|
|
632
|
+
expectedBranch,
|
|
633
|
+
}),
|
|
634
|
+
});
|
|
635
|
+
result = repaired.result;
|
|
636
|
+
if (result.ok) {
|
|
637
|
+
return { ok: true, reason: repaired.repaired ? `repaired-${result.kind}` : result.kind };
|
|
638
|
+
}
|
|
639
|
+
const repairDetail = repaired.repairReason
|
|
640
|
+
? ` (repair skipped: ${repaired.repairReason})`
|
|
641
|
+
: "";
|
|
642
|
+
return { ok: false, reason: `${result.kind}: ${result.reason}${repairDetail}` };
|
|
643
|
+
}
|
|
644
|
+
return { ok: true, reason: result.kind };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── RecoveryAdapter (folded) ─────────────────────────────────────────────
|
|
648
|
+
|
|
649
|
+
private classifyAndRecover(input: {
|
|
650
|
+
error: unknown;
|
|
651
|
+
unitType?: string;
|
|
652
|
+
unitId?: string;
|
|
653
|
+
}): { action: "retry" | "escalate" | "stop"; reason: string } {
|
|
654
|
+
const recovery = classifyFailure(input);
|
|
655
|
+
return { action: recovery.action, reason: recovery.reason };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── Lifecycle verbs ──────────────────────────────────────────────────────
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* #442: graduated stuck recovery, ported from the legacy
|
|
662
|
+
* auto/phases.ts:runDispatch path that Phase 3 retires. The ring-buffer
|
|
663
|
+
* hard-stops (stuck-loop saturation and finalized-repeat) would otherwise
|
|
664
|
+
* KILL a unit that actually completed on disk but whose DB row is still
|
|
665
|
+
* stale. Before hard-stopping, verify the expected artifact exists; if so,
|
|
666
|
+
* refresh the DB from it, invalidate caches and reset the dispatch ring so
|
|
667
|
+
* the next advance picks the correct next unit. Bounded to one attempt per
|
|
668
|
+
* stuck key per episode (reset on lifecycle + genuine finalize) to avoid an
|
|
669
|
+
* unbounded recover→re-saturate→recover loop — mirrors the legacy
|
|
670
|
+
* Level-1-recover-then-Level-2-hard-stop escalation.
|
|
671
|
+
*
|
|
672
|
+
* Returns true when recovery succeeded; the caller should re-loop (return a
|
|
673
|
+
* skipped result) instead of stopping.
|
|
674
|
+
*/
|
|
675
|
+
private tryStuckArtifactRecovery(unitType: string, unitId: string): boolean {
|
|
676
|
+
const key = `${unitType}:${unitId}`;
|
|
677
|
+
if (this.lastStuckRecoveryKey === key) return false; // already tried this episode
|
|
678
|
+
const basePath = this.getLiveDispatchBasePath();
|
|
679
|
+
if (!verifyExpectedArtifact(unitType, unitId, basePath)) return false;
|
|
680
|
+
const refreshed = refreshRecoveryDbForArtifact(unitType, unitId, basePath);
|
|
681
|
+
// Fatal failures cannot be recovered — hard-stop. Non-fatal (e.g. plan-slice
|
|
682
|
+
// DB refresh hiccup) still fall through: invalidating caches and resetting
|
|
683
|
+
// the ring gives the next advance a clean slate to pick up the correct state,
|
|
684
|
+
// mirroring the legacy Level-1 "continue" escalation path.
|
|
685
|
+
if (!refreshed.ok && refreshed.fatal) return false;
|
|
686
|
+
this.lastStuckRecoveryKey = key;
|
|
687
|
+
invalidateAllCaches();
|
|
688
|
+
this.dispatchKeyWindow = [];
|
|
689
|
+
this.lastAdvanceKey = null;
|
|
690
|
+
this.lastFinalizedUnitKey = null;
|
|
691
|
+
return true;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
private stuckRecovered(
|
|
695
|
+
decision: { unitType: string; unitId: string },
|
|
696
|
+
stateSnapshot: GSDState,
|
|
697
|
+
): AutoAdvanceResult {
|
|
698
|
+
const recovered: AutoAdvanceResult = {
|
|
699
|
+
kind: "skipped",
|
|
700
|
+
reason: `stuck-recovery: ${decision.unitType} ${decision.unitId} artifact found on disk; DB refreshed`,
|
|
701
|
+
stateSnapshot,
|
|
702
|
+
};
|
|
703
|
+
this.status.phase = "running";
|
|
704
|
+
this.status.activeUnit = undefined;
|
|
705
|
+
this.bumpTransition();
|
|
706
|
+
this.journalTransition({
|
|
707
|
+
name: "advance-skipped",
|
|
708
|
+
reason: recovered.reason,
|
|
709
|
+
unitType: decision.unitType,
|
|
710
|
+
unitId: decision.unitId,
|
|
711
|
+
});
|
|
712
|
+
this.postAdvanceRecord(recovered);
|
|
713
|
+
return recovered;
|
|
40
714
|
}
|
|
41
715
|
|
|
42
716
|
public async start(_sessionContext: AutoSessionContext): Promise<AutoAdvanceResult> {
|
|
43
717
|
this.lastAdvanceKey = null;
|
|
44
718
|
this.lastFinalizedUnitKey = null;
|
|
45
719
|
this.dispatchKeyWindow = [];
|
|
720
|
+
this.lastStuckRecoveryKey = null;
|
|
46
721
|
this.status.phase = "running";
|
|
47
722
|
this.bumpTransition();
|
|
48
|
-
|
|
49
|
-
|
|
723
|
+
this.journalTransition({ name: "start" });
|
|
724
|
+
this.notifyLifecycle({ name: "start" });
|
|
50
725
|
return { kind: "started" };
|
|
51
726
|
}
|
|
52
727
|
|
|
53
728
|
public async advance(): Promise<AutoAdvanceResult> {
|
|
729
|
+
debugCount("dispatches");
|
|
730
|
+
const stopAdvanceTimer = debugTime("orchestrator-advance");
|
|
54
731
|
try {
|
|
55
|
-
|
|
732
|
+
this.ensureLockOwnership();
|
|
56
733
|
|
|
57
|
-
const staleMsg = this.
|
|
734
|
+
const staleMsg = this.checkResourcesStale();
|
|
58
735
|
if (staleMsg) {
|
|
59
|
-
await this.
|
|
736
|
+
await this.emitUokGate({
|
|
60
737
|
gateId: "resource-version-guard",
|
|
61
738
|
gateType: "policy",
|
|
62
739
|
outcome: "fail",
|
|
@@ -65,11 +742,11 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
65
742
|
findings: staleMsg,
|
|
66
743
|
});
|
|
67
744
|
const blocked: AutoAdvanceResult = { kind: "blocked", reason: staleMsg, action: "pause" };
|
|
68
|
-
|
|
69
|
-
|
|
745
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
746
|
+
this.postAdvanceRecord(blocked);
|
|
70
747
|
return blocked;
|
|
71
748
|
}
|
|
72
|
-
await this.
|
|
749
|
+
await this.emitUokGate({
|
|
73
750
|
gateId: "resource-version-guard",
|
|
74
751
|
gateType: "policy",
|
|
75
752
|
outcome: "pass",
|
|
@@ -77,9 +754,9 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
77
754
|
rationale: "resource version guard passed",
|
|
78
755
|
});
|
|
79
756
|
|
|
80
|
-
const gate = await this.
|
|
757
|
+
const gate = await this.preAdvanceGate();
|
|
81
758
|
if (gate.kind === "fail") {
|
|
82
|
-
await this.
|
|
759
|
+
await this.emitUokGate({
|
|
83
760
|
gateId: "pre-dispatch-health-gate",
|
|
84
761
|
gateType: "execution",
|
|
85
762
|
outcome: "manual-attention",
|
|
@@ -92,12 +769,12 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
92
769
|
reason: gate.reason,
|
|
93
770
|
action: gate.action ?? "pause",
|
|
94
771
|
};
|
|
95
|
-
|
|
96
|
-
|
|
772
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
773
|
+
this.postAdvanceRecord(blocked);
|
|
97
774
|
return blocked;
|
|
98
775
|
}
|
|
99
776
|
if (gate.kind === "threw") {
|
|
100
|
-
await this.
|
|
777
|
+
await this.emitUokGate({
|
|
101
778
|
gateId: "pre-dispatch-health-gate",
|
|
102
779
|
gateType: "execution",
|
|
103
780
|
outcome: "manual-attention",
|
|
@@ -107,7 +784,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
107
784
|
});
|
|
108
785
|
// intentional fall-through: matches runPreDispatch behaviour
|
|
109
786
|
} else {
|
|
110
|
-
await this.
|
|
787
|
+
await this.emitUokGate({
|
|
111
788
|
gateId: "pre-dispatch-health-gate",
|
|
112
789
|
gateType: "execution",
|
|
113
790
|
outcome: "pass",
|
|
@@ -117,7 +794,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
117
794
|
});
|
|
118
795
|
}
|
|
119
796
|
|
|
120
|
-
const reconciliation = await this.
|
|
797
|
+
const reconciliation = await this.reconcileBeforeDispatch();
|
|
121
798
|
if (!reconciliation.ok || !reconciliation.stateSnapshot) {
|
|
122
799
|
const blocked: AutoAdvanceResult = {
|
|
123
800
|
kind: "blocked",
|
|
@@ -125,12 +802,12 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
125
802
|
action: "pause",
|
|
126
803
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
127
804
|
};
|
|
128
|
-
|
|
129
|
-
|
|
805
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
806
|
+
this.postAdvanceRecord(blocked);
|
|
130
807
|
return blocked;
|
|
131
808
|
}
|
|
132
809
|
|
|
133
|
-
const decision = await this.
|
|
810
|
+
const decision = await this.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
|
|
134
811
|
if (!decision) {
|
|
135
812
|
const stopped: AutoAdvanceResult = {
|
|
136
813
|
kind: "stopped",
|
|
@@ -142,8 +819,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
142
819
|
this.lastAdvanceKey = null;
|
|
143
820
|
this.dispatchKeyWindow = [];
|
|
144
821
|
this.bumpTransition();
|
|
145
|
-
|
|
146
|
-
|
|
822
|
+
this.journalTransition({ name: "advance-stopped", reason: stopped.reason });
|
|
823
|
+
this.postAdvanceRecord(stopped);
|
|
147
824
|
return stopped;
|
|
148
825
|
}
|
|
149
826
|
if ("kind" in decision && decision.kind === "skipped") {
|
|
@@ -155,8 +832,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
155
832
|
this.status.phase = "running";
|
|
156
833
|
this.status.activeUnit = undefined;
|
|
157
834
|
this.bumpTransition();
|
|
158
|
-
|
|
159
|
-
|
|
835
|
+
this.journalTransition({ name: "advance-skipped", reason: skipped.reason });
|
|
836
|
+
this.postAdvanceRecord(skipped);
|
|
160
837
|
return skipped;
|
|
161
838
|
}
|
|
162
839
|
if (!("unitType" in decision)) {
|
|
@@ -166,8 +843,8 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
166
843
|
action: decision.action,
|
|
167
844
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
168
845
|
};
|
|
169
|
-
|
|
170
|
-
|
|
846
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
847
|
+
this.postAdvanceRecord(blocked);
|
|
171
848
|
return blocked;
|
|
172
849
|
}
|
|
173
850
|
|
|
@@ -184,19 +861,25 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
184
861
|
|
|
185
862
|
const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
|
|
186
863
|
if (this.lastFinalizedUnitKey === nextKey) {
|
|
864
|
+
// #442: the unit re-dispatched immediately after finalizing may have
|
|
865
|
+
// actually completed on disk with a stale DB. Verify + recover before
|
|
866
|
+
// hard-stopping (legacy graduated stuck-recovery parity).
|
|
867
|
+
if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
|
|
868
|
+
return this.stuckRecovered(decision, reconciliation.stateSnapshot);
|
|
869
|
+
}
|
|
187
870
|
const blocked: AutoAdvanceResult = {
|
|
188
871
|
kind: "blocked",
|
|
189
872
|
reason: `state did not advance after finalized ${decision.unitType} ${decision.unitId}`,
|
|
190
873
|
action: "stop",
|
|
191
874
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
192
875
|
};
|
|
193
|
-
|
|
876
|
+
this.journalTransition({
|
|
194
877
|
name: "advance-blocked",
|
|
195
878
|
reason: blocked.reason,
|
|
196
879
|
unitType: decision.unitType,
|
|
197
880
|
unitId: decision.unitId,
|
|
198
881
|
});
|
|
199
|
-
|
|
882
|
+
this.postAdvanceRecord(blocked);
|
|
200
883
|
return blocked;
|
|
201
884
|
}
|
|
202
885
|
|
|
@@ -208,13 +891,13 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
208
891
|
// stuck-loop for the saturated-window case.
|
|
209
892
|
if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
|
|
210
893
|
const blocked: AutoAdvanceResult = { kind: "blocked", reason: "idempotent advance: unit already active", action: "pause" };
|
|
211
|
-
|
|
894
|
+
this.journalTransition({
|
|
212
895
|
name: "advance-blocked",
|
|
213
896
|
reason: blocked.reason,
|
|
214
897
|
unitType: decision.unitType,
|
|
215
898
|
unitId: decision.unitId,
|
|
216
899
|
});
|
|
217
|
-
|
|
900
|
+
this.postAdvanceRecord(blocked);
|
|
218
901
|
return blocked;
|
|
219
902
|
}
|
|
220
903
|
|
|
@@ -223,22 +906,28 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
223
906
|
// picking the same unit across the whole window and must hard-stop with
|
|
224
907
|
// a diagnosable reason.
|
|
225
908
|
if (matchingCount >= STUCK_WINDOW_SIZE) {
|
|
909
|
+
// #442: before declaring a stuck loop, verify the unit didn't actually
|
|
910
|
+
// complete on disk (stale DB) and recover if so — legacy graduated
|
|
911
|
+
// stuck-recovery parity. Otherwise hard-stop with a diagnosable reason.
|
|
912
|
+
if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
|
|
913
|
+
return this.stuckRecovered(decision, reconciliation.stateSnapshot);
|
|
914
|
+
}
|
|
226
915
|
const blocked: AutoAdvanceResult = {
|
|
227
916
|
kind: "blocked",
|
|
228
917
|
reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
|
|
229
918
|
action: "stop",
|
|
230
919
|
};
|
|
231
|
-
|
|
920
|
+
this.journalTransition({
|
|
232
921
|
name: "advance-blocked",
|
|
233
922
|
reason: blocked.reason,
|
|
234
923
|
unitType: decision.unitType,
|
|
235
924
|
unitId: decision.unitId,
|
|
236
925
|
});
|
|
237
|
-
|
|
926
|
+
this.postAdvanceRecord(blocked);
|
|
238
927
|
return blocked;
|
|
239
928
|
}
|
|
240
929
|
|
|
241
|
-
const contract =
|
|
930
|
+
const contract = this.compileUnitToolContract(decision.unitType);
|
|
242
931
|
if (!contract.ok) {
|
|
243
932
|
const blocked: AutoAdvanceResult = {
|
|
244
933
|
kind: "blocked",
|
|
@@ -246,17 +935,17 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
246
935
|
action: "pause",
|
|
247
936
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
248
937
|
};
|
|
249
|
-
|
|
938
|
+
this.journalTransition({
|
|
250
939
|
name: "advance-blocked",
|
|
251
940
|
reason: blocked.reason,
|
|
252
941
|
unitType: decision.unitType,
|
|
253
942
|
unitId: decision.unitId,
|
|
254
943
|
});
|
|
255
|
-
|
|
944
|
+
this.postAdvanceRecord(blocked);
|
|
256
945
|
return blocked;
|
|
257
946
|
}
|
|
258
947
|
|
|
259
|
-
const worktree = await this.
|
|
948
|
+
const worktree = await this.prepareWorktreeForUnit(decision.unitType, decision.unitId);
|
|
260
949
|
if (!worktree.ok) {
|
|
261
950
|
const blocked: AutoAdvanceResult = {
|
|
262
951
|
kind: "blocked",
|
|
@@ -264,13 +953,13 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
264
953
|
action: "pause",
|
|
265
954
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
266
955
|
};
|
|
267
|
-
|
|
956
|
+
this.journalTransition({
|
|
268
957
|
name: "advance-blocked",
|
|
269
958
|
reason: blocked.reason,
|
|
270
959
|
unitType: decision.unitType,
|
|
271
960
|
unitId: decision.unitId,
|
|
272
961
|
});
|
|
273
|
-
|
|
962
|
+
this.postAdvanceRecord(blocked);
|
|
274
963
|
return blocked;
|
|
275
964
|
}
|
|
276
965
|
|
|
@@ -279,23 +968,23 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
279
968
|
this.lastAdvanceKey = nextKey;
|
|
280
969
|
this.bumpTransition();
|
|
281
970
|
|
|
282
|
-
|
|
971
|
+
this.journalTransition({
|
|
283
972
|
name: "advance",
|
|
284
973
|
reason: decision.reason,
|
|
285
974
|
unitType: decision.unitType,
|
|
286
975
|
unitId: decision.unitId,
|
|
287
976
|
});
|
|
288
|
-
|
|
977
|
+
// syncAfterUnit was a no-op in the wired WorktreeAdapter.
|
|
289
978
|
|
|
290
979
|
const advanced: AutoAdvanceResult = {
|
|
291
980
|
kind: "advanced",
|
|
292
981
|
unit: { unitType: decision.unitType, unitId: decision.unitId },
|
|
293
982
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
294
983
|
};
|
|
295
|
-
|
|
984
|
+
this.postAdvanceRecord(advanced);
|
|
296
985
|
return advanced;
|
|
297
986
|
} catch (error) {
|
|
298
|
-
const recovery =
|
|
987
|
+
const recovery = this.classifyAndRecover({
|
|
299
988
|
error,
|
|
300
989
|
unitType: this.status.activeUnit?.unitType,
|
|
301
990
|
unitId: this.status.activeUnit?.unitId,
|
|
@@ -327,17 +1016,19 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
327
1016
|
: result.kind === "stopped"
|
|
328
1017
|
? "advance-stopped"
|
|
329
1018
|
: "advance-error";
|
|
330
|
-
|
|
1019
|
+
this.journalTransition({ name: journalName, reason: recovery.reason });
|
|
331
1020
|
|
|
332
1021
|
if (result.kind === "paused") {
|
|
333
|
-
|
|
1022
|
+
this.notifyLifecycle({ name: "pause", detail: recovery.reason });
|
|
334
1023
|
} else if (result.kind === "stopped") {
|
|
335
|
-
|
|
1024
|
+
this.notifyLifecycle({ name: "stopped", detail: recovery.reason });
|
|
336
1025
|
} else if (result.kind === "error") {
|
|
337
|
-
|
|
1026
|
+
this.notifyLifecycle({ name: "error", detail: recovery.reason });
|
|
338
1027
|
}
|
|
339
|
-
|
|
1028
|
+
this.postAdvanceRecord(result);
|
|
340
1029
|
return result;
|
|
1030
|
+
} finally {
|
|
1031
|
+
stopAdvanceTimer();
|
|
341
1032
|
}
|
|
342
1033
|
}
|
|
343
1034
|
|
|
@@ -345,10 +1036,11 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
345
1036
|
this.lastAdvanceKey = null;
|
|
346
1037
|
this.lastFinalizedUnitKey = null;
|
|
347
1038
|
this.dispatchKeyWindow = [];
|
|
1039
|
+
this.lastStuckRecoveryKey = null;
|
|
348
1040
|
this.status.phase = "running";
|
|
349
1041
|
this.bumpTransition();
|
|
350
|
-
|
|
351
|
-
|
|
1042
|
+
this.journalTransition({ name: "resume" });
|
|
1043
|
+
this.notifyLifecycle({ name: "resume" });
|
|
352
1044
|
return { kind: "resumed" };
|
|
353
1045
|
}
|
|
354
1046
|
|
|
@@ -356,15 +1048,16 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
356
1048
|
if (this.status.phase === "stopped") {
|
|
357
1049
|
return { kind: "stopped", reason };
|
|
358
1050
|
}
|
|
359
|
-
|
|
1051
|
+
// cleanupOnStop was a no-op in the wired WorktreeAdapter.
|
|
360
1052
|
this.status.phase = "stopped";
|
|
361
1053
|
this.status.activeUnit = undefined;
|
|
362
1054
|
this.lastAdvanceKey = null;
|
|
363
1055
|
this.lastFinalizedUnitKey = null;
|
|
364
1056
|
this.dispatchKeyWindow = [];
|
|
1057
|
+
this.lastStuckRecoveryKey = null;
|
|
365
1058
|
this.bumpTransition();
|
|
366
|
-
|
|
367
|
-
|
|
1059
|
+
this.journalTransition({ name: "stop", reason });
|
|
1060
|
+
this.notifyLifecycle({ name: "stop", detail: reason });
|
|
368
1061
|
return { kind: "stopped", reason };
|
|
369
1062
|
}
|
|
370
1063
|
|
|
@@ -382,8 +1075,10 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
382
1075
|
this.status.activeUnit = undefined;
|
|
383
1076
|
this.lastAdvanceKey = null;
|
|
384
1077
|
this.lastFinalizedUnitKey = unitKey;
|
|
1078
|
+
// Genuine progress — re-enable graduated stuck recovery for future episodes.
|
|
1079
|
+
this.lastStuckRecoveryKey = null;
|
|
385
1080
|
this.bumpTransition();
|
|
386
|
-
|
|
1081
|
+
this.journalTransition({
|
|
387
1082
|
name: "unit-finalized",
|
|
388
1083
|
unitType: unit.unitType,
|
|
389
1084
|
unitId: unit.unitId,
|
|
@@ -403,7 +1098,7 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
403
1098
|
this.lastAdvanceKey = null;
|
|
404
1099
|
this.lastFinalizedUnitKey = null;
|
|
405
1100
|
this.bumpTransition();
|
|
406
|
-
|
|
1101
|
+
this.journalTransition({
|
|
407
1102
|
name: "unit-retry",
|
|
408
1103
|
reason: "finalize-retry",
|
|
409
1104
|
unitType: unit.unitType,
|
|
@@ -417,6 +1112,47 @@ export class AutoOrchestrator implements AutoOrchestrationModule {
|
|
|
417
1112
|
}
|
|
418
1113
|
}
|
|
419
1114
|
|
|
420
|
-
|
|
421
|
-
|
|
1115
|
+
function isUsableLiveOrchestratorBasePath(basePath: string): boolean {
|
|
1116
|
+
if (!basePath || !existsSync(basePath)) return false;
|
|
1117
|
+
if (!detectWorktreeName(basePath)) return true;
|
|
1118
|
+
|
|
1119
|
+
try {
|
|
1120
|
+
return readFileSync(join(basePath, ".git"), "utf8").trim().startsWith("gitdir: ");
|
|
1121
|
+
} catch {
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
/**
|
|
1127
|
+
* Resolve the base path the live orchestrator should dispatch from, falling
|
|
1128
|
+
* back to the project root when the captured worktree path has been removed
|
|
1129
|
+
* (e.g. after milestone-merge cleanup). Exported for the closeout-regression
|
|
1130
|
+
* tests and reused by the orchestrator's getLiveDispatchBasePath.
|
|
1131
|
+
*/
|
|
1132
|
+
export function resolveLiveOrchestratorBasePath(input: {
|
|
1133
|
+
capturedBasePath: string;
|
|
1134
|
+
runtimeBasePath: string;
|
|
1135
|
+
sessionBasePath?: string | null;
|
|
1136
|
+
originalBasePath?: string | null;
|
|
1137
|
+
}): string {
|
|
1138
|
+
const primary = input.sessionBasePath || input.capturedBasePath;
|
|
1139
|
+
if (isUsableLiveOrchestratorBasePath(primary)) return primary;
|
|
1140
|
+
|
|
1141
|
+
const fallbacks = [
|
|
1142
|
+
input.originalBasePath,
|
|
1143
|
+
input.runtimeBasePath,
|
|
1144
|
+
resolveProjectRoot(input.capturedBasePath),
|
|
1145
|
+
];
|
|
1146
|
+
|
|
1147
|
+
for (const candidate of fallbacks) {
|
|
1148
|
+
if (candidate && isUsableLiveOrchestratorBasePath(candidate)) {
|
|
1149
|
+
return candidate;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
return input.runtimeBasePath || input.capturedBasePath;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
export function createAutoOrchestrator(context: OrchestratorContext): AutoOrchestrationModule {
|
|
1157
|
+
return new AutoOrchestrator(context);
|
|
422
1158
|
}
|