@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
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+
4
+ import { requestSandboxTaskCreate, SandboxControlClientError } from '../sandbox/control/client.ts';
5
+ import { SANDBOX_CONTROL_MAX_BYTES } from '../sandbox/control/protocol.ts';
6
+ import { validateTaskCreateCandidate } from '../task/create.ts';
7
+ import { createTask, type TaskCreateResult } from '../task/create-service.ts';
8
+
9
+ function failed(code: string, message: string, retryable = false): TaskCreateResult {
10
+ return {
11
+ status: retryable ? 'blocked' : 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
12
+ operations: [], warnings: [], error: { code, message, retryable }
13
+ };
14
+ }
15
+
16
+ function output(result: TaskCreateResult): void {
17
+ process.stdout.write(`${JSON.stringify(result)}\n`);
18
+ process.exitCode = result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0;
19
+ }
20
+
21
+ function taskCreate(args: string[]): void {
22
+ const inputIndex = args.indexOf('--input');
23
+ const input = inputIndex >= 0 ? args[inputIndex + 1] : undefined;
24
+ if (!input || args.length !== 2 || inputIndex !== 0) {
25
+ output(failed('TASK_CREATE_PAYLOAD_INVALID', 'Usage: agent-infra-internal task-create --input <candidate.json>'));
26
+ return;
27
+ }
28
+ try {
29
+ const inputPath = path.resolve(input);
30
+ const stat = fs.lstatSync(inputPath);
31
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error('TASK_CREATE_INPUT_INVALID: input must be a regular file');
32
+ if (stat.size > SANDBOX_CONTROL_MAX_BYTES) throw new Error('TASK_CREATE_INPUT_TOO_LARGE: input exceeds the control limit');
33
+ const candidate = validateTaskCreateCandidate(JSON.parse(fs.readFileSync(inputPath, 'utf8')));
34
+ if (process.env.AGENT_INFRA_CONTROL_TOKEN) {
35
+ const response = requestSandboxTaskCreate({ candidate });
36
+ if (response.phase === 'rejected') {
37
+ output(failed(
38
+ response.error?.code ?? 'SANDBOX_CONTROL_REJECTED',
39
+ response.error?.message ?? response.stderr,
40
+ response.error?.retryable ?? false
41
+ ));
42
+ return;
43
+ }
44
+ process.stdout.write(response.stdout);
45
+ process.stderr.write(response.stderr);
46
+ process.exitCode = response.exitCode;
47
+ return;
48
+ }
49
+ output(createTask(candidate, { repoRoot: process.cwd() }));
50
+ } catch (error) {
51
+ if (error instanceof SandboxControlClientError) {
52
+ output(failed(error.detail.code, error.detail.message, error.detail.retryable));
53
+ return;
54
+ }
55
+ const message = error instanceof Error ? error.message : String(error);
56
+ const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
57
+ output(failed(code, message));
58
+ }
59
+ }
60
+
61
+ export { taskCreate };
@@ -9,18 +9,20 @@ import {
9
9
  checkpointCommitIntent,
10
10
  completeCommitIntent,
11
11
  pauseOrchestration,
12
- prepareOrchestrationDelegation,
13
12
  recoverCommitIntent,
14
13
  routeOrchestration,
15
14
  sealMatchingOrchestrationDelegation,
16
15
  sealOrchestrationDelegation,
16
+ startCommitAttempt,
17
17
  statusCommitIntent,
18
- statusOrchestration
18
+ statusOrchestration,
19
+ terminateCommitAttempt
19
20
  } from '../task/orchestration.ts';
21
+ import { prepareCodexOrchestrationDelegation } from '../task/codex-orchestration.ts';
20
22
  import { isAgentClientId } from '../agent-clients/types.ts';
21
23
  import type { AgentClientId } from '../agent-clients/types.ts';
22
24
 
23
- 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';
25
+ 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';
24
26
 
