fix(handoff): route a task's reporting channel with the task - #12
Merged
Merged
Conversation
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
force-pushed
the
fm/route-brief-path
branch
from
September 2, 2026 08:15
e7c7d5b to
1f93ba8
Compare
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
force-pushed
the
fm/route-brief-path
branch
from
September 2, 2026 08:26
1f93ba8 to
37647c3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The defect
A brief pins its home into the reporting command (
bin/fm-brief.sh):That pin is load-bearing — it is what stops a secondmate's own
FM_HOMEfrom diverting an escalation — but it does not follow the item when the item moves.bin/fm-backlog-handoff.shmoved 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 thefmx-platsecondmate and still pointing at the main home'sstate/<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 everyfm-status.shreporting command inside its brief is retargeted at the destination home — root asFM_HOME, state dir asFM_STATE_OVERRIDE, the destination's ownbin/fm-status.shwhen 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:
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.fm-status.shinvocation and quoted<key>.statuspaths 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.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 flightentries refused — and their briefs do not travel;.fm-secondmate-homeis refused and not written into;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_redstamped 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 realbin/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 nofm-status.shof its own.gate-t4-handoff-safety-preserved— each pre-existing refusal re-proven with a brief dir present in every arm.gates/verify.shreports exactly the two declared reds —gate-l2-loop-audit-levelandm1-hook-registered— and no others. The nine gates the sweep demoted fromfrozentogreenwere re-frozen;gates/ledger.jsondiffed againstorigin/mainshows 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