fix(agent-pod): stateless role pool (/new per reuse), never commit PLAN.md
This commit is contained in:
@@ -19,8 +19,11 @@ data:
|
||||
// task to need a role spawns it, every later task for that role reuses the
|
||||
// same tmux pane via `tmux send-keys` (see runOnPool) -- the same nudge
|
||||
// mechanism that used to only fire on a stall now doubles as "give this
|
||||
// agent its next task." A role's conversation history accumulates across
|
||||
// every task it ever handles for that repo; nothing resets it mid-pipeline.
|
||||
// agent its next task." Every role reads everything it needs fresh off disk
|
||||
// each call, so every pane gets a `/new` before every reuse instead of
|
||||
// accumulating history that degrades and eventually errors out task after
|
||||
// task -- same pane, same agent-manager session, zero memory of the last
|
||||
// task it handled.
|
||||
// Because only one implementer/judge/etc. exists per repo, tasks within a
|
||||
// phase run strictly sequentially against the pool -- no per-task worktree,
|
||||
// no per-task branch, no merge-back step; every task commits straight onto
|
||||
@@ -85,6 +88,22 @@ data:
|
||||
|
||||
const ROLE_SKILLS = new Set(["planner", "investigator", "info-collector", "implementer", "judge", "resolver"]);
|
||||
|
||||
// Every role reads everything it needs fresh off disk each call -- PLAN.md,
|
||||
// the task spec, judge's verdict file, `git diff` against baseBranch --
|
||||
// nothing depends on remembering earlier tasks. Left to accumulate, a
|
||||
// pooled session's conversation grows without bound across every task in a
|
||||
// repo and both correctness and reliability degrade hard once it does
|
||||
// (observed: a planner session at ~1.5M cumulative tokens started erroring
|
||||
// out every call, an investigator session that far gone started narrating a
|
||||
// different codebase entirely). So every role gets reset to a clean
|
||||
// conversation before every reuse instead of just being nudged with the
|
||||
// next prompt -- same pane, same agent-manager session (still
|
||||
// visible/attachable), zero history carried between tasks.
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
const HARD_RULES =
|
||||
"Read and follow ~/.pi/agent/skills/karpathy-guidelines/SKILL.md and " +
|
||||
"~/.pi/agent/skills/caveman/SKILL.md as hard rules for this entire task, before anything else. ";
|
||||
@@ -215,11 +234,14 @@ data:
|
||||
|
||||
// Runs one task's worth of work on a persistent per-role agent: spawns the
|
||||
// role's session the first time it's ever needed for this repo, sends every
|
||||
// later prompt into that same tmux pane via send-keys. pool is a plain
|
||||
// object keyed by role name ("planner"/"investigator"/"implementer"/
|
||||
// "judge"), shared across every task in a repo's pipeline (see
|
||||
// runRepoPipeline) -- it IS the 4-agent pool, one entry per role, filled in
|
||||
// lazily as each role gets its first task.
|
||||
// later prompt into that same tmux pane via send-keys -- prefixed with a
|
||||
// `/new` first, so the pane and agent-manager session stay the same but the
|
||||
// model starts that prompt with a clean conversation, no history carried
|
||||
// over from whatever task this role last handled. pool is a plain object
|
||||
// keyed by role name ("planner"/"investigator"/"implementer"/"judge"),
|
||||
// shared across every task in a repo's pipeline (see runRepoPipeline) -- it
|
||||
// IS the 4-agent pool, one entry per role, filled in lazily as each role
|
||||
// gets its first task.
|
||||
async function runOnPool(pool, cwd, repoId, role, prompt, sentinelFile) {
|
||||
fs.rmSync(sentinelFile, { force: true });
|
||||
const label = `${repoId}-${role}`;
|
||||
@@ -235,6 +257,8 @@ data:
|
||||
target = amSessionName(spawned.out);
|
||||
pool[role] = target;
|
||||
} else {
|
||||
await runAmTmux(["send-keys", "-t", target, "/new", "Enter"]);
|
||||
await sleep(1000);
|
||||
await runAmTmux(["send-keys", "-t", target, HARD_RULES + prompt, "Enter"]);
|
||||
}
|
||||
|
||||
@@ -257,14 +281,16 @@ data:
|
||||
|
||||
function plannerPrompt(task, specHint) {
|
||||
return (
|
||||
`Use the planner skill to draft PLAN.md for task ${task}, reading its spec (${specHint}). Commit PLAN.md. ` +
|
||||
`Use the planner skill to draft PLAN.md for task ${task}, reading its spec (${specHint}). PLAN.md is scratch ` +
|
||||
`state for this harness, not a deliverable -- do NOT commit it or add it to git. ` +
|
||||
`Then run: touch .stage-done-${task}-planner`
|
||||
);
|
||||
}
|
||||
|
||||
function investigatorPrompt(task) {
|
||||
return (
|
||||
`Use the investigator skill to confirm PLAN.md against real sources for task ${task}, append findings, commit. ` +
|
||||
`Use the investigator skill to confirm PLAN.md against real sources for task ${task}, append findings. ` +
|
||||
`PLAN.md is scratch state for this harness, not a deliverable -- do NOT commit it or add it to git. ` +
|
||||
`Then run: touch .stage-done-${task}-investigator`
|
||||
);
|
||||
}
|
||||
@@ -291,9 +317,12 @@ data:
|
||||
// PLAN.md, investigator confirms it, then implementer and judge go back and
|
||||
// forth -- judge's FAIL rationale lands in .task-result-<task>, which the
|
||||
// next implementer attempt is told to read and address. After
|
||||
// MAX_IMPLEMENT_ATTEMPTS straight fails, the SAME planner agent is asked to
|
||||
// judge whether the plan itself is wrong (it still remembers drafting it);
|
||||
// if so it revises PLAN.md and the implementer gets a fresh attempt budget.
|
||||
// MAX_IMPLEMENT_ATTEMPTS straight fails, the planner role is asked to judge
|
||||
// whether the plan itself is wrong -- fresh conversation, same as any other
|
||||
// planner call, reading PLAN.md/the judge feedback/the
|
||||
// diff off disk rather than remembering having drafted the original plan.
|
||||
// If it decides the approach is wrong it revises PLAN.md and the implementer
|
||||
// gets a fresh attempt budget.
|
||||
// MAX_PLAN_REVISIONS caps this from looping forever on a task that's
|
||||
// genuinely stuck. All work happens directly in cwd (the repo's one shared
|
||||
// clone, currently checked out to the phase branch) -- no worktree, since
|
||||
@@ -326,6 +355,12 @@ data:
|
||||
};
|
||||
};
|
||||
|
||||
// PLAN.md is scratch state for this one task, not a deliverable (see
|
||||
// plannerPrompt/investigatorPrompt -- it's gitignored too, as a backstop
|
||||
// in case an agent commits it anyway). Discard it once the task is done,
|
||||
// whatever the outcome, so it never bleeds into the next task's planner
|
||||
// call or sits around as stale harness clutter in the shared clone.
|
||||
try {
|
||||
if (judgeOnly) {
|
||||
const quick = await stage(
|
||||
"judge",
|
||||
@@ -389,9 +424,10 @@ data:
|
||||
"planner",
|
||||
`Implementer failed judge review ${MAX_IMPLEMENT_ATTEMPTS} times in a row for task ${task}. Read PLAN.md, ` +
|
||||
`the judge's feedback in .task-result-${task}, and the current diff against ${baseBranch}...HEAD. Decide ` +
|
||||
`whether the plan's approach itself is wrong, not just the implementation -- if so, revise PLAN.md and ` +
|
||||
`commit. If you change the approach, also use the investigator skill to confirm the new approach against ` +
|
||||
`real sources before committing. If the plan is sound, note why in PLAN.md and leave it as-is. Then run: ` +
|
||||
`whether the plan's approach itself is wrong, not just the implementation -- if so, revise PLAN.md. If ` +
|
||||
`you change the approach, also use the investigator skill to confirm the new approach against real ` +
|
||||
`sources. If the plan is sound, note why in PLAN.md and leave it as-is. PLAN.md is scratch state for ` +
|
||||
`this harness, not a deliverable -- do NOT commit it or add it to git. Then run: ` +
|
||||
`touch .stage-done-${task}-planner-revise-${planRevisions}`,
|
||||
path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`),
|
||||
"planner-revise"
|
||||
@@ -409,6 +445,9 @@ data:
|
||||
return { task, status: "unresolved", judgeRationale: resultText, implementAttempts: implementAttempt, planRevisions };
|
||||
}
|
||||
return { task, status: verdict === "PASS" ? "done" : "done-with-concerns", judgeRationale: resultText };
|
||||
} finally {
|
||||
fs.rmSync(path.join(cwd, "PLAN.md"), { force: true });
|
||||
}
|
||||
}
|
||||
|
||||
// Runs every task in a phase (no declared dependency between them) strictly
|
||||
@@ -544,6 +583,9 @@ data:
|
||||
".task-result-*",
|
||||
".phase-result-*",
|
||||
".stage-done-*",
|
||||
"",
|
||||
"# agent-harness: PLAN.md is per-task planner scratch state, never a deliverable",
|
||||
"PLAN.md",
|
||||
].join("\n");
|
||||
fs.appendFileSync(path.join(cwd, ".gitignore"), gitignoreAdditions + "\n");
|
||||
await runGit(cwd, ["add", ".gitignore"]);
|
||||
|
||||
Reference in New Issue
Block a user