From 1fb668064e2379ad74e42d1d6e0df8d8d183babd Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 25 Feb 2026 16:17:25 -0500 Subject: [PATCH] update --- README.md | 5 + landing/index.html | 53 +++------ src/cmd-doctor.mjs | 232 +++++++++++++++++++++++++++++++++++++++ src/constants.mjs | 1 + src/lib.mjs | 7 ++ src/ops.mjs | 63 +++++++++++ test/cmd-doctor.test.mjs | 65 +++++++++++ test/ops.test.mjs | 34 ++++++ 8 files changed, 422 insertions(+), 38 deletions(-) create mode 100644 src/cmd-doctor.mjs create mode 100644 test/cmd-doctor.test.mjs diff --git a/README.md b/README.md index bec9b94..b81388d 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ npx --no-install torch-lock check --cadence daily npx --no-install torch-lock dashboard --port 4173 --host 127.0.0.1 ``` +`torch-lock init` and `torch-lock update` now auto-upsert the `## TORCH Memory Integration` block into root-level `AGENTS.md` and `CLAUDE.md` (and create `AGENTS.md` if neither file exists). + > TORCH is distributed from GitHub tarballs and is not currently published to the npm registry. > Use pinned commit tarballs (`archive/.tar.gz`) for repeatable installs. Avoid `refs/heads/main.tar.gz` for production rollouts. @@ -119,6 +121,9 @@ npx --no-install torch-lock init # Update TORCH configuration and scripts npx --no-install torch-lock update +# Validate setup and print actionable fixes +npx --no-install torch-lock doctor + # Remove TORCH completely from this project npx --no-install torch-lock remove diff --git a/landing/index.html b/landing/index.html index 17def05..4ad2697 100644 --- a/landing/index.html +++ b/landing/index.html @@ -938,9 +938,9 @@

Copy this happy-path prompt to onboard TORCH:

-

Requires Node.js 22+.

+

Requires Node.js 22+. `init` also hooks TORCH memory instructions into AGENTS.md / CLAUDE.md.

- Hey Jules, can you install and setup TORCH into this repo? Here is the install command: + Hey Jules, can you install and setup TORCH into this repo in one pass? Please run this command: npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init
-
-

Hook memory into your existing agents

-

- Add these instructions to your agent's system prompt (e.g. in AGENTS.md or CLAUDE.md) to enable the TORCH memory system. -

- -
- ## TORCH Memory Integration -You have access to the TORCH memory system. -1. READ: Check `.scheduler-memory/latest/${cadence}/memories.md` for past learnings. -2. WRITE: Before exiting, save new insights to `memory-update.md` so future runs can learn from this session. - -
-
-

Scheduler Meta Prompts

