Skip to content

Make a recorded task readable: enforce data quality at the write path, and rebuild the record page around the reader - #291

Open
FZ2000 wants to merge 25 commits into
mikehasa:mainfrom
FZ2000:verdict-first-receipts
Open

FZ2000 wants to merge 25 commits into
mikehasa:mainfrom
FZ2000:verdict-first-receipts

Conversation

@FZ2000

@FZ2000 FZ2000 commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

Supersedes #211, which is contained in this branch. Two halves that only work together: rules that make a record complete (#211) were never enough, because a complete record can still be meaningless — so this also makes the contract ask for meaning, and rebuilds the page that displays it.

Why

A reviewer using the app said: "noone will understand the page", "all of these are useless info". Measured on a real render, they were right. On task_5f7dbea9 the Usage+Recording tail was a near-constant ~1,250px regardless of how much work was done, and it broke down as:

share of that tail
named absence 42.8%
restatement 25.5%
a raw task id 9.9%
structure 12.0%
actual new fact about the work 9.7%

Twelve statements, of which seven said "we don't know" and two repeated the title. The cause was structural: the page's sections were named after the receipt's own dimensions keys — Actions, Cost, Evidence, Gaps, Recording — so it faithfully reported what the recorder knew about its own recording.

The write path

A goal, at last. There was no goal field anywhere, so completed was unjudgeable: objectives is the list of section titles, which are steps, and on most records objectives[0] is the task title again. task_goal is recorded once on a task's first section. It is first-write-wins, unlike summary which is deliberately last-write-wins — a later section restating it must not redefine what the task was opened for. A task with none says so and never borrows a step title.

That chain broke in the middle once and nothing noticed: the MCP accepted it and the receipt read it off the work item, but WorkEvent dropped it in between. It is pinned end to end now, because only the whole chain is the behaviour.

summary asks for consequence. Its description said "lead with the result, not the process", which produced changelog entries. This one was fully compliant and the reviewer called it meaningless:

"Handing off with parenthesised negatives working and one case still red. ($12.34) and ($1,234.56) parse to the signed value… 6 of 7 parse tests pass…"

All mechanism, no purpose, no cost. The description now asks for the consequence first, with a worked weak-vs-strong contrast in the description itself. The description is the mechanism: across 38 live agent runs (below) no recording call was refused by a rule, and the records came back shaped by what the descriptions ask for. What an agent produces under the new description, reading only the tool descriptions:

"Do not point the importer at an export yet expecting one bad row to be skipped — it will abort the whole file. add(), subtract(), percentage()… let decimal.InvalidOperation escape while parse_amount() raises ValueError…"

Pre-rules history is accepted, not backfilled. Rules bind new writes; inventing summaries for work nobody remembers is fabrication. Those records are named as predating the rules rather than implied to be sloppy.

Prose is asked for and shown, never graded

An earlier revision of this branch also graded what agents wrote: 13 detectors over 119 hard-coded phrases decided whether a summary "stated an outcome" or "named a consequence", refused summaries under 40 characters, refused a check named e2e as "too generic", and picked which sentence of a check summary "is the finding". That layer is gone (7ad4c1a, −889 lines; semantic_rules.py 1,306 → 920, its only remaining regex strips control characters).

It was measured before it was removed. On 40 hand-labelled summaries from a real ledger the classifiers agreed with a human 25–50% of the time, against 88% for always answering "useful"; they would have attached an advisory to 94% of completed sections; they refused "Bumped requests to 2.32.3; CI green." while accepting "I did some work on this and then I finished doing it."; and they told honest review outcomes that they "described process". Nothing showed an advisory ever changing a later write.

Every structural rule stays — required fields, enums, path shape, the placeholder refusal, the file anchor — as do hygiene, the descriptions, and the structured fields. Two things the review found that a grep would not: the markdown renderer re-parsed the reducer's own attention label by word matching and ran the same matcher over titles agents wrote (it now splits on the payload's kind); and the old echo detector had been hiding a server artifact — released servers filled an omitted summary with the literal "<name>: <result>" — which is now inverted exactly, as the machine format it is.

Measured with real agents

benchmarks/agent_recording runs real headless agents (Claude Code and opencode verified; Codex and Gemini adapters written, unverified on this machine) in sandboxed repositories whose task never mentions recording, serves them one checkout's MCP server and instruction block, and grades the result twice: objectively, against the repository's diff and the harness's own run of the verification command; and with a blind judge on the four reader questions. --src points it at a different checkout, which is how the removal above was tested before and after.

The removal changed nothing agents record. Four of five scenarios score identically (read time 20.9s vs 21.0s); on the fifth the structured record is identical in all 12 runs, and the judge gap has no mechanism behind it and is within noise (3/6 vs 0/6, p≈0.18). Replayed over what the agent wrote under the old contract, the word lists would have told it to rewrite 4 of 8 summaries — including both correct "this is not a bug" conclusions, which the judge scored 6 and 7 of 8.

What it found instead is the real to-do list, and every item is about shaping the ask:

  • A partial result is recorded as a success — 12 runs out of 12. Told not to edit vendor/, the agent monkeypatched the vendored function, then recorded completed, no failed check, no next_step. The record reads green over a red repository. The word lists never touched this.
  • Checks are recorded when something is fixed, not when something is found. On "not a bug" and "blocked", the agent ran the tests or the script and did not record it.
  • The title is frozen at started, before the outcome is known — "Fix is_business_day wrong for 2026-07-03" still heads a record whose conclusion is that nothing was wrong.
  • files invites a false claim on work that changed nothing.

