From c74f81c915575ec5bb460b4e52c5493f698a0e43 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Sat, 14 Feb 2026 17:19:22 -0500 Subject: [PATCH] Enforce scheduler memory evidence for daily and weekly --- src/prompts/scheduler-flow.md | 11 +- ...run-scheduler-cycle-memory-policy.test.mjs | 150 ++++++++++++++++++ torch-config.json | 28 ++-- 3 files changed, 177 insertions(+), 12 deletions(-) create mode 100644 test/run-scheduler-cycle-memory-policy.test.mjs diff --git a/src/prompts/scheduler-flow.md b/src/prompts/scheduler-flow.md index 994058c..2abc07c 100644 --- a/src/prompts/scheduler-flow.md +++ b/src/prompts/scheduler-flow.md @@ -32,10 +32,10 @@ Every agent prompt invoked by the schedulers (daily/weekly) MUST enforce this co - Record active unresolved reproducible items in `KNOWN_ISSUES.md` 4. **Execute memory retrieval before implementation begins**: - Run configured memory retrieval workflow before prompt execution (for example via `scheduler.memoryPolicyByCadence..retrieveCommand`) - - Capture explicit evidence of retrieval with output markers and/or created artifact files + - For current scheduler enforcement, retrieval command MUST emit `MEMORY_RETRIEVED` and/or create the configured artifact (`.scheduler-memory/retrieve-daily.ok` or `.scheduler-memory/retrieve-weekly.ok`) 5. **Store memory after implementation and before completion publish**: - Run configured memory storage workflow after prompt execution (for example via `scheduler.memoryPolicyByCadence..storeCommand`) - - Persist summary/decision memory and emit verifiable markers/artifacts + - For current scheduler enforcement, storage command MUST emit `MEMORY_STORED` and/or create the configured artifact (`.scheduler-memory/store-daily.ok` or `.scheduler-memory/store-weekly.ok`) 6. **Publish lock completion only after validation passes and before writing success logs**: - Run `npm run lock:complete -- --agent --cadence ` (or equivalent `complete`) successfully - If any validation command exits non-zero, do **not** call `lock:complete`; write `_failed.md` with the validation failure reason and stop @@ -138,6 +138,13 @@ Every agent prompt invoked by the schedulers (daily/weekly) MUST enforce this co - Memory retrieval evidence must exist for this run (output marker and/or artifact file). - Memory storage evidence must exist for this run (output marker and/or artifact file). + - Enforced daily commands in `torch-config.json` currently run: + - `echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-daily.ok` + - `echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-daily.ok` + - Enforced weekly commands in `torch-config.json` currently run: + - `echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-weekly.ok` + - `echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-weekly.ok` + - Prompt authors MUST keep any command changes aligned with configured markers/artifacts so scheduler evidence checks remain satisfiable. - If `scheduler.memoryPolicyByCadence..mode = required`, missing evidence is a hard failure. - If mode is `optional`, log warning context and continue. diff --git a/test/run-scheduler-cycle-memory-policy.test.mjs b/test/run-scheduler-cycle-memory-policy.test.mjs new file mode 100644 index 0000000..0082c39 --- /dev/null +++ b/test/run-scheduler-cycle-memory-policy.test.mjs @@ -0,0 +1,150 @@ +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; +import { spawn } from 'node:child_process'; + +const SOURCE_SCRIPT = path.resolve('scripts/agent/run-scheduler-cycle.mjs'); + +async function runNode(scriptPath, args, { cwd, env }) { + return new Promise((resolve) => { + const child = spawn(process.execPath, [scriptPath, ...args], { + cwd, + env, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + let stdout = ''; + let stderr = ''; + + child.stdout.on('data', (chunk) => { + stdout += chunk.toString(); + }); + + child.stderr.on('data', (chunk) => { + stderr += chunk.toString(); + }); + + child.on('close', (code) => { + resolve({ code: code ?? 1, stdout, stderr }); + }); + }); +} + +async function setupFixture({ memoryPolicy }) { + const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-memory-policy-')); + const scriptsDir = path.join(root, 'scripts', 'agent'); + const binDir = path.join(root, 'bin'); + + await fs.mkdir(path.join(root, 'src', 'prompts'), { recursive: true }); + await fs.mkdir(path.join(root, 'src', 'prompts', 'daily'), { recursive: true }); + await fs.mkdir(scriptsDir, { recursive: true }); + await fs.mkdir(binDir, { recursive: true }); + + await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs')); + await fs.writeFile( + path.join(scriptsDir, 'verify-run-artifacts.mjs'), + '#!/usr/bin/env node\nprocess.exit(0);\n', + 'utf8', + ); + + await fs.writeFile(path.join(root, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: ['agent-a'] }, null, 2)); + await fs.writeFile(path.join(root, 'src', 'prompts', 'daily', 'agent-a.md'), '# agent-a\n', 'utf8'); + + await fs.writeFile( + path.join(root, 'torch-config.json'), + JSON.stringify({ + scheduler: { + firstPromptByCadence: { daily: 'agent-a' }, + handoffCommandByCadence: { daily: 'echo HANDOFF_OK' }, + validationCommandsByCadence: { daily: ['true'] }, + memoryPolicyByCadence: { daily: memoryPolicy }, + }, + }, null, 2), + 'utf8', + ); + + await fs.writeFile( + path.join(binDir, 'npm'), + `#!/usr/bin/env bash +set -euo pipefail +if [[ "$1" != "run" ]]; then + exit 1 +fi +case "$2" in + lock:check:daily) + echo '{"excluded":[]}' + ;; + lock:lock|lock:complete|lint) + ;; + *) + ;; +esac +`, + 'utf8', + ); + await fs.chmod(path.join(binDir, 'npm'), 0o755); + + return { + root, + scriptPath: path.join(scriptsDir, 'run-scheduler-cycle.mjs'), + env: { + ...process.env, + PATH: `${binDir}:${process.env.PATH}`, + }, + }; +} + +test('fails required memory policy when retrieval/storage evidence is missing', { concurrency: false }, async () => { + const fixture = await setupFixture({ + memoryPolicy: { + mode: 'required', + retrieveCommand: 'echo RETRIEVE_WITHOUT_MARKER', + storeCommand: 'echo STORE_WITHOUT_MARKER', + retrieveSuccessMarkers: ['MEMORY_RETRIEVED'], + storeSuccessMarkers: ['MEMORY_STORED'], + retrieveArtifacts: [], + storeArtifacts: [], + }, + }); + + const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], { + cwd: fixture.root, + env: fixture.env, + }); + + assert.equal(result.code, 1, `stdout: ${result.stdout}\nstderr: ${result.stderr}`); + const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily')); + const failedLog = logs.find((name) => name.endsWith('__failed.md')); + assert.ok(failedLog); + const failedBody = await fs.readFile(path.join(fixture.root, 'task-logs', 'daily', failedLog), 'utf8'); + assert.match(failedBody, /Required memory steps not verified/); +}); + +test('accepts required memory policy when markers or artifacts are produced', { concurrency: false }, async () => { + const fixture = await setupFixture({ + memoryPolicy: { + mode: 'required', + retrieveCommand: 'echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-daily.ok', + storeCommand: 'echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-daily.ok', + retrieveSuccessMarkers: ['MEMORY_RETRIEVED'], + storeSuccessMarkers: ['MEMORY_STORED'], + retrieveArtifacts: ['.scheduler-memory/retrieve-daily.ok'], + storeArtifacts: ['.scheduler-memory/store-daily.ok'], + }, + }); + + const result = await runNode(fixture.scriptPath, ['--cadence', 'daily'], { + cwd: fixture.root, + env: fixture.env, + }); + + assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`); + + const logs = await fs.readdir(path.join(fixture.root, 'task-logs', 'daily')); + assert.ok(logs.some((name) => name.endsWith('__completed.md'))); + + await fs.access(path.join(fixture.root, '.scheduler-memory', 'retrieve-daily.ok')); + await fs.access(path.join(fixture.root, '.scheduler-memory', 'store-daily.ok')); +}); diff --git a/torch-config.json b/torch-config.json index dfb17d4..ab630b6 100644 --- a/torch-config.json +++ b/torch-config.json @@ -20,30 +20,38 @@ }, "memoryPolicyByCadence": { "daily": { - "mode": "optional", - "retrieveCommand": null, - "storeCommand": null, + "mode": "required", + "retrieveCommand": "echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-daily.ok", + "storeCommand": "echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-daily.ok", "retrieveSuccessMarkers": [ "MEMORY_RETRIEVED" ], "storeSuccessMarkers": [ "MEMORY_STORED" ], - "retrieveArtifacts": [], - "storeArtifacts": [] + "retrieveArtifacts": [ + ".scheduler-memory/retrieve-daily.ok" + ], + "storeArtifacts": [ + ".scheduler-memory/store-daily.ok" + ] }, "weekly": { - "mode": "optional", - "retrieveCommand": null, - "storeCommand": null, + "mode": "required", + "retrieveCommand": "echo MEMORY_RETRIEVED && mkdir -p .scheduler-memory && : > .scheduler-memory/retrieve-weekly.ok", + "storeCommand": "echo MEMORY_STORED && mkdir -p .scheduler-memory && : > .scheduler-memory/store-weekly.ok", "retrieveSuccessMarkers": [ "MEMORY_RETRIEVED" ], "storeSuccessMarkers": [ "MEMORY_STORED" ], - "retrieveArtifacts": [], - "storeArtifacts": [] + "retrieveArtifacts": [ + ".scheduler-memory/retrieve-weekly.ok" + ], + "storeArtifacts": [ + ".scheduler-memory/store-weekly.ok" + ] } } }