Add owner-gated fleet evolution

This commit is contained in:
thePR0M3TH3AN committed 2026-09-27 17:39:12 -04:00
1 parent acb4dc401d
commit d1cd79cccb
22 files changed
+888 -11

No files matched your search

+78 -1
View File
@@ -19,13 +19,14 @@ import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { writeNewFile } from './kernel/files.mjs';
import { createWorktrees, planWorktrees } from './kernel/worktrees.mjs';
import { planFleetDown, planFleetUp, startFleet, stopFleet } from './runtime/lifecycle.mjs';
import { planAreaUp, planFleetDown, planFleetUp, startFleet, stopFleet } from './runtime/lifecycle.mjs';
import { ResourceService } from './resources/service.mjs';
import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs';
import { observeProject } from './observability/snapshot.mjs';
import { ContextTelemetryService } from './telemetry/context.mjs';
import { ScheduleService } from './schedules/service.mjs';
import { createFleetDesignBrief } from './design/brief.mjs';
import { FleetEvolutionService } from './evolution/service.mjs';
const HELP = `TORCH — portable agent fleet
@@ -90,6 +91,13 @@ Usage:
torch schedules runs [--id <schedule>] [--json]
torch schedules plan --id <schedule> --actor <id|owner> [--json]
torch schedules run --id <schedule> --actor <id|owner> [--yes] [--json]
torch fleet changes [--state <state>] [--json]
torch fleet get --change <id> [--json]
torch fleet propose --from session-manager --proposal <path> [--json]
torch fleet approve --change <id> --by <owner> --yes [--json]
torch fleet plan --change <id> [--json]
torch fleet activate --change <id> --by <owner> --yes [--json]
torch fleet start --change <id> [--fresh] [--dry-run] --yes [--json]
torch doctor [--json]
torch uninstall [--dry-run] [--purge] [--json]
`;
@@ -132,6 +140,13 @@ function loadProposal(cwd, proposalPath) {
}
}
function loadJsonFile(cwd, inputPath, { label = 'JSON input', code = 'INPUT_INVALID' } = {}) {
if (!inputPath) throw new TorchError(`${label} path is required`, { code });
try { return JSON.parse(readFileSync(resolve(cwd, inputPath), 'utf8')); } catch (error) {
throw new TorchError(`Cannot read ${label}: ${inputPath}`, { code, details: error.message });
}
}
function withControlPlane(repository, env, callback) {
const controlPlane = openControlPlane({ repositoryRoot: repository.root, env });
try {
@@ -168,6 +183,7 @@ function runCandidateAcceptance(candidateRoot, spawn) {
['init-analyze', ['SCN-init-read-only']],
['spec-aware-design', ['SCN-spec-fleet-design', 'SCN-cli-spec-design']],
['ai-fleet-bootstrap', ['SCN-ai-fleet-bootstrap']],
['fleet-evolution', ['SCN-fleet-evolution', 'SCN-cli-fleet-evolution']],
['review-install-roster', ['SCN-cli-domain-review']],
['branches-worktrees', ['SCN-worktree-bootstrap']],
['runtime-identities', ['SCN-mixed-runtime']],
@@ -534,6 +550,67 @@ export async function runCli(argv = process.argv.slice(2), {
})), { json });
return 0;
}
if (command === 'fleet') {
const operation = argv[1] ?? 'changes';
const control = openControlPlane({ repositoryRoot: repository.root, env });
try {
const evolution = new FleetEvolutionService({ repositoryRoot: repository.root, controlPlane: control });
const changeId = optionValue(argv, '--change');
if (operation === 'changes') print({ changes: evolution.list({ state: optionValue(argv, '--state') }) }, { json });
else if (operation === 'get') print(evolution.get(changeId), { json });
else if (operation === 'propose') {
const input = loadJsonFile(cwd, optionValue(argv, '--proposal'), {
label: 'Fleet domain proposal', code: 'FLEET_PROPOSAL_INVALID',
});
print(evolution.proposeDomain({
...input, proposer: optionValue(argv, '--from') ?? input.proposer,
}), { json });
} else if (operation === 'approve') {
if (!argv.includes('--yes')) {
throw new TorchError('Owner approval of a Fleet change requires explicit --yes.', { code: 'APPROVAL_REQUIRED' });
}
print(evolution.approve({ changeId, approvedBy: optionValue(argv, '--by') }), { json });
} else if (operation === 'plan') print(evolution.planActivation(changeId), { json });
else if (operation === 'activate') {
if (!argv.includes('--yes')) {
throw new TorchError('Fleet activation commits configuration and creates a worktree. Review fleet plan, then use --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(evolution.activate({ changeId, approvedBy: optionValue(argv, '--by') }), { json });
} else if (operation === 'start') {
const change = evolution.get(changeId);
if (change.state !== 'active') {
throw new TorchError('Only an active Fleet domain can start a runtime session', {
code: 'FLEET_CHANGE_STATE_CONFLICT', details: { state: change.state },
});
}
const adapters = runtimeAdapters();
const plan = planAreaUp({
repositoryRoot: repository.root, controlPlane: control, areaId: change.domain.id,
adapters, fresh: argv.includes('--fresh'),
});
if (argv.includes('--dry-run')) {
print(plan, { json });
return plan.canProceed ? 0 : 1;
}
if (!argv.includes('--yes')) {
throw new TorchError('Starting the new domain invokes its configured AI runtime. Review --dry-run, then use --yes.', {
code: 'APPROVAL_REQUIRED',
});
}
print(startFleet({
plan, controlPlane: control, adapters,
executor: (launch) => spawn(launch.command, launch.args, {
cwd: launch.cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}),
}), { json });
} else throw new TorchError(`Unknown fleet operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
return 0;
} finally {
control.close();
}
}
if (command === 'backlog') {
const operation = argv[1] ?? 'list';
const control = openControlPlane({ repositoryRoot: repository.root, env });
+18
View File
@@ -78,7 +78,9 @@ function rowToAgent(row, rosterAgent) {
sharedPaths: rosterAgent.shared_paths ?? [],
neighbours: rosterAgent.neighbours ?? [],
requiredChecks: rosterAgent.required_checks ?? [],
resources: rosterAgent.resources ?? [],
runtime: row?.runtime ?? null,
model: rosterAgent.model ?? null,
runtimeSessionId: row?.runtime_session_id ?? null,
state: row?.state ?? 'offline',
summary: row?.summary ?? null,
@@ -224,11 +226,27 @@ export class ControlPlane {
for (const areaId of this.agents.keys()) insert.run(areaId, 'offline');
}
refreshRoster() {
const roster = readJson(join(this.repositoryRoot, '.torch', 'roster.yaml'), 'INVALID_TORCH_ROSTER');
const agents = new Map((roster.areas ?? []).map((area) => [area.id, area]));
if (!agents.has('session-manager')) {
throw new TorchError('Roster does not contain the required session-manager identity', {
code: 'INVALID_TORCH_ROSTER',
});
}
const insert = this.database.prepare('INSERT OR IGNORE INTO identities (area_id, state) VALUES (?, ?)');
for (const areaId of agents.keys()) insert.run(areaId, 'offline');
this.roster = roster;
this.agents = agents;
return { schema: roster.schema, areaIds: [...agents.keys()] };
}
close() {
this.database.close();
}
assertIdentity(areaId) {
this.refreshRoster();
const normalized = requiredText(areaId, 'areaId');
if (!this.agents.has(normalized)) {
throw new TorchError(`Unknown Fleet identity: ${normalized}`, {
+1
View File
@@ -77,6 +77,7 @@ export function createFleetDesignBrief({ repository, analysis, baseline } = {})
domainFields: [
'id', 'title', 'kind', 'scope', 'not_scope', 'owned_paths', 'shared_paths',
'neighbours', 'required_checks', 'runtime', 'evidence',
'resources',
],
topLevelFields: [
'schema', 'generatedAt', 'repository', 'review', 'specifications', 'domains',
+411
View File
@@ -0,0 +1,411 @@
import { execFileSync } from 'node:child_process';
import { createHash, randomUUID } from 'node:crypto';
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { dirname, isAbsolute, join, resolve } from 'node:path';
import { TorchError } from '../kernel/errors.mjs';
import { readInstallManifest } from '../kernel/install.mjs';
import { ensureTaskIgnored, removeTaskIgnore } from '../kernel/worktrees.mjs';
const CHANGE_STATES = Object.freeze(['proposed', 'approved', 'provisioning', 'active', 'rejected']);
function requiredText(value, name) {
if (typeof value !== 'string' || !value.trim()) {
throw new TorchError(`${name} must be a non-empty string`, {
code: 'INVALID_FLEET_CHANGE', details: { field: name },
});
}
return value.trim();
}
function textList(value, name, { required = false } = {}) {
if (!Array.isArray(value) || (required && value.length === 0)
|| value.some((entry) => typeof entry !== 'string' || !entry.trim())) {
throw new TorchError(`${name} must be ${required ? 'a non-empty' : 'an'} array of non-empty strings`, {
code: 'INVALID_FLEET_CHANGE', details: { field: name },
});
}
return [...new Set(value.map((entry) => entry.trim()))];
}
function readJson(path, code) {
try { return JSON.parse(readFileSync(path, 'utf8')); } catch (error) {
throw new TorchError(`Cannot read ${path}`, { code, details: error.message });
}
}
function sha256(content) {
return createHash('sha256').update(content).digest('hex');
}
function git(root, args, { optional = false } = {}) {
try {
return execFileSync('git', ['-C', root, ...args], {
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
}).trim();
} catch (error) {
if (optional) return '';
throw new TorchError(`Git command failed: git ${args.join(' ')}`, {
code: 'GIT_COMMAND_FAILED', details: error.stderr?.toString().trim() || error.message,
});
}
}
function expandPath(value, root) {
if (value === '~') return homedir();
if (value.startsWith('~/')) return join(homedir(), value.slice(2));
return isAbsolute(value) ? value : resolve(root, value);
}
function ownershipPrefixes(pattern) {
const normalized = pattern.replaceAll('\\', '/');
const wildcard = normalized.search(/[?*[]/);
return (wildcard === -1 ? normalized : normalized.slice(0, wildcard)).replace(/\/$/, '');
}
function pathsOverlap(left, right) {
if (left === right) return true;
const a = ownershipPrefixes(left);
const b = ownershipPrefixes(right);
return Boolean(a && b && (a.startsWith(`${b}/`) || b.startsWith(`${a}/`)));
}
function normalizeDomain(input) {
const id = requiredText(input?.id, 'domain.id');
if (!/^[a-z0-9][a-z0-9-]*$/.test(id) || id === 'session-manager') {
throw new TorchError(`Invalid proposed domain ID: ${id}`, {
code: 'INVALID_FLEET_CHANGE', details: { field: 'domain.id' },
});
}
const runtime = requiredText(input.runtime ?? 'claude', 'domain.runtime');
const model = input.model === undefined || input.model === null
? null : requiredText(input.model, 'domain.model');
const branch = input.branch === undefined || input.branch === null
? null : requiredText(input.branch, 'domain.branch');
const worktreeName = input.worktree_name === undefined || input.worktree_name === null
? null : requiredText(input.worktree_name, 'domain.worktree_name');
if (worktreeName && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(worktreeName)) {
throw new TorchError(`Invalid worktree name: ${worktreeName}`, {
code: 'INVALID_FLEET_CHANGE', details: { field: 'domain.worktree_name' },
});
}
return {
id,
title: requiredText(input.title, 'domain.title'),
kind: requiredText(input.kind ?? 'development', 'domain.kind'),
scope: textList(input.scope, 'domain.scope', { required: true }),
not_scope: textList(input.not_scope ?? [], 'domain.not_scope'),
owned_paths: textList(input.owned_paths, 'domain.owned_paths', { required: true }),
shared_paths: textList(input.shared_paths ?? [], 'domain.shared_paths'),
neighbours: textList(input.neighbours ?? [], 'domain.neighbours'),
required_checks: textList(input.required_checks ?? [], 'domain.required_checks'),
resources: textList(input.resources ?? [], 'domain.resources'),
runtime,
model,
branch,
worktree_name: worktreeName,
};
}
function rowToChange(row) {
if (!row) return null;
return {
id: row.id, state: row.state, proposer: row.proposer,
createdAt: row.created_at, updatedAt: row.updated_at, baseCommit: row.base_commit,
approvedBy: row.approved_by, approvedAt: row.approved_at,
activationCommit: row.activation_commit, domain: JSON.parse(row.domain_json),
rationale: row.rationale, expectedBenefit: JSON.parse(row.expected_benefit_json),
evidence: JSON.parse(row.evidence_json),
};
}
function promptFor(domain) {
return [
`# ${domain.title}`, '', `Area ID: ${domain.id}`, '', '## Owns', '',
...domain.scope.map((item) => `- ${item}`), '', '## Does not own', '',
...(domain.not_scope.length ? domain.not_scope : ['No exclusions recorded.']).map((item) => `- ${item}`),
'', '## Neighbours', '',
...(domain.neighbours.length ? domain.neighbours : ['None identified.']).map((item) => `- ${item}`),
'', '## Resources', '',
...(domain.resources.length ? domain.resources : ['No scarce resources declared.']).map((item) => `- ${item}`),
'', '## First move', '',
'Query live ownership, read the assigned backlog item, and report current repository evidence to the Session Manager.',
'',
].join('\n');
}
function initialize(database) {
database.exec(`
CREATE TABLE IF NOT EXISTS fleet_changes (
id TEXT PRIMARY KEY,
state TEXT NOT NULL,
proposer TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
base_commit TEXT NOT NULL,
approved_by TEXT,
approved_at TEXT,
activation_commit TEXT,
domain_json TEXT NOT NULL,
rationale TEXT NOT NULL,
expected_benefit_json TEXT NOT NULL,
evidence_json TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS fleet_changes_state_created
ON fleet_changes(state, created_at, id);
`);
}
export class FleetEvolutionService {
constructor({ repositoryRoot, controlPlane, clock = () => new Date(), idFactory = randomUUID } = {}) {
if (!controlPlane) throw new TorchError('Fleet evolution requires the TORCH control plane', { code: 'CONTROL_PLANE_REQUIRED' });
this.repositoryRoot = resolve(repositoryRoot ?? controlPlane.repositoryRoot);
this.controlPlane = controlPlane;
this.database = controlPlane.database;
this.clock = clock;
this.idFactory = idFactory;
initialize(this.database);
}
get(changeId) {
const id = requiredText(changeId, 'changeId');
const change = rowToChange(this.database.prepare('SELECT * FROM fleet_changes WHERE id = ?').get(id));
if (!change) throw new TorchError(`Unknown Fleet change: ${id}`, { code: 'FLEET_CHANGE_NOT_FOUND' });
return change;
}
list({ state } = {}) {
if (state && !CHANGE_STATES.includes(state)) {
throw new TorchError(`Invalid Fleet change state: ${state}`, { code: 'INVALID_FLEET_CHANGE_STATE' });
}
const rows = state
? this.database.prepare('SELECT * FROM fleet_changes WHERE state = ? ORDER BY created_at, id').all(state)
: this.database.prepare('SELECT * FROM fleet_changes ORDER BY created_at, id').all();
return rows.map(rowToChange);
}
proposeDomain({ proposer, domain: input, rationale, expectedBenefit, evidence } = {}) {
const actor = this.controlPlane.assertIdentity(proposer);
if (actor !== 'session-manager') {
throw new TorchError('Only the Session Manager may propose a persistent Fleet domain', {
code: 'FLEET_CHANGE_AUTHORITY_REQUIRED', details: { actor },
});
}
const domain = normalizeDomain(input);
const agents = this.controlPlane.listAgents();
if (agents.some((agent) => agent.areaId === domain.id)) {
throw new TorchError(`Fleet identity already exists: ${domain.id}`, { code: 'FLEET_IDENTITY_EXISTS' });
}
for (const neighbour of domain.neighbours) this.controlPlane.assertIdentity(neighbour);
const benefit = {
summary: requiredText(expectedBenefit?.summary, 'expectedBenefit.summary'),
recurringWork: requiredText(expectedBenefit?.recurringWork, 'expectedBenefit.recurringWork'),
contextLocality: requiredText(expectedBenefit?.contextLocality, 'expectedBenefit.contextLocality'),
coordinationCost: requiredText(expectedBenefit?.coordinationCost, 'expectedBenefit.coordinationCost'),
};
const evidenceList = textList(evidence, 'evidence', { required: true });
const collisions = agents.flatMap((agent) => domain.owned_paths.flatMap((proposedPath) =>
agent.ownedPaths.filter((existingPath) => pathsOverlap(proposedPath, existingPath))
.map((existingPath) => ({ areaId: agent.areaId, proposedPath, existingPath }))));
if (collisions.length) {
throw new TorchError('A new domain cannot silently overlap existing owned paths; propose a split or ownership change instead', {
code: 'FLEET_OWNERSHIP_COLLISION', details: collisions,
});
}
const config = readJson(join(this.repositoryRoot, '.torch', 'torch.yaml'), 'CONFIG_INVALID');
if (!config.runtimes?.[domain.runtime]) {
throw new TorchError(`Runtime is not configured for the Fleet: ${domain.runtime}`, {
code: 'FLEET_RUNTIME_NOT_CONFIGURED', details: { runtime: domain.runtime },
});
}
const knownChecks = new Set((config.checks ?? []).map((check) => check.id));
const unknownChecks = domain.required_checks.filter((check) => !knownChecks.has(check));
if (unknownChecks.length) {
throw new TorchError('Proposed domain references unknown checks', {
code: 'FLEET_CHECK_NOT_CONFIGURED', details: { checks: unknownChecks },
});
}
const knownResources = new Set((config.resources ?? []).map((resource) => resource.id));
const unknownResources = domain.resources.filter((resource) => !knownResources.has(resource));
if (unknownResources.length) {
throw new TorchError('Proposed domain references unknown resources', {
code: 'FLEET_RESOURCE_NOT_CONFIGURED', details: { resources: unknownResources },
});
}
const now = this.clock().toISOString();
const record = {
id: this.idFactory(), state: 'proposed', proposer: actor, createdAt: now, updatedAt: now,
baseCommit: git(this.repositoryRoot, ['rev-parse', 'HEAD']), approvedBy: null,
approvedAt: null, activationCommit: null, domain,
rationale: requiredText(rationale, 'rationale'), expectedBenefit: benefit, evidence: evidenceList,
};
this.database.prepare(`
INSERT INTO fleet_changes (
id, state, proposer, created_at, updated_at, base_commit, approved_by,
approved_at, activation_commit, domain_json, rationale,
expected_benefit_json, evidence_json
) VALUES (?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, ?, ?)
`).run(
record.id, record.state, record.proposer, record.createdAt, record.updatedAt,
record.baseCommit, JSON.stringify(record.domain), record.rationale,
JSON.stringify(record.expectedBenefit), JSON.stringify(record.evidence),
);
this.controlPlane.audit({
actorId: actor, operation: 'fleet-change.propose-domain', entityType: 'fleet-change', entityId: record.id,
details: { domainId: domain.id, baseCommit: record.baseCommit },
});
return record;
}
approve({ changeId, approvedBy } = {}) {
const change = this.get(changeId);
if (change.state !== 'proposed') {
throw new TorchError(`Fleet change ${change.id} is ${change.state}, not proposed`, { code: 'FLEET_CHANGE_STATE_CONFLICT' });
}
const owner = requiredText(approvedBy, 'approvedBy');
const now = this.clock().toISOString();
this.database.prepare(`
UPDATE fleet_changes SET state = 'approved', approved_by = ?, approved_at = ?, updated_at = ?
WHERE id = ? AND state = 'proposed'
`).run(owner, now, now, change.id);
this.controlPlane.audit({
actorId: 'session-manager', operation: 'fleet-change.owner-approved', entityType: 'fleet-change', entityId: change.id,
details: { approvedBy: owner },
});
return this.get(change.id);
}
planActivation(changeId) {
const change = this.get(changeId);
const config = readJson(join(this.repositoryRoot, '.torch', 'torch.yaml'), 'CONFIG_INVALID');
const roster = readJson(join(this.repositoryRoot, '.torch', 'roster.yaml'), 'ROSTER_INVALID');
const currentHead = git(this.repositoryRoot, ['rev-parse', 'HEAD']);
const dirtyEntries = git(this.repositoryRoot, ['status', '--porcelain']).split('\n').filter(Boolean);
const trackedConfig = ['.torch/torch.yaml', '.torch/roster.yaml'].every((path) =>
Boolean(git(this.repositoryRoot, ['ls-files', '--error-unmatch', path], { optional: true })));
const prefix = config.git?.branch_prefix ?? 'torch/';
const branch = change.domain.branch ?? `${prefix}${change.domain.id}`;
const parent = expandPath(config.paths.worktree_parent, this.repositoryRoot);
const worktreeName = change.domain.worktree_name ?? change.domain.id;
const worktree = change.domain.worktree_name
? join(parent, worktreeName) : join(parent, config.project.id, worktreeName);
const blockers = [];
if (change.state !== 'approved' && change.state !== 'provisioning') blockers.push({ type: 'state', actual: change.state });
if (change.state === 'approved' && currentHead !== change.baseCommit) {
blockers.push({ type: 'stale-head', expected: change.baseCommit, actual: currentHead });
}
if (change.state === 'approved' && dirtyEntries.length) blockers.push({ type: 'dirty-main', entries: dirtyEntries });
if (!trackedConfig) blockers.push({ type: 'configuration-not-committed' });
if (config.domains.some((domain) => domain.id === change.domain.id) && change.state !== 'provisioning') {
blockers.push({ type: 'domain-already-configured', domainId: change.domain.id });
}
if (roster.areas.some((area) => area.id === change.domain.id) && change.state !== 'provisioning') {
blockers.push({ type: 'identity-already-rostered', domainId: change.domain.id });
}
if (git(this.repositoryRoot, ['branch', '--list', branch], { optional: true })) {
blockers.push({ type: 'branch-exists', branch });
}
if (existsSync(worktree)) blockers.push({ type: 'worktree-path-exists', worktree });
return {
action: 'activate-domain', changeId: change.id, domainId: change.domain.id,
state: change.state, branch, worktree, currentHead, baseCommit: change.baseCommit,
blockers, canProceed: blockers.length === 0, mutationPerformed: false,
};
}
activate({ changeId, approvedBy } = {}) {
const change = this.get(changeId);
if (change.approvedBy !== requiredText(approvedBy, 'approvedBy')) {
throw new TorchError('Fleet activation must name the same owner who approved the change', {
code: 'FLEET_CHANGE_AUTHORITY_REQUIRED', details: { approvedBy: change.approvedBy },
});
}
const plan = this.planActivation(change.id);
if (!plan.canProceed) {
throw new TorchError('Fleet domain activation is blocked', {
code: 'FLEET_CHANGE_BLOCKED', details: plan.blockers,
});
}
if (change.state === 'approved') {
const manifest = readInstallManifest(this.repositoryRoot);
const ignore = ensureTaskIgnored(this.repositoryRoot, manifest.installationId);
try {
this.commitConfiguration(change, plan, ignore);
} catch (error) {
removeTaskIgnore(ignore, manifest.installationId);
throw error;
}
}
const current = this.get(change.id);
mkdirSync(dirname(plan.worktree), { recursive: true });
git(this.repositoryRoot, ['worktree', 'add', '-b', plan.branch, plan.worktree, current.activationCommit]);
writeFileSync(join(plan.worktree, '.task'), `Unassigned · ${change.domain.id}\n`, { encoding: 'utf8', flag: 'wx' });
const now = this.clock().toISOString();
this.database.prepare("UPDATE fleet_changes SET state = 'active', updated_at = ? WHERE id = ?")
.run(now, change.id);
this.controlPlane.refreshRoster();
this.controlPlane.audit({
actorId: 'session-manager', operation: 'fleet-change.activate-domain', entityType: 'fleet-change', entityId: change.id,
details: { domainId: change.domain.id, branch: plan.branch, worktree: plan.worktree },
});
return {
...this.get(change.id), branch: plan.branch, worktree: plan.worktree,
identity: this.controlPlane.identity(change.domain.id),
runtime: { state: 'offline', next: `Start ${change.domain.id} through the approved ${change.domain.runtime} adapter.` },
mutationPerformed: true,
};
}
commitConfiguration(change, plan, ignore) {
const configPath = join(this.repositoryRoot, '.torch', 'torch.yaml');
const rosterPath = join(this.repositoryRoot, '.torch', 'roster.yaml');
const config = readJson(configPath, 'CONFIG_INVALID');
const roster = readJson(rosterPath, 'ROSTER_INVALID');
const manifest = readInstallManifest(this.repositoryRoot);
const domain = change.domain;
config.domains.push(domain);
roster.areas.push(domain);
for (const neighbourId of domain.neighbours) {
const configured = config.domains.find((candidate) => candidate.id === neighbourId);
const rostered = roster.areas.find((candidate) => candidate.id === neighbourId);
if (configured && !configured.neighbours.includes(domain.id)) configured.neighbours.push(domain.id);
if (rostered && !rostered.neighbours.includes(domain.id)) rostered.neighbours.push(domain.id);
}
const promptRelative = `.torch/prompts/${domain.id}.md`;
const changeRelative = `.torch/fleet-changes/${change.id}.approved.json`;
const approvedRecord = {
schema: 'torch.dev/fleet-change/v1alpha1', ...change, state: 'approved',
activation: { branch: plan.branch, worktree: plan.worktree },
};
const serialized = new Map([
['.torch/torch.yaml', `${JSON.stringify(config, null, 2)}\n`],
['.torch/roster.yaml', `${JSON.stringify(roster, null, 2)}\n`],
[promptRelative, promptFor(domain)],
[changeRelative, `${JSON.stringify(approvedRecord, null, 2)}\n`],
]);
manifest.external.push({ type: 'worktree', area: domain.id, path: plan.worktree, branch: plan.branch });
if (ignore?.added) manifest.patched.push({
type: 'git-info-exclude-line', path: ignore.path, line: '.task', installationId: manifest.installationId,
});
for (const [relativePath, content] of serialized) {
const existing = manifest.created.find((record) => record.path === relativePath);
if (existing) existing.sha256 = sha256(content);
else manifest.created.push({ path: relativePath, sha256: sha256(content) });
}
serialized.set('.torch/install-manifest.json', `${JSON.stringify(manifest, null, 2)}\n`);
for (const [relativePath, content] of serialized) {
const path = join(this.repositoryRoot, relativePath);
mkdirSync(dirname(path), { recursive: true });
const temporary = `${path}.tmp-${process.pid}-${randomUUID()}`;
writeFileSync(temporary, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
renameSync(temporary, path);
}
git(this.repositoryRoot, ['add', ...serialized.keys()]);
git(this.repositoryRoot, ['commit', '-m', `chore(torch): add ${domain.id} domain`]);
const activationCommit = git(this.repositoryRoot, ['rev-parse', 'HEAD']);
const now = this.clock().toISOString();
this.database.prepare(`
UPDATE fleet_changes SET state = 'provisioning', activation_commit = ?, updated_at = ? WHERE id = ?
`).run(activationCommit, now, change.id);
}
}
+6 -1
View File
@@ -137,6 +137,7 @@ function domainFromComponent(component) {
shared_paths: [],
neighbours: [],
required_checks: [],
resources: [],
runtime: 'claude',
evidence: component.evidence,
};
@@ -162,7 +163,7 @@ function specDomain(signal, specification) {
: [`responsibilities described by ${specification.path}:${signal.line}`],
not_scope: [],
owned_paths: signal.pathReferences,
shared_paths: [], neighbours: [], required_checks: [], runtime: 'claude',
shared_paths: [], neighbours: [], required_checks: [], resources: [], runtime: 'claude',
evidence: [`${specification.path}:${signal.line}`],
design_status: signal.pathReferences.length ? 'evidence-backed' : 'needs-owner-path-review',
};
@@ -200,6 +201,7 @@ export function proposeDomains({ repository, analysis }) {
id: 'core', title: 'Core project', kind: 'development',
scope: ['initial project implementation and architecture'],
not_scope: [], owned_paths: ['**'], shared_paths: [], neighbours: [], required_checks: [],
resources: [],
runtime: 'claude',
evidence: analysis.inventory.manifests,
});
@@ -212,6 +214,7 @@ export function proposeDomains({ repository, analysis }) {
not_scope: ['implementing domain features solely to make checks pass'],
owned_paths: ['test/**', 'tests/**', 'e2e/**'], shared_paths: [], neighbours: [],
required_checks: [], evidence: graph.verificationSurfaces.slice(0, 12),
resources: [],
runtime: 'claude',
});
}
@@ -222,6 +225,7 @@ export function proposeDomains({ repository, analysis }) {
not_scope: ['feature implementation', 'deployment without configured authority'],
owned_paths: graph.operationalSurfaces.filter((path) => /deploy|release|workflow/i.test(path)).slice(0, 20),
shared_paths: [], neighbours: [], required_checks: [],
resources: [],
runtime: 'claude',
evidence: graph.operationalSurfaces.slice(0, 12),
});
@@ -333,6 +337,7 @@ export function validateApprovedProposal({ proposal, repository }) {
ids.add(domain.id);
if (!domain.scope?.length) problems.push(`domain ${domain.id} has no scope`);
if (!Array.isArray(domain.not_scope)) problems.push(`domain ${domain.id} has no not_scope list`);
if (domain.resources !== undefined && !Array.isArray(domain.resources)) problems.push(`domain ${domain.id} has an invalid resources list`);
if (!domain.owned_paths?.length) problems.push(`domain ${domain.id} has no owned paths`);
if (domain.runtime !== undefined && (typeof domain.runtime !== 'string' || !domain.runtime.trim())) {
problems.push(`domain ${domain.id} has an invalid runtime`);
+11 -4
View File
@@ -67,7 +67,9 @@ function initialFiles({ repository, projectId, createdAt, proposal }) {
shared_paths: domain.shared_paths ?? [],
neighbours: domain.neighbours ?? [],
required_checks: domain.required_checks ?? [],
resources: domain.resources ?? [],
runtime: domain.runtime ?? 'claude',
model: domain.model ?? null,
branch: domain.branch ?? null,
worktree_name: domain.worktree_name ?? null,
})),
@@ -85,6 +87,10 @@ function initialFiles({ repository, projectId, createdAt, proposal }) {
}, ...config.domains],
};
const managerPrompt = `${promptContent(
proposal.session_manager?.prompt_source,
'# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n',
).trimEnd()}\n\n## Fleet evolution\n\nWhen recurring work has no coherent owner, or a durable specialist would materially improve context locality, ownership clarity, or verification, use \`torch_propose_domain\` to submit an evidence-backed Fleet change. Use \`torch_list_fleet_changes\` to follow its state. Never create, approve, or activate a persistent identity yourself; owner approval and CLI activation are separate. Also recommend merge or retirement when coordination cost exceeds the value of a persistent session.\n`;
const files = new Map([
['torch.yaml', jsonYaml(config)],
['roster.yaml', jsonYaml(roster)],
@@ -94,10 +100,7 @@ function initialFiles({ repository, projectId, createdAt, proposal }) {
proposal.common_prompt_source,
'# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n',
)],
['prompts/session-manager.md', promptContent(
proposal.session_manager?.prompt_source,
'# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n',
)],
['prompts/session-manager.md', managerPrompt],
['backlog/.gitkeep', ''],
['INSTALLATION.md', `# TORCH installation\n\nInstalled ${createdAt}. Run \`torch doctor\` before starting the fleet.\n`],
]);
@@ -119,6 +122,10 @@ function initialFiles({ repository, projectId, createdAt, proposal }) {
'',
...(domain.neighbours.length ? domain.neighbours : ['None identified.']).map((item) => `- ${item}`),
'',
'## Resources',
'',
...(domain.resources.length ? domain.resources : ['No scarce resources declared.']).map((item) => `- ${item}`),
'',
'## First move',
'',
'Query live ownership, read the assigned backlog item, and report current repository evidence to the Session Manager.',
+2 -2
View File
@@ -116,7 +116,7 @@ export function planWorktrees({ repository, parentOverride } = {}) {
};
}
function ensureTaskIgnored(root, installationId) {
export function ensureTaskIgnored(root, installationId) {
const common = git(root, ['rev-parse', '--git-common-dir']);
const commonPath = isAbsolute(common) ? common : resolve(root, common);
const excludePath = join(commonPath, 'info', 'exclude');
@@ -127,7 +127,7 @@ function ensureTaskIgnored(root, installationId) {
return { path: excludePath, added: true };
}
function removeTaskIgnore(record, installationId) {
export function removeTaskIgnore(record, installationId) {
if (!record.added || !existsSync(record.path)) return;
const marker = `# TORCH ${installationId}\n.task\n`;
const content = readFileSync(record.path, 'utf8');
+3
View File
@@ -9,6 +9,7 @@ import { ResourceService } from '../resources/service.mjs';
import { createTorchMcpServer } from './tools.mjs';
import { ContextTelemetryService } from '../telemetry/context.mjs';
import { ScheduleService } from '../schedules/service.mjs';
import { FleetEvolutionService } from '../evolution/service.mjs';
function optionValue(argv, name) {
const direct = argv.find((argument) => argument.startsWith(`${name}=`));
@@ -29,8 +30,10 @@ const backlogService = new BacklogService({
});
const contextTelemetryService = new ContextTelemetryService({ controlPlane });
const scheduleService = new ScheduleService({ repositoryRoot, controlPlane });
const evolutionService = new FleetEvolutionService({ repositoryRoot, controlPlane });
const handle = serveStdio(() => createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService,
}), {
onerror: (error) => console.error(`TORCH MCP: ${error.message}`),
});
+48
View File
@@ -53,6 +53,9 @@ export const TORCH_MCP_TOOL_NAMES = Object.freeze([
'torch_schedule_runs',
'torch_plan_schedule',
'torch_run_schedule',
'torch_list_fleet_changes',
'torch_get_fleet_change',
'torch_propose_domain',
]);
function claimedIdentity(controlPlane, actorId, claim, field) {
@@ -72,6 +75,7 @@ function claimedIdentity(controlPlane, actorId, claim, field) {
export function createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService,
} = {}) {
const tools = new Map([
['torch_identity', {
@@ -391,6 +395,48 @@ export function createTorchToolset(controlPlane, {
invoke: ({ schedule_id: scheduleId }) => scheduleService.run({ scheduleId, actorId, approved: false }),
});
}
if (evolutionService) {
tools.set('torch_list_fleet_changes', {
description: 'List durable proposed, approved, provisioning, or active Fleet organization changes.',
schema: { state: z.enum(['proposed', 'approved', 'provisioning', 'active', 'rejected']).optional() },
invoke: ({ state }) => ({ changes: evolutionService.list({ state }) }),
});
tools.set('torch_get_fleet_change', {
description: 'Read one durable Fleet organization change and its owner-approval state.',
schema: { change_id: z.string().min(1) },
invoke: ({ change_id: changeId }) => evolutionService.get(changeId),
});
tools.set('torch_propose_domain', {
description: 'Propose an evidence-backed persistent domain as the bound Session Manager. This never approves or activates it.',
schema: {
proposer: z.string().min(1).optional(),
domain: z.object({
id: z.string().min(1), title: z.string().min(1), kind: z.string().min(1).optional(),
scope: z.array(z.string().min(1)).min(1), not_scope: z.array(z.string().min(1)).optional(),
owned_paths: z.array(z.string().min(1)).min(1), shared_paths: z.array(z.string().min(1)).optional(),
neighbours: z.array(z.string().min(1)).optional(), required_checks: z.array(z.string().min(1)).optional(),
resources: z.array(z.string().min(1)).optional(),
runtime: z.string().min(1).optional(), branch: z.string().min(1).optional(),
model: z.string().min(1).optional(),
worktree_name: z.string().min(1).optional(),
}),
rationale: z.string().min(1),
expected_benefit: z.object({
summary: z.string().min(1), recurring_work: z.string().min(1),
context_locality: z.string().min(1), coordination_cost: z.string().min(1),
}),
evidence: z.array(z.string().min(1)).min(1),
},
invoke: ({ proposer, expected_benefit: benefit, ...input }) => evolutionService.proposeDomain({
...input,
proposer: claimedIdentity(controlPlane, actorId, proposer, 'proposer'),
expectedBenefit: {
summary: benefit.summary, recurringWork: benefit.recurring_work,
contextLocality: benefit.context_locality, coordinationCost: benefit.coordination_cost,
},
}),
});
}
return tools;
}
@@ -403,6 +449,7 @@ export function callTorchTool(controlPlane, name, input = {}, options = {}) {
export function createTorchMcpServer(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService,
} = {}) {
controlPlane.assertIdentity(actorId);
const server = new McpServer(
@@ -413,6 +460,7 @@ export function createTorchMcpServer(controlPlane, {
);
for (const [name, tool] of createTorchToolset(controlPlane, {
actorId, backlogService, checkService, resourceService, integrationService, contextTelemetryService, scheduleService,
evolutionService,
})) {
server.registerTool(name, { description: tool.description, inputSchema: tool.schema }, async (input) => {
try {
+8
View File
@@ -78,6 +78,7 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
checks: { definitions: config.checks ?? [], receipts: [] },
resources: { definitions: config.resources ?? [], holders: [], waiters: [] },
integration: [],
fleetChanges: [],
schedules: config.schedules ?? [],
audit: [],
contextLocality: {
@@ -135,6 +136,13 @@ export function observeProject({ repositoryRoot, env = process.env, now = () =>
SELECT id, source_area AS sourceArea, source_commit AS sourceCommit, target_branch AS targetBranch, state, reason, updated_at AS updatedAt
FROM integration_requests ORDER BY updated_at DESC LIMIT 40
`);
result.fleetChanges = rows(database, 'fleet_changes', `
SELECT id, state, proposer, json_extract(domain_json, '$.id') AS domainId,
json_extract(domain_json, '$.title') AS title,
json_extract(expected_benefit_json, '$.summary') AS expectedBenefit,
approved_by AS approvedBy, updated_at AS updatedAt
FROM fleet_changes ORDER BY updated_at DESC, id LIMIT 40
`);
result.audit = rows(database, 'audit_events', `
SELECT id, actor_id AS actorId, operation, entity_type AS entityType, entity_id AS entityId, created_at AS createdAt
FROM audit_events ORDER BY created_at DESC LIMIT 30
+57
View File
@@ -134,6 +134,63 @@ export function planFleetUp({ repositoryRoot, controlPlane, adapter, adapters, f
};
}
export function planAreaUp({ repositoryRoot, controlPlane, areaId, adapter, adapters, fresh = false } = {}) {
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 (!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 prompts = promptPaths(localStateRoot(manifest), worktree, area.id);
const runtimeConfig = config.runtimes?.[runtime] ?? {};
const model = area.model ?? runtimeConfig.model;
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, background, message: startupMessage(area.id, false),
})
: runtimeAdapter.createSession({
areaId: area.id, title: area.title, worktree: worktree.path, promptFile: prompts.combined,
firstMessage: startupMessage(area.id, true), model, background,
});
const action = {
areaId: area.id, title: area.title, mode: shouldResume ? 'resume' : 'create',
runtime: runtimeAdapter.name, runtimeSessionId: runtimePlan.runtimeSessionId,
requiresRuntimeIdCapture: runtimePlan.requiresRuntimeIdCapture ?? false,
completionState: runtimePlan.completionState ?? 'starting',
worktree: worktree.path, branch: worktree.branch, promptFile: prompts.combined,
promptSources: [prompts.common, prompts.area], launch: runtimePlan.launch,
};
return {
action: 'area-up', projectId: manifest.projectId, areaId, fresh,
actions: [action], blockers, canProceed: true, mutationPerformed: false,
};
}
function writeCombinedPrompt(action) {
const content = action.promptSources.map((path) => readFileSync(path, 'utf8').trimEnd()).join('\n\n');
mkdirSync(dirname(action.promptFile), { recursive: true });
+1
View File
@@ -11,6 +11,7 @@ export const CANDIDATE_ACCEPTANCE_SCENARIOS = Object.freeze([
'init-analyze',
'spec-aware-design',
'ai-fleet-bootstrap',
'fleet-evolution',
'review-install-roster',
'branches-worktrees',
'runtime-identities',