mirror of
https://github.com/PR0M3TH3AN/SeedPass.git
synced 2026-11-06 04:42:35 +00:00
Setup TORCH agent system
- Created `torch/` directory and initialized package.json - Installed TORCH dependency from https://github.com/PR0M3TH3AN/TORCH - Initialized TORCH environment with `npx torch-lock init` - Updated .gitignore to exclude node_modules and generated files Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
1 parent
a31db2bcd6
commit
77be82c09a
255 files changed
+39918
No files matched your search
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env node
|
||||
import { runRelayHealthCheck } from '../../src/relay-health.mjs';
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
cadence: 'daily',
|
||||
timeoutMs: 6000,
|
||||
allRelaysDownMinutes: 10,
|
||||
minSuccessRate: 0.7,
|
||||
windowMinutes: 60,
|
||||
};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const value = argv[i];
|
||||
if (!value.startsWith('--') && !args.cadence) {
|
||||
args.cadence = value;
|
||||
continue;
|
||||
}
|
||||
if (value === '--cadence') {
|
||||
args.cadence = argv[i + 1] || args.cadence;
|
||||
i += 1;
|
||||
} else if (value === '--timeout-ms') {
|
||||
args.timeoutMs = Number.parseInt(argv[i + 1], 10) || args.timeoutMs;
|
||||
i += 1;
|
||||
} else if (value === '--all-relays-down-minutes') {
|
||||
args.allRelaysDownMinutes = Number.parseInt(argv[i + 1], 10) || args.allRelaysDownMinutes;
|
||||
i += 1;
|
||||
} else if (value === '--min-success-rate') {
|
||||
const parsed = Number.parseFloat(argv[i + 1]);
|
||||
args.minSuccessRate = Number.isFinite(parsed) ? parsed : args.minSuccessRate;
|
||||
i += 1;
|
||||
} else if (value === '--window-minutes') {
|
||||
args.windowMinutes = Number.parseInt(argv[i + 1], 10) || args.windowMinutes;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return args;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
if (args.cadence !== 'daily' && args.cadence !== 'weekly') {
|
||||
console.error('Usage: node scripts/agent/check-relay-health.mjs --cadence <daily|weekly>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const result = await runRelayHealthCheck(args);
|
||||
if (!result.ok) {
|
||||
result.failureCategory = 'all relays unhealthy';
|
||||
}
|
||||
console.log(JSON.stringify(result));
|
||||
process.exit(result.ok ? 0 : 2);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error ?? 'unknown error');
|
||||
console.log(JSON.stringify({ ok: false, failureCategory: 'health check failed', error: message }));
|
||||
process.exit(2);
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { chromium } from '@playwright/test';
|
||||
import fs from 'fs';
|
||||
|
||||
const LOG_FILE = 'artifacts/debug_frontend.log';
|
||||
|
||||
(async () => {
|
||||
const browser = await chromium.launch();
|
||||
const page = await browser.newPage();
|
||||
const errors = [];
|
||||
|
||||
const logStream = fs.createWriteStream(LOG_FILE, { flags: 'w' });
|
||||
const log = (msg) => {
|
||||
console.log(msg);
|
||||
logStream.write(msg + '\n');
|
||||
};
|
||||
|
||||
log(`[${new Date().toISOString()}] Starting debug session...`);
|
||||
|
||||
page.on('console', msg => {
|
||||
const text = msg.text();
|
||||
const type = msg.type();
|
||||
const output = `[console:${type}] ${text}`;
|
||||
|
||||
if (type === 'error') {
|
||||
log(output);
|
||||
errors.push(output);
|
||||
} else {
|
||||
// Log non-errors only to file to reduce noise, or log everything?
|
||||
// For debugging, everything is useful.
|
||||
log(output);
|
||||
}
|
||||
});
|
||||
|
||||
page.on('pageerror', err => {
|
||||
const output = `[pageerror] ${err.message}\n${err.stack}`;
|
||||
log(output);
|
||||
errors.push(output);
|
||||
});
|
||||
|
||||
page.on('requestfailed', request => {
|
||||
const failure = request.failure();
|
||||
const errorText = failure ? failure.errorText : 'Unknown error';
|
||||
const output = `[requestfailed] ${request.url()} - ${errorText}`;
|
||||
log(output);
|
||||
errors.push(output);
|
||||
});
|
||||
|
||||
try {
|
||||
log(`Navigating to http://localhost:4173/dashboard/...`);
|
||||
await page.goto('http://localhost:4173/dashboard/', { waitUntil: 'networkidle', timeout: 10000 });
|
||||
log(`Navigation complete.`);
|
||||
// Give it a bit more time for any delayed errors
|
||||
await page.waitForTimeout(2000);
|
||||
} catch (e) {
|
||||
log(`[script:error] Navigation failed or timeout: ${e.message}`);
|
||||
errors.push(`Navigation failed: ${e.message}`);
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
// Don't close stream immediately as logs might still be flushing? No, synchronous write.
|
||||
await new Promise(resolve => logStream.end(resolve));
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error(`\nFound ${errors.length} errors.`);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log(`\nNo errors found.`);
|
||||
process.exit(0);
|
||||
}
|
||||
})();
|
||||
@@ -0,0 +1,140 @@
|
||||
import { parseLockEvent } from '../../src/lib.mjs';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// Seeded random number generator (simple LCG)
|
||||
function mulberry32(a) {
|
||||
return function () {
|
||||
let t = (a += 0x6d2b79f5);
|
||||
t = Math.imul(t ^ (t >>> 15), t | 1);
|
||||
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
||||
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const SEED = process.env.SEED ? parseInt(process.env.SEED, 10) : Date.now();
|
||||
const ITERATIONS = 10000;
|
||||
const rand = mulberry32(SEED);
|
||||
|
||||
console.log(`Starting fuzz run. SEED=${SEED}, ITERATIONS=${ITERATIONS}`);
|
||||
|
||||
const REPRO_DIR = 'examples/reproducers/fuzz-lock-event-' + new Date().toISOString().slice(0, 10).replace(/-/g, '');
|
||||
if (!fs.existsSync(REPRO_DIR)) {
|
||||
fs.mkdirSync(REPRO_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
let failures = 0;
|
||||
|
||||
function randomString(length) {
|
||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(rand() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function randomJson() {
|
||||
const types = ['object', 'array', 'string', 'number', 'boolean', 'null'];
|
||||
const type = types[Math.floor(rand() * types.length)];
|
||||
|
||||
if (type === 'object') {
|
||||
const obj = {};
|
||||
const keys = Math.floor(rand() * 5);
|
||||
for (let i = 0; i < keys; i++) {
|
||||
obj[randomString(5)] = randomJson();
|
||||
}
|
||||
return obj;
|
||||
} else if (type === 'array') {
|
||||
const arr = [];
|
||||
const len = Math.floor(rand() * 5);
|
||||
for (let i = 0; i < len; i++) {
|
||||
arr.push(randomJson());
|
||||
}
|
||||
return arr;
|
||||
} else if (type === 'string') {
|
||||
return randomString(10);
|
||||
} else if (type === 'number') {
|
||||
return rand() * 1000;
|
||||
} else if (type === 'boolean') {
|
||||
return rand() > 0.5;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function generateEvent() {
|
||||
const malformed = rand() > 0.8;
|
||||
const content = malformed ? randomString(100) : JSON.stringify(randomJson());
|
||||
|
||||
const tags = [];
|
||||
const numTags = Math.floor(rand() * 5);
|
||||
for (let i = 0; i < numTags; i++) {
|
||||
const tagLen = Math.floor(rand() * 3) + 1;
|
||||
const tag = [];
|
||||
for (let j = 0; j < tagLen; j++) {
|
||||
tag.push(randomString(10));
|
||||
}
|
||||
tags.push(tag);
|
||||
}
|
||||
|
||||
// Sometimes add specific tags
|
||||
if (rand() > 0.5) {
|
||||
tags.push(['d', randomString(20)]);
|
||||
}
|
||||
if (rand() > 0.5) {
|
||||
tags.push(['expiration', String(Math.floor(Date.now() / 1000) + 1000)]);
|
||||
}
|
||||
|
||||
return {
|
||||
id: randomString(64), // Mock ID
|
||||
pubkey: randomString(64), // Mock Pubkey
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: tags,
|
||||
content: content
|
||||
};
|
||||
}
|
||||
|
||||
for (let i = 0; i < ITERATIONS; i++) {
|
||||
const event = generateEvent();
|
||||
|
||||
try {
|
||||
parseLockEvent(event);
|
||||
} catch (err) {
|
||||
failures++;
|
||||
console.error(`Failure #${failures} at iteration ${i}: ${err.message}`);
|
||||
|
||||
const reproFile = path.join(REPRO_DIR, `repro-${i}.json`);
|
||||
fs.writeFileSync(reproFile, JSON.stringify(event, null, 2));
|
||||
|
||||
const reproScript = path.join(REPRO_DIR, `run-repro-${i}.mjs`);
|
||||
const scriptContent = `
|
||||
import { parseLockEvent } from '../../../src/lib.mjs';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const event = JSON.parse(fs.readFileSync('repro-${i}.json', 'utf8'));
|
||||
console.log('Running repro case...');
|
||||
try {
|
||||
parseLockEvent(event);
|
||||
console.log('Success (no crash)');
|
||||
} catch (err) {
|
||||
console.error('Crash reproduced:', err);
|
||||
process.exit(1);
|
||||
}
|
||||
`;
|
||||
fs.writeFileSync(reproScript, scriptContent);
|
||||
}
|
||||
}
|
||||
|
||||
if (failures === 0) {
|
||||
console.log('Fuzzing completed successfully. No failures found.');
|
||||
// Clean up empty directory if no failures
|
||||
try {
|
||||
fs.rmdirSync(REPRO_DIR);
|
||||
} catch {
|
||||
// Ignore error if directory is not empty or missing
|
||||
}
|
||||
} else {
|
||||
console.log(`Fuzzing completed with ${failures} failures.`);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Relay } from 'nostr-tools/relay';
|
||||
import { generateSecretKey, finalizeEvent } from 'nostr-tools/pure';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
// --- Configuration ---
|
||||
|
||||
const RELAY_URL = process.env.RELAY_URL;
|
||||
const CLIENTS = parseInt(process.env.CLIENTS || '1000', 10);
|
||||
const DURATION_SEC = parseInt(process.env.DURATION_SEC || '600', 10);
|
||||
const RATE_EPS = parseInt(process.env.RATE_EPS || '10', 10);
|
||||
const MIX = parseFloat(process.env.MIX || '0.9'); // 90% view events, 10% metadata
|
||||
const DRY_RUN = process.env.DRY_RUN === '1';
|
||||
const REPORT_DIR = 'artifacts';
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
// const nowUnix = () => Math.floor(Date.now() / 1000);
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
function ensureDir(dir) {
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
// --- Logic ---
|
||||
|
||||
async function runLoadTest() {
|
||||
console.log(`Starting Load Test Agent...`);
|
||||
console.log(`Config: CLIENTS=${CLIENTS}, DURATION=${DURATION_SEC}s, RATE=${RATE_EPS}eps, MIX=${MIX}, DRY_RUN=${DRY_RUN}`);
|
||||
|
||||
if (!RELAY_URL && !DRY_RUN) {
|
||||
console.error('ERROR: RELAY_URL environment variable is required (unless DRY_RUN=1).');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const endTime = startTime + (DURATION_SEC * 1000);
|
||||
|
||||
let totalEventsAttempted = 0;
|
||||
let totalEventsSuccess = 0;
|
||||
let totalEventsFailed = 0;
|
||||
let latencies = [];
|
||||
let errors = {};
|
||||
|
||||
if (DRY_RUN) {
|
||||
console.log('DRY RUN MODE: Simulating load without network calls.');
|
||||
|
||||
while (Date.now() < endTime) {
|
||||
// Simulate rate
|
||||
const batchSize = Math.ceil(RATE_EPS / 10); // Check 10 times per second
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
totalEventsAttempted++;
|
||||
// Simulate random success/failure and latency
|
||||
const isSuccess = Math.random() > 0.01; // 1% failure
|
||||
if (isSuccess) {
|
||||
totalEventsSuccess++;
|
||||
latencies.push(Math.floor(Math.random() * 50) + 10); // 10-60ms
|
||||
} else {
|
||||
totalEventsFailed++;
|
||||
const errType = Math.random() > 0.5 ? 'Timeout' : 'ConnectionRefused';
|
||||
errors[errType] = (errors[errType] || 0) + 1;
|
||||
}
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`Connecting to ${RELAY_URL} with ${CLIENTS} simulated clients (implementation pending, using simplified single-connection for initial harness)...`);
|
||||
|
||||
// For this initial version, we will use a single connection to validate the harness structure
|
||||
// as properly simulating 1000 clients requires more complex setup (workers/etc) which is out of scope for "Minimal footprint".
|
||||
// We will simulate the *rate* but maybe not full connection concurrency in this first pass if strictly constrained.
|
||||
// However, the prompt asks for "Create N clients".
|
||||
// Let's try to create a few clients to prove we can.
|
||||
|
||||
const activeRelays = [];
|
||||
let relay;
|
||||
try {
|
||||
relay = await Relay.connect(RELAY_URL);
|
||||
activeRelays.push(relay);
|
||||
console.log(`Connected to ${RELAY_URL}`);
|
||||
} catch (err) {
|
||||
console.error(`Failed to connect to ${RELAY_URL}:`, err);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Generate ephemeral key for signing
|
||||
const sk = generateSecretKey();
|
||||
// const pk = getPublicKey(sk); // Unused for now
|
||||
|
||||
// Load Loop
|
||||
while (Date.now() < endTime) {
|
||||
const batchStart = Date.now();
|
||||
const batchSize = Math.ceil(RATE_EPS / 10);
|
||||
|
||||
const promises = [];
|
||||
for (let i=0; i<batchSize; i++) {
|
||||
totalEventsAttempted++;
|
||||
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [],
|
||||
content: `Load test event ${totalEventsAttempted}`,
|
||||
};
|
||||
|
||||
const event = finalizeEvent(eventTemplate, sk);
|
||||
|
||||
promises.push((async () => {
|
||||
const start = Date.now();
|
||||
try {
|
||||
await relay.publish(event);
|
||||
const latency = Date.now() - start;
|
||||
totalEventsSuccess++;
|
||||
latencies.push(latency);
|
||||
} catch (err) {
|
||||
totalEventsFailed++;
|
||||
const msg = err.message || String(err);
|
||||
errors[msg] = (errors[msg] || 0) + 1;
|
||||
}
|
||||
})());
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const elapsed = Date.now() - batchStart;
|
||||
if (elapsed < 100) {
|
||||
await sleep(100 - elapsed);
|
||||
}
|
||||
}
|
||||
|
||||
activeRelays.forEach(r => r.close());
|
||||
}
|
||||
|
||||
// Report
|
||||
latencies.sort((a, b) => a - b);
|
||||
const p50 = latencies[Math.floor(latencies.length * 0.5)] || 0;
|
||||
const p90 = latencies[Math.floor(latencies.length * 0.9)] || 0;
|
||||
const p95 = latencies[Math.floor(latencies.length * 0.95)] || 0;
|
||||
const p99 = latencies[Math.floor(latencies.length * 0.99)] || 0;
|
||||
|
||||
const report = {
|
||||
timestamp: new Date().toISOString(),
|
||||
config: {
|
||||
relayUrl: DRY_RUN ? 'N/A (Dry Run)' : RELAY_URL,
|
||||
clients: CLIENTS,
|
||||
durationSec: DURATION_SEC,
|
||||
rateEps: RATE_EPS,
|
||||
mix: MIX
|
||||
},
|
||||
summary: {
|
||||
totalEventsAttempted,
|
||||
totalEventsSuccess,
|
||||
totalEventsFailed,
|
||||
throughputEps: totalEventsSuccess / DURATION_SEC,
|
||||
latencies: { p50, p90, p95, p99 }
|
||||
},
|
||||
errors
|
||||
};
|
||||
|
||||
ensureDir(REPORT_DIR);
|
||||
const reportPath = path.join(REPORT_DIR, `load-report-${new Date().toISOString().split('T')[0]}.json`);
|
||||
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2));
|
||||
console.log(`Report generated at ${reportPath}`);
|
||||
}
|
||||
|
||||
runLoadTest().catch(err => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
|
||||
const REQUIRED_ENV = ['SCHEDULER_PROMPT_PATH', 'SCHEDULER_AGENT', 'SCHEDULER_CADENCE'];
|
||||
|
||||
function fail(message, code = 1) {
|
||||
console.error(`[scheduler] ${message}`);
|
||||
process.exit(code);
|
||||
}
|
||||
|
||||
function resolveRunner(promptMarkdown) {
|
||||
const customRunner = process.env.SCHEDULER_AGENT_RUNNER_COMMAND?.trim();
|
||||
if (customRunner) {
|
||||
return { command: 'bash', args: ['-lc', customRunner] };
|
||||
}
|
||||
|
||||
const platform = (process.env.AGENT_PLATFORM || 'codex').trim().toLowerCase();
|
||||
if (platform === 'codex') {
|
||||
return { command: 'codex', args: ['exec', promptMarkdown] };
|
||||
}
|
||||
|
||||
if (platform === 'claude') {
|
||||
return { command: 'claude', args: ['-p', promptMarkdown] };
|
||||
}
|
||||
|
||||
if (platform === 'linux') {
|
||||
return { command: 'echo', args: [`[scheduler] Platform 'linux' detected. simulated execution for prompt.`] };
|
||||
}
|
||||
|
||||
fail(`Unsupported AGENT_PLATFORM="${platform}". Set SCHEDULER_AGENT_RUNNER_COMMAND to override.`);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
for (const key of REQUIRED_ENV) {
|
||||
if (!process.env[key] || !String(process.env[key]).trim()) {
|
||||
fail(`Missing required environment variable ${key}.`);
|
||||
}
|
||||
}
|
||||
|
||||
const promptPath = path.resolve(process.cwd(), process.env.SCHEDULER_PROMPT_PATH);
|
||||
let promptMarkdown;
|
||||
|
||||
try {
|
||||
promptMarkdown = await fs.readFile(promptPath, 'utf8');
|
||||
} catch {
|
||||
fail(`Unable to read scheduler prompt at ${promptPath}.`);
|
||||
}
|
||||
|
||||
if (!promptMarkdown.trim()) {
|
||||
fail(`Scheduler prompt at ${promptPath} is empty.`);
|
||||
}
|
||||
|
||||
const cadence = process.env.SCHEDULER_CADENCE;
|
||||
const memoriesPath = path.resolve(process.cwd(), `.scheduler-memory/latest/${cadence}/memories.md`);
|
||||
try {
|
||||
const memories = await fs.readFile(memoriesPath, 'utf8');
|
||||
if (memories && memories.trim()) {
|
||||
promptMarkdown += `\n\n# Retrieved Memories\n\n${memories.trim()}\n`;
|
||||
console.error(`[scheduler] Injected memories from ${memoriesPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code !== 'ENOENT') {
|
||||
console.error(`[scheduler] Warning: Failed to read memories from ${memoriesPath}: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const runner = resolveRunner(promptMarkdown);
|
||||
const child = spawn(runner.command, runner.args, {
|
||||
cwd: process.cwd(),
|
||||
env: {
|
||||
...process.env,
|
||||
SCHEDULER_PROMPT_PATH: promptPath,
|
||||
SCHEDULER_PROMPT_MARKDOWN: promptMarkdown,
|
||||
},
|
||||
stdio: 'inherit',
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
fail(`Runner command not found: ${runner.command}`, 127);
|
||||
}
|
||||
fail(`Runner execution failed: ${error.message}`);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code ?? 1);
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,177 @@
|
||||
import { Relay, useWebSocketImplementation } from 'nostr-tools/relay';
|
||||
import { generateSecretKey, finalizeEvent, getPublicKey } from 'nostr-tools/pure';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { spawn } from 'node:child_process';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
useWebSocketImplementation(WebSocket);
|
||||
|
||||
// --- Configuration ---
|
||||
const ARGS = process.argv.slice(2);
|
||||
const getArg = (key) => {
|
||||
const idx = ARGS.findIndex(a => a.startsWith(`--${key}=`));
|
||||
if (idx !== -1) return ARGS[idx].split('=')[1];
|
||||
const flagIdx = ARGS.indexOf(`--${key}`);
|
||||
if (flagIdx !== -1 && ARGS[flagIdx + 1] && !ARGS[flagIdx + 1].startsWith('--')) return ARGS[flagIdx + 1];
|
||||
return null;
|
||||
};
|
||||
const hasArg = (key) => ARGS.includes(`--${key}`);
|
||||
|
||||
const RELAYS = (getArg('relays') || process.env.RELAY_URLS || '').split(',').filter(Boolean);
|
||||
const SERVE_CMD = getArg('serve') || 'npx';
|
||||
const CONFIRM_PUBLIC = hasArg('confirm-public');
|
||||
const OUT_DIR = getArg('out') || 'artifacts';
|
||||
|
||||
// --- Logging ---
|
||||
const LOG_FILE = path.join(OUT_DIR, `smoke-${new Date().toISOString().replace(/[:.]/g, '-')}.log`);
|
||||
const JSON_FILE = path.join(OUT_DIR, `smoke-${new Date().toISOString().split('T')[0]}.json`);
|
||||
|
||||
if (!fs.existsSync(OUT_DIR)) fs.mkdirSync(OUT_DIR, { recursive: true });
|
||||
|
||||
function log(msg) {
|
||||
const ts = new Date().toISOString();
|
||||
const line = `[${ts}] ${msg}`;
|
||||
console.log(line);
|
||||
fs.appendFileSync(LOG_FILE, line + '\n');
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
async function waitForServer(url, timeoutMs = 10000) {
|
||||
const start = Date.now();
|
||||
while (Date.now() - start < timeoutMs) {
|
||||
try {
|
||||
const res = await fetch(url);
|
||||
if (res.ok) return true;
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// --- Main ---
|
||||
async function main() {
|
||||
log('Starting Smoke Test Agent...');
|
||||
log(`Config: RELAYS=${RELAYS.join(',')}, SERVE=${SERVE_CMD}, OUT=${OUT_DIR}`);
|
||||
|
||||
if (RELAYS.length === 0) {
|
||||
log('ERROR: No relays specified. Use --relays or RELAY_URLS.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const results = {
|
||||
timestamp: new Date().toISOString(),
|
||||
steps: [],
|
||||
success: false
|
||||
};
|
||||
|
||||
let serverProcess = null;
|
||||
|
||||
try {
|
||||
// 1. Start Server
|
||||
if (SERVE_CMD !== 'none') {
|
||||
log('Step 1: Starting local server...');
|
||||
const port = 8000;
|
||||
if (SERVE_CMD === 'npx') {
|
||||
serverProcess = spawn('npx', ['serve', 'dist', '-p', String(port)], { stdio: 'ignore', shell: true });
|
||||
} else if (SERVE_CMD === 'python') {
|
||||
serverProcess = spawn('python3', ['-m', 'http.server', String(port), '-d', 'dist'], { stdio: 'ignore', shell: true });
|
||||
}
|
||||
|
||||
const serverUrl = `http://localhost:${port}`;
|
||||
log(`Waiting for server at ${serverUrl}...`);
|
||||
const ready = await waitForServer(serverUrl);
|
||||
if (!ready) {
|
||||
throw new Error('Server failed to start within timeout');
|
||||
}
|
||||
log('Server is ready.');
|
||||
results.steps.push({ name: 'start-server', status: 'ok', details: `Started ${SERVE_CMD} on port ${port}` });
|
||||
} else {
|
||||
log('Step 1: Skipping server start (none selected).');
|
||||
results.steps.push({ name: 'start-server', status: 'skipped' });
|
||||
}
|
||||
|
||||
// 2. Connect to Relay
|
||||
log(`Step 2: Connecting to relay ${RELAYS[0]}...`);
|
||||
const relay = await Relay.connect(RELAYS[0]);
|
||||
log(`Connected to ${RELAYS[0]}`);
|
||||
results.steps.push({ name: 'connect-relay', status: 'ok', details: RELAYS[0] });
|
||||
|
||||
// 3. Generate Identity
|
||||
log('Step 3: Generating ephemeral identity...');
|
||||
const sk = generateSecretKey();
|
||||
const pk = getPublicKey(sk);
|
||||
log(`Pubkey: ${pk}`);
|
||||
results.steps.push({ name: 'generate-identity', status: 'ok', details: pk });
|
||||
|
||||
// 4. Publish Event
|
||||
log('Step 4: Publishing smoke test event...');
|
||||
const eventTemplate = {
|
||||
kind: 1,
|
||||
created_at: Math.floor(Date.now() / 1000),
|
||||
tags: [['t', 'smoke-test']],
|
||||
content: `Smoke test run at ${new Date().toISOString()}`,
|
||||
};
|
||||
const event = finalizeEvent(eventTemplate, sk);
|
||||
await relay.publish(event);
|
||||
log(`Published event: ${event.id}`);
|
||||
results.steps.push({ name: 'publish-event', status: 'ok', details: event.id });
|
||||
|
||||
// 5. Verify Event
|
||||
log('Step 5: Verifying event read-back...');
|
||||
const sub = relay.subscribe([
|
||||
{ ids: [event.id] },
|
||||
], {
|
||||
onevent(evt) {
|
||||
log(`Received event: ${evt.id}`);
|
||||
if (evt.id === event.id) {
|
||||
log('Verification SUCCESS: Event ID matches.');
|
||||
results.steps.push({ name: 'verify-event', status: 'ok', details: 'Event retrieved and matched' });
|
||||
sub.close();
|
||||
}
|
||||
},
|
||||
oneose() {
|
||||
sub.close();
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for event
|
||||
let attempts = 0;
|
||||
let verified = false;
|
||||
while (attempts < 10) {
|
||||
await sleep(1000);
|
||||
const found = results.steps.find(s => s.name === 'verify-event');
|
||||
if (found) {
|
||||
verified = true;
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
}
|
||||
|
||||
if (!verified) {
|
||||
throw new Error('Verification failed: Event not received within timeout.');
|
||||
}
|
||||
|
||||
results.success = true;
|
||||
relay.close();
|
||||
|
||||
} catch (err) {
|
||||
log(`ERROR: ${err.message}`);
|
||||
results.error = err.message;
|
||||
results.success = false;
|
||||
process.exitCode = 1;
|
||||
} finally {
|
||||
if (serverProcess) {
|
||||
log('Stopping server...');
|
||||
serverProcess.kill();
|
||||
}
|
||||
fs.writeFileSync(JSON_FILE, JSON.stringify(results, null, 2));
|
||||
log(`Artifacts written to ${OUT_DIR}`);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,242 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const VALID_CADENCES = ['daily', 'weekly'];
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
sinceDays: 14,
|
||||
logRoot: path.resolve(process.cwd(), 'task-logs'),
|
||||
outDir: path.resolve(process.cwd(), 'artifacts', 'lock-reliability'),
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token === '--since-days') {
|
||||
options.sinceDays = Number.parseInt(argv[i + 1] || '14', 10);
|
||||
i += 1;
|
||||
} else if (token === '--log-root') {
|
||||
options.logRoot = path.resolve(process.cwd(), argv[i + 1] || 'task-logs');
|
||||
i += 1;
|
||||
} else if (token === '--out-dir') {
|
||||
options.outDir = path.resolve(process.cwd(), argv[i + 1] || path.join('artifacts', 'lock-reliability'));
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (!Number.isFinite(options.sinceDays) || options.sinceDays < 0) {
|
||||
throw new Error(`Invalid --since-days value: ${options.sinceDays}`);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
function parseFrontmatter(markdown) {
|
||||
const text = String(markdown || '');
|
||||
if (!text.startsWith('---\n')) return {};
|
||||
const end = text.indexOf('\n---', 4);
|
||||
if (end === -1) return {};
|
||||
const frontmatter = text.slice(4, end).split(/\r?\n/);
|
||||
const result = {};
|
||||
for (const line of frontmatter) {
|
||||
const match = line.match(/^([a-zA-Z0-9_]+):\s*(.+)\s*$/);
|
||||
if (!match) continue;
|
||||
const [, key, rawValue] = match;
|
||||
result[key] = rawValue.replace(/^'/, '').replace(/'$/, '').replace(/''/g, "'");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function inc(map, key) {
|
||||
map.set(key, (map.get(key) || 0) + 1);
|
||||
}
|
||||
|
||||
function collectRelayEndpoints({ frontmatter, body }) {
|
||||
const relays = new Set();
|
||||
const relayList = frontmatter.relay_list || '';
|
||||
relayList.split(',').map((v) => v.trim()).filter(Boolean).forEach((relay) => relays.add(relay));
|
||||
|
||||
const allText = `${body}\n${Object.values(frontmatter).join('\n')}`;
|
||||
const urlMatches = allText.match(/wss?:\/\/[^\s'`),]+/g) || [];
|
||||
for (const url of urlMatches) {
|
||||
relays.add(url.trim());
|
||||
}
|
||||
|
||||
if (!relays.size) relays.add('unknown');
|
||||
return [...relays];
|
||||
}
|
||||
|
||||
function getBackendCategory({ frontmatter, body }) {
|
||||
if (frontmatter.backend_category) return frontmatter.backend_category;
|
||||
if (frontmatter.preflight_failure_category) return `preflight:${frontmatter.preflight_failure_category}`;
|
||||
|
||||
const reasonMatch = body.match(/- reason:\s*([^\n]+)/i) || body.match(/reason:\s*([^\n]+)/i);
|
||||
const reason = reasonMatch?.[1]?.trim() || '';
|
||||
|
||||
if (/lock backend/i.test(reason)) return 'lock backend error (uncategorized)';
|
||||
if (/completion publish failed/i.test(reason)) return 'completion publish failed';
|
||||
if (/preflight/i.test(reason)) return 'preflight failed (uncategorized)';
|
||||
return null;
|
||||
}
|
||||
|
||||
function toSortedObject(counterMap) {
|
||||
return Object.fromEntries([...counterMap.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])));
|
||||
}
|
||||
|
||||
function asMarkdown(report) {
|
||||
const lines = [];
|
||||
lines.push('# Lock Reliability Summary');
|
||||
lines.push('');
|
||||
lines.push(`- Generated at: ${report.generatedAt}`);
|
||||
lines.push(`- Window: last ${report.sinceDays} day(s)`);
|
||||
lines.push(`- Logs scanned: ${report.logsScanned}`);
|
||||
lines.push(`- Runs considered: ${report.runsConsidered}`);
|
||||
lines.push('');
|
||||
|
||||
lines.push('## Totals');
|
||||
lines.push('');
|
||||
lines.push(`- completed: ${report.statusCounts.completed || 0}`);
|
||||
lines.push(`- failed: ${report.statusCounts.failed || 0}`);
|
||||
lines.push('');
|
||||
|
||||
const sections = [
|
||||
['By platform', report.byPlatform],
|
||||
['By cadence', report.byCadence],
|
||||
['By backend error category', report.byBackendCategory],
|
||||
['By relay endpoint', report.byRelayEndpoint],
|
||||
];
|
||||
|
||||
for (const [title, obj] of sections) {
|
||||
lines.push(`## ${title}`);
|
||||
lines.push('');
|
||||
const entries = Object.entries(obj);
|
||||
if (!entries.length) {
|
||||
lines.push('- (none)');
|
||||
} else {
|
||||
for (const [key, value] of entries) {
|
||||
lines.push(`- ${key}: ${value}`);
|
||||
}
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
if (report.recurringBackendErrors.length) {
|
||||
lines.push('## Recurring backend errors (count >= 2)');
|
||||
lines.push('');
|
||||
for (const entry of report.recurringBackendErrors) {
|
||||
lines.push(`- ${entry.category}: ${entry.count}`);
|
||||
}
|
||||
lines.push('');
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
async function readLogFile(filePath, cadence, cutoffMs) {
|
||||
const stat = await fs.stat(filePath);
|
||||
if (!stat.isFile() || stat.mtimeMs < cutoffMs) return null;
|
||||
|
||||
const basename = path.basename(filePath);
|
||||
const match = basename.match(/^.+__([^_]+?)__(completed|failed)\.md$/);
|
||||
if (!match) return null;
|
||||
|
||||
const body = await fs.readFile(filePath, 'utf8');
|
||||
const frontmatter = parseFrontmatter(body);
|
||||
const status = frontmatter.status || match[2];
|
||||
const platform = frontmatter.platform || 'unknown';
|
||||
const backendCategory = status === 'failed'
|
||||
? getBackendCategory({ frontmatter, body })
|
||||
: 'n/a';
|
||||
const relayEndpoints = collectRelayEndpoints({ frontmatter, body });
|
||||
|
||||
return {
|
||||
filePath,
|
||||
cadence: frontmatter.cadence || cadence,
|
||||
status,
|
||||
platform,
|
||||
backendCategory,
|
||||
relayEndpoints,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const now = Date.now();
|
||||
const cutoffMs = now - (options.sinceDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const byPlatform = new Map();
|
||||
const byCadence = new Map();
|
||||
const byBackendCategory = new Map();
|
||||
const byRelayEndpoint = new Map();
|
||||
const statusCounts = new Map();
|
||||
const records = [];
|
||||
let logsScanned = 0;
|
||||
|
||||
for (const cadence of VALID_CADENCES) {
|
||||
const cadenceDir = path.join(options.logRoot, cadence);
|
||||
let entries;
|
||||
try {
|
||||
entries = await fs.readdir(cadenceDir);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(cadenceDir, entry);
|
||||
logsScanned += 1;
|
||||
const record = await readLogFile(fullPath, cadence, cutoffMs);
|
||||
if (!record) continue;
|
||||
|
||||
records.push(record);
|
||||
inc(statusCounts, record.status);
|
||||
inc(byPlatform, record.platform);
|
||||
inc(byCadence, record.cadence);
|
||||
if (record.status === 'failed' && record.backendCategory) {
|
||||
inc(byBackendCategory, record.backendCategory);
|
||||
}
|
||||
for (const relay of record.relayEndpoints) {
|
||||
inc(byRelayEndpoint, relay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const recurringBackendErrors = [...byBackendCategory.entries()]
|
||||
.filter(([, count]) => count >= 2)
|
||||
.sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]))
|
||||
.map(([category, count]) => ({ category, count }));
|
||||
|
||||
const report = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
sinceDays: options.sinceDays,
|
||||
logRoot: options.logRoot,
|
||||
logsScanned,
|
||||
runsConsidered: records.length,
|
||||
statusCounts: toSortedObject(statusCounts),
|
||||
byPlatform: toSortedObject(byPlatform),
|
||||
byCadence: toSortedObject(byCadence),
|
||||
byBackendCategory: toSortedObject(byBackendCategory),
|
||||
byRelayEndpoint: toSortedObject(byRelayEndpoint),
|
||||
recurringBackendErrors,
|
||||
};
|
||||
|
||||
await fs.mkdir(options.outDir, { recursive: true });
|
||||
const markdown = asMarkdown(report);
|
||||
const markdownPath = path.join(options.outDir, 'lock-reliability-summary.md');
|
||||
const jsonPath = path.join(options.outDir, 'lock-reliability-summary.json');
|
||||
|
||||
await fs.writeFile(markdownPath, markdown, 'utf8');
|
||||
await fs.writeFile(jsonPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
||||
|
||||
console.log(JSON.stringify({
|
||||
ok: true,
|
||||
markdown: path.relative(process.cwd(), markdownPath),
|
||||
json: path.relative(process.cwd(), jsonPath),
|
||||
recurringBackendErrors,
|
||||
}));
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error?.stack || error?.message || String(error));
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,362 @@
|
||||
#!/usr/bin/env node
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
const REQUIRED_ARTIFACTS = [
|
||||
{
|
||||
dir: 'src/context',
|
||||
expectedPrefix: 'CONTEXT_',
|
||||
structureCheck: (content) =>
|
||||
/\bgoal\b/i.test(content) && /\bscope\b/i.test(content) && /\bconstraints\b/i.test(content),
|
||||
structureHint: 'must include goal/scope/constraints',
|
||||
},
|
||||
{
|
||||
dir: 'src/todo',
|
||||
expectedPrefix: 'TODO_',
|
||||
structureCheck: (content) =>
|
||||
/\bpending(?:\s+tasks?)?\b[\s\S]{0,400}^\s*[-*]\s+/im.test(content)
|
||||
|| /\bcompleted(?:\s+tasks?)?\b[\s\S]{0,400}^\s*[-*]\s+/im.test(content),
|
||||
structureHint: 'must include at least one pending or completed item',
|
||||
},
|
||||
{
|
||||
dir: 'src/decisions',
|
||||
expectedPrefix: 'DECISIONS_',
|
||||
structureCheck: (content) => /\bdecision\b/i.test(content) && /\brationale\b/i.test(content),
|
||||
structureHint: 'must include decision + rationale',
|
||||
},
|
||||
{
|
||||
dir: 'src/test_logs',
|
||||
expectedPrefix: 'TEST_LOG_',
|
||||
structureCheck: (content) => {
|
||||
const hasCommand = /(^|\n)\s*[-*]?\s*\**command\**\s*:/im.test(content);
|
||||
const hasResult = /(^|\n)\s*[-*]?\s*\**result\**\s*:/im.test(content);
|
||||
return hasCommand && hasResult;
|
||||
},
|
||||
structureHint: 'must include command/result pairs',
|
||||
},
|
||||
];
|
||||
|
||||
const FAILURE_KEYWORD_RE = /\b(fail|failed|failure|error)\b/i;
|
||||
const STATUS_RESOLVED_RE = /\b(resolved|closed|fixed)\b/i;
|
||||
const IDENTIFIER_INLINE_PATTERN = String.raw`\[([A-Za-z0-9._-]{3,})\]`;
|
||||
|
||||
function parseArgs(argv) {
|
||||
const args = {
|
||||
since: null,
|
||||
session: null,
|
||||
checkFailureNotes: false,
|
||||
agent: null,
|
||||
cadence: null,
|
||||
promptPath: null,
|
||||
runStart: null,
|
||||
};
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const value = argv[i];
|
||||
if (value === '--since') {
|
||||
args.since = argv[i + 1] || null;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === '--session') {
|
||||
args.session = argv[i + 1] || null;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === '--check-failure-notes') {
|
||||
args.checkFailureNotes = true;
|
||||
continue;
|
||||
}
|
||||
if (value === '--agent') {
|
||||
args.agent = argv[i + 1] || null;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === '--cadence') {
|
||||
args.cadence = argv[i + 1] || null;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === '--prompt-path') {
|
||||
args.promptPath = argv[i + 1] || null;
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (value === '--run-start') {
|
||||
args.runStart = argv[i + 1] || null;
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
function toMs(iso) {
|
||||
if (!iso) return null;
|
||||
const value = Date.parse(iso);
|
||||
return Number.isFinite(value) ? value : null;
|
||||
}
|
||||
|
||||
function isNotNeededNote(content) {
|
||||
return /\bnot needed\b/i.test(content);
|
||||
}
|
||||
|
||||
async function listRecentFiles(dirPath, sinceMs) {
|
||||
try {
|
||||
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
||||
const files = [];
|
||||
for (const entry of entries) {
|
||||
if (!entry.isFile()) continue;
|
||||
const filePath = path.join(dirPath, entry.name);
|
||||
const stat = await fs.stat(filePath);
|
||||
if (sinceMs != null && stat.mtimeMs < sinceMs) continue;
|
||||
files.push({ name: entry.name, filePath, mtimeMs: stat.mtimeMs });
|
||||
}
|
||||
return files.sort((a, b) => a.mtimeMs - b.mtimeMs);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async function hasExplicitNotNeeded(files) {
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file.filePath, 'utf8').catch(() => '');
|
||||
if (isNotNeededNote(content)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getMatchingArtifacts(files, { expectedPrefix, session }) {
|
||||
if (session) {
|
||||
return files.filter((file) => file.name.includes(session));
|
||||
}
|
||||
return files.filter((file) => file.name.startsWith(expectedPrefix));
|
||||
}
|
||||
|
||||
async function hasValidArtifactStructure(files, artifact) {
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file.filePath, 'utf8').catch(() => '');
|
||||
if (artifact.structureCheck(content)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function readMetadataValue(content, keys) {
|
||||
for (const key of keys) {
|
||||
const re = new RegExp(`^\\s*(?:[-*]\\s*)?\\**${key}\\**\\s*:\\s*(.+)$`, 'im');
|
||||
const match = content.match(re);
|
||||
if (match && match[1]) {
|
||||
return match[1].trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractIdentifiers(content) {
|
||||
const ids = new Set();
|
||||
const lines = content.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
if (/(issue|incident|failure)[-_\s]*id/i.test(line)) {
|
||||
const match = line.match(/([A-Za-z0-9._-]{3,})\s*$/);
|
||||
if (match) ids.add(match[1]);
|
||||
}
|
||||
for (const inline of line.matchAll(new RegExp(IDENTIFIER_INLINE_PATTERN, 'g'))) {
|
||||
ids.add(inline[1]);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
function collectUnresolvedKnownIssueIds(content) {
|
||||
const ids = new Set();
|
||||
const sections = content.split(/^###\s+/m).slice(1);
|
||||
for (const sectionRaw of sections) {
|
||||
const section = sectionRaw.trim();
|
||||
const statusMatch = section.match(/\*\*Status:\*\*\s*(.+)$/im);
|
||||
const status = (statusMatch?.[1] || '').trim();
|
||||
if (status && STATUS_RESOLVED_RE.test(status)) {
|
||||
continue;
|
||||
}
|
||||
for (const id of extractIdentifiers(section)) {
|
||||
ids.add(id);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
async function validateArtifactMetadata(files, { agent, cadence, promptPath, runStart }) {
|
||||
const failures = [];
|
||||
const promptPathResolved = promptPath ? path.resolve(process.cwd(), promptPath) : null;
|
||||
const promptRelative = promptPathResolved ? path.relative(process.cwd(), promptPathResolved) : null;
|
||||
const promptBase = promptPathResolved ? path.basename(promptPathResolved) : null;
|
||||
|
||||
for (const file of files) {
|
||||
const content = await fs.readFile(file.filePath, 'utf8').catch(() => '');
|
||||
const fileErrors = [];
|
||||
|
||||
const agentValue = readMetadataValue(content, ['agent']);
|
||||
const cadenceValue = readMetadataValue(content, ['cadence']);
|
||||
const sessionValue = readMetadataValue(content, ['session']);
|
||||
const runStartValue = readMetadataValue(content, ['run-start', 'run_start', 'run start']);
|
||||
|
||||
if (!agentValue) fileErrors.push('missing `agent:` metadata');
|
||||
if (!cadenceValue) fileErrors.push('missing `cadence:` metadata');
|
||||
if (!sessionValue && !runStartValue) fileErrors.push('missing `session:` or `run-start:` metadata');
|
||||
|
||||
if (agent && agentValue && agentValue !== agent) {
|
||||
fileErrors.push(`agent metadata "${agentValue}" does not match active SCHEDULER_AGENT "${agent}"`);
|
||||
}
|
||||
if (cadence && cadenceValue && cadenceValue !== cadence) {
|
||||
fileErrors.push(`cadence metadata "${cadenceValue}" does not match active cadence "${cadence}"`);
|
||||
}
|
||||
if (runStart && runStartValue && runStartValue !== runStart) {
|
||||
fileErrors.push(`run-start metadata "${runStartValue}" does not match active run-start "${runStart}"`);
|
||||
}
|
||||
|
||||
if (agent && !content.includes(agent)) {
|
||||
fileErrors.push(`content does not reference active SCHEDULER_AGENT "${agent}"`);
|
||||
}
|
||||
|
||||
if (promptPathResolved) {
|
||||
const referencesPrompt = [promptPathResolved, promptRelative, promptBase].filter(Boolean)
|
||||
.some((candidate) => content.includes(candidate));
|
||||
if (!referencesPrompt) {
|
||||
fileErrors.push(`content does not reference active prompt file (${promptRelative || promptPathResolved})`);
|
||||
}
|
||||
}
|
||||
|
||||
if (fileErrors.length) {
|
||||
failures.push(`${path.relative(process.cwd(), file.filePath)}: ${fileErrors.join('; ')}`);
|
||||
}
|
||||
}
|
||||
|
||||
return failures;
|
||||
}
|
||||
|
||||
async function checkFailureTracking({ sinceMs }) {
|
||||
const recentTestLogs = await listRecentFiles(path.resolve(process.cwd(), 'src/test_logs'), sinceMs);
|
||||
const failureIds = new Set();
|
||||
const logsWithFailureNoIds = [];
|
||||
|
||||
for (const file of recentTestLogs) {
|
||||
const content = await fs.readFile(file.filePath, 'utf8').catch(() => '');
|
||||
if (FAILURE_KEYWORD_RE.test(content)) {
|
||||
const ids = extractIdentifiers(content);
|
||||
if (!ids.size) {
|
||||
logsWithFailureNoIds.push(path.relative(process.cwd(), file.filePath));
|
||||
}
|
||||
for (const id of ids) failureIds.add(id);
|
||||
}
|
||||
}
|
||||
|
||||
if (!failureIds.size && !logsWithFailureNoIds.length) {
|
||||
return { ok: true, message: null };
|
||||
}
|
||||
|
||||
if (logsWithFailureNoIds.length) {
|
||||
return {
|
||||
ok: false,
|
||||
message: `Failure-related output detected in test logs without identifiers. Add Issue/Incident identifiers to: ${logsWithFailureNoIds.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
const knownIssuesPath = path.resolve(process.cwd(), 'KNOWN_ISSUES.md');
|
||||
const knownIssuesContent = await fs.readFile(knownIssuesPath, 'utf8').catch(() => '');
|
||||
const unresolvedKnownIssueIds = collectUnresolvedKnownIssueIds(knownIssuesContent);
|
||||
const incidents = await listRecentFiles(path.resolve(process.cwd(), 'docs/agent-handoffs/incidents'), sinceMs);
|
||||
|
||||
const incidentIds = new Set();
|
||||
for (const incident of incidents) {
|
||||
const incidentContent = await fs.readFile(incident.filePath, 'utf8').catch(() => '');
|
||||
for (const id of extractIdentifiers(incidentContent)) incidentIds.add(id);
|
||||
}
|
||||
|
||||
const unmatched = [...failureIds].filter((id) => !unresolvedKnownIssueIds.has(id) && !incidentIds.has(id));
|
||||
|
||||
if (!unmatched.length) {
|
||||
return { ok: true, message: null };
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: `Failure identifiers found in src/test_logs are not cross-linked to unresolved KNOWN_ISSUES.md entries or new incident notes: ${unmatched.join(', ')}`,
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const sinceMs = toMs(args.since);
|
||||
const activeAgent = args.agent || process.env.SCHEDULER_AGENT || null;
|
||||
const activeCadence = args.cadence || process.env.SCHEDULER_CADENCE || null;
|
||||
const activePromptPath = args.promptPath || process.env.SCHEDULER_PROMPT_PATH || null;
|
||||
const activeRunStart = args.runStart || null;
|
||||
|
||||
if (args.since && sinceMs == null) {
|
||||
console.error(`Invalid --since value: ${args.since}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
if (!activeAgent || !activePromptPath) {
|
||||
console.error('Missing scheduler context: provide --agent and --prompt-path (or set SCHEDULER_AGENT/SCHEDULER_PROMPT_PATH).');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const missing = [];
|
||||
|
||||
for (const artifact of REQUIRED_ARTIFACTS) {
|
||||
const dirPath = path.resolve(process.cwd(), artifact.dir);
|
||||
const files = await listRecentFiles(dirPath, sinceMs);
|
||||
const matchingArtifacts = getMatchingArtifacts(files, {
|
||||
expectedPrefix: artifact.expectedPrefix,
|
||||
session: args.session,
|
||||
});
|
||||
|
||||
if (!matchingArtifacts.length) {
|
||||
const hasNotNeeded = await hasExplicitNotNeeded(files);
|
||||
if (hasNotNeeded) continue;
|
||||
|
||||
const expectedName = args.session
|
||||
? `${artifact.expectedPrefix}${args.session}.md`
|
||||
: `${artifact.expectedPrefix}<timestamp>.md`;
|
||||
missing.push(`${artifact.dir}/${expectedName}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
const hasValidStructure = await hasValidArtifactStructure(matchingArtifacts, artifact);
|
||||
if (!hasValidStructure) {
|
||||
missing.push(`${artifact.dir}: ${artifact.structureHint}`);
|
||||
}
|
||||
|
||||
const metadataFailures = await validateArtifactMetadata(matchingArtifacts, {
|
||||
agent: activeAgent,
|
||||
cadence: activeCadence,
|
||||
promptPath: activePromptPath,
|
||||
runStart: activeRunStart,
|
||||
});
|
||||
missing.push(...metadataFailures);
|
||||
}
|
||||
|
||||
if (args.checkFailureNotes) {
|
||||
const failureTracking = await checkFailureTracking({ sinceMs });
|
||||
if (!failureTracking.ok && failureTracking.message) {
|
||||
missing.push(failureTracking.message);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length) {
|
||||
console.error('Missing required run artifacts:');
|
||||
for (const item of missing) {
|
||||
console.error(`- ${item}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Run artifacts verified.');
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,88 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import http from 'node:http';
|
||||
import { performance } from 'node:perf_hooks';
|
||||
|
||||
const PORT = 3456;
|
||||
const REQUESTS = 2000;
|
||||
const CONCURRENCY = 50;
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
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 (e) {
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
throw new Error('Server did not start');
|
||||
}
|
||||
|
||||
function makeRequest() {
|
||||
return new Promise((resolve, reject) => {
|
||||
// Request a small file that exists
|
||||
const req = http.get(`http://localhost:${PORT}/torch-config.example.json`, (res) => {
|
||||
res.resume(); // consume body
|
||||
res.on('end', resolve);
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function benchmark() {
|
||||
console.log('Starting server...');
|
||||
const serverProcess = spawn('node', ['bin/torch-lock.mjs', 'dashboard', '--port', String(PORT)], {
|
||||
stdio: 'ignore' // Suppress logs for cleaner output
|
||||
});
|
||||
|
||||
try {
|
||||
await waitForServer();
|
||||
console.log('Server ready.');
|
||||
|
||||
const start = performance.now();
|
||||
|
||||
let sent = 0;
|
||||
const worker = async () => {
|
||||
while (true) {
|
||||
const current = sent++;
|
||||
if (current >= REQUESTS) break;
|
||||
try {
|
||||
await makeRequest();
|
||||
} catch (e) {
|
||||
// console.error(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const workers = [];
|
||||
for (let i = 0; i < CONCURRENCY; i++) {
|
||||
workers.push(worker());
|
||||
}
|
||||
|
||||
await Promise.all(workers);
|
||||
|
||||
const end = performance.now();
|
||||
const duration = end - start;
|
||||
const rps = (REQUESTS / duration) * 1000;
|
||||
|
||||
console.log(`Requests: ${REQUESTS}`);
|
||||
console.log(`Duration: ${duration.toFixed(2)}ms`);
|
||||
console.log(`RPS: ${rps.toFixed(2)}`);
|
||||
|
||||
} finally {
|
||||
serverProcess.kill();
|
||||
}
|
||||
}
|
||||
|
||||
benchmark().catch(console.error);
|
||||
@@ -0,0 +1,106 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
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 {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return content.split('\n').length;
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${filePath}: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 (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}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const reportMode = values.report;
|
||||
|
||||
const files = scanDirectory('.');
|
||||
let oversizedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const lines = countLines(file);
|
||||
if (lines > MAX_LINES) {
|
||||
const excess = lines - MAX_LINES;
|
||||
console.log(`${file}: ${lines} lines (excess: ${excess})`);
|
||||
oversizedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!reportMode && oversizedCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,113 @@
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { parseArgs } from 'node:util';
|
||||
|
||||
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 {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
// Basic regex to find assignment to innerHTML
|
||||
const regex = /\.innerHTML\s*=/g;
|
||||
const matches = content.match(regex);
|
||||
return matches ? matches.length : 0;
|
||||
} catch (error) {
|
||||
console.error(`Error reading ${filePath}: ${error.message}`);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
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 (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}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
const reportMode = values.report;
|
||||
|
||||
const files = scanDirectory('.');
|
||||
let totalAssignments = 0;
|
||||
let offenderCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
const count = countInnerHTML(file);
|
||||
if (count > 0) {
|
||||
console.log(`${file}: ${count} assignments`);
|
||||
totalAssignments += count;
|
||||
offenderCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (reportMode && totalAssignments > 0) {
|
||||
console.log(`Total assignments: ${totalAssignments}`);
|
||||
}
|
||||
|
||||
if (!reportMode && offenderCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -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);
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
set -euo pipefail
|
||||
node --input-type=module -e "import { ingestEvents, getRelevantMemories } from './src/services/memory/index.js'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; const fallbackCadence='weekly'; const cadence=process.env.SCHEDULER_CADENCE || fallbackCadence; const agentId=process.env.SCHEDULER_AGENT || ('scheduler-memory-' + cadence); const promptPath=process.env.SCHEDULER_PROMPT_PATH || ''; const runId=(process.env.SCHEDULER_RUN_ID || process.env.SCHEDULER_SESSION_ID || process.env.RUN_ID || ('session-' + Date.now().toString(36))); const promptRaw=promptPath ? readFileSync(promptPath, 'utf8') : ''; const promptLines=promptRaw.split(/\\r?\\n/).map((line)=>line.trim()).filter(Boolean); const promptIntent=promptLines.find((line)=>line.startsWith('#') || line.toLowerCase().startsWith('goal')) || promptLines[0] || 'scheduler memory retrieval'; const query=['scheduler', cadence, 'agent', agentId, 'intent', promptIntent.replace(/^#+\\s*/, '')].join(' | '); const timestamp=Date.now(); const events=[{ agent_id: agentId, content: 'Memory retrieval seed for ' + cadence + ' :: ' + promptIntent, timestamp, tags:['scheduler', cadence, 'retrieve'], metadata:{ session_id:runId, source:'scheduler-retrieve', importance:0.4, prompt_path:promptPath } }]; const ingested=await ingestEvents(events,{ agent_id: agentId }); const retrieved=await getRelevantMemories({ agent_id: agentId, query, k: 5 }); const sessionDir='.scheduler-memory/' + runId; const latestDir='.scheduler-memory/latest/' + cadence; mkdirSync(sessionDir,{ recursive:true }); mkdirSync(latestDir,{ recursive:true }); const artifact={ cadence, operation:'retrieve', runId, servicePath:'src/services/memory/index.js#getRelevantMemories', inputs:{ agentId, promptPath, promptIntent, query, events:events.length }, outputs:{ ingestedCount:ingested.length, retrievedCount:retrieved.length }, status:'ok' }; writeFileSync(sessionDir + '/retrieve.json', JSON.stringify(artifact, null, 2)); writeFileSync(sessionDir + '/retrieve.ok', 'MEMORY_RETRIEVED\\n'); writeFileSync(latestDir + '/retrieve.ok', 'MEMORY_RETRIEVED\\n');"
|
||||
echo MEMORY_RETRIEVED
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
set -euo pipefail
|
||||
node --input-type=module -e "import { ingestEvents } from './src/services/memory/index.js'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; const fallbackCadence='weekly'; const cadence=process.env.SCHEDULER_CADENCE || fallbackCadence; const agentId=process.env.SCHEDULER_AGENT || ('scheduler-memory-' + cadence); const promptPath=process.env.SCHEDULER_PROMPT_PATH || ''; const runId=(process.env.SCHEDULER_RUN_ID || process.env.SCHEDULER_SESSION_ID || process.env.RUN_ID || ('session-' + Date.now().toString(36))); const promptRaw=promptPath ? readFileSync(promptPath, 'utf8') : ''; const promptLines=promptRaw.split(/\\r?\\n/).map((line)=>line.trim()).filter(Boolean); const promptIntent=promptLines.find((line)=>line.startsWith('#') || line.toLowerCase().startsWith('goal')) || promptLines[0] || 'scheduler memory store'; const baseTs=Date.now(); const events=[{ agent_id: agentId, content: 'Store memory event A for ' + cadence + ' :: ' + promptIntent, timestamp: baseTs, tags:['scheduler', cadence, 'store'], metadata:{ session_id:runId, source:'scheduler-store', importance:0.55, prompt_path:promptPath } },{ agent_id: agentId, content: 'Store memory event B for ' + cadence + ' :: ' + promptIntent, timestamp: baseTs + 1, tags:['scheduler', cadence, 'store'], metadata:{ session_id:runId, source:'scheduler-store', importance:0.55, prompt_path:promptPath } }]; const stored=await ingestEvents(events,{ agent_id: agentId }); const sessionDir='.scheduler-memory/' + runId; const latestDir='.scheduler-memory/latest/' + cadence; mkdirSync(sessionDir,{ recursive:true }); mkdirSync(latestDir,{ recursive:true }); const artifact={ cadence, operation:'store', runId, servicePath:'src/services/memory/index.js#ingestEvents', inputs:{ agentId, promptPath, promptIntent, events:events.length }, outputs:{ storedCount:stored.length, summaries:stored.map((m)=>m.summary) }, status:'ok' }; writeFileSync(sessionDir + '/store.json', JSON.stringify(artifact, null, 2)); writeFileSync(sessionDir + '/store.ok', 'MEMORY_STORED\\n'); writeFileSync(latestDir + '/store.ok', 'MEMORY_STORED\\n');"
|
||||
echo MEMORY_STORED
|
||||
@@ -0,0 +1,79 @@
|
||||
import { readFile, readdir } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const PROMPT_DIRS = ['src/prompts/daily', 'src/prompts/weekly'];
|
||||
|
||||
const REQUIRED_SECTION_HEADING = '## Required startup + artifacts + memory + issue capture';
|
||||
|
||||
const REQUIRED_TOKENS = [
|
||||
'`AGENTS.md`',
|
||||
'`CLAUDE.md`',
|
||||
'`KNOWN_ISSUES.md`',
|
||||
'`docs/agent-handoffs/README.md`',
|
||||
'`src/context/`',
|
||||
'`src/todo/`',
|
||||
'`src/decisions/`',
|
||||
'`src/test_logs/`',
|
||||
'`docs/agent-handoffs/incidents/`',
|
||||
'memory retrieval before implementation',
|
||||
'memory storage after implementation',
|
||||
];
|
||||
|
||||
async function listPromptFiles() {
|
||||
const files = [];
|
||||
|
||||
for (const dir of PROMPT_DIRS) {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
for (const entry of entries) {
|
||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(join(dir, entry.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return files.sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
function validateFile(filePath, content) {
|
||||
const issues = [];
|
||||
|
||||
if (!content.includes(REQUIRED_SECTION_HEADING)) {
|
||||
issues.push(`missing required section heading: "${REQUIRED_SECTION_HEADING}"`);
|
||||
return issues;
|
||||
}
|
||||
|
||||
const sectionStart = content.indexOf(REQUIRED_SECTION_HEADING);
|
||||
const remainder = content.slice(sectionStart + REQUIRED_SECTION_HEADING.length);
|
||||
const nextHeadingIndex = remainder.search(/\n##\s+/);
|
||||
const sectionBody =
|
||||
nextHeadingIndex >= 0 ? remainder.slice(0, nextHeadingIndex) : remainder;
|
||||
|
||||
for (const token of REQUIRED_TOKENS) {
|
||||
if (!sectionBody.includes(token)) {
|
||||
issues.push(`missing contract token in required section: ${token}`);
|
||||
}
|
||||
}
|
||||
|
||||
return issues;
|
||||
}
|
||||
|
||||
const files = await listPromptFiles();
|
||||
const errors = [];
|
||||
|
||||
for (const file of files) {
|
||||
const content = await readFile(file, 'utf8');
|
||||
const issues = validateFile(file, content);
|
||||
for (const issue of issues) {
|
||||
errors.push(`[prompt-contract] ${file}: ${issue}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('Prompt contract drift detected:');
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Prompt contract validated for all daily/weekly prompts.');
|
||||
@@ -0,0 +1,203 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const FILES = {
|
||||
meta: 'src/prompts/META_PROMPTS.md',
|
||||
flow: 'src/prompts/scheduler-flow.md',
|
||||
scheduler: 'scripts/agent/run-scheduler-cycle.mjs',
|
||||
};
|
||||
|
||||
function collectMissing(text, requiredSnippets) {
|
||||
return requiredSnippets.filter((snippet) => !text.includes(snippet));
|
||||
}
|
||||
|
||||
function ensureOrdered(text, snippets) {
|
||||
const positions = [];
|
||||
for (const snippet of snippets) {
|
||||
const idx = text.indexOf(snippet);
|
||||
if (idx === -1) {
|
||||
return { ok: false, missing: snippet };
|
||||
}
|
||||
positions.push(idx);
|
||||
}
|
||||
|
||||
for (let i = 1; i < positions.length; i += 1) {
|
||||
if (positions[i] < positions[i - 1]) {
|
||||
return { ok: false, outOfOrder: [snippets[i - 1], snippets[i]] };
|
||||
}
|
||||
}
|
||||
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const invariants = [
|
||||
{
|
||||
name: 'lock check + exclusion fallback',
|
||||
promptSnippets: {
|
||||
meta: [
|
||||
'Run preflight to get the exclusion set:',
|
||||
'Use `excluded` from the JSON output as the canonical exclusion set.',
|
||||
'fallback to the union of `locked`, `paused`, and `completed`',
|
||||
],
|
||||
flow: [
|
||||
'canonical exclusion rule',
|
||||
],
|
||||
},
|
||||
schedulerSnippets: [
|
||||
"runCommand('npm', ['run', `lock:check:${cadence}`",
|
||||
'checkPayload.excluded',
|
||||
'checkPayload.locked',
|
||||
'checkPayload.paused',
|
||||
'checkPayload.completed',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'roster selection source + all-excluded stop',
|
||||
promptSnippets: {
|
||||
meta: [
|
||||
'Read roster from src/prompts/roster.json',
|
||||
'scheduler.firstPromptByCadence.daily',
|
||||
'scheduler.firstPromptByCadence.weekly',
|
||||
'All roster tasks currently claimed by other agents',
|
||||
],
|
||||
flow: [
|
||||
'Roster source: `src/prompts/roster.json`',
|
||||
'Read `scheduler.firstPromptByCadence.<cadence>` from `torch-config.json` if present.',
|
||||
'All roster tasks currently claimed by other agents',
|
||||
],
|
||||
},
|
||||
schedulerSnippets: [
|
||||
"readJson(path.resolve(process.cwd(), 'src/prompts/roster.json')",
|
||||
'parseFrontmatterAgent(content) || parseAgentFromFilename(latestFile)',
|
||||
'scheduler.firstPromptByCadence?.[cadence]',
|
||||
'ALL_EXCLUDED_REASON',
|
||||
"reason: ALL_EXCLUDED_REASON",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'memory retrieve/store execution + required evidence enforcement',
|
||||
promptSnippets: {
|
||||
meta: [
|
||||
'Run required memory workflow for this cadence:',
|
||||
'Validate memory evidence:',
|
||||
'fail the run if either check is missing',
|
||||
],
|
||||
flow: [
|
||||
'Confirm memory contract completion:',
|
||||
'Memory retrieval evidence must exist for this run',
|
||||
'Memory storage evidence must exist for this run',
|
||||
'mode = required`, missing evidence is a hard failure',
|
||||
],
|
||||
},
|
||||
schedulerSnippets: [
|
||||
'schedulerConfig.memoryPolicy.retrieveCommand',
|
||||
'schedulerConfig.memoryPolicy.storeCommand',
|
||||
"name: 'retrieve'",
|
||||
"name: 'store'",
|
||||
"schedulerConfig.memoryPolicy.mode === 'required'",
|
||||
"reason: 'Required memory steps not verified'",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'artifact verification checkpoint',
|
||||
promptSnippets: {
|
||||
flow: [
|
||||
'Verify required run artifacts for the current run window.',
|
||||
'verify-run-artifacts.mjs --since <run-start-iso> --check-failure-notes',
|
||||
'If artifact verification exits non-zero: write `_failed.md` and stop.',
|
||||
],
|
||||
},
|
||||
schedulerSnippets: [
|
||||
"runCommand('node', [",
|
||||
"'scripts/agent/verify-run-artifacts.mjs'",
|
||||
"'--check-failure-notes'",
|
||||
"reason: 'Missing required run artifacts'",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'failure stop conditions + completion ordering',
|
||||
promptSnippets: {
|
||||
meta: [
|
||||
'If any validation command exits non-zero, do not call `lock:complete`',
|
||||
'Publish completion before writing `_completed.md`',
|
||||
'Only after step 10 succeeds, write final task log (`_completed.md`',
|
||||
],
|
||||
flow: [
|
||||
'step 12 MUST NOT be executed (`lock:complete` is forbidden until validation passes)',
|
||||
'Publish completion before writing final success log:',
|
||||
'`_completed.md` MUST be created only after completion publish succeeds.',
|
||||
'backend_category',
|
||||
'lock_command',
|
||||
'lock_stderr_excerpt',
|
||||
],
|
||||
},
|
||||
schedulerSnippets: [
|
||||
"reason: 'Validation failed'",
|
||||
'const completeResult = await runCommand(',
|
||||
"['run', 'lock:complete'",
|
||||
"status: 'completed'",
|
||||
'Completion publish failed. Retry npm run lock:complete',
|
||||
"reason: 'Lock backend error'",
|
||||
'classifyLockBackendError',
|
||||
'backend_category',
|
||||
'lock_command',
|
||||
'lock_stderr_excerpt',
|
||||
'lock_stdout_excerpt',
|
||||
],
|
||||
orderedSchedulerSnippets: [
|
||||
"reason: 'Validation failed'",
|
||||
'const completeResult = await runCommand(',
|
||||
"status: 'completed'",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
const [meta, flow, scheduler] = await Promise.all([
|
||||
readFile(FILES.meta, 'utf8'),
|
||||
readFile(FILES.flow, 'utf8'),
|
||||
readFile(FILES.scheduler, 'utf8'),
|
||||
]);
|
||||
|
||||
const errors = [];
|
||||
|
||||
for (const invariant of invariants) {
|
||||
if (invariant.promptSnippets?.meta) {
|
||||
const missingMeta = collectMissing(meta, invariant.promptSnippets.meta);
|
||||
for (const snippet of missingMeta) {
|
||||
errors.push(`[${invariant.name}] ${FILES.meta} missing snippet: ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (invariant.promptSnippets?.flow) {
|
||||
const missingFlow = collectMissing(flow, invariant.promptSnippets.flow);
|
||||
for (const snippet of missingFlow) {
|
||||
errors.push(`[${invariant.name}] ${FILES.flow} missing snippet: ${snippet}`);
|
||||
}
|
||||
}
|
||||
|
||||
const missingScheduler = collectMissing(scheduler, invariant.schedulerSnippets);
|
||||
for (const snippet of missingScheduler) {
|
||||
errors.push(`[${invariant.name}] ${FILES.scheduler} missing checkpoint snippet: ${snippet}`);
|
||||
}
|
||||
|
||||
if (invariant.orderedSchedulerSnippets) {
|
||||
const order = ensureOrdered(scheduler, invariant.orderedSchedulerSnippets);
|
||||
if (!order.ok && order.missing) {
|
||||
errors.push(`[${invariant.name}] ${FILES.scheduler} missing ordered snippet: ${order.missing}`);
|
||||
} else if (!order.ok && order.outOfOrder) {
|
||||
errors.push(
|
||||
`[${invariant.name}] ${FILES.scheduler} ordering drift: "${order.outOfOrder[0]}" must appear before "${order.outOfOrder[1]}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('Scheduler flow parity drift detected:');
|
||||
for (const error of errors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Scheduler flow parity validated for prompts and scheduler implementation.');
|
||||
@@ -0,0 +1,163 @@
|
||||
import { readdir, readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
||||
const CONFIG = {
|
||||
daily: {
|
||||
schedulerFile: 'src/prompts/daily-scheduler.md',
|
||||
promptDir: 'src/prompts/daily',
|
||||
},
|
||||
weekly: {
|
||||
schedulerFile: 'src/prompts/weekly-scheduler.md',
|
||||
promptDir: 'src/prompts/weekly',
|
||||
},
|
||||
};
|
||||
|
||||
function parseTableRows(markdown, schedulerFile) {
|
||||
const rows = [];
|
||||
for (const line of markdown.split('\n')) {
|
||||
const match = line.match(/^\|\s*(\d+)\s*\|\s*([^|]+?)\s*\|\s*`([^`]+)`\s*\|\s*$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
rows.push({
|
||||
index: Number(match[1]),
|
||||
agent: match[2].trim(),
|
||||
promptFile: match[3].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
if (rows.length === 0) {
|
||||
throw new Error(`No scheduler table rows found in ${schedulerFile}`);
|
||||
}
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
function diffLists(expected, actual) {
|
||||
return {
|
||||
missing: expected.filter((item) => !actual.includes(item)),
|
||||
extra: actual.filter((item) => !expected.includes(item)),
|
||||
};
|
||||
}
|
||||
|
||||
function validateScheduler(cadence, roster, rows, promptFiles) {
|
||||
const errors = [];
|
||||
|
||||
if (rows.length !== roster.length) {
|
||||
errors.push(
|
||||
`[${cadence}] row count mismatch: roster has ${roster.length}, table has ${rows.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let i = 0; i < rows.length; i += 1) {
|
||||
const row = rows[i];
|
||||
const expectedIndex = i + 1;
|
||||
if (row.index !== expectedIndex) {
|
||||
errors.push(`[${cadence}] row ${i + 1} has index ${row.index}, expected ${expectedIndex}`);
|
||||
}
|
||||
|
||||
const expectedAgent = roster[i];
|
||||
if (expectedAgent && row.agent !== expectedAgent) {
|
||||
errors.push(
|
||||
`[${cadence}] row ${i + 1} agent mismatch: table has "${row.agent}", roster expects "${expectedAgent}"`,
|
||||
);
|
||||
}
|
||||
|
||||
const expectedPromptFile = `${row.agent}.md`;
|
||||
if (row.promptFile !== expectedPromptFile) {
|
||||
errors.push(
|
||||
`[${cadence}] row ${i + 1} prompt file mismatch: table has "${row.promptFile}", expected "${expectedPromptFile}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const tableAgents = rows.map((row) => row.agent);
|
||||
const rosterVsTable = diffLists(roster, tableAgents);
|
||||
if (rosterVsTable.missing.length > 0 || rosterVsTable.extra.length > 0) {
|
||||
errors.push(
|
||||
`[${cadence}] roster/table mismatch: missing in table [${rosterVsTable.missing.join(', ')}], extra in table [${rosterVsTable.extra.join(', ')}]`,
|
||||
);
|
||||
}
|
||||
|
||||
const rosterPromptFiles = roster.map((agent) => `${agent}.md`);
|
||||
const filesVsRoster = diffLists(rosterPromptFiles, promptFiles);
|
||||
if (filesVsRoster.missing.length > 0 || filesVsRoster.extra.length > 0) {
|
||||
errors.push(
|
||||
`[${cadence}] roster/files mismatch: missing files [${filesVsRoster.missing.join(', ')}], extra files [${filesVsRoster.extra.join(', ')}]`,
|
||||
);
|
||||
}
|
||||
|
||||
const filesVsTable = diffLists(promptFiles, rows.map((row) => row.promptFile));
|
||||
if (filesVsTable.missing.length > 0 || filesVsTable.extra.length > 0) {
|
||||
errors.push(
|
||||
`[${cadence}] table/files mismatch: missing in table [${filesVsTable.missing.join(', ')}], extra in table [${filesVsTable.extra.join(', ')}]`,
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
async function listMarkdownFiles(dir) {
|
||||
const entries = await readdir(dir, { withFileTypes: true });
|
||||
const files = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listMarkdownFiles(fullPath)));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.isFile() && entry.name.endsWith('.md')) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
async function validateCanonicalPromptPaths() {
|
||||
const errors = [];
|
||||
const markdownFiles = await listMarkdownFiles('src/prompts');
|
||||
const invalidPathPattern = /src\/src\/(context|todo|decisions|test_logs)\b/;
|
||||
|
||||
for (const file of markdownFiles) {
|
||||
const content = await readFile(file, 'utf8');
|
||||
if (!invalidPathPattern.test(content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
errors.push(
|
||||
`[prompt-paths] ${file} contains invalid canonical path references using "src/src/..."; expected "src/..."`,
|
||||
);
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
const roster = JSON.parse(await readFile('src/prompts/roster.json', 'utf8'));
|
||||
|
||||
const allErrors = [];
|
||||
|
||||
for (const [cadence, cfg] of Object.entries(CONFIG)) {
|
||||
const schedulerMd = await readFile(cfg.schedulerFile, 'utf8');
|
||||
const rows = parseTableRows(schedulerMd, cfg.schedulerFile);
|
||||
const promptFiles = (await readdir(cfg.promptDir))
|
||||
.filter((filename) => filename.endsWith('.md'))
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
allErrors.push(...validateScheduler(cadence, roster[cadence] ?? [], rows, promptFiles));
|
||||
}
|
||||
|
||||
allErrors.push(...(await validateCanonicalPromptPaths()));
|
||||
|
||||
if (allErrors.length > 0) {
|
||||
console.error('Scheduler/roster drift detected:');
|
||||
for (const error of allErrors) {
|
||||
console.error(`- ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Scheduler tables, roster, and prompt filenames are in sync.');
|
||||
Reference in new issue
Block a user