mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-11-06 21:01:54 +00:00
Begin portable TORCH fleet rewrite
This commit is contained in:
1 parent
a46832314f
commit
cce059fc3a
16 files changed
+2258
-613
No files matched your search
@@ -0,0 +1,87 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { extname } from 'node:path';
|
||||
import { trackedFiles } from './git.mjs';
|
||||
|
||||
const LANGUAGE_BY_EXTENSION = new Map([
|
||||
['.js', 'JavaScript'], ['.mjs', 'JavaScript'], ['.cjs', 'JavaScript'],
|
||||
['.ts', 'TypeScript'], ['.tsx', 'TypeScript'], ['.jsx', 'JavaScript'],
|
||||
['.py', 'Python'], ['.rs', 'Rust'], ['.go', 'Go'], ['.java', 'Java'],
|
||||
['.c', 'C'], ['.h', 'C/C++'], ['.cc', 'C++'], ['.cpp', 'C++'],
|
||||
['.cs', 'C#'], ['.rb', 'Ruby'], ['.php', 'PHP'], ['.swift', 'Swift'],
|
||||
['.kt', 'Kotlin'], ['.sh', 'Shell'], ['.bash', 'Shell'], ['.ps1', 'PowerShell'],
|
||||
['.html', 'HTML'], ['.css', 'CSS'], ['.scss', 'CSS'], ['.vue', 'Vue'],
|
||||
['.svelte', 'Svelte'], ['.sol', 'Solidity'],
|
||||
]);
|
||||
|
||||
const MANIFESTS = [
|
||||
'package.json', 'pyproject.toml', 'requirements.txt', 'Cargo.toml', 'go.mod',
|
||||
'pom.xml', 'build.gradle', 'Makefile', 'CMakeLists.txt', 'Dockerfile',
|
||||
];
|
||||
|
||||
export function analyzeRepository(repository) {
|
||||
const files = trackedFiles(repository.root);
|
||||
const languageCounts = new Map();
|
||||
const topLevel = new Set();
|
||||
|
||||
for (const file of files) {
|
||||
const language = LANGUAGE_BY_EXTENSION.get(extname(file).toLowerCase());
|
||||
if (language) languageCounts.set(language, (languageCounts.get(language) ?? 0) + 1);
|
||||
const first = file.split('/')[0];
|
||||
if (file.includes('/')) topLevel.add(first);
|
||||
}
|
||||
|
||||
const languages = [...languageCounts.entries()]
|
||||
.map(([name, files]) => ({ name, files }))
|
||||
.sort((a, b) => b.files - a.files || a.name.localeCompare(b.name));
|
||||
const manifests = MANIFESTS.filter((file) => files.includes(file));
|
||||
const operations = files.filter((file) =>
|
||||
/(^|\/)(\.github\/workflows|Dockerfile|docker-compose|vercel\.json|systemd|deploy|release|cron)/i.test(file));
|
||||
const testFiles = files.filter((file) => /(^|\/)(test|tests|spec|e2e)(\/|\.|$)/i.test(file));
|
||||
const docs = files.filter((file) => /(^|\/)(docs?|README)(\/|\.|$)/i.test(file));
|
||||
const sharedCandidates = files.filter((file) =>
|
||||
/(^|\/)(package(-lock)?\.json|[^/]*config[^/]*|schema|schemas|types|shared)(\/|\.|$)/i.test(file));
|
||||
|
||||
const checks = [];
|
||||
const packagePath = `${repository.root}/package.json`;
|
||||
if (existsSync(packagePath)) {
|
||||
try {
|
||||
const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
|
||||
for (const [name, command] of Object.entries(pkg.scripts ?? {})) {
|
||||
if (/test|check|lint|type|build|verify|audit/i.test(name)) checks.push({ name, command });
|
||||
}
|
||||
} catch {
|
||||
// The analyzer reports observable repository shape. Invalid manifests are
|
||||
// surfaced later by doctor and should not make read-only analysis mutate.
|
||||
}
|
||||
}
|
||||
|
||||
const scarceResources = [];
|
||||
const joined = files.join('\n').toLowerCase();
|
||||
if (/playwright|puppeteer|cypress|browser/.test(joined)) scarceResources.push('browser');
|
||||
if (/cuda|gpu|webgl|three\.js|threejs/.test(joined)) scarceResources.push('gpu');
|
||||
if (/postgres|mysql|sqlite|database/.test(joined)) scarceResources.push('database');
|
||||
|
||||
return {
|
||||
generatedAt: new Date().toISOString(),
|
||||
repository: {
|
||||
root: repository.root,
|
||||
name: repository.name,
|
||||
branch: repository.branch,
|
||||
head: repository.head,
|
||||
hasRemote: repository.remotes.length > 0,
|
||||
},
|
||||
inventory: {
|
||||
trackedFileCount: files.length,
|
||||
languages,
|
||||
manifests,
|
||||
topLevelDirectories: [...topLevel].sort(),
|
||||
testFileCount: testFiles.length,
|
||||
documentationFileCount: docs.length,
|
||||
operations: operations.slice(0, 100),
|
||||
sharedSurfaceCandidates: sharedCandidates.slice(0, 100),
|
||||
checks,
|
||||
scarceResources,
|
||||
},
|
||||
mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { fileHash } from './files.mjs';
|
||||
import { projectStatePath } from './paths.mjs';
|
||||
|
||||
function parseJsonYaml(path) {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
}
|
||||
|
||||
export function diagnoseProject({ repository, env = process.env }) {
|
||||
const trackedRoot = join(repository.root, '.torch');
|
||||
const configPath = join(trackedRoot, 'torch.yaml');
|
||||
const manifestPath = join(trackedRoot, 'install-manifest.json');
|
||||
const findings = [];
|
||||
|
||||
if (!existsSync(configPath)) {
|
||||
return { status: 'not-installed', healthy: false, findings: [{ severity: 'error', code: 'CONFIG_MISSING', path: configPath }] };
|
||||
}
|
||||
if (!existsSync(manifestPath)) {
|
||||
return { status: 'broken', healthy: false, findings: [{ severity: 'error', code: 'MANIFEST_MISSING', path: manifestPath }] };
|
||||
}
|
||||
|
||||
let config;
|
||||
let manifest;
|
||||
try { config = parseJsonYaml(configPath); } catch (error) {
|
||||
findings.push({ severity: 'error', code: 'CONFIG_INVALID', message: error.message });
|
||||
}
|
||||
try { manifest = parseJsonYaml(manifestPath); } catch (error) {
|
||||
findings.push({ severity: 'error', code: 'MANIFEST_INVALID', message: error.message });
|
||||
}
|
||||
|
||||
if (manifest) {
|
||||
for (const record of manifest.created ?? []) {
|
||||
const path = join(repository.root, record.path);
|
||||
if (!existsSync(path)) findings.push({ severity: 'error', code: 'OWNED_FILE_MISSING', path: record.path });
|
||||
else if (fileHash(path) !== record.sha256) findings.push({ severity: 'info', code: 'OWNED_FILE_MODIFIED', path: record.path });
|
||||
}
|
||||
const stateRoot = projectStatePath(manifest.projectId, env);
|
||||
if (!existsSync(stateRoot)) findings.push({ severity: 'error', code: 'LOCAL_STATE_MISSING', path: stateRoot });
|
||||
}
|
||||
|
||||
if (config?.project?.id && manifest?.projectId && config.project.id !== manifest.projectId) {
|
||||
findings.push({ severity: 'error', code: 'PROJECT_ID_MISMATCH' });
|
||||
}
|
||||
if (repository.dirtyEntries.length) {
|
||||
findings.push({ severity: 'info', code: 'WORKTREE_DIRTY', count: repository.dirtyEntries.length });
|
||||
}
|
||||
|
||||
return {
|
||||
status: findings.some((finding) => finding.severity === 'error') ? 'broken' : 'ok',
|
||||
healthy: !findings.some((finding) => finding.severity === 'error'),
|
||||
projectId: config?.project?.id ?? null,
|
||||
findings,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export class TorchError extends Error {
|
||||
constructor(message, { code = 'TORCH_ERROR', details = undefined } = {}) {
|
||||
super(message);
|
||||
this.name = 'TorchError';
|
||||
this.code = code;
|
||||
this.details = details;
|
||||
}
|
||||
}
|
||||
|
||||
export function asErrorRecord(error) {
|
||||
return {
|
||||
error: error?.code ?? 'UNEXPECTED_ERROR',
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
...(error?.details === undefined ? {} : { details: error.details }),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { createHash } from 'node:crypto';
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
||||
import { dirname } from 'node:path';
|
||||
import { TorchError } from './errors.mjs';
|
||||
|
||||
export function sha256(content) {
|
||||
return createHash('sha256').update(content).digest('hex');
|
||||
}
|
||||
|
||||
export function writeNewFile(path, content) {
|
||||
if (existsSync(path)) {
|
||||
throw new TorchError(`Refusing to overwrite existing path: ${path}`, {
|
||||
code: 'PATH_ALREADY_EXISTS',
|
||||
details: { path },
|
||||
});
|
||||
}
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
writeFileSync(path, content, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||
return { path, sha256: sha256(content) };
|
||||
}
|
||||
|
||||
export function fileHash(path) {
|
||||
return sha256(readFileSync(path));
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import { realpathSync } from 'node:fs';
|
||||
import { basename } from 'node:path';
|
||||
import { TorchError } from './errors.mjs';
|
||||
|
||||
function git(cwd, args, { optional = false } = {}) {
|
||||
try {
|
||||
return execFileSync('git', ['-C', cwd, ...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() || undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function inspectRepository(cwd = process.cwd()) {
|
||||
const rootText = git(cwd, ['rev-parse', '--show-toplevel'], { optional: true });
|
||||
if (!rootText) {
|
||||
throw new TorchError(`Not a Git repository: ${cwd}`, { code: 'NOT_GIT_REPOSITORY' });
|
||||
}
|
||||
|
||||
const root = realpathSync(rootText);
|
||||
const branch = git(root, ['branch', '--show-current'], { optional: true }) || null;
|
||||
const head = git(root, ['rev-parse', 'HEAD'], { optional: true }) || null;
|
||||
const initialCommit = head
|
||||
? git(root, ['rev-list', '--max-parents=0', 'HEAD'], { optional: true }).split('\n')[0] || null
|
||||
: null;
|
||||
const status = git(root, ['status', '--porcelain=v1', '--untracked-files=all'], { optional: true });
|
||||
const remotesText = git(root, ['remote', '-v'], { optional: true });
|
||||
const remotes = remotesText
|
||||
? remotesText.split('\n').map((line) => {
|
||||
const [name, url, kindText = ''] = line.split(/\s+/);
|
||||
return { name, url, kind: kindText.replace(/[()]/g, '') || 'unknown' };
|
||||
})
|
||||
: [];
|
||||
|
||||
return {
|
||||
root,
|
||||
name: basename(root),
|
||||
branch,
|
||||
head,
|
||||
initialCommit,
|
||||
dirtyEntries: status ? status.split('\n') : [],
|
||||
remotes,
|
||||
canonicalRemote: remotes.find((remote) => remote.name === 'origin' && remote.kind === 'fetch') ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
export function trackedFiles(root) {
|
||||
const output = git(root, ['ls-files', '-z'], { optional: true });
|
||||
return output ? output.split('\0').filter(Boolean) : [];
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import {
|
||||
existsSync, mkdirSync, readFileSync, readdirSync, rmSync, writeFileSync,
|
||||
} from 'node:fs';
|
||||
import { dirname, join, relative, resolve, sep } from 'node:path';
|
||||
import { TorchError } from './errors.mjs';
|
||||
import { fileHash, writeNewFile } from './files.mjs';
|
||||
import { projectStatePath } from './paths.mjs';
|
||||
|
||||
const TRACKED_DIR = '.torch';
|
||||
|
||||
function jsonYaml(value) {
|
||||
return `${JSON.stringify(value, null, 2)}\n`;
|
||||
}
|
||||
|
||||
function initialFiles({ repository, projectId, createdAt }) {
|
||||
const config = {
|
||||
schema: 'torch.dev/v1alpha1',
|
||||
project: {
|
||||
id: projectId,
|
||||
name: repository.name,
|
||||
main_branch: repository.branch || 'main',
|
||||
},
|
||||
paths: { tracked_state: TRACKED_DIR, worktree_parent: '~/TORCHWorktrees' },
|
||||
repository: repository.canonicalRemote
|
||||
? { canonical: { type: 'remote', remote: repository.canonicalRemote.name } }
|
||||
: { canonical: { type: 'unconfigured' } },
|
||||
forge: { provider: 'none' },
|
||||
git: {
|
||||
branch_prefix: 'torch/', convergence: 'merge', allow_rebase: false,
|
||||
allow_force_push: false, allow_bare_stash: false,
|
||||
},
|
||||
synchronization: { strategy: 'dispatcher-managed', auto_merge_worktrees: false },
|
||||
session_manager: { id: 'session-manager', start_last: true },
|
||||
domains: [],
|
||||
};
|
||||
const roster = {
|
||||
schema: 'torch.dev/roster/v1alpha1',
|
||||
areas: [{
|
||||
id: 'session-manager', title: 'TORCH Session Manager',
|
||||
scope: ['routing', 'priority', 'ownership rulings', 'fleet health'],
|
||||
not_scope: ['project feature implementation by default'],
|
||||
neighbours: ['all'],
|
||||
}],
|
||||
};
|
||||
|
||||
return new Map([
|
||||
['torch.yaml', jsonYaml(config)],
|
||||
['roster.yaml', jsonYaml(roster)],
|
||||
['decisions.md', '# TORCH decisions\n\nNo project decisions recorded yet.\n'],
|
||||
['RESUME-BRIEF.md', '# TORCH resume brief\n\nFleet not started yet.\n'],
|
||||
['prompts/COMMON.md', '# Common fleet rules\n\nRepository state outranks conversation memory. Query ownership before crossing a domain boundary.\n'],
|
||||
['prompts/session-manager.md', '# TORCH Session Manager\n\nRoute owner requests, establish ownership and priority, and keep routine coordination inside the fleet.\n'],
|
||||
['backlog/.gitkeep', ''],
|
||||
['INSTALLATION.md', `# TORCH installation\n\nInstalled ${createdAt}. Run \`torch doctor\` before starting the fleet.\n`],
|
||||
]);
|
||||
}
|
||||
|
||||
function manifestPath(root) {
|
||||
return join(root, TRACKED_DIR, 'install-manifest.json');
|
||||
}
|
||||
|
||||
function readManifest(root) {
|
||||
const path = manifestPath(root);
|
||||
if (!existsSync(path)) {
|
||||
throw new TorchError(`No TORCH ownership manifest at ${path}`, { code: 'NOT_INSTALLED' });
|
||||
}
|
||||
try {
|
||||
return JSON.parse(readFileSync(path, 'utf8'));
|
||||
} catch (error) {
|
||||
throw new TorchError(`Invalid TORCH ownership manifest: ${path}`, {
|
||||
code: 'INVALID_INSTALL_MANIFEST', details: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function safeProjectPath(root, relativePath) {
|
||||
const target = resolve(root, relativePath);
|
||||
const prefix = `${resolve(root)}${sep}`;
|
||||
if (!target.startsWith(prefix)) {
|
||||
throw new TorchError(`Manifest path escapes project root: ${relativePath}`, {
|
||||
code: 'UNSAFE_MANIFEST_PATH', details: { relativePath },
|
||||
});
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
export function planInstall({ repository, env = process.env }) {
|
||||
const trackedRoot = join(repository.root, TRACKED_DIR);
|
||||
return {
|
||||
action: 'install',
|
||||
projectRoot: repository.root,
|
||||
trackedRoot,
|
||||
stateParent: join(projectStatePath('<new-project-id>', env), '..'),
|
||||
wouldCreateTrackedState: !existsSync(trackedRoot),
|
||||
mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function installProject({
|
||||
repository,
|
||||
env = process.env,
|
||||
projectId = randomUUID(),
|
||||
now = () => new Date(),
|
||||
} = {}) {
|
||||
const trackedRoot = join(repository.root, TRACKED_DIR);
|
||||
if (existsSync(trackedRoot)) {
|
||||
throw new TorchError(`Refusing to replace existing ${trackedRoot}`, {
|
||||
code: 'ALREADY_INSTALLED', details: { trackedRoot },
|
||||
});
|
||||
}
|
||||
|
||||
const createdAt = now().toISOString();
|
||||
const stateRoot = projectStatePath(projectId, env);
|
||||
if (existsSync(stateRoot)) {
|
||||
throw new TorchError(`Refusing to replace existing TORCH state: ${stateRoot}`, {
|
||||
code: 'STATE_ALREADY_EXISTS', details: { stateRoot },
|
||||
});
|
||||
}
|
||||
|
||||
const created = [];
|
||||
try {
|
||||
for (const [path, content] of initialFiles({ repository, projectId, createdAt })) {
|
||||
const absolute = join(trackedRoot, path);
|
||||
const record = writeNewFile(absolute, content);
|
||||
created.push({ path: relative(repository.root, record.path), sha256: record.sha256 });
|
||||
}
|
||||
|
||||
mkdirSync(dirname(stateRoot), { recursive: true });
|
||||
mkdirSync(stateRoot, { recursive: false });
|
||||
for (const dir of ['sessions', 'messages', 'locks', 'leases', 'checks', 'integration', 'logs']) {
|
||||
mkdirSync(join(stateRoot, dir));
|
||||
}
|
||||
writeFileSync(join(stateRoot, 'project.json'), jsonYaml({
|
||||
schema: 'torch.dev/local-project/v1alpha1', projectId,
|
||||
root: repository.root, createdAt, detachedAt: null,
|
||||
}), { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
||||
|
||||
const manifest = {
|
||||
schema: 'torch.dev/install-manifest/v1alpha1',
|
||||
installationId: randomUUID(),
|
||||
projectId,
|
||||
createdAt,
|
||||
created,
|
||||
patched: [],
|
||||
external: [{ type: 'local-state', path: stateRoot }],
|
||||
};
|
||||
writeNewFile(manifestPath(repository.root), jsonYaml(manifest));
|
||||
return { projectId, trackedRoot, stateRoot, manifest, mutationPerformed: true };
|
||||
} catch (error) {
|
||||
if (existsSync(trackedRoot)) rmSync(trackedRoot, { recursive: true, force: true });
|
||||
if (existsSync(stateRoot)) rmSync(stateRoot, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function nonEmptyEntries(path) {
|
||||
if (!existsSync(path)) return [];
|
||||
return readdirSync(path, { recursive: true, withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name !== 'project.json')
|
||||
.map((entry) => entry.name);
|
||||
}
|
||||
|
||||
export function planUninstall({ repository, purge = false }) {
|
||||
const manifest = readManifest(repository.root);
|
||||
const problems = [];
|
||||
for (const record of manifest.created) {
|
||||
const path = safeProjectPath(repository.root, record.path);
|
||||
if (!existsSync(path)) problems.push({ type: 'missing', path: record.path });
|
||||
else if (fileHash(path) !== record.sha256) problems.push({ type: 'modified', path: record.path });
|
||||
}
|
||||
for (const entry of manifest.external ?? []) {
|
||||
if (entry.type === 'local-state') {
|
||||
const extras = nonEmptyEntries(entry.path);
|
||||
if (extras.length) problems.push({ type: 'local-state-not-empty', path: entry.path, entries: extras });
|
||||
}
|
||||
}
|
||||
return {
|
||||
action: purge ? 'purge' : 'detach', manifest, problems,
|
||||
canProceed: !purge || problems.length === 0,
|
||||
mutationPerformed: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function uninstallProject({ repository, purge = false, dryRun = false }) {
|
||||
const plan = planUninstall({ repository, purge });
|
||||
if (dryRun || !purge) return plan;
|
||||
if (!plan.canProceed) {
|
||||
throw new TorchError('TORCH purge stopped because managed state changed', {
|
||||
code: 'UNSAFE_TO_PURGE', details: plan.problems,
|
||||
});
|
||||
}
|
||||
|
||||
for (const record of [...plan.manifest.created].reverse()) {
|
||||
rmSync(safeProjectPath(repository.root, record.path));
|
||||
}
|
||||
rmSync(manifestPath(repository.root));
|
||||
rmSync(join(repository.root, TRACKED_DIR), { recursive: true });
|
||||
for (const entry of plan.manifest.external ?? []) {
|
||||
if (entry.type === 'local-state' && existsSync(entry.path)) rmSync(entry.path, { recursive: true });
|
||||
}
|
||||
return { ...plan, mutationPerformed: true };
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { homedir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
|
||||
export function torchDataHome(env = process.env) {
|
||||
const dataHome = env.XDG_DATA_HOME || join(homedir(), '.local', 'share');
|
||||
return join(dataHome, 'torch');
|
||||
}
|
||||
|
||||
export function projectStatePath(projectId, env = process.env) {
|
||||
return join(torchDataHome(env), 'projects', projectId);
|
||||
}
|
||||
Reference in new issue
Block a user