@fitlab-ai/agent-infra 0.9.5 → 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 (106) hide show
  1. package/dist/bin/internal-cli.js +3 -3
  2. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +126 -16
  3. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +14 -2
  4. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +48 -4
  5. package/dist/lib/agent-clients/adapters/codex.js +1 -1
  6. package/dist/lib/git/workflow.js +0 -1
  7. package/dist/lib/internal/codex-lifecycle.js +100 -2
  8. package/dist/lib/internal/release-workflow.js +108 -11
  9. package/dist/lib/internal/sandbox-control.js +43 -6
  10. package/dist/lib/internal/task-create.js +12 -4
  11. package/dist/lib/internal/task-orchestration.js +5 -4
  12. package/dist/lib/platform/issue-comments.js +2 -1
  13. package/dist/lib/release/workflow.js +29 -21
  14. package/dist/lib/sandbox/commands/create.js +15 -16
  15. package/dist/lib/sandbox/commands/prune.js +40 -16
  16. package/dist/lib/sandbox/commands/rm.js +318 -89
  17. package/dist/lib/sandbox/control/client.js +138 -22
  18. package/dist/lib/sandbox/control/executor.js +136 -0
  19. package/dist/lib/sandbox/control/lifecycle.js +348 -0
  20. package/dist/lib/sandbox/control/protocol.js +62 -26
  21. package/dist/lib/sandbox/control/server.js +222 -102
  22. package/dist/lib/sandbox/control/state.js +181 -0
  23. package/dist/lib/sandbox/index.js +2 -2
  24. package/dist/lib/sandbox/managed-fs.js +14 -2
  25. package/dist/lib/sandbox/mounts.js +35 -24
  26. package/dist/lib/sandbox/recovery.js +262 -45
  27. package/dist/lib/sandbox/workspace-view.js +73 -10
  28. package/dist/lib/sandbox/worktree-safety.js +196 -0
  29. package/dist/lib/server/process-control.js +6 -0
  30. package/dist/lib/task/artifact-lifecycle.js +5 -4
  31. package/dist/lib/task/codex-orchestration.js +161 -0
  32. package/dist/lib/task/commands/status.js +3 -0
  33. package/dist/lib/task/commands/validate.js +289 -0
  34. package/dist/lib/task/commit-finalization.js +36 -9
  35. package/dist/lib/task/delegation-receipts.js +25 -0
  36. package/dist/lib/task/events.js +10 -4
  37. package/dist/lib/task/finalization-artifacts.js +2 -1
  38. package/dist/lib/task/implementation-inputs.js +1 -7
  39. package/dist/lib/task/index.js +9 -0
  40. package/dist/lib/task/orchestration.js +133 -4
  41. package/dist/lib/task/verification-engine.js +38 -6
  42. package/dist/lib/task/verification.js +1 -0
  43. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +142 -15
  44. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +19 -3
  45. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +51 -3
  46. package/lib/agent-clients/adapters/codex.ts +1 -1
  47. package/lib/git/workflow.ts +0 -1
  48. package/lib/internal/codex-lifecycle.ts +107 -3
  49. package/lib/internal/release-workflow.ts +107 -11
  50. package/lib/internal/sandbox-control.ts +38 -6
  51. package/lib/internal/task-create.ts +16 -4
  52. package/lib/internal/task-orchestration.ts +4 -4
  53. package/lib/platform/issue-comments.ts +2 -1
  54. package/lib/release/workflow.ts +69 -20
  55. package/lib/sandbox/commands/create.ts +17 -21
  56. package/lib/sandbox/commands/prune.ts +53 -15
  57. package/lib/sandbox/commands/rm.ts +368 -103
  58. package/lib/sandbox/control/client.ts +152 -28
  59. package/lib/sandbox/control/executor.ts +163 -0
  60. package/lib/sandbox/control/lifecycle.ts +342 -0
  61. package/lib/sandbox/control/protocol.ts +96 -69
  62. package/lib/sandbox/control/server.ts +239 -110
  63. package/lib/sandbox/control/state.ts +196 -0
  64. package/lib/sandbox/index.ts +2 -2
  65. package/lib/sandbox/managed-fs.ts +17 -2
  66. package/lib/sandbox/mounts.ts +31 -13
  67. package/lib/sandbox/recovery.ts +301 -60
  68. package/lib/sandbox/workspace-view.ts +81 -11
  69. package/lib/sandbox/worktree-safety.ts +234 -0
  70. package/lib/server/process-control.ts +11 -0
  71. package/lib/task/artifact-lifecycle.ts +5 -4
  72. package/lib/task/codex-orchestration.ts +233 -0
  73. package/lib/task/commands/status.ts +2 -0
  74. package/lib/task/commands/validate.ts +282 -0
  75. package/lib/task/commit-finalization.ts +52 -9
  76. package/lib/task/delegation-receipts.ts +52 -3
  77. package/lib/task/events.ts +9 -4
  78. package/lib/task/finalization-artifacts.ts +2 -1
  79. package/lib/task/implementation-inputs.ts +1 -5
  80. package/lib/task/index.ts +8 -0
  81. package/lib/task/orchestration.ts +199 -4
  82. package/lib/task/verification-engine.ts +42 -8
  83. package/lib/task/verification.ts +2 -1
  84. package/package.json +1 -1
  85. package/templates/.agents/hooks/lifecycle-delegation.js +48 -12
  86. package/templates/.agents/rules/lifecycle-orchestration.en.md +1 -1
  87. package/templates/.agents/rules/lifecycle-orchestration.zh-CN.md +1 -1
  88. package/templates/.agents/rules/no-mid-flow-questions.en.md +1 -0
  89. package/templates/.agents/rules/no-mid-flow-questions.zh-CN.md +1 -0
  90. package/templates/.agents/skills/post-release/SKILL.en.md +32 -6
  91. package/templates/.agents/skills/post-release/SKILL.zh-CN.md +32 -6
  92. package/templates/.agents/skills/run-manual-validation/SKILL.en.md +42 -0
  93. package/templates/.agents/skills/run-manual-validation/SKILL.zh-CN.md +42 -0
  94. package/templates/.agents/skills/run-manual-validation/config/verify.en.json +14 -0
  95. package/templates/.agents/skills/run-manual-validation/config/verify.zh-CN.json +14 -0
  96. package/templates/.agents/skills/run-manual-validation/reference/report-template.en.md +44 -0
  97. package/templates/.agents/skills/run-manual-validation/reference/report-template.zh-CN.md +44 -0
  98. package/templates/.agents/skills/run-task/SKILL.en.md +2 -2
  99. package/templates/.agents/skills/run-task/SKILL.zh-CN.md +2 -2
  100. package/templates/.agents/skills/run-task/reference/host-validation.en.md +3 -3
  101. package/templates/.agents/skills/run-task/reference/host-validation.zh-CN.md +3 -3
  102. package/templates/.claude/commands/run-manual-validation.en.md +9 -0
  103. package/templates/.claude/commands/run-manual-validation.zh-CN.md +9 -0
  104. package/templates/.codex/hooks.json +8 -8
  105. package/templates/.opencode/commands/run-manual-validation.en.md +11 -0
  106. package/templates/.opencode/commands/run-manual-validation.zh-CN.md +11 -0
@@ -154,11 +154,47 @@ function inspectLocalReleaseFacts(cwd, version, run = command) {
154
154
  const localTagConflict = Boolean(tagCommit && !localTag && !localTagAncestor);
155
155
  const postSubject = `chore: prepare next dev iteration after v${version}`;
156
156
  const postLog = localTag || localTagAncestor
157
- ? git(cwd, ['log', '--format=%s', `${tag}..HEAD`], run) ?? ''
157
+ ? git(cwd, ['log', '--format=%H%x00%s', `${tag}..HEAD`], run) ?? ''
158
158
  : '';
159
- const postCommit = postLog.split('\n').some((subject) => subject === postSubject);
159
+ const postCommit = postLog.split('\n').map((line) => line.split('\0'))
160
+ .find(([, subject]) => subject === postSubject)?.[0] ?? null;
160
161
  return { localTag, localTagAncestor, localTagConflict, postCommit };
161
162
  }
163
+ function inspectPostReleaseFacts(cwd, commit, run = command) {
164
+ const inspected = inspectGitWorkflow(cwd);
165
+ const snapshot = inspected.snapshot;
166
+ const branch = snapshot?.branch ?? '';
167
+ const remoteLine = branch ? git(cwd, ['ls-remote', '--heads', 'origin', `refs/heads/${branch}`], run) : null;
168
+ const remoteHead = remoteLine?.split(/\s+/)[0] ?? null;
169
+ const packageJson = commit ? git(cwd, ['show', `${commit}:package.json`], run, false) : null;
170
+ let newVersion = null;
171
+ if (packageJson) {
172
+ try {
173
+ const version = JSON.parse(packageJson).version;
174
+ newVersion = typeof version === 'string' ? version : null;
175
+ }
176
+ catch {
177
+ newVersion = null;
178
+ }
179
+ }
180
+ const paths = commit ? git(cwd, ['diff-tree', '--root', '--no-commit-id', '--name-only', '-r', '-z', commit], run, false) : null;
181
+ const changedPaths = paths ? paths.split('\0').filter(Boolean)
182
+ .sort((left, right) => Buffer.from(left).compare(Buffer.from(right))) : [];
183
+ const digest = commit ? git(cwd, ['show', `${commit}:${DEMO_DIGEST_PATH}`], run) : null;
184
+ return {
185
+ commit,
186
+ isHead: Boolean(commit && snapshot?.head === commit),
187
+ published: Boolean(commit && remoteHead === commit),
188
+ branch,
189
+ upstream: snapshot?.upstream ?? null,
190
+ remoteHead,
191
+ newVersion,
192
+ changedPaths,
193
+ demoInputSha256: digest && /^[0-9a-f]{64}$/.test(digest) ? digest : null,
194
+ worktree: snapshot?.worktree ?? [],
195
+ staged: snapshot?.staged ?? []
196
+ };
197
+ }
162
198
  function releaseSmokeStatus(workflows, version, tagCommit) {
163
199
  const manualTitle = `Post-Release Smoke v${version}`;
164
200
  const targets = workflows.filter((run) => {
@@ -209,12 +245,15 @@ async function inspectFacts(cwd, version) {
209
245
  const workflows = platform.workflows;
210
246
  const smoke = releaseSmokeStatus(workflows, version, localTagTarget);
211
247
  return {
212
- ...local,
248
+ localTag: local.localTag,
249
+ localTagAncestor: local.localTagAncestor,
250
+ localTagConflict: local.localTagConflict,
213
251
  remoteBranch: Boolean(remoteBranch && remoteBranch.split(/\s+/)[0] === head), remoteTag: Boolean(remoteTag),
214
252
  githubRelease: platform.platform.type !== 'github'
215
253
  ? true
216
254
  : platform.status === 'blocked' ? null : Boolean(platform.release?.published),
217
- npm: npm.published, homebrew: homebrew.published, smoke
255
+ npm: npm.published, homebrew: homebrew.published, smoke,
256
+ post: inspectPostReleaseFacts(cwd, local.postCommit)
218
257
  };
219
258
  }
220
259
  function parsePorcelainPath(record) {
@@ -245,13 +284,21 @@ function updateReleaseMetadata(cwd, version) {
245
284
  fs.writeFileSync(filePath, content);
246
285
  }
247
286
  }
287
+ function optionValues(args, name) {
288
+ const values = [];
289
+ for (let index = 0; index < args.length; index += 1) {
290
+ if (args[index] === name)
291
+ values.push(args[index + 1] ?? '');
292
+ }
293
+ return values;
294
+ }
248
295
  async function releaseWorkflow(args = []) {
249
296
  const [action, rawVersion, ...rest] = args;
250
297
  const version = String(rawVersion || '').replace(/^v/, '');
251
298
  const cwdIndex = rest.indexOf('--cwd');
252
299
  const cwd = path.resolve(cwdIndex >= 0 && rest[cwdIndex + 1] ? rest[cwdIndex + 1] : process.cwd());
253
- if (!['inspect', 'prepare', 'publish', 'post'].includes(action || '') || !semver.valid(version)) {
254
- process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post <version>' } })}\n`);
300
+ if (!['inspect', 'prepare', 'publish', 'post-prepare', 'post-publish'].includes(action || '') || !semver.valid(version)) {
301
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, error: { code: 'RELEASE_INPUT_INVALID', message: 'Usage: release-workflow inspect|prepare|publish|post-prepare|post-publish <version>' } })}\n`);
255
302
  process.exitCode = 1;
256
303
  return;
257
304
  }
@@ -331,10 +378,56 @@ async function releaseWorkflow(args = []) {
331
378
  process.exitCode = result.status === 'failed' ? 1 : result.status === 'degraded' ? 2 : 0;
332
379
  return;
333
380
  }
381
+ if (action === 'post-publish') {
382
+ const expectedValues = optionValues(rest, '--expected-sha256');
383
+ if (expectedValues.length !== 1 || !/^sha256:[0-9a-f]{64}$/.test(expectedValues[0])) {
384
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_INPUT_INVALID', message: 'post-publish requires one --expected-sha256 sha256:<64 hex>' } })}\n`);
385
+ process.exitCode = 1;
386
+ return;
387
+ }
388
+ if (before.phase === 'complete') {
389
+ process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
390
+ return;
391
+ }
392
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
393
+ if (before.phase !== 'post-prepared' || !confirmation) {
394
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: confirmation ? 'RELEASE_PHASE_INVALID' : 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post-release facts cannot authorize publish' } })}\n`);
395
+ process.exitCode = 1;
396
+ return;
397
+ }
398
+ if (confirmation.sha256 !== expectedValues[0]) {
399
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: false, snapshot: before, error: { code: 'RELEASE_POST_SNAPSHOT_MISMATCH', message: 'Post-release confirmation snapshot changed' } })}\n`);
400
+ process.exitCode = 1;
401
+ return;
402
+ }
403
+ const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
404
+ if (!channelsComplete || before.facts.smoke !== 'success') {
405
+ process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
406
+ process.exitCode = before.facts.smoke === 'failed' ? 1 : 2;
407
+ return;
408
+ }
409
+ const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [before.facts.post.branch] });
410
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
411
+ if (snapshot.phase === 'complete') {
412
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: 'applied', changed: true, snapshot, error: null })}\n`);
413
+ return;
414
+ }
415
+ const blocked = pushed.status !== 'failed';
416
+ process.stdout.write(`${JSON.stringify({ ...pushed, status: blocked ? 'blocked' : 'failed', snapshot, error: pushed.error ?? { code: 'RELEASE_PROGRESS_PENDING', message: 'Post-release push is not visible at the expected remote head' } })}\n`);
417
+ process.exitCode = blocked ? 2 : 1;
418
+ return;
419
+ }
334
420
  if (before.phase === 'complete') {
335
421
  process.stdout.write(`${JSON.stringify({ status: 'no-op', changed: false, snapshot: before, error: null })}\n`);
336
422
  return;
337
423
  }
