@fitlab-ai/agent-infra 0.11.9 → 0.11.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 (219) hide show
  1. package/dist/bin/internal-cli.js +0 -6
  2. package/dist/lib/agent-clients/adapter.js +20 -0
  3. package/dist/lib/agent-clients/adapters/codex-sandbox.js +13 -0
  4. package/dist/lib/agent-clients/adapters/codex.js +4 -0
  5. package/dist/lib/git/workflow.js +1 -1
  6. package/dist/lib/internal/cli-route-inventory.js +0 -3
  7. package/dist/lib/internal/manual-validation.js +2 -2
  8. package/dist/lib/internal/platform-pr.js +4 -5
  9. package/dist/lib/internal/sandbox-control.js +30 -5
  10. package/dist/lib/internal/task-artifact.js +1 -1
  11. package/dist/lib/internal/task-create.js +15 -1
  12. package/dist/lib/internal/task-finalization.js +14 -25
  13. package/dist/lib/internal/task-operation-registry.js +6 -141
  14. package/dist/lib/internal/task-platform-recovery.js +9 -4
  15. package/dist/lib/platform/completion-backfill.js +2 -1
  16. package/dist/lib/platform/context.js +4 -2
  17. package/dist/lib/platform/github-client.js +20 -17
  18. package/dist/lib/platform/github-provider.js +42 -22
  19. package/dist/lib/platform/github-release-notes.js +4 -2
  20. package/dist/lib/platform/in-label-sync.js +2 -1
  21. package/dist/lib/platform/issue-comments.js +13 -9
  22. package/dist/lib/platform/operation-coordinator.js +1 -1
  23. package/dist/lib/platform/pr-review.js +2 -1
  24. package/dist/lib/platform/pr-summary.js +45 -8
  25. package/dist/lib/platform/provider-bridge.js +4 -5
  26. package/dist/lib/platform/provider-contract.d.ts +3 -6
  27. package/dist/lib/platform/provider-contract.js +3 -15
  28. package/dist/lib/platform/provider-validation.js +9 -4
  29. package/dist/lib/platform/pull-requests.js +83 -16
  30. package/dist/lib/platform/verification-sync.js +17 -14
  31. package/dist/lib/sandbox/commands/create.js +18 -2
  32. package/dist/lib/sandbox/commands/enter.js +47 -33
  33. package/dist/lib/sandbox/control/client.js +99 -17
  34. package/dist/lib/sandbox/control/executor.js +12 -14
  35. package/dist/lib/sandbox/control/host-recovery.js +218 -0
  36. package/dist/lib/sandbox/control/protocol.js +2 -3
  37. package/dist/lib/sandbox/control/server.js +42 -2
  38. package/dist/lib/sandbox/environment.js +150 -0
  39. package/dist/lib/sandbox/recovery.js +8 -0
  40. package/dist/lib/sandbox/removal.js +0 -1
  41. package/dist/lib/sandbox/workspace-identity.js +0 -14
  42. package/dist/lib/sandbox/workspace-view.js +33 -6
  43. package/dist/lib/task/artifact-command.js +18 -3
  44. package/dist/lib/task/artifact-lifecycle.js +121 -63
  45. package/dist/lib/task/artifact-selection.js +1 -1
  46. package/dist/lib/task/capabilities.js +59 -22
  47. package/dist/lib/task/commands/status.js +0 -27
  48. package/dist/lib/task/commit-identity.js +22 -7
  49. package/dist/lib/task/commit-intent.js +67 -26
  50. package/dist/lib/task/commit-operation.js +19 -8
  51. package/dist/lib/task/completion-facts.js +32 -10
  52. package/dist/lib/task/control-authority.js +2 -6
  53. package/dist/lib/task/current-contract.js +1 -5
  54. package/dist/lib/task/events.js +9 -275
  55. package/dist/lib/task/execution-inputs.js +14 -0
  56. package/dist/lib/task/finalization-envelope.js +4 -0
  57. package/dist/lib/task/finalization.js +43 -86
  58. package/dist/lib/task/frontmatter.js +25 -2
  59. package/dist/lib/task/ledger-intents.js +6 -8
  60. package/dist/lib/task/orchestration.js +11 -10
  61. package/dist/lib/task/platform-operation-journal.js +29 -10
  62. package/dist/lib/task/platform-operation-recovery.js +109 -35
  63. package/dist/lib/task/resolve-ref.js +8 -3
  64. package/dist/lib/task/review-fingerprint.js +4 -26
  65. package/dist/lib/task/verification-engine.js +95 -6
  66. package/dist/lib/task/write.js +0 -11
  67. package/dist/lifecycle-build-manifest.json +3 -3
  68. package/lib/agent-clients/adapter.ts +22 -0
  69. package/lib/agent-clients/adapters/codex-sandbox.ts +13 -0
  70. package/lib/agent-clients/adapters/codex.ts +4 -0
  71. package/lib/git/workflow.ts +1 -1
  72. package/lib/internal/cli-route-inventory.ts +0 -2
  73. package/lib/internal/manual-validation.ts +2 -2
  74. package/lib/internal/platform-pr.ts +3 -4
  75. package/lib/internal/sandbox-control.ts +39 -6
  76. package/lib/internal/task-artifact.ts +1 -1
  77. package/lib/internal/task-create.ts +15 -2
  78. package/lib/internal/task-finalization.ts +16 -26
  79. package/lib/internal/task-operation-registry.ts +6 -153
  80. package/lib/internal/task-platform-recovery.ts +9 -4
  81. package/lib/platform/completion-backfill.ts +2 -1
  82. package/lib/platform/context.ts +8 -4
  83. package/lib/platform/github-client.ts +24 -20
  84. package/lib/platform/github-provider.ts +47 -26
  85. package/lib/platform/github-release-notes.ts +5 -3
  86. package/lib/platform/in-label-sync.ts +2 -1
  87. package/lib/platform/issue-comments.ts +14 -9
  88. package/lib/platform/operation-coordinator.ts +1 -1
  89. package/lib/platform/pr-review.ts +2 -1
  90. package/lib/platform/pr-summary.ts +43 -7
  91. package/lib/platform/provider-bridge.ts +10 -9
  92. package/lib/platform/provider-contract.ts +6 -21
  93. package/lib/platform/provider-loader.ts +7 -6
  94. package/lib/platform/provider-validation.ts +30 -8
  95. package/lib/platform/pull-requests.ts +78 -15
  96. package/lib/platform/repository-metadata.ts +3 -2
  97. package/lib/platform/security-alerts.ts +1 -1
  98. package/lib/platform/types.ts +2 -0
  99. package/lib/platform/verification-sync.ts +18 -14
  100. package/lib/sandbox/commands/create.ts +24 -2
  101. package/lib/sandbox/commands/enter.ts +42 -39
  102. package/lib/sandbox/control/client.ts +107 -19
  103. package/lib/sandbox/control/executor.ts +13 -15
  104. package/lib/sandbox/control/host-recovery.ts +195 -0
  105. package/lib/sandbox/control/protocol.ts +4 -5
  106. package/lib/sandbox/control/server.ts +42 -1
  107. package/lib/sandbox/environment.ts +162 -0
  108. package/lib/sandbox/recovery.ts +10 -0
  109. package/lib/sandbox/removal.ts +2 -10
  110. package/lib/sandbox/tool-types.ts +1 -0
  111. package/lib/sandbox/workspace-identity.ts +0 -26
  112. package/lib/sandbox/workspace-view.ts +33 -8
  113. package/lib/task/artifact-command.ts +19 -3
  114. package/lib/task/artifact-lifecycle.ts +102 -53
  115. package/lib/task/artifact-selection.ts +1 -1
  116. package/lib/task/capabilities.ts +52 -26
  117. package/lib/task/commands/status.ts +0 -31
  118. package/lib/task/commit-identity.ts +24 -7
  119. package/lib/task/commit-intent.ts +58 -24
  120. package/lib/task/commit-operation.ts +17 -8
  121. package/lib/task/completion-facts.ts +31 -9
  122. package/lib/task/control-authority.ts +1 -8
  123. package/lib/task/current-contract.ts +1 -6
  124. package/lib/task/events.ts +9 -248
  125. package/lib/task/execution-inputs.ts +25 -0
  126. package/lib/task/finalization-envelope.ts +21 -0
  127. package/lib/task/finalization.ts +40 -82
  128. package/lib/task/frontmatter.ts +26 -2
  129. package/lib/task/ledger-intents.ts +6 -8
  130. package/lib/task/orchestration.ts +11 -17
  131. package/lib/task/platform-operation-journal.ts +47 -13
  132. package/lib/task/platform-operation-recovery.ts +102 -37
  133. package/lib/task/resolve-ref.ts +8 -3
  134. package/lib/task/review-fingerprint.ts +4 -26
  135. package/lib/task/verification-engine.ts +103 -9
  136. package/lib/task/write.ts +0 -19
  137. package/package.json +1 -1
  138. package/templates/.agents/rules/README.en.md +1 -1
  139. package/templates/.agents/rules/README.zh-CN.md +1 -1
  140. package/templates/.agents/rules/commit-and-pr.en.md +5 -5
  141. package/templates/.agents/rules/commit-and-pr.zh-CN.md +5 -5
  142. package/templates/.agents/rules/decision-qualification.en.md +1 -1
  143. package/templates/.agents/rules/decision-qualification.zh-CN.md +1 -1
  144. package/templates/.agents/rules/issue-sync.en.md +1 -1
  145. package/templates/.agents/rules/issue-sync.zh-CN.md +1 -1
  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 +1 -1
  153. package/templates/.agents/skills/analyze-task/SKILL.zh-CN.md +1 -1
  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/close-codescan/SKILL.en.md +3 -0
  159. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +3 -0
  160. package/templates/.agents/skills/close-dependabot/SKILL.en.md +3 -0
  161. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +3 -0
  162. package/templates/.agents/skills/code-task/SKILL.en.md +25 -9
  163. package/templates/.agents/skills/code-task/SKILL.zh-CN.md +27 -10
  164. package/templates/.agents/skills/code-task/reference/branch-management.en.md +1 -1
  165. package/templates/.agents/skills/code-task/reference/branch-management.zh-CN.md +1 -1
  166. package/templates/.agents/skills/code-task/reference/code-rules.en.md +1 -1
  167. package/templates/.agents/skills/code-task/reference/code-rules.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 +5 -16
  171. package/templates/.agents/skills/code-task/reference/fix-mode.zh-CN.md +5 -16
  172. package/templates/.agents/skills/code-task/reference/report-template.en.md +1 -1
  173. package/templates/.agents/skills/code-task/reference/report-template.zh-CN.md +1 -1
  174. package/templates/.agents/skills/complete-manual-validation/SKILL.en.md +1 -1
  175. package/templates/.agents/skills/complete-manual-validation/SKILL.zh-CN.md +1 -1
  176. package/templates/.agents/skills/complete-task/SKILL.en.md +15 -12
  177. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +11 -10
  178. package/templates/.agents/skills/create-pr/SKILL.en.md +1 -1
  179. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +1 -1
  180. package/templates/.agents/skills/create-pr/reference/branch-strategy.en.md +2 -2
  181. package/templates/.agents/skills/create-pr/reference/branch-strategy.zh-CN.md +2 -2
  182. package/templates/.agents/skills/create-task/SKILL.en.md +3 -5
  183. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +3 -4
  184. package/templates/.agents/skills/import-codescan/SKILL.en.md +1 -1
  185. package/templates/.agents/skills/import-codescan/SKILL.zh-CN.md +1 -1
  186. package/templates/.agents/skills/import-dependabot/SKILL.en.md +1 -1
  187. package/templates/.agents/skills/import-dependabot/SKILL.zh-CN.md +1 -1
  188. package/templates/.agents/skills/import-issue/SKILL.en.md +1 -1
  189. package/templates/.agents/skills/import-issue/SKILL.zh-CN.md +1 -1
  190. package/templates/.agents/skills/plan-task/SKILL.en.md +1 -1
  191. package/templates/.agents/skills/plan-task/SKILL.zh-CN.md +1 -1
  192. package/templates/.agents/skills/review-code/SKILL.en.md +4 -4
  193. package/templates/.agents/skills/review-code/SKILL.zh-CN.md +4 -4
  194. package/templates/.agents/skills/review-pr/SKILL.en.md +1 -1
  195. package/templates/.agents/skills/review-pr/SKILL.zh-CN.md +1 -1
  196. package/templates/.agents/skills/test/SKILL.en.md +4 -1
  197. package/templates/.agents/skills/test/SKILL.zh-CN.md +4 -1
  198. package/templates/.agents/skills/test-integration/SKILL.en.md +5 -2
  199. package/templates/.agents/skills/test-integration/SKILL.zh-CN.md +5 -2
  200. package/templates/.agents/skills/update-agent-infra/SKILL.en.md +5 -2
  201. package/templates/.agents/skills/update-agent-infra/SKILL.zh-CN.md +5 -2
  202. package/templates/.agents/skills/upgrade-dependency/SKILL.en.md +5 -2
  203. package/templates/.agents/skills/upgrade-dependency/SKILL.zh-CN.md +5 -2
  204. package/templates/.agents/skills/watch-pr/SKILL.en.md +2 -2
  205. package/templates/.agents/skills/watch-pr/SKILL.zh-CN.md +2 -2
  206. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  207. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  208. package/templates/.agents/templates/task.zh-CN.md +0 -12
  209. package/templates/.github/workflows/pr-label.yml +1 -2
  210. package/dist/lib/internal/task-invalidation.js +0 -63
  211. package/dist/lib/task/finalization-handoff.js +0 -93
  212. package/dist/lib/task/intermediate-cleanup.js +0 -327
  213. package/dist/lib/task/invalidation-command.js +0 -88
  214. package/dist/lib/task/invalidation.js +0 -242
  215. package/lib/internal/task-invalidation.ts +0 -41
  216. package/lib/task/finalization-handoff.ts +0 -105
  217. package/lib/task/intermediate-cleanup.ts +0 -393
  218. package/lib/task/invalidation-command.ts +0 -88
  219. package/lib/task/invalidation.ts +0 -313
