@fitlab-ai/agent-infra 0.9.4 → 0.9.6

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 (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -36,6 +36,8 @@ import {
36
36
  diffWorkspaceSnapshots
37
37
  } from './workspace-snapshot.ts';
38
38
  import type { RepositorySnapshot } from './workspace-snapshot.ts';
39
+ import type { WorkspaceSnapshotContext } from './workspace-snapshot.ts';
40
+ import { assertGitRepositoryBinding } from '../git/worktree-identity.ts';
39
41
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.ts';
40
42
  import {
41
43
  CommitIntentError,
@@ -55,6 +57,12 @@ import {
55
57
  } from './commit-finalization.ts';
56
58
  import type { CommitFinalizationInspection } from './commit-finalization.ts';
57
59
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
60
+ import {
61
+ appendActivityEntry,
62
+ commitAttemptStartedNote,
63
+ locateActivityLog,
64
+ pairEntries
65
+ } from './activity-log.ts';
58
66
 
59
67
  type OrchestrationStatus = 'running' | 'paused' | 'completed';
60
68
  type LegacyOrchestrationModelPolicy = Readonly<{
@@ -66,7 +74,7 @@ type ModelPolicySource = Readonly<{
66
74
  client: AgentClientId;
67
75
  resolvedAt: string;
68
76
  }>;
69
- type OrchestrationRecovery = Readonly<{
77
+ type ModelPolicyRecovery = Readonly<{
70
78
  code: 'MODEL_POLICY_SUPPLEMENTED';
71
79
  recoveredAt: string;
72
80
  previousSchemaVersion: 1;
@@ -77,6 +85,26 @@ type OrchestrationRecovery = Readonly<{
77
85
  pendingDelegation: false;
78
86
  resultingStatus: OrchestrationStatus;
79
87
  }>;
88
+ type ClientCapabilityRecovery = Readonly<{
89
+ code: 'CLIENT_CAPABILITY_ENABLED';
90
+ recoveredAt: string;
91
+ previousSchemaVersion: 2;
92
+ previousStatus: 'paused';
93
+ previousPause: Readonly<{ code: 'ORCHESTRATION_CLIENT_UNSUPPORTED'; message: string; recoverable: boolean }>;
94
+ client: 'codex';
95
+ guards: Readonly<{
96
+ stepCount: 0;
97
+ nextStage: null;
98
+ baselineEmpty: true;
99
+ receiptCount: 0;
100
+ pendingDelegation: false;
101
+ commitAuthorizationUnused: true;
102
+ completionEvidenceAbsent: true;
103
+ commitIntentAbsent: true;
104
+ }>;
105
+ resultingStatus: 'running';
106
+ }>;
107
+ type OrchestrationRecovery = ModelPolicyRecovery | ClientCapabilityRecovery;
80
108
  type CleanCompletionEvidence = Readonly<{
81
109
  kind: 'reviewed-head-clean';
82
110
  observedAt: string;
@@ -147,12 +175,13 @@ type OrchestrationCompletionPlanResult = Readonly<{
147
175
  }>;
148
176
  type OrchestrationOptions = {
149
177
  repoRoot?: string;
178
+ gitWorktreeRoot?: string;
150
179
  id?: () => string;
151
180
  now?: () => string;
152
181
  maxSteps?: number;
153
182
  client?: AgentClientId;
154
183
  modelPolicy?: OrchestrationModelPolicy;
155
- captureWorkspace?: (repoRoot: string, taskId: string | null) => string;
184
+ captureWorkspace?: (context: WorkspaceSnapshotContext) => string;
156
185
  captureRepository?: (repoRoot: string) => RepositorySnapshot;
157
186
  inspectPullRequest?: (taskId: string, options: { cwd: string }) => Readonly<{
158
187
  status: string;
@@ -188,6 +217,7 @@ type CommitIntentResult = Readonly<{
188
217
  committedHead: string | null;
189
218
  needsAnchor: boolean;
190
219
  needsLog: boolean;
220
+ attempt: Readonly<{ attempt: string; baseline: string; agent: string }> | null;
191
221
  }>;
192
222
  token?: string;
193
223
  error: Readonly<{ code: string; message: string }> | null;
@@ -259,6 +289,11 @@ function repositoryHead(repoRoot: string): string {
259
289
  }).trim();
260
290
  }
261
291
 
292
+ function gitRootFor(stateRoot: string, options: OrchestrationOptions): string {
293
+ if (!options.gitWorktreeRoot) return stateRoot;
294
+ return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
295
+ }
296
+
262
297
  function isAncestor(repoRoot: string, ancestor: string, descendant: string): boolean {
263
298
  return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
264
299
  cwd: repoRoot,
@@ -287,7 +322,8 @@ function commitFinalizationView(inspection: CommitFinalizationInspection): NonNu
287
322
  currentHead: inspection.currentHead,
288
323
  committedHead: inspection.committedHead,
289
324
  needsAnchor: inspection.needsAnchor,
290
- needsLog: inspection.needsLog
325
+ needsLog: inspection.needsLog,
326
+ attempt: inspection.attempt
291
327
  };
292
328
  }
293
329
 
@@ -353,6 +389,23 @@ function resolveProjectPolicy(repoRoot: string, client: AgentClientId): Orchestr
353
389
  return normalizeAgentClients(raw).state[client].orchestration;
354
390
  }
355
391
 
392
+ function canRecoverCodexUnsupportedPause(run: OrchestrationRun, taskDir: string): boolean {
393
+ return run.schemaVersion === 2
394
+ && run.status === 'paused'
395
+ && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
396
+ && run.modelPolicySource?.client === 'codex'
397
+ && run.stepCount === 0
398
+ && run.nextStage === null
399
+ && run.baseline === ''
400
+ && run.pendingDelegation === null
401
+ && run.receipts.length === 0
402
+ && run.commitAuthorization?.issuedAt === null
403
+ && run.commitAuthorization?.consumedAt === null
404
+ && run.completionEvidence == null
405
+ && !fs.existsSync(commitIntentPath(taskDir))
406
+ && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
407
+ }
408
+
356
409
  function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
357
410
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
358
411
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
@@ -399,6 +452,39 @@ function beginOrResumeOrchestration(taskRef: string, options: OrchestrationOptio
399
452
  return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
400
453
  }
401
454
  }
455
+ if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
456
+ if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
457
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
458
+ }
459
+ const now = (options.now ?? (() => new Date().toISOString()))();
460
+ const previousPause = existing.pause as ClientCapabilityRecovery['previousPause'];
461
+ const recovery: ClientCapabilityRecovery = {
462
+ code: 'CLIENT_CAPABILITY_ENABLED',
463
+ recoveredAt: now,
464
+ previousSchemaVersion: 2,
465
+ previousStatus: 'paused',
466
+ previousPause,
467
+ client: 'codex',
468
+ guards: {
469
+ stepCount: 0,
470
+ nextStage: null,
471
+ baselineEmpty: true,
472
+ receiptCount: 0,
473
+ pendingDelegation: false,
474
+ commitAuthorizationUnused: true,
475
+ completionEvidenceAbsent: true,
476
+ commitIntentAbsent: true
477
+ },
478
+ resultingStatus: 'running'
479
+ };
480
+ const resumed = withUpdatedRun(existing, {
481
+ status: 'running',
482
+ pause: null,
483
+ recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
484
+ }, () => now);
485
+ saveRun(resolved.taskDir, resumed);
486
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
487
+ }
402
488
  if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
403
489
  const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
404
490
  saveRun(resolved.taskDir, resumed);
@@ -607,6 +693,12 @@ function routeFromFacts(taskDir: string): Omit<OrchestrationNext, 'requestedMode
607
693
  function routeOrchestration(taskRef: string, options: OrchestrationOptions = {}): OrchestrationResult {
608
694
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
609
695
  if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
696
+ let gitRoot: string;
697
+ try {
698
+ gitRoot = gitRootFor(resolved.repoRoot, options);
699
+ } catch (error) {
700
+ return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
701
+ }
610
702
  let content: string;
611
703
  try {
612
704
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
@@ -677,7 +769,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
677
769
  const capture = options.captureRepository ?? captureRepositorySnapshot;
678
770
  let before: RepositorySnapshot;
679
771
  try {
680
- before = capture(resolved.repoRoot);
772
+ before = capture(gitRoot);
681
773
  } catch (error) {
682
774
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
683
775
  }
@@ -693,7 +785,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
693
785
  options.inspectPullRequest ?? inspectPlatformPullRequest;
694
786
  let inspection: ReturnType<NonNullable<OrchestrationOptions['inspectPullRequest']>>;
695
787
  try {
696
- inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
788
+ inspection = inspect(resolved.taskId, { cwd: gitRoot });
697
789
  } catch (error) {
698
790
  return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
699
791
  }
@@ -714,7 +806,7 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
714
806
 
715
807
  let after: RepositorySnapshot;
716
808
  try {
717
- after = capture(resolved.repoRoot);
809
+ after = capture(gitRoot);
718
810
  } catch (error) {
719
811
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
720
812
  }
@@ -748,16 +840,119 @@ function routeOrchestration(taskRef: string, options: OrchestrationOptions = {})
748
840
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
749
841
  }
750
842
 
843
+ function appendCommitActivity(
844
+ resolved: Extract<ReturnType<typeof resolveTaskRef>, { ok: true }>,
845
+ step: string,
846
+ agent: string,
847
+ note: string
848
+ ): Readonly<{ code: string; message: string }> | null {
849
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
850
+ const activity = locateActivityLog(taskContent);
851
+ if (!activity) return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
852
+ const metadata = captureTaskWriteMetadata();
853
+ const written = writeTask({
854
+ taskRef: resolved.taskId,
855
+ expectedState: 'active',
856
+ mutations: [{
857
+ kind: 'section',
858
+ aliases: ['活动日志', 'Activity Log'],
859
+ heading: activity.heading,
860
+ body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
861
+ }]
862
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
863
+ return written.status === 'failed' ? written.error : null;
864
+ }
865
+
866
+ function startCommitAttempt(
867
+ taskRef: string,
868
+ input: Readonly<{ agent: string }>,
869
+ options: OrchestrationOptions = {}
870
+ ): CommitIntentResult {
871
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
872
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
873
+ try {
874
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
875
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
876
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
877
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
878
+ }
879
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
880
+ const activity = locateActivityLog(taskContent);
881
+ if (!activity) return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
882
+ const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
883
+ if (open.length > 0) {
884
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
885
+ }
886
+ const currentHead = repositoryHead(gitRoot);
887
+ const attempt = (options.id ?? randomUUID)();
888
+ const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
889
+ attempt,
890
+ baseline: currentHead,
891
+ agent: input.agent
892
+ }));
893
+ if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
894
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
895
+ return {
896
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
897
+ finalization: commitFinalizationView(inspection), error: null
898
+ };
899
+ });
900
+ } catch (error) {
901
+ return mapCommitIntentError(error, resolved.taskId);
902
+ }
903
+ }
904
+
905
+ function terminateCommitAttempt(
906
+ taskRef: string,
907
+ input: Readonly<{ attempt: string; agent: string; code: string }>,
908
+ options: OrchestrationOptions = {}
909
+ ): CommitIntentResult {
910
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
911
+ if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
912
+ try {
913
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
914
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
915
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
916
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
917
+ }
918
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
919
+ if (
920
+ inspection.disposition !== 'retryable-start'
921
+ || inspection.attempt?.attempt !== input.attempt
922
+ || inspection.attempt.agent !== input.agent
923
+ || inspection.attempt.baseline !== inspection.currentHead
924
+ ) {
925
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
926
+ }
927
+ const error = appendCommitActivity(
928
+ resolved,
929
+ 'Commit [aborted]',
930
+ input.agent,
931
+ `aborted; attempt=${input.attempt}; code=${input.code}`
932
+ );
933
+ if (error) return commitIntentFailed(error.code, error.message, resolved.taskId);
934
+ return {
935
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
936
+ finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
937
+ error: null
938
+ };
939
+ });
940
+ } catch (error) {
941
+ return mapCommitIntentError(error, resolved.taskId);
942
+ }
943
+ }
944
+
751
945
  function beginCommitIntent(
752
946
  taskRef: string,
753
- input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string }>,
947
+ input: Readonly<{ agent: string; orchestrated: boolean; baselineHead: string; attempt: string }>,
754
948
  options: OrchestrationOptions = {}
