@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
@@ -13,10 +13,12 @@ import { inspectPlatformPullRequest } from '../platform/pull-requests.js';
13
13
  import { getAgentClientCapability, getAgentClientDelegationEvidence, getAgentClientModelSelection } from '../agent-clients/registry.js';
14
14
  import { isAgentClientId } from '../agent-clients/types.js';
15
15
  import { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots } from './workspace-snapshot.js';
16
+ import { assertGitRepositoryBinding } from '../git/worktree-identity.js';
16
17
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
17
18
  import { CommitIntentError, commitIntentPath, createCommitIntent, digest, readCommitIntent, removeCommitIntent, removeCommitIntentByDigest, serialize, updateCommitIntent } from './commit-intent.js';
18
19
  import { inspectCommitFinalization, planCommitTaskFinalization } from './commit-finalization.js';
19
20
  import { captureTaskWriteMetadata, writeTask } from './write.js';
21
+ import { appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries } from './activity-log.js';
20
22
  function supportsLifecycleDelegation(client) {
21
23
  return getAgentClientCapability(client, 'subagents').level !== 'unsupported'
22
24
  && getAgentClientCapability(client, 'orchestration').level !== 'unsupported'
@@ -71,6 +73,11 @@ function repositoryHead(repoRoot) {
71
73
  stdio: ['ignore', 'pipe', 'pipe']
72
74
  }).trim();
73
75
  }
