Skip to content

ready-to-review-mergeable ships by copy-paste: 7 repos, 3 divergent versions, and session state written into the working tree #54

Description

@lapc506

Measured, 2026-07-31

/ready-to-review-mergeable and stop-prs-green.sh are copied per repo. There is no propagation mechanism, and they have already diverged.

Repo stop-prs-green.sh CURRENT_REPO fallback
dojo-os 221 lines DojoCodingLabs/dojo-os
freedom-academy-admin 115 lines line absent
freedom-academy-demolab-gcp 115 lines line absent
freedom-academy-moodle-plugins 115 lines line absent
dojo-infra-gitops 221 + 1 line changed DojoCodingLabs/dojo-infra-gitops

Read via gh api repos/…/contents/.claude/hooks/stop-prs-green.sh, distinguishing "line absent" from "could not read" — all four were read successfully.

The three freedom-academy copies are 106 lines older and predate cross-repo handling entirely. Everything dojo-os learned — including the plugin PR #370 lesson its own header documents — never propagated.

The asymmetry is what makes it durable: dojo-os is the repo that evolves and the repo where the hook gets exercised, so the stale copies are stale and unnoticed.

Three defects, in order of durability

1. Distribution is copy-paste

Porting the command to a sixth repo today required hand-editing the CURRENT_REPO fallback, which is hardcoded to DojoCodingLabs/dojo-os:

CURRENT_REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null)
if [[ -z "$CURRENT_REPO" ]]; then
  CURRENT_REPO="DojoCodingLabs/dojo-os"
fi

Copying that verbatim reproduces the exact cross-repo bug the header warns about one screen above: a bare 23 in the state file gets evaluated against dojo-os#23 — an unrelated PR — and the gate passes or blocks on the wrong evidence.

A value that must change on every port is a distribution problem, not a config problem.

2. Session state lives in the working tree

STATE_FILE=".claude/.implement-prs" — written by the agent, read by the hook. It is the only channel between an in-context instruction and an out-of-context enforcer, so the mechanism is necessary. The location is not.

  • Not gitignored in any repo that ships the hook. Surfaces as ??; a git add . commits it.
  • Shared across sessions in one checkout, and across worktrees.
  • Survives abandonment. The hook deletes it when everything is green, but not when a session dies — so the next session starts gated on PRs it never opened.

Measured today: 0 of 50 open PRs across 5 repos touch it, and it 404s on the default branch of all 6 repos. The risk has never materialised. .gitignore shipped as a stopgap in dojo-infra-gitops; the real fix is the session scratch directory, keyed by session, which only makes sense to do once — upstream.

3. The gh-failure path is unobservable

- gh / network failure -> exit 0 with a warning (never block on infra).

Failing open is correct — a network blip must not trap a session. But it means "I could not look" resolves to "proceed", and the only trace is a WARN on a subprocess's stderr, which is the same destination as console.error.

Nobody can currently answer how many green stops were actually unverified. Not asking for fail-closed — asking for the warning to land somewhere countable.

Proposed

  • Ship the command + hook from the plugin, so there is one copy and porting is installation, not duplication
  • Resolve the current repo without a hardcoded fallback, or make the fallback an install-time value rather than a source edit
  • Move the state file to the session scratch directory, keyed by session
  • Emit the gh-failure warning somewhere countable, so fail-open stays honest
  • Migration path for the 3 divergent freedom-academy copies — they will not self-heal

Context

Found while porting the command to dojo-infra-gitops during the 2026-07-31 alerts observability audit. The audit's recurring finding was controls that detect correctly and report nowhere; this one is the inverse and worth keeping — it is the only hard gate the audit found. Which is exactly why its distribution should not be manual.

Created by Claude Code on behalf of @andres

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions