@fitlab-ai/agent-infra 0.9.8 → 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 +6 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- 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 +56 -67
- package/dist/lib/git/workflow.js +10 -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 +2 -1
- package/dist/lib/internal/sandbox-control.js +79 -2
- package/dist/lib/internal/task-finalization.js +54 -0
- package/dist/lib/internal/task-orchestration.js +156 -291
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/sandbox/commands/rm.js +316 -55
- package/dist/lib/sandbox/control/client.js +190 -6
- package/dist/lib/sandbox/control/controller-registration.js +273 -0
- package/dist/lib/sandbox/control/executor.js +137 -5
- package/dist/lib/sandbox/control/lifecycle.js +3 -1
- package/dist/lib/sandbox/control/protocol.js +67 -6
- package/dist/lib/sandbox/index.js +5 -3
- package/dist/lib/sandbox/managed-fs.js +5 -0
- package/dist/lib/sandbox/task-resolver.js +50 -6
- package/dist/lib/sandbox/workspace-identity.js +27 -0
- package/dist/lib/sandbox/worktree-safety.js +215 -2
- 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/delegation-receipts.js +243 -2
- package/dist/lib/task/finalization.js +567 -0
- package/dist/lib/task/operation-outcome.js +33 -0
- package/dist/lib/task/orchestration.js +187 -761
- 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 +113 -60
- package/dist/lib/task/verification.js +2 -1
- package/dist/lib/task/workflow-warning-intents.js +50 -1
- package/dist/lifecycle-build-manifest.json +3 -3
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
- package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- 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 +83 -91
- package/lib/git/workflow.ts +11 -2
- 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 +2 -1
- package/lib/internal/sandbox-control.ts +92 -2
- package/lib/internal/task-finalization.ts +68 -0
- package/lib/internal/task-orchestration.ts +20 -103
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +347 -0
- package/lib/sandbox/control/executor.ts +163 -5
- package/lib/sandbox/control/lifecycle.ts +2 -1
- package/lib/sandbox/control/protocol.ts +80 -7
- package/lib/sandbox/index.ts +5 -3
- package/lib/sandbox/managed-fs.ts +5 -0
- package/lib/sandbox/task-resolver.ts +58 -7
- package/lib/sandbox/workspace-identity.ts +50 -2
- package/lib/sandbox/workspace-view.ts +3 -3
- package/lib/sandbox/worktree-safety.ts +236 -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/delegation-receipts.ts +258 -1
- package/lib/task/finalization.ts +719 -0
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +217 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +118 -63
- package/lib/task/verification.ts +3 -2
- package/lib/task/workflow-warning-intents.ts +57 -2
- 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 +1 -21
- package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
- 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/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';
|
|
@@ -15,6 +14,7 @@ import {
|
|
|
15
14
|
consumeDelegation,
|
|
16
15
|
dispatchDelegation,
|
|
17
16
|
foldBlankToNull,
|
|
17
|
+
isDelegationReceipt,
|
|
18
18
|
managedDelegationRole,
|
|
19
19
|
prepareDelegation,
|
|
20
20
|
sealDelegation
|
|
@@ -49,75 +49,17 @@ import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
|
|
|
49
49
|
import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
|
|
50
50
|
import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
|
|
51
51
|
import { hasActiveCodexLifecycleEvidence } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
52
|
-
import {
|
|
53
|
-
CommitIntentError,
|
|
54
|
-
commitIntentPath,
|
|
55
|
-
createCommitIntent,
|
|
56
|
-
digest,
|
|
57
|
-
readCommitIntent,
|
|
58
|
-
removeCommitIntent,
|
|
59
|
-
removeCommitIntentByDigest,
|
|
60
|
-
serialize,
|
|
61
|
-
updateCommitIntent
|
|
62
|
-
} from './commit-intent.ts';
|
|
63
|
-
import type { CommitIntent, PushEvidence } from './commit-intent.ts';
|
|
64
|
-
import {
|
|
65
|
-
inspectCommitFinalization,
|
|
66
|
-
planCommitTaskFinalization
|
|
67
|
-
} from './commit-finalization.ts';
|
|
68
|
-
import type { CommitFinalizationInspection } from './commit-finalization.ts';
|
|
69
|
-
import { captureTaskWriteMetadata, writeTask } from './write.ts';
|
|
70
|
-
import {
|
|
71
|
-
appendActivityEntry,
|
|
72
|
-
commitAttemptStartedNote,
|
|
73
|
-
locateActivityLog,
|
|
74
|
-
pairEntries
|
|
75
|
-
} from './activity-log.ts';
|
|
52
|
+
import type { CodexCapabilityProvenanceDetail } from '../agent-clients/adapters/codex-lifecycle/capability-store.ts';
|
|
76
53
|
|
|
77
54
|
type OrchestrationStatus = 'running' | 'paused' | 'completed';
|
|
78
|
-
type LegacyOrchestrationModelPolicy = Readonly<{
|
|
79
|
-
executor: string;
|
|
80
|
-
reviewer: string;
|
|
81
|
-
}>;
|
|
82
55
|
type ModelPolicySource = Readonly<{
|
|
83
56
|
kind: 'explicit' | 'project-config';
|
|
84
57
|
client: AgentClientId;
|
|
85
58
|
resolvedAt: string;
|
|
86
59
|
}>;
|
|
87
|
-
type ModelPolicyRecovery = Readonly<{
|
|
88
|
-
code: 'MODEL_POLICY_SUPPLEMENTED';
|
|
89
|
-
recoveredAt: string;
|
|
90
|
-
previousSchemaVersion: 1;
|
|
91
|
-
previousStatus: OrchestrationStatus;
|
|
92
|
-
previousPause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
93
|
-
policySource: ModelPolicySource;
|
|
94
|
-
receiptCount: 0;
|
|
95
|
-
pendingDelegation: false;
|
|
96
|
-
resultingStatus: OrchestrationStatus;
|
|
97
|
-
}>;
|
|
98
|
-
type ClientCapabilityRecovery = Readonly<{
|
|
99
|
-
code: 'CLIENT_CAPABILITY_ENABLED';
|
|
100
|
-
recoveredAt: string;
|
|
101
|
-
previousSchemaVersion: 2;
|
|
102
|
-
previousStatus: 'paused';
|
|
103
|
-
previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
|
|
104
|
-
client: 'codex';
|
|
105
|
-
guards: Readonly<{
|
|
106
|
-
stepCount: 0;
|
|
107
|
-
nextStage: null;
|
|
108
|
-
baselineEmpty: true;
|
|
109
|
-
receiptCount: 0;
|
|
110
|
-
pendingDelegation: false;
|
|
111
|
-
commitAuthorizationUnused: true;
|
|
112
|
-
completionEvidenceAbsent: true;
|
|
113
|
-
commitIntentAbsent: true;
|
|
114
|
-
}>;
|
|
115
|
-
resultingStatus: 'running';
|
|
116
|
-
}>;
|
|
117
60
|
type ClaudeCodeCapabilityRecovery = Readonly<{
|
|
118
|
-
code: '
|
|
61
|
+
code: 'CLIENT_CAPABILITY_ENABLED';
|
|
119
62
|
recoveredAt: string;
|
|
120
|
-
previousSchemaVersion: 3;
|
|
121
63
|
previousStatus: 'paused';
|
|
122
64
|
previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
|
|
123
65
|
client: 'claude-code';
|
|
@@ -129,29 +71,10 @@ type ClaudeCodeCapabilityRecovery = Readonly<{
|
|
|
129
71
|
pendingDelegation: false;
|
|
130
72
|
commitAuthorizationUnused: true;
|
|
131
73
|
completionEvidenceAbsent: true;
|
|
132
|
-
commitIntentAbsent: true;
|
|
133
74
|
}>;
|
|
134
75
|
resultingStatus: 'running';
|
|
135
76
|
}>;
|
|
136
|
-
type
|
|
137
|
-
code: 'SCHEMA_V3_MIGRATED';
|
|
138
|
-
recoveredAt: string;
|
|
139
|
-
previousSchemaVersion: 2;
|
|
140
|
-
previousStatus: 'paused';
|
|
141
|
-
previousPause: Readonly<{ code: string; message: string; recoverable: boolean }>;
|
|
142
|
-
guards: Readonly<{
|
|
143
|
-
stepCount: 0;
|
|
144
|
-
nextStage: null;
|
|
145
|
-
baselineEmpty: true;
|
|
146
|
-
receiptCount: 0;
|
|
147
|
-
pendingDelegation: false;
|
|
148
|
-
commitAuthorizationUnused: true;
|
|
149
|
-
completionEvidenceAbsent: true;
|
|
150
|
-
commitIntentAbsent: true;
|
|
151
|
-
}>;
|
|
152
|
-
resultingStatus: 'running';
|
|
153
|
-
}>;
|
|
154
|
-
type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery | ClaudeCodeCapabilityRecovery | SchemaMigrationRecovery;
|
|
77
|
+
type OrchestrationRecovery = ClaudeCodeCapabilityRecovery;
|
|
155
78
|
type CleanCompletionEvidence = Readonly<{
|
|
156
79
|
kind: 'reviewed-head-clean';
|
|
157
80
|
observedAt: string;
|
|
@@ -163,22 +86,21 @@ type CleanCompletionEvidence = Readonly<{
|
|
|
163
86
|
prHead: string;
|
|
164
87
|
}>;
|
|
165
88
|
type OrchestrationRun = Readonly<{
|
|
166
|
-
schemaVersion: 1 | 2 | 3;
|
|
167
89
|
taskId: string;
|
|
168
90
|
runId: string;
|
|
169
91
|
status: OrchestrationStatus;
|
|
170
92
|
nextStage: DelegationStage | null;
|
|
171
93
|
stepCount: number;
|
|
172
94
|
maxSteps: number;
|
|
173
|
-
modelPolicy
|
|
174
|
-
modelPolicySource
|
|
175
|
-
recoveryHistory
|
|
95
|
+
modelPolicy: OrchestrationModelPolicy;
|
|
96
|
+
modelPolicySource: ModelPolicySource;
|
|
97
|
+
recoveryHistory: readonly OrchestrationRecovery[];
|
|
176
98
|
baseline: string;
|
|
177
99
|
pendingDelegation: DelegationReceipt | null;
|
|
178
100
|
receipts: readonly DelegationReceipt[];
|
|
179
101
|
pause: Readonly<{ code: string; message: string; recoverable: boolean }> | null;
|
|
180
102
|
commitAuthorization: Readonly<{ issuedAt: string | null; consumedAt: string | null }>;
|
|
181
|
-
completionEvidence
|
|
103
|
+
completionEvidence: CleanCompletionEvidence | null;
|
|
182
104
|
createdAt: string;
|
|
183
105
|
updatedAt: string;
|
|
184
106
|
}>;
|
|
@@ -203,6 +125,7 @@ type OrchestrationResult = Readonly<{
|
|
|
203
125
|
client?: AgentClientId;
|
|
204
126
|
missingFields?: readonly string[];
|
|
205
127
|
modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
|
|
128
|
+
detail?: CodexCapabilityProvenanceDetail;
|
|
206
129
|
}> | null;
|
|
207
130
|
}>;
|
|
208
131
|
type OrchestrationStageIdentity = Readonly<{
|
|
@@ -239,40 +162,14 @@ type OrchestrationOptions = {
|
|
|
239
162
|
}>;
|
|
240
163
|
diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
|
|
241
164
|
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
165
|
+
validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
166
|
+
consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
242
167
|
token?: () => string;
|
|
243
168
|
monotonicNow?: () => number;
|
|
244
169
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
245
170
|
hasActiveLifecycleEvidence?: (receipt: DelegationReceipt) => boolean;
|
|
246
171
|
};
|
|
247
172
|
|
|
248
|
-
type CommitIntentResult = Readonly<{
|
|
249
|
-
status: 'ready' | 'failed';
|
|
250
|
-
changed: boolean;
|
|
251
|
-
taskId: string | null;
|
|
252
|
-
intent: Readonly<{
|
|
253
|
-
mode: CommitIntent['mode'];
|
|
254
|
-
phase: CommitIntent['phase'];
|
|
255
|
-
baselineHead: string;
|
|
256
|
-
currentHead: string;
|
|
257
|
-
committedHead: string | null;
|
|
258
|
-
pushEvidence: PushEvidence | null;
|
|
259
|
-
runId: string | null;
|
|
260
|
-
receiptId: string | null;
|
|
261
|
-
}> | null;
|
|
262
|
-
finalization?: Readonly<{
|
|
263
|
-
disposition: CommitFinalizationInspection['disposition'];
|
|
264
|
-
code: CommitFinalizationInspection['code'];
|
|
265
|
-
message: string;
|
|
266
|
-
currentHead: string;
|
|
267
|
-
committedHead: string | null;
|
|
268
|
-
needsAnchor: boolean;
|
|
269
|
-
needsLog: boolean;
|
|
270
|
-
attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
|
|
271
|
-
}>;
|
|
272
|
-
token?: string;
|
|
273
|
-
error: Readonly<{ code: string; message: string }> | null;
|
|
274
|
-
}>;
|
|
275
|
-
|
|
276
173
|
function supportsLifecycleDelegation(client: AgentClientId): boolean {
|
|
277
174
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
278
175
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -284,24 +181,147 @@ function orchestrationPath(taskDir: string): string {
|
|
|
284
181
|
return path.join(taskDir, 'orchestration.json');
|
|
285
182
|
}
|
|
286
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
|
+
|
|
287
315
|
function readRun(taskDir: string): OrchestrationRun | null {
|
|
288
316
|
const file = orchestrationPath(taskDir);
|
|
289
317
|
if (!fs.existsSync(file)) return null;
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
reasoningEffort: run.modelPolicy.executor.reasoningEffort
|
|
298
|
-
},
|
|
299
|
-
reviewer: {
|
|
300
|
-
model: run.modelPolicy.reviewer.model,
|
|
301
|
-
reasoningEffort: run.modelPolicy.reviewer.reasoningEffort
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
};
|
|
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
|
+
}
|
|
305
325
|
}
|
|
306
326
|
|
|
307
327
|
function atomicWrite(file: string, value: unknown): void {
|
|
@@ -327,79 +347,11 @@ function failed(
|
|
|
327
347
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
328
348
|
}
|
|
329
349
|
|
|
330
|
-
function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
|
|
331
|
-
return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
function repositoryHead(repoRoot: string): string {
|
|
335
|
-
return execFileSync('git', ['rev-parse', 'HEAD'], {
|
|
336
|
-
cwd: repoRoot,
|
|
337
|
-
encoding: 'utf8',
|
|
338
|
-
stdio: ['ignore', 'pipe', 'pipe']
|
|
339
|
-
}).trim();
|
|
340
|
-
}
|
|
341
|
-
|
|
342
350
|
function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
|
|
343
351
|
if (!options.gitWorktreeRoot) return stateRoot;
|
|
344
352
|
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
345
353
|
}
|
|
346
354
|
|
|
347
|
-
function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
|
|
348
|
-
return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
|
|
349
|
-
cwd: repoRoot,
|
|
350
|
-
stdio: 'ignore'
|
|
351
|
-
}).status === 0;
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
|
|
355
|
-
return {
|
|
356
|
-
mode: intent.mode,
|
|
357
|
-
phase: intent.phase,
|
|
358
|
-
baselineHead: intent.baselineHead,
|
|
359
|
-
currentHead,
|
|
360
|
-
committedHead: intent.committedHead,
|
|
361
|
-
pushEvidence: intent.pushEvidence,
|
|
362
|
-
runId: intent.orchestration?.runId ?? null,
|
|
363
|
-
receiptId: intent.orchestration?.receiptId ?? null
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
|
|
368
|
-
return {
|
|
369
|
-
disposition: inspection.disposition,
|
|
370
|
-
code: inspection.code,
|
|
371
|
-
message: inspection.message,
|
|
372
|
-
currentHead: inspection.currentHead,
|
|
373
|
-
committedHead: inspection.committedHead,
|
|
374
|
-
needsAnchor: inspection.needsAnchor,
|
|
375
|
-
needsLog: inspection.needsLog,
|
|
376
|
-
attempt: inspection.attempt
|
|
377
|
-
};
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
|
|
381
|
-
return run !== null
|
|
382
|
-
&& [1, 2, 3].includes(run.schemaVersion)
|
|
383
|
-
&& run.taskId === taskId
|
|
384
|
-
&& ['running', 'paused', 'completed'].includes(run.status)
|
|
385
|
-
&& Array.isArray(run.receipts)
|
|
386
|
-
&& Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
|
|
387
|
-
&& typeof run.commitAuthorization === 'object'
|
|
388
|
-
&& run.commitAuthorization !== null
|
|
389
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
|
|
390
|
-
&& Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
|
|
394
|
-
if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
|
|
395
|
-
if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
|
|
396
|
-
return commitIntentFailed(
|
|
397
|
-
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
398
|
-
error instanceof Error ? error.message : String(error),
|
|
399
|
-
taskId
|
|
400
|
-
);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
355
|
function validModel(value: unknown): value is string {
|
|
404
356
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
405
357
|
}
|
|
@@ -424,12 +376,8 @@ function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationMod
|
|
|
424
376
|
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
425
377
|
}
|
|
426
378
|
|
|
427
|
-
function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
|
|
428
|
-
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
429
|
-
}
|
|
430
|
-
|
|
431
379
|
function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
|
|
432
|
-
return
|
|
380
|
+
return run.modelPolicy[role];
|
|
433
381
|
}
|
|
434
382
|
|
|
435
383
|
function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
|
|
@@ -439,26 +387,8 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
|
|
|
439
387
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
440
388
|
}
|
|
441
389
|
|
|
442
|
-
function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
|
|
443
|
-
return run.schemaVersion === 2
|
|
444
|
-
&& run.status === 'paused'
|
|
445
|
-
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
446
|
-
&& run.modelPolicySource?.client === 'codex'
|
|
447
|
-
&& run.stepCount === 0
|
|
448
|
-
&& run.nextStage === null
|
|
449
|
-
&& run.baseline === ''
|
|
450
|
-
&& run.pendingDelegation === null
|
|
451
|
-
&& run.receipts.length === 0
|
|
452
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
453
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
454
|
-
&& run.completionEvidence == null
|
|
455
|
-
&& !fs.existsSync(commitIntentPath(taskDir))
|
|
456
|
-
&& (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
|
|
457
|
-
}
|
|
458
|
-
|
|
459
390
|
function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
|
|
460
|
-
return run.
|
|
461
|
-
&& run.status === 'paused'
|
|
391
|
+
return run.status === 'paused'
|
|
462
392
|
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
463
393
|
&& run.modelPolicySource?.client === 'claude-code'
|
|
464
394
|
&& run.stepCount === 0
|
|
@@ -469,23 +399,7 @@ function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: st
|
|
|
469
399
|
&& run.commitAuthorization?.issuedAt === null
|
|
470
400
|
&& run.commitAuthorization?.consumedAt === null
|
|
471
401
|
&& run.completionEvidence == null
|
|
472
|
-
&&
|
|
473
|
-
&& (run.recoveryHistory ?? []).length === 0;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function canMigrateRecoverableV2Pause(run: OrchestrationRun, taskDir: string): boolean {
|
|
477
|
-
return run.schemaVersion === 2
|
|
478
|
-
&& run.status === 'paused'
|
|
479
|
-
&& run.pause?.recoverable === true
|
|
480
|
-
&& run.stepCount === 0
|
|
481
|
-
&& run.nextStage === null
|
|
482
|
-
&& run.baseline === ''
|
|
483
|
-
&& run.pendingDelegation === null
|
|
484
|
-
&& run.receipts.length === 0
|
|
485
|
-
&& run.commitAuthorization?.issuedAt === null
|
|
486
|
-
&& run.commitAuthorization?.consumedAt === null
|
|
487
|
-
&& run.completionEvidence == null
|
|
488
|
-
&& !fs.existsSync(commitIntentPath(taskDir));
|
|
402
|
+
&& run.recoveryHistory.length === 0;
|
|
489
403
|
}
|
|
490
404
|
|
|
491
405
|
function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -501,235 +415,56 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
501
415
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
502
416
|
}
|
|
503
417
|
if (existing) {
|
|
504
|
-
if (
|
|
505
|
-
![1, 2, 3].includes(existing.schemaVersion)
|
|
506
|
-
|| !Array.isArray(existing.receipts)
|
|
507
|
-
|| !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')
|
|
508
|
-
) {
|
|
509
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
510
|
-
}
|
|
511
|
-
if (existing.status === 'completed' && existing.schemaVersion === 1) {
|
|
418
|
+
if (existing.status === 'completed') {
|
|
512
419
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
513
420
|
}
|
|
514
|
-
if (existing.
|
|
515
|
-
|
|
516
|
-
if (
|
|
517
|
-
policyError
|
|
518
|
-
|| !existing.modelPolicySource
|
|
519
|
-
|| !isAgentClientId(existing.modelPolicySource.client)
|
|
520
|
-
|| !Array.isArray(existing.recoveryHistory)
|
|
521
|
-
) {
|
|
522
|
-
return failed('ORCHESTRATION_STATE_INVALID', `schemaVersion ${existing.schemaVersion} orchestration state is incomplete`, resolved.taskId);
|
|
523
|
-
}
|
|
524
|
-
if (existing.status === 'completed') {
|
|
525
|
-
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
526
|
-
}
|
|
527
|
-
if (existing.schemaVersion === 2 && existing.status === 'running') {
|
|
528
|
-
return failed(
|
|
529
|
-
'ORCHESTRATION_STATE_INVALID',
|
|
530
|
-
'active schemaVersion 2 runs cannot be advanced by lifecycle protocol v3',
|
|
531
|
-
resolved.taskId
|
|
532
|
-
);
|
|
533
|
-
}
|
|
534
|
-
if (existing.modelPolicySource.client !== options.client) {
|
|
535
|
-
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
536
|
-
}
|
|
537
|
-
if (options.modelPolicy) {
|
|
538
|
-
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
539
|
-
if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
540
|
-
if (!sameModelPolicy(existing.modelPolicy as OrchestrationModelPolicy, options.modelPolicy)) {
|
|
541
|
-
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
545
|
-
if (existing.modelPolicySource.client === 'claude-code') {
|
|
546
|
-
if (!canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
547
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
548
|
-
}
|
|
549
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
550
|
-
const previousPause = existing.pause as ClaudeCodeCapabilityRecovery['previousPause'];
|
|
551
|
-
const recovery: ClaudeCodeCapabilityRecovery = {
|
|
552
|
-
code: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION',
|
|
553
|
-
recoveredAt: now,
|
|
554
|
-
previousSchemaVersion: 3,
|
|
555
|
-
previousStatus: 'paused',
|
|
556
|
-
previousPause,
|
|
557
|
-
client: 'claude-code',
|
|
558
|
-
guards: {
|
|
559
|
-
stepCount: 0,
|
|
560
|
-
nextStage: null,
|
|
561
|
-
baselineEmpty: true,
|
|
562
|
-
receiptCount: 0,
|
|
563
|
-
pendingDelegation: false,
|
|
564
|
-
commitAuthorizationUnused: true,
|
|
565
|
-
completionEvidenceAbsent: true,
|
|
566
|
-
commitIntentAbsent: true
|
|
567
|
-
},
|
|
568
|
-
resultingStatus: 'running'
|
|
569
|
-
};
|
|
570
|
-
const resumed = withUpdatedRun(existing, {
|
|
571
|
-
status: 'running',
|
|
572
|
-
pause: null,
|
|
573
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
574
|
-
}, () => now);
|
|
575
|
-
saveRun(resolved.taskDir, resumed);
|
|
576
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
577
|
-
}
|
|
578
|
-
if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
|
|
579
|
-
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
580
|
-
}
|
|
581
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
582
|
-
const previousPause = existing.pause as ClientCapabilityRecovery['previousPause'];
|
|
583
|
-
const recovery: ClientCapabilityRecovery = {
|
|
584
|
-
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
585
|
-
recoveredAt: now,
|
|
586
|
-
previousSchemaVersion: 2,
|
|
587
|
-
previousStatus: 'paused',
|
|
588
|
-
previousPause,
|
|
589
|
-
client: 'codex',
|
|
590
|
-
guards: {
|
|
591
|
-
stepCount: 0,
|
|
592
|
-
nextStage: null,
|
|
593
|
-
baselineEmpty: true,
|
|
594
|
-
receiptCount: 0,
|
|
595
|
-
pendingDelegation: false,
|
|
596
|
-
commitAuthorizationUnused: true,
|
|
597
|
-
completionEvidenceAbsent: true,
|
|
598
|
-
commitIntentAbsent: true
|
|
599
|
-
},
|
|
600
|
-
resultingStatus: 'running'
|
|
601
|
-
};
|
|
602
|
-
const resumed = withUpdatedRun(existing, {
|
|
603
|
-
schemaVersion: 3,
|
|
604
|
-
status: 'running',
|
|
605
|
-
pause: null,
|
|
606
|
-
recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
|
|
607
|
-
}, () => now);
|
|
608
|
-
saveRun(resolved.taskDir, resumed);
|
|
609
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
610
|
-
}
|
|
611
|
-
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
612
|
-
if (existing.schemaVersion === 2 && !canMigrateRecoverableV2Pause(existing, resolved.taskDir)) {
|
|
613
|
-
const paused = withUpdatedRun(existing, {
|
|
614
|
-
pause: {
|
|
615
|
-
code: 'ORCHESTRATION_SCHEMA_MIGRATION_REQUIRED',
|
|
616
|
-
message: 'schemaVersion 2 recoverable pause contains execution evidence and cannot migrate automatically',
|
|
617
|
-
recoverable: false
|
|
618
|
-
}
|
|
619
|
-
});
|
|
620
|
-
saveRun(resolved.taskDir, paused);
|
|
621
|
-
return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
|
|
622
|
-
}
|
|
623
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
624
|
-
const recovery: SchemaMigrationRecovery | null = existing.schemaVersion === 2 ? {
|
|
625
|
-
code: 'SCHEMA_V3_MIGRATED',
|
|
626
|
-
recoveredAt: now,
|
|
627
|
-
previousSchemaVersion: 2,
|
|
628
|
-
previousStatus: 'paused',
|
|
629
|
-
previousPause: existing.pause,
|
|
630
|
-
guards: {
|
|
631
|
-
stepCount: 0,
|
|
632
|
-
nextStage: null,
|
|
633
|
-
baselineEmpty: true,
|
|
634
|
-
receiptCount: 0,
|
|
635
|
-
pendingDelegation: false,
|
|
636
|
-
commitAuthorizationUnused: true,
|
|
637
|
-
completionEvidenceAbsent: true,
|
|
638
|
-
commitIntentAbsent: true
|
|
639
|
-
},
|
|
640
|
-
resultingStatus: 'running'
|
|
641
|
-
} : null;
|
|
642
|
-
const resumed = withUpdatedRun(existing, {
|
|
643
|
-
schemaVersion: 3,
|
|
644
|
-
status: 'running',
|
|
645
|
-
pause: null,
|
|
646
|
-
...(recovery ? { recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery]) } : {})
|
|
647
|
-
}, () => now);
|
|
648
|
-
saveRun(resolved.taskDir, resumed);
|
|
649
|
-
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
650
|
-
}
|
|
651
|
-
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);
|
|
652
423
|
}
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
policy = resolveProjectPolicy(resolved.repoRoot, options.client);
|
|
659
|
-
sourceKind = 'project-config';
|
|
660
|
-
} catch (error) {
|
|
661
|
-
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);
|
|
662
429
|
}
|
|
663
430
|
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
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 };
|
|
695
461
|
}
|
|
696
|
-
if (
|
|
697
|
-
existing.
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
existing.modelPolicy.executor !== policy!.executor.model
|
|
701
|
-
|| existing.modelPolicy.reviewer !== policy!.reviewer.model
|
|
702
|
-
)
|
|
703
|
-
) {
|
|
704
|
-
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 };
|
|
705
466
|
}
|
|
706
|
-
|
|
707
|
-
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
708
|
-
const clearsModelPause = existing.pause?.code === 'ORCHESTRATION_MODEL_EVIDENCE_MISSING';
|
|
709
|
-
const resultingStatus = clearsModelPause ? 'running' : existing.status;
|
|
710
|
-
const recovery: OrchestrationRecovery = {
|
|
711
|
-
code: 'MODEL_POLICY_SUPPLEMENTED',
|
|
712
|
-
recoveredAt: now,
|
|
713
|
-
previousSchemaVersion: 1,
|
|
714
|
-
previousStatus: existing.status,
|
|
715
|
-
previousPause: existing.pause,
|
|
716
|
-
policySource: source,
|
|
717
|
-
receiptCount: 0,
|
|
718
|
-
pendingDelegation: false,
|
|
719
|
-
resultingStatus
|
|
720
|
-
};
|
|
721
|
-
const recovered: OrchestrationRun = Object.freeze({
|
|
722
|
-
...existing,
|
|
723
|
-
schemaVersion: 3,
|
|
724
|
-
status: resultingStatus,
|
|
725
|
-
modelPolicy: policy!,
|
|
726
|
-
modelPolicySource: source,
|
|
727
|
-
recoveryHistory: Object.freeze([recovery]),
|
|
728
|
-
pause: clearsModelPause ? null : existing.pause,
|
|
729
|
-
updatedAt: now
|
|
730
|
-
});
|
|
731
|
-
saveRun(resolved.taskDir, recovered);
|
|
732
|
-
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 };
|
|
733
468
|
}
|
|
734
469
|
|
|
735
470
|
let policy = options.modelPolicy;
|
|
@@ -751,7 +486,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
751
486
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
752
487
|
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
753
488
|
const run: OrchestrationRun = {
|
|
754
|
-
schemaVersion: 3,
|
|
755
489
|
taskId: resolved.taskId,
|
|
756
490
|
runId: (options.id ?? randomUUID)(),
|
|
757
491
|
status: 'running',
|
|
@@ -998,488 +732,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
998
732
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
999
733
|
}
|
|
1000
734
|
|
|
1001
|
-
function appendCommitActivity(
|
|
1002
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1003
|
-
step: string,
|
|
1004
|
-
agent: string,
|
|
1005
|
-
note: string
|
|
1006
|
-
): Readonly<{ code: string; message: string }> | null {
|
|
1007
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
1008
|
-
const activity = locateActivityLog(taskContent);
|
|
1009
|
-
if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
|
|
1010
|
-
const metadata = captureTaskWriteMetadata();
|
|
1011
|
-
const written = writeTask({
|
|
1012
|
-
taskRef: resolved.taskId,
|
|
1013
|
-
expectedState: 'active',
|
|
1014
|
-
mutations: [{
|
|
1015
|
-
kind: 'section',
|
|
1016
|
-
aliases: ['活动日志', 'Activity Log'],
|
|
1017
|
-
heading: activity.heading,
|
|
1018
|
-
body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
|
|
1019
|
-
}]
|
|
1020
|
-
}, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
|
|
1021
|
-
return written.status === 'failed' ? written.error : null;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
function startCommitAttempt(
|
|
1025
|
-
taskRef: string,
|
|
1026
|
-
input: Readonly<{ agent: string }>,
|
|
1027
|
-
options: OrchestrationOptions = {}
|
|
1028
|
-
): CommitIntentResult {
|
|
1029
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1030
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1031
|
-
try {
|
|
1032
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1033
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
|
|
1034
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1035
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
1036
|
-
}
|
|
1037
|
-
const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
|
|
1038
|
-
const activity = locateActivityLog(taskContent);
|
|
1039
|
-
if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
|
|
1040
|
-
const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
|
|
1041
|
-
if (open.length > 0) {
|
|
1042
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
|
|
1043
|
-
}
|
|
1044
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1045
|
-
const attempt = (options.id ?? randomUUID)();
|
|
1046
|
-
const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
|
|
1047
|
-
attempt,
|
|
1048
|
-
baseline: currentHead,
|
|
1049
|
-
agent: input.agent
|
|
1050
|
-
}));
|
|
1051
|
-
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
1052
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1053
|
-
return {
|
|
1054
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
1055
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
1056
|
-
};
|
|
1057
|
-
});
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1060
|
-
}
|
|
1061
|
-
}
|
|
1062
|
-
|
|
1063
|
-
function terminateCommitAttempt(
|
|
1064
|
-
taskRef: string,
|
|
1065
|
-
input: Readonly<{ attempt: string; agent: string; code: string }>,
|
|
1066
|
-
options: OrchestrationOptions = {}
|
|
1067
|
-
): CommitIntentResult {
|
|
1068
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1069
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1070
|
-
try {
|
|
1071
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1072
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
|
|
1073
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1074
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
|
|
1075
|
-
}
|
|
1076
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1077
|
-
if (
|
|
1078
|
-
inspection.disposition !== 'retryable-start'
|
|
1079
|
-
|| inspection.attempt?.attempt !== input.attempt
|
|
1080
|
-
|| inspection.attempt.agent !== input.agent
|
|
1081
|
-
|| inspection.attempt.baseline !== inspection.currentHead
|
|
1082
|
-
) {
|
|
1083
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
|
|
1084
|
-
}
|
|
1085
|
-
const error = appendCommitActivity(
|
|
1086
|
-
resolved,
|
|
1087
|
-
'Commit [aborted]',
|
|
1088
|
-
input.agent,
|
|
1089
|
-
`aborted; attempt=${input.attempt}; code=${input.code}`
|
|
1090
|
-
);
|
|
1091
|
-
if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
|
|
1092
|
-
return {
|
|
1093
|
-
status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
|
|
1094
|
-
finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
|
|
1095
|
-
error: null
|
|
1096
|
-
};
|
|
1097
|
-
});
|
|
1098
|
-
} catch (error) {
|
|
1099
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1100
|
-
}
|
|
1101
|
-
}
|
|
1102
|
-
|
|
1103
|
-
function beginCommitIntent(
|
|
1104
|
-
taskRef: string,
|
|
1105
|
-
input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
|
|
1106
|
-
options: OrchestrationOptions = {}
|
|
1107
|
-
): CommitIntentResult {
|
|
1108
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1109
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1110
|
-
try {
|
|
1111
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1112
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
|
|
1113
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1114
|
-
if (currentHead !== input.baselineHead) {
|
|
1115
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
|
|
1116
|
-
}
|
|
1117
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1118
|
-
try {
|
|
1119
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1120
|
-
} catch (error) {
|
|
1121
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1122
|
-
}
|
|
1123
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
|
|
1124
|
-
}
|
|
1125
|
-
const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1126
|
-
if (
|
|
1127
|
-
attemptInspection.disposition !== 'retryable-start'
|
|
1128
|
-
|| attemptInspection.attempt?.attempt !== input.attempt
|
|
1129
|
-
|| attemptInspection.attempt.agent !== input.agent
|
|
1130
|
-
|| attemptInspection.attempt.baseline !== input.baselineHead
|
|
1131
|
-
) {
|
|
1132
|
-
return commitIntentFailed(
|
|
1133
|
-
'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
|
|
1134
|
-
'commit begin requires one matching retryable Commit attempt',
|
|
1135
|
-
resolved.taskId
|
|
1136
|
-
);
|
|
1137
|
-
}
|
|
1138
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
1139
|
-
let run: OrchestrationRun | null = null;
|
|
1140
|
-
let sourceRunBytes: string | null = null;
|
|
1141
|
-
if (fs.existsSync(runFile)) {
|
|
1142
|
-
sourceRunBytes = fs.readFileSync(runFile, 'utf8');
|
|
1143
|
-
try {
|
|
1144
|
-
run = readRun(resolved.taskDir);
|
|
1145
|
-
} catch (error) {
|
|
1146
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1147
|
-
}
|
|
1148
|
-
if (!validCommitRun(run, resolved.taskId)) {
|
|
1149
|
-
return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
1154
|
-
const failAndPause = (code: string, message: string): CommitIntentResult => {
|
|
1155
|
-
if (run?.status === 'running') {
|
|
1156
|
-
saveRun(resolved.taskDir, withUpdatedRun(run, {
|
|
1157
|
-
status: 'paused', pause: { code, message, recoverable: true }
|
|
1158
|
-
}, () => now));
|
|
1159
|
-
}
|
|
1160
|
-
return commitIntentFailed(code, message, resolved.taskId);
|
|
1161
|
-
};
|
|
1162
|
-
let orchestration: CommitIntent['orchestration'] = null;
|
|
1163
|
-
if (!input.orchestrated) {
|
|
1164
|
-
if (run?.pendingDelegation) {
|
|
1165
|
-
return commitIntentFailed(
|
|
1166
|
-
'ORCHESTRATION_STANDALONE_BUSY',
|
|
1167
|
-
'standalone commit is blocked by a pending orchestration delegation',
|
|
1168
|
-
resolved.taskId
|
|
1169
|
-
);
|
|
1170
|
-
}
|
|
1171
|
-
} else {
|
|
1172
|
-
if (!run || run.status !== 'running') {
|
|
1173
|
-
return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
|
|
1174
|
-
}
|
|
1175
|
-
const receipt = run.pendingDelegation;
|
|
1176
|
-
if (
|
|
1177
|
-
!receipt
|
|
1178
|
-
|| receipt.status !== 'activated'
|
|
1179
|
-
|| receipt.stage !== 'commit'
|
|
1180
|
-
|| receipt.round !== 1
|
|
1181
|
-
|| receipt.artifact !== 'commit'
|
|
1182
|
-
|| receipt.role !== 'executor'
|
|
1183
|
-
) {
|
|
1184
|
-
return failAndPause(
|
|
1185
|
-
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1186
|
-
'orchestrated commit requires one matching activated commit delegation'
|
|
1187
|
-
);
|
|
1188
|
-
}
|
|
1189
|
-
if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
|
|
1190
|
-
return failAndPause(
|
|
1191
|
-
'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
|
|
1192
|
-
'orchestrated commit requires an unconsumed one-use authorization'
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
const planned = planOrchestrationStageCompletion(taskRef, {
|
|
1196
|
-
stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
|
|
1197
|
-
}, { ...options, now: () => now });
|
|
1198
|
-
if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
|
|
1199
|
-
return failAndPause(
|
|
1200
|
-
planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1201
|
-
planned.result.error?.message ?? 'commit delegation could not be completed'
|
|
1202
|
-
);
|
|
1203
|
-
}
|
|
1204
|
-
const plannedRunBytes = serialize(planned.plan.updatedRun);
|
|
1205
|
-
orchestration = {
|
|
1206
|
-
runId: run.runId,
|
|
1207
|
-
receiptId: receipt.id,
|
|
1208
|
-
authorizationIssuedAt: run.commitAuthorization.issuedAt,
|
|
1209
|
-
sourceRunDigest: digest(sourceRunBytes!),
|
|
1210
|
-
plannedRunDigest: digest(plannedRunBytes),
|
|
1211
|
-
completionUpdatedAt: planned.plan.updatedRun.updatedAt,
|
|
1212
|
-
plannedReceipt: planned.plan.updatedRun.pendingDelegation
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
const created = createCommitIntent(resolved.taskDir, {
|
|
1217
|
-
taskId: resolved.taskId,
|
|
1218
|
-
mode: input.orchestrated ? 'orchestrated' : 'standalone',
|
|
1219
|
-
phase: 'prepared',
|
|
1220
|
-
baselineHead: input.baselineHead,
|
|
1221
|
-
committedHead: null,
|
|
1222
|
-
pushEvidence: null,
|
|
1223
|
-
orchestration,
|
|
1224
|
-
createdAt: now,
|
|
1225
|
-
updatedAt: now
|
|
1226
|
-
}, { token: options.token });
|
|
1227
|
-
return {
|
|
1228
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1229
|
-
intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
|
|
1230
|
-
};
|
|
1231
|
-
});
|
|
1232
|
-
} catch (error) {
|
|
1233
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
|
|
1237
|
-
function checkpointCommitIntent(
|
|
1238
|
-
taskRef: string,
|
|
1239
|
-
input: Readonly<{
|
|
1240
|
-
token: string;
|
|
1241
|
-
kind: 'committed' | 'pushed';
|
|
1242
|
-
head: string;
|
|
1243
|
-
remote?: string;
|
|
1244
|
-
ref?: string;
|
|
1245
|
-
}>,
|
|
1246
|
-
options: OrchestrationOptions = {}
|
|
1247
|
-
): CommitIntentResult {
|
|
1248
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1249
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1250
|
-
try {
|
|
1251
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1252
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
|
|
1253
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1254
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1255
|
-
if (currentHead !== input.head) {
|
|
1256
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
|
|
1257
|
-
}
|
|
1258
|
-
if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
|
|
1259
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
|
|
1260
|
-
}
|
|
1261
|
-
if (input.kind === 'pushed') {
|
|
1262
|
-
const expectedHead = intent.committedHead ?? intent.baselineHead;
|
|
1263
|
-
if (input.head !== expectedHead || !input.remote || !input.ref) {
|
|
1264
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
|
|
1265
|
-
}
|
|
1266
|
-
}
|
|
1267
|
-
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
1268
|
-
const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
|
|
1269
|
-
? { phase: 'committed', committedHead: input.head, updatedAt: now }
|
|
1270
|
-
: {
|
|
1271
|
-
phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
|
|
1272
|
-
pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
|
|
1273
|
-
});
|
|
1274
|
-
return {
|
|
1275
|
-
status: 'ready', changed: true, taskId: resolved.taskId,
|
|
1276
|
-
intent: commitIntentView(updated, currentHead), error: null
|
|
1277
|
-
};
|
|
1278
|
-
});
|
|
1279
|
-
} catch (error) {
|
|
1280
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1281
|
-
}
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function completeCommitOrchestration(
|
|
1285
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1286
|
-
intent: CommitIntent,
|
|
1287
|
-
agent: string
|
|
1288
|
-
): Readonly<{ code: string; message: string }> | null {
|
|
1289
|
-
if (intent.orchestration === null) return null;
|
|
1290
|
-
if (intent.orchestration.plannedReceipt.agent !== agent) {
|
|
1291
|
-
return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
|
|
1292
|
-
}
|
|
1293
|
-
const runFile = orchestrationPath(resolved.taskDir);
|
|
1294
|
-
if (!fs.existsSync(runFile)) {
|
|
1295
|
-
return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1296
|
-
}
|
|
1297
|
-
const currentBytes = fs.readFileSync(runFile, 'utf8');
|
|
1298
|
-
const currentDigest = digest(currentBytes);
|
|
1299
|
-
if (currentDigest === intent.orchestration.sourceRunDigest) {
|
|
1300
|
-
const run = readRun(resolved.taskDir);
|
|
1301
|
-
if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
|
|
1302
|
-
const plannedRun: OrchestrationRun = Object.freeze({
|
|
1303
|
-
...run,
|
|
1304
|
-
pendingDelegation: intent.orchestration.plannedReceipt,
|
|
1305
|
-
updatedAt: intent.orchestration.completionUpdatedAt
|
|
1306
|
-
});
|
|
1307
|
-
if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
|
|
1308
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
|
|
1309
|
-
}
|
|
1310
|
-
atomicWrite(runFile, plannedRun);
|
|
1311
|
-
if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
|
|
1312
|
-
return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
|
|
1313
|
-
}
|
|
1314
|
-
} else if (currentDigest !== intent.orchestration.plannedRunDigest) {
|
|
1315
|
-
return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
|
|
1316
|
-
}
|
|
1317
|
-
return null;
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
function finalizeCommitIntentUnlocked(
|
|
1321
|
-
resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
|
|
1322
|
-
inspection: CommitFinalizationInspection,
|
|
1323
|
-
agent: string,
|
|
1324
|
-
remove: () => void
|
|
1325
|
-
): CommitIntentResult {
|
|
1326
|
-
if (inspection.disposition !== 'recoverable' || !inspection.intent) {
|
|
1327
|
-
return commitIntentFailed(
|
|
1328
|
-
inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
|
|
1329
|
-
inspection.message,
|
|
1330
|
-
resolved.taskId
|
|
1331
|
-
);
|
|
1332
|
-
}
|
|
1333
|
-
if (
|
|
1334
|
-
inspection.intent.orchestration !== null
|
|
1335
|
-
&& inspection.intent.orchestration.plannedReceipt.agent !== agent
|
|
1336
|
-
) {
|
|
1337
|
-
return commitIntentFailed(
|
|
1338
|
-
'ORCHESTRATION_PROVENANCE_MISMATCH',
|
|
1339
|
-
'completion agent does not match the planned receipt',
|
|
1340
|
-
resolved.taskId
|
|
1341
|
-
);
|
|
1342
|
-
}
|
|
1343
|
-
const metadata = captureTaskWriteMetadata();
|
|
1344
|
-
const taskPlan = planCommitTaskFinalization(
|
|
1345
|
-
resolved.taskDir,
|
|
1346
|
-
inspection,
|
|
1347
|
-
agent,
|
|
1348
|
-
metadata.timestamp
|
|
1349
|
-
);
|
|
1350
|
-
const written = writeTask({
|
|
1351
|
-
taskRef: resolved.taskId,
|
|
1352
|
-
expectedState: 'active',
|
|
1353
|
-
mutations: taskPlan.mutations
|
|
1354
|
-
}, {
|
|
1355
|
-
repoRoot: resolved.repoRoot,
|
|
1356
|
-
metadataProvider: () => metadata
|
|
1357
|
-
});
|
|
1358
|
-
if (written.status === 'failed') {
|
|
1359
|
-
return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
|
|
1360
|
-
}
|
|
1361
|
-
const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
|
|
1362
|
-
if (orchestrationError) {
|
|
1363
|
-
return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
|
|
1364
|
-
}
|
|
1365
|
-
try {
|
|
1366
|
-
remove();
|
|
1367
|
-
} catch (error) {
|
|
1368
|
-
return commitIntentFailed(
|
|
1369
|
-
'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
|
|
1370
|
-
error instanceof Error ? error.message : String(error),
|
|
1371
|
-
resolved.taskId
|
|
1372
|
-
);
|
|
1373
|
-
}
|
|
1374
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
function completeCommitIntent(
|
|
1378
|
-
taskRef: string,
|
|
1379
|
-
input: Readonly<{ token: string; agent: string }>,
|
|
1380
|
-
options: OrchestrationOptions = {}
|
|
1381
|
-
): CommitIntentResult {
|
|
1382
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1383
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1384
|
-
try {
|
|
1385
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1386
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
|
|
1387
|
-
readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1388
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1389
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1390
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1391
|
-
});
|
|
1392
|
-
});
|
|
1393
|
-
} catch (error) {
|
|
1394
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1395
|
-
}
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
function recoverCommitIntent(
|
|
1399
|
-
taskRef: string,
|
|
1400
|
-
input: Readonly<{ agent: string }>,
|
|
1401
|
-
options: OrchestrationOptions = {}
|
|
1402
|
-
): CommitIntentResult {
|
|
1403
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1404
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1405
|
-
try {
|
|
1406
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1407
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
|
|
1408
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1409
|
-
if (inspection.disposition === 'retryable-start') {
|
|
1410
|
-
return {
|
|
1411
|
-
status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
|
|
1412
|
-
finalization: commitFinalizationView(inspection), error: null
|
|
1413
|
-
};
|
|
1414
|
-
}
|
|
1415
|
-
if (inspection.disposition === 'prepared' && inspection.intentDigest) {
|
|
1416
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
|
|
1417
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1418
|
-
}
|
|
1419
|
-
if (!inspection.intentDigest) {
|
|
1420
|
-
return commitIntentFailed(
|
|
1421
|
-
inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
|
|
1422
|
-
inspection.message,
|
|
1423
|
-
resolved.taskId
|
|
1424
|
-
);
|
|
1425
|
-
}
|
|
1426
|
-
return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
|
|
1427
|
-
removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
|
|
1428
|
-
});
|
|
1429
|
-
});
|
|
1430
|
-
} catch (error) {
|
|
1431
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1432
|
-
}
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function abortCommitIntent(
|
|
1436
|
-
taskRef: string,
|
|
1437
|
-
input: Readonly<{ token: string; expectedHead: string }>,
|
|
1438
|
-
options: OrchestrationOptions = {}
|
|
1439
|
-
): CommitIntentResult {
|
|
1440
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1441
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1442
|
-
try {
|
|
1443
|
-
const gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1444
|
-
return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
|
|
1445
|
-
const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1446
|
-
const currentHead = repositoryHead(gitRoot);
|
|
1447
|
-
if (
|
|
1448
|
-
intent.phase !== 'prepared'
|
|
1449
|
-
|| intent.pushEvidence !== null
|
|
1450
|
-
|| input.expectedHead !== intent.baselineHead
|
|
1451
|
-
|| currentHead !== intent.baselineHead
|
|
1452
|
-
) {
|
|
1453
|
-
return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
|
|
1454
|
-
}
|
|
1455
|
-
removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
|
|
1456
|
-
return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
|
|
1457
|
-
});
|
|
1458
|
-
} catch (error) {
|
|
1459
|
-
return mapCommitIntentError(error, resolved.taskId);
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
|
|
1463
|
-
function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
|
|
1464
|
-
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1465
|
-
if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
|
|
1466
|
-
let gitRoot: string;
|
|
1467
|
-
try {
|
|
1468
|
-
gitRoot = gitRootFor(resolved.repoRoot, options);
|
|
1469
|
-
} catch (error) {
|
|
1470
|
-
return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
1471
|
-
}
|
|
1472
|
-
const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
|
|
1473
|
-
return {
|
|
1474
|
-
status: 'ready',
|
|
1475
|
-
changed: false,
|
|
1476
|
-
taskId: resolved.taskId,
|
|
1477
|
-
intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
|
|
1478
|
-
finalization: commitFinalizationView(inspection),
|
|
1479
|
-
error: null
|
|
1480
|
-
};
|
|
1481
|
-
}
|
|
1482
|
-
|
|
1483
735
|
function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
1484
736
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1485
737
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
@@ -1500,27 +752,11 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1500
752
|
): OrchestrationResult {
|
|
1501
753
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1502
754
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1503
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
1504
|
-
try {
|
|
1505
|
-
readCommitIntent(resolved.taskDir, resolved.taskId);
|
|
1506
|
-
} catch (error) {
|
|
1507
|
-
return failed(
|
|
1508
|
-
'ORCHESTRATION_COMMIT_INTENT_INVALID',
|
|
1509
|
-
error instanceof Error ? error.message : String(error),
|
|
1510
|
-
resolved.taskId
|
|
1511
|
-
);
|
|
1512
|
-
}
|
|
1513
|
-
return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
|
|
1514
|
-
}
|
|
1515
755
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
1516
756
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
1517
757
|
}
|
|
1518
758
|
const run = readRun(resolved.taskDir);
|
|
1519
759
|
if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
|
|
1520
|
-
if (run.schemaVersion !== 3) {
|
|
1521
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'delegation preparation requires orchestration schemaVersion 3', resolved.taskId);
|
|
1522
|
-
}
|
|
1523
|
-
if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
|
|
1524
760
|
if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
1525
761
|
const repositoryPending = matchingDelegations(() => true, options);
|
|
1526
762
|
if (repositoryPending.length > 0) {
|
|
@@ -1546,6 +782,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1546
782
|
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1547
783
|
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1548
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
|
+
}
|
|
1549
794
|
let beforeFingerprint: string;
|
|
1550
795
|
try {
|
|
1551
796
|
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
@@ -1570,6 +815,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1570
815
|
lifecycleProvenance: input.lifecycleProvenance ?? null,
|
|
1571
816
|
beforeFingerprint
|
|
1572
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
|
+
}
|
|
1573
827
|
const updated = withUpdatedRun(run, {
|
|
1574
828
|
nextStage: next.stage,
|
|
1575
829
|
pendingDelegation: receipt,
|
|
@@ -1619,8 +873,8 @@ function dispatchOrchestrationDelegationUnlocked(
|
|
|
1619
873
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1620
874
|
const run = readRun(resolved.taskDir);
|
|
1621
875
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1622
|
-
if (run.
|
|
1623
|
-
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);
|
|
1624
878
|
}
|
|
1625
879
|
const result = dispatchDelegation(run.pendingDelegation, {
|
|
1626
880
|
now: options.now,
|
|
@@ -1667,7 +921,7 @@ function matchingDelegations(
|
|
|
1667
921
|
.filter((entry) => entry.isDirectory())
|
|
1668
922
|
.flatMap((entry) => {
|
|
1669
923
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1670
|
-
if (run
|
|
924
|
+
if (!run || run.status !== 'running' || !run.pendingDelegation) return [];
|
|
1671
925
|
const receipt = run.pendingDelegation;
|
|
1672
926
|
return predicate(receipt)
|
|
1673
927
|
? [{ taskId: entry.name, run }]
|
|
@@ -1920,7 +1174,6 @@ function activateOrchestrationDelegation(
|
|
|
1920
1174
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1921
1175
|
const run = readRun(resolved.taskDir);
|
|
1922
1176
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1923
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'active delegation requires schemaVersion 3', resolved.taskId);
|
|
1924
1177
|
if (run.status !== 'running') {
|
|
1925
1178
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1926
1179
|
}
|
|
@@ -1951,9 +1204,6 @@ async function awaitOrchestrationDelegationActivation(
|
|
|
1951
1204
|
if (!run?.pendingDelegation) {
|
|
1952
1205
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1953
1206
|
}
|
|
1954
|
-
if (run.schemaVersion !== 3) {
|
|
1955
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
|
|
1956
|
-
}
|
|
1957
1207
|
const receipt = run.pendingDelegation;
|
|
1958
1208
|
const matches = receipt.stage === event.stage
|
|
1959
1209
|
&& receipt.round === event.round
|
|
@@ -2051,9 +1301,6 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
2051
1301
|
resolved.taskId
|
|
2052
1302
|
);
|
|
2053
1303
|
}
|
|
2054
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
2055
|
-
return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
|
|
2056
|
-
}
|
|
2057
1304
|
const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
2058
1305
|
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
2059
1306
|
stateRoot: resolved.repoRoot,
|
|
@@ -2093,7 +1340,6 @@ function completeOrchestrationStage(
|
|
|
2093
1340
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2094
1341
|
const run = readRun(resolved.taskDir);
|
|
2095
1342
|
if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
|
|
2096
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage completion requires schemaVersion 3', resolved.taskId);
|
|
2097
1343
|
if (!run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
|
|
2098
1344
|
const result = completeDelegationStage(run.pendingDelegation, event);
|
|
2099
1345
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -2116,7 +1362,6 @@ function inspectOrchestrationStage(
|
|
|
2116
1362
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
2117
1363
|
}
|
|
2118
1364
|
if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
|
|
2119
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage inspection requires schemaVersion 3', resolved.taskId);
|
|
2120
1365
|
const receipt = run.pendingDelegation;
|
|
2121
1366
|
if (
|
|
2122
1367
|
!receipt
|
|
@@ -2175,7 +1420,6 @@ function sealOrchestrationDelegation(
|
|
|
2175
1420
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2176
1421
|
const run = readRun(resolved.taskDir);
|
|
2177
1422
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
2178
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'delegation sealing requires schemaVersion 3', resolved.taskId);
|
|
2179
1423
|
const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
|
|
2180
1424
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2181
1425
|
const updated = withUpdatedRun(run, { pendingDelegation: result.receipt });
|
|
@@ -2188,7 +1432,6 @@ function advanceOrchestration(taskRef: string, options: OrchestrationOptions = {
|
|
|
2188
1432
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2189
1433
|
const run = readRun(resolved.taskDir);
|
|
2190
1434
|
if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
2191
|
-
if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'orchestration advance requires schemaVersion 3', resolved.taskId);
|
|
2192
1435
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
2193
1436
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2194
1437
|
const completed = result.receipt.stage === 'commit';
|
|
@@ -2227,17 +1470,14 @@ export {
|
|
|
2227
1470
|
activateOrchestrationDelegation,
|
|
2228
1471
|
awaitOrchestrationDelegationActivation,
|
|
2229
1472
|
advanceOrchestration,
|
|
2230
|
-
abortCommitIntent,
|
|
2231
|
-
beginCommitIntent,
|
|
2232
1473
|
beginOrResumeOrchestration,
|
|
2233
|
-
checkpointCommitIntent,
|
|
2234
1474
|
commitOrchestrationStageCompletion,
|
|
2235
|
-
completeCommitIntent,
|
|
2236
1475
|
completeOrchestrationStage,
|
|
2237
1476
|
dispatchOrchestrationDelegation,
|
|
2238
1477
|
inspectOrchestrationStage,
|
|
2239
1478
|
hasActivatableOrchestrationDelegation,
|
|
2240
1479
|
hasSealableOrchestrationDelegation,
|
|
1480
|
+
OrchestrationStateError,
|
|
2241
1481
|
orchestrationPath,
|
|
2242
1482
|
pauseMatchingOrchestrationDelegation,
|
|
2243
1483
|
pauseOrchestration,
|
|
@@ -2245,19 +1485,14 @@ export {
|
|
|
2245
1485
|
prepareOrchestrationDelegation,
|
|
2246
1486
|
readRun,
|
|
2247
1487
|
reconcileMatchingOrchestrationDelegation,
|
|
2248
|
-
recoverCommitIntent,
|
|
2249
1488
|
recoverPreparedOrchestrationDelegation,
|
|
2250
1489
|
routeOrchestration,
|
|
2251
1490
|
sealMatchingOrchestrationDelegation,
|
|
2252
1491
|
sealMatchingOrchestrationDelegationWithHostEvidence,
|
|
2253
1492
|
sealOrchestrationDelegation,
|
|
2254
|
-
startCommitAttempt,
|
|
2255
|
-
statusCommitIntent,
|
|
2256
1493
|
statusOrchestration,
|
|
2257
|
-
terminateCommitAttempt
|
|
2258
1494
|
};
|
|
2259
1495
|
export type {
|
|
2260
|
-
CommitIntentResult,
|
|
2261
1496
|
OrchestrationCompletionPlanResult,
|
|
2262
1497
|
OrchestrationModelPolicy,
|
|
2263
1498
|
OrchestrationNext,
|