@fitlab-ai/agent-infra 0.11.3 → 0.11.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 (172) hide show
  1. package/README.md +17 -8
  2. package/README.zh-CN.md +17 -8
  3. package/dist/bin/internal-cli.js +1 -6
  4. package/dist/lib/decide.js +4 -41
  5. package/dist/lib/git/branch-worktree.js +45 -0
  6. package/dist/lib/internal/cli-route-inventory.js +1 -4
  7. package/dist/lib/internal/task-activity.js +7 -40
  8. package/dist/lib/internal/task-artifact.js +1 -1
  9. package/dist/lib/internal/task-event.js +4 -37
  10. package/dist/lib/internal/task-finalization.js +48 -1
  11. package/dist/lib/internal/task-ledger.js +14 -44
  12. package/dist/lib/internal/task-lifecycle.js +1 -1
  13. package/dist/lib/internal/task-operation-registry.js +2 -4
  14. package/dist/lib/internal/task-review.js +5 -37
  15. package/dist/lib/internal/task-verify.js +3 -47
  16. package/dist/lib/internal/task-warning.js +4 -40
  17. package/dist/lib/platform/completion-backfill.js +63 -17
  18. package/dist/lib/platform/pr-summary.js +4 -23
  19. package/dist/lib/platform/verification-sync.js +12 -49
  20. package/dist/lib/sandbox/control/client.js +4 -1
  21. package/dist/lib/sandbox/control/executor.js +12 -4
  22. package/dist/lib/sandbox/control/protocol.js +2 -1
  23. package/dist/lib/sandbox/control/server.js +26 -143
  24. package/dist/lib/sandbox/removal.js +15 -8
  25. package/dist/lib/sandbox/task-cleanup.js +13 -20
  26. package/dist/lib/task/activity-intent.js +1 -2
  27. package/dist/lib/task/artifact-command.js +11 -4
  28. package/dist/lib/task/artifact-lifecycle.js +217 -85
  29. package/dist/lib/task/artifact-operations.js +37 -28
  30. package/dist/lib/task/artifact-receipts.js +4 -1
  31. package/dist/lib/task/artifact-schema.js +8 -7
  32. package/dist/lib/task/artifact-selection.js +25 -0
  33. package/dist/lib/task/capabilities.js +158 -39
  34. package/dist/lib/task/commands/status.js +9 -2
  35. package/dist/lib/task/control-authority.js +12 -168
  36. package/dist/lib/task/decision-intents.js +14 -4
  37. package/dist/lib/task/delivery.js +1 -43
  38. package/dist/lib/task/events.js +68 -73
  39. package/dist/lib/task/finalization-handoff.js +92 -0
  40. package/dist/lib/task/finalization.js +423 -154
  41. package/dist/lib/task/intermediate-cleanup.js +9 -148
  42. package/dist/lib/task/ledger-intents.js +159 -20
  43. package/dist/lib/task/lifecycle-path.js +75 -0
  44. package/dist/lib/task/lifecycle.js +60 -11
  45. package/dist/lib/task/local-artifact-finalization.js +13 -47
  46. package/dist/lib/task/orchestration.js +17 -7
  47. package/dist/lib/task/review-command.js +1 -2
  48. package/dist/lib/task/review-finalization.js +4 -18
  49. package/dist/lib/task/rework-intent.js +70 -4
  50. package/dist/lib/task/snapshot.js +6 -6
  51. package/dist/lib/task/verification-engine.js +25 -30
  52. package/dist/lib/task/verification.js +2 -13
  53. package/dist/lib/task/workflow-warning-intents.js +23 -10
  54. package/dist/lib/task/workspace-snapshot.js +7 -2
  55. package/dist/lib/task/write.js +1 -2
  56. package/dist/lifecycle-build-manifest.json +3 -3
  57. package/lib/decide.ts +5 -35
  58. package/lib/git/branch-worktree.ts +41 -0
  59. package/lib/internal/cli-route-inventory.ts +1 -3
  60. package/lib/internal/task-activity.ts +6 -34
  61. package/lib/internal/task-artifact.ts +1 -1
  62. package/lib/internal/task-event.ts +4 -37
  63. package/lib/internal/task-finalization.ts +47 -1
  64. package/lib/internal/task-ledger.ts +15 -41
  65. package/lib/internal/task-lifecycle.ts +2 -2
  66. package/lib/internal/task-operation-registry.ts +2 -4
  67. package/lib/internal/task-review.ts +5 -31
  68. package/lib/internal/task-verify.ts +3 -39
  69. package/lib/internal/task-warning.ts +3 -32
  70. package/lib/platform/completion-backfill.ts +78 -16
  71. package/lib/platform/pr-summary.ts +4 -25
  72. package/lib/platform/verification-sync.ts +13 -53
  73. package/lib/sandbox/control/client.ts +5 -1
  74. package/lib/sandbox/control/executor.ts +13 -4
  75. package/lib/sandbox/control/protocol.ts +3 -2
  76. package/lib/sandbox/control/server.ts +21 -146
  77. package/lib/sandbox/removal.ts +23 -7
  78. package/lib/sandbox/task-cleanup.ts +12 -20
  79. package/lib/task/activity-intent.ts +1 -2
  80. package/lib/task/artifact-command.ts +14 -4
  81. package/lib/task/artifact-lifecycle.ts +236 -85
  82. package/lib/task/artifact-operations.ts +59 -28
  83. package/lib/task/artifact-receipts.ts +4 -1
  84. package/lib/task/artifact-schema.ts +10 -7
  85. package/lib/task/artifact-selection.ts +53 -0
  86. package/lib/task/capabilities.ts +154 -40
  87. package/lib/task/commands/status.ts +11 -2
  88. package/lib/task/control-authority.ts +17 -189
  89. package/lib/task/decision-intents.ts +11 -4
  90. package/lib/task/delivery.ts +1 -40
  91. package/lib/task/events.ts +73 -74
  92. package/lib/task/finalization-handoff.ts +104 -0
  93. package/lib/task/finalization.ts +388 -122
  94. package/lib/task/intermediate-cleanup.ts +9 -125
  95. package/lib/task/ledger-intents.ts +157 -20
  96. package/lib/task/lifecycle-path.ts +98 -0
  97. package/lib/task/lifecycle.ts +58 -13
  98. package/lib/task/local-artifact-finalization.ts +14 -56
  99. package/lib/task/orchestration.ts +26 -8
  100. package/lib/task/review-command.ts +2 -5
  101. package/lib/task/review-finalization.ts +4 -20
  102. package/lib/task/rework-intent.ts +75 -5
  103. package/lib/task/snapshot.ts +6 -6
  104. package/lib/task/verification-engine.ts +29 -30
  105. package/lib/task/verification.ts +3 -16
  106. package/lib/task/workflow-warning-intents.ts +20 -10
  107. package/lib/task/workspace-snapshot.ts +3 -2
  108. package/lib/task/write.ts +1 -4
  109. package/package.json +1 -1
  110. package/templates/.agents/rules/review-handshake.en.md +4 -4
  111. package/templates/.agents/rules/review-handshake.zh-CN.md +4 -4
  112. package/templates/.agents/skills/analyze-task/SKILL.en.md +21 -5
  113. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +21 -5
  114. package/templates/.agents/skills/code-task/SKILL.en.md +14 -8
  115. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +18 -13
  116. package/templates/.agents/skills/code-task/reference/code-rules.en.md +2 -2
  117. package/templates/.agents/skills/code-task/reference/code-rules.zh-CN.md +2 -2
  118. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +4 -4
  119. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +4 -4
  120. package/templates/.agents/skills/code-task/reference/report-template.en.md +7 -7
  121. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +7 -7
  122. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  123. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  124. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  125. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  126. package/templates/.agents/skills/complete-task/SKILL.en.md +2 -2
  127. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +2 -2
  128. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  129. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  130. package/templates/.agents/skills/plan-task/SKILL.en.md +13 -10
  131. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +13 -10
  132. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -1
  133. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -1
  134. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  135. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  136. package/templates/.agents/skills/review-code/SKILL.en.md +5 -1
  137. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -1
  138. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  139. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  140. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -1
  141. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -1
  142. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  143. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  144. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  145. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  146. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  147. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  148. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  149. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  150. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  151. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  152. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +18 -2
  153. package/templates/.agents/templates/task.en.md +2 -2
  154. package/templates/.agents/templates/task.zh-CN.md +2 -2
  155. package/templates/.agents/workflows/bug-fix.en.yaml +11 -6
  156. package/templates/.agents/workflows/bug-fix.zh-CN.yaml +11 -6
  157. package/templates/.agents/workflows/feature-development.en.yaml +11 -6
  158. package/templates/.agents/workflows/feature-development.zh-CN.yaml +11 -6
  159. package/templates/.agents/workflows/refactoring.en.yaml +11 -6
  160. package/templates/.agents/workflows/refactoring.zh-CN.yaml +11 -6
  161. package/dist/lib/internal/task-override.js +0 -138
  162. package/dist/lib/sandbox/control/finalization-response.js +0 -33
  163. package/dist/lib/task/artifact-recovery.js +0 -476
  164. package/dist/lib/task/artifact-repair-intent.js +0 -181
  165. package/dist/lib/task/guard-override.js +0 -31
  166. package/dist/lib/task/human-override.js +0 -1259
  167. package/lib/internal/task-override.ts +0 -144
  168. package/lib/sandbox/control/finalization-response.ts +0 -35
  169. package/lib/task/artifact-recovery.ts +0 -572
  170. package/lib/task/artifact-repair-intent.ts +0 -227
  171. package/lib/task/guard-override.ts +0 -50
  172. package/lib/task/human-override.ts +0 -1522
