@fitlab-ai/agent-infra 0.9.3 → 0.9.5

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 (203) hide show
  1. package/README.md +3 -1
  2. package/README.zh-CN.md +3 -1
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +144 -123
  6. package/dist/lib/agent-clients/adapter.js +94 -4
  7. package/dist/lib/{sandbox/credentials.js → agent-clients/adapters/claude-code-credentials.js} +4 -14
  8. package/dist/lib/agent-clients/adapters/claude-code-refresh.js +85 -0
  9. package/dist/lib/agent-clients/adapters/claude-code-sandbox.js +247 -0
  10. package/dist/lib/agent-clients/adapters/claude-code.js +17 -3
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  12. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  14. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  15. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +108 -0
  16. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  17. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  18. package/dist/lib/agent-clients/registry.js +9 -1
  19. package/dist/lib/git/worktree-identity.js +64 -0
  20. package/dist/lib/internal/codex-lifecycle.js +202 -0
  21. package/dist/lib/internal/sandbox-control.js +24 -0
  22. package/dist/lib/internal/task-activity.js +45 -13
  23. package/dist/lib/internal/task-create.js +48 -0
  24. package/dist/lib/internal/task-ledger.js +12 -3
  25. package/dist/lib/internal/task-lifecycle.js +19 -1
  26. package/dist/lib/internal/task-orchestration.js +170 -27
  27. package/dist/lib/platform/github-client.js +19 -2
  28. package/dist/lib/platform/pr-checks.js +1 -1
  29. package/dist/lib/platform/pull-requests.js +47 -1
  30. package/dist/lib/platform/releases.js +13 -5
  31. package/dist/lib/process-data/index.js +239 -0
  32. package/dist/lib/process-data/normalize.js +198 -0
  33. package/dist/lib/process-data/privacy.js +33 -0
  34. package/dist/lib/process-data/reconcile.js +95 -0
  35. package/dist/lib/process-data/repair.js +58 -0
  36. package/dist/lib/process-data/sources.js +345 -0
  37. package/dist/lib/process-data/store.js +282 -0
  38. package/dist/lib/process-data/types.js +2 -0
  39. package/dist/lib/run/index.js +4 -3
  40. package/dist/lib/run/tui.js +7 -8
  41. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  42. package/dist/lib/sandbox/capture.js +7 -2
  43. package/dist/lib/sandbox/commands/create.js +110 -228
  44. package/dist/lib/sandbox/commands/enter.js +15 -47
  45. package/dist/lib/sandbox/commands/list-running.js +16 -6
  46. package/dist/lib/sandbox/commands/ls.js +10 -6
  47. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  48. package/dist/lib/sandbox/commands/refresh.js +4 -84
  49. package/dist/lib/sandbox/commands/rm.js +24 -3
  50. package/dist/lib/sandbox/commands/show.js +12 -3
  51. package/dist/lib/sandbox/commands/start.js +7 -4
  52. package/dist/lib/sandbox/config.js +2 -0
  53. package/dist/lib/sandbox/constants.js +6 -0
  54. package/dist/lib/sandbox/control/client.js +58 -0
  55. package/dist/lib/sandbox/control/protocol.js +54 -0
  56. package/dist/lib/sandbox/control/server.js +144 -0
  57. package/dist/lib/sandbox/dockerfile.js +36 -13
  58. package/dist/lib/sandbox/mounts.js +17 -4
  59. package/dist/lib/sandbox/recovery.js +128 -5
  60. package/dist/lib/sandbox/redaction.js +12 -0
  61. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  62. package/dist/lib/sandbox/workspace-identity.js +95 -0
  63. package/dist/lib/sandbox/workspace-view.js +109 -0
  64. package/dist/lib/task/activity-intent.js +257 -51
  65. package/dist/lib/task/activity-log.js +32 -5
  66. package/dist/lib/task/commit-finalization.js +278 -0
  67. package/dist/lib/task/commit-intent.js +165 -0
  68. package/dist/lib/task/create-service.js +137 -0
  69. package/dist/lib/task/create.js +333 -0
  70. package/dist/lib/task/decision-intents.js +31 -17
  71. package/dist/lib/task/delegation-receipts.js +2 -2
  72. package/dist/lib/task/implementation-inputs.js +54 -7
  73. package/dist/lib/task/ledger-intents.js +35 -1
  74. package/dist/lib/task/orchestration.js +505 -34
  75. package/dist/lib/task/resolve-ref.js +41 -1
  76. package/dist/lib/task/review-fingerprint.js +4 -0
  77. package/dist/lib/task/workspace-snapshot.js +78 -3
  78. package/lib/agent-clients/adapter.ts +130 -6
  79. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  80. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  81. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  82. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  83. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  84. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  85. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  86. package/lib/agent-clients/adapters/codex.ts +2 -2
  87. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  88. package/lib/agent-clients/adapters/opencode.ts +18 -3
  89. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  90. package/lib/agent-clients/registry.ts +9 -1
  91. package/lib/git/worktree-identity.ts +86 -0
  92. package/lib/internal/codex-lifecycle.ts +204 -0
  93. package/lib/internal/sandbox-control.ts +24 -0
  94. package/lib/internal/task-activity.ts +50 -14
  95. package/lib/internal/task-create.ts +49 -0
  96. package/lib/internal/task-ledger.ts +12 -3
  97. package/lib/internal/task-lifecycle.ts +22 -1
  98. package/lib/internal/task-orchestration.ts +117 -21
  99. package/lib/platform/github-client.ts +20 -2
  100. package/lib/platform/pr-checks.ts +1 -1
  101. package/lib/platform/pull-requests.ts +49 -1
  102. package/lib/platform/releases.ts +16 -4
  103. package/lib/process-data/index.ts +224 -0
  104. package/lib/process-data/normalize.ts +202 -0
  105. package/lib/process-data/privacy.ts +41 -0
  106. package/lib/process-data/reconcile.ts +110 -0
  107. package/lib/process-data/repair.ts +64 -0
  108. package/lib/process-data/sources.ts +354 -0
  109. package/lib/process-data/store.ts +315 -0
  110. package/lib/process-data/types.ts +135 -0
  111. package/lib/run/index.ts +5 -3
  112. package/lib/run/tui.ts +11 -8
  113. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  114. package/lib/sandbox/capture.ts +9 -2
  115. package/lib/sandbox/commands/create.ts +127 -260
  116. package/lib/sandbox/commands/enter.ts +13 -59
  117. package/lib/sandbox/commands/list-running.ts +23 -6
  118. package/lib/sandbox/commands/ls.ts +22 -6
  119. package/lib/sandbox/commands/rebuild.ts +4 -3
  120. package/lib/sandbox/commands/refresh.ts +4 -126
  121. package/lib/sandbox/commands/rm.ts +24 -3
  122. package/lib/sandbox/commands/show.ts +29 -3
  123. package/lib/sandbox/commands/start.ts +6 -4
  124. package/lib/sandbox/config.ts +4 -0
  125. package/lib/sandbox/constants.ts +8 -0
  126. package/lib/sandbox/control/client.ts +79 -0
  127. package/lib/sandbox/control/protocol.ts +110 -0
  128. package/lib/sandbox/control/server.ts +155 -0
  129. package/lib/sandbox/dockerfile.ts +66 -13
  130. package/lib/sandbox/mounts.ts +24 -4
  131. package/lib/sandbox/recovery.ts +163 -6
  132. package/lib/sandbox/redaction.ts +14 -0
  133. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  134. package/lib/sandbox/tool-types.ts +16 -0
  135. package/lib/sandbox/workspace-identity.ts +123 -0
  136. package/lib/sandbox/workspace-view.ts +150 -0
  137. package/lib/task/activity-intent.ts +350 -61
  138. package/lib/task/activity-log.ts +46 -7
  139. package/lib/task/commit-finalization.ts +376 -0
  140. package/lib/task/commit-intent.ts +230 -0
  141. package/lib/task/create-service.ts +171 -0
  142. package/lib/task/create.ts +372 -0
  143. package/lib/task/decision-intents.ts +32 -12
  144. package/lib/task/delegation-receipts.ts +2 -2
  145. package/lib/task/implementation-inputs.ts +62 -8
  146. package/lib/task/ledger-intents.ts +43 -3
  147. package/lib/task/orchestration.ts +673 -41
  148. package/lib/task/resolve-ref.ts +46 -1
  149. package/lib/task/review-fingerprint.ts +9 -0
  150. package/lib/task/workspace-snapshot.ts +98 -4
  151. package/package.json +7 -7
  152. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  153. package/templates/.agents/rules/README.en.md +1 -1
  154. package/templates/.agents/rules/README.zh-CN.md +1 -1
  155. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  156. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  157. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  158. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  159. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  160. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  161. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  162. package/templates/.agents/rules/task-management.en.md +3 -0
  163. package/templates/.agents/rules/task-management.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  166. package/templates/.agents/skills/commit/SKILL.en.md +21 -17
  167. package/templates/.agents/skills/commit/SKILL.zh-CN.md +28 -16
  168. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +50 -0
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +56 -0
  170. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  171. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  172. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  173. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  174. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  175. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  176. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  177. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  178. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  179. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  180. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  181. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  182. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  183. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  184. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  185. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  186. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  187. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  188. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  189. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  190. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  191. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  192. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  193. package/templates/.agents/templates/task.en.md +1 -1
  194. package/templates/.agents/templates/task.zh-CN.md +1 -1
  195. package/templates/.claude/commands/watch-pr.en.md +1 -1
  196. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  197. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  198. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  199. package/templates/.codex/hooks.json +50 -0
  200. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  201. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  202. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  203. package/templates/.gemini/commands/_project_/review-pr.zh-CN.toml +0 -10
