@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.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +6 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +173 -10
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +56 -67
  10. package/dist/lib/git/workflow.js +10 -1
  11. package/dist/lib/internal/codex-lifecycle.js +19 -5
  12. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  13. package/dist/lib/internal/git-workflow.js +5 -7
  14. package/dist/lib/internal/platform-pr.js +20 -7
  15. package/dist/lib/internal/release-workflow.js +2 -1
  16. package/dist/lib/internal/sandbox-control.js +79 -2
  17. package/dist/lib/internal/task-finalization.js +54 -0
  18. package/dist/lib/internal/task-orchestration.js +156 -291
  19. package/dist/lib/platform/pr-summary.js +48 -10
  20. package/dist/lib/platform/pull-requests.js +205 -41
  21. package/dist/lib/sandbox/commands/rm.js +316 -55
  22. package/dist/lib/sandbox/control/client.js +190 -6
  23. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  24. package/dist/lib/sandbox/control/executor.js +137 -5
  25. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  26. package/dist/lib/sandbox/control/protocol.js +67 -6
  27. package/dist/lib/sandbox/index.js +5 -3
  28. package/dist/lib/sandbox/managed-fs.js +5 -0
  29. package/dist/lib/sandbox/task-resolver.js +50 -6
  30. package/dist/lib/sandbox/workspace-identity.js +27 -0
  31. package/dist/lib/sandbox/worktree-safety.js +215 -2
  32. package/dist/lib/task/codex-orchestration.js +158 -33
  33. package/dist/lib/task/commands/status.js +59 -1
  34. package/dist/lib/task/commit-operation.js +335 -0
  35. package/dist/lib/task/commit-policy.js +18 -0
  36. package/dist/lib/task/delegation-receipts.js +243 -2
  37. package/dist/lib/task/finalization.js +567 -0
  38. package/dist/lib/task/operation-outcome.js +33 -0
  39. package/dist/lib/task/orchestration.js +187 -761
  40. package/dist/lib/task/short-id.js +59 -1
  41. package/dist/lib/task/task-execution-lock.js +6 -1
  42. package/dist/lib/task/verification-engine.js +113 -60
  43. package/dist/lib/task/verification.js +2 -1
  44. package/dist/lib/task/workflow-warning-intents.js +50 -1
  45. package/dist/lifecycle-build-manifest.json +3 -3
  46. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  47. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  48. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  49. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  50. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  51. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  52. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +83 -91
  53. package/lib/git/workflow.ts +11 -2
  54. package/lib/internal/codex-lifecycle.ts +18 -5
  55. package/lib/internal/codex-sandbox-controller.ts +6 -12
  56. package/lib/internal/git-workflow.ts +5 -6
  57. package/lib/internal/platform-pr.ts +14 -7
  58. package/lib/internal/release-workflow.ts +2 -1
  59. package/lib/internal/sandbox-control.ts +92 -2
  60. package/lib/internal/task-finalization.ts +68 -0
  61. package/lib/internal/task-orchestration.ts +20 -103
  62. package/lib/platform/pr-summary.ts +55 -11
  63. package/lib/platform/pull-requests.ts +211 -41
  64. package/lib/sandbox/commands/rm.ts +413 -61
  65. package/lib/sandbox/control/client.ts +255 -6
  66. package/lib/sandbox/control/controller-registration.ts +347 -0
  67. package/lib/sandbox/control/executor.ts +163 -5
  68. package/lib/sandbox/control/lifecycle.ts +2 -1
  69. package/lib/sandbox/control/protocol.ts +80 -7
  70. package/lib/sandbox/index.ts +5 -3
  71. package/lib/sandbox/managed-fs.ts +5 -0
  72. package/lib/sandbox/task-resolver.ts +58 -7
  73. package/lib/sandbox/workspace-identity.ts +50 -2
  74. package/lib/sandbox/workspace-view.ts +3 -3
  75. package/lib/sandbox/worktree-safety.ts +236 -2
  76. package/lib/task/codex-orchestration.ts +167 -33
  77. package/lib/task/commands/status.ts +78 -1
  78. package/lib/task/commit-operation.ts +430 -0
  79. package/lib/task/commit-policy.ts +30 -0
  80. package/lib/task/delegation-receipts.ts +258 -1
  81. package/lib/task/finalization.ts +719 -0
  82. package/lib/task/operation-outcome.ts +48 -0
  83. package/lib/task/orchestration.ts +217 -982
  84. package/lib/task/short-id.ts +65 -1
  85. package/lib/task/task-execution-lock.ts +10 -1
  86. package/lib/task/verification-engine.ts +118 -63
  87. package/lib/task/verification.ts +3 -2
  88. package/lib/task/workflow-warning-intents.ts +57 -2
  89. package/package.json +2 -2
  90. package/templates/.agents/QUICKSTART.en.md +2 -2
  91. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  92. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  93. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  94. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  95. package/templates/.agents/rules/task-management.en.md +2 -2
  96. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  97. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  98. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  99. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  100. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  101. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  102. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  103. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  104. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  105. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  106. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  107. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  108. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  109. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  111. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  112. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  113. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  114. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  115. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  116. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  117. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  118. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  119. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  120. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  121. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  122. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  123. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  124. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  125. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  126. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  127. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  128. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  129. package/templates/.codex/hooks.json +4 -4
  130. package/dist/lib/task/commit-finalization.js +0 -305
  131. package/dist/lib/task/commit-intent.js +0 -165
  132. package/lib/task/commit-finalization.ts +0 -419
  133. 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: 'CLIENT_CAPABILITY_ENABLED_NO_MIGRATION';
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 SchemaMigrationRecovery = Readonly<{
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?: OrchestrationModelPolicy | LegacyOrchestrationModelPolicy;
174
- modelPolicySource?: ModelPolicySource;
175
- recoveryHistory?: readonly OrchestrationRecovery[];
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?: CleanCompletionEvidence | null;
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 run = JSON.parse(fs.readFileSync(file, 'utf8')) as OrchestrationRun;
291
- if (![2, 3].includes(run.schemaVersion) || !isV2Policy(run.modelPolicy)) return run;
292
- return {
293
- ...run,
294
- modelPolicy: {
295
- executor: {
296
- model: run.modelPolicy.executor.model,
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 isV2Policy(run.modelPolicy) ? run.modelPolicy[role] : null;
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.schemaVersion === 3
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
- && !fs.existsSync(commitIntentPath(taskDir))
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.schemaVersion === 2 || existing.schemaVersion === 3) {
515
- const policyError = validateModelPolicy(isV2Policy(existing.modelPolicy) ? existing.modelPolicy : undefined);
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
- let policy: OrchestrationModelPolicy | undefined = options.modelPolicy;
655
- let sourceKind: ModelPolicySource['kind'] = 'explicit';
656
- if (!policy) {
657
- try {
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
- const policyError = validateModelPolicy(policy);
665
- if (policyError) {
666
- return failed(policyError.code, policyError.message, resolved.taskId, {
667
- client: options.client,
668
- missingFields: ['executor.model', 'executor.reasoningEffort', 'reviewer.model', 'reviewer.reasoningEffort'],
669
- modelSelectionContext: getAgentClientModelSelection(options.client)
670
- });
671
- }
672
- if (existing.pendingDelegation !== null) {
673
- const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_DELEGATION_BUSY';
674
- if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
675
- const paused = withUpdatedRun(existing, {
676
- status: 'paused',
677
- pause: { code: 'ORCHESTRATION_DELEGATION_BUSY', message: 'legacy run has a pending delegation', recoverable: false }
678
- });
679
- saveRun(resolved.taskDir, paused);
680
- return { status: 'paused', changed: true, taskId: resolved.taskId, run: paused, next: null, error: null };
681
- }
682
- if (existing.receipts.length > 0) {
683
- const alreadyPaused = existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED';
684
- if (alreadyPaused) return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
685
- const paused = withUpdatedRun(existing, {
686
- status: 'paused',
687
- pause: {
688
- code: 'ORCHESTRATION_HISTORICAL_EFFORT_UNVERIFIED',
689
- message: 'legacy receipts do not contain actual reasoning-effort evidence',
690
- recoverable: false
691
- }
692
- });
693
- saveRun(resolved.taskDir, paused);
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.modelPolicy
698
- && typeof existing.modelPolicy.executor === 'string'
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
- const now = (options.now ?? (() => new Date().toISOString()))();
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.schemaVersion !== 3 || run.status !== 'running') {
1623
- return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running schemaVersion 3 orchestration', resolved.taskId);
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?.schemaVersion !== 3 || run.status !== 'running' || !run.pendingDelegation) return [];
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,