424
+ if (before.phase === 'post-prepared') {
425
+ const confirmation = 'postConfirmation' in before ? before.postConfirmation : null;
426
+ const status = confirmation ? 'no-op' : 'blocked';
427
+ process.stdout.write(`${JSON.stringify({ status, changed: false, snapshot: before, error: confirmation ? null : { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Post commit is not the clean current HEAD' } })}\n`);
428
+ process.exitCode = confirmation ? 0 : 2;
429
+ return;
430
+ }
338
431
  const channelsComplete = before.facts.githubRelease === true && before.facts.npm === true && before.facts.homebrew === true;
339
432
  if (!['published', 'post-pending'].includes(before.phase) || !channelsComplete || before.facts.smoke !== 'success') {
340
433
  process.stdout.write(`${JSON.stringify({ status: before.facts.smoke === 'failed' ? 'failed' : 'blocked', changed: false, snapshot: before, error: { code: 'RELEASE_CHANNELS_PENDING', message: 'Release channels or smoke workflow are not complete' } })}\n`);
@@ -373,10 +466,14 @@ async function releaseWorkflow(args = []) {
373
466
  process.exitCode = 1;
374
467
  return;
375
468
  }
376
- const branch = git(cwd, ['branch', '--show-current']) || '';
377
- const pushed = pushGitRefs({ cwd, remote: 'origin', refs: [branch] });
378
- process.stdout.write(`${JSON.stringify({ ...pushed, demo, snapshot: releaseSnapshot(version, await inspectFacts(cwd, version)) })}\n`);
379
- process.exitCode = pushed.status === 'failed' ? 1 : pushed.status === 'degraded' ? 2 : 0;
469
+ const snapshot = releaseSnapshot(version, await inspectFacts(cwd, version));
470
+ const confirmation = 'postConfirmation' in snapshot ? snapshot.postConfirmation : null;
471
+ if (snapshot.phase !== 'post-prepared' || !confirmation) {
472
+ process.stdout.write(`${JSON.stringify({ status: 'failed', changed: true, demo, snapshot, error: { code: 'RELEASE_POST_CONFIRMATION_UNAVAILABLE', message: 'Prepared post commit did not produce a confirmation snapshot' } })}\n`);
473
+ process.exitCode = 1;
474
+ return;
475
+ }
476
+ process.stdout.write(`${JSON.stringify({ status: 'applied', changed: true, demo, snapshot, error: null })}\n`);
380
477
  }
