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

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

| 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.
