diff --git a/qa/SKILL.md b/qa/SKILL.md index e8ec76b..9aba59d 100644 --- a/qa/SKILL.md +++ b/qa/SKILL.md @@ -1,21 +1,19 @@ --- name: fomo-qa -description: Prepares a clean, consistent QA environment for dogfooding fomo-kernel and walks the real product routes — review, refresh, and what follows — as one continuous campaign in a single conversation. Use when the user says /fomo-qa, "dogfood fomo-kernel", 跑一次 fomo QA, 走一次復盤驗收, 準備乾淨的測試環境, 幫我 QA fomo-kernel. Its purpose is to kill the rework that comes from every session testing a different environment: a fixed version gate (test only on the latest origin/main, refuse when behind) + a clean detached dogfood worktree (never used for development) + a simulated new user (a dogfood-only coach root, cleared with qa_env.sh reset, never the real ~/.trade-coach) + standardized data sources (real trades / mock persona / test-drive). This is the acceptance tool used while developing and maintaining fomo-kernel, not the product itself; to review a real user's trades, use the product skill fomo-kernel instead. Never touches the real records in investment_note. +description: Prepares a clean, consistent QA environment for dogfooding fomo-kernel and walks the real product routes — review, refresh, and what follows — as one continuous campaign in a single conversation. Use when the user says /fomo-qa, "dogfood fomo-kernel", 跑一次 fomo QA, 走一次復盤驗收, 準備乾淨的測試環境, 幫我 QA fomo-kernel. Maintainer tooling for developing fomo-kernel, not the product itself — to review a real user's trades, use the product skill fomo-kernel instead. --- # fomo-qa This makes "prepare a clean, consistent fomo-kernel dogfood environment" a procedure. **It is a QA tool for development and maintenance**, answering "I changed the engine — walking through as a real user would, is the experience right?", and it guarantees every run tests **the same thing**: the latest `origin/main`. -**This is the mandatory standard path for every fomo-kernel dogfood (v1, fixed 2026-07-20)** — every dogfood starts here. Do not improvise an environment on the spot. +**This is the mandatory standard path for every fomo-kernel dogfood** — every dogfood starts here. Do not improvise an environment on the spot. -**Cross-client contract source (since 2026-07-21)**: `docs/qa-runbook.md` in the `kol_collector/fomo-kernel` repository (PR #275) defines what counts as a compliant QA run — **seven** gates (version gate / isolated root / receipt throughout / verdict+verify / archived manifest / privacy lint / **findings disposition**). A session missing any of them is not a QA run and its conclusions may not be cited. The seventh was added on 2026-07-27 (#417): a session passing the first six while leaving behind no replayable asset is exactly what this loop actually looked like for a year. That `docs/qa-runbook.md`, together with the repo-root `AGENTS.md`, **must stay independent of this skill and of any personal registry** — fomo-kernel is a public product a stranger clones on any machine, and cannot assume they have this skill. When the two disagree, the runbook wins. +**Cross-client contract source**: `docs/qa-runbook.md` in the `kol_collector/fomo-kernel` repository defines what counts as a compliant QA run — **seven** gates (version gate / isolated root / receipt throughout / verdict+verify / archived manifest / privacy lint / **findings disposition**). A session missing any of them is not a QA run and its conclusions may not be cited. `docs/qa-runbook.md`, together with the repo-root `AGENTS.md`, **must stay independent of this skill and of any personal registry** — fomo-kernel is a public product a stranger clones on any machine, and cannot assume they have this skill. When the two disagree, the runbook wins. -**What this skill is**: the runbook's seven gates automated into a repeatable procedure plus the `qa_env.sh` tool, for ting's own maintenance of fomo-kernel. Since 2026-07-21 it is reachable through `ai-harness`'s discovery registry, so Claude, Codex and Antigravity all call the same canonical content under the same name `fomo-qa` (see `ai-harness/inventory/fomo-qa.json`). **It is not for anyone else** — an external user has no such skill and needs none; they follow the public `docs/qa-runbook.md` above. +**What this skill is**: the runbook's seven gates automated into a repeatable procedure plus the `qa_env.sh` tool, for ting's own maintenance of fomo-kernel. It is reachable through `ai-harness`'s discovery registry, so Claude, Codex and Antigravity all call the same canonical content under the same name `fomo-qa` (see `ai-harness/inventory/fomo-qa.json`). **It is not for anyone else** — an external user has no such skill and needs none; they follow the public `docs/qa-runbook.md` above. -> Why it exists: a 2026-07-19 audit found 17 of 18 worktrees behind main (the worst by 28 commits). Dogfood had been running on development worktrees each pinned to its own base — testing some past slice of the product, with no way to learn afterwards which slice. "It runs" is not "it tested the latest version". This skill blocks that mechanically (issue #250). - -> **Its place in the eval system**: this is also the execution procedure that `docs/eval-design.md`'s evidence level **4 (human review)** and observation surface 1 (content-free interaction receipts) had always lacked. A walkthrough ends by producing a `ux_receipt` owner verdict, turning the layer eval currently marks "pending owner dogfood" into a machine-readable annotation that accumulates. **Since 2026-07-27 it produces one more thing**: step 6 converts every miss into a replayable episode under `evals/episodes/`, so a dogfood produces a permanent regression asset rather than a one-off observation — that is what "what we get is stable enough to keep" means. +> **Its place in the eval system**: this is the execution procedure for `docs/eval-design.md`'s evidence level **4 (human review)** and observation surface 1 (content-free interaction receipts). A walkthrough ends with a `ux_receipt` owner verdict, a machine-readable annotation that accumulates; Step 6 converts every miss into a replayable episode under `evals/episodes/`, so a dogfood leaves a permanent regression asset rather than a one-off observation. ## When to use it @@ -25,21 +23,21 @@ This makes "prepare a clean, consistent fomo-kernel dogfood environment" a proce **Not** for reviewing a real user's trades — that is the product skill `fomo-kernel`. This skill only makes the acceptance environment clean and consistent; the walkthrough itself still follows the product's `SKILL.md`. -## Coverage (v1, fixed — do not claim beyond it) +## Coverage (do not claim beyond it) -This round verifies **L1: environment consistency + engine CLI contract + agent walkthrough behavior**. Completing `/fomo-qa` means "the engine and agent behavior were verified on the latest version". It does **not** mean "the user's experience was verified on every client". The following are **known follow-ups, out of this procedure**; do not claim the experience is green after a run (that is precisely the false-pass trap in [#230](https://github.com/atomchung/fomo-kernel/issues/230)): +This procedure verifies **L1: environment consistency + engine CLI contract + agent walkthrough behavior**. Completing `/fomo-qa` means "the engine and agent behavior were verified on the latest version". It does **not** mean "the user's experience was verified on every client". The following are **known gaps, outside this procedure**; do not claim the experience is green after a run: -- **L2 card visuals** (next round): the card HTML is never actually rendered in a browser and compared by screenshot (today even `test_card_html.py` makes string assertions and takes zero screenshots). Visual consistency, layout and dark mode can only be eyeballed. -- **L3 interaction delivery** (partly an inherent ceiling): "the option buttons really appeared and the user could really click them" is a client-layer fact (Claude native options vs typing by hand in Codex) and cannot be verified headlessly. Here we verify only the contract layer — the card text was pasted into the conversation, the questions were presented — which `tools/ux_receipt.py` can carry. The native-interaction half on Codex is #230's ceiling and rests on the owner's manual verdict. -- **A general HTML interaction document + the fastest plain-text completion experience**: next round. +- **L2 card visuals**: the card HTML is never actually rendered in a browser and compared by screenshot. Visual consistency, layout and dark mode can only be eyeballed. +- **L3 interaction delivery** (partly an inherent ceiling): "the option buttons really appeared and the user could really click them" is a client-layer fact (Claude native options vs typing by hand in Codex) and cannot be verified headlessly. Here we verify only the contract layer — the card text was pasted into the conversation, the questions were presented — which `tools/ux_receipt.py` can carry. The native-interaction half on Codex rests on the owner's manual verdict. +- **A general HTML interaction document + the fastest plain-text completion experience**: not covered. ## Hard isolation guardrails (read first, non-negotiable) 1. **Never touch the real records**: `~/Side_project/investment_note/` holds ting's real investment records. Only the "real trades" data source reads **one** CSV there, read-only; nothing else is read or written. -2. **Coach state is isolated to a dogfood-only root, and the real one is not reachable**: dogfood always uses a separate `~/.trade-coach-dogfood`, fully apart from the `~/.trade-coach` you use with the real product. Clear it with `qa_env.sh reset` (which backs up first and fail-closed refuses to touch the real root or investment_note). The real `~/.trade-coach` is managed only by `reset-fomo-coach.sh`; the dogfood procedure never touches it. Never hand-write an `rm` against any coach root. **`TRADE_COACH_HOME` routes writers only** — it never stopped anything reading the real root, and in #557 an agent that judged the isolated root unfamiliar and empty read the real ledger on its own initiative. Step 0's `isolate` closes that path by replacing `HOME`, and is a gate rather than a request. A dogfood root that looks empty is a fresh dogfood root; never go looking for a fuller one, and never compose an absolute path to the real one — that is the one hole this lane cannot close. +2. **Coach state is isolated to a dogfood-only root, and the real one is not reachable**: dogfood always uses a separate `~/.trade-coach-dogfood`, fully apart from the `~/.trade-coach` you use with the real product. Clear it with `qa_env.sh reset` (which backs up first and fail-closed refuses to touch the real root or investment_note). The real `~/.trade-coach` is managed only by `reset-fomo-coach.sh`; the dogfood procedure never touches it. Never hand-write an `rm` against any coach root. **`TRADE_COACH_HOME` routes writers only** — it does not stop anything from reading the real root, so an agent that finds the isolated root unfamiliar and empty can read the real ledger on its own initiative. Step 0's `isolate` closes that path by replacing `HOME`, and is a gate rather than a request. A dogfood root that looks empty is a fresh dogfood root; never go looking for a fuller one, and never compose an absolute path to the real one — that is the one hole this lane cannot close. 3. **Work only in the dogfood worktree**: every engine command runs inside the detached worktree created by `qa_env.sh up`. `qa_env.sh` is itself fail-closed and only operates on a worktree whose path contains `dogfood`, so a slip cannot discard another session's uncommitted work. 4. **Do not change product code**: QA reads, it does not edit. If the walkthrough finds a bug, write it down and open an issue; do not fix it in the dogfood worktree (it is detached and exists to be tested, not developed). -5. **Public text passes the privacy lint first (bought by the #274 incident)**: the repository is public, and real tickers, specific amounts, or `TICKER#date#seq` position ids **must never** appear in an issue, PR, comment or commit message — text channels count, not just files. If this QA session used real trade data, run every draft destined for GitHub through the lint first (from `skills/fomo-kernel/` inside the dogfood worktree): +5. **Public text passes the privacy lint first**: the repository is public, and real tickers, specific amounts, or `TICKER#date#seq` position ids **must never** appear in an issue, PR, comment or commit message — text channels count, not just files. If this QA session used real trade data, run every draft destined for GitHub through the lint first (from `skills/fomo-kernel/` inside the dogfood worktree): ```bash python3 tools/privacy_lint.py --against ~/Side_project/investment_note/trades/fomo/trades.csv /tmp/draft.md @@ -59,7 +57,7 @@ This round verifies **L1: environment consistency + engine CLI contract + agent ### Step 0 — Isolate this shell, then the version gate (read-only) -`qa_env.sh` **refuses every command** until the shell it runs in has the account's own coach root out of reach (#557), so this line comes before everything else — including `status` — and has to be repeated in every later shell of the campaign: +`qa_env.sh` **refuses every command** until the shell it runs in has the account's own coach root out of reach, so this line comes before everything else — including `status` — and has to be repeated in every later shell of the campaign: ```bash eval "$(~/.claude/skills/fomo-qa/qa_env.sh isolate)" @@ -67,7 +65,7 @@ eval "$(~/.claude/skills/fomo-qa/qa_env.sh isolate)" It exports the dogfood `TRADE_COACH_HOME` **and** replaces `HOME` with a throwaway directory, so that `~/.trade-coach` — the path `review.py`, `coach.py`, `tools/ux_receipt.py` and any improvised shell command all compose by default — names nothing. Every value in the block is resolved against the account's real home *before* the override, which is why the repo, the dogfood worktree, the receipt archive, `git`'s configuration and Python's user-installed packages keep working after it. Export any `FOMO_DOGFOOD_*` overrides for a concurrent session **before** this line, while `$HOME` is still the account's own. -It is a bounded guarantee, and reporting it as more than that is the failure it was written against: an absolute path typed on purpose still reaches the real root, and only running the campaign in a container would change that. Do not type one. +It is a bounded guarantee; do not report it as more than that: an absolute path typed on purpose still reaches the real root, and only running the campaign in a container would change that. Do not type one. ```bash ~/.claude/skills/fomo-qa/qa_env.sh status @@ -93,7 +91,7 @@ cd ~/Side_project/kol_collector/fomo-kernel-dogfood/skills/fomo-kernel Steps 0–3 are the **campaign** setup: one worktree, one isolated root, one client/model/effort identity, one acceptance campaign. They happen **once**. Everything after them is a **route run** — one `first_review`, one `refresh`, one `weekly_review` — and a conversation may contain several, each with its own receipt and its own archived `run_id`. -Three identities that are deliberately not one-to-one (#544): +Three identities that are deliberately not one-to-one: | | What it is | How many | |---|---|---| @@ -103,7 +101,7 @@ Three identities that are deliberately not one-to-one (#544): **Never merge route runs into one receipt.** A `first_review` owes two cards and a cash anchor, a `refresh` owes a card-free change surface and no cards at all — one mixed trace could satisfy neither verifier. The reusable unit is the campaign, not the receipt. -**Step 0's `isolate` already routed the whole toolchain into the dogfood-only coach root** — `review.py`, `coach.py` and `tools/ux_receipt.py` **all three** honor `TRADE_COACH_HOME` (ux_receipt since the #269 fix, merged in PR #275), so that one export keeps `prepare`/`preview`/`finalize`/`data-status` and the receipt consistent throughout. If this is a new shell, re-run it before anything else: +**Step 0's `isolate` already routed the whole toolchain into the dogfood-only coach root** — `review.py`, `coach.py` and `tools/ux_receipt.py` **all three** honor `TRADE_COACH_HOME`, so that one export keeps `prepare`/`preview`/`finalize`/`data-status` and the receipt consistent throughout. If this is a new shell, re-run it before anything else: ```bash eval "$(~/.claude/skills/fomo-qa/qa_env.sh isolate)" @@ -115,9 +113,9 @@ eval "$(~/.claude/skills/fomo-qa/qa_env.sh isolate)" ~/.claude/skills/fomo-qa/qa_env.sh reset # back up, then clear the dogfood root to a fresh new user ``` -- **Simulate a returning user** (runs weekly-review / due-revisit): do **not** reset. Keep the dogfood coach state left by a previous campaign and go straight to Step 3. **Lesson from 2026-07-20: a freshly reset session can never test memory continuity or problem-ledger continuity** (memory is `not_applicable`, and an empty prior problem ledger will not "catch" you). Verifying "did last time's problem follow up?" requires a book that already has a finalized review behind it — do not mistake its absence for a fix working. +- **Simulate a returning user** (runs weekly-review / due-revisit): do **not** reset. Keep the dogfood coach state left by a previous campaign and go straight to Step 3. **A freshly reset session can never test memory continuity or problem-ledger continuity** (memory is `not_applicable`, and an empty prior problem ledger will not "catch" you). Verifying "did last time's problem follow up?" requires a book that already has a finalized review behind it — do not mistake its absence for a fix working. -Confirm with the user which one to simulate; default to "brand-new user" when unsure. This is a **campaign-level** choice, made once. It is not re-asked before each route run: a campaign that opened fresh and then finalized a `first_review` **is** a returning user for everything that follows, without a reset and without leaving the conversation. That is the cheapest way to reach the returning-user routes, and since #544 it is the documented one. +Confirm with the user which one to simulate; default to "brand-new user" when unsure. This is a **campaign-level** choice, made once. It is not re-asked before each route run: a campaign that opened fresh and then finalized a `first_review` **is** a returning user for everything that follows, without a reset and without leaving the conversation. That is the cheapest way to reach the returning-user routes. **This isolation must survive into every later shell** — if commands each start a new shell, re-run the `isolate` line in every one, for every route run in the campaign. `qa_env.sh` refuses when it has not been; the engine and receipt commands do not, because they are product commands a real user runs against their own root, so a shell that quietly lost it writes the dogfood run into the real book. Establish it first, every time. @@ -165,7 +163,7 @@ Below is a **complete, directly copyable `first_review` trace**. The order is a python3 tools/ux_receipt.py start --session-id --client claude --route first_review \ --adapter validated_widget --question-mode native_options --card-mode widget -# 1) Cash anchor (#357): exactly once on first_review / weekly_review, and where it goes +# 1) Cash anchor: exactly once on first_review / weekly_review, and where it goes # follows the outcome. `found_in_source` is read out of the statement during prepare, so # it must come before the first question and the first card — recording it later is # judged out of order. When the source carries no balance the plan says so @@ -177,12 +175,12 @@ python3 tools/ux_receipt.py event --session-id --event cash_anchor_checked # 2) One row per question asked. Question text never enters the trace: a question from a # validated dynamic surface records "source + sha256 of the presented text" instead, and -# the two must appear together (this replaces the removed --question-id). +# the two must appear together. python3 tools/ux_receipt.py event --session-id --event question_presented \ --mode native_options --surface-source validated_dynamic --surface-digest <64-hex-digest> # 3) Record this the instant the user answers the last required question, before running -# preview — it is #236's measurement start for "answered → card". +# preview — it starts the "answered → card" measurement. python3 tools/ux_receipt.py event --session-id --event answers_received # 4) Cards are always "artifact first, presented second", and both rows need --stage @@ -192,7 +190,7 @@ python3 tools/ux_receipt.py event --session-id --event card_presented \ --stage preview --mode widget # 5) Record when the "pick a rule / write your own / skip" choice is shown. -# --grounding-check-file is required (#293) +# --grounding-check-file is required python3 tools/ux_receipt.py event --session-id --event rule_choice_presented \ --mode native_options --grounding-check-file @@ -217,12 +215,12 @@ python3 tools/ux_receipt.py event --session-id --event card_presented \ } ``` -A candidate with no `grounding` omits the field entirely (like `candidate_1`) — **do not invent a sentence to fill it**. That is precisely the half #293 cannot catch and only a human can hold. +A candidate with no `grounding` omits the field entirely (like `candidate_1`) — **do not invent a sentence to fill it**. That is precisely the half the mechanical check cannot catch and only a human can hold. -**The `weekly_review` route carries one extra opener, and `verify` enforces it** (the trace above is `first_review`; do not copy the opener into it). When `prepare` selects `weekly_review`, show the user the rule agreed last time **before the first question and the first card**. Two rows differ from the trace above — that opener, and the cash anchor. This trace deliberately shows the other cash shape: a source with no balance row, so the plan came back `absent`, and the balance was asked for in the same message as a preview card. Where the outcome row lands from there depends on the outcome (#663), because only one of the two recomputes anything: +**The `weekly_review` route carries one extra opener, and `verify` enforces it** (the trace above is `first_review`; do not copy the opener into it). When `prepare` selects `weekly_review`, show the user the rule agreed last time **before the first question and the first card**. Two rows differ from the trace above — that opener, and the cash anchor. This trace deliberately shows the other cash shape: a source with no balance row, so the plan came back `absent`, and the balance was asked for in the same message as a preview card. Where the outcome row lands from there depends on the outcome, because only one of the two recomputes anything: - `declined` — nothing recomputes, so the card that asked the question is also the settled one, and the row sits **after** it, exactly as before. -- `provided` — always triggers `add-cash` and a recompute, so the card the user actually used to choose or skip the rule is the one rendered *afterward*, never the one that asked. The trace's one `stage=preview` artifact/`card_presented` pair is reserved for that later, settled card, so the row sits **before** it: record `cash_anchor_checked --cash-outcome provided` when the user answers, run `add-cash` and rerun `preview` on the session it returns — keeping THIS session id for the whole trace, since a receipt records one conversation, not one engine session — and only then record the deferred `artifact_generated`/`card_presented` pair for the card that recompute produced. The first, pre-cash card was real and the user did see it; it simply gets no artifact/presentation row of its own, because it is an intermediate interaction, not the accepted decision artifact. A pair recorded before this row would receipt that superseded card instead, which `verify` now refuses by name. +- `provided` — always triggers `add-cash` and a recompute, so the card the user actually chooses or skips the rule from is the one rendered *afterward*, never the one that asked. The trace's one `stage=preview` artifact/`card_presented` pair is reserved for that later, settled card, so the row sits **before** it: record `cash_anchor_checked --cash-outcome provided` when the user answers, run `add-cash` and rerun `preview` on the session it returns — keeping THIS session id for the whole trace, since a receipt records one conversation, not one engine session — and only then record the deferred `artifact_generated`/`card_presented` pair for the card that recompute produced. The first, pre-cash card was real and the user did see it; it simply gets no artifact/presentation row of its own, because it is an intermediate interaction, not the accepted decision artifact. A pair recorded before this row would receipt that superseded card instead, which `verify` refuses by name. This trace shows `provided`. Everything else — questions, answers received, both card stages, rule choice — is copied verbatim, with the preview pair moved to follow the cash row instead of preceding it: @@ -240,7 +238,7 @@ python3 tools/ux_receipt.py event --session-id --event memory_presented \ python3 tools/ux_receipt.py event --session-id --event question_presented --mode native_options python3 tools/ux_receipt.py event --session-id --event answers_received # The FIRST preview (holdings-only) renders and is shown here, in one message with the -# rule choice and the cash question — a real, user-visible step. #663: it is an +# rule choice and the cash question — a real, user-visible step. It is an # intermediate interaction, not the accepted decision artifact, so it gets no # artifact_generated/card_presented row of its own; only the cash answer is recorded now. # The user gave a balance, so run `review.py add-cash --session-id --cash ` and @@ -259,7 +257,7 @@ python3 tools/ux_receipt.py event --session-id --event artifact_generated \ python3 tools/ux_receipt.py event --session-id --event card_presented --stage final --mode widget ``` -The wrap-up has the same shape as Step 5, except **`--memory` must be `pass` or `fail`**: a weekly session does not accept `not_applicable`, and `verify --require-owner-verdict` refuses it — memory continuity is the entire reason this route exists, so it may not be waived as inapplicable. This block is written out in full rather than pointing back at Step 5 precisely because that difference is the part copying would miss, and it only bites at the moment of archiving: +The wrap-up has the same shape as Step 5, except **`--memory` must be `pass` or `fail`**: a weekly session does not accept `not_applicable`, and `verify --require-owner-verdict` refuses it — memory continuity is the entire reason this route exists, so it may not be waived as inapplicable. This block is written out in full because that difference is the part a copy of Step 5 would miss, and it only bites at the moment of archiving: ```bash # qa-trace: weekly_review @@ -284,11 +282,11 @@ python3 engine/review.py preview --session-id --answers /tmp/answers.json python3 engine/review.py finalize --session-id --answers /tmp/answers.json --narrative /tmp/narrative.json ``` -Three differences from `first_review` decide the trace, and all three were read off a real run rather than assumed: +Three differences from `first_review` decide the trace: -- **No cash anchor row.** The route's contract does not carry the #357 pre-flight, because the snapshot envelope declares `cash` inline (`references/ux-receipt.md` says so, and `verify` demands `cash_anchor_checked` only on `first_review` / `weekly_review`). Recording one here proves nothing that the envelope did not already state. -- **No question rows.** The observed plan came back with `question_queue: []` and `card_plan.question_policy` of `{"min": 0, "max": 0, "route": "snapshot_review"}` — the budget is structurally zero, because a snapshot holds no action history to ask about. Do not manufacture a question or a dynamic surface to fill the gap; the flow says so too. -- **The rule choice is still reached.** `preview` returned `candidate_rules` with a `grounding` sentence on each candidate, so `--grounding-check-file` is required here exactly as on `first_review`. This is the route's one real control, and it is what `--controls` judges. +- **No cash anchor row.** The route's contract does not carry the cash-anchor pre-flight, because the snapshot envelope declares `cash` inline (`references/ux-receipt.md` says so, and `verify` demands `cash_anchor_checked` only on `first_review` / `weekly_review`). Recording one here proves nothing that the envelope did not already state. +- **No question rows.** The plan comes back with `question_queue: []` and `card_plan.question_policy` of `{"min": 0, "max": 0, "route": "snapshot_review"}` — the budget is structurally zero, because a snapshot holds no action history to ask about. Do not manufacture a question or a dynamic surface to fill the gap; the flow says so too. +- **The rule choice is still reached.** `preview` returns `candidate_rules`, each with a `grounding` sentence when the engine has one, so `--grounding-check-file` is required here exactly as on `first_review`. This is the route's one real control, and it is what `--controls` judges. Everything else matches `first_review`: both card stages, artifact before card, `--memory not_applicable` (a snapshot review has no prior period to carry, and unlike `weekly_review` this route accepts that value). @@ -301,7 +299,7 @@ python3 tools/ux_receipt.py start --session-id --client claude --route snap # 1) No cash_anchor_checked and no question_presented on this route — see above. # The latency marker still belongs here, immediately before `preview`: it is -# what makes the wait until the card appears measurable (#236), and on a route +# what makes the wait until the card appears measurable, and on a route # that asks nothing it times the authored thesis_updates/narrative going in. python3 tools/ux_receipt.py event --session-id --event answers_received @@ -312,7 +310,7 @@ python3 tools/ux_receipt.py event --session-id --event artifact_generated \ python3 tools/ux_receipt.py event --session-id --event card_presented \ --stage preview --mode widget -# 3) The rule choice, with its #293 grounding evidence. +# 3) The rule choice, with its grounding evidence. python3 tools/ux_receipt.py event --session-id --event rule_choice_presented \ --mode native_options --grounding-check-file @@ -335,7 +333,7 @@ python3 tools/ux_receipt.py verify --session-id \ ### The `refresh` route: recording the book comes before reviewing it -Once a book exists, a newer holdings view is **not** a review. `prepare --route snapshot_review` refuses any declaration the book-update lane would raise a confirmation for, and names the lane (#530). Its `{"status": "error"}` payload carries this message, observed verbatim on exit code 2: +Once a book exists, a newer holdings view is **not** a review. `prepare --route snapshot_review` refuses any declaration the book-update lane would raise a confirmation for, and names the lane. Its `{"status": "error"}` payload carries this message on exit code 2: ```text this holdings view has changes only you can settle before the recorded book can @@ -354,13 +352,13 @@ python3 engine/review.py refresh --snapshot-json /tmp/fomo-kernel-positions.json --answers /tmp/refresh-answers.json ``` -Then come back to the review with the same declaration; the observed rerun of `prepare --route snapshot_review` succeeded and carried `engine_state.snapshot_reconciliation` with `status: reconciled`, because the refresh had already brought the book up to date. +Then come back to the review with the same declaration; the rerun of `prepare --route snapshot_review` then succeeds and carries `engine_state.snapshot_reconciliation` with `status: reconciled`, because the refresh had already brought the book up to date. What makes this route's trace different: - **A refresh creates no session**, so the trace is keyed by the engine's own `refresh_id` (`references/ux-receipt.md`). Declare capabilities only after step 1 has returned one. - **No card events at all.** `verify` refuses `artifact_generated`, `card_presented`, `widget_attempt_failed` and `rule_choice_presented` on this route — a card delivery that structurally cannot have happened. What the trace owes instead is a **change surface**. -- **The question row depends on what the engine raised, and so does the verdict.** Step 1 returns `status: pending_confirmation` with a non-empty `pending_confirmations` (the observed run raised a disappearance and an appearance), or `status: ready` with `pending_confirmations: []` when only small, cash, market or currency differences moved. The first shape presents **one** question covering every raised item — never one per ticker — and its verdict carries `--controls pass|fail`; the second presents no question and its verdict must carry `--controls not_applicable`. +- **The question row depends on what the engine raised, and so does the verdict.** Step 1 returns `status: pending_confirmation` with a non-empty `pending_confirmations` (for example a disappearance and an appearance), or `status: ready` with `pending_confirmations: []` when only small, cash, market or currency differences moved. The first shape presents **one** question covering every raised item — never one per ticker — and its verdict carries `--controls pass|fail`; the second presents no question and its verdict must carry `--controls not_applicable`. - **That `--controls` choice is not recoverable.** Recording `--controls pass` on a refresh that raised nothing is accepted at write time and then rejected by `verify` with `owner controls verdict must be not_applicable on a refresh trace`. The trace is append-only, so the run is void and has to be walked again. Decide it from step 1's `pending_confirmations`, before the verdict, not after. ```bash @@ -401,33 +399,33 @@ python3 tools/ux_receipt.py verify --session-id \ --require-recorded-owner-verdict --require-owner-verdict --require-timing-integrity --require-findings ``` -Archive the two receipts separately. The refresh and the review that follows it are one journey and one `--case-id`, so the second archive is `--state-mode continued --parent-run-id ` — a fresh review archived beside a refresh it actually continued would lose exactly the lineage #520 added. +Archive the two receipts separately. The refresh and the review that follows it are one journey and one `--case-id`, so the second archive is `--state-mode continued --parent-run-id ` — a fresh review archived beside a refresh it actually continued would lose that lineage. -**Three walkthrough rules (from the 2026-07-20 owner_live audit correction; breaking any one voids that QA session)**: -1. **Declare capability honestly, and try the widget once per session — with the right tool**: the single walkthrough deviation on 2026-07-20 was under-declaring `card_modes` with zero widget attempts. #249's rich HTML card was generated, but the owner saw flat Markdown throughout (the main reason card=fail). A graphical surface must declare `widget`: try the widget first, and on failure record `widget_attempt_failed` before degrading to Markdown — do not let "the artifact was green" stand in for "it was delivered" again. +**Three walkthrough rules (breaking any one voids that QA session)**: +1. **Declare capability honestly, and try the widget once per session — with the right tool**: a graphical surface must declare `widget`. Try the widget first, and on failure record `widget_attempt_failed` before degrading to Markdown — do not let "the artifact was green" stand in for "it was delivered". - **New lesson, 2026-07-21 (see the #230 comments)**: trying the widget does not mean grabbing whatever tool sounds like a renderer. Generic chart/dashboard visualization tools (some MCP `show_widget`/`visualize` tools, for instance) usually carry their own design system and will normalize or strip a large third-party `