@fitlab-ai/agent-infra 0.9.3 → 0.9.4

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 (175) hide show
  1. package/README.md +2 -0
  2. package/README.zh-CN.md +2 -0
  3. package/bin/cli.ts +12 -0
  4. package/dist/bin/cli.js +14 -0
  5. package/dist/bin/internal-cli.js +134 -123
  6. package/dist/lib/agent-clients/adapter.js +92 -2
  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/opencode-sandbox.js +108 -0
  12. package/dist/lib/agent-clients/adapters/opencode.js +15 -3
  13. package/dist/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  14. package/dist/lib/agent-clients/registry.js +9 -1
  15. package/dist/lib/internal/sandbox-control.js +24 -0
  16. package/dist/lib/internal/task-activity.js +45 -13
  17. package/dist/lib/internal/task-ledger.js +12 -3
  18. package/dist/lib/internal/task-lifecycle.js +19 -1
  19. package/dist/lib/internal/task-orchestration.js +122 -18
  20. package/dist/lib/platform/github-client.js +19 -2
  21. package/dist/lib/platform/pr-checks.js +1 -1
  22. package/dist/lib/platform/pull-requests.js +47 -1
  23. package/dist/lib/platform/releases.js +13 -5
  24. package/dist/lib/process-data/index.js +239 -0
  25. package/dist/lib/process-data/normalize.js +198 -0
  26. package/dist/lib/process-data/privacy.js +33 -0
  27. package/dist/lib/process-data/reconcile.js +95 -0
  28. package/dist/lib/process-data/repair.js +58 -0
  29. package/dist/lib/process-data/sources.js +345 -0
  30. package/dist/lib/process-data/store.js +282 -0
  31. package/dist/lib/process-data/types.js +2 -0
  32. package/dist/lib/run/index.js +4 -3
  33. package/dist/lib/run/tui.js +7 -8
  34. package/dist/lib/sandbox/agent-client-reconciler.js +26 -2
  35. package/dist/lib/sandbox/capture.js +7 -2
  36. package/dist/lib/sandbox/commands/create.js +109 -228
  37. package/dist/lib/sandbox/commands/enter.js +15 -47
  38. package/dist/lib/sandbox/commands/list-running.js +16 -6
  39. package/dist/lib/sandbox/commands/ls.js +10 -6
  40. package/dist/lib/sandbox/commands/rebuild.js +4 -3
  41. package/dist/lib/sandbox/commands/refresh.js +4 -84
  42. package/dist/lib/sandbox/commands/rm.js +24 -3
  43. package/dist/lib/sandbox/commands/show.js +12 -3
  44. package/dist/lib/sandbox/commands/start.js +7 -4
  45. package/dist/lib/sandbox/config.js +2 -0
  46. package/dist/lib/sandbox/constants.js +6 -0
  47. package/dist/lib/sandbox/control/client.js +44 -0
  48. package/dist/lib/sandbox/control/protocol.js +35 -0
  49. package/dist/lib/sandbox/control/server.js +109 -0
  50. package/dist/lib/sandbox/dockerfile.js +36 -13
  51. package/dist/lib/sandbox/mounts.js +17 -4
  52. package/dist/lib/sandbox/recovery.js +128 -5
  53. package/dist/lib/sandbox/redaction.js +12 -0
  54. package/dist/lib/sandbox/runtimes/base.dockerfile +24 -74
  55. package/dist/lib/sandbox/workspace-identity.js +95 -0
  56. package/dist/lib/sandbox/workspace-view.js +103 -0
  57. package/dist/lib/task/activity-intent.js +257 -51
  58. package/dist/lib/task/commit-finalization.js +267 -0
  59. package/dist/lib/task/commit-intent.js +165 -0
  60. package/dist/lib/task/decision-intents.js +31 -17
  61. package/dist/lib/task/delegation-receipts.js +2 -2
  62. package/dist/lib/task/implementation-inputs.js +54 -7
  63. package/dist/lib/task/ledger-intents.js +35 -1
  64. package/dist/lib/task/orchestration.js +363 -28
  65. package/dist/lib/task/resolve-ref.js +41 -1
  66. package/lib/agent-clients/adapter.ts +126 -2
  67. package/lib/{sandbox/credentials.ts → agent-clients/adapters/claude-code-credentials.ts} +3 -18
  68. package/lib/agent-clients/adapters/claude-code-refresh.ts +128 -0
  69. package/lib/agent-clients/adapters/claude-code-sandbox.ts +295 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +21 -3
  71. package/lib/agent-clients/adapters/opencode-sandbox.ts +136 -0
  72. package/lib/agent-clients/adapters/opencode.ts +18 -3
  73. package/lib/agent-clients/adapters/runtimes/claude-code.dockerfile +65 -0
  74. package/lib/agent-clients/registry.ts +9 -1
  75. package/lib/internal/sandbox-control.ts +24 -0
  76. package/lib/internal/task-activity.ts +50 -14
  77. package/lib/internal/task-ledger.ts +12 -3
  78. package/lib/internal/task-lifecycle.ts +22 -1
  79. package/lib/internal/task-orchestration.ts +81 -11
  80. package/lib/platform/github-client.ts +20 -2
  81. package/lib/platform/pr-checks.ts +1 -1
  82. package/lib/platform/pull-requests.ts +49 -1
  83. package/lib/platform/releases.ts +16 -4
  84. package/lib/process-data/index.ts +224 -0
  85. package/lib/process-data/normalize.ts +202 -0
  86. package/lib/process-data/privacy.ts +41 -0
  87. package/lib/process-data/reconcile.ts +110 -0
  88. package/lib/process-data/repair.ts +64 -0
  89. package/lib/process-data/sources.ts +354 -0
  90. package/lib/process-data/store.ts +315 -0
  91. package/lib/process-data/types.ts +135 -0
  92. package/lib/run/index.ts +5 -3
  93. package/lib/run/tui.ts +11 -8
  94. package/lib/sandbox/agent-client-reconciler.ts +44 -2
  95. package/lib/sandbox/capture.ts +9 -2
  96. package/lib/sandbox/commands/create.ts +126 -260
  97. package/lib/sandbox/commands/enter.ts +13 -59
  98. package/lib/sandbox/commands/list-running.ts +23 -6
  99. package/lib/sandbox/commands/ls.ts +22 -6
  100. package/lib/sandbox/commands/rebuild.ts +4 -3
  101. package/lib/sandbox/commands/refresh.ts +4 -126
  102. package/lib/sandbox/commands/rm.ts +24 -3
  103. package/lib/sandbox/commands/show.ts +29 -3
  104. package/lib/sandbox/commands/start.ts +6 -4
  105. package/lib/sandbox/config.ts +4 -0
  106. package/lib/sandbox/constants.ts +8 -0
  107. package/lib/sandbox/control/client.ts +55 -0
  108. package/lib/sandbox/control/protocol.ts +75 -0
  109. package/lib/sandbox/control/server.ts +117 -0
  110. package/lib/sandbox/dockerfile.ts +66 -13
  111. package/lib/sandbox/mounts.ts +24 -4
  112. package/lib/sandbox/recovery.ts +163 -6
  113. package/lib/sandbox/redaction.ts +14 -0
  114. package/lib/sandbox/runtimes/base.dockerfile +24 -74
  115. package/lib/sandbox/tool-types.ts +16 -0
  116. package/lib/sandbox/workspace-identity.ts +123 -0
  117. package/lib/sandbox/workspace-view.ts +143 -0
  118. package/lib/task/activity-intent.ts +350 -61
  119. package/lib/task/commit-finalization.ts +356 -0
  120. package/lib/task/commit-intent.ts +230 -0
  121. package/lib/task/decision-intents.ts +32 -12
  122. package/lib/task/delegation-receipts.ts +2 -2
  123. package/lib/task/implementation-inputs.ts +62 -8
  124. package/lib/task/ledger-intents.ts +43 -3
  125. package/lib/task/orchestration.ts +501 -33
  126. package/lib/task/resolve-ref.ts +46 -1
  127. package/package.json +1 -1
  128. package/templates/.agents/rules/README.en.md +1 -1
  129. package/templates/.agents/rules/README.zh-CN.md +1 -1
  130. package/templates/.agents/rules/human-decision-context.en.md +11 -2
  131. package/templates/.agents/rules/human-decision-context.zh-CN.md +11 -2
  132. package/templates/.agents/rules/pr-checks-commands.en.md +1 -1
  133. package/templates/.agents/rules/pr-checks-commands.github.en.md +1 -1
  134. package/templates/.agents/rules/pr-checks-commands.github.zh-CN.md +2 -2
  135. package/templates/.agents/rules/pr-checks-commands.zh-CN.md +1 -1
  136. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  137. package/templates/.agents/rules/task-management.en.md +2 -0
  138. package/templates/.agents/rules/task-management.zh-CN.md +2 -0
  139. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  140. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +1 -1
  141. package/templates/.agents/skills/commit/SKILL.en.md +21 -11
  142. package/templates/.agents/skills/commit/SKILL.zh-CN.md +27 -11
  143. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +49 -0
  144. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +55 -0
  145. package/templates/.agents/skills/commit/reference/task-status-update.en.md +4 -4
  146. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +4 -4
  147. package/templates/.agents/skills/complete-task/SKILL.en.md +1 -1
  148. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +1 -1
  149. package/templates/.agents/skills/create-pr/SKILL.en.md +4 -4
  150. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +4 -4
  151. package/templates/.agents/skills/create-task/reference/context-capture.en.md +34 -0
  152. package/templates/.agents/skills/create-task/reference/context-capture.zh-CN.md +34 -0
  153. package/templates/.agents/skills/review-code/SKILL.en.md +2 -2
  154. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +2 -2
  155. package/templates/.agents/skills/review-code/reference/output-templates.en.md +1 -1
  156. package/templates/.agents/skills/review-code/reference/output-templates.zh-CN.md +1 -1
  157. package/templates/.agents/skills/review-pr/SKILL.en.md +36 -8
  158. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +36 -8
  159. package/templates/.agents/skills/review-pr/reference/report-template.en.md +1 -1
  160. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +1 -1
  161. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +67 -50
  162. package/templates/.agents/skills/watch-pr/SKILL.en.md +4 -4
  163. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +4 -4
  164. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +2 -2
  165. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +2 -2
  166. package/templates/.agents/templates/task.en.md +1 -1
  167. package/templates/.agents/templates/task.zh-CN.md +1 -1
  168. package/templates/.claude/commands/watch-pr.en.md +1 -1
  169. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  170. package/templates/.claude/rules/tool-preferences.en.md +20 -0
  171. package/templates/.claude/rules/tool-preferences.zh-CN.md +20 -0
  172. package/templates/.opencode/commands/watch-pr.en.md +1 -1
  173. package/templates/.opencode/commands/watch-pr.zh-CN.md +1 -1
  174. package/templates/.gemini/commands/_project_/review-pr.en.toml +0 -10
  175. 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';
