Dev Journal — 2026-08-26 to 27
Two days, 37 commits, D-031 to D-049. This is the concepts journal, not a changelog: what we
learned and why it changed the design. The decision records in specs/decisions.md carry the
detail; this is the through-line.
The one idea that shaped everything
Almost every serious bug in this system shares one shape: the code keeps running, the logs
keep reading healthy, and something is quietly doing nothing. None crashed. None logged an
error. Each was found by accident — reading a journal, noticing two numbers disagree, a test
written for another purpose.
By the sixth instance it stopped being bad luck and became a taxonomy (specs/notes/012):
| class | what it looks like | the question that catches it |
|---|---|---|
| Silent no-op | a path that never runs, or runs and does nothing | Can I tell "ran and found nothing" from "never ran"? |
| Absence-as-zero | a missing value coerced to a benign default that LOOSENS a constraint | Does this default make things safer or looser? |
| Unit confusion at an LLM boundary | a number in the wrong units, accepted | Is it range-checked against something the system computed? |
| Accumulation not priced | correct per-event weight, unbounded repetition | What is the CUMULATIVE bound? |
| Identity collision | two logical things sharing one key | Is the key unique per entity, or per category? |
| Order-dependence as decision | a tie broken by list order | If both are true, is the winner chosen or accidental? |
Three mechanisms came out of it, and they are the most reusable thing built in two days:
trdrbot health— asks of every subsystem: ran ≥ threshold and produced nothing?
doctoranswers "can this system start"; health answers "is it doing anything". It found a
real problem on its first run.- Instrument the null path. Any early
continue/returnmeaning "nothing happened" must
leave evidence saying why. Success paths get instrumented by habit; null paths fail quietly. - One test per bug that actually happened. 72 now. Verification done in a throwaway shell
snippet protects nothing against the next edit — which is what we had been doing all week.
The mechanism that has caught the most, without being designed to: two independent paths
computing the same quantity, with the disagreement surfaced. Stale bars died because the decide
cycle checked research's numbers against live quotes out loud. Bootstrap drift died because a
convergence test compared two estimators that should have agreed.
Concepts we built
The funnel got a top
The system could only act on theses it was handed. Now it makes them.
- Research cycle (D-032) — computed technicals + news + prediction-market odds → a regime
page and company dossiers in the wiki → falsifiable opportunities into the inbox. - Discovery (D-035) — the news nominates the companies. Broad market news → the LLM
nominates 3-5 names it must justify from cited evidence → each nominee goes through the same
deterministic gauntlet → only then a second LLM call synthesises opportunities.
The division of labour is the point: numbers are computed, never asked of the LLM; the LLM
does synthesis and judgment only; event dates must come from supplied material, not model
memory. On its first run the research desk correctly wrote "date unknown" four times rather than
recalling one.
Bootstrap Monte Carlo, and the bug that taught us most
Instead of assuming lognormal returns, resample the stock's own history. Real returns have
fatter tails; the gap between lognormal-P(profit) and bootstrap-P(profit) is itself the
signal — when they diverge, the edge depends on the tail assumption.
The lesson was in getting it wrong. A convergence test (bootstrap of normal data must match the
closed form) failed by 16 percentage points, because raw resampling inherits the sample
period's directional luck: a year that happened to rally gets projected forward as structural.
Recency bias with a formula wrapped around it. Demeaning fixed it — keep the shape, strip the
luck, and let direction be something the agent states rather than something the data
smuggles in.
The agent later read this correctly and unprompted: "the bootstrap disagreement… is directional
and it makes sense. Real returns have fatter tails, so the model overstates short-premium win
rates (74→68, 61→44)."
Risk shape as a first-class input (D-040)
We simulated only terminal outcomes. No delta, gamma, theta, vega anywhere. Now: Black-Scholes
computed in-house (r=0; rho omitted deliberately — at ≤7 DTE it is cents), per-candidate greeks
in trader units, per-leg IV so measured skew is priced rather than erased, the market's 1σ
expected move rendered against the thesis band, entry greeks stored on each position, and
book-level greeks in every decide cycle — which turns "three bullish spreads on three tickers
are one +delta/−vega bet" from a sentence into a number.
Refusal discipline throughout: 0 DTE or 0 IV returns None, because an expiring option has a
cliff, not smooth sensitivities.
The constitution (D-041, D-049)
Ten epistemic principles seeded into elfmem's SELF frame with PERMANENT decay, each traceable
to a real incident. The standing test: a principle you cannot trace is a platitude.
Scope was the hard part. Anything a deterministic check can enforce stays in code — so
luck-neutrality, friction, payoff-ratio refusal and the book caps are all deliberately not
principles. What remains is the residue nothing else reaches: how to reason, and how to
remember.
Getting them visible took four silent gates: blocks queue in an inbox until consolidation;
consolidation rewrites content and the rewrite is what renders; consolidate() caps at 5
per call so the surplus gets LLM-analysed anyway; and top_k defaults to 5, so ten principles
render five. Each passed a naive success check.
Then the agent cited "Principle 10" for principle 3 — the frame re-orders by proven
load-bearingness, so numbers mean nothing. Prompting it to cite by wording failed, because the
rendered list showed only numbers. The fix was structural: put the name in the block,
[regimes] A pattern learned in one regime is…. The only handle worth citing is one the agent
can see.
Learning that actually learns
- Interim scoring bounded (D-034) — one unresolved position had accumulated eight interim
scores, 0.8 of evidence against a real resolution's 1.0, every onehit=Falsefrom a −$45
bid/ask wobble on a healthy spread. The per-event weight was right; repetition was the hole
nobody priced. Now fires only on first entry into a materiality band. - The constitution shielded from P&L (D-042) — credit assignment flattened all frames, so a
losing trade would have pushed signal onto all ten principles, which carry PERMANENT decay and
would never recover. Split credited (task+attention) from provenance (all frames). - Minds shielded from interim marks (D-043) —
mind_outcometakes a binary hit with no
weight, so every interim score recorded a full miss: the SPY mind sat at confidence 0.34,
0/1, on a position that was profitable and whose horizon had not arrived.
Judgment, not gating
A recurring design principle, stated once and applied everywhere: observability beats gating.
record_position now reports which signals a position's exit rules actually watch, and warns
when only the noisy mark is watched. health reports rather than blocks. The one exception is
close_all_positions (D-046), refused while more than one position is open — the agent reached
for it meaning to close one spread and followed it with a sell on a leg the sweep had already
closed; only fill sequencing prevented a naked short put. That is not a judgment gate: it
refuses one instrument whose blast radius exceeds any single-position intent.
What to do when nothing is happening (D-043)
An empty inbox is not one state. Running every analysis every tick is the amateur answer — it
costs money and manufactures activity. A ladder instead: sleep (nothing at risk, nothing
moved, looked recently) → review (a 0.4% move under a held underlying, or 90 minutes of
silence while holding risk) → hunt (capital idle and deployable).
The asymmetry that sets the thresholds: the cost of not looking scales with what is at risk;
the cost of looking scales with LLM spend. And the case we kept getting wrong: idle capital
is a position too — 100% cash at 0% expected return.
Gated on being able to act: no hunting when the risk cap is full. Do not hunt when you
cannot shoot.
Size earned by competence, not the calendar (D-047 → D-048)
The most instructive sequence of the two days, because the first answer was wrong.
We measured the sizing and found a deadlock, not a conservatism trade-off:
resolved trades n=0 n=3 n=5 n=7 n=8 n=12
contracts sized 0 0 0 0 0 1
With no record, shrinkage pulls a stated 70% to the base rate, Kelly on a typical payoff lands
at exactly 0.000, and the system can never place the trade that would build the record it
needs in order to be allowed to trade. Eight days of 0% was the live trajectory.
The resolution: size and learning rate are independent here. You learn from the number of
resolved theses, not their size, and survival is guaranteed structurally by defined-risk legs
plus a book cap. So sizing tiny buys no extra safety and no extra learning — it only shrinks the
result. A desk hands a new trader a bounded exploration allocation and expects to pay for
the information.
First attempt phased this on days-to-deadline — which had a bug waiting for the day after: once
days_left went negative it would enter no-new-risk permanently. Replaced with a
competence ladder (explore → establish → scale → mature) with the calendar removed from
sizing entirely (there is a test asserting assess() contains no date logic). The deadline
became a position-level horizon check: can this specific trade resolve before a hard stop?
The distinctive criterion: attribution is a promotion gate, not just a metric. A profit on a
wrong thesis is luck, and a book of luck is not competence however good the P&L. Promotion past
ESTABLISH requires that most resolved theses were attributable — that the agent knows why
it was right.
Two ladder inversions were caught by a monotonicity test, both of which had shipped: an earned
record sizing smaller than an unproven one, and promotion from EXPLORE to ESTABLISH taking
sizing from 1 contract to 0. Root cause of the second: below MIN_SAMPLE the shrinkage is a
blunt heuristic, not a measurement — it must size down, never veto.
Prompts: provenance now, machinery later (D-045)
Measured: 8 artefacts, ~3,140 tokens. They are not homogeneous, and that decides the design —
four free-standing prompts (extractable), three tool-contract docstrings that are a third of
the surface and must stay welded to their function signatures, and one ratified constitution
already living in elfmem.
The A/B point that settles it: P&L cannot be the metric. Our own power calculation says a
genuinely 60%-edge agent beats a coin flip only 69% of the time over 20 trades. Two prompts
cannot be distinguished by returns in this window. What can be measured are behaviours,
which accrue per cycle rather than per trade — 50+ decide cycles against ~10 trades.
So: build the provenance, defer the machinery. Every decision journals {prompt: fingerprint},
because that is the only part with a deadline — a decision recorded today without a prompt label
can never be compared against tomorrow's.
Working with a dependency in flight
We integrated elfmem while it was actively being developed, and wrote two field reports back
(elf0_mem_sim/docs/). Both were actioned within a day: FrameResult.dropped,
LearnResult.pending_consolidation, ConsolidateResult.analyses_unused, OutcomeResult.
skipped_constitutional, and ATTENTION's exclude_tag_patterns all shipped.
The most useful thing that came back was a correction: one recommendation rested on a source
comment that turned out to be wrong. The report reasoned correctly from a false premise. Worth
remembering — a confident diagnosis is only as good as the comment it trusted.
Two workarounds are now deleted because upstream fixed the cause properly. That is the right
outcome, and it only happened because the reports were specific enough to act on: real error
text, real measurements, and a clear separation between what we needed and what we merely
preferred.
Where it stands
Flat → one closed profitable trade → one live position. Equity $100,211.
The first NVDA trade is the first attributable thesis on the board: real band [220, 245],
horizon 2026-09-03, underlying_stop at 218 (its own stated gap-fill level), risk derived not
declared, entry greeks recorded. The previous position could never be scored at all.
And the reasoning that opened it is the thing worth keeping:
"The edge is genuinely small and I won't dress it up. … The 230/240 was the only positive
one, at +$10/contract … Honest probability 38%, which is what I recorded — a
losing-more-often-than-not trade with 3.8:1 payoff, not a 'probably wins' trade."
An agent that records 38% on a trade it chooses to take is an agent whose calibration means
something.