Skip to content

feat(bin): derive and self-refresh the two mechanical sections of the context briefs - #1876

Open
JaredHuynhning wants to merge 8 commits into
kunchenguid:mainfrom
JaredHuynhning:fm/fm-context-briefs
Open

feat(bin): derive and self-refresh the two mechanical sections of the context briefs#1876
JaredHuynhning wants to merge 8 commits into
kunchenguid:mainfrom
JaredHuynhning:fm/fm-context-briefs

Conversation

@JaredHuynhning

Copy link
Copy Markdown

Intent

The captain reads three per-context briefs to reload himself on a project without asking firstmate anything. Two of their four sections can be derived from records that already exist, so derive them, and make that regeneration happen on its own rather than when somebody remembers. His words on 07/08/2026, choosing this over hand-maintenance: the mechanical sections must be "always current" and "cannot drift".

Why this exists and the failure it is built against: the captain loses the thread when several projects arrive at once, and the briefs give him somewhere to look instead. A brief that is quietly two days behind looks exactly like one that is current, which makes a stale brief worse than no brief. The same day produced three separate instances of that class: a service running code three days old while every signal read green, a page reporting a date taken from the clock, and a login gate nobody could see. Anything hand-maintained drifts and anything derived from a record does not.

The three files already exist at data/briefs/back-office.md, data/briefs/growth.md and data/briefs/the-machine.md, surfaced to the captain through a symlink at ~/Knowledge/Firstmate so they render in Obsidian. Their existing shape is the contract. This task automates two of their four sections and does not redesign them.

The context to repository mapping is the captain's own grouping and is not derivable: back-office covers mldinvoicing, mldaccounts and mld-bi; growth covers mld-website and MLDExpansion; the-machine covers firstmate and transformation. That mapping had to live somewhere a human can edit without touching code, and an unmapped repository had to become a visible line in the output rather than being silently dropped.

What was asked for:

  1. Regenerate the two derivable sections and only those two. "Waiting on you" comes from the captain-held backlog items for that context's repositories, grouped so the list is readable rather than a dump of thirty ids, with any item carrying a genuine deadline preserved at the top, read through the compatible backlog tooling rather than by parsing the markdown by hand. "Running now" comes from live task state, state/*.meta plus the current-state reader, and says so plainly when nothing is running. The narrative and the pointers are hand-written and the generator must never touch a character of them.

  2. Make the boundary impossible to get wrong. Explicit begin and end markers around each generated block. A run that cannot find its markers must refuse and say so, never guess and never append. A test had to put hand-written prose immediately either side of a block, regenerate, and prove the prose byte-identical afterwards, and that test had to be proved red before it was proved green, meaning it must fail against a naive implementation.

  3. Stamp both halves with their own age. Every generated block records when it was generated. The narrative section carries a last-reviewed date that the generator reads but never writes. A brief must show its own age rather than imply freshness, so the captain can see at a glance which half he is trusting.

  4. Run it without being asked, at the moments the captain named: work lands, a decision resolves, a job finishes, or he asks. Wire it to the existing lifecycle points rather than to a schedule, because a timer is the wrong mechanism and this repository's own rule says a scheduled check needs a continuous probe beside it. Also make it runnable by hand for the fourth case.

  5. Make a broken generator visible. If it stops running the blocks silently freeze and the captain keeps trusting them, which is exactly the failure this task exists to prevent. The generated block's own timestamp is that probe, so an old one must be visible in the rendered file rather than only in a log.

Definition of done as observable conditions: running the generator updates only the two derived sections in all three files with no other line changed; hand-written prose adjacent to a block survives regeneration byte-identically, proven by a test that fails against a naive implementation; a file with a missing or damaged marker causes a refusal with a clear message and is left untouched; every generated block states when it was generated and the narrative states when it was last reviewed; the generator fires on its own at the lifecycle points above, demonstrated rather than asserted; an unmapped repository appears in the output as an explicit line; and the three existing briefs still read as documents a person would want to read, checked by opening them rather than only diffing them.

Constraints that were given: follow the firstmate-coding-guidelines skill because this changes firstmate's shared tracked material; data/ is private and gitignored and the three briefs must never be committed; do not invent content, and if a section has no data say so plainly rather than writing filler; no em-dashes and no semicolons in anything a person reads, which includes every line this generator emits; Australian English.