25
27
  function usageFailure(message: string): void {
26
28
  process.stdout.write(`${JSON.stringify({
@@ -31,7 +33,7 @@ function usageFailure(message: string): void {
31
33
  process.exitCode = 2;
32
34
  }
33
35
 
34
- function taskOrchestration(args: string[] = []): void {
36
+ async function taskOrchestration(args: string[] = []): Promise<void> {
35
37
  if (args[0] === '--help' || args[0] === '-h') {
36
38
  process.stdout.write(USAGE);
37
39
  return;
@@ -43,7 +45,7 @@ function taskOrchestration(args: string[] = []): void {
43
45
  const [taskRef, intent] = args;
44
46
  if (![
45
47
  'begin-or-resume', 'route', 'prepare', 'hook-start', 'hook-stop', 'advance', 'pause', 'status',
46
- 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-status'
48
+ 'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
47
49
  ].includes(intent!)) {
48
50
  usageFailure(`unknown intent '${intent}'`);
49
51
  return;
@@ -67,6 +69,7 @@ function taskOrchestration(args: string[] = []): void {
67
69
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
68
70
  '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
69
71
  '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
72
+ , '--attempt', '--git-worktree-root'
70
73
  ].includes(flag)) {
71
74
  usageFailure(`unknown option '${flag}'`);
72
75
  return;
@@ -84,12 +87,14 @@ function taskOrchestration(args: string[] = []): void {
84
87
  values[flag] = value;
85
88
  }
86
89
  const commitIntentFlags: Record<string, readonly string[]> = {
87
- 'commit-begin': ['--agent', '--baseline-head', '--orchestrated'],
88
- 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref'],
89
- 'commit-complete': ['--token', '--agent'],
90
- 'commit-recover': ['--agent'],
91
- 'commit-abort': ['--token', '--expected-head'],
92
- 'commit-status': []
90
+ 'commit-start': ['--agent', '--git-worktree-root'],
91
+ 'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
92
+ 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
93
+ 'commit-complete': ['--token', '--agent', '--git-worktree-root'],
94
+ 'commit-recover': ['--agent', '--git-worktree-root'],
95
+ 'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
96
+ 'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
97
+ 'commit-status': ['--git-worktree-root']
93
98
  };
94
99
  if (intent! in commitIntentFlags) {
95
100
  const allowed = commitIntentFlags[intent!]!;
@@ -97,24 +102,33 @@ function taskOrchestration(args: string[] = []): void {
97
102
  if (invalid) { usageFailure(`intent '${intent}' does not accept '${invalid}'`); return; }
98
103
  } else {
99
104
  const commitOnly = [...seen].find((flag) => [
100
- '--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
105
+ '--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
101
106
  ].includes(flag));
102
107
  if (commitOnly) { usageFailure(`intent '${intent}' does not accept '${commitOnly}'`); return; }
103
108
  }
104
109
  const requireValues = (flags: string[]) => flags.find((flag) => values[flag] === undefined);
110
+ const coreOptions = values['--git-worktree-root'] === undefined
111
+ ? {}
112
+ : { gitWorktreeRoot: values['--git-worktree-root'] };
105
113
  if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
106
114
  usageFailure(`unknown client '${values['--client']}'`);
107
115
  return;
108
116
  }
109
117
  let result;
110
- if (intent === 'commit-begin') {
111
- const missing = requireValues(['--agent', '--baseline-head']);
118
+ if (intent === 'commit-start') {
119
+ const missing = requireValues(['--agent']);
120
+ if (missing) { usageFailure(`intent 'commit-start' requires '${missing}'`); return; }
121
+ const agent = normalizeAgentToken(values['--agent']!);
122
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
123
+ result = startCommitAttempt(taskRef!, { agent }, coreOptions);
124
+ } else if (intent === 'commit-begin') {
125
+ const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
112
126
  if (missing) { usageFailure(`intent 'commit-begin' requires '${missing}'`); return; }
113
127
  const agent = normalizeAgentToken(values['--agent']!);
114
128
  if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
115
129
  result = beginCommitIntent(taskRef!, {
116
- agent, orchestrated, baselineHead: values['--baseline-head']!
117
- });
130
+ agent, orchestrated, baselineHead: values['--baseline-head']!, attempt: values['--attempt']!
131
+ }, coreOptions);
118
132
  } else if (intent === 'commit-checkpoint') {
119
133
  const missing = requireValues(['--token', '--kind', '--head']);
120
134
  if (missing) { usageFailure(`intent 'commit-checkpoint' requires '${missing}'`); return; }
@@ -130,25 +144,36 @@ function taskOrchestration(args: string[] = []): void {
130
144
  result = checkpointCommitIntent(taskRef!, {
131
145
  token: values['--token']!, kind: values['--kind'] as 'committed' | 'pushed',
132
146
  head: values['--head']!, remote: values['--remote'], ref: values['--ref']
133
- });
147
+ }, coreOptions);
134
148
  } else if (intent === 'commit-complete') {
135
149
  const missing = requireValues(['--token', '--agent']);
136
150
  if (missing) { usageFailure(`intent 'commit-complete' requires '${missing}'`); return; }
137
151
  const agent = normalizeAgentToken(values['--agent']!);
138
152
  if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
139
- result = completeCommitIntent(taskRef!, { token: values['--token']!, agent });
153
+ result = completeCommitIntent(taskRef!, { token: values['--token']!, agent }, coreOptions);
140
154
  } else if (intent === 'commit-recover') {
141
155
  const missing = requireValues(['--agent']);
142
156
  if (missing) { usageFailure(`intent 'commit-recover' requires '${missing}'`); return; }
143
157
  const agent = normalizeAgentToken(values['--agent']!);
144
158
  if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
145
- result = recoverCommitIntent(taskRef!, { agent });
159
+ result = recoverCommitIntent(taskRef!, { agent }, coreOptions);
146
160
  } else if (intent === 'commit-abort') {
147
161
  const missing = requireValues(['--token', '--expected-head']);
148
162
  if (missing) { usageFailure(`intent 'commit-abort' requires '${missing}'`); return; }
149
- result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! });
163
+ result = abortCommitIntent(taskRef!, { token: values['--token']!, expectedHead: values['--expected-head']! }, coreOptions);
164
+ } else if (intent === 'commit-terminate') {
165
+ const missing = requireValues(['--attempt', '--agent', '--code']);
166
+ if (missing) { usageFailure(`intent 'commit-terminate' requires '${missing}'`); return; }
167
+ const agent = normalizeAgentToken(values['--agent']!);
168
+ if (!agent) { usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`); return; }
169
+ if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code']!)) {
170
+ usageFailure('--code must be a stable uppercase identifier'); return;
171
+ }
172
+ result = terminateCommitAttempt(taskRef!, {
173
+ attempt: values['--attempt']!, agent, code: values['--code']!
174
+ }, coreOptions);
150
175
  } else if (intent === 'commit-status') {
151
- result = statusCommitIntent(taskRef!);
176
+ result = statusCommitIntent(taskRef!, coreOptions);
152
177
  } else if (intent === 'begin-or-resume') {
153
178
  const missing = requireValues(['--client']);
154
179
  if (missing) { usageFailure(`intent 'begin-or-resume' requires '${missing}'`); return; }
@@ -166,6 +191,7 @@ function taskOrchestration(args: string[] = []): void {
166
191
  return;
167
192
  }
168
193
  result = beginOrResumeOrchestration(taskRef!, {
194
+ ...coreOptions,
169
195
  maxSteps,
170
196
  client: values['--client'] as AgentClientId,
171
197
  modelPolicy: hasAnyPolicy ? {
@@ -180,17 +206,17 @@ function taskOrchestration(args: string[] = []): void {
180
206
  } : undefined
181
207
  });
182
208
  } else if (intent === 'route') {
183
- result = routeOrchestration(taskRef!);
209
+ result = routeOrchestration(taskRef!, coreOptions);
184
210
  } else if (intent === 'status') {
185
- result = statusOrchestration(taskRef!);
211
+ result = statusOrchestration(taskRef!, coreOptions);
186
212
  } else if (intent === 'prepare') {
187
213
  const missing = requireValues(['--client']);
188
214
  if (missing) { usageFailure(`intent 'prepare' requires '${missing}'`); return; }
189
- result = prepareOrchestrationDelegation(taskRef!, {
215
+ result = await prepareCodexOrchestrationDelegation(taskRef!, {
190
216
  client: values['--client'] as AgentClientId,
191
217
  requestedModel: values['--requested-model'],
192
218
  requestedReasoningEffort: values['--requested-reasoning-effort']
193
- });
219
+ }, { orchestrationOptions: coreOptions });
194
220
  } else if (intent === 'hook-start') {
195
221
  const missing = requireValues([
196
222
  ...(taskRef === 'auto' ? ['--client'] : []),
@@ -206,15 +232,15 @@ function taskOrchestration(args: string[] = []): void {
206
232
  reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
207
233
  };
208
234
  result = taskRef === 'auto'
209
- ? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event)
210
- : activateOrchestrationDelegation(taskRef!, event);
235
+ ? activateMatchingOrchestrationDelegation(values['--client'] as AgentClientId, event, coreOptions)
236
+ : activateOrchestrationDelegation(taskRef!, event, coreOptions);
211
237
  } else if (intent === 'hook-stop') {
212
238
  if (taskRef === 'auto') {
213
239
  const missing = requireValues(['--client', '--native-agent', '--child-id']);
214
240
  if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
215
241
  result = sealMatchingOrchestrationDelegation(values['--client'] as AgentClientId, {
216
242
  nativeAgent: values['--native-agent']!, childId: values['--child-id']!
217
- });
243
+ }, coreOptions);
218
244
  } else {
219
245
  const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
220
246
  if (missing) { usageFailure(`intent 'hook-stop' requires '${missing}'`); return; }
@@ -224,15 +250,15 @@ function taskOrchestration(args: string[] = []): void {
224
250
  childId: values['--child-id']!, exitCode,
225
251
  afterFingerprint: values['--after-fingerprint']!,
226
252
  changedPaths: values['--changed-paths'] ? values['--changed-paths']!.split(',').filter(Boolean) : []
227
- });
253
+ }, coreOptions);
228
254
  }