@@ -1637,21 +1616,19 @@ You have access to the TORCH memory system. // Update prompt text const installCmd = document.getElementById('install-cmd'); let promptText = ''; - - if (agent === 'jules') { - promptText = "Hey Jules, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'codex') { - promptText = "Hey Codex, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'opencode') { - promptText = "Hey OpenCode, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'claude') { - promptText = "Hey Claude, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'gemini') { - promptText = "Hey Gemini, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'qwen') { - promptText = "Hey Qwen, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; - } else if (agent === 'antigravity') { - promptText = "Hey Antigravity, can you install and setup TORCH into this repo? Here is the install command:\n\nnpm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init"; + const agentLabelById = { + jules: 'Jules', + codex: 'Codex', + opencode: 'OpenCode', + claude: 'Claude', + gemini: 'Gemini', + qwen: 'Qwen', + antigravity: 'Antigravity', + }; + const installCommand = 'npm install https://github.com/PR0M3TH3AN/TORCH/archive/refs/heads/main.tar.gz && npx --no-install torch-lock init'; + const agentLabel = agentLabelById[agent]; + if (agentLabel) { + promptText = `Hey ${agentLabel}, can you install and setup TORCH into this repo in one pass? Please run this command:\n\n${installCommand}`; } if (installCmd) { diff --git a/src/cmd-doctor.mjs b/src/cmd-doctor.mjs new file mode 100644 index 0000000..27320f2 --- /dev/null +++ b/src/cmd-doctor.mjs @@ -0,0 +1,232 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { detectTorchInstallDir } from './cmd-remove.mjs'; + +const MEMORY_HEADING = '## TORCH Memory Integration'; +const MEMORY_READ_LINE = '`.scheduler-memory/latest/${cadence}/memories.md`'; +const MEMORY_WRITE_LINE = '`memory-update.md`'; + +function parseMajorNodeVersion(version) { + const major = Number.parseInt(String(version).split('.')[0], 10); + return Number.isFinite(major) ? major : null; +} + +function createCheck(id, status, summary, fix = null) { + return { id, status, summary, fix }; +} + +function hasMemoryHook(content) { + return content.includes(MEMORY_HEADING) + && content.includes(MEMORY_READ_LINE) + && content.includes(MEMORY_WRITE_LINE); +} + +/** + * Runs setup and install diagnostics for a TORCH-enabled repository. + * + * @param {Object} [opts] + * @param {string} [opts.cwd] + * @param {string} [opts.nodeVersion] + * @returns {{ + * ok: boolean, + * summary: { passed: number, warned: number, failed: number }, + * checks: Array<{ id: string, status: 'pass'|'warn'|'fail', summary: string, fix: string|null }> + * }} + */ +export function runDoctorChecks(opts = {}) { + const cwd = opts.cwd || process.cwd(); + const nodeVersion = opts.nodeVersion || process.versions.node; + const checks = []; + + const nodeMajor = parseMajorNodeVersion(nodeVersion); + if (nodeMajor !== null && nodeMajor >= 22) { + checks.push(createCheck('node-version', 'pass', `Node.js ${nodeVersion} meets requirement (>= 22).`)); + } else { + checks.push(createCheck( + 'node-version', + 'fail', + `Node.js ${nodeVersion} is below requirement (>= 22).`, + 'Install Node.js 22+ and rerun `npx --no-install torch-lock doctor`.', + )); + } + + const installDir = detectTorchInstallDir(cwd); + if (!installDir) { + checks.push(createCheck( + 'install-detection', + 'fail', + 'No TORCH installation detected.', + 'Run `npm install --force && npx --no-install torch-lock init`.', + )); + } else { + const installLabel = installDir === '.' ? 'root install (.)' : `${installDir}/`; + checks.push(createCheck('install-detection', 'pass', `Detected TORCH install at ${installLabel}.`)); + } + + const configPath = path.join(cwd, 'torch-config.json'); + let config = null; + if (!fs.existsSync(configPath)) { + checks.push(createCheck( + 'torch-config', + 'fail', + 'Missing torch-config.json in project root.', + 'Run `npx --no-install torch-lock init --force` or `npx --no-install torch-lock update --force`.', + )); + } else { + try { + config = JSON.parse(fs.readFileSync(configPath, 'utf8')); + checks.push(createCheck('torch-config', 'pass', 'torch-config.json exists and parses as JSON.')); + } catch (error) { + checks.push(createCheck( + 'torch-config', + 'fail', + `torch-config.json is invalid JSON: ${error.message}`, + 'Fix JSON syntax or regenerate with `npx --no-install torch-lock update --force`.', + )); + } + } + + if (config) { + const dailyHandoff = config?.scheduler?.handoffCommandByCadence?.daily; + const weeklyHandoff = config?.scheduler?.handoffCommandByCadence?.weekly; + if (dailyHandoff && weeklyHandoff) { + checks.push(createCheck('handoff-config', 'pass', 'Scheduler handoff commands are configured for daily and weekly.')); + } else { + checks.push(createCheck( + 'handoff-config', + 'fail', + 'Scheduler handoff command is missing for daily and/or weekly cadence.', + 'Run `npx --no-install torch-lock update --force` to rehydrate scheduler config.', + )); + } + } + + if (installDir) { + const installRoot = path.resolve(cwd, installDir); + const expectedPaths = [ + path.join(installRoot, 'roster.json'), + path.join(installRoot, 'prompts', 'daily'), + path.join(installRoot, 'prompts', 'weekly'), + path.join(installRoot, 'scripts', 'agent', 'run-selected-prompt.mjs'), + ]; + const missing = expectedPaths.filter((targetPath) => !fs.existsSync(targetPath)); + if (missing.length === 0) { + checks.push(createCheck('install-files', 'pass', 'Core TORCH install files are present.')); + } else { + const missingRel = missing.map((targetPath) => path.relative(cwd, targetPath)).join(', '); + checks.push(createCheck( + 'install-files', + 'fail', + `Missing install artifacts: ${missingRel}`, + 'Run `npx --no-install torch-lock update --force` to repair copied files.', + )); + } + + const nodeModulesPath = path.join(installRoot, 'node_modules'); + if (fs.existsSync(nodeModulesPath)) { + checks.push(createCheck('install-deps', 'pass', `${path.relative(cwd, nodeModulesPath)} exists.`)); + } else { + checks.push(createCheck( + 'install-deps', + 'warn', + `${path.relative(cwd, nodeModulesPath)} is missing; TORCH package dependencies may not be installed yet.`, + `Run \`npm install --prefix ${installDir === '.' ? '.' : installDir}\`.`, + )); + } + } + + const promptFiles = ['AGENTS.md', 'CLAUDE.md'] + .map((name) => ({ name, filePath: path.join(cwd, name) })) + .filter((entry) => fs.existsSync(entry.filePath)); + + if (promptFiles.length === 0) { + checks.push(createCheck( + 'memory-hook', + 'warn', + 'No AGENTS.md or CLAUDE.md found in repository root.', + 'Run `npx --no-install torch-lock update --force` to create/update agent prompt hooks.', + )); + } else { + const withHook = []; + const withoutHook = []; + for (const entry of promptFiles) { + const content = fs.readFileSync(entry.filePath, 'utf8'); + if (hasMemoryHook(content)) { + withHook.push(entry.name); + } else { + withoutHook.push(entry.name); + } + } + + if (withHook.length > 0 && withoutHook.length === 0) { + checks.push(createCheck('memory-hook', 'pass', `Memory hook present in: ${withHook.join(', ')}.`)); + } else if (withHook.length > 0) { + checks.push(createCheck( + 'memory-hook', + 'warn', + `Memory hook present in ${withHook.join(', ')} but missing in ${withoutHook.join(', ')}.`, + 'Run `npx --no-install torch-lock update --force` to upsert missing prompt hooks.', + )); + } else { + checks.push(createCheck( + 'memory-hook', + 'fail', + `Memory hook missing from: ${withoutHook.join(', ')}.`, + 'Run `npx --no-install torch-lock update --force` to inject TORCH memory integration.', + )); + } + } + + const summary = { + passed: checks.filter((check) => check.status === 'pass').length, + warned: checks.filter((check) => check.status === 'warn').length, + failed: checks.filter((check) => check.status === 'fail').length, + }; + + return { + ok: summary.failed === 0, + summary, + checks, + }; +} + +/** + * Prints a human-readable setup diagnostic report. + * + * @param {Object} [opts] + * @param {string} [opts.cwd] + * @param {boolean} [opts.json] + * @param {Function} [opts.log] + * @returns {ReturnType} + */ +export function cmdDoctor(opts = {}) { + const { + cwd = process.cwd(), + json = false, + log = console.log, + } = opts; + + const report = runDoctorChecks({ cwd, nodeVersion: opts.nodeVersion }); + if (json) { + log(JSON.stringify(report, null, 2)); + return report; + } + + log('TORCH Doctor Report'); + log(`Root: ${cwd}`); + for (const check of report.checks) { + const badge = check.status === 'pass' ? '[PASS]' : check.status === 'warn' ? '[WARN]' : '[FAIL]'; + log(`${badge} ${check.id}: ${check.summary}`); + } + + log(''); + log(`Summary: ${report.summary.passed} passed, ${report.summary.warned} warned, ${report.summary.failed} failed.`); + if (report.summary.warned > 0 || report.summary.failed > 0) { + log('Recommended fixes:'); + for (const check of report.checks.filter((item) => item.status !== 'pass' && item.fix)) { + log(`- ${check.fix}`); + } + } + + return report; +} diff --git a/src/constants.mjs b/src/constants.mjs index be12c6b..143aae3 100644 --- a/src/constants.mjs +++ b/src/constants.mjs @@ -45,6 +45,7 @@ Commands: list [--cadence ] Print active lock table health --cadence Probe relay websocket + publish/read health dashboard [--port ] [--host ] Serve the dashboard (default: ${DEFAULT_DASHBOARD_PORT}) + doctor [--json] Validate local TORCH setup and print fixes init [--force] Initialize torch/ directory in current project update [--force] Update torch/ configuration (backups, merges) remove [--force] Remove all TORCH files and configuration from project diff --git a/src/lib.mjs b/src/lib.mjs index 280f439..0ebb1e6 100644 --- a/src/lib.mjs +++ b/src/lib.mjs @@ -44,6 +44,7 @@ import { cmdRollback } from './cmd-rollback.mjs'; import { cmdCheck } from './cmd-check.mjs'; import { cmdList } from './cmd-list.mjs'; import { cmdComplete } from './cmd-complete.mjs'; +import { cmdDoctor } from './cmd-doctor.mjs'; useWebSocketImplementation(WebSocket); @@ -308,6 +309,12 @@ const COMMAND_HANDLERS = { dashboard: async (args) => { await cmdDashboard(args.port, args.host); }, + doctor: async (args) => { + const report = cmdDoctor({ json: args.json }); + if (!report.ok) { + throw new ExitError(2, 'Doctor found setup failures'); + } + }, init: async (args) => { await cmdInit(args.force); }, diff --git a/src/ops.mjs b/src/ops.mjs index 9383d57..567e2a9 100644 --- a/src/ops.mjs +++ b/src/ops.mjs @@ -54,6 +54,12 @@ const APP_DIRS = ['src', 'bin', 'dashboard', 'landing', 'assets', 'scripts']; /** Individual files to sync from package root to install directory. */ const APP_FILES = ['package.json', 'build.mjs', 'README.md', 'torch-config.example.json', 'TORCH.md', 'eslint.config.mjs']; +const MEMORY_PROMPT_FILES = ['AGENTS.md', 'CLAUDE.md']; +const MEMORY_INTEGRATION_HEADING = '## TORCH Memory Integration'; +const MEMORY_INTEGRATION_BLOCK = `${MEMORY_INTEGRATION_HEADING} +You have access to the TORCH memory system. +1. READ: Check \`.scheduler-memory/latest/\${cadence}/memories.md\` for past learnings. +2. WRITE: Before exiting, save new insights to \`memory-update.md\` so future runs can learn from this session.`; /** * Resolves the absolute paths for the installation. @@ -478,6 +484,61 @@ function injectHostScriptsIfNeeded(paths, installDir) { } } +/** + * Upserts the TORCH memory integration section inside a system prompt file. + * + * @param {string} content - Existing file content. + * @returns {string} Updated file content. + */ +function upsertMemoryIntegrationBlock(content) { + if (content.includes(MEMORY_INTEGRATION_BLOCK)) { + return content; + } + + const escapedHeading = MEMORY_INTEGRATION_HEADING.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const sectionRegex = new RegExp(`(^|\\n)${escapedHeading}\\n[\\s\\S]*?(?=\\n##\\s|$)`, 'm'); + + if (sectionRegex.test(content)) { + return content.replace(sectionRegex, (_match, prefix) => `${prefix}${MEMORY_INTEGRATION_BLOCK}`); + } + + const trimmed = content.trimEnd(); + if (!trimmed) { + return `${MEMORY_INTEGRATION_BLOCK}\n`; + } + return `${trimmed}\n\n${MEMORY_INTEGRATION_BLOCK}\n`; +} + +/** + * Ensures AGENTS/CLAUDE system prompt files include TORCH memory integration. + * If neither file exists, creates AGENTS.md with the integration block. + * + * @param {string} root - Host project root. + */ +function ensureMemoryPromptHook(root) { + const existingTargets = MEMORY_PROMPT_FILES.filter((file) => fs.existsSync(path.join(root, file))); + + if (existingTargets.length === 0) { + const defaultPath = path.join(root, 'AGENTS.md'); + fs.writeFileSync(defaultPath, `${MEMORY_INTEGRATION_BLOCK}\n`, 'utf8'); + console.log(`Created ${path.relative(root, defaultPath)} with TORCH memory integration.`); + return; + } + + for (const file of existingTargets) { + const filePath = path.join(root, file); + const current = fs.readFileSync(filePath, 'utf8'); + const next = upsertMemoryIntegrationBlock(current); + + if (next !== current) { + fs.writeFileSync(filePath, next, 'utf8'); + console.log(`Updated ${path.relative(root, filePath)} with TORCH memory integration.`); + } else { + console.log(`TORCH memory integration already present in ${path.relative(root, filePath)}.`); + } + } +} + /** * Main entry point for `torch-lock init`. * @@ -505,6 +566,7 @@ export async function cmdInit(force = false, cwd = process.cwd(), mockAnswers = configureTorch(cwd, paths, installDir, namespace, relays, hashtag); const dashboardUrl = createDashboardLinkFile(paths, namespace, relays, hashtag); injectHostScriptsIfNeeded(paths, installDir); + ensureMemoryPromptHook(paths.root); console.log('\nInitialization complete.'); console.log('You can now customize the files in ' + path.relative(cwd, paths.torchDir) + '/'); @@ -712,5 +774,6 @@ export function cmdUpdate(force = false, cwd = process.cwd()) { } } + ensureMemoryPromptHook(paths.root); console.log('\nUpdate complete.'); } diff --git a/test/cmd-doctor.test.mjs b/test/cmd-doctor.test.mjs new file mode 100644 index 0000000..be8bc73 --- /dev/null +++ b/test/cmd-doctor.test.mjs @@ -0,0 +1,65 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import path from 'node:path'; +import os from 'node:os'; +import { cmdInit } from '../src/ops.mjs'; +import { runDoctorChecks, cmdDoctor } from '../src/cmd-doctor.mjs'; + +const MOCK_CONFIG = { + installDir: 'torch', + namespace: 'doctor-test-namespace', + hashtag: 'doctor-test-hashtag', + relays: ['wss://relay.damus.io'], +}; + +test('runDoctorChecks reports failure when TORCH is not installed', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-missing-')); + + const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' }); + assert.equal(report.ok, false); + assert.ok(report.checks.some((check) => check.id === 'install-detection' && check.status === 'fail')); +}); + +test('runDoctorChecks passes core checks after init', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-healthy-')); + await cmdInit(false, root, MOCK_CONFIG); + + // Simulate post-init dependency install for health check. + await fs.mkdir(path.join(root, 'torch', 'node_modules'), { recursive: true }); + + const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' }); + assert.equal(report.ok, true); + assert.ok(report.checks.some((check) => check.id === 'memory-hook' && check.status === 'pass')); + assert.ok(report.checks.some((check) => check.id === 'install-files' && check.status === 'pass')); +}); + +test('runDoctorChecks fails when memory hook is missing from existing AGENTS.md', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-hook-missing-')); + await cmdInit(false, root, MOCK_CONFIG); + + await fs.writeFile(path.join(root, 'AGENTS.md'), '# AGENTS\nNo memory section.\n', 'utf8'); + + const report = runDoctorChecks({ cwd: root, nodeVersion: '22.10.0' }); + assert.equal(report.ok, false); + const memoryCheck = report.checks.find((check) => check.id === 'memory-hook'); + assert.ok(memoryCheck); + assert.equal(memoryCheck.status, 'fail'); +}); + +test('cmdDoctor emits JSON report when requested', async () => { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'torch-doctor-json-')); + await cmdInit(false, root, MOCK_CONFIG); + + const logs = []; + const report = cmdDoctor({ + cwd: root, + json: true, + nodeVersion: '22.10.0', + log: (line) => logs.push(String(line)), + }); + + assert.ok(report.summary.passed >= 1); + assert.equal(logs.length, 1); + assert.doesNotThrow(() => JSON.parse(logs[0])); +}); diff --git a/test/ops.test.mjs b/test/ops.test.mjs index 99c4849..3d9a3c9 100644 --- a/test/ops.test.mjs +++ b/test/ops.test.mjs @@ -193,3 +193,37 @@ test('cmdInit creates torch-config.json with random namespace', async () => { 'weekly handoff command should be configured for host installs', ); }); + +test('cmdInit upserts TORCH memory integration into existing AGENTS.md and CLAUDE.md', async () => { + const projectRoot = path.join(tempBase, 'project_memory_hook_init'); + fs.mkdirSync(projectRoot, { recursive: true }); + + const agentsPath = path.join(projectRoot, 'AGENTS.md'); + const claudePath = path.join(projectRoot, 'CLAUDE.md'); + fs.writeFileSync(agentsPath, '# Team Policy\n\nKeep tests strict.\n', 'utf8'); + fs.writeFileSync(claudePath, '# Local Guidance\n\nFollow repo conventions.\n', 'utf8'); + + await cmdInit(false, projectRoot, MOCK_CONFIG); + + const agentsContent = fs.readFileSync(agentsPath, 'utf8'); + const claudeContent = fs.readFileSync(claudePath, 'utf8'); + assert.ok(agentsContent.includes('## TORCH Memory Integration'), 'AGENTS.md should include memory heading'); + assert.ok(claudeContent.includes('## TORCH Memory Integration'), 'CLAUDE.md should include memory heading'); + assert.ok(agentsContent.includes('Keep tests strict.'), 'AGENTS.md existing content should be preserved'); + assert.ok(claudeContent.includes('Follow repo conventions.'), 'CLAUDE.md existing content should be preserved'); +}); + +test('cmdUpdate keeps TORCH memory integration idempotent', async () => { + const projectRoot = path.join(tempBase, 'project_memory_hook_update'); + fs.mkdirSync(projectRoot, { recursive: true }); + + const agentsPath = path.join(projectRoot, 'AGENTS.md'); + fs.writeFileSync(agentsPath, '# Agent Rules\n', 'utf8'); + + await cmdInit(false, projectRoot, MOCK_CONFIG); + await cmdUpdate(false, projectRoot); + + const agentsContent = fs.readFileSync(agentsPath, 'utf8'); + const headingMatches = agentsContent.match(/## TORCH Memory Integration/g) ?? []; + assert.strictEqual(headingMatches.length, 1, 'Memory integration block should not be duplicated'); +});