381
- export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
478
+ export { changedPaths, computeDemoInputDigest, inspectFacts, inspectLocalReleaseFacts, inspectPostReleaseFacts, inspectPostWorktree, releaseSmokeStatus, releaseWorkflow, runOptionalDemo };
382
479
  //# sourceMappingURL=release-workflow.js.map
@@ -1,24 +1,61 @@
1
- import { requestSandboxControl } from '../sandbox/control/client.js';
1
+ import { requestSandboxControl, SandboxControlClientError } from '../sandbox/control/client.js';
2
2
  import { serveSandboxControl } from '../sandbox/control/server.js';
3
- function sandboxControl(args) {
3
+ import { runSandboxControlExecutor } from '../sandbox/control/executor.js';
4
+ async function sandboxControl(args) {
4
5
  const [operation, ...rest] = args;
5
6
  if (operation === 'serve') {
6
7
  const manifestIndex = rest.indexOf('--manifest');
7
8
  const manifest = manifestIndex >= 0 ? rest[manifestIndex + 1] : undefined;
8
9
  if (!manifest)
9
10
  throw new Error('sandbox-control serve requires --manifest');
10
- serveSandboxControl(manifest);
11
+ const controller = new AbortController();
12
+ const abort = () => controller.abort();
13
+ process.once('SIGINT', abort);
14
+ process.once('SIGTERM', abort);
15
+ try {
16
+ await serveSandboxControl(manifest, controller.signal);
17
+ }
18
+ finally {
19
+ process.off('SIGINT', abort);
20
+ process.off('SIGTERM', abort);
21
+ }
22
+ return;
23
+ }
24
+ if (operation === 'execute') {
25
+ const requestIndex = rest.indexOf('--request');
26
+ const nonceIndex = rest.indexOf('--nonce');
27
+ const request = requestIndex >= 0 ? rest[requestIndex + 1] : undefined;
28
+ const nonce = nonceIndex >= 0 ? rest[nonceIndex + 1] : undefined;
29
+ if (!request || !nonce)
30
+ throw new Error('sandbox-control execute requires --request and --nonce');
31
+ await runSandboxControlExecutor(request, nonce);
11
32
  return;
12
33
  }
13
34
  if (operation === 'client') {
14
35
  const [family = '', ...commandArgs] = rest;
15
- const response = requestSandboxControl({ family, args: commandArgs });
36
+ let response;
37
+ try {
38
+ response = requestSandboxControl({ family, args: commandArgs });
39
+ }
40
+ catch (error) {
41
+ if (!(error instanceof SandboxControlClientError))
42
+ throw error;
43
+ process.stderr.write(`${error.detail.message}\n`);
44
+ process.exitCode = error.detail.retryable ? 75 : 1;
45
+ return;
46
+ }
16
47
  process.stdout.write(response.stdout);
17
48
  process.stderr.write(response.stderr);
18
- process.exitCode = response.exitCode;
49
+ if (response.phase === 'rejected') {
50
+ process.stderr.write(response.error?.message ?? response.stderr);
51
+ process.exitCode = response.error?.retryable ? 75 : 1;
52
+ }
53
+ else {
54
+ process.exitCode = response.exitCode ?? 1;
55
+ }
19
56
  return;
20
57
  }
21
- throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | client <family> [args...]');
58
+ throw new Error('Usage: agent-infra-internal sandbox-control serve --manifest <path> | execute --request <path> --nonce <nonce> | client <family> [args...]');
22
59
  }
23
60
  export { sandboxControl };
24
61
  //# sourceMappingURL=sandbox-control.js.map
@@ -1,13 +1,13 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- import { requestSandboxTaskCreate } from '../sandbox/control/client.js';
3
+ import { requestSandboxTaskCreate, SandboxControlClientError } from '../sandbox/control/client.js';
4
4
  import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.js';
5
5
  import { validateTaskCreateCandidate } from '../task/create.js';
6
6
  import { createTask } from '../task/create-service.js';
7
- function failed(code, message) {
7
+ function failed(code, message, retryable = false) {
8
8
  return {
9
- status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
10
- operations: [], warnings: [], error: { code, message, retryable: false }
9
+ status: retryable ? 'blocked' : 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
10
+ operations: [], warnings: [], error: { code, message, retryable }
11
11
  };
12
12
  }
13
13
  function output(result) {
@@ -31,6 +31,10 @@ function taskCreate(args) {
31
31
  const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
32
32
  if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
33
33
  const response = requestSandboxTaskCreate({ candidate });
34
+ if (response.phase === 'rejected') {
35
+ output(failed(response.error?.code ?? 'SANDBOX_CONTROL_REJECTED', response.error?.message ?? response.stderr, response.error?.retryable ?? false));
36
+ return;
37
+ }
34
38
  process.stdout.write(response.stdout);
35
39
  process.stderr.write(response.stderr);
36
40
  process.exitCode = response.exitCode;
@@ -39,6 +43,10 @@ function taskCreate(args) {
39
43
  output(createTask(candidate, { repoRoot: process.cwd() }));
40
44
  }
41
45
  catch (error) {
46
+ if (error instanceof SandboxControlClientError) {
47
+ output(failed(error.detail.code, error.detail.message, error.detail.retryable));
48
+ return;
49
+ }
42
50
  const message = error instanceof Error ? error.message : String(error);
43
51
  const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
44
52
  output(failed(code, message));
@@ -1,5 +1,6 @@
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, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } 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
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) {
@@ -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;
@@ -260,11 +261,11 @@ function taskOrchestration(args = []) {
260
261
  usageFailure(`intent 'prepare' requires '${missing}'`);
261
262
  return;
262
263
  }
263
- result = prepareOrchestrationDelegation(taskRef, {
264
+ result = await prepareCodexOrchestrationDelegation(taskRef, {
264
265
  client: values['--client'],
265
266
  requestedModel: values['--requested-model'],
266
267
  requestedReasoningEffort: values['--requested-reasoning-effort']
267
- }, coreOptions);
268
+ }, { orchestrationOptions: coreOptions });
268
269
  }
269
270
  else if (intent === 'hook-start') {
270
271
  const missing = requireValues([
@@ -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,6 +960,7 @@ 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,
@@ -982,6 +981,7 @@ export async function create(args) {
982
981
  shellConfigHostDir: hostShellConfig.hostDir,
983
982
  workspaceViewRoot: workspaceView.root,
984
983
  controlDir: control.channelDir,
984
+ controlStatusDir: control.statusDir,
985
985
  ...(target.workspace.mode === 'task-bound'
986
986
  ? {
987
987
  taskSource: assertSandboxTaskSource(effectiveConfig.repoRoot, target.workspace.taskId),
@@ -1052,7 +1052,11 @@ export async function create(args) {
1052
1052
  '-e',
1053
1053
  `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1054
1054
  '-e',
1055
+ `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1056
+ '-e',
1055
1057
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1058
+ '-e',
1059
+ 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1056
1060
  '--label',
1057
1061
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1058
1062
  ...coreVolumes,
@@ -1070,12 +1074,7 @@ export async function create(args) {
1070
1074
  '/workspace',
1071
1075
  effectiveConfig.imageName
1072
1076
  ]);
1073
- const broker = spawn(process.execPath, [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath], { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' });
1074
- broker.once('error', () => {
1075
- // Fresh readiness verification below remains the source of truth;
1076
- // consume detached spawn errors so they cannot crash the command.
1077
- });
1078
- broker.unref();
1077
+ await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1079
1078
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1080
1079
  }
1081
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
  }