@@ -2,7 +2,7 @@ import { createDirectHostExecutionContext, dispatchTaskControlOperation, parseTa
2
2
  import { lifecycleIntentCatalog } from '../task/lifecycle.js';
3
3
  import { detectRepoRoot, resolveTaskRef } from '../task/resolve-ref.js';
4
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
5
- const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\nOverride: --override-ticket <ticket> --override-target <target> --override-scope <scope>\n`;
5
+ const USAGE = `Usage: agent-infra-internal task-lifecycle <N | TASK-id> <intent> --agent <agent> [intent flags] [--dry-run]\n\nIntents: ${lifecycleIntentCatalog.join(', ')}\nrecover-started: --auto\n`;
6
6
  function usageFailure(message) {
7
7
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'LIFECYCLE_PAYLOAD_INVALID', message } })}\n`);
8
8
  process.stderr.write(USAGE);
@@ -85,6 +85,7 @@ function internalTaskRoutes() {
85
85
  descriptor('internal', 'task-ledger', 'finding-review', 'task-bound', 'progress'),
86
86
  descriptor('internal', 'task-ledger', 'decision-upsert', 'task-bound', 'progress'),
87
87
  descriptor('internal', 'task-ledger', 'rework-intent-upsert', 'task-bound', 'progress'),
88
+ descriptor('internal', 'task-ledger', 'rework-intent-rebuild', 'task-bound', 'progress'),
88
89
  descriptor('internal', 'task-warning', 'list', 'task-bound', 'diagnostic'),
89
90
  descriptor('internal', 'task-warning', 'add', 'task-bound', 'progress'),
90
91
  descriptor('internal', 'task-warning', 'set-status', 'task-bound', 'progress'),
@@ -102,9 +103,6 @@ function internalTaskRoutes() {
102
103
  descriptor('internal', 'task-review', 'finalize-summary', 'task-bound', 'progress'),
103
104
  descriptor('internal', 'task-event', 'event', 'task-bound', 'progress'),
104
105
  descriptor('internal', 'task-invalidation', 'reconcile', 'task-bound', 'progress'),
105
- descriptor('internal', 'task-override', 'diagnose', 'task-bound', 'diagnostic'),
106
- descriptor('internal', 'task-override', 'issue', 'task-bound', 'progress'),
107
- descriptor('internal', 'task-override', 'consume', 'task-bound', 'progress'),
108
106
  descriptor('internal', 'task-lifecycle', 'intent', 'task-bound', 'progress'),
109
107
  descriptor('internal', 'task-finalization', 'complete', 'task-bound', 'progress'),
110
108
  descriptor('internal', 'task-short-id', 'list', 'conditional', 'diagnostic'),
@@ -180,7 +178,7 @@ export const INTERNAL_DISPATCHER_ROUTES = Object.freeze([
180
178
  'platform-comment', 'platform-issue', 'platform-pr', 'platform-pr-review', 'pr-review-grade',
181
179
  'platform-checks', 'task-context', 'task-ledger', 'task-warning', 'task-activity', 'task-artifact',
182
180
  'task-orchestration', 'task-review', 'task-event', 'task-invalidation', 'task-lifecycle',
183
- 'task-finalization', 'task-override', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
181
+ 'task-finalization', 'task-short-id', 'task-snapshot', 'task-verify', 'task-validate', 'manual-validation'
184
182
  ]);
185
183
  export const PUBLIC_DISPATCHER_ROUTES = Object.freeze([
186
184
  'agent-client', 'cp', 'data', 'decide', 'help', 'init', 'merge', 'run', 'sandbox', 'server', 'task', 'sync', 'update', 'version'
@@ -1,10 +1,9 @@
1
1
  import { parseReviewCommand } from '../task/review-command.js';
2
2
  import { finalizeReviewSummary, preflightReviewSummary } from '../task/review-finalization.js';
3
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
4
3
  import { resolveTaskRef } from '../task/resolve-ref.js';
5
4
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
6
5
  import { ensureInternalHandlerRoute } from './cli-route-inventory.js';
7
- const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
6
+ const USAGE = 'Usage: agent-infra-internal task-review <task-ref> <preflight|finalize-summary> --stage <analysis|plan|code> --artifact <review-*.md> [--orchestrated] [--dry-run]\n';
8
7
  function failUsage(message) {
9
8
  process.stdout.write(`${JSON.stringify({
10
9
  status: 'failed',
@@ -30,48 +29,19 @@ async function taskReview(args = []) {
30
29
  failUsage(error instanceof Error ? error.message : String(error));
31
30
  return;
32
31
  }
33
- const { stage, artifact, dryRun, orchestrated, overrideTicket, overrideTarget, overrideScope } = request;
32
+ const { stage, artifact, dryRun, orchestrated } = request;
34
33
  const operation = args[1];
35
- const dryRunConflict = overrideDryRunConflict({ dryRun, overrideTicket, overrideTarget, overrideScope });
36
- if (dryRunConflict) {
37
- failUsage(dryRunConflict.message);
38
- return;
39
- }
40
34
  const resolved = resolveTaskRef(args[0]);
41
35
  if (!resolved.ok) {
42
36
  failUsage(resolved.message);
43
37
  return;
44
38
  }
45
39
  let result;
46
- let humanOverride = null;
47
40
  try {
48
41
  result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-review.${operation}`, async () => {
49
- let current = operation === 'preflight'
42
+ return operation === 'preflight'
50
43
  ? preflightReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true })
51
44
  : finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true });
52
- if (operation === 'preflight')
53
- return current;
54
- if (current.status !== 'failed' || !overrideTicket)
55
- return current;
56
- if (!overrideTarget || !overrideScope) {
57
- failUsage('override ticket requires target and scope');
58
- return current;
59
- }
60
- const consumed = await consumeHumanOverride({
61
- taskRef: args[0], ticketId: overrideTicket,
62
- failureId: failureId('review-finalization', current.error?.code ?? 'TASK_STATE_MISMATCH'),
63
- target: overrideTarget, scope: overrideScope
64
- }, {
65
- effectExecutor: (capability) => {
66
- const retried = finalizeReviewSummary({ taskRef: args[0], stage, artifact, orchestrated, dryRun }, { lockAlreadyHeld: true, manualOverride: capability });
67
- current = retried;
68
- return retried.status === 'failed' || retried.status === 'planned'
69
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no review effect was committed' : `${retried.error?.code ?? 'REVIEW_FAILED'}: ${retried.error?.message ?? 'manual review effect failed'}` }
70
- : null;
71
- }
72
- });
73
- humanOverride = consumed;
74
- return current;
75
45
  });
76
46
  }
