fix(agent-pod): sync coordinator.js ConfigMap, was stale since auto-discovery landed

The pod's coordinator-src ConfigMap still had the pre-auto-discovery version -- --tasks was required, no task-board parsing, no self-chained stages, no judge model routing. Regenerated from the current source.
This commit is contained in:
Story Crater Bot
2026-08-18 18:42:45 -07:00
parent fd0aa901fb
commit e204f0c050
+86 -12
View File
@@ -43,6 +43,19 @@ data:
const AGENT_PROVIDER = process.env.AGENT_PROVIDER || "";
const AGENT_MODEL = process.env.AGENT_MODEL || "";
// judge (both per-task and phase-judge -- both spawn with stageLabel
// "judge", see runTaskInteractive/runPipeline) can run a different model
// than the rest of the chain, e.g. homelab-reasoning instead of
// homelab-ornith now that verifier/PRM is retired. Falls back to
// AGENT_PROVIDER/AGENT_MODEL when unset, so a run that doesn't care keeps
// one uniform model everywhere, same as before this existed.
const JUDGE_PROVIDER = process.env.JUDGE_PROVIDER || AGENT_PROVIDER;
const JUDGE_MODEL = process.env.JUDGE_MODEL || AGENT_MODEL;
function providerModelFor(stageLabel) {
return stageLabel === "judge" ? { provider: JUDGE_PROVIDER, model: JUDGE_MODEL } : { provider: AGENT_PROVIDER, model: AGENT_MODEL };
}
// agent-manager's private tmux server and session-naming scheme
// (internal/tmux/tmux.go: defaultSocket = "agentmgr", sessionName(id) =
// "am_"+id) -- stable, documented internals of the fork, used here only
@@ -174,8 +187,9 @@ data:
function spawnCommandLine(cwd, task, stageLabel, prompt) {
const label = `${taskSessionLabel(task)}-${stageLabel}`;
const parts = [AGENT_MANAGER_BIN, "spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--prompt", HARD_RULES + prompt];
if (AGENT_PROVIDER) parts.push("--provider", AGENT_PROVIDER);
if (AGENT_MODEL) parts.push("--model", AGENT_MODEL);
const { provider, model } = providerModelFor(stageLabel);
if (provider) parts.push("--provider", provider);
if (model) parts.push("--model", model);
return parts.map(shQuote).join(" ");
}
@@ -296,8 +310,9 @@ data:
fs.rmSync(sentinelFile, { force: true });
const spawnArgs = ["spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--prompt", stagePrompt];
if (AGENT_PROVIDER) spawnArgs.push("--provider", AGENT_PROVIDER);
if (AGENT_MODEL) spawnArgs.push("--model", AGENT_MODEL);
const { provider, model } = providerModelFor(stageLabel);
if (provider) spawnArgs.push("--provider", provider);
if (model) spawnArgs.push("--model", model);
const spawned = await runCmd(AGENT_MANAGER_BIN, spawnArgs);
if (spawned.code !== 0) return { ok: false, crashed: true, error: spawned.out, sessionName: label };
const sessionId = spawned.out;
@@ -508,7 +523,18 @@ data:
return { task, status: verdict === "PASS" ? "done" : "done-with-concerns", judgeRationale: resultText };
}
const PHASE_CONCURRENCY = 3;
// How many tasks' independent chains can be mid-flight at once within a
// phase. This is the real buffer against a single-instance backend: a
// lock around agent-manager spawn wouldn't work (spawn is fire-and-forget
// -- it creates the tmux session and returns immediately, the actual LLM
// call happens later, asynchronously, inside that detached session, so a
// lock held only for the spawn call releases before the call it's
// supposed to guard even starts). Ollama itself queues concurrent
// requests to one loaded model rather than erroring, but each one waits
// out the others -- PHASE_CONCURRENCY=1 makes that explicit: tasks run
// strictly one after another, one chain ever active against the backend
// at a time. Bump it back up once there's more than one model instance.
const PHASE_CONCURRENCY = Number(process.env.PHASE_CONCURRENCY) || 3;
// Runs one phase (a batch of tasks with no declared dependency on each
// other) with up to PHASE_CONCURRENCY tasks in flight at once. Each task
@@ -568,6 +594,37 @@ data:
}
}
// Discovers phases/tasks from the repo's own tasks/INDEX.md instead of
// requiring the caller to pass --tasks. Matches this convention's board
// shape (see e.g. Poimen/agent-rust's tasks/INDEX.md): a numbered phase
// heading ("## 1 — Foundations · T0.x"), followed by a markdown table
// whose rows link to each task's own spec file ("| [T0.1](T0.1-....md) |
// ... |"). Headings that aren't a numbered phase (prose sections like
// "## Ordering — declared, never derived", "## Progress") are skipped --
// only "## <digits> — ..." starts a new phase. Returns null if
// tasks/INDEX.md doesn't exist; an empty array if it exists but no phase
// yielded any task rows.
function parseTaskBoard(cwd) {
const indexPath = path.join(cwd, "tasks", "INDEX.md");
if (!fs.existsSync(indexPath)) return null;
const phaseHeaderRe = /^##\s+\d+\s+—/;
const taskRowRe = /^\|\s*\[([A-Za-z0-9.]+)\]\(/;
const phases = [];
let current = null;
for (const line of fs.readFileSync(indexPath, "utf8").split("\n")) {
if (phaseHeaderRe.test(line)) {
current = [];
phases.push(current);
continue;
}
const m = line.match(taskRowRe);
if (m && current) current.push(m[1]);
}
return phases.filter((phase) => phase.length > 0);
}
function phaseLabelFor(phaseTasks, index) {
const first = phaseTasks[0];
const id = typeof first === "string" ? first : first.id;
@@ -593,14 +650,12 @@ data:
const cwd = path.join(WORK_DIR, pipelineId);
fs.mkdirSync(cwd, { recursive: true });
const phases = Array.isArray(tasks[0]) ? tasks : [tasks];
const pipelineSession = {
id: pipelineId,
status: "running",
taskResults: [],
activeTasks: {},
totalTasks: phases.flat().length,
totalTasks: 0,
startedAt: new Date().toISOString(),
};
@@ -615,6 +670,22 @@ data:
if (clone.code !== 0) return finish("clone-crashed");
if (!fs.existsSync(path.join(cwd, ".git"))) return finish("clone-missing");
// No --tasks given: discover the phase/task board from the repo's own
// tasks/INDEX.md (see parseTaskBoard) instead of requiring the caller to
// already know it. Each discovered task runs judgeOnly first -- a cheap
// "is this already done on this branch" check against the board's own
// possibly-stale checkmarks, falling through to the full planner/
// investigator/implementer/judge flow only when it isn't.
let phases = tasks ? (Array.isArray(tasks[0]) ? tasks : [tasks]) : parseTaskBoard(cwd);
if (!phases || phases.length === 0) {
pipelineSession.gitError = "no --tasks given and tasks/INDEX.md not found or empty";
return finish("no-tasks-found");
}
if (!tasks) {
phases = phases.map((phase) => phase.map((id) => ({ id, judgeOnly: true })));
}
pipelineSession.totalTasks = phases.flat().length;
for (let i = 0; i < phases.length; i++) {
const phaseTasks = phases[i];
const phaseLabel = phaseLabelFor(phaseTasks, i);
@@ -734,11 +805,14 @@ data:
async function main() {
const opts = parseArgs(process.argv.slice(2));
if (!opts.repo || !opts.tasks) {
console.error("usage: coordinator.js --repo <url> --tasks T0.1,T0.2;T1.1,T1.2,... [--base main] [--branch <name>]");
if (!opts.repo) {
console.error(
"usage: coordinator.js --repo <url> [--tasks T0.1,T0.2;T1.1,T1.2,...] [--base main] [--branch <name>]\n" +
" --tasks omitted: discovers phases from the cloned repo's own tasks/INDEX.md"
);
process.exit(1);
}
const phases = opts.tasks.split(";").map((phase) => phase.split(","));
const phases = opts.tasks ? opts.tasks.split(";").map((phase) => phase.split(",")) : null;
const pipelineId = require("node:crypto").randomUUID();
const result = await runPipeline({ pipelineId, repo: opts.repo, baseBranch: opts.base, tasks: phases, branchName: opts.branch });
process.exit(result.status === "completed" ? 0 : 1);
@@ -748,4 +822,4 @@ data:
main();
}
module.exports = { runPipeline, spawnCommandLine, spawnAndWaitStage, waitForStage };
module.exports = { runPipeline, spawnCommandLine, spawnAndWaitStage, waitForStage, parseTaskBoard };