fix(worker,shiftengine,chart): make a reading Role able to review — and unable to write #32

Merged
agent-builder merged 4 commits from fix/rc14-refinement-enablers into development 2026-07-30 20:16:31 +00:00
Member

Everything here came out of running rc.13 in production. Nothing was found by reading.

The reader could not review, and was trusted on a false statement

A Shift's reading Roles dispatched correctly and then failed on arrival with
exec: "opencode": executable file not found in $PATH. That reads as "add the binary to the image".
It isn't — shipping opencode would only have turned failed to start into started, reviewed, and
threw the review away
. Three defects sat behind it:

  • A reader never received the code. The clone is --depth 50 --branch <base>, and --depth
    implies --single-branch, so the writer's branch was not in the repository at all — while the
    review contract told the agent "the repository checkout is your working directory, on branch
    <shift branch>"
    . Readers now fetch that branch and stand on it, with the base still present so
    git diff base...branch is the change under review.

  • A reader was never told the pull request existed. priorPR was interpolated only into the
    writer's contract, which made "review the pull request" unsayable.

  • A reader held a full push credential. AGENT_BUILDER_TOKEN is requireEnv on every worker
    pod, BaseEnv: os.Environ() handed it to the agent, and authURL baked it into origin. The
    contract said "you hold no lease on this branch and no write credential, so a push will be
    rejected by the forge"
    — untrue on every real deployment. The only control was a sentence the
    model could disprove by trying.

    A reading run now has the token scrubbed from its agent's environment and its origin reset to a
    credential-free URL. The scrub matches on value, not variable name, so a future variable
    carrying the same token cannot quietly reopen the hole. The contract now describes the control
    that exists instead of asserting one that didn't.

A clean reader was recorded as having pushed

Standing on the writer's branch, a reader finds the writer's PR, so the runErr == nil && prURL != ""
arm credited it with pr_updated. On any plan whose last Round is a review, that is every
successful Shift. Readers now resolve to no_change_needed and carry their findings.

A successful review announced itself as a stoppage

trackerMessage keyed its opening line on the settled state, and a configured plan settles
needs_human on success — the last word is "a person is asked to merge". So a successful
multi-Round Shift opened its board comment with "Ploeg stopped working this item" directly above
the pull request it had just produced. A PR existing now outranks the state.

Also here, from the same rollout

  • Worker ServiceAccount create/name split. The old guard read a name as "an external account
    exists", so naming the chart's own default suppressed the account the pods then referenced and
    every Job died with serviceaccount "ploeg-worker" not found.
  • The DinD hazard label on the Job as well as the Pod.
  • maxFixRounds serialised from chart values, so ADR-0017's request-changes loop is reachable at all.

Verification

Mutation-tested in both directions, because a failing test alone cannot tell a working gate from a
broken one:

  • Disable the credential scrub → the reading-run tests fail; restore it → they pass, and the
    writer-keeps-its-token case holds throughout.
  • Restore the old trackerMessage condition → the new regression case fails and the other four keep
    passing.

gofmt, go vet, go build, go test ./..., helm lint and ./scripts/helm-golden.sh check all
green locally.

Not in this PR

The ACP and claude-code adapters still cannot return findings at all — neither sets
PLOEG_OUTCOME_FILE nor populates Findings, so only openhands and exec can carry a review.
That is what a second harness needs before ADR-0051 can be ratified, and it is tracked separately.
The reader path here is proven on openhands with dind: false, which needs no new image.

🤖 Generated with Claude Code

