Skip to content

Harden the event journal against failed writes, crashes and oversized frames (CF-6, journal mechanics) - #309

Merged
BunsDev merged 19 commits into
mainfrom
cf6-journal-hardening
Oct 19, 2026
Merged

BunsDev merged 19 commits into
mainfrom
cf6-journal-hardening

Conversation

@BunsDev

@BunsDev BunsDev commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Part of #279 (CF-6), under #273. This covers the journal mechanics on main that the #279 review proved defective, plus the fault, convergence, migration and rollback tests #279 asks for. Claim.

Not in this PR: the request half of CF-6 (cf6-requests) stays with its owner and still waits on CF-3 (#293). This PR doesn't touch runner.ts, human-requests.ts or the iPhone app.

Unchanged:

  • MODEX_EVENT_JOURNAL stays opt-in.
  • The journal stays non-authoritative.
  • The 250 ms batching and 7-day / 250,000-row retention stay as they were, as do the 10,000-event, 4 MB and 8 MB limits.

Not to be merged without Val's go-ahead.

Defects fixed (each test red first)

Defect (review finding) Fix Red before
A write that fails during resume() (the host's /events, the phone's /v1/snapshot) dropped the batch silently, never reached onError, and later resumes skipped past it (1) Every failed write reports through onError once and marks the journal failed: it records nothing more and resumes no cursor, and resume() answers reload unit: resume threw SQLITE_FULL; paired: the phone got 500
A crash inside the 250 ms window kept the journal id, so a pre-crash cursor resumed past an answer the state files held (2) meta.open is "1" until a clean close; a journal reopened still open, or after a failed write, takes a new id unit: same id; SIGKILL e2e test: same id
One frame over 1 MB trapped a paired desktop in an endless resumed-reconnect loop (5) The client forgets its cursor before dropping such a frame, so it reloads the second connection resumed
mobileItem had no default, so an unknown kind (after a downgrade) went out as null and the phone rejected the whole snapshot (8) An inert info notice, without the item's own fields returned undefined

Hostile and convergence tests

Paired tests run through the real host, phone server and desktop client over TLS.

Faults:

  • A real Modex process is SIGKILLed right after an approval is answered. Afterwards:
    • pre-crash cursors reload on both clients;
    • the phone's reload shows the card answered;
    • a counting backend ran nothing, and the CLI heard one decision.
  • A write fails during a phone read, under index.ts's wiring:
    • the phone reloads;
    • the desktop reconnects without the journal;
    • live events continue.

Unusable cursors reload both clients:

  • ahead of the log;
  • pruned;
  • from a deleted journal.

A control case shows that a current cursor resumes.

Replay limit: about 2.7 MB of missed events replays, each exactly once; about 4.5 MB reloads.

Duplicates: a batch delivered twice applies once.

One mixed transcript, every path: the run is recorded by a live journal and by one that batches the whole run.

  • Hostile frames:
    • a duplicated answer;
    • a late tool.update after tool.done;
    • an old attempt's late text after a retry that reuses its native id;
    • a request the CLI withdraws in the same batch it opened.
  • Views compared:
    • the saved files;
    • the runner;
    • the live stream;
    • both journals, each also replayed twice;
    • the phone's snapshot plus its delta;
    • a desktop that slept through the middle;
    • a restarted runner.
  • Result:
    • every view agrees;
    • the CLI ran twice (the turn and its retry);
    • it heard one "yes", and the withdrawn request settled "no".

Migration and rollback:

  • A file built with v0.1.0's exact schema (events only, no meta):
    • keeps its rows and gains an id;
    • replays unchanged;
    • leaves the draft and state files byte-identical.
  • A file this build wrote:
    • v0.1.0's and v0.2.0's own statements still read and append to it;
    • v0.2.0 keeps its id.

Privacy:

  • A real ClaudeBackend turn fails with a Pages route token in its launch.
  • The token reaches no event, no journal row and no file (SQLite and WAL included).

Drafts: a crash, a deleted journal and a failed write leave every non-journal file byte-identical.

e2e (the app):

  • The journal keeps its id across a clean relaunch.
  • It takes a new one after the Electron process is SIGKILLed.

Docs

docs/event-journal.md covers:

  • the read-path failure;
  • the identity rule;
  • the 1 MB frame;
  • unknown kinds on the phone;
  • the new tests;
  • a new Compatibility and rollback section with the exact schema:
    • events unchanged since v0.1.0;
    • meta keys id, pruned_through, open;
    • what v0.1.0 and v0.2.0 do after a downgrade.

docs/status.md gains a CF-6 journal-faults line.

Verification

Base 57e7607 (main, with CF-3). Head e973246. All 11 commits are signed.

Environment: macOS 26.7.1 (25G241), arm64, 16 cores, 48 GB. Node v24.18.1, pnpm 10.34.5, Electron 44.4.5, Playwright 1.63.0, Xcode 26.6, an iOS 26.5 iPhone 16 Pro simulator. The Mac was loaded: load average 25–68, with other agent sessions running.

Unit: pnpm build && pnpm typecheck && pnpm test on c4565a9, the merge of main. The only change after it is doc wrapping.

  • Build and typecheck passed.
  • Root: 109/109 and 16/16.
  • Desktop: 1,611 tests. 1,595 passed, 16 skipped, 0 failed, 0 cancelled.
  • The remaining stages: 21/21, 10/10 and 16/16.
  • On 48eaaf9, before the merge, two tests in files this PR doesn't touch were cancelled by 5–6 s timeouts. Both pass alone (claude-lifecycle 25/25, demo 3/3).

e2e: playwright test with event-journal, companion, desktop-host-launch, store-desktop, workspace-store and human-requests, on e973246. 15/15 passed, including the new kill test. The other 69 specs were not run locally; CI runs the full suite.

Native: on a dedicated temporary simulator.

  • xcodebuild … -only-testing:ModexCompanionTests test: 45/45 passed, 0 skipped.
  • Scripts/test-e2e.sh (paired iPhone, real fixture from this tree, MODEX_UI_TIMEOUT_SCALE=2): 43/43 passed. That includes one deliberate expected failure, a simulated screenshot failure.

Red-first evidence:

  • unit: resume threw SQLITE_FULL; the journal kept its id after a crash; the second connection resumed into the oversized frame; mobileItem returned undefined.
  • Paired: rerun with origin/main's event-journal.ts. The crash test failed on "the journal a dead process left open takes a new identity", and the phone-read failure test got 500.

Not run:

  • live Claude or Codex (fixtures and fake processes only);
  • a physical iPhone;
  • Windows and Linux;
  • the full e2e suite locally.

Update after #308 (the CF-6 request half) landed. main was merged in as 545c09a.

Still open (CF-6)

  • The request half (cf6-requests, after Answer each approval and question once, and show Codex questions as cards (CF-3) #293). Review findings 3, 4, 6, 7, 9, 10 and 11:
    • lazy restart cleanup of cards;
    • delivery: "sent" before the write;
    • capability negotiation and older paired desktops;
    • iPhone withdrawn/Denied;
    • attribution;
    • a stranded request after a failed save;
    • the phone not refreshing after a 409.
  • Older builds after a downgrade.
    • v0.1.0 and v0.2.0 still map plan to null for the phone; this fix only applies from this build on.
    • v0.2.0 doesn't rotate the id after a crash.
  • Untested here:
    • the 8 MB slow-stream drop;
    • power loss with synchronous = NORMAL;
    • native Windows and Linux.

…crash

CF-6 (#279), journal mechanics. Two defects the #279 review proved on main:

- A write that failed while a client read (resume() flushes) was rethrown to
  the host or the phone, which answered 500. The batch was gone, onError never
  ran, and the next resume continued past the lost rows. Now every failed write
  reports through onError once and marks the journal failed: it records nothing
  more and resumes no cursor. resume() answers reload instead of throwing.
- A process that died inside the 250 ms batch window reopened with the same
  journal id. A client's pre-crash cursor then resumed past events the state
  files already held, so a paired desktop kept an answered card looking open.
  The journal now records `open` in meta and clears it only on a clean close. A
  journal reopened while still marked open, or one whose write failed, takes a
  new id, so every earlier cursor reloads. A clean relaunch keeps the id.

Tests, written red first:
- the read-path failure;
- the crash reopen;
- a deleted journal's cursor.

A crash, a deleted journal and a failed write leave every other file under
MODEX_HOME, drafts included, byte-identical.

Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), journal mechanics. A paired desktop drops its event stream when a
frame passes its 1 MB buffer. With the journal on, it then resumed from the same
cursor, got the same frame replayed, and dropped again. It looped until 4 MB of
later events piled up, and received nothing after the frame. The review proved
this on main with a 1.1 MB item.

The client now forgets its cursor before that drop, so the reconnect reloads
state, which carries the item another way, and live events continue.

The test, written red first, drives the real host and client over TLS. Before
the fix, the second connection resumed. Now it reloads once, settles, and
receives the next event.

Coven-Harness: Claude Code / claude-opus-5-5
…d to end

CF-6 (#279), journal mechanics. Both tests use the real host, phone server and
desktop client over TLS.

- A real Modex process answers an approval. The CLI records the decision and
  the process is SIGKILLed inside the journal's batch window. After the relaunch:
  - the journal has a new id;
  - the paired desktop's and the phone's pre-crash cursors both reload;
  - the phone's reload shows the card answered;
  - a counting backend ran nothing, and the CLI heard exactly one decision.
- A write that fails while the phone reads stops the journal through
  index.ts's wiring:
  - the phone reloads;
  - the desktop reconnects without the journal and reloads;
  - live events continue.

Both pass with the previous two commits and fail against main's
event-journal.ts. Against main, the crash test sees the same journal id, and
the phone's read gets 500.

Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), unsupported-client fallback. mobileItem had no default case, so a
kind the Mac doesn't know went out as undefined, which JSON writes as null. The
phone then rejected the whole snapshot (DecodingError.valueNotFound). After a
downgrade, the state files can hold such items: today v0.1.0 and v0.2.0 already
drop `plan` this way. It now maps to an info notice saying the item comes from
a newer Modex. Its own fields are not forwarded, since they may hold anything.
The iPhone already shows any notice as plain, inert text.

The test was written red first. It uses a made-up kind, so it holds once CF-3
adds a question case.

Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279). These run through the real host, phone server and desktop client
over TLS.

- Unusable cursors reload both clients:
  - a cursor ahead of the log;
  - a pruned cursor;
  - a cursor from a journal whose file was deleted.

  A control case first shows that a current cursor resumes, so the client
  really presents the cursor it holds.
- About 2.7 MB of missed events replays, each event exactly once. About 4.5 MB
  reloads instead, and none of it streams.
- A journal batch broadcast twice applies once on the paired desktop.

These describe existing behavior, so they passed when written. Before this,
only unit tests covered these paths.

Coven-Harness: Claude Code / claude-opus-5-5
…call counts

CF-6 (#279). One run is recorded by a live journal and by one that batches the
whole run, so a card's opening and its answer or withdrawal coalesce.

Attempt 1:
- a commentary message;
- a tool that succeeds, then gets a late tool.update;
- an approval answered yes, and clicked again;
- a second tool;
- a final message, then the attempt fails.

The retry reuses the native message id. The first attempt's sink then speaks
late, and the CLI withdraws a second approval before anyone answers.

All of these agree:
- the saved files (what a journal-off reload loads);
- the runner's projection;
- the live stream;
- both journals' replays, each also replayed twice;
- the phone's mid-run snapshot plus its delta;
- a paired desktop that slept through the middle;
- a restarted runner.

The transcript keeps:
- one answered and one withdrawn card;
- finished tools that never reopen;
- the old attempt's late text in no item.

The backend ran twice, the turn and its retry. The CLI heard one "yes", and the
withdrawn request settled "no".

These describe existing behavior, so they passed when written. Memory-only
items are compared through JSON, as the files and clients see them.

Coven-Harness: Claude Code / claude-opus-5-5
…is one

CF-6 (#279), migration and rollback. The existing migration test wrote its
"old" journal with current code. These use the schema older builds actually
had, copied from their release tags.

- Forward: a file built with v0.1.0's exact schema opens on this build. It has
  only an events table, with no meta, no identity and no cursors. It keeps every
  row, gains an identity, replays to the saved transcript, and reloads a client
  from before cursors. The identity holds across clean relaunches. The draft
  file and the state files stay byte-identical.
- Rollback: a file this build wrote is opened with v0.1.0's statements and
  then v0.2.0's, verbatim. Both read and append to it, and v0.2.0 keeps the
  identity and ignores the new `open` flag. Back on this build, the older
  builds' use doesn't look like a crash.

Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279), privacy. CF-2 strips route capability tokens from a backend's
debug report. This test drives that path through the runner with the journal
on. A real ClaudeBackend launches over a fake process with a Pages route whose
URL holds a token. The CLI then fails, and Modex stores the failure with its
launch report.

The failure still names the route, so the report was really stored. The token
reaches no event, no journal row and no file under MODEX_HOME, the SQLite file
and its WAL included.

This passed when written, because CF-2's redaction already holds.

Coven-Harness: Claude Code / claude-opus-5-5
…bility

CF-6 (#279). docs/event-journal.md now covers:
- a write that fails during a read: the journal turns off and clients reload;
- the `open` flag, and the new identity after a crash or a lost write;
- a desktop reloading after a frame over 1 MB;
- the phone's notice for unknown item kinds;
- the new fault and convergence tests;
- a compatibility and rollback section: the exact tables and meta keys, how a
  v0.1.0 file migrates, what v0.2.0 and v0.1.0 still do and don't do after a
  downgrade, and why deleting events.sqlite is always safe.

status.md gains a CF-6 journal faults line under "On main, unreleased". The
request-half line in event-journal.md is unchanged, since cf6-requests owns it.

Coven-Harness: Claude Code / claude-opus-5-5
Brings in CF-3 (#293). One conflict, in mobileItem: CF-3 adds its question
case where this branch adds the default for unknown kinds, so both are kept,
the question case first. The rollback note now names question alongside plan
as a kind v0.1.0 and v0.2.0 send to the phone as null.

Coven-Harness: Claude Code / claude-opus-5-5
Coven-Harness: Claude Code / claude-opus-5-5
@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
modex Ready Ready Preview Oct 19, 2026 5:24pm UTC

Request Review

@github-code-quality

github-code-quality Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Code coverage overview

Languages: TypeScript

TypeScript / code-coverage-agent

main cad3e54 cf6-journal-hardenin... b18a90a +/-
63% 63% 0%

ℹ️ Calculations represent line coverage. Results may change as calculations are updated.


Updated October 19, 2026 17:55 UTC

Brings in the CF-6 request half (#308) and #306, #307. One conflict, in
mobileItem: #308 rewrote the question case's text, adding the delivery wording,
where this branch adds the default for unknown kinds. Both are kept: main's
question text, then the default case.

Coven-Harness: Claude Code / claude-opus-5-5
Brings in #312 (CF-7 clarification half) and #313 (CF-8 request variants).

Coven-Harness: Claude Code / claude-opus-5-5
CF-6 (#279). This test was written and run with the rest of this branch, but
it was left out of the commits. It passed on the working tree at e973246
(15/15 targeted e2e), while #309's description and the #279 receipt already
listed it.

With MODEX_EVENT_JOURNAL=1, the app keeps its journal id across a clean
relaunch, so paired clients still resume. After its Electron process is
SIGKILLed, the next launch takes a new id, so a cursor from before the crash
reloads. The test reads `meta` through a read-only handle, so it never marks
the journal open or closed itself.

Coven-Harness: Claude Code / claude-opus-5-5
@BunsDev

BunsDev commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Correction to the #309 receipt (2026-10-19)

The receipt gave #309's head as e973246 and listed an e2e/event-journal.spec.ts test: the app keeps its journal id across a clean relaunch and takes a new one after a SIGKILL. That test was written, and it passed in the 15/15 targeted e2e run on the working tree, but it was never committed. So e973246, and the 545c09a head that followed it, did not contain it, and CI on those heads never ran it.

It is now committed as 0e2964e, on top of a merge of main at b804e9c, which brings in #312 and #313. The current state:

The rest of the receipt stands. Val has authorized merging #309 through the guard once its CI passes.

Brings in #314 (v0.2.0 publication record).

Coven-Harness: Claude Code / claude-opus-5-5
Brings in #310 (README and routines spec wording).

Coven-Harness: Claude Code / claude-opus-5-5
Brings in #299 (CF-5 memory citations) and #315 (macOS CI timeout).

Coven-Harness: Claude Code / claude-opus-5-5
Brings in #288 (CF-4: completed Codex web searches fold again).

Coven-Harness: Claude Code / claude-opus-5-5
Brings in #304 (move to a new workspace) and #311 (CF-5 retained full log).

Coven-Harness: Claude Code / claude-opus-5-5
@BunsDev
BunsDev merged commit c345779 into main Oct 19, 2026
13 checks passed
@BunsDev
BunsDev deleted the cf6-journal-hardening branch October 19, 2026 17:55
BunsDev added a commit that referenced this pull request Oct 19, 2026
Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 19, 2026
… and test a card open at a crash (CF-6) (#329)

* Record who answered when the phone switches to YOLO or a paired desktop answers (CF-6, finding 9)

Switching a thread to YOLO answers the approvals already waiting, and each one
was saved as answered by this Mac's window even when the phone set YOLO.
Answers and policy changes from a desktop paired through the host were also
saved as the window's own.

- The phone's policy route passes "phone" to setApprovals, and the runner's
  YOLO switch answers as whoever made the change.
- Commands that arrive through the desktop host carry no IPC event, so
  thread:answer, thread:answer-question and thread:update record
  "paired-desktop" for them.
- The card says "Answered from a paired desktop." under such an answer.

Red first: the companion policy test (by was undefined), the answerNote case,
the new runner test (updateThread had no by option) and the Store e2e (no
note under the paired desktop's approval).

Coven-Harness: Claude Code / claude-opus-5-5

* Test that a card open at a crash reads closed on the phone's first poll (CF-6, finding 3)

After a crash the new process closes a card nobody answered only when it first
reads that thread. The phone's poll reads its journal cursor before that, so on
main at 396b17c the first poll answered delta: true with no items and the card
stayed actionable (probe p2 in the #279 review).

#309 gives the journal a new id after an unclean shutdown. The phone's old
cursor then reloads to a full snapshot, which the same read settles. This
test fails on 396b17c ("delta: true, items: 0") and passes with #309's
journal. A clean quit, which stops the turn before the journal closes, passes
either way and is kept as the control case.

Coven-Harness: Claude Code / claude-opus-5-5

* Record the paired-desktop answer and the crash test in status and the journal docs (CF-6)

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 20, 2026
Conflicts:
- companion.ts: keeps this branch's question notice text and main's
  fallback notice for an unknown item kind (#329).
- docs/human-requests.md: keeps this branch's phone paragraph and main's
  "Who answered" bullet and paired-desktop window line (#329).

journal-resume.test.ts (#309) was written against delivery being
recorded the moment an answer is accepted. On this branch a backend
confirms its write through sink.deliverRequest, as the mock backend and
the runner tests do. Its two fake backends now pass a native request id
and write inside deliverRequest, so the crash test still checks that the
card reloads answered and the CLI hears one decision.

Full desktop unit suite: 1,704 tests, 1,688 passed, 16 skipped, 0 failed.

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 20, 2026
… lands #316) (#335)

* Record native answer delivery only after its transport handoff

Keep locally accepted requests resolving until the native write receipt. Make stale authority, withdrawal, Stop, and failed or closed writes inert uncertain outcomes without retries. Deduplicate native requests and preserve sensitive answers and approval policies.

Coven-Harness: Codex / gpt-6
(cherry picked from commit 18ca1a8)

* Hand the iPhone fixture's approval to its CLI, so the phone sees it decided

#316 records a human answer as delivered only after the backend hands it
to its CLI (TurnSink.deliverRequest). Until then the phone is sent a
"Sending decision on your Mac." notice instead of the approval. The
built-in mock and image backends were updated to call it. The iPhone
UI test's Mac fixture has its own backend and was not, so the phone
never saw "Approved" and CompanionFlowTests.swift:348 failed in CI.

The fixture now gives its approval a native request id and hands the
answer off the way the built-in mock does, counting the decision only
once it is consumed.

Locally, on the iPhone 16 Pro simulator (iOS 26.5, Xcode 26.6),
testPairApproveAndFollowUpThroughTheMac failed at line 348 on the
cherry-picked #316, just after "phone answers ... with yes". With this
change it passes in 88.9 s, through the follow-up, pause and resume.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand off decisions in the journal-resume test backends, as #316 requires

#309 (CF-6 journal mechanics) landed after #316 was written. Its two
journal-resume tests use backends that await sink.approval and then
act, without the handoff #316 introduced (TurnSink.deliverRequest).
Under #316 such an answer stays "sending", and a crash makes it
"uncertain". So "the card shows the answer" after a crash, and the
coalesced transcript's answered card, both failed.

Both test backends now hand the decision to their "CLI" through
deliverRequest and record what it heard inside that write, which is
what the tests' own comments describe ("handed to the CLI"). The
withdrawn card is untouched, since it is never delivered.

journal-resume passes 9/9. Desktop unit: 1,688 passed, 16 skipped of
1,704; second stage 21/21. store-desktop 16/16.

Coven-Harness: Claude Code / claude-opus-5-5

* List the delivery receipts under On main, unreleased

#316 described its change under In flight as under review. Once landed,
it belongs with the other landed slices.

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 20, 2026
main requires branches to be up to date, so every landing sent each open
PR back through CI. On 2026-10-19 that cost #309 three extra macOS runs,
and on 2026-10-20 #336, #340 and #343 one or more each.

The ruleset file gains a merge_queue rule (squash, all-green groups, up
to 3 groups building and 5 PRs per merge, a 90-minute check timeout,
since the macOS job alone may take 45). It drops the up-to-date
requirement, which the queue replaces: a queued PR is tested on top of
main and the PRs ahead of it.

CI runs on merge_group, so each queued group reports the required
unit + e2e (macOS) check; without it the queue drops every PR. The
iPhone job and the coverage upload skip queued groups: neither is
required, the PR already ran them, and main's push run repeats them.

README, AGENTS.md and status.md say PRs land through the queue. The live
ruleset (23989202) takes this file once this lands, so the queue starts
only after main can report checks for it.

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 21, 2026
…359)

* Prepare desktop 0.3.0 and Companion 0.3.0 (8) (#337)

* Accept a v<version>-rc.<N> tag in the release identity check

Since #186 the identity check refused every tag but v<version>, so a release
could no longer be rehearsed with an rc tag. A rehearsal tag now names the
package version and an rc number from 1 with no leading zero; the version
itself stays strictly x.y.z and the identity output is unchanged. The rc run
is the real workflow, lands on a draft named after the rc tag, and is deleted
afterwards (docs/release-signing.md, "Rehearsing with an rc tag").

Coven-Harness: Claude Code / claude-opus-5-5

* Bump the desktop app to 0.3.0

Coven-Harness: Claude Code / claude-opus-5-5

* Prepare desktop 0.3.0 and Companion 0.3.0 (8)

Companion moves to 0.3.0 (8) so its next TestFlight build, which reads the thread again when
the Mac refuses an answer from the phone (#318), is cut from the same tag. The review records
the scope since v0.2.0, the v0.3.0-rc.1 hosted rehearsal and the local verification. status.md
lists the preparation under In flight.

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Leave a request open when saving its answer fails (CF-6, finding 10) (#336)

Before this change, answer() and answerQuestion() had the broker accept the
answer (the request became resolving) and dropped the pending entry before
saving the card. When that save threw, as it does on a full disk, the
request stayed resolving with no way to answer it. A retry was refused as
already-resolved, the CLI heard nothing, and the in-memory card read as
sending until Stop.

The card is now saved before the pending entry is dropped. If the save
fails, the broker state and the card go back to what they were and the
error goes to the caller, so the person can answer again. This is the same
rule addItem follows. A failed write that still reached the disk reads as
uncertain after a crash, never as sent; a test pins that case.

Coven-Harness: Claude Code / claude-opus-5-5

* End a stopped Codex turn's background commands, or say they may still run (#341)

Live smoke on 2026-10-20 (codex-cli 0.163.0, unified_exec on): Codex
moved `sleep 30` to a background terminal after 1 s, Stop's
turn/interrupt ended the turn but not the terminal, and the file was
written 30 s later while Modex showed the call interrupted.

Modex now opts into the app-server's experimental methods. When Codex
reports a turn interrupted with commands still open, it terminates
that turn's own background terminals (never an earlier turn's), lists
them again, and calls a command interrupted only once Codex no longer
lists it. A command Codex cannot confirm, or an older CLI without these
methods, reads unknown with a notice.

Red first: four new tests failed on main; all five pass, and the
Codex backend file passes 34/34. Live: the fixed build passed edit,
check, final answer and Stop (the file was never written).

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Record v0.3.0 publication and advance the website fallback (#339)

* Advance the website download fallback to 0.3.0

release.json's verified fallback and the site's release tests move from 0.2.0 to 0.3.0, as #314
did for 0.2.0. Site tests: 10 of 10; site e2e: 7 of 7.

Coven-Harness: Claude Code / claude-opus-5-5

* Record v0.3.0 publication

v0.3.0 was published on 2026-10-20 (07:41 UTC) from signed tag v0.3.0 on 30ee279, with macOS,
Windows and Linux installers, at Val's request. Release run 39382565427 passed signing,
notarization, stapling, packaged E2E, the Windows and Linux checks and all three signed native
updater scenarios, each starting with the automatic download. The downloaded installers passed
independent checks, and the packaged E2E passed 363 cases against the download. The updater
parser selects the 0.3.0 ZIP and its digest from the live releases/latest for 0.2.0, 0.1.0 and
0.0.12.

The publishing request omitted tag_name, and GitHub published the draft as untagged for 25
seconds. Installed Macs saw no update during that window. The review records it.

- status: a Shipped row linking the review. "On main, unreleased" moves to the review's What
  shipped and is empty. The preparation entry becomes the two gates still open: the release
  Mac's banner upgrade from v0.2.0, and Companion build 8 to TestFlight. These supersede
  v0.2.0's gates. The fidelity entry points at the review.
- review: the release run, download checks, publication, and What shipped. The run's release
  manifest is kept as evidence.
- README: over-the-air updates start with v0.3.0. ios-companion: 0.3.0 (build 8).
- landing-page, native-platforms: the fallback and installer publication move to v0.3.0.

Coven-Harness: Claude Code / claude-opus-5-5

* Define the Code Review read contract and fixtures (CR-1) (#334)

* Define the Code Review read contract and fixtures (CR-1)

CR-1 of tracker #319 (#320). The contract every Gate A slice works
against, with nothing wired yet: no IPC channel, no UI, no GitHub call.

- src/shared/code-review.ts: locator, account-scoped key and revision;
  ready/partial/stale/unavailable reads; check and diff coverage rules
  (pending is never passing, a missing patch is not a zero-line change);
  Gate A capabilities; and validateRequest, main's check on everything
  the renderer may send.
- test/fixtures/code-review.ts: synthetic R1-R4 data and faults, plain
  data with a fixed clock. Nothing is copied from the screenshots.
- docs/design/code-review.md: the decisions Val confirmed on #320, a
  source map checked at 9c0b063, the reference manifest and calibration,
  and an owner and test for each of 76 reference controls.
- docs/status.md: Code Review under In flight.

Coven-Harness: Claude Code / claude-opus-5-5

* Fix the CR-1 review findings: synthetic fixtures, reply scope, re-runs

The independent review of #334 found that the fixtures were the
reference PRs with the nouns swapped, some sentences verbatim, and that
three contract rules did not hold as written.

- Fixtures: rewritten from scratch as a made-up library-lending project.
  Only layout and density carry over. A new test checks that every
  patch's hunk headers count its lines and the totals add up.
- acceptsReply takes the operation and implements the connection
  exception its comment promised. scopeOf(request, account) defines a
  reply's scope as its request's, so main and the renderer agree.
- summarizeChecks counts only the newest run of a re-run check (ids
  grow with creation), so a passing re-run replaces its failure.
- inboxGroup marks partial groups of a stale inbox stale; knownCount
  never calls a stale total exact.
- validateRequest rejects lone surrogates in any text and invisible
  format characters in paths and refs.
- An unavailable read names no issue: installing or signing in to gh is
  the person's fix.
- Design page: who posted the #320 decisions, typography and spacing
  measurements with two token proposals, the composer's model and
  access-mode icons, scroll regions and avatars in the inventory, a
  seventh open decision, and corrected source-map wording.

Coven-Harness: Claude Code / claude-opus-5-5

* Match re-runs by reporter and reject C1 controls (CR-1 re-review)

The re-review of #334 confirmed the ten findings fixed and left these:

- A check run is matched to its re-run by reporter as well as name and
  head (PrCheck.reporter: the app slug or the status creator), so
  another reporter's same-named check is not hidden. Without numeric
  ids, a queued re-run counts as the latest.
- CONTROL covers C0, DEL, C1 and the line and paragraph separators.
- Design page: the encodeURIComponent rule moves to the engineering
  decisions, ModelMenu's line is 195, C82 cites the access pill, and
  C77-C82 follow C76.

Coven-Harness: Claude Code / claude-opus-5-5

* Keep keyboard focus when an approval is answered, its turn folds, Stop ends it, or Jump to latest is used (CF-8) (#342)

* Keep keyboard focus when an approval is answered and its turn folds

The CF-8 keyboard walkthrough found that pressing Approve or Deny with
the keyboard dropped focus onto the page body: the answer buttons
unmount once pressed, and a finished turn then hides its folded work.
A keyboard or VoiceOver user lost their place twice.

Focus now moves to the card's answer line ("Sending decision…", then
"Approved" or "Denied"). When the finished turn folds work that holds
focus, it moves to the turn's "Worked for" toggle. Focus elsewhere,
such as the composer, is left alone.

Red first: both keyboard tests failed on main (answer line inactive).
e2e/keyboard-answers.spec.ts passes 3/3; the full desktop e2e passes
366 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box when a keyboard Stop ends the turn

The second walkthrough probe found the same defect at Stop: the button
goes away when the turn ends, and focus fell onto the page body. When
Stop was pressed with focus on it, focus now moves to the message box,
where the next message is typed.

Red first: the new keyboard test failed on the previous commit
(composer inactive); keyboard-answers.spec.ts passes 4/4 and the full
desktop e2e passes 367 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box after Jump to latest from the keyboard

The third walkthrough probe found that Jump to latest, once activated,
goes away at the bottom and left focus on the page body. When the
button held focus, focus now moves to the message box there.

Red first: the new test failed (message box inactive). The keyboard
spec passes 5/5, app.spec with it 19/19, and conversation-continuity
plus typed-turns, which also scroll, 8/8.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Restore main's status.md after the #339 merge, keeping only this PR's entry

The merge kept both sides of a block #339 had moved, which duplicated
the released entries. The page is now main's, plus this PR's one entry.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a Codex web search's query and the results Codex reported (CF-5) (#344)

A live codex-cli 0.163.0 capture (test/fixtures/codex-websearch-0.163.0.json)
shows the webSearch item starting with an empty query, no action and
no results, and completing with the query, a search action and
text_result entries (title, url, domain, snippet). Modex titled the row
"search " from the empty start, read neither action nor results, and
stored the JSON string "" as its output.

codexWebSearch (shared/evidence.ts) keeps the query, the action and
results with an http(s) link, each text bounded, and counts what it
drops. The Codex backend titles the row "search the web" until the
query arrives, then "search <query>", and carries the search on the
tool item. Expanded, the results show as links with snippets, marked
data-provenance="codex-search" and labelled as unchecked. The renderer
checks each link again before showing it, and nothing is fetched until
a link is clicked. Older searches no longer show "".

Red first: the three evidence tests and the Codex backend test failed
on a stub, and the e2e test failed ("done", not "2 results"). Now
evidence, codex-backend and conversation-events pass 69/69, the
conversation-evidence spec passes 7/7, and the desktop unit suite
1,709 passed with 16 skipped.

Part of #278 (CF-5), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Land main through a merge queue (#345)

main requires branches to be up to date, so every landing sent each open
PR back through CI. On 2026-10-19 that cost #309 three extra macOS runs,
and on 2026-10-20 #336, #340 and #343 one or more each.

The ruleset file gains a merge_queue rule (squash, all-green groups, up
to 3 groups building and 5 PRs per merge, a 90-minute check timeout,
since the macOS job alone may take 45). It drops the up-to-date
requirement, which the queue replaces: a queued PR is tested on top of
main and the PRs ahead of it.

CI runs on merge_group, so each queued group reports the required
unit + e2e (macOS) check; without it the queue drops every PR. The
iPhone job and the coverage upload skip queued groups: neither is
required, the PR already ran them, and main's push run repeats them.

README, AGENTS.md and status.md say PRs land through the queue. The live
ruleset (23989202) takes this file once this lands, so the queue starts
only after main can report checks for it.

Coven-Harness: Claude Code / claude-opus-5-5

* Say the phone no longer shows a withdrawn approval as declined (#343)

human-requests.md still said the iPhone shows a withdrawn approval as
declined. Since #335, mobileItem (companion.ts) sends a withdrawn,
sending or uncertain approval to the phone as a notice that says so.
The companion fallback test now covers the withdrawn case, which had
no test (CF-6 #279, finding 7).

Coven-Harness: Claude Code / claude-opus-5-5

* Note that the merge queue needs auto-merge, and list the keyboard Jump to latest CI failure (steward cycle 17) (#347)

The first PR queued after #345 failed to enqueue: "Auto merge is not
allowed for this repository". gh pr merge adds a PR to a merge queue by
enabling auto-merge, so the repository now allows it, as OpenCoven/coven
does, and README says why.

e2e/keyboard-answers.spec.ts:88 (#342) failed in three CI runs of #345 in
a row and passed on #342, on main and 25/25 locally. It goes under Known
rough edges with what the failure snapshot shows and a link to the
analysis on #342.

Coven-Harness: Claude Code / claude-opus-5-5

* Prove a queue-landed branch merged when its tip merges cleanly into the landed tree (#350)

Since #345, main lands through a merge queue, which squashes each PR onto main as it stands at
the PR's turn. A branch that merged main into itself, then landed behind another PR, had no proof
that worktree.sh remove would accept:
- the squash tree also holds the PRs ahead of it, so it is not the tip's tree;
- the branch's merge commits rule out the binary-diff proof.

cf6-docs-followup (#343) hit this on 2026-10-20 and had to be pointed at its squash commit by hand.

The new merge-tree proof merges the exact PR tip onto the squash commit's parent with Git's own
merge. It accepts only a clean merge (exit 0) whose tree is exactly the landed tree. A configured
merge.<name>.driver turns the check off, because a driver can drop a side's changes and still
report a clean merge.

New tests:
- a queued branch with merge commits is proven and removed, and its archive ref holds the tip;
- a wrong head, a wrong base, a different tree, a conflicted merge (even when the squash holds
  exactly the conflicted tree) and a configured driver that keeps main's side each stay
  not-proven-merged.

Checked against #343's real branch and PR record in a throwaway clone. The old proof returns null.
The new proof returns merge-tree with tree 6fc493b, the tree of 0a67354.

Coven-Harness: Claude Code / claude-opus-5-5

* Hold Jump to latest when the composer grows or a Page Up is still moving (#352)

* Hold Jump to latest when the composer grows or a Page Up is still moving

e2e/keyboard-answers.spec.ts:88 (#342) failed on slow CI runners: after
Enter, Jump to latest stayed. A trace in the app found two causes.

- A scroll event is delivered before the browser reports a resize. If
  the composer grew right after the jump (a follow-up chip), onScroll
  measured a shorter view and stopped following. It now keeps following
  while the view has not moved above where auto-scroll put it, as
  follow() already did.
- A Page Up still animating keeps applying its remaining distance after
  the page sets a new position: plain assignment, scrollTo instant and
  an overflow toggle all lose to it. For a second after a jump, a scroll
  with no wheel, key, pointer or touch input since the jump is that
  leftover motion, and the view pins back to the bottom. The reader's
  own scroll still wins at once.

Red first, each test alone on main's ThreadView: the composer-growth
test failed (the button stayed), the Page Up test failed (the view went
back up 138 px), and the scroll-up guard passed. With the fix,
keyboard-answers passes 24/24 (8 tests x 3), and app, conversation-
continuity, typed-turns, conversation-rendering and layout 43/43.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Move the Jump to latest rough edge to the resolved list, with its cause

The entry #347 added leaves Known rough edges, as written, under #352,
with the two causes the trace found.

Coven-Harness: Claude Code / claude-opus-5-5

* Say whether a check verified a turn's last change (CF-7 verification) (#351)

* Say whether a check verified a turn's last change, in its task record (CF-7)

verification() in shared/task-context.ts reads a turn's tools in
transcript order:
- A check is a known test, build, typecheck or lint command, read
  inside sh/bash/zsh -c with quotes respected. An exit-zero echo or
  the model saying "tests pass" is not one.
- A check passes only as succeeded with exit code 0 or none reported.
- A check goes stale when an edit that was not denied, or a command
  that might write, runs after it.
- The turn reads verified, failed, unknown, stale or none.

taskRecord carries it, and the note gets a Verification section that
marks stale checks. Anything short of verified goes under Not settled;
a failure a later fix superseded does not. Plan steps keep the CLI's
statuses: neither CLI links a check to a step.

Red first: five of six new tests failed on a stub, then edits and
displayCommand on theirs. task-context passes 13/13; the desktop
unit suite 1,723 passed, 16 skipped.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a turn's verification beside Save to Notes (CF-7)

A finished turn's footer reads, for example, "Verified: pnpm test passed
after the last change" or "Not verified: pnpm test ran before a later
change". It also covers a check that failed, a check that did not say
how it ended, and a turn whose changes no check followed. A check
shows as the script its shell ran, with the exact command on hover. A
turn that changed nothing and ran no check shows nothing.

Red first: the new e2e test found no verification lines (expected 2,
received 0). task-to-notes.spec.ts passes 3/3.

docs/task-context.md gains a Verification section, and status.md an
entry.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* List #352's Jump to latest fix under On main, unreleased (steward cycle 19) (#353)

#352 changed what readers see: after a jump, the transcript no longer stops short of the bottom
when the message box grows or a Page Up is still scrolling. It moved the CI rough edge to the
resolved list but added no unreleased entry, so the next release review would not have listed it.
The entry sits beside CF-8's keyboard entry and links the resolved list's account of the causes.

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 21, 2026
* Prepare desktop 0.3.0 and Companion 0.3.0 (8) (#337)

* Accept a v<version>-rc.<N> tag in the release identity check

Since #186 the identity check refused every tag but v<version>, so a release
could no longer be rehearsed with an rc tag. A rehearsal tag now names the
package version and an rc number from 1 with no leading zero; the version
itself stays strictly x.y.z and the identity output is unchanged. The rc run
is the real workflow, lands on a draft named after the rc tag, and is deleted
afterwards (docs/release-signing.md, "Rehearsing with an rc tag").

Coven-Harness: Claude Code / claude-opus-5-5

* Bump the desktop app to 0.3.0

Coven-Harness: Claude Code / claude-opus-5-5

* Prepare desktop 0.3.0 and Companion 0.3.0 (8)

Companion moves to 0.3.0 (8) so its next TestFlight build, which reads the thread again when
the Mac refuses an answer from the phone (#318), is cut from the same tag. The review records
the scope since v0.2.0, the v0.3.0-rc.1 hosted rehearsal and the local verification. status.md
lists the preparation under In flight.

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Record the v0.3.0 local verification

Coven-Harness: Claude Code / claude-opus-5-5

* Leave a request open when saving its answer fails (CF-6, finding 10) (#336)

Before this change, answer() and answerQuestion() had the broker accept the
answer (the request became resolving) and dropped the pending entry before
saving the card. When that save threw, as it does on a full disk, the
request stayed resolving with no way to answer it. A retry was refused as
already-resolved, the CLI heard nothing, and the in-memory card read as
sending until Stop.

The card is now saved before the pending entry is dropped. If the save
fails, the broker state and the card go back to what they were and the
error goes to the caller, so the person can answer again. This is the same
rule addItem follows. A failed write that still reached the disk reads as
uncertain after a crash, never as sent; a test pins that case.

Coven-Harness: Claude Code / claude-opus-5-5

* End a stopped Codex turn's background commands, or say they may still run (#341)

Live smoke on 2026-10-20 (codex-cli 0.163.0, unified_exec on): Codex
moved `sleep 30` to a background terminal after 1 s, Stop's
turn/interrupt ended the turn but not the terminal, and the file was
written 30 s later while Modex showed the call interrupted.

Modex now opts into the app-server's experimental methods. When Codex
reports a turn interrupted with commands still open, it terminates
that turn's own background terminals (never an earlier turn's), lists
them again, and calls a command interrupted only once Codex no longer
lists it. A command Codex cannot confirm, or an older CLI without these
methods, reads unknown with a notice.

Red first: four new tests failed on main; all five pass, and the
Codex backend file passes 34/34. Live: the fixed build passed edit,
check, final answer and Stop (the file was never written).

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Record v0.3.0 publication and advance the website fallback (#339)

* Advance the website download fallback to 0.3.0

release.json's verified fallback and the site's release tests move from 0.2.0 to 0.3.0, as #314
did for 0.2.0. Site tests: 10 of 10; site e2e: 7 of 7.

Coven-Harness: Claude Code / claude-opus-5-5

* Record v0.3.0 publication

v0.3.0 was published on 2026-10-20 (07:41 UTC) from signed tag v0.3.0 on 30ee279, with macOS,
Windows and Linux installers, at Val's request. Release run 39382565427 passed signing,
notarization, stapling, packaged E2E, the Windows and Linux checks and all three signed native
updater scenarios, each starting with the automatic download. The downloaded installers passed
independent checks, and the packaged E2E passed 363 cases against the download. The updater
parser selects the 0.3.0 ZIP and its digest from the live releases/latest for 0.2.0, 0.1.0 and
0.0.12.

The publishing request omitted tag_name, and GitHub published the draft as untagged for 25
seconds. Installed Macs saw no update during that window. The review records it.

- status: a Shipped row linking the review. "On main, unreleased" moves to the review's What
  shipped and is empty. The preparation entry becomes the two gates still open: the release
  Mac's banner upgrade from v0.2.0, and Companion build 8 to TestFlight. These supersede
  v0.2.0's gates. The fidelity entry points at the review.
- review: the release run, download checks, publication, and What shipped. The run's release
  manifest is kept as evidence.
- README: over-the-air updates start with v0.3.0. ios-companion: 0.3.0 (build 8).
- landing-page, native-platforms: the fallback and installer publication move to v0.3.0.

Coven-Harness: Claude Code / claude-opus-5-5

* Define the Code Review read contract and fixtures (CR-1) (#334)

* Define the Code Review read contract and fixtures (CR-1)

CR-1 of tracker #319 (#320). The contract every Gate A slice works
against, with nothing wired yet: no IPC channel, no UI, no GitHub call.

- src/shared/code-review.ts: locator, account-scoped key and revision;
  ready/partial/stale/unavailable reads; check and diff coverage rules
  (pending is never passing, a missing patch is not a zero-line change);
  Gate A capabilities; and validateRequest, main's check on everything
  the renderer may send.
- test/fixtures/code-review.ts: synthetic R1-R4 data and faults, plain
  data with a fixed clock. Nothing is copied from the screenshots.
- docs/design/code-review.md: the decisions Val confirmed on #320, a
  source map checked at 9c0b063, the reference manifest and calibration,
  and an owner and test for each of 76 reference controls.
- docs/status.md: Code Review under In flight.

Coven-Harness: Claude Code / claude-opus-5-5

* Fix the CR-1 review findings: synthetic fixtures, reply scope, re-runs

The independent review of #334 found that the fixtures were the
reference PRs with the nouns swapped, some sentences verbatim, and that
three contract rules did not hold as written.

- Fixtures: rewritten from scratch as a made-up library-lending project.
  Only layout and density carry over. A new test checks that every
  patch's hunk headers count its lines and the totals add up.
- acceptsReply takes the operation and implements the connection
  exception its comment promised. scopeOf(request, account) defines a
  reply's scope as its request's, so main and the renderer agree.
- summarizeChecks counts only the newest run of a re-run check (ids
  grow with creation), so a passing re-run replaces its failure.
- inboxGroup marks partial groups of a stale inbox stale; knownCount
  never calls a stale total exact.
- validateRequest rejects lone surrogates in any text and invisible
  format characters in paths and refs.
- An unavailable read names no issue: installing or signing in to gh is
  the person's fix.
- Design page: who posted the #320 decisions, typography and spacing
  measurements with two token proposals, the composer's model and
  access-mode icons, scroll regions and avatars in the inventory, a
  seventh open decision, and corrected source-map wording.

Coven-Harness: Claude Code / claude-opus-5-5

* Match re-runs by reporter and reject C1 controls (CR-1 re-review)

The re-review of #334 confirmed the ten findings fixed and left these:

- A check run is matched to its re-run by reporter as well as name and
  head (PrCheck.reporter: the app slug or the status creator), so
  another reporter's same-named check is not hidden. Without numeric
  ids, a queued re-run counts as the latest.
- CONTROL covers C0, DEL, C1 and the line and paragraph separators.
- Design page: the encodeURIComponent rule moves to the engineering
  decisions, ModelMenu's line is 195, C82 cites the access pill, and
  C77-C82 follow C76.

Coven-Harness: Claude Code / claude-opus-5-5

* Keep keyboard focus when an approval is answered, its turn folds, Stop ends it, or Jump to latest is used (CF-8) (#342)

* Keep keyboard focus when an approval is answered and its turn folds

The CF-8 keyboard walkthrough found that pressing Approve or Deny with
the keyboard dropped focus onto the page body: the answer buttons
unmount once pressed, and a finished turn then hides its folded work.
A keyboard or VoiceOver user lost their place twice.

Focus now moves to the card's answer line ("Sending decision…", then
"Approved" or "Denied"). When the finished turn folds work that holds
focus, it moves to the turn's "Worked for" toggle. Focus elsewhere,
such as the composer, is left alone.

Red first: both keyboard tests failed on main (answer line inactive).
e2e/keyboard-answers.spec.ts passes 3/3; the full desktop e2e passes
366 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box when a keyboard Stop ends the turn

The second walkthrough probe found the same defect at Stop: the button
goes away when the turn ends, and focus fell onto the page body. When
Stop was pressed with focus on it, focus now moves to the message box,
where the next message is typed.

Red first: the new keyboard test failed on the previous commit
(composer inactive); keyboard-answers.spec.ts passes 4/4 and the full
desktop e2e passes 367 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box after Jump to latest from the keyboard

The third walkthrough probe found that Jump to latest, once activated,
goes away at the bottom and left focus on the page body. When the
button held focus, focus now moves to the message box there.

Red first: the new test failed (message box inactive). The keyboard
spec passes 5/5, app.spec with it 19/19, and conversation-continuity
plus typed-turns, which also scroll, 8/8.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Restore main's status.md after the #339 merge, keeping only this PR's entry

The merge kept both sides of a block #339 had moved, which duplicated
the released entries. The page is now main's, plus this PR's one entry.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a Codex web search's query and the results Codex reported (CF-5) (#344)

A live codex-cli 0.163.0 capture (test/fixtures/codex-websearch-0.163.0.json)
shows the webSearch item starting with an empty query, no action and
no results, and completing with the query, a search action and
text_result entries (title, url, domain, snippet). Modex titled the row
"search " from the empty start, read neither action nor results, and
stored the JSON string "" as its output.

codexWebSearch (shared/evidence.ts) keeps the query, the action and
results with an http(s) link, each text bounded, and counts what it
drops. The Codex backend titles the row "search the web" until the
query arrives, then "search <query>", and carries the search on the
tool item. Expanded, the results show as links with snippets, marked
data-provenance="codex-search" and labelled as unchecked. The renderer
checks each link again before showing it, and nothing is fetched until
a link is clicked. Older searches no longer show "".

Red first: the three evidence tests and the Codex backend test failed
on a stub, and the e2e test failed ("done", not "2 results"). Now
evidence, codex-backend and conversation-events pass 69/69, the
conversation-evidence spec passes 7/7, and the desktop unit suite
1,709 passed with 16 skipped.

Part of #278 (CF-5), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Land main through a merge queue (#345)

main requires branches to be up to date, so every landing sent each open
PR back through CI. On 2026-10-19 that cost #309 three extra macOS runs,
and on 2026-10-20 #336, #340 and #343 one or more each.

The ruleset file gains a merge_queue rule (squash, all-green groups, up
to 3 groups building and 5 PRs per merge, a 90-minute check timeout,
since the macOS job alone may take 45). It drops the up-to-date
requirement, which the queue replaces: a queued PR is tested on top of
main and the PRs ahead of it.

CI runs on merge_group, so each queued group reports the required
unit + e2e (macOS) check; without it the queue drops every PR. The
iPhone job and the coverage upload skip queued groups: neither is
required, the PR already ran them, and main's push run repeats them.

README, AGENTS.md and status.md say PRs land through the queue. The live
ruleset (23989202) takes this file once this lands, so the queue starts
only after main can report checks for it.

Coven-Harness: Claude Code / claude-opus-5-5

* Say the phone no longer shows a withdrawn approval as declined (#343)

human-requests.md still said the iPhone shows a withdrawn approval as
declined. Since #335, mobileItem (companion.ts) sends a withdrawn,
sending or uncertain approval to the phone as a notice that says so.
The companion fallback test now covers the withdrawn case, which had
no test (CF-6 #279, finding 7).

Coven-Harness: Claude Code / claude-opus-5-5

* Note that the merge queue needs auto-merge, and list the keyboard Jump to latest CI failure (steward cycle 17) (#347)

The first PR queued after #345 failed to enqueue: "Auto merge is not
allowed for this repository". gh pr merge adds a PR to a merge queue by
enabling auto-merge, so the repository now allows it, as OpenCoven/coven
does, and README says why.

e2e/keyboard-answers.spec.ts:88 (#342) failed in three CI runs of #345 in
a row and passed on #342, on main and 25/25 locally. It goes under Known
rough edges with what the failure snapshot shows and a link to the
analysis on #342.

Coven-Harness: Claude Code / claude-opus-5-5

* Prove a queue-landed branch merged when its tip merges cleanly into the landed tree (#350)

Since #345, main lands through a merge queue, which squashes each PR onto main as it stands at
the PR's turn. A branch that merged main into itself, then landed behind another PR, had no proof
that worktree.sh remove would accept:
- the squash tree also holds the PRs ahead of it, so it is not the tip's tree;
- the branch's merge commits rule out the binary-diff proof.

cf6-docs-followup (#343) hit this on 2026-10-20 and had to be pointed at its squash commit by hand.

The new merge-tree proof merges the exact PR tip onto the squash commit's parent with Git's own
merge. It accepts only a clean merge (exit 0) whose tree is exactly the landed tree. A configured
merge.<name>.driver turns the check off, because a driver can drop a side's changes and still
report a clean merge.

New tests:
- a queued branch with merge commits is proven and removed, and its archive ref holds the tip;
- a wrong head, a wrong base, a different tree, a conflicted merge (even when the squash holds
  exactly the conflicted tree) and a configured driver that keeps main's side each stay
  not-proven-merged.

Checked against #343's real branch and PR record in a throwaway clone. The old proof returns null.
The new proof returns merge-tree with tree 6fc493b, the tree of 0a67354.

Coven-Harness: Claude Code / claude-opus-5-5

* Hold Jump to latest when the composer grows or a Page Up is still moving (#352)

* Hold Jump to latest when the composer grows or a Page Up is still moving

e2e/keyboard-answers.spec.ts:88 (#342) failed on slow CI runners: after
Enter, Jump to latest stayed. A trace in the app found two causes.

- A scroll event is delivered before the browser reports a resize. If
  the composer grew right after the jump (a follow-up chip), onScroll
  measured a shorter view and stopped following. It now keeps following
  while the view has not moved above where auto-scroll put it, as
  follow() already did.
- A Page Up still animating keeps applying its remaining distance after
  the page sets a new position: plain assignment, scrollTo instant and
  an overflow toggle all lose to it. For a second after a jump, a scroll
  with no wheel, key, pointer or touch input since the jump is that
  leftover motion, and the view pins back to the bottom. The reader's
  own scroll still wins at once.

Red first, each test alone on main's ThreadView: the composer-growth
test failed (the button stayed), the Page Up test failed (the view went
back up 138 px), and the scroll-up guard passed. With the fix,
keyboard-answers passes 24/24 (8 tests x 3), and app, conversation-
continuity, typed-turns, conversation-rendering and layout 43/43.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Move the Jump to latest rough edge to the resolved list, with its cause

The entry #347 added leaves Known rough edges, as written, under #352,
with the two causes the trace found.

Coven-Harness: Claude Code / claude-opus-5-5

* Say whether a check verified a turn's last change (CF-7 verification) (#351)

* Say whether a check verified a turn's last change, in its task record (CF-7)

verification() in shared/task-context.ts reads a turn's tools in
transcript order:
- A check is a known test, build, typecheck or lint command, read
  inside sh/bash/zsh -c with quotes respected. An exit-zero echo or
  the model saying "tests pass" is not one.
- A check passes only as succeeded with exit code 0 or none reported.
- A check goes stale when an edit that was not denied, or a command
  that might write, runs after it.
- The turn reads verified, failed, unknown, stale or none.

taskRecord carries it, and the note gets a Verification section that
marks stale checks. Anything short of verified goes under Not settled;
a failure a later fix superseded does not. Plan steps keep the CLI's
statuses: neither CLI links a check to a step.

Red first: five of six new tests failed on a stub, then edits and
displayCommand on theirs. task-context passes 13/13; the desktop
unit suite 1,723 passed, 16 skipped.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a turn's verification beside Save to Notes (CF-7)

A finished turn's footer reads, for example, "Verified: pnpm test passed
after the last change" or "Not verified: pnpm test ran before a later
change". It also covers a check that failed, a check that did not say
how it ended, and a turn whose changes no check followed. A check
shows as the script its shell ran, with the exact command on hover. A
turn that changed nothing and ran no check shows nothing.

Red first: the new e2e test found no verification lines (expected 2,
received 0). task-to-notes.spec.ts passes 3/3.

docs/task-context.md gains a Verification section, and status.md an
entry.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* List #352's Jump to latest fix under On main, unreleased (steward cycle 19) (#353)

#352 changed what readers see: after a jump, the transcript no longer stops short of the bottom
when the message box grows or a Page Up is still scrolling. It moved the CI rough edge to the
resolved list but added no unreleased entry, so the next release review would not have listed it.
The entry sits beside CF-8's keyboard entry and links the resolved list's account of the causes.

Coven-Harness: Claude Code / claude-opus-5-5

* Mark the CF-7 verification plan done in #351 (steward cycle 20) (#356)

#351 landed every task in docs/superpowers/plans/2026-10-20-cf7-verification.md, but the plan
still read as not started. The five boxes are ticked from #351's own evidence:
- the red and green unit tests;
- the footer e2e (3/3);
- task-context.md and status.md updated;
- pnpm build and typecheck passed locally;
- the full e2e suite passed in CI, on the PR and in the merge queue (run 39508701952). The local
  run stopped when the disk filled.

A dated status line says that linking a check to a plan step stays open on #280.

Coven-Harness: Claude Code / claude-opus-5-5
BunsDev added a commit that referenced this pull request Oct 21, 2026
* Keep keyboard focus when an approval is answered, its turn folds, Stop ends it, or Jump to latest is used (CF-8) (#342)

* Keep keyboard focus when an approval is answered and its turn folds

The CF-8 keyboard walkthrough found that pressing Approve or Deny with
the keyboard dropped focus onto the page body: the answer buttons
unmount once pressed, and a finished turn then hides its folded work.
A keyboard or VoiceOver user lost their place twice.

Focus now moves to the card's answer line ("Sending decision…", then
"Approved" or "Denied"). When the finished turn folds work that holds
focus, it moves to the turn's "Worked for" toggle. Focus elsewhere,
such as the composer, is left alone.

Red first: both keyboard tests failed on main (answer line inactive).
e2e/keyboard-answers.spec.ts passes 3/3; the full desktop e2e passes
366 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box when a keyboard Stop ends the turn

The second walkthrough probe found the same defect at Stop: the button
goes away when the turn ends, and focus fell onto the page body. When
Stop was pressed with focus on it, focus now moves to the message box,
where the next message is typed.

Red first: the new keyboard test failed on the previous commit
(composer inactive); keyboard-answers.spec.ts passes 4/4 and the full
desktop e2e passes 367 with 6 opt-in skips.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Hand focus to the message box after Jump to latest from the keyboard

The third walkthrough probe found that Jump to latest, once activated,
goes away at the bottom and left focus on the page body. When the
button held focus, focus now moves to the message box there.

Red first: the new test failed (message box inactive). The keyboard
spec passes 5/5, app.spec with it 19/19, and conversation-continuity
plus typed-turns, which also scroll, 8/8.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Restore main's status.md after the #339 merge, keeping only this PR's entry

The merge kept both sides of a block #339 had moved, which duplicated
the released entries. The page is now main's, plus this PR's one entry.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a Codex web search's query and the results Codex reported (CF-5) (#344)

A live codex-cli 0.163.0 capture (test/fixtures/codex-websearch-0.163.0.json)
shows the webSearch item starting with an empty query, no action and
no results, and completing with the query, a search action and
text_result entries (title, url, domain, snippet). Modex titled the row
"search " from the empty start, read neither action nor results, and
stored the JSON string "" as its output.

codexWebSearch (shared/evidence.ts) keeps the query, the action and
results with an http(s) link, each text bounded, and counts what it
drops. The Codex backend titles the row "search the web" until the
query arrives, then "search <query>", and carries the search on the
tool item. Expanded, the results show as links with snippets, marked
data-provenance="codex-search" and labelled as unchecked. The renderer
checks each link again before showing it, and nothing is fetched until
a link is clicked. Older searches no longer show "".

Red first: the three evidence tests and the Codex backend test failed
on a stub, and the e2e test failed ("done", not "2 results"). Now
evidence, codex-backend and conversation-events pass 69/69, the
conversation-evidence spec passes 7/7, and the desktop unit suite
1,709 passed with 16 skipped.

Part of #278 (CF-5), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Land main through a merge queue (#345)

main requires branches to be up to date, so every landing sent each open
PR back through CI. On 2026-10-19 that cost #309 three extra macOS runs,
and on 2026-10-20 #336, #340 and #343 one or more each.

The ruleset file gains a merge_queue rule (squash, all-green groups, up
to 3 groups building and 5 PRs per merge, a 90-minute check timeout,
since the macOS job alone may take 45). It drops the up-to-date
requirement, which the queue replaces: a queued PR is tested on top of
main and the PRs ahead of it.

CI runs on merge_group, so each queued group reports the required
unit + e2e (macOS) check; without it the queue drops every PR. The
iPhone job and the coverage upload skip queued groups: neither is
required, the PR already ran them, and main's push run repeats them.

README, AGENTS.md and status.md say PRs land through the queue. The live
ruleset (23989202) takes this file once this lands, so the queue starts
only after main can report checks for it.

Coven-Harness: Claude Code / claude-opus-5-5

* Say the phone no longer shows a withdrawn approval as declined (#343)

human-requests.md still said the iPhone shows a withdrawn approval as
declined. Since #335, mobileItem (companion.ts) sends a withdrawn,
sending or uncertain approval to the phone as a notice that says so.
The companion fallback test now covers the withdrawn case, which had
no test (CF-6 #279, finding 7).

Coven-Harness: Claude Code / claude-opus-5-5

* Note that the merge queue needs auto-merge, and list the keyboard Jump to latest CI failure (steward cycle 17) (#347)

The first PR queued after #345 failed to enqueue: "Auto merge is not
allowed for this repository". gh pr merge adds a PR to a merge queue by
enabling auto-merge, so the repository now allows it, as OpenCoven/coven
does, and README says why.

e2e/keyboard-answers.spec.ts:88 (#342) failed in three CI runs of #345 in
a row and passed on #342, on main and 25/25 locally. It goes under Known
rough edges with what the failure snapshot shows and a link to the
analysis on #342.

Coven-Harness: Claude Code / claude-opus-5-5

* Prove a queue-landed branch merged when its tip merges cleanly into the landed tree (#350)

Since #345, main lands through a merge queue, which squashes each PR onto main as it stands at
the PR's turn. A branch that merged main into itself, then landed behind another PR, had no proof
that worktree.sh remove would accept:
- the squash tree also holds the PRs ahead of it, so it is not the tip's tree;
- the branch's merge commits rule out the binary-diff proof.

cf6-docs-followup (#343) hit this on 2026-10-20 and had to be pointed at its squash commit by hand.

The new merge-tree proof merges the exact PR tip onto the squash commit's parent with Git's own
merge. It accepts only a clean merge (exit 0) whose tree is exactly the landed tree. A configured
merge.<name>.driver turns the check off, because a driver can drop a side's changes and still
report a clean merge.

New tests:
- a queued branch with merge commits is proven and removed, and its archive ref holds the tip;
- a wrong head, a wrong base, a different tree, a conflicted merge (even when the squash holds
  exactly the conflicted tree) and a configured driver that keeps main's side each stay
  not-proven-merged.

Checked against #343's real branch and PR record in a throwaway clone. The old proof returns null.
The new proof returns merge-tree with tree 6fc493b, the tree of 0a67354.

Coven-Harness: Claude Code / claude-opus-5-5

* Hold Jump to latest when the composer grows or a Page Up is still moving (#352)

* Hold Jump to latest when the composer grows or a Page Up is still moving

e2e/keyboard-answers.spec.ts:88 (#342) failed on slow CI runners: after
Enter, Jump to latest stayed. A trace in the app found two causes.

- A scroll event is delivered before the browser reports a resize. If
  the composer grew right after the jump (a follow-up chip), onScroll
  measured a shorter view and stopped following. It now keeps following
  while the view has not moved above where auto-scroll put it, as
  follow() already did.
- A Page Up still animating keeps applying its remaining distance after
  the page sets a new position: plain assignment, scrollTo instant and
  an overflow toggle all lose to it. For a second after a jump, a scroll
  with no wheel, key, pointer or touch input since the jump is that
  leftover motion, and the view pins back to the bottom. The reader's
  own scroll still wins at once.

Red first, each test alone on main's ThreadView: the composer-growth
test failed (the button stayed), the Page Up test failed (the view went
back up 138 px), and the scroll-up guard passed. With the fix,
keyboard-answers passes 24/24 (8 tests x 3), and app, conversation-
continuity, typed-turns, conversation-rendering and layout 43/43.

Part of #281 (CF-8), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Move the Jump to latest rough edge to the resolved list, with its cause

The entry #347 added leaves Known rough edges, as written, under #352,
with the two causes the trace found.

Coven-Harness: Claude Code / claude-opus-5-5

* Invalidate retained PR data after authoritative visibility loss

* Say whether a check verified a turn's last change (CF-7 verification) (#351)

* Say whether a check verified a turn's last change, in its task record (CF-7)

verification() in shared/task-context.ts reads a turn's tools in
transcript order:
- A check is a known test, build, typecheck or lint command, read
  inside sh/bash/zsh -c with quotes respected. An exit-zero echo or
  the model saying "tests pass" is not one.
- A check passes only as succeeded with exit code 0 or none reported.
- A check goes stale when an edit that was not denied, or a command
  that might write, runs after it.
- The turn reads verified, failed, unknown, stale or none.

taskRecord carries it, and the note gets a Verification section that
marks stale checks. Anything short of verified goes under Not settled;
a failure a later fix superseded does not. Plan steps keep the CLI's
statuses: neither CLI links a check to a step.

Red first: five of six new tests failed on a stub, then edits and
displayCommand on theirs. task-context passes 13/13; the desktop
unit suite 1,723 passed, 16 skipped.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* Show a turn's verification beside Save to Notes (CF-7)

A finished turn's footer reads, for example, "Verified: pnpm test passed
after the last change" or "Not verified: pnpm test ran before a later
change". It also covers a check that failed, a check that did not say
how it ended, and a turn whose changes no check followed. A check
shows as the script its shell ran, with the exact command on hover. A
turn that changed nothing and ran no check shows nothing.

Red first: the new e2e test found no verification lines (expected 2,
received 0). task-to-notes.spec.ts passes 3/3.

docs/task-context.md gains a Verification section, and status.md an
entry.

Part of #280 (CF-7), tracker #273.

Coven-Harness: Claude Code / claude-opus-5-5

* List #352's Jump to latest fix under On main, unreleased (steward cycle 19) (#353)

#352 changed what readers see: after a jump, the transcript no longer stops short of the bottom
when the message box grows or a Page Up is still scrolling. It moved the CI rough edge to the
resolved list but added no unreleased entry, so the next release review would not have listed it.
The entry sits beside CF-8's keyboard entry and links the resolved list's account of the causes.

Coven-Harness: Claude Code / claude-opus-5-5

* Mark the CF-7 verification plan done in #351 (steward cycle 20) (#356)

#351 landed every task in docs/superpowers/plans/2026-10-20-cf7-verification.md, but the plan
still read as not started. The five boxes are ticked from #351's own evidence:
- the red and green unit tests;
- the footer e2e (3/3);
- task-context.md and status.md updated;
- pnpm build and typecheck passed locally;
- the full e2e suite passed in CI, on the PR and in the merge queue (run 39508701952). The local
  run stopped when the disk filled.

A dated status line says that linking a check to a plan step stays open on #280.

Coven-Harness: Claude Code / claude-opus-5-5

* Invalidate shared read resources without losing partial inbox results

This branch was successfully deployed

1 active deployment
Preview — b18a90a6 Deployed Oct 19, 2026 by vercel[bot]
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