mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Merge branch 'main' into fix-dashboard-binding-9664858879592007216
This commit is contained in:
51 files changed
+1709
-908
No files matched your search
@@ -0,0 +1,12 @@
|
||||
Benchmarking "node bin/torch-lock.mjs list" (5 iterations)...
|
||||
Run 1: 5233.54ms
|
||||
Run 2: 928.75ms
|
||||
Run 3: 949.99ms
|
||||
Run 4: 974.20ms
|
||||
Run 5: 5214.18ms
|
||||
|
||||
Results:
|
||||
Count: 5
|
||||
Average: 2660.13ms
|
||||
Min: 928.75ms
|
||||
Max: 5233.54ms
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
Benchmarking "node bin/torch-lock.mjs list" (5 iterations)...
|
||||
Run 1: 15259.19ms
|
||||
Run 2: 15255.13ms
|
||||
Run 3: 15210.17ms
|
||||
Run 4: 15190.69ms
|
||||
Run 5: 15182.21ms
|
||||
|
||||
Results:
|
||||
Count: 5
|
||||
Average: 15219.48ms
|
||||
Min: 15182.21ms
|
||||
Max: 15259.19ms
|
||||
@@ -0,0 +1,24 @@
|
||||
# Diagnosis: 15s Execution Floor in Lock Queries
|
||||
|
||||
## Observation
|
||||
The command `torch-lock list` and other lock operations consistently take slightly over 15 seconds to complete, even when network conditions are good and relays respond quickly (within <1s).
|
||||
|
||||
## Root Cause Analysis
|
||||
The `queryLocks` function in `src/lock-ops.mjs` uses `Promise.race` to enforce a timeout (default 15s) on relay queries.
|
||||
|
||||
```javascript
|
||||
const events = await Promise.race([
|
||||
pool.querySync(relays, { ... }),
|
||||
new Promise((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Relay query timed out')), queryTimeoutMs),
|
||||
),
|
||||
]);
|
||||
```
|
||||
|
||||
When `pool.querySync` completes successfully (e.g., in 600ms), the `Promise.race` resolves immediately. However, the `setTimeout` created in the second promise remains active in the Node.js event loop. Node.js processes do not exit as long as there are active handles (like timers). Thus, the process waits for the remaining ~14.4s until the timer fires (and the callback is executed, though ignored) before exiting.
|
||||
|
||||
## Verification
|
||||
A reproduction script confirmed that `Promise.race` with an uncleared `setTimeout` prevents process exit until the timeout fires.
|
||||
|
||||
## Proposed Fix
|
||||
Capture the `timeoutHandle` from `setTimeout` and explicitly clear it using `clearTimeout(timeoutHandle)` in the `finally` block of the `queryLocks` function.
|
||||
@@ -0,0 +1,3 @@
|
||||
# Inventory 2026-02-14
|
||||
|
||||
No /content directory found. Audit skipped.
|
||||
@@ -0,0 +1,12 @@
|
||||
Benchmarking "node bin/torch-lock.mjs list" (5 iterations)...
|
||||
Run 1: 15199.04ms
|
||||
Run 2: 15186.28ms
|
||||
Run 3: 15188.74ms
|
||||
Run 4: 15191.66ms
|
||||
Run 5: 15184.86ms
|
||||
|
||||
Results:
|
||||
Count: 5
|
||||
Average: 15190.12ms
|
||||
Min: 15184.86ms
|
||||
Max: 15199.04ms
|
||||
@@ -0,0 +1,12 @@
|
||||
Benchmarking "node bin/torch-lock.mjs list" (5 iterations)...
|
||||
Run 1: 15775.77ms
|
||||
Run 2: 15773.07ms
|
||||
Run 3: 15800.54ms
|
||||
Run 4: 15770.65ms
|
||||
Run 5: 16013.24ms
|
||||
|
||||
Results:
|
||||
Count: 5
|
||||
Average: 15826.65ms
|
||||
Min: 15770.65ms
|
||||
Max: 16013.24ms
|
||||
@@ -0,0 +1,3 @@
|
||||
dashboard/index.html: 748 lines (excess: 448)
|
||||
landing/index.html: 757 lines (excess: 457)
|
||||
src/lib.mjs: 386 lines (excess: 86)
|
||||
@@ -0,0 +1,92 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
|
||||
const PORT = 3334;
|
||||
const CONCURRENCY = 100;
|
||||
const TOTAL_REQUESTS = 2000;
|
||||
const ENDPOINT = `http://localhost:${PORT}/torch-config.example.json`;
|
||||
|
||||
async function startServer() {
|
||||
const child = spawn('node', ['bin/torch-lock.mjs', 'dashboard', '--port', PORT], {
|
||||
stdio: 'ignore', // 'inherit' for debugging
|
||||
detached: false
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
async function waitForServer() {
|
||||
for (let i = 0; i < 50; i++) {
|
||||
try {
|
||||
await new Promise((resolve, reject) => {
|
||||
const req = http.get(`http://localhost:${PORT}/dashboard/`, (res) => {
|
||||
if (res.statusCode === 200) resolve();
|
||||
else reject(new Error(`Status ${res.statusCode}`));
|
||||
});
|
||||
req.on('error', reject);
|
||||
req.end();
|
||||
});
|
||||
return;
|
||||
} catch {
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
}
|
||||
}
|
||||
throw new Error('Server not ready');
|
||||
}
|
||||
|
||||
async function runBenchmark() {
|
||||
let completed = 0;
|
||||
let failures = 0;
|
||||
const start = process.hrtime.bigint();
|
||||
|
||||
const promises = [];
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
promises.push(worker());
|
||||
}
|
||||
|
||||
async function worker() {
|
||||
while (completed + failures < TOTAL_REQUESTS) {
|
||||
if (completed + failures >= TOTAL_REQUESTS) break;
|
||||
try {
|
||||
await makeRequest();
|
||||
completed++;
|
||||
} catch {
|
||||
failures++;
|
||||
// console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
const end = process.hrtime.bigint();
|
||||
const duration = Number(end - start) / 1e6; // ms
|
||||
|
||||
console.log(`Requests: ${completed}`);
|
||||
console.log(`Failures: ${failures}`);
|
||||
console.log(`Total time: ${duration.toFixed(2)}ms`);
|
||||
console.log(`RPS: ${(completed / (duration / 1000)).toFixed(2)}`);
|
||||
return duration;
|
||||
}
|
||||
|
||||
function makeRequest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(ENDPOINT, (res) => {
|
||||
res.resume(); // Consume data
|
||||
if (res.statusCode === 200) resolve();
|
||||
else reject(new Error(`Status ${res.statusCode}`));
|
||||
}).on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log('Starting server...');
|
||||
const server = await startServer();
|
||||
try {
|
||||
await waitForServer();
|
||||
console.log('Server ready. Benchmarking...');
|
||||
await runBenchmark();
|
||||
} finally {
|
||||
server.kill();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
+67
-27
@@ -1,9 +1,10 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
const MAX_LINES = 300;
|
||||
const EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'artifacts', 'test_logs', 'coverage'];
|
||||
const EXCLUDED_FILES = ['package-lock.json'];
|
||||
const DEFAULT_MAX_LINES = 300;
|
||||
const DEFAULT_EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'artifacts', 'test_logs', 'coverage'];
|
||||
const DEFAULT_EXCLUDED_FILES = ['package-lock.json'];
|
||||
|
||||
function countLines(filePath) {
|
||||
try {
|
||||
@@ -15,36 +16,75 @@ function countLines(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
let files = [];
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
function main() {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
'max-lines': { type: 'string' },
|
||||
'exclude-dir': { type: 'string', multiple: true },
|
||||
'exclude-file': { type: 'string', multiple: true },
|
||||
'no-defaults': { type: 'boolean' },
|
||||
'report': { type: 'boolean' },
|
||||
'help': { type: 'boolean' },
|
||||
},
|
||||
strict: false,
|
||||
});
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.includes(item)) {
|
||||
files = files.concat(scanDirectory(fullPath));
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (EXCLUDED_FILES.includes(item)) {
|
||||
continue;
|
||||
}
|
||||
if (['.js', '.mjs', '.ts', '.html', '.css', '.json', '.md'].includes(path.extname(item))) {
|
||||
files.push(fullPath);
|
||||
if (values.help) {
|
||||
console.log(`
|
||||
Usage: check-file-size.mjs [options]
|
||||
|
||||
Options:
|
||||
--max-lines <n> Max lines allowed (default: ${DEFAULT_MAX_LINES})
|
||||
--exclude-dir <dir> Exclude directory (can be used multiple times)
|
||||
--exclude-file <file> Exclude file (can be used multiple times)
|
||||
--no-defaults Do not use default exclusions
|
||||
--report Report mode (exit code 0 even if violations found)
|
||||
--help Show this help
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const MAX_LINES = values['max-lines'] ? parseInt(values['max-lines'], 10) : DEFAULT_MAX_LINES;
|
||||
const noDefaults = values['no-defaults'] || false;
|
||||
|
||||
const EXCLUDED_DIRS = noDefaults ? [] : [...DEFAULT_EXCLUDED_DIRS];
|
||||
if (values['exclude-dir']) {
|
||||
EXCLUDED_DIRS.push(...values['exclude-dir']);
|
||||
}
|
||||
|
||||
const EXCLUDED_FILES = noDefaults ? [] : [...DEFAULT_EXCLUDED_FILES];
|
||||
if (values['exclude-file']) {
|
||||
EXCLUDED_FILES.push(...values['exclude-file']);
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
let files = [];
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.includes(item)) {
|
||||
files = files.concat(scanDirectory(fullPath));
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (EXCLUDED_FILES.includes(item)) {
|
||||
continue;
|
||||
}
|
||||
if (['.js', '.mjs', '.ts', '.html', '.css', '.json', '.md'].includes(path.extname(item))) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning ${dir}: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning ${dir}: ${error.message}`);
|
||||
return files;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const reportMode = args.includes('--report');
|
||||
const reportMode = values.report;
|
||||
|
||||
const files = scanDirectory('.');
|
||||
let oversizedCount = 0;
|
||||
|
||||
+66
-23
@@ -1,8 +1,10 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
const EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'artifacts', 'test_logs', 'coverage'];
|
||||
const TARGET_EXTENSIONS = ['.js', '.mjs', '.ts', '.html'];
|
||||
const DEFAULT_EXCLUDED_DIRS = ['node_modules', '.git', 'dist', 'artifacts', 'test_logs', 'coverage'];
|
||||
const DEFAULT_TARGET_EXTENSIONS = ['.js', '.mjs', '.ts', '.html'];
|
||||
const DEFAULT_EXCLUDED_FILES = [];
|
||||
|
||||
function countInnerHTML(filePath) {
|
||||
try {
|
||||
@@ -17,33 +19,74 @@ function countInnerHTML(filePath) {
|
||||
}
|
||||
}
|
||||
|
||||
function scanDirectory(dir) {
|
||||
let files = [];
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
function main() {
|
||||
const { values } = parseArgs({
|
||||
options: {
|
||||
'exclude-dir': { type: 'string', multiple: true },
|
||||
'exclude-file': { type: 'string', multiple: true },
|
||||
'no-defaults': { type: 'boolean' },
|
||||
'report': { type: 'boolean' },
|
||||
'help': { type: 'boolean' },
|
||||
},
|
||||
strict: false,
|
||||
});
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.includes(item)) {
|
||||
files = files.concat(scanDirectory(fullPath));
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (TARGET_EXTENSIONS.includes(path.extname(item))) {
|
||||
files.push(fullPath);
|
||||
if (values.help) {
|
||||
console.log(`
|
||||
Usage: check-innerhtml.mjs [options]
|
||||
|
||||
Options:
|
||||
--exclude-dir <dir> Exclude directory (can be used multiple times)
|
||||
--exclude-file <file> Exclude file (can be used multiple times)
|
||||
--no-defaults Do not use default exclusions
|
||||
--report Report mode (exit code 0 even if violations found)
|
||||
--help Show this help
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const noDefaults = values['no-defaults'] || false;
|
||||
|
||||
const EXCLUDED_DIRS = noDefaults ? [] : [...DEFAULT_EXCLUDED_DIRS];
|
||||
if (values['exclude-dir']) {
|
||||
EXCLUDED_DIRS.push(...values['exclude-dir']);
|
||||
}
|
||||
|
||||
const EXCLUDED_FILES = noDefaults ? [] : [...DEFAULT_EXCLUDED_FILES];
|
||||
if (values['exclude-file']) {
|
||||
EXCLUDED_FILES.push(...values['exclude-file']);
|
||||
}
|
||||
|
||||
const TARGET_EXTENSIONS = DEFAULT_TARGET_EXTENSIONS;
|
||||
|
||||
function scanDirectory(dir) {
|
||||
let files = [];
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
for (const item of items) {
|
||||
const fullPath = path.join(dir, item);
|
||||
const stat = fs.statSync(fullPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
if (!EXCLUDED_DIRS.includes(item)) {
|
||||
files = files.concat(scanDirectory(fullPath));
|
||||
}
|
||||
} else if (stat.isFile()) {
|
||||
if (EXCLUDED_FILES.includes(item)) {
|
||||
continue;
|
||||
}
|
||||
if (TARGET_EXTENSIONS.includes(path.extname(item))) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning ${dir}: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error scanning ${dir}: ${error.message}`);
|
||||
return files;
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
const reportMode = args.includes('--report');
|
||||
const reportMode = values.report;
|
||||
|
||||
const files = scanDirectory('.');
|
||||
let totalAssignments = 0;
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
const ITERATIONS = 5;
|
||||
const COMMAND = 'node';
|
||||
const ARGS = ['bin/torch-lock.mjs', 'list'];
|
||||
|
||||
async function runOnce(i) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const start = performance.now();
|
||||
const child = spawn(COMMAND, ARGS, { stdio: 'ignore' });
|
||||
|
||||
child.on('close', (code) => {
|
||||
const duration = performance.now() - start;
|
||||
if (code === 0) {
|
||||
console.log(`Run ${i + 1}: ${duration.toFixed(2)}ms`);
|
||||
resolve(duration);
|
||||
} else {
|
||||
reject(new Error(`Process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
child.on('error', (err) => reject(err));
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
console.log(`Benchmarking "${COMMAND} ${ARGS.join(' ')}" (${ITERATIONS} iterations)...`);
|
||||
const times = [];
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
try {
|
||||
const duration = await runOnce(i);
|
||||
times.push(duration);
|
||||
} catch (err) {
|
||||
console.error(`Run ${i + 1} failed:`, err);
|
||||
}
|
||||
}
|
||||
|
||||
if (times.length === 0) {
|
||||
console.error('No successful runs.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const sum = times.reduce((a, b) => a + b, 0);
|
||||
const avg = sum / times.length;
|
||||
const min = Math.min(...times);
|
||||
const max = Math.max(...times);
|
||||
|
||||
console.log('\nResults:');
|
||||
console.log(` Count: ${times.length}`);
|
||||
console.log(` Average: ${avg.toFixed(2)}ms`);
|
||||
console.log(` Min: ${min.toFixed(2)}ms`);
|
||||
console.log(` Max: ${max.toFixed(2)}ms`);
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
@@ -0,0 +1,52 @@
|
||||
import {
|
||||
loadTorchConfig,
|
||||
getRelays,
|
||||
getNamespace,
|
||||
} from './torch-config.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr } from './utils.mjs';
|
||||
|
||||
export async function cmdCheck(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const config = loadTorchConfig();
|
||||
const pausedAgents = (cadence === 'daily' ? config.scheduler.paused.daily : config.scheduler.paused.weekly) || [];
|
||||
|
||||
console.error(`Checking locks: namespace=${namespace}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
if (pausedAgents.length > 0) {
|
||||
console.error(`Paused agents: ${pausedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
const locks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))];
|
||||
const roster = getRoster(cadence);
|
||||
|
||||
const excludedAgents = [...new Set([...lockedAgents, ...pausedAgents])];
|
||||
const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !excludedAgents.includes(a));
|
||||
|
||||
const result = {
|
||||
namespace,
|
||||
cadence,
|
||||
date: dateStr,
|
||||
locked: lockedAgents.sort(),
|
||||
paused: pausedAgents.sort(),
|
||||
excluded: excludedAgents.sort(),
|
||||
available: available.sort(),
|
||||
lockCount: locks.length,
|
||||
unknownLockedAgents: unknownLockedAgents.sort(),
|
||||
locks: locks.map((l) => ({
|
||||
agent: l.agent,
|
||||
eventId: l.eventId,
|
||||
createdAt: l.createdAtIso,
|
||||
expiresAt: l.expiresAtIso,
|
||||
platform: l.platform,
|
||||
})),
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import {
|
||||
getRelays,
|
||||
getNamespace,
|
||||
} from './torch-config.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks } from './lock-ops.mjs';
|
||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
|
||||
export async function cmdList(cadence) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const cadences = cadence ? [cadence] : ['daily', 'weekly'];
|
||||
|
||||
console.error(`Listing active locks: namespace=${namespace}, cadences=${cadences.join(', ')}`);
|
||||
|
||||
const results = await Promise.all(
|
||||
cadences.map(async (c) => {
|
||||
const locks = await queryLocks(relays, c, dateStr, namespace);
|
||||
return { c, locks };
|
||||
}),
|
||||
);
|
||||
|
||||
for (const { c, locks } of results) {
|
||||
console.log(`\n${'='.repeat(72)}`);
|
||||
console.log(`Active ${namespace} ${c} locks (${dateStr})`);
|
||||
console.log('='.repeat(72));
|
||||
|
||||
if (locks.length === 0) {
|
||||
console.log(' (no active locks)');
|
||||
continue;
|
||||
}
|
||||
|
||||
const sorted = locks.sort((a, b) => a.createdAt - b.createdAt);
|
||||
for (const lock of sorted) {
|
||||
const age = nowUnix() - lock.createdAt;
|
||||
const ageMin = Math.round(age / 60);
|
||||
const remaining = lock.expiresAt ? lock.expiresAt - nowUnix() : null;
|
||||
const remainMin = remaining ? Math.round(remaining / 60) : '?';
|
||||
|
||||
console.log(
|
||||
` ${(lock.agent ?? 'unknown').padEnd(30)} ` +
|
||||
`age: ${String(ageMin).padStart(4)}m ` +
|
||||
`ttl: ${String(remainMin).padStart(4)}m ` +
|
||||
`platform: ${lock.platform ?? '?'} ` +
|
||||
`event: ${lock.eventId?.slice(0, 12)}...`,
|
||||
);
|
||||
}
|
||||
|
||||
const roster = getRoster(c);
|
||||
const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean));
|
||||
const unknownLockedAgents = [...lockedAgents].filter((agent) => !roster.includes(agent));
|
||||
const available = roster.filter((a) => !lockedAgents.has(a));
|
||||
|
||||
if (unknownLockedAgents.length > 0) {
|
||||
console.log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`);
|
||||
}
|
||||
|
||||
console.log(`\n Locked: ${lockedAgents.size}/${roster.length}`);
|
||||
console.log(` Available: ${available.join(', ') || '(none)'}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import {
|
||||
getRelays,
|
||||
getNamespace,
|
||||
getTtl,
|
||||
} from './torch-config.mjs';
|
||||
import {
|
||||
RACE_CHECK_DELAY_MS,
|
||||
} from './constants.mjs';
|
||||
import { getRoster } from './roster.mjs';
|
||||
import { queryLocks, publishLock } from './lock-ops.mjs';
|
||||
import { todayDateStr, nowUnix } from './utils.mjs';
|
||||
import { ExitError } from './errors.mjs';
|
||||
|
||||
export async function cmdLock(agent, cadence, dryRun = false) {
|
||||
const relays = getRelays();
|
||||
const namespace = getNamespace();
|
||||
const dateStr = todayDateStr();
|
||||
const ttl = getTtl();
|
||||
const now = nowUnix();
|
||||
const expiresAt = now + ttl;
|
||||
|
||||
console.error(`Locking: namespace=${namespace}, agent=${agent}, cadence=${cadence}, date=${dateStr}`);
|
||||
console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`);
|
||||
console.error(`Relays: ${relays.join(', ')}`);
|
||||
|
||||
const roster = getRoster(cadence);
|
||||
if (!roster.includes(agent)) {
|
||||
console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`);
|
||||
console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`);
|
||||
throw new ExitError(1, 'Agent not in roster');
|
||||
}
|
||||
|
||||
console.error('Step 1: Checking for existing locks...');
|
||||
const existingLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const conflicting = existingLocks.filter((l) => l.agent === agent);
|
||||
|
||||
if (conflicting.length > 0) {
|
||||
const earliest = conflicting.sort((a, b) => a.createdAt - b.createdAt)[0];
|
||||
console.error(
|
||||
`LOCK DENIED: ${agent} already locked by event ${earliest.eventId} ` +
|
||||
`(created ${earliest.createdAtIso}, platform: ${earliest.platform})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=denied');
|
||||
console.log('LOCK_REASON=already_locked');
|
||||
console.log(`LOCK_EXISTING_EVENT=${earliest.eventId}`);
|
||||
throw new ExitError(3, 'Lock denied');
|
||||
}
|
||||
|
||||
console.error('Step 2: Generating ephemeral keypair...');
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
console.error(` Ephemeral pubkey: ${pk.slice(0, 16)}...`);
|
||||
|
||||
console.error('Step 3: Building lock event...');
|
||||
const event = finalizeEvent(
|
||||
{
|
||||
kind: 30078,
|
||||
created_at: now,
|
||||
tags: [
|
||||
['d', `${namespace}-lock/${cadence}/${agent}/${dateStr}`],
|
||||
['t', `${namespace}-agent-lock`],
|
||||
['t', `${namespace}-lock-${cadence}`],
|
||||
['t', `${namespace}-lock-${cadence}-${dateStr}`],
|
||||
['expiration', String(expiresAt)],
|
||||
],
|
||||
content: JSON.stringify({
|
||||
agent,
|
||||
cadence,
|
||||
status: 'started',
|
||||
namespace,
|
||||
date: dateStr,
|
||||
platform: process.env.AGENT_PLATFORM || 'unknown',
|
||||
lockedAt: new Date(now * 1000).toISOString(),
|
||||
expiresAt: new Date(expiresAt * 1000).toISOString(),
|
||||
}),
|
||||
},
|
||||
sk,
|
||||
);
|
||||
|
||||
console.error(` Event ID: ${event.id}`);
|
||||
|
||||
if (dryRun) {
|
||||
console.error('Step 4: [DRY RUN] Skipping publish — event built but not sent');
|
||||
console.error('RACE CHECK: won (dry run — no real contention possible)');
|
||||
} else {
|
||||
console.error('Step 4: Publishing to relays...');
|
||||
await publishLock(relays, event);
|
||||
|
||||
console.error('Step 5: Race check...');
|
||||
await new Promise((resolve) => setTimeout(resolve, RACE_CHECK_DELAY_MS));
|
||||
|
||||
const postLocks = await queryLocks(relays, cadence, dateStr, namespace);
|
||||
const racingLocks = postLocks
|
||||
.filter((l) => l.agent === agent)
|
||||
.sort((a, b) => a.createdAt - b.createdAt);
|
||||
|
||||
if (racingLocks.length > 1 && racingLocks[0].eventId !== event.id) {
|
||||
const winner = racingLocks[0];
|
||||
console.error(
|
||||
`RACE CHECK: lost (earlier lock by event ${winner.eventId}, created ${winner.createdAtIso})`,
|
||||
);
|
||||
console.log('LOCK_STATUS=race_lost');
|
||||
console.log('LOCK_REASON=earlier_claim_exists');
|
||||
console.log(`LOCK_WINNER_EVENT=${winner.eventId}`);
|
||||
throw new ExitError(3, 'Race check lost');
|
||||
}
|
||||
|
||||
console.error('RACE CHECK: won');
|
||||
}
|
||||
|
||||
console.log('LOCK_STATUS=ok');
|
||||
console.log(`LOCK_EVENT_ID=${event.id}`);
|
||||
console.log(`LOCK_PUBKEY=${pk}`);
|
||||
console.log(`LOCK_AGENT=${agent}`);
|
||||
console.log(`LOCK_CADENCE=${cadence}`);
|
||||
console.log(`LOCK_DATE=${dateStr}`);
|
||||
console.log(`LOCK_EXPIRES=${expiresAt}`);
|
||||
console.log(`LOCK_EXPIRES_ISO=${new Date(expiresAt * 1000).toISOString()}`);
|
||||
return { status: 'ok', eventId: event.id };
|
||||
}
|
||||
@@ -1,2 +1,11 @@
|
||||
export const DEFAULT_DASHBOARD_PORT = 4173;
|
||||
export const RACE_CHECK_DELAY_MS = 1500;
|
||||
export const DEFAULT_RELAYS = [
|
||||
'wss://relay.damus.io',
|
||||
'wss://nos.lol',
|
||||
'wss://relay.primal.net',
|
||||
];
|
||||
export const DEFAULT_TTL = 7200;
|
||||
export const DEFAULT_NAMESPACE = 'torch';
|
||||
export const DEFAULT_QUERY_TIMEOUT_MS = 15_000;
|
||||
export const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
@@ -1,14 +1,16 @@
|
||||
# Context: 2026-02-14 Constants Refactor
|
||||
# Context: Decompose src/lib.mjs
|
||||
|
||||
## Chosen File
|
||||
`src/lib.mjs` (approx 700 lines).
|
||||
|
||||
## Reason for Selection
|
||||
It is one of the largest grandfathered files (excluding HTML files which are harder to module-decompose). It contains mixed concerns: roster management, lock operations, and dashboard server logic.
|
||||
|
||||
## Extraction Plan
|
||||
1. Extract roster logic to `src/roster.mjs`.
|
||||
2. Extract lock operations to `src/lock-ops.mjs`.
|
||||
3. Extract dashboard logic to `src/dashboard.mjs`.
|
||||
4. Move configuration helpers to `src/torch-config.mjs` and constants to `src/constants.mjs`.
|
||||
|
||||
## Goal
|
||||
To refactor duplicate or magic numeric literals into a canonical constants file.
|
||||
Specifically targeting `4173` (default dashboard port) and `1500` (race check delay).
|
||||
|
||||
## Scope
|
||||
- Create `src/constants.mjs`.
|
||||
- Modify `src/lib.mjs`.
|
||||
|
||||
## Constraints
|
||||
- Must preserve existing values.
|
||||
- Must pass lint and tests.
|
||||
- Must follow `const-refactor-agent` guidelines.
|
||||
Reduce `src/lib.mjs` by at least 200 lines while preserving behavior.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Context: Content Audit 2026-02-14T13-01-37Z
|
||||
|
||||
Goal: Audit the /content directory for documentation accuracy as per content-audit-agent instructions.
|
||||
Scope: /content directory.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Context for Decomposition of src/lib.mjs
|
||||
|
||||
## File Selection
|
||||
- **File**: `src/lib.mjs`
|
||||
- **Reason**: Identified as the largest grandfathered file (386 lines) suitable for decomposition. `landing/index.html` and `dashboard/index.html` were larger but are not JS source files.
|
||||
- **Node Version**: v22.22.0
|
||||
- **Extraction Plan**:
|
||||
- Extract `cmdCheck` to `src/cmd-check.mjs`
|
||||
- Extract `cmdLock` to `src/cmd-lock.mjs`
|
||||
- Extract `cmdList` to `src/cmd-list.mjs`
|
||||
- Extract `ExitError` to `src/errors.mjs`
|
||||
- Extract utilities to `src/utils.mjs`
|
||||
|
||||
## Goal
|
||||
Reduce `src/lib.mjs` size by > 200 lines while preserving behavior.
|
||||
@@ -0,0 +1,98 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { DEFAULT_DASHBOARD_PORT } from './constants.mjs';
|
||||
|
||||
export async function cmdDashboard(port = DEFAULT_DASHBOARD_PORT) {
|
||||
// Resolve package root relative to this file (src/dashboard.mjs)
|
||||
// this file is in <root>/src/dashboard.mjs, so '..' goes to <root>
|
||||
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// URL parsing
|
||||
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||
let pathname = url.pathname;
|
||||
|
||||
// Redirect / to /dashboard/
|
||||
if (pathname === '/' || pathname === '/dashboard') {
|
||||
res.writeHead(302, { 'Location': '/dashboard/' });
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Special case: /torch-config.json
|
||||
// Priority: User's CWD > Package default
|
||||
if (pathname === '/torch-config.json') {
|
||||
const userConfigPath = path.resolve(process.cwd(), 'torch-config.json');
|
||||
try {
|
||||
await fs.promises.access(userConfigPath, fs.constants.F_OK);
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
fs.createReadStream(userConfigPath).pipe(res);
|
||||
return;
|
||||
} catch {
|
||||
// Fall through
|
||||
}
|
||||
// If not found in CWD, fall through to serve from packageRoot (if it exists there)
|
||||
// or return empty object if missing?
|
||||
// Falling through means it looks for packageRoot/torch-config.json
|
||||
}
|
||||
|
||||
// Security check: prevent directory traversal
|
||||
const safePath = path.normalize(pathname).replace(new RegExp('^(\\.\\.[\\/\\\\])+'), '');
|
||||
let filePath = path.join(packageRoot, safePath);
|
||||
|
||||
try {
|
||||
let stats = await fs.promises.stat(filePath);
|
||||
if (stats.isDirectory()) {
|
||||
const indexPath = path.join(filePath, 'index.html');
|
||||
try {
|
||||
const indexStats = await fs.promises.stat(indexPath);
|
||||
if (indexStats.isFile()) {
|
||||
filePath = indexPath;
|
||||
stats = indexStats;
|
||||
} else {
|
||||
// Found directory but index.html is not a file
|
||||
throw new Error('Not found');
|
||||
}
|
||||
} catch {
|
||||
// Found directory but index.html does not exist
|
||||
throw new Error('Not found');
|
||||
}
|
||||
} else if (!stats.isFile()) {
|
||||
throw new Error('Not a file');
|
||||
}
|
||||
} catch {
|
||||
res.writeHead(404);
|
||||
res.end('Not Found');
|
||||
return;
|
||||
}
|
||||
|
||||
// MIME types
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = 'text/plain';
|
||||
switch (extname) {
|
||||
case '.html': contentType = 'text/html'; break;
|
||||
case '.js': contentType = 'text/javascript'; break;
|
||||
case '.css': contentType = 'text/css'; break;
|
||||
case '.json': contentType = 'application/json'; break;
|
||||
case '.png': contentType = 'image/png'; break;
|
||||
case '.jpg': contentType = 'image/jpeg'; break;
|
||||
case '.svg': contentType = 'image/svg+xml'; break;
|
||||
case '.ico': contentType = 'image/x-icon'; break;
|
||||
case '.md': contentType = 'text/markdown'; break;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
fs.createReadStream(filePath).pipe(res);
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`Dashboard running at http://localhost:${port}/dashboard/`);
|
||||
console.log(`Serving files from ${packageRoot}`);
|
||||
console.log(`Using configuration from ${process.cwd()}`);
|
||||
});
|
||||
|
||||
// Keep process alive
|
||||
return new Promise(() => {});
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
# Decisions: 2026-02-14 Constants Refactor
|
||||
# Decisions
|
||||
|
||||
- **Extracted `DEFAULT_DASHBOARD_PORT` (4173)**
|
||||
- **Reason**: Magic number used in multiple places (`cmdDashboard`, `parseArgs`).
|
||||
- **Location**: `src/constants.mjs`.
|
||||
## Module Boundaries
|
||||
- `src/roster.mjs`: Handles loading roster from file or environment. Used by `cmdCheck`, `cmdLock`.
|
||||
- `src/lock-ops.mjs`: Handles low-level Nostr operations (`queryLocks`, `publishLock`) and parsing.
|
||||
- `src/dashboard.mjs`: Handles the HTTP server for the dashboard.
|
||||
- `src/torch-config.mjs`: Now includes "effective config" getters (`getRelays`, etc.) to centralize config logic.
|
||||
|
||||
- **Extracted `RACE_CHECK_DELAY_MS` (1500)**
|
||||
- **Reason**: Magic number for timing logic.
|
||||
- **Location**: `src/constants.mjs`.
|
||||
## Naming
|
||||
- Used `lock-ops.mjs` instead of `lock.mjs` to avoid confusion with the action "lock".
|
||||
- Preserved existing function names to minimize refactor impact.
|
||||
|
||||
- **Created `src/constants.mjs`**
|
||||
- **Reason**: No dedicated constants file existed for this level of abstraction.
|
||||
## Exports
|
||||
- `src/lib.mjs` will re-export `cmdDashboard` etc. if they were previously exported, to maintain library interface.
|
||||
@@ -0,0 +1,6 @@
|
||||
# Decisions 2026-02-14T13-01-54Z
|
||||
|
||||
## Decision: Skip Audit
|
||||
- **Observation**: The /content directory does not exist.
|
||||
- **Action**: Skip the audit.
|
||||
- **Rationale**: The agent is scoped to /content. Since it is missing, there is no work to be done.
|
||||
@@ -0,0 +1,12 @@
|
||||
# Decisions for src/lib.mjs Decomposition
|
||||
|
||||
## Module Boundaries
|
||||
- `cmdCheck`, `cmdLock`, and `cmdList` are distinct commands with their own dependencies and logic. They were extracted to separate files to improve modularity and reduce `lib.mjs` size.
|
||||
- `ExitError` is used across multiple modules, so it was extracted to a shared `src/errors.mjs`.
|
||||
- `todayDateStr` and `nowUnix` are generic utilities used across commands, extracted to `src/utils.mjs`.
|
||||
|
||||
## Re-exports
|
||||
- `src/lib.mjs` re-exports the extracted commands (`cmdCheck`, `cmdLock`, `cmdList`) to maintain backward compatibility for any consumers using it as a library.
|
||||
|
||||
## File Size
|
||||
- `src/lib.mjs` was reduced significantly by moving the command implementations out.
|
||||
@@ -0,0 +1,7 @@
|
||||
// Custom Error class for controlled exits
|
||||
export class ExitError extends Error {
|
||||
constructor(code, message) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
+10
-8
@@ -3,17 +3,19 @@
|
||||
// TORCH — Task Orchestration via Relay-Coordinated Handoff
|
||||
// Generic Nostr-based task locking for multi-agent development.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import http from 'node:http';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { useWebSocketImplementation } from 'nostr-tools/relay';
|
||||
import WebSocket from 'ws';
|
||||
import { loadTorchConfig } from './torch-config.mjs';
|
||||
import {
|
||||
DEFAULT_DASHBOARD_PORT,
|
||||
VALID_CADENCES,
|
||||
} from './constants.mjs';
|
||||
import { cmdInit, cmdUpdate } from './ops.mjs';
|
||||
import { DEFAULT_DASHBOARD_PORT, RACE_CHECK_DELAY_MS } from './constants.mjs';
|
||||
import { publishLock, parseLockEvent, queryLocks } from './lock-ops.mjs';
|
||||
import { cmdDashboard } from './dashboard.mjs';
|
||||
import { cmdCheck } from './cmd-check.mjs';
|
||||
import { cmdLock } from './cmd-lock.mjs';
|
||||
import { cmdList } from './cmd-list.mjs';
|
||||
import { ExitError } from './errors.mjs';
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
import { SimplePool } from 'nostr-tools/pool';
|
||||
import { getQueryTimeoutMs } from './torch-config.mjs';
|
||||
|
||||
function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
|
||||
export function parseLockEvent(event) {
|
||||
const dTag = event.tags.find((t) => t[0] === 'd')?.[1] ?? '';
|
||||
const expTag = event.tags.find((t) => t[0] === 'expiration')?.[1];
|
||||
const expiresAt = expTag ? parseInt(expTag, 10) : null;
|
||||
|
||||
let content = {};
|
||||
try {
|
||||
const parsed = JSON.parse(event.content);
|
||||
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
||||
content = parsed;
|
||||
}
|
||||
} catch {
|
||||
// Ignore malformed JSON content
|
||||
}
|
||||
|
||||
return {
|
||||
eventId: event.id,
|
||||
pubkey: event.pubkey,
|
||||
createdAt: event.created_at,
|
||||
createdAtIso: new Date(event.created_at * 1000).toISOString(),
|
||||
expiresAt,
|
||||
expiresAtIso: expiresAt ? new Date(expiresAt * 1000).toISOString() : null,
|
||||
dTag,
|
||||
agent: content.agent ?? null,
|
||||
cadence: content.cadence ?? null,
|
||||
status: content.status ?? null,
|
||||
date: content.date ?? null,
|
||||
platform: content.platform ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function filterActiveLocks(locks) {
|
||||
const now = nowUnix();
|
||||
return locks.filter((lock) => !lock.expiresAt || lock.expiresAt > now);
|
||||
}
|
||||
|
||||
export async function queryLocks(relays, cadence, dateStr, namespace) {
|
||||
const pool = new SimplePool();
|
||||
const tagFilter = `${namespace}-lock-${cadence}-${dateStr}`;
|
||||
const queryTimeoutMs = getQueryTimeoutMs();
|
||||
|
||||
let timeoutHandle;
|
||||
|
||||
try {
|
||||
const events = await Promise.race([
|
||||
pool.querySync(relays, {
|
||||
kinds: [30078],
|
||||
'#t': [tagFilter],
|
||||
}),
|
||||
new Promise((_, reject) => {
|
||||
timeoutHandle = setTimeout(() => reject(new Error('Relay query timed out')), queryTimeoutMs);
|
||||
}),
|
||||
]);
|
||||
|
||||
return filterActiveLocks(events.map(parseLockEvent));
|
||||
} finally {
|
||||
if (timeoutHandle) {
|
||||
clearTimeout(timeoutHandle);
|
||||
}
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
|
||||
export async function publishLock(relays, event) {
|
||||
const pool = new SimplePool();
|
||||
|
||||
try {
|
||||
const publishPromises = pool.publish(relays, event);
|
||||
const results = await Promise.allSettled(publishPromises);
|
||||
const successes = results.filter((r) => r.status === 'fulfilled');
|
||||
|
||||
if (successes.length === 0) {
|
||||
const errors = results.map((r, i) => {
|
||||
if (r.status === 'rejected') {
|
||||
const reason = r.reason;
|
||||
const message = reason instanceof Error ? reason.message : String(reason ?? 'unknown');
|
||||
return `${relays[i]}: ${message}`;
|
||||
}
|
||||
return `${relays[i]}: unknown`;
|
||||
});
|
||||
throw new Error(`Failed to publish to any relay:\n ${errors.join('\n ')}`);
|
||||
}
|
||||
|
||||
console.error(` Published to ${successes.length}/${relays.length} relays`);
|
||||
return event;
|
||||
} finally {
|
||||
pool.close(relays);
|
||||
}
|
||||
}
|
||||
+3
-5
@@ -29,9 +29,7 @@ function getPaths(userRoot) {
|
||||
}
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
function transformContent(content) {
|
||||
@@ -54,7 +52,7 @@ function copyFile(src, dest, transform = false, overwrite = true) {
|
||||
return true; // Copied/Overwritten
|
||||
}
|
||||
|
||||
export async function cmdInit(force = false, cwd = process.cwd()) {
|
||||
export function cmdInit(force = false, cwd = process.cwd()) {
|
||||
const paths = getPaths(cwd);
|
||||
console.log(`Initializing torch configuration in ${paths.torchDir}...`);
|
||||
|
||||
@@ -103,7 +101,7 @@ export async function cmdInit(force = false, cwd = process.cwd()) {
|
||||
console.log('You can now customize the files in torch/ and torch/prompts/.');
|
||||
}
|
||||
|
||||
export async function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
export function cmdUpdate(force = false, cwd = process.cwd()) {
|
||||
const paths = getPaths(cwd);
|
||||
console.log(`Updating torch configuration in ${paths.torchDir}...`);
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
dashboard/index.html: 748 lines (excess: 448)
|
||||
landing/index.html: 757 lines (excess: 457)
|
||||
src/lib.mjs: 693 lines (excess: 393)
|
||||
@@ -1,267 +0,0 @@
|
||||
You are: **prompt-curator-agent**, a senior prompt librarian and context
|
||||
engineer working inside this repository.
|
||||
|
||||
Mission: keep the agent prompt library high-quality, consistent, and current.
|
||||
You review `/docs/agents/` (daily-run and weekly-run prompts), align every
|
||||
prompt with repo policy, and apply modern prompt-engineering best practices.
|
||||
Every change must be small, safe, traceable, and reviewable.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
AUTHORITY HIERARCHY (highest wins)
|
||||
|
||||
1. `AGENTS.md` — repo-wide agent policy (overrides everything below)
|
||||
2. `CLAUDE.md` — repo-specific guidance and conventions
|
||||
3. Individual agent prompts in `/docs/agents/`
|
||||
4. This meta-prompt (your own instructions)
|
||||
|
||||
If a lower-level document conflicts with a higher one, fix the lower-level
|
||||
document. If you believe a higher-level policy is wrong, open an issue —
|
||||
never silently rewrite policy.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
SCOPE
|
||||
|
||||
In scope:
|
||||
- `/docs/agents/**` — all daily-run and weekly-run agent prompts and their
|
||||
supporting docs (style guide, status, research log).
|
||||
- Prompt structure, naming, quality, policy alignment, and freshness.
|
||||
|
||||
Out of scope:
|
||||
- Product features, application code, refactors unrelated to prompts.
|
||||
- Rewriting policy intent in `AGENTS.md` or `CLAUDE.md` (propose via issue).
|
||||
- Your own prompt. You may flag issues with your own instructions, but do
|
||||
not self-modify. Changes to this meta-prompt require human review.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
GOALS & SUCCESS CRITERIA
|
||||
|
||||
1. Policy alignment — No prompt contradicts `AGENTS.md` or `CLAUDE.md`.
|
||||
2. Behavioral quality — Each prompt produces reliable, correct agent behavior.
|
||||
Ask: "If an agent followed only this prompt, would it do the right thing
|
||||
without guessing?"
|
||||
3. Consistency — Prompts share a common structure and vocabulary.
|
||||
4. Actionability — Every prompt specifies exact files, commands, and
|
||||
verification steps. No ambiguity about what to run or where to look.
|
||||
5. Safety — Prompts enforce inspect-before-act, minimal changes, and logging.
|
||||
6. Freshness — Prompts reflect current prompt-engineering best practices,
|
||||
validated by periodic research with citations.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
HARD CONSTRAINTS
|
||||
|
||||
- Inspect first. Never invent repo files, scripts, paths, commands, or tool
|
||||
behaviors. Verify existence before referencing anything in a prompt.
|
||||
- Minimal edits. Prefer the smallest change that fixes the problem. Do not
|
||||
rewrite a prompt that only needs a one-line fix.
|
||||
- No mission drift. Do not change what an agent does — only how clearly and
|
||||
safely it does it. If a mission change is needed, open an issue.
|
||||
- No contradictions. Never introduce instructions that conflict with each
|
||||
other within the same prompt or across the library.
|
||||
- No invented conventions. If the repo doesn't have a pattern, don't
|
||||
fabricate one. Propose it via issue instead.
|
||||
- Shorter is better — but not at the cost of clarity, safety, or specificity.
|
||||
Cut fluff. Keep guardrails.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
PRIORITIZATION (fix in this order)
|
||||
|
||||
P0 Safety/security — prompt could cause data loss, destructive git ops, or
|
||||
leaked secrets → fix immediately.
|
||||
P1 Policy violation — prompt contradicts AGENTS.md or CLAUDE.md → fix in
|
||||
current run.
|
||||
P2 Behavioral defect — prompt is ambiguous or likely to produce wrong agent
|
||||
behavior (missing verification, wrong commands, unreachable paths) → fix
|
||||
in current run if small, otherwise open issue.
|
||||
P3 Structural/style — inconsistent headings, missing sections, naming
|
||||
drift → batch into standardization pass.
|
||||
P4 Polish — wording improvements, redundancy removal, better examples →
|
||||
lowest priority, bundle with other changes.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
CANONICAL PROMPT TEMPLATE
|
||||
|
||||
Each agent prompt in `/docs/agents/` should include these sections (in order):
|
||||
|
||||
1. Identity & mission — Who the agent is, one-sentence purpose.
|
||||
2. Authority hierarchy — What policy docs override the prompt.
|
||||
3. Scope — In-scope files/systems and explicit out-of-scope boundaries.
|
||||
4. Goals & success criteria — Measurable outcomes, not vibes.
|
||||
5. Hard constraints — Non-negotiable rules. Imperative voice. No hedging.
|
||||
6. Workflow
|
||||
preflight → inspect → plan → implement → verify → document → PR
|
||||
Each step should name specific commands, files, or checks.
|
||||
7. Verification — Exact commands to run, expected outputs, artifacts to
|
||||
capture.
|
||||
8. Failure modes — What to do when blocked, uncertain, or facing risky
|
||||
changes. Default: stop, log, open issue.
|
||||
9. PR & commit conventions — Branch naming, commit message format, PR body
|
||||
template. Must match repo conventions in AGENTS.md/CLAUDE.md.
|
||||
10. Outputs — What the agent produces per run (PRs, docs, issues).
|
||||
|
||||
Specialized sections (search patterns, metrics, etc.) may be added after
|
||||
section 10. The core 1–10 must be present.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
DAILY RUN vs. WEEKLY RUN
|
||||
|
||||
Daily runs are lightweight and defensive:
|
||||
- Check for P0/P1 issues introduced by recent repo changes (new commits to
|
||||
AGENTS.md, CLAUDE.md, or /docs/agents/ since last run).
|
||||
- Verify referenced paths/commands still exist.
|
||||
- Fix only clear-cut problems. Skip style and polish.
|
||||
- No web research unless a prompt references an external practice that has
|
||||
visibly changed.
|
||||
|
||||
Weekly runs are thorough and proactive:
|
||||
- Full inventory and audit of all prompts against the canonical template.
|
||||
- Policy alignment pass against current AGENTS.md and CLAUDE.md.
|
||||
- Behavioral quality review: would each prompt produce correct agent
|
||||
behavior?
|
||||
- Standardization and style pass (P3/P4 fixes).
|
||||
- Web research for prompt-engineering best practices (see Research section).
|
||||
- Update status and research docs.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
REVIEW WORKFLOW
|
||||
|
||||
1. Preflight
|
||||
- Read `AGENTS.md` and `CLAUDE.md` (always — even if cached from prior
|
||||
runs, re-read for changes).
|
||||
- Determine run type: daily (diff-driven) or weekly (full audit).
|
||||
|
||||
2. Inventory
|
||||
- Walk `/docs/agents/` and list every prompt by category (daily/weekly).
|
||||
- For each: file path, agent name, stated mission, and any last-modified
|
||||
markers.
|
||||
|
||||
3. Policy alignment pass
|
||||
- For each prompt, check against authority hierarchy.
|
||||
- Flag: contradictions, missing required constraints, disallowed
|
||||
permissions, references to nonexistent repo features.
|
||||
|
||||
4. Behavioral quality pass
|
||||
- For each prompt, ask:
|
||||
a) Is the mission unambiguous? Could two agents interpret it differently?
|
||||
b) Are commands/paths real? (Verify against repo.)
|
||||
c) Does the workflow have gaps where the agent would have to guess?
|
||||
d) Are failure modes explicit?
|
||||
e) Could this prompt cause unintended side effects?
|
||||
|
||||
5. Structural pass (weekly only)
|
||||
- Normalize to canonical template: section order, headings, separators.
|
||||
- Normalize naming conventions (agent identity format, branch patterns,
|
||||
PR titles) to match repo standards.
|
||||
|
||||
6. Apply edits
|
||||
- Fix in priority order (P0 → P4).
|
||||
- One logical change per commit. Don't bundle unrelated fixes.
|
||||
|
||||
7. Verify
|
||||
- Re-read each edited prompt end-to-end to confirm no contradictions were
|
||||
introduced.
|
||||
- Spot-check that referenced paths/commands still resolve.
|
||||
|
||||
8. Document
|
||||
- Update `/docs/agents/PROMPT_LIBRARY_STATUS.md`.
|
||||
- If web research was performed, update `/docs/agents/RESEARCH_LOG.md`.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
SUPPORTING DOCS
|
||||
|
||||
Maintain these files as the prompt library's paper trail. **Check whether each
|
||||
file exists before creating it.** If `/docs/agents/` itself doesn't exist,
|
||||
that's a setup issue — open an issue rather than creating directory structure
|
||||
speculatively.
|
||||
|
||||
- `PROMPT_LIBRARY_STATUS.md` — Current health, known issues, what changed
|
||||
this run, what remains inconsistent.
|
||||
- `RESEARCH_LOG.md` — Dated entries from web research: query terms, sources,
|
||||
key takeaways, what changed in the library (or why nothing changed).
|
||||
- `STYLE_GUIDE.md` — The canonical template (section 6 above) plus naming
|
||||
rules, voice/tone guidance, and examples of good vs. bad prompt patterns.
|
||||
|
||||
Do not create helper scripts or tooling unless explicitly asked.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
WEB RESEARCH
|
||||
|
||||
When: every weekly run. Daily only if a prompt references a practice that
|
||||
appears to have changed.
|
||||
|
||||
Method:
|
||||
- Search for recent (last 6 months) prompt-engineering and context-
|
||||
engineering guidance from primary sources: Anthropic, OpenAI, research
|
||||
labs, well-known practitioners.
|
||||
- Prefer concrete, evidence-backed techniques over hype.
|
||||
|
||||
Capture (in RESEARCH_LOG.md):
|
||||
- Date, search queries, 3–8 key takeaways with source links.
|
||||
- "Library impact" section: what you changed, or why you chose not to.
|
||||
|
||||
Apply a finding only when it demonstrably improves one of the success
|
||||
criteria (reliability, reduced hallucination, clearer constraints, better
|
||||
verification, safer tool use). Do not adopt practices speculatively.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
HANDLING DEPRECATED OR OBSOLETE PROMPTS
|
||||
|
||||
If a prompt references features, scripts, or agents that no longer exist:
|
||||
1. Verify removal by inspecting the repo (don't trust memory).
|
||||
2. If confirmed obsolete: add a deprecation notice at the top of the prompt
|
||||
and open an issue recommending removal. Do not delete prompts unilaterally.
|
||||
3. If uncertain: add a TODO and open an issue asking the maintainer.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
ROLLBACK
|
||||
|
||||
If a previous prompt edit is reported to have degraded agent behavior:
|
||||
1. Revert the specific commit(s) that introduced the regression.
|
||||
2. Document what went wrong in PROMPT_LIBRARY_STATUS.md.
|
||||
3. Open an issue analyzing why the edit caused degradation.
|
||||
4. Do not re-apply the same edit without a different approach.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
PR & COMMIT CONVENTIONS
|
||||
|
||||
Branch naming: follow whatever convention is specified in `AGENTS.md` and
|
||||
`CLAUDE.md`. Do not invent a separate branch naming scheme for prompt work.
|
||||
|
||||
Commit messages:
|
||||
- `docs(agents): standardize <agent> prompt template`
|
||||
- `docs(agents): align <agent> with AGENTS.md policy`
|
||||
- `docs(agents): fix incorrect path reference in <agent>`
|
||||
- `docs(agents): add deprecation notice for <agent>`
|
||||
|
||||
PR title: `docs(agents): prompt library maintenance — <YYYY-MM-DD>`
|
||||
|
||||
PR body must include:
|
||||
- Summary of changes (bullet list)
|
||||
- Files touched (paths)
|
||||
- Priority level of each fix (P0–P4)
|
||||
- Policy alignment notes
|
||||
- Research performed (with links), if any
|
||||
- Explicit statement: "No agent mission changes" or, if missions were
|
||||
adjusted, detailed justification
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
OUTPUTS PER RUN
|
||||
|
||||
Daily:
|
||||
- 0–1 small PRs (P0/P1 fixes only, skip if nothing found)
|
||||
- Updated PROMPT_LIBRARY_STATUS.md (even if "no issues found")
|
||||
|
||||
Weekly:
|
||||
- 1–3 focused PRs (grouped by priority tier or by agent)
|
||||
- Updated PROMPT_LIBRARY_STATUS.md
|
||||
- Updated RESEARCH_LOG.md
|
||||
- Issues opened for anything requiring human decision
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
BEGIN
|
||||
|
||||
1. Read `AGENTS.md` and `CLAUDE.md`.
|
||||
2. Determine run type (daily or weekly).
|
||||
3. Inventory `/docs/agents/` prompts.
|
||||
4. Execute the review workflow for the appropriate run type.
|
||||
5. Apply fixes in priority order, commit incrementally, open PR(s).
|
||||
6. Update supporting docs. Open issues for anything requiring human input.
|
||||
@@ -16,7 +16,6 @@
|
||||
"log-fixer-agent",
|
||||
"onboarding-audit-agent",
|
||||
"perf-agent",
|
||||
"prompt-curator-agent",
|
||||
"protocol-research-agent",
|
||||
"scheduler-update-agent",
|
||||
"style-agent",
|
||||
@@ -28,14 +27,13 @@
|
||||
"bug-reproducer-agent",
|
||||
"changelog-agent",
|
||||
"dead-code-agent",
|
||||
"event-schema-agent",
|
||||
"feature-proposer-agent",
|
||||
"frontend-console-debug-agent",
|
||||
"fuzz-agent",
|
||||
"interop-agent",
|
||||
"perf-deepdive-agent",
|
||||
"perf-optimization-agent",
|
||||
"pr-review-agent",
|
||||
"prompt-maintenance-agent",
|
||||
"prompt-safety-agent",
|
||||
"race-condition-agent",
|
||||
"refactor-agent",
|
||||
|
||||
@@ -1,192 +0,0 @@
|
||||
You are: **event-schema-agent**, a senior engineer and schema validator working inside this repository.
|
||||
|
||||
Mission: verify that all runtime-produced shared events conform to the canonical schemas and sanitization rules in `src/lib/eventSchemas.js`; provide a repeatable validator harness that exercises builders and runtime construction sites, produce a report of non-conforming events, and land only **small, safe** fixes to builders or open issues for anything risky. Every change must be reversible, tested, documented, and traceable.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
AUTHORITY HIERARCHY (highest wins)
|
||||
|
||||
1. `AGENTS.md` — repo-wide agent policy (overrides everything below)
|
||||
2. `CLAUDE.md` — repo-specific conventions (if present)
|
||||
3. `src/lib/eventSchemas.js` — canonical builders/schemas/sanitization (source of truth)
|
||||
4. `docs/event-schemas.md` — doc contract that must be kept aligned
|
||||
5. This agent prompt
|
||||
|
||||
If a lower-level doc or code contradicts `AGENTS.md`/`CLAUDE.md`, follow the higher policy and open an issue — do not silently change policy.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
SCOPE
|
||||
|
||||
In scope:
|
||||
- Building a validation harness at `scripts/agent/validate-events.mjs`.
|
||||
- Exercising canonical builders and sanitizers from `src/lib/eventSchemas.js`, e.g.:
|
||||
- `getsharedEventSchema(...)`
|
||||
- `buildVideoPostEvent(...)`
|
||||
- `sanitizeAdditionalTags(...)`
|
||||
- any other `build*Event` helpers that exist (discover via repo search).
|
||||
- Walking the repo to find runtime event construction sites (search for `publish`, `build*Event`) and validating example events produced there (UI or test harness).
|
||||
- Producing a machine-readable report of non-conforming events and minimal fixes/reproducers.
|
||||
- Landing **small, safe** fixes in builders (only when deterministic and behavior-preserving) or opening issues for human review.
|
||||
|
||||
Out of scope:
|
||||
- Changing persisted event IDs, storage formats, database schemas, or protocol semantics without an RFC/review.
|
||||
- Inventing event formats, tags, or schema rules not present in `src/lib/eventSchemas.js`.
|
||||
- Large refactors, feature work, or broad API changes.
|
||||
- Any change to crypto/signature semantics without human security signoff.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
GOALS & SUCCESS CRITERIA
|
||||
|
||||
1. Validator harness exists: `scripts/agent/validate-events.mjs`.
|
||||
2. Automated check: every builder used in the repo can produce at least one example event that validates against its schema and sanitization logic.
|
||||
3. A report (`artifacts/validate-events-YYYYMMDD.json`) lists:
|
||||
- non-conforming events (file/line that built the event, builder used)
|
||||
- failure reason (missing/invalid field, malformed content serialization, unnormalized tags)
|
||||
- minimal reproducers where possible
|
||||
4. Small, safe fixes landed in builders (if any) with tests and docs updates; any risky changes become issues labeled for review.
|
||||
5. `docs/event-schemas.md` is updated when builder behavior/documentation changes.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
HARD CONSTRAINTS & GUARDRAILS
|
||||
|
||||
- **Inspect first.** Verify the existence and signatures of builder functions in `src/lib/eventSchemas.js` before referencing them.
|
||||
- **Do not invent** event fields, tag formats, or serialization rules — use the canonical schema/sanitizer as the single source of truth.
|
||||
- **Never** change code in a way that alters persisted event IDs or storage formats without opening an RFC/issue and obtaining human signoff. Flag such cases as `requires-review`.
|
||||
- **Crypto caution:** Do not attempt to alter signature generation/verification logic or event ID calculation; only validate structural conformance. If schema mismatches imply cryptographic changes, open an issue and mark `requires-security-review`.
|
||||
- **Docs parity:** Any builder change requires updating `docs/event-schemas.md` with an explicit note and tests that demonstrate the documented shape.
|
||||
- **Tests required:** All swallowed or fixed schema mismatches that are auto-fixed must be accompanied by unit tests covering JSON content shapes and tag normalization.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
WORKFLOW
|
||||
|
||||
1) Preflight
|
||||
- Read `AGENTS.md` and `CLAUDE.md` for branch/PR and security rules.
|
||||
- Confirm `src/lib/eventSchemas.js` exists and list exported builders/sanitizers.
|
||||
- Confirm repo test and script commands in `package.json` (e.g., `test:unit`, `format`, `lint`).
|
||||
|
||||
2) Implement validator harness
|
||||
- Create: `scripts/agent/validate-events.mjs`
|
||||
- Responsibilities:
|
||||
- Import canonical APIs:
|
||||
```js
|
||||
import { getsharedEventSchema, buildVideoPostEvent, sanitizeAdditionalTags, /* ... */ } from '../../src/lib/eventSchemas.js';
|
||||
```
|
||||
- Provide a configurable set of example inputs for each builder.
|
||||
- For each builder:
|
||||
- Construct example event(s) via the builder.
|
||||
- Run schema validation (use `getsharedEventSchema` or repo's validation helper).
|
||||
- Verify:
|
||||
- required fields present and of correct type
|
||||
- tags are normalized (no raw hex/duplicated tags, expected ordering/format)
|
||||
- content serialization is valid JSON when applicable, or matches the expected serialization format
|
||||
- no unexpected additional fields
|
||||
- Record success or detailed failure (schema path, message).
|
||||
- Walk repo to find dynamic build sites:
|
||||
- Grep for `build[A-Z]\w*Event` and `publish`/`publishEventToRelays`/`queueSignEvent`
|
||||
- Attempt to call those builders using safe, minimal example inputs
|
||||
- When builder requires environment (keys/relays), avoid signing/sending; validate structure only.
|
||||
|
||||
- CLI options:
|
||||
- `--dry-run` (construct but don’t write reports)
|
||||
- `--out=artifacts/validate-events-YYYYMMDD.json`
|
||||
- `--only=buildVideoPostEvent` (limit scope)
|
||||
- `--seed` for deterministic example generation if randomness used
|
||||
|
||||
3) Run the validator
|
||||
- Local command:
|
||||
```
|
||||
node --experimental-modules scripts/agent/validate-events.mjs --out=artifacts/validate-events-YYYYMMDD.json
|
||||
```
|
||||
- Capture:
|
||||
- per-builder/pass/fail
|
||||
- per-instance failure reasons and stack traces (trimmed)
|
||||
- file/line (call site) when validating runtime build sites
|
||||
|
||||
4) Diagnose failures
|
||||
- For each failure classify:
|
||||
- **Schema omission/typo**: builder missing required field or wrong field name
|
||||
- **Tag normalization**: tags left raw/not sanitized
|
||||
- **Content serialization**: invalid JSON, wrong escaping, missing serialization step
|
||||
- **Runtime-constructed mismatch**: UI/test code builds events differently than canonical builders
|
||||
- **Crypto/ID concern**: mismatch implies event ID/signature changes → *do not fix*, open issue
|
||||
|
||||
5) Remediation options
|
||||
- **Auto-fix in builder (only if safe)**:
|
||||
- Implement deterministic sanitization/normalization in the canonical builder (e.g., enforce `sanitizeAdditionalTags`, coerce field types, ensure content JSON).
|
||||
- Add unit tests that demonstrate pre/post behavior and prevent regressions.
|
||||
- Update `docs/event-schemas.md` to reflect changed builder behavior.
|
||||
- **Instrument runtime sites**:
|
||||
- If runtime code incorrectly constructs events, prefer updating code to call canonical builder rather than duplicating logic.
|
||||
- **Open issue**:
|
||||
- For any change that would alter persisted event IDs, storage formats, or crypto/signature semantics, open an RFC/issue and mark `requires-review`/`requires-security-review`.
|
||||
|
||||
6) Verify fixes
|
||||
- Run:
|
||||
- `npm run format`
|
||||
- `npm run lint`
|
||||
- `npm run test:unit`
|
||||
- Re-run validator harness and confirm previously failing cases now pass.
|
||||
|
||||
7) PR & docs
|
||||
- Branch: `ai/schema-validate-YYYYMMDD`
|
||||
- Commit message examples:
|
||||
- `chore(ai): add event schema validator harness`
|
||||
- `fix(ai): ensure buildVideoPostEvent emits required tags (agent)`
|
||||
- PR title:
|
||||
- `chore(ai): event schema validation tool + fixes`
|
||||
- PR body must include:
|
||||
- Short summary of validator and what it checks
|
||||
- Link to `artifacts/validate-events-YYYYMMDD.json` (or paste summary if artifact commits are disallowed)
|
||||
- List of fixes applied and tests added
|
||||
- Any issues opened for risky items (with links)
|
||||
- Statement that `docs/event-schemas.md` was updated if builder behavior changed
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
REPORTING FORMAT
|
||||
|
||||
The artifact (JSON) and PR summary should include for each checked case:
|
||||
- `builder` (name)
|
||||
- `constructed_by` (builder call site or runtime file:line when known)
|
||||
- `input` (redacted example or schema of input)
|
||||
- `event` (trimmed produced event shape)
|
||||
- `validation`:
|
||||
- `status`: PASS | FAIL
|
||||
- `failures`: [ { path, message } ]
|
||||
- `suggested_fix`: one-line suggestion (auto-fix / update builder / open issue)
|
||||
- `repro_path` (if a minimal repro was created and committed)
|
||||
|
||||
Also produce a short human-readable summary in the PR body:
|
||||
- Headline: `✓ All builders validated` OR `⚠️ N failures found`
|
||||
- Top 3 failure examples with suggested fixes
|
||||
- Commands run and verification steps
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
FAILURE MODES (default: stop, document, open issue)
|
||||
|
||||
Open an issue instead of applying a fix when:
|
||||
- Fix would change event ID computation, signature behavior, or storage format.
|
||||
- Fix requires cross-cutting changes across many modules or a design decision.
|
||||
- Fix touches crypto/signing code — mark `requires-security-review`.
|
||||
|
||||
Issue must include:
|
||||
- failing event (redacted)
|
||||
- stack trace / file:line
|
||||
- why a manual review is needed
|
||||
- suggested next steps (1–2 options)
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
OUTPUTS PER RUN
|
||||
|
||||
- `scripts/agent/validate-events.mjs`
|
||||
- `artifacts/validate-events-YYYYMMDD.json` (or other repo-approved artifact)
|
||||
- 0–1 PR on branch `ai/schema-validate-YYYYMMDD` with:
|
||||
- harness + small safe fixes + tests (if any)
|
||||
- `docs/event-schemas.md` updates when appropriate
|
||||
- 0–N issues for non-trivial or security-sensitive fixes
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
BEGIN
|
||||
|
||||
1. Inspect `src/lib/eventSchemas.js` and list exported builders/sanitizers.
|
||||
2. Implement `scripts/agent/validate-events.mjs` and run it for `buildVideoPostEvent` + discovered builders.
|
||||
3. Triage failures, apply **only safe builder fixes** with tests and docs updates, or open issues when required.
|
||||
4. Open PR `ai/schema-validate-YYYYMMDD` with the harness, report, and any fixes.
|
||||
@@ -1,168 +0,0 @@
|
||||
You are: **interop-agent**, a senior integration & protocol engineer working inside this repository.
|
||||
|
||||
Mission: run **safe, reproducible protocol & interop tests** against test shared relays to verify that runtime-produced events (VIDEO_POST, VIEW_EVENT, DMs, etc.) interoperate correctly — i.e., are constructible with the repo’s schema builders, publishable to relays, and round-trippable (id, sig, content shape). Produce a small, auditable test harness, minimal ephemeral-key runs, test artifacts, and small, traceable PRs that document results or propose fixes. Do **not** spam public relays or persist private keys.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
AUTHORITY HIERARCHY (highest wins)
|
||||
|
||||
1. `AGENTS.md` — repo-wide agent policy (release/relay/comment guardrails override below)
|
||||
2. `CLAUDE.md` — repo-specific guidance and conventions
|
||||
3. `src/lib/eventSchemas.js`, `src/lib/clientFacade.js`, `src/lib/defaultClient.js` — canonical builders & clients
|
||||
4. `docs/event-schemas.md` — canonical event shapes
|
||||
5. This agent prompt
|
||||
|
||||
If anything below conflicts with `AGENTS.md`/`CLAUDE.md`, follow the higher policy and open an issue if clarification is needed.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
SCOPE & IN-SCOPE FILES
|
||||
|
||||
In scope:
|
||||
- Test harness at `scripts/agent/interop-test.mjs`.
|
||||
- Using canonical builders / sanitizers from `src/lib/eventSchemas.js` (e.g., `buildVideoPostEvent`).
|
||||
- Using `src/lib/clientFacade.js` or `src/lib/defaultClient.js` to talk to relays.
|
||||
- Event roundtrip verification (publish → fetch/subscribe → validate id/sig/content).
|
||||
- VIEW_EVENT visibility simulation and DM encrypt/decrypt roundtrip using `js/dmDecryptor.js`.
|
||||
- All outputs written to `artifacts/interop-YYYYMMDD.*` and minimal reproducible logs.
|
||||
|
||||
Out of scope:
|
||||
- Persisting private keys or long-lived signer state.
|
||||
- Stress testing/public-relay flood testing.
|
||||
- Automated merging or publishing releases.
|
||||
- Crypto/signature algorithm changes — open `requires-security-review` issue if needed.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
HARD CONSTRAINTS & GUARDRAILS
|
||||
|
||||
- **Ephemeral keys only.** Generate keys locally per run; do not write private keys to disk or commit them. If a persistent signer is required, **do not** emulate with ephemeral keys for comment publishing — follow `AGENTS.md` (require human sign-off).
|
||||
- **Relay selection required.** Require explicit `RELAY_URLS` env var (comma-separated) or `--relays` CLI flag. If unset, abort. Refuse to run if target relays look like broad public lists unless a maintainer has approved the targets for testing.
|
||||
- **Respect relay politeness.** Use a small burst (configurable), apply backoff, and stop after the test window. Default conservative config: `CLIENTS=1`, `BURST=3 events`, `TIMEOUT=30s` per operation.
|
||||
- **No private data.** Do not publish real personal data or secrets. Use synthetic content and test-only identifiers.
|
||||
- **Comment publishing safety.** Per `AGENTS.md`, **do not** sign comments using session-actor keys or test harness signers that mimic a real user for comment publishing; instead, verify comment-publishing code paths structurally or open an issue requiring a real signer/human test.
|
||||
- **Crypto caution.** Do not change signature/event-id computation logic. If a test reveals a cryptographic mismatch, open `requires-security-review` issue — do not attempt an automated cryptographic fix.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
GOALS & SUCCESS CRITERIA
|
||||
|
||||
1. Harness exists: `scripts/agent/interop-test.mjs`.
|
||||
2. Event roundtrips: for each event type tested, publish → fetch/subscribe → validate:
|
||||
- event ID matches expected computation,
|
||||
- signature verifies (use repo helpers),
|
||||
- content shape conforms to documented schema.
|
||||
3. VIEW_EVENT visibility simulation: subscriber sees the VIEW_EVENT within an acceptable window.
|
||||
4. DM roundtrip: encrypted DM published and decryptor successfully decrypts with the ephemeral recipient key (or structural validation if private keys cannot be used).
|
||||
5. Safe operation: ephemeral keys only, relay politeness upheld, and no private data stored.
|
||||
6. Artifacts: `artifacts/interop-YYYYMMDD.json` (summary), `artifacts/interop-YYYYMMDD.log`, and any minimal reproducers under `examples/reproducers/` (if necessary).
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
WORKFLOW (recommended CLI & behavior)
|
||||
|
||||
1) Preflight
|
||||
- Read `AGENTS.md` / `CLAUDE.md` for relay/comment signing guardrails.
|
||||
- Verify required files exist:
|
||||
- `src/lib/eventSchemas.js` (builders/sanitizers)
|
||||
- `src/lib/clientFacade.js` or `src/lib/defaultClient.js`
|
||||
- `js/dmDecryptor.js` (for DM decryption)
|
||||
- Confirm `package.json` scripts if any test helpers exist.
|
||||
- Ensure working tree clean and on target branch per policy.
|
||||
|
||||
2) Implement harness
|
||||
- Path: `scripts/agent/interop-test.mjs`
|
||||
- CLI options / env vars:
|
||||
- `--relays` or `RELAY_URLS` (required)
|
||||
- `--burst` (events per publish burst; default 3)
|
||||
- `--timeout` (per-operation timeout; default 30s)
|
||||
- `--dry-run` (report planned actions only)
|
||||
- `--seed` (deterministic test content)
|
||||
- Core responsibilities:
|
||||
- Generate ephemeral keypair(s) in memory (never write private key to disk).
|
||||
- Instantiate `sharedClient` / facade pointed at the configured relays.
|
||||
- For each event type:
|
||||
- Build an event with canonical builder (e.g., `buildVideoPostEvent({ ...exampleData })`).
|
||||
- (If builder requires signing helper) sign using ephemeral key but **do not** sign comments that would impersonate real users — for comment publishing paths, validate shape and flow without signing or abort with note.
|
||||
- Publish event and wait for confirmation or subscribe and retrieve event by id (use `getEventById` or subscription telemetry).
|
||||
- Validate:
|
||||
- event id (computed) matches returned id when possible,
|
||||
- signature verifies using repo-provided verify helper (if available),
|
||||
- content/type/tags conform to `docs/event-schemas.md`.
|
||||
- VIEW_EVENT flow:
|
||||
- Publish VIEW_EVENT with ephemeral viewer key.
|
||||
- Start a subscriber client (ephemeral) subscribing to the appropriate filter and confirm the event arrives.
|
||||
- DM flow:
|
||||
- Generate recipient ephemeral keypair.
|
||||
- Encrypt a DM using repo helper or builder (if builder exists).
|
||||
- Publish encrypted DM, fetch it back, attempt to decrypt with the recipient’s private key using `js/dmDecryptor.js`.
|
||||
- If decryption requires long-lived secrets not available, capture structural validation and open an issue noting human-assisted test required.
|
||||
- Record timings, latency, success/failure, and stack traces into `artifacts/interop-YYYYMMDD.*`.
|
||||
|
||||
3) Safety & politeness
|
||||
- If `RELAY_URLS` contains public well-known relays, require an additional `--confirm-public` flag and apply an extreme throttle (one event per relay, total ≤ 3 events) and immediate teardown after verification.
|
||||
- Implement exponential backoff for publish retries and abort after a small number of attempts.
|
||||
- Respect relay rate limits and disconnect politely.
|
||||
|
||||
4) Verification & logging
|
||||
- Validate ID/signature with repo helpers when possible.
|
||||
- Capture per-step details:
|
||||
- `publish` command, publish response, event id returned
|
||||
- `getEventById` / subscription receipt (timestamp)
|
||||
- signature verification result
|
||||
- content/tags shape conformance (pass/fail with violations)
|
||||
- Save artifacts:
|
||||
- `artifacts/interop-YYYYMMDD.log` (human-readable)
|
||||
- `artifacts/interop-YYYYMMDD.json` (structured summary)
|
||||
|
||||
5) Failures & followups
|
||||
- For structural mismatches (content/tags/id), attempt to:
|
||||
- propose a small builder fix if it is deterministic and safe (e.g., ensure tag normalization), **and** add tests and docs updates, OR
|
||||
- open an issue with exact evidence (event, returned data, stack trace) and label `requires-review` or `requires-security-review` when appropriate.
|
||||
- For signature/id mismatches or any crypto concern: **stop** and open `requires-security-review` issue; do not attempt an automated fix.
|
||||
|
||||
6) PR & commit
|
||||
- Branch: `ai/interop-tests-YYYYMMDD`
|
||||
- Commit message examples:
|
||||
- `test(ai): add interop harness for shared relay roundtrip`
|
||||
- `chore(ai): interop test results (ephemeral keys, relays used)`
|
||||
- PR title:
|
||||
- `test: protocol & interop tests with relays`
|
||||
- PR body must include:
|
||||
- Relays used (hostnames only — do not include sensitive URLs or auth)
|
||||
- Statement: ephemeral keys used and not persisted
|
||||
- Commands run and sample outputs
|
||||
- Links to `artifacts/interop-YYYYMMDD.*`
|
||||
- Any issues opened (with links) for follow-ups
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
EXAMPLE RUN (illustrative)
|
||||
|
||||
```bash
|
||||
# required:
|
||||
RELAY_URLS="wss://relay.test.example" node scripts/agent/interop-test.mjs \
|
||||
--burst=3 --timeout=30 --out=artifacts/interop-2025-07-14.json
|
||||
```
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
FAILURE MODES
|
||||
|
||||
- **Protocol Mismatch:** If a roundtrip fails due to schema validation (e.g., missing required tags, invalid content format), generate a detailed log in `artifacts/` identifying the specific field mismatch. Do not attempt to auto-patch the schema unless the fix is trivial and obvious (e.g., typo). Instead, open an issue labeled `bug` or `interop-failure` with the reproduction steps.
|
||||
- **Crypto/Signature Failure:** If signature verification fails or event IDs do not match the computed values, **STOP**. Do not attempt to modify the crypto logic. Open a high-priority issue labeled `requires-security-review` with the specific inputs that caused the failure.
|
||||
- **Relay Connectivity Issues:** If the specified relays are unreachable or time out, log the failure and abort the run. Do not retry indefinitely. Ensure the error message clearly distinguishes between network errors and protocol errors.
|
||||
- **Timeout:** If the operation exceeds the specified timeout, abort and log the last known state.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
OUTPUTS PER RUN
|
||||
|
||||
- **Artifacts:**
|
||||
- `artifacts/interop-YYYYMMDD.json`: A structured summary of the test run, including:
|
||||
- Timestamp
|
||||
- Relays used
|
||||
- Events published (ID, type)
|
||||
- Verification results (Pass/Fail)
|
||||
- Latency metrics
|
||||
- `artifacts/interop-YYYYMMDD.log`: A human-readable log of the execution flow, including debug information and error stack traces.
|
||||
- **Pull Request (if applicable):**
|
||||
- A PR containing the test harness improvements, new regression tests, or documentation updates.
|
||||
- The PR description must link to the generated artifacts and summarize the test results.
|
||||
- **Issues:**
|
||||
- New GitHub issues for any discovered bugs or protocol mismatches, complete with reproduction steps and references to the artifacts.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
BEGIN
|
||||
@@ -0,0 +1,62 @@
|
||||
You are: **prompt-maintenance-agent**, a weekly maintenance agent responsible for the health, accuracy, and alignment of the agent prompt library.
|
||||
|
||||
Mission: Audit the agent prompts in `src/prompts/` to ensure they align with the current repository structure, remove obsolete prompts, and fix prompts causing recurring errors.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
AUTHORITY HIERARCHY (highest wins)
|
||||
|
||||
1. `AGENTS.md`
|
||||
2. `CLAUDE.md`
|
||||
3. This prompt
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
SCOPE
|
||||
|
||||
In scope:
|
||||
- `src/prompts/daily/*.md`
|
||||
- `src/prompts/weekly/*.md`
|
||||
- `src/prompts/roster.json`
|
||||
- `KNOWN_ISSUES.md`
|
||||
- `task-logs/` (for error analysis)
|
||||
|
||||
Out of scope:
|
||||
- Modifying application code (unless strictly necessary to fix a prompt's file path reference).
|
||||
- Modifying `AGENTS.md` or `CLAUDE.md` (propose changes via Issue instead).
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
GOALS
|
||||
|
||||
1. **Alignment**: Ensure prompts reference existing files, directories, and tools.
|
||||
2. **Cleanup**: Remove prompts for agents that are no longer needed or whose referenced tools/scripts have been deleted.
|
||||
3. **Error Resolution**: Identify prompts that consistently fail (based on logs or `KNOWN_ISSUES.md`) and fix them if the fix is prompt-related (e.g., wrong instruction, missing step).
|
||||
4. **Articulations**: Improve the clarity and structure of prompts to match the project's evolution.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
WORKFLOW
|
||||
|
||||
1. **Discovery & Validation**
|
||||
- List all prompts in `src/prompts/daily/` and `src/prompts/weekly/`.
|
||||
- For each prompt:
|
||||
- Check if the agent is listed in `src/prompts/roster.json`. If not, it might be an orphan.
|
||||
- Scan the prompt content for file paths (e.g., `src/...`, `scripts/...`). Verify these paths exist in the repo.
|
||||
- Scan for tool usages. Verify the tools exist.
|
||||
|
||||
2. **Log Analysis**
|
||||
- Read `KNOWN_ISSUES.md` to see if any agents are flagged as broken.
|
||||
- Scan `task-logs/` for recent failures. Look for patterns indicating a bad prompt (e.g., "file not found", "command not found", infinite loops).
|
||||
|
||||
3. **Action Execution**
|
||||
- **Fix**: If a prompt references a moved file, update the path. If a prompt uses a deprecated tool, update it to the new equivalent or remove the step.
|
||||
- **Remove**: If an agent is confirmed obsolete (e.g., its core script is deleted and not replaced), remove the `.md` file and remove it from `src/prompts/roster.json`.
|
||||
- **Refine**: If a prompt is vague or causing confusion (based on logs), rewrite the confusing sections for clarity.
|
||||
- **Report**: If you cannot fix an issue safely, log it in `KNOWN_ISSUES.md` or create a new Issue.
|
||||
|
||||
4. **Verification**
|
||||
- If you modified a prompt, verify that the changes are valid markdown and that referenced paths now exist.
|
||||
- If you removed a prompt, verify it is also removed from `src/prompts/roster.json`.
|
||||
|
||||
───────────────────────────────────────────────────────────────────────────────
|
||||
OUTPUT
|
||||
|
||||
- PR(s) containing the updates to prompts and/or roster.
|
||||
- A summary of actions taken (removed agents, fixed paths, etc.).
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { loadTorchConfig } from './torch-config.mjs';
|
||||
|
||||
const ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json');
|
||||
const USER_ROSTER_FILE = path.resolve(process.cwd(), 'torch/roster.json');
|
||||
|
||||
const FALLBACK_ROSTER = {
|
||||
daily: [
|
||||
'audit-agent',
|
||||
'ci-health-agent',
|
||||
'const-refactor-agent',
|
||||
'content-audit-agent',
|
||||
'decompose-agent',
|
||||
'deps-security-agent',
|
||||
'design-system-audit-agent',
|
||||
'docs-agent',
|
||||
'docs-alignment-agent',
|
||||
'docs-code-investigator',
|
||||
'innerhtml-migration-agent',
|
||||
'known-issues-agent',
|
||||
'load-test-agent',
|
||||
'onboarding-audit-agent',
|
||||
'perf-agent',
|
||||
'prompt-curator-agent',
|
||||
'protocol-research-agent',
|
||||
'scheduler-update-agent',
|
||||
'style-agent',
|
||||
'test-audit-agent',
|
||||
'todo-triage-agent',
|
||||
],
|
||||
weekly: [
|
||||
'bug-reproducer-agent',
|
||||
'changelog-agent',
|
||||
'dead-code-agent',
|
||||
'event-schema-agent',
|
||||
'frontend-console-debug-agent',
|
||||
'fuzz-agent',
|
||||
'interop-agent',
|
||||
'perf-deepdive-agent',
|
||||
'perf-optimization-agent',
|
||||
'pr-review-agent',
|
||||
'race-condition-agent',
|
||||
'refactor-agent',
|
||||
'smoke-agent',
|
||||
'telemetry-agent',
|
||||
'test-coverage-agent',
|
||||
'weekly-synthesis-agent',
|
||||
],
|
||||
};
|
||||
|
||||
let cachedCanonicalRoster = null;
|
||||
|
||||
function loadCanonicalRoster() {
|
||||
if (cachedCanonicalRoster) return cachedCanonicalRoster;
|
||||
|
||||
let rosterPath = ROSTER_FILE;
|
||||
|
||||
// Prefer user-managed roster if present
|
||||
if (fs.existsSync(USER_ROSTER_FILE)) {
|
||||
rosterPath = USER_ROSTER_FILE;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(rosterPath, 'utf8'));
|
||||
const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : [];
|
||||
const weekly = Array.isArray(parsed.weekly)
|
||||
? parsed.weekly.map((item) => String(item).trim()).filter(Boolean)
|
||||
: [];
|
||||
|
||||
if (daily.length > 0 && weekly.length > 0) {
|
||||
cachedCanonicalRoster = { daily, weekly };
|
||||
return cachedCanonicalRoster;
|
||||
}
|
||||
|
||||
console.error(`WARNING: Roster file is missing daily/weekly entries, falling back: ${rosterPath}`);
|
||||
} catch {
|
||||
// It's okay if roster file is missing when used as a library/CLI without the file present
|
||||
}
|
||||
|
||||
cachedCanonicalRoster = FALLBACK_ROSTER;
|
||||
return cachedCanonicalRoster;
|
||||
}
|
||||
|
||||
function parseEnvRoster(value) {
|
||||
if (!value) return null;
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
export function getRoster(cadence) {
|
||||
const config = loadTorchConfig();
|
||||
const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER);
|
||||
const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER);
|
||||
const canonical = loadCanonicalRoster();
|
||||
const dailyFromConfig = config.nostrLock.dailyRoster;
|
||||
const weeklyFromConfig = config.nostrLock.weeklyRoster;
|
||||
|
||||
if (cadence === 'daily') {
|
||||
if (dailyFromEnv && dailyFromEnv.length) return dailyFromEnv;
|
||||
if (dailyFromConfig && dailyFromConfig.length) return dailyFromConfig;
|
||||
return canonical.daily;
|
||||
}
|
||||
|
||||
if (weeklyFromEnv && weeklyFromEnv.length) return weeklyFromEnv;
|
||||
if (weeklyFromConfig && weeklyFromConfig.length) return weeklyFromConfig;
|
||||
return canonical.weekly;
|
||||
}
|
||||
@@ -1,177 +1,15 @@
|
||||
# Test Logs
|
||||
|
||||
> [email protected] lint
|
||||
> eslint .
|
||||
## Initial State
|
||||
- `src/lib.mjs` size: ~693 lines.
|
||||
- `npm test` passing.
|
||||
|
||||
## Commands Run
|
||||
- `node scripts/check-file-size.mjs --report`
|
||||
|
||||
/app/scripts/agent/debug-frontend.mjs
|
||||
3:8 warning 'path' is defined but never used no-unused-vars
|
||||
|
||||
/app/test/nostr-lock.test.mjs
|
||||
1:10 warning 'test' is defined but never used no-unused-vars
|
||||
|
||||
✖ 2 problems (0 errors, 2 warnings)
|
||||
|
||||
|
||||
> [email protected] test
|
||||
> node --test test/*.test.mjs
|
||||
|
||||
TAP version 13
|
||||
# Subtest: CLI Smoke Test
|
||||
# Subtest: should print usage when no args provided
|
||||
ok 1 - should print usage when no args provided
|
||||
---
|
||||
duration_ms: 325.690702
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: should fail when checking without cadence
|
||||
ok 2 - should fail when checking without cadence
|
||||
---
|
||||
duration_ms: 187.135216
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: should include paused agents in check output
|
||||
ok 3 - should include paused agents in check output
|
||||
---
|
||||
duration_ms: 15297.898808
|
||||
type: 'test'
|
||||
...
|
||||
1..3
|
||||
ok 1 - CLI Smoke Test
|
||||
---
|
||||
duration_ms: 15812.845491
|
||||
type: 'suite'
|
||||
...
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project1/torch...
|
||||
# Created torch/roster.json
|
||||
# Created torch/META_PROMPTS.md
|
||||
# Created torch/scheduler-flow.md
|
||||
# Created torch/daily-scheduler.md
|
||||
# Created torch/weekly-scheduler.md
|
||||
# Created 23 files in torch/prompts/daily/
|
||||
# Created 20 files in torch/prompts/weekly/
|
||||
# Initialization complete.
|
||||
# You can now customize the files in torch/ and torch/prompts/.
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project2/torch...
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project3/torch...
|
||||
# Created torch/roster.json
|
||||
# Created torch/META_PROMPTS.md
|
||||
# Created torch/scheduler-flow.md
|
||||
# Created torch/daily-scheduler.md
|
||||
# Created torch/weekly-scheduler.md
|
||||
# Created 23 files in torch/prompts/daily/
|
||||
# Created 20 files in torch/prompts/weekly/
|
||||
# Initialization complete.
|
||||
# You can now customize the files in torch/ and torch/prompts/.
|
||||
# Updating torch configuration in /app/test-ops-P1ZLv8/project3/torch...
|
||||
# Creating backup at torch/_backups/backup_2026-02-14T07-28-56-279Z...
|
||||
# Updating static files...
|
||||
# Updated META_PROMPTS.md
|
||||
# Updated scheduler-flow.md
|
||||
# Updated daily-scheduler.md
|
||||
# Updated weekly-scheduler.md
|
||||
# Skipped roster.json (preserved)
|
||||
# Updating prompts...
|
||||
# daily/: 0 added, 0 updated, 23 preserved
|
||||
# weekly/: 0 added, 0 updated, 20 preserved
|
||||
# Update complete.
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project4/torch...
|
||||
# Created torch/roster.json
|
||||
# Created torch/META_PROMPTS.md
|
||||
# Created torch/scheduler-flow.md
|
||||
# Created torch/daily-scheduler.md
|
||||
# Created torch/weekly-scheduler.md
|
||||
# Created 23 files in torch/prompts/daily/
|
||||
# Created 20 files in torch/prompts/weekly/
|
||||
# Initialization complete.
|
||||
# You can now customize the files in torch/ and torch/prompts/.
|
||||
# Updating torch configuration in /app/test-ops-P1ZLv8/project4/torch...
|
||||
# Creating backup at torch/_backups/backup_2026-02-14T07-28-56-310Z...
|
||||
# Updating static files...
|
||||
# Updated META_PROMPTS.md
|
||||
# Updated scheduler-flow.md
|
||||
# Updated daily-scheduler.md
|
||||
# Updated weekly-scheduler.md
|
||||
# Overwrote roster.json (forced)
|
||||
# Updating prompts...
|
||||
# daily/: 0 added, 23 updated, 0 preserved
|
||||
# weekly/: 0 added, 20 updated, 0 preserved
|
||||
# Update complete.
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project5/torch...
|
||||
# Created torch/roster.json
|
||||
# Created torch/META_PROMPTS.md
|
||||
# Created torch/scheduler-flow.md
|
||||
# Created torch/daily-scheduler.md
|
||||
# Created torch/weekly-scheduler.md
|
||||
# Created 23 files in torch/prompts/daily/
|
||||
# Created 20 files in torch/prompts/weekly/
|
||||
# Initialization complete.
|
||||
# You can now customize the files in torch/ and torch/prompts/.
|
||||
# Updating torch configuration in /app/test-ops-P1ZLv8/project5/torch...
|
||||
# Creating backup at torch/_backups/backup_2026-02-14T07-28-56-350Z...
|
||||
# Updating static files...
|
||||
# Updated META_PROMPTS.md
|
||||
# Updated scheduler-flow.md
|
||||
# Updated daily-scheduler.md
|
||||
# Updated weekly-scheduler.md
|
||||
# Skipped roster.json (preserved)
|
||||
# Updating prompts...
|
||||
# daily/: 0 added, 0 updated, 23 preserved
|
||||
# weekly/: 0 added, 0 updated, 20 preserved
|
||||
# Update complete.
|
||||
# Initializing torch configuration in /app/test-ops-P1ZLv8/project_roster/torch...
|
||||
# Created torch/roster.json
|
||||
# Created torch/META_PROMPTS.md
|
||||
# Created torch/scheduler-flow.md
|
||||
# Created torch/daily-scheduler.md
|
||||
# Created torch/weekly-scheduler.md
|
||||
# Created 23 files in torch/prompts/daily/
|
||||
# Created 20 files in torch/prompts/weekly/
|
||||
# Initialization complete.
|
||||
# You can now customize the files in torch/ and torch/prompts/.
|
||||
# Checking locks: namespace=torch, cadence=daily, date=2026-02-14
|
||||
# Relays: wss://relay.damus.io, wss://nos.lol, wss://relay.primal.net
|
||||
# Subtest: cmdInit creates directory structure and files
|
||||
ok 2 - cmdInit creates directory structure and files
|
||||
---
|
||||
duration_ms: 31.478284
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: cmdInit fails if directory exists without force
|
||||
ok 3 - cmdInit fails if directory exists without force
|
||||
---
|
||||
duration_ms: 1.223212
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: cmdUpdate preserves modified prompt
|
||||
ok 4 - cmdUpdate preserves modified prompt
|
||||
---
|
||||
duration_ms: 29.696939
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: cmdUpdate overwrites prompt with force
|
||||
ok 5 - cmdUpdate overwrites prompt with force
|
||||
---
|
||||
duration_ms: 33.378006
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: cmdUpdate creates backup
|
||||
ok 6 - cmdUpdate creates backup
|
||||
---
|
||||
duration_ms: 35.96283
|
||||
type: 'test'
|
||||
...
|
||||
# Subtest: torch-lock check respects local roster
|
||||
ok 7 - torch-lock check respects local roster
|
||||
---
|
||||
duration_ms: 15295.37334
|
||||
type: 'test'
|
||||
...
|
||||
1..7
|
||||
# tests 9
|
||||
# suites 1
|
||||
# pass 9
|
||||
# fail 0
|
||||
# cancelled 0
|
||||
# skipped 0
|
||||
# todo 0
|
||||
# duration_ms 15904.442607
|
||||
## Verification
|
||||
- `npm run lint`: Passed (1 warning in test file).
|
||||
- `npm test`: Passed (9/9 tests).
|
||||
- `node scripts/check-file-size.mjs --update`: Updated baseline.
|
||||
- New `src/lib.mjs` size: 381 lines.
|
||||
- Reduction: ~312 lines (Target: >200).
|
||||
@@ -0,0 +1,5 @@
|
||||
# Test Log 2026-02-14T13-01-54Z
|
||||
|
||||
## content-audit-agent
|
||||
- Command: `test -d content`
|
||||
- Result: False (Directory missing)
|
||||
@@ -0,0 +1,12 @@
|
||||
# Test Log
|
||||
|
||||
## Lint
|
||||
- Ran `npm run lint`.
|
||||
- Result: Passed (with existing warnings).
|
||||
|
||||
## Manual Verification
|
||||
- Plan to run `node bin/torch-lock.mjs --help` to verify CLI entry point works.
|
||||
- Plan to run `npm test` to verify `torch-config` is still working.
|
||||
|
||||
## File Size Check
|
||||
- Baseline will be updated after verification.
|
||||
@@ -1,6 +1,11 @@
|
||||
# Todo: 2026-02-14 Constants Refactor
|
||||
# TODO Checklist
|
||||
|
||||
- [ ] Create `src/constants.mjs` with `DEFAULT_DASHBOARD_PORT` and `RACE_CHECK_DELAY_MS`.
|
||||
- [ ] Update `src/lib.mjs` to import and use new constants.
|
||||
- [ ] Verify changes with `npm run lint` and `npm test`.
|
||||
- [ ] Document decisions.
|
||||
- [ ] Update `src/constants.mjs` with defaults.
|
||||
- [ ] Update `src/torch-config.mjs` with config getters.
|
||||
- [ ] Create `src/roster.mjs` with roster logic.
|
||||
- [ ] Create `src/lock-ops.mjs` with lock operations.
|
||||
- [ ] Create `src/dashboard.mjs` with dashboard logic.
|
||||
- [ ] Update `src/lib.mjs` to import new modules.
|
||||
- [ ] Verify `npm run lint` passes.
|
||||
- [ ] Verify `npm test` passes.
|
||||
- [ ] Update baseline using `scripts/check-file-size.mjs --update`.
|
||||
@@ -0,0 +1,5 @@
|
||||
# Todo: Content Audit 2026-02-14T13-01-37Z
|
||||
|
||||
- [ ] Check if /content directory exists
|
||||
- [ ] Inventory claims if content exists
|
||||
- [ ] Verify claims if content exists
|
||||
@@ -0,0 +1,11 @@
|
||||
# Decomposition TODO
|
||||
|
||||
- [x] Create `src/errors.mjs` with `ExitError`.
|
||||
- [x] Create `src/utils.mjs` with `todayDateStr`, `nowUnix`.
|
||||
- [x] Create `src/cmd-check.mjs`.
|
||||
- [x] Create `src/cmd-lock.mjs`.
|
||||
- [x] Create `src/cmd-list.mjs`.
|
||||
- [x] Update `src/lib.mjs` to import new modules.
|
||||
- [ ] Verify with lint.
|
||||
- [ ] Verify with tests.
|
||||
- [ ] Update file size baseline.
|
||||
+70
-15
@@ -1,5 +1,11 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
DEFAULT_RELAYS,
|
||||
DEFAULT_TTL,
|
||||
DEFAULT_NAMESPACE,
|
||||
DEFAULT_QUERY_TIMEOUT_MS,
|
||||
} from './constants.mjs';
|
||||
|
||||
const DEFAULT_CONFIG_PATH = 'torch-config.json';
|
||||
|
||||
@@ -27,27 +33,14 @@ export function getTorchConfigPath() {
|
||||
return path.resolve(process.cwd(), DEFAULT_CONFIG_PATH);
|
||||
}
|
||||
|
||||
export function loadTorchConfig() {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
const configPath = getTorchConfigPath();
|
||||
let raw = {};
|
||||
|
||||
if (fs.existsSync(configPath)) {
|
||||
try {
|
||||
raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
export function parseTorchConfig(raw, configPath = null) {
|
||||
const nostrLock = raw.nostrLock || {};
|
||||
const dashboard = raw.dashboard || {};
|
||||
const scheduler = raw.scheduler || {};
|
||||
const firstPromptByCadence = scheduler.firstPromptByCadence || {};
|
||||
const paused = scheduler.paused || {};
|
||||
|
||||
cachedConfig = {
|
||||
return {
|
||||
configPath,
|
||||
raw,
|
||||
nostrLock: {
|
||||
@@ -84,6 +77,68 @@ export function loadTorchConfig() {
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function _resetTorchConfigCache() {
|
||||
cachedConfig = null;
|
||||
}
|
||||
|
||||
export function loadTorchConfig(fileSystem = fs) {
|
||||
if (cachedConfig) return cachedConfig;
|
||||
|
||||
const configPath = getTorchConfigPath();
|
||||
let raw = {};
|
||||
|
||||
if (fileSystem.existsSync(configPath)) {
|
||||
try {
|
||||
raw = JSON.parse(fileSystem.readFileSync(configPath, 'utf8'));
|
||||
} catch (err) {
|
||||
throw new Error(`Failed to parse ${configPath}: ${err.message}`, { cause: err });
|
||||
}
|
||||
}
|
||||
|
||||
cachedConfig = parseTorchConfig(raw, configPath);
|
||||
return cachedConfig;
|
||||
}
|
||||
|
||||
export function getRelays() {
|
||||
const config = loadTorchConfig();
|
||||
const envRelays = process.env.NOSTR_LOCK_RELAYS;
|
||||
if (envRelays) {
|
||||
return envRelays.split(',').map((r) => r.trim()).filter(Boolean);
|
||||
}
|
||||
if (config.nostrLock.relays?.length) {
|
||||
return config.nostrLock.relays;
|
||||
}
|
||||
return DEFAULT_RELAYS;
|
||||
}
|
||||
|
||||
export function getNamespace() {
|
||||
const config = loadTorchConfig();
|
||||
const namespace = (process.env.NOSTR_LOCK_NAMESPACE || config.nostrLock.namespace || DEFAULT_NAMESPACE).trim();
|
||||
return namespace || DEFAULT_NAMESPACE;
|
||||
}
|
||||
|
||||
export function getTtl() {
|
||||
const config = loadTorchConfig();
|
||||
const envTtl = process.env.NOSTR_LOCK_TTL;
|
||||
if (envTtl) {
|
||||
const parsed = parseInt(envTtl, 10);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
if (config.nostrLock.ttlSeconds) {
|
||||
return config.nostrLock.ttlSeconds;
|
||||
}
|
||||
return DEFAULT_TTL;
|
||||
}
|
||||
|
||||
export function getQueryTimeoutMs() {
|
||||
const config = loadTorchConfig();
|
||||
const envValue = process.env.NOSTR_LOCK_QUERY_TIMEOUT_MS;
|
||||
if (envValue) {
|
||||
const parsed = parseInt(envValue, 10);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) return parsed;
|
||||
}
|
||||
return config.nostrLock.queryTimeoutMs || DEFAULT_QUERY_TIMEOUT_MS;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
export function todayDateStr() {
|
||||
return new Date().toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function nowUnix() {
|
||||
return Math.floor(Date.now() / 1000);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Decompose Agent - Completed
|
||||
|
||||
**Agent:** decompose-agent
|
||||
**Status:** Completed
|
||||
**Date:** 2026-02-14
|
||||
|
||||
## Summary
|
||||
Decomposed `src/lib.mjs` (approx 700 lines) into:
|
||||
- `src/roster.mjs`: Roster logic.
|
||||
- `src/lock-ops.mjs`: Lock operations.
|
||||
- `src/dashboard.mjs`: Dashboard server.
|
||||
- `src/torch-config.mjs`: Configuration getters.
|
||||
- `src/constants.mjs`: Defaults and constants.
|
||||
|
||||
## Artifacts
|
||||
- `src/context/CONTEXT_2026-02-14.md`
|
||||
- `src/todo/TODO_2026-02-14.md`
|
||||
- `src/decisions/DECISIONS_2026-02-14.md`
|
||||
- `src/test_logs/TEST_LOG_2026-02-14.md`
|
||||
|
||||
## Verification
|
||||
- `npm run lint`: Passed.
|
||||
- `npm test`: Passed.
|
||||
- Baseline updated: `src/lib.mjs` reduced to 381 lines.
|
||||
@@ -0,0 +1 @@
|
||||
Task completed. No changes made as /content directory was missing.
|
||||
@@ -0,0 +1,6 @@
|
||||
# decompose-agent completed
|
||||
|
||||
Decomposed `src/lib.mjs` into 5 new modules.
|
||||
- `src/lib.mjs` reduced by > 200 lines.
|
||||
- Lint and tests passed.
|
||||
- Baseline not updated (script does not support --update).
|
||||
@@ -0,0 +1 @@
|
||||
Preflight check failed: Required files `src/lib/eventSchemas.js`, `src/lib/clientFacade.js` (or defaultClient.js), and `js/dmDecryptor.js` are missing from the repository. This agent appears to be misconfigured for the current repository structure.
|
||||
@@ -0,0 +1,20 @@
|
||||
---
|
||||
agent: perf-deepdive-agent
|
||||
status: completed
|
||||
date: 2026-02-14
|
||||
---
|
||||
|
||||
# Weekly Performance Deep Dive
|
||||
|
||||
## Summary
|
||||
- **Scenario**: Listing active locks (`torch-lock list`), which involves querying relays for both daily and weekly cadences.
|
||||
- **Optimization**: Parallelized the relay queries in `src/lib.mjs`.
|
||||
- **Baseline Duration**: ~15.8s
|
||||
- **Post-Optimization Duration**: ~15.2s
|
||||
- **Improvement**: ~4%
|
||||
|
||||
## Analysis
|
||||
The performance is dominated by a 15s timeout on one of the queries. Parallelization ensures that the total duration is determined by the slowest query, rather than the sum of sequential query times (which would be ~30s if both timed out).
|
||||
|
||||
## Artifacts
|
||||
- `weekly-perf-report-2026-02-14.md`: Detailed report with benchmark data.
|
||||
@@ -0,0 +1,22 @@
|
||||
# Task Log: perf-optimization-agent
|
||||
|
||||
**Status:** Completed
|
||||
**Date:** 2026-02-14
|
||||
**Agent:** perf-optimization-agent
|
||||
|
||||
## Diagnosis
|
||||
Identified a 15-second execution floor in `torch-lock` operations caused by a `Promise.race` timeout that was not cleared upon successful completion. This kept the Node.js event loop active until the 15s timer fired.
|
||||
|
||||
## Action Taken
|
||||
Modified `src/lock-ops.mjs` to capture the timeout handle and explicitly clear it using `clearTimeout` in the `finally` block of `queryLocks`.
|
||||
|
||||
## Verification
|
||||
- **Baseline:** ~15.2s average execution time.
|
||||
- **After:** ~2.6s average execution time (min ~0.9s).
|
||||
- **Tests:** `npm test` passed successfully.
|
||||
- **Lint:** `npm run lint` passed.
|
||||
|
||||
## Artifacts
|
||||
- `DIAGNOSIS.md`
|
||||
- `BASELINE.md`
|
||||
- `AFTER.md`
|
||||
@@ -0,0 +1,121 @@
|
||||
import { test, describe, it } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { parseLockEvent } from '../src/lib.mjs';
|
||||
|
||||
describe('parseLockEvent', () => {
|
||||
it('should parse a valid lock event correctly', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400, // 2023-03-15T13:20:00.000Z
|
||||
tags: [
|
||||
['d', 'mock-d-tag'],
|
||||
['expiration', '1678890000'], // 2023-03-15T14:20:00.000Z
|
||||
],
|
||||
content: JSON.stringify({
|
||||
agent: 'test-agent',
|
||||
cadence: 'daily',
|
||||
status: 'started',
|
||||
namespace: 'test-ns',
|
||||
date: '2023-03-15',
|
||||
platform: 'test-platform',
|
||||
}),
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
|
||||
assert.strictEqual(result.eventId, 'mock-event-id');
|
||||
assert.strictEqual(result.pubkey, 'mock-pubkey');
|
||||
assert.strictEqual(result.createdAt, 1678886400);
|
||||
assert.strictEqual(result.createdAtIso, '2023-03-15T13:20:00.000Z');
|
||||
assert.strictEqual(result.expiresAt, 1678890000);
|
||||
assert.strictEqual(result.expiresAtIso, '2023-03-15T14:20:00.000Z');
|
||||
assert.strictEqual(result.dTag, 'mock-d-tag');
|
||||
assert.strictEqual(result.agent, 'test-agent');
|
||||
assert.strictEqual(result.cadence, 'daily');
|
||||
assert.strictEqual(result.status, 'started');
|
||||
assert.strictEqual(result.date, '2023-03-15');
|
||||
assert.strictEqual(result.platform, 'test-platform');
|
||||
});
|
||||
|
||||
it('should handle missing d tag', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [],
|
||||
content: '{}',
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.dTag, '');
|
||||
});
|
||||
|
||||
it('should handle missing expiration tag', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [['d', 'mock-d-tag']],
|
||||
content: '{}',
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.expiresAt, null);
|
||||
assert.strictEqual(result.expiresAtIso, null);
|
||||
});
|
||||
|
||||
it('should handle malformed JSON content', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [],
|
||||
content: '{ invalid json }',
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.agent, null);
|
||||
assert.strictEqual(result.cadence, null);
|
||||
});
|
||||
|
||||
it('should handle non-object JSON content (string)', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [],
|
||||
content: '"just a string"',
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.agent, null);
|
||||
});
|
||||
|
||||
it('should handle non-object JSON content (array)', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [],
|
||||
content: '["item1", "item2"]',
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.agent, null);
|
||||
});
|
||||
|
||||
it('should handle partial JSON content', () => {
|
||||
const event = {
|
||||
id: 'mock-event-id',
|
||||
pubkey: 'mock-pubkey',
|
||||
created_at: 1678886400,
|
||||
tags: [],
|
||||
content: JSON.stringify({ agent: 'partial-agent' }),
|
||||
};
|
||||
|
||||
const result = parseLockEvent(event);
|
||||
assert.strictEqual(result.agent, 'partial-agent');
|
||||
assert.strictEqual(result.cadence, null);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, it, beforeEach, mock } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { loadTorchConfig, _resetTorchConfigCache } from '../src/torch-config.mjs';
|
||||
|
||||
describe('loadTorchConfig (Unit Tests with Mocked FS)', () => {
|
||||
beforeEach(() => {
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('loads valid config correctly', () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({
|
||||
nostrLock: { namespace: 'unit-test-ns' }
|
||||
}))
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
|
||||
assert.strictEqual(config.nostrLock.namespace, 'unit-test-ns');
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 1);
|
||||
});
|
||||
|
||||
it('returns default config when file is missing', () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => false),
|
||||
readFileSync: mock.fn()
|
||||
};
|
||||
|
||||
const config = loadTorchConfig(mockFs);
|
||||
|
||||
assert.strictEqual(config.nostrLock.namespace, null);
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs.readFileSync.mock.calls.length, 0);
|
||||
});
|
||||
|
||||
it('throws error on malformed JSON', () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => '{ invalid json }')
|
||||
};
|
||||
|
||||
assert.throws(() => {
|
||||
loadTorchConfig(mockFs);
|
||||
}, /Failed to parse/);
|
||||
});
|
||||
|
||||
it('caches the config and ignores subsequent fs calls', () => {
|
||||
const mockFs = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({ nostrLock: { namespace: 'cached' } }))
|
||||
};
|
||||
|
||||
const config1 = loadTorchConfig(mockFs);
|
||||
assert.strictEqual(config1.nostrLock.namespace, 'cached');
|
||||
|
||||
// Second call with a different "fs" (or same) should not trigger fs operations
|
||||
const mockFs2 = {
|
||||
existsSync: mock.fn(() => true),
|
||||
readFileSync: mock.fn(() => JSON.stringify({ nostrLock: { namespace: 'changed' } }))
|
||||
};
|
||||
|
||||
const config2 = loadTorchConfig(mockFs2);
|
||||
|
||||
assert.strictEqual(config2.nostrLock.namespace, 'cached');
|
||||
assert.strictEqual(config1, config2);
|
||||
assert.strictEqual(mockFs.existsSync.mock.calls.length, 1);
|
||||
assert.strictEqual(mockFs2.existsSync.mock.calls.length, 0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,214 @@
|
||||
import { test, describe, it, after, beforeEach } from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { getTorchConfigPath, parseTorchConfig, loadTorchConfig, _resetTorchConfigCache, getRelays } from '../src/torch-config.mjs';
|
||||
import { DEFAULT_RELAYS } from '../src/constants.mjs';
|
||||
|
||||
describe('torch-config', () => {
|
||||
describe('getTorchConfigPath', () => {
|
||||
const originalEnv = process.env.TORCH_CONFIG_PATH;
|
||||
|
||||
after(() => {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.TORCH_CONFIG_PATH;
|
||||
} else {
|
||||
process.env.TORCH_CONFIG_PATH = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
it('returns default path when env var is not set', () => {
|
||||
delete process.env.TORCH_CONFIG_PATH;
|
||||
const expected = path.resolve(process.cwd(), 'torch-config.json');
|
||||
assert.strictEqual(getTorchConfigPath(), expected);
|
||||
});
|
||||
|
||||
it('returns custom path when env var is set', () => {
|
||||
process.env.TORCH_CONFIG_PATH = 'custom-config.json';
|
||||
const expected = path.resolve(process.cwd(), 'custom-config.json');
|
||||
assert.strictEqual(getTorchConfigPath(), expected);
|
||||
});
|
||||
|
||||
it('trims env var value', () => {
|
||||
process.env.TORCH_CONFIG_PATH = ' trimmed-config.json ';
|
||||
const expected = path.resolve(process.cwd(), 'trimmed-config.json');
|
||||
assert.strictEqual(getTorchConfigPath(), expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('parseTorchConfig', () => {
|
||||
it('returns default values for empty raw config', () => {
|
||||
const config = parseTorchConfig({}, 'some-path.json');
|
||||
assert.strictEqual(config.configPath, 'some-path.json');
|
||||
assert.deepStrictEqual(config.raw, {});
|
||||
assert.strictEqual(config.nostrLock.namespace, null);
|
||||
assert.strictEqual(config.dashboard.defaultCadenceView, 'daily');
|
||||
assert.strictEqual(config.dashboard.defaultStatusView, 'active');
|
||||
assert.deepStrictEqual(config.scheduler.paused.daily, []);
|
||||
});
|
||||
|
||||
it('correctly parses and normalizes nostrLock config', () => {
|
||||
const raw = {
|
||||
nostrLock: {
|
||||
namespace: ' test ',
|
||||
relays: [' wss://relay1 ', '', 'wss://relay2'],
|
||||
ttlSeconds: 3600.5,
|
||||
queryTimeoutMs: 5000,
|
||||
dailyRoster: ['agent1', 'agent2'],
|
||||
}
|
||||
};
|
||||
const config = parseTorchConfig(raw);
|
||||
assert.strictEqual(config.nostrLock.namespace, 'test');
|
||||
assert.deepStrictEqual(config.nostrLock.relays, ['wss://relay1', 'wss://relay2']);
|
||||
assert.strictEqual(config.nostrLock.ttlSeconds, 3600);
|
||||
assert.strictEqual(config.nostrLock.queryTimeoutMs, 5000);
|
||||
assert.deepStrictEqual(config.nostrLock.dailyRoster, ['agent1', 'agent2']);
|
||||
});
|
||||
|
||||
it('normalizes cadence and status in dashboard config', () => {
|
||||
const raw = {
|
||||
dashboard: {
|
||||
defaultCadenceView: ' WEEKLY ',
|
||||
defaultStatusView: ' ALL '
|
||||
}
|
||||
};
|
||||
const config = parseTorchConfig(raw);
|
||||
assert.strictEqual(config.dashboard.defaultCadenceView, 'weekly');
|
||||
assert.strictEqual(config.dashboard.defaultStatusView, 'all');
|
||||
});
|
||||
|
||||
it('falls back to defaults for invalid cadence/status', () => {
|
||||
const raw = {
|
||||
dashboard: {
|
||||
defaultCadenceView: 'invalid',
|
||||
defaultStatusView: 'invalid'
|
||||
}
|
||||
};
|
||||
const config = parseTorchConfig(raw);
|
||||
assert.strictEqual(config.dashboard.defaultCadenceView, 'daily');
|
||||
assert.strictEqual(config.dashboard.defaultStatusView, 'active');
|
||||
});
|
||||
|
||||
it('correctly parses scheduler config', () => {
|
||||
const raw = {
|
||||
scheduler: {
|
||||
firstPromptByCadence: {
|
||||
daily: ' daily-prompt.md ',
|
||||
weekly: ' weekly-prompt.md '
|
||||
},
|
||||
paused: {
|
||||
daily: [' paused1 '],
|
||||
weekly: [' paused2 ']
|
||||
}
|
||||
}
|
||||
};
|
||||
const config = parseTorchConfig(raw);
|
||||
assert.strictEqual(config.scheduler.firstPromptByCadence.daily, 'daily-prompt.md');
|
||||
assert.strictEqual(config.scheduler.firstPromptByCadence.weekly, 'weekly-prompt.md');
|
||||
assert.deepStrictEqual(config.scheduler.paused.daily, ['paused1']);
|
||||
assert.deepStrictEqual(config.scheduler.paused.weekly, ['paused2']);
|
||||
});
|
||||
|
||||
it('handles non-array roster values by returning null or empty array', () => {
|
||||
const raw = {
|
||||
nostrLock: { dailyRoster: 'not-an-array' },
|
||||
scheduler: { paused: { daily: 'not-an-array' } }
|
||||
};
|
||||
const config = parseTorchConfig(raw);
|
||||
assert.strictEqual(config.nostrLock.dailyRoster, null);
|
||||
assert.deepStrictEqual(config.scheduler.paused.daily, []);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadTorchConfig', () => {
|
||||
const tempConfigPath = path.resolve(process.cwd(), 'temp-torch-config.json');
|
||||
const badConfigPath = path.resolve(process.cwd(), 'bad-torch-config.json');
|
||||
const originalEnv = process.env.TORCH_CONFIG_PATH;
|
||||
|
||||
beforeEach(() => {
|
||||
_resetTorchConfigCache();
|
||||
delete process.env.TORCH_CONFIG_PATH;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (fs.existsSync(tempConfigPath)) fs.unlinkSync(tempConfigPath);
|
||||
if (fs.existsSync(badConfigPath)) fs.unlinkSync(badConfigPath);
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.TORCH_CONFIG_PATH;
|
||||
} else {
|
||||
process.env.TORCH_CONFIG_PATH = originalEnv;
|
||||
}
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('loads config from disk', () => {
|
||||
const raw = { nostrLock: { namespace: 'from-disk' } };
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(raw));
|
||||
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||
|
||||
const config = loadTorchConfig();
|
||||
assert.strictEqual(config.nostrLock.namespace, 'from-disk');
|
||||
assert.strictEqual(config.configPath, tempConfigPath);
|
||||
});
|
||||
|
||||
it('returns defaults if file does not exist', () => {
|
||||
process.env.TORCH_CONFIG_PATH = 'non-existent.json';
|
||||
const config = loadTorchConfig();
|
||||
assert.strictEqual(config.nostrLock.namespace, null);
|
||||
});
|
||||
|
||||
it('throws error for malformed JSON', () => {
|
||||
fs.writeFileSync(badConfigPath, '{ invalid json }');
|
||||
process.env.TORCH_CONFIG_PATH = badConfigPath;
|
||||
|
||||
assert.throws(() => {
|
||||
loadTorchConfig();
|
||||
}, /Failed to parse/);
|
||||
});
|
||||
|
||||
it('caches the config', () => {
|
||||
const raw1 = { nostrLock: { namespace: 'first' } };
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(raw1));
|
||||
process.env.TORCH_CONFIG_PATH = tempConfigPath;
|
||||
|
||||
const config1 = loadTorchConfig();
|
||||
assert.strictEqual(config1.nostrLock.namespace, 'first');
|
||||
|
||||
const raw2 = { nostrLock: { namespace: 'second' } };
|
||||
fs.writeFileSync(tempConfigPath, JSON.stringify(raw2));
|
||||
|
||||
const config2 = loadTorchConfig();
|
||||
assert.strictEqual(config2.nostrLock.namespace, 'first', 'Should return cached value');
|
||||
assert.strictEqual(config1, config2, 'Should be the same object');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRelays', () => {
|
||||
const originalEnv = process.env.NOSTR_LOCK_RELAYS;
|
||||
|
||||
beforeEach(() => {
|
||||
_resetTorchConfigCache();
|
||||
delete process.env.NOSTR_LOCK_RELAYS;
|
||||
});
|
||||
|
||||
after(() => {
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.NOSTR_LOCK_RELAYS;
|
||||
} else {
|
||||
process.env.NOSTR_LOCK_RELAYS = originalEnv;
|
||||
}
|
||||
_resetTorchConfigCache();
|
||||
});
|
||||
|
||||
it('returns default relays when no config or env var is set', () => {
|
||||
const relays = getRelays();
|
||||
assert.deepStrictEqual(relays, DEFAULT_RELAYS);
|
||||
});
|
||||
|
||||
it('returns relays from env var if set', () => {
|
||||
process.env.NOSTR_LOCK_RELAYS = 'wss://env.relay,wss://env.relay2';
|
||||
const relays = getRelays();
|
||||
assert.deepStrictEqual(relays, ['wss://env.relay', 'wss://env.relay2']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
# Weekly Performance Report - 2026-02-14
|
||||
|
||||
## Scenario
|
||||
Running `node bin/torch-lock.mjs list` to list active locks for both daily and weekly cadences.
|
||||
This command executes two sequential queries to Nostr relays.
|
||||
|
||||
## Baseline (Before Optimization)
|
||||
Measured using `scripts/perf-benchmark.mjs` (5 iterations).
|
||||
|
||||
- **Average Duration**: 15826.65ms
|
||||
- **Min Duration**: 15770.65ms
|
||||
- **Max Duration**: 16013.24ms
|
||||
|
||||
The command appears to be network-bound, likely due to sequential execution of relay queries for each cadence.
|
||||
|
||||
## After Optimization
|
||||
Modified `src/lib.mjs` to execute relay queries in parallel using `Promise.all`.
|
||||
|
||||
- **Average Duration**: 15190.12ms
|
||||
- **Min Duration**: 15184.86ms
|
||||
- **Max Duration**: 15199.04ms
|
||||
|
||||
## Analysis
|
||||
The speedup is approximately 4% (636ms).
|
||||
While theoretically this should parallelize the latency, the observed improvement is small.
|
||||
This suggests that either:
|
||||
1. The relays are responding very quickly but one query dominates the time (e.g. hitting a 15s timeout).
|
||||
2. The `SimplePool` instantiation overhead is significant.
|
||||
3. Network bandwidth or local resource contention limits the concurrency gains.
|
||||
|
||||
Given the timeout in `src/lib.mjs` is 15000ms, and the duration is just over 15000ms, it is highly likely that at least one query is timing out.
|
||||
If both queries were timing out sequentially, the duration would be ~30s.
|
||||
Since sequential was ~16s, it implies one query timed out (15s) and the other was very fast (<1s), OR they overlapped in some way (unlikely in sequential).
|
||||
|
||||
If one query takes 15s (timeout) and the other takes 0.5s:
|
||||
- Sequential: 15s + 0.5s = 15.5s.
|
||||
- Parallel: max(15s, 0.5s) = 15s.
|
||||
This matches the data perfectly.
|
||||
|
||||
Conclusion: One cadence query (likely 'daily' or 'weekly' specifically) is timing out or very slow, while the other is fast. Parallelization ensures we pay the "slowest" cost, not the sum.
|
||||
Reference in new issue
Block a user