@@ -37,10 +37,7 @@ function validatePlatformProvider(value, providerType) {
37
37
  ok: false,
38
38
  error: {
39
39
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
40
- message: 'Provider must expose matching type, contractVersion, and context.resolve',
41
- retryable: false,
42
- providerType,
43
- phase: 'provider-validation'
40
+ message: 'Provider must expose matching type, contractVersion, and context.resolve'
44
41
  }
45
42
  };
46
43
  }
@@ -51,10 +48,7 @@ function validatePlatformProvider(value, providerType) {
51
48
  ok: false,
52
49
  error: {
53
50
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
54
- message: 'Provider identity declaration is invalid',
55
- retryable: false,
56
- providerType,
57
- phase: 'provider-validation'
51
+ message: 'Provider identity declaration is invalid'
58
52
  }
59
53
  };
60
54
  }
@@ -68,10 +62,7 @@ function validatePlatformProvider(value, providerType) {
68
62
  ok: false,
69
63
  error: {
70
64
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
71
- message: `Provider operation group ${groupName} is incomplete`,
72
- retryable: false,
73
- providerType,
74
- phase: 'provider-validation'
65
+ message: `Provider operation group ${groupName} is incomplete`
75
66
  }
76
67
  };
77
68
  }
@@ -84,9 +75,6 @@ function validatePlatformProvider(value, providerType) {
84
75
  error: {
85
76
  code: 'PLATFORM_PROVIDER_CONTRACT_INVALID',
86
77
  message: `Provider operation group ${groupName} requires an identity declaration for ${resourceKind}`,
87
- retryable: false,
88
- providerType,
89
- phase: 'provider-validation'
90
78
  }
91
79
  };
