@fitlab-ai/agent-infra 0.11.10 → 0.11.12

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 (219) hide show
  1. package/dist/bin/internal-cli.js +2 -2
  2. package/dist/lib/agent-clients/adapter.js +27 -1
  3. package/dist/lib/agent-clients/adapters/claude-code.js +5 -1
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +78 -63
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/binding.js +114 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/cli.js +447 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-client.js +130 -0
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-operation.js +131 -0
  9. package/dist/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.js +2 -2
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +48 -1
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/recovery.js +124 -62
  12. package/dist/lib/{internal/codex-sandbox-controller.js → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.js} +3 -3
  13. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +3 -5
  14. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +222 -210
  15. package/dist/lib/agent-clients/adapters/codex-orchestration.js +468 -0
  16. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  17. package/dist/lib/agent-clients/adapters/codex.js +44 -1
  18. package/dist/lib/agent-clients/adapters/index.js +2 -10
  19. package/dist/lib/agent-clients/tokens.js +3 -2
  20. package/dist/lib/internal/agent-client.js +132 -72
  21. package/dist/lib/internal/cli-route-inventory.js +2 -2
  22. package/dist/lib/internal/manual-validation.js +2 -2
  23. package/dist/lib/internal/platform-pr.js +4 -5
  24. package/dist/lib/internal/task-artifact.js +1 -1
  25. package/dist/lib/internal/task-create.js +15 -1
  26. package/dist/lib/internal/task-finalization.js +1 -3
  27. package/dist/lib/internal/task-operation-registry.js +5 -139
  28. package/dist/lib/internal/task-validate.js +1 -1
  29. package/dist/lib/platform/issues.js +18 -6
  30. package/dist/lib/platform/pr-summary.js +45 -8
  31. package/dist/lib/platform/pull-requests.js +79 -14
  32. package/dist/lib/platform/verification-sync.js +17 -14
  33. package/dist/lib/runtime/agent-runtime.js +6 -29
  34. package/dist/lib/sandbox/commands/control-status.js +1 -0
  35. package/dist/lib/sandbox/commands/create.js +19 -2
  36. package/dist/lib/sandbox/commands/enter.js +47 -33
  37. package/dist/lib/sandbox/control/agent-operation.js +34 -0
  38. package/dist/lib/sandbox/control/client.js +68 -161
  39. package/dist/lib/sandbox/control/executor.js +13 -87
  40. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  41. package/dist/lib/sandbox/control/protocol.js +15 -44
  42. package/dist/lib/sandbox/control/server.js +57 -51
  43. package/dist/lib/sandbox/environment.js +150 -0
  44. package/dist/lib/sandbox/mounts.js +2 -1
  45. package/dist/lib/sandbox/recovery.js +11 -1
  46. package/dist/lib/sandbox/removal.js +9 -5
  47. package/dist/lib/sandbox/workspace-identity.js +0 -14
  48. package/dist/lib/sandbox/workspace-view.js +41 -23
  49. package/dist/lib/task/artifact-command.js +7 -2
  50. package/dist/lib/task/artifact-lifecycle.js +2 -1
  51. package/dist/lib/task/capabilities.js +4 -1
  52. package/dist/lib/task/control-authority.js +54 -266
  53. package/dist/lib/task/control-recovery.js +1 -1
  54. package/dist/lib/task/create.js +2 -2
  55. package/dist/lib/task/delegation-receipts.js +74 -161
  56. package/dist/lib/task/events.js +89 -45
  57. package/dist/lib/task/finalization.js +58 -60
  58. package/dist/lib/task/frontmatter.js +24 -1
  59. package/dist/lib/task/ledger-intents.js +2 -1
  60. package/dist/lib/task/lifecycle.js +84 -97
  61. package/dist/lib/task/orchestration.js +182 -129
  62. package/dist/lib/task/platform-operation-journal.js +12 -0
  63. package/dist/lib/task/platform-operation-recovery.js +14 -7
  64. package/dist/lib/task/resolve-ref.js +8 -3
  65. package/dist/lib/task/runtime-paths.js +10 -0
  66. package/dist/lib/task/verification-engine.js +28 -15
  67. package/dist/lib/task/workspace-snapshot.js +1 -1
  68. package/dist/lifecycle-build-manifest.json +3 -3
  69. package/lib/agent-clients/adapter.ts +61 -0
  70. package/lib/agent-clients/adapters/claude-code.ts +5 -1
  71. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +105 -80
  72. package/lib/agent-clients/adapters/codex-lifecycle/binding.ts +147 -0
  73. package/lib/agent-clients/adapters/codex-lifecycle/cli.ts +510 -0
  74. package/lib/agent-clients/adapters/codex-lifecycle/controller-client.ts +158 -0
  75. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +1 -1
  76. package/lib/agent-clients/adapters/codex-lifecycle/controller-operation.ts +151 -0
  77. package/lib/{sandbox/control → agent-clients/adapters/codex-lifecycle}/controller-registration.ts +4 -4
  78. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +43 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +8 -5
  80. package/lib/agent-clients/adapters/codex-lifecycle/recovery.ts +190 -65
  81. package/lib/{internal/codex-sandbox-controller.ts → agent-clients/adapters/codex-lifecycle/sandbox-controller-cli.ts} +2 -2
  82. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +3 -5
  83. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +272 -223
  84. package/lib/agent-clients/adapters/codex-orchestration.ts +632 -0
  85. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  86. package/lib/agent-clients/adapters/codex.ts +48 -1
  87. package/lib/agent-clients/adapters/index.ts +7 -20
  88. package/lib/agent-clients/tokens.ts +4 -3
  89. package/lib/internal/agent-client.ts +142 -73
  90. package/lib/internal/cli-route-inventory.ts +2 -2
  91. package/lib/internal/manual-validation.ts +2 -2
  92. package/lib/internal/platform-pr.ts +3 -4
  93. package/lib/internal/task-artifact.ts +1 -1
  94. package/lib/internal/task-create.ts +15 -2
  95. package/lib/internal/task-finalization.ts +1 -3
  96. package/lib/internal/task-operation-registry.ts +5 -151
  97. package/lib/internal/task-validate.ts +1 -1
  98. package/lib/platform/issues.ts +18 -6
  99. package/lib/platform/pr-summary.ts +43 -7
  100. package/lib/platform/pull-requests.ts +74 -13
  101. package/lib/platform/verification-sync.ts +18 -14
  102. package/lib/runtime/agent-runtime.ts +6 -41
  103. package/lib/sandbox/commands/control-status.ts +1 -0
  104. package/lib/sandbox/commands/create.ts +25 -2
  105. package/lib/sandbox/commands/enter.ts +42 -39
  106. package/lib/sandbox/control/agent-operation.ts +49 -0
  107. package/lib/sandbox/control/client.ts +86 -210
  108. package/lib/sandbox/control/executor.ts +17 -96
  109. package/lib/sandbox/control/host-recovery.ts +195 -0
  110. package/lib/sandbox/control/protocol.ts +24 -55
  111. package/lib/sandbox/control/server.ts +54 -52
  112. package/lib/sandbox/environment.ts +162 -0
  113. package/lib/sandbox/mounts.ts +2 -1
  114. package/lib/sandbox/recovery.ts +13 -1
  115. package/lib/sandbox/removal.ts +12 -5
  116. package/lib/sandbox/tool-types.ts +1 -0
  117. package/lib/sandbox/workspace-identity.ts +0 -26
  118. package/lib/sandbox/workspace-view.ts +42 -24
  119. package/lib/task/artifact-command.ts +9 -2
  120. package/lib/task/artifact-lifecycle.ts +2 -1
  121. package/lib/task/capabilities.ts +5 -2
  122. package/lib/task/control-authority.ts +65 -382
  123. package/lib/task/control-recovery.ts +2 -2
  124. package/lib/task/create.ts +3 -3
  125. package/lib/task/delegation-receipts.ts +87 -231
  126. package/lib/task/events.ts +101 -43
  127. package/lib/task/finalization.ts +57 -58
  128. package/lib/task/frontmatter.ts +25 -1
  129. package/lib/task/ledger-intents.ts +2 -1
  130. package/lib/task/lifecycle.ts +82 -87
  131. package/lib/task/orchestration.ts +237 -176
  132. package/lib/task/platform-operation-journal.ts +29 -2
  133. package/lib/task/platform-operation-recovery.ts +12 -6
  134. package/lib/task/resolve-ref.ts +8 -3
  135. package/lib/task/runtime-paths.ts +12 -0
  136. package/lib/task/verification-engine.ts +27 -16
  137. package/lib/task/workspace-snapshot.ts +1 -1
  138. package/package.json +1 -1
  139. package/templates/.agents/hooks/lifecycle-delegation.js +1 -25
  140. package/templates/.agents/rules/README.en.md +2 -2
  141. package/templates/.agents/rules/README.zh-CN.md +2 -2
  142. package/templates/.agents/rules/issue-sync.en.md +1 -1
  143. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  144. package/templates/.agents/rules/next-step-output.en.md +6 -19
  145. package/templates/.agents/rules/next-step-output.zh-CN.md +6 -19
  146. package/templates/.agents/rules/no-mid-flow-questions.en.md +19 -0
  147. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +19 -0
  148. package/templates/.agents/rules/review-handshake.en.md +3 -0
  149. package/templates/.agents/rules/review-handshake.zh-CN.md +3 -0
  150. package/templates/.agents/rules/task-management.en.md +1 -1
  151. package/templates/.agents/rules/task-management.zh-CN.md +1 -1
  152. package/templates/.agents/skills/analyze-task/SKILL.en.md +2 -2
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +2 -2
  154. package/templates/.agents/skills/block-task/SKILL.en.md +1 -1
  155. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +1 -1
  156. package/templates/.agents/skills/cancel-task/SKILL.en.md +1 -1
  157. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +1 -1
  158. package/templates/.agents/skills/check-task/SKILL.en.md +1 -1
  159. package/templates/.agents/skills/check-task/SKILL.zh-CN.md +1 -1
  160. package/templates/.agents/skills/close-codescan/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  163. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  164. package/templates/.agents/skills/code-task/SKILL.en.md +1 -1
  165. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +2 -2
  166. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  168. package/templates/.agents/skills/code-task/reference/dual-mode.en.md +2 -0
  169. package/templates/.agents/skills/code-task/reference/dual-mode.zh-CN.md +2 -0
  170. package/templates/.agents/skills/code-task/reference/fix-mode.en.md +1 -1
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +1 -1
  172. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -2
  173. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -2
  174. package/templates/.agents/skills/complete-task/SKILL.en.md +0 -3
  175. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +0 -3
  176. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  177. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  178. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  179. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  180. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  181. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  182. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  183. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  184. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/review-analysis/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/review-analysis/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +5 -5
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +5 -5
  194. package/templates/.agents/skills/review-plan/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-plan/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  197. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  198. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +1 -1
  199. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +1 -1
  200. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  201. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  202. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +4 -1
  205. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +4 -1
  206. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  207. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  208. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  209. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  210. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  211. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  212. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +0 -546
  213. package/dist/lib/internal/codex-lifecycle.js +0 -385
  214. package/dist/lib/task/codex-orchestration.js +0 -206
  215. package/dist/lib/task/finalization-handoff.js +0 -93
  216. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +0 -776
  217. package/lib/internal/codex-lifecycle.ts +0 -390
  218. package/lib/task/codex-orchestration.ts +0 -276
  219. package/lib/task/finalization-handoff.ts +0 -105