@@ -36,6 +37,24 @@ import {
36
37
  } from './workspace-snapshot.ts';
37
38
  import type { RepositorySnapshot } from './workspace-snapshot.ts';
38
39
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
40
+ import {
41
+ CommitIntentError,
42
+ commitIntentPath,
43
+ createCommitIntent,
44
+ digest,
45
+ readCommitIntent,
46
+ removeCommitIntent,
47
+ removeCommitIntentByDigest,
48
+ serialize,
49
+ updateCommitIntent
50
+ } from './commit-intent.ts';
51
+ import type { CommitIntent, PushEvidence } from './commit-intent.ts';
52
+ import {
53
+ inspectCommitFinalization,
54
+ planCommitTaskFinalization
55
+ } from './commit-finalization.ts';
56
+ import type { CommitFinalizationInspection } from './commit-finalization.ts';
57
+ import { captureTaskWriteMetadata, writeTask } from './write.ts';
39
58
 
40
59
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
41
60
  type LegacyOrchestrationModelPolicy = Readonly<{
@@ -144,8 +163,36 @@ type OrchestrationOptions = {
144
163
  }>;
145
164
  diffWorkspace?: (repoRoot: string, before: string, after: string) => string[];
146
165
  supportsLifecycleDelegation?: (client: AgentClientId) => boolean;
166
+ token?: () => string;
147
167
  };
