@fitlab-ai/agent-infra 0.9.8 → 0.9.10
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/codex-lifecycle/app-server.js +58 -21
- package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
- package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
- package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +127 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +63 -92
- package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
- package/dist/lib/agent-clients/adapters/index.js +3 -1
- package/dist/lib/agent-clients/adapters/traecli.js +66 -0
- package/dist/lib/agent-clients/tokens.js +2 -2
- package/dist/lib/agent-clients/types.js +2 -1
- package/dist/lib/defaults.json +5 -0
- package/dist/lib/git/workflow.js +10 -1
- package/dist/lib/internal/codex-lifecycle.js +20 -6
- package/dist/lib/internal/codex-sandbox-controller.js +9 -15
- 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/{task/commands/validate.js → internal/task-validate.js} +75 -42
- package/dist/lib/platform/pr-summary.js +48 -10
- package/dist/lib/platform/pull-requests.js +205 -41
- package/dist/lib/run/tui.js +5 -2
- 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 +275 -0
- package/dist/lib/sandbox/control/executor.js +140 -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 +161 -39
- 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 +248 -6
- package/dist/lib/task/finalization.js +593 -0
- package/dist/lib/task/index.js +0 -9
- 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 +110 -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 +37 -10
- package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
- package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -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 +91 -118
- package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
- package/lib/agent-clients/adapters/index.ts +3 -1
- package/lib/agent-clients/adapters/traecli.ts +67 -0
- package/lib/agent-clients/tokens.ts +2 -2
- package/lib/agent-clients/types.ts +2 -1
- package/lib/defaults.json +5 -0
- package/lib/git/workflow.ts +11 -2
- package/lib/internal/codex-lifecycle.ts +19 -6
- package/lib/internal/codex-sandbox-controller.ts +9 -14
- 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/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
- package/lib/platform/pr-summary.ts +55 -11
- package/lib/platform/pull-requests.ts +211 -41
- package/lib/run/tui.ts +5 -3
- package/lib/sandbox/commands/rm.ts +413 -61
- package/lib/sandbox/control/client.ts +255 -6
- package/lib/sandbox/control/controller-registration.ts +352 -0
- package/lib/sandbox/control/executor.ts +169 -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 +168 -40
- 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 +263 -4
- package/lib/task/finalization.ts +761 -0
- package/lib/task/index.ts +0 -8
- package/lib/task/operation-outcome.ts +48 -0
- package/lib/task/orchestration.ts +218 -982
- package/lib/task/short-id.ts +65 -1
- package/lib/task/task-execution-lock.ts +10 -1
- package/lib/task/verification-engine.ts +115 -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 +3 -4
- package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
- package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
- package/templates/.agents/rules/README.en.md +1 -0
- package/templates/.agents/rules/README.zh-CN.md +1 -0
- package/templates/.agents/rules/compatibility-policy.en.md +39 -0
- package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
- package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
- package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
- package/templates/.agents/rules/pr-sync.en.md +1 -1
- package/templates/.agents/rules/pr-sync.github.en.md +2 -1
- package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
- package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
- package/templates/.agents/rules/review-method.en.md +1 -0
- package/templates/.agents/rules/review-method.zh-CN.md +1 -0
- 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/analyze-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
- 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/code-task/SKILL.en.md +1 -0
- package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
- 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 +17 -9
- package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
- package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
- package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
- 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/plan-task/SKILL.en.md +2 -1
- package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
- package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
- package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
- package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
- package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
- package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
- package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
- package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
- package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
- package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
- package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
- package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
- 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/templates/.trae/skills/update-agent-infra.en.md +8 -0
- package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
- 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
|
}>;
|
|
@@ -197,12 +119,14 @@ type OrchestrationResult = Readonly<{
|
|
|
197
119
|
taskId: string | null;
|
|
198
120
|
run: OrchestrationRun | null;
|
|
199
121
|
next: OrchestrationNext | null;
|
|
122
|
+
warnings?: readonly Readonly<{ code: string; message: string; action: string }>[];
|
|
200
123
|
error: Readonly<{
|
|
201
124
|
code: string;
|
|
202
125
|
message: string;
|
|
203
126
|
client?: AgentClientId;
|
|
204
127
|
missingFields?: readonly string[];
|
|
205
128
|
modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
|
|
129
|
+
detail?: CodexCapabilityProvenanceDetail;
|
|
206
130
|
}> | null;
|
|
207
131
|
}>;
|
|
208
132
|
type OrchestrationStageIdentity = Readonly<{
|
|
@@ -239,40 +163,14 @@ type OrchestrationOptions = {
|
|
|
239
163
|
}>;
|
|
240
164
|
diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
|
|
241
165
|
supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
|
|
166
|
+
validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
167
|
+
consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
|
|
242
168
|
token?: () => string;
|
|
243
169
|
monotonicNow?: () => number;
|
|
244
170
|
sleep?: (milliseconds: number) => Promise<void>;
|
|
245
171
|
hasActiveLifecycleEvidence?: (receipt: DelegationReceipt) => boolean;
|
|
246
172
|
};
|
|
247
173
|
|
|
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
174
|
function supportsLifecycleDelegation(client: AgentClientId): boolean {
|
|
277
175
|
return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
|
|
278
176
|
&& getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
|
|
@@ -284,24 +182,147 @@ function orchestrationPath(taskDir: string): string {
|
|
|
284
182
|
return path.join(taskDir, 'orchestration.json');
|
|
285
183
|
}
|
|
286
184
|
|
|
185
|
+
const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; the file was left unchanged; rebuild the sandbox or manually repair the state before retrying';
|
|
186
|
+
|
|
187
|
+
class OrchestrationStateError extends Error {
|
|
188
|
+
readonly code = 'ORCHESTRATION_STATE_INVALID';
|
|
189
|
+
readonly taskId: string | null;
|
|
190
|
+
|
|
191
|
+
constructor(taskId: string | null = null) {
|
|
192
|
+
super(ORCHESTRATION_STATE_INVALID_MESSAGE);
|
|
193
|
+
this.name = 'OrchestrationStateError';
|
|
194
|
+
this.taskId = taskId;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
199
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function hasExactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
|
|
203
|
+
if (!isRecord(value)) return false;
|
|
204
|
+
const actual = Object.keys(value);
|
|
205
|
+
return actual.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function exactText(value: unknown): value is string {
|
|
209
|
+
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function nullableText(value: unknown): value is string | null {
|
|
213
|
+
return value === null || exactText(value);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function isPause(value: unknown): value is NonNullable<OrchestrationRun['pause']> {
|
|
217
|
+
return hasExactKeys(value, ['code', 'message', 'recoverable'])
|
|
218
|
+
&& exactText(value.code)
|
|
219
|
+
&& exactText(value.message)
|
|
220
|
+
&& typeof value.recoverable === 'boolean';
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function isModelPolicySource(value: unknown): value is ModelPolicySource {
|
|
224
|
+
return hasExactKeys(value, ['kind', 'client', 'resolvedAt'])
|
|
225
|
+
&& ['explicit', 'project-config'].includes(value.kind as string)
|
|
226
|
+
&& isAgentClientId(value.client)
|
|
227
|
+
&& exactText(value.resolvedAt);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function isModelPolicy(value: unknown): value is OrchestrationModelPolicy {
|
|
231
|
+
if (!hasExactKeys(value, ['executor', 'reviewer'])) return false;
|
|
232
|
+
return [value.executor, value.reviewer].every((role) => hasExactKeys(role, ['model', 'reasoningEffort'])
|
|
233
|
+
&& exactText(role.model)
|
|
234
|
+
&& exactText(role.reasoningEffort));
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const RECOVERY_GUARD_KEYS = [
|
|
238
|
+
'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
|
|
239
|
+
'commitAuthorizationUnused', 'completionEvidenceAbsent'
|
|
240
|
+
] as const;
|
|
241
|
+
|
|
242
|
+
function isRecovery(value: unknown): value is OrchestrationRecovery {
|
|
243
|
+
if (!hasExactKeys(value, [
|
|
244
|
+
'code', 'recoveredAt', 'previousStatus', 'previousPause', 'client', 'guards', 'resultingStatus'
|
|
245
|
+
])) return false;
|
|
246
|
+
const guards = value.guards;
|
|
247
|
+
return value.code === 'CLIENT_CAPABILITY_ENABLED'
|
|
248
|
+
&& exactText(value.recoveredAt)
|
|
249
|
+
&& value.previousStatus === 'paused'
|
|
250
|
+
&& isPause(value.previousPause)
|
|
251
|
+
&& value.previousPause.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
252
|
+
&& value.client === 'claude-code'
|
|
253
|
+
&& hasExactKeys(guards, RECOVERY_GUARD_KEYS)
|
|
254
|
+
&& guards.stepCount === 0
|
|
255
|
+
&& guards.nextStage === null
|
|
256
|
+
&& guards.baselineEmpty === true
|
|
257
|
+
&& guards.receiptCount === 0
|
|
258
|
+
&& guards.pendingDelegation === false
|
|
259
|
+
&& guards.commitAuthorizationUnused === true
|
|
260
|
+
&& guards.completionEvidenceAbsent === true
|
|
261
|
+
&& value.resultingStatus === 'running';
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
|
|
265
|
+
return hasExactKeys(value, [
|
|
266
|
+
'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
|
|
267
|
+
])
|
|
268
|
+
&& value.kind === 'reviewed-head-clean'
|
|
269
|
+
&& exactText(value.observedAt)
|
|
270
|
+
&& exactText(value.head)
|
|
271
|
+
&& exactText(value.headTree)
|
|
272
|
+
&& exactText(value.worktreeTree)
|
|
273
|
+
&& exactText(value.lastReviewedCommit)
|
|
274
|
+
&& Number.isSafeInteger(value.prNumber)
|
|
275
|
+
&& (value.prNumber as number) > 0
|
|
276
|
+
&& exactText(value.prHead);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const ORCHESTRATION_RUN_KEYS = [
|
|
280
|
+
'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
|
|
281
|
+
'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
|
|
282
|
+
'pause', 'commitAuthorization', 'completionEvidence', 'createdAt', 'updatedAt'
|
|
283
|
+
] as const;
|
|
284
|
+
|
|
285
|
+
function parseOrchestrationRun(value: unknown, expectedTaskId?: string): OrchestrationRun {
|
|
286
|
+
if (!hasExactKeys(value, ORCHESTRATION_RUN_KEYS)
|
|
287
|
+
|| !exactText(value.taskId)
|
|
288
|
+
|| (expectedTaskId !== undefined && value.taskId !== expectedTaskId)
|
|
289
|
+
|| !exactText(value.runId)
|
|
290
|
+
|| !['running', 'paused', 'completed'].includes(value.status as string)
|
|
291
|
+
|| !(value.nextStage === null || ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code', 'commit'].includes(value.nextStage as string))
|
|
292
|
+
|| !Number.isSafeInteger(value.stepCount) || (value.stepCount as number) < 0
|
|
293
|
+
|| !Number.isSafeInteger(value.maxSteps) || (value.maxSteps as number) < 1
|
|
294
|
+
|| !isModelPolicy(value.modelPolicy)
|
|
295
|
+
|| !isModelPolicySource(value.modelPolicySource)
|
|
296
|
+
|| !Array.isArray(value.recoveryHistory) || !value.recoveryHistory.every(isRecovery)
|
|
297
|
+
|| typeof value.baseline !== 'string'
|
|
298
|
+
|| !(value.pendingDelegation === null || isDelegationReceipt(value.pendingDelegation))
|
|
299
|
+
|| !Array.isArray(value.receipts) || !value.receipts.every(isDelegationReceipt)
|
|
300
|
+
|| !(value.pause === null || isPause(value.pause))
|
|
301
|
+
|| !hasExactKeys(value.commitAuthorization, ['issuedAt', 'consumedAt'])
|
|
302
|
+
|| !nullableText(value.commitAuthorization.issuedAt)
|
|
303
|
+
|| !nullableText(value.commitAuthorization.consumedAt)
|
|
304
|
+
|| !(value.completionEvidence === null || isCompletionEvidence(value.completionEvidence))
|
|
305
|
+
|| !exactText(value.createdAt)
|
|
306
|
+
|| !exactText(value.updatedAt)) {
|
|
307
|
+
throw new OrchestrationStateError();
|
|
308
|
+
}
|
|
309
|
+
const receipts = [...value.receipts, ...(value.pendingDelegation ? [value.pendingDelegation] : [])];
|
|
310
|
+
if (receipts.some((receipt) => receipt.taskId !== value.taskId || receipt.runId !== value.runId)) {
|
|
311
|
+
throw new OrchestrationStateError();
|
|
312
|
+
}
|
|
313
|
+
return value as OrchestrationRun;
|
|
314
|
+
}
|
|
315
|
+
|
|
287
316
|
function readRun(taskDir: string): OrchestrationRun | null {
|
|
288
317
|
const file = orchestrationPath(taskDir);
|
|
289
318
|
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
|
-
};
|
|
319
|
+
const taskId = path.basename(taskDir);
|
|
320
|
+
try {
|
|
321
|
+
return parseOrchestrationRun(JSON.parse(fs.readFileSync(file, 'utf8')), taskId);
|
|
322
|
+
} catch (error) {
|
|
323
|
+
if (error instanceof OrchestrationStateError) throw new OrchestrationStateError(taskId);
|
|
324
|
+
throw new OrchestrationStateError(taskId);
|
|
325
|
+
}
|
|
305
326
|
}
|
|
306
327
|
|
|
307
328
|
function atomicWrite(file: string, value: unknown): void {
|
|
@@ -327,79 +348,11 @@ function failed(
|
|
|
327
348
|
return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
|
|
328
349
|
}
|
|
329
350
|
|
|
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
351
|
function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
|
|
343
352
|
if (!options.gitWorktreeRoot) return stateRoot;
|
|
344
353
|
return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
|
|
345
354
|
}
|
|
346
355
|
|
|
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
356
|
function validModel(value: unknown): value is string {
|
|
404
357
|
return typeof value === 'string' && value.length > 0 && value.trim() === value;
|
|
405
358
|
}
|
|
@@ -424,12 +377,8 @@ function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationMod
|
|
|
424
377
|
&& left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
|
|
425
378
|
}
|
|
426
379
|
|
|
427
|
-
function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
|
|
428
|
-
return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
|
|
429
|
-
}
|
|
430
|
-
|
|
431
380
|
function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
|
|
432
|
-
return
|
|
381
|
+
return run.modelPolicy[role];
|
|
433
382
|
}
|
|
434
383
|
|
|
435
384
|
function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
|
|
@@ -439,26 +388,8 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
|
|
|
439
388
|
return normalizeAgentClients(raw).state[client].orchestration;
|
|
440
389
|
}
|
|
441
390
|
|
|
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
391
|
function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
|
|
460
|
-
return run.
|
|
461
|
-
&& run.status === 'paused'
|
|
392
|
+
return run.status === 'paused'
|
|
462
393
|
&& run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
|
|
463
394
|
&& run.modelPolicySource?.client === 'claude-code'
|
|
464
395
|
&& run.stepCount === 0
|
|
@@ -469,23 +400,7 @@ function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: st
|
|
|
469
400
|
&& run.commitAuthorization?.issuedAt === null
|
|
470
401
|
&& run.commitAuthorization?.consumedAt === null
|
|
471
402
|
&& 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));
|
|
403
|
+
&& run.recoveryHistory.length === 0;
|
|
489
404
|
}
|
|
490
405
|
|
|
491
406
|
function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
@@ -501,235 +416,56 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
501
416
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
502
417
|
}
|
|
503
418
|
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) {
|
|
419
|
+
if (existing.status === 'completed') {
|
|
512
420
|
return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
513
421
|
}
|
|
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 };
|
|
422
|
+
if (existing.modelPolicySource.client !== options.client) {
|
|
423
|
+
return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
|
|
652
424
|
}
|
|
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);
|
|
425
|
+
if (options.modelPolicy) {
|
|
426
|
+
const suppliedError = validateModelPolicy(options.modelPolicy);
|
|
427
|
+
if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
|
|
428
|
+
if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
|
|
429
|
+
return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
|
|
662
430
|
}
|
|
663
431
|
}
|
|
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 };
|
|
432
|
+
if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
|
|
433
|
+
if (existing.modelPolicySource.client !== 'claude-code'
|
|
434
|
+
|| !canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
|
|
435
|
+
return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
436
|
+
}
|
|
437
|
+
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
438
|
+
const recovery: ClaudeCodeCapabilityRecovery = {
|
|
439
|
+
code: 'CLIENT_CAPABILITY_ENABLED',
|
|
440
|
+
recoveredAt: now,
|
|
441
|
+
previousStatus: 'paused',
|
|
442
|
+
previousPause: existing.pause as ClaudeCodeCapabilityRecovery['previousPause'],
|
|
443
|
+
client: 'claude-code',
|
|
444
|
+
guards: {
|
|
445
|
+
stepCount: 0,
|
|
446
|
+
nextStage: null,
|
|
447
|
+
baselineEmpty: true,
|
|
448
|
+
receiptCount: 0,
|
|
449
|
+
pendingDelegation: false,
|
|
450
|
+
commitAuthorizationUnused: true,
|
|
451
|
+
completionEvidenceAbsent: true
|
|
452
|
+
},
|
|
453
|
+
resultingStatus: 'running'
|
|
454
|
+
};
|
|
455
|
+
const resumed = withUpdatedRun(existing, {
|
|
456
|
+
status: 'running',
|
|
457
|
+
pause: null,
|
|
458
|
+
recoveryHistory: Object.freeze([...existing.recoveryHistory, recovery])
|
|
459
|
+
}, () => now);
|
|
460
|
+
saveRun(resolved.taskDir, resumed);
|
|
461
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
695
462
|
}
|
|
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);
|
|
463
|
+
if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
|
|
464
|
+
const resumed = withUpdatedRun(existing, { status: 'running', pause: null }, options.now);
|
|
465
|
+
saveRun(resolved.taskDir, resumed);
|
|
466
|
+
return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
|
|
705
467
|
}
|
|
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 };
|
|
468
|
+
return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
|
|
733
469
|
}
|
|
734
470
|
|
|
735
471
|
let policy = options.modelPolicy;
|
|
@@ -751,7 +487,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
|
|
|
751
487
|
const now = (options.now ?? (() => new Date().toISOString()))();
|
|
752
488
|
const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
|
|
753
489
|
const run: OrchestrationRun = {
|
|
754
|
-
schemaVersion: 3,
|
|
755
490
|
taskId: resolved.taskId,
|
|
756
491
|
runId: (options.id ?? randomUUID)(),
|
|
757
492
|
status: 'running',
|
|
@@ -998,488 +733,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
|
|
|
998
733
|
return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
|
|
999
734
|
}
|
|
1000
735
|
|
|
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
736
|
function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
|
|
1484
737
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1485
738
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
@@ -1500,27 +753,11 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1500
753
|
): OrchestrationResult {
|
|
1501
754
|
const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
|
|
1502
755
|
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
756
|
if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
|
|
1516
757
|
return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
|
|
1517
758
|
}
|
|
1518
759
|
const run = readRun(resolved.taskDir);
|
|
1519
760
|
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
761
|
if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
|
|
1525
762
|
const repositoryPending = matchingDelegations(() => true, options);
|
|
1526
763
|
if (repositoryPending.length > 0) {
|
|
@@ -1546,6 +783,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1546
783
|
if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
|
|
1547
784
|
return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
|
|
1548
785
|
}
|
|
786
|
+
const capabilityValidationError = options.validateLifecycleCapability?.();
|
|
787
|
+
if (capabilityValidationError) {
|
|
788
|
+
return failed(
|
|
789
|
+
capabilityValidationError.code,
|
|
790
|
+
capabilityValidationError.message,
|
|
791
|
+
resolved.taskId,
|
|
792
|
+
capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {}
|
|
793
|
+
);
|
|
794
|
+
}
|
|
1549
795
|
let beforeFingerprint: string;
|
|
1550
796
|
try {
|
|
1551
797
|
beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
@@ -1570,6 +816,15 @@ function prepareOrchestrationDelegationUnlocked(
|
|
|
1570
816
|
lifecycleProvenance: input.lifecycleProvenance ?? null,
|
|
1571
817
|
beforeFingerprint
|
|
1572
818
|
}, { id: options.id, now: options.now, monotonicNow: options.monotonicNow });
|
|
819
|
+
const capabilityError = options.consumeLifecycleCapability?.();
|
|
820
|
+
if (capabilityError) {
|
|
821
|
+
return failed(
|
|
822
|
+
capabilityError.code,
|
|
823
|
+
capabilityError.message,
|
|
824
|
+
resolved.taskId,
|
|
825
|
+
capabilityError.detail ? { detail: capabilityError.detail } : {}
|
|
826
|
+
);
|
|
827
|
+
}
|
|
1573
828
|
const updated = withUpdatedRun(run, {
|
|
1574
829
|
nextStage: next.stage,
|
|
1575
830
|
pendingDelegation: receipt,
|
|
@@ -1619,8 +874,8 @@ function dispatchOrchestrationDelegationUnlocked(
|
|
|
1619
874
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1620
875
|
const run = readRun(resolved.taskDir);
|
|
1621
876
|
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
|
|
877
|
+
if (run.status !== 'running') {
|
|
878
|
+
return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running orchestration', resolved.taskId);
|
|
1624
879
|
}
|
|
1625
880
|
const result = dispatchDelegation(run.pendingDelegation, {
|
|
1626
881
|
now: options.now,
|
|
@@ -1667,7 +922,7 @@ function matchingDelegations(
|
|
|
1667
922
|
.filter((entry) => entry.isDirectory())
|
|
1668
923
|
.flatMap((entry) => {
|
|
1669
924
|
const run = readRun(path.join(activeRoot, entry.name));
|
|
1670
|
-
if (run
|
|
925
|
+
if (!run || run.status !== 'running' || !run.pendingDelegation) return [];
|
|
1671
926
|
const receipt = run.pendingDelegation;
|
|
1672
927
|
return predicate(receipt)
|
|
1673
928
|
? [{ taskId: entry.name, run }]
|
|
@@ -1920,7 +1175,6 @@ function activateOrchestrationDelegation(
|
|
|
1920
1175
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
1921
1176
|
const run = readRun(resolved.taskDir);
|
|
1922
1177
|
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
1178
|
if (run.status !== 'running') {
|
|
1925
1179
|
return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
|
|
1926
1180
|
}
|
|
@@ -1951,9 +1205,6 @@ async function awaitOrchestrationDelegationActivation(
|
|
|
1951
1205
|
if (!run?.pendingDelegation) {
|
|
1952
1206
|
return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
|
|
1953
1207
|
}
|
|
1954
|
-
if (run.schemaVersion !== 3) {
|
|
1955
|
-
return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
|
|
1956
|
-
}
|
|
1957
1208
|
const receipt = run.pendingDelegation;
|
|
1958
1209
|
const matches = receipt.stage === event.stage
|
|
1959
1210
|
&& receipt.round === event.round
|
|
@@ -2051,9 +1302,6 @@ function recoverPreparedOrchestrationDelegation(
|
|
|
2051
1302
|
resolved.taskId
|
|
2052
1303
|
);
|
|
2053
1304
|
}
|
|
2054
|
-
if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
|
|
2055
|
-
return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
|
|
2056
|
-
}
|
|
2057
1305
|
const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
|
|
2058
1306
|
gitRoot: gitRootFor(resolved.repoRoot, options),
|
|
2059
1307
|
stateRoot: resolved.repoRoot,
|
|
@@ -2093,7 +1341,6 @@ function completeOrchestrationStage(
|
|
|
2093
1341
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2094
1342
|
const run = readRun(resolved.taskDir);
|
|
2095
1343
|
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
1344
|
if (!run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
|
|
2098
1345
|
const result = completeDelegationStage(run.pendingDelegation, event);
|
|
2099
1346
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
@@ -2116,7 +1363,6 @@ function inspectOrchestrationStage(
|
|
|
2116
1363
|
return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
|
|
2117
1364
|
}
|
|
2118
1365
|
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
1366
|
const receipt = run.pendingDelegation;
|
|
2121
1367
|
if (
|
|
2122
1368
|
!receipt
|
|
@@ -2175,7 +1421,6 @@ function sealOrchestrationDelegation(
|
|
|
2175
1421
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2176
1422
|
const run = readRun(resolved.taskDir);
|
|
2177
1423
|
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
1424
|
const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
|
|
2180
1425
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2181
1426
|
const updated = withUpdatedRun(run, { pendingDelegation: result.receipt });
|
|
@@ -2188,7 +1433,6 @@ function advanceOrchestration(taskRef: string, options: OrchestrationOptions = {
|
|
|
2188
1433
|
if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
|
|
2189
1434
|
const run = readRun(resolved.taskDir);
|
|
2190
1435
|
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
1436
|
const result = consumeDelegation(run.pendingDelegation, { now: options.now });
|
|
2193
1437
|
if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
|
|
2194
1438
|
const completed = result.receipt.stage === 'commit';
|
|
@@ -2227,17 +1471,14 @@ export {
|
|
|
2227
1471
|
activateOrchestrationDelegation,
|
|
2228
1472
|
awaitOrchestrationDelegationActivation,
|
|
2229
1473
|
advanceOrchestration,
|
|
2230
|
-
abortCommitIntent,
|
|
2231
|
-
beginCommitIntent,
|
|
2232
1474
|
beginOrResumeOrchestration,
|
|
2233
|
-
checkpointCommitIntent,
|
|
2234
1475
|
commitOrchestrationStageCompletion,
|
|
2235
|
-
completeCommitIntent,
|
|
2236
1476
|
completeOrchestrationStage,
|
|
2237
1477
|
dispatchOrchestrationDelegation,
|
|
2238
1478
|
inspectOrchestrationStage,
|
|
2239
1479
|
hasActivatableOrchestrationDelegation,
|
|
2240
1480
|
hasSealableOrchestrationDelegation,
|
|
1481
|
+
OrchestrationStateError,
|
|
2241
1482
|
orchestrationPath,
|
|
2242
1483
|
pauseMatchingOrchestrationDelegation,
|
|
2243
1484
|
pauseOrchestration,
|
|
@@ -2245,19 +1486,14 @@ export {
|
|
|
2245
1486
|
prepareOrchestrationDelegation,
|
|
2246
1487
|
readRun,
|
|
2247
1488
|
reconcileMatchingOrchestrationDelegation,
|
|
2248
|
-
recoverCommitIntent,
|
|
2249
1489
|
recoverPreparedOrchestrationDelegation,
|
|
2250
1490
|
routeOrchestration,
|
|
2251
1491
|
sealMatchingOrchestrationDelegation,
|
|
2252
1492
|
sealMatchingOrchestrationDelegationWithHostEvidence,
|
|
2253
1493
|
sealOrchestrationDelegation,
|
|
2254
|
-
startCommitAttempt,
|
|
2255
|
-
statusCommitIntent,
|
|
2256
1494
|
statusOrchestration,
|
|
2257
|
-
terminateCommitAttempt
|
|
2258
1495
|
};
|
|
2259
1496
|
export type {
|
|
2260
|
-
CommitIntentResult,
|
|
2261
1497
|
OrchestrationCompletionPlanResult,
|
|
2262
1498
|
OrchestrationModelPolicy,
|
|
2263
1499
|
OrchestrationNext,
|