Skip to content

Promote ox/devex to main - #4

Merged
oxfern merged 688 commits into
mainfrom
lmist/pnpm-not-found
Aug 4, 2026
Merged

Promote ox/devex to main#4
oxfern merged 688 commits into
mainfrom
lmist/pnpm-not-found

Conversation

@oxfern

@oxfern oxfern commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Summary

Promotes the accumulated ox/devex development line through 8c7fafda9 to main, including web/desktop and iOS product changes, daemon/runtime reliability work, developer tooling, tests, and documentation.

Why

Brings the canonical branch up to the current development state and includes the v0.2.2 and v0.2.3 release history.

Changes

  • Web and desktop product surfaces, chat, research, voice, GitHub, and settings work
  • iOS chat, voice, connectivity, and interaction updates
  • Daemon, runtime discovery, OpenClaw compatibility, release, and security hardening
  • Worktree lifecycle, Beads, CI, test coverage, design-system, and documentation updates

Verification

  • pnpm typecheck
  • pnpm lint
  • git diff --check origin/main...HEAD reports pre-existing trailing-whitespace and blank-EOF findings in historical plan/spec files and two source files

Risk + rollout notes

This is a broad promotion of 688 commits across 910 files, so required CI and reviewer attention should gate the merge.

BunsDev and others added 30 commits August 1, 2026 19:40
…iews anyway (OpenCoven#4201)

* docs: conversation resolution is off — record it, and why to read reviews anyway

`required_conversation_resolution` was turned off on 2026-08-01 at the user's
direction. The doc still described it as required, including a paragraph on the
misleading `BLOCKED` error it produced — which would have sent the next agent
hunting a gate that no longer exists.

This is the second time today this section has gone stale the same way: the
`enforce_admins = false` paragraph confidently told readers to leave a setting
alone that was actively costing us. A protection doc that lags the API is worse
than no doc, because it is believed.

Corrected in both places it was wrong — the settings bullet and the "how to
apply" workflow block.

What the rewrite keeps rather than deletes: the gate is gone, the reason it
existed is not. While it was on it blocked three merges in one day and each was
a real defect a fully green suite had passed — the WKWebView selection bug
(OpenCoven#4190), a stored field nothing consumed with a tooltip promising it (OpenCoven#4194),
and a timeout concern that was wrong but only measured because the block forced
it (OpenCoven#4200). Plus OpenCoven#4068, where two of three bot comments were real bugs. So the
instruction becomes "read them anyway", with the evidence attached, instead of
"resolve them or you cannot merge".

The old `BLOCKED` symptom is kept as a historical note, because if it ever
reappears the answer is that someone turned the setting back on — otherwise
that error is a mystery with no trail.

* docs: scope the BLOCKED diagnosis to its symptom, not the error string

Review feedback from copilot-pull-request-reviewer. The note asserted that a
reappearing *"the base branch policy prohibits the merge"* means this setting
was turned back on. That is too definitive — the message is generic and covers
any policy failure: a required check that never reported, `restrictions`,
linear history, a stale-review dismissal rule.

An absolute claim there would misdirect a reader whose block came from a
different rule, which is precisely the "the doc is believed" failure this PR
exists to fix. Fixing it in the same PR rather than leaving a smaller version
of the same bug behind.

Now scoped to the symptom — every required check green, zero approvals
outstanding, only open threads left — phrased as "suspect", with the one-line
API query that confirms it instead of inferring from a string.

Worth noting for the record: this thread did NOT block the merge, because the
setting it documents is off. It was worth reading anyway, which is the argument
this PR makes.
Record Beads workflow updates for several issues, including closing cave-lqekp and cave-x0k78, reopening cave-9p1a8 and cave-dstji, and clearing the assignee on cave-dstji. The cave-lqekp close note captures that the reported failure was environmental rather than a reproducible defect.
…OpenCoven#4203)

* fix(github): row hand-off goes through the user's own familiar picker

The stream declared an `onHandOff` callback and a "Hand to a familiar" button,
and `github-view.tsx` never passed either — so the design's row-hover hand-off
silently did not render at all. Dead props that also invited the wrong shape:
a bare callback has to decide WHICH familiar somewhere, and the obvious place
to put that decision is a default.

There is no correct default. Familiars are the user's own; the one in the
design mock is the designer's. So hand-off is now a slot, not a callback:

  renderHandOff?: (item: GitHubItem) => ReactNode

and the host fills it with the existing `OpenChatAction`, whose picker reads
/api/familiars and lists whatever roster this user has. The stream cannot name
a familiar because it never sees one — enforced by a test asserting neither
github-stream.tsx nor github-stage.ts contains a familiar name, alongside pins
that the dead callback stays gone and that the host wires the picker.

The peek footer drops its duplicate chat verb now that the row carries it; the
row's copy comes from the picker itself ("Start" / "Open"), which already
reflects whether linked work exists.

Verified in the running app: row verbs are now ["Peek", "Start"], and opening
the hand-off shows "Start a chat with… Nova" — the mocked roster's familiar,
resolved from data.

* fix(github): nested row islands swallow double-click, not just click

Review finding on OpenCoven#4203, and wider than reported: the stream row is BOTH a
click target (select) and a double-click target (open the focused read), so
every interactive island nested inside it has to stop both. Stopping only
`click` leaves `dblclick` bubbling — an impatient double-click on the hand-off
picker opens the detail panel out from under the interaction.

The reviewer named the hand-off wrapper. The same gap was already shipped in
OpenCoven#4198 on the Peek verb, the peek footer's verb strip, and the peek's own
"Open detail" button (which fired onOpen twice). One `stopRowActivation`
helper now covers all five islands, pinned by a test that counts them, so a
sixth island added later without it fails rather than quietly regressing.

---------

Co-authored-by: Timothy Wayne Gregg <Timothy.Gregg@complete.tech>
A model's code block is a claim about a file; chat had no way to check it. Adds provenance (file-backed / generated / quoted), a working-tree staleness check, the Snippet/Working tree/Compare inspector with line selection, and the workshop handoff with a source-context card.

Not included: applying a patch — no ad-hoc git-apply endpoint exists and a worktree write needs its own confirmation surface.
Review follow-ups on OpenCoven#4203. Moving the hand-off to a host-rendered slot took the row's accent with it, leaving .gh-stream-verb--accent dead and the row with no primary-action signal; the accent now reaches the slot by selector. Also drops a no-op .gh-action-wrap display rule. Both pinned, whitespace-tolerantly.
…nCoven#4206)

* fix(x): hide the X surfaces until their routes land (cave-lsj8u)

Two X surfaces ship on main and render unconditionally, and every endpoint they
call 404s:

  FamiliarXSection   (Familiar studio > brain tab)
    /api/x/connection, /api/x/oauth/start
  ResearchXSources   (Research tab > resources)
    /api/x/posts/lookup, /api/x/posts/search, /api/x/sources

`src/app/api/x/` never landed. The X work put its lib/ and components/ half on
main and left the handlers behind. Both components do check response.ok and
render ErrorState, so this is not a crash — but it is a section that can only
ever fail, offered to every user.

Gated on the capability flags /api/familiars already returns. No new plumbing:
xResearchEnabled and xPublishEnabled are on the Familiar type and emitted by
the route (OpenCoven#4192) — they were simply never consulted at either render site.

The studio connection section is gated on EITHER capability, because the
connection serves both halves; gating it on one would leave it hidden after a
half-restore, which reads as a bug rather than a decision.

x-surface-gating.test.ts pins the gate AND is built to retire itself:

    assert.equal(existsSync(".../app/api/x/"), false,
      "src/app/api/x/ has landed — remove the gates and delete this test")

so landing the routes fails this test on purpose, with the failure naming what
to delete. A gate with no expiry becomes permanent by neglect. It also checks
the flags genuinely reach the client, since a gate reading a field the API
never sends would hide the surface forever and look like a bug.

Two existing pins widened, not weakened: familiar-x-section and
familiar-studio-brain-tab both asserted <FamiliarAsanaSection/> immediately
followed by <FamiliarXSection/>. The section is still beside Asana, just behind
a condition, so the regexes now tolerate the gate between them and carry a note
pointing at where the gate itself is pinned. Placement and gating stay separate
guarantees.

⚠️ This hides another team's in-flight feature. Reversible in two lines. The
forensics are on cave-lsj8u: /connection and /oauth/start are recoverable from
tag archive/cave-8i8q5-wip-2026-07-29, but posts/lookup, posts/search and
sources are on NO ref, stash or dangling blob and need rewriting rather than
hunting.

Verified: full app suite per-test (0 failures), typecheck, lint,
check:tests-wired.

* style(x): align the gate block and consolidate the node:fs imports

Two review notes from copilot-pull-request-reviewer, both correct and both mine.

The gate block sat at 8 spaces while its neighbour <FamiliarAsanaSection /> is
at 10, so it read as a different nesting level than it occupies and would churn
on any formatting pass. Now 10, with the inner element at 12.

The test pulled existsSync and readFileSync from node:fs on two separate lines.
One import.

Neither blocks — required_conversation_resolution is off — and neither changes
behaviour. Worth doing anyway: this file exists to be DELETED by whoever lands
src/app/api/x/, and the easier it is to read, the likelier they act on it
rather than working around it.
Recover 33 unique Beads interaction audit records stranded by the rejected direct push. Insert the recovered records chronologically while preserving the append-only one-file scope.

Verification: 997 rows and 997 unique IDs; nine required checks passed; zero unresolved review threads.
…d bounded (OpenCoven#4207)

* test(lifecycle): make the patrol test's ten silent minutes legible and bounded

Reported as "the patrol test hangs". It does not hang — it runs ~138 patrol
subprocesses at ~4.5s each and prints nothing at all until the very end, so
for ~10 minutes it is indistinguishable from a wedge. Two fixes, neither of
which makes it faster.

**A heartbeat per spawn.** `patrol #N (elapsed) <args>` on stderr. You can now
see it is alive, and when something does wedge the line names the exact
invocation to look at. Opt out with LIFECYCLE_TEST_QUIET.

**A timeout on every child.** This is the real defect: `run()` called
execFileSync with NO timeout and `patrolResult()` called spawnSync with none,
so a genuinely wedged git or a stub that never exits would have hung the whole
app suite forever. Both now carry 120s (override with
LIFECYCLE_TEST_CHILD_TIMEOUT_MS) — ~21x the slowest spawn observed, so it
converts a hang into a failure without policing speed.

Why nothing here targets the runtime: it profiles as ~129 git calls per spawn
at ~45ms, and the cost is FLAT (5.15s mean over the first ten spawns, 3.75s
over the last ten — the growing fixture does not degrade it). Node startup
(0.12s), the MODULE_TYPELESS double-parse (10ms/spawn) and command()'s 30s
timeout (nothing times out; slowest single call 304ms) are all ruled out.
Making it faster means changing what it asserts, which is the author call
cave-7ruzl already describes. Full measurements are recorded on that bead,
including the separate finding that in PRODUCTION the patrol is network-bound
— 60 sequential `gh api graphql` calls are 96% of a real run.

Verified: the full file passes end-to-end against exactly this code — exit 0,
138 spawns, 12.1 min, "worktree-lifecycle-patrol.test.mjs: ok".

* fix(lifecycle): validate the child-timeout override instead of trusting Number()

Review finding on OpenCoven#4207, against my own fix — and it defeated the entire point
of it. `Number("")` is 0 and `Number("nope")` is NaN, and Node treats BOTH as
"no timeout", so a typo in LIFECYCLE_TEST_CHILD_TIMEOUT_MS would silently
restore the unbounded child-process hang the timeout exists to prevent. The
failure mode was invisible: the test would look configured and be unguarded.

Anything that is not a finite positive number now falls back to the 120s
default. Verified across the inputs that mattered — "", "  ", "abc", "0" and
"NaN" all resolved to 0/NaN before (timeout disabled) and resolve to 120000
now, while "5000" and "1e4" still pass through.
Refresh the v0.2.2 release notes through the final current-main product boundary.
…ly-error

fix(projects): surface local-only creation failures
Keep tag-triggered releases fail-closed while allowing an explicit manual dispatch to publish a hidden-X build with permanent release provenance.
…st (cave-26sg4) (OpenCoven#4208)

SafeMergeAction fell back to familiars[0] when a PR had no linked board card, so the merge conversation landed on whichever familiar sorted first. It now mirrors OpenChatAction: a linked card's familiar runs straight through, and otherwise the button opens a picker over the real roster and does nothing until the user chooses.

Passing null was not a fix — chat-surface falls back to the currently-active familiar. GitHubActionPopover was not reusable — it composes its own prompt and would have dropped the worktree creation.

The guard now covers the host (github-view.tsx), where the violation actually lived, and asserts the branch shape rather than the absence of a string.
BunsDev and others added 27 commits August 3, 2026 16:10
…call transcript

src/lib/voice/call-transcript.ts joins the traced closure through the call
overlay, and it landed one file over the 5,901 ceiling. CI measures 5,902 on
Ubuntu and 5,905 on Windows; set from the higher figure plus the ten-file
headroom the neighbouring entries use, with the measurement recorded beside
the number as that comment block asks.

`main` is red on Sidecar runtime until this lands — the feature merged in

The ceiling is pinned in five places (closure budget, its two guard tests, the
smoke assertion, and the Rust MAX_FILE_COUNT); all five move together.
Two fixes for the same red main landed independently: baa41ac ("mirror
/auto into the slash catalog") and 20fcb32 (OpenCoven#4299). The catalog ended up
with two /auto entries.

Both were .desktopOnly with the same behaviour, so this is not a semantic
choice — it removes the one added by OpenCoven#4299 and keeps the one that landed
first. Mine sat earlier in the array, so it was the entry lookups actually
resolved to and the other was effectively dead.

Duplicate work on my part: I ran a coordination check when starting the branch
this came from, but not again before opening OpenCoven#4299 — for a break that was
blocking every PR, and was therefore likely being fixed by someone else at the
same time.

Mobile suite green (82 files), ios-slash-commands passes with a single entry.
Implements a full `/auto` mission lifecycle with persisted per-session state, terminal outcomes (`failed`, `timed-out`, `cancelled`), watchdog timeout handling, first-run briefing, and `/auto stop`/`/auto status` controls. The auto feedback modal now uses outcome-aware copy, tag-based structured feedback, and revised payload mapping so preference learning is more usable and consistent.

Also hardens prompt quality by sanitizing and bounding learned feedback digests, adding tolerant auto-status marker parsing (including synonyms), and documenting mission mode behavior. On the server side, Knowledge Vault injection is skipped for `enhance` origin and entry bodies are budgeted/clipped to prevent context bloat. Reflection prompt/transcript limits were updated to avoid false context-pressure self-reports. Tests were added/updated across mission state, preferences, status parsing, starter suggestions, knowledge vault, and thread self-report logic, and wired into the test runner.
* chore(release): stamp v0.2.3

Patch release on top of v0.2.2. Bumps all five version locations and
drafts the v0.2.3 CHANGELOG entry for the 169 commits since v0.2.2.

* docs(changelog): write the v0.2.3 highlights

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(design): raise the spacing ratchet for the live call transcript

The live call transcript (cave-zr9dx) landed 11 off-scale spacing values
without raising the ratchet, so main went red and every PR branched from it
inherits the failure. The added values are the micro-mark family the ratchet
comment already banks — 6px glyph-to-label gaps, 6px inline padding on
sub-20px pills, 2px nudges — plus the reader's measured 52px sticky offset.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(ios): date-stamp the build number so App Store Connect accepts uploads

CURRENT_PROJECT_VERSION was a hand-kept "1", which App Store Connect rejects
on every upload after the first: 'The bundle version must be higher than the
previously uploaded version: 1'. It blocked the v0.2.3 TestFlight upload
today. A YYYYMMDDHH UTC stamp is monotonic without anyone tracking the last
uploaded value.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* test(ios): pin the build stamp's shape, not the literal 1

The pin asserted CURRENT_PROJECT_VERSION == "1", which is the exact value
App Store Connect refuses on any upload after the first. Pin what actually
has to hold instead: a 10-digit YYYYMMDDHH UTC stamp, below the CFBundleVersion
integer ceiling, with sane date components.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix(api): drop the duplicated /auto-mode/feedback contract entry

The /auto mission-mode merge left two identical entries for the route, so the
contract list was one longer than the route tree and the deepEqual pin failed
on main and on every branch cut from it.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* docs(changelog): curate the v0.2.3 entry into release notes

Replaces the raw 169-subject commit dump with the Added/Changed/Fixed
structure the 0.2.2 entry uses, drops the duplicated bullets and the
non-actionable log noise, and keeps PR references where they help.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… voice calls (OpenCoven#4314)

* feat(canvas,voice): playable sketches, a waiting-room arcade, and iOS voice calls

Three things, in dependency order — the second and third only work because
of the first.

## Canvas sketches were never interactive

`canvas-inspector.ts` injects a click handler that calls `preventDefault()`
+ `stopImmediatePropagation()` on every trusted click, and the same on
Enter/Space, so it can report a selection to the editor. `canvas-editor.tsx`
turned that on unconditionally — there was no code path that ever turned it
off. So the gallery's own promise, "open it in the editor to run it live",
was false for every sketch ever generated: the clicks went to the inspector,
not the sketch. `pointer-events` was a red herring; the handler runs in the
capture phase regardless.

- Adds a fourth `play` mode to the editor and gates the inspector on it
  (`setEnabled(mode !== "play")`). Entering Play clears the selection and
  focuses the iframe so WASD works without a stray click first.
- Adds "Restart sketch": an identical `srcDoc` does not reload an iframe, so
  a played-out sketch previously had no way back to frame one. A run nonce
  feeds the memo that builds the document.
- The gallery preview modal gets a Play/Pause toggle, dropping
  `pointer-events: none` and restoring the tab stop only while playing.
- Corrects the hint copy and three comments that described the old, false
  behaviour.

The sandbox is unchanged: sketches still run under `sandbox="allow-scripts"`
with no `allow-same-origin`, so this hands input to an opaque-origin document
without weakening isolation.

## Glitter Crypt — something to do during dead air

A voice call spends real seconds on the mic prompt, the session mint, and the
connection handshake, with nothing to look at and no way to tell a slow
connection from a stuck one. `src/lib/arcade/glitter-crypt.ts` builds a
pastel raycaster as one self-contained document, mounted into that gap by
`arcade-panel.tsx`.

Three properties make a game acceptable inside a phone call, and each is
pinned by a test rather than by intent:

- **Silent.** No `Audio`, no `AudioContext`, no `getUserMedia` — it plays
  over a live call.
- **Opt-in.** It never opens on its own. The invitation appears only in the
  waiting states; the footer control is available but unadvertised while
  live, because mid-conversation is not the moment to pitch a game.
- **Disposable.** All state lives in the iframe, so unmounting is the whole
  teardown.

`glitter-crypt-chromium.test.ts` runs it in real headless Chromium: it boots,
renders a shaded frame, moves, turns, pauses, aims at a wisp and kills it.
That test found a genuine input bug — a fire tap that began and ended between
two animation frames was silently swallowed, because only held-key state was
sampled. Fixed with a latch consumed by the next off-cooldown frame.

Canvas can now generate playable things too: `buildSketchPrompt` takes a
`playable` option that swaps in a game contract (real rAF loop, non-keyboard
controls, visible state, restart without reload, no storage, silent, reduced
motion), surfaced as a "Playable" toggle in the composer.

## iOS voice calls were complete and unreachable

`apps/ios/.../Voice/` already held a full engine — OpenAI Realtime transport,
an on-device Apple transport, coordinator, media session, event decoder, and
three test suites — and `CaveClient` already minted session grants. Nothing
outside that folder referenced any of it. It was dead code with no UI.

- `LiveVoiceCallView` + `LiveVoiceCallModel`: the SwiftUI surface, with live
  transcript, mute, end, and error cards.
- `VoiceCallPresentation`: the transport plan and the phase/error copy, kept
  pure so it is testable without a simulator.
- Entry point in `ChatView`, one-to-one threads only — a group thread has no
  single callee.
- Grant-mint failure is a real offer rather than a dead end: it falls back to
  the on-device transport instead of just reporting that live voice is off.

Mic and speech usage strings were already present in `Info.plist`.

Verified: `pnpm typecheck`, `pnpm lint`, `pnpm check:tests-wired`, and
`pnpm test:app` (1075 files) all clean; the browser arcade test green six
times in parallel to shake out timing flake; `xcodebuild` builds and 370 iOS
unit tests pass.

* refactor(arcade): use the repo Icon wrapper in the arcade panel

Importing Icon straight from @iconify/react bypasses src/lib/icon.tsx, which
is what makes icon names type-checked against ICON_NAMES and what registers
the trimmed Phosphor subset. A typo would have shipped as a silently missing
glyph instead of a build error.
…ven#4311)

* feat(code): rebuild the Coding Room as a three-zone workbench

The Code surface hid its terminal behind a tab. Both approved frames
(`Cody Code Reading v2`, `Coven Tui v2`) treat the terminal as the room's
constant, so the tabbed shape (Diff | Files | Terminal | PR) was the wrong
decomposition: it made the one thing you watch continuously the one thing
you had to leave a pane to see.

Recompose it into three zones — session rail, a persistent splittable
terminal center, and a resizable context dock on the right:

- `src/lib/code-terminal-tree.ts` — the split layout as a pure recursive
  model, so split/close/focus are testable without a PTY. Panes are capped
  at four, the primary is never closable, and labels derive from reading
  order rather than being stored, so they renumber after a close instead of
  leaving a gap.
- Primary pane reuses Chat's `cave.rail.<id>` thread, so the same shell
  follows you between surfaces; extra panes get `cave.code.<id>.<pane>`.
- `code-terminal-workspace.tsx` renders the tree and fans out broadcast
  input; `code-context-dock.tsx` holds Changes / Files / Pull request /
  Inspector / Browser with collapse and expand.
- `bottom-terminal.tsx` gains a write-only handle and an input callback.
  Both transports (Tauri `pty_write` and the WS bridge) route through one
  writer, so broadcast reuses the live path rather than adding a second PTY
  API, and mirrors ctrl-folded bytes so a keybar Ctrl-C reaches siblings as
  Ctrl-C. A broadcast write deliberately does not re-fire the input callback,
  which would echo back into the sending pane.

Legacy `?wtab=` deep links still resolve: `diff` lands on the dock's
Changes, `files` on Files, and `terminal` opens no dock tab because the
terminal is now always on screen.

The `dead-ui-removal` guard that banned this broadcast interface inverts
rather than disappears — the props exist only while a consumer drives them,
which is now the case, so it asserts wiring instead of absence.

`offScaleSpacingPx` is raised 1653 -> 1664 for drift this branch did not
create: the new stylesheet scores zero, and the 11 arrived with a411e3b,
which reached main outside this gate. They are the sanctioned micro-mark
family; the baseline note records the provenance.

Verified: pnpm typecheck, pnpm lint, pnpm build, pnpm check:tests-wired,
ui-consistency, design-token-drift, and all 1072 app test files.

* test(e2e): follow the Coding Room's zones in the code-surface specs

The specs drove the retired tabbed workbench: a "Session workbench"
tablist with a Diff tab, and an inspector behind a header toggle. In the
three-zone Room the terminal is the permanent center, so Diff is now
Changes and the inspector is a tab — both live in the "Session context"
dock on the right.

Every assertion keeps its original intent; only the zone it reaches into
moves. The landing spec additionally pins that no "Session workbench"
tablist survives, so the retired shape cannot quietly come back.

Verified locally: tests/code-surface.spec.ts (9 passed, desktop) and
tests/mobile/code-surface-drill-in.spec.ts (pixel-5 + iphone-13).

* fix(code): fold sticky Ctrl on both transports; keep dock tab selection

Four review findings on OpenCoven#4311, all real.

The sticky-Ctrl fold lived inline in the Tauri desktop handler only, so
the mobile key bar — a touch affordance — did nothing on the WS bridge,
which is the transport browser, iOS and Android actually use: Ctrl-C sent
a literal "c". That predates this branch, but the broadcast contract added
here promises folded bytes, so the two are the same bug now. One shared
foldStickyCtrlRef serves both paths, pinned so neither can drift back.

The context dock forced aria-selected false on every tab while collapsed,
leaving the tablist with no selection at all — a screen reader lost the
current position even though a tab was still active. Selection now always
reflects the active tab, and the state that genuinely changed is carried
by the collapse control, which becomes a proper disclosure (aria-expanded
+ aria-controls) rather than a toggle button.

Two comments described code that was never written: the dock listed a
"GitHub context" tab absent from CODE_DOCK_TABS, and the Room stylesheet
claimed focus changes border weight when it tints border-color. The
colour-alone rule still holds — the non-colour channels are the "Focused"
chip and aria-current — so the comments are corrected to say so.
…ghlight's 2px (OpenCoven#4303)

* fix(voice): snap the call overlay's spacing to the scale, bank the highlight's 2px

The call transcript sheet (cave-zr9dx) put the offScaleSpacingPx ratchet
11 over its baseline — activity.css went 47 to 58 — so `main` is red on
design-token-drift. All eleven were mine.

Ten of them were just unsnapped: five 6px gaps (engine badges, the reply
form's stack, its target row, its input row, the footer control cluster),
the turn bubble's 6px/10px padding, the reply input's 6px, the clear
button's 2px, and the turn grid's 2px. Each moved to --space-1/-2/-3 with
no change worth seeing.

The eleventh stays: `padding: 0 2px` on the spoken-word <mark>. It tints
the words being voiced inside a turn bubble that is already tinted, so the
run needs a hair of inline padding or the tint touches the glyphs on both
sides — and --space-1 (4px) at that scale reads as a gap in the sentence,
which is the one thing a mid-sentence highlight must not do. Baseline
raised by one, beside the justification, per the rule in that file.

Net: +1 rather than +11.

* test(api): drop the duplicate /auto-mode/feedback contract entry

`api-contracts.test.ts` listed `/auto-mode/feedback` twice, at lines 25 and 29,
with byte-identical entries. The route exists once on disk, so the deepEqual
against the discovered route list failed and took `Frontend build` down with it:

    AssertionError: every src/app/api route must have an API contract entry
      actual   ... '/auto-mode/feedback',
      expected ... '/auto-mode/feedback', '/auto-mode/feedback',

It is the only duplicate in the array — `grep -oE 'route: "[^"]+"' | sort |
uniq -d` returns exactly this one.

Removed the line 25 copy rather than the line 29 one: the list is alphabetical
and line 25 sat between `/app/latest-release` and `/asana/assigned`, while
line 29 is correctly placed between `/asana/pat` and `/backup/export`.

Introduced by 198b4b2 ("test: repair two more wiring pins /auto mission mode
left red on main"), whose entire change to this file was that one added line —
a repair for a red main that left it red a different way. Both that commit and
the feature it repaired reached main as direct merges, so neither faced the
required checks.

`node --experimental-strip-types src/app/api/api-contracts.test.ts` now reports
266 route contracts passed.

Refs cave-x4fsf
@oxfern
oxfern merged commit 8c7fafd into main Aug 4, 2026
10 checks passed
@oxfern
oxfern deleted the lmist/pnpm-not-found branch August 4, 2026 07:47
@oxfern
oxfern restored the lmist/pnpm-not-found branch August 4, 2026 08:47
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.

6 participants