@fitlab-ai/agent-infra 0.9.7 → 0.9.9

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 (195) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +11 -1
  4. package/dist/lib/agent-clients/adapters/claude-code.js +3 -3
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +60 -22
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/build-identity.js +8 -1
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/capability-store.js +175 -11
  8. package/dist/lib/agent-clients/adapters/codex-lifecycle/controller-context.js +147 -0
  9. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -0
  10. package/dist/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.js +86 -69
  11. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +2 -1
  12. package/dist/lib/decide.js +68 -14
  13. package/dist/lib/git/workflow.js +10 -1
  14. package/dist/lib/init.js +1 -1
  15. package/dist/lib/internal/codex-lifecycle.js +19 -5
  16. package/dist/lib/internal/codex-sandbox-controller.js +6 -13
  17. package/dist/lib/internal/git-workflow.js +5 -7
  18. package/dist/lib/internal/platform-pr.js +20 -7
  19. package/dist/lib/internal/release-workflow.js +3 -4
  20. package/dist/lib/internal/sandbox-control.js +79 -2
  21. package/dist/lib/internal/task-activity.js +57 -7
  22. package/dist/lib/internal/task-event.js +57 -4
  23. package/dist/lib/internal/task-finalization.js +54 -0
  24. package/dist/lib/internal/task-ledger.js +59 -5
  25. package/dist/lib/internal/task-lifecycle.js +59 -8
  26. package/dist/lib/internal/task-orchestration.js +161 -288
  27. package/dist/lib/internal/task-override.js +131 -0
  28. package/dist/lib/internal/task-review.js +64 -6
  29. package/dist/lib/internal/task-verify.js +65 -3
  30. package/dist/lib/internal/task-warning.js +57 -4
  31. package/dist/lib/platform/pr-summary.js +53 -13
  32. package/dist/lib/platform/pull-requests.js +205 -41
  33. package/dist/lib/runtime/agent-runtime.js +56 -0
  34. package/dist/lib/sandbox/commands/create.js +389 -311
  35. package/dist/lib/sandbox/commands/rm.js +316 -55
  36. package/dist/lib/sandbox/control/client.js +190 -6
  37. package/dist/lib/sandbox/control/container-identity.js +1 -1
  38. package/dist/lib/sandbox/control/controller-registration.js +273 -0
  39. package/dist/lib/sandbox/control/executor.js +145 -5
  40. package/dist/lib/sandbox/control/lifecycle.js +395 -73
  41. package/dist/lib/sandbox/control/protocol.js +67 -6
  42. package/dist/lib/sandbox/control/state.js +27 -11
  43. package/dist/lib/sandbox/engine.js +14 -7
  44. package/dist/lib/sandbox/index.js +5 -3
  45. package/dist/lib/sandbox/managed-fs.js +5 -0
  46. package/dist/lib/sandbox/mounts.js +20 -5
  47. package/dist/lib/sandbox/recovery.js +78 -10
  48. package/dist/lib/sandbox/task-resolver.js +50 -6
  49. package/dist/lib/sandbox/workspace-identity.js +27 -0
  50. package/dist/lib/sandbox/workspace-view.js +62 -40
  51. package/dist/lib/sandbox/worktree-safety.js +215 -2
  52. package/dist/lib/server/process-state.js +51 -1
  53. package/dist/lib/task/activity-intent.js +4 -1
  54. package/dist/lib/task/codex-orchestration.js +158 -33
  55. package/dist/lib/task/commands/status.js +59 -1
  56. package/dist/lib/task/commit-operation.js +335 -0
  57. package/dist/lib/task/commit-policy.js +18 -0
  58. package/dist/lib/task/decision-intents.js +4 -2
  59. package/dist/lib/task/delegation-receipts.js +279 -17
  60. package/dist/lib/task/events.js +9 -5
  61. package/dist/lib/task/finalization.js +567 -0
  62. package/dist/lib/task/guard-override.js +31 -0
  63. package/dist/lib/task/human-override.js +1240 -0
  64. package/dist/lib/task/ledger-intents.js +6 -3
  65. package/dist/lib/task/lifecycle.js +69 -14
  66. package/dist/lib/task/operation-outcome.js +33 -0
  67. package/dist/lib/task/orchestration.js +199 -719
  68. package/dist/lib/task/review-finalization.js +3 -2
  69. package/dist/lib/task/short-id.js +59 -1
  70. package/dist/lib/task/task-execution-lock.js +6 -1
  71. package/dist/lib/task/verification-engine.js +138 -59
  72. package/dist/lib/task/verification.js +15 -2
  73. package/dist/lib/task/workflow-warning-intents.js +54 -3
  74. package/dist/lib/task/write.js +2 -1
  75. package/dist/lifecycle-build-manifest.json +3 -3
  76. package/lib/agent-clients/adapters/claude-code.ts +3 -3
  77. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +63 -20
  78. package/lib/agent-clients/adapters/codex-lifecycle/build-identity.ts +12 -0
  79. package/lib/agent-clients/adapters/codex-lifecycle/capability-store.ts +278 -13
  80. package/lib/agent-clients/adapters/codex-lifecycle/controller-context.ts +180 -0
  81. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -0
  82. package/lib/agent-clients/adapters/codex-lifecycle/manifest-files.json +9 -1
  83. package/lib/agent-clients/adapters/codex-lifecycle/sandbox-controller.ts +112 -93
  84. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +2 -1
  85. package/lib/decide.ts +59 -14
  86. package/lib/git/workflow.ts +11 -2
  87. package/lib/init.ts +1 -1
  88. package/lib/internal/codex-lifecycle.ts +18 -5
  89. package/lib/internal/codex-sandbox-controller.ts +6 -12
  90. package/lib/internal/git-workflow.ts +5 -6
  91. package/lib/internal/platform-pr.ts +14 -7
  92. package/lib/internal/release-workflow.ts +3 -4
  93. package/lib/internal/sandbox-control.ts +92 -2
  94. package/lib/internal/task-activity.ts +46 -6
  95. package/lib/internal/task-event.ts +53 -4
  96. package/lib/internal/task-finalization.ts +68 -0
  97. package/lib/internal/task-ledger.ts +48 -4
  98. package/lib/internal/task-lifecycle.ts +56 -12
  99. package/lib/internal/task-orchestration.ts +32 -107
  100. package/lib/internal/task-override.ts +138 -0
  101. package/lib/internal/task-review.ts +51 -6
  102. package/lib/internal/task-verify.ts +52 -3
  103. package/lib/internal/task-warning.ts +44 -3
  104. package/lib/platform/pr-summary.ts +65 -14
  105. package/lib/platform/pull-requests.ts +211 -41
  106. package/lib/runtime/agent-runtime.ts +76 -0
  107. package/lib/sandbox/commands/create.ts +88 -4
  108. package/lib/sandbox/commands/rm.ts +413 -61
  109. package/lib/sandbox/config.ts +2 -1
  110. package/lib/sandbox/control/client.ts +255 -6
  111. package/lib/sandbox/control/container-identity.ts +3 -3
  112. package/lib/sandbox/control/controller-registration.ts +347 -0
  113. package/lib/sandbox/control/executor.ts +172 -6
  114. package/lib/sandbox/control/lifecycle.ts +465 -77
  115. package/lib/sandbox/control/protocol.ts +85 -9
  116. package/lib/sandbox/control/state.ts +35 -18
  117. package/lib/sandbox/engine.ts +20 -9
  118. package/lib/sandbox/index.ts +5 -3
  119. package/lib/sandbox/managed-fs.ts +5 -0
  120. package/lib/sandbox/mounts.ts +23 -5
  121. package/lib/sandbox/recovery.ts +90 -9
  122. package/lib/sandbox/task-resolver.ts +58 -7
  123. package/lib/sandbox/workspace-identity.ts +50 -2
  124. package/lib/sandbox/workspace-view.ts +73 -43
  125. package/lib/sandbox/worktree-safety.ts +236 -2
  126. package/lib/server/process-state.ts +48 -1
  127. package/lib/task/activity-intent.ts +4 -2
  128. package/lib/task/codex-orchestration.ts +167 -33
  129. package/lib/task/commands/status.ts +78 -1
  130. package/lib/task/commit-operation.ts +430 -0
  131. package/lib/task/commit-policy.ts +30 -0
  132. package/lib/task/decision-intents.ts +4 -2
  133. package/lib/task/delegation-receipts.ts +304 -19
  134. package/lib/task/events.ts +10 -5
  135. package/lib/task/finalization.ts +719 -0
  136. package/lib/task/guard-override.ts +50 -0
  137. package/lib/task/human-override.ts +1503 -0
  138. package/lib/task/ledger-intents.ts +6 -3
  139. package/lib/task/lifecycle.ts +71 -14
  140. package/lib/task/operation-outcome.ts +48 -0
  141. package/lib/task/orchestration.ts +239 -922
  142. package/lib/task/review-finalization.ts +6 -2
  143. package/lib/task/short-id.ts +65 -1
  144. package/lib/task/task-execution-lock.ts +10 -1
  145. package/lib/task/verification-engine.ts +145 -64
  146. package/lib/task/verification.ts +18 -3
  147. package/lib/task/workflow-warning-intents.ts +61 -4
  148. package/lib/task/write.ts +4 -1
  149. package/package.json +2 -2
  150. package/templates/.agents/QUICKSTART.en.md +2 -2
  151. package/templates/.agents/QUICKSTART.zh-CN.md +2 -2
  152. package/templates/.agents/hooks/lifecycle-delegation.js +9 -24
  153. package/templates/.agents/rules/lifecycle-orchestration.en.md +4 -3
  154. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +4 -3
  155. package/templates/.agents/rules/task-management.en.md +2 -2
  156. package/templates/.agents/rules/task-management.zh-CN.md +2 -2
  157. package/templates/.agents/skills/block-task/SKILL.en.md +2 -4
  158. package/templates/.agents/skills/block-task/SKILL.zh-CN.md +2 -4
  159. package/templates/.agents/skills/cancel-task/SKILL.en.md +3 -3
  160. package/templates/.agents/skills/cancel-task/SKILL.zh-CN.md +3 -3
  161. package/templates/.agents/skills/close-codescan/SKILL.en.md +6 -2
  162. package/templates/.agents/skills/close-codescan/SKILL.zh-CN.md +6 -2
  163. package/templates/.agents/skills/close-dependabot/SKILL.en.md +6 -2
  164. package/templates/.agents/skills/close-dependabot/SKILL.zh-CN.md +6 -2
  165. package/templates/.agents/skills/commit/SKILL.en.md +41 -125
  166. package/templates/.agents/skills/commit/SKILL.zh-CN.md +52 -123
  167. package/templates/.agents/skills/commit/reference/commit-message.en.md +1 -1
  168. package/templates/.agents/skills/commit/reference/commit-message.zh-CN.md +1 -1
  169. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +14 -43
  170. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +14 -49
  171. package/templates/.agents/skills/commit/reference/pr-summary-sync.en.md +1 -1
  172. package/templates/.agents/skills/commit/reference/pr-summary-sync.zh-CN.md +1 -1
  173. package/templates/.agents/skills/commit/reference/task-status-update.en.md +2 -35
  174. package/templates/.agents/skills/commit/reference/task-status-update.zh-CN.md +2 -35
  175. package/templates/.agents/skills/complete-task/SKILL.en.md +31 -38
  176. package/templates/.agents/skills/complete-task/SKILL.zh-CN.md +31 -38
  177. package/templates/.agents/skills/complete-task/config/verify.en.json +1 -0
  178. package/templates/.agents/skills/complete-task/config/verify.zh-CN.json +1 -0
  179. package/templates/.agents/skills/complete-task/reference/external-delivery.en.md +1 -1
  180. package/templates/.agents/skills/complete-task/reference/external-delivery.zh-CN.md +1 -1
  181. package/templates/.agents/skills/create-pr/SKILL.en.md +6 -4
  182. package/templates/.agents/skills/create-pr/SKILL.zh-CN.md +6 -4
  183. package/templates/.agents/skills/create-pr/reference/comment-publish.en.md +2 -2
  184. package/templates/.agents/skills/create-pr/reference/comment-publish.zh-CN.md +2 -2
  185. package/templates/.agents/skills/run-task/SKILL.en.md +5 -5
  186. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +5 -5
  187. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -1
  188. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -1
  189. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.en.md +1 -1
  190. package/templates/.agents/skills/watch-pr/reference/monitor-and-heal.zh-CN.md +1 -1
  191. package/templates/.codex/hooks.json +4 -4
  192. package/dist/lib/task/commit-finalization.js +0 -305
  193. package/dist/lib/task/commit-intent.js +0 -165
  194. package/lib/task/commit-finalization.ts +0 -419
  195. package/lib/task/commit-intent.ts +0 -230
@@ -1,10 +1,11 @@
1
1
  import { validateTaskCreateCandidate } from '../../task/create.js';
2
+ import { normalizeAgentToken } from '../../agent-clients/tokens.js';
2
3
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
3
4
  export const SANDBOX_CONTROL_ADMISSION_WINDOW_MS = 2_000;
4
5
  export const SANDBOX_CONTROL_STATUS_INTERVAL_MS = 250;
5
6
  export const SANDBOX_CONTROL_STATUS_STALE_MS = 1_500;
6
7
  export const SANDBOX_CONTROL_FUTURE_SKEW_MS = 1_000;
7
- export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
8
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-finalization', 'task-create', 'codex-controller'];
8
9
  export const DEFAULT_SANDBOX_CONTROL_TIMING = Object.freeze({
9
10
  controlTickMs: 250,
10
11
  parkedBindingInitialMs: 1_000,
@@ -33,7 +34,7 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
33
34
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request must be an object');
34
35
  }
35
36
  const request = value;
36
- if (request.version !== 2 || typeof request.id !== 'string'
37
+ if (request.version !== 3 || typeof request.id !== 'string'
37
38
  || !/^[a-f0-9-]{16,64}$/.test(request.id) || request.token !== manifest.token
38
39
  || typeof request.family !== 'string' || !isSandboxControlFamily(request.family)
39
40
  || !Number.isSafeInteger(request.issuedAt) || !Number.isSafeInteger(request.expiresAt))
@@ -53,13 +54,47 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
53
54
  fail('SANDBOX_CONTROL_REQUEST_TOO_LARGE', 'request exceeds the control limit');
54
55
  }
55
56
  if (request.family === 'task-create') {
56
- const expected = ['candidate', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
57
+ const expected = ['candidate', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
57
58
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')) {
58
59
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
59
60
  }
61
+ if (request.controllerProcess !== null || request.controllerProof !== null) {
62
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create cannot carry controller authority');
63
+ }
60
64
  return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
61
65
  }
62
- const expected = ['args', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
66
+ if (request.family === 'codex-controller') {
67
+ const expected = ['args', 'command', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
68
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
69
+ || !Array.isArray(request.args) || request.args.length !== 0
70
+ || !['open', 'close', 'verify'].includes(request.command)
71
+ || !validControllerProcess(request.controllerProcess)
72
+ || (request.command === 'open' && request.controllerProof !== null)
73
+ || (request.command !== 'open' && !validControllerProof(request.controllerProof))
74
+ || (request.command === 'verify'
75
+ && JSON.stringify(request.controllerProcess) !== JSON.stringify(request.controllerProof.controllerProcess))) {
76
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller request schema is invalid');
77
+ }
78
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
79
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot register a Codex controller');
80
+ }
81
+ return request;
82
+ }
83
+ if (request.family === 'task-finalization') {
84
+ const expected = ['agent', 'args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'operation', 'token', 'version'];
85
+ if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
86
+ || request.operation !== 'complete'
87
+ || !Array.isArray(request.args) || request.args.length !== 0
88
+ || typeof request.agent !== 'string' || normalizeAgentToken(request.agent) !== request.agent
89
+ || request.controllerProcess !== null || request.controllerProof !== null) {
90
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-finalization request schema or authorization is invalid');
91
+ }
92
+ if (manifest.mode !== 'task-bound' || !manifest.taskId) {
93
+ fail('SANDBOX_CONTROL_BRANCH_ONLY', 'branch-only sandboxes cannot finalize tasks');
94
+ }
95
+ return request;
96
+ }
97
+ const expected = ['args', 'controllerProcess', 'controllerProof', 'expiresAt', 'family', 'generation', 'id', 'issuedAt', 'token', 'version'];
63
98
  if (Object.keys(request).sort().join(',') !== expected.sort().join(',')
64
99
  || !Array.isArray(request.args) || !request.args.every((arg) => typeof arg === 'string')) {
65
100
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'request schema or authorization is invalid');
@@ -71,15 +106,41 @@ export function validateSandboxControlRequest(value, manifest, options = {}) {
71
106
  && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
72
107
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'worktree binding is reserved for the control broker');
73
108
  }
109
+ if (request.controllerProcess !== null
110
+ || (request.controllerProof !== null && !validControllerProof(request.controllerProof))) {
111
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller authority is invalid');
112
+ }
113
+ if (request.family !== 'task-orchestration' && request.controllerProof !== null) {
114
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', 'controller proof is not allowed for this family');
115
+ }
74
116
  return request;
75
117
  }