148
168
 
169
+ type CommitIntentResult = Readonly<{
170
+ status: 'ready' | 'failed';
171
+ changed: boolean;
172
+ taskId: string | null;
173
+ intent: Readonly<{
174
+ mode: CommitIntent['mode'];
175
+ phase: CommitIntent['phase'];
176
+ baselineHead: string;
177
+ currentHead: string;
178
+ committedHead: string | null;
179
+ pushEvidence: PushEvidence | null;
180
+ runId: string | null;
181
+ receiptId: string | null;
182
+ }> | null;
183
+ finalization?: Readonly<{
184
+ disposition: CommitFinalizationInspection['disposition'];
185
+ code: CommitFinalizationInspection['code'];
186
+ message: string;
187
+ currentHead: string;
188
+ committedHead: string | null;
189
+ needsAnchor: boolean;
190
+ needsLog: boolean;
191
+ }>;
192
+ token?: string;
193
+ error: Readonly<{ code: string; message: string }> | null;
194
+ }>;
195
+
149
196
  function supportsLifecycleDelegation(client: AgentClientId): boolean {
150
197
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
151
198
  && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
@@ -187,8 +234,8 @@ function saveRun(taskDir: string, run: OrchestrationRun): void {
187
234
  atomicWrite(orchestrationPath(taskDir), run);
188
235
  }
189
236
 
190
- function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>): OrchestrationRun {
191
- return Object.freeze({ ...run, ...updates, updatedAt: new Date().toISOString() });
237
+ function withUpdatedRun(run: OrchestrationRun, updates: Partial<OrchestrationRun>, now?: () => string): OrchestrationRun {
238
+ return Object.freeze({ ...run, ...updates, updatedAt: (now ?? (() => new Date().toISOString()))() });
192
239
  }
193
240
 
194
241
  function failed(
@@ -200,6 +247,73 @@ function failed(
200
247
  return { status: 'failed', changed: false, taskId, run: null, next: null, error: { code, message, ...details } };
201
248
  }
202
249
 
250
+ function commitIntentFailed(code: string, message: string, taskId: string | null = null): CommitIntentResult {
251
+ return { status: 'failed', changed: false, taskId, intent: null, error: { code, message } };
252
+ }
253
+
254
+ function repositoryHead(repoRoot: string): string {
255
+ return execFileSync('git', ['rev-parse', 'HEAD'], {
256
+ cwd: repoRoot,
257
+ encoding: 'utf8',
258
+ stdio: ['ignore', 'pipe', 'pipe']
259
+ }).trim();
260
+ }
261
+
262
+ function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
263
+ return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
264
+ cwd: repoRoot,
265
+ stdio: 'ignore'
266
+ }).status === 0;
267
+ }
268
+
269
+ function commitIntentView(intent: CommitIntent, currentHead: string): NonNullable<CommitIntentResult['intent']> {
270
+ return {
271
+ mode: intent.mode,
272
+ phase: intent.phase,
273
+ baselineHead: intent.baselineHead,
274
+ currentHead,
275
+ committedHead: intent.committedHead,
276
+ pushEvidence: intent.pushEvidence,
277
+ runId: intent.orchestration?.runId ?? null,
278
+ receiptId: intent.orchestration?.receiptId ?? null
279
+ };
280
+ }
281
+
282
+ function commitFinalizationView(inspection: CommitFinalizationInspection): NonNullable<CommitIntentResult['finalization']> {
283
+ return {
284
+ disposition: inspection.disposition,
285
+ code: inspection.code,
286
+ message: inspection.message,
287
+ currentHead: inspection.currentHead,
288
+ committedHead: inspection.committedHead,
289
+ needsAnchor: inspection.needsAnchor,
290
+ needsLog: inspection.needsLog
291
+ };
292
+ }
293
+
294
+ function validCommitRun(run: OrchestrationRun | null, taskId: string): run is OrchestrationRun {
295
+ return run !== null
296
+ && [1, 2].includes(run.schemaVersion)
297
+ && run.taskId === taskId
298
+ && ['running', 'paused', 'completed'].includes(run.status)
299
+ && Array.isArray(run.receipts)
300
+ && Object.prototype.hasOwnProperty.call(run, 'pendingDelegation')
301
+ && typeof run.commitAuthorization === 'object'
302
+ && run.commitAuthorization !== null
303
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'issuedAt')
304
+ && Object.prototype.hasOwnProperty.call(run.commitAuthorization, 'consumedAt');
305
+ }
306
+
307
+ function mapCommitIntentError(error: unknown, taskId: string | null): CommitIntentResult {
308
+ if (error instanceof CommitIntentError) return commitIntentFailed(error.code, error.message, taskId);
309
+ if (error instanceof TaskExecutionLockError) return commitIntentFailed(error.code, error.message, taskId);
310
+ return commitIntentFailed(
311
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
312
+ error instanceof Error ? error.message : String(error),
313
+ taskId
314
+ );
315
+ }
316
+
203
317
  function validModel(value: unknown): value is string {
204
318
  return typeof value === 'string' && value.length > 0 && value.trim() === value;
205
319
  }