Three harness guards exist because live runs did what they guard against: PWD is set (opencode trusted the inherited one and first ran in the real repository, where it read the eval's ground truth — it changed nothing); verification ignores conftest.py; and off-limits paths are restored before reality is judged.

The page

Four sections, each answering a question a reviewer arrives with — what was this for / did it work / can I trust it / what do I do now.

before after
named absence 11.7% of page 1.9%
page height 7,270px 6,254px
HEAD when recorded: 4 prints, 2 values 2 group headers
the shared contradiction 3 byte-identical prints 1 banner
evidence tier 3 registers (prose, word, pip) 1

Passed. Exit 0. test. Agent-reported — four fragments punctuated as sentences — is one separated line. Summaries clipped at a character count, cutting exactly the clause carrying observed-vs-expected; they clip at a sentence boundary. Deleted: the five-tile strip (its SESSIONS tile read 1 on four of five records), the Usage and Recording headings, the WEEKLY PLAN and SOURCES rows, and the Recording ▸ TASK row that printed the title a third time.

Three latent CI failures this found, unrelated to the change

  • A timezone bomb. The committed worked examples were generated in local time; CI's pytest job runs TZ=UTC. Green on a machine in the right zone, red in CI, with a diff (13:3820:38) that reads like a data change. The generator now pins UTC — verified by generating from three zones and comparing checksums.
  • A terminal-width bomb, found by CI on this branch and the same shape as the timezone one. At 80 columns now wrapped fresh tokens and cache-read tokens onto two lines each, and since rich bottom-aligns headers the last header line read tokens │ tokens — two adjacent columns named identically. The test guarding exactly this tried to pin its width with monkeypatch.setenv("COLUMNS", ...), which cannot work: rich reads COLUMNS once inside Console.__init__ and cli.console is built at import, so the pin was a silent no-op and the test really rendered at whatever width launched pytest. Green on every developer terminal, red in CI. Fixed in the renderer — both token columns and the money column are no_wrap, so the narrow case drops the redundant half (fresh toke…) instead of the informative one (tokens), and a starved money column can never render ≈$1,554… — and in the harness, where a conftest fixture pins the render width to 80. Before it, the suite failed 15 tests at 60 columns and 19 at 40; now 40, 80 and 200 return the same verdict.
  • A stray MANIFEST.txt in the reference directory from an interrupted re-record, describing images that were discarded. It broke the promotion tool's inventory check; that check was right.

Verified

Every CI step run locally under CI's own environment (TZ=UTC, release config), because the failures on #267 all lived in shell and Python scripts swift test never executes.

pytest 4,899 passed, 1 skipped (the opt-in live eval) · swift test -c release 725 passed, 6 skipped · all four CI scripts pass · visual-snapshots verify matches · tree byte-clean afterwards, which is the property that actually failed before.

The 96 visual references, re-recorded

#267 made the six suites able to run, and running them showed the references were stale against every pane, not only this page: Metrics.pageMaxWidth caps pane content width, so Dashboard and Usage each moved 22–27% of pixels without being edited directly.

Re-recorded on a host matching the pinned axes, after first ruling out a regression — no Space/radius/Metrics constant changed value, and both size changes are explainable (menu 880→990 is the recent-sessions block plus a wrapped cost-basis legend; setup-failure 1028→1044 is one added line). Reviewed per suite rather than trusting the green verify, which the record script itself calls tautological.

The Work receipt reference is also the first end-to-end proof of the goal fix: the page renders Agent-reported · Review the generated PNG matrix where it used to say no goal was recorded — a field that had to survive four separate serializers to get there.

Known gaps, stated rather than hidden

  • No exits exist. There is no commit, repo or PR URL anywhere on the payload, so what do I do now is the next step alone.
  • Task identity is the client session, so one task can mix unrelated work under a single verdict.
  • The absence budget is still 53% on a usage-only task — a task with no recorded work is honestly mostly absence, and that may be correct rather than broken.
  • verify-fixes.py reads 29 of 30. The miss replays the recording machine's own live ledger against a <1% bar that predates its history (6.6% here, by the file-anchor rule and a few missing summaries); it fails identically without this branch's last changes, and REVIEW_GUIDE.md says so rather than the transcript claiming 30 of 30.
  • The eval's numbers are a first reading: 38 runs, two agents, one judge model.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4

agentacct and others added 25 commits September 13, 2026 16:16
The rules, the single enforcement point, the MCP surface that carries them to
agents, and the fix this PR is named for.

**The rules** (semantic_rules.py — read this file first). Each rule carries the
measurement that justifies it: a title must render (R1); control characters are
collapsed before the length cap (R2); a terminal section carries its outcome
(R4); a check names something a reviewer could re-run or is objectively anchored
(R5); a check's name identifies it, because supersession keys on it (R6).

**One enforcement point** (service.py). _enforce_semantic_rules is called from
record_event, the single writer every lane funnels through, so no surface can
store what another refuses. Machine-recorded events — imported usage, session
observations, finding dispositions — are deliberately out of scope: no agent
authored them, so a refusal would drop a fact instead of correcting a report.

**The fix** (hooks.py). Codex had no SessionStart hook and the context validator
hardcoded client == "claude-code", so a captured Codex context was discarded
anyway. Measured consequence: 1,161 sections with ZERO carrying a
client_session_id, and 61% of work never joining to the usage that paid for it.
Contexts are now per-client, so one client cannot overwrite another's ids.

**The MCP surface** (mcp.py). Descriptors for client_session_id,
client_transcript_id and project_dir, which had no description at all despite
deciding whether imported usage ever joins. And agentacct_work_status, because
next_step and blocker were write-only — accepted, stored, returned by no tool.

**Extraction** (client_usage.py). Codex usage rows carried no revision watermark
(0 of 372 against 2005 of 2005 for claude), so whole-second ties parked as
conflicts that could never clear. Claude identity budget 256 KiB -> 2 MiB:
94.87% -> 100% of 6,274 real transcripts resolve, at no measurable cost.

**Display alignment** (display_budget.py and three call sites). Field caps and the
space each field renders into were chosen independently, so a summary was handed
to a card that renders a two-line label.

Measured cost, replayed against the real ledger: 9 of 1,521 records refused
(0.59%), every one genuinely incomplete — no outcome, no evidence pointer and no
exit code. Zero legitimate reports refused.

Most test files here are mechanical fixture completion: the rules require fields
most fixtures never carried. 1,503 added lines against 85 removed, and worth
skipping in review.
design-plans/data-quality/ holds the reasoning behind the rules rather than just
the rules: the findings with each measurement, the rule catalogue where every rule
states what justifies it and what false-positive budget it must meet, the text and
display analyses, an assessment of what the app does and does not deliver, the
hook and MCP audit, and a review guide.

Three tools, all read-only against the live store:

* audit-agent-data.py — aggregate audit of the real ledger, plus a --replay mode
  that sends every stored record back through the live write path so a rule that
  refuses legitimate work shows up as a number rather than an opinion
* complete-test-fixtures.py — the AST-guided fixture completion used across 44
  test files in the previous commit. It refuses to write a file that does not
  parse, and building it surfaced four bugs in itself, each documented in the file
* verify-fixes.py — 30 checks that drive the real code paths a client drives and
  exit non-zero if any claim fails. Its output is committed at
  design-plans/data-quality/evidence/VERIFICATION.txt
Two things a reviewer and a reader both hit.

The guide still described a 168-file stacked diff. This PR is 74 files and
three commits, so the reading order, the skip list and the numbers are now
stated against the current base, every figure measured on this head: 74 files;
eleven product files at 1,127 insertions; 40 modified test files at +305/-86
against the four new suites at 1,218 lines and 1,467 tests; 4,348 tests green
at each code commit (0a03525 verified in a clean worktree) and 4,349 at the
head; 30/30 verify-fixes checks; 9 replay refusals of 1,541.

It also gains a "what this PR does not prove" section: no live Codex session
has run through the new hook, the rules bind only new writes, the 54-character
card budget is geometric rather than screen-measured, summary quality is still
the largest gap, and no human has reviewed the app since these changes.

Its commands were checked by running them: both tools import agentacct, so the
guide, the verify-fixes docstring and the audit docstring now name
.venv/bin/python instead of a bare python3 (which stops at "import fastapi" on
a system interpreter). The fixture tool is stdlib-only and says so.

The receipt fix: the evidence-coverage definition printed by the CLI, the TUI
and every exported Markdown receipt read "X of Y checkable steps carry a
passing check". That is a formula written for a reader of the source, shipped
to a reader of the receipt, and it sits in both published worked examples,
where it reads as a renderer bug. It now states the rule in words, the worked
examples are regenerated from the receipt engine rather than hand-edited, and
test_receipt_markdown.py fails on any "N of M"-shaped token in a rendered
receipt.

The counting copy that is still wrong ("1 checks", "touched 3 file(s)",
"1 step(s) ran in subagents") is filed as item 6 in ASSESSMENT.md section 7
with the exact lines: it spans Python and Swift, and the reference images that
draw it can only be re-recorded on macOS 26.6.
`design-plans/data-quality/tools/demo-data-quality.py` shows the loop a user and
an agent are actually in, then goes looking for what is still wrong.

Parts 1-3 drive the real code paths on temporary stores and print the exact text
each side sees: the Codex SessionStart context, the refusal an agent gets for a
completed section with no summary (verbatim, with the corrected call in it), the
two-word retry refused, process prose accepted, the outcome sentence accepted and
joined to its session, and then what the card and the receipt render. The
evidence lane follows the same shape, including the two cases that keep it
honest: `files: ["."]` stores nothing and cannot satisfy reproducibility, blank
before/after summaries are not evidence, and the run-scoped outcome lane says
"no runs found" until a run exists. Part 3 is adversarial: all three write lanes
refuse the same record with the same message, control characters, an ANSI
escape, a right-to-left override, emoji and a 4-byte character never reach the
store, a 200-character title is refused rather than truncated, and the
false-positive guard shows a legitimate unicode, multiline record still landing.

Part 4 is not a certificate. It runs the opposite way round: five known defects
are reported live, with counts, and a line says the design record is stale if one
stops reproducing. Today it confirms all five - a one-check receipt renders as
"1 checks" and every count prints as "3 file(s)"/"1 command(s)", 28 of 542 real
work-item titles exceed the card's 54 characters, 61.3% of work items carry no
usage record and 76.0% no evidence, most stored summaries still describe process
rather than outcome, and the 5 stored terminal sections with no summary are still
there because the rules bind new writes only.

`tests/test_data_quality_demo.py` runs Parts 1-3 in CI, so the demo cannot rot
into a document, and asserts the demo keeps naming its own limits. The demo's own
summary ends "Part 4 is why this is not perfect" - the artifact makes no
perfection claim, and the review guide says to read Part 4 before believing
Parts 1-3.
…e wrong code

audit-agent-data.py lacked the src/ path prelude its sibling tools use, so
--replay resolved `agentacct` to whatever was installed. Against a checkout
predating the rules (the editable install points at design/setup-work-overhaul,
which has no semantic_rules), `agentacct.mcp` still imports, the write path has
no rules, every record is accepted, and the tool prints "refused: 0" while
`choke_point_replay` quietly reports "skipped" — a green against the wrong code,
the exact failure mode this tool exists to catch.

- Put this checkout's src/ first, like verify-fixes.py and demo-data-quality.py,
  so --replay tests THIS tree by default.
- Add an `agentacct_under_test` provenance block naming the imported package and
  whether `agentacct.semantic_rules` is present.
- When the rules cannot be imported, emit an explicit `error` in both replay
  blocks and exit non-zero (3) instead of reporting a false `refused: 0`.

Verified: with no PYTHONPATH the tool now reports 9/1548 refused (previously a
false 0); with the rules module blocked it exits 3 with a clear error. Sibling
tools still run clean and nothing imports this script.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013w9bhGCPP2g37g4xiehwDV
…at were not true

A ground-truth audit against a real repo found that a reviewer could learn
THAT something was recorded and whether the agent claimed it was checked,
but almost never what happened. Of 55 questions a reviewer naturally asks
across four task shapes, 1 was fully answerable and 40 were not.

Three statements the product made were false:

- "The agent's command argument was not stored" when it was stored. The
  projection hard-nulled it and kept only a boolean, conflating an
  agent-supplied command with a hook digest that genuinely has no text.
  These are now two named states.
- actions_synopsis.state said "exact" while the same payload showed 4
  captured record_section calls against 5 recorded sections. "exact" is
  now earned by comparing capture against the ledger, and degrades to
  "partial" with a sentence naming the shortfall.
- revision_label asserted "at <sha>" when the server reads HEAD at record
  time and agents record before committing, so every stamp named the
  commit BEFORE the work. Proven: a passing tests/test_subtract.py check
  was stamped with a commit containing neither subtract() nor that file.
  The label now leads with its basis, and a check whose declared files are
  absent at the stamped revision carries a named contradiction.

The contract was manufacturing unreadable records. `name` doubled as the
supersession join key, so a compliant agent used the command string and
produced two byte-identical cards; `summary` was advertised as safely
omittable, so failures arrived with no description. Supersession now keys
on a check_key (or command + evidence_type + section_id), `name` is a
human label beside its command, and a failed or errored check must carry
the assertion. check_identity stays stable across commits, and legacy rows
whose name IS the command keep superseding byte for byte.

Superseded runs stay in the receipt so a fail->pass recovery can be
rendered at all; checkpoint prose becomes its own ordered beat instead of
being overwritten by the next write; gaps rank by what they prevent rather
than by which ingestion source was silent.

Measured cost: replaying 1,900 stored records, 116 are refused (6.11%),
nothing stored is altered.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
The record page had no check list at all: `grep "included:" WorkPane.swift`
returned two call sites, [.actions, .cost] and [.task, .agents], so
dimensions.evidence.checks[] -- result labels, exit codes, revisions,
files -- was shown on no surface. The task whose entire evidence was a
fail at one commit and a pass at the next rendered that as a card reading
"2 records".

Now: a Checks block lists every run with its result, exit code, revision
and files, superseded runs greyed rather than hidden; Activity defaults to
an ordered list (the canvas becomes its zoom control) because three events
inside 0.33s is the worst possible case for a proportional time axis; and
the header always states how many records are loaded instead of gating
that count on an active filter.

Fields that were decoded and then dropped are now rendered: lane/lane_label
(absent from CodingKeys entirely, so vertical position on the canvas
carried no meaning), section_kind, next_step, and noteText -- the reducer's
own result/exit-code disagreement, which the app detected and never said.

A passing task used to show strictly LESS evidence than a failing one,
because the only surface naming a check rendered `if let attention`. That
slot now carries a positive evidence callout when there is no attention
item, and a finding marked reviewed keeps its failure summary, exit code
and next step instead of having them deleted.

Data marks no longer wear the interactive accent: chartBar and chartCatRead
move to indigo, and the shared-scale tool-call bars in ReceiptsPane -- pure
cobalt in both themes -- move with them. AccentReservationTests pins the
rule with an OKLab floor, proves the floor is real by requiring
accentPressed to fail it, and source-scans for a data mark painted with
Theme.accent so the next one cannot land silently.

The six visual-regression suites skip on this host: the baselines were
recorded on macOS 26.6 and this is 26.5.1. They are stale after this
change and must be re-recorded on a 26.6 host before they mean anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
main advanced 18 commits while this branch was in flight, including two
independent redesigns of the same surfaces: mikehasa#260 (readable Session and Work
detail views) and mikehasa#256 (folder-anchored session groupings). Eleven files
conflicted. Resolution notes, where the two sides genuinely disagreed:

