@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,5 +1,45 @@
|
|
|
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.
|
|
12
|
+
import { debugCount, debugTime } from "../debug-logger.js";
|
|
13
|
+
import { reconcileBeforeDispatch } from "../state-reconciliation.js";
|
|
14
|
+
import { resolveDispatch } from "../auto-dispatch.js";
|
|
15
|
+
import { classifyFailure } from "../recovery-classification.js";
|
|
16
|
+
import { verifyExpectedArtifact, refreshRecoveryDbForArtifact } from "../auto-recovery.js";
|
|
17
|
+
import { invalidateAllCaches } from "../cache.js";
|
|
18
|
+
import { compileUnitToolContract } from "../tool-contract.js";
|
|
19
|
+
import { createWorktreeSafetyModule } from "../worktree-safety.js";
|
|
20
|
+
import { repairAutoWorktreeSafetyFailure } from "../auto-worktree-repair.js";
|
|
21
|
+
import { resolveManifest } from "../unit-context-manifest.js";
|
|
22
|
+
import { preDispatchHealthGate, recordHealthSnapshot, } from "../doctor-proactive.js";
|
|
23
|
+
import { checkResourcesStale, autoWorktreeBranch, mergeMilestoneToMain } from "../auto-worktree.js";
|
|
24
|
+
import { getSessionLockStatus } from "../session-lock.js";
|
|
25
|
+
import { resolveUokFlags } from "../uok/flags.js";
|
|
26
|
+
import { emitJournalEvent as _emitJournalEvent } from "../journal.js";
|
|
27
|
+
import { loadEffectiveGSDPreferences, getIsolationMode } from "../preferences.js";
|
|
28
|
+
import { detectWorktreeName, resolveProjectRoot } from "../worktree.js";
|
|
29
|
+
import { GitServiceImpl } from "../git-service.js";
|
|
30
|
+
import { WorktreeStateProjection } from "../worktree-state-projection.js";
|
|
31
|
+
import { WorktreeLifecycle } from "../worktree-lifecycle.js";
|
|
32
|
+
import { createWorkspace, scopeMilestone } from "../workspace.js";
|
|
33
|
+
import { supportsStructuredQuestions } from "../workflow-mcp.js";
|
|
34
|
+
import { getToolBaselineSnapshot } from "../auto-model-selection.js";
|
|
35
|
+
import { deriveState } from "../state.js";
|
|
36
|
+
import { parseUnitId } from "../unit-id.js";
|
|
37
|
+
import { isClosedStatus } from "../status-guards.js";
|
|
38
|
+
import { isDbAvailable, getSlice, getTask, refreshOpenDatabaseFromDisk, } from "../gsd-db.js";
|
|
39
|
+
import { getErrorMessage } from "../error-utils.js";
|
|
40
|
+
import { logWarning } from "../workflow-logger.js";
|
|
41
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
42
|
+
import { join } from "node:path";
|
|
3
43
|
function now() {
|
|
4
44
|
return Date.now();
|
|
5
45
|
}
|
|
@@ -18,34 +58,543 @@ function noRemainingUnitsReason(stateSnapshot) {
|
|
|
18
58
|
}
|
|
19
59
|
return "no remaining units";
|
|
20
60
|
}
|
|
61
|
+
function getAlreadyClosedDispatchReason(unitType, unitId) {
|
|
62
|
+
if (!isDbAvailable())
|
|
63
|
+
return null;
|
|
64
|
+
refreshOpenDatabaseFromDisk();
|
|
65
|
+
const { milestone, slice, task } = parseUnitId(unitId);
|
|
66
|
+
if (unitType === "execute-task" && milestone && slice && task) {
|
|
67
|
+
const row = getTask(milestone, slice, task);
|
|
68
|
+
return row && isClosedStatus(row.status)
|
|
69
|
+
? `execute-task ${unitId} is already ${row.status}`
|
|
70
|
+
: null;
|
|
71
|
+
}
|
|
72
|
+
if (unitType === "complete-slice" && milestone && slice) {
|
|
73
|
+
const row = getSlice(milestone, slice);
|
|
74
|
+
return row && isClosedStatus(row.status)
|
|
75
|
+
? `complete-slice ${unitId} is already ${row.status}`
|
|
76
|
+
: null;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
function shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath) {
|
|
81
|
+
const activeMilestoneId = state.activeMilestone?.id;
|
|
82
|
+
const currentMilestoneId = activeSession?.currentMilestoneId;
|
|
83
|
+
if (!activeSession || !activeMilestoneId || !currentMilestoneId || activeMilestoneId === currentMilestoneId) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const scopedWorktreeMilestone = (activeSession.basePath ? detectWorktreeName(activeSession.basePath) : null) ??
|
|
87
|
+
detectWorktreeName(activeDispatchBasePath);
|
|
88
|
+
if (scopedWorktreeMilestone && scopedWorktreeMilestone !== activeMilestoneId) {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
const currentMilestone = state.registry.find((milestone) => milestone.id === currentMilestoneId);
|
|
92
|
+
return !!currentMilestone && isClosedStatus(currentMilestone.status);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Pure dispatch-decision function — formerly `createWiredDispatchAdapter`'s
|
|
96
|
+
* `decideNextUnit`. Folded out of the closure so the orchestrator can call it
|
|
97
|
+
* directly and tests can drive the exact dispatch decision logic against real
|
|
98
|
+
* fixtures without re-introducing an adapter seam.
|
|
99
|
+
*
|
|
100
|
+
* Derives session-derived dispatch inputs the same way phases.ts:runDispatch
|
|
101
|
+
* does (#5789): prefers caller-supplied values when present so test harnesses
|
|
102
|
+
* and alternative wirings can inject deterministic snapshots; otherwise pulls
|
|
103
|
+
* from the captured pi/ctx references.
|
|
104
|
+
*/
|
|
105
|
+
export async function decideOrchestratorDispatch(ctx, pi, dispatchBasePath, session, input) {
|
|
106
|
+
const state = input.stateSnapshot;
|
|
107
|
+
const active = state.activeMilestone;
|
|
108
|
+
if (!active)
|
|
109
|
+
return null;
|
|
110
|
+
const activeSession = input.session ?? session;
|
|
111
|
+
const activeDispatchBasePath = activeSession?.basePath || dispatchBasePath;
|
|
112
|
+
if (activeSession && shouldAdoptActiveMilestone(state, activeSession, activeDispatchBasePath)) {
|
|
113
|
+
activeSession.currentMilestoneId = active.id;
|
|
114
|
+
}
|
|
115
|
+
const prefs = loadEffectiveGSDPreferences(activeDispatchBasePath)?.preferences;
|
|
116
|
+
// Derive session-derived dispatch inputs the same way phases.ts:runDispatch does
|
|
117
|
+
// (#5789). Prefer caller-supplied values when present so test harnesses and
|
|
118
|
+
// alternative wirings can inject deterministic snapshots; otherwise pull from
|
|
119
|
+
// the captured pi/ctx references.
|
|
120
|
+
const sessionProvider = input.sessionProvider ?? ctx.model?.provider;
|
|
121
|
+
const sessionContextWindow = input.sessionContextWindow ?? ctx.model?.contextWindow;
|
|
122
|
+
const modelRegistry = input.modelRegistry ?? ctx.modelRegistry;
|
|
123
|
+
const authMode = sessionProvider && typeof ctx.modelRegistry?.getProviderAuthMode === "function"
|
|
124
|
+
? ctx.modelRegistry.getProviderAuthMode(sessionProvider)
|
|
125
|
+
: undefined;
|
|
126
|
+
// Use baseline snapshot — same reason as phases.ts:runDispatch: the live
|
|
127
|
+
// active set may be narrowed by the prior unit before selectAndApplyModel
|
|
128
|
+
// restores it, causing false transport-preflight failures (#477 follow-up).
|
|
129
|
+
const activeTools = getToolBaselineSnapshot(pi);
|
|
130
|
+
// Mirrors runDispatch: deep-planning keeps approval gates in plain chat
|
|
131
|
+
// because structured questions can be cancelled outside the chat turn on
|
|
132
|
+
// some transports.
|
|
133
|
+
const structuredQuestionsAvailable = input.structuredQuestionsAvailable ??
|
|
134
|
+
(prefs?.planning_depth === "deep"
|
|
135
|
+
? "false"
|
|
136
|
+
: supportsStructuredQuestions(activeTools, {
|
|
137
|
+
authMode,
|
|
138
|
+
baseUrl: ctx.model?.baseUrl,
|
|
139
|
+
})
|
|
140
|
+
? "true"
|
|
141
|
+
: "false");
|
|
142
|
+
const pendingRetry = session?.pendingVerificationRetryDispatch;
|
|
143
|
+
if (session && pendingRetry) {
|
|
144
|
+
session.pendingVerificationRetryDispatch = null;
|
|
145
|
+
const alreadyClosedReason = getAlreadyClosedDispatchReason(pendingRetry.unitType, pendingRetry.unitId);
|
|
146
|
+
if (alreadyClosedReason) {
|
|
147
|
+
session.pendingOrchestrationDispatch = null;
|
|
148
|
+
session.pendingVerificationRetry = null;
|
|
149
|
+
return { kind: "skipped", reason: alreadyClosedReason };
|
|
150
|
+
}
|
|
151
|
+
session.pendingOrchestrationDispatch = pendingRetry;
|
|
152
|
+
return {
|
|
153
|
+
unitType: pendingRetry.unitType,
|
|
154
|
+
unitId: pendingRetry.unitId,
|
|
155
|
+
reason: "verification-retry",
|
|
156
|
+
preconditions: [],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const action = await resolveDispatch({
|
|
160
|
+
basePath: activeDispatchBasePath,
|
|
161
|
+
mid: active.id,
|
|
162
|
+
midTitle: active.title,
|
|
163
|
+
state,
|
|
164
|
+
prefs,
|
|
165
|
+
session: activeSession,
|
|
166
|
+
structuredQuestionsAvailable,
|
|
167
|
+
sessionContextWindow,
|
|
168
|
+
sessionProvider,
|
|
169
|
+
modelRegistry,
|
|
170
|
+
activeTools,
|
|
171
|
+
sessionAuthMode: authMode,
|
|
172
|
+
sessionBaseUrl: ctx.model?.baseUrl,
|
|
173
|
+
});
|
|
174
|
+
if (action.action === "stop") {
|
|
175
|
+
if (session)
|
|
176
|
+
session.pendingOrchestrationDispatch = null;
|
|
177
|
+
return {
|
|
178
|
+
kind: "blocked",
|
|
179
|
+
reason: action.reason,
|
|
180
|
+
action: action.level === "warning" ? "pause" : "stop",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (action.action !== "dispatch") {
|
|
184
|
+
if (session)
|
|
185
|
+
session.pendingOrchestrationDispatch = null;
|
|
186
|
+
return {
|
|
187
|
+
kind: "skipped",
|
|
188
|
+
reason: action.matchedRule ?? "dispatch-skip",
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const alreadyClosedReason = getAlreadyClosedDispatchReason(action.unitType, action.unitId);
|
|
192
|
+
if (alreadyClosedReason) {
|
|
193
|
+
if (session) {
|
|
194
|
+
session.pendingOrchestrationDispatch = null;
|
|
195
|
+
session.pendingVerificationRetry = null;
|
|
196
|
+
}
|
|
197
|
+
return { kind: "skipped", reason: alreadyClosedReason };
|
|
198
|
+
}
|
|
199
|
+
if (session) {
|
|
200
|
+
const pending = {
|
|
201
|
+
unitType: action.unitType,
|
|
202
|
+
unitId: action.unitId,
|
|
203
|
+
prompt: action.prompt,
|
|
204
|
+
pauseAfterUatDispatch: action.pauseAfterDispatch ?? false,
|
|
205
|
+
state,
|
|
206
|
+
mid: active.id,
|
|
207
|
+
midTitle: active.title,
|
|
208
|
+
};
|
|
209
|
+
session.pendingOrchestrationDispatch = pending;
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
unitType: action.unitType,
|
|
213
|
+
unitId: action.unitId,
|
|
214
|
+
reason: action.matchedRule ?? "dispatch",
|
|
215
|
+
preconditions: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
21
218
|
export class AutoOrchestrator {
|
|
22
219
|
status = {
|
|
23
220
|
phase: "idle",
|
|
24
221
|
transitionCount: 0,
|
|
25
222
|
};
|
|
26
|
-
|
|
223
|
+
ctx;
|
|
224
|
+
pi;
|
|
225
|
+
dispatchBasePath;
|
|
226
|
+
runtimeBasePath;
|
|
227
|
+
s;
|
|
228
|
+
flowId;
|
|
229
|
+
seq = 0;
|
|
27
230
|
lastAdvanceKey = null;
|
|
28
231
|
lastFinalizedUnitKey = null;
|
|
29
232
|
dispatchKeyWindow = [];
|
|
30
|
-
|
|
31
|
-
|
|
233
|
+
// #442: the unit key we last attempted graduated stuck-recovery for. Bounds
|
|
234
|
+
// recovery to one attempt per stuck episode per run (reset on start/resume/
|
|
235
|
+
// stop), mirroring the legacy Level-1-then-Level-2 escalation in phases.ts.
|
|
236
|
+
lastStuckRecoveryKey = null;
|
|
237
|
+
constructor(context) {
|
|
238
|
+
this.ctx = context.ctx;
|
|
239
|
+
this.pi = context.pi;
|
|
240
|
+
this.dispatchBasePath = context.dispatchBasePath;
|
|
241
|
+
this.runtimeBasePath = context.runtimeBasePath;
|
|
242
|
+
this.s = context.session;
|
|
243
|
+
this.flowId = `auto-orchestrator-${Date.now()}`;
|
|
244
|
+
}
|
|
245
|
+
// ── Live base-path resolution (was the wiring factory's getLiveDispatchBasePath) ──
|
|
246
|
+
getLiveDispatchBasePath() {
|
|
247
|
+
return resolveLiveOrchestratorBasePath({
|
|
248
|
+
capturedBasePath: this.dispatchBasePath,
|
|
249
|
+
runtimeBasePath: this.runtimeBasePath,
|
|
250
|
+
sessionBasePath: this.s.basePath,
|
|
251
|
+
originalBasePath: this.s.originalBasePath,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// ── RuntimePersistenceAdapter (folded) ───────────────────────────────────
|
|
255
|
+
ensureLockOwnership() {
|
|
256
|
+
const status = getSessionLockStatus(this.runtimeBasePath);
|
|
257
|
+
if (!status.valid || status.failureReason === "pid-mismatch") {
|
|
258
|
+
throw new Error("session lock held by another process");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Map an orchestrator lifecycle event name to its journal eventType and emit
|
|
263
|
+
* it. The name→eventType ternary is preserved byte-for-byte from the legacy
|
|
264
|
+
* wired RuntimePersistenceAdapter.journalTransition.
|
|
265
|
+
*/
|
|
266
|
+
journalTransition(event) {
|
|
267
|
+
const eventType = event.name === "start"
|
|
268
|
+
? "orchestrator-iteration-start"
|
|
269
|
+
: event.name === "resume"
|
|
270
|
+
? "orchestrator-iteration-start"
|
|
271
|
+
: event.name === "advance"
|
|
272
|
+
? "orchestrator-dispatch-match"
|
|
273
|
+
: event.name === "advance-blocked"
|
|
274
|
+
? "orchestrator-guard-block"
|
|
275
|
+
: event.name === "advance-stopped"
|
|
276
|
+
? "orchestrator-dispatch-stop"
|
|
277
|
+
: event.name === "advance-error"
|
|
278
|
+
? "orchestrator-iteration-end"
|
|
279
|
+
: event.name === "advance-paused" || event.name === "advance-retry"
|
|
280
|
+
? "orchestrator-guard-block"
|
|
281
|
+
: event.name === "stop"
|
|
282
|
+
? "orchestrator-terminal"
|
|
283
|
+
: "orchestrator-iteration-end";
|
|
284
|
+
_emitJournalEvent(this.runtimeBasePath, {
|
|
285
|
+
ts: new Date().toISOString(),
|
|
286
|
+
flowId: this.flowId,
|
|
287
|
+
seq: ++this.seq,
|
|
288
|
+
eventType,
|
|
289
|
+
data: {
|
|
290
|
+
source: "auto-orchestrator",
|
|
291
|
+
name: event.name,
|
|
292
|
+
reason: event.reason,
|
|
293
|
+
unitType: event.unitType,
|
|
294
|
+
unitId: event.unitId,
|
|
295
|
+
},
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
// ── NotificationAdapter (folded) ─────────────────────────────────────────
|
|
299
|
+
notifyLifecycle(event) {
|
|
300
|
+
if (event.name === "error") {
|
|
301
|
+
this.ctx.ui.notify(event.detail ?? "auto orchestration error", "error");
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
// ── HealthAdapter (folded) ───────────────────────────────────────────────
|
|
305
|
+
checkResourcesStale() {
|
|
306
|
+
return checkResourcesStale(this.s.resourceVersionOnStart);
|
|
307
|
+
}
|
|
308
|
+
async preAdvanceGate() {
|
|
309
|
+
try {
|
|
310
|
+
const gate = await preDispatchHealthGate(this.getLiveDispatchBasePath());
|
|
311
|
+
if (gate.proceed) {
|
|
312
|
+
return {
|
|
313
|
+
kind: "pass",
|
|
314
|
+
fixesApplied: gate.fixesApplied,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
kind: "fail",
|
|
319
|
+
reason: gate.reason ?? "Pre-dispatch health check failed — run /gsd doctor for details.",
|
|
320
|
+
action: gate.severity ?? "pause",
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
catch (error) {
|
|
324
|
+
return { kind: "threw", error };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
postAdvanceRecord(result) {
|
|
328
|
+
if (result.kind === "error") {
|
|
329
|
+
recordHealthSnapshot(1, 0, 0, [{
|
|
330
|
+
code: "orchestration-error",
|
|
331
|
+
message: result.reason ?? "orchestration error",
|
|
332
|
+
severity: "error",
|
|
333
|
+
unitId: "orchestration",
|
|
334
|
+
}], [], "orchestration");
|
|
335
|
+
}
|
|
336
|
+
else if (result.kind === "blocked") {
|
|
337
|
+
recordHealthSnapshot(0, 1, 0, [{
|
|
338
|
+
code: "orchestration-blocked",
|
|
339
|
+
message: result.reason ?? "orchestration blocked",
|
|
340
|
+
severity: "warning",
|
|
341
|
+
unitId: "orchestration",
|
|
342
|
+
}], [], "orchestration");
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// ── UokGateAdapter (folded) ──────────────────────────────────────────────
|
|
346
|
+
async emitUokGate(input) {
|
|
347
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
348
|
+
const prefs = loadEffectiveGSDPreferences(activeBasePath)?.preferences;
|
|
349
|
+
const uokFlags = resolveUokFlags(prefs);
|
|
350
|
+
if (!uokFlags.gates)
|
|
351
|
+
return;
|
|
352
|
+
const milestoneId = input.milestoneId ?? this.s.currentMilestoneId ?? undefined;
|
|
353
|
+
try {
|
|
354
|
+
const { UokGateRunner } = await import("../uok/gate-runner.js");
|
|
355
|
+
const runner = new UokGateRunner();
|
|
356
|
+
runner.register({
|
|
357
|
+
id: input.gateId,
|
|
358
|
+
type: input.gateType,
|
|
359
|
+
execute: async () => ({
|
|
360
|
+
outcome: input.outcome,
|
|
361
|
+
failureClass: input.failureClass,
|
|
362
|
+
rationale: input.rationale,
|
|
363
|
+
findings: input.findings ?? "",
|
|
364
|
+
}),
|
|
365
|
+
});
|
|
366
|
+
await runner.run(input.gateId, {
|
|
367
|
+
basePath: activeBasePath,
|
|
368
|
+
traceId: `pre-dispatch:${this.flowId}`,
|
|
369
|
+
turnId: `orch-${this.seq}`,
|
|
370
|
+
milestoneId,
|
|
371
|
+
unitType: "pre-dispatch",
|
|
372
|
+
unitId: `orch-${this.seq}`,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
logWarning("engine", `uok gate emit failed: ${getErrorMessage(err)}`, {
|
|
377
|
+
file: "orchestrator.ts",
|
|
378
|
+
gateId: input.gateId,
|
|
379
|
+
gateType: input.gateType,
|
|
380
|
+
...(milestoneId ? { milestoneId } : {}),
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// ── StateReconciliationAdapter (folded) ──────────────────────────────────
|
|
385
|
+
async reconcileBeforeDispatch() {
|
|
386
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
387
|
+
const result = await reconcileBeforeDispatch(activeBasePath);
|
|
388
|
+
// Failure-path summaries written by gsd_summary_save create
|
|
389
|
+
// artifact-db-status-divergence blockers for tasks that are still
|
|
390
|
+
// pending (gsd_task_complete never ran). These tasks can still be
|
|
391
|
+
// dispatched and the drift self-heals once they complete successfully.
|
|
392
|
+
const hardBlockers = result.blockers.filter((b) => !b.includes("has SUMMARY artifact while DB status is") &&
|
|
393
|
+
!b.includes("has SUMMARY on disk while DB status is") &&
|
|
394
|
+
!b.includes("has task SUMMARY artifacts but no DB tasks"));
|
|
395
|
+
if (hardBlockers.length > 0) {
|
|
396
|
+
return {
|
|
397
|
+
ok: false,
|
|
398
|
+
reason: hardBlockers[0],
|
|
399
|
+
stateSnapshot: result.stateSnapshot,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const repairedKinds = result.repaired.map((d) => d.kind);
|
|
403
|
+
return {
|
|
404
|
+
ok: true,
|
|
405
|
+
reason: repairedKinds.length > 0
|
|
406
|
+
? `repaired: ${repairedKinds.join(", ")}`
|
|
407
|
+
: "clean",
|
|
408
|
+
stateSnapshot: result.stateSnapshot,
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
// ── DispatchAdapter (folded) ─────────────────────────────────────────────
|
|
412
|
+
decideNextUnit(input) {
|
|
413
|
+
return decideOrchestratorDispatch(this.ctx, this.pi, this.dispatchBasePath, this.s, input);
|
|
414
|
+
}
|
|
415
|
+
// ── ToolContractAdapter (folded) ─────────────────────────────────────────
|
|
416
|
+
compileUnitToolContract(unitType) {
|
|
417
|
+
const result = compileUnitToolContract(unitType);
|
|
418
|
+
if (!result.ok)
|
|
419
|
+
return { ok: false, reason: result.detail };
|
|
420
|
+
return { ok: true, reason: result.contract.validationRules.join(", ") };
|
|
421
|
+
}
|
|
422
|
+
// ── WorktreeAdapter (folded) ─────────────────────────────────────────────
|
|
423
|
+
getEffectiveUnitIsolationMode(basePath) {
|
|
424
|
+
const configuredMode = getIsolationMode(basePath);
|
|
425
|
+
return configuredMode === "worktree" && this.s.isolationDegraded ? "branch" : configuredMode;
|
|
426
|
+
}
|
|
427
|
+
buildLifecycle() {
|
|
428
|
+
return new WorktreeLifecycle(this.s, {
|
|
429
|
+
gitServiceFactory: (basePath) => {
|
|
430
|
+
const gitConfig = loadEffectiveGSDPreferences()?.preferences?.git ?? {};
|
|
431
|
+
return new GitServiceImpl(basePath, gitConfig);
|
|
432
|
+
},
|
|
433
|
+
worktreeProjection: new WorktreeStateProjection(),
|
|
434
|
+
mergeMilestoneToMain,
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
rebuildScope(rawPath, milestoneId) {
|
|
438
|
+
if (!milestoneId) {
|
|
439
|
+
this.s.scope = null;
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
const workspace = createWorkspace(rawPath);
|
|
444
|
+
this.s.scope = scopeMilestone(workspace, milestoneId);
|
|
445
|
+
}
|
|
446
|
+
catch {
|
|
447
|
+
// Non-fatal — scope is additive. Existing readers still use basePath.
|
|
448
|
+
this.s.scope = null;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
async prepareWorktreeForUnit(unitType, unitId) {
|
|
452
|
+
const isolationMode = this.getEffectiveUnitIsolationMode(this.runtimeBasePath);
|
|
453
|
+
const manifest = resolveManifest(unitType);
|
|
454
|
+
if (!manifest) {
|
|
455
|
+
return {
|
|
456
|
+
ok: false,
|
|
457
|
+
reason: `No Unit manifest is registered for ${unitType}`,
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
if (isolationMode !== "worktree") {
|
|
461
|
+
return { ok: true, reason: "not-required" };
|
|
462
|
+
}
|
|
463
|
+
const writeScope = manifest.tools.mode === "all" || manifest.tools.mode === "docs"
|
|
464
|
+
? "source-writing"
|
|
465
|
+
: "planning-only";
|
|
466
|
+
const safety = createWorktreeSafetyModule();
|
|
467
|
+
const activeBasePath = this.getLiveDispatchBasePath();
|
|
468
|
+
const snapshot = await deriveState(activeBasePath);
|
|
469
|
+
const milestoneId = snapshot.activeMilestone?.id ?? null;
|
|
470
|
+
const expectedBranch = milestoneId ? autoWorktreeBranch(milestoneId) : null;
|
|
471
|
+
let result = safety.validateUnitRoot({
|
|
472
|
+
unitType,
|
|
473
|
+
unitId,
|
|
474
|
+
writeScope,
|
|
475
|
+
projectRoot: this.runtimeBasePath,
|
|
476
|
+
unitRoot: activeBasePath,
|
|
477
|
+
milestoneId,
|
|
478
|
+
isolationMode,
|
|
479
|
+
expectedBranch,
|
|
480
|
+
});
|
|
481
|
+
if (!result.ok) {
|
|
482
|
+
const repaired = await repairAutoWorktreeSafetyFailure({
|
|
483
|
+
safetyResult: result,
|
|
484
|
+
projectRoot: this.runtimeBasePath,
|
|
485
|
+
activeRoot: activeBasePath,
|
|
486
|
+
milestoneId,
|
|
487
|
+
enterMilestone: async (id) => {
|
|
488
|
+
this.buildLifecycle().adoptSessionRoot(this.runtimeBasePath, this.s.originalBasePath || this.runtimeBasePath);
|
|
489
|
+
const enterResult = this.buildLifecycle().enterMilestone(id, {
|
|
490
|
+
notify: this.ctx.ui.notify.bind(this.ctx.ui),
|
|
491
|
+
});
|
|
492
|
+
if (!enterResult.ok)
|
|
493
|
+
return { ok: false, reason: enterResult.reason };
|
|
494
|
+
this.rebuildScope(this.s.basePath, this.s.currentMilestoneId);
|
|
495
|
+
return { ok: true };
|
|
496
|
+
},
|
|
497
|
+
revalidate: () => safety.validateUnitRoot({
|
|
498
|
+
unitType,
|
|
499
|
+
unitId,
|
|
500
|
+
writeScope,
|
|
501
|
+
projectRoot: this.runtimeBasePath,
|
|
502
|
+
unitRoot: this.getLiveDispatchBasePath(),
|
|
503
|
+
milestoneId,
|
|
504
|
+
isolationMode: this.getEffectiveUnitIsolationMode(this.runtimeBasePath),
|
|
505
|
+
expectedBranch,
|
|
506
|
+
}),
|
|
507
|
+
});
|
|
508
|
+
result = repaired.result;
|
|
509
|
+
if (result.ok) {
|
|
510
|
+
return { ok: true, reason: repaired.repaired ? `repaired-${result.kind}` : result.kind };
|
|
511
|
+
}
|
|
512
|
+
const repairDetail = repaired.repairReason
|
|
513
|
+
? ` (repair skipped: ${repaired.repairReason})`
|
|
514
|
+
: "";
|
|
515
|
+
return { ok: false, reason: `${result.kind}: ${result.reason}${repairDetail}` };
|
|
516
|
+
}
|
|
517
|
+
return { ok: true, reason: result.kind };
|
|
518
|
+
}
|
|
519
|
+
// ── RecoveryAdapter (folded) ─────────────────────────────────────────────
|
|
520
|
+
classifyAndRecover(input) {
|
|
521
|
+
const recovery = classifyFailure(input);
|
|
522
|
+
return { action: recovery.action, reason: recovery.reason };
|
|
523
|
+
}
|
|
524
|
+
// ── Lifecycle verbs ──────────────────────────────────────────────────────
|
|
525
|
+
/**
|
|
526
|
+
* #442: graduated stuck recovery, ported from the legacy
|
|
527
|
+
* auto/phases.ts:runDispatch path that Phase 3 retires. The ring-buffer
|
|
528
|
+
* hard-stops (stuck-loop saturation and finalized-repeat) would otherwise
|
|
529
|
+
* KILL a unit that actually completed on disk but whose DB row is still
|
|
530
|
+
* stale. Before hard-stopping, verify the expected artifact exists; if so,
|
|
531
|
+
* refresh the DB from it, invalidate caches and reset the dispatch ring so
|
|
532
|
+
* the next advance picks the correct next unit. Bounded to one attempt per
|
|
533
|
+
* stuck key per episode (reset on lifecycle + genuine finalize) to avoid an
|
|
534
|
+
* unbounded recover→re-saturate→recover loop — mirrors the legacy
|
|
535
|
+
* Level-1-recover-then-Level-2-hard-stop escalation.
|
|
536
|
+
*
|
|
537
|
+
* Returns true when recovery succeeded; the caller should re-loop (return a
|
|
538
|
+
* skipped result) instead of stopping.
|
|
539
|
+
*/
|
|
540
|
+
tryStuckArtifactRecovery(unitType, unitId) {
|
|
541
|
+
const key = `${unitType}:${unitId}`;
|
|
542
|
+
if (this.lastStuckRecoveryKey === key)
|
|
543
|
+
return false; // already tried this episode
|
|
544
|
+
const basePath = this.getLiveDispatchBasePath();
|
|
545
|
+
if (!verifyExpectedArtifact(unitType, unitId, basePath))
|
|
546
|
+
return false;
|
|
547
|
+
const refreshed = refreshRecoveryDbForArtifact(unitType, unitId, basePath);
|
|
548
|
+
// Fatal failures cannot be recovered — hard-stop. Non-fatal (e.g. plan-slice
|
|
549
|
+
// DB refresh hiccup) still fall through: invalidating caches and resetting
|
|
550
|
+
// the ring gives the next advance a clean slate to pick up the correct state,
|
|
551
|
+
// mirroring the legacy Level-1 "continue" escalation path.
|
|
552
|
+
if (!refreshed.ok && refreshed.fatal)
|
|
553
|
+
return false;
|
|
554
|
+
this.lastStuckRecoveryKey = key;
|
|
555
|
+
invalidateAllCaches();
|
|
556
|
+
this.dispatchKeyWindow = [];
|
|
557
|
+
this.lastAdvanceKey = null;
|
|
558
|
+
this.lastFinalizedUnitKey = null;
|
|
559
|
+
return true;
|
|
560
|
+
}
|
|
561
|
+
stuckRecovered(decision, stateSnapshot) {
|
|
562
|
+
const recovered = {
|
|
563
|
+
kind: "skipped",
|
|
564
|
+
reason: `stuck-recovery: ${decision.unitType} ${decision.unitId} artifact found on disk; DB refreshed`,
|
|
565
|
+
stateSnapshot,
|
|
566
|
+
};
|
|
567
|
+
this.status.phase = "running";
|
|
568
|
+
this.status.activeUnit = undefined;
|
|
569
|
+
this.bumpTransition();
|
|
570
|
+
this.journalTransition({
|
|
571
|
+
name: "advance-skipped",
|
|
572
|
+
reason: recovered.reason,
|
|
573
|
+
unitType: decision.unitType,
|
|
574
|
+
unitId: decision.unitId,
|
|
575
|
+
});
|
|
576
|
+
this.postAdvanceRecord(recovered);
|
|
577
|
+
return recovered;
|
|
32
578
|
}
|
|
33
579
|
async start(_sessionContext) {
|
|
34
580
|
this.lastAdvanceKey = null;
|
|
35
581
|
this.lastFinalizedUnitKey = null;
|
|
36
582
|
this.dispatchKeyWindow = [];
|
|
583
|
+
this.lastStuckRecoveryKey = null;
|
|
37
584
|
this.status.phase = "running";
|
|
38
585
|
this.bumpTransition();
|
|
39
|
-
|
|
40
|
-
|
|
586
|
+
this.journalTransition({ name: "start" });
|
|
587
|
+
this.notifyLifecycle({ name: "start" });
|
|
41
588
|
return { kind: "started" };
|
|
42
589
|
}
|
|
43
590
|
async advance() {
|
|
591
|
+
debugCount("dispatches");
|
|
592
|
+
const stopAdvanceTimer = debugTime("orchestrator-advance");
|
|
44
593
|
try {
|
|
45
|
-
|
|
46
|
-
const staleMsg = this.
|
|
594
|
+
this.ensureLockOwnership();
|
|
595
|
+
const staleMsg = this.checkResourcesStale();
|
|
47
596
|
if (staleMsg) {
|
|
48
|
-
await this.
|
|
597
|
+
await this.emitUokGate({
|
|
49
598
|
gateId: "resource-version-guard",
|
|
50
599
|
gateType: "policy",
|
|
51
600
|
outcome: "fail",
|
|
@@ -54,20 +603,20 @@ export class AutoOrchestrator {
|
|
|
54
603
|
findings: staleMsg,
|
|
55
604
|
});
|
|
56
605
|
const blocked = { kind: "blocked", reason: staleMsg, action: "pause" };
|
|
57
|
-
|
|
58
|
-
|
|
606
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
607
|
+
this.postAdvanceRecord(blocked);
|
|
59
608
|
return blocked;
|
|
60
609
|
}
|
|
61
|
-
await this.
|
|
610
|
+
await this.emitUokGate({
|
|
62
611
|
gateId: "resource-version-guard",
|
|
63
612
|
gateType: "policy",
|
|
64
613
|
outcome: "pass",
|
|
65
614
|
failureClass: "none",
|
|
66
615
|
rationale: "resource version guard passed",
|
|
67
616
|
});
|
|
68
|
-
const gate = await this.
|
|
617
|
+
const gate = await this.preAdvanceGate();
|
|
69
618
|
if (gate.kind === "fail") {
|
|
70
|
-
await this.
|
|
619
|
+
await this.emitUokGate({
|
|
71
620
|
gateId: "pre-dispatch-health-gate",
|
|
72
621
|
gateType: "execution",
|
|
73
622
|
outcome: "manual-attention",
|
|
@@ -80,12 +629,12 @@ export class AutoOrchestrator {
|
|
|
80
629
|
reason: gate.reason,
|
|
81
630
|
action: gate.action ?? "pause",
|
|
82
631
|
};
|
|
83
|
-
|
|
84
|
-
|
|
632
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
633
|
+
this.postAdvanceRecord(blocked);
|
|
85
634
|
return blocked;
|
|
86
635
|
}
|
|
87
636
|
if (gate.kind === "threw") {
|
|
88
|
-
await this.
|
|
637
|
+
await this.emitUokGate({
|
|
89
638
|
gateId: "pre-dispatch-health-gate",
|
|
90
639
|
gateType: "execution",
|
|
91
640
|
outcome: "manual-attention",
|
|
@@ -96,7 +645,7 @@ export class AutoOrchestrator {
|
|
|
96
645
|
// intentional fall-through: matches runPreDispatch behaviour
|
|
97
646
|
}
|
|
98
647
|
else {
|
|
99
|
-
await this.
|
|
648
|
+
await this.emitUokGate({
|
|
100
649
|
gateId: "pre-dispatch-health-gate",
|
|
101
650
|
gateType: "execution",
|
|
102
651
|
outcome: "pass",
|
|
@@ -105,7 +654,7 @@ export class AutoOrchestrator {
|
|
|
105
654
|
findings: gate.fixesApplied?.join(", ") ?? "",
|
|
106
655
|
});
|
|
107
656
|
}
|
|
108
|
-
const reconciliation = await this.
|
|
657
|
+
const reconciliation = await this.reconcileBeforeDispatch();
|
|
109
658
|
if (!reconciliation.ok || !reconciliation.stateSnapshot) {
|
|
110
659
|
const blocked = {
|
|
111
660
|
kind: "blocked",
|
|
@@ -113,11 +662,11 @@ export class AutoOrchestrator {
|
|
|
113
662
|
action: "pause",
|
|
114
663
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
115
664
|
};
|
|
116
|
-
|
|
117
|
-
|
|
665
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
666
|
+
this.postAdvanceRecord(blocked);
|
|
118
667
|
return blocked;
|
|
119
668
|
}
|
|
120
|
-
const decision = await this.
|
|
669
|
+
const decision = await this.decideNextUnit({ stateSnapshot: reconciliation.stateSnapshot });
|
|
121
670
|
if (!decision) {
|
|
122
671
|
const stopped = {
|
|
123
672
|
kind: "stopped",
|
|
@@ -129,8 +678,8 @@ export class AutoOrchestrator {
|
|
|
129
678
|
this.lastAdvanceKey = null;
|
|
130
679
|
this.dispatchKeyWindow = [];
|
|
131
680
|
this.bumpTransition();
|
|
132
|
-
|
|
133
|
-
|
|
681
|
+
this.journalTransition({ name: "advance-stopped", reason: stopped.reason });
|
|
682
|
+
this.postAdvanceRecord(stopped);
|
|
134
683
|
return stopped;
|
|
135
684
|
}
|
|
136
685
|
if ("kind" in decision && decision.kind === "skipped") {
|
|
@@ -142,8 +691,8 @@ export class AutoOrchestrator {
|
|
|
142
691
|
this.status.phase = "running";
|
|
143
692
|
this.status.activeUnit = undefined;
|
|
144
693
|
this.bumpTransition();
|
|
145
|
-
|
|
146
|
-
|
|
694
|
+
this.journalTransition({ name: "advance-skipped", reason: skipped.reason });
|
|
695
|
+
this.postAdvanceRecord(skipped);
|
|
147
696
|
return skipped;
|
|
148
697
|
}
|
|
149
698
|
if (!("unitType" in decision)) {
|
|
@@ -153,8 +702,8 @@ export class AutoOrchestrator {
|
|
|
153
702
|
action: decision.action,
|
|
154
703
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
155
704
|
};
|
|
156
|
-
|
|
157
|
-
|
|
705
|
+
this.journalTransition({ name: "advance-blocked", reason: blocked.reason });
|
|
706
|
+
this.postAdvanceRecord(blocked);
|
|
158
707
|
return blocked;
|
|
159
708
|
}
|
|
160
709
|
const nextKey = `${decision.unitType}:${decision.unitId}`;
|
|
@@ -168,19 +717,25 @@ export class AutoOrchestrator {
|
|
|
168
717
|
}
|
|
169
718
|
const matchingCount = this.dispatchKeyWindow.filter((k) => k === nextKey).length;
|
|
170
719
|
if (this.lastFinalizedUnitKey === nextKey) {
|
|
720
|
+
// #442: the unit re-dispatched immediately after finalizing may have
|
|
721
|
+
// actually completed on disk with a stale DB. Verify + recover before
|
|
722
|
+
// hard-stopping (legacy graduated stuck-recovery parity).
|
|
723
|
+
if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
|
|
724
|
+
return this.stuckRecovered(decision, reconciliation.stateSnapshot);
|
|
725
|
+
}
|
|
171
726
|
const blocked = {
|
|
172
727
|
kind: "blocked",
|
|
173
728
|
reason: `state did not advance after finalized ${decision.unitType} ${decision.unitId}`,
|
|
174
729
|
action: "stop",
|
|
175
730
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
176
731
|
};
|
|
177
|
-
|
|
732
|
+
this.journalTransition({
|
|
178
733
|
name: "advance-blocked",
|
|
179
734
|
reason: blocked.reason,
|
|
180
735
|
unitType: decision.unitType,
|
|
181
736
|
unitId: decision.unitId,
|
|
182
737
|
});
|
|
183
|
-
|
|
738
|
+
this.postAdvanceRecord(blocked);
|
|
184
739
|
return blocked;
|
|
185
740
|
}
|
|
186
741
|
// Idempotency: same key as immediately previous successful advance.
|
|
@@ -191,13 +746,13 @@ export class AutoOrchestrator {
|
|
|
191
746
|
// stuck-loop for the saturated-window case.
|
|
192
747
|
if (this.lastAdvanceKey === nextKey && matchingCount < STUCK_WINDOW_SIZE) {
|
|
193
748
|
const blocked = { kind: "blocked", reason: "idempotent advance: unit already active", action: "pause" };
|
|
194
|
-
|
|
749
|
+
this.journalTransition({
|
|
195
750
|
name: "advance-blocked",
|
|
196
751
|
reason: blocked.reason,
|
|
197
752
|
unitType: decision.unitType,
|
|
198
753
|
unitId: decision.unitId,
|
|
199
754
|
});
|
|
200
|
-
|
|
755
|
+
this.postAdvanceRecord(blocked);
|
|
201
756
|
return blocked;
|
|
202
757
|
}
|
|
203
758
|
// Stuck-loop detection: when the ring is saturated with copies of
|
|
@@ -205,21 +760,27 @@ export class AutoOrchestrator {
|
|
|
205
760
|
// picking the same unit across the whole window and must hard-stop with
|
|
206
761
|
// a diagnosable reason.
|
|
207
762
|
if (matchingCount >= STUCK_WINDOW_SIZE) {
|
|
763
|
+
// #442: before declaring a stuck loop, verify the unit didn't actually
|
|
764
|
+
// complete on disk (stale DB) and recover if so — legacy graduated
|
|
765
|
+
// stuck-recovery parity. Otherwise hard-stop with a diagnosable reason.
|
|
766
|
+
if (this.tryStuckArtifactRecovery(decision.unitType, decision.unitId)) {
|
|
767
|
+
return this.stuckRecovered(decision, reconciliation.stateSnapshot);
|
|
768
|
+
}
|
|
208
769
|
const blocked = {
|
|
209
770
|
kind: "blocked",
|
|
210
771
|
reason: `stuck-loop: ${nextKey} picked ${matchingCount} times`,
|
|
211
772
|
action: "stop",
|
|
212
773
|
};
|
|
213
|
-
|
|
774
|
+
this.journalTransition({
|
|
214
775
|
name: "advance-blocked",
|
|
215
776
|
reason: blocked.reason,
|
|
216
777
|
unitType: decision.unitType,
|
|
217
778
|
unitId: decision.unitId,
|
|
218
779
|
});
|
|
219
|
-
|
|
780
|
+
this.postAdvanceRecord(blocked);
|
|
220
781
|
return blocked;
|
|
221
782
|
}
|
|
222
|
-
const contract =
|
|
783
|
+
const contract = this.compileUnitToolContract(decision.unitType);
|
|
223
784
|
if (!contract.ok) {
|
|
224
785
|
const blocked = {
|
|
225
786
|
kind: "blocked",
|
|
@@ -227,16 +788,16 @@ export class AutoOrchestrator {
|
|
|
227
788
|
action: "pause",
|
|
228
789
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
229
790
|
};
|
|
230
|
-
|
|
791
|
+
this.journalTransition({
|
|
231
792
|
name: "advance-blocked",
|
|
232
793
|
reason: blocked.reason,
|
|
233
794
|
unitType: decision.unitType,
|
|
234
795
|
unitId: decision.unitId,
|
|
235
796
|
});
|
|
236
|
-
|
|
797
|
+
this.postAdvanceRecord(blocked);
|
|
237
798
|
return blocked;
|
|
238
799
|
}
|
|
239
|
-
const worktree = await this.
|
|
800
|
+
const worktree = await this.prepareWorktreeForUnit(decision.unitType, decision.unitId);
|
|
240
801
|
if (!worktree.ok) {
|
|
241
802
|
const blocked = {
|
|
242
803
|
kind: "blocked",
|
|
@@ -244,36 +805,36 @@ export class AutoOrchestrator {
|
|
|
244
805
|
action: "pause",
|
|
245
806
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
246
807
|
};
|
|
247
|
-
|
|
808
|
+
this.journalTransition({
|
|
248
809
|
name: "advance-blocked",
|
|
249
810
|
reason: blocked.reason,
|
|
250
811
|
unitType: decision.unitType,
|
|
251
812
|
unitId: decision.unitId,
|
|
252
813
|
});
|
|
253
|
-
|
|
814
|
+
this.postAdvanceRecord(blocked);
|
|
254
815
|
return blocked;
|
|
255
816
|
}
|
|
256
817
|
this.status.activeUnit = { unitType: decision.unitType, unitId: decision.unitId };
|
|
257
818
|
this.status.phase = "running";
|
|
258
819
|
this.lastAdvanceKey = nextKey;
|
|
259
820
|
this.bumpTransition();
|
|
260
|
-
|
|
821
|
+
this.journalTransition({
|
|
261
822
|
name: "advance",
|
|
262
823
|
reason: decision.reason,
|
|
263
824
|
unitType: decision.unitType,
|
|
264
825
|
unitId: decision.unitId,
|
|
265
826
|
});
|
|
266
|
-
|
|
827
|
+
// syncAfterUnit was a no-op in the wired WorktreeAdapter.
|
|
267
828
|
const advanced = {
|
|
268
829
|
kind: "advanced",
|
|
269
830
|
unit: { unitType: decision.unitType, unitId: decision.unitId },
|
|
270
831
|
stateSnapshot: reconciliation.stateSnapshot,
|
|
271
832
|
};
|
|
272
|
-
|
|
833
|
+
this.postAdvanceRecord(advanced);
|
|
273
834
|
return advanced;
|
|
274
835
|
}
|
|
275
836
|
catch (error) {
|
|
276
|
-
const recovery =
|
|
837
|
+
const recovery = this.classifyAndRecover({
|
|
277
838
|
error,
|
|
278
839
|
unitType: this.status.activeUnit?.unitType,
|
|
279
840
|
unitId: this.status.activeUnit?.unitId,
|
|
@@ -304,43 +865,48 @@ export class AutoOrchestrator {
|
|
|
304
865
|
: result.kind === "stopped"
|
|
305
866
|
? "advance-stopped"
|
|
306
867
|
: "advance-error";
|
|
307
|
-
|
|
868
|
+
this.journalTransition({ name: journalName, reason: recovery.reason });
|
|
308
869
|
if (result.kind === "paused") {
|
|
309
|
-
|
|
870
|
+
this.notifyLifecycle({ name: "pause", detail: recovery.reason });
|
|
310
871
|
}
|
|
311
872
|
else if (result.kind === "stopped") {
|
|
312
|
-
|
|
873
|
+
this.notifyLifecycle({ name: "stopped", detail: recovery.reason });
|
|
313
874
|
}
|
|
314
875
|
else if (result.kind === "error") {
|
|
315
|
-
|
|
876
|
+
this.notifyLifecycle({ name: "error", detail: recovery.reason });
|
|
316
877
|
}
|
|
317
|
-
|
|
878
|
+
this.postAdvanceRecord(result);
|
|
318
879
|
return result;
|
|
319
880
|
}
|
|
881
|
+
finally {
|
|
882
|
+
stopAdvanceTimer();
|
|
883
|
+
}
|
|
320
884
|
}
|
|
321
885
|
async resume() {
|
|
322
886
|
this.lastAdvanceKey = null;
|
|
323
887
|
this.lastFinalizedUnitKey = null;
|
|
324
888
|
this.dispatchKeyWindow = [];
|
|
889
|
+
this.lastStuckRecoveryKey = null;
|
|
325
890
|
this.status.phase = "running";
|
|
326
891
|
this.bumpTransition();
|
|
327
|
-
|
|
328
|
-
|
|
892
|
+
this.journalTransition({ name: "resume" });
|
|
893
|
+
this.notifyLifecycle({ name: "resume" });
|
|
329
894
|
return { kind: "resumed" };
|
|
330
895
|
}
|
|
331
896
|
async stop(reason) {
|
|
332
897
|
if (this.status.phase === "stopped") {
|
|
333
898
|
return { kind: "stopped", reason };
|
|
334
899
|
}
|
|
335
|
-
|
|
900
|
+
// cleanupOnStop was a no-op in the wired WorktreeAdapter.
|
|
336
901
|
this.status.phase = "stopped";
|
|
337
902
|
this.status.activeUnit = undefined;
|
|
338
903
|
this.lastAdvanceKey = null;
|
|
339
904
|
this.lastFinalizedUnitKey = null;
|
|
340
905
|
this.dispatchKeyWindow = [];
|
|
906
|
+
this.lastStuckRecoveryKey = null;
|
|
341
907
|
this.bumpTransition();
|
|
342
|
-
|
|
343
|
-
|
|
908
|
+
this.journalTransition({ name: "stop", reason });
|
|
909
|
+
this.notifyLifecycle({ name: "stop", detail: reason });
|
|
344
910
|
return { kind: "stopped", reason };
|
|
345
911
|
}
|
|
346
912
|
getStatus() {
|
|
@@ -356,8 +922,10 @@ export class AutoOrchestrator {
|
|
|
356
922
|
this.status.activeUnit = undefined;
|
|
357
923
|
this.lastAdvanceKey = null;
|
|
358
924
|
this.lastFinalizedUnitKey = unitKey;
|
|
925
|
+
// Genuine progress — re-enable graduated stuck recovery for future episodes.
|
|
926
|
+
this.lastStuckRecoveryKey = null;
|
|
359
927
|
this.bumpTransition();
|
|
360
|
-
|
|
928
|
+
this.journalTransition({
|
|
361
929
|
name: "unit-finalized",
|
|
362
930
|
unitType: unit.unitType,
|
|
363
931
|
unitId: unit.unitId,
|
|
@@ -376,7 +944,7 @@ export class AutoOrchestrator {
|
|
|
376
944
|
this.lastAdvanceKey = null;
|
|
377
945
|
this.lastFinalizedUnitKey = null;
|
|
378
946
|
this.bumpTransition();
|
|
379
|
-
|
|
947
|
+
this.journalTransition({
|
|
380
948
|
name: "unit-retry",
|
|
381
949
|
reason: "finalize-retry",
|
|
382
950
|
unitType: unit.unitType,
|
|
@@ -388,6 +956,40 @@ export class AutoOrchestrator {
|
|
|
388
956
|
this.status.lastTransitionAt = now();
|
|
389
957
|
}
|
|
390
958
|
}
|
|
391
|
-
|
|
392
|
-
|
|
959
|
+
function isUsableLiveOrchestratorBasePath(basePath) {
|
|
960
|
+
if (!basePath || !existsSync(basePath))
|
|
961
|
+
return false;
|
|
962
|
+
if (!detectWorktreeName(basePath))
|
|
963
|
+
return true;
|
|
964
|
+
try {
|
|
965
|
+
return readFileSync(join(basePath, ".git"), "utf8").trim().startsWith("gitdir: ");
|
|
966
|
+
}
|
|
967
|
+
catch {
|
|
968
|
+
return false;
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
/**
|
|
972
|
+
* Resolve the base path the live orchestrator should dispatch from, falling
|
|
973
|
+
* back to the project root when the captured worktree path has been removed
|
|
974
|
+
* (e.g. after milestone-merge cleanup). Exported for the closeout-regression
|
|
975
|
+
* tests and reused by the orchestrator's getLiveDispatchBasePath.
|
|
976
|
+
*/
|
|
977
|
+
export function resolveLiveOrchestratorBasePath(input) {
|
|
978
|
+
const primary = input.sessionBasePath || input.capturedBasePath;
|
|
979
|
+
if (isUsableLiveOrchestratorBasePath(primary))
|
|
980
|
+
return primary;
|
|
981
|
+
const fallbacks = [
|
|
982
|
+
input.originalBasePath,
|
|
983
|
+
input.runtimeBasePath,
|
|
984
|
+
resolveProjectRoot(input.capturedBasePath),
|
|
985
|
+
];
|
|
986
|
+
for (const candidate of fallbacks) {
|
|
987
|
+
if (candidate && isUsableLiveOrchestratorBasePath(candidate)) {
|
|
988
|
+
return candidate;
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
return input.runtimeBasePath || input.capturedBasePath;
|
|
992
|
+
}
|
|
993
|
+
export function createAutoOrchestrator(context) {
|
|
994
|
+
return new AutoOrchestrator(context);
|
|
393
995
|
}
|