76
+ function gitRootFor(stateRoot, options) {
77
+ if (!options.gitWorktreeRoot)
78
+ return stateRoot;
79
+ return assertGitRepositoryBinding(stateRoot, options.gitWorktreeRoot).worktreeRoot;
80
+ }
74
81
  function isAncestor(repoRoot, ancestor, descendant) {
75
82
  return spawnSync('git', ['merge-base', '--is-ancestor', ancestor, descendant], {
76
83
  cwd: repoRoot,
@@ -97,7 +104,8 @@ function commitFinalizationView(inspection) {
97
104
  currentHead: inspection.currentHead,
98
105
  committedHead: inspection.committedHead,
99
106
  needsAnchor: inspection.needsAnchor,
100
- needsLog: inspection.needsLog
107
+ needsLog: inspection.needsLog,
108
+ attempt: inspection.attempt
101
109
  };
102
110
  }
103
111
  function validCommitRun(run, taskId) {
@@ -151,6 +159,22 @@ function resolveProjectPolicy(repoRoot, client) {
151
159
  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
152
160
  return normalizeAgentClients(raw).state[client].orchestration;
153
161
  }
162
+ function canRecoverCodexUnsupportedPause(run, taskDir) {
163
+ return run.schemaVersion === 2
164
+ && run.status === 'paused'
165
+ && run.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
166
+ && run.modelPolicySource?.client === 'codex'
167
+ && run.stepCount === 0
168
+ && run.nextStage === null
169
+ && run.baseline === ''
170
+ && run.pendingDelegation === null
171
+ && run.receipts.length === 0
172
+ && run.commitAuthorization?.issuedAt === null
173
+ && run.commitAuthorization?.consumedAt === null
174
+ && run.completionEvidence == null
175
+ && !fs.existsSync(commitIntentPath(taskDir))
176
+ && (run.recoveryHistory ?? []).every((entry) => entry.code === 'MODEL_POLICY_SUPPLEMENTED');
177
+ }
154
178
  function beginOrResumeOrchestration(taskRef, options = {}) {
155
179
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
156
180
  if (!resolved.ok)
@@ -196,6 +220,39 @@ function beginOrResumeOrchestration(taskRef, options = {}) {
196
220
  return failed('ORCHESTRATION_MODEL_POLICY_MISMATCH', 'provided model policy does not match the persisted run policy', resolved.taskId);
197
221
  }
198
222
  }
223
+ if (existing.status === 'paused' && existing.pause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED') {
224
+ if (!canRecoverCodexUnsupportedPause(existing, resolved.taskDir)) {
225
+ return { status: 'paused', changed: false, taskId: resolved.taskId, run: existing, next: null, error: null };
226
+ }
227
+ const now = (options.now ?? (() => new Date().toISOString()))();
228
+ const previousPause = existing.pause;
229
+ const recovery = {
230
+ code: 'CLIENT_CAPABILITY_ENABLED',
231
+ recoveredAt: now,
232
+ previousSchemaVersion: 2,
233
+ previousStatus: 'paused',
234
+ previousPause,
235
+ client: 'codex',
236
+ guards: {
237
+ stepCount: 0,
238
+ nextStage: null,
239
+ baselineEmpty: true,
240
+ receiptCount: 0,
241
+ pendingDelegation: false,
242
+ commitAuthorizationUnused: true,
243
+ completionEvidenceAbsent: true,
244
+ commitIntentAbsent: true
245
+ },
246
+ resultingStatus: 'running'
247
+ };
248
+ const resumed = withUpdatedRun(existing, {
249
+ status: 'running',
250
+ pause: null,
251
+ recoveryHistory: Object.freeze([...(existing.recoveryHistory ?? []), recovery])
252
+ }, () => now);
253
+ saveRun(resolved.taskDir, resumed);
254
+ return { status: 'running', changed: true, taskId: resolved.taskId, run: resumed, next: null, error: null };
255
+ }
199
256
  if (existing.status === 'paused' && existing.pause?.recoverable && existing.pendingDelegation === null) {
200
257
  const resumed = withUpdatedRun(existing, { status: 'running', pause: null });
201
258
  saveRun(resolved.taskDir, resumed);
@@ -399,6 +456,13 @@ function routeOrchestration(taskRef, options = {}) {
399
456
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
400
457
  if (!resolved.ok)
401
458
  return failed(resolved.code, resolved.message, resolved.taskId);
459
+ let gitRoot;
460
+ try {
461
+ gitRoot = gitRootFor(resolved.repoRoot, options);
462
+ }
463
+ catch (error) {
464
+ return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
465
+ }
402
466
  let content;
403
467
  try {
404
468
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
@@ -469,7 +533,7 @@ function routeOrchestration(taskRef, options = {}) {
469
533
  const capture = options.captureRepository ?? captureRepositorySnapshot;
470
534
  let before;
471
535
  try {
472
- before = capture(resolved.repoRoot);
536
+ before = capture(gitRoot);
473
537
  }
474
538
  catch (error) {
475
539
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -484,7 +548,7 @@ function routeOrchestration(taskRef, options = {}) {
484
548
  const inspect = options.inspectPullRequest ?? inspectPlatformPullRequest;
485
549
  let inspection;
486
550
  try {
487
- inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
551
+ inspection = inspect(resolved.taskId, { cwd: gitRoot });
488
552
  }
489
553
  catch (error) {
490
554
  return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -505,7 +569,7 @@ function routeOrchestration(taskRef, options = {}) {
505
569
  }
506
570
  let after;
507
571
  try {
508
- after = capture(resolved.repoRoot);
572
+ after = capture(gitRoot);
509
573
  }
510
574
  catch (error) {
511
575
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -538,13 +602,101 @@ function routeOrchestration(taskRef, options = {}) {
538
602
  }
539
603
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
540
604
  }
605
+ function appendCommitActivity(resolved, step, agent, note) {
606
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
607
+ const activity = locateActivityLog(taskContent);
608
+ if (!activity)
609
+ return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
610
+ const metadata = captureTaskWriteMetadata();
611
+ const written = writeTask({
612
+ taskRef: resolved.taskId,
613
+ expectedState: 'active',
614
+ mutations: [{
615
+ kind: 'section',
616
+ aliases: ['活动日志', 'Activity Log'],
617
+ heading: activity.heading,
618
+ body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
619
+ }]
620
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
621
+ return written.status === 'failed' ? written.error : null;
622
+ }
623
+ function startCommitAttempt(taskRef, input, options = {}) {
624
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
625
+ if (!resolved.ok)
626
+ return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
627
+ try {
628
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
629
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
630
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
631
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
632
+ }
633
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
634
+ const activity = locateActivityLog(taskContent);
635
+ if (!activity)
636
+ return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
637
+ const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
638
+ if (open.length > 0) {
639
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
640
+ }
641
+ const currentHead = repositoryHead(gitRoot);
642
+ const attempt = (options.id ?? randomUUID)();
643
+ const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
644
+ attempt,
645
+ baseline: currentHead,
646
+ agent: input.agent
647
+ }));
648
+ if (error)
649
+ return commitIntentFailed(error.code, error.message, resolved.taskId);
650
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
651
+ return {
652
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
653
+ finalization: commitFinalizationView(inspection), error: null
654
+ };
655
+ });
656
+ }
657
+ catch (error) {
658
+ return mapCommitIntentError(error, resolved.taskId);
659
+ }
660
+ }
661
+ function terminateCommitAttempt(taskRef, input, options = {}) {
662
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
663
+ if (!resolved.ok)
664
+ return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
665
+ try {
666
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
667
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
668
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
669
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
670
+ }
671
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
672
+ if (inspection.disposition !== 'retryable-start'
673
+ || inspection.attempt?.attempt !== input.attempt
674
+ || inspection.attempt.agent !== input.agent
675
+ || inspection.attempt.baseline !== inspection.currentHead) {
676
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
677
+ }
678
+ const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
679
+ if (error)
680
+ return commitIntentFailed(error.code, error.message, resolved.taskId);
681
+ return {
682
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
683
+ finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
684
+ error: null
685
+ };
686
+ });
687
+ }
688
+ catch (error) {
689
+ return mapCommitIntentError(error, resolved.taskId);
690
+ }
691
+ }
541
692
  function beginCommitIntent(taskRef, input, options = {}) {
542
693
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
543
694
  if (!resolved.ok)
544
695
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
545
696
  try {
697
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
546
698
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
547
- const currentHead = repositoryHead(resolved.repoRoot);
699
+ const currentHead = repositoryHead(gitRoot);
548
700
  if (currentHead !== input.baselineHead) {
549
701
  return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
550
702
  }
@@ -557,6 +709,13 @@ function beginCommitIntent(taskRef, input, options = {}) {
557
709
  }
558
710
  return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
559
711
  }
712
+ const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
713
+ if (attemptInspection.disposition !== 'retryable-start'
714
+ || attemptInspection.attempt?.attempt !== input.attempt
715
+ || attemptInspection.attempt.agent !== input.agent
716
+ || attemptInspection.attempt.baseline !== input.baselineHead) {
717
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
718
+ }
560
719
  const runFile = orchestrationPath(resolved.taskDir);
561
720
  let run = null;
562
721
  let sourceRunBytes = null;
@@ -646,13 +805,14 @@ function checkpointCommitIntent(taskRef, input, options = {}) {
646
805
  if (!resolved.ok)
647
806
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
648
807
  try {
808
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
649
809
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
650
810
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
651
- const currentHead = repositoryHead(resolved.repoRoot);
811
+ const currentHead = repositoryHead(gitRoot);
652
812
  if (currentHead !== input.head) {
653
813
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
654
814
  }
655
- if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
815
+ if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
656
816
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
657
817
  }
658
818
  if (input.kind === 'pushed') {
@@ -750,9 +910,10 @@ function completeCommitIntent(taskRef, input, options = {}) {
750
910
  if (!resolved.ok)
751
911
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
752
912
  try {
913
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
753
914
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
754
915
  readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
755
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
916
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
756
917
  return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
757
918
  removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
758
919
  });
@@ -767,8 +928,15 @@ function recoverCommitIntent(taskRef, input, options = {}) {
767
928
  if (!resolved.ok)
768
929
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
769
930
  try {
931
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
770
932
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
771
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
933
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
934
+ if (inspection.disposition === 'retryable-start') {
935
+ return {
936
+ status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
937
+ finalization: commitFinalizationView(inspection), error: null
938
+ };
939
+ }
772
940
  if (inspection.disposition === 'prepared' && inspection.intentDigest) {
773
941
  removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
774
942
  return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
@@ -790,9 +958,10 @@ function abortCommitIntent(taskRef, input, options = {}) {
790
958
  if (!resolved.ok)
791
959
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
792
960
  try {
961
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
793
962
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
794
963
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
795
- const currentHead = repositoryHead(resolved.repoRoot);
964
+ const currentHead = repositoryHead(gitRoot);
796
965
  if (intent.phase !== 'prepared'
797
966
  || intent.pushEvidence !== null
798
967
  || input.expectedHead !== intent.baselineHead
@@ -811,7 +980,14 @@ function statusCommitIntent(taskRef, options = {}) {
811
980
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
812
981
  if (!resolved.ok)
813
982
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
814
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
983
+ let gitRoot;
984
+ try {
985
+ gitRoot = gitRootFor(resolved.repoRoot, options);
986
+ }
987
+ catch (error) {
988
+ return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
989
+ }
990
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
815
991
  return {
816
992
  status: 'ready',
817
993
  changed: false,
@@ -878,7 +1054,11 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
878
1054
  }
879
1055
  let beforeFingerprint;
880
1056
  try {
881
- beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
1057
+ beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1058
+ gitRoot: gitRootFor(resolved.repoRoot, options),
1059
+ stateRoot: resolved.repoRoot,
1060
+ taskId: resolved.taskId
1061
+ });
882
1062
  }
883
1063
  catch (error) {
884
1064
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -928,7 +1108,7 @@ function matchingDelegations(predicate, options) {
928
1108
  .filter((entry) => entry.isDirectory())
929
1109
  .flatMap((entry) => {
930
1110
  const run = readRun(path.join(activeRoot, entry.name));
931
- if (!run?.pendingDelegation)
1111
+ if (run?.status !== 'running' || !run.pendingDelegation)
932
1112
  return [];
933
1113
  const receipt = run.pendingDelegation;
934
1114
  return predicate(receipt)
@@ -949,13 +1129,36 @@ function activateMatchingOrchestrationDelegation(client, event, options = {}) {
949
1129
  if (!role)
950
1130
  return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
951
1131
  const matched = uniqueMatchingDelegation(client, (receipt) => receipt.status === 'prepared', options);
952
- if ('status' in matched)
953
- return matched;
1132
+ if ('status' in matched) {
1133
+ if (matched.error?.code !== 'ORCHESTRATION_DELEGATION_MISSING')
1134
+ return matched;
1135
+ const replay = uniqueMatchingDelegation(client, (receipt) => receipt.childId === event.childId && ['activated', 'stage-completed'].includes(receipt.status), options);
1136
+ if ('status' in replay)
1137
+ return matched;
1138
+ const receipt = replay.run.pendingDelegation;
1139
+ const sameEvidence = receipt.parentId === event.parentId
1140
+ && receipt.spawnMode === event.spawnMode
1141
+ && receipt.actualModel === event.actualModel
1142
+ && receipt.actualReasoningEffort === event.actualReasoningEffort
1143
+ && receipt.modelFallbackReason === (event.modelFallbackReason ?? null)
1144
+ && receipt.reasoningEffortFallbackReason === (event.reasoningEffortFallbackReason ?? null)
1145
+ && receipt.hostEvidence?.hookDefinitionHash === event.hostEvidence?.hookDefinitionHash;
1146
+ if (!sameEvidence) {
1147
+ return pauseOrchestration(replay.taskId, 'DELEGATION_REPLAY_CONFLICT', 'replayed native start evidence conflicts with the active receipt', true, options);
1148
+ }
1149
+ return { status: replay.run.status, changed: false, taskId: replay.taskId, run: replay.run, next: null, error: null };
1150
+ }
954
1151
  if (matched.run.receipts.some((receipt) => receipt.childId === event.childId)) {
955
1152
  return pauseOrchestration(matched.taskId, 'DELEGATION_IDENTITY_REUSED', 'native child identity was already used by this run', true, options);
956
1153
  }
957
1154
  return activateOrchestrationDelegation(matched.taskId, event, options);
958
1155
  }
1156
+ function pauseMatchingOrchestrationDelegation(client, code, message, options = {}) {
1157
+ const matched = uniqueMatchingDelegation(client, () => true, options);
1158
+ if ('status' in matched)
1159
+ return matched;
1160
+ return pauseOrchestration(matched.taskId, code, message, true, options);
1161
+ }
959
1162
  function sealMatchingOrchestrationDelegation(client, event, options = {}) {
960
1163
  const role = managedDelegationRole(event.nativeAgent);
961
1164
  if (!role)
@@ -969,9 +1172,14 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
969
1172
  }
970
1173
  const repoRoot = options.repoRoot ?? process.cwd();
971
1174
  try {
1175
+ const gitRoot = gitRootFor(repoRoot, options);
972
1176
  const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
973
- const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(repoRoot, snapshotTaskId);
974
- const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(repoRoot, receipt.beforeFingerprint, afterFingerprint);
1177
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1178
+ gitRoot,
1179
+ stateRoot: repoRoot,
1180
+ taskId: snapshotTaskId
1181
+ });
1182
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
975
1183
  return sealOrchestrationDelegation(matched.taskId, {
976
1184
  childId: event.childId,
977
1185
  exitCode: 0,
@@ -983,6 +1191,63 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
983
1191
  return pauseOrchestration(matched.taskId, 'ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), true, options);
984
1192
  }
985
1193
  }
1194
+ function sealMatchingOrchestrationDelegationWithHostEvidence(client, event, consumeEvidence, options = {}) {
1195
+ const role = managedDelegationRole(event.nativeAgent);
1196
+ if (!role)
1197
+ return failed('DELEGATION_IGNORED', `subagent '${event.nativeAgent}' is not lifecycle-managed`);
1198
+ const matched = uniqueMatchingDelegation(client, (receipt) => ((receipt.status === 'stage-completed' || receipt.status === 'sealed')
1199
+ && receipt.childId === event.childId), options);
1200
+ if ('status' in matched)
1201
+ return matched;
1202
+ const receipt = matched.run.pendingDelegation;
1203
+ if (receipt.status === 'sealed') {
1204
+ return reconcileMatchingOrchestrationDelegation(client, event.childId, options);
1205
+ }
1206
+ if (receipt.role !== role) {
1207
+ return pauseOrchestration(matched.taskId, 'DELEGATION_ROLE_MISMATCH', `managed role ${role} does not match ${receipt.role}`, true, options);
1208
+ }
1209
+ const repoRoot = options.repoRoot ?? process.cwd();
1210
+ try {
1211
+ const gitRoot = gitRootFor(repoRoot, options);
1212
+ const snapshotTaskId = receipt.workspaceSnapshotScope === 'task' ? receipt.taskId : null;
1213
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1214
+ gitRoot,
1215
+ stateRoot: repoRoot,
1216
+ taskId: snapshotTaskId
1217
+ });
1218
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
1219
+ const baseEvent = { childId: event.childId, exitCode: 0, afterFingerprint, changedPaths };
1220
+ const validated = sealDelegation(receipt, baseEvent, { now: options.now });
1221
+ if (!validated.ok) {
1222
+ return pauseOrchestration(matched.taskId, validated.code, validated.message, true, options);
1223
+ }
1224
+ const hostEvidence = consumeEvidence(receipt);
1225
+ const sealed = sealDelegation(receipt, { ...baseEvent, hostEvidence }, { now: options.now });
1226
+ if (!sealed.ok)
1227
+ return pauseOrchestration(matched.taskId, sealed.code, sealed.message, true, options);
1228
+ const resolved = resolveTaskRef(matched.taskId, { repoRoot });
1229
+ if (!resolved.ok)
1230
+ return failed(resolved.code, resolved.message, resolved.taskId);
1231
+ const updated = withUpdatedRun(matched.run, { pendingDelegation: sealed.receipt }, options.now);
1232
+ saveRun(resolved.taskDir, updated);
1233
+ return { status: 'running', changed: true, taskId: matched.taskId, run: updated, next: null, error: null };
1234
+ }
1235
+ catch (error) {
1236
+ return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_EVIDENCE_FAILED', error instanceof Error ? error.message : String(error), true, options);
1237
+ }
1238
+ }
1239
+ function reconcileMatchingOrchestrationDelegation(client, childId, options = {}) {
1240
+ const matched = uniqueMatchingDelegation(client, (receipt) => receipt.childId === childId, options);
1241
+ if ('status' in matched)
1242
+ return matched;
1243
+ const receipt = matched.run.pendingDelegation;
1244
+ if (receipt.status !== 'sealed'
1245
+ || receipt.hostEvidence?.consumer !== receipt.id
1246
+ || receipt.hostEvidence.consumedAt === null) {
1247
+ return pauseOrchestration(matched.taskId, 'ORCHESTRATION_CODEX_RECONCILIATION_FAILED', 'Codex native spawn did not produce one sealed receipt with consumed host evidence', true, options);
1248
+ }
1249
+ return { status: matched.run.status, changed: false, taskId: matched.taskId, run: matched.run, next: null, error: null };
1250
+ }
986
1251
  function activateOrchestrationDelegation(taskRef, event, options = {}) {
987
1252
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
988
1253
  if (!resolved.ok)
@@ -1115,5 +1380,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
1115
1380
  saveRun(resolved.taskDir, updated);
1116
1381
  return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
1117
1382
  }
1118
- export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration };
1383
+ export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseMatchingOrchestrationDelegation, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, reconcileMatchingOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealMatchingOrchestrationDelegationWithHostEvidence, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
1119
1384
  //# sourceMappingURL=orchestration.js.map
@@ -1,6 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { artifactName, maxRound } from "./review-artifacts.js";
4
+ const REVIEWED_TREE_RE = /^[a-f0-9]{40,64}$/;
5
+ export function parseReviewedGitTree(value) {
6
+ return REVIEWED_TREE_RE.test(value) ? value : null;
7
+ }
4
8
  // Paths excluded from the post-review coverage gate. Fail-closed: the gate
5
9
  // covers ALL tracked changes by default. This default denylist is EMPTY —
6
10
  // projects opt into exclusions explicitly via review.post_review_exclude_globs.
@@ -239,12 +239,29 @@ function checkOrchestrationEvidence({ taskDir }) {
239
239
  return failResult('orchestration-evidence', 'Run model policy source or recovery history is invalid');
240
240
  }
241
241
  for (const recovery of run.recoveryHistory) {
242
- if (recovery.code !== 'MODEL_POLICY_SUPPLEMENTED'
243
- || recovery.previousSchemaVersion !== 1
244
- || recovery.receiptCount !== 0
245
- || recovery.pendingDelegation !== false
246
- || !exactText(recovery.recoveredAt)
247
- || !recovery.policySource) {
242
+ const validModelRecovery = recovery.code === 'MODEL_POLICY_SUPPLEMENTED'
243
+ && recovery.previousSchemaVersion === 1
244
+ && recovery.receiptCount === 0
245
+ && recovery.pendingDelegation === false
246
+ && exactText(recovery.recoveredAt)
247
+ && recovery.policySource;
248
+ const guards = recovery.guards;
249
+ const validCapabilityRecovery = recovery.code === 'CLIENT_CAPABILITY_ENABLED'
250
+ && recovery.previousSchemaVersion === 2
251
+ && recovery.previousStatus === 'paused'
252
+ && recovery.previousPause?.code === 'ORCHESTRATION_CLIENT_UNSUPPORTED'
253
+ && recovery.client === 'codex'
254
+ && recovery.resultingStatus === 'running'
255
+ && exactText(recovery.recoveredAt)
256
+ && guards?.stepCount === 0
257
+ && guards?.nextStage === null
258
+ && guards?.baselineEmpty === true
259
+ && guards?.receiptCount === 0
260
+ && guards?.pendingDelegation === false
261
+ && guards?.commitAuthorizationUnused === true
262
+ && guards?.completionEvidenceAbsent === true
263
+ && guards?.commitIntentAbsent === true;
264
+ if (!validModelRecovery && !validCapabilityRecovery) {
248
265
  return failResult('orchestration-evidence', 'Run recovery history contains invalid provenance');
249
266
  }
250
267
  }
@@ -298,6 +315,21 @@ function checkOrchestrationEvidence({ taskDir }) {
298
315
  && receipt.reasoningEffortFallbackReason !== null) {
299
316
  return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has an unrelated reasoning-effort fallback reason`);
300
317
  }
318
+ if (receipt.client === 'codex') {
319
+ const host = receipt.hostEvidence;
320
+ if (host?.kind !== 'codex-lifecycle-v1'
321
+ || !exactText(host.hookDefinitionHash)
322
+ || !Number.isSafeInteger(host.startRevision)
323
+ || host.startRevision < 1) {
324
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex start evidence`);
325
+ }
326
+ if (['sealed', 'consumed'].includes(receipt.status) && (!Number.isSafeInteger(host.stopRevision)
327
+ || host.stopRevision <= host.startRevision
328
+ || host.consumer !== receipt.id
329
+ || !exactText(host.consumedAt))) {
330
+ return failResult('orchestration-evidence', `Receipt '${receipt.id ?? '(unknown)'}' has invalid Codex consumed stop evidence`);
331
+ }
332
+ }
301
333
  }
302
334
  }
303
335
  if (run.receipts.some((receipt) => receipt.status !== 'consumed')) {
@@ -16,6 +16,7 @@ const VERIFICATION_CATALOG = {
16
16
  'code.completed': gate('code-task', 'active', 'code'),
17
17
  'review-code.completed': gate('review-code', 'active', 'review-code'),
18
18
  'manual-validation.completed': gate('complete-manual-validation', 'active', 'manual-validation'),
19
+ 'validation-run.completed': gate('run-manual-validation', 'active', 'validation-run'),
19
20
  'block-task.completed': gate('block-task', 'blocked'),
20
21
  'cancel-task.completed': gate('cancel-task', 'completed'),
21
22
  'commit.completed': gate('commit', 'active'),
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  import fs from 'node:fs';
2
3
  import os from 'node:os';
3
4
  import path from 'node:path';
@@ -25,12 +26,70 @@ function captureWorktreeTree(repoRoot, forcedPath) {
25
26
  fs.rmSync(tempDir, { recursive: true, force: true });
26
27
  }
27
28
  }
28
- function captureWorkspaceSnapshot(repoRoot, taskId) {
29
+ function captureLegacyWorkspaceSnapshot(repoRoot, taskId) {
29
30
  const workspaceRelative = taskId
30
31
  ? `.agents/workspace/active/${taskId}`
31
32
  : '.agents/workspace/active';
32
33
  return captureWorktreeTree(repoRoot, workspaceRelative);
33
34
  }
35
+ function taskFileFingerprints(stateRoot, taskId) {
36
+ const activeRoot = path.join(stateRoot, '.agents', 'workspace', 'active');
37
+ const roots = taskId ? [path.join(activeRoot, taskId)] : [activeRoot];
38
+ const records = [];
39
+ const visit = (absolute, relative) => {
40
+ const stat = fs.lstatSync(absolute);
41
+ if (stat.isDirectory()) {
42
+ for (const name of fs.readdirSync(absolute).sort()) {
43
+ visit(path.join(absolute, name), path.posix.join(relative, name));
44
+ }
45
+ return;
46
+ }
47
+ if (!stat.isFile() && !stat.isSymbolicLink())
48
+ return;
49
+ const content = stat.isSymbolicLink()
50
+ ? Buffer.from(fs.readlinkSync(absolute))
51
+ : fs.readFileSync(absolute);
52
+ records.push({
53
+ path: relative,
54
+ mode: stat.mode & 0o777,
55
+ kind: stat.isSymbolicLink() ? 'symlink' : 'file',
56
+ sha256: createHash('sha256').update(content).digest('hex')
57
+ });
58
+ };
59
+ for (const root of roots) {
60
+ if (!fs.existsSync(root))
61
+ continue;
62
+ const relative = path.posix.join('.agents/workspace/active', path.relative(activeRoot, root).split(path.sep).join('/'));
63
+ visit(root, relative.replace(/\/$/, ''));
64
+ }
65
+ return records.sort((left, right) => left.path.localeCompare(right.path));
66
+ }
67
+ function encodeWorkspaceFingerprint(value) {
68
+ return `ws2:${Buffer.from(JSON.stringify(value)).toString('base64url')}`;
69
+ }
70
+ function decodeWorkspaceFingerprint(value) {
71
+ if (!value.startsWith('ws2:'))
72
+ return null;
73
+ try {
74
+ const parsed = JSON.parse(Buffer.from(value.slice(4), 'base64url').toString('utf8'));
75
+ if (parsed.version !== 2 || !/^[a-f0-9]{40,64}$/.test(parsed.gitTree) || !Array.isArray(parsed.taskFiles)) {
76
+ throw new Error('invalid workspace fingerprint fields');
77
+ }
78
+ return parsed;
79
+ }
80
+ catch (error) {
81
+ throw new Error(`ORCHESTRATION_SNAPSHOT_FAILED: ${error instanceof Error ? error.message : String(error)}`);
82
+ }
83
+ }
84
+ function captureWorkspaceSnapshot(contextOrRoot, taskId) {
85
+ if (typeof contextOrRoot === 'string')
86
+ return captureLegacyWorkspaceSnapshot(contextOrRoot, taskId ?? null);
87
+ return encodeWorkspaceFingerprint({
88
+ version: 2,
89
+ gitTree: captureWorktreeTree(contextOrRoot.gitRoot, null),
90
+ taskFiles: taskFileFingerprints(contextOrRoot.stateRoot, contextOrRoot.taskId)
91
+ });
92
+ }
34
93
  function captureRepositorySnapshot(repoRoot) {
35
94
  return {
36
95
  head: git(repoRoot, null, ['rev-parse', 'HEAD']).trim(),
@@ -39,8 +98,24 @@ function captureRepositorySnapshot(repoRoot) {
39
98
  };
40
99
  }
41
100
  function diffWorkspaceSnapshots(repoRoot, before, after) {
42
- const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
43
- return output.split('\0').filter(Boolean);
101
+ const beforeV2 = decodeWorkspaceFingerprint(before);
102
+ const afterV2 = decodeWorkspaceFingerprint(after);
103
+ if ((beforeV2 === null) !== (afterV2 === null)) {
104
+ throw new Error('ORCHESTRATION_SNAPSHOT_FAILED: workspace fingerprint versions cannot be mixed');
105
+ }
106
+ if (!beforeV2 || !afterV2) {
107
+ const output = git(repoRoot, null, ['diff', '--name-only', '-z', before, after]);
108
+ return output.split('\0').filter(Boolean);
109
+ }
110
+ const output = git(repoRoot, null, ['diff', '--name-only', '-z', beforeV2.gitTree, afterV2.gitTree]);
111
+ const changed = new Set(output.split('\0').filter(Boolean));
112
+ const beforeFiles = new Map(beforeV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
113
+ const afterFiles = new Map(afterV2.taskFiles.map((record) => [record.path, JSON.stringify(record)]));
114
+ for (const filePath of new Set([...beforeFiles.keys(), ...afterFiles.keys()])) {
115
+ if (beforeFiles.get(filePath) !== afterFiles.get(filePath))
116
+ changed.add(filePath);
117
+ }
118
+ return [...changed].sort();
44
119
  }
45
120
  export { captureRepositorySnapshot, captureWorkspaceSnapshot, diffWorkspaceSnapshots };
46
121
  //# sourceMappingURL=workspace-snapshot.js.map