229
255
  } else if (intent === 'advance') {
230
- result = advanceOrchestration(taskRef!);
256
+ result = advanceOrchestration(taskRef!, coreOptions);
231
257
  } else {
232
258
  const missing = requireValues(['--code', '--message', '--recoverable']);
233
259
  if (missing) { usageFailure(`intent 'pause' requires '${missing}'`); return; }
234
260
  if (!['true', 'false'].includes(values['--recoverable']!)) { usageFailure('--recoverable must be true or false'); return; }
235
- result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true');
261
+ result = pauseOrchestration(taskRef!, values['--code']!, values['--message']!, values['--recoverable'] === 'true', coreOptions);
236
262
  }
237
263
  process.stdout.write(`${JSON.stringify(result)}\n`);
238
264
  if (result.status === 'failed') process.exitCode = 1;
@@ -39,6 +39,7 @@ const ARTIFACT_TITLES: Record<string, string> = {
39
39
  code: '实现报告',
40
40
  'review-code': '代码审查',
41
41
  'manual-validation': '人工验证报告',
42
+ 'validation-run': '验证运行证据',
42
43
  'pr-review': 'PR 审查报告'
43
44
  };
44
45
 
@@ -75,7 +76,7 @@ function renderTaskComment(content: string, taskId: string, agent: string): stri
75
76
 
