From e204f0c0503f00d909946664634e8476dc7bebdf Mon Sep 17 00:00:00 2001 From: Story Crater Bot <19826264+Riotpiaole@users.noreply.github.com> Date: Tue, 18 Aug 2026 18:42:45 -0700 Subject: [PATCH] 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. --- k8s/apps/agent-pod/coordinator-configmap.yaml | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/k8s/apps/agent-pod/coordinator-configmap.yaml b/k8s/apps/agent-pod/coordinator-configmap.yaml index 8783639..ca8a548 100644 --- a/k8s/apps/agent-pod/coordinator-configmap.yaml +++ b/k8s/apps/agent-pod/coordinator-configmap.yaml @@ -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 "## — ..." 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 --tasks T0.1,T0.2;T1.1,T1.2,... [--base main] [--branch ]"); + if (!opts.repo) { + console.error( + "usage: coordinator.js --repo [--tasks T0.1,T0.2;T1.1,T1.2,...] [--base main] [--branch ]\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 };