@fitlab-ai/agent-infra 0.9.9 → 0.9.11

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 (309) hide show
  1. package/README.md +13 -3
  2. package/README.zh-CN.md +13 -3
  3. package/bin/cli.ts +17 -4
  4. package/dist/bin/cli.js +19 -4
  5. package/dist/bin/internal-cli.js +55 -4
  6. package/dist/lib/agent-client.js +0 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +17 -10
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +11 -31
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +15 -33
  10. package/dist/lib/agent-clients/adapters/codex-sandbox.js +72 -11
  11. package/dist/lib/agent-clients/adapters/index.js +3 -1
  12. package/dist/lib/agent-clients/adapters/opencode-sandbox.js +0 -4
  13. package/dist/lib/agent-clients/adapters/traecli.js +65 -0
  14. package/dist/lib/agent-clients/config.js +14 -99
  15. package/dist/lib/agent-clients/reconcile.js +1 -11
  16. package/dist/lib/agent-clients/schema.js +5 -4
  17. package/dist/lib/agent-clients/tokens.js +7 -17
  18. package/dist/lib/agent-clients/types.js +2 -1
  19. package/dist/lib/decide.js +14 -30
  20. package/dist/lib/defaults.json +9 -0
  21. package/dist/lib/init.js +2 -2
  22. package/dist/lib/internal/codex-lifecycle.js +2 -2
  23. package/dist/lib/internal/codex-sandbox-controller.js +4 -3
  24. package/dist/lib/internal/release-workflow.js +0 -1
  25. package/dist/lib/internal/sandbox-control.js +2 -2
  26. package/dist/lib/internal/task-finalization.js +19 -9
  27. package/dist/lib/internal/task-ledger.js +8 -2
  28. package/dist/lib/internal/task-lifecycle.js +17 -93
  29. package/dist/lib/internal/task-orchestration.js +30 -216
  30. package/dist/lib/{task/commands/validate.js → internal/task-validate.js} +77 -43
  31. package/dist/lib/merge.js +205 -103
  32. package/dist/lib/process-data/normalize.js +2 -9
  33. package/dist/lib/run/host.js +46 -16
  34. package/dist/lib/run/tui.js +5 -2
  35. package/dist/lib/sandbox/agent-client-reconciler.js +4 -12
  36. package/dist/lib/sandbox/commands/create.js +3 -3
  37. package/dist/lib/sandbox/commands/enter.js +18 -4
  38. package/dist/lib/sandbox/config.js +3 -12
  39. package/dist/lib/sandbox/control/controller-registration.js +4 -2
  40. package/dist/lib/sandbox/control/executor.js +135 -40
  41. package/dist/lib/sandbox/control/lifecycle.js +29 -20
  42. package/dist/lib/sandbox/control/server.js +18 -16
  43. package/dist/lib/sandbox/mounts.js +3 -2
  44. package/dist/lib/sandbox/recovery.js +123 -22
  45. package/dist/lib/sandbox/workspace-identity.js +14 -0
  46. package/dist/lib/sandbox/workspace-view.js +8 -9
  47. package/dist/lib/self-update.js +278 -0
  48. package/dist/lib/server/protocol.js +5 -5
  49. package/dist/lib/task/activity-log.js +4 -1
  50. package/dist/lib/task/artifact-lifecycle.js +4 -2
  51. package/dist/lib/task/codex-orchestration.js +32 -35
  52. package/dist/lib/task/command-options.js +1 -3
  53. package/dist/lib/task/commands/cat.js +0 -5
  54. package/dist/lib/task/commands/decisions.js +76 -92
  55. package/dist/lib/task/commands/files.js +6 -6
  56. package/dist/lib/task/commands/grep.js +25 -61
  57. package/dist/lib/task/commands/issue-body.js +6 -6
  58. package/dist/lib/task/commands/log.js +25 -10
  59. package/dist/lib/task/commands/show.js +5 -6
  60. package/dist/lib/task/commands/status.js +8 -8
  61. package/dist/lib/task/commit-operation.js +21 -25
  62. package/dist/lib/task/commit-policy.js +1 -1
  63. package/dist/lib/task/control-authority.js +431 -0
  64. package/dist/lib/task/create.js +6 -1
  65. package/dist/lib/task/current-contract.js +35 -0
  66. package/dist/lib/task/decision-details.js +263 -0
  67. package/dist/lib/task/decision-intents.js +5 -2
  68. package/dist/lib/task/delegation-receipts.js +8 -7
  69. package/dist/lib/task/events.js +11 -5
  70. package/dist/lib/task/finalization.js +72 -46
  71. package/dist/lib/task/index.js +19 -29
  72. package/dist/lib/task/ledger-intents.js +8 -9
  73. package/dist/lib/task/ledger.js +13 -6
  74. package/dist/lib/task/lifecycle.js +16 -0
  75. package/dist/lib/task/orchestration.js +51 -14
  76. package/dist/lib/task/review-artifacts.js +31 -12
  77. package/dist/lib/task/review-finalization.js +19 -6
  78. package/dist/lib/task/verification-engine.js +38 -18
  79. package/dist/lib/task/verification-review.js +1 -1
  80. package/dist/lib/task/workspace-snapshot.js +5 -11
  81. package/dist/lib/task/write.js +11 -1
  82. package/dist/lib/update.js +6 -37
  83. package/dist/lib/version.js +5 -1
  84. package/dist/lifecycle-build-manifest.json +3 -3
  85. package/lib/agent-client.ts +0 -1
  86. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +25 -10
  87. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +16 -33
  88. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +16 -35
  89. package/lib/agent-clients/adapters/codex-sandbox.ts +89 -14
  90. package/lib/agent-clients/adapters/index.ts +3 -1
  91. package/lib/agent-clients/adapters/opencode-sandbox.ts +0 -5
  92. package/lib/agent-clients/adapters/traecli.ts +66 -0
  93. package/lib/agent-clients/config.ts +14 -144
  94. package/lib/agent-clients/reconcile.ts +1 -12
  95. package/lib/agent-clients/schema.ts +5 -4
  96. package/lib/agent-clients/tokens.ts +7 -20
  97. package/lib/agent-clients/types.ts +2 -1
  98. package/lib/decide.ts +12 -22
  99. package/lib/defaults.json +9 -0
  100. package/lib/init.ts +2 -2
  101. package/lib/internal/codex-lifecycle.ts +2 -2
  102. package/lib/internal/codex-sandbox-controller.ts +4 -3
  103. package/lib/internal/release-workflow.ts +0 -1
  104. package/lib/internal/sandbox-control.ts +2 -2
  105. package/lib/internal/task-finalization.ts +27 -13
  106. package/lib/internal/task-ledger.ts +8 -2
  107. package/lib/internal/task-lifecycle.ts +33 -76
  108. package/lib/internal/task-orchestration.ts +33 -190
  109. package/lib/{task/commands/validate.ts → internal/task-validate.ts} +77 -41
  110. package/lib/merge.ts +236 -120
  111. package/lib/process-data/normalize.ts +2 -8
  112. package/lib/run/host.ts +63 -14
  113. package/lib/run/tui.ts +5 -3
  114. package/lib/sandbox/agent-client-reconciler.ts +5 -15
  115. package/lib/sandbox/commands/create.ts +4 -4
  116. package/lib/sandbox/commands/enter.ts +24 -4
  117. package/lib/sandbox/config.ts +2 -14
  118. package/lib/sandbox/control/container-identity.ts +2 -2
  119. package/lib/sandbox/control/controller-registration.ts +8 -3
  120. package/lib/sandbox/control/executor.ts +156 -53
  121. package/lib/sandbox/control/lifecycle.ts +34 -28
  122. package/lib/sandbox/control/protocol.ts +1 -3
  123. package/lib/sandbox/control/server.ts +15 -12
  124. package/lib/sandbox/control/state.ts +7 -7
  125. package/lib/sandbox/mounts.ts +4 -3
  126. package/lib/sandbox/recovery.ts +162 -34
  127. package/lib/sandbox/tools.ts +1 -1
  128. package/lib/sandbox/workspace-identity.ts +28 -0
  129. package/lib/sandbox/workspace-view.ts +14 -10
  130. package/lib/self-update.ts +341 -0
  131. package/lib/server/protocol.ts +5 -5
  132. package/lib/task/activity-log.ts +6 -1
  133. package/lib/task/artifact-lifecycle.ts +5 -3
  134. package/lib/task/codex-orchestration.ts +40 -45
  135. package/lib/task/command-options.ts +1 -2
  136. package/lib/task/commands/cat.ts +0 -4
  137. package/lib/task/commands/decisions.ts +80 -75
  138. package/lib/task/commands/files.ts +6 -6
  139. package/lib/task/commands/grep.ts +23 -51
  140. package/lib/task/commands/issue-body.ts +6 -6
  141. package/lib/task/commands/log.ts +22 -10
  142. package/lib/task/commands/show.ts +5 -6
  143. package/lib/task/commands/status.ts +8 -8
  144. package/lib/task/commit-operation.ts +25 -30
  145. package/lib/task/commit-policy.ts +1 -2
  146. package/lib/task/control-authority.ts +563 -0
  147. package/lib/task/create.ts +6 -1
  148. package/lib/task/current-contract.ts +49 -0
  149. package/lib/task/decision-details.ts +340 -0
  150. package/lib/task/decision-intents.ts +7 -2
  151. package/lib/task/delegation-receipts.ts +8 -6
  152. package/lib/task/events.ts +10 -5
  153. package/lib/task/finalization.ts +84 -42
  154. package/lib/task/index.ts +19 -28
  155. package/lib/task/ledger-intents.ts +6 -8
  156. package/lib/task/ledger.ts +17 -7
  157. package/lib/task/lifecycle.ts +20 -0
  158. package/lib/task/orchestration.ts +42 -16
  159. package/lib/task/review-artifacts.ts +53 -15
  160. package/lib/task/review-finalization.ts +33 -7
  161. package/lib/task/verification-engine.ts +33 -21
  162. package/lib/task/verification-review.ts +1 -1
  163. package/lib/task/workspace-snapshot.ts +4 -13
  164. package/lib/task/write.ts +20 -1
  165. package/lib/update.ts +6 -40
  166. package/lib/version.ts +6 -1
  167. package/package.json +1 -1
  168. package/templates/.agents/QUICKSTART.en.md +2 -3
  169. package/templates/.agents/QUICKSTART.zh-CN.md +2 -3
  170. package/templates/.agents/README.en.md +5 -3
  171. package/templates/.agents/README.zh-CN.md +5 -3
  172. package/templates/.agents/rules/README.en.md +1 -0
  173. package/templates/.agents/rules/README.zh-CN.md +1 -0
  174. package/templates/.agents/rules/compatibility-policy.en.md +40 -0
  175. package/templates/.agents/rules/compatibility-policy.zh-CN.md +40 -0
  176. package/templates/.agents/rules/create-issue.github.en.md +1 -1
  177. package/templates/.agents/rules/create-issue.github.zh-CN.md +1 -1
  178. package/templates/.agents/rules/human-decision-context.en.md +9 -7
  179. package/templates/.agents/rules/human-decision-context.zh-CN.md +9 -7
  180. package/templates/.agents/rules/issue-fields.github.en.md +1 -1
  181. package/templates/.agents/rules/issue-fields.github.zh-CN.md +1 -1
  182. package/templates/.agents/rules/issue-pr-commands.en.md +1 -1
  183. package/templates/.agents/rules/issue-pr-commands.github.en.md +1 -1
  184. package/templates/.agents/rules/issue-pr-commands.github.zh-CN.md +1 -1
  185. package/templates/.agents/rules/issue-pr-commands.zh-CN.md +1 -1
  186. package/templates/.agents/rules/issue-sync.github.en.md +1 -1
  187. package/templates/.agents/rules/issue-sync.github.zh-CN.md +2 -2
  188. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -4
  189. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -4
  190. package/templates/.agents/rules/milestone-inference.github.en.md +1 -1
  191. package/templates/.agents/rules/milestone-inference.github.zh-CN.md +1 -1
  192. package/templates/.agents/rules/next-step-output.en.md +3 -3
  193. package/templates/.agents/rules/next-step-output.zh-CN.md +3 -3
  194. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -1
  195. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -1
  196. package/templates/.agents/rules/pr-sync.en.md +2 -2
  197. package/templates/.agents/rules/pr-sync.github.en.md +3 -2
  198. package/templates/.agents/rules/pr-sync.github.zh-CN.md +3 -1
  199. package/templates/.agents/rules/pr-sync.zh-CN.md +2 -1
  200. package/templates/.agents/rules/release-commands.github.en.md +6 -0
  201. package/templates/.agents/rules/release-commands.github.zh-CN.md +6 -0
  202. package/templates/.agents/rules/review-handshake.en.md +1 -1
  203. package/templates/.agents/rules/review-handshake.zh-CN.md +1 -1
  204. package/templates/.agents/rules/review-method.en.md +1 -0
  205. package/templates/.agents/rules/review-method.zh-CN.md +1 -0
  206. package/templates/.agents/rules/task-management.en.md +1 -2
  207. package/templates/.agents/rules/task-management.zh-CN.md +1 -2
  208. package/templates/.agents/skills/analyze-task/SKILL.en.md +4 -3
  209. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +4 -3
  210. package/templates/.agents/skills/block-task/SKILL.en.md +2 -2
  211. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -2
  212. package/templates/.agents/skills/cancel-task/SKILL.en.md +2 -2
  213. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +2 -2
  214. package/templates/.agents/skills/check-task/SKILL.en.md +2 -2
  215. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +2 -2
  216. package/templates/.agents/skills/close-codescan/SKILL.en.md +1 -1
  217. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +1 -1
  218. package/templates/.agents/skills/close-dependabot/SKILL.en.md +1 -1
  219. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +1 -1
  220. package/templates/.agents/skills/code-task/SKILL.en.md +4 -3
  221. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +5 -3
  222. package/templates/.agents/skills/commit/SKILL.en.md +27 -7
  223. package/templates/.agents/skills/commit/SKILL.zh-CN.md +3 -4
  224. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +5 -5
  225. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +5 -5
  226. package/templates/.agents/skills/complete-task/SKILL.en.md +3 -3
  227. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +3 -3
  228. package/templates/.agents/skills/create-pr/SKILL.en.md +15 -11
  229. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +15 -11
  230. package/templates/.agents/skills/create-pr/reference/change-report.en.md +46 -0
  231. package/templates/.agents/skills/create-pr/reference/change-report.zh-CN.md +46 -0
  232. package/templates/.agents/skills/create-pr/scripts/change-report.mjs +203 -0
  233. package/templates/.agents/skills/create-release-note/SKILL.en.md +5 -6
  234. package/templates/.agents/skills/create-release-note/SKILL.zh-CN.md +5 -6
  235. package/templates/.agents/skills/create-task/SKILL.en.md +1 -1
  236. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +1 -1
  237. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  238. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  239. package/templates/.agents/skills/plan-task/SKILL.en.md +5 -4
  240. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +5 -4
  241. package/templates/.agents/skills/restore-task/SKILL.en.md +1 -1
  242. package/templates/.agents/skills/restore-task/SKILL.zh-CN.md +1 -1
  243. package/templates/.agents/skills/review-analysis/SKILL.en.md +5 -3
  244. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +5 -3
  245. package/templates/.agents/skills/review-analysis/config/verify.en.json +1 -0
  246. package/templates/.agents/skills/review-analysis/config/verify.zh-CN.json +1 -0
  247. package/templates/.agents/skills/review-code/SKILL.en.md +5 -3
  248. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -3
  249. package/templates/.agents/skills/review-code/config/verify.en.json +1 -0
  250. package/templates/.agents/skills/review-code/config/verify.zh-CN.json +1 -0
  251. package/templates/.agents/skills/review-code/reference/migration-risks.en.md +9 -6
  252. package/templates/.agents/skills/review-code/reference/migration-risks.zh-CN.md +9 -6
  253. package/templates/.agents/skills/review-plan/SKILL.en.md +5 -3
  254. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +5 -3
  255. package/templates/.agents/skills/review-plan/config/verify.en.json +1 -0
  256. package/templates/.agents/skills/review-plan/config/verify.zh-CN.json +1 -0
  257. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +7 -3
  258. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +7 -3
  259. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.en.md +1 -1
  260. package/templates/.agents/skills/run-manual-validation/reference/discovery-and-execution.zh-CN.md +1 -1
  261. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +2 -2
  262. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +2 -2
  263. package/templates/.agents/skills/run-task/SKILL.en.md +6 -2
  264. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +6 -2
  265. package/templates/.agents/skills/run-task/reference/host-validation.en.md +2 -2
  266. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +2 -2
  267. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +2 -2
  268. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +2 -2
  269. package/templates/.agents/skills/update-agent-infra/scripts/sync-templates.js +159 -104
  270. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  271. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  272. package/templates/.agents/templates/task.en.md +1 -1
  273. package/templates/.agents/templates/task.zh-CN.md +1 -1
  274. package/templates/.claude/commands/analyze-task.en.md +1 -1
  275. package/templates/.claude/commands/analyze-task.zh-CN.md +1 -1
  276. package/templates/.claude/commands/block-task.en.md +1 -1
  277. package/templates/.claude/commands/block-task.zh-CN.md +1 -1
  278. package/templates/.claude/commands/cancel-task.en.md +1 -1
  279. package/templates/.claude/commands/cancel-task.zh-CN.md +1 -1
  280. package/templates/.claude/commands/check-task.en.md +1 -1
  281. package/templates/.claude/commands/check-task.zh-CN.md +1 -1
  282. package/templates/.claude/commands/code-task.en.md +1 -1
  283. package/templates/.claude/commands/code-task.zh-CN.md +1 -1
  284. package/templates/.claude/commands/commit.en.md +1 -1
  285. package/templates/.claude/commands/commit.zh-CN.md +1 -1
  286. package/templates/.claude/commands/complete-manual-validation.en.md +1 -1
  287. package/templates/.claude/commands/complete-manual-validation.zh-CN.md +1 -1
  288. package/templates/.claude/commands/complete-task.en.md +1 -1
  289. package/templates/.claude/commands/complete-task.zh-CN.md +1 -1
  290. package/templates/.claude/commands/create-pr.en.md +1 -1
  291. package/templates/.claude/commands/create-pr.zh-CN.md +1 -1
  292. package/templates/.claude/commands/plan-task.en.md +1 -1
  293. package/templates/.claude/commands/plan-task.zh-CN.md +1 -1
  294. package/templates/.claude/commands/review-analysis.en.md +1 -1
  295. package/templates/.claude/commands/review-analysis.zh-CN.md +1 -1
  296. package/templates/.claude/commands/review-code.en.md +1 -1
  297. package/templates/.claude/commands/review-code.zh-CN.md +1 -1
  298. package/templates/.claude/commands/review-plan.en.md +1 -1
  299. package/templates/.claude/commands/review-plan.zh-CN.md +1 -1
  300. package/templates/.claude/commands/run-manual-validation.en.md +1 -1
  301. package/templates/.claude/commands/run-manual-validation.zh-CN.md +1 -1
  302. package/templates/.claude/commands/run-task.en.md +1 -1
  303. package/templates/.claude/commands/run-task.zh-CN.md +1 -1
  304. package/templates/.claude/commands/watch-pr.en.md +1 -1
  305. package/templates/.claude/commands/watch-pr.zh-CN.md +1 -1
  306. package/templates/.traecli/commands/update-agent-infra.en.md +7 -0
  307. package/templates/.traecli/commands/update-agent-infra.zh-CN.md +7 -0
  308. package/dist/lib/builtin-tuis.js +0 -40
  309. package/lib/builtin-tuis.ts +0 -56