@@ -1,6 +1,7 @@
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';
4
5
 
5
6
  import { parseTypedTaskFrontmatter } from './frontmatter.ts';
6
7
  import { parseReviewSummary } from './review-artifacts.ts';
@@ -35,7 +36,33 @@ import {
35
36
  diffWorkspaceSnapshots
36
37
  } from './workspace-snapshot.ts';
37
38
  import type { RepositorySnapshot } from './workspace-snapshot.ts';
39
+ import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
40
+ import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
38
41
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
42
+ import {
43
+ CommitIntentError,
44
+ commitIntentPath,
45
+ createCommitIntent,
46
+ digest,
47
+ readCommitIntent,
48
+ removeCommitIntent,
49
+ removeCommitIntentByDigest,
50
+ serialize,
51
+ updateCommitIntent
52
+ } from './commit-intent.ts';
53
+ import type { CommitIntent, PushEvidence } from './commit-intent.ts';
54
+ import {
55
+ inspectCommitFinalization,
56
+ planCommitTaskFinalization
57
+ } from './commit-finalization.ts';
58
+ import type { CommitFinalizationInspection } from './commit-finalization.ts';
59
+ import { captureTaskWriteMetadata, writeTask } from './write.ts';
60
+ import {
61
+ appendActivityEntry,
62
+ commitAttemptStartedNote,
63
+ locateActivityLog,
64
+ pairEntries
65
+ } from './activity-log.ts';
39
66
 