77
47
  catch (error) {
@@ -81,10 +51,8 @@ async function taskReview(args = []) {
81
51
  process.exitCode = 1;
82
52
  return;
83
53
  }
84
- process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
85
- const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
86
- humanOverride.status === 'failed');
87
- if (result.status === 'failed' || overrideFailed)
54
+ process.stdout.write(`${JSON.stringify(result)}\n`);
55
+ if (result.status === 'failed')
88
56
  process.exitCode = 1;
89
57
  }
90
58
  export { taskReview };
@@ -1,9 +1,8 @@
1
1
  import { renderTaskVerification, verifyTaskEvent } from '../task/verification.js';
2
- import { consumeHumanOverride, failureId } from '../task/human-override.js';
3
2
  import { resolveTaskRef } from '../task/resolve-ref.js';
4
3
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
5
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
- const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text] [--override-ticket <ticket> --override-target <target> --override-scope <scope>]\n';
5
+ const USAGE = 'Usage: agent-infra-internal task-verify <N | TASK-id> <verification-event> [--artifact <canonical.md>] [--format json|text]\n';
7
6
  function fail(message) {
8
7
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'VERIFY_PAYLOAD_INVALID', message } })}\n`);
9
8
  process.stderr.write(USAGE);
@@ -25,13 +24,10 @@ async function taskVerify(args = []) {
25
24
  const event = args[1];
26
25
  let artifact;
27
26
  let format = 'json';
28
- let overrideTicket;
29
- let overrideTarget;
30
- let overrideScope;
31
27
  const seen = new Set();
32
28
  for (let index = 2; index < args.length; index += 1) {
33
29
  const flag = args[index];
34
- if (!['--artifact', '--format', '--override-ticket', '--override-target', '--override-scope'].includes(flag)) {
30
+ if (!['--artifact', '--format'].includes(flag)) {
35
31
  fail(`unknown option '${flag}'`);
36
32
  return;
37
33
  }
@@ -47,12 +43,6 @@ async function taskVerify(args = []) {
47
43
  seen.add(flag);
48
44
  if (flag === '--artifact')
49
45
  artifact = value;
50
- else if (flag === '--override-ticket')
51
- overrideTicket = value;
52
- else if (flag === '--override-target')
53
- overrideTarget = value;
54
- else if (flag === '--override-scope')
55
- overrideScope = value;
56
46
  else if (value === 'json' || value === 'text')
57
47
  format = value;
58
48
  else {
@@ -66,40 +56,8 @@ async function taskVerify(args = []) {
66
56
  return;
67
57
  }
68
58
  let result;
69
- let humanOverride = null;
70
59
  try {
71
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => {
72
- let current = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) });
73
- if (current.status === 'pass' || !overrideTicket)
74
- return current;
75
- if (!overrideTarget || !overrideScope) {
76
- fail('override ticket requires target and scope');
77
- return current;
78
- }
79
- const failureProducer = current.status === 'blocked' || current.status === 'fail'
80
- ? 'verification-engine'
81
- : 'task-verify';
82
- const failureCode = current.status === 'blocked'
83
- ? 'CHECK_BLOCKED'
84
- : current.status === 'fail'
85
- ? 'CHECK_FAILED'
86
- : current.error?.code ?? 'VERIFY_TASK_STATE_MISMATCH';
87
- const consumed = await consumeHumanOverride({
88
- taskRef, ticketId: overrideTicket,
89
- failureId: failureId(failureProducer, failureCode),
90
- target: overrideTarget, scope: overrideScope
91
- }, {
92
- effectExecutor: async (capability) => {
93
- const retried = await verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }, { manualOverride: capability });
94
- current = retried;
95
- return retried.status !== 'pass'
96
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: `${retried.error?.code ?? 'VERIFY_FAILED'}: ${retried.error?.message ?? 'manual verification effect failed'}` }
97
- : null;
98
- }
99
- });
100
- humanOverride = consumed;
101
- return current;
102
- });
60
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-verify.${event}`, async () => verifyTaskEvent({ taskRef, event, ...(artifact ? { artifact } : {}) }));
103
61
  }