@@ -634,6 +748,357 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
634
748
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
635
749
  }
636
750
 
751
+ function beginCommitIntent(
752
+ taskRef: string,
753
+ input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string }>,
754
+ options: OrchestrationOptions = {}
755
+ ): CommitIntentResult {
756
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
757
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
758
+ try {
759
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
760
+ const currentHead = repositoryHead(resolved.repoRoot);
761
+ if (currentHead !== input.baselineHead) {
762
+ return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
763
+ }
764
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
765
+ try {
766
+ readCommitIntent(resolved.taskDir, resolved.taskId);
767
+ } catch (error) {
768
+ return mapCommitIntentError(error, resolved.taskId);
769
+ }
770
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
771
+ }
772
+
773
+ const runFile = orchestrationPath(resolved.taskDir);
774
+ let run: OrchestrationRun | null = null;
775
+ let sourceRunBytes: string | null = null;
776
+ if (fs.existsSync(runFile)) {
777
+ sourceRunBytes = fs.readFileSync(runFile, 'utf8');
778
+ try {
779
+ run = readRun(resolved.taskDir);
780
+ } catch (error) {
781
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
782
+ }
783
+ if (!validCommitRun(run, resolved.taskId)) {
784
+ return commitIntentFailed('ORCHESTRATION_STATE_INVALID', 'orchestration state has an invalid schema', resolved.taskId);
785
+ }
786
+ }
787
+
788
+ const now = (options.now ?? (() => new Date().toISOString()))();
789
+ const failAndPause = (code: string, message: string): CommitIntentResult => {
790
+ if (run?.status === 'running') {
791
+ saveRun(resolved.taskDir, withUpdatedRun(run, {
792
+ status: 'paused', pause: { code, message, recoverable: true }
793
+ }, () => now));
794
+ }
795
+ return commitIntentFailed(code, message, resolved.taskId);
796
+ };
797
+ let orchestration: CommitIntent['orchestration'] = null;
798
+ if (!input.orchestrated) {
799
+ if (run?.pendingDelegation) {
800
+ return commitIntentFailed(
801
+ 'ORCHESTRATION_STANDALONE_BUSY',
802
+ 'standalone commit is blocked by a pending orchestration delegation',
803
+ resolved.taskId
804
+ );
805
+ }
806
+ } else {
807
+ if (!run || run.status !== 'running') {
808
+ return commitIntentFailed('ORCHESTRATION_RUN_NOT_RUNNING', 'orchestrated commit requires a running orchestration', resolved.taskId);
809
+ }
810
+ const receipt = run.pendingDelegation;
811
+ if (
812
+ !receipt
813
+ || receipt.status !== 'activated'
814
+ || receipt.stage !== 'commit'
815
+ || receipt.round !== 1
816
+ || receipt.artifact !== 'commit'
817
+ || receipt.role !== 'executor'
818
+ ) {
819
+ return failAndPause(
820
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
821
+ 'orchestrated commit requires one matching activated commit delegation'
822
+ );
823
+ }
824
+ if (!run.commitAuthorization.issuedAt || run.commitAuthorization.consumedAt) {
825
+ return failAndPause(
826
+ 'ORCHESTRATION_COMMIT_AUTHORIZATION_INVALID',
827
+ 'orchestrated commit requires an unconsumed one-use authorization'
828
+ );
829
+ }
830
+ const planned = planOrchestrationStageCompletion(taskRef, {
831
+ stage: 'commit', round: 1, artifact: 'commit', role: 'executor', agent: input.agent
832
+ }, { ...options, now: () => now });
833
+ if (planned.result.status === 'failed' || !planned.plan?.updatedRun.pendingDelegation) {
834
+ return failAndPause(
835
+ planned.result.error?.code ?? 'ORCHESTRATION_PROVENANCE_MISMATCH',
836
+ planned.result.error?.message ?? 'commit delegation could not be completed'
837
+ );
838
+ }
839
+ const plannedRunBytes = serialize(planned.plan.updatedRun);
840
+ orchestration = {
841
+ runId: run.runId,
842
+ receiptId: receipt.id,
843
+ authorizationIssuedAt: run.commitAuthorization.issuedAt,
844
+ sourceRunDigest: digest(sourceRunBytes!),
845
+ plannedRunDigest: digest(plannedRunBytes),
846
+ completionUpdatedAt: planned.plan.updatedRun.updatedAt,
847
+ plannedReceipt: planned.plan.updatedRun.pendingDelegation
848
+ };
849
+ }
850
+
851
+ const created = createCommitIntent(resolved.taskDir, {
852
+ taskId: resolved.taskId,
853
+ mode: input.orchestrated ? 'orchestrated' : 'standalone',
854
+ phase: 'prepared',
855
+ baselineHead: input.baselineHead,
856
+ committedHead: null,
857
+ pushEvidence: null,
858
+ orchestration,
859
+ createdAt: now,
860
+ updatedAt: now
861
+ }, { token: options.token });
862
+ return {
863
+ status: 'ready', changed: true, taskId: resolved.taskId,
864
+ intent: commitIntentView(created.intent, currentHead), token: created.token, error: null
865
+ };
866
+ });
867
+ } catch (error) {
868
+ return mapCommitIntentError(error, resolved.taskId);
869
+ }
870
+ }
871
+
872
+ function checkpointCommitIntent(
873
+ taskRef: string,
874
+ input: Readonly<{
875
+ token: string;
876
+ kind: 'committed' | 'pushed';
877
+ head: string;
878
+ remote?: string;
879
+ ref?: string;
880
+ }>,
881
+ options: OrchestrationOptions = {}
882
+ ): CommitIntentResult {
883
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
884
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
885
+ try {
886
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
887
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
888
+ const currentHead = repositoryHead(resolved.repoRoot);
889
+ if (currentHead !== input.head) {
890
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
891
+ }
892
+ if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
893
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
894
+ }
895
+ if (input.kind === 'pushed') {
896
+ const expectedHead = intent.committedHead ?? intent.baselineHead;
897
+ if (input.head !== expectedHead || !input.remote || !input.ref) {
898
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_STATE_INVALID', 'pushed checkpoint requires matching remote, ref and HEAD', resolved.taskId);
899
+ }
900
+ }
901
+ const now = (options.now ?? (() => new Date().toISOString()))();
902
+ const updated = updateCommitIntent(resolved.taskDir, resolved.taskId, input.token, input.kind === 'committed'
903
+ ? { phase: 'committed', committedHead: input.head, updatedAt: now }
904
+ : {
905
+ phase: 'pushed', committedHead: intent.committedHead ?? intent.baselineHead, updatedAt: now,
906
+ pushEvidence: { remote: input.remote!, ref: input.ref!, head: input.head }
907
+ });
908
+ return {
909
+ status: 'ready', changed: true, taskId: resolved.taskId,
910
+ intent: commitIntentView(updated, currentHead), error: null
911
+ };
912
+ });
913
+ } catch (error) {
914
+ return mapCommitIntentError(error, resolved.taskId);
915
+ }
916
+ }
917
+
918
+ function completeCommitOrchestration(
919
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
920
+ intent: CommitIntent,
921
+ agent: string
922
+ ): Readonly<{ code: string; message: string }> | null {
923
+ if (intent.orchestration === null) return null;
924
+ if (intent.orchestration.plannedReceipt.agent !== agent) {
925
+ return { code: 'ORCHESTRATION_PROVENANCE_MISMATCH', message: 'completion agent does not match the planned receipt' };
926
+ }
927
+ const runFile = orchestrationPath(resolved.taskDir);
928
+ if (!fs.existsSync(runFile)) {
929
+ return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
930
+ }
931
+ const currentBytes = fs.readFileSync(runFile, 'utf8');
932
+ const currentDigest = digest(currentBytes);
933
+ if (currentDigest === intent.orchestration.sourceRunDigest) {
934
+ const run = readRun(resolved.taskDir);
935
+ if (!run) return { code: 'ORCHESTRATION_RUN_MISSING', message: 'orchestration run disappeared' };
936
+ const plannedRun: OrchestrationRun = Object.freeze({
937
+ ...run,
938
+ pendingDelegation: intent.orchestration.plannedReceipt,
939
+ updatedAt: intent.orchestration.completionUpdatedAt
940
+ });
941
+ if (digest(serialize(plannedRun)) !== intent.orchestration.plannedRunDigest) {
942
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'planned orchestration bytes no longer match the intent' };
943
+ }
944
+ atomicWrite(runFile, plannedRun);
945
+ if (digest(fs.readFileSync(runFile)) !== intent.orchestration.plannedRunDigest) {
946
+ return { code: 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL', message: 'orchestration completion could not be verified' };
947
+ }
948
+ } else if (currentDigest !== intent.orchestration.plannedRunDigest) {
949
+ return { code: 'ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', message: 'orchestration state changed after commit begin' };
950
+ }
951
+ return null;
952
+ }
953
+
954
+ function finalizeCommitIntentUnlocked(
955
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
956
+ inspection: CommitFinalizationInspection,
957
+ agent: string,
958
+ remove: () => void
959
+ ): CommitIntentResult {
960
+ if (inspection.disposition !== 'recoverable' || !inspection.intent) {
961
+ return commitIntentFailed(
962
+ inspection.code ?? 'COMMIT_FINALIZATION_PENDING',
963
+ inspection.message,
964
+ resolved.taskId
965
+ );
966
+ }
967
+ if (
968
+ inspection.intent.orchestration !== null
969
+ && inspection.intent.orchestration.plannedReceipt.agent !== agent
970
+ ) {
971
+ return commitIntentFailed(
972
+ 'ORCHESTRATION_PROVENANCE_MISMATCH',
973
+ 'completion agent does not match the planned receipt',
974
+ resolved.taskId
975
+ );
976
+ }
977
+ const metadata = captureTaskWriteMetadata();
978
+ const taskPlan = planCommitTaskFinalization(
979
+ resolved.taskDir,
980
+ inspection,
981
+ agent,
982
+ metadata.timestamp
983
+ );
984
+ const written = writeTask({
985
+ taskRef: resolved.taskId,
986
+ expectedState: 'active',
987
+ mutations: taskPlan.mutations
988
+ }, {
989
+ repoRoot: resolved.repoRoot,
990
+ metadataProvider: () => metadata
991
+ });
992
+ if (written.status === 'failed') {
993
+ return commitIntentFailed(written.error.code, written.error.message, resolved.taskId);
994
+ }
995
+ const orchestrationError = completeCommitOrchestration(resolved, inspection.intent, agent);
996
+ if (orchestrationError) {
997
+ return commitIntentFailed(orchestrationError.code, orchestrationError.message, resolved.taskId);
998
+ }
999
+ try {
1000
+ remove();
1001
+ } catch (error) {
1002
+ return commitIntentFailed(
1003
+ 'ORCHESTRATION_COMMIT_COMPLETE_PARTIAL',
1004
+ error instanceof Error ? error.message : String(error),
1005
+ resolved.taskId
1006
+ );
1007
+ }
1008
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1009
+ }
1010
+
1011
+ function completeCommitIntent(
1012
+ taskRef: string,
1013
+ input: Readonly<{ token: string; agent: string }>,
1014
+ options: OrchestrationOptions = {}
1015
+ ): CommitIntentResult {
1016
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1017
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1018
+ try {
1019
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
1020
+ readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1021
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1022
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1023
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1024
+ });
1025
+ });
1026
+ } catch (error) {
1027
+ return mapCommitIntentError(error, resolved.taskId);
1028
+ }
1029
+ }
1030
+
1031
+ function recoverCommitIntent(
1032
+ taskRef: string,
1033
+ input: Readonly<{ agent: string }>,
1034
+ options: OrchestrationOptions = {}
1035
+ ): CommitIntentResult {
1036
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1037
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1038
+ try {
1039
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
1040
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1041
+ if (inspection.disposition === 'prepared' && inspection.intentDigest) {
1042
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
1043
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1044
+ }
1045
+ if (!inspection.intentDigest) {
1046
+ return commitIntentFailed(
1047
+ inspection.code ?? 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
1048
+ inspection.message,
1049
+ resolved.taskId
1050
+ );
1051
+ }
1052
+ return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1053
+ removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest!);
1054
+ });
1055
+ });
1056
+ } catch (error) {
1057
+ return mapCommitIntentError(error, resolved.taskId);
1058
+ }
1059
+ }
1060
+
1061
+ function abortCommitIntent(
1062
+ taskRef: string,
1063
+ input: Readonly<{ token: string; expectedHead: string }>,
1064
+ options: OrchestrationOptions = {}
1065
+ ): CommitIntentResult {
1066
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1067
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1068
+ try {
1069
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
1070
+ const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1071
+ const currentHead = repositoryHead(resolved.repoRoot);
1072
+ if (
1073
+ intent.phase !== 'prepared'
1074
+ || intent.pushEvidence !== null
1075
+ || input.expectedHead !== intent.baselineHead
1076
+ || currentHead !== intent.baselineHead
1077
+ ) {
1078
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit intent has side effects or repository drift', resolved.taskId);
1079
+ }
1080
+ removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1081
+ return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
1082
+ });
1083
+ } catch (error) {
1084
+ return mapCommitIntentError(error, resolved.taskId);
1085
+ }
1086
+ }
1087
+
1088
+ function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
1089
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1090
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1091
+ const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1092
+ return {
1093
+ status: 'ready',
1094
+ changed: false,
1095
+ taskId: resolved.taskId,
1096
+ intent: inspection.intent ? commitIntentView(inspection.intent, inspection.currentHead) : null,
1097
+ finalization: commitFinalizationView(inspection),
1098
+ error: null
1099
+ };
1100
+ }
1101
+
637
1102
  function statusOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
