@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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