Skip to content

radio adoption: #182's repo filter never executes and fails open — route on from:, and stop testing a branch production can't reach - #211

Merged
martin-conur merged 1 commit into
mainfrom
task/radio-adopt-from-routing
Sep 15, 2026
Merged

martin-conur merged 1 commit into
mainfrom
task/radio-adopt-from-routing

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Closes #210

What was wrong

_adopt_legacy_pm_inbox routed the legacy pm backlog on a message's repo:
frontmatter field. cmd_send writes that field only when --repo is
passed, and nothing passes it — 0 of the 163 radio send call sites across
every prompt and doc, 0 of the 399 real messages in the mailbox. So the
filter's inner branch had never once executed in production.

That alone would be harmless. What made it a bug is the direction it failed:
the untagged fall-through adopts, with a comment explaining why that is
correct. A guard that never runs and fails open is indistinguishable from one
that runs and says yes. And the single signal that would have exposed it could
not fire either — skipped was incremented only inside the dead branch, so the
left N foreign-repo message(s) for their own PM log line was structurally
unreachable.

Observed 2026-09-14: this repo's PM registered and migrated 53 messages
belonging to four other repos
out of mailbox/pm/ into its own inbox. No
warning, no counter, no log line. One was live.

The fix

Route on from:, which every message carries and which encodes the repo by
construction (worker-<reponame>-<slug>, reviewer-<reponame>-pr<N>,
pm-<reponame>).

Direction is what makes the sender the right field. Adoption candidates are
worker→pm mail, so they are all literally to: pm — the recipient carries no
information. This is the mirror of #201's dead-letter routing, where the sender
is also the signal, there because that mail is PM→worker and the PM that sent a
ping is who needs the report.

The test is one-sided: adopt iff from matches worker-<my-repo>-* /
reviewer-<my-repo>-* / pm-<my-repo>. Only one candidate name is ever
substituted into the pattern, so the hyphen-splitting ambiguity
_resolve_pm_by_slug_split has to rank around never arises.

Three outcomes now, in _adopt_route:

from: outcome
names this repo adopt
names a different repo (worker-?*-?* / reviewer-?*-?* / pm-?*) leave for its own PM — and left N foreign-repo message(s) finally fires
names no repo (unknown, bare pm, slugless worker-a) adopt first-come, and log it

repo: is kept as the higher-precedence arm for the day something starts
passing --repo. Per the spec this does not change cmd_send to begin
writing it — a wire-format change affecting every sender, for nothing from:
doesn't already provide.

Everything else about adoption is untouched: the pm-*-only and
live-literal-pm guards, the fresh-register gate, the single-winner lock and
its stale-lock reclaim, collision-safe migration, the adopted-from: header,
and the ordering that runs adoption before gc's piggyback — which #201 gave
a second reason to respect.

The tests are the point of the ticket

The four existing filter tests each fabricate a repo: field via --repo.
They pinned a branch production cannot reach, which is precisely why this
shipped inert and stayed that way for months.

Twelve new tests build every fixture the way cmd_send really builds one —
no hand-injected repo:, senders in the real role grammar. Confirmed to fail
against the unfixed binary before the fix was applied:

not ok 20 register: pm-<repo> leaves an untagged foreign-repo message for their own PM (#210)
#   expected : 1
#   actual   : 0
not ok 21 register: the 'left N foreign-repo' log line actually fires (#210)

The old four are kept and relabelled as what they are — coverage of the
higher-precedence arm, with a comment saying production doesn't reach it. Same
discipline as #205 for $TASK_FORCE_HOME and #208 for radio-env-injection:
make the harness unable to lie about the thing it's checking.

Recovery sub-decision: no migration

The 53 already-misadopted messages retain their from: and carry an
adopted-from: header, so a one-shot re-route is mechanically possible. Not
doing it.
All 53 are acked and their PRs are merged — re-routing would move
dead mail between mailboxes for no reader, and would add a one-shot code path
with no second use. Recording the decision rather than leaving it to omission,
as the spec asked.

Verification

  • ./run_tests.sh1059 passed, 0 failed (31 in radio_legacy_pm_adopt.bats, up from 19)
  • shellcheck -x bin/radio — clean
  • tools/check-drift.sh — 21 groups checked, clean

Docs

  • CHANGELOG.md — entry under ## [Unreleased], with the re-run note
  • README hook table row, and the adoption paragraph in all four
    claude-*/steering/*-workflow.example.md templates plus this repo's
    .claude/gh-workflow.md

Upgrading: re-run task-init <loadout> in each configured repo to pick up the
revised adoption paragraph. No migration needed — nothing in the mailbox
changes until the next fresh pm-<repo> register.

🤖 Generated with Claude Code

…ute on from:, and stop testing a branch production can't reach

`_adopt_legacy_pm_inbox` decided which legacy-`pm` backlog messages to adopt
from a message's `repo:` frontmatter field. `cmd_send` writes that field only
when `--repo` is passed, and nothing passes it — 0 of 163 `radio send` call
sites, 0 of 399 real messages — so the filter's inner branch had never executed
in production, and its untagged fall-through *adopts*. A guard that never ran
was indistinguishable from one that ran and said yes; the one line that would
have exposed it (`left N foreign-repo message(s)`) could not fire either,
because `skipped` only incremented inside the dead branch. Observed: one PM
adopting 53 messages belonging to four other repos, silently.

Route on `from:` instead — every message carries it, and it encodes the repo by
construction. Direction is what makes the sender the right field: adoption
candidates are worker→pm mail, so they are all literally `to: pm` and the
recipient carries nothing (the mirror of #201's dead-letter routing). The test
is one-sided — adopt iff `from` matches `worker-<my-repo>-*` /
`reviewer-<my-repo>-*` / `pm-<my-repo>` — so only one candidate name is ever
compared and `_resolve_pm_by_slug_split`'s hyphen ambiguity never arises.

An unattributable message (`from: unknown`) keeps the first-come treatment, the
only option available, but is now counted and logged as such. `repo:` stays the
higher-precedence arm; `cmd_send` is deliberately NOT changed to start writing
it (a wire-format change for no benefit `from:` doesn't already give).

Twelve new tests build every fixture the way `cmd_send` really does, with no
hand-injected `repo:`, and were confirmed to fail against the unfixed binary.
The four existing `--repo` tests are kept and relabelled as what they are: the
higher-precedence arm, a branch production does not currently reach — which is
exactly why this shipped inert.

No recovery migration: the 53 misadopted messages are all acked and their PRs
merged, so re-routing would move dead mail between mailboxes for no reader.

Closes #210

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@martin-conur
martin-conur merged commit 70ecf2e into main Sep 15, 2026
4 checks passed
@martin-conur
martin-conur deleted the task/radio-adopt-from-routing branch September 15, 2026 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

radio adoption: #182's repo filter never executes and fails open — route on from:, and stop testing a branch production can't reach

1 participant