- STEP ROW: kept ours (StepCard), deleted main's StepSpineRow and
  StepDetailBody. StepCard takes its tally from the reducer where main's
  composed the wording in Swift, and main's row put
  .accessibilityElement(children: .ignore) on a Button, which strips the
  button role and its press action. Kept main's SessionStepSpine container
  -- the numbered rail is a real improvement -- and made it render StepCard,
  so both surfaces now use one step-list component.
- CODEX REVISION WATERMARK: kept main's, deleted ours. Both sides fixed the
  same bug; ours returned microseconds with basis rollout_file_mtime_us
  while the sibling observation on the same session carries nanoseconds.
  Mixing units on an ordering field is a real defect, and ours would have
  failed main's own test.
- RECORD OUTCOME BARS: deleted main's. They were unreferenced once the hero
  resolution dropped the call, and they carried an accent-as-data-mark and a
  green-tier disagreement.
- ACCENT RESERVATION: main's Theme.sourceColor returned the bare interactive
  cobalt for claude-code and painted legend swatches and timeline lane bars
  with it. Renamed to chartSource* so AccentReservationTests' OKLab floor
  applies automatically rather than by hand.

swift build clean, 679 Swift tests / 6 skipped (the host-gated visual
baselines), 4,735 Python tests, surface parity 69. The audit fix this branch
exists for was re-verified on rendered pixels after the merge: a task whose
evidence is a test failing at one commit and passing at the next still lists
both runs without clicking.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
… change

The app was consuming ~19% of the main thread while nobody touched it. Measured
on a mounted window with a forced layout flush: 581 ms of main-thread CPU every
3 seconds on a 44-step record, against an idle control of 3.7 ms.

Cause, and it was not where I guessed. There is no event-log poll involved --
GlanceState polls at 30 s and the window refresh at 60 s. The cost was a
3-second per-task receipt poll in the Work pane (WorkPane.swift) whose result
was assigned unconditionally (DashboardStore `receipt = payload`), so Observation
rebuilt the whole record page 20 times a minute. Three consecutive /v1/receipt
responses for the same task were byte-identical (156,247 bytes, `cmp` clean):
the page was being rebuilt to draw the same pixels.

Nor was the expensive surface the one this branch added. Ablation measured the
Checks block at 3370 vs 3424 ms (noise) and the ordered Activity list and the
canvas at no measurable cost. Essentially all of it was the numbered step spine.

Fixes, each with its measured number:
- The receipt poll now compares a fingerprint of the raw response bytes and
  skips republishing when they match. 1893.7 ms -> 0.0 ms per idle poll tick on
  the heaviest task, reproduced independently against a 0.0-0.2 ms control.
- Three repeated collections moved onto the project's own lazy container
  (ScrollContentStack): record-page build -58% / -55% / -41%.
- Column widths in the Work table are memoised on (text, size, bold, tracking):
  a real but small 7-20% of the back-to-table build, reported small rather than
  dressed up.
- The timeline poll no longer writes three unconditional @State fields per tick.
  Behaviour-preserving; NOT measured, and not claimed as a speedup.

The republish gate is the risky half: a gate that is too eager wastes a rebuild,
but one that is too clever shows a reviewer stale evidence and says nothing. It
is now a pure `receiptIsAlreadyOnScreen` whose every condition fails OPEN, with
six tests pinning the direction that would otherwise fail quietly -- that a
receipt whose bytes changed always reaches the screen.

690 Swift tests, 6 skipped (the host-gated visual baselines). Python 4,735 and
surface parity 69 unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
… page, make focus visible

Three interaction defects, all introduced by the record-page rebuild.

THE TIME CANVAS. A user reported that "the timeline window increase and
decrease scroll feature is gone". Nothing was deleted: `listSuitsData` decided
between the ordered list and the time canvas with `recordCount <= 4 || span < 5`,
and most tasks have four records or fewer, so the canvas -- and with it
scroll-to-zoom, pinch, +/- and the window scroller -- stopped appearing. Count
was a proxy for the wrong question. The two surfaces differ on whether the time
axis SEPARATES the records: a list orders, a canvas positions, and the canvas
earns its place exactly when position carries information order does not. The
predicate now reads the records' own stamps and picks the list only when they
cannot be told apart on an axis at all. Three records at 8:13:12/:14/:17 are a
shape; twenty records sharing one stamp are a list at any N.

Three things compounded it, all fixed here. The choice was stored per task, so
a reviewer who wanted the canvas had to re-choose it on every task forever; it
is now an app-wide default that a per-task override still beats. The picker
called hold(), so changing how records are DRAWN silently dropped live follow --
drawing is not an investigation. And every control in the Activity heading
rendered as chrome-less prose, which is why a changed default read as a deleted
feature; the picker and "Show all time" now carry a resting hairline.

THE CHECKS CLICK. Pressing a row asked the timeline to select the matching
event, and that path scrolled the outer document to reveal the timeline's
selection -- throwing the reader ~2,628 pt past the row they had just expanded,
which then sat 1,400 pt above the viewport. The canvas now reframes internally
and the document moves only far enough to reveal the row's own detail, and only
when it is not already on screen. Measured after: 0 pt of movement on four
presses where the row was already visible.

FOCUS. Three separate causes for one symptom. The canvas stroked its ring in the
parent NSView underneath an opaque hosting view, so AX reported FOCUSED and
nothing was drawn. The task table takes one keyboard stop for the whole table by
design, but its rows read the ambient isFocused that the container's focus sets,
so every visible row ringed at once. And the record document was not in the key
loop at all: with Full Keyboard Access off, Tab yielded ten stops and not one of
them was on the page.

Tab now walks 26 record-page stops, breadcrumb through Copy task ID, including
every check row and step-spine row. A focused control is visibly focused:
work.breadcrumb.back goes from 95 to 434 accent pixels, 423 differing pixels,
max channel delta 210.

702 Swift tests, 6 skipped (the host-gated visual baselines). Python 4,735 and
surface parity 69 unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
A reviewer reported the timeline's zoom and window-resize "gone" three times.
Twice the cause was mine and already fixed. The third time they were right in a
way no amount of re-enabling could address: on a task whose whole recorded span
is 0.30 s, WorkTimeCanvasLayout clamps the visible window to
min(minimumVisibleSpan, full.span), so the window ALREADY equals the full span
and no zoom, pinch, key or handle drag can change it. The scroller nonetheless
kept its Tab stop, its AXIncrement/AXDecrement, its help text and its
resize/openHand cursor. It looked and announced itself as a live control and
was inert.

The floor stays absolute at 5 s, and the reasoning is now written down beside
it: a proportional floor would zoom a 0.3 s task to the same ratio as a two-day
one, and proportional zoom is not legible zoom because the axis does not scale
with the task. The consequence -- a task under the floor cannot be narrowed at
all -- is not a bug to hide but a state to NAME.

So `canNarrow` is computed once, and when it is false the surface stops
advertising: no handle buttons, no adjustable actions, no Tab stop, no resize
cursor, and the slot carries the named state from the Python vocabulary instead
of a dead control. Gestures that cannot change the window no longer swallow the
event.

This is the honesty rule the rest of the product already follows: absence is
always a named state, never a dash, a zero, or a control that does nothing.

46 canvas tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
…dget

A reviewer looking at the running app said the page was unreadable and the
information useless. Measured on a real render, they were right: the
Usage+Recording tail was a near-constant ~1,250px on every record regardless of
how much work was done, and on one task that tail was 42.8% named absence, 25.5%
restatement, 9.9% a raw task id, and 9.7% new fact -- twelve statements, of
which seven said "we don't know" and two repeated the title.

The cause was structural. The page was organised around the receipt's OWN data
model -- Actions, Cost, Evidence, Gaps, Recording are `dimensions` keys -- so it
faithfully reported what the recorder knew about its own recording, and almost
nothing about the work.

THE ASK. There was no goal field anywhere, so "completed" was unjudgeable:
`objectives` is the list of SECTION TITLES, which are steps, and on most records
objectives[0] is the Task title again. `task_goal` is now recorded once on a
task's first section and projected to dimensions.task.goal. It is FIRST-write-
wins, unlike `summary`, which is deliberately last-write-wins: a later section
restating the goal must not redefine what the task was opened for, and a later
section omitting it must not erase it. A task with none says so, and never
borrows a step title -- answering "what was this for" with "what someone did
next" is the confusion the field exists to end.

That chain broke in the middle once already and nothing noticed: the MCP
accepted task_goal and the receipt read it off the work item, but WorkEvent
dropped it in between, so the page said no goal was recorded for tasks whose
agent had recorded one. It is pinned end to end now, because only the whole
chain is the behaviour.

THE SUMMARY. Its description asked agents to "lead with the result, not the
process", which produced changelog entries: mechanism and numbers, no purpose
and no consequence. It now asks for the CONSEQUENCE first -- what a reader
should believe or do -- then the mechanism, with a worked weak-vs-strong
contrast in the description itself. Descriptions do the work refusals cannot:
the last contract round took seven recording calls with zero refusals because
the descriptions taught before the agent wrote.

THE ABSENCE BUDGET. Absence stays NAMED; that rule is right and is not weakened.
But it collapses to one line with its detail behind a disclosure, and may no
longer occupy more space than the facts it is absent from. Two absences are
exempt: a missing goal and a missing next step, because a record with no stated
purpose is one a reviewer should distrust.

Also: the same revision-contradiction sentence was computed per check and
printed verbatim on every row that shared a revision; it is emitted once at the
block level now. Check summaries clipped at a character count, cutting exactly
the clause carrying the observed-vs-expected value; they clip at a sentence
boundary instead. Result/exit/type/source were emitted as separate words that
surfaces joined with full stops, so "Passed. Exit 0. test." read as a stutter;
they are one line with a stated separator.

