@fitlab-ai/agent-infra 0.11.4 → 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 (111) hide show
  1. package/dist/bin/internal-cli.js +0 -5
  2. package/dist/lib/decide.js +4 -41
  3. package/dist/lib/git/branch-worktree.js +45 -0
  4. package/dist/lib/internal/cli-route-inventory.js +0 -3
  5. package/dist/lib/internal/task-activity.js +7 -40
  6. package/dist/lib/internal/task-event.js +4 -37
  7. package/dist/lib/internal/task-finalization.js +9 -2
  8. package/dist/lib/internal/task-ledger.js +5 -43
  9. package/dist/lib/internal/task-lifecycle.js +1 -1
  10. package/dist/lib/internal/task-operation-registry.js +1 -4
  11. package/dist/lib/internal/task-review.js +5 -37
  12. package/dist/lib/internal/task-verify.js +3 -47
  13. package/dist/lib/internal/task-warning.js +4 -40
  14. package/dist/lib/platform/pr-summary.js +4 -23
  15. package/dist/lib/platform/verification-sync.js +12 -49
  16. package/dist/lib/sandbox/control/client.js +4 -1
  17. package/dist/lib/sandbox/control/executor.js +12 -4
  18. package/dist/lib/sandbox/control/protocol.js +2 -1
  19. package/dist/lib/sandbox/removal.js +15 -8
  20. package/dist/lib/task/activity-intent.js +1 -2
  21. package/dist/lib/task/artifact-operations.js +36 -2
  22. package/dist/lib/task/artifact-schema.js +7 -7
  23. package/dist/lib/task/control-authority.js +9 -166
  24. package/dist/lib/task/decision-intents.js +2 -4
  25. package/dist/lib/task/events.js +7 -28
  26. package/dist/lib/task/finalization-handoff.js +92 -0
  27. package/dist/lib/task/finalization.js +123 -12
  28. package/dist/lib/task/intermediate-cleanup.js +9 -148
  29. package/dist/lib/task/ledger-intents.js +3 -6
  30. package/dist/lib/task/lifecycle.js +50 -11
  31. package/dist/lib/task/local-artifact-finalization.js +13 -28
  32. package/dist/lib/task/review-command.js +1 -2
  33. package/dist/lib/task/review-finalization.js +1 -2
  34. package/dist/lib/task/snapshot.js +6 -6
  35. package/dist/lib/task/verification-engine.js +16 -11
  36. package/dist/lib/task/verification.js +1 -13
  37. package/dist/lib/task/workflow-warning-intents.js +2 -4
  38. package/dist/lib/task/write.js +1 -2
  39. package/dist/lifecycle-build-manifest.json +3 -3
  40. package/lib/decide.ts +5 -35
  41. package/lib/git/branch-worktree.ts +41 -0
  42. package/lib/internal/cli-route-inventory.ts +0 -2
  43. package/lib/internal/task-activity.ts +6 -34
  44. package/lib/internal/task-event.ts +4 -37
  45. package/lib/internal/task-finalization.ts +9 -2
  46. package/lib/internal/task-ledger.ts +5 -38
  47. package/lib/internal/task-lifecycle.ts +2 -2
  48. package/lib/internal/task-operation-registry.ts +1 -4
  49. package/lib/internal/task-review.ts +5 -31
  50. package/lib/internal/task-verify.ts +3 -39
  51. package/lib/internal/task-warning.ts +3 -32
  52. package/lib/platform/pr-summary.ts +4 -25
  53. package/lib/platform/verification-sync.ts +13 -53
  54. package/lib/sandbox/control/client.ts +5 -1
  55. package/lib/sandbox/control/executor.ts +13 -4
  56. package/lib/sandbox/control/protocol.ts +3 -2
  57. package/lib/sandbox/removal.ts +23 -7
  58. package/lib/task/activity-intent.ts +1 -2
  59. package/lib/task/artifact-operations.ts +58 -2
  60. package/lib/task/artifact-schema.ts +9 -7
  61. package/lib/task/control-authority.ts +14 -188
  62. package/lib/task/decision-intents.ts +2 -4
  63. package/lib/task/events.ts +8 -27
  64. package/lib/task/finalization-handoff.ts +104 -0
  65. package/lib/task/finalization.ts +120 -12
  66. package/lib/task/intermediate-cleanup.ts +9 -125
  67. package/lib/task/ledger-intents.ts +3 -6
  68. package/lib/task/lifecycle.ts +45 -13
  69. package/lib/task/local-artifact-finalization.ts +16 -35
  70. package/lib/task/review-command.ts +2 -5
  71. package/lib/task/review-finalization.ts +1 -4
  72. package/lib/task/snapshot.ts +6 -6
  73. package/lib/task/verification-engine.ts +20 -10
  74. package/lib/task/verification.ts +1 -15
  75. package/lib/task/workflow-warning-intents.ts +2 -4
  76. package/lib/task/write.ts +1 -4
  77. package/package.json +1 -1
  78. package/templates/.agents/skills/analyze-task/SKILL.en.md +11 -2
  79. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +11 -2
  80. package/templates/.agents/skills/code-task/reference/report-template.en.md +6 -6
  81. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +6 -6
  82. package/templates/.agents/skills/complete-manual-validation/config/verify.en.json +2 -2
  83. package/templates/.agents/skills/complete-manual-validation/config/verify.zh-CN.json +2 -2
  84. package/templates/.agents/skills/complete-manual-validation/reference/report-template.en.md +1 -3
  85. package/templates/.agents/skills/complete-manual-validation/reference/report-template.zh-CN.md +1 -3
  86. package/templates/.agents/skills/complete-task/config/verify.en.json +2 -2
  87. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +2 -2
  88. package/templates/.agents/skills/review-analysis/reference/report-template.en.md +3 -3
  89. package/templates/.agents/skills/review-analysis/reference/report-template.zh-CN.md +3 -3
  90. package/templates/.agents/skills/review-code/reference/report-template.en.md +3 -3
  91. package/templates/.agents/skills/review-code/reference/report-template.zh-CN.md +3 -3
  92. package/templates/.agents/skills/review-plan/reference/report-template.en.md +3 -3
  93. package/templates/.agents/skills/review-plan/reference/report-template.zh-CN.md +3 -3
  94. package/templates/.agents/skills/review-pr/config/verify.en.json +3 -1
  95. package/templates/.agents/skills/review-pr/config/verify.zh-CN.json +3 -1
  96. package/templates/.agents/skills/review-pr/reference/report-template.en.md +3 -3
  97. package/templates/.agents/skills/review-pr/reference/report-template.zh-CN.md +3 -3
  98. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +2 -2
  99. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +2 -2
  100. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  101. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  102. package/dist/lib/internal/task-override.js +0 -138
  103. package/dist/lib/task/artifact-recovery.js +0 -476
  104. package/dist/lib/task/artifact-repair-intent.js +0 -181
  105. package/dist/lib/task/guard-override.js +0 -31
  106. package/dist/lib/task/human-override.js +0 -1259
  107. package/lib/internal/task-override.ts +0 -144
  108. package/lib/task/artifact-recovery.ts +0 -572
  109. package/lib/task/artifact-repair-intent.ts +0 -227
  110. package/lib/task/guard-override.ts +0 -50
  111. package/lib/task/human-override.ts +0 -1522