40
67
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
41
68
  type LegacyOrchestrationModelPolicy = Readonly<{
@@ -128,12 +155,13 @@ type OrchestrationCompletionPlanResult = Readonly<{
128
155
  }>;
129
156
  type OrchestrationOptions = {
130
157
  repoRoot?: string;
158
+ gitWorktreeRoot?: string;
131
159
  id?: () => string;
132
160
  now?: () => string;
133
161
  maxSteps?: number;
134
162
  client?: AgentClientId;
135
163
  modelPolicy?: OrchestrationModelPolicy;
136
- captureWorkspace?: (repoRoot: string, taskId: string | null) => string;
164
+ captureWorkspace?: (context: WorkspaceSnapshotContext) => string;
137
165
  captureRepository?: (repoRoot: string) => RepositorySnapshot;
138
166
  inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
139
167
  status: string;
@@ -144,8 +172,37 @@ type OrchestrationOptions = {
144
172
  }>;
145
173
  diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
146
174
  supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
175
+ token?: () => string;
147
176
  };
148
177
 
178
+ type CommitIntentResult = Readonly<{
179
+ status: 'ready' | 'failed';
180
+ changed: boolean;
181
+ taskId: string | null;
182
+ intent: Readonly<{
183
+ mode: CommitIntent['mode'];
184
+ phase: CommitIntent['phase'];
185
+ baselineHead: string;
186
+ currentHead: string;
187
+ committedHead: string | null;
188
+ pushEvidence: PushEvidence | null;
189
+ runId: string | null;
190
+ receiptId: string | null;
191
+ }> | null;
192
+ finalization?: Readonly<{
193
+ disposition: CommitFinalizationInspection['disposition'];
194
+ code: CommitFinalizationInspection['code'];
195
+ message: string;
196
+ currentHead: string;
197
+ committedHead: string | null;
198
+ needsAnchor: boolean;
199
+ needsLog: boolean;
200
+ attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
201
+ }>;
202
+ token?: string;
203
+ error: Readonly<{ code: string; message: string }> | null;
204
+ }>;
205
+
149
206
  function supportsLifecycleDelegation(client: AgentClientId): boolean {
150
207
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
151
208
  && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
@@ -187,8 +244,8 @@ function saveRun(taskDir: string, run: OrchestrationRun): void {
187
244
  atomicWrite(orchestrationPath(taskDir), run);
188
245
  }
189
246
 
190
- function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>): OrchestrationRun {
191
- return Object.freeze({ ...run, ...updates, updatedAt: new Date().toISOString() });
247
+ function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>, now?: () => string): OrchestrationRun {
248
+ return Object.freeze({ ...run, ...updates, updatedAt: (now ?? (() => new Date().toISOString()))() });
192
249
  }
193
250
 
194
251
  function failed(
@@ -200,6 +257,79 @@ function failed(
200
257
  return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
201
258
  }
202
259
 
260
+ function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
261
+ return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
262
+ }
263
+
264
+ function repositoryHead(repoRoot: string): string {
265
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
266
+ cwd: repoRoot,
267
+ encoding: 'utf8',
268
+ stdio: ['ignore', 'pipe', 'pipe']
269
+ }).trim();
270
+ }
271
+
272
+ function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
273
+ if (!options.gitWorktreeRoot) return stateRoot;
274
+ return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
275
+ }
276
+
277
+ function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
278
+ return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
279
+ cwd: repoRoot,
280
+ stdio: 'ignore'
281
+ }).status === 0;
282
+ }
283
+
284
+ function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
285
+ return {
286
+ mode: intent.mode,
287
+ phase: intent.phase,
288
+ baselineHead: intent.baselineHead,
289
+ currentHead,
290
+ committedHead: intent.committedHead,
291
+ pushEvidence: intent.pushEvidence,
292
+ runId: intent.orchestration?.runId ?? null,
293
+ receiptId: intent.orchestration?.receiptId ?? null
294
+ };
295
+ }
296
+
297
+ function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
298
+ return {
299
+ disposition: inspection.disposition,
300
+ code: inspection.code,
301
+ message: inspection.message,
302
+ currentHead: inspection.currentHead,
303
+ committedHead: inspection.committedHead,
304
+ needsAnchor: inspection.needsAnchor,
305
+ needsLog: inspection.needsLog,
306
+ attempt: inspection.attempt
307
+ };
308
+ }
309
+
310
+ function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
311
+ return run !== null
312
+ && [1, 2].includes(run.schemaVersion)
313
+ && run.taskId === taskId
314
+ && ['running', 'paused', 'completed'].includes(run.status)
315
+ && Array.isArray(run.receipts)
316
+ && Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
317
+ && typeof run.commitAuthorization === 'object'
318
+ && run.commitAuthorization !== null
319
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
320
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
321
+ }
322
+
323
+ function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
324
+ if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
325
+ if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
326
+ return commitIntentFailed(
327
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
328
+ error instanceof Error ? error.message : String(error),
329
+ taskId
330
+ );
331
+ }
332
+
203
333
  function validModel(value: unknown): value is string {
204
334
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
205
335
  }