Everything here came out of running rc.13 in production. Nothing was found by reading. ## The reader could not review, and was trusted on a false statement A Shift's reading Roles dispatched correctly and then failed on arrival with `exec: "opencode": executable file not found in $PATH`. That reads as "add the binary to the image". It isn't — shipping opencode would only have turned *failed to start* into *started, reviewed, and threw the review away*. Three defects sat behind it: - **A reader never received the code.** The clone is `--depth 50 --branch <base>`, and `--depth` implies `--single-branch`, so the writer's branch was not in the repository at all — while the review contract told the agent *"the repository checkout is your working directory, on branch \<shift branch\>"*. Readers now fetch that branch and stand on it, with the base still present so `git diff base...branch` is the change under review. - **A reader was never told the pull request existed.** `priorPR` was interpolated only into the writer's contract, which made "review the pull request" unsayable. - **A reader held a full push credential.** `AGENT_BUILDER_TOKEN` is `requireEnv` on every worker pod, `BaseEnv: os.Environ()` handed it to the agent, and `authURL` baked it into `origin`. The contract said *"you hold no lease on this branch and no write credential, so a push will be rejected by the forge"* — untrue on every real deployment. The only control was a sentence the model could disprove by trying. A reading run now has the token scrubbed from its agent's environment and its `origin` reset to a credential-free URL. The scrub matches on **value, not variable name**, so a future variable carrying the same token cannot quietly reopen the hole. The contract now describes the control that exists instead of asserting one that didn't. ## A clean reader was recorded as having pushed Standing on the writer's branch, a reader finds the writer's PR, so the `runErr == nil && prURL != ""` arm credited it with `pr_updated`. On any plan whose last Round is a review, that is *every* successful Shift. Readers now resolve to `no_change_needed` and carry their findings. ## A successful review announced itself as a stoppage `trackerMessage` keyed its opening line on the settled state, and a configured plan settles `needs_human` on **success** — the last word is "a person is asked to merge". So a successful multi-Round Shift opened its board comment with *"Ploeg stopped working this item"* directly above the pull request it had just produced. A PR existing now outranks the state. ## Also here, from the same rollout - Worker ServiceAccount `create`/`name` split. The old guard read a *name* as "an external account exists", so naming the chart's own default suppressed the account the pods then referenced and every Job died with `serviceaccount "ploeg-worker" not found`. - The DinD hazard label on the Job as well as the Pod. - `maxFixRounds` serialised from chart values, so ADR-0017's request-changes loop is reachable at all. ## Verification Mutation-tested in both directions, because a failing test alone cannot tell a working gate from a broken one: - Disable the credential scrub → the reading-run tests fail; restore it → they pass, and the writer-keeps-its-token case holds throughout. - Restore the old `trackerMessage` condition → the new regression case fails and the other four keep passing. `gofmt`, `go vet`, `go build`, `go test ./...`, `helm lint` and `./scripts/helm-golden.sh check` all green locally. ## Not in this PR The ACP and claude-code adapters still cannot return findings at all — neither sets `PLOEG_OUTCOME_FILE` nor populates `Findings`, so only `openhands` and `exec` can carry a review. That is what a second harness needs before ADR-0051 can be ratified, and it is tracked separately. The reader path here is proven on `openhands` with `dind: false`, which needs no new image. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(chart): three defects found by running rc.13 in production
All checks were successful
On Pull Request / checks (pull_request) Successful in 6m13s
11d2284ebe
All three broke, or hid, real dispatch on 2026-07-30.

1. The worker ServiceAccount guard inferred create from name.
   `if and .Values.executor.enabled (not .Values.executor.serviceAccountName)`
   read a name as "an external account exists", so naming the chart's OWN
   default suppressed the account the pod template then referenced and every
   worker Job died with `serviceaccount "ploeg-worker" not found`. Naming the
   default and saying nothing must be equivalent; they were opposite. Split
   into executor.serviceAccount.{create,name}, and route the object and the
   reference through one helper so they cannot disagree again.

2. The DinD hazard label never reached the Job.
   It was stamped on the pod TEMPLATE only. Kyverno autogens a Job rule for
   pod-security-baseline-enforce, and a Job selector matches the JOB's own
   labels — so the PolicyException could not admit the Job and every DinD team
   was rejected at admission. KEDA copies ScaledJob labels onto its Jobs, so
   the label now goes there too, gated by the same role->team->global dind
   resolution the pod template uses (extracted to ploeg.roleUsesDind).
   Consumers can then key their exception on the hazard instead of on a list
   of workload names.