638
1103
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
639
1104
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
@@ -649,6 +1114,18 @@ function prepareOrchestrationDelegationUnlocked(
649
1114
  ): OrchestrationResult {
650
1115
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
651
1116
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1117
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
1118
+ try {
1119
+ readCommitIntent(resolved.taskDir, resolved.taskId);
1120
+ } catch (error) {
1121
+ return failed(
1122
+ 'ORCHESTRATION_COMMIT_INTENT_INVALID',
1123
+ error instanceof Error ? error.message : String(error),
1124
+ resolved.taskId
1125
+ );
1126
+ }
1127
+ return failed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent blocks delegation preparation', resolved.taskId);
1128
+ }
652
1129
  if (!(options.supportsLifecycleDelegation ?? supportsLifecycleDelegation)(input.client)) {
653
1130
  return failed('ORCHESTRATION_CLIENT_UNSUPPORTED', `client '${input.client}' does not support lifecycle orchestration`, resolved.taskId);
654
1131
  }
@@ -656,8 +1133,7 @@ function prepareOrchestrationDelegationUnlocked(
656
1133
  if (!run || run.status !== 'running') return failed('ORCHESTRATION_RUN_NOT_RUNNING', 'a running orchestration is required', resolved.taskId);
657
1134
  if (!isV2Policy(run.modelPolicy)) return failed('ORCHESTRATION_MODEL_EVIDENCE_MISSING', 'running orchestration has no persisted model policy', resolved.taskId);
658
1135
  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));
