@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.
- package/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/dist/bin/internal-cli.js +10 -0
- package/dist/lib/agent-clients/adapter.js +2 -2
- package/dist/lib/agent-clients/adapters/codex-lifecycle/app-server.js +382 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/evidence.js +163 -0
- package/dist/lib/agent-clients/adapters/codex-lifecycle/store.js +211 -0
- package/dist/lib/agent-clients/adapters/codex.js +2 -2
- package/dist/lib/git/worktree-identity.js +64 -0
- package/dist/lib/internal/codex-lifecycle.js +202 -0
- package/dist/lib/internal/task-create.js +48 -0
- package/dist/lib/internal/task-orchestration.js +68 -29
- package/dist/lib/sandbox/commands/create.js +1 -0
- package/dist/lib/sandbox/control/client.js +29 -15
- package/dist/lib/sandbox/control/protocol.js +23 -4
- package/dist/lib/sandbox/control/server.js +53 -18
- package/dist/lib/sandbox/workspace-view.js +8 -2
- package/dist/lib/task/activity-log.js +32 -5
- package/dist/lib/task/commit-finalization.js +14 -3
- package/dist/lib/task/create-service.js +137 -0
- package/dist/lib/task/create.js +333 -0
- package/dist/lib/task/orchestration.js +151 -15
- package/dist/lib/task/review-fingerprint.js +4 -0
- package/dist/lib/task/workspace-snapshot.js +78 -3
- package/lib/agent-clients/adapter.ts +4 -4
- package/lib/agent-clients/adapters/codex-lifecycle/app-server.ts +442 -0
- package/lib/agent-clients/adapters/codex-lifecycle/evidence.ts +307 -0
- package/lib/agent-clients/adapters/codex-lifecycle/store.ts +250 -0
- package/lib/agent-clients/adapters/codex.ts +2 -2
- package/lib/git/worktree-identity.ts +86 -0
- package/lib/internal/codex-lifecycle.ts +204 -0
- package/lib/internal/task-create.ts +49 -0
- package/lib/internal/task-orchestration.ts +54 -28
- package/lib/sandbox/commands/create.ts +1 -0
- package/lib/sandbox/control/client.ts +42 -18
- package/lib/sandbox/control/protocol.ts +43 -8
- package/lib/sandbox/control/server.ts +60 -22
- package/lib/sandbox/workspace-view.ts +9 -2
- package/lib/task/activity-log.ts +46 -7
- package/lib/task/commit-finalization.ts +23 -3
- package/lib/task/create-service.ts +171 -0
- package/lib/task/create.ts +372 -0
- package/lib/task/orchestration.ts +182 -18
- package/lib/task/review-fingerprint.ts +9 -0
- package/lib/task/workspace-snapshot.ts +98 -4
- package/package.json +7 -7
- package/templates/.agents/hooks/lifecycle-delegation.js +47 -3
- package/templates/.agents/rules/task-management.en.md +1 -0
- package/templates/.agents/rules/task-management.zh-CN.md +1 -0
- package/templates/.agents/skills/commit/SKILL.en.md +3 -9
- package/templates/.agents/skills/commit/SKILL.zh-CN.md +6 -10
- package/templates/.agents/skills/commit/reference/commit-orchestration.en.md +4 -3
- package/templates/.agents/skills/commit/reference/commit-orchestration.zh-CN.md +5 -4
- package/templates/.agents/skills/create-task/SKILL.en.md +26 -43
- package/templates/.agents/skills/create-task/SKILL.zh-CN.md +26 -43
- package/templates/.codex/hooks.json +50 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
preflightCodexLifecycleEvidence,
|
|
8
|
+
resolveCodexTerminal,
|
|
9
|
+
resolveCodexThread
|
|
10
|
+
} from '../agent-clients/adapters/codex-lifecycle/app-server.ts';
|
|
11
|
+
import { createCodexLifecycleStore } from '../agent-clients/adapters/codex-lifecycle/store.ts';
|
|
12
|
+
import type { CodexLifecycleEvent } from '../agent-clients/adapters/codex-lifecycle/evidence.ts';
|
|
13
|
+
|
|
14
|
+
const USAGE = 'Usage: agent-infra-internal codex-lifecycle <hook-event|resolve-start|resolve-stop|preflight|consume> [options]\n';
|
|
15
|
+
const MANAGED_AGENT = /^agent-infra-lifecycle-(executor|reviewer)$/;
|
|
16
|
+
|
|
17
|
+
type Parsed = Readonly<{ operation: string; values: Readonly<Record<string, string>> }>;
|
|
18
|
+
|
|
19
|
+
function output(value: unknown): void {
|
|
20
|
+
process.stdout.write(`${JSON.stringify(value)}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function failure(code: string, message: string): void {
|
|
24
|
+
output({ status: 'failed', changed: false, evidence: null, diagnostics: [], error: { code, message } });
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function parse(args: string[]): Parsed | null {
|
|
29
|
+
if (args[0] === '--help' || args[0] === '-h') {
|
|
30
|
+
process.stdout.write(USAGE);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
const operation = args[0];
|
|
34
|
+
if (!operation) {
|
|
35
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'operation is required');
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const allowed = {
|
|
39
|
+
'hook-event': ['--event'],
|
|
40
|
+
'resolve-start': ['--child-id'],
|
|
41
|
+
'resolve-stop': ['--child-id'],
|
|
42
|
+
preflight: ['--format', '--session-id', '--turn-id', '--tool-use-id'],
|
|
43
|
+
consume: ['--child-id', '--consumer']
|
|
44
|
+
}[operation];
|
|
45
|
+
if (!allowed) {
|
|
46
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', 'unknown operation');
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const values: Record<string, string> = {};
|
|
50
|
+
for (let index = 1; index < args.length; index += 1) {
|
|
51
|
+
const flag = args[index]!;
|
|
52
|
+
if (!allowed.includes(flag) || Object.hasOwn(values, flag)) {
|
|
53
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `invalid or duplicate option '${flag}'`);
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const value = args[++index];
|
|
57
|
+
if (!value || value.startsWith('--')) {
|
|
58
|
+
failure('CODEX_LIFECYCLE_PAYLOAD_INVALID', `option '${flag}' requires a value`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
values[flag] = value;
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({ operation, values: Object.freeze(values) });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function cliVersion(): string {
|
|
67
|
+
const result = spawnSync('codex', ['--version'], { encoding: 'utf8' });
|
|
68
|
+
const version = /codex-cli\s+(\d+\.\d+\.\d+)/.exec(result.stdout ?? '')?.[1];
|
|
69
|
+
if (result.status !== 0 || !version) throw new Error('Codex CLI version is unavailable');
|
|
70
|
+
return version;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function hookDefinitionHash(): string {
|
|
74
|
+
const file = path.join(process.cwd(), '.codex', 'hooks.json');
|
|
75
|
+
return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function readStdin(): Promise<unknown> {
|
|
79
|
+
let input = '';
|
|
80
|
+
process.stdin.setEncoding('utf8');
|
|
81
|
+
for await (const chunk of process.stdin) {
|
|
82
|
+
input += chunk;
|
|
83
|
+
if (Buffer.byteLength(input) > 64 * 1024) throw new Error('Codex lifecycle input exceeds 64 KiB');
|
|
84
|
+
}
|
|
85
|
+
return JSON.parse(input || '{}') as unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function recordFromPayload(phase: string, payload: unknown): CodexLifecycleEvent | null {
|
|
89
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
90
|
+
throw new Error('Codex lifecycle hook payload must be an object');
|
|
91
|
+
}
|
|
92
|
+
const value = payload as Record<string, unknown>;
|
|
93
|
+
const text = (key: string) => typeof value[key] === 'string' ? value[key] as string : '';
|
|
94
|
+
if (!MANAGED_AGENT.test(text('nativeAgent'))) return null;
|
|
95
|
+
if (phase === 'pre-tool') return {
|
|
96
|
+
type: 'hook-spawn',
|
|
97
|
+
sessionId: text('sessionId'),
|
|
98
|
+
turnId: text('turnId'),
|
|
99
|
+
toolUseId: text('toolUseId'),
|
|
100
|
+
nativeAgent: text('nativeAgent'),
|
|
101
|
+
...(text('requestedModel') ? { requestedModel: text('requestedModel') } : {}),
|
|
102
|
+
...(text('requestedReasoningEffort') ? { requestedReasoningEffort: text('requestedReasoningEffort') } : {}),
|
|
103
|
+
hookDefinitionHash: text('hookDefinitionHash')
|
|
104
|
+
};
|
|
105
|
+
if (phase === 'subagent-start') return {
|
|
106
|
+
type: 'hook-child',
|
|
107
|
+
sessionId: text('sessionId'),
|
|
108
|
+
turnId: text('turnId'),
|
|
109
|
+
childThreadId: text('childThreadId'),
|
|
110
|
+
nativeAgent: text('nativeAgent')
|
|
111
|
+
};
|
|
112
|
+
if (phase === 'subagent-stop') return {
|
|
113
|
+
type: 'hook-stop',
|
|
114
|
+
sessionId: text('sessionId'),
|
|
115
|
+
turnId: text('turnId'),
|
|
116
|
+
childThreadId: text('childThreadId'),
|
|
117
|
+
nativeAgent: text('nativeAgent')
|
|
118
|
+
};
|
|
119
|
+
if (phase === 'post-tool') return null;
|
|
120
|
+
throw new Error(`unknown Codex lifecycle hook phase '${phase}'`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function codexLifecycle(args: string[] = []): Promise<void> {
|
|
124
|
+
const parsed = parse(args);
|
|
125
|
+
if (!parsed || process.exitCode) return;
|
|
126
|
+
try {
|
|
127
|
+
if (parsed.operation === 'preflight') {
|
|
128
|
+
const format = parsed.values['--format'] ?? 'json';
|
|
129
|
+
if (!['text', 'json'].includes(format)) throw new Error("format must be 'text' or 'json'");
|
|
130
|
+
const runtimeFields = [
|
|
131
|
+
parsed.values['--session-id'],
|
|
132
|
+
parsed.values['--turn-id'],
|
|
133
|
+
parsed.values['--tool-use-id']
|
|
134
|
+
];
|
|
135
|
+
if (runtimeFields.some(Boolean) && !runtimeFields.every(Boolean)) {
|
|
136
|
+
throw new Error('preflight runtime identity requires --session-id, --turn-id, and --tool-use-id together');
|
|
137
|
+
}
|
|
138
|
+
const runtimeIdentity = runtimeFields.every(Boolean) ? {
|
|
139
|
+
sessionId: runtimeFields[0]!,
|
|
140
|
+
turnId: runtimeFields[1]!,
|
|
141
|
+
toolUseId: runtimeFields[2]!
|
|
142
|
+
} : undefined;
|
|
143
|
+
const result = await preflightCodexLifecycleEvidence(process.cwd(), runtimeIdentity);
|
|
144
|
+
if (format === 'text') {
|
|
145
|
+
process.stdout.write(`Codex lifecycle static preflight: ready\nCLI: ${result.cliVersion}\nRuntime hook liveness: ${result.runtimeLiveness ? 'observed' : 'not-yet-observed'}\n`);
|
|
146
|
+
} else output({ status: 'ready', changed: false, evidence: result, diagnostics: result.diagnostics, error: null });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const store = createCodexLifecycleStore({ cliVersion: cliVersion() });
|
|
151
|
+
if (parsed.operation === 'hook-event') {
|
|
152
|
+
const phase = parsed.values['--event'];
|
|
153
|
+
if (!phase || !['pre-tool', 'subagent-start', 'post-tool', 'subagent-stop'].includes(phase)) {
|
|
154
|
+
throw new Error('hook-event requires a known --event');
|
|
155
|
+
}
|
|
156
|
+
const event = recordFromPayload(phase, await readStdin());
|
|
157
|
+
if (!event) {
|
|
158
|
+
output({ status: 'ignored', changed: false, evidence: null, diagnostics: [], error: null });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const result = store.apply(event);
|
|
162
|
+
output({ status: result.state.status, changed: true, evidence: result.state, diagnostics: [], error: result.state.error });
|
|
163
|
+
if (result.state.status === 'invalid') process.exitCode = 1;
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const childThreadId = parsed.values['--child-id'];
|
|
168
|
+
if (!childThreadId) throw new Error(`operation '${parsed.operation}' requires --child-id`);
|
|
169
|
+
if (parsed.operation === 'resolve-start') {
|
|
170
|
+
if (store.read(childThreadId).state.spawn?.hookDefinitionHash !== hookDefinitionHash()) {
|
|
171
|
+
throw new Error('Codex lifecycle hook definition hash is stale');
|
|
172
|
+
}
|
|
173
|
+
const resolved = await resolveCodexThread(childThreadId);
|
|
174
|
+
let latest = store.apply(resolved.resolution.thread);
|
|
175
|
+
for (const reroute of resolved.reroutes) latest = store.apply(reroute);
|
|
176
|
+
latest = store.apply(resolved.resolution.settings);
|
|
177
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.startEvidence, diagnostics: resolved.diagnostics, error: latest.state.error });
|
|
178
|
+
if (latest.state.status !== 'start-ready') process.exitCode = 1;
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (parsed.operation === 'resolve-stop') {
|
|
182
|
+
const terminal = await resolveCodexTerminal(childThreadId);
|
|
183
|
+
const latest = store.apply(terminal);
|
|
184
|
+
if (latest.state.status !== 'stop-ready') {
|
|
185
|
+
throw new Error(`Codex lifecycle stop evidence is not ready (status=${latest.state.status})`);
|
|
186
|
+
}
|
|
187
|
+
output({ status: latest.state.status, changed: true, evidence: latest.state.stopEvidence, diagnostics: [], error: latest.state.error });
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (parsed.operation === 'consume') {
|
|
191
|
+
const consumer = parsed.values['--consumer'];
|
|
192
|
+
if (!consumer) throw new Error('consume requires --consumer');
|
|
193
|
+
const consumed = store.consume(childThreadId, consumer, hookDefinitionHash());
|
|
194
|
+
output({ status: 'consumed', changed: true, evidence: {
|
|
195
|
+
start: consumed.state.startEvidence,
|
|
196
|
+
stop: consumed.state.stopEvidence
|
|
197
|
+
}, diagnostics: [], error: null });
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
failure('CODEX_LIFECYCLE_FAILED', error instanceof Error ? error.message : String(error));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export { codexLifecycle };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { requestSandboxTaskCreate } 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): TaskCreateResult {
|
|
10
|
+
return {
|
|
11
|
+
status: 'failed', changed: false, task: { id: null, shortId: null }, issue: null,
|
|
12
|
+
operations: [], warnings: [], error: { code, message, retryable: false }
|
|
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
|
+
process.stdout.write(response.stdout);
|
|
37
|
+
process.stderr.write(response.stderr);
|
|
38
|
+
process.exitCode = response.exitCode;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
output(createTask(candidate, { repoRoot: process.cwd() }));
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
44
|
+
const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'TASK_CREATE_INPUT_INVALID';
|
|
45
|
+
output(failed(code, message));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export { taskCreate };
|
|
@@ -14,13 +14,15 @@ import {
|
|
|
14
14
|
routeOrchestration,
|
|
15
15
|
sealMatchingOrchestrationDelegation,
|
|
16
16
|
sealOrchestrationDelegation,
|
|
17
|
+
startCommitAttempt,
|
|
17
18
|
statusCommitIntent,
|
|
18
|
-
statusOrchestration
|
|
19
|
+
statusOrchestration,
|
|
20
|
+
terminateCommitAttempt
|
|
19
21
|
} from '../task/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({
|
|
@@ -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-
|
|
88
|
-
'commit-
|
|
89
|
-
'commit-
|
|
90
|
-
'commit-
|
|
91
|
-
'commit-
|
|
92
|
-
'commit-
|
|
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-
|
|
111
|
-
const missing = requireValues(['--agent'
|
|
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,9 +206,9 @@ 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; }
|
|
@@ -190,7 +216,7 @@ function taskOrchestration(args: string[] = []): void {
|
|
|
190
216
|
client: values['--client'] as AgentClientId,
|
|
191
217
|
requestedModel: values['--requested-model'],
|
|
192
218
|
requestedReasoningEffort: values['--requested-reasoning-effort']
|
|
193
|
-
});
|
|
219
|
+
}, 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;
|
|
@@ -1345,6 +1345,7 @@ export async function create(args: string[]): Promise<void> {
|
|
|
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,
|
|
@@ -5,37 +5,29 @@ import {
|
|
|
5
5
|
SANDBOX_CONTROL_MAX_BYTES,
|
|
6
6
|
isSandboxControlFamily,
|
|
7
7
|
type SandboxControlRequest,
|
|
8
|
-
type SandboxControlResponse
|
|
8
|
+
type SandboxControlResponse,
|
|
9
|
+
type SandboxTaskCreateRequest
|
|
9
10
|
} from './protocol.ts';
|
|
11
|
+
import type { TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
10
12
|
|
|
11
13
|
function sleep(milliseconds: number): void {
|
|
12
14
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
13
15
|
}
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
family: string;
|
|
17
|
-
args: string[];
|
|
17
|
+
function exchangeSandboxControl(request: SandboxControlRequest, params: Readonly<{
|
|
18
18
|
channelDir?: string;
|
|
19
|
-
token?: string;
|
|
20
19
|
timeoutMs?: number;
|
|
21
20
|
}>): SandboxControlResponse {
|
|
22
|
-
if (!isSandboxControlFamily(params.family)) {
|
|
23
|
-
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
24
|
-
}
|
|
25
21
|
const channelDir = params.channelDir ?? process.env.AGENT_INFRA_CONTROL_DIR ?? '/run/agent-infra/control';
|
|
26
|
-
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
27
|
-
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
28
|
-
const requestsDir = path.join(channelDir, 'requests');
|
|
29
|
-
const responsesDir = path.join(channelDir, 'responses');
|
|
30
|
-
const id = randomUUID();
|
|
31
|
-
const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
|
|
32
22
|
const encoded = `${JSON.stringify(request)}\n`;
|
|
33
23
|
if (Buffer.byteLength(encoded, 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
34
24
|
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
35
25
|
}
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
const
|
|
26
|
+
const requestsDir = path.join(channelDir, 'requests');
|
|
27
|
+
const responsesDir = path.join(channelDir, 'responses');
|
|
28
|
+
const temporary = path.join(requestsDir, `.${request.id}.tmp`);
|
|
29
|
+
const requestPath = path.join(requestsDir, `${request.id}.json`);
|
|
30
|
+
const responsePath = path.join(responsesDir, `${request.id}.json`);
|
|
39
31
|
fs.writeFileSync(temporary, encoded, { mode: 0o600, flag: 'wx' });
|
|
40
32
|
fs.renameSync(temporary, requestPath);
|
|
41
33
|
const deadline = Date.now() + (params.timeoutMs ?? 30_000);
|
|
@@ -44,7 +36,7 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
44
36
|
const raw = fs.readFileSync(responsePath, 'utf8');
|
|
45
37
|
fs.unlinkSync(responsePath);
|
|
46
38
|
const response = JSON.parse(raw) as SandboxControlResponse;
|
|
47
|
-
if (response.version !== 1 || response.id !== id || !Number.isInteger(response.exitCode)) {
|
|
39
|
+
if (response.version !== 1 || response.id !== request.id || !Number.isInteger(response.exitCode)) {
|
|
48
40
|
throw new Error('SANDBOX_CONTROL_RESPONSE_INVALID: broker response is invalid');
|
|
49
41
|
}
|
|
50
42
|
return response;
|
|
@@ -53,3 +45,35 @@ export function requestSandboxControl(params: Readonly<{
|
|
|
53
45
|
}
|
|
54
46
|
throw new Error(`SANDBOX_CONTROL_TIMEOUT: broker did not respond within ${params.timeoutMs ?? 30_000}ms`);
|
|
55
47
|
}
|
|
48
|
+
|
|
49
|
+
export function requestSandboxControl(params: Readonly<{
|
|
50
|
+
family: string;
|
|
51
|
+
args: string[];
|
|
52
|
+
channelDir?: string;
|
|
53
|
+
token?: string;
|
|
54
|
+
timeoutMs?: number;
|
|
55
|
+
}>): SandboxControlResponse {
|
|
56
|
+
if (!isSandboxControlFamily(params.family)) {
|
|
57
|
+
throw new Error(`SANDBOX_CONTROL_COMMAND_DENIED: '${params.family}' is not allowed`);
|
|
58
|
+
}
|
|
59
|
+
if (params.family === 'task-create') throw new Error("SANDBOX_CONTROL_COMMAND_DENIED: 'task-create' requires a typed candidate");
|
|
60
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
61
|
+
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
62
|
+
const id = randomUUID();
|
|
63
|
+
const request: SandboxControlRequest = { version: 1, id, token, family: params.family, args: params.args };
|
|
64
|
+
return exchangeSandboxControl(request, params);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function requestSandboxTaskCreate(params: Readonly<{
|
|
68
|
+
candidate: TaskCreateCandidateV1;
|
|
69
|
+
channelDir?: string;
|
|
70
|
+
token?: string;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
}>): SandboxControlResponse {
|
|
73
|
+
const token = params.token ?? process.env.AGENT_INFRA_CONTROL_TOKEN;
|
|
74
|
+
if (!token) throw new Error('SANDBOX_CONTROL_UNAVAILABLE: control token is missing');
|
|
75
|
+
const request: SandboxTaskCreateRequest = {
|
|
76
|
+
version: 1, id: randomUUID(), token, family: 'task-create', candidate: params.candidate
|
|
77
|
+
};
|
|
78
|
+
return exchangeSandboxControl(request, { ...params, timeoutMs: params.timeoutMs ?? 120_000 });
|
|
79
|
+
}
|
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
import { validateTaskCreateCandidate, type TaskCreateCandidateV1 } from '../../task/create.ts';
|
|
2
|
+
|
|
1
3
|
export const SANDBOX_CONTROL_MAX_BYTES = 64 * 1024;
|
|
2
|
-
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration'] as const;
|
|
4
|
+
export const SANDBOX_CONTROL_FAMILIES = ['task-lifecycle', 'task-orchestration', 'task-create'] as const;
|
|
3
5
|
|
|
4
6
|
export type SandboxControlFamily = typeof SANDBOX_CONTROL_FAMILIES[number];
|
|
5
7
|
|
|
6
8
|
export type SandboxControlManifest = Readonly<{
|
|
7
|
-
version:
|
|
9
|
+
version: 2;
|
|
8
10
|
repoRoot: string;
|
|
11
|
+
worktreeRoot: string;
|
|
9
12
|
project: string;
|
|
10
13
|
container: string;
|
|
11
14
|
branch: string;
|
|
@@ -15,14 +18,24 @@ export type SandboxControlManifest = Readonly<{
|
|
|
15
18
|
channelDir: string;
|
|
16
19
|
}>;
|
|
17
20
|
|
|
18
|
-
export type
|
|
21
|
+
export type SandboxTaskCommandRequest = Readonly<{
|
|
19
22
|
version: 1;
|
|
20
23
|
id: string;
|
|
21
24
|
token: string;
|
|
22
|
-
family:
|
|
25
|
+
family: 'task-lifecycle' | 'task-orchestration';
|
|
23
26
|
args: string[];
|
|
24
27
|
}>;
|
|
25
28
|
|
|
29
|
+
export type SandboxTaskCreateRequest = Readonly<{
|
|
30
|
+
version: 1;
|
|
31
|
+
id: string;
|
|
32
|
+
token: string;
|
|
33
|
+
family: 'task-create';
|
|
34
|
+
candidate: TaskCreateCandidateV1;
|
|
35
|
+
}>;
|
|
36
|
+
|
|
37
|
+
export type SandboxControlRequest = SandboxTaskCommandRequest | SandboxTaskCreateRequest;
|
|
38
|
+
|
|
26
39
|
export type SandboxControlResponse = Readonly<{
|
|
27
40
|
version: 1;
|
|
28
41
|
id: string;
|
|
@@ -42,7 +55,7 @@ export function validateSandboxControlRequest(
|
|
|
42
55
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
43
56
|
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request must be an object');
|
|
44
57
|
}
|
|
45
|
-
const request = value as
|
|
58
|
+
const request = value as Record<string, unknown>;
|
|
46
59
|
if (
|
|
47
60
|
request.version !== 1
|
|
48
61
|
|| typeof request.id !== 'string'
|
|
@@ -50,6 +63,22 @@ export function validateSandboxControlRequest(
|
|
|
50
63
|
|| request.token !== manifest.token
|
|
51
64
|
|| typeof request.family !== 'string'
|
|
52
65
|
|| !isSandboxControlFamily(request.family)
|
|
66
|
+
) {
|
|
67
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
68
|
+
}
|
|
69
|
+
if (Buffer.byteLength(JSON.stringify(request), 'utf8') > SANDBOX_CONTROL_MAX_BYTES) {
|
|
70
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_TOO_LARGE: request exceeds the control limit');
|
|
71
|
+
}
|
|
72
|
+
if (request.family === 'task-create') {
|
|
73
|
+
const keys = Object.keys(request).sort().join(',');
|
|
74
|
+
if (keys !== ['candidate', 'family', 'id', 'token', 'version'].sort().join(',')) {
|
|
75
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: request schema or authorization is invalid');
|
|
76
|
+
}
|
|
77
|
+
return { ...request, candidate: validateTaskCreateCandidate(request.candidate) } as SandboxTaskCreateRequest;
|
|
78
|
+
}
|
|
79
|
+
const keys = Object.keys(request).sort().join(',');
|
|
80
|
+
if (
|
|
81
|
+
keys !== ['args', 'family', 'id', 'token', 'version'].sort().join(',')
|
|
53
82
|
|| !Array.isArray(request.args)
|
|
54
83
|
|| !request.args.every((arg) => typeof arg === 'string')
|
|
55
84
|
) {
|
|
@@ -58,16 +87,22 @@ export function validateSandboxControlRequest(
|
|
|
58
87
|
if (manifest.mode !== 'task-bound' || !manifest.taskId) {
|
|
59
88
|
throw new Error('SANDBOX_CONTROL_BRANCH_ONLY: branch-only sandboxes cannot coordinate tasks');
|
|
60
89
|
}
|
|
61
|
-
if (
|
|
62
|
-
|
|
90
|
+
if (
|
|
91
|
+
request.family === 'task-orchestration'
|
|
92
|
+
&& request.args.some((arg) => arg === '--git-worktree-root' || arg.startsWith('--git-worktree-root='))
|
|
93
|
+
) {
|
|
94
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: worktree binding is reserved for the control broker');
|
|
63
95
|
}
|
|
64
|
-
return request as
|
|
96
|
+
return request as SandboxTaskCommandRequest;
|
|
65
97
|
}
|
|
66
98
|
|
|
67
99
|
export function bindSandboxControlTask(
|
|
68
100
|
request: SandboxControlRequest,
|
|
69
101
|
taskId: string
|
|
70
102
|
): string[] {
|
|
103
|
+
if (request.family === 'task-create') {
|
|
104
|
+
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: task-create requests do not bind a current task');
|
|
105
|
+
}
|
|
71
106
|
if (request.args.length === 0) {
|
|
72
107
|
throw new Error('SANDBOX_CONTROL_REQUEST_INVALID: command arguments are required');
|
|
73
108
|
}
|