92
80
  }
@@ -462,9 +462,11 @@ const PROVIDER_ERROR_CATALOG = {
462
462
  REMOTE_INVALID: { message: 'The platform remote is invalid', retryable: false },
463
463
  UPSTREAM_UNRESOLVED: { message: 'The platform upstream repository could not be resolved', retryable: false },
464
464
  RESOURCE_NOT_FOUND: { message: 'The platform resource was not found', retryable: false },
465
+ COMMENT_LIST_FAILED: { message: 'Comments could not be listed', retryable: true },
465
466
  PR_NOT_FOUND: { message: 'The pull request was not found', retryable: false },
466
467
  ISSUE_NOT_FOUND: { message: 'The issue was not found', retryable: false },
467
468
  REMOTE_BUSY: { message: 'Platform provider is temporarily busy', retryable: true },
469
+ IN_LABEL_SYNC_PARTIAL: { message: 'In-label synchronization is partial or unknown', retryable: true },
468
470
  NETWORK_TRANSIENT: { message: 'The platform request failed temporarily', retryable: true },
469
471
  NETWORK_RETRY: { message: 'The platform request failed temporarily', retryable: true },
470
472
  NETWORK_RETRY_EXHAUSTED: { message: 'The platform request retries were exhausted', retryable: true },
@@ -505,11 +507,14 @@ function safeProviderError(providerType, operation, value) {
505
507
  return {
506
508
  code: definition ? item.code : 'PLATFORM_PROVIDER_OPERATION_FAILED',
507
509
  message: definition?.message || PROVIDER_ERROR_CATALOG.PLATFORM_PROVIDER_OPERATION_FAILED.message,
508
- retryable: definition?.retryable || false,
510
+ retryable: definition?.retryable ?? false,
509
511
  providerType,
510
512
  phase: operation
511
513
  };
512
514
  }
515
+ function providerErrorRetryable(code) {
516
+ return PROVIDER_ERROR_CATALOG[code]?.retryable ?? false;
517
+ }
513
518
  function validators(providerType, declaration) {
514
519
  return {
515
520
  'context.resolve': (value) => validateContext(value, providerType),
@@ -608,8 +613,8 @@ async function invokeProviderOperation(providerType, operation, call, validate)
608
613
  try {
609
614
  exactKeys(envelope, Object.hasOwn(envelope, 'value') ? ['ok', 'error', 'value'] : ['ok', 'error'], `${operation} result`);
610
615
  const error = envelope.error;
611
- exactKeys(error, ['code', 'message', 'retryable'], `${operation} error`);
612
- if (typeof error.code !== 'string' || !error.code || typeof error.message !== 'string' || !error.message || typeof error.retryable !== 'boolean')
616
+ exactKeys(error, ['code', 'message'], `${operation} error`);
617
+ if (typeof error.code !== 'string' || !error.code || typeof error.message !== 'string' || !error.message)
613
618
  throw new Error('invalid provider error');
614
619
  return {
615
620
  ok: false,
@@ -648,5 +653,5 @@ function wrapProviderOperations(provider) {
648
653
  }
649
654
  return wrapped;
650
655
  }
651
- export { invokeProviderOperation, validationError, wrapProviderOperations };
656
+ export { invokeProviderOperation, providerErrorRetryable, validationError, wrapProviderOperations };
652
657
  //# sourceMappingURL=provider-validation.js.map
@@ -9,6 +9,7 @@ import { extractSection } from '../task/sections.js';
9
9
  import { captureTaskWriteMetadata, writeTask } from '../task/write.js';
10
10
  import { resolvePlatformProviderContext } from './context.js';
11
11
  import { inspectPlatformIssue } from './issues.js';
12
+ import { providerErrorRetryable } from './provider-validation.js';
12
13
  import { planPullRequestMetadata } from './pull-request-metadata.js';
13
14
  import { planInLabelUpdate, validateInLabelMapping } from './in-label-sync.js';
14
15
  import { platformResult } from './types.js';
@@ -316,6 +317,21 @@ function normalizeProviderPullRequest(remote, repository, fallbackNumber, fallba
316
317
  Object.defineProperty(snapshot, 'identity', { value: identity, enumerable: false, configurable: true });
317
318
  return snapshot;
318
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
+ }
319
335
  function pullRequestIdentity(pullRequest) {
320
336
  return pullRequest.identity || { kind: 'number', value: pullRequest.number };
321
337
  }
@@ -338,6 +354,9 @@ async function inspectExternalPullRequest(base, prIdentity) {
338
354
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
339
355
  if (!inspected.ok)
340
356
  return providerPullRequestError(base, inspected.error, prNumber);
357
+ const identityError = inspectedPullRequestIdentityError(inspected.value, prIdentity);
358
+ if (identityError)
359
+ return providerPullRequestError(base, identityError, prNumber);
341
360
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, prNumber || 0, fallback);
342
361
  return result('no-op', base.resolved.taskId, base.issueNumber, prNumber, {
343
362
  platform: base.context.platform,
@@ -391,6 +410,12 @@ async function inspectPlatformPullRequestByNumber(prNumber, options = {}) {
391
410
  platform: context.platform, capabilities: context.capabilities,
392
411
  resource: { kind: 'pull-request', number, identity }, error: providerError(inspected.error, 'PLATFORM_PROVIDER_OPERATION_FAILED')
393
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
+ });
394
419
  return result('no-op', null, null, number, {
395
420
  platform: context.platform, capabilities: context.capabilities,
396
421
  resource: { kind: 'pull-request', number, identity },
@@ -447,27 +472,38 @@ function resourceIdentityLabel(identity) {
447
472
  const number = resourceIdentityNumber(identity);
448
473
  return number === null ? `${identity.kind} ${identity.value}` : `#${number}`;
449
474
  }
450
- function inLabelResource(kind, identity, before, expected, after, effect) {
451
- 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 };
452
477
  }
453
- async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
478
+ async function syncPlatformPullRequestInLabels(prToken, options = {}) {
454
479
  const cwd = path.resolve(options.cwd || process.cwd());
480
+ const token = String(prToken);
455
481
  const loaded = await resolvePlatformProviderContext({ cwd, client: options.client });
456
482
  const context = loaded.ok ? loaded.value.context : loaded.context;
457
483
  const usable = (context.status === 'no-op' || context.status === 'degraded') && context.platform.repository;
458
484
  if (!loaded.ok || !usable)
459
- return result(context.status, null, null, prNumber, {
485
+ return result(context.status, null, null, null, {
460
486
  platform: context.platform, capabilities: context.capabilities, operations: context.operations, error: context.error
461
487
  });
462
- if (!Number.isSafeInteger(prNumber) || prNumber <= 0)
463
- return result('failed', null, null, prNumber, {
488
+ if (!token || token.trim() !== token)
489
+ return result('failed', null, null, null, {
464
490
  platform: context.platform, capabilities: context.capabilities,
465
- 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 }
466
492
  });
467
493
  const provider = loaded.value.provider;
468
494
  const operationContext = providerOperationContext(loaded.value);
469
495
  const repository = context.platform.repository;
470
- 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);
471
507
  const providerFailure = (error) => result(error.code === 'PLATFORM_CAPABILITY_UNSUPPORTED' ? 'degraded' : providerStatus(error), null, null, prNumber, {
472
508
  platform: context.platform,
473
509
  capabilities: context.capabilities,
@@ -479,7 +515,13 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
479
515
  : unsupportedProviderOperation(provider, 'changeRequests.inspect');
480
516
  if (!inspected.ok)
481
517
  return providerFailure(inspected.error);
482
- 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);
483
525
  const files = provider.changeRequests?.listFiles
484
526
  ? await provider.changeRequests.listFiles({ context: operationContext, target: pullRequestIdentity })
485
527
  : unsupportedProviderOperation(provider, 'changeRequests.listFiles');
@@ -548,7 +590,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
548
590
  }))
549
591
  ];
550
592
  const willWrite = context.capabilities.triage && !options.dryRun;
551
- 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));
552
594
  if (options.dryRun || !context.capabilities.triage)
553
595
  return result(!context.capabilities.triage || association.value.length !== 1 ? 'degraded' : plans.some((item) => item.plan.changed) ? 'planned' : 'no-op', null, issueNumber, prNumber, {
554
596
  platform: context.platform, capabilities: context.capabilities, pullRequest: fetched,
@@ -582,9 +624,10 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
582
624
  : unsupportedProviderOperation(provider, 'changeRequests.update');
583
625
  if (!synced.ok) {
584
626
  const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
585
- state.effect = synced.error.retryable ? 'unknown' : 'no-op';
627
+ const retryable = synced.error.retryable || providerErrorRetryable(synced.error.code);
628
+ state.effect = retryable ? 'unknown' : 'no-op';
586
629
  state.after = state.effect === 'unknown' ? null : item.snapshot.labels;
587
- const partial = successfulWrites > 0 || synced.error.retryable;
630
+ const partial = successfulWrites > 0 || retryable;
588
631
  const error = partial
589
632
  ? { ...synced.error, code: 'IN_LABEL_SYNC_PARTIAL', message: `In-label synchronization is partial or unknown: ${synced.error.message}` }
590
633
  : synced.error;
@@ -609,6 +652,16 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
609
652
  evidence: { kind: 'pull-request-files', pullRequestFiles: files.value, closingIssues: closingIssueIdentities },
610
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}` }
611
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
+ }
612
665
  const after = reread.value.labels || [];
613
666
  const state = resourcesState.find((resource) => resource.kind === item.kind && resourceIdentityEquals(resource.identity, item.identity));
614
667
  state.after = after;
@@ -621,7 +674,7 @@ async function syncPlatformPullRequestInLabels(prNumber, options = {}) {
621
674
  error: { code: 'IN_LABEL_SYNC_PARTIAL', message: `${item.kind} ${resourceIdentityLabel(item.identity)} did not converge to the expected in: labels`, retryable: true }
622
675
  });
623
676
  if (item.kind === 'pull-request')
624
- currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber, currentPullRequest);
677
+ currentPullRequest = normalizeProviderPullRequest(reread.value, repository, prNumber ?? currentPullRequest.number, currentPullRequest);
625
678
  }
626
679
  return result(association.value.length !== 1 ? 'degraded' : successfulWrites > 0 ? 'applied' : 'no-op', null, issueNumber, prNumber, {
627
680
  changed: successfulWrites > 0, platform: context.platform, capabilities: context.capabilities,
@@ -854,9 +907,13 @@ async function resolveExternalPullRequest(taskRef, options) {
854
907
  ? await base.provider.changeRequests.inspect({
855
908
  context: providerOperationContext(base.loadedContext),
856
909
  target: pullRequestIdentity(selected.selected)
857
- }).then((response) => response.ok
858
- ? { ok: true, value: normalizeProviderPullRequest(response.value, base.context.platform.repository, selected.selected.number, selected.selected) }
859
- : 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
+ })
860
917
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
861
918
  if (!rechecked.ok || !rechecked.value)
862
919
  return externalResult(result(rechecked.ok ? 'failed' : rechecked.error.retryable ? 'blocked' : 'failed', base.resolved.taskId, base.issueNumber, base.prNumber, {
@@ -1150,6 +1207,9 @@ async function createExternalPullRequest(taskRef, options, base) {
1150
1207
  : unsupportedProviderOperation(base.provider, 'changeRequests.inspect');
1151
1208
  if (!inspected.ok)
1152
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 });
1153
1213
  const pullRequest = normalizeProviderPullRequest(inspected.value, base.context.platform.repository, inspected.value.number ?? fallback.number, fallback);
1154
1214
  if (!pullRequest.nodeId || !isUsableResourceIdentity(pullRequestIdentity(pullRequest)))
1155
1215
  return withCreation(result('failed', base.resolved.taskId, base.issueNumber, null, {
@@ -1352,6 +1412,13 @@ async function syncPlatformPullRequestImpl(taskRef, options) {
1352
1412
  code: 'IN_LABEL_SYNC_PARTIAL',
1353
1413
  message: `In-label synchronization is partial or unknown: ${reread.error.message}`
1354
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));
1355
1422
  finalPullRequest = normalizeProviderPullRequest(reread.value, base.context.platform.repository, base.prNumber || 0, inspected.pullRequest);
1356
1423
  const expected = ((planned.find((operation) => operation.name === 'labels')?.value || [])
1357
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`);
@@ -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 = {
@@ -866,6 +872,9 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
866
872
  task: async () => {
867
873
  for (const { tool, dir } of effectiveResolvedTools) {
868
874
  fs.mkdirSync(dir, { recursive: true });
875
+ for (const { hostSubdir } of tool.hostStateMounts ?? []) {
876
+ fs.mkdirSync(path.join(dir, hostSubdir), { recursive: true });
877
+ }
869
878
  for (const { hostPath, sandboxName } of tool.hostPreSeedFiles ?? []) {
870
879
  const destination = path.join(dir, sandboxName);
871
880
  if (fs.existsSync(hostPath) && !fs.existsSync(destination)) {
@@ -1105,6 +1114,10 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1105
1114
  '-v',
1106
1115
  volumeArg(engine, hostPath, path.posix.join(tool.containerMount, containerSubpath))
1107
1116
  ]));
1117
+ const hostStateMountVolumes = effectiveResolvedTools.flatMap(({ tool, dir }) => (tool.hostStateMounts ?? []).flatMap(({ hostSubdir, containerSubpath }) => [
1118
+ '-v',
1119
+ volumeArg(engine, path.join(dir, hostSubdir), path.posix.join(tool.containerMount, containerSubpath))
1120
+ ]));
1108
1121
  fs.mkdirSync(shareCommon, { recursive: true });
1109
1122
  fs.mkdirSync(shareBranch, { recursive: true });
1110
1123
  fs.mkdirSync(clipboardHostDir(effectiveConfig.home), { recursive: true, mode: 0o700 });
@@ -1133,6 +1146,8 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1133
1146
  `${sandboxBranchLabel(effectiveConfig)}=${branch}`,
1134
1147
  '--label',
1135
1148
  `${sandboxWorkspaceModeLabel(effectiveConfig)}=${target.workspace.mode}`,
1149
+ '-e',
1150
+ 'AGENT_INFRA_SANDBOX=1',
1136
1151
  ...(target.workspace.mode === 'task-bound'
1137
1152
  ? [
1138
1153
  '--label',
@@ -1163,6 +1178,7 @@ export async function create(args, { runProjectInitCommand = true } = {}) {
1163
1178
  ...tmpfsArgs,
1164
1179
  ...tmpfsSeedPlan.flatMap(({ volumeArgs }) => volumeArgs),
1165
1180
  ...liveMountVolumes,
1181
+ ...hostStateMountVolumes,
1166
1182
  ...envFile.dockerArgs,
1167
1183
  ...tzFlags,
1168
1184
  '-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({