← build log 2026-09-02

Dev Journal - 2026-09-02: the loop closes, and the process that wasn't running it

The brief was the whole system at once: build the one safe self-improvement signal, then drive
everything end to end, find what is wrong, and fix it without adding weight. Twelve decisions
landed. The one that mattered most was not a bug in the code. It was that the code was not
running.


Gate regret, computed

I-73 had said it plainly: a rejected thesis is registered with probability_stated=False so it
never enters calibration, and it still carries a band and a horizon so it still resolves. "We
refused it and it would have held" is therefore a score for the gate computed from outcomes the
gate cannot reach - the only self-improvement signal in the system that does not violate the
measured/measurer rule. Recorded since D-081. Never computed.

Now it is, and the first live reading says something: the no_chain gate refused theses that held
40% of the time against 42% admitted. That gate was a throttle, not a filter. Mutation-verified both
ways - letting a rejected row into calibration fails, computing the baseline over everything fails
- because the safety argument is the first assertion.

The scaffold that found a bug on its first run

scaffold_whole_system.py drives every deterministic stage against a fake broker on a copy of the
real data: snapshot, reconcile, exit rules, the decide-cycle tools in the sequence the agent calls
them, housekeeping, the ledger, calibration, the ladder, the Coach's tally. Eight scenarios. The
question at each seam is not "does this function work" but "does the loop close".

S1 failed on the check that matters most. A thesis the agent had traded resolved, and
calibration did not move.

simulate_experiments pre-registers at a 0.5 placeholder - correct, the trial must count for N.
record_position then took the agent's confidence, the number size_position had just sized
against, documented to the agent as "scored ... Brier/Murphy calibration", and only linked the
position. Confirmed live: 13 thesis rows, 2 traded, 0 stated. The claims with money behind
them were the only claims outside the record that decides how much money goes behind the next.

The suite had also been counting down. Eight tests hardcoded dates that were "tomorrow" when
written; the calendar rolled overnight and they crossed the 1-day time stop on a clean tree.
suite_at.py N now runs the suite with the clock advanced, and D-032's rule applies to fixtures.

Four auditors, one finding that subsumed the rest

I sent four adversarial auditors at the order path, the exit engine, the learning chain, and the
thesis sources. They came back with real defects - a close declared successful because the call
didn't raise, assigned shares adopted as a position no rule could ever close, every forecast scored
a trading day early on the UTC clock, one lesson lost forever to an unguarded raise, research
never telling the model the window, the muse passing a None deadline to its options gate, the
Coach re-promoting whatever the operator reverted, and two positions able to share a leg so that
both their stops read a fabricated denominator.

All fixed. But the sources auditor's first finding was this:

The live agent is not running any of the code you asked me to audit.

trdrbot run, PID 4808, started Monday 31 August at 15:53 - before D-100, before the deadline
came off, before the discovery corpse was found, before the Coach's nonce was fixed, before the
universe was widened, before the risk appetite existed. Python does not reload modules. The config
comment says so. Forty hours and seven decisions, every one of them a fix that was not executing,
and the journal proves it: the discovery TypeError still firing after the fix commit, an
integer nonce still being written, the old five-name universe with no widening row.

And the in-memory config still held deadline: 2026-09-04. In two days the stale
forecast_window would have gone permanently into the past, every candidate from every source
would have failed the horizon gate, and the old force-close sweep would have flattened the whole
book.

It is restarted. It is on HEAD. And trdrbot health now compares the live process's git sha to
HEAD and names how many code commits it is behind - the probe fired correctly on the very next
commit, which is the only way to know a detector works.

The second batch: a brake that was fully open

The auditors' reports came back in two halves. The second half was smaller findings, and I nearly
filed them as "later". Two of them were not small.

The overconfidence brake was open. The system shrinks every stated probability toward
ignorance in proportion to its measured reliability - how far its forecasts have historically
sat from the truth - and the top rung of the competence ladder refuses to open until that number is
small. The number was 0.0. Flawlessly calibrated, on 73 forecasts. What had happened: the
Ferro-Fricker small-sample correction, which subtracts the within-bin noise from the raw
estimate, had exceeded the raw estimate, and the code clamped the difference to zero. So the
shrink trusted every stated edge in full, and the MATURE gate was auto-passed - on a sample far too
small to say anything. An over-correction does not mean "perfect"; it means "indistinguishable
from zero at this n", which is unmeasured. It is now None, and every reader already treats
None as not-yet-a-measurement: the edge is halved, the top rung stays shut. Three tests had
pinned the clamp. They are reversed, with the reason written in.