76
77
  function artifactIdentity(artifact: string): { stem: string; title: string } {
77
78
  const stem = path.basename(artifact, '.md');
78
- const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|pr-review)(?:-r(\d+))?$/);
79
+ const match = stem.match(/^(analysis|review-analysis|plan|review-plan|code|review-code|manual-validation|validation-run|pr-review)(?:-r(\d+))?$/);
79
80
  if (!match) throw new Error(`unsupported artifact '${artifact}'`);
80
81
  const base = ARTIFACT_TITLES[match[1]!]!;
81
82
  const round = match[2] ? Number(match[2]) : 1;
@@ -1,32 +1,81 @@
1
- type ReleasePhase = 'unprepared' | 'prepared' | 'partially-published' | 'published' | 'post-pending' | 'complete';
2
- type ReleaseFacts = { localTag: boolean; localTagAncestor?: boolean; localTagConflict?: boolean; remoteBranch: boolean; remoteTag: boolean; githubRelease: boolean | null; npm: boolean | null; homebrew: boolean | null; smoke: 'success' | 'pending' | 'failed' | null; postCommit: boolean };
1
+ import crypto from 'node:crypto';
2
+
3
+ type ReleasePhase = 'unprepared' | 'prepared' | 'partially-published' | 'published' | 'post-pending' | 'post-prepared' | 'complete';
4
+ type PostReleaseFacts = {
5
+ commit: string | null;
6
+ isHead: boolean;
7
+ published: boolean;
8
+ branch: string;
9
+ upstream: string | null;
10
+ remoteHead: string | null;
11
+ newVersion: string | null;
12
+ changedPaths: string[];
13
+ demoInputSha256: string | null;
14
+ worktree: string[];
15
+ staged: string[];
16
+ };
17
+ type ReleaseFacts = {
18
+ localTag: boolean;
19
+ localTagAncestor?: boolean;
20
+ localTagConflict?: boolean;
21
+ remoteBranch: boolean;
22
+ remoteTag: boolean;
23
+ githubRelease: boolean | null;
24
+ npm: boolean | null;
25
+ homebrew: boolean | null;
26
+ smoke: 'success' | 'pending' | 'failed' | null;
27
+ post: PostReleaseFacts;
28
+ };
29
+ type PostConfirmation = {
30
+ version: string;
31
+ commit: string;
32
+ branch: string;
33
+ upstream: string | null;
34
+ remoteHead: string | null;
35
+ newVersion: string | null;
36
+ changedPaths: string[];
37
+ demoInputSha256: string | null;
38
+ worktree: string[];
39
+ staged: string[];
40
+ sha256: string;
41
+ };
42
+
43
+ function byteSort(values: readonly string[]): string[] {
44
+ return [...values].sort((left, right) => Buffer.from(left).compare(Buffer.from(right)));
45
+ }
3
46
 
