@fitlab-ai/agent-infra 0.9.4 → 0.9.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +10 -0
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  9. package/dist/lib/git/worktree-identity.js +64 -0
  10. package/dist/lib/internal/codex-lifecycle.js +202 -0
  11. package/dist/lib/internal/task-create.js +48 -0
  12. package/dist/lib/internal/task-orchestration.js +68 -29
  13. package/dist/lib/sandbox/commands/create.js +1 -0
  14. package/dist/lib/sandbox/control/client.js +29 -15
  15. package/dist/lib/sandbox/control/protocol.js +23 -4
  16. package/dist/lib/sandbox/control/server.js +53 -18
  17. package/dist/lib/sandbox/workspace-view.js +8 -2
  18. package/dist/lib/task/activity-log.js +32 -5
  19. package/dist/lib/task/commit-finalization.js +14 -3
  20. package/dist/lib/task/create-service.js +137 -0
  21. package/dist/lib/task/create.js +333 -0
  22. package/dist/lib/task/orchestration.js +151 -15
  23. package/dist/lib/task/review-fingerprint.js +4 -0
  24. package/dist/lib/task/workspace-snapshot.js +78 -3
  25. package/lib/agent-clients/adapter.ts +4 -4
  26. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  27. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  28. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  29. package/lib/agent-clients/adapters/codex.ts +2 -2
  30. package/lib/git/worktree-identity.ts +86 -0
  31. package/lib/internal/codex-lifecycle.ts +204 -0
  32. package/lib/internal/task-create.ts +49 -0
  33. package/lib/internal/task-orchestration.ts +54 -28
  34. package/lib/sandbox/commands/create.ts +1 -0
  35. package/lib/sandbox/control/client.ts +42 -18
  36. package/lib/sandbox/control/protocol.ts +43 -8
  37. package/lib/sandbox/control/server.ts +60 -22
  38. package/lib/sandbox/workspace-view.ts +9 -2
  39. package/lib/task/activity-log.ts +46 -7
  40. package/lib/task/commit-finalization.ts +23 -3
  41. package/lib/task/create-service.ts +171 -0
  42. package/lib/task/create.ts +372 -0
  43. package/lib/task/orchestration.ts +182 -18
  44. package/lib/task/review-fingerprint.ts +9 -0
  45. package/lib/task/workspace-snapshot.ts +98 -4
  46. package/package.json +7 -7
  47. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  48. package/templates/.agents/rules/task-management.en.md +1 -0
  49. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  50. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  51. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  52. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  53. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  54. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  55. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  56. package/templates/.codex/hooks.json +50 -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) {
@@ -399,6 +407,13 @@ function routeOrchestration(taskRef, options = {}) {
399
407
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
400
408
  if (!resolved.ok)
401
409
  return failed(resolved.code, resolved.message, resolved.taskId);
410
+ let gitRoot;
411
+ try {
412
+ gitRoot = gitRootFor(resolved.repoRoot, options);
413
+ }
414
+ catch (error) {
415
+ return failed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
416
+ }
402
417
  let content;
403
418
  try {
404
419
  content = fs.readFileSync(resolved.taskMdPath, 'utf8');
@@ -469,7 +484,7 @@ function routeOrchestration(taskRef, options = {}) {
469
484
  const capture = options.captureRepository ?? captureRepositorySnapshot;
470
485
  let before;
471
486
  try {
472
- before = capture(resolved.repoRoot);
487
+ before = capture(gitRoot);
473
488
  }
474
489
  catch (error) {
475
490
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -484,7 +499,7 @@ function routeOrchestration(taskRef, options = {}) {
484
499
  const inspect = options.inspectPullRequest ?? inspectPlatformPullRequest;
485
500
  let inspection;
486
501
  try {
487
- inspection = inspect(resolved.taskId, { cwd: resolved.repoRoot });
502
+ inspection = inspect(resolved.taskId, { cwd: gitRoot });
488
503
  }
489
504
  catch (error) {
490
505
  return failed('ORCHESTRATION_PR_INSPECTION_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -505,7 +520,7 @@ function routeOrchestration(taskRef, options = {}) {
505
520
  }
506
521
  let after;
507
522
  try {
508
- after = capture(resolved.repoRoot);
523
+ after = capture(gitRoot);
509
524
  }
510
525
  catch (error) {
511
526
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -538,13 +553,101 @@ function routeOrchestration(taskRef, options = {}) {
538
553
  }
539
554
  return { status: 'running', changed: false, taskId: resolved.taskId, run, next, error: null };
540
555
  }
556
+ function appendCommitActivity(resolved, step, agent, note) {
557
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
558
+ const activity = locateActivityLog(taskContent);
559
+ if (!activity)
560
+ return { code: 'ORCHESTRATION_TASK_INVALID', message: 'task activity log is missing or ambiguous' };
561
+ const metadata = captureTaskWriteMetadata();
562
+ const written = writeTask({
563
+ taskRef: resolved.taskId,
564
+ expectedState: 'active',
565
+ mutations: [{
566
+ kind: 'section',
567
+ aliases: ['活动日志', 'Activity Log'],
568
+ heading: activity.heading,
569
+ body: appendActivityEntry(activity, { time: metadata.timestamp, step, agent, note })
570
+ }]
571
+ }, { repoRoot: resolved.repoRoot, metadataProvider: () => metadata });
572
+ return written.status === 'failed' ? written.error : null;
573
+ }
574
+ function startCommitAttempt(taskRef, input, options = {}) {
575
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
576
+ if (!resolved.ok)
577
+ return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
578
+ try {
579
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
580
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.start', () => {
581
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
582
+ return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
583
+ }
584
+ const taskContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
585
+ const activity = locateActivityLog(taskContent);
586
+ if (!activity)
587
+ return commitIntentFailed('ORCHESTRATION_TASK_INVALID', 'task activity log is missing or ambiguous', resolved.taskId);
588
+ const open = pairEntries(activity.entries).filter((row) => row.step === 'Commit' && row.started !== '' && row.done === '');
589
+ if (open.length > 0) {
590
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_BUSY', 'an open Commit activity entry already exists', resolved.taskId);
591
+ }
592
+ const currentHead = repositoryHead(gitRoot);
593
+ const attempt = (options.id ?? randomUUID)();
594
+ const error = appendCommitActivity(resolved, 'Commit [started]', input.agent, commitAttemptStartedNote({
595
+ attempt,
596
+ baseline: currentHead,
597
+ agent: input.agent
598
+ }));
599
+ if (error)
600
+ return commitIntentFailed(error.code, error.message, resolved.taskId);
601
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
602
+ return {
603
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
604
+ finalization: commitFinalizationView(inspection), error: null
605
+ };
606
+ });
607
+ }
608
+ catch (error) {
609
+ return mapCommitIntentError(error, resolved.taskId);
610
+ }
611
+ }
612
+ function terminateCommitAttempt(taskRef, input, options = {}) {
613
+ const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
614
+ if (!resolved.ok)
615
+ return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
616
+ try {
617
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
618
+ return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-attempt.terminate', () => {
619
+ if (fs.existsSync(commitIntentPath(resolved.taskDir))) {
620
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'an active commit intent must be aborted or recovered first', resolved.taskId);
621
+ }
622
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
623
+ if (inspection.disposition !== 'retryable-start'
624
+ || inspection.attempt?.attempt !== input.attempt
625
+ || inspection.attempt.agent !== input.agent
626
+ || inspection.attempt.baseline !== inspection.currentHead) {
627
+ return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'commit attempt identity or repository HEAD has drifted', resolved.taskId);
628
+ }
629
+ const error = appendCommitActivity(resolved, 'Commit [aborted]', input.agent, `aborted; attempt=${input.attempt}; code=${input.code}`);
630
+ if (error)
631
+ return commitIntentFailed(error.code, error.message, resolved.taskId);
632
+ return {
633
+ status: 'ready', changed: true, taskId: resolved.taskId, intent: null,
634
+ finalization: commitFinalizationView(inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId)),
635
+ error: null
636
+ };
637
+ });
638
+ }
639
+ catch (error) {
640
+ return mapCommitIntentError(error, resolved.taskId);
641
+ }
642
+ }
541
643
  function beginCommitIntent(taskRef, input, options = {}) {
542
644
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
543
645
  if (!resolved.ok)
544
646
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
545
647
  try {
648
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
546
649
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.begin', () => {
547
- const currentHead = repositoryHead(resolved.repoRoot);
650
+ const currentHead = repositoryHead(gitRoot);
548
651
  if (currentHead !== input.baselineHead) {
549
652
  return commitIntentFailed('ORCHESTRATION_COMMIT_BASELINE_MISMATCH', 'baseline HEAD does not match the repository HEAD', resolved.taskId);
550
653
  }
@@ -557,6 +660,13 @@ function beginCommitIntent(taskRef, input, options = {}) {
557
660
  }
558
661
  return commitIntentFailed('ORCHESTRATION_COMMIT_INTENT_BUSY', 'an active commit intent already exists', resolved.taskId);
559
662
  }
663
+ const attemptInspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
664
+ if (attemptInspection.disposition !== 'retryable-start'
665
+ || attemptInspection.attempt?.attempt !== input.attempt
666
+ || attemptInspection.attempt.agent !== input.agent
667
+ || attemptInspection.attempt.baseline !== input.baselineHead) {
668
+ return commitIntentFailed('ORCHESTRATION_COMMIT_ATTEMPT_MISMATCH', 'commit begin requires one matching retryable Commit attempt', resolved.taskId);
669
+ }
560
670
  const runFile = orchestrationPath(resolved.taskDir);
561
671
  let run = null;
562
672
  let sourceRunBytes = null;
@@ -646,13 +756,14 @@ function checkpointCommitIntent(taskRef, input, options = {}) {
646
756
  if (!resolved.ok)
647
757
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
648
758
  try {
759
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
649
760
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.checkpoint', () => {
650
761
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
651
- const currentHead = repositoryHead(resolved.repoRoot);
762
+ const currentHead = repositoryHead(gitRoot);
652
763
  if (currentHead !== input.head) {
653
764
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'checkpoint HEAD does not match the repository HEAD', resolved.taskId);
654
765
  }
655
- if (input.kind === 'committed' && !isAncestor(resolved.repoRoot, intent.baselineHead, input.head)) {
766
+ if (input.kind === 'committed' && !isAncestor(gitRoot, intent.baselineHead, input.head)) {
656
767
  return commitIntentFailed('ORCHESTRATION_COMMIT_RECOVERY_REQUIRED', 'committed HEAD does not descend from the baseline', resolved.taskId);
657
768
  }
658
769
  if (input.kind === 'pushed') {
@@ -750,9 +861,10 @@ function completeCommitIntent(taskRef, input, options = {}) {
750
861
  if (!resolved.ok)
751
862
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
752
863
  try {
864
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
753
865
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.complete', () => {
754
866
  readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
755
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
867
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
756
868
  return finalizeCommitIntentUnlocked(resolved, inspection, input.agent, () => {
757
869
  removeCommitIntent(resolved.taskDir, resolved.taskId, input.token);
758
870
  });
@@ -767,8 +879,15 @@ function recoverCommitIntent(taskRef, input, options = {}) {
767
879
  if (!resolved.ok)
768
880
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
769
881
  try {
882
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
770
883
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.recover', () => {
771
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
884
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
885
+ if (inspection.disposition === 'retryable-start') {
886
+ return {
887
+ status: 'ready', changed: false, taskId: resolved.taskId, intent: null,
888
+ finalization: commitFinalizationView(inspection), error: null
889
+ };
890
+ }
772
891
  if (inspection.disposition === 'prepared' && inspection.intentDigest) {
773
892
  removeCommitIntentByDigest(resolved.taskDir, resolved.taskId, inspection.intentDigest);
774
893
  return { status: 'ready', changed: true, taskId: resolved.taskId, intent: null, error: null };
@@ -790,9 +909,10 @@ function abortCommitIntent(taskRef, input, options = {}) {
790
909
  if (!resolved.ok)
791
910
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
792
911
  try {
912
+ const gitRoot = gitRootFor(resolved.repoRoot, options);
793
913
  return withTaskExecutionLock(resolved.repoRoot, resolved.taskId, 'commit-intent.abort', () => {
794
914
  const intent = readCommitIntent(resolved.taskDir, resolved.taskId, input.token);
795
- const currentHead = repositoryHead(resolved.repoRoot);
915
+ const currentHead = repositoryHead(gitRoot);
796
916
  if (intent.phase !== 'prepared'
797
917
  || intent.pushEvidence !== null
798
918
  || input.expectedHead !== intent.baselineHead
@@ -811,7 +931,14 @@ function statusCommitIntent(taskRef, options = {}) {
811
931
  const resolved = resolveTaskRef(taskRef, { repoRoot: options.repoRoot });
812
932
  if (!resolved.ok)
813
933
  return commitIntentFailed(resolved.code, resolved.message, resolved.taskId);
814
- const inspection = inspectCommitFinalization(resolved.taskDir, resolved.repoRoot, resolved.taskId);
934
+ let gitRoot;
935
+ try {
936
+ gitRoot = gitRootFor(resolved.repoRoot, options);
937
+ }
938
+ catch (error) {
939
+ return commitIntentFailed('ORCHESTRATION_WORKTREE_INVALID', error instanceof Error ? error.message : String(error), resolved.taskId);
940
+ }
941
+ const inspection = inspectCommitFinalization(resolved.taskDir, gitRoot, resolved.taskId);
815
942
  return {
816
943
  status: 'ready',
817
944
  changed: false,
@@ -878,7 +1005,11 @@ function prepareOrchestrationDelegationUnlocked(taskRef, input, options = {}) {
878
1005
  }
879
1006
  let beforeFingerprint;
880
1007
  try {
881
- beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)(resolved.repoRoot, resolved.taskId);
1008
+ beforeFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1009
+ gitRoot: gitRootFor(resolved.repoRoot, options),
1010
+ stateRoot: resolved.repoRoot,
1011
+ taskId: resolved.taskId
1012
+ });
882
1013
  }
883
1014
  catch (error) {
884
1015
  return failed('ORCHESTRATION_SNAPSHOT_FAILED', error instanceof Error ? error.message : String(error), resolved.taskId);
@@ -969,9 +1100,14 @@ function sealMatchingOrchestrationDelegation(client, event, options = {}) {
969
1100
  }
970
1101
  const repoRoot = options.repoRoot ?? process.cwd();
971
1102
  try {
1103
+ const gitRoot = gitRootFor(repoRoot, options);
972
1104
  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);
1105
+ const afterFingerprint = (options.captureWorkspace ?? captureWorkspaceSnapshot)({
1106
+ gitRoot,
1107
+ stateRoot: repoRoot,
1108
+ taskId: snapshotTaskId
1109
+ });
1110
+ const changedPaths = (options.diffWorkspace ?? diffWorkspaceSnapshots)(gitRoot, receipt.beforeFingerprint, afterFingerprint);
975
1111
  return sealOrchestrationDelegation(matched.taskId, {
976
1112
  childId: event.childId,
977
1113
  exitCode: 0,
@@ -1115,5 +1251,5 @@ function pauseOrchestration(taskRef, code, message, recoverable, options = {}) {
1115
1251
  saveRun(resolved.taskDir, updated);
1116
1252
  return { status: 'paused', changed: true, taskId: resolved.taskId, run: updated, next: null, error: null };
1117
1253
  }
1118
- export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration };
1254
+ export { activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, abortCommitIntent, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, commitOrchestrationStageCompletion, completeCommitIntent, completeOrchestrationStage, inspectOrchestrationStage, orchestrationPath, pauseOrchestration, planOrchestrationStageCompletion, prepareOrchestrationDelegation, readRun, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt };
1119
1255
  //# 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.
@@ -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
@@ -39,8 +39,8 @@ type AgentClientModelSelectionContext =
39
39
  }>;
40
40
 
41
41
  type AgentClientDelegationEvidence = Readonly<{
42
- actualModel: 'host-event' | 'unavailable';
43
- actualReasoningEffort: 'host-event' | 'spawn-ack' | 'unavailable';
42
+ actualModel: 'host-event' | 'app-server' | 'unavailable';
43
+ actualReasoningEffort: 'host-event' | 'app-server' | 'spawn-ack' | 'unavailable';
44
44
  }>;
45
45
 
46
46
  type AgentClientSeedCommand = Readonly<{
@@ -331,8 +331,8 @@ function defineAgentClientAdapter(
331
331
  const evidence = candidate.delegationEvidence;
332
332
  if (
333
333
  !evidence
334
- || !['host-event', 'unavailable'].includes(evidence.actualModel)
335
- || !['host-event', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)
334
+ || !['host-event', 'app-server', 'unavailable'].includes(evidence.actualModel)
335
+ || !['host-event', 'app-server', 'spawn-ack', 'unavailable'].includes(evidence.actualReasoningEffort)
336
336
  ) {
337
337
  throw new Error(`Agent Client '${candidate.id}' has invalid delegation evidence`);
338
338
  }