4,829 Python tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
… keys

The page's sections were named after the receipt's `dimensions` keys, so a
reader got Actions, Cost, Evidence, Gaps, Recording and Sources -- the
recorder's vocabulary for its own bookkeeping. It is now four sections, each
answering a question a reviewer actually arrives with:

  1. What was this for   -- the goal, under the title. Net-new; it takes the
     slot of the dead `Recording -> TASK` row, which printed the title a third
     time on every record.
  2. Did it work         -- one state word, its consequence, the agent's summary
     and ONE exhibit, in the hero. The separate attention/evidence callout band
     (302-326px) moves inside it as that exhibit slot.
  3. Can I trust it      -- the tier stated ONCE, coverage, and the check rows
     grouped by revision.
  4. What do I do now    -- the recorded next step, in full, unclamped, in its
     single remaining print.

Deleted: the five-tile strip (the SESSIONS tile read `1` on four of five
records); the `Usage` and `Recording` headings; the WEEKLY PLAN row; the SOURCES
chip row; the AGENTS row (its string moves into the hero meta line rather than
being dropped); the TASK ID row (into the evidence disclosure, with its copy
button); the `+N more objective(s)` suffix.

Said once instead of N times: the evidence tier went from three registers -- a
prose sentence, a bare word and a pip -- to one. `HEAD when recorded:` was
printed four times for two distinct values; the rows are grouped by revision and
each group states it once. The shared contradiction went from three
byte-identical prints to one banner. The callout's verbatim copy of a check row
is gone.

Measured on task_5f7dbea9: named absence 11.7% -> 1.9% of page height, page
7,270 -> 6,254px. Within the evidence section absence is 4.6% of the section,
inverting the 4.4:1 absence-to-fact ratio the rebuild started from.

Not fixed here, and named in the PR rather than hidden: there is no commit, repo
or PR URL anywhere on the payload, so "what do I do now" is the next step alone;
and task identity is the client session, so one task can mix unrelated work
under a single verdict. The absence budget is also still 53% on a usage-only
task -- a task with no recorded work is honestly mostly absence, and that may be
correct rather than broken.

716 Swift tests, 6 skipped (the visual baselines, stale against this page).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
# Conflicts:
#	apps/agentacct/Sources/agentacct/SnapshotRunner.swift
#	apps/agentacct/Sources/agentacct/WorkTimelineView.swift
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/PLATFORM.json
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-minimum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-minimum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-trust-unavailable-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-trust-unavailable-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-weekly-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/dashboard-weekly-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/menu-connected-dense-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/menu-connected-dense-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/menu-connected-sparse-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/menu-connected-sparse-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/setup-failure-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/setup-failure-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-disconnected-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-disconnected-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-minimum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-minimum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-weekly-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/usage-weekly-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-dynamic-type-stress-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-dynamic-type-stress-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-exact-compact-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-exact-compact-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-exact-regular-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-exact-regular-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-layout-stress-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-layout-stress-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-semantic-edge-cases-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-semantic-edge-cases-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-semantic-gallery-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-actions-semantic-gallery-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-attention-receipt-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-attention-receipt-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-accessibility-rtl-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-accessibility-rtl-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-all-passed-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-all-passed-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-compact-accessibility-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-compact-accessibility-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-compact-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-compact-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-expanded-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-expanded-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-overview-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-checks-overview-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-empty-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-empty-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-list-error-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-list-error-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-list-loading-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-list-loading-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-accessibility-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-accessibility-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-accessibility-maximum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-accessibility-maximum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-error-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-error-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-loading-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-loading-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-minimum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-minimum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-stale-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-receipt-stale-reference-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-compact-accessibility-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-compact-accessibility-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-compact-checks-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-compact-checks-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-dense-checks-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-dense-checks-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-expanded-current-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-expanded-current-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-expanded-history-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-expanded-history-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-hierarchy-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-hierarchy-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-load-failure-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-load-failure-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-retrying-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-retrying-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-rtl-accessibility-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-rtl-accessibility-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-rtl-stress-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-session-steps-rtl-stress-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-accessibility-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-accessibility-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-accessibility-maximum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-accessibility-maximum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-minimum-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-minimum-light.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-reference-dark.png
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/work-table-reference-light.png
#	src/agentacct/tui.py
…eproducible

test_worked_example_docs_are_in_sync regenerates the two worked examples and
diffs them against what is committed. The receipt engine renders wall-clock
times and nothing pinned a zone, so the committed files carried whichever zone
their author happened to be in -- and CI's pytest job runs TZ=UTC.

That made the test a timezone bomb: green on a machine in the right zone, red in
CI, and the diff that explains it ("13:38" -> "20:38") looks like a data change
rather than a clock. Proved both directions on this machine: the previously
committed docs FAIL under TZ=UTC and pass in local time; the regenerated ones do
the opposite.

The generator now pins TZ=UTC before it renders. Verified by generating from
three zones -- ambient, America/New_York and Asia/Tokyo -- and comparing
checksums: identical bytes. The test passes in all three.

This is the same class of defect as the visual-snapshot confound fixed in mikehasa#267,
where a rendered clock made reference images depend on the recording machine's
zone. A generated artifact can only be diffed against its source of truth if
generating it twice gives the same bytes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
MANIFEST.txt was swept into the record-page commit by a broad `git add`. It came
from a re-record round that was interrupted before it finished, so it describes
images that were regenerated and then discarded -- the PNGs in this directory
are the untouched originals. A manifest that does not match its own directory is
worse than none.

It also broke the promotion tool, whose reference-destination inventory admits
PNGs and PLATFORM.json and nothing else: two rollback tests failed with
"reference destination inventory mismatch". That check is right, and the file
was wrong to be there.

A per-image checksum record is a good idea -- it would let the next re-record
diff against a number instead of a memory -- but it should be produced BY a real
re-record and admitted to the validator deliberately, not arrive as a side
effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
# Conflicts:
#	apps/agentacct/Sources/agentacct/ReceiptsPane.swift
#	apps/agentacct/Sources/agentacct/Theme.swift
#	apps/agentacct/Sources/agentacct/UsageCapacity.swift
#	apps/agentacct/Sources/agentacct/WorkTimeCanvas.swift
#	docs/examples/compare-claude-code-and-codex.md
#	docs/examples/when-an-agent-says-done.md
#	src/agentacct/cli.py
#	src/agentacct/receipt.py
#	src/agentacct/receipt_markdown.py
…lly uses