1136
+ const repositoryPending = matchingDelegations(() => true, options);
661
1137
  if (repositoryPending.length > 0) {
662
1138
  return failed('ORCHESTRATION_DELEGATION_BUSY', 'the repository already has a pending lifecycle delegation', resolved.taskId);
663
1139
  }
@@ -718,9 +1194,14 @@ function prepareOrchestrationDelegation(
718
1194
  try {
719
1195
  return withTaskExecutionLock(
720
1196
  resolved.repoRoot,
721
- resolved.taskId,
722
- 'task-orchestration.prepare',
723
- () => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
1197
+ '__repository__',
1198
+ 'task-orchestration.prepare.repository',
1199
+ () => withTaskExecutionLock(
1200
+ resolved.repoRoot,
1201
+ resolved.taskId,
1202
+ 'task-orchestration.prepare.task',
1203
+ () => prepareOrchestrationDelegationUnlocked(taskRef, input, options)
1204
+ )
724
1205
  );
725
1206
  } catch (error) {
726
1207
  if (error instanceof TaskExecutionLockError) return failed(error.code, error.message, resolved.taskId);
@@ -729,7 +1210,6 @@ function prepareOrchestrationDelegation(
729
1210
  }
730
1211
 
731
1212
  function matchingDelegations(
732
- client: AgentClientId,
733
1213
  predicate: (receipt: DelegationReceipt) => boolean,
734
1214
  options: OrchestrationOptions
735
1215
  ): Array<{ taskId: string; run: OrchestrationRun }> {
@@ -742,7 +1222,7 @@ function matchingDelegations(
742
1222
  const run = readRun(path.join(activeRoot, entry.name));
743
1223
  if (!run?.pendingDelegation) return [];
744
1224
  const receipt = run.pendingDelegation;
745
- return receipt.client === client && predicate(receipt)
1225
+ return predicate(receipt)
746
1226
  ? [{ taskId: entry.name, run }]
747
1227
  : [];
748
1228
  });
@@ -753,7 +1233,10 @@ function uniqueMatchingDelegation(
753
1233
  predicate: (receipt: DelegationReceipt) => boolean,
754
1234
  options: OrchestrationOptions
755
1235
  ): { taskId: string; run: OrchestrationRun } | OrchestrationResult {
756
- const matches = matchingDelegations(client, predicate, options);
1236
+ const matches = matchingDelegations(
1237
+ (receipt) => receipt.client === client && predicate(receipt),
1238
+ options
1239
+ );
757
1240
  if (matches.length === 0) return failed('ORCHESTRATION_DELEGATION_MISSING', 'no matching lifecycle delegation exists');
758
1241
  if (matches.length > 1) return failed('ORCHESTRATION_DELEGATION_AMBIGUOUS', 'multiple lifecycle delegations match the native hook event');
759
1242
  return matches[0]!;
@@ -901,7 +1384,7 @@ function planOrchestrationStageCompletion(
901
1384
  }
902
1385
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
903
1386
  if (!resolved.ok) return { result: failed(resolved.code, resolved.message, resolved.taskId), plan: null };
904
- const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt });
1387
+ const updatedRun = withUpdatedRun(inspected.run, { pendingDelegation: completed.receipt }, options.now);
905
1388
  return {
906
1389
  result: { status: 'running', changed: true, taskId: resolved.taskId, run: updatedRun, next: null, error: null },
907
1390
  plan: {
@@ -916,27 +1399,6 @@ function commitOrchestrationStageCompletion(plan: OrchestrationStageCompletion):
916
1399
  saveRun(plan.taskDir, plan.updatedRun);
917
1400
  }
918
1401
 
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
1402
  function sealOrchestrationDelegation(
941
1403
  taskRef: string,
942
1404
  event: Parameters<typeof sealDelegation>[1],
@@ -995,9 +1457,12 @@ export {
995
1457
  activateMatchingOrchestrationDelegation,
996
1458
  activateOrchestrationDelegation,
997
1459
  advanceOrchestration,
1460
+ abortCommitIntent,
1461
+ beginCommitIntent,
998
1462
  beginOrResumeOrchestration,
1463
+ checkpointCommitIntent,
999
1464
  commitOrchestrationStageCompletion,
1000
- completeCommitOrchestrationStage,
1465
+ completeCommitIntent,
1001
1466
  completeOrchestrationStage,
1002
1467
  inspectOrchestrationStage,
1003
1468
  orchestrationPath,
@@ -1005,12 +1470,15 @@ export {
1005
1470
  planOrchestrationStageCompletion,
1006
1471
  prepareOrchestrationDelegation,
1007
1472
  readRun,
1473
+ recoverCommitIntent,
1008
1474
  routeOrchestration,
1009
1475
  sealMatchingOrchestrationDelegation,
1010
1476
  sealOrchestrationDelegation,
1477
+ statusCommitIntent,
1011
1478
  statusOrchestration
1012
1479
  };
1013
1480
  export type {
1481
+ CommitIntentResult,
1014
1482
  OrchestrationCompletionPlanResult,
1015
1483
  OrchestrationModelPolicy,
1016
1484
  OrchestrationNext,