feat(bin): generate the operator status board from fleet state - #1871
Closed
notno wants to merge 7 commits into
Closed
feat(bin): generate the operator status board from fleet state#1871notno wants to merge 7 commits into
notno wants to merge 7 commits into
Conversation
* docs(agents): trim section 2 layout tree to a thin pointer surface Replace the exhaustive annotated layout tree with a compact top-level summary plus explicit owner pointers (docs/configuration.md for layout and config schemas, producing script headers for artifact fields). Normative tree annotations without another owner survive as section 2 prose: the state/ dot-file never-edit rule, the procevent supervision-required presence rule, the symlink edit-the-original rule, and the read-script-headers-first rule. AGENTS.md drops from 63,377 to 53,563 chars. * no-mistakes: apply CI fixes
Add bin/fm-board.sh, which renders the four-column operator board - Waiting on you, Under way, Queued, Landed - as one self-contained HTML artifact built from bin/fm-fleet-snapshot.sh rather than written by hand. The script owns only what is mechanically derivable from durable state: a stamped card per live report, queued items with their unresolved blockers and date gates, recent completions with their delivery artifact, and the mechanical half of Waiting on you (locally recorded pull requests, captain-held items, and every unresolved decision key folded out of the status logs). Option lists, recommendations and captain-only chores cannot be inferred from a status line, so they arrive through --cards as authored input that merges into the same column. That input is validated as a whole and refused on any problem rather than half-rendered, and a decision key with no authored card still renders, marked unelaborated, so nothing pending can go unmentioned. The artifact carries no external reference, escapes every rendered string, reduces operator-home paths before rendering, and refuses the write if one survives. It states the snapshot's age and says when it has gone too stale to trust. The script writes the file and prints its path; it runs no poll and arms nothing.
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
Ship bin/fm-board.sh: one executable that renders the operator-facing status board as a self-contained Lavish HTML artifact, built from live fleet state instead of by hand.
WHY THIS EXISTS
The board had been hand-written three times in one evening and the columns changed each time. It works - two rounds of operator decisions came back through it - but every refresh cost ~300 lines of hand-authored HTML, and hand-authored means it drifts from reality the moment anything moves. The reference artifact is /home/nathan/firstmate/.lavish/bridge.html; it is the accepted design, not a suggestion: same four columns, same card shapes, same interaction.
THE CENTRAL DESIGN DECISION - what the script owns and what it does not
The script owns everything mechanically derivable from durable state:
The script must NOT try to derive the judgment half. An option list, a recommendation, the reasoning for preferring one option, and the "only you can do these" chores cannot be inferred from a status line - attempting it would produce confident nonsense. Instead the script has a clean seam: an optional input file of authored cards merged into the Waiting-on-you column, rendered alongside the mechanical cards. The input format was mine to design; it must be simple to author by hand and validated rather than trusted.
A decision key present in the durable logs but absent from the authored input must still appear, marked as unelaborated, rather than being silently dropped. Silence about a pending decision is the one failure this board cannot have.
INTERACTION
Reproduce the reference artifact's behaviour: choices update local state only; one send control queues a single prompt carrying every answered question and sends it; the control then resets so a further round can be answered on the same page. Answers must arrive identified by question, unambiguously - the operator's click is the payload, so an ambiguous one is worse than none.
Emit a self-contained artifact with the age of the snapshot visible and honest, including when it has gone stale enough not to be trusted. The script writes the artifact and prints its path; arming it as a wake source stays the operator's separate step, and the script must not run any blocking poll.
TESTS
Colocated per repo pattern, against fixture state rather than a live fleet: each column renders from its source; a blocked queued item shows its blocker; an unelaborated decision key still appears; malformed authored input is refused rather than half-rendered; the emitted HTML is well-formed and self-contained; no secret, token, or absolute private path leaks into the artifact. Shellcheck clean.
DECISIONS AND TRADEOFFS MADE WHILE BUILDING - deliberate, not oversights
field: valuelines, chosen over JSON/TOML because a human authors it by hand under time pressure. A small inline markup subset (bold, italic,code) is applied ONLY to authored text and only AFTER HTML escaping, so authored text cannot inject markup. Snapshot-derived prose gets escaping without markup, since an asterisk in a status line is an asterisk.binds: <task-id>:<decision-key>field validated against the live open-decision set, rather than by key-name coincidence. A typo therefore refuses the render instead of silently degrading into a free-standing card that claims to elaborate something it does not. A binds value naming no open decision is a hard refusal that lists the available identifiers.VERIFICATION ALREADY DONE
bin/fm-lint.sh clean. 20 colocated cases in tests/fm-board.test.sh pass. tests/fm-test-run.test.sh, tests/fm-documentation-audiences.test.sh and tests/fm-fleet-snapshot-view.test.sh still pass. bin/fm-doc-audience-check.sh ok. The test is registered in the snapshot-bearings family in bin/fm-test-run.sh and the runner's coverage guard passes.
KNOWN GAP, deliberately disclosed: browser automation is broken in this worktree - chrome-devtools-axi returns "Target closed" immediately after navigation, and a one-line static HTML page fails identically, so it is the environment and not the artifact. The page has not been seen rendered by a browser or a human. The CSS was reviewed by hand instead, which caught and fixed a specificity bug that rendered all fine print as body text, a missing position:relative on option labels, and missing overflow guards on headings and options. The emitted script is syntax-checked with node --check in the suite.
AGENTS.md gained one line pointing at the new command where it already discusses choosing a visual surface, per the repo's size-discipline and trigger-hygiene rules.
DELIVERY: push and PR must target notno/firstmate, never kunchenguid/firstmate. The shared gate clone's origin was repointed to the fork for exactly this reason. Repo-visible text stays neutral engineering prose and never describes the operating structure or roles.
What Changed
bin/fm-board.shrenders the four-column operator board (Under way, Queued, Landed, Waiting on you) as one self-contained HTML artifact overbin/fm-fleet-snapshot.sh --json, so the snapshot stays the single owner of fleet truth. Waiting-on-you folds in recorded PRs awaiting merge, unresolved durable decision keys, captain-held queued items, and decisions and landings rolled up from registered delegated homes. The page carries hand-written vanilla CSS with no CDN or external asset, computes its age from the snapshot's own timestamp and degrades through "going stale" to "too old to trust", queues every answered question into a single send control that resets for a further round, and the write is refused outright if an operator-home path survives into the output.--cardsfile of line-oriented[decision]/[chores]blocks is merged into that column, validated rather than trusted - balanced backticks, slug-shaped keys, andbinds: <task-id>:<decision-key>checked against the live open-decision set - with malformed input refused atfile:line, the valid identifiers listed, and no artifact written. A durable decision key with no authored card still renders, marked unelaborated. Inline**bold**/*italic*/`code`applies only to authored text and only after escaping.tests/fm-board.test.sh(37 colocated cases against one fixture snapshot, including per-column sourcing, blocker and date-gate display, refusal paths, self-containment, and leak checks), registers it in thesnapshot-bearingsfamily inbin/fm-test-run.sh, and documents the command indocs/scripts.md,docs/architecture.md, andAGENTS.md. Review and test rounds on the branch hardened snapshot shape guards, made unreadable decision state fail loudly instead of silently reporting nothing pending, widened the waiting count to include chores, and made.subblock-level so muted sub-text stops welding onto the title above it.Risk Assessment
✅ Low: Every finding raised across four review rounds is now closed and independently verified against the snapshot schema and jq's actual behavior, the change remains purely additive and read-only with respect to the fleet, and the only open item is an informational audit note requiring no action.
Testing
Ran the smallest relevant automated set - the 37 colocated cases in tests/fm-board.test.sh, all passing - and separately proved the new muted-sub-text case is a real regression test by rendering with the pre-fix bin/fm-board.sh, where it fails, then restoring. Because the defect was invisible to every text assertion in three prior rounds, I drove the actual rendered surface: a fixture fleet seeded to carry all three list-based .sub call sites at once was rendered to HTML and screenshotted in headless Chrome before and after the fix, in dark and light palettes and at both the three-column and single-column breakpoints. The before images reproduce the welded text ("Ship the mesh accuracy notewaiting on the captain's word", "Cut the 0.9 release noteneeds your sign-off on the wording", "broken-mateinvalid home: not a directory"); the after images show each reason on its own muted line, with every pre-existing .sub usage in Queued, Landed and Under way still reading correctly. I also re-exercised the answer-and-send interaction on the current artifact and confirmed one prompt is queued identified by question and binds, sent, and the control resets for a further round. Working tree left clean.
/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-3col-dark-before.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-3col-dark-after.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-3col-light-before.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-3col-light-after.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-1col-dark-before.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-1col-dark-after.png)/tmp/no-mistakes-evidence/01KZDCX5PWMJQAXZCH97F25YEH/board-send-flow-after.png)Evidence: Rendered board HTML used for the after screenshots (self-contained artifact as an operator would open it)
Evidence: New regression case fails against the pre-fix renderer
Evidence: Evidence harness that builds the fixture fleet and renders the board
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-board.sh:570- Landed concatenates two differently-ordered sources and then slices, so it does not show "recent completions" once both sources are populated.$records[] | select(.state == "done")preserves raw backlog file order (fm-fleet-snapshot.sh:parse_row keepsorderas read), while$s.secondmate_landed.recordswas already sortedsort_by(completion.date) | reverseby fm-fleet-snapshot.sh:1330. The union is never re-sorted, and[0:$landed]then takes the first N. With the default--landed 8and the live backlog's 10- [x]rows, every delegated landing falls past the cut and is silently reported as "older landings not shown" no matter how recent it is - contradicting the script's own header claim (bin/fm-board.sh:20-21, "including the landings rolled up from delegated homes"). Fix:sort_by([(.completion.date // ""), .id]) | reverseover the union before slicing, matching what the snapshot already does per home. Currently latent only becausedata/secondmates.mddoes not exist yet.bin/fm-board.sh:418- Open decisions held in a delegated home never reach the board.$workfiltersselect(.kind != "secondmate"), and$durableis folded only from$work[].hints.open_decisions, sosecondmate_current.records[].summary.decisions_open(which the snapshot populates with bothneeds-decisionandcaptain-holdentries, fm-fleet-snapshot.sh:706-709) has no reader. The board deliberately reaches into delegated state forsecondmate_landedbut not for their pending decisions, so once a secondmate is registered a captain decision in that home is invisible - the exact silence the unelaborated-key rule exists to prevent ("Silence about a pending decision is the one failure this board cannot have"). A registered secondmate also renders no Under-way card at all. Flagging rather than fixing because the intent names only three mechanical sources, so whether delegated decisions are in scope is your call.bin/fm-board.sh:366- The option<value>is the only authored identifier that is not validated, and the shell parser and the jq renderer disagree about it. The parser acceptsoption: a|b | Label(it contains ' | ') and takesoptval=a|b, but the renderer'ssub("^[^|]* \\| "; "")is anchored and[^|]*cannot cross the pipe, so the substitution does not match and the label renders as the whole linea|b | Labelwhile the radio's value isa|b- verified with jq 1.7. Separately, the emitted script joins answers withlines.join(' | ')(bin/fm-board.sh:754), so a value or title containing ' | ' makes the prompt text ambiguous, which the intent calls out as "worse than none". Validate the option value as a slug the waykeyalready is at bin/fm-board.sh:246.bin/fm-board.sh:460-bindsis parsed and hard-validated against the live open-decision set, but is never rendered into the artifact - the radio carries onlyname=<card key>anddata-question=<title>. The answer payload therefore comes back asblind-prose (Prose above blind clips): auditwith no link to the durablelisten-b:blind-srcidentity that was just proved correct. Emitting adata-bindsattribute and folding it into theanswers[...]object would make the click map back to the decision it answers without any new validation.bin/fm-board.sh:481- Every truncation runsesc | clip($n), i.e. it clips the HTML-escaped string. A cut landing inside an entity leaves a fragment (a &amp; bclipped at 5 renders literally asa &am…), and the character budget is spent on escape expansion, so a summary dense in&/</quotes is truncated far shorter than the intended 700. Reversing toclip($n) | escfixes both. Same pattern at lines 478, 492, 504-505, 524, 527, 546, 549, 554.bin/fm-board.sh:803- The final refusal guard greps only for$REDACT_HOME($HOME), not for$SNAPSHOT_HOME(the snapshot'sfm_home).redactreduces both, but the backstop only covers one of them, so when FM_HOME lives outside HOME - which the test suite itself does, FM_HOME=$TMP_ROOT/home - a field that bypassedesc/mdwould be written rather than refused. No such field exists today (I checked every interpolation in the render program), so this is defence-in-depth, but the stated boundary is "the write is REFUSED outright if the home path survives". Add the samegrep -qFon a non-empty$SNAPSHOT_HOME.bin/fm-board.sh:450-$waiting_countcounts authored decisions, unelaborated decisions, the PR card (1) and the held card (1), but not chores cards - even though a chores card renders in the same column and is literally "only you can do these". A board carrying just one chores card shows no count pill and no "Nothing is waiting on you" line, so the pill under-reports against the comment right above it ("the count that has to be honest: everything actually waiting on the operator"). Counting each chores card as 1, consistent with the PR and held cards, would close it - flagged rather than fixed because the omission may be deliberate (chores are not answerable).bin/fm-board.sh:544- Simplification: the date-gate/hold-reason rendering uses an if/elif plus a follow-upif $until != null and $r.hold_reason != nullthat re-emits the same hold-reason span. The whole three-branch construction is exactly equivalent to two independent conditionals - one emittingheld untilwhen$until != null, one emitting the reason whenhold_reason != null- with the reason markup written once instead of twice.bin/fm-board.sh:295- Simplification: the whole-file pass re-greps the entire accumulated$rowsblob once per card (printf ... | grep -qxF "$i\ttype\tdecision") to recover the card type it already knew when it appended the header. The function already keeps parallel per-card arrays (keys,have_title,blocklines, ...); pushing the type onto one more array at bin/fm-board.sh:196 turns this into an array lookup and drops a subprocess per card.bin/fm-board.sh:124-require_countaccepts any digit string, so--landed 08or--stale-mins 030passes CLI validation and is then handed tojq --argjson, where a leading zero is not valid JSON. The failure surfaces as the genericrendering the board failedat bin/fm-board.sh:791 rather than a usage error naming the flag. Stripping leading zeros (or rejecting them inrequire_count) keeps the diagnostic at the boundary that knows the flag name.🔧 Fix: fold delegated decisions into the board and fix column bounds
4 issues (2 warnings, 2 infos) still open:
bin/fm-board.sh:486- The new shape guards do not guard against the one shape they most need to.(if ((.decisions_open // []) | type) == "array" then .decisions_open else [] end)[]tests the type of the defaulted value but then iterates the raw field, so a null or missingdecisions_opentakes the then-branch and yieldsnull[]. I confirmed with jq 1.7 that both{"decisions_open":null}and{}abort withCannot iterate over null(rc=5) - a non-array such as a string is the only case actually handled. Reachable through the documented--snapshot <file>flag, whose validation at bin/fm-board.sh:157 only requireshas("tasks") and has("backlog"): a saved or older-schema snapshot with asecondmate_current.records[]entry lackingdecisions_openkills the whole render with the opaquerendering the board failed, rather than skipping that home as the surrounding comment ("Keep it defensive about shape") intends. Same hole at bin/fm-board.sh:189 (DURABLE_KEYS) and bin/fm-board.sh:504 (omitted). Fix:((.decisions_open // []) | if type == "array" then . else [] end)[].bin/fm-board.sh:184-DURABLE_KEYS=$(... | jq -r '...')has no|| die, so any failure of that jq leaves the bind-validation domain silently empty and the script continues. Becauseparse_cardsruns before the render, the first thing the operator sees is not a render failure but a confident wrong refusal: bin/fm-board.sh:320-332 reportsbinds names no open decision: <id>followed bythere are no open decisions right nowfor a decision that is genuinely open. That is the board asserting nothing is pending while something is - the failure the design says it cannot have - delivered as an exit-2 refusal. Combined with the null-hole above it is concretely reachable, but the missing check is worth closing on its own.MATE_HOMESat bin/fm-board.sh:412 is likewise unchecked, though its failure is caught downstream by--argjsonrejecting an empty string.bin/fm-board.sh:492- Delegated status-sourced decisions render with markedly less text than their main-home equivalents. The mapping istask_title: (.summary // .key)andsummary: (.reason // ""), but a status decision in the home summary is built as{..., summary:(.summary | trunc(160)), reason:null}(fm-fleet-snapshot.sh:708), soreasonis null, the<p>at bin/fm-board.sh:556 is skipped entirely, and the whole decision text sits in the<h3>clipped to 90 byescn(90)- discarding up to 70 characters the snapshot carried, with nowhere else on the card to read them. A main-home decision gets a 90-char title and a 700-char body. Captain-hold entries are unaffected since they populate both fields. Putting.summaryin the body slot for delegated entries would close it; flagged rather than fixed because the field mapping is deliberate.bin/fm-board.sh:672- The count now reports chores per item on the reasoning that each chore is a separate act, but the PR and held cards still contributen: 1regardless of how many entries they list. A board with five PRs awaiting merge and five captain-held items shows2 itemswhile a three-item chores card shows3. The same reasoning that moved chores to per-item applies to$prs | lengthand$held | length; leaving them per-card keeps the pill understating exactly the mechanical half it was widened to report honestly.🔧 Fix: guard snapshot shapes and fail loudly on unreadable decisions
2 infos still open:
tests/fm-board.test.sh:578- Theshape_case "a main-home task with a null decision list"case does not exercise the main-home guard it names. It mutates$SNAP3, whose only task issample-mate- the DELEGATING fixture's state dir holds justsample-mate.meta, written byfm_write_secondmate_metawithkind=secondmate(tests/lib.sh:245-260), and its backlog has no In-flight rows. BothDURABLE_KEYS(bin/fm-board.sh:189) and$work(bin/fm-board.sh:471) dropkind == "secondmate"before reaching the guards at bin/fm-board.sh:190 and :483, so this case passes identically against the old broken guard. The four delegated cases are real; the main-home guard - one of the two sites this round was opened to fix - is left unpinned. Point the case at$SNAPinstead, whoseship-a/listen-btasks arekind: ship, adjusting the surviving-content assertion accordingly.bin/fm-board.sh:568- Delegated captain-hold cards now print their title twice. The snapshot builds those entries as{summary:(.title | trunc(160)), reason:(.hold_reason | trunc(160))}(fm-fleet-snapshot.sh:662-663), and the board maps bothtask_title: (.summary // .key)andsummary: (.summary // "")to that same value, so the<h3>at bin/fm-board.sh:562 and the new<p>at bin/fm-board.sh:567 render identical text before the hold reason appears. For needs-decision entries the new body is exactly right - it restores the text the 90-char heading clip was dropping, which is what was asked for - but for captain-holds, whosesummaryis just the title, it is pure repetition on every such card. Gating the body on$d.summary != $d.task_titlekeeps the fix and drops the echo; flagged rather than fixed since rendering the summary in the body was an explicit instruction.🔧 Fix: route delegated decision text by entry kind, pin guards
1 info still open:
tests/fm-board.test.sh:577- Audit result for the "must fail against pre-fix code" standard, since it was asked for explicitly. Six of the eight shape cases discriminate; three variants are tolerance coverage rather than regression coverage, and all three still assert real behavior, so no action is needed. I confirmed the pre-fix forms with jq 1.7. Main home, old form(.hints.open_decisions // [])[]: null and missing both already returned[]because//catches null, so those two cases pass identically pre-fix - but"unavailable"aborts withCannot iterate over stringand["not-an-object"]aborts withCannot index string with string "key", so the main-home guard and theselect(type == "object")added in round 3 are both genuinely pinned now, which was the substance of the round-3 finding. Delegated, old form: null, missing and a nullomittedall abort withCannot iterate over null, while"unavailable"was already handled by the old guard's else-branch. The other audited cases discriminate: the broken-domain test asserts the absence of the exactthere are no open decisions right nowstring the pre-fix path emitted, and the chores-only case carries>3 items<, which the pre-fix counter (0, no pill) could not produce.🔧 **Test** - 2 issues found → auto-fixed ✅
bin/fm-board.sh:610- Rendered-board defect (now visible, since the page has been screenshotted): inheld_card(bin/fm-board.sh:609-610) andunread_homes_card(bin/fm-board.sh:584-585) the muted<span class="sub">is concatenated straight onto</strong>with no separator..sub(bin/fm-board.sh:771) sets only font-size and color - it is an inline span with no margin - and unlike every other.subusage these two sit inside<li>rather than a.rowflex column that would stack them. The result renders as run-together text: "Ship the mesh accuracy notewaiting on the captain's word", "Cut the 0.9 release noteneeds your sign-off on the wording", "broken-mateinvalid home: not a directory".pr_cardat line 596 inserts a literal space in the same position, so this is an omission rather than an intended layout. Reproduced in two independent renders (board.png, board-held.png). The suite cannot catch it: it asserts the strings are present and that the hold title occurs exactly once, both of which hold true while the words are glued together. Fixing the renderer is outside this test phase's scope, so the author needs to decide whether to land the one-character/CSS fix and add a colocated case that asserts the separation.bin/fm-board.sh:646- Pre-existing and outside this change, but newly visible on this surface: the Queued column renders the backlog itemwinlane-dwith its raw annotations inside the title - "Revisit the Windows build lane (repo: litany) (kind: ship) (hold: time gate: revisit on/after 2026-08-20) (hold-until: 2026-08-20)" - while its siblings render clean titles. The board is faithful here;bin/fm-fleet-snapshot.shitself emits that string in.titlefor this record (its sibling fieldsrepo,kindandhold_reasonall parsed correctly), most likely because thehold:value contains its own colons. bin/fm-fleet-snapshot.sh is untouched by this branch, and the same fixture line appears in tests/fm-board.test.sh, so no action is needed for this change - noting it only because it is the one visibly ragged card on an otherwise clean board.bash tests/fm-board.test.sh- all 36 colocated cases pass against fixture statebash tests/fm-test-run.test.sh- runner family registration and coverage guard pass after fm-board.test.sh was added to the snapshot-bearings familySeeded a realistic operator home (in-flight tasks with a recorded PR, an open decision key, a blocked queued item, a date-gated item, a captain hold, two landings with differing delivery artifacts) and ranbin/fm-board.sh --cards cards.txt --out board.html --title 'The Bridge'on its own live snapshot - artifact written, path printedScreenshotted the rendered artifact withchrome.exe --headless=new --screenshotover a\\wsl.localhostUNC path, after confirmingchrome-devtools-axi openfails withTarget closedandchrome-devtools-axi screenshotreports a path it never writesDrove the page's own interaction by appending an evidence-only driver to a copy: stubbedwindow.lavish, clicked an option, clicked the single#sendcontrol, and screenshotted the capturedqueuePrompttext and payload plus the post-send reset stateRewound the emittedgeneratedtimestamp in two copies to +45min and +3h and screenshotted the header to confirm the honest "going stale" and "too old to trust, ask for a fresh board" degradationCaptured a CLI refusal transcript:--cardswithbinds: listen-b:no-such-keyand with an unbalanced backtick both exit 2, name file:line, list the valid identifiers, and write no artifactRendered a second board carrying a captain hold and an unreadable delegated home (bin/fm-board.sh --out board-held.html) to confirm the run-together sub-text defect in both affected card typesLeak boundary on the real artifact: grepped board.html for the fixture token, the absolute home path, the tmux window endpoint, and<link/<script src/@import/url(http/<img- all absent🔧 Fix: block-level .sub so list sub-text stops welding onto titles
✅ Re-checked - no issues remain.
bash tests/fm-board.test.sh- all 37 colocated cases pass on the target commitRegression proof:git show HEAD~1:bin/fm-board.sh > bin/fm-board.sh && bash tests/fm-board.test.shfails onmuted sub-text takes its own line wherever it appears(not ok - muted sub-text is inline, so it welds onto the title it follows: .sub{font-size:12px;color:var(--muted)}), thengit checkout -- bin/fm-board.shrestores and it passesRendered real boards from a fixture fleet seeded with all three<li>-based.subsites (captain-held x2, recorded PR, unreadable delegated home) plus authored decision/chores cards, viabin/fm-board.sh --out ... --cards ...Screenshotted before/after withchrome.exe --headless=new --screenshotat--window-size=1600,1200(three-column) and--window-size=760,1500(single-column, below the 1080px breakpoint)Captured the light palette explicitly with--blink-settings=preferredColorScheme=1and the dark palette via headless defaultRe-ran the answer/send interaction against the current artifact with the evidence driver: one queued prompt carryingquestion+binds,sendQueuedPromptscalled, control resets toSent - ready for morewith radios and.pickedhighlights clearedConfirmedgit status --porcelainclean after all evidence work; removed the Windows temp render directorydocs/scripts.md:3- docs/scripts.md presents itself as the bin/ toolbelt inventory, but 27 shipped scripts are absent from it, including operator-relevant ones like fm-procevent.sh, fm-procevent-lavish.sh, fm-lint.sh, fm-doc-audience-check.sh, and the fm-remote-* helpers. This predates the change and closing it would mean a broad table rewrite outside this change's scope, so only the row for the new fm-board.sh was added here. Worth a follow-up that either completes the table or states explicitly that it is a curated subset rather than a full inventory.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.