Deliberate decisions taken while doing the work, which a reviewer reading only the diff would not know were chosen on purpose:

  • Task spawn was added as a fifth lifecycle refresh point beyond the four the captain named. Without it, "Running now" is wrong from the moment a task starts until some unrelated event fires, which is the drift the task exists to prevent. It costs roughly six seconds on a spawn, which was accepted and reported to the captain.

  • Persistent secondmates are deliberately excluded from "Running now". A secondmate is a standing direct report rather than a work item and an idle one is healthy, per AGENTS.md section 10, so listing one as running work would be wrong.

  • An unmapped repository is reported as an explicit stdout line and by --check, and is deliberately not filed into any brief. Putting it under an arbitrary context would be worse than reporting it, and putting it in all three would be noise. This is reported as a fact and never as a failure exit status.

  • --install-markers is a separate, explicit, one-time subcommand rather than something the generate path does on its own. That is what lets the generate path refuse strictly instead of ever inserting a boundary it inferred. It deletes nothing.

  • --check exists as a read-only audit but was deliberately NOT wired into the session-start bootstrap diagnostics. The captain's stated probe is the timestamp in the rendered file, and adding a bootstrap diagnostic token would have meant new machinery across AGENTS.md, the bootstrap header and the bootstrap-diagnostics skill for a signal the file already carries.

  • Intermediate records write "-" for an absent field rather than an empty one, because a tab is IFS whitespace and bash read collapses two adjacent tabs into one, which silently shifts every later column. This was a real bug found and fixed during the work, and the sentinel is the fix rather than a stylistic choice.

  • Source decision titles are relayed verbatim even when a captain-authored title contains a semicolon. Rewriting a record the captain wrote would be inventing content, which the task forbids. The no-semicolon constraint is honoured in every line the generator itself writes.

  • The generator wraps tasks-axi in a cd "$FM_HOME" helper, matching how bin/fm-decision-hold.sh already resolves the backlog, so the reads resolve against this home rather than the caller's working directory.

  • Several shellcheck SC2016 disables are present because the format strings contain Markdown code-span backticks, which a shell linter cannot tell from command substitution.

  • The new test is mapped to the snapshot-bearings family in bin/fm-test-run.sh, and lands in the portable-serial lane by default.

Verification already performed: bin/fm-lint.sh exits 0 across the repository, bin/fm-doc-audience-check.sh exits 0 with the new operator-example registered, bin/fm-test-run.sh --check-coverage passes, and the new suite plus the affected families (pr-forge, backend-dispatch, pure-contract-unit, snapshot-bearings, plus fm-test-run, fm-documentation-audiences, fm-lint and fm-gitignore-config) run green. The generator was also exercised against a throwaway copy of the captain's real briefs and backlog, and all three rendered files were read rather than only diffed.

One pre-existing failure is present and is NOT caused by this change: tests/fm-teardown.test.sh fails on "herdr-preflight-missing-adapter: the retryable pre-return refusal was not explained visibly". It was confirmed to fail identically on the unmodified base commit with this branch's changes stashed.

