diff --git a/README.md b/README.md index 58d1d6a..c8870b0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ From the repository root: ```bash npm install npm run lock:check:daily -AGENT_PLATFORM=codex npm run lock:lock -- --agent documentation-agent --cadence daily +AGENT_PLATFORM=codex npm run lock:lock -- --agent docs-agent --cadence daily npm run lock:list ``` @@ -52,12 +52,23 @@ Declared in `package.json` and pinned: - `NOSTR_LOCK_WEEKLY_ROSTER` - `AGENT_PLATFORM` +## Roster precedence + +The lock CLI resolves roster names in this order: + +1. `NOSTR_LOCK_DAILY_ROSTER` / `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated env overrides). +2. `src/prompts/roster.json` (`daily` / `weekly` canonical scheduler roster). +3. Built-in fallback roster (used only if `src/prompts/roster.json` is unreadable). + +`lock --agent` validates names against the resolved cadence roster, and `check`/`list` report lock events whose agent names do not match scheduler roster entries exactly. + + ## Example ```bash NOSTR_LOCK_NAMESPACE=my-project \ AGENT_PLATFORM=codex \ -node src/nostr-lock.mjs lock --agent documentation-agent --cadence daily +node src/nostr-lock.mjs lock --agent docs-agent --cadence daily ``` [Tip Jar](https://tipjar.bitvid.network/?n=npub15jnttpymeytm80hatjqcvhhqhzrhx6gxp8pq0wn93rhnu8s9h9dsha32lx) diff --git a/src/docs/TORCH.md b/src/docs/TORCH.md index aa3a695..7d16158 100644 --- a/src/docs/TORCH.md +++ b/src/docs/TORCH.md @@ -9,7 +9,7 @@ TORCH is a decentralized task-locking protocol for multi-agent software developm node src/nostr-lock.mjs check --cadence daily # Claim a task -AGENT_PLATFORM=codex node src/nostr-lock.mjs lock --agent documentation-agent --cadence daily +AGENT_PLATFORM=codex node src/nostr-lock.mjs lock --agent docs-agent --cadence daily # List all active locks node src/nostr-lock.mjs list @@ -35,6 +35,17 @@ node src/nostr-lock.mjs list - `NOSTR_LOCK_WEEKLY_ROSTER` - `AGENT_PLATFORM` +## Roster precedence + +The lock CLI resolves roster names in this order: + +1. `NOSTR_LOCK_DAILY_ROSTER` / `NOSTR_LOCK_WEEKLY_ROSTER` (comma-separated env overrides). +2. `src/prompts/roster.json` (`daily` / `weekly` canonical scheduler roster). +3. Built-in fallback roster (used only if `src/prompts/roster.json` is unreadable). + +`lock --agent` validates names against the resolved cadence roster, and `check`/`list` report lock events whose agent names do not match scheduler roster entries exactly. + + ## Exit codes - `0`: success diff --git a/src/nostr-lock.mjs b/src/nostr-lock.mjs index 1b0e95f..e90ddb8 100644 --- a/src/nostr-lock.mjs +++ b/src/nostr-lock.mjs @@ -3,6 +3,9 @@ // TORCH — Task Orchestration via Relay-Coordinated Handoff // Generic Nostr-based task locking for multi-agent development. +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; import { generateSecretKey, getPublicKey, finalizeEvent } from 'nostr-tools/pure'; import { SimplePool } from 'nostr-tools/pool'; import { useWebSocketImplementation } from 'nostr-tools/relay'; @@ -21,21 +24,52 @@ const DEFAULT_NAMESPACE = 'torch'; const QUERY_TIMEOUT_MS = 15_000; const VALID_CADENCES = new Set(['daily', 'weekly']); -const DEFAULT_DAILY_ROSTER = [ - 'documentation-agent', - 'quality-agent', - 'security-agent', - 'performance-agent', - 'refactor-agent', -]; +const ROSTER_FILE = path.resolve(path.dirname(fileURLToPath(import.meta.url)), 'prompts/roster.json'); +const FALLBACK_ROSTER = { + daily: [ + 'audit-agent', + 'ci-health-agent', + 'const-refactor-agent', + 'content-audit-agent', + 'decompose-agent', + 'deps-security-agent', + 'design-system-audit-agent', + 'docs-agent', + 'docs-alignment-agent', + 'docs-code-investigator', + 'innerhtml-migration-agent', + 'known-issues-agent', + 'load-test-agent', + 'onboarding-audit-agent', + 'perf-agent', + 'prompt-curator-agent', + 'protocol-research-agent', + 'scheduler-update-agent', + 'style-agent', + 'test-audit-agent', + 'todo-triage-agent', + ], + weekly: [ + 'bug-reproducer-agent', + 'changelog-agent', + 'dead-code-agent', + 'event-schema-agent', + 'frontend-console-debug-agent', + 'fuzz-agent', + 'interop-agent', + 'perf-deepdive-agent', + 'perf-optimization-agent', + 'pr-review-agent', + 'race-condition-agent', + 'refactor-agent', + 'smoke-agent', + 'telemetry-agent', + 'test-coverage-agent', + 'weekly-synthesis-agent', + ], +}; -const DEFAULT_WEEKLY_ROSTER = [ - 'bug-reproducer-agent', - 'integration-agent', - 'release-agent', - 'test-coverage-agent', - 'weekly-synthesis-agent', -]; +let cachedCanonicalRoster = null; function getRelays() { const envRelays = process.env.NOSTR_LOCK_RELAYS; @@ -59,6 +93,31 @@ function getTtl() { return DEFAULT_TTL; } + +function loadCanonicalRoster() { + if (cachedCanonicalRoster) return cachedCanonicalRoster; + + try { + const parsed = JSON.parse(fs.readFileSync(ROSTER_FILE, 'utf8')); + const daily = Array.isArray(parsed.daily) ? parsed.daily.map((item) => String(item).trim()).filter(Boolean) : []; + const weekly = Array.isArray(parsed.weekly) + ? parsed.weekly.map((item) => String(item).trim()).filter(Boolean) + : []; + + if (daily.length > 0 && weekly.length > 0) { + cachedCanonicalRoster = { daily, weekly }; + return cachedCanonicalRoster; + } + + console.error(`WARNING: Roster file is missing daily/weekly entries, falling back: ${ROSTER_FILE}`); + } catch (err) { + console.error(`WARNING: Failed to read roster file (${ROSTER_FILE}): ${err.message}`); + } + + cachedCanonicalRoster = FALLBACK_ROSTER; + return cachedCanonicalRoster; +} + function parseEnvRoster(value) { if (!value) return null; return value @@ -70,12 +129,13 @@ function parseEnvRoster(value) { function getRoster(cadence) { const dailyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_DAILY_ROSTER); const weeklyFromEnv = parseEnvRoster(process.env.NOSTR_LOCK_WEEKLY_ROSTER); + const canonical = loadCanonicalRoster(); if (cadence === 'daily') { - return dailyFromEnv && dailyFromEnv.length ? dailyFromEnv : DEFAULT_DAILY_ROSTER; + return dailyFromEnv && dailyFromEnv.length ? dailyFromEnv : canonical.daily; } - return weeklyFromEnv && weeklyFromEnv.length ? weeklyFromEnv : DEFAULT_WEEKLY_ROSTER; + return weeklyFromEnv && weeklyFromEnv.length ? weeklyFromEnv : canonical.weekly; } function todayDateStr() { @@ -178,6 +238,7 @@ async function cmdCheck(cadence) { const locks = await queryLocks(relays, cadence, dateStr, namespace); const lockedAgents = [...new Set(locks.map((l) => l.agent).filter(Boolean))]; const roster = getRoster(cadence); + const unknownLockedAgents = lockedAgents.filter((agent) => !roster.includes(agent)); const available = roster.filter((a) => !lockedAgents.includes(a)); console.log( @@ -189,6 +250,7 @@ async function cmdCheck(cadence) { locked: lockedAgents.sort(), available: available.sort(), lockCount: locks.length, + unknownLockedAgents: unknownLockedAgents.sort(), locks: locks.map((l) => ({ agent: l.agent, eventId: l.eventId, @@ -215,6 +277,13 @@ async function cmdLock(agent, cadence, dryRun = false) { console.error(`TTL: ${ttl}s, expires: ${new Date(expiresAt * 1000).toISOString()}`); console.error(`Relays: ${relays.join(', ')}`); + const roster = getRoster(cadence); + if (!roster.includes(agent)) { + console.error(`ERROR: agent "${agent}" is not in the ${cadence} roster`); + console.error(`Allowed ${cadence} agents: ${roster.join(', ')}`); + process.exit(1); + } + console.error('Step 1: Checking for existing locks...'); const existingLocks = await queryLocks(relays, cadence, dateStr, namespace); const conflicting = existingLocks.filter((l) => l.agent === agent); @@ -341,8 +410,13 @@ async function cmdList(cadence) { const roster = getRoster(c); const lockedAgents = new Set(locks.map((l) => l.agent).filter(Boolean)); + const unknownLockedAgents = [...lockedAgents].filter((agent) => !roster.includes(agent)); const available = roster.filter((a) => !lockedAgents.has(a)); + if (unknownLockedAgents.length > 0) { + console.log(` Warning: lock events found with non-roster agent names: ${unknownLockedAgents.join(', ')}`); + } + console.log(`\n Locked: ${lockedAgents.size}/${roster.length}`); console.log(` Available: ${available.join(', ') || '(none)'}`); } diff --git a/src/prompts/daily-scheduler.md b/src/prompts/daily-scheduler.md index 1deb70b..d205ff3 100644 --- a/src/prompts/daily-scheduler.md +++ b/src/prompts/daily-scheduler.md @@ -2,6 +2,8 @@ Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure. +Canonical roster source: `src/prompts/roster.json` (`daily` key). + ## Daily Cadence Configuration - `cadence`: `daily` diff --git a/src/prompts/roster.json b/src/prompts/roster.json new file mode 100644 index 0000000..b7b43ee --- /dev/null +++ b/src/prompts/roster.json @@ -0,0 +1,43 @@ +{ + "daily": [ + "audit-agent", + "ci-health-agent", + "const-refactor-agent", + "content-audit-agent", + "decompose-agent", + "deps-security-agent", + "design-system-audit-agent", + "docs-agent", + "docs-alignment-agent", + "docs-code-investigator", + "innerhtml-migration-agent", + "known-issues-agent", + "load-test-agent", + "onboarding-audit-agent", + "perf-agent", + "prompt-curator-agent", + "protocol-research-agent", + "scheduler-update-agent", + "style-agent", + "test-audit-agent", + "todo-triage-agent" + ], + "weekly": [ + "bug-reproducer-agent", + "changelog-agent", + "dead-code-agent", + "event-schema-agent", + "frontend-console-debug-agent", + "fuzz-agent", + "interop-agent", + "perf-deepdive-agent", + "perf-optimization-agent", + "pr-review-agent", + "race-condition-agent", + "refactor-agent", + "smoke-agent", + "telemetry-agent", + "test-coverage-agent", + "weekly-synthesis-agent" + ] +} diff --git a/src/prompts/weekly-scheduler.md b/src/prompts/weekly-scheduler.md index 5d91256..89a962b 100644 --- a/src/prompts/weekly-scheduler.md +++ b/src/prompts/weekly-scheduler.md @@ -2,6 +2,8 @@ Use `src/prompts/scheduler-flow.md` as the authoritative scheduler procedure. +Canonical roster source: `src/prompts/roster.json` (`weekly` key). + ## Weekly Cadence Configuration - `cadence`: `weekly`