@@ -16,6 +16,7 @@ import { resourceIdentityEquals } from './resource-identity.js';
16
16
  import { buildPrChangeReport, CANONICAL_REPORT_PLACEHOLDER, readPrChangeReport, replaceCanonicalReportPlaceholder, runMechanicalChangeReport, taskIntentDigest, validateMechanicalReport, validatePrecheckCandidate, writePrChangeReportAtomic } from './pr-change-report.js';
17
17
  import { MANUAL_VALIDATION_RECEIPT_PLACEHOLDER, manualValidationFinalSummaryProjectionMatches } from '../task/manual-validation-receipt.js';
18
18
  import { readManualValidationCompletion } from '../task/manual-validation-completion.js';
19
+ import { readManualValidationTransaction } from '../task/manual-validation-transaction.js';
19
20
  function summaryMarker(taskId) {
20
21
  return `<!-- sync-pr:${taskId}:summary -->`;
21
22
  }
@@ -119,6 +120,20 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
119
120
  return { ok: false, status: inspectionError(error), error };
120
121
  }
121
122
  const remote = inspected.value;
123
+ if (!remote.identity) {
124
+ return {
125
+ ok: false,
126
+ status: 'failed',
127
+ error: { code: 'PR_IDENTITY_MISSING', message: 'Provider pull-request inspection did not return a canonical identity', retryable: false }
128
+ };
129
+ }
130
+ if (!resourceIdentityEquals(remote.identity, prIdentity)) {
131
+ return {
132
+ ok: false,
133
+ status: 'failed',
134
+ error: { code: 'PR_IDENTITY_MISMATCH', message: 'Provider returned a different pull request identity than requested', retryable: false }
135
+ };
136
+ }
122
137
  const number = remote.number ?? resourceIdentityNumber(remote.identity) ?? resourceIdentityNumber(prIdentity) ?? 0;