The goal still did not reach the page. There are FOUR places a section's
metadata becomes a work event, and the previous commit fixed three: WorkEvent's
field, both of its serializers, and the item fold. The one that actually feeds
the ledger -- `work_ledger._work_event`, which reads metadata directly and never
constructs a WorkEvent -- was untouched, so every work item still had
`task_goal: None` and the receipt printed "No goal was recorded for this task".

The earlier end-to-end check missed it because it was not end to end. It
hand-built a work item and fed that to `_task_dimension`, proving the reader
works on an input the writer never produces. Verified now against the real
builder on the real store: 2 of the stored events carry a goal, and both reach
`dimensions.task.goal` through `build_work_ledger`.

The regression test asserts through `build_work_ledger` for the same reason.

4,854 Python tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
CI failed a header assertion that passes on every developer terminal. The
cause was not the test: at 80 columns the `now` windows table wrapped
"fresh tokens" and "cache-read tokens" onto two lines each, and because
rich bottom-aligns headers the last header line read

    window | tokens | tokens | cost | basis | sessions

— two adjacent columns with identical names, the qualifier stranded on the
line above. That is the ambiguity this branch exists to remove, so the fix
belongs in the renderer.

Both token columns are now no_wrap. This is not cosmetic: wrapping is the
one degradation that keeps the shared noun and drops the distinguishing
qualifier. Truncation degrades the other way ("fresh toke…" vs
"cache-read…"), so forbidding the wrap makes the narrow case lose the
redundant half instead of the informative one. In practice rich protects
the pinned columns and squeezes the others, so both headers survive intact
down to 60 columns.

The money column is no_wrap for a different reason. Pinning only the token
columns starved it and cost began rendering as "≈$1,554…", which is worse
than an ambiguous header: it reads as a real figure but could be $1,554.67
or $1,554,000. Six columns do not fit in 80, so this fixes the sacrifice
order — what gives way is `sessions` and `cost basis`, whose truncation is
visible and whose meaning survives it. Same treatment in the by-client and
top-models tables.

Also fixes why this reached CI at all. The test tried to pin its width with
monkeypatch.setenv("COLUMNS", "200"), which cannot work: rich reads COLUMNS
once inside Console.__init__ and freezes it in _width, and cli.console is
built at import, long before any test body runs. The pin was a silent no-op
and the test really rendered at whatever width launched pytest.

That was a whole class of latent failure, not one test. A conftest fixture
now pins the render width to 80 — the standard narrow terminal and the
width CI uses, deliberately not a roomy one, since pinning wide would have
hidden this defect. Typer's help and usage-error output builds its own
console from rich_utils.MAX_WIDTH, pinned alongside.

Before the fixture the suite failed 15 tests at 60 columns and 19 at 40.
Now 40, 80 and 200 all return 4854 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
The six visual suites have been failing since mikehasa#267 made them able to run
again. They were not wrong: the references date from ea75e2d and this
branch has since rewritten the record page, added the verdict and gap
lines, fixed focus visibility and the counting copy, and — the reason the
churn is pane-wide rather than confined to Work — introduced
Metrics.pageMaxWidth, a content cap applied to every pane. Dashboard and
Usage moved 22-27% of pixels without either pane being edited directly.

Checked first that the churn was not a regression: no Space/radius/Metrics
constant changed value, and the deliberate 0.5pt falsification mutation
from an earlier session is confirmed absent from the tree. The two size
changes are both explainable — menu-connected-sparse 880->990 is the
recent-sessions block plus a wrapped cost-basis legend, setup-failure
1028->1044 one added line.

Reviewed representative references from every changed suite rather than
trusting the green verify, which the record script correctly calls
tautological. Menu, Work receipt, Dashboard and Usage all hold the design
laws: the decision and evidence axes stay two colours, pip shape carries
the evidence tier, cobalt appears only on controls, green only on the
externally-verified trust rail, and absence is named everywhere it occurs
("not gradeable", "no usage recorded", "Reset time not reported",
"revision not captured") rather than shown as a zero.

The Work receipt reference also confirms the task_goal fix end to end: the
page renders "Agent-reported · Review the generated PNG matrix" where it
previously said no goal was recorded. That field had to be carried through
four separate serializers; this is the first proof it survives all the way
into a rendered pixel.

PLATFORM.json is rewritten by the recorder from what it observed, so it now
carries a real host, commit and timestamp instead of the hand-written
"unknown" left behind when the directory was renamed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
Two conflicts, both resolved to keep BOTH sides' intent rather than
picking a winner.