@@ -493,6 +623,12 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
493
623
  function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
494
624
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
495
625
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
626
+ let gitRoot: string;
627
+ try {
628
+ gitRoot = gitRootFor(resolved.repoRoot, options);
629
+ } catch (error) {
630
+ return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
631
+ }
496
632
  let content: string;
497
633
  try {
498
634
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
@@ -563,7 +699,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
563
699
  const capture = options.captureRepository ?? captureRepositorySnapshot;
564
700
  let before: RepositorySnapshot;
565
701
  try {
566
- before = capture(resolved.repoRoot);
702
+ before = capture(gitRoot);
567
703
  } catch (error) {
568
704
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
569
705
  }
@@ -579,7 +715,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
579
715
  options.inspectPullRequest ?? inspectPlatformPullRequest;
580
716
  let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
581
717
  try {
582
- inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
718
+ inspection = inspect(resolved.taskId, { cwd: gitRoot });
583
719
  } catch (error) {
584
720
  return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
585
721
  }
@@ -600,7 +736,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
600
736
 
601
737
  let after: RepositorySnapshot;
602
738
  try {
603
- after = capture(resolved.repoRoot);
739
+ after = capture(gitRoot);
604
740
  } catch (error) {
605
741
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
606
742
  }
@@ -634,6 +770,488 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
634
770
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
635
771
  }
636
772
 