755
949
  ): CommitIntentResult {
756
950
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
757
951
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
758
952
  try {
953
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
759
954
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
760
- const currentHead = repositoryHead(resolved.repoRoot);
955
+ const currentHead = repositoryHead(gitRoot);
761
956
  if (currentHead !== input.baselineHead) {
762
957
  return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
763
958
  }
@@ -769,7 +964,19 @@ function beginCommitIntent(
769
964
  }
770
965
  return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
771
966
  }
772
-
967
+ const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
968
+ if (
969
+ attemptInspection.disposition !== 'retryable-start'
970
+ || attemptInspection.attempt?.attempt !== input.attempt
971
+ || attemptInspection.attempt.agent !== input.agent
972
+ || attemptInspection.attempt.baseline !== input.baselineHead
973
+ ) {
974
+ return commitIntentFailed(
975
+ 'ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH',
976
+ 'commit begin requires one matching retryable Commit attempt',
977
+ resolved.taskId
978
+ );
979
+ }
773
980
  const runFile = orchestrationPath(resolved.taskDir);
774
981
  let run: OrchestrationRun | null = null;
775
982
  let sourceRunBytes: string | null = null;
@@ -883,13 +1090,14 @@ function checkpointCommitIntent(
883
1090
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
884
1091
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
885
1092
  try {
1093
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
886
1094
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
887
1095
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
888
- const currentHead = repositoryHead(resolved.repoRoot);
1096
+ const currentHead = repositoryHead(gitRoot);
889
1097
  if (currentHead !== input.head) {
890
1098
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
891
1099
  }
892
- if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
1100
+ if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
893
1101
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
894
1102
  }
895
1103
  if (input.kind === 'pushed') {
@@ -1016,9 +1224,10 @@ function completeCommitIntent(
1016
1224
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1017
1225
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1018
1226
  try {
1227
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1019
1228
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
1020
1229
  readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1021
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1230
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1022
1231
  return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
1023
1232
  removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1024
1233
  });
@@ -1036,8 +1245,15 @@ function recoverCommitIntent(
1036
1245
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1037
1246
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1038
1247
  try {
1248
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1039
1249
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
1040
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1250
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1251
+ if (inspection.disposition === 'retryable-start') {
1252
+ return {
1253
+ status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
1254
+ finalization: commitFinalizationView(inspection), error: null
1255
+ };
1256
+ }
1041
1257
  if (inspection.disposition === 'prepared' && inspection.intentDigest) {
1042
1258
  removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
1043
1259
  return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
@@ -1066,9 +1282,10 @@ function abortCommitIntent(
1066
1282
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1067
1283
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1068
1284
  try {
1285
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
1069
1286
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
1070
1287
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
1071
- const currentHead = repositoryHead(resolved.repoRoot);
1288
+ const currentHead = repositoryHead(gitRoot);
1072
1289
  if (
1073
1290
  intent.phase !== 'prepared'
1074
1291
  || intent.pushEvidence !== null
@@ -1088,7 +1305,13 @@ function abortCommitIntent(
1088
1305
  function statusCommitIntent(taskRef: string, options: OrchestrationOptions = {}): CommitIntentResult {
1089
1306
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
1090
1307
  if (!resolved.ok) return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
1091
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
1308
+ let gitRoot: string;
1309
+ try {
1310
+ gitRoot = gitRootFor(resolved.repoRoot, options);
1311
+ } catch (error) {
1312
+ return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
1313
+ }
1314
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
1092
1315
  return {
1093
1316
  status: 'ready',
1094
1317
  changed: false,
@@ -1156,7 +1379,11 @@ function prepareOrchestrationDelegationUnlocked(
1156
1379
  }
1157
1380
  let beforeFingerprint: string;
1158
1381
  try {
1159
- beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
1382
+ beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1383
+ gitRoot: gitRootFor(resolved.repoRoot, options),
1384
+ stateRoot: resolved.repoRoot,
1385
+ taskId: resolved.taskId
1386
+ });
1160
1387
  } catch (error) {
1161
1388
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
1162
1389
  }
@@ -1220,7 +1447,7 @@ function matchingDelegations(
1220
1447
  .filter((entry) => entry.isDirectory())
1221
1448
  .flatMap((entry) => {
1222
1449
  const run = readRun(path.join(activeRoot, entry.name));
1223
- if (!run?.pendingDelegation) return [];
1450
+ if (run?.status !== 'running' || !run.pendingDelegation) return [];
1224
1451
  const receipt = run.pendingDelegation;
1225
1452
  return predicate(receipt)
1226
1453
  ? [{ taskId: entry.name, run }]
@@ -1254,13 +1481,44 @@ function activateMatchingOrchestrationDelegation(
1254
1481
  (receipt) => receipt.status === 'prepared',
1255
1482
  options
1256
1483
  );
1257
- if ('status' in matched) return matched;
1484
+ if ('status' in matched) {
1485
+ if (matched.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING') return matched;
1486
+ const replay = uniqueMatchingDelegation(
1487
+ client,
1488
+ (receipt) => receipt.childId === event.childId && ['activated', 'stage-completed'].includes(receipt.status),
1489
+ options
1490
+ );
1491
+ if ('status' in replay) return matched;
1492
+ const receipt = replay.run.pendingDelegation!;
1493
+ const sameEvidence = receipt.parentId === event.parentId
1494
+ && receipt.spawnMode === event.spawnMode
1495
+ && receipt.actualModel === event.actualModel
1496
+ && receipt.actualReasoningEffort === event.actualReasoningEffort
1497
+ && receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
1498
+ && receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
1499
+ && receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
1500
+ if (!sameEvidence) {
1501
+ return pauseOrchestration(replay.taskId, 'DELEGATION_REPLAY_CONFLICT', 'replayed native start evidence conflicts with the active receipt', true, options);
1502
+ }
1503
+ return { status: replay.run.status, changed: false, taskId: replay.taskId, run: replay.run, next: null, error: null };
1504
+ }
1258
1505
  if (matched.run.receipts.some((receipt) => receipt.childId === event.childId)) {
1259
1506
  return pauseOrchestration(matched.taskId, 'DELEGATION_IDENTITY_REUSED', 'native child identity was already used by this run', true, options);
1260
1507
  }
1261
1508
  return activateOrchestrationDelegation(matched.taskId, event, options);
1262
1509
  }
1263
1510
 
1511
+ function pauseMatchingOrchestrationDelegation(
1512
+ client: AgentClientId,
1513
+ code: string,
1514
+ message: string,
1515
+ options: OrchestrationOptions = {}
1516
+ ): OrchestrationResult {
1517
+ const matched = uniqueMatchingDelegation(client, () => true, options);
1518
+ if ('status' in matched) return matched;
1519
+ return pauseOrchestration(matched.taskId, code, message, true, options);
1520
+ }
1521
+
1264
1522
  function sealMatchingOrchestrationDelegation(
1265
1523
  client: AgentClientId,
1266
1524
  event: Readonly<{ nativeAgent: string; childId: string }>,
@@ -1280,9 +1538,14 @@ function sealMatchingOrchestrationDelegation(
1280
1538
  }
1281
1539
  const repoRoot = options.repoRoot ?? process.cwd();
1282
1540
  try {
1541
+ const gitRoot = gitRootFor(repoRoot, options);
1283
1542
  const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
1284
- const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot, snapshotTaskId);
1285
- const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(repoRoot, receipt.beforeFingerprint, afterFingerprint);
1543
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1544
+ gitRoot,
1545
+ stateRoot: repoRoot,
1546
+ taskId: snapshotTaskId
1547
+ });
1548
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
1286
1549
  return sealOrchestrationDelegation(matched.taskId, {
1287
1550
  childId: event.childId,
1288
1551
  exitCode: 0,
@@ -1300,6 +1563,96 @@ function sealMatchingOrchestrationDelegation(
1300
1563
  }
1301
1564
  }
1302
1565
 
1566
+ function sealMatchingOrchestrationDelegationWithHostEvidence(
1567
+ client: AgentClientId,
1568
+ event: Readonly<{ nativeAgent: string; childId: string }>,
1569
+ consumeEvidence: (receipt: DelegationReceipt) => Readonly<{
1570
+ stopRevision: number;
1571
+ consumer: string;
1572
+ consumedAt: string;
1573
+ }>,
1574
+ options: OrchestrationOptions = {}
1575
+ ): OrchestrationResult {
1576
+ const role = managedDelegationRole(event.nativeAgent);
1577
+ if (!role) return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
1578
+ const matched = uniqueMatchingDelegation(
1579
+ client,
1580
+ (receipt) => (
1581
+ (receipt.status === 'stage-completed' || receipt.status === 'sealed')
1582
+ && receipt.childId === event.childId
1583
+ ),
1584
+ options
1585
+ );
1586
+ if ('status' in matched) return matched;
1587
+ const receipt = matched.run.pendingDelegation!;
1588
+ if (receipt.status === 'sealed') {
1589
+ return reconcileMatchingOrchestrationDelegation(client, event.childId, options);
1590
+ }
1591
+ if (receipt.role !== role) {
1592
+ return pauseOrchestration(matched.taskId, 'DELEGATION_ROLE_MISMATCH', `managed role ${role} does not match ${receipt.role}`, true, options);
1593
+ }
1594
+ const repoRoot = options.repoRoot ?? process.cwd();
1595
+ try {
1596
+ const gitRoot = gitRootFor(repoRoot, options);
1597
+ const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
1598
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1599
+ gitRoot,
1600
+ stateRoot: repoRoot,
1601
+ taskId: snapshotTaskId
1602
+ });
1603
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
1604
+ const baseEvent = { childId: event.childId, exitCode: 0, afterFingerprint, changedPaths };
1605
+ const validated = sealDelegation(receipt, baseEvent, { now: options.now });
1606
+ if (!validated.ok) {
1607
+ return pauseOrchestration(matched.taskId, validated.code, validated.message, true, options);
1608
+ }
1609
+ const hostEvidence = consumeEvidence(receipt);
1610
+ const sealed = sealDelegation(receipt, { ...baseEvent, hostEvidence }, { now: options.now });
1611
+ if (!sealed.ok) return pauseOrchestration(matched.taskId, sealed.code, sealed.message, true, options);
1612
+ const resolved = resolveTaskRef(matched.taskId, { repoRoot });
1613
+ if (!resolved.ok) return failed(resolved.code, resolved.message, resolved.taskId);
1614
+ const updated = withUpdatedRun(matched.run, { pendingDelegation: sealed.receipt }, options.now);
1615
+ saveRun(resolved.taskDir, updated);
1616
+ return { status: 'running', changed: true, taskId: matched.taskId, run: updated, next: null, error: null };
1617
+ } catch (error) {
1618
+ return pauseOrchestration(
1619
+ matched.taskId,
1620
+ 'ORCHESTRATION_CODEX_EVIDENCE_FAILED',
1621
+ error instanceof Error ? error.message : String(error),
1622
+ true,
1623
+ options
1624
+ );
1625
+ }
1626
+ }
1627
+
1628
+ function reconcileMatchingOrchestrationDelegation(
1629
+ client: AgentClientId,
1630
+ childId: string,
1631
+ options: OrchestrationOptions = {}
1632
+ ): OrchestrationResult {
1633
+ const matched = uniqueMatchingDelegation(
1634
+ client,
1635
+ (receipt) => receipt.childId === childId,
1636
+ options
1637
+ );
1638
+ if ('status' in matched) return matched;
1639
+ const receipt = matched.run.pendingDelegation!;
1640
+ if (
1641
+ receipt.status !== 'sealed'
1642
+ || receipt.hostEvidence?.consumer !== receipt.id
1643
+ || receipt.hostEvidence.consumedAt === null
1644
+ ) {
1645
+ return pauseOrchestration(
1646
+ matched.taskId,
1647
+ 'ORCHESTRATION_CODEX_RECONCILIATION_FAILED',
1648
+ 'Codex native spawn did not produce one sealed receipt with consumed host evidence',
1649
+ true,
1650
+ options
1651
+ );
1652
+ }
1653
+ return { status: matched.run.status, changed: false, taskId: matched.taskId, run: matched.run, next: null, error: null };
1654
+ }
1655
+
1303
1656
  function activateOrchestrationDelegation(
1304
1657
  taskRef: string,
1305
1658
  event: Parameters<typeof activateDelegation>[1],
@@ -1466,16 +1819,21 @@ export {
1466
1819
  completeOrchestrationStage,
1467
1820
  inspectOrchestrationStage,
1468
1821
  orchestrationPath,
1822
+ pauseMatchingOrchestrationDelegation,
1469
1823
  pauseOrchestration,
1470
1824
  planOrchestrationStageCompletion,
1471
1825
  prepareOrchestrationDelegation,
1472
1826
  readRun,
1827
+ reconcileMatchingOrchestrationDelegation,
1473
1828
  recoverCommitIntent,
1474
1829
  routeOrchestration,
1475
1830
  sealMatchingOrchestrationDelegation,
1831
+ sealMatchingOrchestrationDelegationWithHostEvidence,
1476
1832
  sealOrchestrationDelegation,
1833
+ startCommitAttempt,
1477
1834
  statusCommitIntent,
1478
- statusOrchestration
1835
+ statusOrchestration,
1836
+ terminateCommitAttempt
1479
1837
  };
1480
1838
  export type {
1481
1839
  CommitIntentResult,
@@ -1486,5 +1844,6 @@ export type {
1486
1844
  OrchestrationRun,
1487
1845
  OrchestrationStageCompletion,
1488
1846
  OrchestrationStageIdentity,
1489
- OrchestrationStatus
1847
+ OrchestrationStatus,
1848
+ OrchestrationOptions
1490
1849
  };
@@ -3,6 +3,13 @@ import path from "node:path";
3
3
 
4
4
  import { artifactName, maxRound } from "./review-artifacts.ts";
5
5
 
6
+ type ReviewedGitTree = string & { readonly __reviewedGitTree: unique symbol };
7
+ const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
8
+
9
+ export function parseReviewedGitTree(value: string): ReviewedGitTree | null {
10
+ return REVIEWED_TREE_RE.test(value) ? value as ReviewedGitTree : null;
11
+ }
12
+
6
13
  // Paths excluded from the post-review coverage gate. Fail-closed: the gate
7
14
  // covers ALL tracked changes by default. This default denylist is EMPTY —
8
15
  // projects opt into exclusions explicitly via review.post_review_exclude_globs.
@@ -71,6 +78,8 @@ export function extractReviewedSnapshotTree(content: string): string {
71
78
  return match ? match[1]!.trim().replace(/`/g, "") : "";
72
79
  }
73
80
 
81
+ export type { ReviewedGitTree };
82
+
74
83
  export function parseReviewVerdict(content: string): string {
75
84
  const match = String(content).match(/^[-*]?\s*\*\*(?:总体结论|Overall Verdict)\*\*[::]\s*(.*?)\s*$/m);
76
85
  return match ? match[1]!.trim() : "";