Skip to content

fix(handoff): route a task's reporting channel with the task - #12

Merged
stoneevenson-biz merged 2 commits into
mainfrom
fm/route-brief-path
Sep 2, 2026
Merged

stoneevenson-biz merged 2 commits into
mainfrom
fm/route-brief-path

Conversation

@stoneevenson-biz

Copy link
Copy Markdown
Owner

The defect

A brief pins its home into the reporting command (bin/fm-brief.sh):

FM_HOME='<home>' FM_STATE_OVERRIDE='<home>/state' bash '<home>/bin/fm-status.sh' '<id>' "<state>: ..."

That pin is load-bearing — it is what stops a secondmate's own FM_HOME from diverting an escalation — but it does not follow the item when the item moves. bin/fm-backlog-handoff.sh moved the backlog line and left the pin behind, so the destination home owned the task and the origin owned the channel.

Observed on fmx-boot-e3, routed to the fmx-plat secondmate and still pointing at the main home's state/<id>.status. Two consequences: reports land where the owning supervisor's watcher never looks, and both homes carry records for one task.

What changed

The item's data/<key>/ dir now travels with its backlog line, and every fm-status.sh reporting command inside its brief is retargeted at the destination home — root as FM_HOME, state dir as FM_STATE_OVERRIDE, the destination's own bin/fm-status.sh when it has one (otherwise the existing script path is kept, since the pins are what decide where the line lands).

Three decisions worth calling out:

  • It is a normalisation, not a diff. Every reporting command is rewritten to the destination whatever it said before, and this runs for every requested key — including one already in the destination backlog. So re-running the handoff is the repair path for an item routed before this existed: bin/fm-backlog-handoff.sh <secondmate-id> <item-key> converges it in place. No one-off sweep is needed, and items already routed are not left broken.
  • It is narrow. Only the fm-status.sh invocation and quoted <key>.status paths are touched. A blanket substitution would also rewrite the task description, which names the origin home for the crewmate's own reasons — corrupting the work to fix the reporting would be the worse defect.
  • It fails loudly rather than half-routing. After rewriting, the brief is re-read: any surviving origin pin aborts the handoff and rolls back. A brief that moved while its channel did not is exactly the defect being fixed.

Safety properties held

Every existing refusal still refuses, now with files inside the transaction. The destination copy lands before anything is removed from the origin, so a failure anywhere rolls back to the item being whole in the origin.

  • ## In flight entries refused — and their briefs do not travel;
  • an unmatched key aborts atomically — neither backlog touched and the brief still whole in the origin;
  • a destination lacking or mismatching .fm-secondmate-home is refused and not written into;
  • idempotent — a re-run duplicates no line and leaves an already-retargeted brief byte-identical;
  • a destination that already holds its own data/<key>/ keeps it (retargeted, unclobbered); the origin's stale copy is named in the output rather than removed, since deleting a dir this script did not place there is not a mechanical move.

Gates

Both frozen, both registered while genuinely red (first_observed_red stamped by a verify run with the implementation absent).

  • gate-t4-routed-brief-reports-to-destination — proves the channel moved, not that a string changed. It scaffolds a real ship brief with the real bin/fm-brief.sh, hands the item off, then extracts the destination brief's own reporting command and runs it, asserting the line landed in the destination home's state dir and not the origin's. Three arms: an item routed with its brief, an item routed before this existed (the repair path), and a destination with no fm-status.sh of its own.
  • gate-t4-handoff-safety-preserved — each pre-existing refusal re-proven with a brief dir present in every arm.

gates/verify.sh reports exactly the two declared reds — gate-l2-loop-audit-level and m1-hook-registered — and no others. The nine gates the sweep demoted from frozen to green were re-frozen; gates/ledger.json diffed against origin/main shows no gate demoted and none vacuous.

Spec: docs/specs/2026-09-01-routed-brief-home.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01HigQ4ZWEsJGLFkQjo5NEV3

stoneevenson-biz added a commit that referenced this pull request Sep 2, 2026
…n it

Quarterdeck reject, second of the same class, and the instruction was the right
one: stop patching the pattern. Three wrong-merge defects came out of this
parser and all three were one shape - a value read from a reference that did not
name it:

  1. the number was matched out of any `*/pull/<digits>`, so a gitlab.com url
     lent its 23 to whichever repository the remotes resolved;
  2. the number survived a url whose repository lost precedence to
     --remote/--repo, merging PR 23 of a repository the url never named;
  3. the slug was read at the FIRST `/pull/` and the number at the LAST, so
     `.../pull/12?next=kunchenguid/pull/99` merged PR 99 while every cross-check saw a
     repository agreeing with itself.

Each was fixed where it was found, and the next arrived through the next door.
A guard against merging the wrong PR had twice resolved the wrong PR itself.

So the matching is gone. `fm_merge_target_parse_pr_url` takes the url apart in
the order a url is defined - fragment off first, then query, then scheme, then
host matched EXACTLY against github.com, then a path of exactly four segments
`<owner>/<repo>/pull/<digits>` - and returns BOTH the repository and the number
from that one parse. They cannot disagree because there is nothing left to
disagree. `fm_merge_target_from_pr_url` and `fm_merge_target_pr_number` are now
thin halves of it rather than two parsers with their own opinions.