773
+ function appendCommitActivity(
774
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
775
+ step: string,
776
+ agent: string,
777
+ note: string
778
+ ): Readonly<{ code: string; message: string }> | null {
779
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
780
+ const activity = locateActivityLog(taskContent);
781
+ if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
782
+ const metadata = captureTaskWriteMetadata();
783
+ const written = writeTask({
784
+ taskRef: resolved.taskId,
785
+ expectedState: 'active',
786
+ mutations: [{
787
+ kind: 'section',
788
+ aliases: ['活动日志', 'Activity Log'],
789
+ heading: activity.heading,
790
+ body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
791
+ }]
792
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
793
+ return written.status === 'failed' ? written.error : null;
794
+ }
795
+
796
+ function startCommitAttempt(
797
+ taskRef: string,
798
+ input: Readonly<{ agent: string }>,
799
+ options: OrchestrationOptions = {}
800
+ ): CommitIntentResult {
801
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
802
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
803
+ try {
804
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
805
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
806
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
807
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
808
+ }
809
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
810
+ const activity = locateActivityLog(taskContent);
811
+ if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
812
+ const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
813
+ if (open.length > 0) {
814
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
815
+ }
816
+ const currentHead = repositoryHead(gitRoot);
817
+ const attempt = (options.id ?? randomUUID)();
818
+ const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
819
+ attempt,
820
+ baseline: currentHead,
821
+ agent: input.agent
822
+ }));
823
+ if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
824
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
825
+ return {
826
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
827
+ finalization: commitFinalizationView(inspection), error: null
828
+ };
829
+ });
830
+ } catch (error) {
831
+ return mapCommitIntentError(error, resolved.taskId);
832
+ }
833
+ }
834
+
835
+ function terminateCommitAttempt(
836
+ taskRef: string,
837
+ input: Readonly<{ attempt: string; agent: string; code: string }>,
838
+ options: OrchestrationOptions = {}
839
+ ): CommitIntentResult {
840
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
841
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
842
+ try {
843
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
844
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
845
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
846
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
847
+ }
848
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
849
+ if (
850
+ inspection.disposition !== 'retryable-start'
851
+ || inspection.attempt?.attempt !== input.attempt
852
+ || inspection.attempt.agent !== input.agent
853
+ || inspection.attempt.baseline !== inspection.currentHead
854
+ ) {
855
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
856
+ }
857
+ const error = appendCommitActivity(
858
+ resolved,
859
+ 'Commit [aborted]',
860
+ input.agent,
861
+ `aborted; attempt=${input.attempt}; code=${input.code}`
862
+ );
863
+ if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
864
+ return {
865
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
866
+ finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
867
+ error: null
868
+ };
869
+ });
870
+ } catch (error) {
871
+ return mapCommitIntentError(error, resolved.taskId);
872
+ }
873
+ }
874
+
875
+ function beginCommitIntent(
876
+ taskRef: string,
877
+ input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
878
+ options: OrchestrationOptions = {}
879
+ ): CommitIntentResult {
880
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
881
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
882
+ try {
883
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
884
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
885
+ const currentHead = repositoryHead(gitRoot);
886
+ if (currentHead !== input.baselineHead) {
887
+ return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
888
+ }
889
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
890
+ try {
891
+ readCommitIntent(resolved.taskDir, resolved.taskId);
892
+ } catch (error) {
893
+ return mapCommitIntentError(error, resolved.taskId);
894
+ }
895
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
896
+ }
897
+ const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
898
+ if (
899
+ attemptInspection.disposition !== 'retryable-start'
900
+ || attemptInspection.attempt?.attempt !== input.attempt
901
+ || attemptInspection.attempt.agent !== input.agent
902
+ || attemptInspection.attempt.baseline !== input.baselineHead
903
+ ) {
904
+ return commitIntentFailed(
905
+ 'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
906
+ 'commit begin requires one matching retryable Commit attempt',
907
+ resolved.taskId
908
+ );
909
+ }
910
+ const runFile = orchestrationPath(resolved.taskDir);
911
+ let run: OrchestrationRun | null = null;
912
+ let sourceRunBytes: string | null = null;
913
+ if (fs.existsSync(runFile)) {
914
+ sourceRunBytes = fs.readFileSync(runFile, 'utf8');
915
+ try {
916
+ run = readRun(resolved.taskDir);
917
+ } catch (error) {
918
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
919
+ }
920
+ if (!validCommitRun(run, resolved.taskId)) {
921
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
922
+ }
923
+ }
924
+
925
+ const now = (options.now ?? (() => new Date().toISOString()))();
926
+ const failAndPause = (code: string, message: string): CommitIntentResult => {
927
+ if (run?.status === 'running') {
928
+ saveRun(resolved.taskDir, withUpdatedRun(run, {
929
+ status: 'paused', pause: { code, message, recoverable: true }
930
+ }, () => now));
931
+ }
932
+ return commitIntentFailed(code, message, resolved.taskId);
933
+ };
934
+ let orchestration: CommitIntent['orchestration'] = null;
935
+ if (!input.orchestrated) {
936
+ if (run?.pendingDelegation) {
937
+ return commitIntentFailed(
938
+ 'ORCHESTRATION_STANDALONE_BUSY',
939
+ 'standalone commit is blocked by a pending orchestration delegation',
940
+ resolved.taskId
941
+ );
942
+ }
943
+ } else {
944
+ if (!run || run.status !== 'running') {
945
+ return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
946
+ }
947
+ const receipt = run.pendingDelegation;
948
+ if (
949
+ !receipt
950
+ || receipt.status !== 'activated'
951
+ || receipt.stage !== 'commit'
952
+ || receipt.round !== 1
953
+ || receipt.artifact !== 'commit'
954
+ || receipt.role !== 'executor'
955
+ ) {
956
+ return failAndPause(
957
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
958
+ 'orchestrated commit requires one matching activated commit delegation'
959
+ );
960
+ }
961
+ if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
962
+ return failAndPause(
963
+ 'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
964
+ 'orchestrated commit requires an unconsumed one-use authorization'
965
+ );
966
+ }
967
+ const planned = planOrchestrationStageCompletion(taskRef, {
968
+ stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
969
+ }, { ...options, now: () => now });
970
+ if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
971
+ return failAndPause(
972
+ planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
973
+ planned.result.error?.message ?? 'commit delegation could not be completed'
974
+ );
975
+ }
976
+ const plannedRunBytes = serialize(planned.plan.updatedRun);
977
+ orchestration = {
978
+ runId: run.runId,
979
+ receiptId: receipt.id,
980
+ authorizationIssuedAt: run.commitAuthorization.issuedAt,
981
+ sourceRunDigest: digest(sourceRunBytes!),
982
+ plannedRunDigest: digest(plannedRunBytes),
983
+ completionUpdatedAt: planned.plan.updatedRun.updatedAt,
984
+ plannedReceipt: planned.plan.updatedRun.pendingDelegation
985
+ };
986
+ }
987
+
988
+ const created = createCommitIntent(resolved.taskDir, {
989
+ taskId: resolved.taskId,
990
+ mode: input.orchestrated ? 'orchestrated' : 'standalone',
991
+ phase: 'prepared',
992
+ baselineHead: input.baselineHead,
993
+ committedHead: null,
994
+ pushEvidence: null,
995
+ orchestration,
996
+ createdAt: now,
997
+ updatedAt: now
998
+ }, { token: options.token });
999
+ return {
1000
+ status: 'ready', changed: true, taskId: resolved.taskId,
1001
+ intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
1002
+ };
1003
+ });
1004
+ } catch (error) {
1005
+ return mapCommitIntentError(error, resolved.taskId);
1006
+ }
1007
+ }
1008
+
1009
+ function checkpointCommitIntent(
1010
+ taskRef: string,
1011
+ input: Readonly<{
1012
+ token: string;
1013
+ kind: 'committed' | 'pushed';
1014
+ head: string;
1015
+ remote?: string;
1016
+ ref?: string;
1017
+ }>,
1018
+ options: OrchestrationOptions = {}
1019
+ ): CommitIntentResult {
1020
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1021
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1022
+ try {
1023
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1024
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
1025
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1026
+ const currentHead = repositoryHead(gitRoot);
1027
+ if (currentHead !== input.head) {
1028
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
1029
+ }
1030
+ if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
1031
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
1032
+ }
1033
+ if (input.kind === 'pushed') {
1034
+ const expectedHead = intent.committedHead ?? intent.baselineHead;
1035
+ if (input.head !== expectedHead || !input.remote || !input.ref) {
1036
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
1037
+ }
1038
+ }
1039
+ const now = (options.now ?? (() => new Date().toISOString()))();
1040
+ const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
1041
+ ? { phase: 'committed', committedHead: input.head, updatedAt: now }
1042
+ : {
1043
+ phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
1044
+ pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
1045
+ });
1046
+ return {
1047
+ status: 'ready', changed: true, taskId: resolved.taskId,
1048
+ intent: commitIntentView(updated, currentHead), error: null
1049
+ };
1050
+ });
1051
+ } catch (error) {
1052
+ return mapCommitIntentError(error, resolved.taskId);
1053
+ }
1054
+ }
1055
+
1056
+ function completeCommitOrchestration(
1057
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1058
+ intent: CommitIntent,
1059
+ agent: string
1060
+ ): Readonly<{ code: string; message: string }> | null {
1061
+ if (intent.orchestration === null) return null;
1062
+ if (intent.orchestration.plannedReceipt.agent !== agent) {
1063
+ return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
1064
+ }
1065
+ const runFile = orchestrationPath(resolved.taskDir);
1066
+ if (!fs.existsSync(runFile)) {
1067
+ return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
1068
+ }
1069
+ const currentBytes = fs.readFileSync(runFile, 'utf8');
1070
+ const currentDigest = digest(currentBytes);
1071
+ if (currentDigest === intent.orchestration.sourceRunDigest) {
1072
+ const run = readRun(resolved.taskDir);
1073
+ if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
1074
+ const plannedRun: OrchestrationRun = Object.freeze({
1075
+ ...run,
1076
+ pendingDelegation: intent.orchestration.plannedReceipt,
1077
+ updatedAt: intent.orchestration.completionUpdatedAt
1078
+ });
1079
+ if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
1080
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
1081
+ }
1082
+ atomicWrite(runFile, plannedRun);
1083
+ if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
1084
+ return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
1085
+ }
1086
+ } else if (currentDigest !== intent.orchestration.plannedRunDigest) {
1087
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
1088
+ }
1089
+ return null;
1090
+ }
1091
+
1092
+ function finalizeCommitIntentUnlocked(
1093
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
1094
+ inspection: CommitFinalizationInspection,
1095
+ agent: string,
1096
+ remove: () => void
1097
+ ): CommitIntentResult {
1098
+ if (inspection.disposition !== 'recoverable' || !inspection.intent) {
1099
+ return commitIntentFailed(
1100
+ inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
1101
+ inspection.message,
1102
+ resolved.taskId
1103
+ );
1104
+ }
1105
+ if (
1106
+ inspection.intent.orchestration !== null
1107
+ && inspection.intent.orchestration.plannedReceipt.agent !== agent
1108
+ ) {
1109
+ return commitIntentFailed(
1110
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
1111
+ 'completion agent does not match the planned receipt',
1112
+ resolved.taskId
1113
+ );
1114
+ }
1115
+ const metadata = captureTaskWriteMetadata();
1116
+ const taskPlan = planCommitTaskFinalization(
1117
+ resolved.taskDir,
1118
+ inspection,
1119
+ agent,
1120
+ metadata.timestamp
1121
+ );
1122
+ const written = writeTask({
1123
+ taskRef: resolved.taskId,
1124
+ expectedState: 'active',
1125
+ mutations: taskPlan.mutations
1126
+ }, {
1127
+ repoRoot: resolved.repoRoot,
1128
+ metadataProvider: () => metadata
1129
+ });
1130
+ if (written.status === 'failed') {
1131
+ return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
1132
+ }
1133
+ const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
1134
+ if (orchestrationError) {
1135
+ return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
1136
+ }
1137
+ try {
1138
+ remove();
1139
+ } catch (error) {
1140
+ return commitIntentFailed(
1141
+ 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
1142
+ error instanceof Error ? error.message : String(error),
1143
+ resolved.taskId
1144
+ );
1145
+ }
1146
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1147
+ }
1148
+
1149
+ function completeCommitIntent(
1150
+ taskRef: string,
1151
+ input: Readonly<{ token: string; agent: string }>,
1152
+ options: OrchestrationOptions = {}
1153
+ ): CommitIntentResult {
1154
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1155
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1156
+ try {
1157
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1158
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
1159
+ readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1160
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1161
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1162
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1163
+ });
1164
+ });
1165
+ } catch (error) {
1166
+ return mapCommitIntentError(error, resolved.taskId);
1167
+ }
1168
+ }
1169
+
1170
+ function recoverCommitIntent(
1171
+ taskRef: string,
1172
+ input: Readonly<{ agent: string }>,
1173
+ options: OrchestrationOptions = {}
1174
+ ): CommitIntentResult {
1175
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1176
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1177
+ try {
1178
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1179
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
1180
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1181
+ if (inspection.disposition === 'retryable-start') {
1182
+ return {
1183
+ status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
1184
+ finalization: commitFinalizationView(inspection), error: null
1185
+ };
1186
+ }
1187
+ if (inspection.disposition === 'prepared' && inspection.intentDigest) {
1188
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
1189
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1190
+ }
1191
+ if (!inspection.intentDigest) {
1192
+ return commitIntentFailed(
1193
+ inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
1194
+ inspection.message,
1195
+ resolved.taskId
1196
+ );
1197
+ }
1198
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1199
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
1200
+ });
1201
+ });
1202
+ } catch (error) {
1203
+ return mapCommitIntentError(error, resolved.taskId);
1204
+ }
1205
+ }
1206
+
1207
+ function abortCommitIntent(
1208
+ taskRef: string,
1209
+ input: Readonly<{ token: string; expectedHead: string }>,
1210
+ options: OrchestrationOptions = {}
1211
+ ): CommitIntentResult {
1212
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1213
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1214
+ try {
1215
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1216
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
1217
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1218
+ const currentHead = repositoryHead(gitRoot);
1219
+ if (
1220
+ intent.phase !== 'prepared'
1221
+ || intent.pushEvidence !== null
1222
+ || input.expectedHead !== intent.baselineHead
1223
+ || currentHead !== intent.baselineHead
1224
+ ) {
1225
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
1226
+ }
1227
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1228
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1229
+ });
1230
+ } catch (error) {
1231
+ return mapCommitIntentError(error, resolved.taskId);
1232
+ }
1233
+ }
1234
+
1235
+ function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
1236
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1237
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1238
+ let gitRoot: string;
1239
+ try {
1240
+ gitRoot = gitRootFor(resolved.repoRoot, options);
1241
+ } catch (error) {
1242
+ return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
1243
+ }
1244
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1245
+ return {
1246
+ status: 'ready',
1247
+ changed: false,
1248
+ taskId: resolved.taskId,
1249
+ intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
1250
+ finalization: commitFinalizationView(inspection),
1251
+ error: null
1252
+ };
1253
+ }
1254
+
637
1255
  function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