What Changed

  • Added bin/fm-context-briefs.sh, which regenerates the "Waiting on you" and "Running now" sections of the three per-context briefs under data/briefs/ from the captain-held backlog (read through the tasks-axi tooling) and live state/*.meta plus the current-state reader, grouping backlog items for readability, floating genuine deadlines to the top, excluding persistent secondmates, and stamping each block with its own generation time while reading but never writing the narrative's last-reviewed date. Rewrites are bounded by explicit begin/end markers: a missing or damaged marker refuses with a message and leaves the file byte-identical, staged writes are locked via fm_lock_acquire_wait with an EXIT trap cleaning temps, and unmapped repositories are reported as an explicit stdout line rather than filed anywhere.
  • Wired an --after-event refresh into the five lifecycle points (fm-spawn.sh, fm-teardown.sh, fm-pr-merge.sh, fm-merge-local.sh, fm-decision-hold.sh) under a single aggregate timeout, and added the manual --check read-only audit (a pure file read that performs no current-state work) and the one-time --install-markers subcommand, which inserts boundaries per heading and deletes nothing.
  • Registered the context-to-repository mapping and the new FM_CONTEXT_BRIEFS_* knobs in docs/configuration.md with an operator example at docs/examples/context-briefs.conf, listed the script in docs/scripts.md and the audience registry, and added tests/fm-context-briefs.test.sh (35 cases) mapped to the snapshot-bearings family in bin/fm-test-run.sh, including a red-first proof that the prose-survival check fails a naive heading-anchored rewrite.

Risk Assessment

✅ Low: The authorised fix is small, correct and verified end to end against the real tasks-axi with both surfaces now agreeing byte for byte, every load-bearing guarantee from the earlier rounds still holds under manual exercise, and the only remaining item is a bounded duplicate line with a one-line remedy.

Testing

Ran the new 35-case context-briefs suite green, then demonstrated the intent end-to-end against a fixture home mirroring the captain's three real contexts: regeneration rewrote only the two marker-bounded sections with adjacent hand-written prose byte-identical, a damaged marker produced a clear refusal leaving the file untouched, every generated block carried its own age while the narrative review line was only read, a backdated block surfaced as stale in both the rendered file and --check, an unmapped repository appeared as an explicit non-failing line, and a real fm-merge-local.sh landing refreshed the briefs with no generator call. Captured full-page screenshots of all three rendered briefs and read them as documents. Also ran the two other affected lifecycle suites green and confirmed by ablation that the flagged fm-teardown failure is pre-existing and unrelated. Worktree left clean.

  • Evidence: Rendered back-office brief as the captain reads it (deadline items first, clusters, generation stamp, untouched narrative and pointers) (local file: /var/folders/_s/01tmj3vx0bn03t1t29k76npw0000gn/T/no-mistakes-evidence/01KZDF9HD8QF7RQV49EDV81GG1/back-office-rendered.png)
  • Evidence: Rendered growth brief (secondmate correctly absent from Running now) (local file: /var/folders/_s/01tmj3vx0bn03t1t29k76npw0000gn/T/no-mistakes-evidence/01KZDF9HD8QF7RQV49EDV81GG1/growth-rendered.png)
  • Evidence: Rendered the-machine brief (empty Running now stated plainly, no filler) (local file: /var/folders/_s/01tmj3vx0bn03t1t29k76npw0000gn/T/no-mistakes-evidence/01KZDF9HD8QF7RQV49EDV81GG1/the-machine-rendered.png)
Evidence: Generator stdout: unmapped repository reported as an explicit line, three briefs updated
unmapped: repository "mldsupport" appears in the records but no context in .../config/context-briefs.conf claims it
updated .../data/briefs/back-office.md
updated .../data/briefs/growth.md
updated .../data/briefs/the-machine.md
Evidence: Only the two derived sections changed: diff of everything outside the markers is empty
### diff of back-office.md, before vs after regeneration
--- /var/folders/_s/01tmj3vx0bn03t1t29k76npw0000gn/T/no-mistakes-evidence/01KZDF9HD8QF7RQV49EDV81GG1/demo-home/back-office.before.md	2026-08-07 18:09:25
+++ /var/folders/_s/01tmj3vx0bn03t1t29k76npw0000gn/T/no-mistakes-evidence/01KZDF9HD8QF7RQV49EDV81GG1/demo-home/data/briefs/back-office.md	2026-08-07 18:09:26
@@ -8,7 +8,30 @@
 
 ## Waiting on you
 <!-- fm:brief:waiting-on-you:begin -->
-placeholder
+
+<!-- fm:brief:generated 2026-08-07T18:09:25+1000 epoch=1786090165 -->
+*Generated Friday 7 August 2026 at 6:09pm. If that date is not recent this section stopped updating, so do not trust it.*
+
+5 decisions are sitting on this side, 2 of them with a date.
+
+**These have a date on them**
+
+- **By 9 August 2026.** Approve the Monday numbers definition. (`mld-bi-reporting-decision-one`, mld-bi)
+- **By 11 August 2026.** Sign off the new late fee wording before it goes out. (`mldinvoicing-billing-decision-one`, mldinvoicing)
+
+**mldinvoicing** (2)
+
+*Raised by `mldinvoicing-billing`*
+
+- Confirm the rounding rule on part payments. (`mldinvoicing-billing-decision-three`)
+- Decide whether credit notes keep the original invoice number. (`mldinvoicing-billing-decision-two`)
+
+**mldaccounts** (1)
+
+- Pick the opening balance date for the migration. (`mldaccounts-ledger-decision-one`)
+
+Run `tasks-axi show <id>` for the full note on any of these.
+
 <!-- fm:brief:waiting-on-you:end -->
 
 ---
@@ -21,7 +44,13 @@
 
 ## Running now
 <!-- fm:brief:running-now:begin -->
-placeholder
+
+<!-- fm:brief:generated 2026-08-07T18:09:25+1000 epoch=1786090165 -->
+*Generated Friday 7 August 2026 at 6:09pm. If that date is not recent this section stopped updating, so do not trust it.*
+
+- **mldinvoicing**, in a state that could not be read. (`mldinv-latefee`)
+  Its pull request: https://github.com/mld/mldinvoicing/pull/412
+
 <!-- fm:brief:running-now:end -->
 
 ---

### every changed line lies strictly between a begin and end marker (0 = yes)
outside-blocks diff exit=0
Evidence: Damaged marker refusal, file left byte-identical, and the read-only --check audit
refused: .../data/briefs/growth.md does not carry exactly one '<!-- fm:brief:running-now:end -->' line, so it was left untouched.
exit=1
### the damaged file was left byte-identical
identical: yes
Evidence: Staleness probe: a three-day-old block is visible in the rendered file and fails --check
*Generated Tuesday 4 August 2026 at 9:00am. If that date is not recent this section stopped updating, so do not trust it.*

back-office: a generated block is 81 hours old, so it has stopped updating
back-office: the hand-written half was last reviewed 6 August 2026, morning
exit=1
Evidence: Lifecycle auto-refresh: a real fm-merge-local.sh landing refreshed the brief with no generator call
### before
12:<!-- fm:brief:generated 2026-08-04T09:00:00+1000 epoch=1785798000 -->

### fm-merge-local.sh mldinv-land (NOT fm-context-briefs.sh)
merged fm/mldinv-land into local main (87c70dd -> 60c00cc)

### after
12:<!-- fm:brief:generated 2026-08-07T18:11:07+1000 epoch=1786090267 -->
51:- **mldinvoicing**, in a state that could not be read. (`mldinv-land`)
Evidence: Generated back-office brief in full (markdown source)
# Back office

The money side. Invoicing, the books, and the numbers you read on a Monday.

*Current as at 6 August 2026, morning.*

---

## Waiting on you
<!-- fm:brief:waiting-on-you:begin -->

<!-- fm:brief:generated 2026-08-07T18:11:07+1000 epoch=1786090267 -->
*Generated Friday 7 August 2026 at 6:11pm. If that date is not recent this section stopped updating, so do not trust it.*

5 decisions are sitting on this side, 2 of them with a date.

**These have a date on them**

- **By 9 August 2026.** Approve the Monday numbers definition. (`mld-bi-reporting-decision-one`, mld-bi)
- **By 11 August 2026.** Sign off the new late fee wording before it goes out. (`mldinvoicing-billing-decision-one`, mldinvoicing)

**mldinvoicing** (2)

*Raised by `mldinvoicing-billing`*

- Confirm the rounding rule on part payments. (`mldinvoicing-billing-decision-three`)
- Decide whether credit notes keep the original invoice number. (`mldinvoicing-billing-decision-two`)

**mldaccounts** (1)

- Pick the opening balance date for the migration. (`mldaccounts-ledger-decision-one`)

Run `tasks-axi show <id>` for the full note on any of these.

<!-- fm:brief:waiting-on-you:end -->

---

## Where it stands

Invoicing is steady. The books are mid migration and the reporting sits on top of both.

---

## Running now
<!-- fm:brief:running-now:begin -->

<!-- fm:brief:generated 2026-08-07T18:11:07+1000 epoch=1786090267 -->
*Generated Friday 7 August 2026 at 6:11pm. If that date is not recent this section stopped updating, so do not trust it.*

- **mldinvoicing**, in a state that could not be read. (`mldinv-land`)
- **mldinvoicing**, in a state that could not be read. (`mldinv-latefee`)
  Its pull request: https://github.com/mld/mldinvoicing/pull/412

<!-- fm:brief:running-now:end -->

---

## Where things live

- Invoicing lives in `mldinvoicing`, the books in `mldaccounts`.

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ⚠️ bin/fm-context-briefs.sh:656 - install_one duplicates the boundary line into the generated block when the heading is immediately followed by the next ## heading or --- rule. With stop == h+1 the range sed -n &#34;$((h+1)),$((stop-1))p&#34; is reversed, and POSIX sed then prints the first address's line, which tail -n &#34;+$stop&#34; prints again. Reproduced against a throwaway home: a brief containing ## Waiting on you directly followed by ## Running now came back with ## Running now copied inside the waiting-on-you block. That also breaks the stated definition of done ("a file with a missing or damaged marker causes a refusal with a clear message and is left untouched"), because install_one for the second heading then refuses with "so it was left untouched" about a file the first install_one had already rewritten. Guard the empty range (skip the sed when stop == h+1) before --install-markers is pointed at any new brief.
  • ⚠️ bin/fm-context-briefs.sh:632 - install_markers guards with WOU_BEGIN present AND RN_BEGIN present, so a half-marked brief re-runs install_one for the pair that is already there and inserts a second copy of it. Reproduced: a second --install-markers run on a partially installed file produced two waiting-on-you:begin lines, after which check_markers refuses that brief forever and --install-markers cannot repair it because it deletes nothing. Guard each install_one by its own begin marker, and stage both insertions into one temp file so a refusal on the second heading leaves the original byte-identical, as the refusal message already claims.
  • ⚠️ bin/fm-context-briefs.sh:259 - captain_items gates only on command -v tasks-axi, bypassing the repo's shared owner of that decision, fm_tasks_axi_backend_available in bin/fm-tasks-axi-lib.sh, which fm-session-start.sh:456, fm-teardown.sh:833 and fm-decision-hold.sh:104 all use. When tasks-axi is absent, below the 0.2.4 floor, a stripped build, or config/backlog-backend=manual, the read returns nothing and render_waiting emits "Nothing is waiting on you for this side." with a fresh timestamp. The intent requires the backlog be "read through the compatible backlog tooling" and that a section with no data "say so plainly rather than writing filler"; a confident false "nothing is waiting" is the drift this feature exists to prevent, and the timestamp probe cannot catch it because the block genuinely is fresh. Distinguish "the backlog could not be read" from "no items".
  • ⚠️ bin/fm-context-briefs.sh:536 - check_markers resolves four line numbers and replace_block then head/tails on them, with no lock held in between, while five entrypoints (fm-spawn.sh, fm-teardown.sh, fm-pr-merge.sh, fm-merge-local.sh, fm-decision-hold.sh) can fire concurrently in a fleet. If a second run's mv lands between the first run's check_markers and its replace_block, head -n and tail -n +N cut at stale offsets and destroy hand-written prose, the one guarantee the change is built around, in a file under gitignored data/ with no version-control recovery. The repo already owns this boundary: wrap generate (or the per-file rewrite) in fm_lock_acquire_wait from bin/fm-wake-lib.sh, as fm-backlog-handoff.sh and fm-bootstrap.sh do.
  • ⚠️ bin/fm-context-briefs.sh:668 - --after-event now runs synchronously at the end of every spawn, teardown, PR merge, local merge and decision hold/resolve, but nothing bounds the refresh as a whole. captain_items issues one unbounded tasks-axi show --full per captain-held item (measured ~47ms each locally, so a 60-item backlog is ~3s of process launches), and running_items issues one fm-crew-state.sh per live task bounded at 20s individually with no aggregate cap, so a degraded backend with eight live tasks can add minutes to a spawn. bin/fm-session-start.sh already treats exactly these subprocesses as unbounded and runs them under one aggregate FM_SESSION_START_TIMEOUT; wrap the --after-event generate in fm_run_timed with a single aggregate bound so a hung tool cannot stall a lifecycle command.
  • ⚠️ bin/fm-context-briefs.sh:574 - The intent records as a deliberate decision that "an unmapped repository is reported as an explicit stdout line and by --check", but check() never calls report_unmapped: it only walks contexts for markers, stamps and the review line. report_unmapped is invoked solely from generate() at line 527, so the read-only audit cannot surface an unmapped repository. Either call report_unmapped from check() or correct the claim.
  • ℹ️ bin/fm-context-briefs.sh:76 - FM_CONTEXT_BRIEFS_MAX_AGE_HOURS is used directly in [ &#34;$age&#34; -ge &#34;$MAX_AGE_HOURS&#34; ] with no validation, while FM_CONTEXT_BRIEFS_TIMEOUT two lines below is validated. A non-numeric value makes the test error and return 2, which reads as "not stale", so a typo silently disables the staleness bound that is the feature's stated probe. Apply the same case ... *[!0-9]* reset used for STATE_TIMEOUT.
  • ℹ️ bin/fm-context-briefs.sh:531 - [ ! -f &#34;$file&#34; ] || [ -L &#34;$file&#34; ] collapses two different conditions into one message, so a brief that exists but is a symlink is refused with "there is no brief at $file", which is false. Given the briefs are surfaced to the captain through a symlink, an operator who symlinks individual files rather than the directory gets a refusal that points at the wrong cause. Split the message so the symlink case says it refuses to write through a symlink.

🔧 Fix: gate brief sources, bound refreshes, fix marker install
4 issues (2 warnings, 2 infos) still open:

  • ⚠️ bin/fm-context-briefs.sh:105 - Sourcing bin/fm-wake-lib.sh at file scope pulls in its unconditional top-level mkdir -p &#34;$STATE&#34; (fm-wake-lib.sh:16), which runs on every invocation before the case statement, including the two paths that promise not to do this. Reproduced: --help against a home with no state/ created it, so --check writes despite the header at line 27 and docs/configuration.md both saying it "writes nothing"; and --after-event against a home whose $STATE parent is not writable exited 0 but printed mkdir: .../state: Permission denied to the lifecycle command's stderr, breaking the silence contract that tests/fm-context-briefs.test.sh:427 asserts (its fixture always has a state dir, so it cannot catch this). A side consequence is that running_readable()'s absent-directory case is now unreachable, leaving the new guard covering only the unreadable-permissions case its test stages. bin/fm-crew-state.sh, the repo's other read-only reader, deliberately does not source this library. Source it lazily inside acquire_briefs_lock, the only consumer, so the lock owner is still the shared one but its mkdir stays off the read-only and silent paths.
  • ⚠️ bin/fm-context-briefs.sh:756 - check() now calls collect_records, which runs running_items, which launches one 20s-bounded fm-crew-state.sh plus one tasks show --full per live task. report_unmapped only ever reads column 2 of running.tsv, the repo_key, and that comes from meta_field &#34;$meta&#34; project with no subprocess at all. So a read-only audit that previously did pure file reads can now block for minutes on a degraded fleet (ten live tasks at the 20s per-task bound is up to 200s) for data it discards, and unlike --after-event there is no aggregate bound on this path. Collect only the repo identities check actually needs, or reuse the record collection without the current-state read.
  • ℹ️ bin/fm-context-briefs.sh:75 - The header states "A refresh that hits the bound leaves every brief exactly as it found it", repeated in docs/configuration.md:176. Staging is per brief inside the generate loop, so a bound hit after the second of three briefs has been moved into place leaves two refreshed and one stale. The per-brief guarantee is real and is exactly what was asked for, but the wording generalises it to the whole run, and this repository's standard is that a stated guarantee matches the behaviour. Reword both places to say the brief being written is left untouched.
  • ℹ️ bin/fm-context-briefs.sh:609 - When fm_run_timed kills the bounded --after-event child's process group, the mktemp workdir under TMPDIR and any in-flight .fm-context-briefs.XXXXXX staged beside the brief are left behind. A killed run used to be accidental; the aggregate bound makes it a designed outcome, and data/briefs is the directory the captain browses through the Obsidian symlink. The lock itself self-heals because fm_lock_try_acquire reclaims a dead holder, so an EXIT trap removing the staging file and workdir is all that is missing.

🔧 Fix: keep read-only paths silent, clean staged temps
3 issues (1 warning, 2 infos) still open:

  • ⚠️ bin/fm-context-briefs.sh:427 - Splitting running_items changed the loop from for meta in &#34;$STATE&#34;/*.meta to while IFS=&#34;$TAB&#34; read -r id rk ... done &lt; &lt;(running_identities), so the loop body's stdin is now the identity stream itself. Every child in that body inherits it: fm-crew-state.sh at line 421 and its no-mistakes axi status, no-mistakes runs and tmux descendants (nothing in fm-crew-state.sh, fm_nm_run or fm_nm_run_bounded redirects stdin), plus tasks show --full through task_title. A child that reads one line steals a live task from the list. Demonstrated with identical fixtures and stdin from /dev/null: this commit rendered 2 of 4 live tasks where fe8eacd rendered 4 of 4, and the truncated block still carried a fresh generation stamp, which is the silently-wrong-but-fresh failure this feature exists to prevent and which the staleness probe cannot catch. I could not make the real reader consume stdin in this sandbox because it short-circuits before the no-mistakes call when the backend target is gone, so the mechanism is proven and the live trigger is not. The repository's other per-task fan-out, bin/fm-fleet-snapshot.sh:410, keeps the plain for meta in &#34;$STATE&#34;/*.meta shape for the same class of reason. Redirect the state and title reads from /dev/null, or feed the loop on fd 3 with read -u 3, or keep the for-loop shape.
  • ℹ️ bin/fm-context-briefs.sh:80 - The header now states "--check writes nothing at all, so it neither creates a directory nor stages a file", repeated in docs/configuration.md as "writes nothing at all including creating no directory". Line 819 creates ${TMPDIR:-/tmp}/fm-context-briefs-check.XXXXXX. The home is genuinely untouched, which I verified, and that is the part that matters, but the claim is absolute and this is the same class of overstatement that was corrected one round ago. Either scope the wording to the home, or build the audit's two record streams without a temp directory since it only needs them long enough for report_unmapped.
  • ℹ️ bin/fm-context-briefs.sh:690 - The expensive half of the audit is gone and the new control-backed test proves it: --check no longer performs a current-state read, so the minutes-long risk on a degraded fleet is closed. collect_records still calls captain_items on the identities path though, so the audit launches the three tasks-axi compatibility probes plus one unbounded tasks show --full per captain-held item (measured ~47ms each, so roughly three seconds on a sixty-item backlog) and keeps only the repo field from each. If --check is meant to be the pure file read it was asked to become, take the repository from the repo column tasks list already prints, or gate the per-item show behind the state depth the same way the running read now is.

🔧 Fix: stop children stealing records, make audit a pure read
1 warning still open:

  • ⚠️ bin/fm-context-briefs.sh:396 - A captain-held item with no repository makes both surfaces name a repository that does not exist, and this commit made them disagree on what to call it. Reproduced against the real tasks-axi 0.2.4, which lists an absent repo as the quoted literal &#34;-&#34;: captain_identities takes field[at["repo"]] verbatim and sub(/^.*\//, &#34;&#34;, key) leaves the quotes, so --check prints unmapped: repository &#34;&#34;-&#34;&#34;, while generate keeps printing unmapped: repository &#34;-&#34; because captain_items strips the quotes to empty and report_unmapped's while IFS=&#34;$TAB&#34; read -r key display skips the leading tab, promoting the display sentinel into the key. Checked across the branch: the generate-side &#34;-&#34; is present in all four commits and is not introduced here, the &#34;&#34;-&#34;&#34; divergence is. Neither is invented content in a brief, only on stdout, but the feature's premise is that the platform never asserts something it did not verify, and --check is one of the two surfaces the definition of done names. Fix at the shared boundary both producers feed: have report_unmapped treat an empty or - key as absent rather than as a repository, and strip tasks-axi's quoting in captain_identities so the audit and the generate path agree on the value.

🔧 Fix: report a record with no repository as such
1 info still open:

  • ℹ️ bin/fm-context-briefs.sh:875 - The third kind column is appended before seen=$(LC_ALL=C sort -u), so two rows that were previously identical no longer collapse: a repository that appears both as a captain decision's repo and as a live task's project is now reported twice. Reproduced with one fixture across the branch, identically on both surfaces: shared-orphan was reported once at ff12dda and e3b09bd and twice at 295fa17. Nothing stated is wrong and it is bounded at two lines per repository, but the definition of done words it as an explicit line and this is the normal shape for a new project not yet added to the mapping, so the noise lands exactly when the report matters. The kind is only needed to word the sentinel rows, so classify first and dedupe the named-repository rows on key and display alone, or drop duplicates from the emitted messages.
✅ **Test** - passed

✅ No issues found.

  • bash tests/fm-context-briefs.test.sh (35 cases, exit 0, including the red-first proof that the prose-survival check fails a naive heading-anchored rewrite)
  • Built a fixture FM_HOME with the captain's real mapping (back-office/growth/the-machine over mldinvoicing, mldaccounts, mld-bi, mld-website, MLDExpansion, firstmate, transformation) plus a stub tasks-axi backlog, live state/*.meta and a persistent secondmate, then ran bin/fm-context-briefs.sh
  • Byte diff of everything outside the four markers, before vs after regeneration, across all three briefs (empty diff)
  • Deleted one &lt;!-- fm:brief:running-now:end --&gt; marker, re-ran the generator, and confirmed the refusal message plus cmp-identical file while the other two briefs still regenerated
  • Backdated a generated stamp to 4 August and ran bin/fm-context-briefs.sh --check (reported an 81-hour-old block, exit 1)
  • bin/fm-context-briefs.sh --check on fresh briefs (exit 0, reads and reports the narrative review line it never writes)
  • Ran the real lifecycle command bin/fm-merge-local.sh mldinv-land with no generator call, and confirmed the stamp refreshed and the landed task appeared in "Running now"
  • bin/fm-context-briefs.sh --install-markers against an unmarked and a partially marked brief
  • grep -n &#39;—\|;&#39; over the three generated briefs (none found) and git ls-files | grep &#39;^data/&#39; (zero tracked briefs)
  • Rendered all three briefs to HTML with pandoc and captured full-page browser screenshots to read them as the captain would
  • bash tests/fm-decision-hold-lifecycle.test.sh and bash tests/fm-pr-merge.test.sh (the other directly affected lifecycle call sites, both green)
  • bash tests/fm-teardown.test.sh with and without this change's five-line call site, to prove the herdr-preflight-missing-adapter failure is pre-existing
⚠️ **Document** - 1 info
  • ℹ️ docs/decision-hold-lifecycle.md - Judgment call: docs/decision-hold-lifecycle.md is the mechanism owner for hold/resolve, and those subcommands now also refresh the context briefs. I deliberately did not add that fact there, because docs/configuration.md "Context briefs" already owns the complete list of lifecycle wiring points and a second copy would be a prose duplicate to keep in sync. A reviewer who considers the decision-hold doc the owner of every hold/resolve side effect would place a one-line pointer there instead.
✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

…cal sections

The captain reads three per-context briefs to reload himself on a project.
Two of their four sections were hand-maintained and drifted, and a brief that
is quietly two days behind looks exactly like one that is current.

bin/fm-context-briefs.sh now owns those two sections. "Waiting on you" comes
from captain-held backlog items read through tasks-axi, grouped by repository
and clustered under the work that raised each set of decisions, with any item
carrying a recorded date lifted to the top. "Running now" comes from
state/<id>.meta reconciled by bin/fm-crew-state.sh, and says so plainly when
nothing is running. The narrative and the pointers are never touched.

The boundary is four explicit markers. A brief whose markers are missing,
duplicated, reversed, or overlapping is refused and left untouched, so the
generator never guesses a boundary and never appends. Hand-written prose
immediately either side of a block survives byte for byte, and the test that
proves it is itself proved against a naive heading-anchored rewrite first.

Both halves show their own age. Each generated block opens with the date and
time it was generated, worded so an old date reads as a warning rather than as
freshness, and --check audits those stamps plus the narrative's own review
line without writing anything.

Regeneration is wired to the lifecycle moments the records change rather than
to a schedule: opening or resolving a captain decision, spawning a task,
landing work through either merge path, and finishing a task. Each calls
--after-event, which is silent, no-ops without a mapping, and always exits 0
so a lifecycle command never fails because a reading surface could not refresh.
tests/fm-context-briefs.test.sh demonstrates that by driving a real local
landing end to end rather than asserting the wiring.

config/context-briefs.conf maps contexts to repositories, hand-edited and
gitignored because the grouping is a human judgement. A repository present in
the records that no context claims is reported as an explicit line.
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