@@ -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 };
@@ -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
  }
@@ -17,6 +17,7 @@ import { acquireSandboxResourceLock } from './control/native-file-lock.js';
17
17
  import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.js';
18
18
  import { getProcessStartTime } from '../server/process-state.js';
19
19
  import { releaseStaleShortIdRegistry } from '../task/short-id.js';
20
+ import { withRepositoryMutationLock } from '../task/task-execution-lock.js';
20
21
  import { fetchSandboxRows } from './commands/list-running.js';
21
22
  import { formatIntermediateCleanupReport, cleanupIntermediateUnderRemovalCoordinator, protectIntermediateCleanupReport, mergeIntermediateCleanupReports, scanIntermediateCleanup } from '../task/intermediate-cleanup.js';
22
23
  import { createSandboxControlBindingEvidence } from './task-cleanup.js';
@@ -670,7 +671,7 @@ async function runRmOneUnderRepositoryLock(config, tools, branch, options = {})
670
671
  return rmOneCore(config, tools, branch, { ...options, target });
671
672
  }
672
673
  async function rmOne(config, tools, branch, options = {}) {
673
- await runRmOneUnderRepositoryLock(config, tools, branch, options);
674
+ await withRepositoryMutationLock(config.repoRoot, () => runRmOneUnderRepositoryLock(config, tools, branch, options));
674
675
  }