638
1256
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
639
1257
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
@@ -649,6 +1267,18 @@ function prepareOrchestrationDelegationUnlocked(
649
1267
  ): OrchestrationResult {
650
1268
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
651
1269
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1270
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1271
+ try {
1272
+ readCommitIntent(resolved.taskDir, resolved.taskId);
1273
+ } catch (error) {
1274
+ return failed(
1275
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
1276
+ error instanceof Error ? error.message : String(error),
1277
+ resolved.taskId
1278
+ );
1279
+ }
1280
+ return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
1281
+ }
652
1282
  if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
653
1283
  return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
654
1284
  }
@@ -656,8 +1286,7 @@ function prepareOrchestrationDelegationUnlocked(
656
1286
  if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
657
1287
  if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
658
1288
  if (run.pendingDelegation) return failed('ORCHESTRATION_DELEGATION_BUSY', 'the run already has a pending delegation', resolved.taskId);
659
- const repositoryPending = (['claude-code', 'codex'] as AgentClientId[])
660
- .flatMap((client) => matchingDelegations(client, () => true, options));
1289
+ const repositoryPending = matchingDelegations(() => true, options);
661
1290
  if (repositoryPending.length > 0) {
662
1291
  return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
663
1292
  }
@@ -680,7 +1309,11 @@ function prepareOrchestrationDelegationUnlocked(
680
1309
  }
681
1310
  let beforeFingerprint: string;
682
1311
  try {
683
- beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
1312
+ beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1313
+ gitRoot: gitRootFor(resolved.repoRoot, options),
1314
+ stateRoot: resolved.repoRoot,
1315
+ taskId: resolved.taskId
1316
+ });
684
1317
  } catch (error) {
685
1318
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
686
1319
  }