The Coach could promote a gutted prompt. Its validator ran str.format over each challenger,
which catches an extra placeholder and says nothing about a missing one. I verified this by
execution: a challenger that deleted {concepts}, {news} and {odds} - the muse's entire
collision mandate - passed validation. The entropy sentinel could not see it either, because it
measures what the muse sampled, never what the prompt used. Now every declared placeholder must
be present and the schema keys the gates read are contract tokens. A test that had explicitly
pinned the opposite - "a missing placeholder is only a problem if code needs it" - is reversed.

And the order path, once more. An unreadable order book had been read as an empty one, so a
position whose entry order was merely unreadable was moved to abandoned - terminal - and then
filled at the broker with no exit rules and nothing watching it. A rejected order and a filled
one both counted as "an order call was made", so the execution row was written and the order
probe read healthy on a book of rejections. And closing position A's leg closed the broker's whole
aggregate in that contract, including the leg position B held, legging B into a bare short for
two ticks. Each is fixed at the seam, with one shared reader for the broker's in-band errors so the
entry and exit paths cannot disagree about what a failure looks like.

The smaller ones were real too: gpt-5-mini priced as gpt-5 by prefix match, 5x on every cost
report; a horizon gate that checked only the far side of the window; two different calibration.ns
on two different pages; a learning probe that netted a failed resolution against a good fill.

The detector that had stopped being read

By evening the seven remaining audit items were done - the loop finally reads the lessons it has
been writing since D-022, corroboration measures the session rather than the drift since entry, a
signal that cannot be READ is counted instead of held silently, a stale IEX print is dropped
instead of stopped on. Then I ran trdrbot health and looked properly at what it said.

Three problems, three warnings. Every single one had already stopped happening.

Three errors from August 26th. Fourteen opportunities rejected as horizon_too_late - by a
deadline D-102 removed and a process D-108 found forty hours stale; I re-ran the gate against the
config actually on disk and all fourteen are admitted. One learn error whose guard had shipped
that same afternoon. And one finding that could never clear at all, because it named a position
whose thesis was never recorded and which therefore could never be attributed, ever.

The detector had been right about all of it once. It was now reporting a lifetime total against a
question about the present, and six days was enough for every line of it to become false. A
detector whose findings never clear is a detector nobody reads
- which is precisely the failure
health exists to prevent, arriving from the inside.

The fix is a boundary rather than a window. Health now scores recurrences over the RUNNING
PROCESS, because that is what separates "the system as it is now" from "the system that had that
bug": a fix lands as a restart, not as a timeout. run.json already records it. A two-day window,
which was my first instinct, would have kept those fourteen rejections failing for two more days
after they had become impossible.

Nothing is deleted - a cause with nothing in the current era reports as history, with its lifetime
count and the age of the last one, because scoping must never become a way of switching a detector
off. And the same cause still occurring still escalates on the same rule.

The permanent one needed a different fix, and a better one. attribution.pending() required a
thesis claim and a parseable horizon, so a closed position missing either was never pending, never
attributed, never counted: a permanently stuck item and a permanently empty queue are the same
observation from outside. The queue drains now. A closed position is in one of three states rather
than four - scoreable and due, scoreable and not yet due, or unscoreable, which gets an answer,
once, carrying the reason. The verdict already existed and already meant exactly that. It also
stops attributable_rate, which gates the top rung of the ladder, from flattering itself by
ignoring the outcomes it could not explain.

Health now reads 0 problems, 0 warnings, with nothing deleted. And the sweep produced this book's
first ever attribution row - unscoreable, "no thesis was recorded at entry", about a trade
opened seven days ago. Not a good outcome. An honest one, finally written down.

What I keep learning here

Every serious defect today was the same shape as every serious defect this project has ever had:
something that ran, returned, logged healthily, and did nothing - or did the wrong thing quietly.
A close that "succeeded". A process that "was running". A forecast that "resolved". A wiki page
that "updated". The scaffold and the auditors found them because they asked, at every seam, and
what did you actually do?
- and the fixes that stuck were the ones that made the system answer
that question out loud.

Shipped: D-104 through D-114. 609 tests, lint clean, the scaffold at 0 failing checks,
tomorrow's suite green, the live loop on HEAD with a probe that will say so if it ever isn't.