mirror of
https://github.com/PR0M3TH3AN/TORCH.git
synced 2026-10-27 07:51:52 +00:00
Fix host-repo scheduler config resolution and handoff defaults
This commit is contained in:
1 parent
723d9c8b53
commit
1fa4d4560e
6 files changed
+153
-4
No files matched your search
@@ -41,6 +41,7 @@
|
||||
// Source of truth: numbered MUST steps 2 and 4-16 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 fsSync from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { randomUUID } from 'node:crypto';
|
||||
|
||||
@@ -72,6 +73,7 @@ import {
|
||||
} from './scheduler-lock.mjs';
|
||||
|
||||
import { detectPlatform } from '../../src/utils.mjs';
|
||||
import { getTorchConfigPath } from '../../src/torch-config.mjs';
|
||||
|
||||
const VALID_CADENCES = new Set(['daily', 'weekly']);
|
||||
const ALL_EXCLUDED_REASON = 'All roster tasks currently claimed by other agents';
|
||||
@@ -328,9 +330,31 @@ function selectNextAgent({ roster, excludedSet, previousAgent, firstPrompt }) {
|
||||
* @returns {Promise<Object>} Normalized scheduler config object.
|
||||
*/
|
||||
async function getSchedulerConfig(cadence, { isInteractive }) {
|
||||
const cfg = await readJson(path.resolve(process.cwd(), 'torch-config.json'), {});
|
||||
const configPath = getTorchConfigPath();
|
||||
const cfg = await readJson(configPath, {});
|
||||
const configDir = path.dirname(configPath);
|
||||
const runtimeDir = process.cwd();
|
||||
|
||||
const resolveNodeCommand = (command) => {
|
||||
if (typeof command !== 'string') return command;
|
||||
const trimmed = command.trim();
|
||||
if (!trimmed.startsWith('node ')) return trimmed;
|
||||
const parts = trimmed.split(/\s+/);
|
||||
if (parts.length < 2) return trimmed;
|
||||
const scriptArg = parts[1];
|
||||
if (!scriptArg || scriptArg.startsWith('-') || path.isAbsolute(scriptArg)) return trimmed;
|
||||
const absoluteScriptPath = path.resolve(configDir, scriptArg);
|
||||
if (!fsSync.existsSync(absoluteScriptPath)) return trimmed;
|
||||
const rewrittenScriptPath = path.relative(runtimeDir, absoluteScriptPath).split(path.sep).join('/');
|
||||
if (!rewrittenScriptPath || rewrittenScriptPath.startsWith('..')) return trimmed;
|
||||
parts[1] = rewrittenScriptPath;
|
||||
return parts.join(' ');
|
||||
};
|
||||
|
||||
const scheduler = cfg.scheduler || {};
|
||||
const handoffCommand = scheduler.handoffCommandByCadence?.[cadence] || null;
|
||||
const defaultHandoffCommand = resolveNodeCommand('node scripts/agent/run-selected-prompt.mjs');
|
||||
const handoffCommandRaw = scheduler.handoffCommandByCadence?.[cadence] || defaultHandoffCommand;
|
||||
const handoffCommand = resolveNodeCommand(handoffCommandRaw);
|
||||
const missingHandoffCommandForMode = !isInteractive && !handoffCommand;
|
||||
const memoryPolicyRaw = scheduler.memoryPolicyByCadence?.[cadence] || {};
|
||||
const mode = memoryPolicyRaw.mode === 'required' ? 'required' : 'optional';
|
||||
@@ -390,10 +414,10 @@ async function getSchedulerConfig(cadence, { isInteractive }) {
|
||||
memoryPolicy: {
|
||||
mode,
|
||||
retrieveCommand: typeof memoryPolicyRaw.retrieveCommand === 'string' && memoryPolicyRaw.retrieveCommand.trim()
|
||||
? memoryPolicyRaw.retrieveCommand.trim()
|
||||
? resolveNodeCommand(memoryPolicyRaw.retrieveCommand.trim())
|
||||
: null,
|
||||
storeCommand: typeof memoryPolicyRaw.storeCommand === 'string' && memoryPolicyRaw.storeCommand.trim()
|
||||
? memoryPolicyRaw.storeCommand.trim()
|
||||
? resolveNodeCommand(memoryPolicyRaw.storeCommand.trim())
|
||||
: null,
|
||||
retrieveSuccessMarkers: normalizeStringList(memoryPolicyRaw.retrieveSuccessMarkers, ['MEMORY_RETRIEVED']),
|
||||
storeSuccessMarkers: normalizeStringList(memoryPolicyRaw.storeSuccessMarkers, ['MEMORY_STORED']),
|
||||
|
||||
@@ -381,9 +381,15 @@ function configureTorch(cwd, paths, installDir, namespace, relays, hashtag) {
|
||||
|
||||
// Configure memory policy with correct paths
|
||||
if (!configData.scheduler) configData.scheduler = {};
|
||||
if (!configData.scheduler.handoffCommandByCadence) {
|
||||
configData.scheduler.handoffCommandByCadence = {};
|
||||
}
|
||||
// Always ensure memory policy exists and points to the correct scripts
|
||||
// We use the installDir to construct the path
|
||||
const scriptPrefix = installDir === '.' ? '' : `${installDir}/`;
|
||||
const handoffPath = `node ${scriptPrefix}scripts/agent/run-selected-prompt.mjs`;
|
||||
configData.scheduler.handoffCommandByCadence.daily = handoffPath;
|
||||
configData.scheduler.handoffCommandByCadence.weekly = handoffPath;
|
||||
|
||||
if (!configData.scheduler.memoryPolicyByCadence) {
|
||||
configData.scheduler.memoryPolicyByCadence = {};
|
||||
|
||||
@@ -182,4 +182,14 @@ test('cmdInit creates torch-config.json with random namespace', async () => {
|
||||
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
assert.strictEqual(config.nostrLock.namespace, 'test-namespace', 'Namespace should match mock');
|
||||
assert.strictEqual(config.dashboard.hashtag, 'test-hashtag', 'Hashtag should match mock');
|
||||
assert.strictEqual(
|
||||
config.scheduler.handoffCommandByCadence.daily,
|
||||
'node torch/scripts/agent/run-selected-prompt.mjs',
|
||||
'daily handoff command should be configured for host installs',
|
||||
);
|
||||
assert.strictEqual(
|
||||
config.scheduler.handoffCommandByCadence.weekly,
|
||||
'node torch/scripts/agent/run-selected-prompt.mjs',
|
||||
'weekly handoff command should be configured for host installs',
|
||||
);
|
||||
});
|
||||
@@ -210,6 +210,110 @@ test('accepts required memory policy when markers or artifacts are produced', {
|
||||
await fs.access(path.join(fixture.root, '.scheduler-memory', 'store-daily.ok'));
|
||||
});
|
||||
|
||||
test('host-mode scheduler resolves parent config and rewrites torch-prefixed node commands', { concurrency: false }, async () => {
|
||||
const root = await fs.mkdtemp(path.join(os.tmpdir(), 'scheduler-host-mode-'));
|
||||
const torchDir = path.join(root, 'torch');
|
||||
const scriptsDir = path.join(torchDir, 'scripts', 'agent');
|
||||
const binDir = path.join(root, 'bin');
|
||||
|
||||
await fs.mkdir(path.join(torchDir, 'src', 'prompts', 'daily'), { recursive: true });
|
||||
await fs.mkdir(path.join(torchDir, 'scripts', 'memory'), { recursive: true });
|
||||
await fs.mkdir(scriptsDir, { recursive: true });
|
||||
await fs.mkdir(binDir, { recursive: true });
|
||||
|
||||
try {
|
||||
await fs.symlink(path.resolve('node_modules'), path.join(torchDir, 'node_modules'));
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
|
||||
await fs.copyFile(SOURCE_SCRIPT, path.join(scriptsDir, 'run-scheduler-cycle.mjs'));
|
||||
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
|
||||
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
|
||||
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(torchDir, 'src', 'utils.mjs'));
|
||||
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(torchDir, 'src', 'torch-config.mjs'));
|
||||
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(torchDir, 'src', 'constants.mjs'));
|
||||
await fs.copyFile(path.resolve('src/relay-health.mjs'), path.join(torchDir, 'src', 'relay-health.mjs'));
|
||||
await fs.copyFile(path.resolve('src/lock-ops.mjs'), path.join(torchDir, 'src', 'lock-ops.mjs'));
|
||||
|
||||
await fs.writeFile(path.join(torchDir, 'scripts', 'agent', 'verify-run-artifacts.mjs'), '#!/usr/bin/env node\nprocess.exit(0);\n', 'utf8');
|
||||
await fs.writeFile(path.join(torchDir, 'src', 'prompts', 'roster.json'), JSON.stringify({ daily: ['agent-a'] }, null, 2), 'utf8');
|
||||
await fs.writeFile(path.join(torchDir, 'src', 'prompts', 'daily', 'agent-a.md'), '# agent-a\n', 'utf8');
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(torchDir, 'scripts', 'memory', 'retrieve.mjs'),
|
||||
'import fs from "node:fs/promises"; await fs.mkdir(".scheduler-memory", { recursive: true }); await fs.writeFile(".scheduler-memory/retrieve-daily.ok", "ok\\n", "utf8"); console.log("MEMORY_RETRIEVED");\n',
|
||||
'utf8',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(torchDir, 'scripts', 'memory', 'store.mjs'),
|
||||
'import fs from "node:fs/promises"; await fs.mkdir(".scheduler-memory", { recursive: true }); await fs.writeFile(".scheduler-memory/store-daily.ok", "ok\\n", "utf8"); console.log("MEMORY_STORED");\n',
|
||||
'utf8',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(torchDir, 'scripts', 'agent', 'run-selected-prompt.mjs'),
|
||||
'#!/usr/bin/env node\nconsole.log("HANDOFF_OK");\n',
|
||||
'utf8',
|
||||
);
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(root, 'torch-config.json'),
|
||||
JSON.stringify({
|
||||
scheduler: {
|
||||
firstPromptByCadence: { daily: 'agent-a' },
|
||||
handoffCommandByCadence: { daily: 'node torch/scripts/agent/run-selected-prompt.mjs' },
|
||||
validationCommandsByCadence: { daily: ['true'] },
|
||||
memoryPolicyByCadence: {
|
||||
daily: {
|
||||
mode: 'required',
|
||||
retrieveCommand: 'node torch/scripts/memory/retrieve.mjs',
|
||||
storeCommand: 'node torch/scripts/memory/store.mjs',
|
||||
retrieveSuccessMarkers: ['MEMORY_RETRIEVED'],
|
||||
storeSuccessMarkers: ['MEMORY_STORED'],
|
||||
retrieveArtifacts: ['.scheduler-memory/retrieve-daily.ok'],
|
||||
storeArtifacts: ['.scheduler-memory/store-daily.ok'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}, 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);
|
||||
|
||||
const result = await runNode(path.join(scriptsDir, 'run-scheduler-cycle.mjs'), ['--cadence', 'daily'], {
|
||||
cwd: torchDir,
|
||||
env: {
|
||||
...process.env,
|
||||
PATH: `${binDir}:${process.env.PATH}`,
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.code, 0, `stdout: ${result.stdout}\nstderr: ${result.stderr}`);
|
||||
assert.match(result.stdout, /Status:\s+completed/);
|
||||
await fs.access(path.join(torchDir, '.scheduler-memory', 'retrieve-daily.ok'));
|
||||
await fs.access(path.join(torchDir, '.scheduler-memory', 'store-daily.ok'));
|
||||
});
|
||||
|
||||
test('records backend failure metadata when lock command exits with code 2', { concurrency: false }, async () => {
|
||||
const fixture = await setupFixture({
|
||||
memoryPolicy: { mode: 'optional' },
|
||||
|
||||
@@ -96,6 +96,7 @@ async function setupFixture({ lockShellBody }) {
|
||||
await fs.copyFile(path.resolve('scripts/agent/scheduler-utils.mjs'), path.join(scriptsDir, 'scheduler-utils.mjs'));
|
||||
await fs.copyFile(path.resolve('scripts/agent/scheduler-lock.mjs'), path.join(scriptsDir, 'scheduler-lock.mjs'));
|
||||
await fs.copyFile(path.resolve('src/utils.mjs'), path.join(root, 'src', 'utils.mjs'));
|
||||
await fs.copyFile(path.resolve('src/torch-config.mjs'), path.join(root, 'src', 'torch-config.mjs'));
|
||||
await fs.copyFile(path.resolve('src/constants.mjs'), path.join(root, 'src', 'constants.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), 'utf8');
|
||||
|
||||
@@ -26,6 +26,10 @@
|
||||
"daily": "scheduler-update-agent",
|
||||
"weekly": "bug-reproducer-agent"
|
||||
},
|
||||
"handoffCommandByCadence": {
|
||||
"daily": "node torch/scripts/agent/run-selected-prompt.mjs",
|
||||
"weekly": "node torch/scripts/agent/run-selected-prompt.mjs"
|
||||
},
|
||||
"memoryPolicyByCadence": {
|
||||
"daily": {
|
||||
"mode": "required",
|
||||
|
||||
Reference in new issue
Block a user