@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
@@ -5,6 +5,8 @@ import path from "node:path";
5
5
  import process from "node:process";
6
6
  import { execFileSync } from "node:child_process";
7
7
 
8
+ import { resolveBranchWorktree } from "../git/branch-worktree.ts";
9
+
8
10
  import { resolvePlatformProviderContext } from "./context.ts";
9
11
  import { hasCheckedRequirement, resolveRequirementSection } from "./issue-metadata.ts";
10
12
  import { requirementSectionAnchors } from "./issues.ts";
@@ -1054,7 +1056,10 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1054
1056
  return { ok: false, type: "check_failed", message: "Task has no delivery_base_ref for in-label evidence" };
1055
1057
  }
1056
1058
  const branch = typeof task.metadata?.branch === "string" ? task.metadata.branch.trim() : "";
1057
- const gitCwd = branch ? worktreeForBranch(shared.repoRoot, branch) ?? taskDir : taskDir;
1059
+ const gitCwd = branch ? resolveBranchWorktree(shared.repoRoot, branch) : taskDir;
1060
+ if (!gitCwd) {
1061
+ return { ok: false, type: "check_failed", message: `Task branch '${branch}' is not checked out in a usable worktree` };
1062
+ }
1058
1063
  const changedFilesResult = gitText(["diff", `${baseRef}...HEAD`, "--name-only"], gitCwd);