4
47
  function deriveReleasePhase(facts: ReleaseFacts): ReleasePhase {
5
48
  if (!facts.localTag && !facts.localTagAncestor) return 'unprepared';
49
+ if (facts.post.commit) return facts.post.published ? 'complete' : 'post-prepared';
6
50
  if (!facts.remoteBranch && !facts.remoteTag) return 'prepared';
7
51
  if (!facts.remoteBranch || !facts.remoteTag) return 'partially-published';
8
52
  if (facts.githubRelease !== true || facts.npm !== true || facts.homebrew !== true) return 'published';
9
- if (facts.smoke !== 'success' || !facts.postCommit) return 'post-pending';
10
- return 'complete';
53
+ return 'post-pending';
11
54
  }
12
55
 
13
- function releaseSnapshot(version: string, facts: ReleaseFacts) {
14
- return { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
56
+ function createPostConfirmation(version: string, post: PostReleaseFacts): PostConfirmation | null {
57
+ if (!post.commit || !post.isHead || post.worktree.length || post.staged.length) return null;
58
+ const canonical = {
59
+ version,
60
+ commit: post.commit,
61
+ branch: post.branch,
62
+ upstream: post.upstream,
63
+ remoteHead: post.remoteHead,
64
+ newVersion: post.newVersion,
65
+ changedPaths: byteSort(post.changedPaths),
66
+ demoInputSha256: post.demoInputSha256,
67
+ worktree: byteSort(post.worktree),
68
+ staged: byteSort(post.staged)
69
+ };
70
+ const sha256 = `sha256:${crypto.createHash('sha256').update(JSON.stringify(canonical), 'utf8').digest('hex')}`;
71
+ return { ...canonical, sha256 };
15
72
  }
16
73
 
17
- async function runReleaseAction(input: { version: string; action: 'inspect' | 'prepare' | 'publish' | 'post'; inspect: () => Promise<ReleaseFacts>; prepare?: () => Promise<void>; publish?: () => Promise<void>; post?: () => Promise<void> }) {
18
- const before = releaseSnapshot(input.version, await input.inspect());
19
- if (input.action === 'inspect') return { status: 'no-op' as const, changed: false, before, snapshot: before, error: null };
20
- if (before.facts.localTagConflict) return { status: 'failed' as const, changed: false, before, snapshot: before, error: { code: 'GIT_TAG_CONFLICT', message: `Tag ${before.tag} is not reachable from HEAD` } };
21
- const allowed = input.action === 'prepare' ? before.phase === 'unprepared'
22
- : input.action === 'publish' ? before.facts.localTag && ['prepared', 'partially-published'].includes(before.phase)
23
- : ['published', 'post-pending'].includes(before.phase);
24
- if (!allowed) return { status: before.phase === 'complete' ? 'no-op' as const : 'failed' as const, changed: false, before, snapshot: before, error: before.phase === 'complete' ? null : { code: 'RELEASE_PHASE_INVALID', message: `Cannot ${input.action} from ${before.phase}` } };
25
- await input[input.action]?.();
26
- const snapshot = releaseSnapshot(input.version, await input.inspect());
27
- const progressed = snapshot.phase !== before.phase;
28
- return { status: progressed ? 'applied' as const : 'blocked' as const, changed: progressed, before, snapshot, error: progressed ? null : { code: 'RELEASE_PROGRESS_PENDING', message: `${input.action} has not reached the next observable phase` } };
74
+ function releaseSnapshot(version: string, facts: ReleaseFacts) {
75
+ const snapshot = { version, tag: `v${version.replace(/^v/, '')}`, phase: deriveReleasePhase(facts), facts };
76
+ const postConfirmation = createPostConfirmation(version, facts.post);
77
+ return postConfirmation ? { ...snapshot, postConfirmation } : snapshot;
29
78
  }
30
79
 
31
- export { deriveReleasePhase, releaseSnapshot, runReleaseAction };
32
- export type { ReleaseFacts, ReleasePhase };
80
+ export { createPostConfirmation, deriveReleasePhase, releaseSnapshot };
81
+ export type { PostConfirmation, PostReleaseFacts, ReleaseFacts, ReleasePhase };
@@ -2,10 +2,9 @@ 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 type { ExecFileSyncOptions, StdioOptions } from 'node:child_process';
7
7
  import { parseArgs } from 'node:util';
8
- import { fileURLToPath } from 'node:url';
9
8
  import * as p from '@clack/prompts';
10
9
  import pc from 'picocolors';
11
10
  import { listAgentClientAdapters } from '../../agent-clients/registry.ts';
@@ -67,14 +66,16 @@ import type { SandboxTool, TmpfsSeedEntry } from '../tools.ts';
67
66
  import {
68
67
  assertFreshSandboxReady,
69
68
  hydrateTmpfsSeedEntries,
70
- prepareTmpfsMounts
69
+ prepareTmpfsMounts,
70
+ startSandboxControlBroker
71
71
  } from '../recovery.ts';
72
72
  import { hostJoin, toEnginePath, volumeArg } from '../engines/wsl2-paths.ts';
73
73
  import { sandboxCoreBindMounts } from '../mounts.ts';
74
74
  import {
75
75
  assertSandboxTaskSource,
76
76
  materializeSandboxControl,
77
- materializeSandboxWorkspaceView
77
+ materializeSandboxWorkspaceView,
78
+ prepareSandboxWorkspaceMountTargets
78
79
  } from '../workspace-view.ts';
79
80
  import { clipboardHostDir, CONTAINER_CLIPBOARD_MOUNT } from '../clipboard/paths.ts';
80
81
  import { validateSelinuxDisableEnv } from '../engines/selinux.ts';
@@ -157,12 +158,6 @@ type HostShellConfig = {
157
158
  mounts: Array<{ hostPath: string; containerPath: string }>;
158
159
  };
159
160
 
160
- function internalCliPath(): string {
161
- const directory = path.dirname(fileURLToPath(import.meta.url));
162
- const extension = path.extname(fileURLToPath(import.meta.url));
163
- return path.resolve(directory, '..', '..', '..', 'bin', `internal-cli${extension}`);
164
- }
165
-
166
161
  function resolveToolDirs(config: Pick<SandboxCreateConfig, 'project'>, tools: SandboxTool[], branch: string): ResolvedTool[] {
167
162
  return tools.map((tool) => {
168
163
  const candidates = toolConfigDirCandidates(tool, config.project, branch);
@@ -1254,9 +1249,13 @@ export async function create(args: string[]): Promise<void> {
1254
1249
  const requestedDescription = target.workspace.mode === 'task-bound'
1255
1250
  ? `task-bound:${target.workspace.taskId}`
1256
1251
  : target.workspace.mode;
1252
+ const recoveryTarget = target.workspace.mode === 'task-bound'
1253
+ ? target.workspace.taskId
1254
+ : branch;
1257
1255
  throw new Error(
1258
1256
  `SANDBOX_WORKSPACE_IDENTITY_CONFLICT: container '${name}' is ${existingDescription}, `
1259
- + `but this request is ${requestedDescription}. Run 'ai sandbox rm ${branch}' and retry.`
1257
+ + `but this request is ${requestedDescription}. `
1258
+ + `Run 'ai sandbox start --recreate ${recoveryTarget}' on the host, then retry.`
1260
1259
  );
1261
1260
  }
1262
1261
  }
@@ -1342,9 +1341,11 @@ export async function create(args: string[]): Promise<void> {
1342
1341
  container,
1343
1342
  identity: target.workspace
1344
1343
  });
1344
+ prepareSandboxWorkspaceMountTargets(worktree);
1345
1345
  const control = materializeSandboxControl({
1346
1346
  base: effectiveConfig.controlBase,
1347
1347
  repoRoot: effectiveConfig.repoRoot,
1348
+ worktreeRoot: worktree,
1348
1349
  project: effectiveConfig.project,
1349
1350
  container,
1350
1351
  branch,
@@ -1361,6 +1362,7 @@ export async function create(args: string[]): Promise<void> {
1361
1362
  shellConfigHostDir: hostShellConfig.hostDir,
1362
1363
  workspaceViewRoot: workspaceView.root,
1363
1364
  controlDir: control.channelDir,
1365
+ controlStatusDir: control.statusDir,
1364
1366
  ...(target.workspace.mode === 'task-bound'
1365
1367
  ? {
1366
1368
  taskSource: assertSandboxTaskSource(
@@ -1444,7 +1446,11 @@ export async function create(args: string[]): Promise<void> {
1444
1446
  '-e',
1445
1447
  `AGENT_INFRA_CONTROL_TOKEN=${control.token}`,
1446
1448
  '-e',
1449
+ `AGENT_INFRA_CONTROL_GENERATION=${control.generation}`,
1450
+ '-e',
1447
1451
  'AGENT_INFRA_CONTROL_DIR=/run/agent-infra/control',
1452
+ '-e',
1453
+ 'AGENT_INFRA_CONTROL_STATUS_DIR=/run/agent-infra/control-status',
1448
1454
  '--label',
1449
1455
  `${sandboxRuntimeCapabilityLabel(effectiveConfig)}=${capabilityPlan.runtimeSignature}`,
1450
1456
  ...coreVolumes,
@@ -1466,16 +1472,7 @@ export async function create(args: string[]): Promise<void> {
1466
1472
  '/workspace',
1467
1473
  effectiveConfig.imageName
1468
1474
  ]);
1469
- const broker = spawn(
1470
- process.execPath,
1471
- [internalCliPath(), 'sandbox-control', 'serve', '--manifest', control.manifestPath],
1472
- { cwd: effectiveConfig.repoRoot, detached: true, stdio: 'ignore' }
1473
- );
1474
- broker.once('error', () => {
1475
- // Fresh readiness verification below remains the source of truth;
1476
- // consume detached spawn errors so they cannot crash the command.
1477
- });
1478
- broker.unref();
1475
+ await startSandboxControlBroker(effectiveConfig.repoRoot, control.manifestPath);
1479
1476
  createdTmpfsSeedPlan = tmpfsSeedPlan;
1480
1477
  } finally {
1481
1478
  envFile.cleanup();
@@ -6,16 +6,27 @@ import pc from 'picocolors';
6
6
  import { loadConfig } from '../config.ts';
7
7
  import type { SandboxConfig } from '../config.ts';
8
8
  import { safeNameCandidates, sandboxBranchLabel, sandboxLabel } from '../constants.ts';
9
- import { detectEngine } from '../engine.ts';
9
+ import { detectEngine, ENGINES } from '../engine.ts';
10
10
  import { hostJoin } from '../engines/wsl2-paths.ts';
11
11
  import { removeManagedDir, removeWorktreeDir } from '../managed-fs.ts';
12
12
  import { parseLabels } from './ls.ts';
13
13
  import { runEngine, runSafe } from '../shell.ts';
14
14
  import { createSandboxCapabilityPlan } from '../agent-client-reconciler.ts';
15
15
  import type { SandboxTool } from '../tools.ts';
16
+ import { createCleanPermit, formatWorktreeSnapshot, inspectWorktrees } from '../worktree-safety.ts';
17
+ import type { WorktreeRemovalPermit } from '../worktree-safety.ts';
16
18
 
17
19
  const USAGE = `Usage: ai sandbox prune [--dry-run]`;
18
20
 
21
+ type PruneDependencies = {
22
+ config?: SandboxConfig;
23
+ tools?: SandboxTool[];
24
+ engine?: string;
25
+ labelsOutput?: string;
26
+ confirm?: typeof p.confirm;
27
+ isCancel?: typeof p.isCancel;
28
+ };
29
+
19
30
  type OrphanKind = 'shell' | 'worktree' | 'share' | 'tool';
20
31
 
21
32
  export type OrphanGroup = {
@@ -107,12 +118,19 @@ export function collectOrphanGroups(
107
118
  return groups;
108
119
  }
109
120
 
110
- export function removeOrphanGroups(config: SandboxConfig, groups: OrphanGroup[]): boolean {
121
+ export function removeOrphanGroups(
122
+ config: SandboxConfig,
123
+ groups: OrphanGroup[],
124
+ permits: ReadonlyMap<string, WorktreeRemovalPermit>,
125
+ allowRegisteredPathFallback = false
126
+ ): boolean {
111
127
  let removedWorktrees = false;
112
128
  for (const group of groups) {
113
129
  for (const dir of group.dirs) {
114
130
  if (group.kind === 'worktree') {
115
- removeWorktreeDir(config.repoRoot, group.base, dir);
131
+ const permit = permits.get(path.resolve(dir));
132
+ if (!permit) throw new Error(`Missing worktree removal permit: ${dir}`);
133
+ removeWorktreeDir(config.repoRoot, group.base, dir, permit, { allowRegisteredPathFallback });
116
134
  removedWorktrees = true;
117
135
  } else {
118
136
  removeManagedDir(group.base, dir);
@@ -142,7 +160,7 @@ function writeGroups(groups: OrphanGroup[]): void {
142
160
  }
143
161
  }
144
162
 
145
- export async function prune(args: string[]): Promise<void> {
163
+ export async function prune(args: string[], dependencies: PruneDependencies = {}): Promise<void> {
146
164
  const { values } = parseArgs({
147
165
  args,
148
166
  strict: true,
@@ -157,9 +175,9 @@ export async function prune(args: string[]): Promise<void> {
157
175
  return;
158
176
  }
159
177
 
160
- const config = loadConfig();
161
- const tools = [...createSandboxCapabilityPlan(config).cleanupInventory];
162
- const engine = detectEngine(config);
178
+ const config = dependencies.config ?? loadConfig();
179
+ const tools = dependencies.tools ?? [...createSandboxCapabilityPlan(config).cleanupInventory];
180
+ const engine = dependencies.engine ?? detectEngine(config);
163
181
  const psArgs = [
164
182
  'ps',
165
183
  '-a',
@@ -168,14 +186,25 @@ export async function prune(args: string[]): Promise<void> {
168
186
  '--format',
169
187
  '{{.Labels}}'
170
188
  ];
171
- let labelsOutput: string;
172
- try {
173
- labelsOutput = runEngine(engine, 'docker', psArgs);
174
- } catch {
175
- throw new Error('Unable to determine active sandbox branches: docker ps failed');
189
+ let labelsOutput = dependencies.labelsOutput;
190
+ if (labelsOutput === undefined) {
191
+ try {
192
+ labelsOutput = runEngine(engine, 'docker', psArgs);
193
+ } catch {
194
+ throw new Error('Unable to determine active sandbox branches: docker ps failed');
195
+ }
176
196
  }
197
+ const confirm = dependencies.confirm ?? p.confirm;
198
+ const isCancel = dependencies.isCancel ?? p.isCancel;
177
199
  const groups = collectOrphanGroups(config, tools, activeBranchesFromLabels(config, labelsOutput));
178
200
  const count = orphanCount(groups);
201
+ const worktrees = groups.filter((group) => group.kind === 'worktree').flatMap((group) => group.dirs);
202
+ const inspections = inspectWorktrees(worktrees);
203
+ const blockers = inspections.filter((inspection) => inspection.status !== 'clean');
204
+ const permits = new Map<string, WorktreeRemovalPermit>();
205
+ for (const inspection of inspections) {
206
+ if (inspection.status === 'clean') permits.set(inspection.snapshot.worktree, createCleanPermit(inspection.snapshot));
207
+ }
179
208
 
180
209
  p.intro(pc.cyan(`Pruning orphaned sandbox state for ${config.project}`));
181
210
 
@@ -186,23 +215,32 @@ export async function prune(args: string[]): Promise<void> {
186
215
  }
187
216
 
188
217
  writeGroups(groups);
218
+ for (const blocker of blockers) {
219
+ p.log.error(blocker.status === 'failed'
220
+ ? `${JSON.stringify(blocker.worktree)}: ${blocker.message}`
221
+ : formatWorktreeSnapshot(blocker.snapshot));
222
+ }
189
223
 
190
224
  if (values['dry-run']) {
191
225
  p.outro(pc.green('Dry run complete'));
192
226
  return;
193
227
  }
194
228
 
195
- const shouldRemove = await p.confirm({
229
+ if (blockers.length > 0) {
230
+ throw new Error('Refusing to prune because worktree preflight found blocker(s); no orphan state was deleted');
231
+ }
232
+
233
+ const shouldRemove = await confirm({
196
234
  message: `Remove ${count} orphaned sandbox state dirs?`,
197
235
  initialValue: true
198
236
  });
199
237
 
200
- if (p.isCancel(shouldRemove) || !shouldRemove) {
238
+ if (isCancel(shouldRemove) || !shouldRemove) {
201
239
  p.outro('Cancelled');
202
240
  return;
203
241
  }
204
242
 
205
- const removedWorktrees = removeOrphanGroups(config, groups);
243
+ const removedWorktrees = removeOrphanGroups(config, groups, permits, engine === ENGINES.WSL2);
206
244
  if (removedWorktrees) {
207
245
  runSafe('git', ['-C', config.repoRoot, 'worktree', 'prune']);
208
246
  }