@fitlab-ai/agent-infra 0.9.7 → 0.9.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +11 -1
- package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
- package/dist/lib/decide.js +68 -14
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/init.js +1 -1
- package/dist/lib/internal/codex-lifecycle.js +19 -5
- package/dist/lib/internal/codex-sandbox-controller.js +6 -13
- package/dist/lib/internal/git-workflow.js +5 -7
- package/dist/lib/internal/platform-pr.js +20 -7
- package/dist/lib/internal/release-workflow.js +3 -4
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-activity.js +57 -7
- package/dist/lib/internal/task-event.js +57 -4
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-ledger.js +59 -5
- package/dist/lib/internal/task-lifecycle.js +59 -8
- package/dist/lib/internal/task-orchestration.js +161 -288
- package/dist/lib/internal/task-override.js +131 -0
- package/dist/lib/internal/task-review.js +64 -6
- package/dist/lib/internal/task-verify.js +65 -3
- package/dist/lib/internal/task-warning.js +57 -4
- package/dist/lib/platform/pr-summary.js +53 -13
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/runtime/agent-runtime.js +56 -0
- package/dist/lib/sandbox/commands/create.js +389 -311
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/container-identity.js +1 -1
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +145 -5
- package/dist/lib/sandbox/control/lifecycle.js +395 -73
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/control/state.js +27 -11
- package/dist/lib/sandbox/engine.js +14 -7
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/mounts.js +20 -5
- package/dist/lib/sandbox/recovery.js +78 -10
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/workspace-view.js +62 -40
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- package/dist/lib/server/process-state.js +51 -1
- package/dist/lib/task/activity-intent.js +4 -1
- package/dist/lib/task/codex-orchestration.js +158 -33
- package/dist/lib/task/commands/status.js +59 -1
- package/dist/lib/task/commit-operation.js +335 -0
- package/dist/lib/task/commit-policy.js +18 -0
- package/dist/lib/task/decision-intents.js +4 -2
- package/dist/lib/task/delegation-receipts.js +279 -17
- package/dist/lib/task/events.js +9 -5
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/guard-override.js +31 -0
- package/dist/lib/task/human-override.js +1240 -0
- package/dist/lib/task/ledger-intents.js +6 -3
- package/dist/lib/task/lifecycle.js +69 -14
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +199 -719
- package/dist/lib/task/review-finalization.js +3 -2
- package/dist/lib/task/short-id.js +59 -1
- package/dist/lib/task/task-execution-lock.js +6 -1
- package/dist/lib/task/verification-engine.js +138 -59
- package/dist/lib/task/verification.js +15 -2
- package/dist/lib/task/workflow-warning-intents.js +54 -3
- package/dist/lib/task/write.js +2 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/claude-code.ts +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
- package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
- package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
- package/lib/decide.ts +59 -14
- package/lib/git/workflow.ts +11 -2
- package/lib/init.ts +1 -1
- package/lib/internal/codex-lifecycle.ts +18 -5
- package/lib/internal/codex-sandbox-controller.ts +6 -12
- package/lib/internal/git-workflow.ts +5 -6
- package/lib/internal/platform-pr.ts +14 -7
- package/lib/internal/release-workflow.ts +3 -4
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-activity.ts +46 -6
- package/lib/internal/task-event.ts +53 -4
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-ledger.ts +48 -4
- package/lib/internal/task-lifecycle.ts +56 -12
- package/lib/internal/task-orchestration.ts +32 -107
- package/lib/internal/task-override.ts +138 -0
- package/lib/internal/task-review.ts +51 -6
- package/lib/internal/task-verify.ts +52 -3
- package/lib/internal/task-warning.ts +44 -3
- package/lib/platform/pr-summary.ts +65 -14
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/runtime/agent-runtime.ts +76 -0
- package/lib/sandbox/commands/create.ts +88 -4
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/config.ts +2 -1
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/container-identity.ts +3 -3
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +172 -6
- package/lib/sandbox/control/lifecycle.ts +465 -77
- package/lib/sandbox/control/protocol.ts +85 -9
- package/lib/sandbox/control/state.ts +35 -18
- package/lib/sandbox/engine.ts +20 -9
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/mounts.ts +23 -5
- package/lib/sandbox/recovery.ts +90 -9
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +73 -43
- package/lib/sandbox/worktree-safety.ts +236 -2
- package/lib/server/process-state.ts +48 -1
- package/lib/task/activity-intent.ts +4 -2
- package/lib/task/codex-orchestration.ts +167 -33
- package/lib/task/commands/status.ts +78 -1
- package/lib/task/commit-operation.ts +430 -0
- package/lib/task/commit-policy.ts +30 -0
- package/lib/task/decision-intents.ts +4 -2
- package/lib/task/delegation-receipts.ts +304 -19
- package/lib/task/events.ts +10 -5
- package/lib/task/finalization.ts +719 -0
- package/lib/task/guard-override.ts +50 -0
- package/lib/task/human-override.ts +1503 -0
- package/lib/task/ledger-intents.ts +6 -3
- package/lib/task/lifecycle.ts +71 -14
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +239 -922
- package/lib/task/review-finalization.ts +6 -2
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +145 -64
- package/lib/task/verification.ts +18 -3
- package/lib/task/workflow-warning-intents.ts +61 -4
- package/lib/task/write.ts +4 -1
- package/package.json +2 -2
- package/templates/.agents/QUICKSTART.en.md +2 -2
- package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
- package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
- package/templates/.agents/rules/task-management.en.md +2 -2
- package/templates/.agents/rules/task-management.zh-CN.md +2 -2
- package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
- package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
- package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
- package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
- package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
- package/templates/.agents/skills/commit/SKILL.en.md +41 -125
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
- package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
- package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
- package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
- package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
- package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
- package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
- package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
- package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
- package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
- package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
- package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
- package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
- package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
- package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
- package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
- package/templates/.codex/hooks.json +4 -4
- package/dist/lib/task/commit-finalization.js +0 -305
- package/dist/lib/task/commit-intent.js +0 -165
- package/lib/task/commit-finalization.ts +0 -419
- package/lib/task/commit-intent.ts +0 -230
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
|
-
import { execFileSync, spawnSync } from 'node:child_process';
|
|
5
4
|
|
|
6
5
|
import { parseTypedTaskFrontmatter } from './frontmatter.ts';
|
|
7
6
|
import { parseReviewSummary } from './review-artifacts.ts';
|
|
@@ -14,6 +13,8 @@ import {
|
|
|
14
13
|
completeDelegationStage,
|
|
15
14
|
consumeDelegation,
|
|
16
15
|
dispatchDelegation,
|
|
16
|
+
foldBlankToNull,
|
|
17
|
+
isDelegationReceipt,
|
|
17
18
|
managedDelegationRole,
|
|
18
19
|
prepareDelegation,
|
|
19
20
|
sealDelegation
|
|
@@ -25,6 +26,7 @@ import type {
|
|
|
25
26
|
DelegationStage
|
|
26
27
|
} from './delegation-receipts.ts';
|
|
27
28
|
import { normalizeAgentClients } from '../agent-clients/config.ts';
|
|
29
|
+
import { resolveAgentRuntimeStoreRoot } from '../runtime/agent-runtime.ts';
|
|
28
30
|
import { inspectPlatformPullRequest } from '../platform/pull-requests.ts';
|
|
29
31
|
import {
|
|
30
32
|
getAgentClientCapability,
|
|
@@ -47,59 +49,20 @@ import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
|
|
|
47
49
|
import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
|
|
48
50
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
49
51
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
50
|
-
import {
|
|
51
|
-
CommitIntentError,
|
|
52
|
-
commitIntentPath,
|
|
53
|
-
createCommitIntent,
|
|
54
|
-
digest,
|
|
55
|
-
readCommitIntent,
|
|
56
|
-
removeCommitIntent,
|
|
57
|
-
removeCommitIntentByDigest,
|
|
58
|
-
serialize,
|
|
59
|
-
updateCommitIntent
|
|
60
|
-
} from './commit-intent.ts';
|
|
61
|
-
import type { CommitIntent, PushEvidence } from './commit-intent.ts';
|
|
62
|
-
import {
|
|
63
|
-
inspectCommitFinalization,
|
|
64
|
-
planCommitTaskFinalization
|
|
65
|
-
} from './commit-finalization.ts';
|
|
66
|
-
import type { CommitFinalizationInspection } from './commit-finalization.ts';
|
|
67
|
-
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
68
|
-
import {
|
|
69
|
-
appendActivityEntry,
|
|
70
|
-
commitAttemptStartedNote,
|
|
71
|
-
locateActivityLog,
|
|
72
|
-
pairEntries
|
|
73
|
-
} from './activity-log.ts';
|
|
52
|
+
import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
74
53
|
|
|
75
54
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
76
|
-
type LegacyOrchestrationModelPolicy = Readonly<{
|
|
77
|
-
executor: string;
|
|
78
|
-
reviewer: string;
|
|
79
|
-
}>;
|
|
80
55
|
type ModelPolicySource = Readonly<{
|
|
81
56
|
kind: 'explicit' | 'project-config';
|
|
82
57
|
client: AgentClientId;
|
|
83
58
|
resolvedAt: string;
|
|
84
59
|
}>;
|
|
85
|
-
type
|
|
86
|
-
code: 'MODEL_POLICY_SUPPLEMENTED';
|
|
87
|
-
recoveredAt: string;
|
|
88
|
-
previousSchemaVersion: 1;
|
|
89
|
-
previousStatus: OrchestrationStatus;
|
|
90
|
-
previousPause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
91
|
-
policySource: ModelPolicySource;
|
|
92
|
-
receiptCount: 0;
|
|
93
|
-
pendingDelegation: false;
|
|
94
|
-
resultingStatus: OrchestrationStatus;
|
|
95
|
-
}>;
|
|
96
|
-
type ClientCapabilityRecovery = Readonly<{
|
|
60
|
+
type ClaudeCodeCapabilityRecovery = Readonly<{
|
|
97
61
|
code: 'CLIENT_CAPABILITY_ENABLED';
|
|
98
62
|
recoveredAt: string;
|
|
99
|
-
previousSchemaVersion: 2;
|
|
100
63
|
previousStatus: 'paused';
|
|
101
64
|
previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
|
|
102
|
-
client: '
|
|
65
|
+
client: 'claude-code';
|
|
103
66
|
guards: Readonly<{
|
|
104
67
|
stepCount: 0;
|
|
105
68
|
nextStage: null;
|
|
@@ -108,29 +71,10 @@ type ClientCapabilityRecovery = Readonly<{
|
|
|
108
71
|
pendingDelegation: false;
|
|
109
72
|
commitAuthorizationUnused: true;
|
|
110
73
|
completionEvidenceAbsent: true;
|
|
111
|
-
commitIntentAbsent: true;
|
|
112
74
|
}>;
|
|
113
75
|
resultingStatus: 'running';
|
|
114
76
|
}>;
|
|
115
|
-
type
|
|
116
|
-
code: 'SCHEMA_V3_MIGRATED';
|
|
117
|
-
recoveredAt: string;
|
|
118
|
-
previousSchemaVersion: 2;
|
|
119
|
-
previousStatus: 'paused';
|
|
120
|
-
previousPause: Readonly<{ code: string; message: string; recoverable: boolean }>;
|
|
121
|
-
guards: Readonly<{
|
|
122
|
-
stepCount: 0;
|
|
123
|
-
nextStage: null;
|
|
124
|
-
baselineEmpty: true;
|
|
125
|
-
receiptCount: 0;
|
|
126
|
-
pendingDelegation: false;
|
|
127
|
-
commitAuthorizationUnused: true;
|
|
128
|
-
completionEvidenceAbsent: true;
|
|
129
|
-
commitIntentAbsent: true;
|
|
130
|
-
}>;
|
|
131
|
-
resultingStatus: 'running';
|
|
132
|
-
}>;
|
|
133
|
-
type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | SchemaMigrationRecovery;
|
|
77
|
+
type OrchestrationRecovery = ClaudeCodeCapabilityRecovery;
|
|
134
78
|
type CleanCompletionEvidence = Readonly<{
|
|
135
79
|
kind: 'reviewed-head-clean';
|
|
136
80
|
observedAt: string;
|
|
@@ -142,22 +86,21 @@ type CleanCompletionEvidence = Readonly<{
|
|
|
142
86
|
prHead: string;
|
|
143
87
|
}>;
|
|
144
88
|
type OrchestrationRun = Readonly<{
|
|
145
|
-
schemaVersion: 1 | 2 | 3;
|
|
146
89
|
taskId: string;
|
|
147
90
|
runId: string;
|
|
148
91
|
status: OrchestrationStatus;
|
|
149
92
|
nextStage: DelegationStage | null;
|
|
150
93
|
stepCount: number;
|
|
151
94
|
maxSteps: number;
|
|
152
|
-
modelPolicy
|
|
153
|
-
modelPolicySource
|
|
154
|
-
recoveryHistory
|
|
95
|
+
modelPolicy: OrchestrationModelPolicy;
|
|
96
|
+
modelPolicySource: ModelPolicySource;
|
|
97
|
+
recoveryHistory: readonly OrchestrationRecovery[];
|
|
155
98
|
baseline: string;
|
|
156
99
|
pendingDelegation: DelegationReceipt | null;
|
|
157
100
|
receipts: readonly DelegationReceipt[];
|
|
158
101
|
pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
159
102
|
commitAuthorization: Readonly<{ issuedAt: string | null; consumedAt: string | null }>;
|
|
160
|
-
completionEvidence
|
|
103
|
+
completionEvidence: CleanCompletionEvidence | null;
|
|
161
104
|
createdAt: string;
|
|
162
105
|
updatedAt: string;
|
|
163
106
|
}>;
|
|
@@ -182,6 +125,7 @@ type OrchestrationResult = Readonly<{
|
|
|
182
125
|
client?: AgentClientId;
|
|
183
126
|
missingFields?: readonly string[];
|
|
184
127
|
modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
|
|
128
|
+
detail?: CodexCapabilityProvenanceDetail;
|
|
185
129
|
}> | null;
|
|
186
130
|
}>;
|
|
187
131
|
type OrchestrationStageIdentity = Readonly<{
|
|
@@ -218,40 +162,14 @@ type OrchestrationOptions = {
|
|
|
218
162
|
}>;
|
|
219
163
|
diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
|
|
220
164
|
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
165
|
+
validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
166
|
+
consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
221
167
|
token?: () => string;
|
|
222
168
|
monotonicNow?: () => number;
|
|
223
169
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
224
170
|
hasActiveLifecycleEvidence?: (receipt: DelegationReceipt) => boolean;
|
|
225
171
|
};
|
|
226
172
|
|
|
227
|
-
type CommitIntentResult = Readonly<{
|
|
228
|
-
status: 'ready' | 'failed';
|
|
229
|
-
changed: boolean;
|
|
230
|
-
taskId: string | null;
|
|
231
|
-
intent: Readonly<{
|
|
232
|
-
mode: CommitIntent['mode'];
|
|
233
|
-
phase: CommitIntent['phase'];
|
|
234
|
-
baselineHead: string;
|
|
235
|
-
currentHead: string;
|
|
236
|
-
committedHead: string | null;
|
|
237
|
-
pushEvidence: PushEvidence | null;
|
|
238
|
-
runId: string | null;
|
|
239
|
-
receiptId: string | null;
|
|
240
|
-
}> | null;
|
|
241
|
-
finalization?: Readonly<{
|
|
242
|
-
disposition: CommitFinalizationInspection['disposition'];
|
|
243
|
-
code: CommitFinalizationInspection['code'];
|
|
244
|
-
message: string;
|
|
245
|
-
currentHead: string;
|
|
246
|
-
committedHead: string | null;
|
|
247
|
-
needsAnchor: boolean;
|
|
248
|
-
needsLog: boolean;
|
|
249
|
-
attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
|
|
250
|
-
}>;
|
|
251
|
-
token?: string;
|
|
252
|
-
error: Readonly<{ code: string; message: string }> | null;
|
|
253
|
-
}>;
|
|
254
|
-
|
|
255
173
|
function supportsLifecycleDelegation(client: AgentClientId): boolean {
|
|
256
174
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
257
175
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -263,24 +181,147 @@ function orchestrationPath(taskDir: string): string {
|
|
|
263
181
|
return path.join(taskDir, 'orchestration.json');
|
|
264
182
|
}
|
|
265
183
|
|
|
184
|
+
const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; finish or clear active runs before upgrading';
|
|
185
|
+
|
|
186
|
+
class OrchestrationStateError extends Error {
|
|
187
|
+
readonly code = 'ORCHESTRATION_STATE_INVALID';
|
|
188
|
+
readonly taskId: string | null;
|
|
189
|
+
|
|
190
|
+
constructor(taskId: string | null = null) {
|
|
191
|
+
super(ORCHESTRATION_STATE_INVALID_MESSAGE);
|
|
192
|
+
this.name = 'OrchestrationStateError';
|
|
193
|
+
this.taskId = taskId;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
198
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function hasExactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
202
|
+
if (!isRecord(value)) return false;
|
|
203
|
+
const actual = Object.keys(value);
|
|
204
|
+
return actual.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function exactText(value: unknown): value is string {
|
|
208
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function nullableText(value: unknown): value is string | null {
|
|
212
|
+
return value === null || exactText(value);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function isPause(value: unknown): value is NonNullable<OrchestrationRun['pause']> {
|
|
216
|
+
return hasExactKeys(value, ['code', 'message', 'recoverable'])
|
|
217
|
+
&& exactText(value.code)
|
|
218
|
+
&& exactText(value.message)
|
|
219
|
+
&& typeof value.recoverable === 'boolean';
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function isModelPolicySource(value: unknown): value is ModelPolicySource {
|
|
223
|
+
return hasExactKeys(value, ['kind', 'client', 'resolvedAt'])
|
|
224
|
+
&& ['explicit', 'project-config'].includes(value.kind as string)
|
|
225
|
+
&& isAgentClientId(value.client)
|
|
226
|
+
&& exactText(value.resolvedAt);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function isModelPolicy(value: unknown): value is OrchestrationModelPolicy {
|
|
230
|
+
if (!hasExactKeys(value, ['executor', 'reviewer'])) return false;
|
|
231
|
+
return [value.executor, value.reviewer].every((role) => hasExactKeys(role, ['model', 'reasoningEffort'])
|
|
232
|
+
&& exactText(role.model)
|
|
233
|
+
&& exactText(role.reasoningEffort));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const RECOVERY_GUARD_KEYS = [
|
|
237
|
+
'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
|
|
238
|
+
'commitAuthorizationUnused', 'completionEvidenceAbsent'
|
|
239
|
+
] as const;
|
|
240
|
+
|
|
241
|
+
function isRecovery(value: unknown): value is OrchestrationRecovery {
|
|
242
|
+
if (!hasExactKeys(value, [
|
|
243
|
+
'code', 'recoveredAt', 'previousStatus', 'previousPause', 'client', 'guards', 'resultingStatus'
|
|
244
|
+
])) return false;
|
|
245
|
+
const guards = value.guards;
|
|
246
|
+
return value.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
247
|
+
&& exactText(value.recoveredAt)
|
|
248
|
+
&& value.previousStatus === 'paused'
|
|
249
|
+
&& isPause(value.previousPause)
|
|
250
|
+
&& value.previousPause.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
251
|
+
&& value.client === 'claude-code'
|
|
252
|
+
&& hasExactKeys(guards, RECOVERY_GUARD_KEYS)
|
|
253
|
+
&& guards.stepCount === 0
|
|
254
|
+
&& guards.nextStage === null
|
|
255
|
+
&& guards.baselineEmpty === true
|
|
256
|
+
&& guards.receiptCount === 0
|
|
257
|
+
&& guards.pendingDelegation === false
|
|
258
|
+
&& guards.commitAuthorizationUnused === true
|
|
259
|
+
&& guards.completionEvidenceAbsent === true
|
|
260
|
+
&& value.resultingStatus === 'running';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
|
|
264
|
+
return hasExactKeys(value, [
|
|
265
|
+
'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
|
|
266
|
+
])
|
|
267
|
+
&& value.kind === 'reviewed-head-clean'
|
|
268
|
+
&& exactText(value.observedAt)
|
|
269
|
+
&& exactText(value.head)
|
|
270
|
+
&& exactText(value.headTree)
|
|
271
|
+
&& exactText(value.worktreeTree)
|
|
272
|
+
&& exactText(value.lastReviewedCommit)
|
|
273
|
+
&& Number.isSafeInteger(value.prNumber)
|
|
274
|
+
&& (value.prNumber as number) > 0
|
|
275
|
+
&& exactText(value.prHead);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const ORCHESTRATION_RUN_KEYS = [
|
|
279
|
+
'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
|
|
280
|
+
'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
|
|
281
|
+
'pause', 'commitAuthorization', 'completionEvidence', 'createdAt', 'updatedAt'
|
|
282
|
+
] as const;
|
|
283
|
+
|
|
284
|
+
function parseOrchestrationRun(value: unknown, expectedTaskId?: string): OrchestrationRun {
|
|
285
|
+
if (!hasExactKeys(value, ORCHESTRATION_RUN_KEYS)
|
|
286
|
+
|| !exactText(value.taskId)
|
|
287
|
+
|| (expectedTaskId !== undefined && value.taskId !== expectedTaskId)
|
|
288
|
+
|| !exactText(value.runId)
|
|
289
|
+
|| !['running', 'paused', 'completed'].includes(value.status as string)
|
|
290
|
+
|| !(value.nextStage === null || ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code', 'commit'].includes(value.nextStage as string))
|
|
291
|
+
|| !Number.isSafeInteger(value.stepCount) || (value.stepCount as number) < 0
|
|
292
|
+
|| !Number.isSafeInteger(value.maxSteps) || (value.maxSteps as number) < 1
|
|
293
|
+
|| !isModelPolicy(value.modelPolicy)
|
|
294
|
+
|| !isModelPolicySource(value.modelPolicySource)
|
|
295
|
+
|| !Array.isArray(value.recoveryHistory) || !value.recoveryHistory.every(isRecovery)
|
|
296
|
+
|| typeof value.baseline !== 'string'
|
|
297
|
+
|| !(value.pendingDelegation === null || isDelegationReceipt(value.pendingDelegation))
|
|
298
|
+
|| !Array.isArray(value.receipts) || !value.receipts.every(isDelegationReceipt)
|
|
299
|
+
|| !(value.pause === null || isPause(value.pause))
|
|
300
|
+
|| !hasExactKeys(value.commitAuthorization, ['issuedAt', 'consumedAt'])
|
|
301
|
+
|| !nullableText(value.commitAuthorization.issuedAt)
|
|
302
|
+
|| !nullableText(value.commitAuthorization.consumedAt)
|
|
303
|
+
|| !(value.completionEvidence === null || isCompletionEvidence(value.completionEvidence))
|
|
304
|
+
|| !exactText(value.createdAt)
|
|
305
|
+
|| !exactText(value.updatedAt)) {
|
|
306
|
+
throw new OrchestrationStateError();
|
|
307
|
+
}
|
|
308
|
+
const receipts = [...value.receipts, ...(value.pendingDelegation ? [value.pendingDelegation] : [])];
|
|
309
|
+
if (receipts.some((receipt) => receipt.taskId !== value.taskId || receipt.runId !== value.runId)) {
|
|
310
|
+
throw new OrchestrationStateError();
|
|
311
|
+
}
|
|
312
|
+
return value as OrchestrationRun;
|
|
313
|
+
}
|
|
314
|
+
|
|
266
315
|
function readRun(taskDir: string): OrchestrationRun | null {
|
|
267
316
|
const file = orchestrationPath(taskDir);
|
|
268
317
|
if (!fs.existsSync(file)) return null;
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
reasoningEffort: run.modelPolicy.executor.reasoningEffort
|
|
277
|
-
},
|
|
278
|
-
reviewer: {
|
|
279
|
-
model: run.modelPolicy.reviewer.model,
|
|
280
|
-
reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
};
|
|
318
|
+
const taskId = path.basename(taskDir);
|
|
319
|
+
try {
|
|
320
|
+
return parseOrchestrationRun(JSON.parse(fs.readFileSync(file, 'utf8')), taskId);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
if (error instanceof OrchestrationStateError) throw new OrchestrationStateError(taskId);
|
|
323
|
+
throw new OrchestrationStateError(taskId);
|
|
324
|
+
}
|
|
284
325
|
}
|
|
285
326
|
|
|
286
327
|
function atomicWrite(file: string, value: unknown): void {
|
|
@@ -306,79 +347,11 @@ function failed(
|
|
|
306
347
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
307
348
|
}
|
|
308
349
|
|
|
309
|
-
function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
|
|
310
|
-
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function repositoryHead(repoRoot: string): string {
|
|
314
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
315
|
-
cwd: repoRoot,
|
|
316
|
-
encoding: 'utf8',
|
|
317
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
318
|
-
}).trim();
|
|
319
|
-
}
|
|
320
|
-
|
|
321
350
|
function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
|
|
322
351
|
if (!options.gitWorktreeRoot) return stateRoot;
|
|
323
352
|
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
324
353
|
}
|
|
325
354
|
|
|
326
|
-
function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
|
|
327
|
-
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
328
|
-
cwd: repoRoot,
|
|
329
|
-
stdio: 'ignore'
|
|
330
|
-
}).status === 0;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
|
|
334
|
-
return {
|
|
335
|
-
mode: intent.mode,
|
|
336
|
-
phase: intent.phase,
|
|
337
|
-
baselineHead: intent.baselineHead,
|
|
338
|
-
currentHead,
|
|
339
|
-
committedHead: intent.committedHead,
|
|
340
|
-
pushEvidence: intent.pushEvidence,
|
|
341
|
-
runId: intent.orchestration?.runId ?? null,
|
|
342
|
-
receiptId: intent.orchestration?.receiptId ?? null
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
|
|
347
|
-
return {
|
|
348
|
-
disposition: inspection.disposition,
|
|
349
|
-
code: inspection.code,
|
|
350
|
-
message: inspection.message,
|
|
351
|
-
currentHead: inspection.currentHead,
|
|
352
|
-
committedHead: inspection.committedHead,
|
|
353
|
-
needsAnchor: inspection.needsAnchor,
|
|
354
|
-
needsLog: inspection.needsLog,
|
|
355
|
-
attempt: inspection.attempt
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
|
|
360
|
-
return run !== null
|
|
361
|
-
&& [1, 2, 3].includes(run.schemaVersion)
|
|
362
|
-
&& run.taskId === taskId
|
|
363
|
-
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
364
|
-
&& Array.isArray(run.receipts)
|
|
365
|
-
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
366
|
-
&& typeof run.commitAuthorization === 'object'
|
|
367
|
-
&& run.commitAuthorization !== null
|
|
368
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
369
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
|
|
373
|
-
if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
|
|
374
|
-
if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
|
|
375
|
-
return commitIntentFailed(
|
|
376
|
-
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
377
|
-
error instanceof Error ? error.message : String(error),
|
|
378
|
-
taskId
|
|
379
|
-
);
|
|
380
|
-
}
|
|
381
|
-
|
|
382
355
|
function validModel(value: unknown): value is string {
|
|
383
356
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
384
357
|
}
|
|
@@ -403,12 +376,8 @@ function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationMod
|
|
|
403
376
|
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
404
377
|
}
|
|
405
378
|
|
|
406
|
-
function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
|
|
407
|
-
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
408
|
-
}
|
|
409
|
-
|
|
410
379
|
function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
|
|
411
|
-
return
|
|
380
|
+
return run.modelPolicy[role];
|
|
412
381
|
}
|
|
413
382
|
|
|
414
383
|
function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
|
|
@@ -418,11 +387,10 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
|
|
|
418
387
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
419
388
|
}
|
|
420
389
|
|
|
421
|
-
function
|
|
422
|
-
return run.
|
|
423
|
-
&& run.status === 'paused'
|
|
390
|
+
function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
|
|
391
|
+
return run.status === 'paused'
|
|
424
392
|
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
425
|
-
&& run.modelPolicySource?.client === '
|
|
393
|
+
&& run.modelPolicySource?.client === 'claude-code'
|
|
426
394
|
&& run.stepCount === 0
|
|
427
395
|
&& run.nextStage === null
|
|
428
396
|
&& run.baseline === ''
|
|
@@ -431,23 +399,7 @@ function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string)
|
|
|
431
399
|
&& run.commitAuthorization?.issuedAt === null
|
|
432
400
|
&& run.commitAuthorization?.consumedAt === null
|
|
433
401
|
&& run.completionEvidence == null
|
|
434
|
-
&&
|
|
435
|
-
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
function canMigrateRecoverableV2Pause(run: OrchestrationRun, taskDir: string): boolean {
|
|
439
|
-
return run.schemaVersion === 2
|
|
440
|
-
&& run.status === 'paused'
|
|
441
|
-
&& run.pause?.recoverable === true
|
|
442
|
-
&& run.stepCount === 0
|
|
443
|
-
&& run.nextStage === null
|
|
444
|
-
&& run.baseline === ''
|
|
445
|
-
&& run.pendingDelegation === null
|
|
446
|
-
&& run.receipts.length === 0
|
|
447
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
448
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
449
|
-
&& run.completionEvidence == null
|
|
450
|
-
&& !fs.existsSync(commitIntentPath(taskDir));
|
|
402
|
+
&& run.recoveryHistory.length === 0;
|
|
451
403
|
}
|
|
452
404
|
|
|
453
405
|
function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -463,202 +415,56 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
463
415
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
464
416
|
}
|
|
465
417
|
if (existing) {
|
|
466
|
-
if (
|
|
467
|
-
![1, 2, 3].includes(existing.schemaVersion)
|
|
468
|
-
|| !Array.isArray(existing.receipts)
|
|
469
|
-
|| !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')
|
|
470
|
-
) {
|
|
471
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
472
|
-
}
|
|
473
|
-
if (existing.status === 'completed' && existing.schemaVersion === 1) {
|
|
418
|
+
if (existing.status === 'completed') {
|
|
474
419
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
475
420
|
}
|
|
476
|
-
if (existing.
|
|
477
|
-
|
|
478
|
-
if (
|
|
479
|
-
policyError
|
|
480
|
-
|| !existing.modelPolicySource
|
|
481
|
-
|| !isAgentClientId(existing.modelPolicySource.client)
|
|
482
|
-
|| !Array.isArray(existing.recoveryHistory)
|
|
483
|
-
) {
|
|
484
|
-
return failed('ORCHESTRATION_STATE_INVALID', `schemaVersion ${existing.schemaVersion} orchestration state is incomplete`, resolved.taskId);
|
|
485
|
-
}
|
|
486
|
-
if (existing.status === 'completed') {
|
|
487
|
-
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
488
|
-
}
|
|
489
|
-
if (existing.schemaVersion === 2 && existing.status === 'running') {
|
|
490
|
-
return failed(
|
|
491
|
-
'ORCHESTRATION_STATE_INVALID',
|
|
492
|
-
'active schemaVersion 2 runs cannot be advanced by lifecycle protocol v3',
|
|
493
|
-
resolved.taskId
|
|
494
|
-
);
|
|
495
|
-
}
|
|
496
|
-
if (existing.modelPolicySource.client !== options.client) {
|
|
497
|
-
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
498
|
-
}
|
|
499
|
-
if (options.modelPolicy) {
|
|
500
|
-
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
501
|
-
if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
502
|
-
if (!sameModelPolicy(existing.modelPolicy as OrchestrationModelPolicy, options.modelPolicy)) {
|
|
503
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
507
|
-
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
508
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
509
|
-
}
|
|
510
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
511
|
-
const previousPause = existing.pause as ClientCapabilityRecovery['previousPause'];
|
|
512
|
-
const recovery: ClientCapabilityRecovery = {
|
|
513
|
-
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
514
|
-
recoveredAt: now,
|
|
515
|
-
previousSchemaVersion: 2,
|
|
516
|
-
previousStatus: 'paused',
|
|
517
|
-
previousPause,
|
|
518
|
-
client: 'codex',
|
|
519
|
-
guards: {
|
|
520
|
-
stepCount: 0,
|
|
521
|
-
nextStage: null,
|
|
522
|
-
baselineEmpty: true,
|
|
523
|
-
receiptCount: 0,
|
|
524
|
-
pendingDelegation: false,
|
|
525
|
-
commitAuthorizationUnused: true,
|
|
526
|
-
completionEvidenceAbsent: true,
|
|
527
|
-
commitIntentAbsent: true
|
|
528
|
-
},
|
|
529
|
-
resultingStatus: 'running'
|
|
530
|
-
};
|
|
531
|
-
const resumed = withUpdatedRun(existing, {
|
|
532
|
-
schemaVersion: 3,
|
|
533
|
-
status: 'running',
|
|
534
|
-
pause: null,
|
|
535
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
536
|
-
}, () => now);
|
|
537
|
-
saveRun(resolved.taskDir, resumed);
|
|
538
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
539
|
-
}
|
|
540
|
-
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
541
|
-
if (existing.schemaVersion === 2 && !canMigrateRecoverableV2Pause(existing, resolved.taskDir)) {
|
|
542
|
-
const paused = withUpdatedRun(existing, {
|
|
543
|
-
pause: {
|
|
544
|
-
code: 'ORCHESTRATION_SCHEMA_MIGRATION_REQUIRED',
|
|
545
|
-
message: 'schemaVersion 2 recoverable pause contains execution evidence and cannot migrate automatically',
|
|
546
|
-
recoverable: false
|
|
547
|
-
}
|
|
548
|
-
});
|
|
549
|
-
saveRun(resolved.taskDir, paused);
|
|
550
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
551
|
-
}
|
|
552
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
553
|
-
const recovery: SchemaMigrationRecovery | null = existing.schemaVersion === 2 ? {
|
|
554
|
-
code: 'SCHEMA_V3_MIGRATED',
|
|
555
|
-
recoveredAt: now,
|
|
556
|
-
previousSchemaVersion: 2,
|
|
557
|
-
previousStatus: 'paused',
|
|
558
|
-
previousPause: existing.pause,
|
|
559
|
-
guards: {
|
|
560
|
-
stepCount: 0,
|
|
561
|
-
nextStage: null,
|
|
562
|
-
baselineEmpty: true,
|
|
563
|
-
receiptCount: 0,
|
|
564
|
-
pendingDelegation: false,
|
|
565
|
-
commitAuthorizationUnused: true,
|
|
566
|
-
completionEvidenceAbsent: true,
|
|
567
|
-
commitIntentAbsent: true
|
|
568
|
-
},
|
|
569
|
-
resultingStatus: 'running'
|
|
570
|
-
} : null;
|
|
571
|
-
const resumed = withUpdatedRun(existing, {
|
|
572
|
-
schemaVersion: 3,
|
|
573
|
-
status: 'running',
|
|
574
|
-
pause: null,
|
|
575
|
-
...(recovery ? { recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery]) } : {})
|
|
576
|
-
}, () => now);
|
|
577
|
-
saveRun(resolved.taskDir, resumed);
|
|
578
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
579
|
-
}
|
|
580
|
-
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
421
|
+
if (existing.modelPolicySource.client !== options.client) {
|
|
422
|
+
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
581
423
|
}
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
policy = resolveProjectPolicy(resolved.repoRoot, options.client);
|
|
588
|
-
sourceKind = 'project-config';
|
|
589
|
-
} catch (error) {
|
|
590
|
-
return failed('ORCHESTRATION_CONFIG_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
424
|
+
if (options.modelPolicy) {
|
|
425
|
+
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
426
|
+
if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
427
|
+
if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
|
|
428
|
+
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
591
429
|
}
|
|
592
430
|
}
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
431
|
+
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
432
|
+
if (existing.modelPolicySource.client !== 'claude-code'
|
|
433
|
+
|| !canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
434
|
+
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
435
|
+
}
|
|
436
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
437
|
+
const recovery: ClaudeCodeCapabilityRecovery = {
|
|
438
|
+
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
439
|
+
recoveredAt: now,
|
|
440
|
+
previousStatus: 'paused',
|
|
441
|
+
previousPause: existing.pause as ClaudeCodeCapabilityRecovery['previousPause'],
|
|
442
|
+
client: 'claude-code',
|
|
443
|
+
guards: {
|
|
444
|
+
stepCount: 0,
|
|
445
|
+
nextStage: null,
|
|
446
|
+
baselineEmpty: true,
|
|
447
|
+
receiptCount: 0,
|
|
448
|
+
pendingDelegation: false,
|
|
449
|
+
commitAuthorizationUnused: true,
|
|
450
|
+
completionEvidenceAbsent: true
|
|
451
|
+
},
|
|
452
|
+
resultingStatus: 'running'
|
|
453
|
+
};
|
|
454
|
+
const resumed = withUpdatedRun(existing, {
|
|
455
|
+
status: 'running',
|
|
456
|
+
pause: null,
|
|
457
|
+
recoveryHistory: Object.freeze([...existing.recoveryHistory, recovery])
|
|
458
|
+
}, () => now);
|
|
459
|
+
saveRun(resolved.taskDir, resumed);
|
|
460
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
624
461
|
}
|
|
625
|
-
if (
|
|
626
|
-
existing.
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
existing.modelPolicy.executor !== policy!.executor.model
|
|
630
|
-
|| existing.modelPolicy.reviewer !== policy!.reviewer.model
|
|
631
|
-
)
|
|
632
|
-
) {
|
|
633
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
462
|
+
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
463
|
+
const resumed = withUpdatedRun(existing, { status: 'running', pause: null }, options.now);
|
|
464
|
+
saveRun(resolved.taskDir, resumed);
|
|
465
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
634
466
|
}
|
|
635
|
-
|
|
636
|
-
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
637
|
-
const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
|
|
638
|
-
const resultingStatus = clearsModelPause ? 'running' : existing.status;
|
|
639
|
-
const recovery: OrchestrationRecovery = {
|
|
640
|
-
code: 'MODEL_POLICY_SUPPLEMENTED',
|
|
641
|
-
recoveredAt: now,
|
|
642
|
-
previousSchemaVersion: 1,
|
|
643
|
-
previousStatus: existing.status,
|
|
644
|
-
previousPause: existing.pause,
|
|
645
|
-
policySource: source,
|
|
646
|
-
receiptCount: 0,
|
|
647
|
-
pendingDelegation: false,
|
|
648
|
-
resultingStatus
|
|
649
|
-
};
|
|
650
|
-
const recovered: OrchestrationRun = Object.freeze({
|
|
651
|
-
...existing,
|
|
652
|
-
schemaVersion: 3,
|
|
653
|
-
status: resultingStatus,
|
|
654
|
-
modelPolicy: policy!,
|
|
655
|
-
modelPolicySource: source,
|
|
656
|
-
recoveryHistory: Object.freeze([recovery]),
|
|
657
|
-
pause: clearsModelPause ? null : existing.pause,
|
|
658
|
-
updatedAt: now
|
|
659
|
-
});
|
|
660
|
-
saveRun(resolved.taskDir, recovered);
|
|
661
|
-
return { status: resultingStatus, changed: true, taskId: resolved.taskId, run: recovered, next: null, error: null };
|
|
467
|
+
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
662
468
|
}
|
|
663
469
|
|
|
664
470
|
let policy = options.modelPolicy;
|
|
@@ -680,7 +486,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
680
486
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
681
487
|
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
682
488
|
const run: OrchestrationRun = {
|
|
683
|
-
schemaVersion: 3,
|
|
684
489
|
taskId: resolved.taskId,
|
|
685
490
|
runId: (options.id ?? randomUUID)(),
|
|
686
491
|
status: 'running',
|
|
@@ -927,488 +732,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
927
732
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
928
733
|
}
|
|
929
734
|
|
|
930
|
-
function appendCommitActivity(
|
|
931
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
932
|
-
step: string,
|
|
933
|
-
agent: string,
|
|
934
|
-
note: string
|
|
935
|
-
): Readonly<{ code: string; message: string }> | null {
|
|
936
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
937
|
-
const activity = locateActivityLog(taskContent);
|
|
938
|
-
if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
939
|
-
const metadata = captureTaskWriteMetadata();
|
|
940
|
-
const written = writeTask({
|
|
941
|
-
taskRef: resolved.taskId,
|
|
942
|
-
expectedState: 'active',
|
|
943
|
-
mutations: [{
|
|
944
|
-
kind: 'section',
|
|
945
|
-
aliases: ['活动日志', 'Activity Log'],
|
|
946
|
-
heading: activity.heading,
|
|
947
|
-
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
948
|
-
}]
|
|
949
|
-
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
950
|
-
return written.status === 'failed' ? written.error : null;
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
function startCommitAttempt(
|
|
954
|
-
taskRef: string,
|
|
955
|
-
input: Readonly<{ agent: string }>,
|
|
956
|
-
options: OrchestrationOptions = {}
|
|
957
|
-
): CommitIntentResult {
|
|
958
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
959
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
960
|
-
try {
|
|
961
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
962
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
963
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
964
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
965
|
-
}
|
|
966
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
967
|
-
const activity = locateActivityLog(taskContent);
|
|
968
|
-
if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
969
|
-
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
970
|
-
if (open.length > 0) {
|
|
971
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
972
|
-
}
|
|
973
|
-
const currentHead = repositoryHead(gitRoot);
|
|
974
|
-
const attempt = (options.id ?? randomUUID)();
|
|
975
|
-
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
976
|
-
attempt,
|
|
977
|
-
baseline: currentHead,
|
|
978
|
-
agent: input.agent
|
|
979
|
-
}));
|
|
980
|
-
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
981
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
982
|
-
return {
|
|
983
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
984
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
985
|
-
};
|
|
986
|
-
});
|
|
987
|
-
} catch (error) {
|
|
988
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
989
|
-
}
|
|
990
|
-
}
|
|
991
|
-
|
|
992
|
-
function terminateCommitAttempt(
|
|
993
|
-
taskRef: string,
|
|
994
|
-
input: Readonly<{ attempt: string; agent: string; code: string }>,
|
|
995
|
-
options: OrchestrationOptions = {}
|
|
996
|
-
): CommitIntentResult {
|
|
997
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
998
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
999
|
-
try {
|
|
1000
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1001
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
1002
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1003
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
1004
|
-
}
|
|
1005
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1006
|
-
if (
|
|
1007
|
-
inspection.disposition !== 'retryable-start'
|
|
1008
|
-
|| inspection.attempt?.attempt !== input.attempt
|
|
1009
|
-
|| inspection.attempt.agent !== input.agent
|
|
1010
|
-
|| inspection.attempt.baseline !== inspection.currentHead
|
|
1011
|
-
) {
|
|
1012
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
1013
|
-
}
|
|
1014
|
-
const error = appendCommitActivity(
|
|
1015
|
-
resolved,
|
|
1016
|
-
'Commit [aborted]',
|
|
1017
|
-
input.agent,
|
|
1018
|
-
`aborted; attempt=${input.attempt}; code=${input.code}`
|
|
1019
|
-
);
|
|
1020
|
-
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
1021
|
-
return {
|
|
1022
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
1023
|
-
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
1024
|
-
error: null
|
|
1025
|
-
};
|
|
1026
|
-
});
|
|
1027
|
-
} catch (error) {
|
|
1028
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
function beginCommitIntent(
|
|
1033
|
-
taskRef: string,
|
|
1034
|
-
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
|
|
1035
|
-
options: OrchestrationOptions = {}
|
|
1036
|
-
): CommitIntentResult {
|
|
1037
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1038
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1039
|
-
try {
|
|
1040
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1041
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
1042
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1043
|
-
if (currentHead !== input.baselineHead) {
|
|
1044
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
1045
|
-
}
|
|
1046
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1047
|
-
try {
|
|
1048
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1049
|
-
} catch (error) {
|
|
1050
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1051
|
-
}
|
|
1052
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
1053
|
-
}
|
|
1054
|
-
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1055
|
-
if (
|
|
1056
|
-
attemptInspection.disposition !== 'retryable-start'
|
|
1057
|
-
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
1058
|
-
|| attemptInspection.attempt.agent !== input.agent
|
|
1059
|
-
|| attemptInspection.attempt.baseline !== input.baselineHead
|
|
1060
|
-
) {
|
|
1061
|
-
return commitIntentFailed(
|
|
1062
|
-
'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
|
|
1063
|
-
'commit begin requires one matching retryable Commit attempt',
|
|
1064
|
-
resolved.taskId
|
|
1065
|
-
);
|
|
1066
|
-
}
|
|
1067
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
1068
|
-
let run: OrchestrationRun | null = null;
|
|
1069
|
-
let sourceRunBytes: string | null = null;
|
|
1070
|
-
if (fs.existsSync(runFile)) {
|
|
1071
|
-
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
1072
|
-
try {
|
|
1073
|
-
run = readRun(resolved.taskDir);
|
|
1074
|
-
} catch (error) {
|
|
1075
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1076
|
-
}
|
|
1077
|
-
if (!validCommitRun(run, resolved.taskId)) {
|
|
1078
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
|
|
1082
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
1083
|
-
const failAndPause = (code: string, message: string): CommitIntentResult => {
|
|
1084
|
-
if (run?.status === 'running') {
|
|
1085
|
-
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
1086
|
-
status: 'paused', pause: { code, message, recoverable: true }
|
|
1087
|
-
}, () => now));
|
|
1088
|
-
}
|
|
1089
|
-
return commitIntentFailed(code, message, resolved.taskId);
|
|
1090
|
-
};
|
|
1091
|
-
let orchestration: CommitIntent['orchestration'] = null;
|
|
1092
|
-
if (!input.orchestrated) {
|
|
1093
|
-
if (run?.pendingDelegation) {
|
|
1094
|
-
return commitIntentFailed(
|
|
1095
|
-
'ORCHESTRATION_STANDALONE_BUSY',
|
|
1096
|
-
'standalone commit is blocked by a pending orchestration delegation',
|
|
1097
|
-
resolved.taskId
|
|
1098
|
-
);
|
|
1099
|
-
}
|
|
1100
|
-
} else {
|
|
1101
|
-
if (!run || run.status !== 'running') {
|
|
1102
|
-
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
1103
|
-
}
|
|
1104
|
-
const receipt = run.pendingDelegation;
|
|
1105
|
-
if (
|
|
1106
|
-
!receipt
|
|
1107
|
-
|| receipt.status !== 'activated'
|
|
1108
|
-
|| receipt.stage !== 'commit'
|
|
1109
|
-
|| receipt.round !== 1
|
|
1110
|
-
|| receipt.artifact !== 'commit'
|
|
1111
|
-
|| receipt.role !== 'executor'
|
|
1112
|
-
) {
|
|
1113
|
-
return failAndPause(
|
|
1114
|
-
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1115
|
-
'orchestrated commit requires one matching activated commit delegation'
|
|
1116
|
-
);
|
|
1117
|
-
}
|
|
1118
|
-
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
1119
|
-
return failAndPause(
|
|
1120
|
-
'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
|
|
1121
|
-
'orchestrated commit requires an unconsumed one-use authorization'
|
|
1122
|
-
);
|
|
1123
|
-
}
|
|
1124
|
-
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
1125
|
-
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
1126
|
-
}, { ...options, now: () => now });
|
|
1127
|
-
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
1128
|
-
return failAndPause(
|
|
1129
|
-
planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1130
|
-
planned.result.error?.message ?? 'commit delegation could not be completed'
|
|
1131
|
-
);
|
|
1132
|
-
}
|
|
1133
|
-
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
1134
|
-
orchestration = {
|
|
1135
|
-
runId: run.runId,
|
|
1136
|
-
receiptId: receipt.id,
|
|
1137
|
-
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
1138
|
-
sourceRunDigest: digest(sourceRunBytes!),
|
|
1139
|
-
plannedRunDigest: digest(plannedRunBytes),
|
|
1140
|
-
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
1141
|
-
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
|
|
1145
|
-
const created = createCommitIntent(resolved.taskDir, {
|
|
1146
|
-
taskId: resolved.taskId,
|
|
1147
|
-
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
1148
|
-
phase: 'prepared',
|
|
1149
|
-
baselineHead: input.baselineHead,
|
|
1150
|
-
committedHead: null,
|
|
1151
|
-
pushEvidence: null,
|
|
1152
|
-
orchestration,
|
|
1153
|
-
createdAt: now,
|
|
1154
|
-
updatedAt: now
|
|
1155
|
-
}, { token: options.token });
|
|
1156
|
-
return {
|
|
1157
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1158
|
-
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
1159
|
-
};
|
|
1160
|
-
});
|
|
1161
|
-
} catch (error) {
|
|
1162
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1163
|
-
}
|
|
1164
|
-
}
|
|
1165
|
-
|
|
1166
|
-
function checkpointCommitIntent(
|
|
1167
|
-
taskRef: string,
|
|
1168
|
-
input: Readonly<{
|
|
1169
|
-
token: string;
|
|
1170
|
-
kind: 'committed' | 'pushed';
|
|
1171
|
-
head: string;
|
|
1172
|
-
remote?: string;
|
|
1173
|
-
ref?: string;
|
|
1174
|
-
}>,
|
|
1175
|
-
options: OrchestrationOptions = {}
|
|
1176
|
-
): CommitIntentResult {
|
|
1177
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1178
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1179
|
-
try {
|
|
1180
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1181
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
1182
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1183
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1184
|
-
if (currentHead !== input.head) {
|
|
1185
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
1186
|
-
}
|
|
1187
|
-
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
1188
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
1189
|
-
}
|
|
1190
|
-
if (input.kind === 'pushed') {
|
|
1191
|
-
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
1192
|
-
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
1193
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
1197
|
-
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
1198
|
-
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
1199
|
-
: {
|
|
1200
|
-
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
1201
|
-
pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
|
|
1202
|
-
});
|
|
1203
|
-
return {
|
|
1204
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1205
|
-
intent: commitIntentView(updated, currentHead), error: null
|
|
1206
|
-
};
|
|
1207
|
-
});
|
|
1208
|
-
} catch (error) {
|
|
1209
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1210
|
-
}
|
|
1211
|
-
}
|
|
1212
|
-
|
|
1213
|
-
function completeCommitOrchestration(
|
|
1214
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1215
|
-
intent: CommitIntent,
|
|
1216
|
-
agent: string
|
|
1217
|
-
): Readonly<{ code: string; message: string }> | null {
|
|
1218
|
-
if (intent.orchestration === null) return null;
|
|
1219
|
-
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
1220
|
-
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
1221
|
-
}
|
|
1222
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
1223
|
-
if (!fs.existsSync(runFile)) {
|
|
1224
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1225
|
-
}
|
|
1226
|
-
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
1227
|
-
const currentDigest = digest(currentBytes);
|
|
1228
|
-
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
1229
|
-
const run = readRun(resolved.taskDir);
|
|
1230
|
-
if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1231
|
-
const plannedRun: OrchestrationRun = Object.freeze({
|
|
1232
|
-
...run,
|
|
1233
|
-
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
1234
|
-
updatedAt: intent.orchestration.completionUpdatedAt
|
|
1235
|
-
});
|
|
1236
|
-
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
1237
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
1238
|
-
}
|
|
1239
|
-
atomicWrite(runFile, plannedRun);
|
|
1240
|
-
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
1241
|
-
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
1242
|
-
}
|
|
1243
|
-
} else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
1244
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
1245
|
-
}
|
|
1246
|
-
return null;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
function finalizeCommitIntentUnlocked(
|
|
1250
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1251
|
-
inspection: CommitFinalizationInspection,
|
|
1252
|
-
agent: string,
|
|
1253
|
-
remove: () => void
|
|
1254
|
-
): CommitIntentResult {
|
|
1255
|
-
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
1256
|
-
return commitIntentFailed(
|
|
1257
|
-
inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
|
|
1258
|
-
inspection.message,
|
|
1259
|
-
resolved.taskId
|
|
1260
|
-
);
|
|
1261
|
-
}
|
|
1262
|
-
if (
|
|
1263
|
-
inspection.intent.orchestration !== null
|
|
1264
|
-
&& inspection.intent.orchestration.plannedReceipt.agent !== agent
|
|
1265
|
-
) {
|
|
1266
|
-
return commitIntentFailed(
|
|
1267
|
-
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1268
|
-
'completion agent does not match the planned receipt',
|
|
1269
|
-
resolved.taskId
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
const metadata = captureTaskWriteMetadata();
|
|
1273
|
-
const taskPlan = planCommitTaskFinalization(
|
|
1274
|
-
resolved.taskDir,
|
|
1275
|
-
inspection,
|
|
1276
|
-
agent,
|
|
1277
|
-
metadata.timestamp
|
|
1278
|
-
);
|
|
1279
|
-
const written = writeTask({
|
|
1280
|
-
taskRef: resolved.taskId,
|
|
1281
|
-
expectedState: 'active',
|
|
1282
|
-
mutations: taskPlan.mutations
|
|
1283
|
-
}, {
|
|
1284
|
-
repoRoot: resolved.repoRoot,
|
|
1285
|
-
metadataProvider: () => metadata
|
|
1286
|
-
});
|
|
1287
|
-
if (written.status === 'failed') {
|
|
1288
|
-
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
1289
|
-
}
|
|
1290
|
-
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
1291
|
-
if (orchestrationError) {
|
|
1292
|
-
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
1293
|
-
}
|
|
1294
|
-
try {
|
|
1295
|
-
remove();
|
|
1296
|
-
} catch (error) {
|
|
1297
|
-
return commitIntentFailed(
|
|
1298
|
-
'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
|
|
1299
|
-
error instanceof Error ? error.message : String(error),
|
|
1300
|
-
resolved.taskId
|
|
1301
|
-
);
|
|
1302
|
-
}
|
|
1303
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1304
|
-
}
|
|
1305
|
-
|
|
1306
|
-
function completeCommitIntent(
|
|
1307
|
-
taskRef: string,
|
|
1308
|
-
input: Readonly<{ token: string; agent: string }>,
|
|
1309
|
-
options: OrchestrationOptions = {}
|
|
1310
|
-
): CommitIntentResult {
|
|
1311
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1312
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1313
|
-
try {
|
|
1314
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1315
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1316
|
-
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1317
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1318
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1319
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1320
|
-
});
|
|
1321
|
-
});
|
|
1322
|
-
} catch (error) {
|
|
1323
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1324
|
-
}
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
function recoverCommitIntent(
|
|
1328
|
-
taskRef: string,
|
|
1329
|
-
input: Readonly<{ agent: string }>,
|
|
1330
|
-
options: OrchestrationOptions = {}
|
|
1331
|
-
): CommitIntentResult {
|
|
1332
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1333
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1334
|
-
try {
|
|
1335
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1336
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1337
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1338
|
-
if (inspection.disposition === 'retryable-start') {
|
|
1339
|
-
return {
|
|
1340
|
-
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
1341
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
1342
|
-
};
|
|
1343
|
-
}
|
|
1344
|
-
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1345
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1346
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1347
|
-
}
|
|
1348
|
-
if (!inspection.intentDigest) {
|
|
1349
|
-
return commitIntentFailed(
|
|
1350
|
-
inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
1351
|
-
inspection.message,
|
|
1352
|
-
resolved.taskId
|
|
1353
|
-
);
|
|
1354
|
-
}
|
|
1355
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1356
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
|
|
1357
|
-
});
|
|
1358
|
-
});
|
|
1359
|
-
} catch (error) {
|
|
1360
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1361
|
-
}
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
function abortCommitIntent(
|
|
1365
|
-
taskRef: string,
|
|
1366
|
-
input: Readonly<{ token: string; expectedHead: string }>,
|
|
1367
|
-
options: OrchestrationOptions = {}
|
|
1368
|
-
): CommitIntentResult {
|
|
1369
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1370
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1371
|
-
try {
|
|
1372
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1373
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1374
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1375
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1376
|
-
if (
|
|
1377
|
-
intent.phase !== 'prepared'
|
|
1378
|
-
|| intent.pushEvidence !== null
|
|
1379
|
-
|| input.expectedHead !== intent.baselineHead
|
|
1380
|
-
|| currentHead !== intent.baselineHead
|
|
1381
|
-
) {
|
|
1382
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1383
|
-
}
|
|
1384
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1385
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1386
|
-
});
|
|
1387
|
-
} catch (error) {
|
|
1388
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1389
|
-
}
|
|
1390
|
-
}
|
|
1391
|
-
|
|
1392
|
-
function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
|
|
1393
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1394
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1395
|
-
let gitRoot: string;
|
|
1396
|
-
try {
|
|
1397
|
-
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1398
|
-
} catch (error) {
|
|
1399
|
-
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1400
|
-
}
|
|
1401
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1402
|
-
return {
|
|
1403
|
-
status: 'ready',
|
|
1404
|
-
changed: false,
|
|
1405
|
-
taskId: resolved.taskId,
|
|
1406
|
-
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1407
|
-
finalization: commitFinalizationView(inspection),
|
|
1408
|
-
error: null
|
|
1409
|
-
};
|
|
1410
|
-
}
|
|
1411
|
-
|
|
1412
735
|
function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
1413
736
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1414
737
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
@@ -1429,27 +752,11 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1429
752
|
): OrchestrationResult {
|
|
1430
753
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1431
754
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1432
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1433
|
-
try {
|
|
1434
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1435
|
-
} catch (error) {
|
|
1436
|
-
return failed(
|
|
1437
|
-
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
1438
|
-
error instanceof Error ? error.message : String(error),
|
|
1439
|
-
resolved.taskId
|
|
1440
|
-
);
|
|
1441
|
-
}
|
|
1442
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1443
|
-
}
|
|
1444
755
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
1445
756
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
1446
757
|
}
|
|
1447
758
|
const run = readRun(resolved.taskDir);
|
|
1448
759
|
if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
1449
|
-
if (run.schemaVersion !== 3) {
|
|
1450
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation preparation requires orchestration schemaVersion 3', resolved.taskId);
|
|
1451
|
-
}
|
|
1452
|
-
if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
1453
760
|
if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
1454
761
|
const repositoryPending = matchingDelegations(() => true, options);
|
|
1455
762
|
if (repositoryPending.length > 0) {
|
|
@@ -1475,6 +782,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1475
782
|
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1476
783
|
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1477
784
|
}
|
|
785
|
+
const capabilityValidationError = options.validateLifecycleCapability?.();
|
|
786
|
+
if (capabilityValidationError) {
|
|
787
|
+
return failed(
|
|
788
|
+
capabilityValidationError.code,
|
|
789
|
+
capabilityValidationError.message,
|
|
790
|
+
resolved.taskId,
|
|
791
|
+
capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {}
|
|
792
|
+
);
|
|
793
|
+
}
|
|
1478
794
|
let beforeFingerprint: string;
|
|
1479
795
|
try {
|
|
1480
796
|
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
@@ -1499,6 +815,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1499
815
|
lifecycleProvenance: input.lifecycleProvenance ?? null,
|
|
1500
816
|
beforeFingerprint
|
|
1501
817
|
}, { id: options.id, now: options.now, monotonicNow: options.monotonicNow });
|
|
818
|
+
const capabilityError = options.consumeLifecycleCapability?.();
|
|
819
|
+
if (capabilityError) {
|
|
820
|
+
return failed(
|
|
821
|
+
capabilityError.code,
|
|
822
|
+
capabilityError.message,
|
|
823
|
+
resolved.taskId,
|
|
824
|
+
capabilityError.detail ? { detail: capabilityError.detail } : {}
|
|
825
|
+
);
|
|
826
|
+
}
|
|
1502
827
|
const updated = withUpdatedRun(run, {
|
|
1503
828
|
nextStage: next.stage,
|
|
1504
829
|
pendingDelegation: receipt,
|
|
@@ -1548,8 +873,8 @@ function dispatchOrchestrationDelegationUnlocked(
|
|
|
1548
873
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1549
874
|
const run = readRun(resolved.taskDir);
|
|
1550
875
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1551
|
-
if (run.
|
|
1552
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running
|
|
876
|
+
if (run.status !== 'running') {
|
|
877
|
+
return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running orchestration', resolved.taskId);
|
|
1553
878
|
}
|
|
1554
879
|
const result = dispatchDelegation(run.pendingDelegation, {
|
|
1555
880
|
now: options.now,
|
|
@@ -1596,7 +921,7 @@ function matchingDelegations(
|
|
|
1596
921
|
.filter((entry) => entry.isDirectory())
|
|
1597
922
|
.flatMap((entry) => {
|
|
1598
923
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1599
|
-
if (run
|
|
924
|
+
if (!run || run.status !== 'running' || !run.pendingDelegation) return [];
|
|
1600
925
|
const receipt = run.pendingDelegation;
|
|
1601
926
|
return predicate(receipt)
|
|
1602
927
|
? [{ taskId: entry.name, run }]
|
|
@@ -1667,9 +992,9 @@ function activateMatchingOrchestrationDelegation(
|
|
|
1667
992
|
if ('status' in replay) return matched;
|
|
1668
993
|
const receipt = replay.run.pendingDelegation!;
|
|
1669
994
|
const sameEvidence = receipt.parentId === event.parentId
|
|
1670
|
-
&& receipt.spawnMode === event.spawnMode
|
|
1671
|
-
&& receipt.actualModel === event.actualModel
|
|
1672
|
-
&& receipt.actualReasoningEffort === event.actualReasoningEffort
|
|
995
|
+
&& (receipt.spawnMode ?? null) === (event.spawnMode ?? null)
|
|
996
|
+
&& (receipt.actualModel ?? null) === foldBlankToNull(event.actualModel)
|
|
997
|
+
&& (receipt.actualReasoningEffort ?? null) === foldBlankToNull(event.actualReasoningEffort)
|
|
1673
998
|
&& receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
|
|
1674
999
|
&& receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
|
|
1675
1000
|
&& receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
|
|
@@ -1697,7 +1022,14 @@ function pauseMatchingOrchestrationDelegation(
|
|
|
1697
1022
|
|
|
1698
1023
|
function sealMatchingOrchestrationDelegation(
|
|
1699
1024
|
client: AgentClientId,
|
|
1700
|
-
event: Readonly<{
|
|
1025
|
+
event: Readonly<{
|
|
1026
|
+
nativeAgent: string;
|
|
1027
|
+
childId: string;
|
|
1028
|
+
actualModel?: string;
|
|
1029
|
+
actualReasoningEffort?: string;
|
|
1030
|
+
modelFallbackReason?: string;
|
|
1031
|
+
reasoningEffortFallbackReason?: string;
|
|
1032
|
+
}>,
|
|
1701
1033
|
options: OrchestrationOptions = {}
|
|
1702
1034
|
): OrchestrationResult {
|
|
1703
1035
|
const role = managedDelegationRole(event.nativeAgent);
|
|
@@ -1726,7 +1058,11 @@ function sealMatchingOrchestrationDelegation(
|
|
|
1726
1058
|
childId: event.childId,
|
|
1727
1059
|
exitCode: 0,
|
|
1728
1060
|
afterFingerprint,
|
|
1729
|
-
changedPaths
|
|
1061
|
+
changedPaths,
|
|
1062
|
+
actualModel: event.actualModel,
|
|
1063
|
+
actualReasoningEffort: event.actualReasoningEffort,
|
|
1064
|
+
modelFallbackReason: event.modelFallbackReason,
|
|
1065
|
+
reasoningEffortFallbackReason: event.reasoningEffortFallbackReason
|
|
1730
1066
|
}, options);
|
|
1731
1067
|
} catch (error) {
|
|
1732
1068
|
return pauseOrchestration(
|
|
@@ -1838,7 +1174,6 @@ function activateOrchestrationDelegation(
|
|
|
1838
1174
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1839
1175
|
const run = readRun(resolved.taskDir);
|
|
1840
1176
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1841
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'active delegation requires schemaVersion 3', resolved.taskId);
|
|
1842
1177
|
if (run.status !== 'running') {
|
|
1843
1178
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1844
1179
|
}
|
|
@@ -1869,9 +1204,6 @@ async function awaitOrchestrationDelegationActivation(
|
|
|
1869
1204
|
if (!run?.pendingDelegation) {
|
|
1870
1205
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1871
1206
|
}
|
|
1872
|
-
if (run.schemaVersion !== 3) {
|
|
1873
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
|
|
1874
|
-
}
|
|
1875
1207
|
const receipt = run.pendingDelegation;
|
|
1876
1208
|
const matches = receipt.stage === event.stage
|
|
1877
1209
|
&& receipt.round === event.round
|
|
@@ -1955,7 +1287,7 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1955
1287
|
receipt.client === 'codex'
|
|
1956
1288
|
&& receipt.lifecycleProvenance
|
|
1957
1289
|
&& hasActiveCodexLifecycleEvidence(
|
|
1958
|
-
|
|
1290
|
+
resolveAgentRuntimeStoreRoot({ repoRoot: resolved.repoRoot, store: 'lifecycle' }),
|
|
1959
1291
|
{
|
|
1960
1292
|
nativeAgent: `agent-infra-lifecycle-${receipt.role}`,
|
|
1961
1293
|
hookDefinitionHash: receipt.lifecycleProvenance.hookDefinitionHash
|
|
@@ -1969,9 +1301,6 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
1969
1301
|
resolved.taskId
|
|
1970
1302
|
);
|
|
1971
1303
|
}
|
|
1972
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1973
|
-
return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
|
|
1974
|
-
}
|
|
1975
1304
|
const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
1976
1305
|
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
1977
1306
|
stateRoot: resolved.repoRoot,
|
|
@@ -2011,7 +1340,6 @@ function completeOrchestrationStage(
|
|
|
2011
1340
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2012
1341
|
const run = readRun(resolved.taskDir);
|
|
2013
1342
|
if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
2014
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage completion requires schemaVersion 3', resolved.taskId);
|
|
2015
1343
|
if (!run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
|
|
2016
1344
|
const result = completeDelegationStage(run.pendingDelegation, event);
|
|
2017
1345
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -2034,7 +1362,6 @@ function inspectOrchestrationStage(
|
|
|
2034
1362
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
2035
1363
|
}
|
|
2036
1364
|
if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
2037
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage inspection requires schemaVersion 3', resolved.taskId);
|
|
2038
1365
|
const receipt = run.pendingDelegation;
|
|
2039
1366
|
if (
|
|
2040
1367
|
!receipt
|
|
@@ -2093,7 +1420,6 @@ function sealOrchestrationDelegation(
|
|
|
2093
1420
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2094
1421
|
const run = readRun(resolved.taskDir);
|
|
2095
1422
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
2096
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'delegation sealing requires schemaVersion 3', resolved.taskId);
|
|
2097
1423
|
const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
|
|
2098
1424
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2099
1425
|
const updated = withUpdatedRun(run, { pendingDelegation: result.receipt });
|
|
@@ -2106,7 +1432,6 @@ function advanceOrchestration(taskRef: string, options: OrchestrationOptions = {
|
|
|
2106
1432
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2107
1433
|
const run = readRun(resolved.taskDir);
|
|
2108
1434
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
2109
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'orchestration advance requires schemaVersion 3', resolved.taskId);
|
|
2110
1435
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
2111
1436
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2112
1437
|
const completed = result.receipt.stage === 'commit';
|
|
@@ -2145,17 +1470,14 @@ export {
|
|
|
2145
1470
|
activateOrchestrationDelegation,
|
|
2146
1471
|
awaitOrchestrationDelegationActivation,
|
|
2147
1472
|
advanceOrchestration,
|
|
2148
|
-
abortCommitIntent,
|
|
2149
|
-
beginCommitIntent,
|
|
2150
1473
|
beginOrResumeOrchestration,
|
|
2151
|
-
checkpointCommitIntent,
|
|
2152
1474
|
commitOrchestrationStageCompletion,
|
|
2153
|
-
completeCommitIntent,
|
|
2154
1475
|
completeOrchestrationStage,
|
|
2155
1476
|
dispatchOrchestrationDelegation,
|
|
2156
1477
|
inspectOrchestrationStage,
|
|
2157
1478
|
hasActivatableOrchestrationDelegation,
|
|
2158
1479
|
hasSealableOrchestrationDelegation,
|
|
1480
|
+
OrchestrationStateError,
|
|
2159
1481
|
orchestrationPath,
|
|
2160
1482
|
pauseMatchingOrchestrationDelegation,
|
|
2161
1483
|
pauseOrchestration,
|
|
@@ -2163,19 +1485,14 @@ export {
|
|
|
2163
1485
|
prepareOrchestrationDelegation,
|
|
2164
1486
|
readRun,
|
|
2165
1487
|
reconcileMatchingOrchestrationDelegation,
|
|
2166
|
-
recoverCommitIntent,
|
|
2167
1488
|
recoverPreparedOrchestrationDelegation,
|
|
2168
1489
|
routeOrchestration,
|
|
2169
1490
|
sealMatchingOrchestrationDelegation,
|
|
2170
1491
|
sealMatchingOrchestrationDelegationWithHostEvidence,
|
|
2171
1492
|
sealOrchestrationDelegation,
|
|
2172
|
-
startCommitAttempt,
|
|
2173
|
-
statusCommitIntent,
|
|
2174
1493
|
statusOrchestration,
|
|
2175
|
-
terminateCommitAttempt
|
|
2176
1494
|
};
|
|
2177
1495
|
export type {
|
|
2178
|
-
CommitIntentResult,
|
|
2179
1496
|
OrchestrationCompletionPlanResult,
|
|
2180
1497
|
OrchestrationModelPolicy,
|
|
2181
1498
|
OrchestrationNext,
|