@fitlab-ai/agent-infra 0.9.4 → 0.9.5

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 (56) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/dist/bin/internal-cli.js +10 -0
  4. package/dist/lib/agent-clients/adapter.js +2 -2
  5. package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
  6. package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
  7. package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
  8. package/dist/lib/agent-clients/adapters/codex.js +2 -2
  9. package/dist/lib/git/worktree-identity.js +64 -0
  10. package/dist/lib/internal/codex-lifecycle.js +202 -0
  11. package/dist/lib/internal/task-create.js +48 -0
  12. package/dist/lib/internal/task-orchestration.js +68 -29
  13. package/dist/lib/sandbox/commands/create.js +1 -0
  14. package/dist/lib/sandbox/control/client.js +29 -15
  15. package/dist/lib/sandbox/control/protocol.js +23 -4
  16. package/dist/lib/sandbox/control/server.js +53 -18
  17. package/dist/lib/sandbox/workspace-view.js +8 -2
  18. package/dist/lib/task/activity-log.js +32 -5
  19. package/dist/lib/task/commit-finalization.js +14 -3
  20. package/dist/lib/task/create-service.js +137 -0
  21. package/dist/lib/task/create.js +333 -0
  22. package/dist/lib/task/orchestration.js +151 -15
  23. package/dist/lib/task/review-fingerprint.js +4 -0
  24. package/dist/lib/task/workspace-snapshot.js +78 -3
  25. package/lib/agent-clients/adapter.ts +4 -4
  26. package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
  27. package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
  28. package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
  29. package/lib/agent-clients/adapters/codex.ts +2 -2
  30. package/lib/git/worktree-identity.ts +86 -0
  31. package/lib/internal/codex-lifecycle.ts +204 -0
  32. package/lib/internal/task-create.ts +49 -0
  33. package/lib/internal/task-orchestration.ts +54 -28
  34. package/lib/sandbox/commands/create.ts +1 -0
  35. package/lib/sandbox/control/client.ts +42 -18
  36. package/lib/sandbox/control/protocol.ts +43 -8
  37. package/lib/sandbox/control/server.ts +60 -22
  38. package/lib/sandbox/workspace-view.ts +9 -2
  39. package/lib/task/activity-log.ts +46 -7
  40. package/lib/task/commit-finalization.ts +23 -3
  41. package/lib/task/create-service.ts +171 -0
  42. package/lib/task/create.ts +372 -0
  43. package/lib/task/orchestration.ts +182 -18
  44. package/lib/task/review-fingerprint.ts +9 -0
  45. package/lib/task/workspace-snapshot.ts +98 -4
  46. package/package.json +7 -7
  47. package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
  48. package/templates/.agents/rules/task-management.en.md +1 -0
  49. package/templates/.agents/rules/task-management.zh-CN.md +1 -0
  50. package/templates/.agents/skills/commit/SKILL.en.md +3 -9
  51. package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
  52. package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
  53. package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
  54. package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
  55. package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
  56. package/templates/.codex/hooks.json +50 -0
