mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-28 16:31:56 +00:00
655 lines
30 KiB
JavaScript
655 lines
30 KiB
JavaScript
import { analyzeRepository } from './kernel/analyze.mjs';
|
|
import { BacklogService } from './backlog/service.mjs';
|
|
import { createClaudeAdapter } from './adapters/claude.mjs';
|
|
import { createCodexAdapter } from './adapters/codex.mjs';
|
|
import { CheckService } from './checks/service.mjs';
|
|
import { classifyRecoverability, createLocalCanonical, planLocalCanonical } from './canonical/local.mjs';
|
|
import { openControlPlane } from './control-plane/service.mjs';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { diagnoseProject } from './kernel/doctor.mjs';
|
|
import { asErrorRecord, TorchError } from './kernel/errors.mjs';
|
|
import { inspectRepository } from './kernel/git.mjs';
|
|
import { inspectSpecifications } from './kernel/specifications.mjs';
|
|
import { installProject, planInstall, uninstallProject } from './kernel/install.mjs';
|
|
import { IntegrationService } from './integration/service.mjs';
|
|
import { analyzeCombatrigFleet } from './importers/combatrig.mjs';
|
|
import { proposeDomains } from './kernel/domains.mjs';
|
|
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 { ResourceService } from './resources/service.mjs';
|
|
import { CANDIDATE_ACCEPTANCE_SCENARIOS, createVersionService } from './self-host/service.mjs';
|
|
|
|
const HELP = `TORCH — portable agent fleet
|
|
|
|
Usage:
|
|
torch init [--repo <path>] [--spec <path>] [--json]
|
|
torch analyze [--repo <path>] [--spec <path>] [--json]
|
|
torch design [--repo <path>] [--spec <path>] [--output <path>] [--json]
|
|
torch domains [--repo <path>] [--spec <path>] [--output <path>] [--json]
|
|
torch install --proposal <path> [--dry-run] [--yes] [--json]
|
|
torch worktrees [--parent <path>] [--dry-run] [--yes] [--json]
|
|
torch up [--fresh] [--dry-run] [--yes] [--json]
|
|
torch down [--dry-run] [--yes] [--json]
|
|
torch identity --area <id> [--json]
|
|
torch agents [--json]
|
|
torch agent --area <id> [--json]
|
|
torch roster [--json]
|
|
torch who-owns (--path <path> | --capability <text>) [--json]
|
|
torch neighbours --area <id> [--json]
|
|
torch message --from <id> --to <id|all> --body <text> [--task <id>] [--path <path>] [--commit <sha>] [--json]
|
|
torch inbox --area <id> [--unacknowledged] [--limit <n>] [--json]
|
|
torch ack --area <id> --message <id> [--json]
|
|
torch status --area <id> --state <state> [--summary <text>] [--runtime <name>] [--session <id>] [--task <id>] [--json]
|
|
torch complete --area <id> --summary <text> [--task <id>] [--evidence <text>] [--commit <sha>] [--json]
|
|
torch blocked --area <id> --summary <text> [--task <id>] [--evidence <text>] [--path <path>] [--commit <sha>] [--json]
|
|
torch coordinate --from <id> --body <text> [--with <id,id>] [--task <id>] [--path <path>] [--json]
|
|
torch handoff --from <id> [--to <id>] (--path <path> | --task <id>) --reason <text> [--json]
|
|
torch backlog list [--state <state>] [--owner <id>] [--json]
|
|
torch backlog get --task <id> [--json]
|
|
torch backlog create --area <id> --title <text> --description <text> --accept <text,...> [--priority <priority>] [--domains <id,...>] [--depends <task,...>] [--json]
|
|
torch backlog transition --task <id> --area <id> --state <state> --revision <n> [--owner <id>] [--evidence <text,...>] [--commit <sha>] [--integration <id>] [--reason <text>] [--note <text>] [--json]
|
|
torch checks list [--json]
|
|
torch checks receipts [--commit <sha>] [--id <check>] [--area <id>] [--json]
|
|
torch checks plan --id <check> --area <id> [--json]
|
|
torch checks run --id <check> --area <id> --yes [--json]
|
|
torch resources list [--json]
|
|
torch resources status --id <resource> [--json]
|
|
torch resources acquire --id <resource> --area <id> [--json]
|
|
torch resources release --id <resource> --area <id> [--json]
|
|
torch resources cancel --id <resource> --area <id> [--json]
|
|
torch integrate list [--state <state>] [--json]
|
|
torch integrate request --area <id> [--commit <sha>] [--json]
|
|
torch integrate evaluate --request <id> [--json]
|
|
torch integrate authorize --request <id> --area <id> [--json]
|
|
torch integrate plan --request <id> --area <id> [--json]
|
|
torch integrate land --request <id> --area <id> --yes [--json]
|
|
torch canonical plan [--json]
|
|
torch canonical create --yes [--json]
|
|
torch recoverability [--commit <sha>] [--json]
|
|
torch candidate plan --source <path> [--version <version>] [--json]
|
|
torch candidate build --source <path> [--version <version>] --yes [--json]
|
|
torch candidate status [--json]
|
|
torch candidate test --version <version> [--json]
|
|
torch upgrade --version <version> [--dry-run] --yes [--json]
|
|
torch rollback [--dry-run] --yes [--json]
|
|
torch import combatrig --source <path> [--output <path>] [--json]
|
|
torch doctor [--json]
|
|
torch uninstall [--dry-run] [--purge] [--json]
|
|
`;
|
|
|
|
function print(value, { json = false } = {}) {
|
|
if (json || typeof value !== 'string') process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
else process.stdout.write(`${value}\n`);
|
|
}
|
|
|
|
function optionValue(argv, name) {
|
|
const direct = argv.find((arg) => arg.startsWith(`${name}=`));
|
|
if (direct) return direct.slice(name.length + 1);
|
|
const index = argv.indexOf(name);
|
|
return index >= 0 ? argv[index + 1] : undefined;
|
|
}
|
|
|
|
function optionValues(argv, name) {
|
|
const values = [];
|
|
for (let index = 0; index < argv.length; index += 1) {
|
|
if (argv[index].startsWith(`${name}=`)) values.push(argv[index].slice(name.length + 1));
|
|
else if (argv[index] === name && argv[index + 1] !== undefined) values.push(argv[index + 1]);
|
|
}
|
|
return values;
|
|
}
|
|
|
|
function commaList(value) {
|
|
return (value ?? '').split(',').map((item) => item.trim()).filter(Boolean);
|
|
}
|
|
|
|
function loadProposal(cwd, proposalPath) {
|
|
if (!proposalPath) {
|
|
throw new TorchError('Installation requires an approved domain proposal from torch domains --output.', {
|
|
code: 'PROPOSAL_REQUIRED',
|
|
});
|
|
}
|
|
try { return JSON.parse(readFileSync(resolve(cwd, proposalPath), 'utf8')); } catch (error) {
|
|
throw new TorchError(`Cannot read domain proposal: ${proposalPath}`, {
|
|
code: 'PROPOSAL_INVALID', details: error.message,
|
|
});
|
|
}
|
|
}
|
|
|
|
function withControlPlane(repository, env, callback) {
|
|
const controlPlane = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
return callback(controlPlane);
|
|
} finally {
|
|
controlPlane.close();
|
|
}
|
|
}
|
|
|
|
function runtimeAdapters() {
|
|
return new Map([
|
|
['claude', createClaudeAdapter()],
|
|
['codex', createCodexAdapter()],
|
|
]);
|
|
}
|
|
|
|
function runCandidateAcceptance(candidateRoot, spawn) {
|
|
const checks = [
|
|
{ id: 'test', args: ['test'] },
|
|
{ id: 'lint', args: ['run', 'lint'] },
|
|
{ id: 'syntax', args: ['run', 'check'] },
|
|
].map((check) => {
|
|
const result = spawn('npm', check.args, {
|
|
cwd: candidateRoot, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 300_000,
|
|
});
|
|
return {
|
|
id: check.id, status: result.status ?? (result.error ? 1 : 0),
|
|
signal: result.signal ?? null, error: result.error?.message ?? null,
|
|
stdout: result.stdout ?? '',
|
|
};
|
|
});
|
|
const testOutput = checks.find((check) => check.id === 'test')?.stdout ?? '';
|
|
const evidenceByScenario = new Map([
|
|
['init-analyze', ['SCN-init-read-only']],
|
|
['spec-aware-design', ['SCN-spec-fleet-design', 'SCN-cli-spec-design']],
|
|
['review-install-roster', ['SCN-cli-domain-review']],
|
|
['branches-worktrees', ['SCN-worktree-bootstrap']],
|
|
['runtime-identities', ['SCN-mixed-runtime']],
|
|
['durable-messaging', ['SCN-durable-message']],
|
|
['ownership-query', ['SCN-ownership-handoff']],
|
|
['unsafe-worktree-detection', ['SCN-worktree-purge-safety']],
|
|
['backlog-integration', ['SCN-backlog-lifecycle', 'SCN-native-integration']],
|
|
['resource-lifecycle', ['SCN-resource-fifo']],
|
|
['capture-stop-resume', ['SCN-fleet-fresh-resume']],
|
|
['detach-uninstall', ['SCN-install-doctor-purge']],
|
|
['combatrig-compatibility', ['SCN-combatrig-import', 'SCN-cli-combatrig-import']],
|
|
]);
|
|
const scenarios = CANDIDATE_ACCEPTANCE_SCENARIOS.filter((scenario) =>
|
|
(evidenceByScenario.get(scenario) ?? []).every((marker) => testOutput.includes(marker)));
|
|
const passed = checks.every((check) => check.status === 0 && !check.signal && !check.error)
|
|
&& scenarios.length === CANDIDATE_ACCEPTANCE_SCENARIOS.length;
|
|
return {
|
|
passed,
|
|
scenarios,
|
|
evidence: checks.map(({ stdout, ...check }) => ({
|
|
...check, outputBytes: Buffer.byteLength(stdout),
|
|
})),
|
|
};
|
|
}
|
|
|
|
export async function runCli(argv = process.argv.slice(2), {
|
|
cwd = process.cwd(), env = process.env, spawn = spawnSync,
|
|
} = {}) {
|
|
const command = argv.find((arg) => !arg.startsWith('-')) ?? 'help';
|
|
const json = argv.includes('--json');
|
|
try {
|
|
if (command === 'help' || argv.includes('--help') || argv.includes('-h')) {
|
|
process.stdout.write(HELP);
|
|
return 0;
|
|
}
|
|
if (command === 'candidate') {
|
|
const operation = argv[1] ?? 'status';
|
|
const versions = createVersionService({ env });
|
|
if (operation === 'status') {
|
|
print(versions.status(), { json });
|
|
return 0;
|
|
}
|
|
if (operation === 'test') {
|
|
const version = optionValue(argv, '--version');
|
|
const status = versions.status();
|
|
const validationPath = resolve(status.root, 'versions', version ?? '', '.torch-validation.json');
|
|
if (!version || !status.installed.includes(version)) {
|
|
throw new TorchError('Candidate test receipt requires an installed version', {
|
|
code: 'VERSION_NOT_INSTALLED', details: { version: version ?? null },
|
|
});
|
|
}
|
|
print(JSON.parse(readFileSync(validationPath, 'utf8')), { json });
|
|
return 0;
|
|
}
|
|
const source = optionValue(argv, '--source');
|
|
const version = optionValue(argv, '--version');
|
|
if (operation === 'plan') {
|
|
print(versions.planCandidate({ source: resolve(cwd, source ?? ''), version }), { json });
|
|
return 0;
|
|
}
|
|
if (operation === 'build') {
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Candidate build copies code and runs its acceptance suite. Review candidate plan, then re-run with --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(versions.installCandidate({
|
|
source: resolve(cwd, source ?? ''), version,
|
|
validate: ({ candidateRoot }) => runCandidateAcceptance(candidateRoot, spawn),
|
|
}), { json });
|
|
return 0;
|
|
}
|
|
throw new TorchError(`Unknown candidate operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
}
|
|
if (command === 'upgrade') {
|
|
const versions = createVersionService({ env });
|
|
const version = optionValue(argv, '--version');
|
|
const status = versions.status();
|
|
const plan = {
|
|
action: 'upgrade', from: status.activeVersion, to: version ?? null,
|
|
canProceed: Boolean(version && status.installed.includes(version)), mutationPerformed: false,
|
|
};
|
|
if (argv.includes('--dry-run')) {
|
|
print(plan, { json });
|
|
return plan.canProceed ? 0 : 1;
|
|
}
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Upgrade atomically changes the active TORCH version. Review --dry-run, then re-run with --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(versions.activate(version), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'rollback') {
|
|
const versions = createVersionService({ env });
|
|
const status = versions.status();
|
|
const plan = {
|
|
action: 'rollback', from: status.activeVersion, to: status.previousVersion,
|
|
canProceed: Boolean(status.previousVersion), mutationPerformed: false,
|
|
};
|
|
if (argv.includes('--dry-run')) {
|
|
print(plan, { json });
|
|
return plan.canProceed ? 0 : 1;
|
|
}
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Rollback atomically restores the previous TORCH version. Review --dry-run, then re-run with --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(versions.rollback(), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'import') {
|
|
const format = argv[1];
|
|
if (format !== 'combatrig') {
|
|
throw new TorchError(`Unsupported fleet import format: ${format ?? '<missing>'}`, {
|
|
code: 'IMPORT_FORMAT_UNSUPPORTED',
|
|
});
|
|
}
|
|
const source = optionValue(argv, '--source');
|
|
if (!source) throw new TorchError('COMBATRIG import requires --source', { code: 'IMPORT_SOURCE_REQUIRED' });
|
|
const sourceRepository = inspectRepository(resolve(cwd, source));
|
|
const proposal = analyzeCombatrigFleet({ repository: sourceRepository });
|
|
const output = optionValue(argv, '--output');
|
|
if (output) {
|
|
const path = resolve(cwd, output);
|
|
writeNewFile(path, `${JSON.stringify(proposal, null, 2)}\n`);
|
|
print({ proposal, output: path, mutationPerformed: true }, { json });
|
|
} else print({ proposal, mutationPerformed: false }, { json });
|
|
return proposal.compatibility.readyForApproval ? 0 : 1;
|
|
}
|
|
const designCommand = ['init', 'analyze', 'domains', 'design'].includes(command);
|
|
const repositoryRoot = designCommand ? resolve(cwd, optionValue(argv, '--repo') ?? '.') : cwd;
|
|
const repository = inspectRepository(repositoryRoot);
|
|
if (command === 'init' || command === 'analyze') {
|
|
const specifications = inspectSpecifications(optionValues(argv, '--spec'), {
|
|
cwd, repositoryRoot: repository.root,
|
|
});
|
|
const analysis = analyzeRepository(repository, { specifications });
|
|
print({
|
|
command, repository, analysis,
|
|
next: 'Run torch design with the same repository/specification inputs, review its proposal, then install.',
|
|
}, { json });
|
|
return 0;
|
|
}
|
|
if (command === 'domains' || command === 'design') {
|
|
const specifications = inspectSpecifications(optionValues(argv, '--spec'), {
|
|
cwd, repositoryRoot: repository.root,
|
|
});
|
|
const analysis = analyzeRepository(repository, { specifications });
|
|
const proposal = proposeDomains({ repository, analysis });
|
|
const output = optionValue(argv, '--output');
|
|
if (output) {
|
|
const path = resolve(cwd, output);
|
|
writeNewFile(path, `${JSON.stringify(proposal, null, 2)}\n`);
|
|
print({ proposal, output: path, mutationPerformed: true }, { json });
|
|
} else {
|
|
print({ proposal, mutationPerformed: false }, { json });
|
|
}
|
|
return 0;
|
|
}
|
|
if (command === 'install') {
|
|
const proposalPath = optionValue(argv, '--proposal');
|
|
if (argv.includes('--dry-run')) {
|
|
const proposal = loadProposal(cwd, proposalPath);
|
|
print({ ...planInstall({ repository, proposal, env }), proposal: resolve(cwd, proposalPath) }, { json });
|
|
return 0;
|
|
}
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Installation requires reviewed approval. Re-run with --dry-run, then --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
const proposal = loadProposal(cwd, proposalPath);
|
|
print(installProject({ repository, proposal, env }), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'doctor') {
|
|
const diagnosis = diagnoseProject({ repository, env });
|
|
print(diagnosis, { json });
|
|
return diagnosis.healthy ? 0 : 1;
|
|
}
|
|
if (command === 'worktrees') {
|
|
const parentOverride = optionValue(argv, '--parent');
|
|
if (argv.includes('--dry-run')) {
|
|
const plan = planWorktrees({ repository, parentOverride });
|
|
print(plan, { json });
|
|
return plan.canProceed ? 0 : 1;
|
|
}
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Worktree creation requires a reviewed dry run. Re-run with --dry-run, then --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(createWorktrees({ repository, parentOverride }), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'up') {
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const adapters = runtimeAdapters();
|
|
const plan = planFleetUp({
|
|
repositoryRoot: repository.root, controlPlane: control, 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('Fleet startup launches configured AI runtimes. Review --dry-run, then re-run with --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 });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'down') {
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const adapters = runtimeAdapters();
|
|
const plan = planFleetDown({ repositoryRoot: repository.root, controlPlane: control, adapters });
|
|
if (argv.includes('--dry-run')) {
|
|
print(plan, { json });
|
|
return plan.canProceed ? 0 : 1;
|
|
}
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Fleet wind-down stops configured runtime sessions. Review --dry-run, then re-run with --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
const runtimeStoppers = new Map([
|
|
['claude', createClaudeAdapter({
|
|
runner: (executable, args) => spawn(executable, args, {
|
|
encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'],
|
|
}),
|
|
})],
|
|
['codex', createCodexAdapter()],
|
|
]);
|
|
print(stopFleet({
|
|
plan, controlPlane: control,
|
|
stopRuntime: (action) => runtimeStoppers.get(action.runtime)
|
|
.stopSession({ runtimeSessionId: action.runtimeSessionId }),
|
|
}), { json });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'identity') {
|
|
print(withControlPlane(repository, env, (control) => control.identity(optionValue(argv, '--area'))), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'agents') {
|
|
print(withControlPlane(repository, env, (control) => ({ agents: control.listAgents() })), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'agent') {
|
|
print(withControlPlane(repository, env, (control) => control.getAgent(optionValue(argv, '--area'))), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'roster') {
|
|
print(withControlPlane(repository, env, (control) => control.getRoster()), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'who-owns') {
|
|
print(withControlPlane(repository, env, (control) => control.whoOwns({
|
|
path: optionValue(argv, '--path'), capability: optionValue(argv, '--capability'),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'neighbours') {
|
|
const areaId = optionValue(argv, '--area');
|
|
print(withControlPlane(repository, env, (control) => ({ areaId, neighbours: control.neighbours(areaId) })), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'message') {
|
|
print(withControlPlane(repository, env, (control) => control.sendMessage({
|
|
sender: optionValue(argv, '--from'), recipient: optionValue(argv, '--to'),
|
|
body: optionValue(argv, '--body'), references: {
|
|
task: optionValue(argv, '--task'), path: optionValue(argv, '--path'),
|
|
commit: optionValue(argv, '--commit'), handoff: optionValue(argv, '--handoff'),
|
|
},
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'inbox') {
|
|
print(withControlPlane(repository, env, (control) => ({
|
|
recipient: optionValue(argv, '--area'),
|
|
messages: control.readMessages({
|
|
recipient: optionValue(argv, '--area'), unacknowledgedOnly: argv.includes('--unacknowledged'),
|
|
limit: Number(optionValue(argv, '--limit') ?? 100),
|
|
}),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'ack') {
|
|
print(withControlPlane(repository, env, (control) => control.ackMessage({
|
|
recipient: optionValue(argv, '--area'), messageId: optionValue(argv, '--message'),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'status') {
|
|
print(withControlPlane(repository, env, (control) => control.reportStatus({
|
|
areaId: optionValue(argv, '--area'), state: optionValue(argv, '--state'),
|
|
summary: optionValue(argv, '--summary'), runtime: optionValue(argv, '--runtime'),
|
|
runtimeSessionId: optionValue(argv, '--session'), task: optionValue(argv, '--task'),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'complete' || command === 'blocked') {
|
|
const input = {
|
|
areaId: optionValue(argv, '--area'), summary: optionValue(argv, '--summary'),
|
|
task: optionValue(argv, '--task'), evidence: optionValue(argv, '--evidence'),
|
|
path: optionValue(argv, '--path'), commit: optionValue(argv, '--commit'),
|
|
};
|
|
print(withControlPlane(repository, env, (control) =>
|
|
(command === 'complete' ? control.reportComplete(input) : control.reportBlocked(input))), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'coordinate') {
|
|
const participants = (optionValue(argv, '--with') ?? '').split(',').filter(Boolean);
|
|
print(withControlPlane(repository, env, (control) => control.requestCoordination({
|
|
sender: optionValue(argv, '--from'), participants, body: optionValue(argv, '--body'),
|
|
task: optionValue(argv, '--task'), path: optionValue(argv, '--path'),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'handoff') {
|
|
print(withControlPlane(repository, env, (control) => control.requestHandoff({
|
|
sender: optionValue(argv, '--from'), recipient: optionValue(argv, '--to') ?? 'session-manager',
|
|
path: optionValue(argv, '--path'), task: optionValue(argv, '--task'), reason: optionValue(argv, '--reason'),
|
|
})), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'backlog') {
|
|
const operation = argv[1] ?? 'list';
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
|
|
const checks = new CheckService({
|
|
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
|
|
});
|
|
const integration = new IntegrationService({
|
|
repositoryRoot: repository.root, controlPlane: control, checkService: checks,
|
|
});
|
|
const backlog = new BacklogService({
|
|
repositoryRoot: repository.root, controlPlane: control,
|
|
integrationLookup: (requestId) => integration.get(requestId),
|
|
});
|
|
if (operation === 'list') print({ tasks: backlog.list({
|
|
state: optionValue(argv, '--state'), owner: optionValue(argv, '--owner'),
|
|
}) }, { json });
|
|
else if (operation === 'get') print(backlog.get(optionValue(argv, '--task')), { json });
|
|
else if (operation === 'create') print(backlog.create({
|
|
actorId: optionValue(argv, '--area'), title: optionValue(argv, '--title'),
|
|
description: optionValue(argv, '--description'), priority: optionValue(argv, '--priority') ?? 'normal',
|
|
acceptanceCriteria: commaList(optionValue(argv, '--accept')),
|
|
affectedDomains: commaList(optionValue(argv, '--domains')),
|
|
dependencies: commaList(optionValue(argv, '--depends')),
|
|
}), { json });
|
|
else if (operation === 'transition') print(backlog.transition({
|
|
taskId: optionValue(argv, '--task'), actorId: optionValue(argv, '--area'),
|
|
to: optionValue(argv, '--state'), expectedRevision: Number(optionValue(argv, '--revision')),
|
|
owner: optionValue(argv, '--owner'), evidence: commaList(optionValue(argv, '--evidence')),
|
|
commit: optionValue(argv, '--commit'), integrationRequest: optionValue(argv, '--integration'),
|
|
blockedReason: optionValue(argv, '--reason'), note: optionValue(argv, '--note'),
|
|
}), { json });
|
|
else throw new TorchError(`Unknown backlog operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'checks') {
|
|
const operation = argv[1] ?? 'list';
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
|
|
const checks = new CheckService({
|
|
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
|
|
});
|
|
if (operation === 'list') print({ checks: checks.listChecks() }, { json });
|
|
else if (operation === 'receipts') print({ receipts: checks.receipts({
|
|
commit: optionValue(argv, '--commit'), checkId: optionValue(argv, '--id'),
|
|
areaId: optionValue(argv, '--area'),
|
|
}) }, { json });
|
|
else if (operation === 'plan') print(checks.plan({
|
|
checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area'),
|
|
}), { json });
|
|
else if (operation === 'run') {
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Configured check execution requires an explicit --yes.', { code: 'APPROVAL_REQUIRED' });
|
|
}
|
|
print(checks.run({ checkId: optionValue(argv, '--id'), areaId: optionValue(argv, '--area') }), { json });
|
|
} else throw new TorchError(`Unknown checks operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'resources') {
|
|
const operation = argv[1] ?? 'list';
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
|
|
const resourceId = optionValue(argv, '--id');
|
|
const areaId = optionValue(argv, '--area');
|
|
if (operation === 'list') print({ resources: resources.list() }, { json });
|
|
else if (operation === 'status') print(resources.status(resourceId), { json });
|
|
else if (operation === 'acquire') print(resources.acquire({ resourceId, areaId }), { json });
|
|
else if (operation === 'release') print(resources.release({ resourceId, areaId }), { json });
|
|
else if (operation === 'cancel') print(resources.cancel({ resourceId, areaId }), { json });
|
|
else throw new TorchError(`Unknown resources operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'integrate') {
|
|
const operation = argv[1] ?? 'list';
|
|
const control = openControlPlane({ repositoryRoot: repository.root, env });
|
|
try {
|
|
const resources = new ResourceService({ repositoryRoot: repository.root, controlPlane: control });
|
|
const checks = new CheckService({
|
|
repositoryRoot: repository.root, controlPlane: control, resourceService: resources,
|
|
});
|
|
const integration = new IntegrationService({
|
|
repositoryRoot: repository.root, controlPlane: control, checkService: checks,
|
|
});
|
|
const requestId = optionValue(argv, '--request');
|
|
const actorId = optionValue(argv, '--area');
|
|
if (operation === 'list') print({ requests: integration.list({ state: optionValue(argv, '--state') }) }, { json });
|
|
else if (operation === 'request') print(integration.request({
|
|
areaId: actorId, commit: optionValue(argv, '--commit'),
|
|
}), { json });
|
|
else if (operation === 'evaluate') print(integration.evaluate(requestId), { json });
|
|
else if (operation === 'authorize') print(integration.authorize({ requestId, actorId }), { json });
|
|
else if (operation === 'plan') print(integration.planLanding({ requestId, actorId }), { json });
|
|
else if (operation === 'land') {
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Landing changes canonical main. Review integrate plan, then re-run with --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(integration.land({ requestId, actorId }), { json });
|
|
} else throw new TorchError(`Unknown integrate operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
return 0;
|
|
} finally {
|
|
control.close();
|
|
}
|
|
}
|
|
if (command === 'canonical') {
|
|
const operation = argv[1] ?? 'plan';
|
|
if (operation === 'plan') {
|
|
const plan = planLocalCanonical({ repositoryRoot: repository.root });
|
|
print(plan, { json });
|
|
return plan.canProceed ? 0 : 1;
|
|
}
|
|
if (operation === 'create') {
|
|
if (!argv.includes('--yes')) {
|
|
throw new TorchError('Local canonical creation changes Git configuration. Review canonical plan, then use --yes.', {
|
|
code: 'APPROVAL_REQUIRED',
|
|
});
|
|
}
|
|
print(createLocalCanonical({ repositoryRoot: repository.root }), { json });
|
|
return 0;
|
|
}
|
|
throw new TorchError(`Unknown canonical operation: ${operation}`, { code: 'UNKNOWN_COMMAND' });
|
|
}
|
|
if (command === 'recoverability') {
|
|
print(classifyRecoverability({
|
|
repositoryRoot: repository.root, commit: optionValue(argv, '--commit') ?? 'HEAD',
|
|
}), { json });
|
|
return 0;
|
|
}
|
|
if (command === 'uninstall') {
|
|
const result = uninstallProject({
|
|
repository,
|
|
purge: argv.includes('--purge'),
|
|
dryRun: argv.includes('--dry-run'),
|
|
});
|
|
print(result, { json });
|
|
return result.canProceed === false ? 1 : 0;
|
|
}
|
|
throw new TorchError(`Unknown command: ${command}`, { code: 'UNKNOWN_COMMAND' });
|
|
} catch (error) {
|
|
print(asErrorRecord(error), { json: true });
|
|
return error instanceof TorchError ? 2 : 1;
|
|
}
|
|
}
|