1059
1064
  if (!changedFilesResult.ok) {
1060
1065
  return { ...changedFilesResult, type: "network_error" };
@@ -1082,25 +1087,6 @@ function computeExpectedInLabels(taskDir: any, repositoryLabels: string[], mappi
1082
1087
  return { ok: true, labels: planned.target, mode: "mapped" };
1083
1088
  }
1084
1089
 
1085
- function worktreeForBranch(repositoryRoot: string, branch: string): string | null {
1086
- try {
1087
- const records = execFileSync("git", ["worktree", "list", "--porcelain"], {
1088
- cwd: repositoryRoot,
1089
- encoding: "utf8",
1090
- stdio: ["pipe", "pipe", "pipe"]
1091
- }).trim().split(/\r?\n\r?\n/);
1092
- const expectedRef = `refs/heads/${branch}`;
1093
- for (const record of records) {
1094
- const worktree = /^worktree (.+)$/m.exec(record)?.[1];
1095
- const branchRef = /^branch (.+)$/m.exec(record)?.[1];
1096
- if (worktree && branchRef === expectedRef) return worktree;
1097
- }
1098
- } catch {
1099
- // Fall back to taskDir so ordinary in-repository task workspaces retain their behavior.
1100
- }
1101
- return null;
1102
- }
1103
-
1104
1090
  function loadInLabelMapping(shared: VerificationShared): any {
1105
1091
  const configPath = path.join(shared.repoRoot, ".agents", ".airc.json");
1106
1092
  if (!fs.existsSync(configPath)) {
@@ -1141,42 +1127,16 @@ function resolvePrHeadSha(context: any): any {
1141
1127
  return fallback();
1142
1128
  }
1143
1129
 
1144
- const worktreeList = gitText(["worktree", "list", "--porcelain"], context.taskDir);
1145
- if (!worktreeList.ok) {
1146
- return fallback();
1147
- }
1148
-
1149
- const matchedWorktree = findWorktreeForBranch(worktreeList.value, branch);
1130
+ const matchedWorktree = resolveBranchWorktree(context.taskDir, branch);
1150
1131
  if (!matchedWorktree) {
1151
- return fallback();
1152
- }
1153
-
1154
- const headInWorktree = gitText(["rev-parse", "HEAD"], matchedWorktree);
1155
- if (!headInWorktree.ok) {
1156
- return fallback();
1157
- }
1158
-
1159
- return headInWorktree;
1160
- }
1161
-
1162
- function findWorktreeForBranch(porcelainOutput: any, branch: any): any {
1163
- let currentWorktree = "";
1164
- for (const rawLine of String(porcelainOutput || "").split("\n")) {
1165
- const line = rawLine.trimEnd();
1166
- if (line.startsWith("worktree ")) {
1167
- currentWorktree = line.slice("worktree ".length).trim();
1168
- continue;
1169
- }
1170
-
1171
- if (line.startsWith("branch refs/heads/")) {
1172
- const usedBranch = line.slice("branch refs/heads/".length).trim();
1173
- if (usedBranch === branch && currentWorktree) {
1174
- return currentWorktree;
1175
- }
1176
- }
1132
+ return {
1133
+ ok: false,
1134
+ type: "check_failed",
1135
+ message: `Task branch '${branch}' is not checked out in a usable worktree`
1136
+ };
1177
1137
  }
1178
1138
 
1179
- return null;
1139
+ return gitText(["rev-parse", "HEAD"], matchedWorktree);
1180
1140
  }
1181
1141
 
1182
1142
  function interpolate(template: any, taskDir: any, artifactFile: any): any {
@@ -393,6 +393,7 @@ export function requestSandboxControl(params: Readonly<{
393
393
 
394
394
  export function requestSandboxTaskFinalization(params: Readonly<{
395
395
  agent: string;
396
+ prepareHandoff?: (binding: Readonly<{ generation: string; requestId: string }>) => string;
396
397
  channelDir?: string;
397
398
  statusDir?: string;
398
399
  token?: string;
@@ -403,15 +404,18 @@ export function requestSandboxTaskFinalization(params: Readonly<{
403
404
  if (!agent) clientError('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization agent is invalid', false);
404
405
  const auth = authority(params);
405
406
  const issuedAt = Date.now();
407
+ const id = randomUUID();
408
+ const handoffSha256 = (params.prepareHandoff ?? (() => '0'.repeat(64)))({ generation: auth.generation, requestId: id });
406
409
  const request: SandboxTaskFinalizationRequest = {
407
410
  version: 3,
408
- id: randomUUID(),
411
+ id,
409
412
  ...auth,
410
413
  issuedAt,
411
414
  expiresAt: issuedAt + SANDBOX_CONTROL_ADMISSION_WINDOW_MS,
412
415
  family: 'task-finalization',
413
416
  operation: 'complete',
414
417
  agent,
418
+ handoffSha256,
415
419
  args: [],
416
420
  controllerProcess: null,
417
421
  controllerProof: null
@@ -2,6 +2,7 @@ import { spawn, type ChildProcess } 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.ts';
6
7
  import {
7
8
  createTask,
@@ -41,6 +42,11 @@ import {
41
42
  openCodexControllerRegistration,
42
43
  resolveCodexControllerBinding
43
44
  } from './controller-registration.ts';
45
+ import { shareBranchDir } from '../constants.ts';
46
+
47
+ function handoffDirectory(manifest: SandboxControlManifest): string {
48
+ return shareBranchDir({ shareBase: path.join(homedir(), '.agent-infra', 'share', manifest.project) }, manifest.branch);
49
+ }
44
50
 
45
51
  export type SandboxControlExecutionResult = {
46
52
  exitCode: number;
@@ -373,10 +379,11 @@ async function executeRequestInner(
373
379
  }
374
380
  }
375
381
  if (request.family === 'task-finalization') {
376
- const operation = parseTaskControlOperation(
382
+ const parsed = parseTaskControlOperation(
377
383
  'task-finalization', [manifest.taskId!, 'complete', '--agent', request.agent]
378
384
  );
379
- if (operation.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
385
+ if (parsed.family !== 'task-finalization') throw new Error('SANDBOX_CONTROL_FINALIZATION_OPERATION_INVALID');
386
+ const operation = { ...parsed, request: { ...parsed.request, handoffSha256: request.handoffSha256 } };
380
387
  const result = dispatchTaskControlOperation(
381
388
  createSandboxExecutorExecutionContext({
382
389
  repoRoot: manifest.repoRoot,
@@ -385,7 +392,8 @@ async function executeRequestInner(
385
392
  taskId: manifest.taskId!,
386
393
  generation: manifest.generation,
387
394
  manifestPath,
388
- requestId: request.id
395
+ requestId: request.id,
396
+ handoffDirectory: handoffDirectory(manifest)
389
397
  }),
390
398
  operation
391
399
  );
@@ -407,7 +415,8 @@ async function executeRequestInner(
407
415
  taskId: manifest.taskId!,
408
416
  generation: manifest.generation,
409
417
  manifestPath,
410
- requestId: request.id
418
+ requestId: request.id,
419
+ handoffDirectory: handoffDirectory(manifest)
411
420
  });
412
421
  const format = (value: unknown): SandboxControlExecutionResult => {
413
422
  const result = value as { status?: string };
@@ -65,7 +65,7 @@ export type SandboxTaskCommandRequest = RequestBase & Readonly<{
65
65
  family: 'task-lifecycle'; args: string[];
66
66
  }>;
67
67
  export type SandboxTaskFinalizationRequest = RequestBase & Readonly<{
68
- family: 'task-finalization'; operation: 'complete'; agent: string; args: [];
68
+ family: 'task-finalization'; operation: 'complete'; agent: string; handoffSha256: string; args: [];
69
69
  }>;
70
70
  export type SandboxTaskCreateRequest = RequestBase & Readonly<{
71
71
  family: 'task-create'; candidate: TaskCreateCandidateV1;
@@ -279,11 +279,12 @@ export function validateSandboxControlRequest(
279
279
  return request as SandboxCodexControllerRequest;
280
280
  }
281
281
  if (request.family === 'task-finalization') {
282
- const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
282
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'handoffSha256', 'id', 'issuedAt', 'operation', 'token', 'version'];
283
283
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
284
284
  || request.operation !== 'complete'
285
285
  || !Array.isArray(request.args) || request.args.length !== 0
286
286
  || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
287
+ || typeof request.handoffSha256 !== 'string' || !/^[a-f0-9]{64}$/u.test(request.handoffSha256)
287
288
  || request.controllerProcess !== null || request.controllerProof !== null) {
288
289
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
289
290
  }
@@ -1,4 +1,3 @@
1
- import { finalizationTerminalResponse } from './finalization-response.ts';
2
1
  import { completedReentryView } from './completed-reentry.ts';
3
2
  import fs from 'node:fs';
4
3
  import path from 'node:path';
@@ -77,7 +76,6 @@ import { readCodexControllerRegistration } from './controller-registration.ts';
77
76
  import { validateSandboxControlIdentity } from './identity-sentinel.ts';
78
77
  import {
79
78
  mergeSandboxTaskView,
80
- taskViewAfterFinalization,
81
79
  taskViewForManifest,
82
80
  type SandboxTaskView
83
81
  } from './task-view.ts';
@@ -207,61 +205,6 @@ export function writeSandboxControlResponse(manifest: SandboxControlManifest, re
207
205
  return true;
208
206
  }
209
207
 
210
- type FinalizationRecovery = Readonly<{
211
- status: 'matched' | 'deferred' | 'not-applicable';
212
- response?: SandboxControlResponse;
213
- }>;
214
-
215
- function finalizationRecoveryResponse(
216
- manifest: SandboxControlManifest,
217
- requestId: string,
218
- exitCode: number
219
- ): FinalizationRecovery {
220
- if (!manifest.taskId || exitCode !== 0) return { status: 'not-applicable' };
221
- let receipt;
222
- try {
223
- receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
224
- if (!receipt || !receipt.controlBinding
225
- || receipt.controlBinding.generation !== manifest.generation
226
- || receipt.controlBinding.requestId !== requestId) return { status: 'deferred' };
227
- const resolved = resolveTaskRef(manifest.taskId, { repoRoot: manifest.repoRoot });
228
- if (!resolved.ok || resolved.state !== 'completed' || receipt.lifecycle !== 'done') return { status: 'deferred' };
229
- } catch {
230
- return { status: 'deferred' };
231
- }
232
- return { status: 'matched', response: finalizationTerminalResponse(manifest.taskId, requestId, receipt) };
233
- }
234
-
235
- function publishFinalizationTaskView(
236
- manifest: SandboxControlManifest,
237
- broker: BrokerOwner,
238
- requestId: string,
239
- state: 'starting' | 'healthy' | 'busy' | 'parked',
240
- reasonCode: string | null,
241
- activeRequestId: string | null
242
- ): SandboxTaskView {
243
- let view: SandboxTaskView;
244
- try {
245
- const resolved = resolveTaskRef(manifest.taskId ?? '', { repoRoot: manifest.repoRoot });
246
- const receipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId ?? '');
247
- view = resolved.ok && resolved.state === 'completed'
248
- ? taskViewAfterFinalization({
249
- taskId: manifest.taskId ?? '', generation: manifest.generation, requestId, receipt
250
- })
251
- : {
252
- state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null,
253
- reasonCode: 'SANDBOX_TASK_VIEW_SOURCE_UNCONFIRMED'
254
- };
255
- } catch {
256
- view = {
257
- state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null,
258
- reasonCode: 'SANDBOX_TASK_VIEW_RECEIPT_INVALID'
259
- };
260
- }
261
- writeSandboxControlStatus(manifest, broker, state, reasonCode, activeRequestId, Date.now(), view);
262
- return view;
263
- }
264
-
265
208
  function payloadReference(payload: ReturnType<typeof createSandboxControlPayload>) {
266
209
  return {
267
210
  version: payload.version,
@@ -458,11 +401,7 @@ export async function recoveryResponse(
458
401
  }, payload.stdout);
459
402
  if (JSON.stringify(payloadTerminal) !== JSON.stringify(terminalResult)) return unknown(request.id);
460
403
  }
461
- const finalization = request.family === 'task-finalization' ? finalizationRecoveryResponse(manifest, request.id, 0) : null;
462
- if (evidence.exitCode === 0 && finalization?.status === 'deferred') return null;
463
- const recovery: RecoveryDomainEvidence = finalization
464
- ? { domain: { consistent: finalization.status === 'matched' } }
465
- : await readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
404
+ const recovery = await readRecoveryDomain(manifest, manifestPath, request, operation, terminalResult, payload?.stdout ?? null);
466
405
  const binding = operationRecoveryBinding(request.id, manifest.generation, manifest.taskId, request.family, operationName!);
467
406
  const decision = classifySandboxControlRecovery({
468
407
  operation,
@@ -476,16 +415,11 @@ export async function recoveryResponse(
476
415
  if (decision.outcome === 'unknown' || decision.outcome === 'in-progress') return unknown(request.id);
477
416
  if (decision.outcome === 'not-executed') return notExecuted(request.id);
478
417
  if (decision.outcome !== 'success' && decision.outcome !== 'failure') return null;
479
- if (request.family === 'task-finalization' && evidence.exitCode === 0) {
480
- if (finalization?.status !== 'matched') return null;
481
- return finalization.response ?? null;
482
- }
483
418
  return genericRecoveryResponse(request, evidence.exitCode, payload);
484
419
  }
485
420
 
486
421
  async function terminalMatchesEvidence(
487
422
  manifest: SandboxControlManifest,
488
- manifestPath: string,
489
423
  request: SandboxControlRequest,
490
424
  response: SandboxControlResponse,
491
425
  evidence: ReturnType<typeof readSandboxControlResultEvidence>,
@@ -493,14 +427,6 @@ async function terminalMatchesEvidence(
493
427
  payloadInvalid: boolean,
494
428
  terminalResult: ReturnType<typeof readSandboxControlTerminalResult> | null
495
429
  ): Promise<{ valid: boolean; payloadReferenced: boolean }> {
496
- if (request.family === 'task-finalization' && evidence.exitCode === 0) {
497
- if (!terminalResult) return { valid: false, payloadReferenced: false };
498
- const expected = await recoveryResponse(manifest, manifestPath, request, evidence, payload, terminalResult);
499
- return {
500
- valid: expected !== null && JSON.stringify(response) === JSON.stringify(expected),
501
- payloadReferenced: false
502
- };
503
- }
504
430
  if (response.version !== 2 || response.id !== request.id || response.phase !== 'completed'
505
431
  || response.exitCode !== evidence.exitCode || response.error !== null) {
506
432
  return { valid: false, payloadReferenced: false };
@@ -539,7 +465,6 @@ function publishExecutionResult(
539
465
  manifest: SandboxControlManifest,
540
466
  request: SandboxControlRequest,
541
467
  result: SandboxControlExecutionResult,
542
- broker: BrokerOwner,
543
468
  brokerOwns: () => boolean
544
469
  ): boolean {
545
470
  const normalized = sanitizeSandboxControlResult(manifest, result);
@@ -553,41 +478,26 @@ function publishExecutionResult(
553
478
  criticalRequestPhase(manifest, request, 'evidence-written', normalized.exitCode === 0 ? 'success' : 'failure');
554
479
  criticalRequestPhase(manifest, request, 'publish-authorized', normalized.exitCode === 0 ? 'success' : 'failure');
555
480
  }
556
- let terminal: SandboxControlResponse | null = null;
557
- if (request.family === 'task-finalization') {
558
- const finalization = finalizationRecoveryResponse(manifest, request.id, normalized.exitCode);
559
- if (finalization.status === 'deferred') return false;
560
- terminal = finalization.response ?? null;
561
- }
562
- if (!terminal) {
563
- const inline: SandboxControlResponse = {
564
- version: 2, id: request.id, phase: 'completed', exitCode: normalized.exitCode,
565
- stdout: normalized.stdout, stderr: normalized.stderr, error: null
566
- };
567
- if (sandboxControlEncodedJsonBytes(inline) <= SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES) {
568
- terminal = inline;
569
- }
481
+ let terminal: SandboxControlResponse = {
482
+ version: 2, id: request.id, phase: 'completed', exitCode: normalized.exitCode,
483
+ stdout: normalized.stdout, stderr: normalized.stderr, error: null
484
+ };
485
+ if (sandboxControlEncodedJsonBytes(terminal) > SANDBOX_CONTROL_MAX_TERMINAL_RECORD_BYTES) {
570
486
  let payload: ReturnType<typeof createSandboxControlPayload> | null = null;
571
- if (!terminal) {
572
- try {
573
- payload = createSandboxControlPayload(manifest, request.id, normalized);
574
- const usage = sandboxControlGenerationUsage(manifest);
575
- if (usage.bytes + sandboxControlEncodedJsonBytes(payload) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES) {
576
- writeSandboxControlPayload(manifest, request.id, normalized);
577
- } else {
578
- payload = null;
579
- }
580
- } catch {
487
+ try {
488
+ payload = createSandboxControlPayload(manifest, request.id, normalized);
489
+ const usage = sandboxControlGenerationUsage(manifest);
490
+ if (usage.bytes + sandboxControlEncodedJsonBytes(payload) <= SANDBOX_CONTROL_MAX_RESPONSE_BYTES) {
491
+ writeSandboxControlPayload(manifest, request.id, normalized);
492
+ } else {
581
493
  payload = null;
582
494
  }
583
- terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish');
495
+ } catch {
496
+ payload = null;
584
497
  }
498
+ terminal = genericRecoveryResponse(request, normalized.exitCode, payload, 'publish');
585
499
  }
586
500
  if (!brokerOwns()) return false;
587
- if (request.family === 'task-finalization' && normalized.exitCode === 0) {
588
- const view = publishFinalizationTaskView(manifest, broker, request.id, 'healthy', null, null);
589
- if (view.state === 'unknown') return false;
590
- }
591
501
  const committed = writeSandboxControlResponse(manifest, terminal);
592
502
  if (committed && fs.existsSync(path.join(manifest.processingDir, request.id, 'transitions'))) {
593
503
  criticalRequestPhase(manifest, request, 'published-committed', normalized.exitCode === 0 ? 'success' : 'failure');
@@ -780,7 +690,6 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
780
690
  if (!terminalResult && request.family !== 'task-create') continue;
781
691
  const reconciliation = await terminalMatchesEvidence(
782
692
  manifest,
783
- manifestPath,
784
693
  request,
785
694
  existingTerminalResponse!,
786
695
  resultEvidence,
@@ -791,10 +700,6 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
791
700
  if (!reconciliation.valid) continue;
792
701
  terminalReconciled = true;
793
702
  payloadReferenced = reconciliation.payloadReferenced;
794
- if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
795
- const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
796
- if (view.state === 'unknown') continue;
797
- }
798
703
  }
799
704
  if (!terminalResult && !terminalReconciled) {
800
705
  if (terminal) continue;
@@ -814,17 +719,13 @@ async function recoverProcessing(manifest: SandboxControlManifest, manifestPath:
814
719
  if (!resultEvidence || !request || payloadInvalid) continue;
815
720
  const recovered = await recoveryResponse(manifest, manifestPath, request, resultEvidence, payload, terminalResult!);
816
721
  if (!recovered) continue;
817
- if (request.family === 'task-finalization' && resultEvidence.exitCode === 0) {
818
- const view = publishFinalizationTaskView(manifest, broker, request.id, 'starting', null, null);
819
- if (view.state === 'unknown') continue;
820
- }
821
722
  writeSandboxControlResponse(manifest, recovered);
822
723
  const preserveRecoveryEvidence = recovered.error?.code === 'SANDBOX_CONTROL_RESULT_UNKNOWN';
823
724
  if (transitionProtocolActive && !preserveRecoveryEvidence) {
824
725
  writeSandboxControlTransition(manifest, { requestId: entry.name, phase: 'recovered' });
825
726
  }
826
727
  if (preserveRecoveryEvidence) continue;
827
- payloadReferenced = Boolean(payload) && request.family !== 'task-finalization';
728
+ payloadReferenced = Boolean(payload);
828
729
  }
829
730
  } else {
830
731
  if (!brokerOwns()) return false;
@@ -922,14 +823,10 @@ export async function serveSandboxControl(
922
823
  try {
923
824
  startupReceipt = readTaskFinalizationReceipt(manifest.repoRoot, manifest.taskId);
924
825
  } catch {
925
- // Preserve malformed receipt evidence as unknown rather than treating it as no receipt.
926
826
  startupReceipt = {};
927
827
  }
928
828
  }
929
- let taskView = taskViewForManifest({
930
- ...manifest,
931
- receipt: startupReceipt
932
- });
829
+ let taskView = taskViewForManifest({ ...manifest, receipt: startupReceipt });
933
830
  try {
934
831
  const previous = readSandboxControlStatus(manifest.publicStatusDir);
935
832
  if (previous.generation === manifest.generation) taskView = mergeSandboxTaskView(taskView, previous.taskView);
@@ -1086,15 +983,7 @@ export async function serveSandboxControl(
1086
983
  if (!brokerOwns()) break;
1087
984
  let terminalCommitted = false;
1088
985
  if (settledExecution.result && settledExecution.resultEvidenceWritten) {
1089
- terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, broker, brokerOwns);
1090
- if (terminalCommitted && settledExecution.request.family === 'task-finalization') {
1091
- try {
1092
- const published = readSandboxControlStatus(manifest.publicStatusDir);
1093
- if (published.generation === manifest.generation) taskView = published.taskView;
1094
- } catch {
1095
- taskView = { state: 'unknown', taskId: manifest.taskId, observedSource: 'unknown', receipt: null, reasonCode: 'SANDBOX_TASK_VIEW_STATUS_INVALID' };
1096
- }
1097
- }
986
+ terminalCommitted = publishExecutionResult(manifest, settledExecution.request, settledExecution.result, brokerOwns);
1098
987
  } else {
1099
988
  terminalCommitted = writeSandboxControlResponse(manifest, unknown(settledExecution.request.id));
1100
989
  }
@@ -1341,7 +1230,7 @@ export async function serveSandboxControl(
1341
1230
  }
1342
1231
  }
1343
1232
  if (owned && active.result && active.resultEvidenceWritten) {
1344
- if (publishExecutionResult(manifest, active.request, active.result, broker, brokerOwns)) {
1233
+ if (publishExecutionResult(manifest, active.request, active.result, brokerOwns)) {
1345
1234
  if (brokerOwns()) {
1346
1235
  removeAcceptedResponse(manifest, active.request.id);
1347
1236
  fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
@@ -1350,22 +1239,8 @@ export async function serveSandboxControl(
1350
1239
  }
1351
1240
  }
1352
1241
  if (active) {
1353
- const terminationConfirmed = active.prepared.terminate(owned);
1354
- if (owned && brokerOwns() && active.request.family === 'task-finalization') {
1355
- const recovered = finalizationRecoveryResponse(manifest, active.request.id, 0);
1356
- const view = recovered.status === 'matched'
1357
- ? publishFinalizationTaskView(manifest, broker, active.request.id, 'healthy', null, null)
1358
- : null;
1359
- if (view?.state !== 'unknown' && recovered.status === 'matched' && recovered.response
1360
- && writeSandboxControlResponse(manifest, recovered.response)) {
1361
- if (terminationConfirmed && brokerOwns()) {
1362
- removeAcceptedResponse(manifest, active.request.id);
1363
- fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
1364
- active = null;
1365
- }
1366
- }
1367
- }
1368
- if (owned && brokerOwns() && active && active.request.family !== 'task-finalization' && !active.resultEvidenceWritten) {
1242
+ active.prepared.terminate(owned);
1243
+ if (owned && brokerOwns() && active && !active.resultEvidenceWritten) {
1369
1244
  writeSandboxControlResponse(manifest, unknown(active.request.id));
1370
1245
  }
1371
1246
  }
@@ -47,6 +47,7 @@ import { toolConfigDirCandidates, toolProjectDirCandidates } from './tools.ts';
47
47
  import type { SandboxTool } from './tools.ts';
48
48
  import { getProcessStartTime } from '../server/process-state.ts';
49
49
  import { releaseStaleShortIdRegistry } from '../task/short-id.ts';
50
+ import { withRepositoryMutationLock } from '../task/task-execution-lock.ts';
50
51
  import { fetchSandboxRows, type SandboxRow } from './commands/list-running.ts';
51
52
  import {
52
53
  formatIntermediateCleanupReport,
@@ -959,7 +960,10 @@ async function rmOne(
959
960
  branch: string,
960
961
  options: RmOneOptions = {}
961
962
  ): Promise<void> {
962
- await runRmOneUnderRepositoryLock(config, tools, branch, options);
963
+ await withRepositoryMutationLock(
964
+ config.repoRoot,
965
+ () => runRmOneUnderRepositoryLock(config, tools, branch, options)
966
+ );
963
967
  }
964
968
 
965
969
  async function removeUncheckedSandbox(
@@ -998,7 +1002,16 @@ async function removeUncheckedSandbox(
998
1002
  return null;
999
1003
  }
1000
1004
 
1005
+ const worktreePermits = options.permits ?? await authorizeWorktrees(
1006
+ existingWorktrees,
1007
+ { allowDirtyDiscard: true, assumeYes: Boolean(options.assumeYes) },
1008
+ { ...options.prompt, interactive: options.interactive }
1009
+ );
1010
+
1001
1011
  for (const container of matchedContainers) runSafeEngine(engine, 'docker', ['rm', '-f', container]);
1012
+ if (!sandboxContainersRemoved(engine, matchedContainers)) {
1013
+ throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
1014
+ }
1002
1015
  for (const root of controlRoots) {
1003
1016
  fs.rmSync(root, { recursive: true, force: true });
1004
1017
  removeEmptyManagedParent(path.join(config.controlBase, config.project), root);
@@ -1011,12 +1024,15 @@ async function removeUncheckedSandbox(
1011
1024
  for (const shell of shellConfigDirCandidates(config, effectiveBranch)) fs.rmSync(shell, { recursive: true, force: true });
1012
1025
  if (shouldRemoveShare) fs.rmSync(sharePath, { recursive: true, force: true });
1013
1026
  if (shouldRemoveWorktree) {
1014
- for (const worktree of existingWorktrees) fs.rmSync(worktree, { recursive: true, force: true });
1015
- runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
1027
+ for (const worktree of existingWorktrees) {
1028
+ const permit = worktreePermits.get(path.resolve(worktree));
1029
+ if (!permit) throw new Error(`SANDBOX_WORKTREE_REMOVAL_PERMIT_MISSING: ${worktree}`);
1030
+ removeWorktreeDir(config.repoRoot, config.worktreeBase, worktree, permit);
1031
+ }
1016
1032
  }
1017
- if (shouldDeleteBranch) runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1018
- if (!sandboxContainersRemoved(engine, matchedContainers)) {
1019
- throw new Error(`SANDBOX_REMOVAL_CONTAINER_STILL_PRESENT: ${matchedContainers.join(', ')}`);
1033
+ if (shouldDeleteBranch) {
1034
+ assertBranchRemovalIdentity(config, effectiveBranch, worktreePermits);
1035
+ runSafe('git', ['-C', config.repoRoot, 'branch', '-D', effectiveBranch]);
1020
1036
  }
1021
1037
  if (target.workspace.mode === 'task-bound') {
1022
1038
  releaseStaleShortIdRegistry(config.repoRoot, target.workspace.taskId);
@@ -1045,7 +1061,7 @@ async function rmPurge(
1045
1061
  tools: SandboxTool[],
1046
1062
  prompt: PromptDependencies = {}
1047
1063
  ): Promise<void> {
1048
- return rmPurgeCore(config, tools, prompt);
1064
+ return withRepositoryMutationLock(config.repoRoot, () => rmPurgeCore(config, tools, prompt));
1049
1065
  }
1050
1066
 
1051
1067
  async function rmPurgeCore(
@@ -8,7 +8,6 @@ import { enumerateAllTaskDirs } from '../task/resolve-ref.ts';
8
8
  import { resolveSandboxCleanupTarget } from './workspace-identity.ts';
9
9
  import { readSandboxControlManifest } from './control/lifecycle.ts';
10
10
  import { readJsonFile, readSandboxControlStatus } from './control/state.ts';
11
- import { finalizationTerminalResponse } from './control/finalization-response.ts';
12
11
 
13
12
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
14
13
 
@@ -83,13 +82,10 @@ function canonicalPath(input: string): string {
83
82
 
84
83
  function taskFinalizationReceiptComplete(
85
84
  receipt: TaskFinalizationReceipt,
86
- taskId: string,
87
- binding: SandboxControlBinding
85
+ taskId: string
88
86
  ): boolean {
89
87
  return receipt.taskId === taskId
90
- && taskFinalizationReceiptState(receipt) === 'complete'
91
- && receipt.controlBinding?.generation === binding.generation
92
- && receipt.controlBinding.requestId === binding.requestId;
88
+ && taskFinalizationReceiptState(receipt) === 'complete';
93
89
  }
94
90
 
95
91
  function terminalControlBindingEvidence(
@@ -114,24 +110,20 @@ function terminalControlBindingEvidence(
114
110
  if (errorCode(error) !== 'ENOENT') return false;
115
111
  }
116
112
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
117
- if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId, binding)) {
113
+ if (!receipt || !taskFinalizationReceiptComplete(receipt, taskId)) {
118
114
  return false;
119
115
  }
120
- const status = readSandboxControlStatus(manifest.publicStatusDir);
121
- const viewReceipt = status.taskView.receipt;
122
- const terminalTaskView = (status.taskView.state === 'current' && status.taskView.observedSource === 'completed')
123
- || (status.taskView.state === 'finalized-stale' && status.taskView.observedSource === 'active');
124
- if (status.generation !== binding.generation || status.state !== 'healthy'
125
- || status.activeRequestId !== null || !terminalTaskView || status.taskView.taskId !== taskId
126
- || !viewReceipt || viewReceipt.generation !== binding.generation
127
- || viewReceipt.requestId !== binding.requestId
128
- || viewReceipt.receiptId !== receipt.receiptId
129
- || viewReceipt.revision !== receipt.revision) return false;
130
116
  const responsePath = path.join(manifest.channelDir, 'responses', `${binding.requestId}.json`);
131
117
  if (!inspectOwnedPath(path.join(controlRoot, 'channel'), responsePath, 'file')) return false;
132
118
  const actual = readJsonFile(responsePath);
133
- const expected = finalizationTerminalResponse(taskId, binding.requestId, receipt);
134
- return JSON.stringify(actual) === JSON.stringify(expected);
119
+ const response = actual as Record<string, unknown>;
120
+ if (response.version !== 2 || response.id !== binding.requestId || response.phase !== 'completed'
121
+ || response.exitCode !== 0 || typeof response.stdout !== 'string') return false;
122
+ const envelope = JSON.parse(response.stdout) as Record<string, unknown>;
123
+ const result = envelope.result as Record<string, unknown> | null;
124
+ return envelope.status === 'completed' && envelope.accepted === true
125
+ && result !== null && typeof result === 'object'
126
+ && result.status === 'completed' && result.taskId === taskId;
135
127
  } catch {
136
128
  return false;
137
129
  }
@@ -157,7 +149,7 @@ function removedControlBindingEvidence(
157
149
  if (frontmatter.id !== taskId || frontmatter.branch !== journal.target.branch) return false;
158
150
  try {
159
151
  const receipt = readTaskFinalizationReceipt(repoRoot, taskId);
160
- return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId, binding);
152
+ return receipt !== null && taskFinalizationReceiptComplete(receipt, taskId);
161
153
  } catch {
162
154
  return false;
163
155
  }
@@ -14,7 +14,6 @@ import { TaskExecutionLockError, withTaskExecutionLock } from './task-execution-
14
14
  import type { TaskExecutionLockOptions } from './task-execution-lock.ts';
15
15
  import { captureTaskWriteMetadata, writeTask } from './write.ts';
16
16
  import type { TaskMutation, TaskOperationSummary, TaskWriteOptions } from './write.ts';
17
- import { allowsManualOverride } from './guard-override.ts';
18
17
 
19
18
  type PrReviewVerdict = 'approved' | 'changes-requested' | 'commented';
20
19
  type PrReviewOutcome = 'aborted' | 'superseded';
@@ -398,7 +397,7 @@ export function applyPrReviewActivityIntent(intent: PrReviewActivityIntent, opti
398
397
  if ('code' in validated) return failed(intent, validated.code, validated.message);
399
398
  const resolved = resolveTaskRef(intent.taskRef, { repoRoot: options.repoRoot });
400
399
  if (!resolved.ok) return failed(intent, resolved.code, resolved.message, resolved.taskId);
401
- if (resolved.state !== 'active' && !allowsManualOverride(options.manualOverride, 'activity-intent', 'TASK_STATE_MISMATCH')) {
400
+ if (resolved.state !== 'active') {
402
401
  return failed(intent, 'TASK_STATE_MISMATCH', `task ${resolved.taskId} is ${resolved.state}, expected active`, resolved.taskId);
403
402
  }
404
403
  if (options.lockAlreadyHeld) return applyLocked(intent, validated.agent, { ...options, repoRoot: resolved.repoRoot });