@@ -718,9 +1351,14 @@ function prepareOrchestrationDelegation(
718
1351
  try {
719
1352
  return withTaskExecutionLock(
720
1353
  resolved.repoRoot,
721
- resolved.taskId,
722
- 'task-orchestration.prepare',
723
- () => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
1354
+ '__repository__',
1355
+ 'task-orchestration.prepare.repository',
1356
+ () => withTaskExecutionLock(
1357
+ resolved.repoRoot,
1358
+ resolved.taskId,
1359
+ 'task-orchestration.prepare.task',
1360
+ () => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
1361
+ )
724
1362
  );
725
1363
  } catch (error) {
726
1364
  if (error instanceof TaskExecutionLockError) return failed(error.code, error.message, resolved.taskId);
@@ -729,7 +1367,6 @@ function prepareOrchestrationDelegation(
729
1367
  }
730
1368
 
731
1369
  function matchingDelegations(
732
- client: AgentClientId,
733
1370
  predicate: (receipt: DelegationReceipt) => boolean,
734
1371
  options: OrchestrationOptions
735
1372
  ): Array<{ taskId: string; run: OrchestrationRun }> {
@@ -742,7 +1379,7 @@ function matchingDelegations(
742
1379
  const run = readRun(path.join(activeRoot, entry.name));
743
1380
  if (!run?.pendingDelegation) return [];
744
1381
  const receipt = run.pendingDelegation;
745
- return receipt.client === client && predicate(receipt)
1382
+ return predicate(receipt)
746
1383
  ? [{ taskId: entry.name, run }]
747
1384
  : [];
748
1385
  });
@@ -753,7 +1390,10 @@ function uniqueMatchingDelegation(
753
1390
  predicate: (receipt: DelegationReceipt) => boolean,
754
1391
  options: OrchestrationOptions
755
1392
  ): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
756
- const matches = matchingDelegations(client, predicate, options);
1393
+ const matches = matchingDelegations(
1394
+ (receipt) => receipt.client === client && predicate(receipt),
1395
+ options
1396
+ );
757
1397
  if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
758
1398
  if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
759
1399
  return matches[0]!;
@@ -797,9 +1437,14 @@ function sealMatchingOrchestrationDelegation(
797
1437
  }
798
1438
  const repoRoot = options.repoRoot ?? process.cwd();
799
1439
  try {
1440
+ const gitRoot = gitRootFor(repoRoot, options);
800
1441
  const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
801
- const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot, snapshotTaskId);
802
- const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(repoRoot, receipt.beforeFingerprint, afterFingerprint);
1442
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1443
+ gitRoot,
1444
+ stateRoot: repoRoot,
1445
+ taskId: snapshotTaskId
1446
+ });
1447
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
803
1448
  return sealOrchestrationDelegation(matched.taskId, {
804
1449
  childId: event.childId,
805
1450
  exitCode: 0,
@@ -901,7 +1546,7 @@ function planOrchestrationStageCompletion(
901
1546
  }
902
1547
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
903
1548
  if (!resolved.ok) return { result: failed(resolved.code, resolved.message, resolved.taskId), plan: null };
904
- const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt });
1549
+ const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt }, options.now);
905
1550
  return {
906
1551
  result: { status: 'running', changed: true, taskId: resolved.taskId, run: updatedRun, next: null, error: null },
907
1552
  plan: {
@@ -916,27 +1561,6 @@ function commitOrchestrationStageCompletion(plan: OrchestrationStageCompletion):
916
1561
  saveRun(plan.taskDir, plan.updatedRun);
917
1562
  }
918
1563
 
919
- function completeCommitOrchestrationStage(
920
- taskRef: string,
921
- agent: string,
922
- options: OrchestrationOptions = {}
923
- ): OrchestrationResult {
924
- const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
925
- if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
926
- const run = readRun(resolved.taskDir);
927
- if (!run) return { status: 'running', changed: false, taskId: resolved.taskId, run: null, next: null, error: null };
928
- const receipt = run.pendingDelegation;
929
- if (!receipt || receipt.stage !== 'commit') {
930
- return failed('ORCHESTRATION_DELEGATION_MISSING', 'active run has no pending commit delegation', resolved.taskId);
931
- }
932
- return completeOrchestrationStage(taskRef, {
933
- stage: receipt.stage,
934
- round: receipt.round,
935
- artifact: receipt.artifact,
936
- agent
937
- }, options);
938
- }
939
-
940
1564
  function sealOrchestrationDelegation(
941
1565
  taskRef: string,
942
1566
  event: Parameters<typeof sealDelegation>[1],
@@ -995,9 +1619,12 @@ export {
995
1619
  activateMatchingOrchestrationDelegation,
996
1620
  activateOrchestrationDelegation,
997
1621
  advanceOrchestration,
1622
+ abortCommitIntent,
1623
+ beginCommitIntent,
998
1624
  beginOrResumeOrchestration,
1625
+ checkpointCommitIntent,
999
1626
  commitOrchestrationStageCompletion,
1000
- completeCommitOrchestrationStage,
1627
+ completeCommitIntent,
1001
1628
  completeOrchestrationStage,
1002
1629
  inspectOrchestrationStage,
1003
1630
  orchestrationPath,
@@ -1005,12 +1632,17 @@ export {
1005
1632
  planOrchestrationStageCompletion,
1006
1633
  prepareOrchestrationDelegation,
1007
1634
  readRun,
1635
+ recoverCommitIntent,
1008
1636
  routeOrchestration,
1009
1637
  sealMatchingOrchestrationDelegation,
1010
1638
  sealOrchestrationDelegation,
1011
- statusOrchestration
1639
+ startCommitAttempt,
1640
+ statusCommitIntent,
1641
+ statusOrchestration,
1642
+ terminateCommitAttempt
1012
1643
  };
1013
1644
  export type {
1645
+ CommitIntentResult,
1014
1646
  OrchestrationCompletionPlanResult,
1015
1647
  OrchestrationModelPolicy,
1016
1648
  OrchestrationNext,