diff --git a/k8s/apps/agent-pod/coordinator-configmap.yaml b/k8s/apps/agent-pod/coordinator-configmap.yaml index 283ea66..2eb5069 100644 --- a/k8s/apps/agent-pod/coordinator-configmap.yaml +++ b/k8s/apps/agent-pod/coordinator-configmap.yaml @@ -260,7 +260,7 @@ data: 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", cwdReminder + HARD_RULES + prompt]; + const spawnArgs = ["spawn", "--tool", "pi", "--cwd", cwd, "--name", label, "--group", repoId, "--prompt", cwdReminder + HARD_RULES + prompt]; const { provider, model } = providerModelFor(role); if (provider) spawnArgs.push("--provider", provider); if (model) spawnArgs.push("--model", model); @@ -474,12 +474,21 @@ data: // judge/etc. exists per repo, so there is no per-task concurrency to have // here anymore (see REPO_CONCURRENCY below for where concurrency now // lives). No worktrees: every task commits directly onto phaseBranch in the - // one shared cwd. - async function runPhase(cwd, baseBranch, phaseTasks, pool, repoId, pipelineSession) { + // one shared cwd. baseBranch here is the TRUE base (e.g. "main") -- judge + // reviews `git diff baseBranch...HEAD`, not phaseBranch...HEAD, which would + // always be empty since HEAD *is* phaseBranch while it's checked out. + // + // Pushes phaseBranch after every task, not just once at full-phase-end: the + // pod is ephemeral and every restart re-clones baseBranch fresh (see + // runRepoPipeline) -- without this, a redeploy mid-phase silently discards + // every task committed so far, and the next run re-decides "is this done?" + // from a clone that never saw any of that work. + async function runPhase(cwd, baseBranch, phaseBranch, phaseTasks, pool, repoId, pipelineSession) { const entries = phaseTasks.map((t) => (typeof t === "string" ? { id: t, judgeOnly: false } : t)); for (const entry of entries) { const result = await runTaskOnPool(cwd, baseBranch, entry.id, pool, repoId, pipelineSession, entry.judgeOnly); pipelineSession.taskResults.push(result); + await runGit(cwd, ["push", "-u", "origin", phaseBranch]); logProgress(pipelineSession); } } @@ -584,13 +593,21 @@ data: const phaseLabel = phaseLabelFor(phaseTasks, i); const phaseBranch = branchName ? `${branchName}/${phaseLabel}` : `agent-run/${repoId}/${phaseLabel}`; - const branchResult = await runGit(cwd, ["checkout", "-b", phaseBranch]); + // Resume a phase branch a prior (since-restarted) run already pushed, + // instead of always branching fresh off baseBranch -- otherwise every + // redeploy silently discards whatever tasks that prior run already + // committed and pushed (see runPhase's per-task push below). + const fetchExisting = await runGit(cwd, ["fetch", "origin", phaseBranch]); + const resuming = fetchExisting.code === 0; + const branchResult = resuming + ? await runGit(cwd, ["checkout", "-b", phaseBranch, "FETCH_HEAD"]) + : await runGit(cwd, ["checkout", "-b", phaseBranch]); if (branchResult.code !== 0) { pipelineSession.gitError = branchResult.out; return finish("branch-crashed"); } - if (i === 0) { + if (i === 0 && !resuming) { const gitignoreAdditions = [ "", "# agent-harness: build artifacts and vendored archives never belong in source control", @@ -616,7 +633,7 @@ data: await runGit(cwd, ["commit", "-m", "chore: broaden .gitignore for agent-run artifacts"]); } - await runPhase(cwd, phaseBranch, phaseTasks, pool, repoId, pipelineSession); + await runPhase(cwd, baseBranch, phaseBranch, phaseTasks, pool, repoId, pipelineSession); const phaseTaskIds = new Set(phaseTasks.map((t) => (typeof t === "string" ? t : t.id))); const phaseResults = pipelineSession.taskResults.filter((r) => phaseTaskIds.has(r.task)); @@ -680,6 +697,15 @@ data: pipelineSession.gitError = push.out; return finish("squash-push-crashed"); } + + // Milestone's content now lives in baseBranch as one squashed commit -- + // the phase branch (and whatever a prior restart already pushed of it) + // has no further reason to exist. Delete it both places so a future run + // never tries to resume a phase that's already done, and so origin + // doesn't accumulate one dangling branch per completed phase forever. + await runGit(cwd, ["branch", "-D", phaseBranch]); + await runGit(cwd, ["push", "origin", "--delete", phaseBranch]); + logProgress(pipelineSession); }