From 19cc4062b4514e0277f5510047f13d7369176144 Mon Sep 17 00:00:00 2001 From: Story Crater Bot <19826264+Riotpiaole@users.noreply.github.com> Date: Wed, 19 Aug 2026 11:26:33 -0700 Subject: [PATCH] fix(agent-pod): absolute paths for every sentinel/verdict file, cwd reminder per call A pooled session's shell cwd drifts as it explores the repo between turns. Seen live: a repo whose internal workspace dir is one letter off from the repo's own directory name was enough for the agent to touch its sentinel one level off from where coordinator watches for it -- coordinator waited out the full timeout for a file that existed, just in the wrong place. --- k8s/apps/agent-pod/coordinator-configmap.yaml | 60 ++++++++++++------- 1 file changed, 37 insertions(+), 23 deletions(-) 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`);