@@ -1,7 +1,7 @@
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, prepareOrchestrationDelegation, recoverCommitIntent, routeOrchestration, sealMatchingOrchestrationDelegation, sealOrchestrationDelegation, startCommitAttempt, statusCommitIntent, statusOrchestration, terminateCommitAttempt } from '../task/orchestration.js';
3
3
  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';
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-start|commit-begin|commit-checkpoint|commit-complete|commit-recover|commit-abort|commit-terminate|commit-status> [options]\n';
5
5
  function usageFailure(message) {
6
6
  process.stdout.write(`${JSON.stringify({
7
7
  status: 'failed', changed: false,
@@ -22,7 +22,7 @@ function taskOrchestration(args = []) {
22
22
  const [taskRef, intent] = args;
23
23
  if (![
24
24
  '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'
25
+ 'commit-start', 'commit-begin', 'commit-checkpoint', 'commit-complete', 'commit-recover', 'commit-abort', 'commit-terminate', 'commit-status'
26
26
  ].includes(intent)) {
27
27
  usageFailure(`unknown intent '${intent}'`);
28
28
  return;
@@ -48,7 +48,8 @@ function taskOrchestration(args = []) {
48
48
  '--native-agent', '--child-id', '--spawn-mode', '--actual-model', '--actual-reasoning-effort',
49
49
  '--model-fallback-reason', '--reasoning-effort-fallback-reason',
50
50
  '--exit-code', '--after-fingerprint', '--changed-paths', '--code', '--message', '--recoverable', '--agent',
51
- '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
51
+ '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head',
52
+ '--attempt', '--git-worktree-root'
52
53
  ].includes(flag)) {
53
54
  usageFailure(`unknown option '${flag}'`);
54
55
  return;
@@ -66,12 +67,14 @@ function taskOrchestration(args = []) {
66
67
  values[flag] = value;
67
68
  }
68
69
  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': []
70
+ 'commit-start': ['--agent', '--git-worktree-root'],
71
+ 'commit-begin': ['--agent', '--baseline-head', '--attempt', '--orchestrated', '--git-worktree-root'],
72
+ 'commit-checkpoint': ['--token', '--kind', '--head', '--remote', '--ref', '--git-worktree-root'],
73
+ 'commit-complete': ['--token', '--agent', '--git-worktree-root'],
74
+ 'commit-recover': ['--agent', '--git-worktree-root'],
75
+ 'commit-abort': ['--token', '--expected-head', '--git-worktree-root'],
76
+ 'commit-terminate': ['--attempt', '--agent', '--code', '--git-worktree-root'],
77
+ 'commit-status': ['--git-worktree-root']
75
78
  };
76
79
  if (intent in commitIntentFlags) {
77
80
  const allowed = commitIntentFlags[intent];
@@ -83,7 +86,7 @@ function taskOrchestration(args = []) {
83
86
  }
84
87
  else {
85
88
  const commitOnly = [...seen].find((flag) => [
86
- '--orchestrated', '--baseline-head', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
89
+ '--orchestrated', '--baseline-head', '--attempt', '--token', '--kind', '--head', '--remote', '--ref', '--expected-head'
87
90
  ].includes(flag));
88
91
  if (commitOnly) {
89
92
  usageFailure(`intent '${intent}' does not accept '${commitOnly}'`);
@@ -91,13 +94,29 @@ function taskOrchestration(args = []) {
91
94
  }
92
95
  }
93
96
  const requireValues = (flags) => flags.find((flag) => values[flag] === undefined);
97
+ const coreOptions = values['--git-worktree-root'] === undefined
98
+ ? {}
99
+ : { gitWorktreeRoot: values['--git-worktree-root'] };
94
100
  if (values['--client'] !== undefined && !isAgentClientId(values['--client'])) {
95
101
  usageFailure(`unknown client '${values['--client']}'`);
96
102
  return;
97
103
  }
98
104
  let result;
99
- if (intent === 'commit-begin') {
100
- const missing = requireValues(['--agent', '--baseline-head']);
105
+ if (intent === 'commit-start') {
106
+ const missing = requireValues(['--agent']);
107
+ if (missing) {
108
+ usageFailure(`intent 'commit-start' requires '${missing}'`);
109
+ return;
110
+ }
111
+ const agent = normalizeAgentToken(values['--agent']);
112
+ if (!agent) {
113
+ usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
114
+ return;
115
+ }
116
+ result = startCommitAttempt(taskRef, { agent }, coreOptions);
117
+ }
118
+ else if (intent === 'commit-begin') {
119
+ const missing = requireValues(['--agent', '--baseline-head', '--attempt']);
101
120
  if (missing) {
102
121
  usageFailure(`intent 'commit-begin' requires '${missing}'`);
103
122
  return;
@@ -108,8 +127,8 @@ function taskOrchestration(args = []) {
108
127
  return;
109
128
  }
110
129
  result = beginCommitIntent(taskRef, {
111
- agent, orchestrated, baselineHead: values['--baseline-head']
112
- });
130
+ agent, orchestrated, baselineHead: values['--baseline-head'], attempt: values['--attempt']
131
+ }, coreOptions);
113
132
  }
114
133
  else if (intent === 'commit-checkpoint') {
115
134
  const missing = requireValues(['--token', '--kind', '--head']);
@@ -135,7 +154,7 @@ function taskOrchestration(args = []) {
135
154
  result = checkpointCommitIntent(taskRef, {
136
155
  token: values['--token'], kind: values['--kind'],
137
156
  head: values['--head'], remote: values['--remote'], ref: values['--ref']
138
- });
157
+ }, coreOptions);
139
158
  }
140
159
  else if (intent === 'commit-complete') {
141
160
  const missing = requireValues(['--token', '--agent']);
@@ -148,7 +167,7 @@ function taskOrchestration(args = []) {
148
167
  usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
149
168
  return;
150
169
  }
151
- result = completeCommitIntent(taskRef, { token: values['--token'], agent });
170
+ result = completeCommitIntent(taskRef, { token: values['--token'], agent }, coreOptions);
152
171
  }
153
172
  else if (intent === 'commit-recover') {
154
173
  const missing = requireValues(['--agent']);
@@ -161,7 +180,7 @@ function taskOrchestration(args = []) {
161
180
  usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
162
181
  return;
163
182
  }
164
- result = recoverCommitIntent(taskRef, { agent });
183
+ result = recoverCommitIntent(taskRef, { agent }, coreOptions);
165
184
  }
166
185
  else if (intent === 'commit-abort') {
167
186
  const missing = requireValues(['--token', '--expected-head']);
@@ -169,10 +188,29 @@ function taskOrchestration(args = []) {
169
188
  usageFailure(`intent 'commit-abort' requires '${missing}'`);
170
189
  return;
171
190
  }
172
- result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] });
191
+ result = abortCommitIntent(taskRef, { token: values['--token'], expectedHead: values['--expected-head'] }, coreOptions);
192
+ }
193
+ else if (intent === 'commit-terminate') {
194
+ const missing = requireValues(['--attempt', '--agent', '--code']);
195
+ if (missing) {
196
+ usageFailure(`intent 'commit-terminate' requires '${missing}'`);
197
+ return;
198
+ }
199
+ const agent = normalizeAgentToken(values['--agent']);
200
+ if (!agent) {
201
+ usageFailure(`invalid --agent '${values['--agent']}': ${AGENT_USAGE_HINT}`);
202
+ return;
203
+ }
204
+ if (!/^[A-Z][A-Z0-9_]{2,63}$/.test(values['--code'])) {
205
+ usageFailure('--code must be a stable uppercase identifier');
206
+ return;
207
+ }
208
+ result = terminateCommitAttempt(taskRef, {
209
+ attempt: values['--attempt'], agent, code: values['--code']
210
+ }, coreOptions);
173
211
  }
174
212
  else if (intent === 'commit-status') {
175
- result = statusCommitIntent(taskRef);
213
+ result = statusCommitIntent(taskRef, coreOptions);
176
214
  }
177
215
  else if (intent === 'begin-or-resume') {
178
216
  const missing = requireValues(['--client']);
@@ -195,6 +233,7 @@ function taskOrchestration(args = []) {
195
233
  return;
196
234
  }
197
235
  result = beginOrResumeOrchestration(taskRef, {
236
+ ...coreOptions,
198
237
  maxSteps,
199
238
  client: values['--client'],
200
239
  modelPolicy: hasAnyPolicy ? {
@@ -210,10 +249,10 @@ function taskOrchestration(args = []) {
210
249
  });
211
250
  }
212
251
  else if (intent === 'route') {
213
- result = routeOrchestration(taskRef);
252
+ result = routeOrchestration(taskRef, coreOptions);
214
253
  }
215
254
  else if (intent === 'status') {
216
- result = statusOrchestration(taskRef);
255
+ result = statusOrchestration(taskRef, coreOptions);
217
256
  }
218
257
  else if (intent === 'prepare') {
219
258
  const missing = requireValues(['--client']);
@@ -225,7 +264,7 @@ function taskOrchestration(args = []) {
225
264
  client: values['--client'],
226
265
  requestedModel: values['--requested-model'],
227
266
  requestedReasoningEffort: values['--requested-reasoning-effort']
228
- });
267
+ }, coreOptions);
229
268
  }
230
269
  else if (intent === 'hook-start') {
231
270
  const missing = requireValues([
@@ -245,8 +284,8 @@ function taskOrchestration(args = []) {
245
284
  reasoningEffortFallbackReason: values['--reasoning-effort-fallback-reason']
246
285
  };
247
286
  result = taskRef === 'auto'
248
- ? activateMatchingOrchestrationDelegation(values['--client'], event)
249
- : activateOrchestrationDelegation(taskRef, event);
287
+ ? activateMatchingOrchestrationDelegation(values['--client'], event, coreOptions)
288
+ : activateOrchestrationDelegation(taskRef, event, coreOptions);
250
289
  }
251
290
  else if (intent === 'hook-stop') {
252
291
  if (taskRef === 'auto') {
@@ -257,7 +296,7 @@ function taskOrchestration(args = []) {
257
296
  }
258
297
  result = sealMatchingOrchestrationDelegation(values['--client'], {
259
298
  nativeAgent: values['--native-agent'], childId: values['--child-id']
260
- });
299
+ }, coreOptions);
261
300
  }
262
301
  else {
263
302
  const missing = requireValues(['--child-id', '--exit-code', '--after-fingerprint']);
@@ -274,11 +313,11 @@ function taskOrchestration(args = []) {
274
313
  childId: values['--child-id'], exitCode,
275
314
  afterFingerprint: values['--after-fingerprint'],
276
315
  changedPaths: values['--changed-paths'] ? values['--changed-paths'].split(',').filter(Boolean) : []
277
- });
316
+ }, coreOptions);
278
317
  }
279
318
  }
280
319
  else if (intent === 'advance') {
281
- result = advanceOrchestration(taskRef);
320
+ result = advanceOrchestration(taskRef, coreOptions);
282
321
  }
283
322
  else {
284
323
  const missing = requireValues(['--code', '--message', '--recoverable']);
@@ -290,7 +329,7 @@ function taskOrchestration(args = []) {
290
329
  usageFailure('--recoverable must be true or false');
291
330
  return;
292
331
  }
293
- result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true');
332
+ result = pauseOrchestration(taskRef, values['--code'], values['--message'], values['--recoverable'] === 'true', coreOptions);
294
333
  }
295
334
  process.stdout.write(`${JSON.stringify(result)}\n`);
296
335
  if (result.status === 'failed')
@@ -965,6 +965,7 @@ export async function create(args) {
965
965
  const control = materializeSandboxControl({
966
966
  base: effectiveConfig.controlBase,
967
967
  repoRoot: effectiveConfig.repoRoot,
968
+ worktreeRoot: worktree,
968
969
  project: effectiveConfig.project,
969
970
  container,
970
971
  branch,
@@ -5,25 +5,17 @@ import { SANDBOX_CONTROL_MAX_BYTES, isSandboxControlFamily } from './protocol.js
5
5
  function sleep(milliseconds) {
6
6
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
7
7
  }
8
- export function requestSandboxControl(params) {
9
- if (!isSandboxControlFamily(params.family)) {
10
- throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
11
- }
8
+ function exchangeSandboxControl(request, params) {
12
9
  const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
13
- const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
14
- if (!token)
15
- throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
16
- const requestsDir = path.join(channelDir, 'requests');
17
- const responsesDir = path.join(channelDir, 'responses');
18
- const id = randomUUID();
19
- const request = { version: 1, id, token, family: params.family, args: params.args };
20
10
  const encoded = `${JSON.stringify(request)}\n`;
21
11
  if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
22
12
  throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
23
13
  }
24
- const temporary = path.join(requestsDir, `.${id}.tmp`);
25
- const requestPath = path.join(requestsDir, `${id}.json`);
26
- const responsePath = path.join(responsesDir, `${id}.json`);
14
+ const requestsDir = path.join(channelDir, 'requests');
15
+ const responsesDir = path.join(channelDir, 'responses');
16
+ const temporary = path.join(requestsDir, `.${request.id}.tmp`);
17
+ const requestPath = path.join(requestsDir, `${request.id}.json`);
18
+ const responsePath = path.join(responsesDir, `${request.id}.json`);
27
19
  fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
28
20
  fs.renameSync(temporary, requestPath);
29
21
  const deadline = Date.now() + (params.timeoutMs ?? 30_000);
@@ -32,7 +24,7 @@ export function requestSandboxControl(params) {
32
24
  const raw = fs.readFileSync(responsePath, 'utf8');
33
25
  fs.unlinkSync(responsePath);
34
26
  const response = JSON.parse(raw);
35
- if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
27
+ if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
36
28
  throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
37
29
  }
38
30
  return response;
@@ -41,4 +33,26 @@ export function requestSandboxControl(params) {
41
33
  }
42
34
  throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
43
35
  }
36
+ export function requestSandboxControl(params) {
37
+ if (!isSandboxControlFamily(params.family)) {
38
+ throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
39
+ }
40
+ if (params.family === 'task-create')
41
+ throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
42
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
43
+ if (!token)
44
+ throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
45
+ const id = randomUUID();
46
+ const request = { version: 1, id, token, family: params.family, args: params.args };
47
+ return exchangeSandboxControl(request, params);
48
+ }
49
+ export function requestSandboxTaskCreate(params) {
50
+ const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
51
+ if (!token)
52
+ throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
53
+ const request = {
54
+ version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
55
+ };
56
+ return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
57
+ }
44
58
  //# sourceMappingURL=client.js.map
@@ -1,5 +1,6 @@
1
+ import { validateTaskCreateCandidate } from '../../task/create.js';
1
2
  export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
2
- export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration'];
3
+ export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'];
3
4
  export function isSandboxControlFamily(value) {
4
5
  return SANDBOX_CONTROL_FAMILIES.includes(value);
5
6
  }
@@ -13,7 +14,21 @@ export function validateSandboxControlRequest(value, manifest) {
13
14
  || !/^[a-f0-9-]{16,64}$/.test(request.id)
14
15
  || request.token !== manifest.token
15
16
  || typeof request.family !== 'string'
16
- || !isSandboxControlFamily(request.family)
17
+ || !isSandboxControlFamily(request.family)) {
18
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
19
+ }
20
+ if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
21
+ throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
22
+ }
23
+ if (request.family === 'task-create') {
24
+ const keys = Object.keys(request).sort().join(',');
25
+ if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
26
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
27
+ }
28
+ return { ...request, candidate: validateTaskCreateCandidate(request.candidate) };
29
+ }
30
+ const keys = Object.keys(request).sort().join(',');
31
+ if (keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
17
32
  || !Array.isArray(request.args)
18
33
  || !request.args.every((arg) => typeof arg === 'string')) {
19
34
  throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
@@ -21,12 +36,16 @@ export function validateSandboxControlRequest(value, manifest) {
21
36
  if (manifest.mode !== 'task-bound' || !manifest.taskId) {
22
37
  throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
23
38
  }
24
- if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
25
- throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
39
+ if (request.family === 'task-orchestration'
40
+ && request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))) {
41
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
26
42
  }
27
43
  return request;
28
44
  }
29
45
  export function bindSandboxControlTask(request, taskId) {
46
+ if (request.family === 'task-create') {
47
+ throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
48
+ }
30
49
  if (request.args.length === 0) {
31
50
  throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
32
51
  }
@@ -2,14 +2,19 @@ import { spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { getProcessStartTime } from '../../server/process-state.js';
5
+ import { createTask } from '../../task/create-service.js';
6
+ import { assertGitWorktreeBinding } from '../../git/worktree-identity.js';
5
7
  import { bindSandboxControlTask, validateSandboxControlRequest } from './protocol.js';
6
8
  function readManifest(manifestPath) {
7
9
  const stat = fs.lstatSync(manifestPath);
8
10
  if (!stat.isFile() || stat.isSymbolicLink())
9
11
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
10
12
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
11
- if (manifest.version !== 1
12
- || typeof manifest.repoRoot !== 'string'
13
+ if (manifest.version !== 2) {
14
+ throw new Error('SANDBOX_CONTROL_MANIFEST_VERSION_INVALID: expected version 2; recreate the sandbox');
15
+ }
16
+ if (typeof manifest.repoRoot !== 'string'
17
+ || typeof manifest.worktreeRoot !== 'string'
13
18
  || typeof manifest.channelDir !== 'string'
14
19
  || typeof manifest.token !== 'string')
15
20
  throw new Error('SANDBOX_CONTROL_MANIFEST_INVALID');
@@ -31,8 +36,23 @@ function writeResponse(directory, response) {
31
36
  fs.writeFileSync(temporary, `${JSON.stringify(response)}\n`, { mode: 0o600, flag: 'wx' });
32
37
  fs.renameSync(temporary, path.join(directory, `${response.id}.json`));
33
38
  }
39
+ export function sandboxControlSafeEnv(env = process.env) {
40
+ return Object.fromEntries(Object.entries(env).filter(([key]) => !key.toUpperCase().startsWith('AGENT_INFRA_CONTROL_')));
41
+ }
42
+ function consumeRequest(directory, id) {
43
+ try {
44
+ fs.writeFileSync(path.join(directory, id), '', { flag: 'wx', mode: 0o600 });
45
+ }
46
+ catch (error) {
47
+ if (error.code === 'EEXIST') {
48
+ throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
49
+ }
50
+ throw error;
51
+ }
52
+ }
34
53
  export function serveSandboxControl(manifestPath, signal = new AbortController().signal) {
35
54
  const manifest = readManifest(manifestPath);
55
+ assertGitWorktreeBinding(manifest.repoRoot, manifest.worktreeRoot, manifest.branch);
36
56
  const brokerPath = path.join(path.dirname(manifestPath), 'broker.json');
37
57
  const startTime = getProcessStartTime(process.pid);
38
58
  if (!startTime)
@@ -45,16 +65,19 @@ export function serveSandboxControl(manifestPath, signal = new AbortController()
45
65
  })}\n`, { mode: 0o600, flag: 'wx' });
46
66
  const requestsDir = path.join(manifest.channelDir, 'requests');
47
67
  const responsesDir = path.join(manifest.channelDir, 'responses');
68
+ const consumedDir = path.join(path.dirname(manifestPath), 'consumed');
48
69
  assertRealDirectory(manifest.channelDir);
49
70
  fs.mkdirSync(requestsDir, { recursive: true, mode: 0o700 });
50
71
  fs.mkdirSync(responsesDir, { recursive: true, mode: 0o700 });
72
+ fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
51
73
  assertRealDirectory(requestsDir, manifest.channelDir);
52
74
  assertRealDirectory(responsesDir, manifest.channelDir);
53
- const consumed = new Set();
75
+ assertRealDirectory(consumedDir, path.dirname(manifestPath));
54
76
  while (!signal.aborted) {
55
77
  const current = readManifest(manifestPath);
56
78
  if (current.token !== manifest.token)
57
79
  return;
80
+ assertGitWorktreeBinding(current.repoRoot, current.worktreeRoot, current.branch);
58
81
  assertRealDirectory(requestsDir, manifest.channelDir);
59
82
  assertRealDirectory(responsesDir, manifest.channelDir);
60
83
  for (const name of fs.readdirSync(requestsDir).sort()) {
@@ -69,21 +92,33 @@ export function serveSandboxControl(manifestPath, signal = new AbortController()
69
92
  throw new Error('SANDBOX_CONTROL_REQUEST_INVALID');
70
93
  const request = validateSandboxControlRequest(JSON.parse(fs.readFileSync(requestPath, 'utf8')), manifest);
71
94
  id = request.id;
72
- if (consumed.has(id))
73
- throw new Error('SANDBOX_CONTROL_REQUEST_REPLAYED');
74
- consumed.add(id);
75
- const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...bindSandboxControlTask(request, manifest.taskId)], {
76
- cwd: manifest.repoRoot,
77
- encoding: 'utf8',
78
- env: Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.startsWith('AGENT_INFRA_CONTROL_')))
79
- });
80
- response = {
81
- version: 1,
82
- id,
83
- exitCode: result.status ?? 1,
84
- stdout: result.stdout ?? '',
85
- stderr: result.stderr ?? ''
86
- };
95
+ consumeRequest(consumedDir, id);
96
+ if (request.family === 'task-create') {
97
+ const result = createTask(request.candidate, { repoRoot: manifest.repoRoot });
98
+ response = {
99
+ version: 1, id,
100
+ exitCode: result.status === 'blocked' ? 2 : result.status === 'failed' ? 1 : 0,
101
+ stdout: `${JSON.stringify(result)}\n`, stderr: ''
102
+ };
103
+ }
104
+ else {
105
+ const boundArgs = bindSandboxControlTask(request, manifest.taskId);
106
+ if (request.family === 'task-orchestration') {
107
+ boundArgs.push('--git-worktree-root', manifest.worktreeRoot);
108
+ }
109
+ const result = spawnSync(process.execPath, ['--experimental-strip-types', '--no-warnings', process.argv[1], request.family, ...boundArgs], {
110
+ cwd: manifest.repoRoot,
111
+ encoding: 'utf8',
112
+ env: sandboxControlSafeEnv()
113
+ });
114
+ response = {
115
+ version: 1,
116
+ id,
117
+ exitCode: result.status ?? 1,
118
+ stdout: result.stdout ?? '',
119
+ stderr: result.stderr ?? ''
120
+ };
121
+ }
87
122
  }