123
138
  const head = remote.head || { repository: context.platform.repository, ref: '', sha: remote.headSha || '' };
124
139
  const base = remote.base || { repository: context.platform.repository, ref: '', sha: remote.baseSha || '' };
@@ -127,7 +142,7 @@ async function inspectBoundPullRequest(context, repoRoot, prIdentity, loadedCont
127
142
  value: {
128
143
  repository: context.platform.repository,
129
144
  number,
130
- identity: remote.identity ?? prIdentity,
145
+ identity: remote.identity,
131
146
  nodeId: remote.id,
132
147
  url: remote.displayUrl || '',
133
148
  state: remote.state === 'closed' ? 'closed' : 'open',
@@ -261,14 +276,18 @@ async function summaryContext(taskRef, options = {}) {
261
276
  artifacts: []
262
277
  };
263
278
  const prIdentity = fact.status === 'valid' && fact.fact.state === 'bound' ? fact.fact.identity.resource : null;
264
- const prNumber = resourceIdentityNumber(prIdentity);
279
+ let prNumber = resourceIdentityNumber(prIdentity);
265
280
  const loaded = await resolvePlatformProviderContext({ cwd: resolved.repoRoot, client: options.client });
266
281
  const context = loaded.ok ? loaded.value.context : loaded.context;
267
282
  let pullRequest = null;
268
283
  if (loaded.ok && prIdentity && context.platform.repository && ['no-op', 'degraded'].includes(context.status)) {
269
284
  const inspected = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
270
- if (inspected.ok)
285
+ if (inspected.ok) {
271
286
  pullRequest = inspected.value;
287
+ if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
288
+ prNumber = inspected.value.number;
289
+ }
290
+ }
272
291
  }
273
292
  const report = summaryReportStatus(reportPath, pullRequest, taskContent, resolved.repoRoot);
274
293
  return {
@@ -493,7 +512,7 @@ async function syncPullRequestSummary(taskRef, options) {
493
512
  const prIdentity = boundFact.identity.resource;
494
513
  const boundPrNumber = resourceIdentityNumber(prIdentity);
495
514
  knownPrNumber = boundPrNumber;
496
- const prNumber = boundPrNumber;
515
+ let prNumber = boundPrNumber;
497
516
  const initial = await inspectBoundPullRequest(context, resolved.repoRoot, prIdentity, loaded.value);
498
517
  if (!initial.ok)
499
518
  return fail(initial.status, context, initial.error);
@@ -505,9 +524,23 @@ async function syncPullRequestSummary(taskRef, options) {
505
524
  if (manual && (manual.phase === 'pending' ? hasFinalManualValidation : !hasFinalManualValidation))
506
525
  return fail('failed', context, { code: 'MANUAL_VALIDATION_SUMMARY_PHASE_INVALID', message: 'manual-validation summary phase does not match the requested writer phase', retryable: false }, prNumber);
507
526
  if (manual) {
508
- if (prNumber === null)
509
- return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a numeric pull-request identity', retryable: false }, prNumber);
510
- if (manual.phase === 'final' && (!manual.transactionId || !manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
527
+ if (!Number.isSafeInteger(initial.value.number) || initial.value.number <= 0)
528
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_PR_NUMBER_REQUIRED', message: 'manual-validation summary requires a positive safe pull-request number from the authoritative snapshot', retryable: false }, prNumber);
529
+ prNumber = initial.value.number;
530
+ knownPrNumber = prNumber;
531
+ if (!manual.transactionId)
532
+ return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'manual-validation summary requires a transaction ID', retryable: false }, prNumber);
533
+ if (manual.phase === 'pending') {
534
+ const transaction = readManualValidationTransaction(resolved.taskDir, {
535
+ transactionId: manual.transactionId,
536
+ taskId: resolved.taskId,
537
+ prNumber,
538
+ prHeadSha: initial.value.head.sha
539
+ });
540
+ if (!transaction.ok)
541
+ return fail('failed', context, platformError(transaction.error), prNumber);
542
+ }
543
+ if (manual.phase === 'final' && (!manual.receiptDigest || !manual.prHeadSha || manual.prHeadSha !== initial.value.head.sha))
511
544
  return fail('failed', context, { code: 'MANUAL_VALIDATION_TRANSACTION_REQUIRED', message: 'final manual-validation summary requires transaction, receipt, and current head identity', retryable: false }, prNumber);
512
545
  if (manual.phase === 'final') {
513
546
  if (manual.authority !== 'coordinator')
@@ -688,7 +721,11 @@ async function syncPullRequestSummary(taskRef, options) {
688
721
  target: JSON.stringify(prIdentity),
689
722
  expectedDigest: createHash('sha256').update(desired).digest('hex'),
690
723
  dependency: 'deferred',
691
- pullRequestSummary: { body: options.body, changeReportFile: options.changeReportFile }
724
+ pullRequestSummary: {
725
+ body: options.body,
726
+ changeReportFile: options.changeReportFile,
727
+ ...(manual?.transactionId ? { manualValidation: { ...manual, transactionId: manual.transactionId } } : {})
728
+ }
692
729
  };
693
730
  if (options.skipQueue)
694
731
  return publish();
@@ -317,6 +317,21 @@ function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallba
317
317
  Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
318
318
  return snapshot;
319
319
  }
320
+ function inspectedPullRequestIdentityError(remote, requested) {
321
+ if (!remote.identity)
322
+ return {
323
+ code: 'PR_IDENTITY_MISSING',
324
+ message: 'Provider pull-request inspection did not return a canonical identity',
325
+ retryable: false
326
+ };
327
+ if (!resourceIdentityEquals(remote.identity, requested))
328
+ return {
329
+ code: 'PR_IDENTITY_MISMATCH',
330
+ message: 'Provider returned a different pull request identity than requested',
331
+ retryable: false
332
+ };
333
+ return null;
334
+ }
320
335
  function pullRequestIdentity(pullRequest) {
321
336
  return pullRequest.identity || { kind: 'number', value: pullRequest.number };
322
337
  }
@@ -339,6 +354,9 @@ async function inspectExternalPullRequest(base, prIdentity) {
339
354
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
340
355
  if (!inspected.ok)
341
356
  return providerPullRequestError(base, inspected.error, prNumber);
357
+ const identityError = inspectedPullRequestIdentityError(inspected.value, prIdentity);
358
+ if (identityError)
359
+ return providerPullRequestError(base, identityError, prNumber);
342
360
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
343
361
  return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
344
362
  platform: base.context.platform,
@@ -392,6 +410,12 @@ async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
392
410
  platform: context.platform, capabilities: context.capabilities,
393
411
  resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
394
412
  });
413
+ const identityError = inspectedPullRequestIdentityError(inspected.value, identity);
414
+ if (identityError)
415
+ return result('failed', null, null, number, {
416
+ platform: context.platform, capabilities: context.capabilities,
417
+ resource: { kind: 'pull-request', number, identity }, error: identityError
418
+ });
395
419
  return result('no-op', null, null, number, {
396
420
  platform: context.platform, capabilities: context.capabilities,
397
421
  resource: { kind: 'pull-request', number, identity },
@@ -448,27 +472,38 @@ function resourceIdentityLabel(identity) {
448
472
  const number = resourceIdentityNumber(identity);
449
473
  return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
450
474
  }
451
- function inLabelResource(kind, identity, before, expected, after, effect) {
452
- return { kind, number: resourceIdentityNumber(identity), identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
475
+ function inLabelResource(kind, identity, before, expected, after, effect, displayNumber = resourceIdentityNumber(identity)) {
476
+ return { kind, number: displayNumber, identity, before: [...before].sort(), expected: [...expected].sort(), after: after ? [...after].sort() : null, effect };
453
477
  }
454
- async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
478
+ async function syncPlatformPullRequestInLabels(prToken, options = {}) {
455
479
  const cwd = path.resolve(options.cwd || process.cwd());
480
+ const token = String(prToken);
456
481
  const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
457
482
  const context = loaded.ok ? loaded.value.context : loaded.context;
458
483
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
459
484
  if (!loaded.ok || !usable)
460
- return result(context.status, null, null, prNumber, {
485
+ return result(context.status, null, null, null, {
461
486
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
462
487
  });
463
- if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
464
- return result('failed', null, null, prNumber, {
488
+ if (!token || token.trim() !== token)
489
+ return result('failed', null, null, null, {
465
490
  platform: context.platform, capabilities: context.capabilities,
466
- error: { code: 'PR_NUMBER_INVALID', message: 'PR number must be positive', retryable: false }
491
+ error: { code: 'PR_IDENTITY_INVALID', message: 'PR identity token must be non-empty and trimmed', retryable: false }
467
492
  });
468
493
  const provider = loaded.value.provider;
469
494
  const operationContext = providerOperationContext(loaded.value);
470
495
  const repository = context.platform.repository;
471
- const pullRequestIdentity = providerResourceToken(provider, 'pull-request', String(prNumber));
496
+ let pullRequestIdentity;
497
+ try {
498
+ pullRequestIdentity = providerResourceToken(provider, 'pull-request', token);
499
+ }
500
+ catch (error) {
501
+ return result('failed', null, null, null, {
502
+ platform: context.platform, capabilities: context.capabilities,
503
+ error: { code: 'PR_IDENTITY_INVALID', message: error instanceof Error ? error.message : String(error), retryable: false }
504
+ });
505
+ }
506
+ let prNumber = resourceIdentityNumber(pullRequestIdentity);
472
507
  const providerFailure = (error) => result(error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error), null, null, prNumber, {
473
508
  platform: context.platform,
474
509
  capabilities: context.capabilities,
@@ -480,7 +515,13 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
480
515
  : unsupportedProviderOperation(provider, 'changeRequests.inspect');
481
516
  if (!inspected.ok)
482
517
  return providerFailure(inspected.error);
483
- const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber);
518
+ const identityError = inspectedPullRequestIdentityError(inspected.value, pullRequestIdentity);
519
+ if (identityError)
520
+ return providerFailure(identityError);
521
+ if (Number.isSafeInteger(inspected.value.number) && inspected.value.number > 0) {
522
+ prNumber = inspected.value.number;
523
+ }
524
+ const fetched = normalizeProviderPullRequest(inspected.value, repository, prNumber ?? 0);
484
525
  const files = provider.changeRequests?.listFiles
485
526
  ? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
486
527
  : unsupportedProviderOperation(provider, 'changeRequests.listFiles');
@@ -549,7 +590,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
549
590
  }))
550
591
  ];
551
592
  const willWrite = context.capabilities.triage && !options.dryRun;
552
- const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op'));
593
+ const resourcesState = plans.map((item) => inLabelResource(item.kind, item.identity, item.snapshot.labels, item.plan.target, item.plan.changed && willWrite ? null : item.snapshot.labels, item.plan.changed && willWrite ? 'unknown' : 'no-op', item.number));
553
594
  if (options.dryRun || !context.capabilities.triage)
554
595
  return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
555
596
  platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
@@ -611,6 +652,16 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
611
652
  evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
612
653
  error: { ...providerError(reread.error, 'PLATFORM_PROVIDER_OPERATION_FAILED'), code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${reread.error.message}` }
613
654
  });
655
+ if (item.kind === 'pull-request') {
656
+ const identityError = inspectedPullRequestIdentityError(reread.value, item.identity);
657
+ if (identityError)
658
+ return result('blocked', null, issueNumber, prNumber, {
659
+ changed: true, platform: context.platform, capabilities: context.capabilities,
660
+ pullRequest: currentPullRequest, resource: { kind: 'pull-request', number: prNumber, identity: pullRequestIdentity }, operations, resources: resourcesState,
661
+ evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
662
+ error: { ...identityError, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${identityError.message}` }
663
+ });
664
+ }
614
665
  const after = reread.value.labels || [];
615
666
  const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
616
667
  state.after = after;
@@ -623,7 +674,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
623
674
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
624
675
  });
625
676
  if (item.kind === 'pull-request')
626
- currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber, currentPullRequest);
677
+ currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber ?? currentPullRequest.number, currentPullRequest);
627
678
  }
628
679
  return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
629
680
  changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
@@ -856,9 +907,13 @@ async function resolveExternalPullRequest(taskRef, options) {
856
907
  ? await base.provider.changeRequests.inspect({
857
908
  context: providerOperationContext(base.loadedContext),
858
909
  target: pullRequestIdentity(selected.selected)
859
- }).then((response) => response.ok
860
- ? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
861
- : response)
910
+ }).then((response) => {
911
+ if (!response.ok)
912
+ return response;
913
+ const identityError = inspectedPullRequestIdentityError(response.value, pullRequestIdentity(selected.selected));
914
+ return identityError ? { ok: false, error: identityError }
915
+ : { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) };
916
+ })
862
917
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
863
918
  if (!rechecked.ok || !rechecked.value)
864
919
  return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
@@ -1152,6 +1207,9 @@ async function createExternalPullRequest(taskRef, options, base) {
1152
1207
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1153
1208
  if (!inspected.ok)
1154
1209
  return withCreation(providerPullRequestError(base, inspected.error, number), { kind: 'created', createdByCurrentOperation: true });
1210
+ const identityError = inspectedPullRequestIdentityError(inspected.value, createdIdentity);
1211
+ if (identityError)
1212
+ return withCreation(providerPullRequestError(base, identityError, number), { kind: 'created', createdByCurrentOperation: true });
1155
1213
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
1156
1214
  if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
1157
1215
  return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -1354,6 +1412,13 @@ async function syncPlatformPullRequestImpl(taskRef, options) {
1354
1412
  code: 'IN_LABEL_SYNC_PARTIAL',
1355
1413
  message: `In-label synchronization is partial or unknown: ${reread.error.message}`
1356
1414
  }, base.prNumber));
1415
+ const identityError = inspectedPullRequestIdentityError(reread.value, base.prIdentity);
1416
+ if (identityError)
1417
+ return softenFailure(providerPullRequestError(base, {
1418
+ ...identityError,
1419
+ code: 'IN_LABEL_SYNC_PARTIAL',
1420
+ message: `Pull-request metadata synchronization is partial or unknown: ${identityError.message}`
1421
+ }, base.prNumber));
1357
1422
  finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
