@fitlab-ai/agent-infra 0.9.4 → 0.9.6

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 (130) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +13 -3
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +492 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +175 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +255 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +3 -3
  9. package/dist/lib/git/workflow.js +0 -1
  10. package/dist/lib/git/worktree-identity.js +64 -0
  11. package/dist/lib/internal/codex-lifecycle.js +300 -0
  12. package/dist/lib/internal/release-workflow.js +108 -11
  13. package/dist/lib/internal/sandbox-control.js +43 -6
  14. package/dist/lib/internal/task-create.js +56 -0
  15. package/dist/lib/internal/task-orchestration.js +71 -31
  16. package/dist/lib/platform/issue-comments.js +2 -1
  17. package/dist/lib/release/workflow.js +29 -21
  18. package/dist/lib/sandbox/commands/create.js +16 -16
  19. package/dist/lib/sandbox/commands/prune.js +40 -16
  20. package/dist/lib/sandbox/commands/rm.js +318 -89
  21. package/dist/lib/sandbox/control/client.js +152 -22
  22. package/dist/lib/sandbox/control/executor.js +136 -0
  23. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  24. package/dist/lib/sandbox/control/protocol.js +73 -18
  25. package/dist/lib/sandbox/control/server.js +235 -80
  26. package/dist/lib/sandbox/control/state.js +181 -0
  27. package/dist/lib/sandbox/index.js +2 -2
  28. package/dist/lib/sandbox/managed-fs.js +14 -2
  29. package/dist/lib/sandbox/mounts.js +35 -24
  30. package/dist/lib/sandbox/recovery.js +262 -45
  31. package/dist/lib/sandbox/workspace-view.js +80 -11
  32. package/dist/lib/sandbox/worktree-safety.js +196 -0
  33. package/dist/lib/server/process-control.js +6 -0
  34. package/dist/lib/task/activity-log.js +32 -5
  35. package/dist/lib/task/artifact-lifecycle.js +5 -4
  36. package/dist/lib/task/codex-orchestration.js +161 -0
  37. package/dist/lib/task/commands/status.js +3 -0
  38. package/dist/lib/task/commands/validate.js +289 -0
  39. package/dist/lib/task/commit-finalization.js +50 -12
  40. package/dist/lib/task/create-service.js +137 -0
  41. package/dist/lib/task/create.js +333 -0
  42. package/dist/lib/task/delegation-receipts.js +25 -0
  43. package/dist/lib/task/events.js +10 -4
  44. package/dist/lib/task/finalization-artifacts.js +2 -1
  45. package/dist/lib/task/implementation-inputs.js +1 -7
  46. package/dist/lib/task/index.js +9 -0
  47. package/dist/lib/task/orchestration.js +283 -18
  48. package/dist/lib/task/review-fingerprint.js +4 -0
  49. package/dist/lib/task/verification-engine.js +38 -6
  50. package/dist/lib/task/verification.js +1 -0
  51. package/dist/lib/task/workspace-snapshot.js +78 -3
  52. package/lib/agent-clients/adapter.ts +4 -4
  53. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +569 -0
  54. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +323 -0
  55. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +298 -0
  56. package/lib/agent-clients/adapters/codex.ts +3 -3
  57. package/lib/git/workflow.ts +0 -1
  58. package/lib/git/worktree-identity.ts +86 -0
  59. package/lib/internal/codex-lifecycle.ts +308 -0
  60. package/lib/internal/release-workflow.ts +107 -11
  61. package/lib/internal/sandbox-control.ts +38 -6
  62. package/lib/internal/task-create.ts +61 -0
  63. package/lib/internal/task-orchestration.ts +57 -31
  64. package/lib/platform/issue-comments.ts +2 -1
  65. package/lib/release/workflow.ts +69 -20
  66. package/lib/sandbox/commands/create.ts +18 -21
  67. package/lib/sandbox/commands/prune.ts +53 -15
  68. package/lib/sandbox/commands/rm.ts +368 -103
  69. package/lib/sandbox/control/client.ts +173 -25
  70. package/lib/sandbox/control/executor.ts +163 -0
  71. package/lib/sandbox/control/lifecycle.ts +342 -0
  72. package/lib/sandbox/control/protocol.ts +110 -48
  73. package/lib/sandbox/control/server.ts +252 -85
  74. package/lib/sandbox/control/state.ts +196 -0
  75. package/lib/sandbox/index.ts +2 -2
  76. package/lib/sandbox/managed-fs.ts +17 -2
  77. package/lib/sandbox/mounts.ts +31 -13
  78. package/lib/sandbox/recovery.ts +301 -60
  79. package/lib/sandbox/workspace-view.ts +89 -12
  80. package/lib/sandbox/worktree-safety.ts +234 -0
  81. package/lib/server/process-control.ts +11 -0
  82. package/lib/task/activity-log.ts +46 -7
  83. package/lib/task/artifact-lifecycle.ts +5 -4
  84. package/lib/task/codex-orchestration.ts +233 -0
  85. package/lib/task/commands/status.ts +2 -0
  86. package/lib/task/commands/validate.ts +282 -0
  87. package/lib/task/commit-finalization.ts +75 -12
  88. package/lib/task/create-service.ts +171 -0
  89. package/lib/task/create.ts +372 -0
  90. package/lib/task/delegation-receipts.ts +52 -3
  91. package/lib/task/events.ts +9 -4
  92. package/lib/task/finalization-artifacts.ts +2 -1
  93. package/lib/task/implementation-inputs.ts +1 -5
  94. package/lib/task/index.ts +8 -0
  95. package/lib/task/orchestration.ts +381 -22
  96. package/lib/task/review-fingerprint.ts +9 -0
  97. package/lib/task/verification-engine.ts +42 -8
  98. package/lib/task/verification.ts +2 -1
  99. package/lib/task/workspace-snapshot.ts +98 -4
  100. package/package.json +7 -7
  101. package/templates/.agents/hooks/lifecycle-delegation.js +88 -8
  102. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  103. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  104. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  105. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  106. package/templates/.agents/rules/task-management.en.md +1 -0
  107. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  108. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  109. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  110. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  111. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  112. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  113. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  114. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  115. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  116. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  117. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  118. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  119. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  120. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  121. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  122. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  123. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  124. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  125. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  126. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  127. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  128. package/templates/.codex/hooks.json +50 -0
  129. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  130. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -1,7 +1,8 @@
