Audit Logging
An append-only JSONL trail of every desk decision, runs, approvals, order events, halts, and injection attempts, written through a dependency-free helper.
The desk keeps an append-only record of what it did: every desk run, every approval, every order event, every trading halt, and every prompt-injection attempt the Macro/News Analyst flags. This realizes the project's Transparency & Audit principle and satisfies `CLAUDE.md` step 6, *log it where the user asks*.
Where the log lives
logs/
├── desk-runs.example.jsonl # committed demo arc (illustrative only, not a track record)
├── desk-runs.jsonl # real, gitignored, holds live account decisions
├── .gitignore # ignores *.jsonl EXCEPT the example
└── .gitkeep # keeps the dir in git even when only real logs existReal logs are gitignored, exactly like `ui/public/desk-state.json`. Only the sanitized *.example.jsonl is committed, so a fresh clone has something to read. Everything is illustrative unless fed real data.
Format: one JSON object per line
- One record = one line. No pretty-printing, no array wrapping the file.
- Append-only. New records are appended; existing lines are never edited, reordered, or deleted. An audit trail you can rewrite is not an audit trail.
- UTF-8, newline-terminated. Blank lines are ignored by the tooling.
Every line carries three required keys, ts (ISO-8601 with timezone offset), event, and session, plus a few descriptive fields per event. A one-line summary is recommended on every record; the dashboard uses it verbatim.
event is one of six values: desk_run, approval, order_placed, order_filled, halt, injection.
Per-event fields
- desk_run
- One entry per pass.
tickers(screened symbols),proposed(the proposed trade ornull, shaped{ symbol, side, qty, strategy, riskDecision }),vetoes(symbols the Risk Manager vetoed),injectionAlerts(count flagged),summary. - approval
- The human decision on a preview card.
symbol,decision(approved/rejected),by(e.g.user),summary. - order_placed
- An order was submitted, only ever after an
approval.symbol,side,qty,type(limit/market), optionallimitPrice,status(e.g.submitted),summary. - order_filled
- A fill confirmation.
symbol,qty(filled),price,status(e.g.filled),summary. - halt
- The desk stopped trading (e.g. the daily-loss halt).
reason(e.g.daily-loss-halt), optionaldayPnlPctandlimitPct,symbolusuallynull(account-level),summary. - injection
- A prompt-injection attempt the Macro/News Analyst flagged, recorded as a quote and never acted on.
source,quote(verbatim),handledBy(usuallymacro-news-analyst),action(quoted and ignored),symbolusuallynull,summary.
Example arc
{"ts":"2026-07-11T10:05:00-04:00","session":"demo","event":"desk_run","tickers":["MSFT","NVDA"],"summary":"Screened 2; proposed BUY 3 MSFT (mean-reversion); NVDA vetoed.","proposed":{"symbol":"MSFT","side":"buy","qty":3,"strategy":"mean-reversion","riskDecision":"APPROVE"},"vetoes":["NVDA"],"injectionAlerts":1}
{"ts":"2026-07-11T10:07:30-04:00","session":"demo","event":"approval","symbol":"MSFT","decision":"approved","by":"user"}
{"ts":"2026-07-11T10:07:31-04:00","session":"demo","event":"order_placed","symbol":"MSFT","side":"buy","qty":3,"type":"limit","limitPrice":415.5,"status":"submitted"}
{"ts":"2026-07-11T10:07:45-04:00","session":"demo","event":"order_filled","symbol":"MSFT","qty":3,"price":415.5,"status":"filled"}The helper: tools/desk-log.mjs
tools/desk-log.mjs is a pure Node ESM helper (no dependencies) that validates and appends records. appendRecord throws, writing nothing, if ts, event, or session is missing or if event is outside the enum, so a malformed record can never enter the trail.
Library:
appendRecord(file, record) → validate required keys + event enum, then append exactly one line
readTail(file, n) → last n parsed records (oldest → newest)
toDeskState(records) → map records to the dashboard decisionLog tail
validateFile(file) → { ok, count, errors: [{ line, message }] }
CLI:
node tools/desk-log.mjs validate <file.jsonl> parse + assert every line
node tools/desk-log.mjs tail <file.jsonl> [n] pretty-print last n
node tools/desk-log.mjs deskstate <file.jsonl> [n] print decisionLog[] JSONMapping to the dashboard
The log surfaces on the dashboard as a decisionLog array, a compact tail flattened to { ts, event, summary, symbol, tone }. tone drives the badge/accent color and is derived deterministically:
| Event | tone |
|---|---|
order_filled | pos |
approval | pos if approved, neg if rejected, else flat |
order_placed | flat |
desk_run | warn if injectionAlerts > 0, else flat |
halt | neg |
injection | warn |
Generate the array and paste it into desk-state.json after a run:
node tools/desk-log.mjs deskstate logs/desk-runs.jsonl 20Related: Prompt-Injection Defense is the source of injection events; Strategies & Risk defines the daily-loss halt a halt event records.