1358
1423
  const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
1359
1424
  .filter((label) => label.startsWith('in:') || label.startsWith('type:'))).sort();
@@ -457,7 +457,7 @@ function checkTaskCommentContent(context, remoteData, shared) {
457
457
  return shared.failResult(CHECK_TYPE, buildCommentContentMismatchMessage("task", context.issueNumber, expectedBody, commentBody), "check_failed");
458
458
  }
459
459
  function checkPrTypeLabel(context, remoteData, shared) {
460
- if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
460
+ if (!context.audit.enabled || !context.hasTriage || !context.prIdentity || remoteData.prLabels === undefined || remoteData.prLabels === null) {
461
461
  return infoResult('PR type-label audit is not applicable because the required PR metadata or capability is unavailable');
462
462
  }
463
463
  const expectedLabel = mapTaskTypeToLabel(context.task.metadata.type);
@@ -465,12 +465,12 @@ function checkPrTypeLabel(context, remoteData, shared) {
465
465
  return infoResult('PR type-label audit is not applicable because the task type has no label mapping');
466
466
  }
467
467
  if (remoteData.prLabels.includes(expectedLabel)) {
468
- return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has expected type label '${expectedLabel}'`);
468
+ return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} has expected type label '${expectedLabel}'`);
469
469
  }
