feat(bin): enforce the outbound artifact transport invariant - #101
Merged
sbracewell64 merged 84 commits intoAug 18, 2026
Merged
Conversation
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 16, 2026 19:24
0abb3ae to
3857f74
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 16, 2026 21:10
8ba1a30 to
71bfd82
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 17, 2026 01:23
e68e445 to
b19f5fc
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
2 times, most recently
from
August 17, 2026 23:46
6ef2adf to
3405b04
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
2 times, most recently
from
August 18, 2026 02:45
88d4060 to
b773a6a
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 18, 2026 05:19
c0cc29f to
1d606b3
Compare
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 18, 2026 13:42
1d606b3 to
06e11d6
Compare
The control plane had one direction. An inbound detector woke firstmate when Browser Sol replied to a control issue that already existed, and nothing owned the outbound direction - that work reaching a gate actually emits the artifact the gate waits on. The gap is measured on two surfaces: four SSSF pull requests sat "pending independent acceptance" for days having never been submitted, and three items held finished work on a real branch with no pull request opened anywhere. Nobody rejected that work; nobody was ever shown it. The invariant: an item may not remain in a state that implies an outstanding outbound artifact while no applicable durable artifact exists. That condition is a control-transport defect, not an external wait, and it never reclassifies to not-waiting to pass. It is stated over artifacts rather than over Sol requests specifically. Building the narrow form and adding the pull-request form later would have produced two mechanisms with two identity rules and two dedupe stories. Channels differ only in whether this code may create the missing artifact: sol-control emits, because asking a question is reversible and carries no delivery authority; pull-request is detect-only, because opening one is a delivery action owned by the task's selected delivery path and outward-facing on an upstream contribution. Exact-head applicability falls out of the identity rather than sitting beside it: the request id digests gate, project, repo, item, pull request and head, so a moved head computes a different id and therefore a different artifact. Idempotency, retry and crash safety are one mechanism - a per-id lock, a checkpoint written before transport, and recovery that re-reads the forge for the id rather than guessing from local state. Existence is always observed on the forge; the local record is correlation and checkpoint, never proof. Recognition reads durable state that already exists, in two tiers, so the invariant is not vacuous on the day it lands: a typed hold-kind marker, and a closed prose token set covering the population that predates it. An untyped match is reported as an incomplete binding rather than passed. bin/fm-bootstrap.sh relays defects at every session start, because the condition produces no failure, no error and no wake, and nothing surfaced these items until a person went looking. Every control is watched-red in docs/verification/outbound-transport-invariant.md under a targeted mutation. One was vacuous on first observation: require_record's refusal ran inside a command substitution, where exit kills only the subshell, so an unreadable record would have been reported as an identity mismatch - a could-not-observe collapsing into a verdict, inside the mechanism built to prevent exactly that. Two further defects appeared only against live data: tab is IFS whitespace, so an untyped gate absorbed the tier; and a hold reason is parsed only to its first comma, which hid all three never-submitted items.
After merging repaired main, eight predicates in the outbound library reported COULD_NOT_OBSERVE. The cause was not the merge: bin/fm-outbound-artifact.sh:520 calls a predicate as `|| ! fm_outbound_is_sha ...`, a continuation line opening with `||` and a negation, which the accepted call-site syntax did not list. The whole file was therefore refused as unparseable and every predicate it consumes lost its caller universe. The verdict was honest - could-not-observe rather than a false DEAD, which is the control behaving as rebuilt - but the coverage loss was real: the module's own predicates became unverifiable by its own control. Two fixes were available and only one of them is legitimate. Rewriting the call site would have made the file pass; widening the accepted syntax to silence a refusal is shaping a control around its own answer, which is the failure this whitelist exists to prevent. So the choice was decided by measurement rather than convenience: `|| !` as a leading continuation occurs 65 times across bin/, and once in this module. That makes it a normal idiom of this codebase and its absence an under-specification of the whitelist, not an oddity in the caller. The measurement is recorded beside the rule so a reader can judge whether the widening was earned rather than take it on faith. Verified in both directions, because adding an accepted call form is exactly the change that can re-open a falsification: the eight predicates resolve to alive, and a QUOTED `|| ! dead_one` is still reported DEAD, so prose cannot confirm a call through the new rule. CORRECTING AN EARLIER COMMIT MESSAGE. The message of "feat(bin): enumerate branches, so the negative claim has a universe" claims the inventory composes "the existing landed-containment test". That is false. It reuses the library's ref-resolution helpers and then implements containment with `merge-base --is-ancestor`, which is commit reachability - the approach fm-landed-lib.sh's own header warns against by name, because squash merge, rebase and local replay each break it while the content has landed. The code was corrected in a later commit; the claim was not, and a false statement in a commit message is a false statement in the permanent record. History is not rewritten to fix it because this branch has already been rebased by several actors and a rewrite would risk work that is not mine, so the correction is recorded here instead, where it travels with the same branch.
…its reason Three things, all about saying what is true rather than changing behaviour. THE COMPLETENESS CLAIM IS SCOPED, in the control's own header where a reader meets it. The per-predicate universe check closes the class for this control's enumeration path: every read it performs is three-valued, and a failed read yields could-not-observe rather than a negative answer. It does NOT close the class for the shared landing library its callers use, because fm_landed_candidate_refs returns success whenever any candidate ref resolved, so a push-target read that fails inside the library leaves that ref absent from a non-empty list and an incomplete candidate set is indistinguishable from a complete one to any caller. That gap is filed as landed-lib-unreadable-push-target-collapses and is deliberately out of scope: the library is shared with the worktree guard, teardown, the decision surface and the task-base library, and changing landing semantics from a task about outbound transport would be an unreviewed change to the guards that protect unlanded work. A control described as class-level that silently depended on someone else's unfixed read would be the coverage inflation the rest of this record refuses. THE LAST SINGLE-VALUE SELECTION NOW CARRIES ITS REASON. Every other one on this surface refuses and names its count; this one takes the first line of merge-tree --write-tree output, which is the resulting tree OID by definition with conflict detail after it. That is reading a fixed field, not choosing among candidates. It was safe before and it was also indistinguishable, on reading, from the three by-position defects fixed earlier - so the rule was refuse-and-name OR carry a written reason, and it now carries one. The other two by-position selections were fixed in earlier rounds and are recorded here as discharged rather than left in the owed list: a head matched by more than one open pull request refuses and names the count and venue, and a backlog id matching more than one record refuses rather than choosing by position, because duplicate ids conceal a corruption rather than merely being ambiguous. The verification record carries the same two statements, plus the measurement that earned the accepted-syntax widening.
The inventory flagged every unlanded fm/* branch without a pull request as a transport defect without ever checking that the work was finished. Ordinary in-progress branches therefore became standing defects at every startup, and a control that fires on every run gets discounted - which is the same silence as reporting nothing, reached more slowly. Candidate scoping is now three-valued rather than two. Only recorded COMPLETED SHIP work can be a defect. Recorded non-ship work is skipped, because an investigation produces a report and never a pull request, so its missing submission is correct - that exclusion is what pays for reading the record at all. Work whose state cannot be established is COULD NOT OBSERVE by name. That third answer is the PRIMARY signal here, not a footnote, and the code says so where the logic lives. The three items this pass was commissioned from were released tasks whose live records were already gone, so the population it exists to catch is the population most likely to land in could-not-observe. It therefore gets its own count and its own headed section, empty sections still print, and a sweep carrying any of it cannot exit clean. Rendered inline it would read as a defect; folded into clean it would disappear. Both durable sources are read, established by measurement rather than assumed. Retention does NOT bound this: the backlog keeps a fixed number of completed entries and rotates the rest into an append-only, unpruned archive holding 589 of them, so the evidence survives release indefinitely. Reading only the backlog would make everything older than that handful unobservable. What does bound it is record completeness - entries naming no deliverable cannot answer - and, more largely, HOME LOCALITY: records are per-home, so a branch produced by a secondmate has its record in that home and is invisible here. Measured live across three projects: 42 branches, 34 join to a durable record, 8 do not. An archive that exists but cannot be read is could-not-observe even when the backlog alone would have answered, because a confident verdict from a corpus we know we could not finish reading is the failure this control exists to prevent. Inbound sender parsing is now strict at the parser boundary, and refuses BEFORE any verdict parsing: a body that cannot establish who sent it must not be read for what it decided. Exactly one from field, whole trimmed value compared against a closed enum, and for an inbound ruling that value must be browser-sol. Prefix, substring, duplicate, unknown, and a ruling claiming to come from firstmate are all invalid, and an invalid sender wakes nothing. Whole-value equality is the point: the malformed sender from the live incident carries the valid role as a PREFIX, so anything weaker accepts it. The anchor control's fixture changed and that cost is deliberate rather than hidden. Its branch had no completion record, so under this change it correctly became could-not-observe rather than a defect. The fixture was given the record that makes it a genuine defect, and a separate control now asserts the in-progress case is could-not-observe. The control was not weakened to pass. Seven new controls, each observed failing for its intended reason in isolation: forcing everything to read as ship work makes both in-progress branches and completed investigations false defects; dropping the archive read loses rotated evidence; removing the unreadable-archive guard answers from a partial corpus; reverting to one interleaved list and folding gaps into the defect count both break the section separation; prefix-matching the sender accepts the live incident string; removing the sender check lets a wrongly-sent ruling wake work.
…valid ruling senders Carried from 1d606b3 on the published head, which this candidate lacked. An invalid inbound sender exited 3 rather than 4, so a could-not-observe collapsed into a different verdict at the exact boundary built to keep them apart. The comment directly above that line already said an invalid sender is could-not-observe - not a defect and not a rejection of the ruling's content, because we did not learn the ruling is wrong, we learned we cannot tell who sent it. The code then exited 3 anyway. The principle was stated correctly and implemented as its opposite three lines later. Also carries the control this candidate was missing entirely: the same invalid sender arriving through the POLL path rather than the direct ruling path, which asserts exit 4 and that the request does not advance. One of the two ways a ruling reaches this system had no coverage at all. Verified after carrying: full outbound suite green, dead-predicate class control clean at alive=74 could_not_observe=0, shellcheck exit 0.
… classification tokens
…h module-local control
sbracewell64
force-pushed
the
fm/control-plane-outbound-transport-invariant
branch
from
August 18, 2026 18:17
46d5649 to
77a93ed
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.
Intent
The developer handed off an in-progress autonomous "away mode" session to a new model/session and asked the agent to rehydrate state from authoritative sources (repos, GitHub PRs/issues, durable task state, immutable evidence, and the FirstMate/Browser Sol control plane) rather than treating the prompt itself as evidence, explicitly requiring that existing branches, PRs, exact-head protections, maker/checker separation, three-valued (observed-good/observed-bad/could-not-observe) evidence semantics, provenance, and existing HOLD/CNO states be preserved and not redone. Concrete goals were: process outstanding Browser Sol control traffic on issue #3 and verify PR #7's head, base, and checks before acting on any ruling, without merging PR #7 on the strength of the prompt; and resume work toward a persistent FirstMate-side poller (control issue #2) that wakes and polls without Captain involvement, with the prior blocker — no qualified maker / independently certifiable mutation path — re-evaluated under the new session, while forbidding any weakening of qualification standards or maker/checker separation to make the current model qualify. Constraints were a strict cost boundary (existing subscriptions and credentials only, no new spend) and an operating posture that routes material engineering judgments to Browser Sol, treats quota/CI/capacity issues as external dependencies, and reserves Captain escalation for genuine Captain-only decisions. Through subsequent supervisor escalations the developer kept the agent driving the same work: proving fixes with watched-red controls, keeping corrections inside their reviewed scope, reconciling diverged branch lineages without destroying unique evidence, and executing force-with-lease publication only after a from-scratch reconstruction confirmed no unique work would be lost.
What Changed
bin/fm-outbound-artifact.shandbin/fm-outbound-artifact-lib.sh: a sweep that joins every item whose durable state implies an outstanding outbound artifact against whether an applicable one exists, with three-valued verdicts (0 holds / 3 defect / 4 could-not-observe), exact-head and request-identity correlation, durable correlation records, andcheck|status|defects|reconcile|emit|ruling|poll|resume|close|showsubcommands. Thesol-controlchannel can emit and reconcile missing requests against the venue in the new optionalconfig/sol-control.json;pull-requestrows stay detect-only.bin/fm-bootstrap.shgainsoutbound_artifact_report, which reconciles when the session holds the fleet lock and runs the detect-only sweep underFM_BOOTSTRAP_DETECT_ONLY, bounded by the newFM_OUTBOUND_BOOTSTRAP_DEADLINE(default 60) split away from the per-probeFM_OUTBOUND_TIMEOUT(default 15). Partial findings are relayed before the incompleteness marker, andOUTBOUND:handling is documented in the bootstrap-diagnostics skill.bin/fm-dead-predicate-check.sh, a fail-closed control that flags enrolled predicates with no accepted call site (unparseable syntax reports could-not-observe rather than passing), and runs it as a CI invariants step. Covered by newtests/fm-outbound-artifact.test.shandtests/fm-dead-predicate-check.test.shplus bootstrap/session-start test updates, with configuration, scripts, vocabulary-collision, anddocs/verification/outbound-transport-invariant.mddocumentation.Risk Assessment
Testing
Ran the three focused suites that own this change (outbound-artifact, dead-predicate, bootstrap) — all green — then drove the commands by hand against real fixture homes to produce operator-visible transcripts, because a green suite over a silence-shaped invariant proves little on its own. The transcripts show the split defect headings with an observed
artifact: comment/900filed under the correlation heading instead of the missing-artifact one, the relay line naming the artifact and the disagreeing request id, could-not-observe counted and sectioned at exit 4,reconcileprinting a refused emit's reason and still reporting every other item at worst-of status 4 with no leftover locks, and a deadlined session start relaying its finding above an explicit INCOMPLETE marker. The dead-predicate control was also run against the real repository (exit 0, alive=77, could_not_observe=0). This surface is a bash CLI with no rendered UI, so the evidence is CLI transcripts rather than screenshots. No failures, no flakes; worktree left clean.Evidence: Outbound sweep CLI transcript — split defect headings, observed-artifact naming, three-valued verdict
$ fm-outbound-artifact.sh status # nothing has been emitted yet outbound artifacts: 0 satisfied, 1 defect, 0 could-not-observe DEFECT - waiting with no applicable durable artifact (1) waiting-item gate: INDEPENDENT_BROWSER_REVIEW_REQUIRED · channel: sol-control · recognised: prose head: a25417df... · artifact: none · FM_OUTBOUND_NO_ARTIFACT [exit 3] $ fm-outbound-artifact.sh emit waiting-item requested: fm-ob-f8fd3f91130f on o/control#2 $ fm-outbound-artifact.sh emit waiting-item # idempotent already requested: fm-ob-f8fd3f91130f (comment 900) --- record under that request id rewritten to name another request --- $ fm-outbound-artifact.sh check outbound artifacts: 0 satisfied, 1 defect, 0 could-not-observe DEFECT - waiting with no applicable durable artifact (0) none DEFECT - the artifact exists, but the correlation record filed under its request id names a different request (1) waiting-item head: a25417df... · artifact: comment/900 · FM_OUTBOUND_IDENTITY_REFUSED [exit 3] $ fm-outbound-artifact.sh defects OUTBOUND: waiting-item has its artifact comment/900 on the forge, but the correlation record filed under fm-ob-f8fd3f91130f names a DIFFERENT request (FM_OUTBOUND_IDENTITY_REFUSED) [exit 3] --- venue unconfigured --- $ fm-outbound-artifact.sh check outbound artifacts: 0 satisfied, 0 defect, 1 could-not-observe COULD NOT OBSERVE ... FM_OUTBOUND_TRANSPORT_UNCONFIGURED [exit 4]Evidence: reconcile: one item's refused transport does not suppress the report
$ fm-outbound-artifact.sh reconcile # item one refuses transport transport failed after 3 attempts; the request is NOT lost - fm-ob-7a6807cab266 is checkpointed at .../outbound-artifacts/fm-ob-7a6807cab266.json waiting-item remains waiting with no artifact. requested: fm-ob-63a13ad444af on o/control#2 OUTBOUND: reconciliation refused an emit (status 4) - its named reason is printed above, and the sweep below still reports every other item OUTBOUND: waiting-item is waiting on INDEPENDENT_BROWSER_REVIEW_REQUIRED with no applicable durable artifact (FM_OUTBOUND_NO_ARTIFACT) - head 5039adb0..., channel sol-control [exit 4] posts the forge accepted: 1 (the second item was still attempted) emit locks left behind: 0Evidence: Session start: a deadlined sweep relays its finding and still marks itself incomplete
$ fm-bootstrap.sh (session start, FM_OUTBOUND_BOOTSTRAP_DEADLINE=1) OUTBOUND: early-item is waiting on a gate with no applicable durable artifact OUTBOUND: sweep unevaluable - bootstrap deadline expired after 1s; any OUTBOUND line above this one is what the sweep established before it was stopped, and the sweep is INCOMPLETEEvidence: Dead-predicate control run against this repository (the CI invariants invocation)
$ bin/fm-dead-predicate-check.sh 233 consumer file(s) UNCHECKED - outside the accepted syntax, so no call site in them was read: ... [230 lines elided] ... This list measures how much of the repository this control can see. Shorten it by making files parse. fm-dead-predicate-check: ok enrolled=3 scanned=119 unchecked=233 alive=77 could_not_observe=0 marked=0 [exit 0]Evidence: Focused suite logs (outbound-artifact / dead-predicate / bootstrap)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-outbound-artifact.sh:1412-RECONCILE_RC=${PIPESTATUS[1]}reads the wrong pipeline stage. The pipeline isprintf | jq | while, so index 1 is jq (always 0 when the sweep JSON parses) and thewhileloop that runscmd_emitis index 2. Verified:printf|cat|while ... exit 7yieldsPIPESTATUS=(0 0 7). Every emit refusal inside reconcile —FM_OUTBOUND_EMIT_IN_FLIGHT(3),FM_OUTBOUND_ARTIFACT_UNOBSERVED(4),TRANSPORT_UNCONFIGURED(4), transport exhaustion (4) — is therefore discarded, and reconcile falls through to the finalsweep_exit, which reports the surviving row as exit 3 (a defect) instead of the emit's own 4 (could-not-observe). That is exactly the 3-vs-4 collapse this module's header forbids, and no test covers a failing emit inside reconcile. Use${PIPESTATUS[2]}.bin/fm-bootstrap.sh:1000-outbound_artifact_reportreusesFM_OUTBOUND_TIMEOUTas the whole-sweep bootstrap deadline, butbin/fm-outbound-artifact.sh:76,90documents and uses the same variable as the timeout for ONE forge/git observation. With the shared default of 15, a single slow probe consumes the entire sweep deadline, and a reconcile that is allowed up toFM_OUTBOUND_MAX_PROBES=40probes at 15s each cannot finish inside 15s on any non-trivial fleet. On timeout the collected output isrm -f'd unprinted, so real defects the sweep already found are replaced byOUTBOUND: sweep unevaluable - bootstrap deadline expired, and the mutatingreconcileis SIGTERM'd mid-run at every session start. The invariant then reads as permanently blind — the reads-as-working-while-doing-nothing shape the module exists to refuse. Raising the value cannot separate the two concerns because it widens both. Needs its own knob (e.g.FM_OUTBOUND_BOOTSTRAP_DEADLINE) and a deadline larger than one probe timeout.bin/fm-outbound-artifact.sh:1428- Option parsing usesRATIONALE=${2:-}; shift 2with no[ "$#" -ge 2 ]guard (same at lines 1441-1443, 1454, 1463-1464). In bash,shift 2with only one positional left changes nothing and returns non-zero, sofm-outbound-artifact.sh emit x --rationale-file,ruling --request,resume --request, orclose --dispositionwith a missing value spins forever instead of erroring. The repo's own idiom guards this —bin/fm-certify.sh:141-144writes--repo) [ "$#" -ge 2 ] || die "--repo needs a value"; REPO=$2; shift 2. Adopt that guard.bin/fm-outbound-artifact.sh:798- Thestalecount callsrecords_allinside the per-row sweep loop.records_allre-reads every file in$RECORD_DIRand runsrecord_valid_for_idon each, which is ~8jqinvocations plus agit rev-parseper record. Cost is O(waiting_rows x records) processes, re-paid on each of reconcile's two sweeps, inside a session-start deadline that is 15s by default. The value does not depend on the row beyond theitem/headfilter, so hoisting onerecords_allsnapshot above the loop and filtering it per row is behaviour-preserving.bin/fm-outbound-artifact.sh:787- In the sol-control branch ofsweep,case $record_rc in 1) rc=4 ;; *) rc=5 ;; esacmaps bothrecord_read2 (could-not-observe) and 5 (identity mismatch) torc=5, which the verdict table renders asFM_OUTBOUND_RECORD_UNREADABLE. A correlation record that is perfectly readable but belongs to a different request is then reported to the operator as an unreadable file, sending them to look for corruption or permissions instead of the correlation defect. This is the exact distinctionrecord_identity_verdict's own header (lines ~372-392) says must not be collapsed. Give the mismatch case its own rc and surfaceFM_OUTBOUND_IDENTITY_REFUSED.bin/fm-outbound-artifact.sh:941-supersede_other_headswalks EVERY record in$RECORD_DIR— includingclosedandsupersededones for unrelated items — and returns 2/5 on the first record whose identity will not revalidate, which makescmd_emitdie at line 1044-1048. Revalidation runsfm_outbound_binding_missing->fm_outbound_head_valid, which forhead_sourceofdeclared/local/empty requires the head object to still resolve in$PROJECTS/<project>. Once a branch is squash-merged and gc'd, a project clone is removed, or a project is retired fromprojects/, that historical record becomes permanently unvalidatable and every future emit for every item in this home refuses with exit 4. The same root cause makesshow,resume, andcloseon those old records unreadable. Fail-closed is clearly intended, but the blast radius across unrelated items looks wider than intended — consider scoping the walk to records for the item being emitted, or exempting terminal (closed/superseded) records.bin/fm-outbound-artifact.sh:1042-cmd_emitsets the globalEMIT_LOCKand relies on the EXIT trap to release it, but underreconcileit runs inside thewhile-loop subshell of a pipeline, and bash resets caught traps in subshells (verified). The per-rid lock directory is never released: within one reconcile each successive item overwritesEMIT_LOCKwithout releasing the previous, and all of them survive the run. Recovery works only becausefm_lock_try_acquiresteals a lock whose numeric pid is dead, so the impact is stale.<rid>.lockdirectories accumulating in$RECORD_DIRrather than a hang. Releasing the lock explicitly at the end ofcmd_emitcloses it.bin/fm-outbound-artifact.sh:1403-reconcilerunscmd_pollplus two fullsweeppasses in one process, and the probe budget is a single process-wide counter file capped atFM_OUTBOUND_MAX_PROBES(default 40). The pre-emit sweep can exhaust the budget on its own (the verification doc records a measured 42-branch inventory), so the post-emit sweep runs capped andsweep_exitreturns 4 withprobe cap 40 reachedeven when reconciliation succeeded. Either budget per sweep or document that reconcile needs roughly double the cap of a barecheck.bin/fm-outbound-artifact.sh:567- Infinished_work_evidence, the candidategrep -F -e "- [x] $item "matches the token anywhere in a line, while the state extractionsed -n 's/^- \[x\] .*/completed/p'is anchored to line start. A matching line that is indented or quotes the row mid-line yields zero parsed states,count=0, and the[ "$count" -eq 1 ] || return 3branch reportsFM_OUTBOUND_WORK_LIFECYCLE_CONFLICT("lifecycle records disagree") for what is actually "no state could be parsed". Return 2 (WORK_STATE_UNOBSERVED) whencountis 0 and reserve 3 forcount > 1.bin/fm-dead-predicate-check.sh:2- The new class control is not invoked bybin/fm-lint.sh, CI, or bootstrap — the only repo-wide guarantee istest_outbound_library_stays_enrolled, which pins the enrollment marker but never runs the checker over the repository. As shipped it therefore catches a new dead predicate only when someone runs it by hand. Separately,function_has_call_siteand the call-form validation loop each re-run the char-by-charstrip_quotedawk per (consumer file x function), so a repo-wide run is O(files x functions) awk processes over the ~118 parseable files; caching the stripped text per file would make wiring it into lint practical.🔧 Fix: fix outbound reconcile status, lock, verdict and deadline splits
5 issues (2 warnings, 3 infos) still open:
bin/fm-outbound-artifact.sh:1483- Now thatcmd_emitruns in the main shell, its refusals exit the process directly, so the finalsweepandrender_defectsat lines 1485-1486 never run. One item's emit refusal therefore suppresses the OUTBOUND report for every other item. The exit-3 case is the worst:bin/fm-bootstrap.sh:1043deliberately skips itsreconciliation exited Nline for status 3, so aFM_OUTBOUND_EMIT_IN_FLIGHT(concurrent session) orFM_OUTBOUND_INCOMPLETE_BINDINGrefusal leaves session start printing onlycmd_emit's un-prefixed stderr - noOUTBOUND:line at all, which is the exact token.agents/skills/bootstrap-diagnostics/SKILL.mdtriggers on, so the handling skill is not loaded and the sweep's other defects are invisible that session. Render the defect report before propagating the emit status (e.g. capture the emit rc, runsweep; render_defects, then exit with it).bin/fm-outbound-artifact.sh:874- The newrc=6row is emitted with an empty artifact field, so it renders asartifact: noneandrender_defects(line 901) describes it with the fixed prose "is waiting on <gate> with no applicable durable artifact". But this branch is reached only aftersol_artifact_presentreturned 0 and set$presentto a real comment id - the artifact exists on the forge; what is wrong is that the local record filed under that request id names a different request. The operator is told the artifact is missing when it is not, which is the same misdescription the split was made to avoid. Passcomment/$presentas the artifact argument, and giveFM_OUTBOUND_IDENTITY_REFUSEDits own defect sentence rather than the no-artifact one..agents/skills/bootstrap-diagnostics/SKILL.md:108-FM_OUTBOUND_IDENTITY_REFUSEDis a new token that now reaches session start on a DEFECT line, but the skill's OUTBOUND entry says "Read the token to choose the repair" and then enumerates onlyFM_OUTBOUND_NO_ARTIFACT,STALE_HEAD,INCOMPLETE_BINDING, andHEAD_UNOBSERVED. An operator hitting the new token gets a defect with no repair. The repair is distinct and worth stating: the correlation record filed under that request id belongs to another request, so it is re-keyed or removed, not re-emitted. (FM_OUTBOUND_CORRELATION_RECORD_MISSINGhas the same gap and predates this round.)bin/fm-outbound-artifact.sh:1483-cmd_emit "$ITEM" "" 0drops the previous|| exit $?. It is correct today only because everycmd_emitfailure path callsexit/dierather than returning; the moment one returns non-zero, reconcile continues silently and reports the next sweep's verdict instead - the same "status credited to a stage that did not produce it" class the comment directly above this line warns about. Keeping|| exit $?costs nothing and makes the property local rather than dependent on a callee's internal convention..github/workflows/ci.yml:429- Measured on this machine (read-only run of the shipped command, not the test suite): exit 0 in 67s,enrolled=3 scanned=119 unchecked=233 alive=76 could_not_observe=0. The per-file quote-walk cache and theindex($0, fn)prefilter make the repo-wide run viable, and the prefilter is sound - every rule that concludes anything embeds the function name, so a line without it could never have matched. Noting the margin rather than asking for a change: 233 of 352 consumer files are UNCHECKED, and exit 4 is red by design, so the green result holds only while every one of the 76 enrolled predicates keeps at least one call site in a parseable file. Moving a call site into a heredoc-bearing file (most test helpers) flips that predicate to could-not-observe and turns CI red with no legitimate suppression available.🔧 Fix: keep reconcile reporting and name observed outbound artifacts
3 issues (1 error, 1 warning, 1 info) still open:
bin/fm-outbound-artifact.sh:1532-( trap release_emit_lock EXIT; cmd_emit "$ITEM" "" 0 )is not an accepted call-site form forbin/fm-dead-predicate-check.sh: its trap rule is anchored^[[:space:]]*trap[[:space:]]+<fn>, and here a(precedestrap. The validation loop therefore declares the whole of bin/fm-outbound-artifact.sh an UNCHECKED consumer, which removes the file that holds nearly every enrolled predicate's call site. Measured at this head by running the shipped command: exit 4,could_not_observe=52, with its own output namingbin/fm-outbound-artifact.sh:1532 unsupported call-site form for release_emit_lock. At the previous head the same command was exit 0 withcould_not_observe=0. This turns red both the CI step added in round 2 (.github/workflows/ci.ymlrun: bin/fm-dead-predicate-check.sh) andtest_repository_has_no_dead_predicates_under_the_control, which round 2 tightened to require exit 0. Fix inside the control's own syntax without changing behaviour: put the trap on its own line -(newlinetrap release_emit_lock EXITnewlinecmd_emit "$ITEM" "" 0newline)- which matches the anchored trap rule. (An# indirect-call: release_emit_lockcomment also works but is the weaker choice, since it suppresses validation of that name file-wide.)bin/fm-bootstrap.sh:1034- The new header (bin/fm-outbound-artifact.sh:30, 55-62) promises the report renders "on the way out REGARDLESS", but at the relay that is the primary consumer it does not: whenFM_OUTBOUND_BOOTSTRAP_DEADLINEexpires, bootstraprm -f "$tmp"and prints only the deadline line, discarding every OUTBOUND row the child had already written. Continuing after a refusal instead of exiting makes that path more reachable, not less: with the forge unobservable, each item now burns up toFM_OUTBOUND_ATTEMPTStransport attempts plus backoff before the next is tried, and the probe cap only short-circuits after 40 probes - so N refusing items serialise well past the 60s deadline where the old early exit stopped at the first. The net effect is the same silence the round-2/3 fixes were about, reached through the enclosing deadline instead of through control flow. Either relay the partial child output alongside the unevaluable line, or stop the emit phase (while still rendering) once a refusal says the forge cannot be observed at all.bin/fm-outbound-artifact.sh:947-render_defectsnow gives the identity refusal its own sentence, but thecheck/statushuman view still files the same row underrender_section defect 'DEFECT - waiting with no applicable durable artifact'. On a fleet whose only defect is an identity refusal, that heading asserts an absence directly above a row body readingartifact: comment/<id>, so the two renderers now disagree about the same row. Either soften the heading to cover both (e.g. "DEFECT - the artifact this wait depends on is missing or not identifiable") or let the section name what its rows actually say.🔧 Fix: relay deadlined sweeps and split the outbound defect heading
1 info still open:
tests/fm-outbound-artifact.test.sh:457- Splitting the defect section changed whatgrep 'DEFECT - waiting with no applicable durable artifact (0)'proves. Three pre-existing assertions (lines 457, 476, 573) use that heading's zero-count as a proxy for "the fleet has no defect"; it now means only "no NON-identity defect", since an identity refusal is counted under the second heading. Not currently wrong - all three are branch-inventory fixtures on the pull-request channel, where therc=6identity path is unreachable, and each also asserts its own exit status independently - but the idiom is now a trap for the next fixture that does reach the sol-control channel. The unambiguous total is one line above:outbound artifacts: N satisfied, M defect, K could-not-observe(bin/fm-outbound-artifact.sh:947) still counts every defect row together, so assertions that mean "zero defects" are better anchored there.✅ **Test** - passed
✅ No issues found.
bash tests/fm-outbound-artifact.test.sh— all cases passbash tests/fm-dead-predicate-check.test.sh— all cases passbash tests/fm-bootstrap.test.sh— all cases pass, includingbootstrap relays what a deadlined sweep established, and still marks it incompleteandbootstrap preserves definitive outbound defect classificationManual CLI drive offm-outbound-artifact.sh status|defects|emit|checkagainst a real fixture home with the suite's forge shim (/tmp/no-mistakes-evidence/01M0AJ1DY9VXKMTEZ90GM60T0W/drive-cli.sh)Manual CLI drive offm-outbound-artifact.sh reconcilewith the first item's transport exhausted, checking post count and leftover emit locks (drive-reconcile.sh)Manualfm-bootstrap.shrun withFM_OUTBOUND_BOOTSTRAP_DEADLINE=1against a sweep that establishes a finding then stalls (drive-bootstrap-deadline.sh)bin/fm-dead-predicate-check.shagainst this repository — the same invocation the CI invariants job runs (.github/workflows/ci.yml:433)🔧 **Document** - 1 issue found → auto-fixed (2) ✅
bin/fm-outbound-artifact-lib.sh:177- bin/fm-outbound-artifact-lib.sh declares two tokens the code never emits: FM_OUTBOUND_TOKEN_AMBIGUOUS (FM_OUTBOUND_AMBIGUOUS_CANDIDATES) and FM_OUTBOUND_TOKEN_ARCHIVE_UNREADABLE (FM_OUTBOUND_DONE_ARCHIVE_UNREADABLE); no bin/ or tests/ file references either, and an unreadable done-archive is actually reported as FM_OUTBOUND_WORK_STATE_UNOBSERVED. The header presents the token block as the closed gate vocabulary, so it documents two classifications that cannot occur - the same 'exists and is never consulted' shape the dead-predicate control in this change was built for, one level down at constants rather than functions. Left alone because the resolution is a code change (emit them or delete them), which this documentation pass may not make.🔧 Fix: emit ambiguity and unreadable-archive outbound classification tokens
2 infos still open:
bin/fm-outbound-artifact-lib.sh:158- The invariant "every token declared in this block must have an emit site" is now stated in the lib header and was verified by hand for all 26 tokens, but nothing enforces it. That is the same drift the dead-predicate control exists to stop, and the control provably cannot cover this case: it scans function definitions for call sites, so a constant declared and never emitted is outside its universe by construction. The next token added to the block can go unemitted exactly as these two did, and the only thing that would catch it is another review finding. A mechanical check is cheap (assert each FM_OUTBOUND_TOKEN_* name appears at a non-declaration site) but adding a new control is a design decision, not a documentation fix, so it is proposed rather than taken: whether the check lives in bin/fm-dead-predicate-check.sh as a second mode, in bin/fm-lint.sh, or as a case in tests/fm-outbound-artifact.test.sh determines what its universe and could-not-observe answer are, which is the part worth deciding deliberately.bin/fm-outbound-artifact.sh:444- Scope note, not an unresolved gap. This step's standing rule is documentation-only, but the accepted fix instruction required emitting two tokens, which is a behaviour change; the mislabelled classification could not be fixed in prose. One existing test assertion (tests/fm-outbound-artifact.test.sh, the unreadable-archive case) pinned the old wrong label FM_OUTBOUND_WORK_STATE_UNOBSERVED and would have gone red, so it was corrected to the new token plus a negative assertion that the neighbour is absent; the two ambiguity sites got the same paired assertions. No refusal behaviour, exit status, printed count, or existing rationale comment was changed at any of the three sites. Flagged so the deviation from the documentation-only rule is on the record rather than discovered in the diff.🔧 Fix: enforce outbound token emit-site invariant with module-local control
✅ Re-checked - no issues remain.
✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.