104
62
  catch (error) {
105
63
  if (!(error instanceof TaskExecutionLockError))
@@ -107,8 +65,6 @@ async function taskVerify(args = []) {
107
65
  fail(`${error.code}: ${error.message}`);
108
66
  return;
109
67
  }
110
- if (humanOverride)
111
- result = { ...result, humanOverride };
112
68
  process.stdout.write(format === 'text' ? renderTaskVerification(result) : `${JSON.stringify(result)}\n`);
113
69
  process.exitCode = result.status === 'pass' ? 0 : result.status === 'blocked' ? 2 : 1;
114
70
  }
@@ -1,13 +1,11 @@
1
1
  import { applyWorkflowWarningIntent } from '../task/workflow-warning-intents.js';
2
- import { consumeHumanOverride, failureId, overrideDryRunConflict } from '../task/human-override.js';
3
2
  import { resolveTaskRef } from '../task/resolve-ref.js';
4
3
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
5
4
  import { ensureInternalHandlerRoute, internalHandlerRoute } from './cli-route-inventory.js';
6
5
  const USAGE = `Usage: agent-infra-internal task-warning <task-ref> <add|set-status|list> [intent flags] [--dry-run]\n`;
7
6
  const FLAGS = {
8
7
  '--step': 'step', '--severity': 'severity', '--code': 'code', '--target': 'target',
9
- '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution',
10
- '--override-ticket': 'overrideTicket', '--override-target': 'overrideTarget', '--override-scope': 'overrideScope'
8
+ '--message': 'message', '--action': 'action', '--id': 'id', '--status': 'status', '--resolution': 'resolution'
11
9
  };
