diff --git a/ESTATE.md b/ESTATE.md
index e088ca5..8f3745c 100644
--- a/ESTATE.md
+++ b/ESTATE.md
@@ -9,7 +9,7 @@ machinery hub and the only repo that changes freely.
State snapshot (2026-07-19): log **13 leaves**, root `3488a2d0…`, key
fingerprint `874c8a00…`, paper **v0.9 camera-ready (23 pp)**, five
-attested components, pacta suite 130 green.
+attested components, pacta suite 135 green.
```mermaid
flowchart LR
diff --git a/WALLET.md b/WALLET.md
index 5158d19..9d4d147 100644
--- a/WALLET.md
+++ b/WALLET.md
@@ -145,14 +145,19 @@ pacta wallet mcp --wallet ./my-warden # stdio JSON-RPC MCP server
## The custody cockpit (human surface, read-only)
-`pacta wallet cockpit --wallet
` serves a local web UI for the
-operator: posture (latch, ledger chain re-verified, pinned quorum),
-the airgap signature queue (observed, never operated), the incident and
-refusal browser, and a receipt inspector driven by the deployed
-verifier. Design law: it renders recomputed evidence with provenance
-lines, never cached status; it cannot approve, sign, unlatch, or modify
-custody state (byte-level read-only guarantee in
-`tests/test_walletui.py`). Details: [docs/cockpit.md](docs/cockpit.md).
+`pacta wallet cockpit --wallet ` (or `--demo` for a throwaway,
+custody-inert wallet to explore from zero) serves a local web UI for the
+operator: posture (verdict banner, latch, ledger chain re-verified,
+pinned quorum), the airgap signature queue (observed, never operated),
+the incident and refusal browser, a receipt inspector driven by the
+deployed verifier, the estate map, and a plain-language guide with a
+full glossary. Design law: it renders recomputed evidence with
+provenance lines, never cached status; it cannot approve, sign, unlatch,
+or modify custody state (byte-level read-only guarantee in
+`tests/test_walletui.py`). UX law: every page explains itself — verdict
+in words, per-panel "how to read this" expanders, jargon linked to the
+`/guide` glossary (also test-enforced). Details:
+[docs/cockpit.md](docs/cockpit.md).
## Agent-native surface (MCP)
diff --git a/docs/cockpit.md b/docs/cockpit.md
index 48312a6..a21f494 100644
--- a/docs/cockpit.md
+++ b/docs/cockpit.md
@@ -16,6 +16,36 @@ recomputed renders as a loud red FAILED-TO-VERIFY panel. There is no
cached green and no neutral gray — a cockpit that shows unverified green
lights would be the anti-warden.
+## The UX law (the design law's twin)
+
+**The cockpit never leaves a human in the dark.** A person who has never
+heard of warden must be able to read every screen. Concretely, every
+page is built from the same anatomy, top to bottom:
+
+1. **Verdict in words** — e.g. CUSTODY HEALTHY / CUSTODY FROZEN
+ (LATCHED) / CUSTODY EVIDENCE BROKEN — before any evidence, with one
+ sentence saying what that means and what to do.
+2. **A plain-language lead** stating what the page shows and what it
+ cannot do.
+3. **Panels that explain themselves**: each opens with a plain sentence,
+ carries a "How to read this panel" expander interpreting every column
+ and every pill, and links each jargon term to the glossary via a
+ small `?`.
+4. **Explained empty states** — an empty list says what empty means and
+ whether it is good news (for incidents, it is).
+5. **The provenance line** — the dashed footer naming the exact function
+ and timestamp that recomputed the panel.
+
+The `/guide` view is the manual: what warden is, how to read any page,
+the color code, a five-minute tour, a glossary of every term (capsule,
+member, pinning, evidence grades R0–R5, ledger, latch, incident, refusal
+receipt, air-gap, attestation/receipt, provenance, DEMO), and an honest
+"what this cockpit cannot tell you" section. Navigation tabs state the
+question each view answers. This contract is enforced by tests
+(`test_guide_view_explains_every_term`,
+`test_every_view_carries_lead_nav_and_explainers`,
+`test_empty_states_are_explained`).
+
## The read-only guarantee
The cockpit cannot approve, sign, unlatch, or modify custody state. It
@@ -27,15 +57,16 @@ wallet directory hash-identical. Human approve/deny is deliberately NOT
here — that would be a custody-semantics change, which belongs to a
separate, explicitly reviewed milestone.
-## The four views
+## The six views
-| view | shows | recomputed by |
+| view | answers | recomputed by |
|---|---|---|
-| **Posture** (`/`) | custody latch state, ledger head with full hash-chain re-verification, the pinned quorum members (backend, component, tier, source commit, binary hash), spending policy verbatim, incident/refusal counts | `Wallet.posture()` / `Wallet.verify_ledger()` |
-| **Signature queue** (`/queue`) | parked airgap signing requests (outbox) and whether the device has answered (inbox) — observed, never operated | airgap outbox/inbox listing |
-| **Incidents & refusals** (`/incidents`) | incident records and signed refusal receipts, verbatim, newest first | `incidents/*.json`, `receipts/*.json` |
-| **Estate map** (`/estate`) | the whole endeavour — every repo, service, mirror, loop — with RUNTIME on every entity (always-on / on-demand / not-running / static) | rendering of ESTATE.md's model (drift-guarded by test) |
-| **Receipt inspector** (`/inspect`) | paste an attestation + transparency receipt + log public key; the verdict, per-signature results, and diagnostics come verbatim from the deployed verifier | `pacta.transparency.verify_receipt` |
+| **Posture** (`/`) | *Is custody healthy right now?* Verdict banner, then: custody latch, ledger with full hash-chain re-verification, the pinned quorum members (backend, component, evidence grade, source commit, binary fingerprint), signing rules verbatim, incident/refusal counts | `Wallet.posture()` / `Wallet.verify_ledger()` |
+| **Queue** (`/queue`) | *What awaits the offline signer?* Parked air-gap signing requests (outbox) and whether the device has answered (inbox) — observed, never operated | airgap outbox/inbox listing |
+| **Incidents** (`/incidents`) | *What has ever gone wrong?* Incident records and signed refusal receipts, verbatim, newest first — with the page explaining why empty is the good state | `incidents/*.json`, `receipts/*.json` |
+| **Inspect** (`/inspect`) | *Can I check a receipt myself?* Paste an attestation + transparency receipt + log public key; the verdict, per-signature results, and diagnostics come verbatim from the deployed verifier | `pacta.transparency.verify_receipt` |
+| **Estate map** (`/estate`) | *Where does this wallet sit in the whole endeavour?* Every repo, service, mirror, loop — with RUNTIME on every entity (always-on / on-demand / not-running / static) | rendering of ESTATE.md's model (drift-guarded by test) |
+| **Guide** (`/guide`) | *What does any of this mean?* The manual: plain-language explanations, color code, tour, full glossary, honest limits — static, no live data | — |
Every panel also states what it does **not** prove (e.g. the quorum
table says binary hashes are pinned but source-to-binary correspondence
diff --git a/src/pacta/cli.py b/src/pacta/cli.py
index acf6bed..aa29a64 100644
--- a/src/pacta/cli.py
+++ b/src/pacta/cli.py
@@ -693,6 +693,7 @@ def cmd_wallet_cockpit(args: argparse.Namespace) -> int:
server = serve(wallet_dir, host=args.host, port=args.port)
host, port = server.server_address[0], server.server_address[1]
print(f"warden cockpit (READ-ONLY) on http://{host}:{port} - Ctrl-C to stop")
+ print(f" first time? start at http://{host}:{port}/guide - every term explained")
try:
server.serve_forever()
except KeyboardInterrupt:
diff --git a/src/pacta/estateview.py b/src/pacta/estateview.py
index f0db9b5..c2ea519 100644
--- a/src/pacta/estateview.py
+++ b/src/pacta/estateview.py
@@ -123,7 +123,7 @@ ESTATE_HTML = r'''LTL estate map — repos, services, loopskey 874c8a00…paper v0.9 · 23 pp · camera-readyattested components 5
- pacta suite 130 green
+ pacta suite 135 greenstate as of 2026-07-20
diff --git a/src/pacta/walletui.py b/src/pacta/walletui.py
index bf38964..f2654e9 100644
--- a/src/pacta/walletui.py
+++ b/src/pacta/walletui.py
@@ -1,9 +1,9 @@
"""walletui - the warden custody cockpit (local, read-only).
A localhost web surface over an existing wallet directory, for the human
-operator who ultimately answers for the money. Four views: posture, the
+operator who ultimately answers for the money. Six views: posture, the
pending-signature queue (airgap outbox), the incident & refusal browser,
-and a receipt inspector.
+a receipt inspector, the estate map, and a plain-language guide.
Design law: THE COCKPIT RENDERS EVIDENCE, IT NEVER ASSERTS IT. Every
panel is recomputed from wallet state or submitted artifacts at request
@@ -12,6 +12,13 @@ the function and timestamp that produced it. Anything that cannot be
recomputed renders as a loud FAILED-TO-VERIFY panel - there is no cached
green and no neutral gray.
+UX law (the design law's twin): THE COCKPIT NEVER LEAVES A HUMAN IN THE
+DARK. Every page opens with a plain-language statement of what it shows;
+every verdict is stated in words, not just color; every panel carries a
+"how to read this" explainer; every piece of jargon links to the /guide
+glossary. A person who has never heard of warden must be able to read
+every screen.
+
Read-only guarantee: this module calls only read paths (``Wallet.posture``,
``verify_ledger``, directory listings) and ``transparency.verify_receipt``
on submitted artifacts (parsed in memory / temp files outside the wallet).
@@ -48,12 +55,23 @@ _STYLE = """
h2{font-size:1.05rem;margin:1.6rem 0 .5rem}
code{font-family:ui-monospace,Menlo,Consolas,monospace;background:#eef0f3;
border-radius:4px;padding:.08rem .3rem;font-size:.88em}
+ .sub{color:var(--ink2);font-size:.85rem;margin:.3rem 0 .6rem}
nav{margin:.7rem 0 1rem;display:flex;gap:.5rem;flex-wrap:wrap}
nav a{color:var(--accent);text-decoration:none;border:1px solid var(--line);
- background:#fff;border-radius:6px;padding:.25rem .7rem;font-size:.85rem}
- nav a.here{border-color:var(--accent);font-weight:600}
+ background:#fff;border-radius:6px;padding:.3rem .7rem;font-size:.85rem;
+ display:flex;flex-direction:column;line-height:1.25;min-width:7.5rem}
+ nav a.here{border-color:var(--accent);font-weight:600;background:var(--accentbg)}
+ nav a .navsub{font-size:.67rem;color:var(--ink2);font-weight:400}
.banner{background:var(--warnbg);border:1px solid var(--warn);color:var(--warn);
border-radius:6px;padding:.45rem .8rem;font-size:.82rem;font-weight:600}
+ .banner a{color:var(--warn)}
+ .lead{font-size:.92rem;margin:.9rem 0 .2rem}
+ .verdict{border-radius:8px;padding:.8rem 1.1rem;margin:.8rem 0;border:1px solid}
+ .verdict strong{font-size:1.05rem;letter-spacing:.02em}
+ .verdict p{margin:.3rem 0 0;font-size:.88rem;font-weight:400}
+ .verdict.ok{background:var(--okbg);border-color:var(--ok);color:var(--ok)}
+ .verdict.warn{background:var(--warnbg);border-color:var(--warn);color:var(--warn)}
+ .verdict.bad{background:var(--badbg);border-color:var(--bad);color:var(--bad)}
.panel{background:#fff;border:1px solid var(--line);border-radius:8px;
padding:.9rem 1.1rem;margin:.7rem 0}
.panel.bad{border-color:var(--bad);background:var(--badbg)}
@@ -64,11 +82,27 @@ _STYLE = """
.pill.ok{background:var(--okbg);color:var(--ok)}
.pill.bad{background:var(--badbg);color:var(--bad)}
.pill.warn{background:var(--warnbg);color:var(--warn)}
+ a.help{display:inline-block;width:1.05rem;height:1.05rem;line-height:1.05rem;text-align:center;
+ border-radius:50%;background:var(--accentbg);color:var(--accent);font-size:.72rem;
+ font-weight:700;text-decoration:none;vertical-align:.15em}
+ details.explain{margin-top:.55rem;font-size:.82rem}
+ details.explain summary{cursor:pointer;color:var(--accent);font-weight:600;font-size:.78rem}
+ details.explain .expl{color:var(--ink2);margin:.4rem 0 0;padding:.5rem .7rem;
+ background:var(--accentbg);border-radius:6px}
+ details.explain .expl ul{margin:.3rem 0;padding-left:1.1rem}
+ details.explain .expl li{margin:.15rem 0}
+ .plain{font-size:.88rem;margin:.3rem 0 .6rem}
+ .empty{color:var(--ink2);font-size:.88rem;background:var(--accentbg);border-radius:6px;
+ padding:.5rem .8rem}
+ pre{overflow-x:auto}
+ .tablewrap{overflow-x:auto}
table{border-collapse:collapse;width:100%;font-size:.88rem;background:#fff}
td,th{border:1px solid var(--line);padding:.4rem .6rem;text-align:left;vertical-align:top}
th{background:var(--accentbg)}
ul.diag{margin:.4rem 0 0;padding-left:1.2rem}
ul.diag li{font-size:.85rem;margin:.2rem 0}
+ dl.gloss dt{font-weight:700;margin-top:.8rem}
+ dl.gloss dd{margin:.15rem 0 0 0;font-size:.88rem;color:var(--ink)}
textarea{width:100%;min-height:7.5rem;font-family:ui-monospace,monospace;font-size:.8rem;
border:1px solid var(--line);border-radius:6px;padding:.5rem}
button{background:var(--accent);color:#fff;border:0;border-radius:6px;
@@ -86,6 +120,18 @@ def _esc(value: Any) -> str:
return html.escape(str(value))
+def _help(anchor: str) -> str:
+ """A small ? that jumps to the glossary entry for a term."""
+ return (f'?')
+
+
+def _explain(body: str) -> str:
+ """The per-panel interpretation aid: always present, opt-in detail."""
+ return (f'How to read this panel'
+ f'
{body}
')
+
+
def _provenance(via: str) -> str:
return f'
recomputed {_esc(_now())} via {_esc(via)} — nothing on this panel is cached or asserted.
FAILED TO VERIFY '
f"{_esc(what)} could not be recomputed: "
f"{_esc(f'{type(error).__name__}: {error}')}. "
- f"A cockpit that cannot verify shows red, never a stale green."
+ f"A cockpit that cannot verify shows red, never a stale green. "
+ f"What to do: check that the wallet directory still exists and is "
+ f"readable, then reload. If this persists, inspect from the command line with "
+ f"pacta wallet posture."
f"{_provenance(via)}
"
)
@@ -184,29 +233,94 @@ def inspect_receipt(attestation_text: str, receipt_text: str,
# ---------------------------------------------------------------------------
-# renderers - pure string builders over collector output
+# page shell - one navigation, one lead paragraph, on every view
# ---------------------------------------------------------------------------
-_VIEWS = [("/", "Posture"), ("/queue", "Signature queue"),
- ("/incidents", "Incidents & refusals"), ("/inspect", "Receipt inspector"),
- ("/estate", "Estate map")]
+_VIEWS = [
+ ("/", "Posture", "is custody healthy right now?"),
+ ("/queue", "Queue", "what awaits the offline signer?"),
+ ("/incidents", "Incidents", "what has ever gone wrong?"),
+ ("/inspect", "Inspect", "check a receipt yourself"),
+ ("/estate", "Estate map", "the whole system, drawn"),
+ ("/guide", "Guide", "every term, explained"),
+]
+
+_LEADS = {
+ "/": ('This page answers one question: is custody healthy right now? '
+ 'The verdict comes first, the evidence behind it below. Every panel ends with a '
+ 'dashed provenance line naming the exact function that just recomputed it — '
+ 'and every small ? jumps to a '
+ 'plain-language explanation.'),
+ "/queue": ('The wallet’s signing key can live on an air-gapped device — a '
+ 'computer with no network connection. To get something signed, the wallet '
+ 'parks a request file in an outbox; a human carries it to the device; the '
+ 'answer lands in an inbox. This page watches those two folders. It cannot '
+ 'approve, refuse, or move anything.'),
+ "/incidents": ('The wallet’s paper trail. An incident is the wallet '
+ 'noticing something wrong and writing it down on the spot. A '
+ 'refusal receipt is the wallet saying no in '
+ 'writing — with the rule it applied and what would fix the request. '
+ 'An empty page here is good news.'),
+ "/inspect": ('Paste verification artifacts below and this page re-runs the wallet’s '
+ 'own verifier on them, on your machine, without writing anything to the '
+ 'wallet. Use it to check evidence somebody handed you before trusting it.'),
+ "/guide": ('Plain-language explanations for everything this cockpit shows. Nothing on '
+ 'this page is live data — this is the manual. The other five tabs are the '
+ 'instruments.'),
+}
def _page(title: str, active: str, body: str, wallet_dir: str) -> str:
nav = "".join(
- f'{label}'
- for href, label in _VIEWS)
+ f'{label}'
+ f'{sub}'
+ for href, label, sub in _VIEWS)
+ demo_badge = ('DEMO WALLET — custody-inert '
+ if "DEMO" in wallet_dir else "")
+ lead = _LEADS.get(active, "")
+ lead_html = f'
{lead}
' if lead else ""
return (
""
+ ""
f"warden cockpit — {_esc(title)}"
f""
- f"
warden cockpit {_esc(wallet_dir)}
"
+ f"
warden custody cockpit {demo_badge}
"
+ f"
Watching wallet {_esc(wallet_dir)} — everything below is "
+ "recomputed live from that directory each time a page loads; nothing is cached, "
+ "nothing is taken on trust.
"
"
READ-ONLY. This cockpit observes and recomputes; it cannot "
- "approve, sign, unlatch, or modify custody state.
"
- f"{body}"
+ "approve, sign, unlatch, or modify custody state. First time here? Start with the "
+ "Guide — every term on these pages is explained there.