apps/agentacct/Sources/agentacct/MainWindow.swift — main (mikehasa#292) re-added
RecordingHealthNoticeStack as a top-trailing overlay and gave it a new
`restart:` control; this branch had deliberately moved that same stack out
of the overlay and into the page flow, because as an overlay it covered the
Signal rail's own title with a card sharing the fill and hairline of the
card beneath it, and the design language has no elevation to separate the
two layers (K09). Taking main's side would have rendered the stack twice
and reinstated the occlusion; taking ours would have dropped the restart
control silently, since `restart` defaults to nil and omitting it is not a
build error. Kept the in-flow placement and carried `restart:
recorderRestart` onto it.

tests/test_work_ledger.py — a purely additive collision: both sides append
new tests at the same point. Kept all seven (our four, main's two plus its
helper).

Verified on the merged tree rather than assumed: pytest 4896 passed ·
swift test -c release 725 passed, 6 skipped · visual references still match,
so mikehasa#292's UI change does not re-stale them · task_goal confirmed intact
through all four serializers, which auto-merge could have broken silently
since main rewrote 150 lines of work_ledger.py.

orjson is a new runtime dependency from main (declared in pyproject);
a stale venv fails collection on 103 modules until it is installed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
Removes every rule that inferred the meaning or quality of agent-written
text from word lists, regexes, fuzzy comparison or character counts.
semantic_rules.py goes from 1,306 lines to 920; the only regex left in it
strips control characters.

Why. Measured against this branch before removal: on 40 hand-labelled
summaries from a real ledger the classifiers agreed with a human 25-50% of
the time, against 88% for always answering "useful". They would have
attached an advisory to 94% of completed sections, which is an advisory
that carries no information. The refusals ran backwards: "Bumped requests
to 2.32.3; CI green." was refused at 36 characters while "I did some work
on this and then I finished doing it." passed, and a failure summary of
"got 3, want 4" -- the exact observed-vs-expected the rule asked for -- was
refused for being short. Honest review outcomes ("Reviewed the auth flow:
the token is validated before the redirect, so the reported bypass is not
reachable") were told they described process. And nothing anywhere showed
an advisory changing what an agent wrote on a later call.

A 15-agent experiment (three contract designs, blind judges, one partly
fixed bug) found content completeness at ceiling under every design,
including bare one-line descriptions. What the long field descriptions
bought was density -- 1,784 characters and 32s to read against 3,192 and
69s -- so the descriptions stay and the grading goes.

Removed: the outcome/process/status and consequence word lists with their
advisories; the 40/20/20-character floors (presence is still required --
a terminal section owes a readable summary, a blocked one a blocker, a
failed check a description); the generic-check-name list that refused
"e2e"; the fuzzy name/result echo detector with its refusal, advisory and
display drop; the goal-echoes-title and thin-goal advisories; and the
contrast-marker list that decided which sentence of a check summary "is
the finding". Previews still clip at whole sentences.

Two things the review found that a grep would not:

receipt_markdown re-parsed the reducer's own attention label by word
matching, and ran the same matcher over step titles agents wrote, so
"Triage the release blocker in CI" lost the word "Blocker". The payload
already carries `kind`; only a failed check's reducer-built label is split
now, and an agent's title prints as written.

The echo detector was also hiding something that was never agent prose:
released servers filled an omitted summary with the literal
"<name>: <result>". That is a machine format and is inverted exactly by
check_recorded_summary, with a test pinning that anything merely
resembling it ("pytest passed", "pytest: passed.") is shown untouched.

Kept: every structural rule (presence, enums, path shape, the placeholder
refusal, the file anchor), hygiene, the field descriptions, and the
structured fields and their serializers.

verify-fixes.py now reads 29 of 30, up from 28 on the previous commit: its
blocked fixture never named files, so the kept file-anchor rule refused it.
The remaining miss replays the recording machine's own live ledger against
a <1% bar that predates its history; it fails identically without this
change, and REVIEW_GUIDE.md now says so instead of the transcript claiming
"30 of 30 -- all claims verified".

pytest 4870 passed at 40 and 200 columns.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
Every other test of the recording contract checks what the server does
with a record. None could say whether a real agent, given only what
agentacct ships, leaves behind something a reviewer can use -- and the
audit of the word-list layer found the repo had no after-measurement for
any contract change. This is that measurement.

benchmarks/agent_recording drops a real headless agent into a small
sandboxed repository whose task never mentions recording, serves it ONE
checkout's MCP server and instruction block (--src picks whose, so a
change can be measured before and after), and grades the result twice:
objectively, by comparing what the record claims with the repository's
diff and the harness's own run of the verification command; and with a
blind judge on the four questions a reviewer opens a record with.
Neither grader inspects wording with word lists.

Five scenarios, each a distinct recording situation: a clean fix, a
partial fix where half the ask is out of bounds, a reported bug that is
not a bug, a blocked task, and two asks of which only one has a test.
Adapters for Claude Code and opencode are verified; Codex and Gemini are
written but unverified here (out of quota; CLI refused the account).

Three guards exist because live runs did the thing they guard against:

- agent_env sets PWD. subprocess(cwd=) does not, and opencode trusted
  PWD: its first run worked in the REAL repository and read this eval's
  ground truth. It changed nothing, and declined to edit the fixture.
- verification ignores conftest.py, and off-limits paths are restored
  before reality is judged. On partial_fix agents met "do not edit
  vendor/" by monkeypatching the vendored function -- from a conftest,
  and from vendor/__init__.py -- and then recorded CI as green.

What 38 runs found (README has the detail). Removing the word lists did
not change what agents record: four of five scenarios score identically
before and after, and on the fifth the structured record is identical in
all 12 runs. Replayed over what the agent wrote under the old contract,
the word lists would have told it to rewrite 4 of 8 summaries, including
both correct "not a bug" conclusions the judge scored 6 and 7 of 8.

The findings that matter are about the contract. A partial result is
recorded as a success 12 times out of 12 -- completed, no failed check,
no next_step -- which the word lists never touched. Checks are recorded
when something is fixed, not when something is found. The title is
frozen at `started`, before the outcome is known. And `files` invites a
false claim on work that changed nothing.

CI runs the deterministic half (29 tests: scenarios are what they claim,
the objective grader tells an honest record from a misleading one, the
guards hold). The live half is opt-in via AGENTACCT_LIVE_AGENT_EVAL.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LJgWrWqv5QB2JbzLdx9JG4
# Conflicts:
#	apps/agentacct/Tests/agentacctTests/ReferenceImages/macos-26-xcode-26.6-arm64-2x/PLATFORM.json
Merging main brought the `ingestion_healthy_sources` and `ingestion_degraded`
fixture lanes (the Diagnostics review matrix from mikehasa#294). They were recorded
before this branch moved the source, watcher and overall state wording into
the reducer (`state_title` / `state_detail`), so on this branch every row
rendered "Source state not reported". Attach the reducer's copy to those two
lanes with `with_ingestion_state_copy` — the same function the daemon uses —
and re-record the four Sources references on the canonical renderer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@FZ2000

FZ2000 commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator Author

Merged main into this branch to clear the conflict. Two things beyond the textual merge:

  • PLATFORM.json: only the provenance stamp conflicted; kept main's, then re-recorded on the canonical renderer.
  • Fixture lanes (ingestion_healthy_sources, ingestion_degraded): main added these Diagnostics lanes in fix(sources): report the store-wide reconciliation fault once, in plain words #294 without the reducer copy this branch introduces (state_title / state_detail), so after the merge every Sources row rendered "Source state not reported". Attached the reducer's copy to the two lanes with with_ingestion_state_copy (the daemon's own function) and re-recorded the four sources-*-reference images. Verified locally: visual-snapshots verify passes; pytest 4900 passed.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant