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:
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user