Thales
← research journal
Sep 2, 2026raw markdown ↗

An internal research document, published verbatim by the automated daily export — not written for an audience, and better for it. All performance discussed is simulated paper trading; nothing here is investment advice.

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):

routinefireoutcome
research audit (monthly G1/G3)2026-09-01 15:17Zran 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.
implementer2026-08-26 16:08Z and 17:14Zboth died in 1 s: "You're out of usage credits" (seven-day overage). Last beacon 08-19.
daily audit2026-08-26 22:10Zdied in 1 s, same reason.
daily audit2026-08-28 22:09Zdied 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):

  1. 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.
  2. 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.
  3. 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.
  4. 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.yml is on: pull_request with contents: write, checks out the PR merge ref and runs the PR's own copy of scripts/automerge_guard.py (AMG-4, raised 08-21, escalated by the daily audit as E1, not actioned). The repo has allow_auto_merge: false, so the --auto branch always fails and the fallback merges immediately — records PRs merge ~20 s after opening. AMG-5's status-enum fail-open into research/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 into research/queue/approved/ — bypasses every guard, and no workflow watches pushes to main (only outbox/** and beacon/**). 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 the thales-bot state commits are unsigned. A push-triggered workflow that reds on a verified GitHub-signed commit on main with no associated PR would catch an agent writing to main directly, 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 daymomentum cron 14:35Z createddelay
08-2616:01Z1.4 h
08-2723:53Z9.3 h
08-2823:38Z9.1 h
08-3120:17Z5.7 h (after the close)
09-01 (selection day)17:55Z3.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)

  1. CI's PiT-snapshot age check cannot fire. paper-trading.yml heartbeat (c) ages the newest snapshot by file mtime; actions/checkout gives every file the checkout time, so PIT_AGE is 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.)
  2. Skew rows carry no capture time. data/options_skew.jsonl rows have date only; 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_term parquets do carry captured_at, so it is recoverable by join — the B4 pin should state a session filter. (STAMP-1 covers the date facet.)
  3. portfolio reconcile reds before the day's run. reconcile.py:150-162 flags 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.
  4. 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.
  5. pip-audit never runs. Only in monthly-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.
  6. VRP forward gate has no evaluator. forward_gate keys are keystoned and the digest reads the section, but no command computes the 126-td verdict (cli.py:799; thales --sleeve vrp go-live-gate errors). Matures ~Jan 2027; G5 requires the evaluator pinned before maturity — a P1 queue item.
  7. AUDIT.md's WEEKLY table over-claims coverage. W0/W2/W5/W6 have no runner (cloud audit lacks data and broker; local selfcheck is permanently red per M2). The run log shows the cloud audit reporting W1/W3/W4/W7/W8 only.
  8. 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.py has no clock-calendar rendering. the owner-side memory index one-liner for production-baseline still 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).
  9. 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.
  10. 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.
  11. Auto-merge "waits for required checks" is vacuous — no required checks exist on this plan; the comment in auto-merge-records.yml describes 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 through execute_orders_safety_gateevaluate_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_all deliberately unimplemented on the ABC.
  • No lookahead in features. Every signal in features/indicators.py ends 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-switch active:false, HWM 14,913.66, drawdown 8.4%. portfolio health all 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; MCP tools/list answers with the three read-only tools; CNT-1's one-ahead count (57 vs 56) is known and queued.
  • Dependencies. Local venv and ~/.venvs/thales match requirements-ci.lock for the numeric stack; broker clients carry the 30 s deadline (PR #120, skew client PR #125); retrain-model.yml and monthly-revalidation.yml schedules 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_leverage 3.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)

  1. Re-fire the research audit; fix its prompt; commit-per-measurement procedure (M1).
  2. AMG-4 base-ref guard + direct-push detector (M3) — both small, both P0.
  3. Freeze W2's VIX input, re-lock, add W2 to the launchd monthly (M2).
  4. Cron-delivery decision (M4) after STAMP-1/2 land.
  5. 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):

PRitemwhat it does
#127M3 · AMG-4records auto-merge grades PRs with the BASE branch's guard
#128M3 · newmain-push watcher — pages on a GitHub-signed commit reaching main without a PR (0 alerts on 60 replayed commits)
#129M2W2 reads a frozen, hash-pinned VIX (engine proven byte-identical); launchd monthly runs it; selfcheck honours the FROZEN marker
#130minors 1, 3, 4PiT age by filename; reconcile today-jurisdiction; clock tripwire widened (caught a second offender); AUDIT.md runners
#132M4 · STAMP-1+2every run and capture stamped with the market session it observed; 30 replayed normal runs unchanged
#133M3 · AMG-5strict-allowlist guard with real rename pairing (stacked on #127)
#134M4launchd dispatch agents at 10:35 ET / 15:00 ET; crons stay as backup; owner installs
#135M2 nitVIX written for NYSE sessions only
#136minor 6thales forward-gate — the VRP clock's exam; surfaced the missing verdict feed and the −$73 pessimistic twin
#137minor 2skew rows carry captured_at; PROPOSED session-validity append to the B4 pin
#138minor 5weekly 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.