mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-29 08:51:53 +00:00
Align runtime adapters with installed CLIs
This commit is contained in:
1 parent
2e0f9dc9ff
commit
d8ab841e5b
6 files changed
+73
-33
No files matched your search
@@ -102,11 +102,11 @@ test_integrity_note:
|
||||
- id: SCN-claude-adapter
|
||||
given: a Claude runtime adapter with a virtualized command runner
|
||||
when: sessions are planned and runtime state is queried
|
||||
then: command construction is deterministic, capabilities are explicit, planning launches no process, and stop requires matching live session and worktree ownership evidence
|
||||
then: background creation omits the ignored synthetic session flag, captures the sole safe manager-assigned ID, launches no process during planning, and stop requires matching live session and worktree ownership evidence
|
||||
- id: SCN-codex-adapter
|
||||
given: a Codex runtime adapter with a virtualized command runner and an identity-bound TORCH control plane
|
||||
when: create, resume, MCP configuration, runtime-ID capture, and steering are planned or attempted
|
||||
then: no provider call occurs, thread IDs come from JSONL, every message is durable, and unsupported native controls fail explicitly
|
||||
then: no provider call occurs, mutually exclusive safety flags are absent, global options precede exec and resume options follow resume, thread IDs come from JSONL, every message is durable, and unsupported native controls fail explicitly
|
||||
- id: SCN-fleet-fresh-resume
|
||||
given: approved identities and managed worktrees with a virtualized Claude executor
|
||||
when: the fleet starts fresh, winds down safely, and plans its next start
|
||||
@@ -267,6 +267,7 @@ test_integrity_note:
|
||||
- SQLite state after process restart
|
||||
- CLI and MCP output over real process boundaries
|
||||
- runtime command plans and execution order
|
||||
- installed-CLI-compatible option sets, subcommand ordering, and manager-assigned Claude background IDs
|
||||
- runtime stop session identity, reported working directory, and fail-closed ownership mismatch
|
||||
- local resume snapshot, tracked resume brief, final worker message, preserved task identity, and presence transitions
|
||||
- immutable check receipt commit and result
|
||||
@@ -312,6 +313,8 @@ test_integrity_note:
|
||||
- repository content redirecting machine-local database writes
|
||||
- MCP and CLI drifting onto separate business logic
|
||||
- launch plans that execute provider commands during preview
|
||||
- virtualized tests blessing commands that the installed provider CLI rejects during parsing
|
||||
- a synthetic Claude UUID being persisted when background mode ignored it
|
||||
- stopping a runtime by PID, parentage, or session label without matching managed-worktree evidence
|
||||
- a mixed-runtime claim implemented as renamed Claude-only commands
|
||||
- runtime sessions launching without their identity-bound TORCH tool surface
|
||||
|
||||
+16
-4
@@ -42,6 +42,16 @@ function mcpName(areaId) {
|
||||
return `torch-${areaId.toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-|-$/g, '')}`;
|
||||
}
|
||||
|
||||
function backgroundSessionId(output) {
|
||||
const lines = String(output ?? '').split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
||||
if (lines.length !== 1 || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/.test(lines[0])) {
|
||||
throw new TorchError('Claude background launch did not return exactly one safe session ID', {
|
||||
code: 'RUNTIME_ID_NOT_CAPTURED', details: { adapter: 'claude' },
|
||||
});
|
||||
}
|
||||
return lines[0];
|
||||
}
|
||||
|
||||
export class ClaudeRuntimeAdapter {
|
||||
constructor({
|
||||
env = process.env,
|
||||
@@ -96,12 +106,12 @@ export class ClaudeRuntimeAdapter {
|
||||
}
|
||||
|
||||
createSession({ areaId, title, worktree, promptFile, firstMessage, model = 'opus', background = true, mcp } = {}) {
|
||||
const runtimeSessionId = this.idFactory();
|
||||
const runtimeSessionId = background ? null : this.idFactory();
|
||||
const args = [];
|
||||
if (background) args.push('--bg');
|
||||
args.push(
|
||||
'--model', text(model, 'model'),
|
||||
'--session-id', runtimeSessionId,
|
||||
...(!background ? ['--session-id', runtimeSessionId] : []),
|
||||
'-n', `TORCH · ${text(title ?? areaId, 'title')}`,
|
||||
'--append-system-prompt-file', text(promptFile, 'promptFile'),
|
||||
...mcpArgs(mcp),
|
||||
@@ -109,6 +119,7 @@ export class ClaudeRuntimeAdapter {
|
||||
if (firstMessage) args.push(text(firstMessage, 'firstMessage'));
|
||||
return {
|
||||
adapter: this.name, areaId: text(areaId, 'areaId'), runtimeSessionId,
|
||||
requiresRuntimeIdCapture: background,
|
||||
launch: commandRecord(this.executable, args, text(worktree, 'worktree')),
|
||||
};
|
||||
}
|
||||
@@ -188,8 +199,9 @@ export class ClaudeRuntimeAdapter {
|
||||
};
|
||||
}
|
||||
|
||||
captureRuntimeId({ areaId, runtimeSessionId } = {}) {
|
||||
return { areaId: text(areaId, 'areaId'), runtimeSessionId: text(runtimeSessionId, 'runtimeSessionId') };
|
||||
captureRuntimeId({ areaId, runtimeSessionId, stdout, output } = {}) {
|
||||
const captured = runtimeSessionId ?? backgroundSessionId(stdout ?? output);
|
||||
return { areaId: text(areaId, 'areaId'), runtimeSessionId: text(captured, 'runtimeSessionId') };
|
||||
}
|
||||
|
||||
installHooks() {
|
||||
|
||||
@@ -113,8 +113,9 @@ export class CodexRuntimeAdapter {
|
||||
const id = text(areaId, 'areaId');
|
||||
const cwd = text(worktree, 'worktree');
|
||||
const args = [
|
||||
'exec', '--json', '--cd', cwd, '--sandbox', 'workspace-write', '--approve-for-me',
|
||||
'--cd', cwd, '--approve-for-me',
|
||||
...mcpArgs(mcp),
|
||||
'exec', '--json',
|
||||
...optionalModelArgs(model),
|
||||
`${text(firstMessage, 'firstMessage')}\n\nRead and follow the TORCH domain prompt at ${text(promptFile, 'promptFile')}.`,
|
||||
];
|
||||
@@ -133,9 +134,10 @@ export class CodexRuntimeAdapter {
|
||||
const sessionId = text(runtimeSessionId, 'runtimeSessionId');
|
||||
const cwd = text(worktree, 'worktree');
|
||||
const args = [
|
||||
'exec', '--json', '--cd', cwd, '--sandbox', 'workspace-write', '--approve-for-me',
|
||||
'--cd', cwd, '--approve-for-me',
|
||||
...mcpArgs(mcp),
|
||||
...optionalModelArgs(model), 'resume', sessionId, text(message, 'message'),
|
||||
'exec', 'resume', '--json',
|
||||
...optionalModelArgs(model), sessionId, text(message, 'message'),
|
||||
];
|
||||
return {
|
||||
adapter: this.name,
|
||||
|
||||
@@ -76,12 +76,16 @@ test('SCN-bootstrap-acceptance: one installed fixture proves the complete organi
|
||||
assert.equal(existsSync(join(worktree.path, '.torch', 'prompts', `${agent.areaId}.md`)), true);
|
||||
}
|
||||
|
||||
let runtime = 0;
|
||||
const adapter = createClaudeAdapter({ idFactory: () => `bootstrap-runtime-${++runtime}` });
|
||||
const up = planFleetUp({ repositoryRoot: context.root, controlPlane: control, adapter, fresh: true });
|
||||
const adapter = createClaudeAdapter();
|
||||
const adapters = new Map([['claude', adapter]]);
|
||||
const up = planFleetUp({ repositoryRoot: context.root, controlPlane: control, adapters, fresh: true });
|
||||
assert.equal(up.canProceed, true, JSON.stringify(up.blockers));
|
||||
assert.equal(up.actions.at(-1).areaId, 'session-manager');
|
||||
const started = startFleet({ plan: up, controlPlane: control, executor: () => ({ status: 0 }) });
|
||||
let runtime = 0;
|
||||
const started = startFleet({
|
||||
plan: up, controlPlane: control, adapters,
|
||||
executor: () => ({ status: 0, stdout: `bootstrap-runtime-${++runtime}\n` }),
|
||||
});
|
||||
assert.equal(new Set(started.started.map((entry) => entry.runtimeSessionId)).size, roster.agents.length);
|
||||
for (const action of up.actions) {
|
||||
assert.match(readFileSync(action.promptFile, 'utf8'), new RegExp(`Area ID: ${action.areaId}|TORCH Session Manager`));
|
||||
|
||||
@@ -46,9 +46,9 @@ function fleetFixture({ workerRuntime = 'claude' } = {}) {
|
||||
test('SCN-fleet-fresh-resume: workers start before manager and stable identities resume captured runtime sessions', () => {
|
||||
const fixture = fleetFixture();
|
||||
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
|
||||
let session = 0;
|
||||
const adapter = createClaudeAdapter({ executable: 'claude', idFactory: () => `runtime-${++session}` });
|
||||
const fresh = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapter, fresh: true });
|
||||
const adapter = createClaudeAdapter({ executable: 'claude' });
|
||||
const adapters = new Map([['claude', adapter]]);
|
||||
const fresh = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapters, fresh: true });
|
||||
assert.equal(fresh.actions.at(-1).areaId, 'session-manager');
|
||||
assert.equal(fresh.actions.every((action) => action.mode === 'create'), true);
|
||||
assert.equal(fresh.actions.every((action) => action.mcp.args.includes(action.areaId)), true);
|
||||
@@ -58,8 +58,11 @@ test('SCN-fleet-fresh-resume: workers start before manager and stable identities
|
||||
|
||||
const launches = [];
|
||||
const started = startFleet({
|
||||
plan: fresh, controlPlane: control,
|
||||
executor: (launch) => { launches.push(launch); return { status: 0 }; },
|
||||
plan: fresh, controlPlane: control, adapters,
|
||||
executor: (launch) => {
|
||||
launches.push(launch);
|
||||
return { status: 0, stdout: `runtime-${launches.length}\n` };
|
||||
},
|
||||
});
|
||||
assert.equal(started.started.at(-1).areaId, 'session-manager');
|
||||
assert.equal(launches.length, fresh.actions.length);
|
||||
@@ -97,13 +100,13 @@ test('SCN-fleet-fresh-resume: workers start before manager and stable identities
|
||||
}).stateRoot, 'project.json');
|
||||
assert.equal(JSON.parse(readFileSync(metadataPath, 'utf8')).detachedAt, detached.detachedAt);
|
||||
|
||||
const resumed = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapter, fresh: false });
|
||||
const resumed = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapters, fresh: false });
|
||||
assert.equal(resumed.actions.every((action) => action.mode === 'resume'), true);
|
||||
assert.deepEqual(
|
||||
resumed.actions.map((action) => action.runtimeSessionId),
|
||||
fresh.actions.map((action) => action.runtimeSessionId),
|
||||
started.started.map((action) => action.runtimeSessionId),
|
||||
);
|
||||
startFleet({ plan: resumed, controlPlane: control, executor: () => ({ status: 0 }) });
|
||||
startFleet({ plan: resumed, controlPlane: control, adapters, executor: () => ({ status: 0 }) });
|
||||
assert.equal(JSON.parse(readFileSync(metadataPath, 'utf8')).detachedAt, null);
|
||||
control.close();
|
||||
});
|
||||
@@ -111,8 +114,7 @@ test('SCN-fleet-fresh-resume: workers start before manager and stable identities
|
||||
test('SCN-fleet-wind-down-safety: active work blocks runtime shutdown and failed startup prevents manager launch', () => {
|
||||
const fixture = fleetFixture();
|
||||
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
|
||||
let session = 0;
|
||||
const adapter = createClaudeAdapter({ idFactory: () => `failure-${++session}` });
|
||||
const adapter = createClaudeAdapter();
|
||||
const plan = planFleetUp({ repositoryRoot: fixture.root, controlPlane: control, adapter, fresh: true });
|
||||
assert.throws(
|
||||
() => startFleet({
|
||||
@@ -148,7 +150,7 @@ test('SCN-mixed-runtime: Codex and Claude share stable identities while planning
|
||||
const fixture = fleetFixture({ workerRuntime: 'codex' });
|
||||
const control = openControlPlane({ repositoryRoot: fixture.root, env: fixture.env });
|
||||
const adapters = new Map([
|
||||
['claude', createClaudeAdapter({ idFactory: () => 'claude-manager-1' })],
|
||||
['claude', createClaudeAdapter()],
|
||||
['codex', createCodexAdapter({ executable: 'codex' })],
|
||||
]);
|
||||
const plan = planFleetUp({
|
||||
@@ -157,18 +159,19 @@ test('SCN-mixed-runtime: Codex and Claude share stable identities while planning
|
||||
assert.equal(plan.canProceed, true);
|
||||
assert.equal(plan.actions[0].areaId, fixture.worker);
|
||||
assert.equal(plan.actions[0].runtime, 'codex');
|
||||
assert.equal(plan.actions[0].launch.args[0], 'exec');
|
||||
assert.equal(plan.actions[0].launch.args.includes('exec'), true);
|
||||
assert.equal(plan.actions[0].launch.args.some((arg) => arg.includes('mcp_servers.')), true);
|
||||
assert.equal(plan.actions[0].launch.args.some((arg) => arg.includes(fixture.worker)), true);
|
||||
assert.equal(plan.actions.at(-1).runtime, 'claude');
|
||||
assert.equal(plan.actions.at(-1).launch.args.includes('--mcp-config'), true);
|
||||
assert.equal(plan.actions.at(-1).runtimeSessionId, 'claude-manager-1');
|
||||
assert.equal(plan.actions.at(-1).runtimeSessionId, null);
|
||||
assert.equal(plan.actions.at(-1).requiresRuntimeIdCapture, true);
|
||||
|
||||
const started = startFleet({
|
||||
plan, controlPlane: control, adapters,
|
||||
executor: (launch) => launch.areaId === fixture.worker
|
||||
? { status: 0, stdout: '{"type":"thread.started","thread_id":"codex-worker-1"}\n' }
|
||||
: { status: 0 },
|
||||
: { status: 0, stdout: 'claude-manager-1\n' },
|
||||
});
|
||||
assert.deepEqual(started.started.map((item) => item.runtimeSessionId), [
|
||||
'codex-worker-1', 'claude-manager-1',
|
||||
|
||||
@@ -85,15 +85,24 @@ test('SCN-claude-adapter: planning is deterministic, capabilities are honest, an
|
||||
areaId: context.worker, title: 'Core', worktree: '/tmp/core', promptFile: '/tmp/core.md',
|
||||
firstMessage: 'Resume TASK-1.', model: 'opus', mcp: configuration.mcp,
|
||||
});
|
||||
assert.equal(launch.runtimeSessionId, 'runtime-123');
|
||||
assert.deepEqual(launch.launch.args.slice(0, 7), [
|
||||
'--bg', '--model', 'opus', '--session-id', 'runtime-123', '-n', 'TORCH · Core',
|
||||
assert.equal(launch.runtimeSessionId, null);
|
||||
assert.equal(launch.requiresRuntimeIdCapture, true);
|
||||
assert.deepEqual(launch.launch.args.slice(0, 5), [
|
||||
'--bg', '--model', 'opus', '-n', 'TORCH · Core',
|
||||
]);
|
||||
assert.equal(launch.launch.args.includes('--session-id'), false);
|
||||
const mcpIndex = launch.launch.args.indexOf('--mcp-config');
|
||||
const launchMcp = JSON.parse(launch.launch.args[mcpIndex + 1]).mcpServers[`torch-${context.worker}`];
|
||||
assert.equal(launchMcp.command, process.execPath);
|
||||
assert.equal(launchMcp.args.at(-1), context.worker);
|
||||
assert.equal(calls.length, 0, 'creating a launch plan must not execute Claude');
|
||||
assert.equal(adapter.captureRuntimeId({
|
||||
areaId: context.worker, stdout: 'runtime-123\n',
|
||||
}).runtimeSessionId, 'runtime-123');
|
||||
assert.throws(
|
||||
() => adapter.captureRuntimeId({ areaId: context.worker, stdout: 'starting\nruntime-123\n' }),
|
||||
(error) => error.code === 'RUNTIME_ID_NOT_CAPTURED',
|
||||
);
|
||||
assert.equal(adapter.getStatus({ runtimeSessionId: 'runtime-123' }).status, 'idle');
|
||||
assert.equal(calls.length, 1);
|
||||
assert.throws(() => adapter.installHooks(), (error) => error.code === 'RUNTIME_CAPABILITY_UNSUPPORTED');
|
||||
@@ -153,8 +162,11 @@ test('SCN-codex-adapter: turns are resumable, identity-bound, durable, and capab
|
||||
assert.equal(launch.runtimeSessionId, null);
|
||||
assert.equal(launch.requiresRuntimeIdCapture, true);
|
||||
assert.equal(launch.completionState, 'idle');
|
||||
assert.deepEqual(launch.launch.args.slice(0, 7), [
|
||||
'exec', '--json', '--cd', '/tmp/codex-core', '--sandbox', 'workspace-write', '--approve-for-me',
|
||||
assert.deepEqual(launch.launch.args.slice(0, 3), ['--cd', '/tmp/codex-core', '--approve-for-me']);
|
||||
assert.equal(launch.launch.args.includes('--sandbox'), false);
|
||||
assert.equal(launch.launch.args.indexOf('--approve-for-me') < launch.launch.args.indexOf('exec'), true);
|
||||
assert.deepEqual(launch.launch.args.slice(launch.launch.args.indexOf('exec'), launch.launch.args.indexOf('exec') + 2), [
|
||||
'exec', '--json',
|
||||
]);
|
||||
assert.equal(launch.launch.args.includes(`mcp_servers.torch-${context.worker}.command=${JSON.stringify(process.execPath)}`), true);
|
||||
assert.equal(launch.launch.args.some((arg) => arg.includes(MCP_SERVER)), true);
|
||||
@@ -168,7 +180,11 @@ test('SCN-codex-adapter: turns are resumable, identity-bound, durable, and capab
|
||||
areaId: context.worker, runtimeSessionId: 'codex-thread-123', worktree: '/tmp/codex-core',
|
||||
message: 'Continue.', mcp: configuration.mcp,
|
||||
});
|
||||
assert.equal(resumed.launch.args.includes('resume'), true);
|
||||
assert.equal(resumed.launch.args.includes('--sandbox'), false);
|
||||
assert.equal(resumed.launch.args.indexOf('--cd') < resumed.launch.args.indexOf('exec'), true);
|
||||
assert.equal(resumed.launch.args.indexOf('--approve-for-me') < resumed.launch.args.indexOf('exec'), true);
|
||||
assert.equal(resumed.launch.args.indexOf('exec') < resumed.launch.args.indexOf('resume'), true);
|
||||
assert.equal(resumed.launch.args.indexOf('resume') < resumed.launch.args.indexOf('--json'), true);
|
||||
assert.equal(resumed.launch.args.includes('codex-thread-123'), true);
|
||||
assert.equal(resumed.launch.args.some((arg) => arg.includes(MCP_SERVER)), true);
|
||||
|
||||
|
||||
Reference in new issue
Block a user