@@ -228,6 +228,7 @@ export async function serveSandboxControl(
228
228
  appendSandboxControlAudit(manifest, 'broker-start', { pid: broker.pid });
229
229
  if (!recoverProcessing(manifest, brokerOwns)) return;
230
230
  while (!signal.aborted) {
231
+ let settledExecution: ActiveExecution | null = null;
231
232
  if (!brokerOwns()) break;
232
233
  let current: SandboxControlManifest;
233
234
  try {
@@ -277,18 +278,7 @@ export async function serveSandboxControl(
277
278
 
278
279
  if (active?.settled) {
279
280
  if (!brokerOwns()) break;
280
- if (active.result) {
281
- if (!brokerOwns()) break;
282
- writeResponse(manifest, {
283
- version: 2, id: active.request.id, phase: 'completed', exitCode: active.result.exitCode,
284
- stdout: active.result.stdout, stderr: active.result.stderr, error: null
285
- });
286
- } else {
287
- if (!brokerOwns()) break;
288
- writeResponse(manifest, unknown(active.request.id));
289
- }
290
- if (!brokerOwns()) break;
291
- fs.rmSync(path.join(manifest.processingDir, active.request.id), { recursive: true, force: true });
281
+ settledExecution = active;
292
282
  active = null;
293
283
  }
294
284
 
@@ -327,6 +317,19 @@ export async function serveSandboxControl(
327
317
  appendSandboxControlAudit(manifest, 'broker-state', { state, reasonCode, requestId: active?.request.id ?? null });
328
318
  lastState = stateKey;
329
319
  }
320
+ if (settledExecution) {
321
+ if (!brokerOwns()) break;
322
+ if (settledExecution.result) {
323
+ writeResponse(manifest, {
324
+ version: 2, id: settledExecution.request.id, phase: 'completed', exitCode: settledExecution.result.exitCode,
325
+ stdout: settledExecution.result.stdout, stderr: settledExecution.result.stderr, error: null
326
+ });
327
+ } else {
328
+ writeResponse(manifest, unknown(settledExecution.request.id));
329
+ }
330
+ if (!brokerOwns()) break;
331
+ fs.rmSync(path.join(manifest.processingDir, settledExecution.request.id), { recursive: true, force: true });
332
+ }
330
333
 
331
334
  let retiring = false;
332
335
  for (const name of fs.readdirSync(requestsDir).sort()) {
@@ -8,7 +8,7 @@ import type { ProcessIdentity, ProcessIdentityProbe } from '../../server/process
8
8
  import type {
9
9
  SandboxControlExecution,
10
10
  SandboxControlLease,
11
- SandboxControlManifestLike,
11
+ SandboxControlManifest,
12
12
  SandboxControlStatus
13
13
  } from './protocol.ts';
14
14
 
@@ -36,12 +36,12 @@ export function readJsonFile(filePath: string): unknown {
36
36
  return JSON.parse(fs.readFileSync(filePath, 'utf8'));
37
37
  }
38
38
 
39
- export function statusPath(manifest: SandboxControlManifestLike): string {
39
+ export function statusPath(manifest: SandboxControlManifest): string {
40
40
  return path.join(manifest.publicStatusDir, 'status.json');
41
41
  }
42
42
 
43
43
  export function writeSandboxControlStatus(
44
- manifest: SandboxControlManifestLike,
44
+ manifest: SandboxControlManifest,
45
45
  broker: ProcessIdentity & { brokerId: string },
46
46
  state: SandboxControlStatus['state'],
47
47
  reasonCode: string | null,
@@ -72,7 +72,7 @@ export function readSandboxControlStatus(directory: string): SandboxControlStatu
72
72
  return parseSandboxControlStatus(readJsonFile(path.join(directory, 'status.json')));
73
73
  }
74
74
 
75
- export function readActiveLease(manifest: SandboxControlManifestLike, now = Date.now()): SandboxControlLease | null {
75
+ export function readActiveLease(manifest: SandboxControlManifest, now = Date.now()): SandboxControlLease | null {
76
76
  const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
77
77
  if (!fs.existsSync(filePath)) return null;
78
78
  const lease = readJsonFile(filePath) as Partial<SandboxControlLease> | null;
@@ -91,7 +91,7 @@ export function readActiveLease(manifest: SandboxControlManifestLike, now = Date
91
91
  }
92
92
 
93
93
  export function cleanupStaleSandboxControlLease(
94
- manifest: SandboxControlManifestLike,
94
+ manifest: SandboxControlManifest,
95
95
  now = Date.now(),
96
96
  options: Readonly<{ identityProbe?: ProcessIdentityProbe }> = {}
97
97
  ): boolean {
@@ -121,7 +121,7 @@ export function cleanupStaleSandboxControlLease(
121
121
  }
122
122
 
123
123
  export function appendSandboxControlAudit(
124
- manifest: SandboxControlManifestLike,
124
+ manifest: SandboxControlManifest,
125
125
  event: string,
126
126
  fields: Record<string, string | number | boolean | null> = {},
127
127
  now = Date.now()
@@ -136,7 +136,7 @@ export function appendSandboxControlAudit(
136
136
  });
137
137
  }
138
138
 
139
- export function executionPath(manifest: SandboxControlManifestLike, requestId: string): string {
139
+ export function executionPath(manifest: SandboxControlManifest, requestId: string): string {
140
140
  return path.join(manifest.processingDir, requestId, 'execution.json');
141
141
  }
142
142
 
@@ -27,11 +27,12 @@ export function sandboxCoreBindMounts(
27
27
  controlDir: string;
28
28
  controlStatusDir: string;
29
29
  runtimeDir?: string;
30
- taskSource?: string;
30
+ taskSources?: string[];
31
31
  taskId?: string;
32
32
  }
33
33
  ): SandboxBindMountDeclaration[] {
34
- const taskBound = Boolean(overrides.taskSource && overrides.taskId);
34
+ const taskSources = overrides.taskSources ?? [];
35
+ const taskBound = Boolean(taskSources.length > 0 && overrides.taskId);
35
36
  const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(
36
37
  ({ state, hostPath }) => taskBound && state === 'active'
37
38
  ? {
@@ -57,7 +58,7 @@ export function sandboxCoreBindMounts(
57
58
  ];
58
59
  if (taskBound) {
59
60
  mounts.push({
60
- hostPaths: [overrides.taskSource!],
61
+ hostPaths: taskSources,
61
62
  containerPath: path.posix.join('/workspace/.agents/workspace/active', overrides.taskId!),
62
63
  readOnly: false
63
64
  });
@@ -28,11 +28,12 @@ import {
28
28
  sandboxControlPaths,
29
29
  sandboxWorkspaceViewPaths
30
30
  } from './workspace-view.ts';
31
+ import { resolveTaskWorkspace } from './task-resolver.ts';
31
32
  import {
32
33
  parseSandboxWorkspaceIdentity,
33
34
  sameSandboxWorkspaceIdentity,
34
35
  type SandboxContainerWorkspaceIdentity,
35
- type SandboxWorkspaceIdentity
36
+ type SandboxWorkspaceReference
36
37
  } from './workspace-identity.ts';
37
38
  import { inspectWorktree, type WorktreeSnapshot } from './worktree-safety.ts';
38
39
  import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.ts';
@@ -91,7 +92,7 @@ export type SandboxRecoverySnapshot = {
91
92
  containerIdValid: boolean;
92
93
  expectedBranch: string;
93
94
  actualBranch: string | null;
94
- expectedWorkspace: SandboxWorkspaceIdentity;
95
+ expectedWorkspace: SandboxWorkspaceReference;
95
96
  actualWorkspace: SandboxContainerWorkspaceIdentity;
96
97
  workspaceTopology?: 'legacy-parent' | 'per-state' | 'unknown';
97
98
  taskView?: { path: string; readable: boolean };
@@ -174,7 +175,8 @@ type EnsureSandboxReadyParams = {
174
175
  config: SandboxConfig;
175
176
  engine: string;
176
177
  branch: string;
177
- workspace?: SandboxWorkspaceIdentity;
178
+ workspace?: SandboxWorkspaceReference;
179
+ reentry?: 'standard' | 'completed';
178
180
  row: SandboxRow;
179
181
  allowRecreate?: boolean;
180
182
  forceRecreate?: boolean;
@@ -187,6 +189,7 @@ type ExpectedMount = {
187
189
  path: string;
188
190
  expectedType: 'bind' | 'tmpfs';
189
191
  hostPaths: string[];
192
+ sourceAccessiblePaths?: string[];
190
193
  expectedRW: boolean;
191
194
  };
192
195
 
@@ -324,7 +327,7 @@ export async function startSandboxControlBroker(repoRoot: string, manifestPath:
324
327
  async function ensureSandboxControlBroker(params: {
325
328
  config: SandboxConfig;
326
329
  container: string;
327
- workspace: SandboxWorkspaceIdentity;
330
+ workspace: SandboxWorkspaceReference;
328
331
  }): Promise<void> {
329
332
  const control = sandboxControlPaths({
330
333
  base: params.config.controlBase ?? path.join(params.config.home, '.agent-infra', 'sandbox-control'),
@@ -603,6 +606,93 @@ function sourceCandidates(engine: string, hostPath: string): string[] {
603
606
  return [...new Set(candidates.map((candidate) => normalizeMountSource(engine, candidate)))];
604
607
  }
605
608
 
609
+ function recoveryTaskSources(repoRoot: string, taskId: string): {
610
+ mountPaths: string[];
611
+ accessiblePaths: string[];
612
+ } {
613
+ const activePath = path.join(repoRoot, '.agents', 'workspace', 'active', taskId);
614
+ const completedPath = path.join(repoRoot, '.agents', 'workspace', 'completed', taskId);
615
+ if (fs.existsSync(activePath) && !fs.existsSync(completedPath)) {
616
+ const source = assertSandboxTaskSource(repoRoot, taskId);
617
+ return { mountPaths: [source], accessiblePaths: [source] };
618
+ }
619
+
620
+ const task = resolveTaskWorkspace(taskId, repoRoot);
621
+ if (task.state === 'active') {
622
+ const source = assertSandboxTaskSource(repoRoot, taskId);
623
+ return { mountPaths: [source], accessiblePaths: [source] };
624
+ }
625
+ if (task.state !== 'completed') {
626
+ throw new Error(`SANDBOX_TASK_SOURCE_STATE_UNSUPPORTED: task ${taskId} is ${task.state}`);
627
+ }
628
+
629
+ const completedRoot = fs.realpathSync.native(
630
+ path.join(repoRoot, '.agents', 'workspace', 'completed')
631
+ );
632
+ const completedSource = fs.realpathSync.native(path.dirname(task.taskMd));
633
+ if (path.dirname(completedSource) !== completedRoot) {
634
+ throw new Error(`SANDBOX_TASK_SOURCE_INVALID: ${completedSource} is not under completed workspace`);
635
+ }
636
+
637
+ const historicalActivePath = path.join(
638
+ repoRoot,
639
+ '.agents',
640
+ 'workspace',
641
+ 'active',
642
+ taskId
643
+ );
644
+ const mountPaths = [completedSource, historicalActivePath];
645
+ if (fs.existsSync(historicalActivePath)) {
646
+ try {
647
+ mountPaths[1] = fs.realpathSync.native(historicalActivePath);
648
+ } catch {
649
+ // Keep the exact historical path as a candidate; the current completed
650
+ // source remains the only source that must still be accessible.
651
+ }
652
+ }
653
+ return { mountPaths, accessiblePaths: [completedSource] };
654
+ }
655
+
656
+ function startCompletedSandboxContainer(params: {
657
+ config: SandboxConfig;
658
+ engine: string;
659
+ container: string;
660
+ taskId: string;
661
+ start: typeof startSandboxContainer;
662
+ }): void {
663
+ const taskSources = recoveryTaskSources(params.config.repoRoot, params.taskId);
664
+ const completedSource = taskSources.accessiblePaths[0]!;
665
+ const activeRoot = path.join(params.config.repoRoot, '.agents', 'workspace', 'active');
666
+ const historicalSource = path.join(activeRoot, params.taskId);
667
+ fs.mkdirSync(activeRoot, { recursive: true });
668
+ try {
669
+ fs.lstatSync(historicalSource);
670
+ throw new Error(
671
+ `SANDBOX_COMPLETED_SOURCE_CONFLICT: historical task source already exists at ${historicalSource}`
672
+ );
673
+ } catch (error) {
674
+ if (!(error instanceof Error && 'code' in error && error.code === 'ENOENT')) throw error;
675
+ }
676
+
677
+ fs.symlinkSync(
678
+ completedSource,
679
+ historicalSource,
680
+ process.platform === 'win32' ? 'junction' : 'dir'
681
+ );
682
+ try {
683
+ params.start(params.engine, params.container);
684
+ } finally {
685
+ const temporarySource = fs.lstatSync(historicalSource);
686
+ const resolvedSource = fs.realpathSync.native(historicalSource);
687
+ if (!temporarySource.isSymbolicLink() || resolvedSource !== completedSource) {
688
+ throw new Error(
689
+ `SANDBOX_COMPLETED_SOURCE_CHANGED: refusing to remove unexpected path at ${historicalSource}`
690
+ );
691
+ }
692
+ fs.unlinkSync(historicalSource);
693
+ }
694
+ }
695
+
606
696
  function sameHostSource(left: string, right: string): boolean {
607
697
  try {
608
698
  const leftStat = fs.statSync(left, { bigint: true });
@@ -629,7 +719,7 @@ function expectedMounts(params: {
629
719
  config: SandboxConfig;
630
720
  branch: string;
631
721
  container: string;
632
- workspace: SandboxWorkspaceIdentity;
722
+ workspace: SandboxWorkspaceReference;
633
723
  tools: readonly SandboxTool[];
634
724
  actualMounts: Map<string, DockerMount>;
635
725
  }): ExpectedMount[] {
@@ -646,21 +736,29 @@ function expectedMounts(params: {
646
736
  container: params.container,
647
737
  identity: params.workspace
648
738
  });
739
+ const taskSources = params.workspace.mode === 'task-bound'
740
+ ? recoveryTaskSources(config.repoRoot, params.workspace.taskId)
741
+ : null;
742
+ const taskId = params.workspace.mode === 'task-bound' ? params.workspace.taskId : null;
649
743
  const core = sandboxCoreBindMounts(config, branch, {
650
744
  workspaceViewRoot: view.root,
651
745
  controlDir: control.channelDir,
652
746
  controlStatusDir: control.statusDir,
653
- ...(params.workspace.mode === 'task-bound'
654
- ? {
747
+ ...(taskSources === null
748
+ ? {}
749
+ : {
655
750
  runtimeDir: control.runtimeDir,
656
- taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
657
- taskId: params.workspace.taskId
658
- }
659
- : {})
751
+ taskSources: taskSources.mountPaths,
752
+ taskId: taskId!
753
+ })
660
754
  }).map((mount) => ({
661
755
  path: mount.containerPath,
662
756
  expectedType: 'bind' as const,
663
757
  hostPaths: mount.hostPaths,
758
+ ...(taskSources !== null && taskId !== null
759
+ && mount.containerPath === `/workspace/.agents/workspace/active/${taskId}`
760
+ ? { sourceAccessiblePaths: taskSources.accessiblePaths }
761
+ : {}),
664
762
  expectedRW: !mount.readOnly
665
763
  }));
666
764
  const live = tools.flatMap((tool) =>
@@ -676,13 +774,7 @@ function expectedMounts(params: {
676
774
  })
677
775
  );
678
776
  const persistentTools = tools
679
- .filter((tool) =>
680
- !tool.tmpfs
681
- && (
682
- config.agentClientSource === 'canonical'
683
- || actualMounts.has(tool.containerMount)
684
- )
685
- )
777
+ .filter((tool) => !tool.tmpfs)
686
778
  .map((tool) => ({
687
779
  path: tool.containerMount,
688
780
  expectedType: 'bind' as const,
@@ -755,7 +847,7 @@ export function collectSandboxRecoverySnapshot(params: {
755
847
  config: SandboxConfig;
756
848
  engine: string;
757
849
  branch: string;
758
- workspace?: SandboxWorkspaceIdentity;
850
+ workspace?: SandboxWorkspaceReference;
759
851
  container: string;
760
852
  deps?: RecoveryCommandDeps;
761
853
  }): SandboxRecoverySnapshot {
@@ -836,13 +928,10 @@ export function collectSandboxRecoverySnapshot(params: {
836
928
  taskId: sandboxTaskIdLabel(params.config)
837
929
  });
838
930
  const selectedToolIds = new Set(tools.map((tool) => tool.id));
839
- const enforceCanonicalPlan = params.config.agentClientSource === 'canonical';
840
- const unexpectedCapabilityMounts = enforceCanonicalPlan
841
- ? capabilityPlan.cleanupInventory
842
- .filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
843
- .map((tool) => tool.containerMount)
844
- .filter((mountPath) => mountsByDestination.has(mountPath))
845
- : [];
931
+ const unexpectedCapabilityMounts = capabilityPlan.cleanupInventory
932
+ .filter((tool) => isAgentClientId(tool.id) && !selectedToolIds.has(tool.id))
933
+ .map((tool) => tool.containerMount)
934
+ .filter((mountPath) => mountsByDestination.has(mountPath));
846
935
  const mountSnapshots = expectedMounts({
847
936
  config: params.config,
848
937
  branch: params.branch,
@@ -880,7 +969,9 @@ export function collectSandboxRecoverySnapshot(params: {
880
969
  sourceAccessible: expected.expectedType === 'tmpfs'
881
970
  ? true
882
971
  : matchedHostPath !== undefined
883
- && hostSourceAccessible(matchedHostPath, expected.expectedRW)
972
+ && (expected.sourceAccessiblePaths ?? [matchedHostPath]).some((hostPath) =>
973
+ hostSourceAccessible(hostPath, expected.expectedRW)
974
+ )
884
975
  };
885
976
  });
886
977
 
@@ -907,9 +998,7 @@ export function collectSandboxRecoverySnapshot(params: {
907
998
  identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
908
999
  && branchLabel === params.branch
909
1000
  && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
910
- runtimeCapabilityOk: enforceCanonicalPlan
911
- ? labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature
912
- : undefined,
1001
+ runtimeCapabilityOk: labels[sandboxRuntimeCapabilityLabel(params.config)] === capabilityPlan.runtimeSignature,
913
1002
  unexpectedCapabilityMounts,
914
1003
  mounts: mountSnapshots,
915
1004
  tmpfs,
@@ -1069,7 +1158,7 @@ function assess(params: {
1069
1158
  config: SandboxConfig;
1070
1159
  engine: string;
1071
1160
  branch: string;
1072
- workspace?: SandboxWorkspaceIdentity;
1161
+ workspace?: SandboxWorkspaceReference;
1073
1162
  container: string;
1074
1163
  deps?: RecoveryCommandDeps;
1075
1164
  }): { snapshot: SandboxRecoverySnapshot; findings: SandboxRecoveryFinding[] } {
@@ -1095,7 +1184,7 @@ export async function assertFreshSandboxReady(params: {
1095
1184
  config: SandboxConfig;
1096
1185
  engine: string;
1097
1186
  branch: string;
1098
- workspace?: SandboxWorkspaceIdentity;
1187
+ workspace?: SandboxWorkspaceReference;
1099
1188
  container: string;
1100
1189
  copiedEntries: TmpfsSeedEntry[];
1101
1190
  deps?: RecoveryCommandDeps;
@@ -1157,11 +1246,21 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
1157
1246
  workspace: params.workspace ?? { mode: 'branch-only' }
1158
1247
  });
1159
1248
  }
1160
- if (params.forceRecreate) {
1249
+ if (params.forceRecreate || (params.reentry === 'completed' && params.allowRecreate)) {
1161
1250
  throw new Error('Explicit container recreation requested.');
1162
1251
  }
1163
1252
  if (!params.row.running) {
1164
- startFn(params.engine, params.row.name);
1253
+ if (params.reentry === 'completed' && params.workspace?.mode === 'task-bound') {
1254
+ startCompletedSandboxContainer({
1255
+ config: params.config,
1256
+ engine: params.engine,
1257
+ container: params.row.name,
1258
+ taskId: params.workspace.taskId,
1259
+ start: startFn
1260
+ });
1261
+ } else {
1262
+ startFn(params.engine, params.row.name);
1263
+ }
1165
1264
  const initial = assess({
1166
1265
  config: params.config,
1167
1266
  engine: params.engine,
@@ -1281,6 +1380,35 @@ export async function ensureSandboxReady(params: EnsureSandboxReadyParams): Prom
1281
1380
  failure = error instanceof Error ? error : new Error(String(error));
1282
1381
  }
1283
1382
 
1383
+ if (params.reentry === 'completed') {
1384
+ if (params.workspace?.mode !== 'task-bound') {
1385
+ throw new Error('SANDBOX_COMPLETED_REENTRY_INVALID: completed re-entry requires a task-bound workspace');
1386
+ }
1387
+ const taskId = params.workspace.taskId;
1388
+ const commandChain = [
1389
+ `docker exec -it ${params.row.name} bash /usr/local/bin/sandbox-tmux-entry`,
1390
+ `ai sandbox rm ${taskId}`,
1391
+ `ai sandbox create ${params.branch}`
1392
+ ];
1393
+ const code = params.allowRecreate || params.forceRecreate
1394
+ ? 'SANDBOX_COMPLETED_RECREATE_UNSUPPORTED'
1395
+ : 'SANDBOX_COMPLETED_REENTRY_FAILED';
1396
+ const lead = code === 'SANDBOX_COMPLETED_RECREATE_UNSUPPORTED'
1397
+ ? 'Completed sandbox replacement is unsupported; the existing container was not replaced.'
1398
+ : 'Completed sandbox recovery failed; the existing container was not replaced.';
1399
+ throw new Error([
1400
+ `${code}: ${lead}`,
1401
+ `Task: ${taskId}; container: ${params.row.name}; branch: ${params.branch}.`,
1402
+ 'To preserve the current container state, enter it manually with:',
1403
+ ` ${commandChain[0]}`,
1404
+ 'If you intentionally want a replacement, the following is the full interactive sandbox cleanup, not a container-only delete.',
1405
+ 'It may remove the worktree, local branch, tool/shell state, and branch share:',
1406
+ ` ${commandChain[1]}`,
1407
+ ` ${commandChain[2]}`,
1408
+ `Details: ${failure.message}`
1409
+ ].join('\n'));
1410
+ }
1411
+
1284
1412
  const dataBoundary =
1285
1413
  'The existing container writable layer, ordinary /tmp data, processes, tmux sessions, and RAM state may be lost by replacement; worktree and host-managed sandbox data are preserved.';
1286
1414
  const recoveryTarget = params.workspace?.mode === 'task-bound'
@@ -41,7 +41,7 @@ type ToolsConfig = {
41
41
  project: string;
42
42
  tools: string[];
43
43
  customTools?: SandboxTool[];
44
- agentClientState?: import('../agent-clients/types.ts').AgentClientState;
44
+ agentClientState: import('../agent-clients/types.ts').AgentClientState;
45
45
  };
46
46
 
47
47
  const TOOL_ID_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
@@ -17,6 +17,8 @@ export type SandboxWorkspaceKey =
17
17
  | Readonly<{ mode: 'task-bound'; taskId: string }>
18
18
  | Readonly<{ mode: 'branch-only' }>;
19
19
 
20
+ export type SandboxWorkspaceReference = SandboxWorkspaceIdentity | SandboxWorkspaceKey;
21
+
20
22
  export type SandboxCleanupTarget = Readonly<{
21
23
  requestedRef: string;
22
24
  branch: string;
@@ -35,6 +37,11 @@ export type SandboxTarget = Readonly<{
35
37
  workspace: SandboxWorkspaceIdentity;
36
38
  }>;
37
39
 
40
+ export type SandboxReentryContext = Readonly<{
41
+ workspace: SandboxWorkspaceReference;
42
+ reentry: 'standard' | 'completed';
43
+ }>;
44
+
38
45
  function stripQuotes(value: string): string {
39
46
  return value.replace(/^(?:"([^"]*)"|'([^']*)')$/, '$1$2');
40
47
  }
@@ -116,6 +123,27 @@ export function resolveSandboxTarget(requestedRef: string, repoRoot: string): Sa
116
123
  return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
117
124
  }
118
125
 
126
+ export function resolveSandboxReentryContext(params: Readonly<{
127
+ target: SandboxTarget;
128
+ containerWorkspace: SandboxContainerWorkspaceIdentity;
129
+ repoRoot: string;
130
+ }>): SandboxReentryContext {
131
+ if (params.target.workspace.mode !== 'branch-only'
132
+ || params.containerWorkspace.mode !== 'task-bound') {
133
+ return { workspace: params.target.workspace, reentry: 'standard' };
134
+ }
135
+
136
+ const task = resolveTaskWorkspace(params.containerWorkspace.taskId, params.repoRoot);
137
+ if (task.state !== 'completed' || task.branch !== params.target.branch) {
138
+ return { workspace: params.target.workspace, reentry: 'standard' };
139
+ }
140
+
141
+ return {
142
+ workspace: { mode: 'task-bound', taskId: task.taskId },
143
+ reentry: 'completed'
144
+ };
145
+ }
146
+
119
147
  export function resolveSandboxCleanupTarget(
120
148
  requestedRef: string,
121
149
  repoRoot: string,
@@ -52,10 +52,15 @@ export type SandboxControlSetup = Readonly<{
52
52
  statusDir: string;
53
53
  runtimeDir: string;
54
54
  manifestPath: string;
55
+ manifestDraft: SandboxControlManifestDraft;
55
56
  token: string;
56
57
  generation: string;
57
58
  }>;
58
59
 
60
+ export type SandboxControlManifestDraft = Readonly<Omit<SandboxControlManifest, 'containerIdentity' | 'engine'> & {
61
+ engine: string;
62
+ }>;
63
+
59
64
  export function sandboxControlPaths(params: Readonly<{
60
65
  base: string;
61
66
  project: string;
@@ -230,14 +235,12 @@ export function materializeSandboxControl(params: Readonly<{
230
235
  const token = randomBytes(32).toString('hex');
231
236
  const generation = randomBytes(16).toString('hex');
232
237
  const repoRoot = fs.realpathSync.native(params.repoRoot);
233
- const manifest: SandboxControlManifest = {
234
- version: 5,
238
+ const manifestDraft: SandboxControlManifestDraft = {
235
239
  engine: params.engine ?? 'docker',
236
240
  repoRoot,
237
241
  worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
238
242
  project: params.project,
239
243
  container: params.container,
240
- containerIdentity: { id: '', labels: {} },
241
244
  branch: params.branch,
242
245
  mode: params.identity.mode,
243
246
  taskId: params.identity.mode === 'task-bound' ? params.identity.taskId : null,
@@ -248,19 +251,19 @@ export function materializeSandboxControl(params: Readonly<{
248
251
  processingDir,
249
252
  runtimeDir
250
253
  };
251
- fs.writeFileSync(manifestPath, `${JSON.stringify(manifest)}\n`, { mode: 0o600 });
252
- return { root, channelDir, statusDir, runtimeDir, manifestPath, token, generation };
254
+ return { root, channelDir, statusDir, runtimeDir, manifestPath, manifestDraft, token, generation };
253
255
  }
254
256
 
255
257
  export function finalizeSandboxControlManifest(
256
258
  setup: SandboxControlSetup,
257
259
  identity: Readonly<{ engine: string; id: string; labels: Readonly<Record<string, string>> }>
258
- ): void {
260
+ ): SandboxControlManifest {
259
261
  if (!identity.engine || !identity.id) throw new Error('SANDBOX_CONTROL_CONTAINER_ID_INVALID');
260
- const manifest = JSON.parse(fs.readFileSync(setup.manifestPath, 'utf8')) as Record<string, unknown>;
261
- manifest.version = 5;
262
- manifest.engine = identity.engine;
263
- manifest.containerIdentity = { id: identity.id, labels: { ...identity.labels } };
262
+ const manifest: SandboxControlManifest = {
263
+ ...setup.manifestDraft,
264
+ engine: identity.engine,
265
+ containerIdentity: { id: identity.id, labels: { ...identity.labels } }
266
+ };
264
267
  const temporary = `${setup.manifestPath}.${process.pid}.${randomBytes(8).toString('hex')}.tmp`;
265
268
  try {
266
269
  fs.writeFileSync(temporary, `${JSON.stringify(manifest)}\n`, { mode: 0o600, flag: 'wx' });
@@ -268,4 +271,5 @@ export function finalizeSandboxControlManifest(
268
271
  } finally {
269
272
  fs.rmSync(temporary, { force: true });
270
273
  }
274
+ return manifest;
271
275
  }