470
- return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on PR #${context.prNumber}`, "check_failed");
470
+ return shared.failResult(CHECK_TYPE, `Expected type label '${expectedLabel}' not found on ${pullRequestLabel(context)}`, "check_failed");
471
471
  }
472
472
  function checkInLabelsMatchPr(context, remoteData, shared) {
473
- if (!context.audit.enabled || !context.hasTriage || !context.prNumber || !remoteData.prLabels) {
473
+ if (!context.audit.enabled || !context.hasTriage || !context.prIdentity || remoteData.prLabels === undefined || remoteData.prLabels === null) {
474
474
  return infoResult('PR in: label audit is not applicable because the required PR metadata or capability is unavailable');
475
475
  }
476
476
  const issueInLabels = extractLabelNames(remoteData.issue.labels)
@@ -480,9 +480,12 @@ function checkInLabelsMatchPr(context, remoteData, shared) {
480
480
  .filter((label) => label.startsWith("in:"))
481
481
  .sort();
482
482
  if (arraysEqual(issueInLabels, prInLabels)) {
483
- return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} in: labels match Issue #${context.issueNumber}`);
483
+ return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} in: labels match Issue #${context.issueNumber}`);
484
484
  }
485
- return shared.failResult(CHECK_TYPE, `in: labels mismatch — PR #${context.prNumber} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
485
+ return shared.failResult(CHECK_TYPE, `in: labels mismatch — ${pullRequestLabel(context)} has [${formatLabelList(prInLabels)}], Issue #${context.issueNumber} has [${formatLabelList(issueInLabels)}]`, "check_failed");
486
+ }
487
+ function pullRequestLabel(context) {
488
+ return context.prNumber ? `PR #${context.prNumber}` : 'Pull request';
486
489
  }
487
490
  function checkInLabelsComputed(context, remoteData, shared) {
488
491
  if (!context.audit.enabled || !context.hasTriage) {
@@ -574,13 +577,13 @@ function checkIssueFields(context, remoteData, shared) {
574
577
  return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} fields match task metadata`);
575
578
  }
576
579
  function checkPrAssignee(context, remoteData, shared) {
577
- if (!context.audit.enabled || !context.hasPush || !context.prNumber) {
580
+ if (!context.audit.enabled || !context.hasPush || !context.prIdentity || remoteData.prAssignees === undefined || remoteData.prAssignees === null) {
578
581
  return infoResult('PR assignee audit is not applicable because it is disabled or required PR metadata/capability is unavailable');
579
582
  }
580
- if (!remoteData.prAssignees || remoteData.prAssignees.length === 0) {
581
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no assignee`, "check_failed");
583
+ if (remoteData.prAssignees.length === 0) {
584
+ return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} has no assignee`, "check_failed");
582
585
  }
583
- return shared.passResult(CHECK_TYPE, `PR #${context.prNumber} has an assignee`);
586
+ return shared.passResult(CHECK_TYPE, `${pullRequestLabel(context)} has an assignee`);
584
587
  }
585
588
  function checkMilestone(context, remoteData, shared) {
586
589
  if (!context.hasTriage) {
@@ -589,16 +592,16 @@ function checkMilestone(context, remoteData, shared) {
589
592
  if (!remoteData.issue?.milestone?.title) {
590
593
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} has no milestone set`, "check_failed");
591
594
  }
592
- if (context.prNumber && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
593
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} has no milestone set`, "check_failed");
595
+ if (context.prIdentity && remoteData.prMilestone !== undefined && !remoteData.prMilestone?.title) {
596
+ return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} has no milestone set`, "check_failed");
594
597
  }
595
598
  if (context.audit.requireSpecificMilestone) {
596
599
  const issueTitle = remoteData.issue.milestone.title;
597
600
  if (VERSION_LINE_REGEX.test(issueTitle)) {
598
601
  return shared.failResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone '${issueTitle}' is a release line; narrow to a specific version (e.g. ${issueTitle.replace(/\.x$/, ".N")}) before continuing`, "check_failed");
599
602
  }
600
- if (context.prNumber && remoteData.prMilestone?.title && VERSION_LINE_REGEX.test(remoteData.prMilestone.title)) {
601
- return shared.failResult(CHECK_TYPE, `PR #${context.prNumber} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`, "check_failed");
603
+ if (context.prIdentity && remoteData.prMilestone?.title && VERSION_LINE_REGEX.test(remoteData.prMilestone.title)) {
604
+ return shared.failResult(CHECK_TYPE, `${pullRequestLabel(context)} milestone '${remoteData.prMilestone.title}' is a release line; narrow to a specific version before continuing`, "check_failed");
602
605
  }
603
606
  }
604
607
  return shared.passResult(CHECK_TYPE, `Issue #${context.issueNumber} milestone satisfies the configured policy`);
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- const CLIENT_NAMESPACE = /^[a-z][a-z0-9-]{0,31}$/u;
2
+ import { resolveTaskRuntimeRoot } from '../task/runtime-paths.js';
3
3
  function runtimeError(code, message) {
4
4
  const error = new Error(`${code}: ${message}`);
5
5
  error.name = code;
@@ -9,16 +9,9 @@ function boundControlContext(env) {
9
9
  return [
10
10
  'AGENT_INFRA_CONTROL_TOKEN',
11
11
  'AGENT_INFRA_CONTROL_GENERATION',
12
- 'AGENT_INFRA_EXECUTOR_MANIFEST',
13
- 'AGENT_INFRA_CODEX_CONTROLLER_CONTEXT'
12
+ 'AGENT_INFRA_EXECUTOR_MANIFEST'
14
13
  ].some((key) => typeof env[key] === 'string' && env[key].length > 0);
15
14
  }
16
- function clientNamespace(client) {
17
- if (!CLIENT_NAMESPACE.test(client)) {
18
- throw runtimeError('AGENT_INFRA_RUNTIME_CLIENT_INVALID', `invalid runtime client namespace '${client}'`);
19
- }
20
- return client;
21
- }
22
15
  export function resolveAgentRuntimeRoot(options = {}) {
23
16
  const env = options.env ?? process.env;
24
17
  const configured = env.AGENT_INFRA_RUNTIME_DIR;
@@ -28,29 +21,13 @@ export function resolveAgentRuntimeRoot(options = {}) {
28
21
  }
29
22
  return path.resolve(configured);
30
23
  }
24
+ const taskId = env.AGENT_INFRA_TASK_ID;
25
+ if (taskId) {
26
+ return resolveTaskRuntimeRoot(taskId, { repoRoot: options.repoRoot });
27
+ }
31
28
  if (boundControlContext(env)) {
32
29
  throw runtimeError('AGENT_INFRA_RUNTIME_DIR_REQUIRED', 'task-bound control context requires AGENT_INFRA_RUNTIME_DIR');
33
30
  }
34
31
  return path.join(path.resolve(options.repoRoot ?? process.cwd()), '.agents', 'workspace', '.runtime');
35
32
  }
36
- export function resolveAgentRuntimeStoreRoot(options = {}) {
37
- const client = clientNamespace(options.client ?? 'codex');
38
- if (options.explicitRoot)
39
- return path.resolve(options.explicitRoot);
40
- const env = options.env ?? process.env;
41
- const runtimeRoot = resolveAgentRuntimeRoot({ repoRoot: options.repoRoot, env });
42
- if (env.AGENT_INFRA_RUNTIME_DIR) {
43
- if (!options.store) {
44
- throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required for task-bound runtime');
45
- }
46
- return path.join(runtimeRoot, 'clients', client, options.store);
47
- }
48
- if (client === 'codex' && options.store) {
49
- return path.join(runtimeRoot, `codex-${options.store}`);
50
- }
51
- if (!options.store) {
52
- throw runtimeError('AGENT_INFRA_RUNTIME_STORE_INVALID', 'store kind is required');
53
- }
54
- return path.join(runtimeRoot, 'clients', client, options.store);
55
- }
56
33
  //# sourceMappingURL=agent-runtime.js.map
@@ -8,6 +8,7 @@ export function readSandboxControlStatusForRow(config, row) {
8
8
  : { mode: 'branch-only' };
9
9
  const control = sandboxControlPaths({
10
10
  base: config.controlBase,
11
+ repoRoot: config.repoRoot,
11
12
  project: config.project,
12
13
  container: row.name,
13
14
  identity
@@ -672,7 +672,7 @@ function readProjectInitCommandDigest(controlRoot) {
672
672
  function writeProjectInitCommandDigest(controlRoot, commandSha256) {
673
673
  atomicWriteJson(path.join(controlRoot, 'project-init.json'), { version: 1, commandSha256 });
674
674
  }
675
- export async function create(args, { runProjectInitCommand = true } = {}) {
675
+ export async function create(args, { runProjectInitCommand = true, workspace: workspaceOverride } = {}) {
676
676
  const { values, positionals } = parseArgs({
677
677
  args,
678
678
  allowPositionals: true,
@@ -696,7 +696,13 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
696
696
  validateSelinuxDisableEnv();
697
697
  const config = loadConfig();
698
698
  const [branchOrTaskId = '', base] = positionals;
699
- const target = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
699
+ const resolvedTarget = resolveSandboxTarget(branchOrTaskId, config.repoRoot);
700
+ const workspace = workspaceOverride?.mode === 'task-bound'
701
+ && resolvedTarget.workspace.mode === 'task-bound'
702
+ && workspaceOverride.taskId === resolvedTarget.workspace.taskId
703
+ ? resolvedTarget.workspace
704
+ : workspaceOverride ?? resolvedTarget.workspace;
705
+ const target = { ...resolvedTarget, workspace };
700
706
  const branch = target.branch;
701
707
  assertValidBranchName(branch);
702
708
  const effectiveConfig = {
@@ -735,6 +741,7 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
735
741
  const container = containerName(effectiveConfig, branch);
736
742
  const controlPaths = sandboxControlPaths({
737
743
  base: effectiveConfig.controlBase,
744
+ repoRoot: effectiveConfig.repoRoot,
738
745
  project: effectiveConfig.project,
739
746
  container,
740
747
  identity: target.workspace
@@ -866,6 +873,9 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
866
873
  task: async () => {
867
874
  for (const { tool, dir } of effectiveResolvedTools) {
868
875
  fs.mkdirSync(dir, { recursive: true });
876
+ for (const { hostSubdir } of tool.hostStateMounts ?? []) {
877
+ fs.mkdirSync(path.join(dir, hostSubdir), { recursive: true });
878
+ }
869
879
  for (const { hostPath, sandboxName } of tool.hostPreSeedFiles ?? []) {
870
880
  const destination = path.join(dir, sandboxName);
871
881
  if (fs.existsSync(hostPath) && !fs.existsSync(destination)) {
@@ -1105,6 +1115,10 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1105
1115
  '-v',
1106
1116
  volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
1107
1117
  ]));
1118
+ const hostStateMountVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.hostStateMounts ?? []).flatMap(({ hostSubdir, containerSubpath }) => [
1119
+ '-v',
1120
+ volumeArg(engine, path.join(dir, hostSubdir), path.posix.join(tool.containerMount, containerSubpath))
1121
+ ]));
1108
1122
  fs.mkdirSync(shareCommon, { recursive: true });
1109
1123
  fs.mkdirSync(shareBranch, { recursive: true });
1110
1124
  fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
@@ -1133,6 +1147,8 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1133
1147
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1134
1148
  '--label',
1135
1149
  `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1150
+ '-e',
1151
+ 'AGENT_INFRA_SANDBOX=1',
1136
1152
  ...(target.workspace.mode === 'task-bound'
1137
1153
  ? [
1138
1154
  '--label',
@@ -1163,6 +1179,7 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1163
1179
  ...tmpfsArgs,
1164
1180
  ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1165
1181
  ...liveMountVolumes,
1182
+ ...hostStateMountVolumes,
1166
1183
  ...envFile.dockerArgs,
1167
1184
  ...tzFlags,
1168
1185
  '-w',
@@ -8,15 +8,16 @@ import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
8
8
  import { redactCommandError } from '../redaction.js';
9
9
  import { runInteractiveWithClipboardBridge } from '../clipboard/bridge.js';
10
10
  import { detectHostTimezone } from '../host-timezone.js';
11
- import { fetchSandboxRows, selectSandboxContainer, } from './list-running.js';
12
- import { resolveSandboxReentryContext, resolveSandboxTarget } from '../workspace-identity.js';
11
+ import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer, } from './list-running.js';
12
+ import { resolveSandboxTarget } from '../workspace-identity.js';
13
13
  const USAGE = `Usage: ai sandbox exec [--recreate] <branch | TASK-id | N> [cmd...]
14
14
 
15
15
  N references an active task short id from
16
16
  .agents/workspace/active/.short-ids.json. They resolve only via that
17
17
  registry — they do not reference a container's row position in
18
- 'ai sandbox ls' output. --recreate is a host recovery flag only before the
19
- target; the same token after the target is passed to the container command.`;
18
+ 'ai sandbox ls' output. --recreate rebuilds the matched container while
19
+ preserving its task or branch identity. The same token after the target is
20
+ passed to the container command.`;
20
21
  const TMUX_ENTRY_PATH = '/usr/local/bin/sandbox-tmux-entry';
21
22
  // Terminal-detection variables that interactive TUIs (e.g. claude-code)
22
23
  // inspect to enable progressive enhancements such as the kitty keyboard
@@ -78,41 +79,54 @@ export async function enter(args) {
78
79
  const config = loadConfig();
79
80
  const engine = detectEngine(config);
80
81
  const parsed = parseEnterArgs(args);
81
- const target = resolveSandboxTarget(parsed.target, config.repoRoot);
82
- const branch = target.branch;
83
- assertValidBranchName(branch);
82
+ const fullTaskId = /^TASK-\d{8}-\d{6}$/.test(parsed.target);
83
+ const target = fullTaskId ? null : resolveSandboxTarget(parsed.target, config.repoRoot);
84
+ const branch = target?.branch ?? '';
85
+ if (branch)
86
+ assertValidBranchName(branch);
84
87
  const { running, nonRunning } = fetchSandboxRows(engine, sandboxLabel(config), sandboxBranchLabel(config), {
85
88
  mode: sandboxWorkspaceModeLabel(config),
86
89
  taskId: sandboxTaskIdLabel(config)
87
90
  });
88
- const found = selectSandboxContainer([...running, ...nonRunning], containerNameCandidates(config, branch));
89
- if (!found) {
90
- throw new Error(`No sandbox found for branch '${branch}'. Run 'ai sandbox create ${branch}' to create one.`);
91
+ const rows = [...running, ...nonRunning];
92
+ const found = fullTaskId
93
+ ? rows.find((row) => row.taskId === parsed.target)
94
+ : rows.find((row) => row.branch === branch)
95
+ ?? selectSandboxContainer(rows, containerNameCandidates(config, branch));
96
+ if (!found)
97
+ throw new Error(`No sandbox found for '${parsed.target}'.`);
98
+ const sandboxBranch = found.branch || branch;
99
+ if (sandboxBranch)
100
+ assertValidBranchName(sandboxBranch);
101
+ if (target?.workspace.mode === 'task-bound' && found.taskId !== target.workspace.taskId) {
102
+ throw new Error(`Sandbox task mismatch for short id '${parsed.target}'.`);
91
103
  }
92
- const containerWorkspace = found.workspaceMode === 'task-bound' && found.taskId
104
+ const workspace = found.workspaceMode === 'task-bound' && found.taskId
93
105
  ? { mode: 'task-bound', taskId: found.taskId }
94
- : found.workspaceMode === 'branch-only'
95
- ? { mode: 'branch-only' }
96
- : { mode: 'legacy-invalid' };
97
- const reentry = resolveSandboxReentryContext({
98
- target,
99
- containerWorkspace,
100
- repoRoot: config.repoRoot
101
- });
102
- const ready = await ensureSandboxReady({
103
- config,
104
- engine,
105
- branch,
106
- workspace: reentry.workspace,
107
- reentry: reentry.reentry,
108
- row: found,
109
- allowRecreate: parsed.recreate,
110
- recreate: async () => {
111
- const { create } = await import('./create.js');
112
- await create([target.requestedRef, '--no-refresh'], { runProjectInitCommand: false });
113
- }
114
- });
115
- const container = ready.container;
106
+ : { mode: 'branch-only' };
107
+ let container = found.name;
108
+ if (parsed.recreate) {
109
+ const ready = await ensureSandboxReady({
110
+ config,
111
+ engine,
112
+ branch: sandboxBranch,
113
+ workspace,
114
+ row: found,
115
+ allowRecreate: true,
116
+ forceRecreate: true,
117
+ recreate: async () => {
118
+ const { create } = await import('./create.js');
119
+ await create([sandboxBranch, '--no-refresh'], {
120
+ runProjectInitCommand: false,
121
+ workspace
122
+ });
123
+ }
124
+ });
125
+ container = ready.container;
126
+ }
127
+ else if (!found.running) {
128
+ startSandboxContainer(engine, found.name);
129
+ }
116
130
  const cmd = parsed.command;
117
131
  const capabilityPlan = createSandboxCapabilityPlan(config);
118
132
  const enterHookResults = await runSandboxHooks({