diff --git a/package.json b/package.json index 2bca5d3..685de7e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,7 @@ "README.md" ], "scripts": { - "test": "node --test test/*.test.mjs tests/*.test.js", + "test": "npm run validate:scheduler && node --test test/*.test.mjs tests/*.test.js", "lock:check:daily": "node bin/torch-lock.mjs check --cadence daily", "lock:check:weekly": "node bin/torch-lock.mjs check --cadence weekly", "lock:list": "node bin/torch-lock.mjs list", @@ -35,7 +35,8 @@ "dashboard:serve": "node bin/torch-lock.mjs dashboard", "build": "node build.mjs", "lint": "eslint .", - "lock:complete": "node bin/torch-lock.mjs complete" + "lock:complete": "node bin/torch-lock.mjs complete", + "validate:scheduler": "node scripts/validate-scheduler-roster.mjs" }, "dependencies": { "nostr-tools": "2.19.4", diff --git a/scripts/validate-scheduler-roster.mjs b/scripts/validate-scheduler-roster.mjs new file mode 100644 index 0000000..72a00eb --- /dev/null +++ b/scripts/validate-scheduler-roster.mjs @@ -0,0 +1,122 @@ +import { readdir, readFile } from 'node:fs/promises'; + +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; +} + +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)); +} + +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.'); diff --git a/src/prompts/daily-scheduler.md b/src/prompts/daily-scheduler.md index 47c5259..88e09fc 100644 --- a/src/prompts/daily-scheduler.md +++ b/src/prompts/daily-scheduler.md @@ -28,9 +28,9 @@ Canonical roster source: `src/prompts/roster.json` (`daily` key). | 11 | innerhtml-migration-agent | `innerhtml-migration-agent.md` | | 12 | known-issues-agent | `known-issues-agent.md` | | 13 | load-test-agent | `load-test-agent.md` | -| 14 | onboarding-audit-agent | `onboarding-audit-agent.md` | -| 15 | perf-agent | `perf-agent.md` | -| 16 | prompt-curator-agent | `prompt-curator-agent.md` | +| 14 | log-fixer-agent | `log-fixer-agent.md` | +| 15 | onboarding-audit-agent | `onboarding-audit-agent.md` | +| 16 | perf-agent | `perf-agent.md` | | 17 | protocol-research-agent | `protocol-research-agent.md` | | 18 | scheduler-update-agent | `scheduler-update-agent.md` | | 19 | style-agent | `style-agent.md` | diff --git a/src/prompts/weekly-scheduler.md b/src/prompts/weekly-scheduler.md index 7eeb515..942f86d 100644 --- a/src/prompts/weekly-scheduler.md +++ b/src/prompts/weekly-scheduler.md @@ -18,17 +18,19 @@ Canonical roster source: `src/prompts/roster.json` (`weekly` key). | 1 | bug-reproducer-agent | `bug-reproducer-agent.md` | | 2 | changelog-agent | `changelog-agent.md` | | 3 | dead-code-agent | `dead-code-agent.md` | -| 4 | event-schema-agent | `event-schema-agent.md` | +| 4 | feature-proposer-agent | `feature-proposer-agent.md` | | 5 | frontend-console-debug-agent | `frontend-console-debug-agent.md` | | 6 | fuzz-agent | `fuzz-agent.md` | -| 7 | interop-agent | `interop-agent.md` | -| 8 | perf-deepdive-agent | `perf-deepdive-agent.md` | -| 9 | perf-optimization-agent | `perf-optimization-agent.md` | -| 10 | pr-review-agent | `pr-review-agent.md` | -| 11 | race-condition-agent | `race-condition-agent.md` | -| 12 | refactor-agent | `refactor-agent.md` | -| 13 | repo-fit-agent | `repo-fit-agent.md` | -| 14 | smoke-agent | `smoke-agent.md` | -| 15 | telemetry-agent | `telemetry-agent.md` | -| 16 | test-coverage-agent | `test-coverage-agent.md` | -| 17 | weekly-synthesis-agent | `weekly-synthesis-agent.md` | +| 7 | perf-deepdive-agent | `perf-deepdive-agent.md` | +| 8 | perf-optimization-agent | `perf-optimization-agent.md` | +| 9 | pr-review-agent | `pr-review-agent.md` | +| 10 | prompt-maintenance-agent | `prompt-maintenance-agent.md` | +| 11 | prompt-safety-agent | `prompt-safety-agent.md` | +| 12 | race-condition-agent | `race-condition-agent.md` | +| 13 | refactor-agent | `refactor-agent.md` | +| 14 | repo-fit-agent | `repo-fit-agent.md` | +| 15 | smoke-agent | `smoke-agent.md` | +| 16 | telemetry-agent | `telemetry-agent.md` | +| 17 | test-coverage-agent | `test-coverage-agent.md` | +| 18 | ui-ux-agent | `ui-ux-agent.md` | +| 19 | weekly-synthesis-agent | `weekly-synthesis-agent.md` |