From e4f700bdabb5d42cb1c1b0fe30ad871a8c8bf674 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:49:48 -0500 Subject: [PATCH] Add scheduler flow parity validation script --- package.json | 2 +- scripts/validate-scheduler-flow-parity.mjs | 195 +++++++++++++++++++++ 2 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 scripts/validate-scheduler-flow-parity.mjs diff --git a/package.json b/package.json index 1dcd943..8c55171 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "build": "node build.mjs", "lint": "eslint .", "lock:complete": "node bin/torch-lock.mjs complete", - "validate:scheduler": "node scripts/validate-scheduler-roster.mjs && node scripts/validate-prompt-contract.mjs", + "validate:scheduler": "node scripts/validate-scheduler-roster.mjs && node scripts/validate-prompt-contract.mjs && node scripts/validate-scheduler-flow-parity.mjs", "scheduler:daily": "node scripts/agent/run-scheduler-cycle.mjs daily", "scheduler:weekly": "node scripts/agent/run-scheduler-cycle.mjs weekly" }, diff --git a/scripts/validate-scheduler-flow-parity.mjs b/scripts/validate-scheduler-flow-parity.mjs new file mode 100644 index 0000000..b666a9a --- /dev/null +++ b/scripts/validate-scheduler-flow-parity.mjs @@ -0,0 +1,195 @@ +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.` 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 --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.', + ], + }, + 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'", + ], + 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.');