Files
TORCH/src/runtime/lifecycle.mjs
T

717 lines
35 KiB
JavaScript

import { execFileSync } from 'node:child_process';
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { TorchError } from '../kernel/errors.mjs';
import { readInstallManifest } from '../kernel/install.mjs';
import { loadFleetDefinition } from '../kernel/worktrees.mjs';
import { organizationGraphFromConfig } from '../kernel/organization.mjs';
import { sha256 } from '../kernel/files.mjs';
import {
costCeilingPlanIssues, profileCapabilityIssues, resolveLaunchPolicy, validateRuntimeAdapter,
} from '../adapters/runtime.mjs';
import { hierarchyOrder } from './hierarchy-order.mjs';
const MCP_ENTRY = fileURLToPath(new URL('../mcp/server.mjs', import.meta.url));
function localStateRoot(manifest) {
const record = (manifest.external ?? []).find((entry) => entry.type === 'local-state');
if (!record?.path) throw new TorchError('Installation manifest has no local state root', { code: 'LOCAL_STATE_MISSING' });
return record.path;
}
function worktreesByArea(manifest) {
return new Map((manifest.external ?? [])
.filter((entry) => entry.type === 'worktree')
.map((entry) => [entry.area, entry]));
}
function selectedAreas(roster, only, blockers, ordered = roster.areas) {
if (only === undefined) return ordered;
const selected = Array.isArray(only)
? [...new Set(only.map((id) => typeof id === 'string' ? id.trim() : '').filter(Boolean))]
: [];
if (!selected.length) {
blockers.push({ code: 'FLEET_SELECTION_EMPTY' });
return [];
}
const known = new Set(ordered.map((area) => area.id));
for (const areaId of selected.filter((id) => !known.has(id))) {
blockers.push({ areaId, code: 'FLEET_IDENTITY_MISSING' });
}
const wanted = new Set(selected);
return ordered.filter((area) => wanted.has(area.id));
}
function adapterMap({ adapter, adapters } = {}) {
if (adapters instanceof Map) return adapters;
if (adapters && typeof adapters === 'object') return new Map(Object.entries(adapters));
if (adapter) return new Map([[adapter.name, adapter]]);
return new Map();
}
function configuredRuntime(area, config) {
if (area.id === 'session-manager') {
return config.session_manager?.runtime ?? area.runtime ?? config.runtimes?.default;
}
return config.domains?.find((domain) => domain.id === area.id)?.runtime ?? area.runtime
?? config.runtimes?.default;
}
function configuredProfile(area, config, runtime) {
const runtimeConfig = config.runtimes?.[runtime] ?? {};
const configured = area.id === 'session-manager'
? config.session_manager : config.domains?.find((domain) => domain.id === area.id);
const launchPolicy = resolveLaunchPolicy({
runtime, adapterDefaults: {}, runtimeConfig, identity: configured,
});
return {
model: configured?.model ?? area.model
?? runtimeConfig.model ?? null,
reasoning: configured?.reasoning ?? area.reasoning
?? runtimeConfig.reasoning ?? null,
launchPolicy,
};
}
function organizationContext(config, areaId) {
const graph = organizationGraphFromConfig(config);
const rolesById = new Map(graph.roles.map((role) => [role.id, role]));
const ownRoles = graph.roles.filter((role) => role.identity_id === areaId);
const ownRoleIds = new Set(ownRoles.map((role) => role.id));
const summarizeRole = (role) => ({
roleId: role.id, title: role.title, kind: role.kind,
responsibilities: role.responsibilities, authority: role.authority,
reportsTo: role.reports_to.map((roleId) => rolesById.get(roleId)?.title ?? roleId),
});
const directReports = new Map();
for (const role of graph.roles) {
if (role.identity_id === areaId || !role.reports_to.some((parent) => ownRoleIds.has(parent))) continue;
directReports.set(role.identity_id, summarizeRole(role));
}
const related = (field) => [...new Map(ownRoles.flatMap((role) => role[field]
.map((roleId) => rolesById.get(roleId)).filter(Boolean)
.filter((role) => role.identity_id !== areaId)
.map((role) => [role.identity_id, { identityId: role.identity_id, title: role.title }]))).values()]
.sort((left, right) => left.identityId.localeCompare(right.identityId));
return {
schema: graph.schema, revision: graph.revision, identityId: areaId,
roles: ownRoles.map(summarizeRole),
directReports: [...directReports.entries()].sort(([left], [right]) => left.localeCompare(right))
.map(([identityId, role]) => ({ identityId, ...role })),
coordinates: related('coordinates'), consultsWith: related('consults_with'),
implementationOwnership: graph.implementation_owners
.filter((owner) => owner.identity_id === areaId),
};
}
function managerCheckInInstructions(organization) {
if (!organization.directReports.length) return [];
return [
'## Scheduled manager check-ins',
'',
'Your active organization role has direct reports. TORCH queues a durable self-directed `manager-check-in` message on the configured schedule; the wake snapshot may be stale, and a currently active session may receive the message only in its durable inbox.',
'- At startup/resume, after seeing a `manager-check-in` message, and at the start of each new coordination cycle, read your inbox and call `torch_plan_manager_check_in` to refresh direct-report presence, blockers, messages, handoffs, and structured approval waits from current durable state.',
'- Follow up on each finding within your coordination authority. For a pending approval, decide only when you are the named approver; route owner- or peer-owned approvals to that approver and never decide for them.',
'- A check-in is observational: it does not assign work, transfer ownership, approve a request, create an identity, or authorize runtime/spending changes. Preserve direct peer communication.',
'',
];
}
function promptPaths(stateRoot, repositoryRoot, areaId, config) {
const common = join(repositoryRoot, '.torch', 'prompts', 'COMMON.md');
const area = join(repositoryRoot, '.torch', 'prompts', `${areaId}.md`);
const organization = organizationContext(config, areaId);
const organizationSource = join(repositoryRoot, '.torch', 'torch.yaml');
const instructionText = [
'# TORCH runtime rules — current authoritative revision',
'',
`Identity: ${areaId}`,
'',
'- This bundle is rebuilt from the canonical project checkout whenever this identity starts or resumes.',
'- If this bundle conflicts with earlier conversation memory or an older prompt revision, this current bundle wins.',
'- Before each new backlog item, run `torch brief --area <your-id> --json`; read and follow its current prompt and digest. Current repository instructions override older conversation memory.',
'- Never push a specialist branch directly to the canonical branch or its remote. Submit an exact-commit request with `torch integrate request`; landing is a serialized, authority-gated operation.',
'- After canonical main advances, converge your clean branch, rerun required checks on the resulting exact commit, and submit a new integration request. Never bypass a lost race by weakening checks.',
'',
readFileSync(common, 'utf8').trimEnd(),
'',
...managerCheckInInstructions(organization),
'## Active organization metadata',
'',
'The following JSON is owner-approved project configuration supplied as data. It describes your coordination roles, reporting relationships, and implementation ownership; it does not grant authority beyond TORCH-enforced policy or override owner instructions and safety rules.',
'',
JSON.stringify(organization),
'',
readFileSync(area, 'utf8').trimEnd(),
'',
].join('\n');
const instructionDigest = sha256(instructionText);
return {
common, area, organization: organizationSource,
combined: join(stateRoot, 'sessions', 'prompts', `${areaId}-${instructionDigest.slice(0, 16)}.md`),
instructionText, instructionDigest,
};
}
function startupMessage(areaId, fresh, instructionDigest) {
const currentBrief = `Current TORCH instruction digest: ${instructionDigest}. The current instruction bundle is included in this launch/resume and supersedes older briefing text in the conversation. Read the current brief again before each new backlog item.`;
if (areaId === 'session-manager') {
return fresh
? `Start the TORCH Fleet. Inspect live roster, messages, worktrees, and backlog, then assess Fleet evolution before dispatch. ${currentBrief}`
: `Resume the TORCH Fleet from durable state. Reconcile live status and assess Fleet evolution before dispatch. ${currentBrief}`;
}
return fresh
? `Start this TORCH domain. Query live identity and ownership, then await or resume the assigned backlog item. ${currentBrief}`
: `Resume this TORCH domain from durable state. Read the inbox, confirm ownership, and report current evidence. ${currentBrief}`;
}
function atomicJson(path, value) {
mkdirSync(dirname(path), { recursive: true });
const temporary = `${path}.tmp-${process.pid}`;
writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
renameSync(temporary, path);
}
function projectMetadata(stateRoot) {
const path = join(stateRoot, 'project.json');
if (!existsSync(path)) throw new TorchError('TORCH local project metadata is missing', { code: 'LOCAL_STATE_MISSING' });
try { return { path, value: JSON.parse(readFileSync(path, 'utf8')) }; } catch (error) {
throw new TorchError('TORCH local project metadata is invalid', { code: 'LOCAL_STATE_INVALID', details: error.message });
}
}
function captureSnapshot({ plan, controlPlane, now = () => new Date() }) {
const snapshotPath = join(plan.stateRoot, 'sessions', 'resume.json');
const capturedAt = now().toISOString();
const database = controlPlane.database;
const hasTable = (name) => Boolean(database.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
).get(name));
const snapshot = {
schema: 'torch.dev/runtime-snapshot/v1alpha1', projectId: plan.projectId,
capturedAt, agents: controlPlane.listAgents(),
worktrees: plan.actions.map((action) => action.worktree).filter(Boolean),
resourceLeases: hasTable('resource_leases') ? database.prepare(`
SELECT resource_id AS resourceId, area_id AS areaId, acquired_at AS acquiredAt,
expires_at AS expiresAt FROM resource_leases WHERE released_at IS NULL ORDER BY resource_id, acquired_at
`).all() : [],
worktreeGuards: hasTable('worktree_guards') ? database.prepare(`
SELECT area_id AS areaId, guard_type AS type, reason, created_at AS createdAt
FROM worktree_guards WHERE released_at IS NULL ORDER BY created_at, id
`).all() : [],
integration: hasTable('integration_requests') ? database.prepare(`
SELECT id, source_area AS sourceArea, source_commit AS sourceCommit, state, reason
FROM integration_requests WHERE state NOT IN ('landed', 'superseded') ORDER BY created_at, id
`).all() : [],
};
atomicJson(snapshotPath, snapshot);
return { snapshotPath, snapshot };
}
function writeResumeBrief(plan, snapshot) {
if (!plan.resumeBriefPath) return null;
const lines = [
'# TORCH resume brief', '',
`Captured: ${snapshot.capturedAt}`,
`Project: ${snapshot.projectId}`,
'', '## Fleet identities', '',
...snapshot.agents.map((agent) =>
`- ${agent.areaId}: ${agent.state}; runtime=${agent.runtime ?? 'none'}; task=${agent.currentTask ?? agent.task ?? 'none'}; summary=${agent.summary ?? 'none'}`),
'', '## Worktrees', '',
...(snapshot.worktrees.length ? snapshot.worktrees.map((worktree) =>
`- ${worktree.areaId}: branch=${worktree.branch}; commit=${worktree.commit}; dirty=${worktree.dirtyEntries.length ? worktree.dirtyEntries.join(', ') : 'no'}`) : ['- No managed worktrees recorded.']),
'', '## Open integration', '',
...(snapshot.integration.length ? snapshot.integration.map((request) =>
`- ${request.id}: ${request.sourceArea} ${request.sourceCommit} is ${request.state}${request.reason ? ` (${request.reason})` : ''}`) : ['- None.']),
'', '## Guards and leases', '',
...(snapshot.worktreeGuards.length ? snapshot.worktreeGuards.map((guard) =>
`- guard: ${guard.areaId} ${guard.type} — ${guard.reason}`) : ['- No active worktree guards.']),
...(snapshot.resourceLeases.length ? snapshot.resourceLeases.map((lease) =>
`- lease: ${lease.areaId} holds ${lease.resourceId} until ${lease.expiresAt}`) : ['- No active resource leases.']),
'', 'Resume by checking live identity, inbox, backlog, worktree state, and exact check receipts before dispatch.', '',
];
const temporary = `${plan.resumeBriefPath}.tmp-${process.pid}`;
writeFileSync(temporary, `${lines.join('\n')}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
renameSync(temporary, plan.resumeBriefPath);
return plan.resumeBriefPath;
}
function inspectWorktree(worktree) {
const git = (args) => execFileSync('git', ['-C', worktree.path, ...args], { encoding: 'utf8' }).trim();
const operations = ['MERGE_HEAD', 'REBASE_HEAD', 'CHERRY_PICK_HEAD', 'REVERT_HEAD']
.filter((name) => existsSync(git(['rev-parse', '--git-path', name])));
return {
areaId: worktree.area,
path: worktree.path,
branch: git(['branch', '--show-current']),
commit: git(['rev-parse', 'HEAD']),
dirtyEntries: git(['status', '--porcelain']).split('\n').filter(Boolean),
operations,
};
}
export function planFleetUp({ repositoryRoot, controlPlane, adapter, adapters, fresh = false, only } = {}) {
const { config, roster } = loadFleetDefinition(repositoryRoot);
const hierarchy = hierarchyOrder(roster, config);
const manifest = readInstallManifest(repositoryRoot);
const worktrees = worktreesByArea(manifest);
const stateRoot = localStateRoot(manifest);
const runtimes = adapterMap({ adapter, adapters });
const blockers = [...hierarchy.blockers];
const actions = [];
for (const area of selectedAreas(roster, only, blockers, hierarchy.areas)) {
const worktree = worktrees.get(area.id);
if (!worktree) {
blockers.push({ areaId: area.id, code: 'WORKTREE_MISSING' });
continue;
}
const runtime = configuredRuntime(area, config);
const runtimeAdapter = runtimes.get(runtime);
if (!runtimeAdapter) {
blockers.push({ areaId: area.id, code: 'RUNTIME_ADAPTER_MISSING', runtime });
continue;
}
if (runtimeAdapter.name !== runtime) {
blockers.push({
areaId: area.id, code: 'RUNTIME_ADAPTER_MISMATCH', runtime, adapter: runtimeAdapter.name,
});
continue;
}
validateRuntimeAdapter(runtimeAdapter);
const identity = controlPlane.identity(area.id);
const runtimeIntegration = runtimeAdapter.configure({
repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY,
projectId: manifest.projectId, stateRoot,
});
if (runtimeIntegration?.mutationPerformed !== false
|| !(runtimeIntegration?.mcp || runtimeIntegration?.integration)) {
blockers.push({
areaId: area.id, code: 'RUNTIME_CONFIGURATION_INVALID', runtime,
});
continue;
}
const prompts = promptPaths(stateRoot, repositoryRoot, area.id, config);
const profile = configuredProfile(area, config, runtime);
const unsupported = profileCapabilityIssues(runtimeAdapter, profile);
if (unsupported.length) {
blockers.push({
areaId: area.id, code: 'RUNTIME_PROFILE_UNSUPPORTED', runtime,
adapter: runtimeAdapter.name, profile, capabilities: unsupported,
});
continue;
}
const runtimeConfig = config.runtimes?.[runtime] ?? {};
const { model, reasoning, launchPolicy } = profile;
const background = runtimeConfig.background ?? true;
const shouldResume = !fresh && identity.runtime === runtime && Boolean(identity.runtimeSessionId);
const runtimePlan = shouldResume
? runtimeAdapter.resumeSession({
areaId: area.id, runtimeSessionId: identity.runtimeSessionId, worktree: worktree.path,
model, reasoning, launchPolicy, background,
message: startupMessage(area.id, false, prompts.instructionDigest),
promptFile: prompts.combined, instructionText: prompts.instructionText,
instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp,
integration: runtimeIntegration.integration,
})
: runtimeAdapter.createSession({
areaId: area.id, title: area.title, worktree: worktree.path, promptFile: prompts.combined,
firstMessage: startupMessage(area.id, true, prompts.instructionDigest),
model, reasoning, launchPolicy, background, mcp: runtimeIntegration.mcp,
integration: runtimeIntegration.integration,
});
actions.push({
areaId: area.id, title: area.title, mode: shouldResume ? 'resume' : 'create',
managerIds: [...(hierarchy.managersByIdentity.get(area.id) ?? [])],
runtime: runtimeAdapter.name, runtimeSessionId: runtimePlan.runtimeSessionId,
profile,
requiresRuntimeIdCapture: runtimePlan.requiresRuntimeIdCapture ?? false,
completionState: runtimePlan.completionState ?? 'starting',
worktree: worktree.path, branch: worktree.branch, promptFile: prompts.combined,
promptSources: [prompts.common, prompts.area, prompts.organization], instructionText: prompts.instructionText,
instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp ?? null,
integration: runtimeIntegration.integration ?? null,
launch: runtimePlan.launch,
});
}
return {
action: 'fleet-up', projectId: manifest.projectId, stateRoot, fresh,
only: only ?? null, startupOrder: actions.map((action) => action.areaId), actions, blockers,
canProceed: blockers.length === 0, mutationPerformed: false,
};
}
export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adapters, fresh = false, maxCostUsd } = {}) {
const { config, roster } = loadFleetDefinition(repositoryRoot);
const manifest = readInstallManifest(repositoryRoot);
const worktree = worktreesByArea(manifest).get(areaId);
const area = roster.areas.find((candidate) => candidate.id === areaId);
const blockers = [];
if (maxCostUsd !== undefined && (!Number.isFinite(maxCostUsd) || maxCostUsd <= 0)) {
blockers.push({ areaId, code: 'RUNTIME_COST_CEILING_INVALID', maxUsd: maxCostUsd });
}
if (!area) blockers.push({ areaId, code: 'FLEET_IDENTITY_MISSING' });
if (!worktree) blockers.push({ areaId, code: 'WORKTREE_MISSING' });
if (blockers.length) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers, canProceed: false, mutationPerformed: false,
};
}
const runtimes = adapterMap({ adapter, adapters });
const runtime = configuredRuntime(area, config);
const runtimeAdapter = runtimes.get(runtime);
if (!runtimeAdapter) blockers.push({ areaId, code: 'RUNTIME_ADAPTER_MISSING', runtime });
else if (runtimeAdapter.name !== runtime) {
blockers.push({ areaId, code: 'RUNTIME_ADAPTER_MISMATCH', runtime, adapter: runtimeAdapter.name });
}
if (blockers.length) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers, canProceed: false, mutationPerformed: false,
};
}
validateRuntimeAdapter(runtimeAdapter);
const identity = controlPlane.identity(area.id);
const runtimeIntegration = runtimeAdapter.configure({
repositoryRoot, areaId: area.id, mcpEntry: MCP_ENTRY,
projectId: manifest.projectId, stateRoot: localStateRoot(manifest),
});
if (runtimeIntegration?.mutationPerformed !== false
|| !(runtimeIntegration?.mcp || runtimeIntegration?.integration)) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers: [{ areaId, code: 'RUNTIME_CONFIGURATION_INVALID', runtime }],
canProceed: false, mutationPerformed: false,
};
}
const prompts = promptPaths(localStateRoot(manifest), repositoryRoot, area.id, config);
const profile = configuredProfile(area, config, runtime);
const unsupported = profileCapabilityIssues(runtimeAdapter, profile);
if (unsupported.length) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers: [{
areaId: area.id, code: 'RUNTIME_PROFILE_UNSUPPORTED', runtime,
adapter: runtimeAdapter.name, profile, capabilities: unsupported,
}], canProceed: false, mutationPerformed: false,
};
}
const runtimeConfig = config.runtimes?.[runtime] ?? {};
const { model, reasoning, launchPolicy } = profile;
const background = runtimeConfig.background ?? true;
const shouldResume = !fresh && identity.runtime === runtime && Boolean(identity.runtimeSessionId);
const operation = shouldResume ? 'resumeSession' : 'createSession';
const declaredCostModes = runtimeAdapter.capabilities.perInvocationCostCeiling[operation];
if (maxCostUsd !== undefined && declaredCostModes.length === 0) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers: [{
areaId, code: 'RUNTIME_COST_CEILING_UNSUPPORTED', runtime: runtimeAdapter.name,
operation, maxUsd: maxCostUsd, background,
}], canProceed: false, mutationPerformed: false,
};
}
const runtimePlan = shouldResume
? runtimeAdapter.resumeSession({
areaId: area.id, runtimeSessionId: identity.runtimeSessionId, worktree: worktree.path,
model, reasoning, launchPolicy, background,
message: startupMessage(area.id, false, prompts.instructionDigest),
promptFile: prompts.combined, instructionText: prompts.instructionText,
instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp,
integration: runtimeIntegration.integration,
...(maxCostUsd === undefined ? {} : { maxCostUsd }),
})
: runtimeAdapter.createSession({
areaId: area.id, title: area.title, worktree: worktree.path, promptFile: prompts.combined,
firstMessage: startupMessage(area.id, true, prompts.instructionDigest),
model, reasoning, launchPolicy, background, mcp: runtimeIntegration.mcp,
integration: runtimeIntegration.integration,
...(maxCostUsd === undefined ? {} : { maxCostUsd }),
});
if (maxCostUsd !== undefined) {
const ceilingIssues = costCeilingPlanIssues(runtimeAdapter, operation, maxCostUsd, runtimePlan);
if (ceilingIssues.length) {
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [], blockers: ceilingIssues.map((issue) => ({ areaId, ...issue, background })),
canProceed: false, mutationPerformed: false,
};
}
}
const action = {
areaId: area.id, title: area.title, mode: shouldResume ? 'resume' : 'create',
runtime: runtimeAdapter.name, runtimeSessionId: runtimePlan.runtimeSessionId,
profile,
...(maxCostUsd === undefined ? {} : { costCeiling: runtimePlan.costCeiling }),
requiresRuntimeIdCapture: runtimePlan.requiresRuntimeIdCapture ?? false,
completionState: runtimePlan.completionState ?? 'starting',
worktree: worktree.path, branch: worktree.branch, promptFile: prompts.combined,
promptSources: [prompts.common, prompts.area, prompts.organization], instructionText: prompts.instructionText,
instructionDigest: prompts.instructionDigest, mcp: runtimeIntegration.mcp ?? null,
integration: runtimeIntegration.integration ?? null,
launch: runtimePlan.launch,
};
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [action], blockers, canProceed: true, mutationPerformed: false,
};
}
function writeCombinedPrompt(action) {
mkdirSync(dirname(action.promptFile), { recursive: true });
writeFileSync(action.promptFile, action.instructionText, { encoding: 'utf8', mode: 0o600 });
}
export function startFleet({ plan, controlPlane, executor, adapters } = {}) {
if (!plan?.canProceed) {
throw new TorchError('Fleet startup plan has unresolved blockers', {
code: 'FLEET_NOT_READY_TO_START', details: plan?.blockers ?? [],
});
}
if (typeof executor !== 'function') {
throw new TorchError('Fleet startup requires an explicitly authorized runtime executor', {
code: 'RUNTIME_EXECUTION_NOT_AUTHORIZED',
});
}
const started = [];
const runtimes = adapterMap({ adapters });
for (const action of plan.actions) {
controlPlane.assertIdentity(action.areaId);
writeCombinedPrompt(action);
const result = executor({ ...action.launch, areaId: action.areaId, runtimeSessionId: action.runtimeSessionId });
if (result?.status !== undefined && result.status !== 0) {
controlPlane.reportStatus({
areaId: action.areaId, state: 'offline', runtime: action.runtime,
runtimeSessionId: action.runtimeSessionId, summary: 'Runtime launch failed.',
});
throw new TorchError(`Fleet startup failed for ${action.areaId}`, {
code: 'FLEET_START_FAILED',
details: { areaId: action.areaId, status: result.status, stderr: result.stderr ?? null, started },
});
}
let runtimeSessionId = action.runtimeSessionId;
if (action.requiresRuntimeIdCapture) {
const runtimeAdapter = runtimes.get(action.runtime);
if (!runtimeAdapter) {
controlPlane.reportStatus({
areaId: action.areaId, state: 'offline', runtime: action.runtime,
summary: 'Runtime launched but its durable session ID could not be captured.',
});
throw new TorchError(`Fleet startup could not capture a runtime ID for ${action.areaId}`, {
code: 'RUNTIME_ADAPTER_MISSING', details: { areaId: action.areaId, runtime: action.runtime, started },
});
}
try {
runtimeSessionId = runtimeAdapter.captureRuntimeId({
areaId: action.areaId, runtimeSessionId, stdout: result?.stdout, output: result?.output,
}).runtimeSessionId;
} catch (error) {
controlPlane.reportStatus({
areaId: action.areaId, state: 'offline', runtime: action.runtime,
summary: 'Runtime launch completed without a capturable durable session ID.',
});
throw new TorchError(`Fleet startup could not capture a runtime ID for ${action.areaId}`, {
code: 'FLEET_START_FAILED',
details: { areaId: action.areaId, cause: error.code ?? error.message, started },
});
}
}
const identity = controlPlane.reportStatus({
areaId: action.areaId, state: action.completionState, runtime: action.runtime,
runtimeSessionId, summary: action.completionState === 'idle'
? `${action.mode} turn completed` : `${action.mode} requested`,
});
started.push({ areaId: action.areaId, runtimeSessionId: identity.runtimeSessionId, mode: action.mode });
}
if (plan.stateRoot) {
const metadata = projectMetadata(plan.stateRoot);
if (metadata.value.detachedAt) {
atomicJson(metadata.path, { ...metadata.value, detachedAt: null, attachedAt: new Date().toISOString() });
}
}
return { projectId: plan.projectId, started, mutationPerformed: true };
}
export function planFleetDown({ repositoryRoot, controlPlane, adapters } = {}) {
const { config, roster } = loadFleetDefinition(repositoryRoot);
const hierarchy = hierarchyOrder(roster, config);
const manifest = readInstallManifest(repositoryRoot);
const worktrees = worktreesByArea(manifest);
const runtimes = adapterMap({ adapters });
const adapterRegistryProvided = adapters !== undefined;
const database = controlPlane.database;
const hasTable = (name) => Boolean(database.prepare(
"SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?",
).get(name));
const actions = [...hierarchy.areas].reverse().map((area) => {
const identity = controlPlane.identity(area.id);
const worktree = worktrees.get(area.id);
return {
areaId: area.id, state: identity.state, runtime: identity.runtime,
managerIds: [...(hierarchy.managersByIdentity.get(area.id) ?? [])],
runtimeSessionId: identity.runtimeSessionId, currentTask: identity.currentTask,
requiresRuntimeStop: identity.runtimeSessionId
? runtimes.get(identity.runtime)?.capabilities.stopSession !== false
: false,
runtimeAdapterMissing: Boolean(
adapterRegistryProvided && identity.runtimeSessionId && !runtimes.has(identity.runtime),
),
worktree: worktree ? inspectWorktree(worktree) : null,
};
});
const blockers = [...hierarchy.blockers, ...actions
.filter((action) => ['starting', 'working', 'stopping'].includes(action.state))
.map((action) => ({ areaId: action.areaId, state: action.state, code: 'ACTIVE_SESSION' }))];
blockers.push(...actions
.filter((action) => action.runtimeAdapterMissing)
.map((action) => ({ areaId: action.areaId, runtime: action.runtime, code: 'RUNTIME_ADAPTER_MISSING' })));
blockers.push(...actions
.filter((action) => action.worktree?.operations.length)
.map((action) => ({
areaId: action.areaId, code: 'GIT_OPERATION_ACTIVE', operations: action.worktree.operations,
})));
if (hasTable('resource_leases')) {
blockers.push(...database.prepare(`
SELECT resource_id AS resourceId, area_id AS areaId, expires_at AS expiresAt
FROM resource_leases WHERE released_at IS NULL ORDER BY resource_id, acquired_at
`).all().map((lease) => ({ ...lease, code: 'RESOURCE_LEASE_ACTIVE' })));
}
if (hasTable('worktree_guards')) {
blockers.push(...database.prepare(`
SELECT area_id AS areaId, guard_type AS type, reason
FROM worktree_guards WHERE released_at IS NULL AND guard_type IN ('check', 'measurement')
ORDER BY created_at, id
`).all().map((guard) => ({ ...guard, code: 'WORKTREE_GUARD_ACTIVE' })));
}
if (hasTable('integration_requests')) {
blockers.push(...database.prepare(`
SELECT id, source_area AS areaId FROM integration_requests WHERE state = 'landing' ORDER BY created_at, id
`).all().map((request) => ({ ...request, code: 'INTEGRATION_LANDING_ACTIVE' })));
}
const managerWorktree = worktrees.get('session-manager');
return {
action: 'fleet-down', projectId: manifest.projectId, stateRoot: localStateRoot(manifest),
resumeBriefPath: managerWorktree ? join(managerWorktree.path, '.torch', 'RESUME-BRIEF.md') : null,
shutdownOrder: actions.map((action) => action.areaId),
actions, blockers, canProceed: blockers.length === 0, mutationPerformed: false,
};
}
export function stopFleet({ plan, controlPlane, stopRuntime, now = () => new Date() } = {}) {
if (!plan?.canProceed) {
throw new TorchError('Fleet wind-down refused while safety conditions remain unresolved', {
code: 'FLEET_NOT_READY_TO_STOP', details: plan?.blockers ?? [],
});
}
if (typeof stopRuntime !== 'function') {
throw new TorchError('Fleet wind-down requires an explicitly authorized runtime stopper', {
code: 'RUNTIME_EXECUTION_NOT_AUTHORIZED',
});
}
const stopped = [];
for (const action of plan.actions) {
if (action.state !== 'offline') {
for (const managerId of action.managerIds ?? []) {
controlPlane.sendMessage({
sender: action.areaId, recipient: managerId,
body: `Final status: ${action.state}. ${action.worktree
? `Branch ${action.worktree.branch} at ${action.worktree.commit}; ${action.worktree.dirtyEntries.length} dirty entries.`
: 'No managed worktree.'}`,
references: { commit: action.worktree?.commit },
});
}
}
if (!action.runtimeSessionId || action.state === 'offline' || action.requiresRuntimeStop === false) {
if (action.state !== 'offline') {
controlPlane.reportStatus({
areaId: action.areaId, state: 'offline', summary: 'No resident runtime process to stop.',
task: action.currentTask,
});
}
stopped.push({
areaId: action.areaId, runtimeSessionId: action.runtimeSessionId,
alreadyOffline: true, residentProcess: action.requiresRuntimeStop !== false,
});
continue;
}
controlPlane.reportStatus({
areaId: action.areaId, state: 'stopping', summary: 'TORCH wind-down requested.', task: action.currentTask,
});
const result = stopRuntime(action);
if (result?.stopped === false || (result?.status !== undefined && result.status !== 0)) {
controlPlane.reportStatus({
areaId: action.areaId, state: 'waiting', summary: 'Runtime stop failed.', task: action.currentTask,
});
throw new TorchError(`Fleet wind-down failed for ${action.areaId}`, {
code: 'FLEET_STOP_FAILED', details: { areaId: action.areaId, result },
});
}
controlPlane.reportStatus({
areaId: action.areaId, state: 'offline', summary: 'Runtime stopped cleanly.', task: action.currentTask,
});
stopped.push({ areaId: action.areaId, runtimeSessionId: action.runtimeSessionId, alreadyOffline: false });
}
const captured = captureSnapshot({ plan, controlPlane, now });
const resumeBriefPath = writeResumeBrief(plan, captured.snapshot);
return {
projectId: plan.projectId, stopped, snapshotPath: captured.snapshotPath,
resumeBriefPath, mutationPerformed: true,
};
}
export function captureFleet({ repositoryRoot, controlPlane, now = () => new Date() } = {}) {
const plan = planFleetDown({ repositoryRoot, controlPlane });
const { snapshotPath } = captureSnapshot({ plan, controlPlane, now });
return {
action: 'capture', projectId: plan.projectId, snapshotPath,
agents: controlPlane.listAgents(), mutationPerformed: true,
};
}
export function createFleetBrief({ repositoryRoot, controlPlane, areaId } = {}) {
const { config, roster } = loadFleetDefinition(repositoryRoot);
const manifest = readInstallManifest(repositoryRoot);
const selected = areaId
? roster.areas.filter((area) => area.id === areaId)
: hierarchyOrder(roster, config).areas;
if (!selected.length) {
throw new TorchError(`Unknown Fleet identity: ${areaId}`, { code: 'UNKNOWN_FLEET_IDENTITY' });
}
return {
schema: 'torch.dev/fleet-brief/v1alpha1', mutationPerformed: false,
areas: selected.map((area) => {
const prompts = promptPaths(localStateRoot(manifest), repositoryRoot, area.id, config);
return {
areaId: area.id, title: area.title, identity: controlPlane.identity(area.id),
promptSources: [prompts.common, prompts.area, prompts.organization],
instructionDigest: prompts.instructionDigest,
prompt: prompts.instructionText,
};
}),
};
}
export function planFleetDetach({ repositoryRoot, controlPlane, adapters } = {}) {
const plan = planFleetDown({ repositoryRoot, controlPlane, adapters });
return { ...plan, action: 'fleet-detach' };
}
export function detachFleet({ plan, controlPlane, stopRuntime, now = () => new Date() } = {}) {
if (plan?.action !== 'fleet-detach') {
throw new TorchError('Fleet detach requires a reviewed detach plan', { code: 'FLEET_DETACH_PLAN_REQUIRED' });
}
const stopped = stopFleet({ plan, controlPlane, stopRuntime, now });
const metadata = projectMetadata(plan.stateRoot);
const detachedAt = now().toISOString();
atomicJson(metadata.path, { ...metadata.value, detachedAt });
return {
action: 'fleet-detach', projectId: plan.projectId, detachedAt,
stopped: stopped.stopped, snapshotPath: stopped.snapshotPath,
preserved: ['tracked organization', 'worktrees', 'branches', 'local state'],
mutationPerformed: true,
};
}