diff --git a/k8s/apps/agent-pod/coordinator-configmap.yaml b/k8s/apps/agent-pod/coordinator-configmap.yaml index 9899f6e..283ea66 100644 --- a/k8s/apps/agent-pod/coordinator-configmap.yaml +++ b/k8s/apps/agent-pod/coordinator-configmap.yaml @@ -246,8 +246,21 @@ data: const label = `${repoId}-${role}`; let target = pool[role]; + // A pooled session's shell cwd drifts as it explores the repo (e.g. cd + // into a Rust workspace subdirectory to read source) and nothing resets + // it back between turns. Seen in practice: a repo whose own internal + // workspace folder is one letter off from the repo's own directory name + // ("poiman" the repo vs. "poimen" the crate workspace inside it) was + // enough for the agent to touch its sentinel one level off from where + // this function is watching for it -- coordinator waits out the full + // STAGE_TIMEOUT_MS for a file that already exists, just in the wrong + // place. State the absolute target directory and use absolute paths for + // every filesystem instruction, so there's nothing for the agent to get + // wrong by reasoning about a relative "current directory." + const cwdReminder = `Your working directory for this task is ${cwd} -- if your shell isn't already there, run: cd ${cwd}\n\n`; + if (!target) { - const spawnArgs = ["spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--prompt", HARD_RULES + prompt]; + const spawnArgs = ["spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--prompt", cwdReminder + HARD_RULES + prompt]; const { provider, model } = providerModelFor(role); if (provider) spawnArgs.push("--provider", provider); if (model) spawnArgs.push("--model", model); @@ -258,7 +271,7 @@ data: } else { await runAmTmux(["send-keys", "-t", target, "/new", "Enter"]); await sleep(1000); - await runAmTmux(["send-keys", "-t", target, HARD_RULES + prompt, "Enter"]); + await runAmTmux(["send-keys", "-t", target, cwdReminder + HARD_RULES + prompt, "Enter"]); } let ok = await waitForSentinel(sentinelFile, STAGE_TIMEOUT_MS); @@ -271,55 +284,56 @@ data: `Decide: is it still making real progress and worth nudging to wrap up, or stuck and worth abandoning?` ); if (resolution === "RETRY") { - await runAmTmux(["send-keys", "-t", target, `Please wrap up now and touch ${path.basename(sentinelFile)} when done.`, "Enter"]); + await runAmTmux(["send-keys", "-t", target, `Please wrap up now and run: touch ${sentinelFile}`, "Enter"]); ok = await waitForSentinel(sentinelFile, NUDGE_TIMEOUT_MS); } } return { ok, sessionName: label }; } - function plannerPrompt(task, specHint, judgeOnly) { + function plannerPrompt(task, specHint, judgeOnly, cwd) { // judgeOnly (auto-discovered tasks only, see parseTaskBoard): planner // itself decides whether the task is already done before planning it, // reading tasks/INDEX.md's own status notes plus git log/current code -- // replaces what used to be a separate judge pre-check call. One LLM round // trip instead of two, and the same agent that's about to plan the task // is the one deciding whether planning it is even necessary. + const resultFile = path.join(cwd, `.task-result-${task}`); const decideStep = judgeOnly ? `First, decide whether task ${task} is already fully implemented on this branch: check ` + `\`git log --oneline --grep '${task}'\`, tasks/INDEX.md's own status notes for this task, and the current ` + `code directly against its spec (${specHint})'s acceptance criteria. Write your decision to ` + - `.task-result-${task} as a single "VERDICT: PASS" (already done, no further work needed) or ` + + `${resultFile} as a single "VERDICT: PASS" (already done, no further work needed) or ` + `"VERDICT: FAIL" (needs work) line plus one line of rationale. If VERDICT is FAIL, continue below and ` + `draft the plan in this same turn; if VERDICT is PASS, skip the rest and go straight to the touch step.\n\n` : ""; return ( `${decideStep}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` + `Then run: touch ${path.join(cwd, `.stage-done-${task}-planner`)}` ); } - function investigatorPrompt(task) { + function investigatorPrompt(task, cwd) { return ( `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` + `Then run: touch ${path.join(cwd, `.stage-done-${task}-investigator`)}` ); } - function implementerPrompt(task, attempt, feedbackHint) { + function implementerPrompt(task, attempt, feedbackHint, cwd) { return ( `Use the implementer skill to implement what the current PLAN.md specifies for task ${task} (commit as you go). ` + - `${feedbackHint} Then run: touch .stage-done-${task}-implementer-${attempt}` + `${feedbackHint} Then run: touch ${path.join(cwd, `.stage-done-${task}-implementer-${attempt}`)}` ); } - function judgePrompt(task, baseBranch, attempt) { + function judgePrompt(task, baseBranch, attempt, cwd) { return ( `Use the judge skill to review the diff against ${baseBranch}...HEAD for task ${task}. Write your verdict to ` + - `.task-result-${task} as a single "VERDICT: PASS" or "VERDICT: FAIL" line plus one line of rationale, then ` + - `run: touch .stage-done-${task}-judge-${attempt}` + `${path.join(cwd, `.task-result-${task}`)} as a single "VERDICT: PASS" or "VERDICT: FAIL" line plus one line of rationale, then ` + + `run: touch ${path.join(cwd, `.stage-done-${task}-judge-${attempt}`)}` ); } @@ -377,7 +391,7 @@ data: // 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 { - let result = await stage("planner", plannerPrompt(task, specHint, judgeOnly), path.join(cwd, `.stage-done-${task}-planner`)); + let result = await stage("planner", plannerPrompt(task, specHint, judgeOnly, cwd), path.join(cwd, `.stage-done-${task}-planner`)); if (!result.ok) return abandon("planner", result); if (judgeOnly) { @@ -389,7 +403,7 @@ data: } } - result = await stage("investigator", investigatorPrompt(task), path.join(cwd, `.stage-done-${task}-investigator`)); + result = await stage("investigator", investigatorPrompt(task, cwd), path.join(cwd, `.stage-done-${task}-investigator`)); if (!result.ok) return abandon("investigator", result); let planRevisions = 0; @@ -402,20 +416,20 @@ data: implementAttempt++; const feedbackHint = fs.existsSync(resultFile) ? justRevisedPlan - ? `.task-result-${task} holds the judge's feedback against the OLD plan, which prompted a plan revision -- ` + + ? `${resultFile} holds the judge's feedback against the OLD plan, which prompted a plan revision -- ` + `PLAN.md has since changed. Read the current PLAN.md as the source of truth, not the old feedback verbatim.` - : `A previous judge review exists at .task-result-${task} -- read it and address every issue it raises.` + : `A previous judge review exists at ${resultFile} -- read it and address every issue it raises.` : ""; justRevisedPlan = false; result = await stage( "implementer", - implementerPrompt(task, implementAttempt, feedbackHint), + implementerPrompt(task, implementAttempt, feedbackHint, cwd), path.join(cwd, `.stage-done-${task}-implementer-${implementAttempt}`) ); if (!result.ok) return abandon("implementer", result, implementAttempt); - result = await stage("judge", judgePrompt(task, baseBranch, implementAttempt), path.join(cwd, `.stage-done-${task}-judge-${implementAttempt}`)); + result = await stage("judge", judgePrompt(task, baseBranch, implementAttempt, cwd), path.join(cwd, `.stage-done-${task}-judge-${implementAttempt}`)); if (!result.ok) return abandon("judge", result, implementAttempt); resultText = fs.existsSync(resultFile) ? fs.readFileSync(resultFile, "utf8") : ""; @@ -428,12 +442,12 @@ data: result = await stage( "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 ` + + `the judge's feedback in ${resultFile}, 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. 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}`, + `touch ${path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`)}`, path.join(cwd, `.stage-done-${task}-planner-revise-${planRevisions}`), "planner-revise" ); @@ -626,8 +640,8 @@ data: `individual task already passed its own judge review -- your job here is different: confirm the ` + `tasks integrate correctly as one coherent narrative, and that real integration tests (not just ` + `each task's isolated unit checks) exist and actually exercise the phase's intended use case end ` + - `to end. Write your verdict to .phase-result-${phaseLabel} as a single "VERDICT: PASS" or ` + - `"VERDICT: FAIL" line plus rationale, then run: touch .stage-done-phase-${phaseLabel}-judge`, + `to end. Write your verdict to ${phaseResultFile} as a single "VERDICT: PASS" or ` + + `"VERDICT: FAIL" line plus rationale, then run: touch ${path.join(cwd, `.stage-done-phase-${phaseLabel}-judge`)}`, path.join(cwd, `.stage-done-phase-${phaseLabel}-judge`) ); await commitPending(cwd, `phase: ${phaseLabel} integration review`);