3. maxFixRounds was never serialised.
   PLOEG_TEAM_PLANS was built as {pool, rounds}, dropping the field entirely,
   so ADR-0017's request_changes loop was unreachable from Helm values — a
   reviewer verdict could never re-open the writing Round.

Verified by rendering: naming the default now both creates and references the
account; create:false skips creation and keeps the external reference; the
hazard label appears on a dind role's ScaledJob and pod template and on
neither for a dind:false role; and maxFixRounds:2 reaches PLOEG_TEAM_PLANS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A reading Run could not review anything, and was trusted not to write on the
strength of a sentence that was false.

- It never received the code. The clone is `--depth 50 --branch <base>`, and
  --depth implies --single-branch, so the writer's branch was absent from the
  repository entirely — while the review contract told the agent "the
  repository checkout is your working directory, on branch <shift branch>".
  Readers now fetch that branch and stand on it, with the base still present
  so `git diff base...branch` is the change under review.

- It was never told the pull request existed. priorPR was interpolated only
  into the writer's contract, so "review the pull request" was unsayable.

- It held a full push credential. AGENT_BUILDER_TOKEN is requireEnv on every
  worker pod, BaseEnv: os.Environ() handed it to the agent, and authURL baked
  it into origin. The contract said "you hold no write credential, so a push
  will be rejected by the forge" — untrue on every deployment. A reading run
  now has the token scrubbed from its agent's environment (matched by value,
  not by name, so a rename cannot reopen it) and its origin reset to a
  credential-free URL. The contract now describes that control instead of
  asserting a fiction.

- A clean reader was recorded as pr_updated. It stands on the writer's branch
  and finds the writer's PR, so the runErr==nil && prURL!="" arm credited it
  with a push it cannot perform — on any plan ending in review, that is every
  successful Shift. Readers resolve to no_change_needed and carry their
  findings.

Mutation-tested both ways: with the scrub disabled the reading-run tests fail,
with it restored they pass, and the writer-keeps-its-token case holds
throughout.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
fix(shiftengine): a successful review must not read as a stoppage
All checks were successful
On Pull Request / checks (pull_request) Successful in 6m6s
7deab6a646
trackerMessage keyed its opening line on the settled state alone. A CONFIGURED
plan settles needs_human on SUCCESS by design — the last word is "a person is
asked to merge" — so every successful multi-Round Shift opened its board
comment with "Ploeg stopped working this item" directly above a link to the
pull request it had just produced.

The state says who owes the next move; it does not say how the run went. A
pull request existing now outranks it, and the genuinely empty-handed case
gets wording that names what is missing.

Regression case added to the existing table and mutation-tested: with the old
condition restored it fails and the other four cases keep passing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agent-builder changed title from fix(chart): three defects found by running rc.13 in production to fix(worker,shiftengine,chart): make a reading Role able to review — and unable to write 2026-07-30 20:06:04 +00:00
fix(worker): a reading Round may run before any branch exists
All checks were successful
On Pull Request / checks (pull_request) Successful in 4m39s
1c55e74785
The branch fetch I added would have parked silver's analyst Role on arrival.
A plan can open with a reading Round — recon the ticket BEFORE the builder
writes — and there is no branch to fetch then, so treating a missing branch as
stuck fails a Round that was never going to find one.

A missing branch now leaves the reader on the base and says so in the log, and
the contract tells the agent which situation it is in: standing on the work
with a diff to read, or ahead of the work with only the existing code. Either
way it is true, which is the whole point of the change it rides on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
agent-builder merged commit 5371fff24c into development 2026-07-30 20:16:31 +00:00
agent-builder deleted branch fix/rc14-refinement-enablers 2026-07-30 20:16:31 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
webgrip/ploeg!32
No description provided.