675
676
  async function removeUncheckedSandbox(config, target, options) {
676
677
  const confirm = options.prompt?.confirm ?? p.confirm;
@@ -703,8 +704,12 @@ async function removeUncheckedSandbox(config, target, options) {
703
704
  p.outro('Cancelled');
704
705
  return null;
705
706
  }
707
+ const worktreePermits = options.permits ?? await authorizeWorktrees(existingWorktrees, { allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) }, { ...options.prompt, interactive: options.interactive });
706
708
  for (const container of matchedContainers)
707
709
  runSafeEngine(engine, 'docker', ['rm', '-f', container]);
710
+ if (!sandboxContainersRemoved(engine, matchedContainers)) {
711
+ throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
712
+ }
708
713
  for (const root of controlRoots) {
709
714
  fs.rmSync(root, { recursive: true, force: true });
710
715
  removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
@@ -720,14 +725,16 @@ async function removeUncheckedSandbox(config, target, options) {
720
725
  if (shouldRemoveShare)
721
726
  fs.rmSync(sharePath, { recursive: true, force: true });
722
727
  if (shouldRemoveWorktree) {
723
- for (const worktree of existingWorktrees)
724
- fs.rmSync(worktree, { recursive: true, force: true });
725
- runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
728
+ for (const worktree of existingWorktrees) {
729
+ const permit = worktreePermits.get(path.resolve(worktree));
730
+ if (!permit)
731
+ throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
732
+ removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
733
+ }
726
734
  }
727
- if (shouldDeleteBranch)
735
+ if (shouldDeleteBranch) {
736
+ assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
728
737
  runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
729
- if (!sandboxContainersRemoved(engine, matchedContainers)) {
730
- throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
731
738
  }
732
739
  if (target.workspace.mode === 'task-bound') {
733
740
  releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
@@ -741,7 +748,7 @@ async function rmOneCore(config, tools, branch, options = {}) {
741
748
  return removeUncheckedSandbox(config, target, options);
742
749
  }
743
750
  async function rmPurge(config, tools, prompt = {}) {
744
- return rmPurgeCore(config, tools, prompt);
751
+ return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
745
752
  }
746
753
  async function rmPurgeCore(config, tools, prompt = {}) {
747
754
  const engine = detectEngine(config);
@@ -6,7 +6,6 @@ import { assertWritableInventory, buildArtifactLinkSection, inspectTaskArtifacts
6
6
  import { parseArtifactName } from './artifact-name.js';
7
7
  import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-lock.js';
8
8
  import { captureTaskWriteMetadata, writeTask } from './write.js';
9
- import { allowsManualOverride } from './guard-override.js';
10
9
  const HEAD_RE = /^[0-9a-f]{40}$/;
11
10
  const REVIEW_ACTION_RE = /^Review PR \(Round ([1-9]\d*)\)( \[started\])?$/;
12
11
  const VERDICTS = new Set(['approved', 'changes-requested', 'commented']);
@@ -294,7 +293,7 @@ export function applyPrReviewActivityIntent(intent, options = {}) {
294
293
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
295
294
  if (!resolved.ok)
296
295
  return failed(intent, resolved.code, resolved.message, resolved.taskId);
297
- if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
296
+ if (resolved.state !== 'active') {
298
297
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
299
298
  }
300
299
  if (options.lockAlreadyHeld)
@@ -166,10 +166,44 @@ function inspectArtifactPatterns(content, schema) {
166
166
  diagnostics
167
167
  };
168
168
  }
169
- function inspectArtifactContract(content, schema) {
169
+ function inspectArtifactStateCheck(content, schema, scopeToStateCheckSection) {
170
+ const stateCheck = schema.sections.find((section) => section.id === 'state-check');
171
+ if (!stateCheck || schema.stateCheckPatterns.length === 0) {
172
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
173
+ }
174
+ if (!scopeToStateCheckSection) {
175
+ const diagnostics = schema.stateCheckPatterns
176
+ .filter((pattern) => !new RegExp(pattern, 'm').test(content))
177
+ .map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `artifact is missing required state-check command '${pattern}'`, stateCheck.id, null));
178
+ return {
179
+ ok: diagnostics.length === 0,
180
+ family: schema.family,
181
+ semanticDigest: canonicalSemanticDigest(content),
182
+ diagnostics
183
+ };
184
+ }
185
+ const scanned = scanVisibleMarkdown(content);
186
+ const headings = scanned.headings.filter((heading) => (heading.level === 2 && sectionHeadings(schema, stateCheck).includes(heading.text)));
187
+ if (headings.length !== 1) {
188
+ return { ok: true, family: schema.family, semanticDigest: canonicalSemanticDigest(content), diagnostics: [] };
189
+ }
190
+ const bodyBounds = sectionBodyBounds(content, headings[0], scanned);
191
+ const body = content.slice(bodyBounds.start, bodyBounds.end);
192
+ const diagnostics = schema.stateCheckPatterns
193
+ .filter((pattern) => !new RegExp(pattern, 'm').test(body))
194
+ .map((pattern) => diagnostic('ARTIFACT_STATE_CHECK_PATTERN_MISSING', `state-check section is missing required command '${pattern}'`, stateCheck.id, lineNumber(content, headings[0].start)));
195
+ return {
196
+ ok: diagnostics.length === 0,
197
+ family: schema.family,
198
+ semanticDigest: canonicalSemanticDigest(content),
199
+ diagnostics
200
+ };
201
+ }
202
+ function inspectArtifactContract(content, schema, options = {}) {
170
203
  const structure = inspectArtifactStructure(content, schema);
204
+ const stateCheck = inspectArtifactStateCheck(content, schema, options.scopeStateCheckSection === true);
171
205
  const patterns = inspectArtifactPatterns(content, schema);
172
- const diagnostics = [...structure.diagnostics, ...patterns.diagnostics];
206
+ const diagnostics = [...structure.diagnostics, ...stateCheck.diagnostics, ...patterns.diagnostics];
173
207
  return {
174
208
  ok: diagnostics.length === 0,
175
209
  family: schema.family,
@@ -81,21 +81,21 @@ function buildSections(family) {
81
81
  marker: `artifact-section:${family}:${id}`
82
82
  }));
83
83
  }
84
+ const TASK_SNAPSHOT_PATTERN = 'agent-infra-internal[ \\t]+task-snapshot\\b';
84
85
  const ARTIFACT_SCHEMAS = [
85
- { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), requiredPatterns: ['^\\$ '] },
86
- { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
87
- { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), requiredPatterns: ['^\\$ '] },
88
- { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), requiredPatterns: ['^### (?:审查决定|Approval Decision)$', '^\\$ '] },
89
- { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), requiredPatterns: ['^\\$ '] },
86
+ { family: 'analysis', title: { zh: '需求分析报告', en: 'Requirements Analysis' }, sections: buildSections('analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
87
+ { family: 'review-analysis', title: { zh: '需求分析审查报告', en: 'Analysis Review Report' }, sections: buildSections('review-analysis'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
88
+ { family: 'plan', title: { zh: '技术方案', en: 'Technical Plan' }, sections: buildSections('plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
89
+ { family: 'review-plan', title: { zh: '技术方案审查报告', en: 'Plan Review Report' }, sections: buildSections('review-plan'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: ['^### (?:审查决定|Approval Decision)$'] },
90
+ { family: 'code', title: { zh: '实现报告', en: 'Implementation Report' }, sections: buildSections('code'), stateCheckPatterns: [TASK_SNAPSHOT_PATTERN], requiredPatterns: [] },
90
91
  { family: 'review-code', title: { zh: '代码审查报告', en: 'Code Review Report' }, sections: buildSections('review-code'), requiredPatterns: [
91
92
  '^### (?:审查决定|Approval Decision)$',
92
- '^\\$ ',
93
93
  '^- \\*\\*(?:总体结论|Overall Verdict)\\*\\*[::]\\s*(?:通过|需要修改|拒绝|Approved|Changes Requested|Rejected)\\s*$',
94
94
  '^- \\*\\*(?:审查基线提交|Review Baseline Commit)\\*\\*[::]\\s*\\S',
95
95
  '^- \\*\\*(?:审查差异基线|Reviewed Diff Base)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$',
96
96
  '^- \\*\\*(?:审查差异指纹|Reviewed Diff Fingerprint)\\*\\*[::]\\s*sha256:[0-9a-f]{64}\\s*$',
97
97
  '^- \\*\\*(?:审查快照树|Reviewed Snapshot Tree)\\*\\*[::]\\s*[0-9a-f]{40,64}\\s*$'
98
- ] }
98
+ ], stateCheckPatterns: [TASK_SNAPSHOT_PATTERN] }
99
99
  ];
100
100
  function getArtifactSchema(family) {
101
101
  return ARTIFACT_SCHEMAS.find((schema) => schema.family === family) ?? null;