12
10
  function usageFailure(message) {
13
11
  process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'WARNING_PAYLOAD_INVALID', message } })}\n`);
@@ -66,9 +64,6 @@ async function taskWarning(args = []) {
66
64
  };
67
65
  const optional = kind === 'list' ? ['status'] : [];
68
66
  const allowed = new Set(['kind', 'taskRef', 'dryRun', ...required[kind], ...optional]);
69
- allowed.add('overrideTicket');
70
- allowed.add('overrideTarget');
71
- allowed.add('overrideScope');
72
67
  const unexpected = Object.keys(values).find((key) => !allowed.has(key));
73
68
  const missing = required[kind].find((key) => values[key] === undefined);
74
69
  if (unexpected) {
@@ -79,43 +74,14 @@ async function taskWarning(args = []) {
79
74
  usageFailure(`${kind} requires '${missing}'`);
80
75
  return;
81
76
  }
82
- const dryRunConflict = overrideDryRunConflict(values);
83
- if (dryRunConflict) {
84
- usageFailure(dryRunConflict.message);
85
- return;
86
- }
87
77
  const resolved = resolveTaskRef(taskRef);
88
78
  if (!resolved.ok) {
89
79
  usageFailure(resolved.message);
90
80
  return;
91
81
  }
92
82
  let result;
93
- let humanOverride = null;
94
83
  try {
95
- result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => {
96
- let current = applyWorkflowWarningIntent(values);
97
- if (current.status !== 'failed' || !values.overrideTicket)
98
- return current;
99
- if (!values.overrideTarget || !values.overrideScope) {
100
- usageFailure('override ticket requires target and scope');
101
- return current;
102
- }
103
- const consumed = await consumeHumanOverride({
104
- taskRef, ticketId: String(values.overrideTicket),
105
- failureId: failureId('workflow-warning', current.error?.code ?? 'TASK_STATE_MISMATCH'),
106
- target: String(values.overrideTarget), scope: String(values.overrideScope)
107
- }, {
108
- effectExecutor: (capability) => {
109
- const retried = applyWorkflowWarningIntent(values, { manualOverride: capability });
110
- current = retried;
111
- return retried.status === 'failed' || retried.status === 'planned'
112
- ? { code: 'OVERRIDE_EFFECT_FAILED', message: retried.status === 'planned' ? 'producer returned planned; no workflow warning effect was committed' : `${retried.error?.code ?? 'WARNING_FAILED'}: ${retried.error?.message ?? 'manual warning effect failed'}` }
113
- : null;
114
- }
115
- });
116
- humanOverride = consumed;
117
- return current;
118
- });
84
+ result = await withTaskExecutionLock(resolved.repoRoot, resolved.taskId, `task-warning.${kind}`, async () => applyWorkflowWarningIntent(values));
119
85
  }
120
86
  catch (error) {
121
87
  if (!(error instanceof TaskExecutionLockError))
@@ -124,10 +90,8 @@ async function taskWarning(args = []) {
124
90
  process.exitCode = 1;
125
91
  return;
126
92
  }
127
- process.stdout.write(`${JSON.stringify(humanOverride ? { ...result, humanOverride } : result)}\n`);
128
- const overrideFailed = Boolean(humanOverride && typeof humanOverride === 'object' &&
129
- humanOverride.status === 'failed');
130
- if (result.status === 'failed' || overrideFailed)
93
+ process.stdout.write(`${JSON.stringify(result)}\n`);
94
+ if (result.status === 'failed')
131
95
  process.exitCode = 1;
132
96
  }
133
97
  export { taskWarning };
@@ -13,21 +13,70 @@ import { taskIssueIdentity } from './task-identities.js';
13
13
  function result(base, artifacts, warnings) {
14
14
  return { ...base, artifacts, warnings };
15
15
  }
16
- function excludedPrReview(taskDir, message) {
17
- const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])(pr-review(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
16
+ function referencedBackfillArtifact(taskDir, message) {
17
+ const names = [...message.matchAll(/(?:^|[^A-Za-z0-9_-])([a-z][a-z-]*(?:-r[1-9]\d*)?\.md)(?=$|[^A-Za-z0-9_.-])/g)]
18
18
  .map((match) => match[1]);
19
19
  if (names.length !== 1)
20
- return null;
20
+ return { status: 'ignored', artifact: null, error: null };
21
21
  const parsed = parseArtifactName(names[0]);
22
- if (!parsed || parsed.family !== 'pr-review')
23
- return null;
22
+ if (!parsed || (parsed.family !== 'pr-review' && !COMPLETION_BACKFILL_FAMILIES.includes(parsed.family))) {
23
+ return { status: 'ignored', artifact: null, error: null };
24
+ }
24
25
  const artifactPath = path.join(taskDir, parsed.name);
25
26
  try {
26
27
  const stat = fs.lstatSync(artifactPath);
27
- return stat.isFile() && !stat.isSymbolicLink() ? parsed.name : null;
28
+ if (stat.isSymbolicLink())
29
+ return {
30
+ status: 'failed', artifact: null,
31
+ error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `SYMBOLIC_LINK: symbolic links are not workflow artifacts: ${parsed.name}`, retryable: false }
32
+ };
33
+ if (!stat.isFile())
34
+ return {
35
+ status: 'failed', artifact: null,
36
+ error: { code: 'ARTIFACT_TOPOLOGY_CONFLICT', message: `NON_REGULAR_FILE: artifact is not a regular file: ${parsed.name}`, retryable: false }
37
+ };
38
+ fs.accessSync(artifactPath, fs.constants.R_OK);
39
+ return { status: 'ready', artifact: parsed.name, error: null };
40
+ }
41
+ catch (error) {
42
+ return {
43
+ status: 'failed', artifact: null,
44
+ error: { code: 'ARTIFACT_READ_FAILED', message: `Unable to inspect referenced artifact '${parsed.name}': ${error instanceof Error ? error.message : String(error)}`, retryable: false }
45
+ };
46
+ }
47
+ }
48
+ function matchingBackfillWarnings(taskDir, content) {
49
+ const matches = [];
50
+ for (const warning of getOpenWorkflowWarnings(content)) {
51
+ if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact')
52
+ continue;
53
+ const reference = referencedBackfillArtifact(taskDir, warning.message);
54
+ if (reference.status === 'failed')
55
+ return { status: 'failed', matches: [], error: reference.error };
56
+ if (reference.status === 'ready')
57
+ matches.push({ warning, artifact: reference.artifact });
58
+ }
59
+ return { status: 'ready', matches, error: null };
60
+ }
61
+ function inspectCompletionBackfillEligibility(taskRef, options = {}) {
62
+ const resolved = resolveTaskRef(taskRef, options.cwd ? { repoRoot: options.cwd } : {});
63
+ if (!resolved.ok)
64
+ return {
65
+ status: 'failed', eligible: false,
66
+ error: { code: resolved.code, message: resolved.message, retryable: false }
67
+ };
68
+ try {
69
+ const content = fs.readFileSync(resolved.taskMdPath, 'utf8');
70
+ const matching = matchingBackfillWarnings(resolved.taskDir, content);
71
+ if (matching.status === 'failed')
72
+ return { status: 'failed', eligible: false, error: matching.error };
73
+ return { status: 'ready', eligible: matching.matches.length > 0, error: null };
28
74
  }
29
- catch {
30
- return null;
75
+ catch (error) {
76
+ return {
77
+ status: 'failed', eligible: false,
78
+ error: { code: 'TASK_READ_FAILED', message: error instanceof Error ? error.message : String(error), retryable: false }
79
+ };
31
80
  }
32
81
  }
33
82
  async function backfillCompletionComments(taskRef, options) {
@@ -46,6 +95,9 @@ async function backfillCompletionComments(taskRef, options) {
46
95
  }
47
96
  }), [], []);
48
97
  const initialContent = fs.readFileSync(resolved.taskMdPath, 'utf8');
98
+ const matching = matchingBackfillWarnings(resolved.taskDir, initialContent);
99
+ if (matching.status === 'failed')
100
+ return result(platformResult('failed', { error: matching.error }), [], []);
49
101
  const issueIdentity = taskIssueIdentity(parseTaskFrontmatter(initialContent));
50
102
  if (!issueIdentity)
51
103
  return result(platformResult('no-op', {
@@ -79,18 +131,12 @@ async function backfillCompletionComments(taskRef, options) {
79
131
  error: latest.error
80
132
  }), artifacts, []);
81
133
  }
82
- const matching = getOpenWorkflowWarnings(initialContent).flatMap((warning) => {
83
- if (warning.step !== 'complete-task' || warning.code !== 'COMMENT_SYNC_FAILED' || warning.target !== 'artifact')
84
- return [];
85
- const excluded = excludedPrReview(resolved.taskDir, warning.message);
86
- return excluded ? [{ warning, excluded }] : [];
87
- });
88
134
  const warnings = [];
89
135
  let metadata = null;
90
- for (const { warning, excluded } of matching) {
136
+ for (const { warning, artifact } of matching.matches) {
91
137
  metadata ??= captureTaskWriteMetadata();
92
138
  const resolution = [
93
- `excluded=${excluded}`,
139
+ `artifact=${artifact}`,
94
140
  `completionFamilies=${COMPLETION_BACKFILL_FAMILIES.join(',')}`,
95
141
  `synchronized=${inventory.artifacts.map((artifact) => artifact.name).join(',') || 'none'}`,
96
142
  `completedAt=${metadata.timestamp}`
@@ -119,5 +165,5 @@ async function backfillCompletionComments(taskRef, options) {
119
165
  error: null
120
166
  }), artifacts, warnings);
121
167
  }
122
- export { backfillCompletionComments };
168
+ export { backfillCompletionComments, inspectCompletionBackfillEligibility };
123
169
  //# sourceMappingURL=completion-backfill.js.map
@@ -2,13 +2,12 @@ import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { enumerateArtifacts } from '../task/artifacts.js';
4
4
  import { parseTypedTaskFrontmatter } from '../task/frontmatter.js';
5
- import { renderHumanOverrideAudit } from '../task/human-override.js';
6
5
  import { resolveTaskRef } from '../task/resolve-ref.js';
7
6
  import { TaskExecutionLockError, withTaskExecutionLock } from '../task/task-execution-lock.js';
8
7
  import { readPrDeliveryFact } from '../task/pr-delivery-fact.js';
9
8
  import { resolvePlatformProviderContext } from './context.js';
10
9
  import { normalizeCommentContent } from './issue-comments.js';
11
- import { CONTROL_MARKER_PATTERN, escapeHtmlText, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
10
+ import { CONTROL_MARKER_PATTERN, sanitizeMarkdownDocument, splitDocumentPlaceholder } from './comment-safety.js';
12
11
  import { platformResult } from './types.js';
13
12
  import { providerError, providerOperationContext, providerStatus, providerResourceToken, resourceIdentityNumber, unsupportedProviderOperation } from './provider-bridge.js';
14
13
  import { resourceIdentityEquals } from './resource-identity.js';
@@ -18,23 +17,6 @@ import { readManualValidationCompletion } from '../task/manual-validation-comple
18
17
  function summaryMarker(taskId) {
19
18
  return `<!-- sync-pr:${taskId}:summary -->`;
20
19
  }
21
- function escapeSummaryAuditText(value) {
22
- return escapeHtmlText(value).replace(/\r\n?/g, '\n');
23
- }
24
- function renderSummaryAuditLine(value) {
25
- const safe = escapeSummaryAuditText(value);
26
- const longestRun = Math.max(0, ...(safe.match(/`+/g) || []).map((run) => run.length));
27
- const delimiter = '`'.repeat(longestRun + 1);
28
- return `${delimiter} ${safe} ${delimiter}`;
29
- }
30
- function renderSafeHumanOverrideAudit(value) {
31
- const normalized = value.trim().replace(/\r\n?/g, '\n');
32
- if (!normalized)
33
- return '';
34
- const [heading, ...rows] = normalized.split('\n');
35
- const safeHeading = heading === '## Human Override Audit' ? heading : renderSummaryAuditLine(heading || '');
36
- return [safeHeading, ...rows.map((row) => row ? renderSummaryAuditLine(row) : '')].join('\n');
37
- }
38
20
  function isSafeSummaryEnvelope(value, taskId) {
39
21
  return value.includes(summaryMarker(taskId))
40
22
  && (value.match(/<!--\s*sync-pr:/gi) || []).length === 1
@@ -71,9 +53,8 @@ function warningResultForPrimary(primaryResult) {
71
53
  function platformError(error) {
72
54
  return { code: error.code, message: error.message, retryable: error.retryable ?? false };
73
55
  }
74
- function buildPullRequestSummary(taskId, body, headSha, humanOverrideAudit = '') {
75
- const sections = [body.replace(/\s+$/, ''), renderSafeHumanOverrideAudit(humanOverrideAudit)].filter(Boolean).join('\n\n');
76
- return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${sections}\n`);
56
+ function buildPullRequestSummary(taskId, body, headSha) {
57
+ return normalizeCommentContent(`${summaryMarker(taskId)}\n<!-- last-commit: ${headSha} -->\n\n${body.replace(/\s+$/, '')}\n`);
77
58
  }
78
59
  function reconcileSummaryComment(comments, taskId, desired) {
79
60
  const marker = summaryMarker(taskId);
@@ -586,7 +567,7 @@ async function syncPullRequestSummary(taskRef, options) {
586
567
  const renderedBody = manual?.phase === 'final'
587
568
  ? replaced.value.replace(MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, `<!-- manual-validation-receipt: transaction=${manual.transactionId}; receipt=${manual.receiptDigest}; evidence=${manualEvidenceDigest}; head=${manual.prHeadSha} -->`)
588
569
  : replaced.value;
589
- const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha, renderHumanOverrideAudit(taskContent));
570
+ const desired = buildPullRequestSummary(resolved.taskId, renderedBody, initial.value.head.sha);
590
571
  if (!isSafeSummaryEnvelope(desired, resolved.taskId))
591
572
  return fail('failed', context, {
592
573
  code: 'PR_SUMMARY_RENDER_INVALID',
@@ -3,6 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { execFileSync } from "node:child_process";
6
+ import { resolveBranchWorktree } from "../git/branch-worktree.js";
6
7
  import { resolvePlatformProviderContext } from "./context.js";
7
8
  import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.js";
8
9
  import { requirementSectionAnchors } from "./issues.js";
@@ -807,7 +808,10 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
807
808
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
808
809
  }
809
810
  const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
810
- const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
811
+ const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
812
+ if (!gitCwd) {
813
+ return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
814
+ }
811
815
  const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
812
816
  if (!changedFilesResult.ok) {
813
817
  return { ...changedFilesResult, type: "network_error" };
@@ -831,26 +835,6 @@ function computeExpectedInLabels(taskDir, repositoryLabels, mappingOverride, sha
831
835
  }
832
836
  return { ok: true, labels: planned.target, mode: "mapped" };
833
837
  }
834
- function worktreeForBranch(repositoryRoot, branch) {
835
- try {
836
- const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
837
- cwd: repositoryRoot,
838
- encoding: "utf8",
839
- stdio: ["pipe", "pipe", "pipe"]
840
- }).trim().split(/\r?\n\r?\n/);
841
- const expectedRef = `refs/heads/${branch}`;
842
- for (const record of records) {
843
- const worktree = /^worktree (.+)$/m.exec(record)?.[1];
844
- const branchRef = /^branch (.+)$/m.exec(record)?.[1];
845
- if (worktree && branchRef === expectedRef)
846
- return worktree;
847
- }
848
- }
849
- catch {
850
- // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
851
- }
852
- return null;
853
- }
854
838
  function loadInLabelMapping(shared) {
855
839
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
856
840
  if (!fs.existsSync(configPath)) {
@@ -888,36 +872,15 @@ function resolvePrHeadSha(context) {
888
872
  if (!branch) {
889
873
  return fallback();
890
874
  }
891
- const worktreeList = gitText(["worktree", "list", "--porcelain"], context.taskDir);
892
- if (!worktreeList.ok) {
893
- return fallback();
894
- }
895
- const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
875
+ const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
896
876
  if (!matchedWorktree) {
897
- return fallback();
898
- }
899
- const headInWorktree = gitText(["rev-parse", "HEAD"], matchedWorktree);
900
- if (!headInWorktree.ok) {
901
- return fallback();
902
- }
903
- return headInWorktree;
904
- }
905
- function findWorktreeForBranch(porcelainOutput, branch) {
906
- let currentWorktree = "";
907
- for (const rawLine of String(porcelainOutput || "").split("\n")) {
908
- const line = rawLine.trimEnd();
909
- if (line.startsWith("worktree ")) {
910
- currentWorktree = line.slice("worktree ".length).trim();
911
- continue;
912
- }
913
- if (line.startsWith("branch refs/heads/")) {
914
- const usedBranch = line.slice("branch refs/heads/".length).trim();
915
- if (usedBranch === branch && currentWorktree) {
916
- return currentWorktree;
917
- }
918
- }
877
+ return {
878
+ ok: false,
879
+ type: "check_failed",
880
+ message: `Task branch '${branch}' is not checked out in a usable worktree`
881
+ };
919
882
  }
920
- return null;
883
+ return gitText(["rev-parse", "HEAD"], matchedWorktree);
921
884
  }
922
885
  function interpolate(template, taskDir, artifactFile) {
923
886
  const artifactStem = artifactFile ? path.basename(artifactFile, path.extname(artifactFile)) : "";
@@ -340,15 +340,18 @@ export function requestSandboxTaskFinalization(params) {
340
340
  clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
341
341
  const auth = authority(params);
342
342
  const issuedAt = Date.now();
343
+ const id = randomUUID();
344
+ const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
343
345
  const request = {
344
346
  version: 3,
345
- id: randomUUID(),
347
+ id,
346
348
  ...auth,
347
349
  issuedAt,
348
350
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
349
351
  family: 'task-finalization',
350
352
  operation: 'complete',
351
353
  agent,
354
+ handoffSha256,
352
355
  args: [],
353
356
  controllerProcess: null,
354
357
  controllerProof: null
@@ -2,6 +2,7 @@ import { spawn } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
+ import { homedir } from 'node:os';
5
6
  import { getProcessStartTime } from '../../server/process-state.js';
6
7
  import { createTask, projectTaskCreateResult, taskCreateExitCode, taskCreateFailure } from '../../task/create-service.js';
7
8
  import { applyTaskFinalization } from '../../task/finalization.js';
@@ -13,6 +14,10 @@ import { createSandboxExecutorExecutionContext, dispatchTaskControlOperation, pa
13
14
  import { assertSandboxControlBrokerOwner, readSandboxControlManifest } from './lifecycle.js';
14
15
  import { computeLifecycleBuildIdentity } from '../../agent-clients/adapters/codex-lifecycle/build-identity.js';
15
16
  import { closeCodexControllerRegistration, CodexControllerRegistrationError, openCodexControllerRegistration, resolveCodexControllerBinding } from './controller-registration.js';
17
+ import { shareBranchDir } from '../constants.js';
18
+ function handoffDirectory(manifest) {
19
+ return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
20
+ }
16
21
  function sameBrokerOwner(left, right) {
17
22
  return left.version === right.version
18
23
  && left.pid === right.pid
@@ -295,9 +300,10 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
295
300
  }
296
301
  }
297
302
  if (request.family === 'task-finalization') {
298
- const operation = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
299
- if (operation.family !== 'task-finalization')
303
+ const parsed = parseTaskControlOperation('task-finalization', [manifest.taskId, 'complete', '--agent', request.agent]);
304
+ if (parsed.family !== 'task-finalization')
300
305
  throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
306
+ const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
301
307
  const result = dispatchTaskControlOperation(createSandboxExecutorExecutionContext({
302
308
  repoRoot: manifest.repoRoot,
303
309
  worktreeRoot: manifest.worktreeRoot,
@@ -305,7 +311,8 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
305
311
  taskId: manifest.taskId,
306
312
  generation: manifest.generation,
307
313
  manifestPath,
308
- requestId: request.id
314
+ requestId: request.id,
315
+ handoffDirectory: handoffDirectory(manifest)
309
316
  }), operation);
310
317
  return finalizationResult(await result);
311
318
  }
@@ -326,7 +333,8 @@ async function executeRequestInner(manifest, manifestPath, request, options = {}
326
333
  taskId: manifest.taskId,
327
334
  generation: manifest.generation,
328
335
  manifestPath,
329
- requestId: request.id
336
+ requestId: request.id,
337
+ handoffDirectory: handoffDirectory(manifest)
330
338
  });
331
339
  const format = (value) => {
332
340
  const result = value;
@@ -158,11 +158,12 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
158
158
  return request;
159
159
  }
160
160
  if (request.family === 'task-finalization') {
161
- const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
161
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
162
162
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
163
163
  || request.operation !== 'complete'
164
164
  || !Array.isArray(request.args) || request.args.length !== 0
165
165
  || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
166
+ || typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
166
167
  || request.controllerProcess !== null || request.controllerProof !== null) {
167
168
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
168
169
  }