Install and setup TORCH in torch/ directory

- Added `torch-lock` dependency to `torch/package.json`
- Initialized TORCH structure using `cmdInit` (mocked interaction)
- Updated `torch/package.json` with TORCH CLI scripts (`lock:check`, `lock:lock`, etc.)
- Updated `torch/torch-config.json` with Nostr configuration
- Added `type: module` to `torch/package.json` to support ES modules
- Cleaned up extraneous directories (`test_logs`, `todo`, etc.) from initialization

Co-authored-by: PR0M3TH3AN <[email protected]>
This commit is contained in:
google-labs-jules[bot]andPR0M3TH3AN committed 2026-02-19 20:16:19 +00:00
1 parent 22cdb75b2b
commit 8761455bd4
170 files changed
+31085 -13

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);
});
+70
View File
@@ -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);
}
})();
+49
View File
@@ -0,0 +1,49 @@
import { chromium } from '@playwright/test';
(async () => {
const browser = await chromium.launch();
const page = await browser.newPage();
let errorCount = 0;
page.on('console', msg => {
const type = msg.type();
if (type === 'error' || type === 'warning') {
console.log(`CONSOLE [${type.toUpperCase()}]: ${msg.text()}`);
if (type === 'error') errorCount++;
}
});
page.on('pageerror', exception => {
console.log(`PAGE ERROR: ${exception}`);
errorCount++;
});
page.on('requestfailed', request => {
// Check if failure() is not null before accessing errorText
const failure = request.failure();
const errorText = failure ? failure.errorText : 'Unknown error';
console.log(`REQUEST FAILED: ${request.url()} - ${errorText}`);
errorCount++;
});
try {
console.log('Navigating to http://localhost:4173/dashboard/ ...');
await page.goto('http://localhost:4173/dashboard/');
console.log('Page loaded. Waiting 5s for any async errors...');
await page.waitForTimeout(5000);
} catch (e) {
console.error('Navigation failed:', e);
errorCount++;
}
await browser.close();
if (errorCount > 0) {
console.log(`\nFound ${errorCount} errors.`);
process.exit(1);
} else {
console.log('\nNo errors found.');
process.exit(0);
}
})();
+140
View File
@@ -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);
}
+201
View File
@@ -0,0 +1,201 @@
import WebSocket from 'ws';
import { useWebSocketImplementation } from 'nostr-tools/relay';
import { SimplePool } from 'nostr-tools/pool';
import { generateSecretKey, finalizeEvent } from 'nostr-tools/pure';
import fs from 'node:fs';
import path from 'node:path';
import { ensureDir } from '../../src/utils.mjs';
useWebSocketImplementation(WebSocket);
const RELAY_URL = process.env.RELAY_URL;
const CLIENTS = parseInt(process.env.CLIENTS || '10', 10);
const DURATION_SEC = parseInt(process.env.DURATION_SEC || '10', 10);
const RATE_EPS = parseInt(process.env.RATE_EPS || '5', 10);
const MIX = parseFloat(process.env.MIX || '0.1'); // 10% metadata
const DRY_RUN = process.env.DRY_RUN !== '0'; // Default to true for safety
const KIND_TEXT = 1;
const KIND_METADATA = 30078; // App Data as proxy for metadata
console.log('Load Test Configuration:');
console.log(` RELAY_URL: ${RELAY_URL || '(none)'}`);
console.log(` CLIENTS: ${CLIENTS}`);
console.log(` DURATION_SEC: ${DURATION_SEC}`);
console.log(` RATE_EPS: ${RATE_EPS}`);
console.log(` MIX: ${MIX}`);
console.log(` DRY_RUN: ${DRY_RUN}`);
if (!RELAY_URL && !DRY_RUN) {
console.error('ERROR: RELAY_URL is required unless DRY_RUN=1');
process.exit(1);
}
const stats = {
totalAttempts: 0,
success: 0,
failed: 0,
latencySum: 0,
latencyMin: Infinity,
latencyMax: 0,
errors: {},
startTime: Date.now(),
endTime: 0
};
async function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function generateEvent(sk) {
const isMetadata = Math.random() < MIX;
const kind = isMetadata ? KIND_METADATA : KIND_TEXT;
const content = isMetadata
? JSON.stringify({ type: 'video-metadata', title: 'Load Test Video', duration: 120, tags: ['test', 'load'] })
: `Load test view event ${Date.now()}`;
const tags = [['t', 'load-test']];
if (isMetadata) {
tags.push(['d', `load-test-${Date.now()}-${Math.random()}`]);
}
return finalizeEvent({
kind,
created_at: Math.floor(Date.now() / 1000),
tags,
content,
}, sk);
}
async function main() {
const reportDir = 'reports/load-test';
ensureDir(reportDir);
if (DRY_RUN) {
console.log('Starting DRY RUN...');
const start = Date.now();
let eventsGenerated = 0;
// Simple simulation loop
while (Date.now() - start < DURATION_SEC * 1000) {
eventsGenerated++;
if (eventsGenerated % RATE_EPS === 0) await sleep(1000);
}
stats.totalAttempts = eventsGenerated;
stats.success = eventsGenerated;
stats.endTime = Date.now();
stats.latencySum = eventsGenerated * 10; // Fake 10ms latency
stats.latencyMin = 5;
stats.latencyMax = 15;
writeReport(reportDir);
return;
}
const pool = new SimplePool();
const sks = Array.from({ length: CLIENTS }, () => generateSecretKey());
console.log(`Starting load test against ${RELAY_URL} for ${DURATION_SEC}s...`);
const start = Date.now();
const end = start + DURATION_SEC * 1000;
const interval = 1000 / RATE_EPS;
return new Promise((resolve) => {
const timer = setInterval(async () => {
if (Date.now() >= end) {
clearInterval(timer);
pool.close([RELAY_URL]);
stats.endTime = Date.now();
writeReport(reportDir);
resolve();
return;
}
const clientIdx = Math.floor(Math.random() * CLIENTS);
const sk = sks[clientIdx];
const event = generateEvent(sk);
stats.totalAttempts++;
const sendStart = Date.now();
try {
await Promise.any(pool.publish([RELAY_URL], event));
const latency = Date.now() - sendStart;
stats.success++;
stats.latencySum += latency;
stats.latencyMin = Math.min(stats.latencyMin, latency);
stats.latencyMax = Math.max(stats.latencyMax, latency);
} catch (err) {
stats.failed++;
const msg = err.message || String(err);
stats.errors[msg] = (stats.errors[msg] || 0) + 1;
}
}, interval);
});
}
function writeReport(dir) {
const duration = (stats.endTime - stats.startTime) / 1000;
const throughput = stats.success / duration;
const avgLatency = stats.success > 0 ? stats.latencySum / stats.success : 0;
const report = {
config: {
relayUrl: RELAY_URL,
clients: CLIENTS,
durationSec: DURATION_SEC,
rateEps: RATE_EPS,
mix: MIX,
dryRun: DRY_RUN
},
results: {
totalAttempts: stats.totalAttempts,
success: stats.success,
failed: stats.failed,
throughputEps: throughput,
latency: {
min: stats.latencyMin === Infinity ? 0 : stats.latencyMin,
max: stats.latencyMax,
avg: avgLatency
},
errors: stats.errors
},
timestamp: new Date().toISOString()
};
const dateStr = new Date().toISOString().slice(0, 10);
const jsonPath = path.join(dir, `load-report-${dateStr}.json`);
fs.writeFileSync(jsonPath, JSON.stringify(report, null, 2));
console.log(`JSON report written to ${jsonPath}`);
const mdPath = path.join(dir, `load-test-report-${dateStr}.md`);
const mdContent = `
# Load Test Report - ${dateStr}
## Configuration
- **Relay**: ${RELAY_URL || '(dry run)'}
- **Clients**: ${CLIENTS}
- **Duration**: ${DURATION_SEC}s
- **Rate**: ${RATE_EPS} eps
- **Mix**: ${MIX}
- **Dry Run**: ${DRY_RUN}
## Results
- **Total Attempts**: ${stats.totalAttempts}
- **Success**: ${stats.success}
- **Failed**: ${stats.failed}
- **Throughput**: ${throughput.toFixed(2)} events/sec
- **Latency (Avg)**: ${avgLatency.toFixed(2)} ms
- **Latency (Min/Max)**: ${stats.latencyMin === Infinity ? 0 : stats.latencyMin} / ${stats.latencyMax} ms
## Errors
\`\`\`json
${JSON.stringify(stats.errors, null, 2)}
\`\`\`
`;
fs.writeFileSync(mdPath, mdContent.trim());
console.log(`Markdown report written to ${mdPath}`);
}
main().catch(console.error);
+790
View File
@@ -0,0 +1,790 @@
#!/usr/bin/env node
// Source of truth: numbered MUST steps 2 and 4-14 in src/prompts/scheduler-flow.md are
// implemented by this script; step 3 (policy-file read) is best-effort and non-fatal.
import fs from 'node:fs/promises';
import path from 'node:path';
import { randomUUID } from 'node:crypto';
import {
runCommand,
parseJsonFromOutput,
readJson,
normalizeStringList,
parseNonNegativeInt,
parseBooleanFlag,
excerptText,
getRunDateKey,
toYamlScalar,
} from './scheduler-utils.mjs';
import {
classifyLockBackendError,
buildLockBackendRemediation,
runLockHealthPreflight,
acquireLockWithRetry,
summarizeLockFailureReasons,
} from './scheduler-lock.mjs';
import { detectPlatform } from '../../src/utils.mjs';
const VALID_CADENCES = new Set(['daily', 'weekly']);
const ALL_EXCLUDED_REASON = 'All roster tasks currently claimed by other agents';
const FAILURE_CATEGORY = {
PROMPT_PARSE: 'prompt_parse_error',
PROMPT_SCHEMA: 'prompt_schema_error',
LOCK_BACKEND: 'lock_backend_error',
EXECUTION: 'execution_error',
};
function parseArgs(argv) {
const args = { cadence: null, platform: process.env.AGENT_PLATFORM || detectPlatform() || 'unknown', model: process.env.AGENT_MODEL || null };
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] || null;
i += 1;
continue;
}
if (value === '--platform') {
args.platform = argv[i + 1] || args.platform;
i += 1;
}
if (value === '--model') {
args.model = argv[i + 1] || args.model;
i += 1;
}
}
return args;
}
function ts() {
return new Date().toISOString().replace(/\.\d{3}Z$/, 'Z').replace(/:/g, '-');
}
function categorizeFailureMetadata(failureCategory, extraMetadata = {}) {
return {
failure_category: failureCategory,
...extraMetadata,
};
}
async function validatePromptFile(promptPath) {
let content;
try {
content = await fs.readFile(promptPath, 'utf8');
} catch (error) {
return {
ok: false,
category: FAILURE_CATEGORY.PROMPT_PARSE,
reason: 'Prompt file parse/read failed',
detail: `Prompt not executed; unable to read prompt file at ${promptPath}: ${error.message}`,
};
}
const lines = String(content).split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
const firstLine = lines[0] || '';
if (!firstLine.startsWith('#') && !firstLine.startsWith('>')) {
return {
ok: false,
category: FAILURE_CATEGORY.PROMPT_SCHEMA,
reason: 'Prompt file schema validation failed',
detail: `Prompt not executed; expected markdown heading or blockquote on first non-empty line in ${promptPath}.`,
};
}
return { ok: true };
}
async function artifactExistsSince(filePath, sinceMs) {
if (!filePath) return false;
const resolved = path.isAbsolute(filePath)
? filePath
: path.resolve(process.cwd(), filePath);
try {
const stat = await fs.stat(resolved);
return stat.isFile() && stat.mtimeMs >= sinceMs;
} catch {
return false;
}
}
async function verifyMemoryStep({ name, markers, artifacts, outputText, sinceMs }) {
const markerMatched = markers.some((marker) => outputText.includes(marker));
const artifactResults = await Promise.all(artifacts.map((artifact) => artifactExistsSince(artifact, sinceMs)));
const artifactMatched = artifactResults.some(Boolean);
return {
name,
markerMatched,
artifactMatched,
complete: markerMatched || artifactMatched,
};
}
function parseFrontmatterAgent(markdown) {
const lines = markdown.split(/\r?\n/);
if (lines[0]?.trim() !== '---') return null;
for (let i = 1; i < lines.length; i += 1) {
const line = lines[i];
if (line.trim() === '---') break;
const match = line.match(/^agent\s*:\s*(.+)$/i);
if (match?.[1]) {
return match[1].trim().replace(/^['"]|['"]$/g, '');
}
}
return null;
}
function parseFrontmatterCreatedAt(markdown) {
const lines = markdown.split(/\r?\n/);
if (lines[0]?.trim() !== '---') return null;
for (let i = 1; i < lines.length; i += 1) {
const line = lines[i];
if (line.trim() === '---') break;
const match = line.match(/^created_at\s*:\s*(.+)$/i);
if (match?.[1]) {
return match[1].trim().replace(/^['"]|['"]$/g, '');
}
}
return null;
}
function parseDateValue(value) {
if (!value || typeof value !== 'string') return null;
const ms = Date.parse(value);
return Number.isFinite(ms) ? ms : null;
}
function parseTimestampFromFilename(filename) {
const match = String(filename).match(/^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/);
if (!match?.[1]) return null;
const iso = match[1].replace(/T(\d{2})-(\d{2})-(\d{2})Z$/, 'T$1:$2:$3Z');
return parseDateValue(iso);
}
function isStrictSchedulerLogFilename(filename) {
return /^(\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z)__[^_]+__(completed|failed)\.md$/.test(String(filename));
}
function parseAgentFromFilename(filename) {
const match = String(filename).match(/^.+__([^_]+?)__(completed|failed)\.md$/);
return match?.[1] || null;
}
async function getLatestFile(logDir) {
await fs.mkdir(logDir, { recursive: true });
const entries = await fs.readdir(logDir, { withFileTypes: true });
const files = entries
.filter((entry) => entry.isFile())
.map((entry) => entry.name)
.filter((filename) => isStrictSchedulerLogFilename(filename))
.sort((a, b) => b.localeCompare(a));
const results = await Promise.all(
files.map(async (filename) => {
const filePath = path.join(logDir, filename);
const content = await fs.readFile(filePath, 'utf8').catch(() => '');
const createdAtMs = parseDateValue(parseFrontmatterCreatedAt(content));
const fileTimestampMs = parseTimestampFromFilename(filename);
const effectiveMs = createdAtMs ?? fileTimestampMs;
if (!effectiveMs) {
console.warn(`[scheduler] Ignoring invalid log timestamp in ${filename}; checking next candidate.`);
return null;
}
return { filename, effectiveMs };
}),
);
let latest = null;
for (const result of results) {
if (!result) continue;
if (!latest || result.effectiveMs > latest.effectiveMs) {
latest = result;
}
}
return latest?.filename || null;
}
function selectNextAgent({ roster, excludedSet, previousAgent, firstPrompt }) {
const previousIndex = roster.indexOf(previousAgent);
const firstIndex = roster.indexOf(firstPrompt);
const startIndex = previousIndex >= 0
? (previousIndex + 1) % roster.length
: (firstIndex >= 0 ? firstIndex : 0);
for (let offset = 0; offset < roster.length; offset += 1) {
const candidate = roster[(startIndex + offset) % roster.length];
if (!excludedSet.has(candidate)) {
return candidate;
}
}
return null;
}
async function getSchedulerConfig(cadence, { isInteractive }) {
const cfg = await readJson(path.resolve(process.cwd(), 'torch-config.json'), {});
const scheduler = cfg.scheduler || {};
const handoffCommand = scheduler.handoffCommandByCadence?.[cadence] || null;
const missingHandoffCommandForMode = !isInteractive && !handoffCommand;
const memoryPolicyRaw = scheduler.memoryPolicyByCadence?.[cadence] || {};
const mode = memoryPolicyRaw.mode === 'required' ? 'required' : 'optional';
const lockRetryRaw = scheduler.lockRetry || {};
const maxRetries = parseNonNegativeInt(
process.env.SCHEDULER_LOCK_MAX_RETRIES,
parseNonNegativeInt(lockRetryRaw.maxRetries, 2),
);
const backoffMs = parseNonNegativeInt(
process.env.SCHEDULER_LOCK_BACKOFF_MS,
parseNonNegativeInt(lockRetryRaw.backoffMs, 250),
);
const jitterMs = parseNonNegativeInt(
process.env.SCHEDULER_LOCK_JITTER_MS,
parseNonNegativeInt(lockRetryRaw.jitterMs, 75),
);
const lockHealthPreflightFromConfig = parseBooleanFlag(scheduler.lockHealthPreflight, false);
const lockHealthPreflightEnabled = parseBooleanFlag(
process.env.SCHEDULER_LOCK_HEALTH_PREFLIGHT,
lockHealthPreflightFromConfig,
);
const lockHealthPreflightSkip = parseBooleanFlag(process.env.SCHEDULER_SKIP_LOCK_HEALTH_PREFLIGHT, false);
const strictLock = parseBooleanFlag(
process.env.SCHEDULER_STRICT_LOCK,
parseBooleanFlag(scheduler.strict_lock, true),
);
const degradedLockRetryWindowMs = parseNonNegativeInt(
process.env.SCHEDULER_DEGRADED_LOCK_RETRY_WINDOW_MS,
parseNonNegativeInt(scheduler.degraded_lock_retry_window, 3600000),
);
const maxDeferrals = parseNonNegativeInt(
process.env.SCHEDULER_MAX_DEFERRALS,
parseNonNegativeInt(scheduler.max_deferrals, 3),
);
return {
firstPrompt: scheduler.firstPromptByCadence?.[cadence] || null,
handoffCommand,
missingHandoffCommandForMode,
validationCommands: Array.isArray(scheduler.validationCommandsByCadence?.[cadence])
? scheduler.validationCommandsByCadence[cadence].filter((cmd) => typeof cmd === 'string' && cmd.trim())
: ['npm run lint'],
lockRetry: {
maxRetries,
backoffMs,
jitterMs,
},
lockHealthPreflight: {
enabled: lockHealthPreflightEnabled,
skip: lockHealthPreflightSkip,
},
lockFailurePolicy: {
strictLock,
degradedLockRetryWindowMs,
maxDeferrals,
},
memoryPolicy: {
mode,
retrieveCommand: typeof memoryPolicyRaw.retrieveCommand === 'string' && memoryPolicyRaw.retrieveCommand.trim()
? memoryPolicyRaw.retrieveCommand.trim()
: null,
storeCommand: typeof memoryPolicyRaw.storeCommand === 'string' && memoryPolicyRaw.storeCommand.trim()
? memoryPolicyRaw.storeCommand.trim()
: null,
retrieveSuccessMarkers: normalizeStringList(memoryPolicyRaw.retrieveSuccessMarkers, ['MEMORY_RETRIEVED']),
storeSuccessMarkers: normalizeStringList(memoryPolicyRaw.storeSuccessMarkers, ['MEMORY_STORED']),
retrieveArtifacts: normalizeStringList(memoryPolicyRaw.retrieveArtifacts),
storeArtifacts: normalizeStringList(memoryPolicyRaw.storeArtifacts),
},
};
}
async function readRunState(cadence) {
const statePath = path.resolve(process.cwd(), 'task-logs', cadence, '.scheduler-run-state.json');
const fallback = { run_date: getRunDateKey(), lock_deferral: null };
const raw = await readJson(statePath, fallback);
if (!raw || typeof raw !== 'object') {
return { statePath, state: fallback };
}
if (raw.run_date !== getRunDateKey()) {
return { statePath, state: fallback };
}
return {
statePath,
state: {
run_date: raw.run_date,
lock_deferral: raw.lock_deferral && typeof raw.lock_deferral === 'object' ? raw.lock_deferral : null,
},
};
}
async function writeRunState(statePath, state) {
await fs.mkdir(path.dirname(statePath), { recursive: true });
await fs.writeFile(statePath, `${JSON.stringify(state, null, 2)}\n`, 'utf8');
}
function createIdempotencyKey({ cadence, selectedAgent, runDate }) {
return `${cadence}:${selectedAgent}:${runDate}:${randomUUID()}`;
}
async function writeLog({ cadence, agent, status, reason, detail, platform, metadata = {} }) {
const logDir = path.resolve(process.cwd(), 'task-logs', cadence);
await fs.mkdir(logDir, { recursive: true });
const file = `${ts()}__${agent}__${status}.md`;
const mergedMetadata = {
platform: platform || process.env.AGENT_PLATFORM || 'unknown',
...metadata,
};
const body = [
'---',
`cadence: ${cadence}`,
`agent: ${agent}`,
`status: ${status}`,
`reason: ${toYamlScalar(reason)}`,
detail ? `detail: ${toYamlScalar(detail)}` : null,
`created_at: ${new Date().toISOString()}`,
`timestamp: ${new Date().toISOString()}`,
...Object.entries(mergedMetadata)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `${key}: ${toYamlScalar(value)}`),
'---',
'',
`# Scheduler ${status}`,
'',
`- reason: ${reason}`,
detail ? `- detail: ${detail}` : null,
...Object.entries(mergedMetadata)
.filter(([, value]) => value !== undefined && value !== null && value !== '')
.map(([key, value]) => `- ${key}: ${value}`),
'',
].filter(Boolean).join('\n');
await fs.writeFile(path.join(logDir, file), body, 'utf8');
return file;
}
async function main() {
const { cadence, platform, model } = parseArgs(process.argv.slice(2));
if (!VALID_CADENCES.has(cadence)) {
console.error('Usage: node scripts/agent/run-scheduler-cycle.mjs <daily|weekly>');
process.exit(1);
}
const roster = (await readJson(path.resolve(process.cwd(), 'src/prompts/roster.json'), {}))[cadence] || [];
if (!Array.isArray(roster) || roster.length === 0) {
console.error(`No roster entries for cadence ${cadence}`);
process.exit(1);
}
const isInteractive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
const schedulerConfig = await getSchedulerConfig(cadence, { isInteractive });
const logDir = path.resolve(process.cwd(), 'task-logs', cadence);
const agentsPath = path.resolve(process.cwd(), 'AGENTS.md');
try {
const agentsContent = await fs.readFile(agentsPath, 'utf8');
process.stdout.write(`${agentsContent}\n`);
} catch {
console.log('No AGENTS.md found; continuing');
}
while (true) {
const { statePath: runStatePath, state: schedulerRunState } = await readRunState(cadence);
if (schedulerConfig.lockHealthPreflight.enabled && !schedulerConfig.lockHealthPreflight.skip) {
const preflight = await runLockHealthPreflight({ cadence, platform });
if (preflight.code !== 0) {
const allRelaysUnhealthy = Boolean(preflight.payload?.summary?.allRelaysUnhealthy);
const incidentSignal = preflight.payload?.incidentSignal || null;
await writeLog({
cadence,
agent: 'scheduler',
status: allRelaysUnhealthy ? 'deferred' : 'failed',
platform,
reason: allRelaysUnhealthy
? 'All relays unhealthy preflight'
: 'Lock backend unavailable preflight',
detail: allRelaysUnhealthy
? `Deferred run before lock acquisition: ${incidentSignal?.reason || 'all relays unhealthy'}. Prompt not executed. ${buildLockBackendRemediation({ cadence, retryWindowMs: schedulerConfig.lockFailurePolicy.degradedLockRetryWindowMs, maxDeferrals: schedulerConfig.lockFailurePolicy.maxDeferrals, incidentSignalId: incidentSignal?.id || null })}`
: `Preflight failed (${preflight.failureCategory}). Prompt not executed. ${buildLockBackendRemediation({ cadence, retryWindowMs: schedulerConfig.lockFailurePolicy.degradedLockRetryWindowMs, maxDeferrals: schedulerConfig.lockFailurePolicy.maxDeferrals, incidentSignalId: incidentSignal?.id || null })}`,
metadata: {
...categorizeFailureMetadata(FAILURE_CATEGORY.LOCK_BACKEND, { failure_class: 'backend_unavailable' }),
preflight_failure_category: preflight.failureCategory,
relay_list: preflight.relayList.join(', ') || '(none)',
preflight_stderr_excerpt: preflight.stderrExcerpt || '(empty)',
preflight_stdout_excerpt: preflight.stdoutExcerpt || '(empty)',
incident_signal_id: incidentSignal?.id || null,
incident_signal_severity: incidentSignal?.severity || null,
preflight_alerts: JSON.stringify(preflight.payload?.alerts || []),
relay_health_history_path: preflight.payload?.historyPath || null,
},
});
process.exit(allRelaysUnhealthy ? 0 : (preflight.code || 1));
}
}
const checkResult = await runCommand('npm', ['run', `lock:check:${cadence}`, '--', '--json', '--quiet']);
const checkPayload = parseJsonFromOutput(`${checkResult.stdout}\n${checkResult.stderr}`) || {};
const excluded = Array.isArray(checkPayload.excluded)
? checkPayload.excluded
: [...new Set([
...(Array.isArray(checkPayload.locked) ? checkPayload.locked : []),
...(Array.isArray(checkPayload.paused) ? checkPayload.paused : []),
...(Array.isArray(checkPayload.completed) ? checkPayload.completed : []),
])];
const excludedSet = new Set(excluded);
const latestFile = await getLatestFile(logDir);
let previousAgent = null;
if (latestFile) {
const latestPath = path.join(logDir, latestFile);
const content = await fs.readFile(latestPath, 'utf8').catch(() => '');
previousAgent = parseFrontmatterAgent(content) || parseAgentFromFilename(latestFile);
if (!roster.includes(previousAgent)) {
previousAgent = null;
}
}
const selectedAgent = selectNextAgent({
roster,
excludedSet,
previousAgent,
firstPrompt: schedulerConfig.firstPrompt,
});
if (!selectedAgent) {
await writeLog({
cadence,
agent: 'scheduler',
status: 'failed',
platform,
reason: ALL_EXCLUDED_REASON,
});
process.exit(1);
}
const deferralForAgent = schedulerRunState.lock_deferral?.selected_agent === selectedAgent
? schedulerRunState.lock_deferral
: null;
const lockAttempt = await acquireLockWithRetry({
selectedAgent,
cadence,
platform,
model,
lockRetry: schedulerConfig.lockRetry,
idempotencyKey: deferralForAgent?.idempotency_key,
});
const lockResult = lockAttempt.result;
if (lockResult.code === 3) {
continue;
}
if (lockResult.code === 2) {
const combinedLockOutput = `${lockResult.stderr}\n${lockResult.stdout}`;
const diagnosticsSummary = summarizeLockFailureReasons(combinedLockOutput);
const backendCategory = lockAttempt.finalBackendCategory || classifyLockBackendError(combinedLockOutput);
const modelPart = model ? ` --model ${model}` : '';
const lockCommand = `AGENT_PLATFORM=${platform} npm run lock:lock -- --agent ${selectedAgent} --cadence ${cadence}${modelPart}`;
const stderrExcerpt = excerptText(lockResult.stderr);
const stdoutExcerpt = excerptText(lockResult.stdout);
const backoffSchedule = lockAttempt.backoffScheduleMs.join(', ');
const existingDeferral = deferralForAgent || {};
const firstFailureAt = existingDeferral.first_failure_timestamp || new Date().toISOString();
const firstFailureMs = parseDateValue(firstFailureAt) || Date.now();
const nowMs = Date.now();
const deferralAttemptCount = parseNonNegativeInt(existingDeferral.attempt_count, 0) + 1;
const idempotencyKey = existingDeferral.idempotency_key
|| createIdempotencyKey({ cadence, selectedAgent, runDate: schedulerRunState.run_date });
const withinRetryWindow = nowMs - firstFailureMs <= schedulerConfig.lockFailurePolicy.degradedLockRetryWindowMs;
const withinDeferralBudget = deferralAttemptCount <= schedulerConfig.lockFailurePolicy.maxDeferrals;
if (!schedulerConfig.lockFailurePolicy.strictLock && withinRetryWindow && withinDeferralBudget) {
schedulerRunState.lock_deferral = {
attempt_count: deferralAttemptCount,
first_failure_timestamp: firstFailureAt,
backend_category: backendCategory,
idempotency_key: idempotencyKey,
selected_agent: selectedAgent,
};
await writeRunState(runStatePath, schedulerRunState);
await writeLog({
cadence,
agent: selectedAgent,
status: 'deferred',
platform,
reason: 'Lock backend deferred',
detail: `Deferred after lock backend failure (${backendCategory}); retry window active and deferral budget remaining. Prompt not executed. ${buildLockBackendRemediation({ cadence, retryWindowMs: schedulerConfig.lockFailurePolicy.degradedLockRetryWindowMs, maxDeferrals: schedulerConfig.lockFailurePolicy.maxDeferrals })}`,
metadata: {
...categorizeFailureMetadata(FAILURE_CATEGORY.LOCK_BACKEND, { failure_class: 'backend_unavailable' }),
deferral_attempt_count: deferralAttemptCount,
deferral_first_failure_timestamp: firstFailureAt,
backend_category: backendCategory,
lock_idempotency_key: idempotencyKey,
},
});
process.exit(0);
}
schedulerRunState.lock_deferral = null;
await writeRunState(runStatePath, schedulerRunState);
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Lock backend error',
detail: `Lock backend error (${backendCategory}) after ${lockAttempt.attempts} attempt(s). Prompt not executed. ${buildLockBackendRemediation({ cadence, retryWindowMs: schedulerConfig.lockFailurePolicy.degradedLockRetryWindowMs, maxDeferrals: schedulerConfig.lockFailurePolicy.maxDeferrals })}`,
metadata: {
...categorizeFailureMetadata(FAILURE_CATEGORY.LOCK_BACKEND, { failure_class: 'backend_unavailable' }),
lock_attempts_total: lockAttempt.attempts,
lock_backoff_schedule_ms: backoffSchedule || '(none)',
lock_correlation_id: diagnosticsSummary.correlationId || lockAttempt.correlationId,
lock_attempt_id: diagnosticsSummary.attemptId || String(lockAttempt.attempts),
lock_total_retry_timeline_ms: diagnosticsSummary.totalElapsedMs,
lock_failure_reason_distribution: JSON.stringify(diagnosticsSummary.reasonDistribution),
backend_category: backendCategory,
deferral_attempt_count: deferralAttemptCount,
deferral_first_failure_timestamp: firstFailureAt,
lock_command: lockCommand,
lock_idempotency_key: idempotencyKey,
lock_stderr_excerpt: stderrExcerpt || '(empty)',
lock_stdout_excerpt: stdoutExcerpt || '(empty)',
},
});
process.exit(2);
}
if (lockResult.code !== 0) {
schedulerRunState.lock_deferral = null;
await writeRunState(runStatePath, schedulerRunState);
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Failed to acquire lock',
});
process.exit(lockResult.code);
}
const promptPath = path.resolve(process.cwd(), 'src/prompts', cadence, `${selectedAgent}.md`);
const promptValidation = await validatePromptFile(promptPath);
if (!promptValidation.ok) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: promptValidation.reason,
detail: promptValidation.detail,
metadata: categorizeFailureMetadata(promptValidation.category, { prompt_path: promptPath }),
});
process.exit(1);
}
schedulerRunState.lock_deferral = null;
await writeRunState(runStatePath, schedulerRunState);
const runArtifactSince = new Date().toISOString();
const runStartMs = Date.parse(runArtifactSince);
const outputChunks = [];
const memoryDir = path.resolve(process.cwd(), 'memory-updates');
await fs.mkdir(memoryDir, { recursive: true });
const memoryFile = path.join(memoryDir, `${ts()}__${selectedAgent}.md`);
const schedulerEnv = {
AGENT_PLATFORM: platform,
...(model ? { AGENT_MODEL: model } : {}),
SCHEDULER_AGENT: selectedAgent,
SCHEDULER_CADENCE: cadence,
SCHEDULER_PROMPT_PATH: promptPath,
SCHEDULER_MEMORY_FILE: memoryFile,
};
if (schedulerConfig.memoryPolicy.retrieveCommand) {
const retrieveResult = await runCommand('bash', ['-lc', schedulerConfig.memoryPolicy.retrieveCommand], {
env: schedulerEnv,
});
outputChunks.push(retrieveResult.stdout, retrieveResult.stderr);
if (retrieveResult.code !== 0) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Memory retrieval command failed',
detail: schedulerConfig.memoryPolicy.retrieveCommand,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.EXECUTION, { failure_class: 'prompt_validation_error' }),
});
process.exit(retrieveResult.code);
}
}
if (schedulerConfig.handoffCommand) {
const handoff = await runCommand('bash', ['-lc', schedulerConfig.handoffCommand], {
env: schedulerEnv,
});
outputChunks.push(handoff.stdout, handoff.stderr);
if (handoff.code !== 0) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Prompt/handoff execution failed',
detail: 'Handoff callback failed.',
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.EXECUTION, { failure_class: 'prompt_validation_error' }),
});
process.exit(handoff.code);
}
} else {
const detail = schedulerConfig.missingHandoffCommandForMode
? 'Missing scheduler handoff command for non-interactive run. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.'
: 'No handoff callback configured. Set scheduler.handoffCommandByCadence.daily|weekly in torch-config.json.';
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Prompt/handoff execution failed',
detail,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.EXECUTION, { failure_class: 'prompt_validation_error' }),
});
process.exit(1);
}
if (schedulerConfig.memoryPolicy.storeCommand) {
const storeResult = await runCommand('bash', ['-lc', schedulerConfig.memoryPolicy.storeCommand], {
env: schedulerEnv,
});
outputChunks.push(storeResult.stdout, storeResult.stderr);
if (storeResult.code !== 0) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Memory storage command failed',
detail: schedulerConfig.memoryPolicy.storeCommand,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.EXECUTION, { failure_class: 'prompt_validation_error' }),
});
process.exit(storeResult.code);
}
}
const memoryOutput = outputChunks.join('\n');
const retrieveCheck = await verifyMemoryStep({
name: 'retrieve',
markers: schedulerConfig.memoryPolicy.retrieveSuccessMarkers,
artifacts: schedulerConfig.memoryPolicy.retrieveArtifacts,
outputText: memoryOutput,
sinceMs: runStartMs,
});
const storeCheck = await verifyMemoryStep({
name: 'store',
markers: schedulerConfig.memoryPolicy.storeSuccessMarkers,
artifacts: schedulerConfig.memoryPolicy.storeArtifacts,
outputText: memoryOutput,
sinceMs: runStartMs,
});
const missingSteps = [retrieveCheck, storeCheck].filter((step) => !step.complete).map((step) => step.name);
if (missingSteps.length && schedulerConfig.memoryPolicy.mode === 'required') {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Required memory steps not verified',
detail: `Missing evidence for: ${missingSteps.join(', ')}`,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.PROMPT_SCHEMA, { failure_class: 'prompt_validation_error' }),
});
process.exit(1);
}
if (missingSteps.length) {
console.warn(`[scheduler] Optional memory evidence missing for ${missingSteps.join(', ')}.`);
}
const artifactCheck = await runCommand('node', [
'scripts/agent/verify-run-artifacts.mjs',
'--since',
runArtifactSince,
'--agent',
selectedAgent,
'--cadence',
cadence,
'--prompt-path',
promptPath,
'--run-start',
runArtifactSince,
'--check-failure-notes',
]);
if (artifactCheck.code !== 0) {
const detail = artifactCheck.stderr.trim() || artifactCheck.stdout.trim() || 'Artifact verification failed.';
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Missing required run artifacts',
detail,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.PROMPT_SCHEMA, { failure_class: 'prompt_validation_error' }),
});
process.exit(artifactCheck.code);
}
for (const validation of schedulerConfig.validationCommands) {
const parts = validation.split(' ').filter(Boolean);
if (!parts.length) continue;
const result = await runCommand(parts[0], parts.slice(1));
if (result.code !== 0) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: 'Validation failed',
detail: validation,
metadata: categorizeFailureMetadata(FAILURE_CATEGORY.EXECUTION, { failure_class: 'prompt_validation_error' }),
});
process.exit(result.code);
}
}
const completeResult = await runCommand(
'npm',
['run', 'lock:complete', '--', '--agent', selectedAgent, '--cadence', cadence, ...(model ? ['--model', model] : [])],
{ env: { AGENT_PLATFORM: platform, ...(model ? { AGENT_MODEL: model } : {}) } },
);
if (completeResult.code !== 0) {
await writeLog({
cadence,
agent: selectedAgent,
status: 'failed',
platform,
reason: `Completion publish failed. Retry npm run lock:complete -- --agent ${selectedAgent} --cadence ${cadence} after verifying relay connectivity`,
});
process.exit(completeResult.code);
}
await writeLog({ cadence, agent: selectedAgent, status: 'completed', platform, reason: 'Scheduler cycle completed successfully' });
process.exit(0);
}
}
main().catch(async (err) => {
console.error(err);
process.exit(1);
});
@@ -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();
+173
View File
@@ -0,0 +1,173 @@
import {
runCommand,
parseJsonFromOutput,
parseJsonEventsFromOutput,
excerptText,
formatDurationMs,
getRunDateKey,
sleep,
} from './scheduler-utils.mjs';
const LOCK_INCIDENT_LINK = 'docs/agent-handoffs/learnings/2026-02-15-relay-health-preflight-job.md';
export function classifyLockBackendError(outputText) {
const text = String(outputText || '').toLowerCase();
if (!text.trim()) return 'unknown_backend_error';
const errorCategoryMatch = text.match(/error_category=([a-z0-9_]+)/);
if (errorCategoryMatch?.[1]) {
return errorCategoryMatch[1];
}
if ((text.includes('relay') || text.includes('query')) && text.includes('timeout')) {
return 'relay_query_timeout';
}
if (text.includes('publish failed to all relays') || text.includes('failed to publish to any relay')) {
return 'relay_publish_quorum_failure';
}
if (
text.includes('connection refused')
|| text.includes('econnrefused')
|| text.includes('getaddrinfo')
|| text.includes('enotfound')
|| text.includes('eai_again')
|| (text.includes('websocket') && text.includes('dns'))
) {
return 'websocket_connection_refused_or_dns';
}
if (
text.includes('invalid url')
|| text.includes('malformed')
|| text.includes('unsupported protocol')
|| text.includes('must start with ws')
|| text.includes('invalid relay')
) {
return 'malformed_relay_url_config';
}
return 'unknown_backend_error';
}
export function buildLockBackendRemediation({ cadence, retryWindowMs, maxDeferrals, incidentSignalId = null }) {
const steps = [
'Likely backend/relay connectivity issue during lock acquisition.',
`Retry window: ${formatDurationMs(retryWindowMs)} (max deferrals: ${maxDeferrals}).`,
`Retry command: npm run scheduler:${cadence}`,
`Run health check: npm run lock:health -- --cadence ${cadence}`,
`Review incident runbook: ${LOCK_INCIDENT_LINK}`,
];
if (incidentSignalId) {
steps.push(`Incident signal: ${incidentSignalId}`);
}
return `Recommended auto-remediation: ${steps.join(' ')}`;
}
export async function runLockHealthPreflight({ cadence, platform }) {
const preflight = await runCommand('npm', ['run', 'lock:health', '--', '--cadence', cadence], {
env: { AGENT_PLATFORM: platform },
});
const payload = parseJsonFromOutput(`${preflight.stdout}\n${preflight.stderr}`) || {};
const relayList = Array.isArray(payload.relays)
? payload.relays.filter((relay) => typeof relay === 'string' && relay.trim()).map((relay) => relay.trim())
: [];
const combinedOutput = `${preflight.stderr}\n${preflight.stdout}`;
const failureCategory = payload.failureCategory || classifyLockBackendError(combinedOutput);
return {
code: preflight.code,
payload,
relayList,
failureCategory,
stderrExcerpt: excerptText(preflight.stderr),
stdoutExcerpt: excerptText(preflight.stdout),
};
}
export async function acquireLockWithRetry({ selectedAgent, cadence, platform, model, lockRetry, idempotencyKey }) {
const lockCommandArgs = ['run', 'lock:lock', '--', '--agent', selectedAgent, '--cadence', cadence];
if (model) {
lockCommandArgs.push('--model', model);
}
const backoffScheduleMs = [];
let attempts = 0;
const correlationId = idempotencyKey || `${cadence}:${selectedAgent}:${getRunDateKey()}`;
while (true) {
attempts += 1;
const result = await runCommand('npm', lockCommandArgs, {
env: {
AGENT_PLATFORM: platform,
...(model ? { AGENT_MODEL: model } : {}),
...(idempotencyKey ? { SCHEDULER_LOCK_IDEMPOTENCY_KEY: idempotencyKey } : {}),
SCHEDULER_LOCK_CORRELATION_ID: correlationId,
SCHEDULER_LOCK_ATTEMPT_ID: String(attempts),
},
});
if (result.code !== 2) {
return { result, attempts, backoffScheduleMs, correlationId };
}
if (attempts > lockRetry.maxRetries) {
return {
result,
attempts,
backoffScheduleMs,
correlationId,
finalBackendCategory: classifyLockBackendError(`${result.stderr}\n${result.stdout}`),
};
}
const exponentialBase = lockRetry.backoffMs * (2 ** (attempts - 1));
const jitter = lockRetry.jitterMs > 0
? Math.floor(Math.random() * (lockRetry.jitterMs + 1))
: 0;
const delayMs = exponentialBase + jitter;
backoffScheduleMs.push(delayMs);
console.log(JSON.stringify({
event: 'scheduler.lock.retry',
attempt: attempts,
max_retries: lockRetry.maxRetries,
delay_ms: delayMs,
correlation_id: correlationId,
selected_agent: selectedAgent,
cadence,
}));
if (delayMs > 0) {
await sleep(delayMs);
}
}
}
export function summarizeLockFailureReasons(outputText) {
const events = parseJsonEventsFromOutput(outputText);
const quorumFailure = events.findLast((entry) => entry.event === 'lock_publish_quorum_failed');
if (quorumFailure && quorumFailure.reasonDistribution && typeof quorumFailure.reasonDistribution === 'object') {
return {
reasonDistribution: quorumFailure.reasonDistribution,
attemptId: quorumFailure.attemptId || null,
correlationId: quorumFailure.correlationId || null,
totalElapsedMs: quorumFailure.totalElapsedMs ?? null,
};
}
const distribution = {};
for (const entry of events) {
if (entry.event !== 'lock_publish_failure') continue;
const reason = typeof entry.reason === 'string' && entry.reason.trim()
? entry.reason.trim()
: 'unknown';
distribution[reason] = (distribution[reason] || 0) + 1;
}
return {
reasonDistribution: distribution,
attemptId: null,
correlationId: null,
totalElapsedMs: null,
};
}
+200
View File
@@ -0,0 +1,200 @@
import fs from 'node:fs/promises';
import { spawn } from 'node:child_process';
const ALLOWED_ENV_KEYS = new Set([
'PATH', 'Path', // Windows compatibility
'HOME', 'USER', 'SHELL', 'TERM', 'LANG', 'LC_ALL', 'LC_CTYPE', 'TZ',
'NODE_ENV', 'NODE_OPTIONS', 'NODE_PATH',
'APPDATA', 'LOCALAPPDATA', 'PROGRAMDATA', 'SystemRoot', 'windir', 'ComSpec', 'PATHEXT',
'TMPDIR', 'TEMP', 'TMP',
'EDITOR', 'VISUAL',
// CI variables
'CI', 'GITHUB_ACTIONS', 'GITHUB_REF', 'GITHUB_HEAD_REF', 'GITHUB_BASE_REF', 'GITHUB_EVENT_NAME', 'GITHUB_SHA',
]);
export function getSafeEnv() {
const safeEnv = {};
for (const key in process.env) {
if (ALLOWED_ENV_KEYS.has(key) ||
key.startsWith('npm_') ||
key.startsWith('NOSTR_') ||
key.startsWith('TORCH_') ||
key.startsWith('SCHEDULER_') ||
key.startsWith('AGENT_') ||
key.startsWith('JULES_') ||
key.startsWith('CODEX_') ||
key.startsWith('CLAUDE_') ||
key.startsWith('ANTHROPIC_') ||
key.startsWith('GOOSE_')) {
safeEnv[key] = process.env[key];
}
}
return safeEnv;
}
/**
* Spawns a child process with a sanitized environment by default.
* @param {string} command
* @param {string[]} args
* @param {object} options
* @param {object} [options.env] - Additional environment variables to merge.
* @param {boolean} [options.inheritProcessEnv=false] - If true, use full process.env. If false (default), use sanitized env.
*/
export async function runCommand(command, args = [], options = {}) {
const baseEnv = options.inheritProcessEnv ? process.env : getSafeEnv();
return new Promise((resolve) => {
const child = spawn(command, args, {
cwd: process.cwd(),
env: { ...baseEnv, ...(options.env || {}) },
stdio: ['ignore', 'pipe', 'pipe'],
});
let stdout = '';
let stderr = '';
const OUTPUT_LIMIT = 20000; // 20KB
let stdoutWritten = 0;
let stderrWritten = 0;
let stdoutTruncated = false;
let stderrTruncated = false;
child.stdout.on('data', (chunk) => {
const text = chunk.toString();
stdout += text;
if (stdoutWritten < OUTPUT_LIMIT) {
const remaining = OUTPUT_LIMIT - stdoutWritten;
if (text.length <= remaining) {
process.stdout.write(text);
stdoutWritten += text.length;
} else {
process.stdout.write(text.slice(0, remaining));
stdoutWritten += remaining;
}
} else if (!stdoutTruncated) {
process.stdout.write('\n...[stdout truncated]...\n');
stdoutTruncated = true;
}
});
child.stderr.on('data', (chunk) => {
const text = chunk.toString();
stderr += text;
if (stderrWritten < OUTPUT_LIMIT) {
const remaining = OUTPUT_LIMIT - stderrWritten;
if (text.length <= remaining) {
process.stderr.write(text);
stderrWritten += text.length;
} else {
process.stderr.write(text.slice(0, remaining));
stderrWritten += remaining;
}
} else if (!stderrTruncated) {
process.stderr.write('\n...[stderr truncated]...\n');
stderrTruncated = true;
}
});
child.on('close', (code) => {
resolve({ code: code ?? 1, stdout, stderr });
});
});
}
export function parseJsonFromOutput(text) {
const trimmed = String(text || '').trim();
if (!trimmed) return null;
const lines = trimmed.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
for (let i = lines.length - 1; i >= 0; i -= 1) {
const line = lines[i];
if (!line.startsWith('{') && !line.startsWith('[')) continue;
try {
return JSON.parse(line);
} catch {
// Keep scanning from the end.
}
}
return null;
}
export function parseJsonEventsFromOutput(text) {
const events = [];
for (const line of String(text || '').split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed.startsWith('{')) continue;
try {
const parsed = JSON.parse(trimmed);
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
events.push(parsed);
}
} catch {
// Ignore non-JSON lines.
}
}
return events;
}
export async function readJson(filePath, fallback) {
try {
const raw = await fs.readFile(filePath, 'utf8');
return JSON.parse(raw);
} catch {
return fallback;
}
}
export function normalizeStringList(value, fallback = []) {
if (!Array.isArray(value)) return fallback;
return value.filter((item) => typeof item === 'string' && item.trim()).map((item) => item.trim());
}
export function parseNonNegativeInt(value, fallback) {
const parsed = Number.parseInt(String(value ?? ''), 10);
if (Number.isFinite(parsed) && parsed >= 0) {
return parsed;
}
return fallback;
}
export function parseBooleanFlag(value, fallback = false) {
if (value === undefined || value === null || value === '') return fallback;
const normalized = String(value).trim().toLowerCase();
if (['1', 'true', 'yes', 'on'].includes(normalized)) return true;
if (['0', 'false', 'no', 'off'].includes(normalized)) return false;
return fallback;
}
export function redactSensitive(text) {
if (!text) return '';
return String(text)
.replace(/\b(BEARER\s+)[A-Za-z0-9._~+/=-]+/gi, '$1[REDACTED]')
.replace(/\b(token|api[_-]?key|secret(?:[_-]?key)?|password|passwd|authorization)\s*[:=]\s*[^\s,;]+/gi, '$1=[REDACTED]')
.replace(/\b(sk|pk|ghp|xoxb|xoxp)_[A-Za-z0-9_-]+\b/g, '[REDACTED]');
}
export function excerptText(text, maxChars = 600) {
const clean = redactSensitive(String(text || '').trim());
if (!clean) return '';
if (clean.length <= maxChars) return clean;
return `${clean.slice(0, maxChars)}…`;
}
export function formatDurationMs(ms) {
if (!Number.isFinite(ms) || ms < 0) return 'unknown';
const minutes = Math.round(ms / 60000);
return `${minutes} minute(s)`;
}
export function getRunDateKey(now = new Date()) {
return now.toISOString().slice(0, 10);
}
export function toYamlScalar(value) {
const str = String(value ?? '');
return `'${str.replace(/'/g, "''")}'`;
}
export function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+177
View File
@@ -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,248 @@
#!/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;
}
const promises = entries.map(async (entry) => {
const fullPath = path.join(cadenceDir, entry);
const record = await readLogFile(fullPath, cadence, cutoffMs);
return record;
});
const results = await Promise.all(promises);
logsScanned += entries.length;
for (const record of results) {
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,374 @@
#!/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) {
// Match "failure id" as a distinct word phrase (ignores "failure identifiers" or "failure IDs")
if (/(?:issue|incident|failure)[-_\s]*id\b/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(() => '');
// Filter out passing TAP lines, comments, and YAML blocks to avoid false positives on test descriptions
const relevantContent = content.split(/\r?\n/)
.filter((line) => {
const trimmed = line.trim();
return !/^ok\s+\d+/.test(trimmed) &&
!trimmed.startsWith('#') &&
!trimmed.startsWith('>') &&
!/^(failureType|code|name|stack|operator|expected|actual):/.test(trimmed);
})
.join('\n');
if (FAILURE_KEYWORD_RE.test(relevantContent)) {
const ids = extractIdentifiers(content); // Extract from full content to find IDs anywhere
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);
});
+88
View File
@@ -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);
+135
View File
@@ -0,0 +1,135 @@
import { createLifecyclePlan } from '../src/services/memory/pruner.js';
import crypto from 'node:crypto';
import { performance } from 'node:perf_hooks';
const ITERATIONS = 3;
const GROUPS = 100;
const GROUP_SIZE = 5;
const LATENCY_MS = 10;
function createMemory(overrides = {}) {
return {
schema_version: 1,
id: overrides.id ?? crypto.randomUUID(),
agent_id: overrides.agent_id ?? 'agent-1',
session_id: overrides.session_id ?? 's1',
type: overrides.type ?? 'event',
content: overrides.content ?? 'default content',
summary: overrides.summary ?? 'default summary',
tags: overrides.tags ?? [],
importance: overrides.importance ?? 0.5,
embedding_id: overrides.embedding_id ?? null,
created_at: overrides.created_at ?? Date.now(),
last_seen: overrides.last_seen ?? Date.now(),
source: overrides.source ?? 'ingest',
ttl_days: overrides.ttl_days ?? null,
merged_into: overrides.merged_into ?? null,
pinned: overrides.pinned ?? false,
};
}
function generateMemories() {
const memories = [];
const now = Date.now();
for (let i = 0; i < GROUPS; i++) {
const groupId = `group-${i}`;
for (let j = 0; j < GROUP_SIZE; j++) {
memories.push(createMemory({
id: `${groupId}-${j}`,
content: `Memory ${j} for group ${groupId}`,
summary: `Summary ${j} for group ${groupId}`,
tags: ['benchmark', groupId],
created_at: now - (j * 100), // Close in time
last_seen: now,
importance: 0.5,
}));
}
}
// Add some random noise memories that won't group
for (let i = 0; i < 50; i++) {
memories.push(createMemory({
id: `noise-${i}`,
content: `Noise memory ${i}`,
tags: [`noise-${i}`], // Unique tags to prevent grouping
created_at: now - (i * 100000), // Spread out
}));
}
return memories;
}
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function runBenchmark() {
console.log(`Setting up benchmark: ${GROUPS} groups of ${GROUP_SIZE} memories + noise.`);
console.log(`Mock latency: ${LATENCY_MS}ms per group condensation.`);
const memories = generateMemories();
// Prepare embeddings
const groupEmbeddings = new Map();
for (let i = 0; i < GROUPS; i++) {
const vec = new Array(GROUPS).fill(0);
vec[i] = 1;
groupEmbeddings.set(`group-${i}`, vec);
}
const getEmbeddingWrapper = (memory) => {
const groupIdTag = memory.tags.find(t => t.startsWith('group-'));
if (groupIdTag && groupEmbeddings.has(groupIdTag)) {
return groupEmbeddings.get(groupIdTag);
}
// Return a unique random vector for noise to prevent grouping
// But since they have unique tags, getEmbedding won't even be called for similarity check
// if tag overlap check fails first?
// groupNearDuplicates logic:
// ... if (!withinWindow || !hasTagOverlap(base.tags, probe.tags)) continue;
// So if tags don't overlap, it continues.
// So embedding doesn't matter for noise if tags are unique.
return [Math.random()];
};
const options = {
retentionMs: 30 * 24 * 60 * 60 * 1000, // 30 days
duplicateWindowMs: 1000 * 60 * 60, // 1 hour
similarityThreshold: 0.99,
getEmbedding: getEmbeddingWrapper,
generateSummary: async (prompt) => {
await delay(LATENCY_MS);
return JSON.stringify({
summary: 'Condensed summary based on prompt.',
importance: 0.8,
});
},
// Mock templateDir to avoid file reading issues if paths are tricky,
// but loadMemoryPromptTemplates reads from file system.
// It should work if run from root.
};
console.log(`Starting ${ITERATIONS} iterations...`);
const times = [];
for (let i = 0; i < ITERATIONS; i++) {
const start = performance.now();
const plan = await createLifecyclePlan(memories, options);
const duration = performance.now() - start;
// Verify grouping happened
const condensedCount = plan.condensedGroups.length;
// We expect exactly GROUPS condensed groups.
if (condensedCount !== GROUPS) {
console.warn(`Warning: Expected ${GROUPS} condensed groups, but got ${condensedCount}. Check grouping logic.`);
}
times.push(duration);
console.log(`Iteration ${i + 1}: ${duration.toFixed(2)}ms`);
}
const avg = times.reduce((a, b) => a + b, 0) / times.length;
console.log(`Average execution time: ${avg.toFixed(2)}ms`);
}
runBenchmark().catch(console.error);
+106
View File
@@ -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();
+113
View File
@@ -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();
+102
View File
@@ -0,0 +1,102 @@
import os
import subprocess
import sys
def get_all_files(extensions=['.mjs', '.js']):
files = []
for root, dirs, filenames in os.walk('.'):
if 'node_modules' in root or '.git' in root or 'artifacts' in root or 'dist' in root:
continue
for filename in filenames:
if any(filename.endswith(ext) for ext in extensions):
files.append(os.path.join(root, filename))
return files
def grep_file(filepath, all_files):
filename = os.path.basename(filepath)
name_no_ext = os.path.splitext(filename)[0]
# Heuristic: search for the filename or the name without extension
# We must exclude the file itself from the search
# Construct grep command
# git grep -F "string" -- .
# Check for full filename usage
try:
# We use git grep because it's fast and respects .gitignore
# We search for the filename string.
# We assume if the filename appears in another file, it MIGHT be used.
# This is conservative.
# Search for filename (e.g. "foo.mjs")
result_full = subprocess.run(
['git', 'grep', '-F', filename, '--', '.'],
capture_output=True, text=True
)
# Search for name without extension (e.g. "foo") if it's not "index"
result_no_ext = None
if name_no_ext != 'index':
result_no_ext = subprocess.run(
['git', 'grep', '-F', name_no_ext, '--', '.'],
capture_output=True, text=True
)
output = result_full.stdout
if result_no_ext:
output += result_no_ext.stdout
lines = output.splitlines()
# Filter out self-references
count = 0
for line in lines:
if line.startswith(filepath.lstrip('./')):
continue
count += 1
return count
except Exception as e:
print(f"Error grepping {filepath}: {e}")
return 1 # Assume used on error
def main():
files = get_all_files()
candidates = []
# Known entry points or special files to exclude from "dead" list immediately
# These might not be imported but are used by scripts or configuration
exclusions = [
'./bin/torch-lock.mjs',
'./src/lib.mjs', # Main entry
'./src/constants.mjs', # Likely used
'./build.mjs',
'./eslint.config.mjs',
]
print(f"Scanning {len(files)} files...")
for f in files:
if f in exclusions:
continue
# Skip test files
if '.test.' in f or '/test/' in f or '/tests/' in f:
continue
count = grep_file(f, files)
if count == 0:
candidates.append(f)
print(f"Candidate: {f}")
if not candidates:
print("No candidates found.")
else:
print(f"Found {len(candidates)} candidates.")
with open('artifacts/candidates.txt', 'w') as f_out:
for c in candidates:
f_out.write(c + '\n')
if __name__ == "__main__":
main()
@@ -0,0 +1,45 @@
import { listProposals, applyProposal, rejectProposal, validateProposal } from '../../src/services/governance/index.js';
async function main() {
console.log('Governance: Scanning for pending proposals...');
const proposals = await listProposals();
const pending = proposals.filter(p => p.status === 'pending');
if (pending.length === 0) {
console.log('No pending proposals found.');
return;
}
console.log(`Found ${pending.length} pending proposals.`);
for (const p of pending) {
console.log(`\nProcessing proposal: ${p.id} (Target: ${p.target})`);
// 1. Validate
const validation = await validateProposal(p.id);
if (!validation.valid) {
console.error(` Validation FAILED: ${validation.reason}`);
await rejectProposal(p.id, validation.reason);
console.log(` Rejected proposal ${p.id}.`);
continue;
}
console.log(' Validation PASSED.');
// 2. Apply
try {
await applyProposal(p.id);
console.log(` Applied proposal ${p.id} successfully.`);
} catch (e) {
console.error(` Application FAILED: ${e.message}`);
// Maybe reject with application error?
await rejectProposal(p.id, `Application failed: ${e.message}`);
}
}
console.log('\nGovernance cycle complete.');
}
main().catch(err => {
console.error('Fatal error in governance script:', err);
process.exit(1);
});
+94
View File
@@ -0,0 +1,94 @@
import { ingestEvents, getRelevantMemories } from '../../src/services/memory/index.js';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import path from 'node:path';
// Parse environment variables
const fallbackCadence = 'daily';
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)}`;
// Extract prompt intent
let promptIntent = `scheduler memory retrieval`;
if (promptPath) {
try {
const promptRaw = readFileSync(promptPath, 'utf8');
const promptLines = promptRaw.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
const intentLine = promptLines.find(line => line.startsWith('#') || line.toLowerCase().startsWith('goal'));
if (intentLine) {
promptIntent = intentLine.replace(/^#+\s*/, '');
} else if (promptLines.length > 0) {
promptIntent = promptLines[0];
}
} catch (err) {
console.warn(`Could not read prompt file: ${promptPath}`, err.message);
}
}
// Construct query
const query = ['scheduler', cadence, 'agent', agentId, 'intent', promptIntent].join(' | ');
const timestamp = Date.now();
// Create retrieval seed event
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
}
}];
try {
// 1. Ingest seed event
const ingested = await ingestEvents(events, { agent_id: agentId });
// 2. Retrieve memories
const retrieved = await getRelevantMemories({ agent_id: agentId, query, k: 5 });
// 3. Prepare artifacts
const sessionDir = path.join('.scheduler-memory', runId);
const latestDir = path.join('.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'
};
// 4. Write artifacts
writeFileSync(path.join(sessionDir, 'retrieve.json'), JSON.stringify(artifact, null, 2));
writeFileSync(path.join(sessionDir, 'retrieve.ok'), 'MEMORY_RETRIEVED\n');
writeFileSync(path.join(latestDir, 'retrieve.ok'), 'MEMORY_RETRIEVED\n');
writeFileSync(path.join(latestDir, 'memories.md'), retrieved.map(m => `- ${m.content}`).join('\n'));
// 5. Output success marker to stdout (for scheduler verification)
console.log('MEMORY_RETRIEVED');
} catch (error) {
console.error('Memory retrieval failed:', error);
process.exit(1);
}
+142
View File
@@ -0,0 +1,142 @@
import { ingestEvents } from '../../src/services/memory/index.js';
import { mkdirSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
import path from 'node:path';
// Parse environment variables
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)}`;
// Extract prompt intent
let promptIntent = `scheduler memory store`;
if (promptPath) {
try {
const promptRaw = readFileSync(promptPath, 'utf8');
const promptLines = promptRaw.split(/\r?\n/).map(line => line.trim()).filter(Boolean);
const intentLine = promptLines.find(line => line.startsWith('#') || line.toLowerCase().startsWith('goal'));
if (intentLine) {
promptIntent = intentLine.replace(/^#+\s*/, '');
} else if (promptLines.length > 0) {
promptIntent = promptLines[0];
}
} catch (err) {
console.warn(`Could not read prompt file: ${promptPath}`, err.message);
}
}
// Check for memory input file
let memoryContent = '';
const cliArgs = process.argv.slice(2);
const fileArgIndex = cliArgs.indexOf('--file');
const explicitFile = fileArgIndex !== -1 ? cliArgs[fileArgIndex + 1] : null;
const envFile = process.env.SCHEDULER_MEMORY_FILE;
const defaultFile = 'memory-update.md';
const targetFile = explicitFile || (envFile && existsSync(envFile) ? envFile : (existsSync(defaultFile) ? defaultFile : null));
if (targetFile) {
try {
if (existsSync(targetFile)) {
memoryContent = readFileSync(targetFile, 'utf8').trim();
console.log(`Loaded memory content from ${targetFile}`);
} else {
console.warn(`Target memory file does not exist: ${targetFile}`);
}
} catch (err) {
console.warn(`Failed to read memory file ${targetFile}:`, err.message);
}
}
const baseTs = Date.now();
let events = [];
if (memoryContent) {
events.push({
agent_id: agentId,
content: memoryContent,
timestamp: baseTs,
tags: ['scheduler', cadence, 'store', 'insight'],
metadata: {
session_id: runId,
source: 'agent-output',
importance: 0.8, // User-provided memory is important
prompt_path: promptPath
}
});
} else {
// Fallback if no file provided (preserve existing behavior for compatibility)
console.warn(`No memory input found (checked --file, env SCHEDULER_MEMORY_FILE=${envFile}, or default ${defaultFile}). using fallback placeholder.`);
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
}
}
];
}
try {
// 1. Ingest events
const stored = await ingestEvents(events, { agent_id: agentId });
// 2. Prepare artifacts
const sessionDir = path.join('.scheduler-memory', runId);
const latestDir = path.join('.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,
sourceFile: targetFile || 'fallback'
},
outputs: {
storedCount: stored.length,
summaries: stored.map(m => m.summary)
},
status: 'ok'
};
// 3. Write artifacts
writeFileSync(path.join(sessionDir, 'store.json'), JSON.stringify(artifact, null, 2));
writeFileSync(path.join(sessionDir, 'store.ok'), 'MEMORY_STORED\n');
writeFileSync(path.join(latestDir, 'store.ok'), 'MEMORY_STORED\n');
// 4. Output success marker to stdout (for scheduler verification)
console.log('MEMORY_STORED');
} catch (error) {
console.error('Memory storage failed:', error);
process.exit(1);
}
+57
View File
@@ -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,65 @@
import { spawn } from 'node:child_process';
import { writeFileSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
const RUNS = 5;
const results = {};
// Parse arguments to separate our flags from test runner args
const args = process.argv.slice(2);
let outputDir = 'reports/test-audit';
const testArgs = [];
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output-dir') {
outputDir = args[i + 1];
i++; // Skip next arg
} else {
testArgs.push(args[i]);
}
}
async function runTests(i) {
console.log(`Run ${i + 1}/${RUNS}...`);
return new Promise((resolve) => {
// Pass strictly the test args to the child process
const child = spawn('node', ['--test', ...testArgs], {
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
child.stdout.on('data', (d) => stdout += d.toString());
child.on('close', (code) => {
const lines = stdout.split('\n');
for (const line of lines) {
if (line.startsWith('not ok ') && !line.includes('# skip') && !line.includes('# todo')) {
const name = line.substring(line.indexOf('-') + 1).trim();
if (!results[name]) results[name] = { pass: 0, fail: 0 };
results[name].fail++;
} else if (line.startsWith('ok ') && !line.includes('# skip') && !line.includes('# todo')) {
const name = line.substring(line.indexOf('-') + 1).trim();
if (!results[name]) results[name] = { pass: 0, fail: 0 };
results[name].pass++;
}
}
resolve();
});
});
}
async function main() {
for (let i = 0; i < RUNS; i++) {
await runTests(i);
}
// Ensure output directory exists
mkdirSync(outputDir, { recursive: true });
const outputPath = join(outputDir, 'flakiness-matrix.json');
writeFileSync(outputPath, JSON.stringify(results, null, 2));
console.log(`Flakiness matrix written to ${outputPath}.`);
}
main();
@@ -0,0 +1,66 @@
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync } from 'node:fs';
import { join } from 'node:path';
function getFiles(dir, files = []) {
try {
const fileList = readdirSync(dir);
for (const file of fileList) {
const name = join(dir, file);
if (statSync(name).isDirectory()) {
getFiles(name, files);
} else if (name.endsWith('.mjs') || name.endsWith('.js')) {
files.push(name);
}
}
} catch (e) {
// ignore if dir doesn't exist
}
return files;
}
// Parse arguments
const args = process.argv.slice(2);
let outputDir = 'reports/test-audit';
for (let i = 0; i < args.length; i++) {
if (args[i] === '--output-dir') {
outputDir = args[i + 1];
i++;
}
}
// Only scan test/ directory now, as tests/ was consolidated
const testFiles = [...getFiles('test')];
const suspicious = [];
for (const file of testFiles) {
const content = readFileSync(file, 'utf8');
const issues = [];
if (content.includes('.only(')) issues.push('Found .only()');
if (content.includes('.skip(')) issues.push('Found .skip()');
if (content.includes('setTimeout(')) issues.push('Found setTimeout()');
if (content.includes('sleep(')) issues.push('Found sleep()');
// Naive check for assertions: look for assert., expect(, t.
// Many tests use 't' context from node:test
if (!content.includes('assert.') && !content.includes('expect(') && !content.includes('t.plan') && !content.includes('strictEqual')) {
// This is very naive, might false positive on 't.ok' etc.
// Let's look for common assertion keywords.
const assertionKeywords = ['assert', 'expect', 'strictEqual', 'deepStrictEqual', 'ok', 'equal'];
const hasAssertion = assertionKeywords.some(k => content.includes(k));
if (!hasAssertion) issues.push('No obvious assertions');
}
if (issues.length > 0) {
suspicious.push({ file, issues });
}
}
// Ensure output directory exists
mkdirSync(outputDir, { recursive: true });
const outputPath = join(outputDir, 'suspicious-tests.json');
writeFileSync(outputPath, JSON.stringify(suspicious, null, 2));
console.log(`Found ${suspicious.length} suspicious files.`);
console.log(`Report written to ${outputPath}.`);
@@ -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.');
+163
View File
@@ -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.');