The accepted shape is exactly `http(s)://github.com/<owner>/<repo>/pull/<digits>`
with an optional query and fragment naming no second pull request. Four things
are REFUSED rather than repaired: a foreign host; a second `/pull/<n>` in the
query; a second `/pull/<n>` in the fragment; anything trailing in the path
(`/files`, `/commits/abc`, `/12/files/pull/77`). Trailing segments are no longer
trimmed - trimming is how a url that says one thing came to mean another.
Refusing a url a human could have meant costs one trimmed paste; accepting one
costs a merge.

Each rejection is gated as its OWN case with its own name - 5.2 foreign host,
5.3 query, 5.4 fragment, 5.5 trailing path, 5.6 malformed path, 5.7 end to end -
precisely so that no single tweak can silently re-open one: a change that
reopens the fragment hole fails the fragment case by name. That is the point of
splitting them rather than looping over one list.

Behaviour change worth stating: `.../pull/12/files` used to merge PR 12 and now
refuses. That was a deliberate call - it is the same trick as
`/pull/12/files/pull/77` wearing an innocent path, and this parser does not
decide which part of a url the caller meant.

gates/verify.sh: green:41 red:2 - exactly gate-l2-loop-audit-level and
m1-hook-registered, both pre-existing declarations this branch does not touch.
tests/run-all.sh: 64 ran, 2 skipped, 0 failed. shellcheck and bash -n clean.
gate-t1-merge-target-resolution re-frozen, still non-vacuous.
stoneevenson-biz and others added 2 commits September 2, 2026 01:25
A brief pins its home into the fm-status.sh command, so an item handed to a
secondmate kept reporting into the home that generated the brief. The
destination owned the task and the origin owned the channel: reports landed in a
state dir the owning supervisor's watcher never polls, and both homes held
records for one task. Observed on fmx-boot-e3, routed to fmx-plat and still
reporting into the main home.

fm-backlog-handoff.sh now carries the item's data/<key>/ dir with its backlog
line and retargets every reporting command inside the brief at the destination
home - its root as FM_HOME, its state dir as FM_STATE_OVERRIDE, its own
bin/fm-status.sh when it has one. The rewrite is a normalisation and runs for
every requested key, including one already at the destination, so re-running a
handoff repairs an item routed before this existed rather than reporting
"already present" and leaving the misroute.

It is deliberately narrow: only the fm-status.sh invocation and quoted
<key>.status paths are touched, never the task description, which names the
origin home for the crewmate's own reasons. After rewriting, the brief is
re-read and any surviving origin pin aborts the handoff and rolls back - a brief
that moved while its channel did not is the defect itself.

Every refusal still refuses, with the files now inside the transaction: the
destination copy lands before anything is removed from the origin, and a
destination that already holds its own data/<key>/ keeps it, retargeted and
unclobbered, with the origin's stale copy named rather than deleted.

Gates gate-t4-routed-brief-reports-to-destination (extracts the destination
brief's own command, runs it, asserts where the line landed) and
gate-t4-handoff-safety-preserved, both frozen.
Spec: docs/specs/2026-09-01-routed-brief-home.md

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HigQ4ZWEsJGLFkQjo5NEV3
… on repair

Two defects the Quarterdeck found in the first cut.

A scout's channel is not its status file - it is the report. fm-brief.sh pins
that deliverable as an absolute path and fm-teardown.sh reads $DATA/$ID/report.md
from its OWN home, so a routed scout was still told to write findings into the
origin home - which this same handoff had just emptied. The owning secondmate
would never see the deliverable and its teardown would then refuse, because the
report it looks for was never written where it looks. Absolute <key>/report.md
paths are now retargeted alongside the status paths. A relative
data/<key>/report.md is left alone: it resolves correctly in whichever home
reads it, so rewriting it would only add risk.

The advertised repair path left the item in both homes. The state the pre-fix
code actually produced is a line in the destination backlog with data/<key>/
still in the origin, and the "nothing to move" path retargeted a copy without
ever running the source removal or naming what it had done. That swapped one
half of the defect for the other: instead of one home owning the task and
another owning the channel, both held a copy and neither was authoritative. The
repair now runs the same source-removal and reporting as the moving path.

Both awk scans carry a degenerate-key guard, since an empty needle makes
index() match every position and the scan never advance.

Gate gate-t4-routed-brief-reports-to-destination grows a fourth arm that writes
the report to the path the routed scout brief NAMES and asserts it lands at the
destination's data/<key>/report.md - the exact path the owning home's teardown
reads. Its arm B fixture is rebuilt to the state the pre-fix code could actually
reach, rather than one pre-copied to the destination that could not expose the
both-homes defect. Both new assertions were confirmed to fail against the
rejected commit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HigQ4ZWEsJGLFkQjo5NEV3
@stoneevenson-biz
stoneevenson-biz merged commit c104bb3 into main Sep 2, 2026
3 of 4 checks passed
@stoneevenson-biz
stoneevenson-biz deleted the fm/route-brief-path branch September 2, 2026 08:33
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.

1 participant