76
118
  export function bindSandboxControlTask(request, taskId) {
77
- if (request.family === 'task-create')
78
- fail('SANDBOX_CONTROL_REQUEST_INVALID', 'task-create requests do not bind a current task');
119
+ if (request.family === 'task-create' || request.family === 'codex-controller' || request.family === 'task-finalization') {
120
+ fail('SANDBOX_CONTROL_REQUEST_INVALID', `${request.family} requests do not bind a current task`);
121
+ }
79
122
  if (request.args.length === 0)
80
123
  fail('SANDBOX_CONTROL_REQUEST_INVALID', 'command arguments are required');
81
124
  return [taskId, ...request.args.slice(1)];
82
125
  }
126
+ function validControllerProcess(value) {
127
+ if (!value || typeof value !== 'object' || Array.isArray(value))
128
+ return false;
129
+ const processValue = value;
130
+ return Object.keys(processValue).sort().join(',') === 'pid,startTime'
131
+ && Number.isSafeInteger(processValue.pid) && processValue.pid > 0
132
+ && Number.isSafeInteger(processValue.startTime) && processValue.startTime >= 0;
133
+ }
134
+ function validControllerProof(value) {
135
+ if (!value || typeof value !== 'object' || Array.isArray(value))
136
+ return false;
137
+ const proof = value;
138
+ return Object.keys(proof).sort().join(',') === 'controllerProcess,leaseId,leaseSecret,version'
139
+ && proof.version === 1
140
+ && typeof proof.leaseId === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseId)
141
+ && typeof proof.leaseSecret === 'string' && /^[a-f0-9]{64}$/u.test(proof.leaseSecret)
142
+ && validControllerProcess(proof.controllerProcess);
143
+ }
83
144
  export function controlError(error) {
84
145
  if (error instanceof SandboxControlProtocolError) {
85
146
  return { code: error.code, message: error.message, retryable: error.retryable };
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { randomUUID } from 'node:crypto';
5
5
  import { buildProcessTreeStopCommand } from '../../server/process-control.js';
6
- import { processIdentityMatches } from '../../server/process-state.js';
6
+ import { getProcessIdentityState } from '../../server/process-state.js';
7
7
  export const SANDBOX_CONTROL_AUDIT_MAX_BYTES = 1024 * 1024;
8
8
  export const SANDBOX_CONTROL_EXECUTION_STOP_MS = 2_000;
9
9
  function assertRegularFile(filePath) {
@@ -72,7 +72,7 @@ export function readActiveLease(manifest, now = Date.now()) {
72
72
  return null;
73
73
  return lease;
74
74
  }
75
- export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
75
+ export function cleanupStaleSandboxControlLease(manifest, now = Date.now(), options = {}) {
76
76
  const filePath = path.join(path.dirname(manifest.publicStatusDir), 'lease.json');
77
77
  if (!fs.existsSync(filePath))
78
78
  return false;
@@ -87,10 +87,13 @@ export function cleanupStaleSandboxControlLease(manifest, now = Date.now()) {
87
87
  || typeof lease.branch !== 'string' || typeof lease.reason !== 'string') {
88
88
  throw new Error('SANDBOX_CONTROL_LEASE_INVALID');
89
89
  }
90
- const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
91
- || lease.branch !== manifest.branch || lease.expiresAt <= now || !processIdentityMatches({
90
+ const ownerState = (options.identityProbe ?? getProcessIdentityState)({
92
91
  pid: lease.owner.pid, startTime: lease.owner.startTime
93
92
  });
93
+ if (ownerState === 'unknown')
94
+ throw new Error('SANDBOX_CONTROL_LEASE_OWNER_UNAVAILABLE');
95
+ const stale = lease.generation !== manifest.generation || lease.taskId !== manifest.taskId
96
+ || lease.branch !== manifest.branch || lease.expiresAt <= now || ownerState === 'dead';
94
97
  if (!stale)
95
98
  return false;
96
99
  if (fs.readFileSync(filePath, 'utf8') !== raw)
@@ -125,29 +128,42 @@ export function readExecution(filePath) {
125
128
  export function terminateSandboxControlExecution(execution, options = {}) {
126
129
  const platform = options.platform ?? process.platform;
127
130
  const processGroupId = execution.child.processGroupId;
128
- const processGroupAlive = (groupId) => {
131
+ const processGroupState = (groupId) => {
129
132
  try {
130
133
  const rows = execFileSync('ps', ['-axo', 'pgid=,stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
131
134
  return rows.split('\n').some((row) => {
132
135
  const match = row.trim().match(/^(\d+)\s+(\S+)/);
133
136
  return Number(match?.[1]) === groupId && !match?.[2]?.startsWith('Z');
134
- });
137
+ }) ? 'alive' : 'dead';
135
138
  }
136
139
  catch {
137
140
  try {
138
141
  process.kill(-groupId, 0);
139
- return true;
142
+ return 'alive';
140
143
  }
141
144
  catch (error) {
142
- return error.code === 'EPERM';
145
+ const code = error.code;
146
+ if (code === 'ESRCH')
147
+ return 'dead';
148
+ if (code === 'EPERM')
149
+ return 'alive';
150
+ return 'unknown';
143
151
  }
144
152
  }
145
153
  };
146
- const treeAlive = () => {
154
+ const treeState = () => {
147
155
  if (platform === 'win32' || !processGroupId) {
148
- return processIdentityMatches({ pid: execution.child.pid, startTime: execution.child.startTime });
156
+ return (options.identityProbe ?? getProcessIdentityState)({
157
+ pid: execution.child.pid, startTime: execution.child.startTime
158
+ });
149
159
  }
150
- return processGroupAlive(processGroupId);
160
+ return processGroupState(processGroupId);
161
+ };
162
+ const treeAlive = () => {
163
+ const state = treeState();
164
+ if (state === 'unknown')
165
+ throw new Error('SANDBOX_CONTROL_EXECUTION_OWNER_UNAVAILABLE');
166
+ return state === 'alive';
151
167
  };
152
168
  if (!treeAlive())
153
169
  return true;
@@ -14,6 +14,12 @@ const PLATFORM_DEFAULTS = Object.freeze({
14
14
  darwin: ENGINES.COLIMA,
15
15
  win32: ENGINES.WSL2
16
16
  });
17
+ function isPerPlatformEngine(engine) {
18
+ return typeof engine === 'object' && engine !== null;
19
+ }
20
+ function resolveEngineForPlatform(engine, os) {
21
+ return isPerPlatformEngine(engine) ? engine[os] : engine;
22
+ }
17
23
  function runFns({ runFn = run, runOkFn = runOk, runSafeFn = runSafe, runVerboseFn = runVerbose } = {}) {
18
24
  return {
19
25
  run: runFn,
@@ -56,23 +62,24 @@ function ensureBuildKit(engine, runOkEngineFn) {
56
62
  }
57
63
  }
58
64
  export function validateSandboxEngine(engine, { platformFn = platform } = {}) {
59
- if (engine === null || engine === undefined) {
65
+ const os = platformFn();
66
+ const resolved = resolveEngineForPlatform(engine, os);
67
+ if (resolved === null || resolved === undefined) {
60
68
  return null;
61
69
  }
62
- const os = platformFn();
63
- if (!(engine in ADAPTERS)) {
70
+ if (!(resolved in ADAPTERS)) {
64
71
  const known = Object.keys(ADAPTERS).join(', ');
65
- throw new Error(`sandbox: invalid "sandbox.engine" value "${engine}" (unknown sandbox engine). `
72
+ throw new Error(`sandbox: invalid "sandbox.engine" value "${resolved}" (unknown sandbox engine). `
66
73
  + `Valid engines: ${known}.`);
67
74
  }
68
- const adapter = ADAPTERS[engine];
75
+ const adapter = ADAPTERS[resolved];
69
76
  if (!adapter.supportedPlatforms.includes(os)) {
70
77
  const supported = enginesForPlatform(os);
71
78
  const supportedList = supported.length > 0 ? supported.join(', ') : 'none';
72
- throw new Error(`sandbox: "sandbox.engine" value "${engine}" is not supported on ${os}. `
79
+ throw new Error(`sandbox: "sandbox.engine" value "${resolved}" is not supported on ${os}. `
73
80
  + `Supported engines on ${os}: ${supportedList}.`);
74
81
  }
75
- return engine;
82
+ return resolved;
76
83
  }
77
84
  export function detectEngine(config = {}, { platformFn = platform } = {}) {
78
85
  const configured = validateSandboxEngine(config.engine, { platformFn });
@@ -15,9 +15,11 @@ Commands:
15
15
  rebuild [--quiet] [--refresh]
16
16
  Rebuild the sandbox image (--refresh pulls base + tools)
17
17
  refresh Sync host Claude Code credentials to all sandbox copies
18
- rm <branch> | --unbound | --purge
19
- Remove one sandbox, all sandboxes not bound to an
20
- active task (--unbound), or tear down everything (--purge)
18
+ rm <branch | TASK-id> | --unbound | --purge
19
+ Remove one sandbox; use a full TASK-id for task-bound
20
+ cleanup and a branch for branch-only cleanup. --unbound
21
+ removes completed task-bound and branch-only sandboxes;
22
+ active, blocked, and archive tasks are protected.
21
23
  start [--recreate] <branch | TASK-id | N>
22
24
  Verify or recover an existing sandbox container;
23
25
  optionally replace only the container on failure
@@ -21,6 +21,11 @@ export function removeWorktreeDir(repoRoot, worktreeBase, dir, permit, { runFn =
21
21
  throw new Error(`Worktree permit target mismatch: ${dir}`);
22
22
  }
23
23
  verifyWorktreePermit(permit);
24
+ if (permit.snapshot.source === 'recovered') {
25
+ removeManagedDir(worktreeBase, dir);
26
+ runSafeFn('git', ['-C', repoRoot, 'worktree', 'prune']);
27
+ return;
28
+ }
24
29
  try {
25
30
  runFn('git', ['-C', repoRoot, 'worktree', 'remove', dir, '--force']);
26
31
  }
@@ -1,7 +1,19 @@
1
1
  import path from 'node:path';
2
2
  import { shareBranchDir, shareCommonDir, shellConfigDirCandidates, worktreeDirCandidates } from './constants.js';
3
+ import { sandboxWorkspaceViewStatePaths } from './workspace-view.js';
3
4
  export function sandboxCoreBindMounts(config, branch, overrides) {
4
5
  const taskBound = Boolean(overrides.taskSource && overrides.taskId);
6
+ const workspaceMounts = sandboxWorkspaceViewStatePaths(overrides.workspaceViewRoot).map(({ state, hostPath }) => taskBound && state === 'active'
7
+ ? {
8
+ hostPaths: [path.join(hostPath, '.short-ids.json')],
9
+ containerPath: '/workspace/.agents/workspace/active/.short-ids.json',
10
+ readOnly: true
11
+ }
12
+ : {
13
+ hostPaths: [hostPath],
14
+ containerPath: path.posix.join('/workspace/.agents/workspace', state),
15
+ readOnly: true
16
+ });
5
17
  const mounts = [
6
18
  {
7
19
  hostPaths: overrides.worktree
@@ -10,11 +22,7 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
10
22
  containerPath: '/workspace',
11
23
  readOnly: false
12
24
  },
13
- {
14
- hostPaths: [overrides.workspaceViewRoot],
15
- containerPath: '/workspace/.agents/workspace',
16
- readOnly: true
17
- },
25
+ ...workspaceMounts,
18
26
  ];
19
27
  if (taskBound) {
20
28
  mounts.push({
@@ -47,6 +55,13 @@ export function sandboxCoreBindMounts(config, branch, overrides) {
47
55
  containerPath: '/run/agent-infra/control',
48
56
  readOnly: false
49
57
  });
58
+ if (taskBound && overrides.runtimeDir) {
59
+ mounts.push({
60
+ hostPaths: [overrides.runtimeDir],
61
+ containerPath: '/run/agent-infra/runtime',
62
+ readOnly: false
63
+ });
64
+ }
50
65
  return mounts;
51
66
  }
52
67
  //# sourceMappingURL=mounts.js.map
@@ -14,7 +14,7 @@ import { inspectWorktree } from './worktree-safety.js';
14
14
  import { commandForEngine, runEngine, runOkEngine, runProbe, runVerboseEngine } from './shell.js';
15
15
  import { declaredTmpfsSeedEntries, resolveTools, toolConfigDirCandidates } from './tools.js';
16
16
  import { fetchSandboxRows, selectSandboxContainer, startSandboxContainer } from './commands/list-running.js';
17
- import { getProcessStartTime, processIdentityMatches, removePidFileIfMatches } from '../server/process-state.js';
17
+ import { getProcessIdentityState, getProcessStartTime, removePidFileIfMatches } from '../server/process-state.js';
18
18
  import { acquireSandboxControlBrokerStartup, garbageCollectSandboxControlRoot, isSandboxControlRootQuiescing, readSandboxControlManifest } from './control/lifecycle.js';
19
19
  import { appendSandboxControlAudit, readSandboxControlStatus } from './control/state.js';
20
20
  import { inspectSandboxControlContainer } from './control/container-identity.js';
@@ -89,7 +89,10 @@ export async function startSandboxControlBroker(repoRoot, manifestPath) {
89
89
  if (broker === null || broker.version !== 3 || owner === null) {
90
90
  throw new Error('SANDBOX_CONTROL_BROKER_VERSION_INVALID: rebuild the sandbox container before starting a new broker');
91
91
  }
92
- const live = owner !== null && processIdentityMatches({ pid: owner.pid, startTime: owner.startTime });
92
+ const ownerState = getProcessIdentityState({ pid: owner.pid, startTime: owner.startTime });
93
+ if (ownerState === 'unknown')
94
+ throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
95
+ const live = ownerState === 'alive';
93
96
  if (live
94
97
  && owner !== null
95
98
  && broker !== null
@@ -176,6 +179,15 @@ async function ensureSandboxControlBroker(params) {
176
179
  const brokerPath = path.join(control.root, 'broker.json');
177
180
  try {
178
181
  const broker = JSON.parse(fs.readFileSync(brokerPath, 'utf8'));
182
+ const validIdentity = typeof broker.pid === 'number'
183
+ && Number.isSafeInteger(broker.pid)
184
+ && typeof broker.startTime === 'number'
185
+ && Number.isSafeInteger(broker.startTime);
186
+ const ownerState = validIdentity
187
+ ? getProcessIdentityState({ pid: broker.pid, startTime: broker.startTime })
188
+ : 'dead';
189
+ if (ownerState === 'unknown')
190
+ throw new Error('SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE');
179
191
  if (broker.version === 3
180
192
  && typeof broker.pid === 'number'
181
193
  && Number.isSafeInteger(broker.pid)
@@ -185,7 +197,7 @@ async function ensureSandboxControlBroker(params) {
185
197
  && broker.brokerId.length > 0
186
198
  && broker.token === validatedManifest.token
187
199
  && broker.generation === validatedManifest.generation
188
- && processIdentityMatches({ pid: broker.pid, startTime: broker.startTime })) {
200
+ && ownerState === 'alive') {
189
201
  try {
190
202
  const status = readSandboxControlStatus(control.statusDir);
191
203
  if (status.generation === validatedManifest.generation && status.broker.pid === broker.pid
@@ -207,7 +219,9 @@ async function ensureSandboxControlBroker(params) {
207
219
  return;
208
220
  }
209
221
  }
210
- catch {
222
+ catch (error) {
223
+ if (error instanceof Error && error.message === 'SANDBOX_CONTROL_BROKER_OWNER_UNAVAILABLE')
224
+ throw error;
211
225
  // A missing, stale, or malformed broker record is replaced below.
212
226
  }
213
227
  await startSandboxControlBroker(params.config.repoRoot, control.manifestPath);
@@ -242,6 +256,30 @@ export function classifySandboxRecovery(snapshot) {
242
256
  message: 'Container runtime capability signature does not match the current sandbox plan.'
243
257
  });
244
258
  }
259
+ if (snapshot.workspaceTopology === 'legacy-parent') {
260
+ findings.push({
261
+ repairKind: 'hard-failure',
262
+ code: 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH',
263
+ message: 'Sandbox uses the legacy workspace parent-mount topology; recreate the container explicitly to migrate it.',
264
+ path: '/workspace/.agents/workspace'
265
+ });
266
+ }
267
+ if (snapshot.taskView && !snapshot.taskView.readable) {
268
+ findings.push({
269
+ repairKind: 'hard-failure',
270
+ code: 'SANDBOX_TASK_VIEW_UNREADABLE',
271
+ message: `Bound task view is not readable at ${snapshot.taskView.path}.`,
272
+ path: snapshot.taskView.path
273
+ });
274
+ }
275
+ if (snapshot.runtimeStoreOk === false) {
276
+ findings.push({
277
+ repairKind: 'hard-failure',
278
+ code: 'SANDBOX_RUNTIME_STORE_UNAVAILABLE',
279
+ message: 'Task-bound runtime store is not writable and readable by devuser.',
280
+ path: '/run/agent-infra/runtime'
281
+ });
282
+ }
245
283
  for (const mountPath of snapshot.unexpectedCapabilityMounts ?? []) {
246
284
  findings.push({
247
285
  repairKind: 'hard-failure',
@@ -250,6 +288,9 @@ export function classifySandboxRecovery(snapshot) {
250
288
  });
251
289
  }
252
290
  for (const mount of snapshot.mounts) {
291
+ if (snapshot.workspaceTopology === 'legacy-parent'
292
+ && mount.path.startsWith('/workspace/.agents/workspace'))
293
+ continue;
253
294
  if (mount.actualType !== mount.expectedType
254
295
  || !mount.sourceMatches
255
296
  || mount.actualRW !== mount.expectedRW
@@ -331,6 +372,20 @@ function inspectContainer(engine, container, runFn) {
331
372
  }
332
373
  return parsed[0];
333
374
  }
375
+ function workspaceTopology(mountsByDestination) {
376
+ if (mountsByDestination.has('/workspace/.agents/workspace'))
377
+ return 'legacy-parent';
378
+ const perState = [
379
+ '/workspace/.agents/workspace/active',
380
+ '/workspace/.agents/workspace/active/.short-ids.json',
381
+ '/workspace/.agents/workspace/completed',
382
+ '/workspace/.agents/workspace/blocked',
383
+ '/workspace/.agents/workspace/archive'
384
+ ];
385
+ return perState.some((destination) => mountsByDestination.has(destination))
386
+ ? 'per-state'
387
+ : 'unknown';
388
+ }
334
389
  function probe(engine, container, script, args, runOkFn, user = 'devuser') {
335
390
  return runOkFn(engine, 'docker', [
336
391
  'exec', '--user', user, container, 'sh', '-c', script, 'agent-infra-recovery', ...args
@@ -391,6 +446,7 @@ function expectedMounts(params) {
391
446
  controlStatusDir: control.statusDir,
392
447
  ...(params.workspace.mode === 'task-bound'
393
448
  ? {
449
+ runtimeDir: control.runtimeDir,
394
450
  taskSource: assertSandboxTaskSource(config.repoRoot, params.workspace.taskId),
395
451
  taskId: params.workspace.taskId
396
452
  }
@@ -489,6 +545,10 @@ export function collectSandboxRecoverySnapshot(params) {
489
545
  const capabilityPlan = createSandboxCapabilityPlan(params.config);
490
546
  const workspace = params.workspace ?? { mode: 'branch-only' };
491
547
  const tools = [...capabilityPlan.tools];
548
+ const topology = workspaceTopology(mountsByDestination);
549
+ const taskViewPath = workspace.mode === 'task-bound'
550
+ ? `/workspace/.agents/workspace/active/${workspace.taskId}/task.md`
551
+ : null;
492
552
  const seeds = declaredTmpfsSeedEntries(tools).map((seed) => {
493
553
  const mounted = mountsByDestination.get(seed.stagingPath)?.Type === 'bind';
494
554
  return {
@@ -507,6 +567,9 @@ export function collectSandboxRecoverySnapshot(params) {
507
567
  permissionsOk: probe(params.engine, params.container, 'test "$(stat -c %u:%g:%a -- "$1")" = "$(id -u devuser):$(id -g devuser):700"', [tool.containerMount], runOkFn),
508
568
  writable: probe(params.engine, params.container, 'probe="$1/.agent-infra-ready-$$"; trap \'rm -f -- "$probe"\' EXIT; : > "$probe"', [tool.containerMount], runOkFn)
509
569
  }));
570
+ const runtimeStoreOk = workspace.mode === 'task-bound'
571
+ ? probe(params.engine, params.container, 'probe="$1/.agent-infra-runtime-$$"; trap \'rm -f -- "$probe"\' EXIT; printf runtime-probe > "$probe" && test "$(cat -- "$probe")" = runtime-probe', ['/run/agent-infra/runtime'], runOkFn)
572
+ : undefined;
510
573
  const labels = inspection.Config?.Labels ?? {};
511
574
  const branchLabel = labels[sandboxBranchLabel(params.config)];
512
575
  const containerWorkspace = parseSandboxWorkspaceIdentity(labels, {
@@ -562,6 +625,14 @@ export function collectSandboxRecoverySnapshot(params) {
562
625
  actualBranch: typeof branchLabel === 'string' ? branchLabel : null,
563
626
  expectedWorkspace: workspace,
564
627
  actualWorkspace: containerWorkspace,
628
+ workspaceTopology: topology,
629
+ ...(taskViewPath === null ? {} : {
630
+ taskView: {
631
+ path: taskViewPath,
632
+ readable: probe(params.engine, params.container, 'test -r "$1"', [taskViewPath], runOkFn)
633
+ }
634
+ }),
635
+ ...(runtimeStoreOk === undefined ? {} : { runtimeStoreOk }),
565
636
  identityOk: typeof inspection.Id === 'string' && inspection.Id.length > 0
566
637
  && branchLabel === params.branch
567
638
  && sameSandboxWorkspaceIdentity(containerWorkspace, workspace),
@@ -677,10 +748,12 @@ function assess(params) {
677
748
  }
678
749
  function canRetryFreshReadiness(findings) {
679
750
  return findings.length > 0 && findings.every((finding) => finding.repairKind === 'hard-failure'
751
+ && finding.code !== 'SANDBOX_WORKSPACE_TOPOLOGY_MISMATCH'
680
752
  && finding.path !== undefined
681
753
  && (finding.path.startsWith('/workspace/.agents/workspace')
682
754
  || finding.path === '/home/devuser/.host-shell-config'
683
- || finding.path === '/home/devuser/.bash_aliases'));
755
+ || finding.path === '/home/devuser/.bash_aliases'
756
+ || finding.path === '/run/agent-infra/runtime'));
684
757
  }
685
758
  export async function assertFreshSandboxReady(params) {
686
759
  validateTmpfsSeedEntries({
@@ -891,14 +964,9 @@ export async function ensureSandboxReady(params) {
891
964
  : `Sandbox recovery failed in place. Replacing only the container. ${dataBoundary}`;
892
965
  warnings.push(warning);
893
966
  (params.writeWarning ?? ((message) => process.stderr.write(`${message}\n`)))(warning);
894
- const runVerboseFn = deps?.runVerbose ?? runVerboseEngine;
895
967
  let replacementResult = null;
896
968
  let replacementFailure = null;
897
969
  try {
898
- if (params.row.running) {
899
- runVerboseFn(params.engine, 'docker', ['stop', params.row.name]);
900
- }
901
- runVerboseFn(params.engine, 'docker', ['rm', params.row.name]);
902
970
  await params.recreate(params.branch);
903
971
  const rows = (deps?.fetchRows ?? fetchSandboxRows)(params.engine, sandboxLabel(params.config), sandboxBranchLabel(params.config));
904
972
  const replacement = selectSandboxContainer([...rows.running, ...rows.nonRunning], containerNameCandidates(params.config, params.branch));
@@ -4,7 +4,7 @@ import path from 'node:path';
4
4
  import { isRemovedHashShortIdInput } from '../task/short-id.js';
5
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
6
6
  const SHORT_ID_RE = /^\d+$/;
7
- const WORKSPACE_DIRS = ['active', 'completed', 'blocked', 'archive'];
7
+ export const TASK_WORKSPACE_STATES = ['active', 'completed', 'blocked', 'archive'];
8
8
  function resolveShortIdStrict(arg, repoRoot) {
9
9
  const scriptPath = path.join(repoRoot, '.agents', 'scripts', 'task-short-id.js');
10
10
  if (!fs.existsSync(scriptPath)) {
@@ -20,14 +20,28 @@ function stripQuotes(value) {
20
20
  return value.replace(/^(["'])(.*)\1$/, '$2');
21
21
  }
22
22
  function readTaskContent(repoRoot, taskId) {
23
- for (const dir of WORKSPACE_DIRS) {
24
- const taskPath = path.join(repoRoot, '.agents', 'workspace', dir, taskId, 'task.md');
23
+ for (const state of TASK_WORKSPACE_STATES) {
24
+ const taskPath = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
25
25
  if (fs.existsSync(taskPath)) {
26
- return fs.readFileSync(taskPath, 'utf8');
26
+ return { content: fs.readFileSync(taskPath, 'utf8'), state, taskMd: taskPath };
27
27
  }
28
28
  }
29
29
  throw new Error(`Task not found: ${taskId}`);
30
30
  }
31
+ function taskWorkspaceCandidates(repoRoot, taskId) {
32
+ return TASK_WORKSPACE_STATES.flatMap((state) => {
33
+ const taskMd = path.join(repoRoot, '.agents', 'workspace', state, taskId, 'task.md');
34
+ if (!fs.existsSync(taskMd))
35
+ return [];
36
+ const content = fs.readFileSync(taskMd, 'utf8');
37
+ return [{
38
+ taskId,
39
+ branch: resolveBranchFromTaskContent(content, taskId),
40
+ state,
41
+ taskMd
42
+ }];
43
+ });
44
+ }
31
45
  function resolveBranchFromTaskContent(content, taskId) {
32
46
  const frontmatterBranch = content.match(/^branch:\s*(.+)$/m);
33
47
  if (frontmatterBranch?.[1]?.trim()) {
@@ -45,13 +59,43 @@ export function resolveTaskBranch(arg, repoRoot) {
45
59
  }
46
60
  if (SHORT_ID_RE.test(arg)) {
47
61
  const taskId = resolveShortIdStrict(arg, repoRoot);
48
- const content = readTaskContent(repoRoot, taskId);
62
+ const { content } = readTaskContent(repoRoot, taskId);
49
63
  return resolveBranchFromTaskContent(content, taskId);
50
64
  }
51
65
  if (!TASK_ID_RE.test(arg)) {
52
66
  return arg;
53
67
  }
54
- const content = readTaskContent(repoRoot, arg);
68
+ const { content } = readTaskContent(repoRoot, arg);
55
69
  return resolveBranchFromTaskContent(content, arg);
56
70
  }
71
+ export function resolveTaskWorkspace(taskId, repoRoot) {
72
+ if (!TASK_ID_RE.test(taskId))
73
+ throw new Error(`Invalid task id: ${taskId}`);
74
+ const matches = taskWorkspaceCandidates(repoRoot, taskId);
75
+ if (matches.length === 0)
76
+ throw new Error(`Task not found: ${taskId}`);
77
+ if (matches.length !== 1) {
78
+ throw new Error(`SANDBOX_TASK_STATE_AMBIGUOUS: task ${taskId} exists in ${matches.map((match) => match.state).join(', ')}`);
79
+ }
80
+ return matches[0];
81
+ }
82
+ export function listTaskWorkspaces(repoRoot, state) {
83
+ const stateRoot = path.join(repoRoot, '.agents', 'workspace', state);
84
+ if (!fs.existsSync(stateRoot))
85
+ return [];
86
+ return fs.readdirSync(stateRoot, { withFileTypes: true })
87
+ .filter((entry) => entry.isDirectory() && TASK_ID_RE.test(entry.name))
88
+ .flatMap((entry) => {
89
+ const taskMd = path.join(stateRoot, entry.name, 'task.md');
90
+ if (!fs.existsSync(taskMd))
91
+ return [];
92
+ const content = fs.readFileSync(taskMd, 'utf8');
93
+ return [{
94
+ taskId: entry.name,
95
+ branch: resolveBranchFromTaskContent(content, entry.name),
96
+ state,
97
+ taskMd
98
+ }];
99
+ });
100
+ }
57
101
  //# sourceMappingURL=task-resolver.js.map
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { isRemovedHashShortIdInput } from '../task/short-id.js';
4
+ import { resolveTaskWorkspace } from './task-resolver.js';
4
5
  const TASK_ID_RE = /^TASK-\d{8}-\d{6}$/;
5
6
  const SHORT_ID_RE = /^\d+$/;
6
7
  function stripQuotes(value) {
@@ -78,6 +79,32 @@ export function resolveSandboxTarget(requestedRef, repoRoot) {
78
79
  }
79
80
  return { requestedRef, branch: requestedRef, workspace: { mode: 'branch-only' } };
80
81
  }
82
+ export function resolveSandboxCleanupTarget(requestedRef, repoRoot, options = {}) {
83
+ if (isRemovedHashShortIdInput(requestedRef)) {
84
+ throw new Error(`Invalid task short id '${requestedRef}': task short ids must use bare digits`);
85
+ }
86
+ if (TASK_ID_RE.test(requestedRef)) {
87
+ const task = resolveTaskWorkspace(requestedRef, repoRoot);
88
+ if (!options.allowProtected && (task.state === 'blocked' || task.state === 'archive')) {
89
+ throw new Error(`SANDBOX_CLEANUP_STATE_UNSUPPORTED: task ${requestedRef} is ${task.state}`);
90
+ }
91
+ return {
92
+ requestedRef,
93
+ branch: task.branch,
94
+ workspace: { mode: 'task-bound', taskId: task.taskId },
95
+ taskState: task.state
96
+ };
97
+ }
98
+ const target = resolveSandboxTarget(requestedRef, repoRoot);
99
+ return {
100
+ requestedRef,
101
+ branch: target.branch,
102
+ workspace: target.workspace.mode === 'task-bound'
103
+ ? { mode: 'task-bound', taskId: target.workspace.taskId }
104
+ : { mode: 'branch-only' },
105
+ taskState: target.workspace.mode === 'task-bound' ? 'active' : 'branch-only'
106
+ };
107
+ }
81
108
  export function parseSandboxWorkspaceIdentity(labels, keys) {
82
109
  const mode = labels[keys.mode];
83
110
  const taskId = labels[keys.taskId];