1
1
  import { normalizeAgentToken, AGENT_USAGE_HINT } from '../agent-clients/tokens.js';
2
- import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, prepareOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, statusCommitIntent, statusOrchestration } from '../task/orchestration.js';
2
+ import { abortCommitIntent, activateMatchingOrchestrationDelegation, activateOrchestrationDelegation, advanceOrchestration, beginCommitIntent, beginOrResumeOrchestration, checkpointCommitIntent, completeCommitIntent, pauseOrchestration, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } from '../task/orchestration.js';
3
+ import { prepareCodexOrchestrationDelegation } from '../task/codex-orchestration.js';
3
4
  import { isAgentClientId } from '../agent-clients/types.js';
4
- const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-status> [options]\n';
5
+ const USAGE = 'Usage: agent-infra-internal task-orchestration <task-ref|auto> <begin-or-resume|route|prepare|hook-start|hook-stop|advance|pause|status|commit-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
5
6
  function usageFailure(message) {
6
7
  process.stdout.write(`${JSON.stringify({
7
8
  status: 'failed', changed: false,
@@ -10,7 +11,7 @@ function usageFailure(message) {
10
11
  process.stderr.write(USAGE);
11
12
  process.exitCode = 2;
12
13
  }
13
- function taskOrchestration(args = []) {
14
+ async function taskOrchestration(args = []) {
14
15
  if (args[0] === '--help' || args[0] === '-h') {
15
16
  process.stdout.write(USAGE);
16
17
  return;
@@ -22,7 +23,7 @@ function taskOrchestration(args = []) {
22
23
  const [taskRef, intent] = args;
23
24
  if (![
24
25
  'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
25
- 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
26
+ 'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
26
27
  ].includes(intent)) {
27
28
  usageFailure(`unknown intent '${intent}'`);
28
29
  return;
@@ -48,7 +49,8 @@ function taskOrchestration(args = []) {
48
49
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
49
50
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
50
51
  '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
51
- '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
52
+ '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head',
53
+ '--attempt', '--git-worktree-root'
52
54
  ].includes(flag)) {
53
55
  usageFailure(`unknown option '${flag}'`);
54
56
  return;
@@ -66,12 +68,14 @@ function taskOrchestration(args = []) {
66
68
  values[flag] = value;
67
69
  }
68
70
  const commitIntentFlags = {
69
- 'commit-begin': ['--agent', '--baseline-head', '--orchestrated'],
70
- 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref'],
71
- 'commit-complete': ['--token', '--agent'],
72
- 'commit-recover': ['--agent'],
73
- 'commit-abort': ['--token', '--expected-head'],
74
- 'commit-status': []
71
+ 'commit-start': ['--agent', '--git-worktree-root'],
72
+ 'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
73
+ 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
74
+ 'commit-complete': ['--token', '--agent', '--git-worktree-root'],
75
+ 'commit-recover': ['--agent', '--git-worktree-root'],
76
+ 'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
77
+ 'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
78
+ 'commit-status': ['--git-worktree-root']
75
79
  };
76
80
  if (intent in commitIntentFlags) {
77
81
  const allowed = commitIntentFlags[intent];
@@ -83,7 +87,7 @@ function taskOrchestration(args = []) {
83
87
  }
84
88
  else {
85
89
  const commitOnly = [...seen].find((flag) => [
86
- '--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
90
+ '--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
87
91
  ].includes(flag));
88
92
  if (commitOnly) {
89
93
  usageFailure(`intent '${intent}' does not accept '${commitOnly}'`);
@@ -91,13 +95,29 @@ function taskOrchestration(args = []) {
91
95
  }
92
96
  }
93
97
  const requireValues = (flags) => flags.find((flag) => values[flag] === undefined);
98
+ const coreOptions = values['--git-worktree-root'] === undefined
99
+ ? {}
100
+ : { gitWorktreeRoot: values['--git-worktree-root'] };
94
101
  if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
95
102
  usageFailure(`unknown client '${values['--client']}'`);
96
103
  return;
97
104
  }
98
105
  let result;
99
- if (intent === 'commit-begin') {
100
- const missing = requireValues(['--agent', '--baseline-head']);
106
+ if (intent === 'commit-start') {
107
+ const missing = requireValues(['--agent']);
108
+ if (missing) {
109
+ usageFailure(`intent 'commit-start' requires '${missing}'`);
110
+ return;
111
+ }
112
+ const agent = normalizeAgentToken(values['--agent']);
113
+ if (!agent) {
114
+ usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
115
+ return;
116
+ }
117
+ result = startCommitAttempt(taskRef, { agent }, coreOptions);
118
+ }
119
+ else if (intent === 'commit-begin') {
120
+ const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
101
121
  if (missing) {
102
122
  usageFailure(`intent 'commit-begin' requires '${missing}'`);
103
123
  return;
@@ -108,8 +128,8 @@ function taskOrchestration(args = []) {
108
128
  return;
109
129
  }
110
130
  result = beginCommitIntent(taskRef, {
111
- agent, orchestrated, baselineHead: values['--baseline-head']
112
- });
131
+ agent, orchestrated, baselineHead: values['--baseline-head'], attempt: values['--attempt']
132
+ }, coreOptions);
113
133
  }
114
134
  else if (intent === 'commit-checkpoint') {
115
135
  const missing = requireValues(['--token', '--kind', '--head']);
@@ -135,7 +155,7 @@ function taskOrchestration(args = []) {
135
155
  result = checkpointCommitIntent(taskRef, {
136
156
  token: values['--token'], kind: values['--kind'],
137
157
  head: values['--head'], remote: values['--remote'], ref: values['--ref']
138
- });
158
+ }, coreOptions);
139
159
  }
140
160
  else if (intent === 'commit-complete') {
141
161
  const missing = requireValues(['--token', '--agent']);
@@ -148,7 +168,7 @@ function taskOrchestration(args = []) {
148
168
  usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
149
169
  return;
150
170
  }
151
- result = completeCommitIntent(taskRef, { token: values['--token'], agent });
171
+ result = completeCommitIntent(taskRef, { token: values['--token'], agent }, coreOptions);
152
172
  }
153
173
  else if (intent === 'commit-recover') {
154
174
  const missing = requireValues(['--agent']);
@@ -161,7 +181,7 @@ function taskOrchestration(args = []) {
161
181
  usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
162
182
  return;
163
183
  }
164
- result = recoverCommitIntent(taskRef, { agent });
184
+ result = recoverCommitIntent(taskRef, { agent }, coreOptions);
165
185
  }
166
186
  else if (intent === 'commit-abort') {
167
187
  const missing = requireValues(['--token', '--expected-head']);
@@ -169,10 +189,29 @@ function taskOrchestration(args = []) {
169
189
  usageFailure(`intent 'commit-abort' requires '${missing}'`);
170
190
  return;
171
191
  }
172
- result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] });
192
+ result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] }, coreOptions);
193
+ }
194
+ else if (intent === 'commit-terminate') {
195
+ const missing = requireValues(['--attempt', '--agent', '--code']);
196
+ if (missing) {
197
+ usageFailure(`intent 'commit-terminate' requires '${missing}'`);
198
+ return;
199
+ }
200
+ const agent = normalizeAgentToken(values['--agent']);
201
+ if (!agent) {
202
+ usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
203
+ return;
204
+ }
205
+ if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code'])) {
206
+ usageFailure('--code must be a stable uppercase identifier');
207
+ return;
208
+ }
209
+ result = terminateCommitAttempt(taskRef, {
210
+ attempt: values['--attempt'], agent, code: values['--code']
211
+ }, coreOptions);
173
212
  }
174
213
  else if (intent === 'commit-status') {
175
- result = statusCommitIntent(taskRef);
214
+ result = statusCommitIntent(taskRef, coreOptions);
176
215
  }
177
216
  else if (intent === 'begin-or-resume') {
178
217
  const missing = requireValues(['--client']);
@@ -195,6 +234,7 @@ function taskOrchestration(args = []) {
195
234
  return;
196
235
  }
197
236
  result = beginOrResumeOrchestration(taskRef, {
237
+ ...coreOptions,
198
238
  maxSteps,
199
239
  client: values['--client'],
200
240
  modelPolicy: hasAnyPolicy ? {
@@ -210,10 +250,10 @@ function taskOrchestration(args = []) {
210
250
  });
211
251
  }
212
252
  else if (intent === 'route') {
213
- result = routeOrchestration(taskRef);
253
+ result = routeOrchestration(taskRef, coreOptions);
214
254
  }
215
255
  else if (intent === 'status') {
216
- result = statusOrchestration(taskRef);
256
+ result = statusOrchestration(taskRef, coreOptions);
217
257
  }
218
258
  else if (intent === 'prepare') {
219
259
  const missing = requireValues(['--client']);
@@ -221,11 +261,11 @@ function taskOrchestration(args = []) {
221
261
  usageFailure(`intent 'prepare' requires '${missing}'`);
222
262
  return;
223
263
  }
224
- result = prepareOrchestrationDelegation(taskRef, {
264
+ result = await prepareCodexOrchestrationDelegation(taskRef, {
225
265
  client: values['--client'],
226
266
  requestedModel: values['--requested-model'],
227
267
  requestedReasoningEffort: values['--requested-reasoning-effort']
228
- });
268
+ }, { orchestrationOptions: coreOptions });
229
269
  }
230
270
  else if (intent === 'hook-start') {
231
271
  const missing = requireValues([
@@ -245,8 +285,8 @@ function taskOrchestration(args = []) {
245
285
  reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
246
286
  };
247
287
  result = taskRef === 'auto'
248
- ? activateMatchingOrchestrationDelegation(values['--client'], event)
249
- : activateOrchestrationDelegation(taskRef, event);
288
+ ? activateMatchingOrchestrationDelegation(values['--client'], event, coreOptions)
289
+ : activateOrchestrationDelegation(taskRef, event, coreOptions);
250
290
  }
251
291
  else if (intent === 'hook-stop') {
252
292
  if (taskRef === 'auto') {
@@ -257,7 +297,7 @@ function taskOrchestration(args = []) {
257
297
  }
258
298
  result = sealMatchingOrchestrationDelegation(values['--client'], {
259
299
  nativeAgent: values['--native-agent'], childId: values['--child-id']
260
- });
300
+ }, coreOptions);
261
301
  }
262
302
  else {
263
303
  const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
@@ -274,11 +314,11 @@ function taskOrchestration(args = []) {
274
314
  childId: values['--child-id'], exitCode,
275
315
  afterFingerprint: values['--after-fingerprint'],
276
316
  changedPaths: values['--changed-paths'] ? values['--changed-paths'].split(',').filter(Boolean) : []
277
- });
317
+ }, coreOptions);
278
318
  }
279
319
  }
280
320
  else if (intent === 'advance') {
281
- result = advanceOrchestration(taskRef);
321
+ result = advanceOrchestration(taskRef, coreOptions);
282
322
  }
283
323
  else {
284
324
  const missing = requireValues(['--code', '--message', '--recoverable']);
@@ -290,7 +330,7 @@ function taskOrchestration(args = []) {
290
330
  usageFailure('--recoverable must be true or false');
291
331
  return;
292
332
  }
293
- result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true');
333
+ result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true', coreOptions);
294
334
  }
295
335
  process.stdout.write(`${JSON.stringify(result)}\n`);
296
336
  if (result.status === 'failed')
@@ -20,6 +20,7 @@ const ARTIFACT_TITLES = {
20
20
  code: '实现报告',
21
21
  'review-code': '代码审查',
22
22
  'manual-validation': '人工验证报告',
23
+ 'validation-run': '验证运行证据',
23
24
  'pr-review': 'PR 审查报告'
24
25
  };
25
26
  function normalizeCommentContent(content) {
@@ -52,7 +53,7 @@ function renderTaskComment(content, taskId, agent) {
52
53
  }
53
54
  function artifactIdentity(artifact) {
54
55
  const stem = path.basename(artifact, '.md');
55
- const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|pr-review)(?:-r(\d+))?$/);
56
+ const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
56
57
  if (!match)
57
58
  throw new Error(`unsupported artifact '${artifact}'`);
58
59
  const base = ARTIFACT_TITLES[match[1]];
@@ -1,34 +1,42 @@
1
+ import crypto from 'node:crypto';
2
+ function byteSort(values) {
3
+ return [...values].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
4
+ }
1
5
  function deriveReleasePhase(facts) {
2
6
  if (!facts.localTag && !facts.localTagAncestor)
3
7
  return 'unprepared';
8
+ if (facts.post.commit)
9
+ return facts.post.published ? 'complete' : 'post-prepared';
4
10
  if (!facts.remoteBranch && !facts.remoteTag)
5
11
  return 'prepared';
6
12
  if (!facts.remoteBranch || !facts.remoteTag)
7
13
  return 'partially-published';
8
14
  if (facts.githubRelease !== true || facts.npm !== true || facts.homebrew !== true)
9
15
  return 'published';
10
- if (facts.smoke !== 'success' || !facts.postCommit)
11
- return 'post-pending';
12
- return 'complete';
16
+ return 'post-pending';
13
17
  }
14
- function releaseSnapshot(version, facts) {
15
- return { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
18
+ function createPostConfirmation(version, post) {
19
+ if (!post.commit || !post.isHead || post.worktree.length || post.staged.length)
20
+ return null;
21
+ const canonical = {
22
+ version,
23
+ commit: post.commit,
24
+ branch: post.branch,
25
+ upstream: post.upstream,
26
+ remoteHead: post.remoteHead,
27
+ newVersion: post.newVersion,
28
+ changedPaths: byteSort(post.changedPaths),
29
+ demoInputSha256: post.demoInputSha256,
30
+ worktree: byteSort(post.worktree),
31
+ staged: byteSort(post.staged)
32
+ };
33
+ const sha256 = `sha256:${crypto.createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
34
+ return { ...canonical, sha256 };
16
35
  }
17
- async function runReleaseAction(input) {
18
- const before = releaseSnapshot(input.version, await input.inspect());
19
- if (input.action === 'inspect')
20
- return { status: 'no-op', changed: false, before, snapshot: before, error: null };
21
- if (before.facts.localTagConflict)
22
- return { status: 'failed', changed: false, before, snapshot: before, error: { code: 'GIT_TAG_CONFLICT', message: `Tag ${before.tag} is not reachable from HEAD` } };
23
- const allowed = input.action === 'prepare' ? before.phase === 'unprepared'
24
- : input.action === 'publish' ? before.facts.localTag && ['prepared', 'partially-published'].includes(before.phase)
25
- : ['published', 'post-pending'].includes(before.phase);
26
- if (!allowed)
27
- return { status: before.phase === 'complete' ? 'no-op' : 'failed', changed: false, before, snapshot: before, error: before.phase === 'complete' ? null : { code: 'RELEASE_PHASE_INVALID', message: `Cannot ${input.action} from ${before.phase}` } };
28
- await input[input.action]?.();
29
- const snapshot = releaseSnapshot(input.version, await input.inspect());
30
- const progressed = snapshot.phase !== before.phase;
31
- return { status: progressed ? 'applied' : 'blocked', changed: progressed, before, snapshot, error: progressed ? null : { code: 'RELEASE_PROGRESS_PENDING', message: `${input.action} has not reached the next observable phase` } };
36
+ function releaseSnapshot(version, facts) {
37
+ const snapshot = { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
38
+ const postConfirmation = createPostConfirmation(version, facts.post);
39
+ return postConfirmation ? { ...snapshot, postConfirmation } : snapshot;
32
40
  }
33
- export { deriveReleasePhase, releaseSnapshot, runReleaseAction };
41
+ export { createPostConfirmation, deriveReleasePhase, releaseSnapshot };
34
42
  //# sourceMappingURL=workflow.js.map
@@ -2,9 +2,8 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { createHash } from 'node:crypto';
5
- import { execFileSync, spawn } from 'node:child_process';
5
+ import { execFileSync } from 'node:child_process';
6
6
  import { parseArgs } from 'node:util';
7
- import { fileURLToPath } from 'node:url';
8
7
  import * as p from '@clack/prompts';
9
8
  import pc from 'picocolors';
10
9
  import { listAgentClientAdapters } from '../../agent-clients/registry.js';
@@ -17,10 +16,10 @@ import { commandForEngine, execEngine, run, runEngine, runOk, runOkEngine, runSa
17
16
  import { parseSandboxWorkspaceIdentity, resolveSandboxTarget, sameSandboxWorkspaceIdentity } from '../workspace-identity.js';
18
17
  import { createSandboxCapabilityPlan, runSandboxHooks, runBoundedSandboxHookCommand } from '../agent-client-reconciler.js';
19
18
  import { resolveTools, tmpfsSeedStagingPath, tmpfsSeedTargetPath, toolConfigDirCandidates } from '../tools.js';
20
- import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts } from '../recovery.js';
19
+ import { assertFreshSandboxReady, hydrateTmpfsSeedEntries, prepareTmpfsMounts, startSandboxControlBroker } from '../recovery.js';
21
20
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.js';
22
21
  import { sandboxCoreBindMounts } from '../mounts.js';
23
- import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView } from '../workspace-view.js';
22
+ import { assertSandboxTaskSource, materializeSandboxControl, materializeSandboxWorkspaceView, prepareSandboxWorkspaceMountTargets } from '../workspace-view.js';
24
23
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.js';
25
24
  import { validateSelinuxDisableEnv } from '../engines/selinux.js';
26
25
  import { dotfilesCacheDir, materializeDotfiles } from '../dotfiles.js';
@@ -65,11 +64,6 @@ const MANAGED_GITHUB_GIT_CONFIG = [
65
64
  '[url "https://github.com/"]',
66
65
  '\tinsteadOf = git@github.com:'
67
66
  ];
68
- function internalCliPath() {
69
- const directory = path.dirname(fileURLToPath(import.meta.url));
70
- const extension = path.extname(fileURLToPath(import.meta.url));
71
- return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
72
- }
73
67
  function resolveToolDirs(config, tools, branch) {
74
68
  return tools.map((tool) => {
75
69
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -895,8 +889,12 @@ export async function create(args) {
895
889
  const requestedDescription = target.workspace.mode === 'task-bound'
896
890
  ? `task-bound:${target.workspace.taskId}`
897
891
  : target.workspace.mode;
892
+ const recoveryTarget = target.workspace.mode === 'task-bound'
893
+ ? target.workspace.taskId
894
+ : branch;
898
895
  throw new Error(`SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
899
- + `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`);
896
+ + `but this request is ${requestedDescription}. `
897
+ + `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`);
900
898
  }
901
899
  }
902
900
  message('Removing old container instance...');
@@ -962,9 +960,11 @@ export async function create(args) {
962
960
  container,
963
961
  identity: target.workspace
964
962
  });
963
+ prepareSandboxWorkspaceMountTargets(worktree);
965
964
  const control = materializeSandboxControl({
966
965
  base: effectiveConfig.controlBase,
967
966
  repoRoot: effectiveConfig.repoRoot,
967
+ worktreeRoot: worktree,
968
968
  project: effectiveConfig.project,
969
969
  container,
970
970
  branch,
@@ -981,6 +981,7 @@ export async function create(args) {
981
981
  shellConfigHostDir: hostShellConfig.hostDir,
982
982
  workspaceViewRoot: workspaceView.root,
983
983
  controlDir: control.channelDir,
984
+ controlStatusDir: control.statusDir,
984
985
  ...(target.workspace.mode === 'task-bound'
985
986
  ? {
986
987
  taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
@@ -1051,7 +1052,11 @@ export async function create(args) {
1051
1052
  '-e',
1052
1053
  `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1053
1054
  '-e',
1055
+ `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1056
+ '-e',
1054
1057
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1058
+ '-e',
1059
+ 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1055
1060
  '--label',
1056
1061
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1057
1062
  ...coreVolumes,
@@ -1069,12 +1074,7 @@ export async function create(args) {
1069
1074
  '/workspace',
1070
1075
  effectiveConfig.imageName
1071
1076
  ]);
1072
- const broker = spawn(process.execPath, [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath], { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' });
1073
- broker.once('error', () => {
1074
- // Fresh readiness verification below remains the source of truth;
1075
- // consume detached spawn errors so they cannot crash the command.
1076
- });
1077
- broker.unref();
1077
+ await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1078
1078
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1079
1079
  }
1080
1080
  finally {
@@ -5,12 +5,13 @@ import * as p from '@clack/prompts';
5
5
  import pc from 'picocolors';
6
6
  import { loadConfig } from '../config.js';
7
7
  import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.js';
8
- import { detectEngine } from '../engine.js';
8
+ import { detectEngine, ENGINES } from '../engine.js';
9
9
  import { hostJoin } from '../engines/wsl2-paths.js';
10
10
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.js';
11
11
  import { parseLabels } from './ls.js';
12
12
  import { runEngine, runSafe } from '../shell.js';
13
13
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.js';
14
+ import { createCleanPermit, formatWorktreeSnapshot, inspectWorktrees } from '../worktree-safety.js';
14
15
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
15
16
  function listChildDirs(base) {
16
17
  if (!fs.existsSync(base)) {
@@ -83,12 +84,15 @@ export function collectOrphanGroups(config, tools, activeBranches) {
83
84
  }
84
85
  return groups;
85
86
  }
86
- export function removeOrphanGroups(config, groups) {
87
+ export function removeOrphanGroups(config, groups, permits, allowRegisteredPathFallback = false) {
87
88
  let removedWorktrees = false;
88
89
  for (const group of groups) {
89
90
  for (const dir of group.dirs) {
90
91
  if (group.kind === 'worktree') {
91
- removeWorktreeDir(config.repoRoot, group.base, dir);
92
+ const permit = permits.get(path.resolve(dir));
93
+ if (!permit)
94
+ throw new Error(`Missing worktree removal permit: ${dir}`);
95
+ removeWorktreeDir(config.repoRoot, group.base, dir, permit, { allowRegisteredPathFallback });
92
96
  removedWorktrees = true;
93
97
  }
94
98
  else {
@@ -115,7 +119,7 @@ function writeGroups(groups) {
115
119
  }
116
120
  }
117
121
  }
118
- export async function prune(args) {
122
+ export async function prune(args, dependencies = {}) {
119
123
  const { values } = parseArgs({
120
124
  args,
121
125
  strict: true,
@@ -128,9 +132,9 @@ export async function prune(args) {
128
132
  process.stdout.write(`${USAGE}\n`);
129
133
  return;
130
134
  }
131
- const config = loadConfig();
132
- const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
133
- const engine = detectEngine(config);
135
+ const config = dependencies.config ?? loadConfig();
136
+ const tools = dependencies.tools ?? [...createSandboxCapabilityPlan(config).cleanupInventory];
137
+ const engine = dependencies.engine ?? detectEngine(config);
134
138
  const psArgs = [
135
139
  'ps',
136
140
  '-a',
@@ -139,15 +143,27 @@ export async function prune(args) {
139
143
  '--format',
140
144
  '{{.Labels}}'
141
145
  ];
142
- let labelsOutput;
143
- try {
144
- labelsOutput = runEngine(engine, 'docker', psArgs);
145
- }
146
- catch {
147
- throw new Error('Unable to determine active sandbox branches: docker ps failed');
146
+ let labelsOutput = dependencies.labelsOutput;
147
+ if (labelsOutput === undefined) {
148
+ try {
149
+ labelsOutput = runEngine(engine, 'docker', psArgs);
150
+ }
151
+ catch {
152
+ throw new Error('Unable to determine active sandbox branches: docker ps failed');
153
+ }
148
154
  }
155
+ const confirm = dependencies.confirm ?? p.confirm;
156
+ const isCancel = dependencies.isCancel ?? p.isCancel;
149
157
  const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
150
158
  const count = orphanCount(groups);
159
+ const worktrees = groups.filter((group) => group.kind === 'worktree').flatMap((group) => group.dirs);
160
+ const inspections = inspectWorktrees(worktrees);
161
+ const blockers = inspections.filter((inspection) => inspection.status !== 'clean');
162
+ const permits = new Map();
163
+ for (const inspection of inspections) {
164
+ if (inspection.status === 'clean')
165
+ permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
166
+ }
151
167
  p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
152
168
  if (count === 0) {
153
169
  p.log.success('No orphaned sandbox state dirs found');
@@ -155,19 +171,27 @@ export async function prune(args) {
155
171
  return;
156
172
  }
157
173
  writeGroups(groups);
174
+ for (const blocker of blockers) {
175
+ p.log.error(blocker.status === 'failed'
176
+ ? `${JSON.stringify(blocker.worktree)}: ${blocker.message}`
177
+ : formatWorktreeSnapshot(blocker.snapshot));
178
+ }
158
179
  if (values['dry-run']) {
159
180
  p.outro(pc.green('Dry run complete'));
160
181
  return;
161
182
  }
162
- const shouldRemove = await p.confirm({
183
+ if (blockers.length > 0) {
184
+ throw new Error('Refusing to prune because worktree preflight found blocker(s); no orphan state was deleted');
185
+ }
186
+ const shouldRemove = await confirm({
163
187
  message: `Remove ${count} orphaned sandbox state dirs?`,
164
188
  initialValue: true
165
189
  });
166
- if (p.isCancel(shouldRemove) || !shouldRemove) {
190
+ if (isCancel(shouldRemove) || !shouldRemove) {
167
191
  p.outro('Cancelled');
168
192
  return;
169
193
  }
170
- const removedWorktrees = removeOrphanGroups(config, groups);
194
+ const removedWorktrees = removeOrphanGroups(config, groups, permits, engine === ENGINES.WSL2);
171
195
  if (removedWorktrees) {
172
196
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
173
197
  }