88
123
  catch (error) {
89
124
  response = {
@@ -79,6 +79,10 @@ export function materializeSandboxWorkspaceView(params) {
79
79
  export function materializeSandboxControl(params) {
80
80
  const { root, channelDir, manifestPath } = sandboxControlPaths(params);
81
81
  assertSafeDirectory(root, path.resolve(params.base));
82
+ const consumedDir = path.join(root, 'consumed');
83
+ assertSafeDirectory(consumedDir, root);
84
+ fs.rmSync(consumedDir, { recursive: true, force: true });
85
+ fs.mkdirSync(consumedDir, { recursive: true, mode: 0o700 });
82
86
  for (const queue of ['requests', 'responses']) {
83
87
  const directory = path.join(channelDir, queue);
84
88
  assertSafeDirectory(directory, root);
@@ -86,9 +90,11 @@ export function materializeSandboxControl(params) {
86
90
  fs.mkdirSync(directory, { recursive: true, mode: 0o700 });
87
91
  }
88
92
  const token = randomBytes(32).toString('hex');
93
+ const repoRoot = fs.realpathSync.native(params.repoRoot);
89
94
  const manifest = {
90
- version: 1,
91
- repoRoot: fs.realpathSync.native(params.repoRoot),
95
+ version: 2,
96
+ repoRoot,
97
+ worktreeRoot: fs.realpathSync.native(params.worktreeRoot),
92
98
  project: params.project,
93
99
  container: params.container,
94
100
  branch: params.branch,
@@ -2,6 +2,9 @@ const HEADING_RE = /^##\s+(活动日志|Activity Log)\s*$/;
2
2
  const NEXT_H2_RE = /^##\s/;
3
3
  const ENTRY_RE = /^- (\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{2}:\d{2}) — \*\*(.+?)\*\* by (.+?) — (.*)$/;
4
4
  const STARTED_SUFFIX_RE = /\s*\[started\]\s*$/;
5
+ const ABORTED_SUFFIX_RE = /\s*\[aborted\]\s*$/;
6
+ const ATTEMPT_RE = /(?:^|;\s*)attempt=([A-Za-z0-9-]{8,64})(?:;|$)/;
7
+ const COMMIT_STARTED_RE = /^started; attempt=([A-Za-z0-9-]{8,64}); baseline=([a-f0-9]{40,64}); agent=([a-z-]+)$/;
5
8
  function parseActivityLog(content) {
6
9
  const section = locateActivityLog(content);
7
10
  return section ? { sectionFound: true, entries: section.entries } : { sectionFound: false, entries: [] };
@@ -34,28 +37,52 @@ function appendActivityEntry(section, entry) {
34
37
  const line = `- ${entry.time} — **${entry.step}** by ${entry.agent} — ${entry.note}`;
35
38
  return section.body ? `${section.body}\n${line}` : line;
36
39
  }
40
+ function activityAttempt(note) {
41
+ return ATTEMPT_RE.exec(note)?.[1] ?? null;
42
+ }
43
+ function parseCommitAttemptStarted(note) {
44
+ const match = COMMIT_STARTED_RE.exec(note);
45
+ return match ? { attempt: match[1], baseline: match[2], agent: match[3] } : null;
46
+ }
47
+ function commitAttemptStartedNote(attempt) {
48
+ return `started; attempt=${attempt.attempt}; baseline=${attempt.baseline}; agent=${attempt.agent}`;
49
+ }
37
50
  function pairEntries(entries) {
38
51
  const rows = [];
39
52
  const open = new Map();
40
53
  for (const entry of entries) {
41
54
  const started = STARTED_SUFFIX_RE.test(entry.step);
42
- const base = entry.step.replace(STARTED_SUFFIX_RE, '');
55
+ const aborted = ABORTED_SUFFIX_RE.test(entry.step);
56
+ const base = entry.step.replace(STARTED_SUFFIX_RE, '').replace(ABORTED_SUFFIX_RE, '');
43
57
  if (started) {
44
- const row = { step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note };
58
+ const attempt = activityAttempt(entry.note);
59
+ const row = {
60
+ step: base, agent: entry.agent, started: entry.time, done: '', note: entry.note,
61
+ ...(attempt ? { attempt } : {})
62
+ };
45
63
  rows.push(row);
46
64
  const queue = open.get(base) ?? [];
47
65
  queue.push(row);
48
66
  open.set(base, queue);
49
67
  }
50
68
  else {
51
- const pending = open.get(base)?.shift();
69
+ const queue = open.get(base) ?? [];
70
+ const terminalAttempt = aborted ? activityAttempt(entry.note) : null;
71
+ const pendingIndex = terminalAttempt === null
72
+ ? 0
73
+ : queue.findIndex((row) => row.attempt === terminalAttempt);
74
+ const pending = pendingIndex >= 0 ? queue.splice(pendingIndex, 1)[0] : undefined;
52
75
  if (pending)
53
76
  Object.assign(pending, { done: entry.time, agent: entry.agent, note: entry.note });
54
77
  else
55
- rows.push({ step: base, agent: entry.agent, started: '', done: entry.time, note: entry.note });
78
+ rows.push({
79
+ step: aborted ? entry.step : base,
80
+ agent: entry.agent, started: '', done: entry.time, note: entry.note,
81
+ ...(terminalAttempt ? { attempt: terminalAttempt } : {})
82
+ });
56
83
  }
57
84
  }
58
85
  return rows;
59
86
  }
60
- export { parseActivityLog, locateActivityLog, appendActivityEntry, pairEntries };
87
+ export { activityAttempt, appendActivityEntry, commitAttemptStartedNote, locateActivityLog, pairEntries, parseActivityLog, parseCommitAttemptStarted };
61
88
  //# sourceMappingURL=activity-log.js.map
@@ -1,11 +1,11 @@
1
1
  import { execFileSync, spawnSync } from 'node:child_process';
2
2
  import fs from 'node:fs';
3
3
  import path from 'node:path';
4
- import { locateActivityLog, appendActivityEntry, pairEntries } from './activity-log.js';
4
+ import { locateActivityLog, appendActivityEntry, pairEntries, parseCommitAttemptStarted } from './activity-log.js';
5
5
  import { commitIntentPath, digest, readCommitIntent } from './commit-intent.js';
6
6
  import { parseTypedTaskFrontmatter } from './frontmatter.js';
7
7
  import { parseReviewSummary } from './review-artifacts.js';
8
- import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact } from './review-fingerprint.js';
8
+ import { extractReviewBaseline, extractReviewedSnapshotTree, findAuthoritativeReviewCodeArtifact, parseReviewedGitTree } from './review-fingerprint.js';
9
9
  const SHA_RE = /^[a-f0-9]{40,64}$/;
10
10
  function git(repoRoot, args) {
11
11
  return execFileSync('git', args, {
@@ -38,6 +38,7 @@ function outcome(disposition, overrides = {}) {
38
38
  needsLog: false,
39
39
  intent: null,
40
40
  intentDigest: null,
41
+ attempt: null,
41
42
  ...overrides
42
43
  };
43
44
  }
@@ -63,6 +64,16 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
63
64
  const openRows = commitRows.filter((row) => row.started !== '' && row.done === '');
64
65
  const file = commitIntentPath(taskDir);
65
66
  if (!fs.existsSync(file)) {
67
+ if (openRows.length === 1) {
68
+ const attempt = parseCommitAttemptStarted(openRows[0].note);
69
+ if (attempt && attempt.baseline === currentHead) {
70
+ return outcome('retryable-start', {
71
+ message: 'a structured Commit attempt can be safely reused',
72
+ currentHead,
73
+ attempt
74
+ });
75
+ }
76
+ }
66
77
  if (openRows.length > 0) {
67
78
  return outcome('orphaned-start', {
68
79
  code: 'COMMIT_FINALIZATION_EVIDENCE_MISSING',
@@ -132,7 +143,7 @@ function inspectCommitFinalization(taskDir, repoRoot, taskId) {
132
143
  const summary = parseReviewSummary(reviewContent);
133
144
  const reviewBaseline = extractReviewBaseline(reviewContent);
134
145
  const reviewTree = extractReviewedSnapshotTree(reviewContent);
135
- if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !SHA_RE.test(reviewTree)) {
146
+ if (!summary.ok || summary.summary.verdict !== 'Approved' || !commitExists(repoRoot, reviewBaseline) || !parseReviewedGitTree(reviewTree)) {
136
147
  return invalid('authoritative review-code evidence is not a valid approved snapshot', currentHead, 'COMMIT_FINALIZATION_EVIDENCE_INVALID', shared);
137
148
  }
138
149
  const committedTree = git(repoRoot, ['rev-parse', `${intent.committedHead}^{tree}`]);