NOL-1 — the owner's one email cuts every NEEDS-OWNER: decision at 240 characters in both renderers, and ops/STEWARD.md mandates the recommendation and the deadline in exactly the positions the cut removes: today's digest carried 5 asks and 0 recommendations, and it is the only digest that will ever carry them
status: open · raised 2026-10-09 (panel run 38; surfaced independently by all three lenses and the chair, then narrowed at adversarial review from three legs to two) · class: a producer and its single reader disagree about what was delivered — the class named by the 2026-10-08 daily-audit memo, third instance · judgement: YES · effort ~0.1 pd · horizon: fired 2026-10-09 and repeats every Friday until fixed
Plain-language summary for an owner reading one paragraph. On 2026-10-07 you
stepped back to one email a week, and the steward now writes the decisions only
you can make as NEEDS-OWNER: lines that lead that email. Two documents
contradict each other. ops/STEWARD.md tells the steward to write each line as
<decision> — <recommendation> — <deadline if any>, with "a one-paragraph
recommendation". The digest then prints only the first 240 characters of it. So
what survives is the problem statement, and what is thrown away is always your
answer and your deadline. In this week's email the phrase "My recommendation"
appears zero times across all five asks. Nothing marks the lines as cut —
no ellipsis, and in the HTML version no pointer to where the full text lives.
The fact that makes this irreversible rather than cosmetic
_gather_steward_week windows on lo = today - 7 days. Measured against the
real committed files:
_gather_steward_week(2026-10-09) -> reports ['2026-10-07','2026-10-08'], 5 needs
_gather_steward_week(2026-10-16) -> reports [], 0 needs
The 2026-10-09 digest is the only digest that will ever carry those five recommendations. Next Friday's window starts at 2026-10-09 and excludes the 10-08 report entirely. This is not "he sees it in full next week" — the text left the email channel permanently when today's digest rendered. (The scheduled send is 15:50Z but the last eight scheduled runs started 20:10Z–22:12Z, so by the time this row is read the send has most likely already happened. The row claims a loss, not a preventable event.)
Ground truth — the live render driven against HEAD c680133 for 2026-10-09
Five NEEDS-OWNER: lines of length 655 · 590 · 408 · 263 · 344 after
_steward_needs strips the 13-character prefix. All five exceed 240, so 5 of
5 are truncated; the first loses 63% of itself. Probing the rendered body:
| probe | in the owner's email |
|---|---|
My recommendation (in all 5 source asks) | LOST, 5 of 5 |
before 10-16 · Rule before 10-19 | LOST |
60-minute grace (the B4 countersign recommendation) | LOST |
2026-10-20 (Actions-allowance exhaustion warning) | LOST |
BTB-1's ruling · dated append | LOST |
captured sessions | present — but only inside the premise "captured sessions or elapsed trading days"; the recommendation choosing it is lost |
Verbatim cut points: ask 1 drops "My recommendation: spend ten minutes before 10-16 …"; ask 2 "… My recommendation: countersign with a 60-minute grace …"; ask 3 "… Choosing after the result is visible would be gate-shopping. My recommendation is captured sessions …"; ask 4 "BTB-1's ruling waits on it."; ask 5 "Record the choice as a dated append before the gate matures (~2027-01)."
Report headlines are truncated too (headline[:90], :834) at source lengths
142 and 191.
The HTML twin is strictly worse than the text twin. The text renderer at
:833 writes "{n} steward report(s) (research/steward/):", at least naming
the directory holding the full text. The HTML block (:1055-1058) renders only
{date} — {headline[:90]} and omits research/steward/ entirely. In a mail
client the owner sees five half-sentences with no ellipsis telling him they are
half-sentences and no pointer, anywhere, to the rest.
LEG 1 — the 240-character cut
The causal chain, all four links verified at HEAD:
ops/STEWARD.md:64(repeated at:153) requires "NEEDS-OWNER:lines (§5) with a one-paragraph recommendation" — the lines are longer than 240 characters by instruction, not by accident.ops/STEWARD.md:243pins the field orderNEEDS-OWNER: <decision> — <recommendation> — <deadline if any>— recommendation second, deadline third, placing the payload in the discarded tail.fleet_digest.py:828f" • {n[:240]}"and:1051_html.escape(n[:240]). The comment at:1041calls the HTML block an "HTML twin … (content parity)"; parity faithfully reproduces the truncation.ops/STEWARD.md:250-253states the requirement the renderer breaks: "That section is the owner's only view of you — make each line stand on its own for someone who has not followed the week."
LEG 2 — the overflow cap drops the NEWEST asks, not the oldest
needs[:10] (:828, :1051) caps the list at ten. The gather iterates
for p in sorted(Path(root).glob("*.md")) — filename order, which for
YYYY-MM-DD.md is date-ascending — and needs.extends in that order with
no de-duplication. So the cap retains the oldest lines and silently
drops the newest.
Demonstrated (six reports × two asks = 12 needs against the cap of 10):
KEPT ask 1/2 written on 2026-10-09 … through … 2026-10-15
DROPPED ask 1 written on 2026-10-16
DROPPED ask 2 written on 2026-10-16
The freshest and most deadline-bound lines are the ones that vanish, beneath a count line printing the true N. Two further consequences:
- The
🙋 N need youcount is a count of lines, not of decisions. Because the steward's "Carried forward" practice restates standing asks and nothing de-duplicates, one decision restated across six daily reports renders six bullets and a🙋 6 need yousubject. Demonstrated:len(needs_owner) == 6againstlen(set(...)) == 1. - Restatements therefore consume cap slots before distinct asks do, so the cap is reached by carry-forward rather than by volume.
Today the cap did not fire (5 of 10). Honest note on size: an earlier draft
of this row projected ~25 needs next Friday from "~5 reports × ~5 needs". That
projection is withdrawn — the only two real reports carry 0 and 5
NEEDS-OWNER: lines (the 10-07 report's two were correctly re-prefixed
DISCHARGED/SUPERSEDED), and n=2 cannot support a weekly rate. The leg rests
on the mechanism, which is verified from the code and needs no rate assumption.
Why nothing caught it — two tests, each blind one layer off
tests/test_execution/test_digest_cadence.py:337-349— added by #228, the fix for panel run 37's own lead finding — asserts on_gathered(...)["needs_owner"], i.e. the gather, which returns full lines. It never renders. Its fixture at:305writes"NEEDS-OWNER: decide {d}", ~29 characters. #228's own merge verification stopped at the same layer, recording that "_gather_steward_week(2026-10-09)returns exactly the five intended NEEDS-OWNER lines". The verification stopped one layer above the truncation.tests/test_execution/test_email_policy.py:367-376does render and asserts the full line appears in both twins — the right property — but its fixture is 47 characters, so it never reaches the cut. Substituting today's real 655-character line into that same pair of assertions makes both FAIL, in text and in HTML.
Two negative controls, run twice (chair and adversarial reviewer
independently): transiently setting n[:240] → n in both renderers
leaves the full suite at 1745 passed / 1 skipped — identical to baseline;
so does needs[:10] → needs[:1]. Nothing in 1745 tests pins either the
length or the cap, and 240 appears in no config file — it is not a knob.
Both edits reverted; tree clean. ops/RESEARCH_PANEL.md's rule 4 — "a detector
that cannot fail is the finding" — applies literally.
What the loss cost this week
The sharpest measure is in the discarded tail of ask 1: "the Actions allowance is forecast to run out again around 2026-10-20, which would darken the backup trading leg and the digest." Three facts compound it:
- It is the only place in the repository that records the forecast —
grep -rn "2026-10-20|allowance"overresearch/steward/,ops/STEWARD.mdand the 10-08 audit memo returns exactly that one line. - The precedent is recent.
CLAUDE.md:389records that the allowance's exhaustion "refused every job 2026-09-21 → 09-30", andweb/public/notes/2026-09-21_actions_refused_every_job_over_billing.mdrecords the result: no trading, no state, no site export, no capture, for eight trading days. - The topology changed hours before the send. Since 2026-10-09 01:00Z the
box is primary and Actions is the only backup — and Actions also runs
fleet-digest.yml, the site export and the beacons. An exhaustion near 2026-10-20 would darken the backup trading leg and the owner's only reporting channel together.
Ask 3 is the one with a near deadline: it asks the owner to pin whether the
60-trading-day kill criteria for shortability and options_iv_term count
captured sessions or elapsed trading days. Measured both ways: shortability
44 of 60 captured (60th = 2026-10-30) vs 53 of 60 elapsed (60th = 2026-10-19);
options_iv_term 40 of 60 captured (2026-11-05) vs 48 of 60 elapsed
(2026-10-26). Choosing after the result is visible is what C0 forbids ("no
threshold moved after its data exists"). The owner received the question and the
date; the recommendation that would let him answer in one word was cut.
What this row does NOT claim. It is information loss, not a safety incident.
Ask 1's lost advice — "read the cutover log on the Mac and fix the SSH
transport" — was already stale when the email rendered: the cutover had
succeeded at 2026-10-09 01:00:22Z. So this week the truncation happened to
delete advice that was obsolete and mildly misleading. An earlier draft argued
the lost line might lead the owner to restore the retired dispatch plists; that
was dramatization and was killed at review — the line never says to restore
them, and both cutover.sh's header and RUNBOOK.md:16 forbid it. The row
stands on the structural claim alone.
Killed at review — recorded so it is not re-proposed
A third leg, "the steward section has no freshness check", was KILLED. The
discharge mechanism exists and works: research/steward/2026-10-07.md:50 shows
the steward rewriting its own line to begin "DISCHARGED 2026-10-07 23:58Z (… marked by the steward 2026-10-08 so the Friday digest stops presenting it as open)", and _gather_steward_week then correctly excluded it — the 10-07
report contributes 0 needs, measured. What remains is a ~19-hour race (the
cutover succeeded 01:00Z, the digest fires ~20:00Z, the steward's next run is
23:00Z). A one-cycle lag with a functioning discharge mechanism is not a queue
row.
Resurrection condition: a NEEDS-OWNER: line survives two consecutive
steward runs after the fact motivating it is demonstrably resolved — i.e. the
discharge mechanism misses, rather than merely lagging one cycle.
Fix shape (propose-only)
- Raise or drop the per-line cap for
NEEDS-OWNER:bullets in both twins. If a layout bound is wanted, set it far higher, append an explicit…, and add a pointer toresearch/steward/<date>.mdin the HTML twin, which today has none. - Replace
needs[:10]with all-lines-plus-a-count, or at minimum reverse the retention order so the newest survive; de-duplicate restated asks and make the🙋 Ncount a count of distinct decisions. - Carry each bullet's source report date (
_steward_needsdiscards it), so a 7-day-old Friday ask is distinguishable from today's. - Or fix it upstream: have
ops/STEWARD.md:243mandate recommendation-first. One line, no code — but see the kill criterion: that alone is not sufficient. - Re-fixture
test_email_policy.py's render assertion with a realistic line (≥400 chars with a trailing "My recommendation: …" clause) so it can fail.
Observability tier only — no trading path, no config value, no pinned rule, no
frozen text; it cannot fail-close trading. ops/STEWARD.md §0 authorizes the
steward to author observability fixes but not to merge its own work; per the
10-08 memo's lesson ("author and merger must be different agents") the daily
audit should author and the steward merge, as they did for #228.
Pre-registered kill criterion
An earlier draft proposed "the fraction of lines whose recommendation clause
survives, ≥0.90 kills the row". That was rejected at review as gameable: it
is not mechanically measurable (the same data reads 0/5 under the probe
"My recommendation" and 1/5 under "any imperative advice in the surviving 240
characters" — ask 4 carries its recommendation inside the cut), and its own
named cure games it, since moving "My recommendation:" to character 1 scores
1.00 while the deadline and the reasoning are still cut.
The criterion is structural instead. Over the research/steward/*.md set
gathered by the Friday digest at evaluation time, measure (i) the count of
NEEDS-OWNER: lines whose rendered form differs from the gathered form,
in each twin, and (ii) whether len(rendered bullets) == len(gathered needs). The row dies when (i) is 0 in both twins and (ii) holds, on two
consecutive Friday sends in which at least one gathered line exceeds 240
characters — the long-line precondition stops a quiet week from discharging
it. Today: (i) = 5 and 5; (ii) 5 == 5 holds, so leg 2 is not yet firing.
Also dies if the owner rules that a 240-character brief plus a pointer is what he wants from this channel — but the pointer must then exist in the HTML twin, which today it does not.
Reopens if a fix lands and a later digest is nonetheless observed to deliver
a NEEDS-OWNER: ask whose rendered form differs from its gathered form.
Prior art (C0) — clean, checked before proposing
grep -rni "truncat|\[:240\]|240 char|NEEDS-OWNER" over research/queue/ (all
four dirs), research/steward/, RESEARCH.md, TECH_DEBT.md, AUDIT.md,
web/public/data/killlist.json and
research/2026-08-01_quant_panel_suggestions.md: the only truncat hits are
dismissed/lab-3 (a hash digest), dismissed/auto-approve (shallow clone) and
built/ksp-1 (a kill-switch band). No nol-style or owner-line slug exists
anywhere in the tree. Nearest neighbours, all different surfaces:
open/dgx-1 (the cadence gate vs rendered alarms), dismissed/fos-2 (the
fleet-email streak split), and run 37's killed-at-review F1(c) (rendering
thales captures as a digest exception). Not a relitigation of any of them, and
distinct from the handed-off C2/C3/C4/Q1/Q2 set.