@fitlab-ai/agent-infra 0.9.8 → 0.9.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (182) 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/codex-lifecycle/app-server.js +58 -21
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +25 -11
  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 +127 -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 +63 -92
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/traecli.js +66 -0
  13. package/dist/lib/agent-clients/tokens.js +2 -2
  14. package/dist/lib/agent-clients/types.js +2 -1
  15. package/dist/lib/defaults.json +5 -0
  16. package/dist/lib/git/workflow.js +10 -1
  17. package/dist/lib/internal/codex-lifecycle.js +20 -6
  18. package/dist/lib/internal/codex-sandbox-controller.js +9 -15
  19. package/dist/lib/internal/git-workflow.js +5 -7
  20. package/dist/lib/internal/platform-pr.js +20 -7
  21. package/dist/lib/internal/release-workflow.js +2 -1
  22. package/dist/lib/internal/sandbox-control.js +79 -2
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-orchestration.js +156 -291
  25. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +75 -42
  26. package/dist/lib/platform/pr-summary.js +48 -10
  27. package/dist/lib/platform/pull-requests.js +205 -41
  28. package/dist/lib/run/tui.js +5 -2
  29. package/dist/lib/sandbox/commands/rm.js +316 -55
  30. package/dist/lib/sandbox/control/client.js +190 -6
  31. package/dist/lib/sandbox/control/controller-registration.js +275 -0
  32. package/dist/lib/sandbox/control/executor.js +140 -5
  33. package/dist/lib/sandbox/control/lifecycle.js +3 -1
  34. package/dist/lib/sandbox/control/protocol.js +67 -6
  35. package/dist/lib/sandbox/index.js +5 -3
  36. package/dist/lib/sandbox/managed-fs.js +5 -0
  37. package/dist/lib/sandbox/task-resolver.js +50 -6
  38. package/dist/lib/sandbox/workspace-identity.js +27 -0
  39. package/dist/lib/sandbox/worktree-safety.js +215 -2
  40. package/dist/lib/task/codex-orchestration.js +161 -39
  41. package/dist/lib/task/commands/status.js +59 -1
  42. package/dist/lib/task/commit-operation.js +335 -0
  43. package/dist/lib/task/commit-policy.js +18 -0
  44. package/dist/lib/task/delegation-receipts.js +248 -6
  45. package/dist/lib/task/finalization.js +593 -0
  46. package/dist/lib/task/index.js +0 -9
  47. package/dist/lib/task/operation-outcome.js +33 -0
  48. package/dist/lib/task/orchestration.js +187 -761
  49. package/dist/lib/task/short-id.js +59 -1
  50. package/dist/lib/task/task-execution-lock.js +6 -1
  51. package/dist/lib/task/verification-engine.js +110 -60
  52. package/dist/lib/task/verification.js +2 -1
  53. package/dist/lib/task/workflow-warning-intents.js +50 -1
  54. package/dist/lifecycle-build-manifest.json +3 -3
  55. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +61 -19
  56. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +37 -10
  57. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +276 -12
  58. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +163 -0
  59. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  60. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  61. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +91 -118
  62. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  63. package/lib/agent-clients/adapters/index.ts +3 -1
  64. package/lib/agent-clients/adapters/traecli.ts +67 -0
  65. package/lib/agent-clients/tokens.ts +2 -2
  66. package/lib/agent-clients/types.ts +2 -1
  67. package/lib/defaults.json +5 -0
  68. package/lib/git/workflow.ts +11 -2
  69. package/lib/internal/codex-lifecycle.ts +19 -6
  70. package/lib/internal/codex-sandbox-controller.ts +9 -14
  71. package/lib/internal/git-workflow.ts +5 -6
  72. package/lib/internal/platform-pr.ts +14 -7
  73. package/lib/internal/release-workflow.ts +2 -1
  74. package/lib/internal/sandbox-control.ts +92 -2
  75. package/lib/internal/task-finalization.ts +68 -0
  76. package/lib/internal/task-orchestration.ts +20 -103
  77. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +75 -40
  78. package/lib/platform/pr-summary.ts +55 -11
  79. package/lib/platform/pull-requests.ts +211 -41
  80. package/lib/run/tui.ts +5 -3
  81. package/lib/sandbox/commands/rm.ts +413 -61
  82. package/lib/sandbox/control/client.ts +255 -6
  83. package/lib/sandbox/control/controller-registration.ts +352 -0
  84. package/lib/sandbox/control/executor.ts +169 -5
  85. package/lib/sandbox/control/lifecycle.ts +2 -1
  86. package/lib/sandbox/control/protocol.ts +80 -7
  87. package/lib/sandbox/index.ts +5 -3
  88. package/lib/sandbox/managed-fs.ts +5 -0
  89. package/lib/sandbox/task-resolver.ts +58 -7
  90. package/lib/sandbox/workspace-identity.ts +50 -2
  91. package/lib/sandbox/workspace-view.ts +3 -3
  92. package/lib/sandbox/worktree-safety.ts +236 -2
  93. package/lib/task/codex-orchestration.ts +168 -40
  94. package/lib/task/commands/status.ts +78 -1
  95. package/lib/task/commit-operation.ts +430 -0
  96. package/lib/task/commit-policy.ts +30 -0
  97. package/lib/task/delegation-receipts.ts +263 -4
  98. package/lib/task/finalization.ts +761 -0
  99. package/lib/task/index.ts +0 -8
  100. package/lib/task/operation-outcome.ts +48 -0
  101. package/lib/task/orchestration.ts +218 -982
  102. package/lib/task/short-id.ts +65 -1
  103. package/lib/task/task-execution-lock.ts +10 -1
  104. package/lib/task/verification-engine.ts +115 -63
  105. package/lib/task/verification.ts +3 -2
  106. package/lib/task/workflow-warning-intents.ts +57 -2
  107. package/package.json +2 -2
  108. package/templates/.agents/QUICKSTART.en.md +3 -4
  109. package/templates/.agents/QUICKSTART.zh-CN.md +3 -4
  110. package/templates/.agents/hooks/lifecycle-delegation.js +1 -21
  111. package/templates/.agents/rules/README.en.md +1 -0
  112. package/templates/.agents/rules/README.zh-CN.md +1 -0
  113. package/templates/.agents/rules/compatibility-policy.en.md +39 -0
  114. package/templates/.agents/rules/compatibility-policy.zh-CN.md +39 -0
  115. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  116. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  117. package/templates/.agents/rules/pr-sync.en.md +1 -1
  118. package/templates/.agents/rules/pr-sync.github.en.md +2 -1
  119. package/templates/.agents/rules/pr-sync.github.zh-CN.md +2 -0
  120. package/templates/.agents/rules/pr-sync.zh-CN.md +1 -0
  121. package/templates/.agents/rules/review-method.en.md +1 -0
  122. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  123. package/templates/.agents/rules/task-management.en.md +2 -2
  124. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  125. package/templates/.agents/skills/analyze-task/SKILL.en.md +1 -0
  126. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -0
  127. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  128. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  129. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  130. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  131. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  132. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  133. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  134. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  135. package/templates/.agents/skills/code-task/SKILL.en.md +1 -0
  136. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -0
  137. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  138. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  139. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  140. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  142. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  143. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  144. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  149. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  150. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  151. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  152. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  153. package/templates/.agents/skills/create-pr/SKILL.en.md +17 -9
  154. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +17 -9
  155. package/templates/.agents/skills/create-pr/reference/change-report.en.md +41 -0
  156. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +41 -0
  157. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  158. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  159. package/templates/.agents/skills/plan-task/SKILL.en.md +2 -1
  160. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +2 -1
  161. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +8 -6
  162. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +8 -6
  163. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +3 -3
  164. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +3 -3
  165. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  166. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  167. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  168. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  169. package/templates/.agents/skills/run-task/SKILL.en.md +6 -6
  170. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -6
  171. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  172. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  173. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +24 -0
  174. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  175. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  176. package/templates/.codex/hooks.json +4 -4
  177. package/templates/.trae/skills/update-agent-infra.en.md +8 -0
  178. package/templates/.trae/skills/update-agent-infra.zh-CN.md +8 -0
  179. package/dist/lib/task/commit-finalization.js +0 -305
  180. package/dist/lib/task/commit-intent.js +0 -165
  181. package/lib/task/commit-finalization.ts +0 -419
  182. 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
  }>;
@@ -197,12 +119,14 @@ type OrchestrationResult = Readonly<{
197
119
  taskId: string | null;
198
120
  run: OrchestrationRun | null;
199
121
  next: OrchestrationNext | null;
122
+ warnings?: readonly Readonly<{ code: string; message: string; action: string }>[];
200
123
  error: Readonly<{
201
124
  code: string;
202
125
  message: string;
203
126
  client?: AgentClientId;
204
127
  missingFields?: readonly string[];
205
128
  modelSelectionContext?: ReturnType<typeof getAgentClientModelSelection>;
129
+ detail?: CodexCapabilityProvenanceDetail;
206
130
  }> | null;
207
131
  }>;
208
132
  type OrchestrationStageIdentity = Readonly<{
@@ -239,40 +163,14 @@ type OrchestrationOptions = {
239
163
  }>;
240
164
  diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
241
165
  supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
166
+ validateLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
167
+ consumeLifecycleCapability?: () => Exclude<OrchestrationResult['error'], null> | null;
242
168
  token?: () => string;
243
169
  monotonicNow?: () => number;
244
170
  sleep?: (milliseconds: number) => Promise<void>;
245
171
  hasActiveLifecycleEvidence?: (receipt: DelegationReceipt) => boolean;
246
172
  };
247
173
 
248
- type CommitIntentResult = Readonly<{
249
- status: 'ready' | 'failed';
250
- changed: boolean;
251
- taskId: string | null;
252
- intent: Readonly<{
253
- mode: CommitIntent['mode'];
254
- phase: CommitIntent['phase'];
255
- baselineHead: string;
256
- currentHead: string;
257
- committedHead: string | null;
258
- pushEvidence: PushEvidence | null;
259
- runId: string | null;
260
- receiptId: string | null;
261
- }> | null;
262
- finalization?: Readonly<{
263
- disposition: CommitFinalizationInspection['disposition'];
264
- code: CommitFinalizationInspection['code'];
265
- message: string;
266
- currentHead: string;
267
- committedHead: string | null;
268
- needsAnchor: boolean;
269
- needsLog: boolean;
270
- attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
271
- }>;
272
- token?: string;
273
- error: Readonly<{ code: string; message: string }> | null;
274
- }>;
275
-
276
174
  function supportsLifecycleDelegation(client: AgentClientId): boolean {
277
175
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
278
176
  && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
@@ -284,24 +182,147 @@ function orchestrationPath(taskDir: string): string {
284
182
  return path.join(taskDir, 'orchestration.json');
285
183
  }
286
184
 
185
+ const ORCHESTRATION_STATE_INVALID_MESSAGE = 'orchestration.json does not match the current runtime structure; the file was left unchanged; rebuild the sandbox or manually repair the state before retrying';
186
+
187
+ class OrchestrationStateError extends Error {
188
+ readonly code = 'ORCHESTRATION_STATE_INVALID';
189
+ readonly taskId: string | null;
190
+
191
+ constructor(taskId: string | null = null) {
192
+ super(ORCHESTRATION_STATE_INVALID_MESSAGE);
193
+ this.name = 'OrchestrationStateError';
194
+ this.taskId = taskId;
195
+ }
196
+ }
197
+
198
+ function isRecord(value: unknown): value is Record<string, unknown> {
199
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
200
+ }
201
+
202
+ function hasExactKeys(value: unknown, keys: readonly string[]): value is Record<string, unknown> {
203
+ if (!isRecord(value)) return false;
204
+ const actual = Object.keys(value);
205
+ return actual.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
206
+ }
207
+
208
+ function exactText(value: unknown): value is string {
209
+ return typeof value === 'string' && value.length > 0 && value.trim() === value;
210
+ }
211
+
212
+ function nullableText(value: unknown): value is string | null {
213
+ return value === null || exactText(value);
214
+ }
215
+
216
+ function isPause(value: unknown): value is NonNullable<OrchestrationRun['pause']> {
217
+ return hasExactKeys(value, ['code', 'message', 'recoverable'])
218
+ && exactText(value.code)
219
+ && exactText(value.message)
220
+ && typeof value.recoverable === 'boolean';
221
+ }
222
+
223
+ function isModelPolicySource(value: unknown): value is ModelPolicySource {
224
+ return hasExactKeys(value, ['kind', 'client', 'resolvedAt'])
225
+ && ['explicit', 'project-config'].includes(value.kind as string)
226
+ && isAgentClientId(value.client)
227
+ && exactText(value.resolvedAt);
228
+ }
229
+
230
+ function isModelPolicy(value: unknown): value is OrchestrationModelPolicy {
231
+ if (!hasExactKeys(value, ['executor', 'reviewer'])) return false;
232
+ return [value.executor, value.reviewer].every((role) => hasExactKeys(role, ['model', 'reasoningEffort'])
233
+ && exactText(role.model)
234
+ && exactText(role.reasoningEffort));
235
+ }
236
+
237
+ const RECOVERY_GUARD_KEYS = [
238
+ 'stepCount', 'nextStage', 'baselineEmpty', 'receiptCount', 'pendingDelegation',
239
+ 'commitAuthorizationUnused', 'completionEvidenceAbsent'
240
+ ] as const;
241
+
242
+ function isRecovery(value: unknown): value is OrchestrationRecovery {
243
+ if (!hasExactKeys(value, [
244
+ 'code', 'recoveredAt', 'previousStatus', 'previousPause', 'client', 'guards', 'resultingStatus'
245
+ ])) return false;
246
+ const guards = value.guards;
247
+ return value.code === 'CLIENT_CAPABILITY_ENABLED'
248
+ && exactText(value.recoveredAt)
249
+ && value.previousStatus === 'paused'
250
+ && isPause(value.previousPause)
251
+ && value.previousPause.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
252
+ && value.client === 'claude-code'
253
+ && hasExactKeys(guards, RECOVERY_GUARD_KEYS)
254
+ && guards.stepCount === 0
255
+ && guards.nextStage === null
256
+ && guards.baselineEmpty === true
257
+ && guards.receiptCount === 0
258
+ && guards.pendingDelegation === false
259
+ && guards.commitAuthorizationUnused === true
260
+ && guards.completionEvidenceAbsent === true
261
+ && value.resultingStatus === 'running';
262
+ }
263
+
264
+ function isCompletionEvidence(value: unknown): value is CleanCompletionEvidence {
265
+ return hasExactKeys(value, [
266
+ 'kind', 'observedAt', 'head', 'headTree', 'worktreeTree', 'lastReviewedCommit', 'prNumber', 'prHead'
267
+ ])
268
+ && value.kind === 'reviewed-head-clean'
269
+ && exactText(value.observedAt)
270
+ && exactText(value.head)
271
+ && exactText(value.headTree)
272
+ && exactText(value.worktreeTree)
273
+ && exactText(value.lastReviewedCommit)
274
+ && Number.isSafeInteger(value.prNumber)
275
+ && (value.prNumber as number) > 0
276
+ && exactText(value.prHead);
277
+ }
278
+
279
+ const ORCHESTRATION_RUN_KEYS = [
280
+ 'taskId', 'runId', 'status', 'nextStage', 'stepCount', 'maxSteps', 'modelPolicy',
281
+ 'modelPolicySource', 'recoveryHistory', 'baseline', 'pendingDelegation', 'receipts',
282
+ 'pause', 'commitAuthorization', 'completionEvidence', 'createdAt', 'updatedAt'
283
+ ] as const;
284
+
285
+ function parseOrchestrationRun(value: unknown, expectedTaskId?: string): OrchestrationRun {
286
+ if (!hasExactKeys(value, ORCHESTRATION_RUN_KEYS)
287
+ || !exactText(value.taskId)
288
+ || (expectedTaskId !== undefined && value.taskId !== expectedTaskId)
289
+ || !exactText(value.runId)
290
+ || !['running', 'paused', 'completed'].includes(value.status as string)
291
+ || !(value.nextStage === null || ['analysis', 'review-analysis', 'plan', 'review-plan', 'code', 'review-code', 'commit'].includes(value.nextStage as string))
292
+ || !Number.isSafeInteger(value.stepCount) || (value.stepCount as number) < 0
293
+ || !Number.isSafeInteger(value.maxSteps) || (value.maxSteps as number) < 1
294
+ || !isModelPolicy(value.modelPolicy)
295
+ || !isModelPolicySource(value.modelPolicySource)
296
+ || !Array.isArray(value.recoveryHistory) || !value.recoveryHistory.every(isRecovery)
297
+ || typeof value.baseline !== 'string'
298
+ || !(value.pendingDelegation === null || isDelegationReceipt(value.pendingDelegation))
299
+ || !Array.isArray(value.receipts) || !value.receipts.every(isDelegationReceipt)
300
+ || !(value.pause === null || isPause(value.pause))
301
+ || !hasExactKeys(value.commitAuthorization, ['issuedAt', 'consumedAt'])
302
+ || !nullableText(value.commitAuthorization.issuedAt)
303
+ || !nullableText(value.commitAuthorization.consumedAt)
304
+ || !(value.completionEvidence === null || isCompletionEvidence(value.completionEvidence))
305
+ || !exactText(value.createdAt)
306
+ || !exactText(value.updatedAt)) {
307
+ throw new OrchestrationStateError();
308
+ }
309
+ const receipts = [...value.receipts, ...(value.pendingDelegation ? [value.pendingDelegation] : [])];
310
+ if (receipts.some((receipt) => receipt.taskId !== value.taskId || receipt.runId !== value.runId)) {
311
+ throw new OrchestrationStateError();
312
+ }
313
+ return value as OrchestrationRun;
314
+ }
315
+
287
316
  function readRun(taskDir: string): OrchestrationRun | null {
288
317
  const file = orchestrationPath(taskDir);
289
318
  if (!fs.existsSync(file)) return null;
290
- const 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
- };
319
+ const taskId = path.basename(taskDir);
320
+ try {
321
+ return parseOrchestrationRun(JSON.parse(fs.readFileSync(file, 'utf8')), taskId);
322
+ } catch (error) {
323
+ if (error instanceof OrchestrationStateError) throw new OrchestrationStateError(taskId);
324
+ throw new OrchestrationStateError(taskId);
325
+ }
305
326
  }
306
327
 
307
328
  function atomicWrite(file: string, value: unknown): void {
@@ -327,79 +348,11 @@ function failed(
327
348
  return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
328
349
  }
329
350
 
330
- function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
331
- return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
332
- }
333
-
334
- function repositoryHead(repoRoot: string): string {
335
- return execFileSync('git', ['rev-parse', 'HEAD'], {
336
- cwd: repoRoot,
337
- encoding: 'utf8',
338
- stdio: ['ignore', 'pipe', 'pipe']
339
- }).trim();
340
- }
341
-
342
351
  function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
343
352
  if (!options.gitWorktreeRoot) return stateRoot;
344
353
  return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
345
354
  }
346
355
 
347
- function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
348
- return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
349
- cwd: repoRoot,
350
- stdio: 'ignore'
351
- }).status === 0;
352
- }
353
-
354
- function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
355
- return {
356
- mode: intent.mode,
357
- phase: intent.phase,
358
- baselineHead: intent.baselineHead,
359
- currentHead,
360
- committedHead: intent.committedHead,
361
- pushEvidence: intent.pushEvidence,
362
- runId: intent.orchestration?.runId ?? null,
363
- receiptId: intent.orchestration?.receiptId ?? null
364
- };
365
- }
366
-
367
- function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
368
- return {
369
- disposition: inspection.disposition,
370
- code: inspection.code,
371
- message: inspection.message,
372
- currentHead: inspection.currentHead,
373
- committedHead: inspection.committedHead,
374
- needsAnchor: inspection.needsAnchor,
375
- needsLog: inspection.needsLog,
376
- attempt: inspection.attempt
377
- };
378
- }
379
-
380
- function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
381
- return run !== null
382
- && [1, 2, 3].includes(run.schemaVersion)
383
- && run.taskId === taskId
384
- && ['running', 'paused', 'completed'].includes(run.status)
385
- && Array.isArray(run.receipts)
386
- && Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
387
- && typeof run.commitAuthorization === 'object'
388
- && run.commitAuthorization !== null
389
- && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
390
- && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
391
- }
392
-
393
- function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
394
- if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
395
- if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
396
- return commitIntentFailed(
397
- 'ORCHESTRATION_COMMIT_INTENT_INVALID',
398
- error instanceof Error ? error.message : String(error),
399
- taskId
400
- );
401
- }
402
-
403
356
  function validModel(value: unknown): value is string {
404
357
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
405
358
  }
@@ -424,12 +377,8 @@ function sameModelPolicy(left: OrchestrationModelPolicy, right: OrchestrationMod
424
377
  && left.reviewer.reasoningEffort === right.reviewer.reasoningEffort;
425
378
  }
426
379
 
427
- function isV2Policy(policy: OrchestrationRun['modelPolicy']): policy is OrchestrationModelPolicy {
428
- return typeof policy?.executor === 'object' && typeof policy?.reviewer === 'object';
429
- }
430
-
431
380
  function rolePolicy(run: OrchestrationRun, role: DelegationRole): OrchestrationRolePolicy | null {
432
- return isV2Policy(run.modelPolicy) ? run.modelPolicy[role] : null;
381
+ return run.modelPolicy[role];
433
382
  }
434
383
 
435
384
  function resolveProjectPolicy(repoRoot: string, client: AgentClientId): OrchestrationModelPolicy | undefined {
@@ -439,26 +388,8 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
439
388
  return normalizeAgentClients(raw).state[client].orchestration;
440
389
  }
441
390
 
442
- function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
443
- return run.schemaVersion === 2
444
- && run.status === 'paused'
445
- && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
446
- && run.modelPolicySource?.client === 'codex'
447
- && run.stepCount === 0
448
- && run.nextStage === null
449
- && run.baseline === ''
450
- && run.pendingDelegation === null
451
- && run.receipts.length === 0
452
- && run.commitAuthorization?.issuedAt === null
453
- && run.commitAuthorization?.consumedAt === null
454
- && run.completionEvidence == null
455
- && !fs.existsSync(commitIntentPath(taskDir))
456
- && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
457
- }
458
-
459
391
  function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
460
- return run.schemaVersion === 3
461
- && run.status === 'paused'
392
+ return run.status === 'paused'
462
393
  && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
463
394
  && run.modelPolicySource?.client === 'claude-code'
464
395
  && run.stepCount === 0
@@ -469,23 +400,7 @@ function canRecoverClaudeCodeUnsupportedPause(run: OrchestrationRun, taskDir: st
469
400
  && run.commitAuthorization?.issuedAt === null
470
401
  && run.commitAuthorization?.consumedAt === null
471
402
  && run.completionEvidence == null
472
- && !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));
403
+ && run.recoveryHistory.length === 0;
489
404
  }
490
405
 
491
406
  function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
@@ -501,235 +416,56 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
501
416
  return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
502
417
  }
503
418
  if (existing) {
504
- if (
505
- ![1, 2, 3].includes(existing.schemaVersion)
506
- || !Array.isArray(existing.receipts)
507
- || !Object.prototype.hasOwnProperty.call(existing, 'pendingDelegation')
508
- ) {
509
- return failed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
510
- }
511
- if (existing.status === 'completed' && existing.schemaVersion === 1) {
419
+ if (existing.status === 'completed') {
512
420
  return { status: 'completed', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
513
421
  }
514
- if (existing.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 };
422
+ if (existing.modelPolicySource.client !== options.client) {
423
+ return failed('ORCHESTRATION_CLIENT_MISMATCH', 'provided client does not match the persisted run policy source', resolved.taskId);
652
424
  }
653
-
654
- 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);
425
+ if (options.modelPolicy) {
426
+ const suppliedError = validateModelPolicy(options.modelPolicy);
427
+ if (suppliedError) return failed(suppliedError.code, suppliedError.message, resolved.taskId);
428
+ if (!sameModelPolicy(existing.modelPolicy, options.modelPolicy)) {
429
+ return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
662
430
  }
663
431
  }
664
- 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 };
432
+ if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
433
+ if (existing.modelPolicySource.client !== 'claude-code'
434
+ || !canRecoverClaudeCodeUnsupportedPause(existing, resolved.taskDir)) {
435
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
436
+ }
437
+ const now = (options.now ?? (() => new Date().toISOString()))();
438
+ const recovery: ClaudeCodeCapabilityRecovery = {
439
+ code: 'CLIENT_CAPABILITY_ENABLED',
440
+ recoveredAt: now,
441
+ previousStatus: 'paused',
442
+ previousPause: existing.pause as ClaudeCodeCapabilityRecovery['previousPause'],
443
+ client: 'claude-code',
444
+ guards: {
445
+ stepCount: 0,
446
+ nextStage: null,
447
+ baselineEmpty: true,
448
+ receiptCount: 0,
449
+ pendingDelegation: false,
450
+ commitAuthorizationUnused: true,
451
+ completionEvidenceAbsent: true
452
+ },
453
+ resultingStatus: 'running'
454
+ };
455
+ const resumed = withUpdatedRun(existing, {
456
+ status: 'running',
457
+ pause: null,
458
+ recoveryHistory: Object.freeze([...existing.recoveryHistory, recovery])
459
+ }, () => now);
460
+ saveRun(resolved.taskDir, resumed);
461
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
695
462
  }
696
- if (
697
- existing.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);
463
+ if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
464
+ const resumed = withUpdatedRun(existing, { status: 'running', pause: null }, options.now);
465
+ saveRun(resolved.taskDir, resumed);
466
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
705
467
  }
706
- 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 };
468
+ return { status: existing.status, changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
733
469
  }
734
470
 
735
471
  let policy = options.modelPolicy;
@@ -751,7 +487,6 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
751
487
  const now = (options.now ?? (() => new Date().toISOString()))();
752
488
  const source: ModelPolicySource = { kind: sourceKind, client: options.client, resolvedAt: now };
753
489
  const run: OrchestrationRun = {
754
- schemaVersion: 3,
755
490
  taskId: resolved.taskId,
756
491
  runId: (options.id ?? randomUUID)(),
757
492
  status: 'running',
@@ -998,488 +733,6 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
998
733
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
999
734
  }
1000
735
 
1001
- function appendCommitActivity(
1002
- resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1003
- step: string,
1004
- agent: string,
1005
- note: string
1006
- ): Readonly<{ code: string; message: string }> | null {
1007
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
1008
- const activity = locateActivityLog(taskContent);
1009
- if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
1010
- const metadata = captureTaskWriteMetadata();
1011
- const written = writeTask({
1012
- taskRef: resolved.taskId,
1013
- expectedState: 'active',
1014
- mutations: [{
1015
- kind: 'section',
1016
- aliases: ['活动日志', 'Activity Log'],
1017
- heading: activity.heading,
1018
- body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
1019
- }]
1020
- }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
1021
- return written.status === 'failed' ? written.error : null;
1022
- }
1023
-
1024
- function startCommitAttempt(
1025
- taskRef: string,
1026
- input: Readonly<{ agent: string }>,
1027
- options: OrchestrationOptions = {}
1028
- ): CommitIntentResult {
1029
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1030
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1031
- try {
1032
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1033
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
1034
- if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1035
- return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
1036
- }
1037
- const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
1038
- const activity = locateActivityLog(taskContent);
1039
- if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
1040
- const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
1041
- if (open.length > 0) {
1042
- return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
1043
- }
1044
- const currentHead = repositoryHead(gitRoot);
1045
- const attempt = (options.id ?? randomUUID)();
1046
- const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
1047
- attempt,
1048
- baseline: currentHead,
1049
- agent: input.agent
1050
- }));
1051
- if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
1052
- const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1053
- return {
1054
- status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
1055
- finalization: commitFinalizationView(inspection), error: null
1056
- };
1057
- });
1058
- } catch (error) {
1059
- return mapCommitIntentError(error, resolved.taskId);
1060
- }
1061
- }
1062
-
1063
- function terminateCommitAttempt(
1064
- taskRef: string,
1065
- input: Readonly<{ attempt: string; agent: string; code: string }>,
1066
- options: OrchestrationOptions = {}
1067
- ): CommitIntentResult {
1068
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1069
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1070
- try {
1071
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1072
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
1073
- if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1074
- return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
1075
- }
1076
- const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1077
- if (
1078
- inspection.disposition !== 'retryable-start'
1079
- || inspection.attempt?.attempt !== input.attempt
1080
- || inspection.attempt.agent !== input.agent
1081
- || inspection.attempt.baseline !== inspection.currentHead
1082
- ) {
1083
- return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
1084
- }
1085
- const error = appendCommitActivity(
1086
- resolved,
1087
- 'Commit [aborted]',
1088
- input.agent,
1089
- `aborted; attempt=${input.attempt}; code=${input.code}`
1090
- );
1091
- if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
1092
- return {
1093
- status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
1094
- finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
1095
- error: null
1096
- };
1097
- });
1098
- } catch (error) {
1099
- return mapCommitIntentError(error, resolved.taskId);
1100
- }
1101
- }
1102
-
1103
- function beginCommitIntent(
1104
- taskRef: string,
1105
- input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
1106
- options: OrchestrationOptions = {}
1107
- ): CommitIntentResult {
1108
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1109
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1110
- try {
1111
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1112
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
1113
- const currentHead = repositoryHead(gitRoot);
1114
- if (currentHead !== input.baselineHead) {
1115
- return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
1116
- }
1117
- if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1118
- try {
1119
- readCommitIntent(resolved.taskDir, resolved.taskId);
1120
- } catch (error) {
1121
- return mapCommitIntentError(error, resolved.taskId);
1122
- }
1123
- return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
1124
- }
1125
- const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1126
- if (
1127
- attemptInspection.disposition !== 'retryable-start'
1128
- || attemptInspection.attempt?.attempt !== input.attempt
1129
- || attemptInspection.attempt.agent !== input.agent
1130
- || attemptInspection.attempt.baseline !== input.baselineHead
1131
- ) {
1132
- return commitIntentFailed(
1133
- 'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
1134
- 'commit begin requires one matching retryable Commit attempt',
1135
- resolved.taskId
1136
- );
1137
- }
1138
- const runFile = orchestrationPath(resolved.taskDir);
1139
- let run: OrchestrationRun | null = null;
1140
- let sourceRunBytes: string | null = null;
1141
- if (fs.existsSync(runFile)) {
1142
- sourceRunBytes = fs.readFileSync(runFile, 'utf8');
1143
- try {
1144
- run = readRun(resolved.taskDir);
1145
- } catch (error) {
1146
- return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
1147
- }
1148
- if (!validCommitRun(run, resolved.taskId)) {
1149
- return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
1150
- }
1151
- }
1152
-
1153
- const now = (options.now ?? (() => new Date().toISOString()))();
1154
- const failAndPause = (code: string, message: string): CommitIntentResult => {
1155
- if (run?.status === 'running') {
1156
- saveRun(resolved.taskDir, withUpdatedRun(run, {
1157
- status: 'paused', pause: { code, message, recoverable: true }
1158
- }, () => now));
1159
- }
1160
- return commitIntentFailed(code, message, resolved.taskId);
1161
- };
1162
- let orchestration: CommitIntent['orchestration'] = null;
1163
- if (!input.orchestrated) {
1164
- if (run?.pendingDelegation) {
1165
- return commitIntentFailed(
1166
- 'ORCHESTRATION_STANDALONE_BUSY',
1167
- 'standalone commit is blocked by a pending orchestration delegation',
1168
- resolved.taskId
1169
- );
1170
- }
1171
- } else {
1172
- if (!run || run.status !== 'running') {
1173
- return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
1174
- }
1175
- const receipt = run.pendingDelegation;
1176
- if (
1177
- !receipt
1178
- || receipt.status !== 'activated'
1179
- || receipt.stage !== 'commit'
1180
- || receipt.round !== 1
1181
- || receipt.artifact !== 'commit'
1182
- || receipt.role !== 'executor'
1183
- ) {
1184
- return failAndPause(
1185
- 'ORCHESTRATION_PROVENANCE_MISMATCH',
1186
- 'orchestrated commit requires one matching activated commit delegation'
1187
- );
1188
- }
1189
- if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
1190
- return failAndPause(
1191
- 'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
1192
- 'orchestrated commit requires an unconsumed one-use authorization'
1193
- );
1194
- }
1195
- const planned = planOrchestrationStageCompletion(taskRef, {
1196
- stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
1197
- }, { ...options, now: () => now });
1198
- if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
1199
- return failAndPause(
1200
- planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
1201
- planned.result.error?.message ?? 'commit delegation could not be completed'
1202
- );
1203
- }
1204
- const plannedRunBytes = serialize(planned.plan.updatedRun);
1205
- orchestration = {
1206
- runId: run.runId,
1207
- receiptId: receipt.id,
1208
- authorizationIssuedAt: run.commitAuthorization.issuedAt,
1209
- sourceRunDigest: digest(sourceRunBytes!),
1210
- plannedRunDigest: digest(plannedRunBytes),
1211
- completionUpdatedAt: planned.plan.updatedRun.updatedAt,
1212
- plannedReceipt: planned.plan.updatedRun.pendingDelegation
1213
- };
1214
- }
1215
-
1216
- const created = createCommitIntent(resolved.taskDir, {
1217
- taskId: resolved.taskId,
1218
- mode: input.orchestrated ? 'orchestrated' : 'standalone',
1219
- phase: 'prepared',
1220
- baselineHead: input.baselineHead,
1221
- committedHead: null,
1222
- pushEvidence: null,
1223
- orchestration,
1224
- createdAt: now,
1225
- updatedAt: now
1226
- }, { token: options.token });
1227
- return {
1228
- status: 'ready', changed: true, taskId: resolved.taskId,
1229
- intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
1230
- };
1231
- });
1232
- } catch (error) {
1233
- return mapCommitIntentError(error, resolved.taskId);
1234
- }
1235
- }
1236
-
1237
- function checkpointCommitIntent(
1238
- taskRef: string,
1239
- input: Readonly<{
1240
- token: string;
1241
- kind: 'committed' | 'pushed';
1242
- head: string;
1243
- remote?: string;
1244
- ref?: string;
1245
- }>,
1246
- options: OrchestrationOptions = {}
1247
- ): CommitIntentResult {
1248
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1249
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1250
- try {
1251
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1252
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
1253
- const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1254
- const currentHead = repositoryHead(gitRoot);
1255
- if (currentHead !== input.head) {
1256
- return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
1257
- }
1258
- if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
1259
- return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
1260
- }
1261
- if (input.kind === 'pushed') {
1262
- const expectedHead = intent.committedHead ?? intent.baselineHead;
1263
- if (input.head !== expectedHead || !input.remote || !input.ref) {
1264
- return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
1265
- }
1266
- }
1267
- const now = (options.now ?? (() => new Date().toISOString()))();
1268
- const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
1269
- ? { phase: 'committed', committedHead: input.head, updatedAt: now }
1270
- : {
1271
- phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
1272
- pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
1273
- });
1274
- return {
1275
- status: 'ready', changed: true, taskId: resolved.taskId,
1276
- intent: commitIntentView(updated, currentHead), error: null
1277
- };
1278
- });
1279
- } catch (error) {
1280
- return mapCommitIntentError(error, resolved.taskId);
1281
- }
1282
- }
1283
-
1284
- function completeCommitOrchestration(
1285
- resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1286
- intent: CommitIntent,
1287
- agent: string
1288
- ): Readonly<{ code: string; message: string }> | null {
1289
- if (intent.orchestration === null) return null;
1290
- if (intent.orchestration.plannedReceipt.agent !== agent) {
1291
- return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
1292
- }
1293
- const runFile = orchestrationPath(resolved.taskDir);
1294
- if (!fs.existsSync(runFile)) {
1295
- return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
1296
- }
1297
- const currentBytes = fs.readFileSync(runFile, 'utf8');
1298
- const currentDigest = digest(currentBytes);
1299
- if (currentDigest === intent.orchestration.sourceRunDigest) {
1300
- const run = readRun(resolved.taskDir);
1301
- if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
1302
- const plannedRun: OrchestrationRun = Object.freeze({
1303
- ...run,
1304
- pendingDelegation: intent.orchestration.plannedReceipt,
1305
- updatedAt: intent.orchestration.completionUpdatedAt
1306
- });
1307
- if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
1308
- return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
1309
- }
1310
- atomicWrite(runFile, plannedRun);
1311
- if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
1312
- return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
1313
- }
1314
- } else if (currentDigest !== intent.orchestration.plannedRunDigest) {
1315
- return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
1316
- }
1317
- return null;
1318
- }
1319
-
1320
- function finalizeCommitIntentUnlocked(
1321
- resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1322
- inspection: CommitFinalizationInspection,
1323
- agent: string,
1324
- remove: () => void
1325
- ): CommitIntentResult {
1326
- if (inspection.disposition !== 'recoverable' || !inspection.intent) {
1327
- return commitIntentFailed(
1328
- inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
1329
- inspection.message,
1330
- resolved.taskId
1331
- );
1332
- }
1333
- if (
1334
- inspection.intent.orchestration !== null
1335
- && inspection.intent.orchestration.plannedReceipt.agent !== agent
1336
- ) {
1337
- return commitIntentFailed(
1338
- 'ORCHESTRATION_PROVENANCE_MISMATCH',
1339
- 'completion agent does not match the planned receipt',
1340
- resolved.taskId
1341
- );
1342
- }
1343
- const metadata = captureTaskWriteMetadata();
1344
- const taskPlan = planCommitTaskFinalization(
1345
- resolved.taskDir,
1346
- inspection,
1347
- agent,
1348
- metadata.timestamp
1349
- );
1350
- const written = writeTask({
1351
- taskRef: resolved.taskId,
1352
- expectedState: 'active',
1353
- mutations: taskPlan.mutations
1354
- }, {
1355
- repoRoot: resolved.repoRoot,
1356
- metadataProvider: () => metadata
1357
- });
1358
- if (written.status === 'failed') {
1359
- return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
1360
- }
1361
- const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
1362
- if (orchestrationError) {
1363
- return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
1364
- }
1365
- try {
1366
- remove();
1367
- } catch (error) {
1368
- return commitIntentFailed(
1369
- 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
1370
- error instanceof Error ? error.message : String(error),
1371
- resolved.taskId
1372
- );
1373
- }
1374
- return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1375
- }
1376
-
1377
- function completeCommitIntent(
1378
- taskRef: string,
1379
- input: Readonly<{ token: string; agent: string }>,
1380
- options: OrchestrationOptions = {}
1381
- ): CommitIntentResult {
1382
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1383
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1384
- try {
1385
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1386
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
1387
- readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1388
- const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1389
- return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1390
- removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1391
- });
1392
- });
1393
- } catch (error) {
1394
- return mapCommitIntentError(error, resolved.taskId);
1395
- }
1396
- }
1397
-
1398
- function recoverCommitIntent(
1399
- taskRef: string,
1400
- input: Readonly<{ agent: string }>,
1401
- options: OrchestrationOptions = {}
1402
- ): CommitIntentResult {
1403
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1404
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1405
- try {
1406
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1407
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
1408
- const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1409
- if (inspection.disposition === 'retryable-start') {
1410
- return {
1411
- status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
1412
- finalization: commitFinalizationView(inspection), error: null
1413
- };
1414
- }
1415
- if (inspection.disposition === 'prepared' && inspection.intentDigest) {
1416
- removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
1417
- return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1418
- }
1419
- if (!inspection.intentDigest) {
1420
- return commitIntentFailed(
1421
- inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
1422
- inspection.message,
1423
- resolved.taskId
1424
- );
1425
- }
1426
- return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1427
- removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
1428
- });
1429
- });
1430
- } catch (error) {
1431
- return mapCommitIntentError(error, resolved.taskId);
1432
- }
1433
- }
1434
-
1435
- function abortCommitIntent(
1436
- taskRef: string,
1437
- input: Readonly<{ token: string; expectedHead: string }>,
1438
- options: OrchestrationOptions = {}
1439
- ): CommitIntentResult {
1440
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1441
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1442
- try {
1443
- const gitRoot = gitRootFor(resolved.repoRoot, options);
1444
- return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
1445
- const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1446
- const currentHead = repositoryHead(gitRoot);
1447
- if (
1448
- intent.phase !== 'prepared'
1449
- || intent.pushEvidence !== null
1450
- || input.expectedHead !== intent.baselineHead
1451
- || currentHead !== intent.baselineHead
1452
- ) {
1453
- return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
1454
- }
1455
- removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1456
- return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1457
- });
1458
- } catch (error) {
1459
- return mapCommitIntentError(error, resolved.taskId);
1460
- }
1461
- }
1462
-
1463
- function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
1464
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1465
- if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1466
- let gitRoot: string;
1467
- try {
1468
- gitRoot = gitRootFor(resolved.repoRoot, options);
1469
- } catch (error) {
1470
- return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
1471
- }
1472
- const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1473
- return {
1474
- status: 'ready',
1475
- changed: false,
1476
- taskId: resolved.taskId,
1477
- intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
1478
- finalization: commitFinalizationView(inspection),
1479
- error: null
1480
- };
1481
- }
1482
-
1483
736
  function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
1484
737
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1485
738
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
@@ -1500,27 +753,11 @@ function prepareOrchestrationDelegationUnlocked(
1500
753
  ): OrchestrationResult {
1501
754
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1502
755
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1503
- if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1504
- try {
1505
- readCommitIntent(resolved.taskDir, resolved.taskId);
1506
- } catch (error) {
1507
- return failed(
1508
- 'ORCHESTRATION_COMMIT_INTENT_INVALID',
1509
- error instanceof Error ? error.message : String(error),
1510
- resolved.taskId
1511
- );
1512
- }
1513
- return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
1514
- }
1515
756
  if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
1516
757
  return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
1517
758
  }
1518
759
  const run = readRun(resolved.taskDir);
1519
760
  if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
1520
- if (run.schemaVersion !== 3) {
1521
- return failed('ORCHESTRATION_STATE_INVALID', 'delegation preparation requires orchestration schemaVersion 3', resolved.taskId);
1522
- }
1523
- if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
1524
761
  if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
1525
762
  const repositoryPending = matchingDelegations(() => true, options);
1526
763
  if (repositoryPending.length > 0) {
@@ -1546,6 +783,15 @@ function prepareOrchestrationDelegationUnlocked(
1546
783
  if (input.requestedReasoningEffort !== expectedPolicy.reasoningEffort) {
1547
784
  return failed('ORCHESTRATION_REQUESTED_REASONING_EFFORT_MISMATCH', `requested reasoning effort does not match the persisted ${next.role} policy`, resolved.taskId);
1548
785
  }
786
+ const capabilityValidationError = options.validateLifecycleCapability?.();
787
+ if (capabilityValidationError) {
788
+ return failed(
789
+ capabilityValidationError.code,
790
+ capabilityValidationError.message,
791
+ resolved.taskId,
792
+ capabilityValidationError.detail ? { detail: capabilityValidationError.detail } : {}
793
+ );
794
+ }
1549
795
  let beforeFingerprint: string;
1550
796
  try {
1551
797
  beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
@@ -1570,6 +816,15 @@ function prepareOrchestrationDelegationUnlocked(
1570
816
  lifecycleProvenance: input.lifecycleProvenance ?? null,
1571
817
  beforeFingerprint
1572
818
  }, { id: options.id, now: options.now, monotonicNow: options.monotonicNow });
819
+ const capabilityError = options.consumeLifecycleCapability?.();
820
+ if (capabilityError) {
821
+ return failed(
822
+ capabilityError.code,
823
+ capabilityError.message,
824
+ resolved.taskId,
825
+ capabilityError.detail ? { detail: capabilityError.detail } : {}
826
+ );
827
+ }
1573
828
  const updated = withUpdatedRun(run, {
1574
829
  nextStage: next.stage,
1575
830
  pendingDelegation: receipt,
@@ -1619,8 +874,8 @@ function dispatchOrchestrationDelegationUnlocked(
1619
874
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1620
875
  const run = readRun(resolved.taskDir);
1621
876
  if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
1622
- if (run.schemaVersion !== 3 || run.status !== 'running') {
1623
- return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running schemaVersion 3 orchestration', resolved.taskId);
877
+ if (run.status !== 'running') {
878
+ return failed('ORCHESTRATION_STATE_INVALID', 'spawn dispatch requires a running orchestration', resolved.taskId);
1624
879
  }
1625
880
  const result = dispatchDelegation(run.pendingDelegation, {
1626
881
  now: options.now,
@@ -1667,7 +922,7 @@ function matchingDelegations(
1667
922
  .filter((entry) => entry.isDirectory())
1668
923
  .flatMap((entry) => {
1669
924
  const run = readRun(path.join(activeRoot, entry.name));
1670
- if (run?.schemaVersion !== 3 || run.status !== 'running' || !run.pendingDelegation) return [];
925
+ if (!run || run.status !== 'running' || !run.pendingDelegation) return [];
1671
926
  const receipt = run.pendingDelegation;
1672
927
  return predicate(receipt)
1673
928
  ? [{ taskId: entry.name, run }]
@@ -1920,7 +1175,6 @@ function activateOrchestrationDelegation(
1920
1175
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1921
1176
  const run = readRun(resolved.taskDir);
1922
1177
  if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
1923
- if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'active delegation requires schemaVersion 3', resolved.taskId);
1924
1178
  if (run.status !== 'running') {
1925
1179
  return { status: run.status, changed: false, taskId: resolved.taskId, run, next: null, error: null };
1926
1180
  }
@@ -1951,9 +1205,6 @@ async function awaitOrchestrationDelegationActivation(
1951
1205
  if (!run?.pendingDelegation) {
1952
1206
  return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
1953
1207
  }
1954
- if (run.schemaVersion !== 3) {
1955
- return failed('ORCHESTRATION_STATE_INVALID', 'activation barrier requires schemaVersion 3', resolved.taskId);
1956
- }
1957
1208
  const receipt = run.pendingDelegation;
1958
1209
  const matches = receipt.stage === event.stage
1959
1210
  && receipt.round === event.round
@@ -2051,9 +1302,6 @@ function recoverPreparedOrchestrationDelegation(
2051
1302
  resolved.taskId
2052
1303
  );
2053
1304
  }
2054
- if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
2055
- return failed('ORCHESTRATION_PREPARED_RECOVERY_UNSAFE', 'active commit intent blocks prepared recovery', resolved.taskId);
2056
- }
2057
1305
  const current = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
2058
1306
  gitRoot: gitRootFor(resolved.repoRoot, options),
2059
1307
  stateRoot: resolved.repoRoot,
@@ -2093,7 +1341,6 @@ function completeOrchestrationStage(
2093
1341
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
2094
1342
  const run = readRun(resolved.taskDir);
2095
1343
  if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
2096
- if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage completion requires schemaVersion 3', resolved.taskId);
2097
1344
  if (!run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending delegation', resolved.taskId);
2098
1345
  const result = completeDelegationStage(run.pendingDelegation, event);
2099
1346
  if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
@@ -2116,7 +1363,6 @@ function inspectOrchestrationStage(
2116
1363
  return failed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
2117
1364
  }
2118
1365
  if (!run) return failed('ORCHESTRATION_RUN_MISSING', 'no orchestration run exists', resolved.taskId);
2119
- if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'stage inspection requires schemaVersion 3', resolved.taskId);
2120
1366
  const receipt = run.pendingDelegation;
2121
1367
  if (
2122
1368
  !receipt
@@ -2175,7 +1421,6 @@ function sealOrchestrationDelegation(
2175
1421
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
2176
1422
  const run = readRun(resolved.taskDir);
2177
1423
  if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
2178
- if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'delegation sealing requires schemaVersion 3', resolved.taskId);
2179
1424
  const result = sealDelegation(run.pendingDelegation, event, { now: options.now });
2180
1425
  if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
2181
1426
  const updated = withUpdatedRun(run, { pendingDelegation: result.receipt });
@@ -2188,7 +1433,6 @@ function advanceOrchestration(taskRef: string, options: OrchestrationOptions = {
2188
1433
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
2189
1434
  const run = readRun(resolved.taskDir);
2190
1435
  if (!run?.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no pending delegation exists', resolved.taskId);
2191
- if (run.schemaVersion !== 3) return failed('ORCHESTRATION_STATE_INVALID', 'orchestration advance requires schemaVersion 3', resolved.taskId);
2192
1436
  const result = consumeDelegation(run.pendingDelegation, { now: options.now });
2193
1437
  if (!result.ok) return pauseOrchestration(taskRef, result.code, result.message, true, options);
2194
1438
  const completed = result.receipt.stage === 'commit';
@@ -2227,17 +1471,14 @@ export {
2227
1471
  activateOrchestrationDelegation,
2228
1472
  awaitOrchestrationDelegationActivation,
2229
1473
  advanceOrchestration,
2230
- abortCommitIntent,
2231
- beginCommitIntent,
2232
1474
  beginOrResumeOrchestration,
2233
- checkpointCommitIntent,
2234
1475
  commitOrchestrationStageCompletion,
2235
- completeCommitIntent,
2236
1476
  completeOrchestrationStage,
2237
1477
  dispatchOrchestrationDelegation,
2238
1478
  inspectOrchestrationStage,
2239
1479
  hasActivatableOrchestrationDelegation,
2240
1480
  hasSealableOrchestrationDelegation,
1481
+ OrchestrationStateError,
2241
1482
  orchestrationPath,
2242
1483
  pauseMatchingOrchestrationDelegation,
2243
1484
  pauseOrchestration,
@@ -2245,19 +1486,14 @@ export {
2245
1486
  prepareOrchestrationDelegation,
2246
1487
  readRun,
2247
1488
  reconcileMatchingOrchestrationDelegation,
2248
- recoverCommitIntent,
2249
1489
  recoverPreparedOrchestrationDelegation,
2250
1490
  routeOrchestration,
2251
1491
  sealMatchingOrchestrationDelegation,
2252
1492
  sealMatchingOrchestrationDelegationWithHostEvidence,
2253
1493
  sealOrchestrationDelegation,
2254
- startCommitAttempt,
2255
- statusCommitIntent,
2256
1494
  statusOrchestration,
2257
- terminateCommitAttempt
2258
1495
  };
2259
1496
  export type {
2260
- CommitIntentResult,
2261
1497
  OrchestrationCompletionPlanResult,
2262
1498
  OrchestrationModelPolicy,
2263
1499
  OrchestrationNext,