2026-09-02 — Deep self-audit (interactive, fresh-context)
Owner-requested, run interactively on the operator's Mac (venv + data + broker keys available, unlike the cloud routines). Read-only: no code, config, state or pinned text was changed; nothing was committed. Every finding below was verified against code, the committed ledgers, the GitHub Actions API, the claude.ai routine run logs, the broker (read-only calls), or by running a negative control. Where a lens got only a targeted pass rather than an exhaustive sweep, that is stated in §6.
Scoring convention: the charter's G1 scale (CRITICAL = 8, MAJOR = 3, MINOR = 1). This is NOT the pinned G1 instrument (that is the monthly research audit, which is finding 1); it is a one-off interactive audit and its residue number is offered for comparison only.
0. The verdict in one paragraph
The trading path is sound and the ledgers are honest: every order goes through
the fail-closed gate (the one known exception is queued as RPL-1), every signal
is lagged one day, the three equity histories have no holes, duplicates,
weekend rows or implausible jumps since their live starts, keystones match on
every sleeve, the kill-switch is off at −8.4% drawdown, and the September
revalidation reproduced the record's numbers (PBO 62.5%, DSR 0.611). The
problems are in the layers that watch the system, not the system. The
monthly research audit — the charter's primary gauge, on its first-ever run —
finished all its measurements and then died on the shared Claude usage quota
before writing a single byte; its month is lost unless re-run. The same quota
starvation killed the daily audit on three of the last seven weekdays and the
implementer on its last fire (the beacons caught every one — the alarm layer
works; the routine layer does not). The engine-regression guard is red, and a
negative control proves the engine is byte-identical to its pin: the guard's
VIX input is re-fetched and committed daily, so the "fixed-window" guard was
never actually frozen. And the auto-merge governance holes the panel raised
twelve days ago (AMG-4, AMG-5) remain open with no detector for the one path
they cannot even see — a direct push to main, which any routine credential
can do.
Residue: 4 MAJOR + 11 MINOR = 23 (no CRITICAL). Breakdown in §1–§3.
1. MAJOR findings
M1 — The cloud observer layer is quota-starved; the first G1 audit ran, measured, and lost everything (NEW evidence on a known class)
What happened. From the routine run logs (claude.ai API, list_runs /
get_run_log):
| routine | fire | outcome |
|---|---|---|
| research audit (monthly G1/G3) | 2026-09-01 15:17Z | ran 338 s, completed M1–M5 and the G1 residue on EXQ-1, then rate_limit: rejected (seven_day_overage_included) at 15:32Z — "You've reached your Fable 5 limit" — before appending research/RESEARCH_AUDIT_LOG.md, opening its PR, or pushing its beacon. Run status: SUCCEEDED, is_error=true. |
| implementer | 2026-08-26 16:08Z and 17:14Z | both died in 1 s: "You're out of usage credits" (seven-day overage). Last beacon 08-19. |
| daily audit | 2026-08-26 22:10Z | died in 1 s, same reason. |
| daily audit | 2026-08-28 22:09Z | died at 23 s: rate_limit: rejected (five_hour). |
The 2026-08-31 weekly audit asked the owner to "confirm at claude.ai/code/routines
whether those runs errored or never fired" — answer: they fired and died on
quota within seconds, except the research audit, which died after doing the
work. Healthchecks caught every miss (DOWN | thales-research-audit at
03:00Z 09-02; August report: daily-audit 5d16h down, implementer 5d9h,
panel 3d1h). So gauge G2 (time-to-loud) holds; what fails is the routine
layer itself, and nothing retries. This session's own eight parallel
reviewers were killed by the same limit at 08:00Z today — the quota is shared
between the routines and interactive work, so a busy interactive day starves
the observers.
Recovered from the lost run's transcript (so it is not lost; the routine should still be re-run to write the log block):
- M1: the panel's procedure never requires recording the considered count, so the proposal-survival denominator is structurally unrecorded; the reviewer kill-rate collapsed to zero in runs 13–16; runs 6/7/8's blocks were deleted from the append-only archive by owner commit 955db93 and survive only in PR bodies (record-integrity caveat).
- M2: registration order clean (RWG-1 leg 2); trial registry untouched since 08-01 (413 rows) — no CPCV/evaluate trial ran all month.
- M3: EXQ-1's kill criterion preserved verbatim and evaluated by real reverts;
EXQ-1 never passed through
queue/approved/(built on owner instruction; the guard correctly DENIED the move; a human merged). - G1 residue on EXQ-1 (PR #117): 6 findings, score 8 — one MAJOR: mass market-order degradation is still undetectable except by archaeology (the order log records neither order type nor limit price; no digest line), which blinds the row's own reopen criterion and can silently contaminate the December TCA gate; PR #117 touched the live execution path and merged with zero review 13 minutes after opening.
Also rotted: the research audit's live prompt (read back via the API)
directs proposals to research/RESEARCH_QUEUE.md ## Open — the frozen archive
since 08-21 — and carries no prompt-rev line, while its mirror
ops/prompts/research-audit.md claims rev 2026-08-21-research-audit-r1
with a placeholder body. Five of six mirrors are still placeholders.
Owner decisions (none self-approved here):
- Re-fire the research audit (
RemoteTrigger run) so September's block exists; consider a second monthly cron line (e.g. the 3rd) as a standing retry. - Procedure change to
ops/RESEARCH_AUDIT.md: append each measurement to the log as it completes (commit-per-measurement), PR at the end — a mid-run death then loses one measurement, not the month. - The structural fix is a quota the routines do not share with interactive sessions (separate account/org), or the pre-committed fallback (flip to opus-5) executed by a mechanism rather than a human — during a declared absence nobody flips it.
- Fix the research-audit prompt (queue path, prompt-rev) and backfill the mirrors.
M2 — The engine-regression guard (W2) is red for a data reason, and nothing runs it (NEW, verified by negative control)
thales verify-baseline fails today:
sharpe_ratio: got 0.795339032362 vs 0.795248013157 (Δ 9.10e-05)
total_return: got 1.31446979677 vs 1.31418507787 (Δ 2.85e-04)
cagr: got 0.0532655042317 vs 0.0532574911304 (Δ 8.01e-06)
Root cause, proven. The dynamic vol-target reads data/macro/vix.parquet
(engine.py:443-463, daily.py:589-631). That file is re-fetched by every
trading run and committed since 2026-07-21 (32 commits). Its first committed
version (587eb35, yfinance, float32, no holiday rows) and today's (FRED, two
decimals, 147 extra rows — US holidays such as 2023-07-04 = 13.70) differ on
6,315 of 6,590 dates in the baseline window. Re-running the identical
fixed-window backtest with the OLD file reproduces the pin to 0.00e+00 on
all four metrics; with the current file it drifts as above. The engine is
byte-identical to its pin; the guard's input was never frozen. (The prices
side IS frozen — data/raw mtimes predate the 06-11 re-lock.)
Why it matters. W2 is the code-integrity tripwire the shop cites ("after
ANY engine change re-pin the baseline"). It now flaps with the macro feed, so
a real regression would be indistinguishable from a VIX re-fetch. And it has
no runner: the cloud weekly audit cannot run backtests, the local launchd
script runs CPCV-on-golden and the oos-monitor only (verified in
scripts/monthly_revalidation.sh), and thales selfcheck reds 3/4 gates on
the operator's Mac by construction (the frozen local data/raw fails the
freshness gate with "59 trading days old — do NOT trade" and 904 staleness
"errors"), so AUDIT.md W0's pass condition cannot be met locally and nobody
runs it. Note the live-path nit: the FRED series' holiday rows make the "63
row" VIX window ≈62 trading days plus a holiday carry — engine and live read
the same file, so parity holds, but the window is not what the comment says.
Fix shape (not applied): point verify-baseline's VIX read at a frozen
copy (data/golden/macro/vix.parquet already exists, MANIFEST-checksummed),
re-lock, and add W2 to the launchd monthly script, which has the venv and the
data. Separately, decide whether selfcheck's freshness/quality gates should
skip when the store is intentionally frozen, so W0 becomes runnable again.
M3 — Governance: the auto-merge holes are unactioned and the direct-push path has no detector (KNOWN-worse + NEW)
Verified against the workflows at HEAD:
auto-merge-records.ymlison: pull_requestwithcontents: write, checks out the PR merge ref and runs the PR's own copy ofscripts/automerge_guard.py(AMG-4, raised 08-21, escalated by the daily audit as E1, not actioned). The repo hasallow_auto_merge: false, so the--autobranch always fails and the fallback merges immediately — records PRs merge ~20 s after opening. AMG-5's status-enum fail-open intoresearch/queue/approved/is likewise still open (12 days).- This is a private repo on the Free plan: branch protection is unavailable
(API returns 403 "Upgrade to GitHub Pro"). Routines push with the owner's
credential. A direct push to
main— including a file dropped straight intoresearch/queue/approved/— bypasses every guard, and no workflow watches pushes tomain(onlyoutbox/**andbeacon/**). The "PRs only" rail is prompt text. - NEW, a workable detector: routine commits are GitHub-web-flow signed
(
verification.verified: true, author "Claude"); the owner's local pushes and thethales-botstate commits areunsigned. A push-triggered workflow that reds on a verified GitHub-signed commit onmainwith no associated PR would catch an agent writing tomaindirectly, with zero false positives on the owner's own pushes.
Owner decisions: (a) the two-line AMG-4 fix (run the guard from
origin/$BASE, which the workflow already does for the queue file); (b) the
direct-push detector above (P0 loudness under the charter); (c) the plan
question — GitHub Pro for branch protection, or a public repo (this is already
a publish-everything shop), or accept the detector-only posture.
M4 — The cron-delivery regime is unmitigated and the decision sits inside the absence window (KNOWN, quantified)
From the Actions API, created_at == run_started_at on every scheduled run —
GitHub is emitting the schedule event late, not queueing runners:
| trading day | momentum cron 14:35Z created | delay |
|---|---|---|
| 08-26 | 16:01Z | 1.4 h |
| 08-27 | 23:53Z | 9.3 h |
| 08-28 | 23:38Z | 9.1 h |
| 08-31 | 20:17Z | 5.7 h (after the close) |
| 09-01 (selection day) | 17:55Z | 3.3 h (in session — luck) |
All three trading workflows pass --skip-market-check, so after-close runs
submit orders that queue to the next open; combined with the UTC-date
idempotency (STAMP-1/2, open in the queue) one late run cost a whole momentum
session and a phantom equity mark (corrected in PR #125). Skew captures fired
at 03:31Z, 02:01Z, 23:09Z and 21:40Z — all outside the session the stream is
meant to observe. The 08-31 research note ends with "that is the owner's
decision"; nothing has changed since, and the owner is absent through 09-08.
Options, with a recommendation. Moving the cron earlier does not survive a
9 h delay. Two things do: (1) several cron lines per workflow (the date-keyed
idempotency makes later firings no-ops after a success) — but only after
STAMP-2 lands, or a late extra firing crosses midnight and consumes the next
slot; (2) a minute-precise external trigger calling workflow_dispatch
(which is not subject to schedule-event delay) with a fine-grained PAT scoped
to Actions:write — from the Mac's launchd (already infra-as-code) or a
third-party cron. Recommend (2) with the schedule kept as backup; it is a
trading-infrastructure change and stays the owner's call.
2. MINOR findings (new unless marked)
- CI's PiT-snapshot age check cannot fire.
paper-trading.ymlheartbeat (c) ages the newest snapshot by file mtime;actions/checkoutgives every file the checkout time, soPIT_AGEis always 0 and the "snapshotter silently stopped" branch is unreachable. Use the filename date. (PIT-1 covers the empty-write half; this is the CI detector being vacuous.) - Skew rows carry no capture time.
data/options_skew.jsonlrows havedateonly; the 09-01 capture ran at 21:41Z (after the close) and is indistinguishable in the primary stream from an in-session one.options_chains/options_iv_termparquets do carrycaptured_at, so it is recoverable by join — the B4 pin should state a session filter. (STAMP-1 covers the date facet.) portfolio reconcilereds before the day's run.reconcile.py:150-162flags any broker equity date without a local entry that is a trading day ≥live_start; Alpaca's history includes the current day, so RUNBOOK step 2 ("when a run goes red: reconcile") produces a false 🔴 DRIFT every trading morning (seen today:2026-09-02). CI runs it after the run, so no production impact. Skip dates ≥ today.- Clock-hermeticity tripwire blind spot.
_CLOCK = \.(today|now|utcnow)\(\)requires empty parens;after = datetime.now(timezone.utc) - timedelta(days=days)(reconcile.py:108) — the original bug's exact shape — is invisible to it, and the suite is green with that line present. Widen to\(and sanction the broker window explicitly. pip-auditnever runs. Only inmonthly-revalidation.yml, whose schedule was disabled 2026-06-01 and which has never been dispatched since; AUDIT.md's Scheduling section still describes it as a monthly step. The lock (06-09) matches the local and launchd venvs for numpy/pandas/polars/scipy/pyarrow, so reproducibility is fine; vulnerability scanning is absent.- VRP forward gate has no evaluator.
forward_gatekeys are keystoned and the digest reads the section, but no command computes the 126-td verdict (cli.py:799;thales --sleeve vrp go-live-gateerrors). Matures ~Jan 2027; G5 requires the evaluator pinned before maturity — a P1 queue item. - AUDIT.md's WEEKLY table over-claims coverage. W0/W2/W5/W6 have no runner (cloud audit lacks data and broker; local
selfcheckis permanently red per M2). The run log shows the cloud audit reporting W1/W3/W4/W7/W8 only. - Record drift, living sections. NORTHSTAR §7 appendix: LOC 32.7k→33.8k, tests 1,128→1,252, workflows 10→13, memos 24→35, verdict rows 2→3; §7 item 5 / §8 cycle 3 (deadline ledger in the digest) not built —
fleet_digest.pyhas no clock-calendar rendering. the owner-side memory index one-liner forproduction-baselinestill cited survfree PBO 50% / DSR 0.665 (the superseded 06-09 numbers; the file body itself is current); the record and the 09-01 launchd CPCV agree on 62.5% / 0.611 / corr −0.53 (index line corrected this session). - Dust. 22 positions under $10 ($62 total) that no run can sell (
min_order_notional$10); health excludes them; a kill-switch liquidation would leave them. Hygiene only. - Tail-of-book sizing. On 09-01 the 50th name's target was $2 (MRNA, blocked by the floor). At 29% gross on $13.6k the HRP tail is economically meaningless; not a defect, but see §4.
- Auto-merge "waits for required checks" is vacuous — no required checks exist on this plan; the comment in
auto-merge-records.ymldescribes a guard that is not there.
3. Checked and sound (each a claim with its evidence)
- Order path. Every submission site enumerated (
alpaca_broker.py:206/248/319,pipeline.py:400/406,daily.py:1251); all but the known RPL-1 market-replace pass throughexecute_orders→_safety_gate→evaluate_safety. Breakers read as specified: manual/fleet halt, account blocked, account-identity pin, degenerate equity, daily-loss (fail-closed when unreadable, PR #120), runaway order/structure counts, notional cap, per-order rejects, over-sell guard in shares, option-shape geometry recomputed from strikes.close_position/close_alldeliberately unimplemented on the ABC. - No lookahead in features. Every signal in
features/indicators.pyends in.shift(1)(momentum, smoothness, volume, short-horizon, dollar volume); live selection augments to the decision date per parity finding F1. - Ledgers. Momentum 113 rows / meanrev 37 / vrp 37: zero holes since each live start, zero duplicates, zero non-trading-day rows, zero >5% daily moves, run-summary lineage counts never decrease. Momentum's 08-28 mark is the broker-true 13,690.89; vrp's 08-28 remains the known phantom (flagged, backfillable).
- Config.
verify-config --all-sleeves: every keystone on momentum/meanrev/vrp, cross-sleeve identity disjoint, all 27 fleet pins match. Kill-switchactive:false, HWM 14,913.66, drawdown 8.4%.portfolio healthall green (leverage 0.29x, max position 2.4%, sector 5.6%). - Forward judges.
oos-monitor: n=56/60, INSUFFICIENT_DATA, live Sharpe −1.70, drift flagged (KS p=0.0097);go-live-gate: NOT PASSED as expected (56/126 days, before 2026-12-01), 0 halts, TCA median 47.3 bps ≤ 50, 0 consecutive DEGRADED (3 verdict rows). Placebo nulls are pinned equal-weight random books with the C5 beta companion (design recorded in the memo). - Tests. 1,251 passed / 1 skipped in 31 s; the skip is the legitimately-vacuous "all sleeve configs exist". Docs-consistency hashes cover the full frozen spans (NORTHSTAR to the "Charter v1" sentinel; December memo; battery/A4/VRP-v2 specs). Observability marker on exactly the 8 reporting files; trading gates run
-m "not observability". - Revalidation + beacons. launchd agents in sync and loaded (exit 0); 09-01 run: PBO 0.625, corr −0.53, Sharpe 0.524, DSR 0.613 on golden — matches the record; verdict ledger appended and on
main. Every routine miss in two weeks produced a healthchecks DOWN mail. - Public tier. No account ids, emails or secrets in
web/public(BLOCK/REDACT scrub); JSON twins stamped 2026-09-01T18:55Z; MCPtools/listanswers with the three read-only tools; CNT-1's one-ahead count (57 vs 56) is known and queued. - Dependencies. Local venv and
~/.venvs/thalesmatchrequirements-ci.lockfor the numeric stack; broker clients carry the 30 s deadline (PR #120, skew client PR #125);retrain-model.ymlandmonthly-revalidation.ymlschedules are disabled in-file with reasons.
4. Plain-language context the owner should hold (not defects)
- Deployment. Half-Kelly says the strategy's own history supports deploying 10.9% of capital (90% CI 1.7–19.1%; P(f≤0) = 2.9%); the VIX-scaled vol target multiplies that by 2.69, capped by the 12% target, giving 29% gross — the book is 71% cash. Under the pre-registered December diff (
max_leverage3.0→1.0) the shadow gross is 10.9%, and the 25% initial deployment puts about 2.7% of the intended allocation in stocks. The system is doing exactly what was pinned; the experiment as specified is nearly all cash. - The 09-08 verdict. DEGRADED requires the live Sharpe's 95% upper bound to fall below 0.795. At n=60 with a point of −1.7 the interval is roughly ±4.4 wide (today's lower bound is −6.1), so the upper bound will sit near +2.7: the modal first verdict is OK, and that will mean "the test cannot tell yet", not "the strategy works". The memo says this; it bears repeating before the date.
- Live vs expected. −3.9% cumulative on the clean clock vs +1.2% implied in-sample; beta 0.43 to SPY (+1.6 Sharpe) — the gap is the strategy, not the market.
5. Suggested order of work (owner's queue — nothing here is self-approved)
- Re-fire the research audit; fix its prompt; commit-per-measurement procedure (M1).
- AMG-4 base-ref guard + direct-push detector (M3) — both small, both P0.
- Freeze W2's VIX input, re-lock, add W2 to the launchd monthly (M2).
- Cron-delivery decision (M4) after STAMP-1/2 land.
- The minors, in the order listed; the PiT mtime check and the hermeticity regex are one-line fixes.
6. Scope and limits
Lenses covered with direct reading and runs: execution path and safety gate,
workflows and governance, ledgers, forward judges, config keystones, tests'
tripwires (two negative controls run), public tier, dependencies, docs
consistency (tests + targeted diffs), routine run logs, broker state.
Lighter passes (no exhaustive sweep): CPCV/evaluate internals beyond the
2026-08-01 record, MCP input validation, TECH_DEBT triage status, the full
CLAUDE.md-vs-CLI diff beyond what test_docs_consistency already enforces.
Eight parallel reviewer agents were launched for exhaustive sweeps and all
died on the session rate limit at 08:00Z; their lenses were re-covered
interactively as listed.
7. Actions taken the same day (owner instruction: "do all of these")
Eleven pull requests, one per item, every guard negative-controlled, all awaiting the owner's merge (none self-merged; the records guard denies every one by construction):
| PR | item | what it does |
|---|---|---|
| #127 | M3 · AMG-4 | records auto-merge grades PRs with the BASE branch's guard |
| #128 | M3 · new | main-push watcher — pages on a GitHub-signed commit reaching main without a PR (0 alerts on 60 replayed commits) |
| #129 | M2 | W2 reads a frozen, hash-pinned VIX (engine proven byte-identical); launchd monthly runs it; selfcheck honours the FROZEN marker |
| #130 | minors 1, 3, 4 | PiT age by filename; reconcile today-jurisdiction; clock tripwire widened (caught a second offender); AUDIT.md runners |
| #132 | M4 · STAMP-1+2 | every run and capture stamped with the market session it observed; 30 replayed normal runs unchanged |
| #133 | M3 · AMG-5 | strict-allowlist guard with real rename pairing (stacked on #127) |
| #134 | M4 | launchd dispatch agents at 10:35 ET / 15:00 ET; crons stay as backup; owner installs |
| #135 | M2 nit | VIX written for NYSE sessions only |
| #136 | minor 6 | thales forward-gate — the VRP clock's exam; surfaced the missing verdict feed and the −$73 pessimistic twin |
| #137 | minor 2 | skew rows carry captured_at; PROPOSED session-validity append to the B4 pin |
| #138 | minor 5 | weekly pip-audit with alert hop — 42 advisories in 12 pinned packages today |
Not built (owner decisions): M1's quota fix (deferred by the owner), the relabel-vs-quarantine ruling, LCK-1 lock regeneration timing, the B4 append's countersign, the retire-criterion verdict writer for VRP.