From 9630eaf855a3aa042cd96a7db5639112233e214f Mon Sep 17 00:00:00 2001 From: Destin Date: Tue, 1 Sep 2026 13:57:02 -0700 Subject: [PATCH 1/9] =?UTF-8?q?roadmap:=20index=20skeleton,=2014=20area=20?= =?UTF-8?q?files,=20shipped.md=20=E2=80=94=20empty,=20tool=20passes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First commit of the migration (spec §6.1.2). The old single-file roadmap is the parent commit of this one; the last single-file commit on master is 315586b2f652cd9e26643611a0a73b1f64854117. shipped.md carries every [x] item from the old file (168, not the 150 the spec estimated): only 19 sat under the old '## Shipped' heading — the other 149 had been flipped in place inside Bugs/Features/release sections, so the plan's sed over the Shipped section alone would have dropped them. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_011TfjZg26EBtRFEZeZHGceQ --- ROADMAP.md | 1503 +---------------------- docs/roadmap/android-only.md | 4 + docs/roadmap/chat-data.md | 4 + docs/roadmap/claude-code-integration.md | 5 + docs/roadmap/dev-workspace.md | 11 + docs/roadmap/files.md | 5 + docs/roadmap/games.md | 2 + docs/roadmap/local-models.md | 3 + docs/roadmap/marketplace.md | 9 + docs/roadmap/native-harness.md | 16 + docs/roadmap/other-features.md | 11 + docs/roadmap/remote-access.md | 2 + docs/roadmap/shipped.md | 476 +++++++ docs/roadmap/sync.md | 2 + docs/roadmap/themes.md | 3 + docs/roadmap/user-interface.md | 3 + 16 files changed, 595 insertions(+), 1464 deletions(-) create mode 100644 docs/roadmap/android-only.md create mode 100644 docs/roadmap/chat-data.md create mode 100644 docs/roadmap/claude-code-integration.md create mode 100644 docs/roadmap/dev-workspace.md create mode 100644 docs/roadmap/files.md create mode 100644 docs/roadmap/games.md create mode 100644 docs/roadmap/local-models.md create mode 100644 docs/roadmap/marketplace.md create mode 100644 docs/roadmap/native-harness.md create mode 100644 docs/roadmap/other-features.md create mode 100644 docs/roadmap/remote-access.md create mode 100644 docs/roadmap/shipped.md create mode 100644 docs/roadmap/sync.md create mode 100644 docs/roadmap/themes.md create mode 100644 docs/roadmap/user-interface.md diff --git a/ROADMAP.md b/ROADMAP.md index 6f96892a..cdc5f21e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,1464 +1,39 @@ -# YouCoded Roadmap - -Single planning surface for the whole product (app + registries + worker + plugins). -Format: checkbox items with backtick tokens — type (`bug`|`feature`|`idea`, default `feature`), -milestone (`vX.Y.Z`), tags (`#kebab-case`, same vocabulary as custom session tags), -issue link (`repo#N`), `(added YYYY-MM-DD)`. Section headers pass their tokens down; -an item's own tokens win. Unknown tokens degrade to tags. `[x]` = shipped (note the -commit/PR in the detail line) — shipped items collect in ## Shipped. -Rolling cleanup (**NEVER EXECUTED as of 2026-08-26 — `docs/archive/roadmap-shipped.md` does not exist and ## Shipped still holds 14 entries dating back to 2026-04-29, four releases of accumulation; this convention needs an owner or an automated step, or it should be dropped rather than left as an unkept promise**): at each release, move ## Shipped entries older than the previous -release to `docs/archive/roadmap-shipped.md` — the ROADMAP is a live planning -surface, not a history. - -## v1.3 — sync release - -- [ ] Ship v1.3: all master content + desktop-only sync `feature` (added 2026-07-15) - Gated on sync being entirely complete (incl. Phase 2 conversation sync). Plan 2c (legacy demolition) + the Backup & Sync popup redesign MERGED 2026-07-15 (youcoded PR #126, merge `0a91850e`); Task 10 (dead legacy backup repo `destin-claude-config`) deleted. Remaining gates: ~~(1) native-session safety fixes~~ ✅ **DONE 2026-07-19** (youcoded#177; the full native sync *parity* work was descoped to v1.3.1 on 2026-07-18 — **then pulled back into v1.3.0 and SHIPPED 2026-07-23 as M2**, youcoded#212 merge `60d56a67`); ~~(2) two-device dogfood~~ ✅ **DONE 2026-07-30** (Destin's confirmation — see the line below); (3) Connect-GitHub live sign-in — ⚠️ **still open, and the gate has changed shape.** PR #122 merged 2026-07-14 (`6910efbc`), so its "do not auto-merge until the walkthrough" wording is moot. Sync no longer uses `gh` for credentials at all (PR #201/#202/#203, 2026-07-22 — `git grep "'gh'" origin/master -- desktop/src/main/sync-spaces/` returns nothing). **But working sync is NOT proof the gate passed** — `github-client.ts:378-380` reports `authed: gh.authed || cs.connected`, so a machine with a pre-existing terminal `gh auth login` (Destin's Z13 has had one since before #122) syncs fine without the in-app modal ever running. **The precise confirmation needed: in the live app, Account → Connected accounts — does the GitHub row show a login signed in from INSIDE YouCoded? Yes → gate closed. Empty or terminal-only → the gate never passed.**; (4) release mechanics — `/audit` (**125 days stale as of 2026-08-26**; the session-start staleness reminder that should have caught this is itself broken — see ## Bugs; the last real audit run is `docs/audits/2026-04-23.md` — the two `2026-07-15-*` files are a changelog and a baseline, not audit runs), version bumps (disk is still 1.2.4 on both platforms; the handoff's "versionCode 17→18" is stale, it's already at 20), CHANGELOG 1.3.0 entry, tag. Status: docs/active/handoffs/2026-07-10-sync-completion-handoff.md. -- [ ] **Terminal text stops two-thirds across the pane** — reproduced 2026-08-27 in a dev instance (xvfb 1440×900, `scripts/ui-review/plans/electron-live-session.json`, evidence `scratch/ui-phase-d-electron/shots-electron-live-session/app/e2-terminal-view.png`): Claude Code's TUI and input line wrap at ~950 px while the pane is 1440 wide, so the PTY never learned the real column count. Hypothesis from the P-20 rig: the mount-time fit is skipped while the terminal grid is 0×0 behind the chat view, and the resize after the first real fit does not reach the PTY (dedup/debounce in `TerminalView.tsx` ~:225–290). Destin has not seen it in his own app; check whether a maximized-at-launch window avoids it `bug` `#terminal` (added 2026-08-27, was ledger P-20.1) -- [x] Theme editor "Terminal Opacity" slider still goes down to 30% on a wallpaper theme, where the P-20.2 floor (80%) makes anything below that a no-op — clamp the slider's minimum to 0.8 under wallpaper/gradient themes, or say so beside it `bug` `#ui` `#themes` (added 2026-08-27; `ThemeScreen.tsx` ~:566) **DONE 2026-09-01** — youcoded PR #376 — slider floors at 80% under wallpaper/gradient themes and shows the effective value. -- [x] `vitest.config.ts` gives every worktree the SAME throwaway HOME (`os.tmpdir()/youcoded-vitest-home`), so two sessions' suites collide (ENOTEMPTY, flaky main-process tests — `native-session-host`, `mcp-startup-wiring`, `web-fetch-tool` timing) — make it per-worktree/per-process `bug` `#tests` (added 2026-08-27; three verify.sh runs flaked this way in one afternoon, each test green alone; **FIXED 2026-08-28 — youcoded PR #362**: the sandbox is now pid-suffixed, so concurrent runs cannot share state at all. Verified the config is evaluated exactly ONCE per run, in the vitest main process, and that `test.env` is what carries HOME into the workers — so globalSetup and every worker agree on the path without re-deriving it. Pinned by two new cases in `home-isolation.test.ts`.) - **Evidence toward gate (2) — 2026-07-20: three-platform sync verified against the remote, not just the UI.** `youcoded-sync-personal` now carries three device records — `destinsZ13` (linux, 01:59), `GalaxyBook` (win32, 07-17 17:34), `Destins-iMac-Pro.local` (darwin, 02:00) — with the darwin record created by a fresh Intel macOS VM running `1.3.0-beta.8` enrolling from zero and hydrating the space (3110 blobs / 3074 conversations / 1 project). Verified via the GitHub API rather than the panel, which matters because the same session proved the panel reports green without having synced (see the two `#sync` bugs under ## Bugs). **Gate NOT flipped — Destin's call:** the darwin leg is a VM, and the first-run required a manual `gh` install (Xcode CLT ships `git`, not `gh`), so "works after a developer-shaped prerequisite" is not the same as the clean-install flow this gate is presumably about. That prerequisite is itself unresolved: nothing surfaces the missing `gh` to the user. - **Gate (2) CLOSED — 2026-07-30, on Destin's confirmation.** The pass ran on `1.3.0-beta.9` (cut from master after M2, CI run 30129579245) installed on BOTH the Z13 (pacman → /opt/YouCoded) and the Intel macOS VM, covering native + CC conversations, tags/notes round-trip, cross-device resume with the model picker, live takeover and its three failure dialogs, and the remote web client. **What closed it is Destin's word, not a written result** — no per-item checklist output was recorded in the workspace, so if a later question turns on "was X specifically exercised", re-run that leg rather than reading this line as evidence of it. Gates (3) and (4) are untouched — sign-in confirmation still owed by Destin, and release mechanics (`/audit`, version bumps incl. Android `versionCode`/`versionName` (disk = 20 / 1.2.4), CHANGELOG 1.3.0 entry, tag) are the last step. - -- [x] Native-session safety fixes (the v1.3 gate) `bug` `#native-runtime` `#sync` (added 2026-07-18, **SHIPPED 2026-07-19 — youcoded#177, merge `fe8529ba`**) - **Descoped from full parity by Destin on 2026-07-18** after the design review found that cross-device native *resume* is blocked by a design question the plumbing can't answer (see the v1.3.1 item). v1.3 ships correctness only — three fixes, all independently reviewable: - 1. **Orphaned harness (investigation Break 4).** `nativeHost.resume()` never destroys an existing live entry, and `takeover.ts` + `session-exit` skip `nativeHost.destroy` entirely — so two `HarnessSession`s can append to one native JSONL, violating the single-writer invariant at `native-home.ts:5-7`. Data corruption; the load-bearing fix is in `resume()`, not the callers. - 2. **Phantom `claude/` records (found 2026-07-18 in review, NOT in the investigation).** `SESSION_SET_FLAG`'s phantom-record gate (`ipc-handlers.ts:2374`) keys off `sessionIdMap.has()`; PR #176's `sessionIdMap.set` for native defeated it, so flagging/noting a live native session seeds a mislabeled `provider:'claude'` record with blank metadata that syncs everywhere and is never pruned. **Confirmed on disk** (`~/YouCoded/Personal/Conversations/claude/e0a23b35-….json`). Needs the gate fixed *and* a cleanup pass for records already written. - 3. **Gate the native lease acquire** (investigation Option A) so the app stops offering a takeover it cannot perform. - Spec: docs/archive/specs/2026-07-18-native-sync-parity-design.md (archived shipped 2026-07-23 — its §§4-6 parity work landed in v1.3.0 via M2, not v1.3.1). - -## v1.3.1 — Android + polish `v1.3.1` - -- [ ] **Native Runtime Parity Program** — full CC/native session parity: control, sync/tags/takeover, skills/commands, status UX, permissions, model intelligence, subagents, Android, onboarding `feature` `#native-runtime` `#sync` (added 2026-07-22, consolidates 22 prior entries) - **THE single doc: `docs/active/specs/2026-09-01-agent-platform-vision-and-state.md`** — §4 is the shipped end state, §5.1 the ordered remaining parity work, §7 how it sits against the capability track, §9 the decisions owed. Consolidated 2026-09-01 from the 2026-08-11 program (now `docs/archive/plans/2026-08-11-native-sessions-remaining-work.md`) and the 2026-07-09 vision spec; both are archived and kept for history only. **Shipped:** M1 session control (youcoded#204, 2026-07-22) · M2 conversations/sync (#212, 2026-07-23 — landed in v1.3.0 because it closes the sync gate) · M3 skills/commands/rules + Plan C (#268, 2026-07-29) and MCP phase 1 (#280, 2026-08-05) · the three dogfood fixes (#287, 2026-08-10) · instruction outlining (#289, merged `4bb760ff` 2026-08-11) and M4's reliability tranche (#290, merged `9a2d8af7` 2026-08-11) · native image delivery (#293, merged `f65fed18` 2026-08-11). **M1–M3 dogfood CLOSED 2026-08-10 on Destin's sign-off** ("mostly pass") — closed by his word, not a recorded per-surface checklist, so re-run a leg rather than assuming it was exercised. **M5 permissions CLOSED 2026-08-13** — 2a the management UI (#311, plus #312) so every "Always allow" can be listed and revoked with the revocation reaching live sessions; 2b Full Auto prompt coherence (#313); 2c Bash always-allow rule shape (#314, merge `542b7e23`), which also made "this exact command" actually exact and stopped a wide grant crossing `&&`. **Remaining, in order (see §2 for the why and the done-condition of each):** make context truncation visible to the user → M6 metadata sourcing (unblocks the cost chip and tiering) → M6 tiering + step budgets → M4 leftovers (cost chip, folderless — **image-by-path SHIPPED 2026-08-11, youcoded#293**) → cwd contract → MCP phase 2 → M7 specialists stage two — plans (design approved in the specialists spec §4; three live probes never run — its own Features entry below) → M8 Android → M9 onboarding. **M7 specialists stage one SHIPPED** — plan 1a (2026-08-12, `8db46236`) and 1b (2026-08-16, `e5ec5b3c`); **plan 1c SHIPPED 2026-08-26 (`62c1f182`)** — Destin's hands-on checklist (`docs/active/handoffs/2026-08-16-specialists-1c-testing-checklist.md`) is still to run. Every other step re-verified unbuilt against master 2026-08-26. Standing rules (Destin 2026-07-22): full parity is the end state; build real features, no interim "not available yet" shims. -- [ ] **Android never reads `youcoded-skills.json` after first run — `SkillConfigStore.load()` has no production caller** `bug` `#android` `#skills` (added 2026-08-28, found while adding the quick-chip edit surface, youcoded#359) - `SkillConfigStore.config` is initialised to an empty `JSONObject` and the constructor does not read the file (`SkillConfigStore.kt:14`, no `init` block). `rg -n "load()" --fixed-strings app/src/main/kotlin/` returns ONLY the definition (`:37`) and `reload()` (`:253-254`) — nothing calls either. The one production entry point, `LocalSkillProvider.ensureMigrated()` (`:817`), calls `configStore.migrate()` **only when the file does not exist**, so a fresh install works for exactly one launch and every launch after that runs against an empty config. - Two consequences, both from an empty `config`: every getter falls through to its default (`getChips()` → `defaultChipsJson()`, `getFavorites()` → `JSONArray()`, `getOverrides()`, `getPackages()`, `getPrivateSkills()`), and `save()` writes `config.toString(2)` **wholesale** (`:267-272`) — so the first write of any kind replaces the file with only the key just set, dropping favorites, chips, overrides, privateSkills, packages and themeFavorites. `getThemeFavorites()` (`:303`) is worse still: it `save()`s on a READ when `themeFavorites` is absent, so merely reading can truncate the file. - Already known and worked around in a test rather than fixed: `LocalSkillProviderInstalledTest.kt:96` — "SkillConfigStore.config stays empty until load() runs. Force load so…". - Desktop is unaffected (`skill-config-store.ts` loads on every access). **Not verified on a device** — read off the source and CI, not reproduced. (**Correction 2026-08-31: there IS an SDK at `/home/destin/.android-sdk`; `ANDROID_HOME` is merely unset.** `JAVA_HOME=/usr/lib/jvm/java-21-openjdk ANDROID_HOME=/home/destin/.android-sdk ./gradlew test -x bundleWebUi` runs the full Android suite locally in ~2 min, so this CAN now be reproduced — a device is still needed for on-device behaviour, but compile/unit verification is no longer blocked.) Reproduce before fixing, and check whether existing installs have already been truncated. This is upstream of the uninstall-cascade fix in youcoded#359, which is correct but insufficient on Android while this stands. - -- [ ] Specialist child-transcript garbage collection — children accumulate in the sessions directory with no user-visible deletion path, and deleting a parent conversation does not sweep its children `bug` `#specialists` (added 2026-08-12, final review of specialists plan 1a; belongs to plan 1c — deferred explicitly by plan 1b's own deferred-item ledger) - **PARTIALLY CLOSED by plan 1b, 2026-08-13 (branch `feat/specialists-bg-lane-b`, integration commit `a933707c`; MERGED to youcoded master 2026-08-16 as part of plan 1b, `e5ec5b3c`):** the other half of this item, `resume(childId)` being unguarded, is fixed — `NativeSessionHost.resume()` now refuses any header with `sessionKind === 'specialist'` (`native-session-host.ts:2650`), and `resumeSpecialist` (Task 6's `task_id` management surface) is the only re-entry door for a specialist child. **What remains open is the GC half stated in this item's title:** no surface lets a user delete a specialist child's transcript, and deleting/archiving a parent conversation does not sweep its children's JSONLs off disk. They are real files under the sessions directory that outlive every conversation that ever spawned them. - **NOT touched by plan 1c either (2026-08-16) — the dependency is now explicit.** 1c added files/definitions, the chat UI backend, and Settings, none of which give a user any way to delete a conversation at all (specialist or not), so a specialist-specific deletion path has nothing to hang off of yet. This item can't ship until a general delete-conversation feature exists in the app; revisit it as part of that feature's design rather than in isolation. -- [x] Specialist consent-key canonicalization — Task's charter-scoped grant key uses the raw model-provided work_dir string, so `.` and the absolute path are distinct grants (fail-safe: extra asks) `bug` `#specialists` (added 2026-08-12; **FIXED 2026-08-13 — plan 1b Task 11, branch `feat/specialists-bg-lane-b`, commit `f9fcd065` / integration merge `de8c4945`; MERGED to youcoded master 2026-08-16 as part of plan 1b, `e5ec5b3c`**: `tools/task.ts`'s `permissionSubject` now resolves `work_dir` through `resolveP`+`toPosix` (the same display-safe canonicalize pair `createChild` already used) before it becomes the rule's stored pattern, so `.`, `./x`, and the absolute form of the same directory mint ONE grant key instead of three.) -- [x] Specialist permission consent card doesn't adapt its copy for a `task_id` management call `bug` `#specialists` (added 2026-08-13, plan 1b review — deferred to the follow-up plan) **SHIPPED 2026-08-26 — plan 1c, youcoded merge `62c1f182`** - The renderer's `AgentView` (`tool-views/ToolBody.tsx`) — the same card used for both the pending-approval ask and the completed display — reads `agent`/`description`/`prompt` as if every Task call were a fresh spawn. A `task_id` steer/resume/interrupt call carries none of those fields the way a spawn does (`TASK_ID_DOCTRINE` in `tools/task.ts` documents the real shape), so a routed ask for, say, a write-capable **resume** of an existing specialist can render with a wrong/blank subagent chip and a "Briefing" section showing the resume prompt as if it were a fresh brief. The ask itself still fires and still requires a real answer — this is not a permission bypass — but the card's copy does not tell the approving user what they are actually approving, which is an informed-consent gap. Plan 1b shipped the routing (Task 8) and the `task_id` surface (Task 6); adapting the card's copy to the call shape is renderer work the follow-up plan (1c) needs to close. - **Implemented on branch `feat/specialists-1c-ui` (worktree `worktrees/specialists-1c`, head `6dd6a1a4` as of 2026-08-26; **no PR opened**; 47 commits ahead / 89 behind; 4 conflicts, all workbench-fixture or doc files), NOT YET merged to youcoded master.** `AgentView`/`AgentSections` (`tool-views/ToolBody.tsx`) now read the live run record and a per-cwd definition lookup instead of treating every Task call as a fresh spawn: a `task_id` call suppresses the charter chip (nothing is being hired) and, before its own run record exists, shows the OTHER child's real title via `useSpecialistRunByChild` instead of a wrong or blank chip. Verified against the actual branch code, not the plan's description of it. Will flip to `[x]` with the master merge sha once Task 15 Step 3 runs. -- [x] A background specialist's Task card reads "complete" the instant the hire is acknowledged, while its Activity section keeps filling as the child works `ux` `#specialists` (added 2026-08-16, Destin's 1b hands-on Test 4 — belongs to plan 1c with the other card/badge work) **SHIPPED 2026-08-26 — plan 1c, youcoded merge `62c1f182`** - Literally true — the Task *tool call* returns immediately for a background hire ("…is now working in the background (task_id: …)") — but the child's stamped events keep streaming into that same card, so the user sees a card marked done that is visibly still working. The card should show a distinct "working in the background" state until the child's own run settles (the ledger already knows: `running` → `completed`/`failed`/`interrupted`), and only then read complete. Foreground hires are unaffected (the tool call spans the whole run). - **Implemented on branch `feat/specialists-1c-ui` (worktree `worktrees/specialists-1c`, head `6dd6a1a4` as of 2026-08-26, no PR), NOT YET merged.** The card's settle/collapse logic and its status icon both now read `run.status` — the ledger's real state — instead of the tool call's own `response`; `ToolCard.tsx` shows "· in the background" for as long as `run.status === 'running'`, and a background hire's launch-ack text no longer collapses the Activity section early (`ToolBody.tsx`'s `settled` comment names Test 4 by name). Will flip to `[x]` on merge. -- [x] Specialist chat UI: a background helper's routed permission ask and its final report both render in the wrong place `ux` `#specialists` (added 2026-08-16, Destin's 1b hands-on Tests 8/9 — plan 1c, Destin's explicit directive) **SHIPPED 2026-08-26 — plan 1c, youcoded merge `62c1f182`** - Two coupled placement fixes, so a background hire looks identical to a foreground one — everything about one helper lives in its launching **Task card**: (A) a child's routed permission ask (child-ask-router → parent broker, surfaced today as a standalone top-level card via the synthetic `perm-` path) must instead nest under the original Task card's **Activity** section, buttons and all — it already carries `specialist.childId`, and the Task card is keyed by `parentToolCallId`. (B) a background report (injected user-role turn, today the standalone `SpecialistReportCard`) must fold back into the **same launching Task card** the way a foreground hire's report already does (the tool result IS the report there), so background and foreground render the same. CONSTRAINT on B: the report is also the model's next input — it reads and replies — so the assistant's *reply* stays a normal message below the card; only the report bubble itself moves into the card. Supersedes tonight's interim `SpecialistReportCard` (commit f8e35415) and the earlier `data.injected` notice — both were stopgaps toward this. Related 1c card work: the consent-card copy item and the "reads complete while still working" item just above. - **Implemented on branch `feat/specialists-1c-ui` (worktree `worktrees/specialists-1c`, head `6dd6a1a4` as of 2026-08-26, no PR), NOT YET merged.** (A) a routed child ask nests as a `tool` segment under its own Task card's Activity, matched by the reducer's nested-ask logic, with `PERMISSION_HELD` setting `askHeld` on that same row. (B) a background report folds into the launching card via the run record and `specialistReport`, and the model's own reply stays a separate message below it, per the constraint above. Will flip to `[x]` on merge. - - -- [ ] Tell the user when a saved permission ALMOST covered a command — both causes, or neither `feature` `#native-runtime` `#permissions` (added 2026-08-13, deferred out of M5 2c by amendment A5) - A wide Bash grant can fail to cover a command in two ways, and both currently present as the app forgetting an approval: (1) the matcher refuses it because the command chains a second command (`npm run build && echo hi` under an "any npm run" grant), and (2) it simply does not match, because the command puts a flag after the branch (`git push origin feat/x --force` under a branch grant — spec §5.3). 2c's spec proposed surfacing only (1), threaded engine → broker → dispatcher → card; the plan dropped it because explaining half the cases still leaves the user unable to trust that they will be told, and put the caveat in the option's wording instead. Do both or neither. Spec: `docs/archive/specs/2026-08-13-bash-always-allow-rule-shape.md` §4.5 + §15 A5. -- [ ] Project-scoped skills in native sessions — `/.claude/skills/` is never discovered `feature` `#native-runtime` (added 2026-08-05; **claim re-verified TRUE 2026-08-26** — `scanProjectSkills` does not exist on master, `scanSkills()` still takes no arguments, `skill-catalog.ts:65` still has no cwd param. Plan written 2026-08-06 (`docs/active/plans/2026-08-06-project-scoped-skills.md`) — build work only, no open questions, no branch) - Claude Code auto-discovers a repo's own `.claude/skills/`; the native harness does not. `scanSkills()` (`desktop/src/main/skill-scanner.ts:21`) takes no arguments and all three of its passes are home-scoped, so a native session opened on a repo cannot use that repo's skills and the drawer under-reports what a CC session in the same folder already runs. **Commands already have the pass** — `command-provider.ts:71-72` scans `/.claude/commands/` — so the precedent exists; skills just never got it. Design settled with Destin 2026-08-05: docs/active/specs/2026-08-05-project-scoped-skills-design.md. Shape: a separate `scanProjectSkills()` (NOT a fourth pass — `scanSkills()` must stay home-scoped or `LocalSkillProvider.installedCache` goes wrong), precedence plugin > project > user matching the command side, shadowing annotated in the drawer rather than silent, auto-load with attribution and no trust gate (the permission engine is the boundary and does not move), and a per-session catalog in `NativeSessionHost` keyed by cwd. - **Carries a real bug fix:** `main.ts:194-197` resolves the project cwd for commands as `sessions[0]?.cwd ?? null` — the FIRST session, not the active one — so with two sessions open on different folders, project *commands* already come from the wrong folder today. Main has no active-session concept (`session-browser.ts` only takes an `activeSessionIds` set), so `cwd` becomes an argument on both `skills:list` and `commands:list` and the renderer, which knows, supplies it. - **Android deferred by Destin 2026-08-05**, and two pre-existing Android gaps go with it: `app/.../skills/SkillScanner.kt` has no project pass, and it has no `~/.claude/skills/` (user-authored) pass at all — it stops after the two plugin passes, so it does NOT mirror the desktop scanner despite being described that way. Both belong to M8. -- [x] Per-project description — synced, user-written label on the project card and list `feature` `#sync` `#ui` (added 2026-08-05, **SHIPPED 2026-08-26 — youcoded PR #330, merge `abd935a3`**). All nine plan tasks landed, plus two rounds of UI fixes from Destin's visual pass: `95af1cb3` (New Conversation to the bottom-right, editor as an auto-sized Textarea) and `3a0db918` (pill hover, add-description affordance, editor geometry). The branch's 570-commit staleness was closed by merging master before the PR; one conflict, in `mock-shim.ts` only, resolved as a union of both sides. `verify.sh --full` green except one PRE-EXISTING failure in `harness-eval-orchestrator.test.ts`, confirmed failing identically on master at `73e2defe` — filed as its own bug below. **The three manual cross-device checks are still outstanding** (they need two real devices and a real build) and §9's mixed-version hazard is unchanged, so both still gate the release this ships in. Docs archived; spec §8's four open questions are all resolved in the archived copy. - Design settled with Destin 2026-08-05: docs/archive/specs/2026-08-05-project-description-design.md. A **label for the user, not agent context** — it never enters a session's prompt. Rides the existing project-registry machinery next to `displayName`, so the synced half is mostly repetition of a shipped pattern. UI approved in the workbench (18 numbered changes, branch `feat/project-description`): description under the path row in italic curly quotes with inline edit, a third truncated line on switcher rows, and the sync status strip compressed into a **pill that opens its own popover** carrying every state's full copy plus its action — including "Stop syncing", which moved off the actions row. - **Two traps that were easy to get silently wrong — both avoided, and both now guarded.** (1) `PROJECT_REGISTRY_SCHEMA` was NOT bumped: `parseEntry` rejects on strict inequality and reads are fail-soft-skip, so bumping it would have made every older device drop *every* record. The field was added tolerantly at schema 1. (2) `updatedAt` was NOT reused: `mergeProjectEntries` picks the newer entry wholesale, so a shared clock would mean a description write on one device silently reverting a rename made on another. `description` got its own `descriptionUpdatedAt` and an independent LWW join. A third trap surfaced only in final review and is the reason `laterOf` takes `{v, at}` wrappers rather than whole entries — a whole-entry compare falls through to `JSON.stringify`, which reads `displayName`/`state` first and broke associativity in 2,116 of 32,768 triples. All three are pinned by `sync-spaces-project-registry.test.ts`; the invariants live in `.claude/rules/sync-spaces.md`. - **STILL OPEN after the merge — the mixed-version hazard** (spec §9, unchanged): an older device that renames or stops a project rebuilds the entry from explicit fields and so drops the description for everyone. The three setters now spread `cur`, which protects the NEXT field added, but nothing protects this one against a build that predates the merge. Window-limited, no data loss beyond retyping, and it argues for shipping in a release where sync clients update together. **Also still open: the three manual cross-device checks** (describe on A → appears on B; rename on A while describing on B → both survive; local folder stays local) — they need two real devices and a real build, so they are Destin's, and they gate the release rather than the merge. - **Android, for the record** (verified in `SessionService.kt` at build time): the two channels deliberately differ. `syncspaces:set-project-description` joined the fast-reject `not-implemented-on-mobile` list next to `syncspaces:rename-project`; `folders:set-description` got a real Kotlin implementation to match `folders:rename`. Spec §8's four open questions are all resolved in the archived copy. -- [ ] App-native hover tooltips — replace browser `title=` on the surfaces users actually watch `feature` `#ui` (added 2026-07-28) - Destin 2026-07-28, on the new /clear "Cleared — still here to read, but not in Claude's context" hint: browser-default tooltips look foreign to the app. **This REVERSES a documented prior decision** — `components/ui/AnchorTip.tsx`'s header states the policy explicitly ("Native `title=` hover hints are NOT this component — they stay as-is (~231 across 63 files). AnchorTip is for rich/click-open info; `title` is for plain hover hints. Two tools, one documented policy."). Re-verified 2026-08-12: **267** `title=` sites across 82 files in `src/renderer/` (was 245 on 2026-07-28), heaviest in SettingsPanel (27), StatusBar / SessionDrawer / HeaderBar (8 each). **AnchorTip cannot be reused as-is** — it renders its OWN (i) glyph as the trigger, so it is an info-button, not a wrapper you can put around a chip or a chat bubble; the new thing is a `` that wraps arbitrary children. It should reuse AnchorTip's hard parts, which are already solved there: portal + the L4 Overlay layer (so it is not trapped behind the panel it describes), capture-phase reposition on scroll/resize, and Esc via the shared `useEscClose` LIFO stack. **Scope recommendation: NOT all 245.** Migrating everything is ~a day plus a visual pass, and each swap turns an attribute into a wrapper element, which can perturb flex/grid — status-bar chips and header buttons especially. Do the surfaces in constant view (status bar, header bar, chat timeline) and leave settings/marketplace on `title` until touched anyway. **Genuine tradeoff to preserve, not just work:** `title=` is free accessibility, cannot overflow or be clipped, and survives the window losing focus — a custom tooltip has to earn all three back, and these triggers are dense (the status bar sits at the screen edge; the timeline scrolls). Deliberately NOT bundled into the native-runtime branch: a 63-file migration inside a 50-commit branch would have made its review materially harder. -- [ ] Android sync + Android-resume fixes `feature` `#android` (added 2026-07-15) - Port longest-first walkSlugParts, thread resumeSessionId through the bridge + cwd guard, store/basename resolver; Android restore-backend demolition follow-up from Plan 2c. **State re-verified 2026-08-12:** (a) Kotlin `walkSlugParts` (`SessionBrowser.kt:373`) is the *shortest-first pre-fix algorithm* (landed `6381ec72` 2026-04-23, predates the desktop longest-first fix) — it's the old bug, not merely an unported improvement. (b) `resumeSessionId` bridge plumbing already EXISTS (`PtyBridge.kt:23`, `SessionRegistry.kt:32`) but the single caller (`SessionService.kt:619`) never passes it, so Android resume is unreachable in practice — remaining work is the call site + cwd guard, not the plumbing. (c) Restore demolition untouched (`RestoreService.kt` 568 lines, `SyncService.kt` pushDrive/pushGithub/pullDrive intact). -- [ ] Misleading error messages — full audit + replacement `bug` (added 2026-07-15) - Per docs/error-message-standards.md; committed as a v1.3.1 followup in CLAUDE.md. Engine sub-fix (cachedir mkdir + stderr drain) already shipped in youcoded PR #123; open scope = workspace-wide audit of every user-facing throw/toast/banner/IPC error string (desktop + Android + worker) + a reusable two-action fallback component. **Update 2026-07-16:** the two-action component is now fully designed (UI-consistency spec change 33 — ErrorState general mode, Option C: neutral card + destructive dot + Report bug/Diagnose with Claude); it lands with the UI-consistency states tranche, leaving only the string audit here. **Correction 2026-07-24: it did NOT land with the states tranche.** Change 33 was deliberately HELD there (spec §15) and stayed held through the end of the migration, because picking recoverable-vs-general PER SITE is this audit's own core decision, not a styling call — shipping a blanket choice first would have prejudged it. The `ErrorState` component is built and now has **two call sites**, both in SettingsPanel (youcoded `4a82e43f`, 2026-07-28; re-verified 2026-08-12 — the 2026-07-26 check found zero — an earlier version of this line said "adopted", which was wrong; the tranche-8 adoption guard could not see it because it derived names from filenames and `states.tsx` is lowercase, fixed in youcoded PR #255); **this entry now owns change 33 outright** (the UI-consistency workstream closed 2026-07-24). Scope here is unchanged: the string audit, plus deciding the mode at each site as you go. **Update 2026-07-22:** the sync/GitHub family is DONE ahead of the audit — the 2026-07-22 sync-setup overhaul (youcoded #201–#203) replaced every gh/git error surface it touched with specific plain-language coded strings ('Not connected to GitHub…', 'GitHub sign-in expired…', auth-vs-offline git classification, REST failures carrying GitHub's own message + HTTP status). Remaining scope = everything OUTSIDE that family. (from knowledge-debt 2026-07-14) -- [ ] Buddy floater on Linux Wayland — XWayland route PROVEN but shelved; next attempt: native Wayland `bug` `#buddy` `#linux` (added 2026-07-17, redirected 2026-07-23, XWayland attempt executed + shelved 2026-07-23) -- [ ] **Landing-page live embed goes fully blurred under framed wallpaper themes** `bug` `#landing-page` (added 2026-08-30, found while building the redesign mockups). Repro on the CURRENT live page: start the embed, open its theme button, pick Meadow Mist — the whole app window becomes one blur (Golden Sunbreak, a floating-chrome theme, is fine). Cause, isolated by stripping styles one at a time in a live instance: any *rounded clip* on the iframe or an ancestor (`.embed-frame`'s `border-radius`+`overflow:hidden`; `clip-path: inset(round)` and a rounded iframe reproduce it too) makes Chrome ignore the clip-path cutout on the app's single `chrome-glass` backdrop-filter surface, so the glass blurs the entire window instead of just the chrome donut. Verified fix (used in the redesign mockups' `build.py`): drop `overflow:hidden` from the wrapper (keep `border-radius` for border/shadow shape — safe without the clip) and round the corners from INSIDE the iframe instead (same-origin: set `border-radius`+`overflow:hidden`+transparent background on the iframe's `documentElement` after load). Today's exposure is small — the embed boots in Midnight and only the in-app Appearance screen can reach a wallpaper theme, and only meadow-mist among the three vendored packs triggers it — but the redesign makes theme switching a primary interaction, so the fix must ship with it. Related repo change staged in worktree `worktrees/site-themes` (branch `feat/site-embed-all-themes`, uncommitted): vendor the four missing community packs (cotton-candy-sky, devils-garden, kuromi-dreamer, strawberry-kitty, ~6.5 MB) into `desktop/src/renderer/dev/workbench/fixtures/themes/` so the embed knows all 7 — `__workbenchAppearanceSync({theme})` silently ignores a slug it doesn't have. The fixture-pinning tests only assert the original three slugs are present (`workbench-shim-semantics`, `workbench-channels`), so adding packs breaks none of them. -- [ ] Chat Search phase 3 — digests `feature` `#chatsearch` (added 2026-08-06, **PHASE 2 (writes) SHIPPED 2026-08-27** — youcoded#346 (outbox drainer) + wecoded-marketplace#70 (flag/tag/note/close/receipt, chatsearch 0.2.0). Exercised by hand against the real store: close applied and repainted with a receipt in <2s, a second close reported `already` with the note still one line, one bad id refused the whole command, an unknown tag was refused naming the existing tags. Phase 3 below is unbuilt.) - **Phase 1 (index + read-only CLI) MERGED 2026-08-06** — youcoded#282 + #283 (transcript-resolution fix), wecoded-marketplace#65 + #66. **Exercised by hand and judged good 2026-08-25** (Destin, dev instance, live index of 1,816 Claude + 154 native conversations / 14,261 indexed turns: *"i think this is good."*) — the six-check list survives in the handoff as a regression list, not a blocker: the checklist and the traps are in `docs/archive/handoffs/2026-08-10-chatsearch-state-of-play.md`, and recall quality should be tested before more is built on top. The follow-on (Preview/Resume from a search hit) **SHIPPED 2026-08-27** — youcoded#343, merge `3b759931`; skill side wecoded-marketplace#68 (merge `314617f8`). Spec `docs/archive/specs/2026-08-10-chatsearch-session-references-design.md`, plan `docs/archive/plans/2026-08-25-chatsearch-session-references-plan.md`. Spec: `docs/active/specs/2026-08-05-chat-search-design.md`. **Phase 2** = the write path — `tag`/`untag`/`note`/`flag` from the CLI through a file outbox the app drains, so mutations apply through the real `tag-registry-service` and `SESSION_META_CHANGED` still fires. Atomic-rename claims, since the live app and every dev instance share `~/.youcoded/`. **Phase 3** = per-work-item digests (`resolved | open | abandoned | unclear`) behind an off-by-default Preferences toggle and a model picker, on the four lazy triggers. Note phase 3 unblocks two things phase 1 ships deliberately inert: the `○` open marker, and `--state open`, which currently answers "cannot be determined yet" rather than returning zero results. Open question carried forward: whether digests should be user-editable (claude.ai's memory summary is). -- [x] Bundled plugins are never upgraded after first install `bug` `#registries` `#chatsearch` (added 2026-08-25, **SHIPPED 2026-08-27 — youcoded#345 + #346, wecoded-marketplace#69 + #70.** `reconcileBundledPlugins()` replaces install-if-missing on both platforms: refresh the private cache clone behind the existing 1 h gate, compare `plugin.json` versions, upgrade via staged copy + rename swap that never deletes the live tree first. The marketplace index now publishes each plugin's own `plugin.json` version (#69) so the app's Update badge compares one number space — shipping the app half without that produces a permanent, unclearable badge on every bundled plugin, which is why #69 merged first.) - `ensureBundledPluginsInstalled` (`skill-provider.ts:803-812`) is install-if-missing: once a bundled plugin is in `installed_plugins.json` it is never touched again, so a user who installed `youcoded-chatsearch` at 0.1.0 keeps that `chatsearch.js` and `SKILL.md` forever. A manual `skills:update` exists (`skill-provider.ts:284`, marketplace UI only), but nothing drives it for bundled ids and the marketplace entry has no version field to detect skew from. Consequence found while revising the session-references design (2026-08-25): any feature that depends on a plugin-side change silently does not exist for existing installs — which is why that design was changed to resolve ids in the app instead of adding output to the CLI. Fix shape: on launch, for each id in `BUNDLED_PLUGIN_IDS`, compare the installed `plugin.json` version against the marketplace clone's and run `update(id)` when behind (needs the 24h index-cache item above resolved first, or the comparison reads a stale index). Until fixed, plugin-side changes reach existing users only on a fresh install. -- [x] A newly bundled plugin cannot install for up to 24h after publishing `bug` `#registries` (added 2026-08-10, **SHIPPED 2026-08-27 — youcoded#345 + #346, wecoded-marketplace#69 + #70.** `reconcileBundledPlugins()` refetches the index once per process when a bundled id is missing from the cached copy, so a just-published bundled plugin installs on the next launch instead of waiting out the 24 h TTL. The refetch invalidates only the index cache — not the featured/defaults caches — so it cannot cost a network round-trip for those on every launch.) - `LocalSkillProvider.fetchIndex` caches the marketplace index at `~/.claude/youcoded-marketplace-cache/index.json` with a 24h TTL (`INDEX_TTL`, `skill-provider.ts`). `ensureBundledPluginsInstalled` → `installMany` → `install(id)` looks the id up in **that cached index**, so between publishing a plugin and the cache expiring, a bundled plugin is silently not installed — the entry is in `BUNDLED_PLUGIN_IDS`, merged to the registry, and absent from `installed_plugins.json`, with no error anywhere. Hit for real with `youcoded-chatsearch` on 2026-08-10: the cache held a 174-entry snapshot fetched 17.7h earlier while the live registry served 327. Workaround = delete that one file and relaunch. Note `installFromLocal` has its own **1h** cache repo refresh, so only the index lookup is the slow gate; the two TTLs disagreeing is itself worth a look. Candidate fixes: bypass/ignore the TTL for ids in `BUNDLED_PLUGIN_IDS`, force a refetch when a bundled id is missing from the cached index, or shorten `INDEX_TTL`. Low user impact (self-heals within a day) but it makes bundled-plugin rollout untestable on the day you ship it. -- [x] Fresh `npm install` leaves the desktop dev app unlaunchable `bug` `#tooling` (added 2026-08-06, **SHIPPED 2026-08-11 — youcoded `979e08e3`: desktop/package.json now carries the allowScripts block (electron true; node-pty/koffi/electron-winstaller explicitly declined), so a clean clone downloads the Electron binary and run-dev.sh comes up. Verified against the origin/master blob 2026-08-12.**) - Hit while dev-testing chatsearch. `desktop/package.json` on master had no `allowScripts` block, so npm blocked electron's postinstall and the binary was never downloaded — `run-dev.sh` then dies with "Electron failed to install correctly" while Vite still comes up, which makes it look like a partial start rather than a missing dependency. It had been papered over by an **uncommitted local** `allowScripts` block in the main checkout, which pulling master removed. Workaround used: `node node_modules/electron/install.js`. Real fix = commit the `allowScripts` block (electron, node-pty, koffi, electron-winstaller) so a clean clone works, or document the approve step in setup. -- [x] Transcript-path validators do prefix checks without a trailing separator `bug` `#tech-debt` (added 2026-08-12, **FIXED 2026-08-12 — youcoded PR #294, merge `cd1d94ac`: both stragglers now append `path.sep`, with mutation-verified tests driving the real IPC handler and WS frame. Review found the whole `claudeProjects` family is now consistent (4/4 sites).**) - Noticed during the slug-encoding spec verification (not part of that fix's scope): `ipc-handlers.ts:929` and `remote-server.ts:1658` validate a caller-supplied transcript path with `resolved.startsWith(claudeProjects)` and no `path.sep`, so a sibling directory like `~/.claude/projects-evil/…` passes the containment check. The other three validators of the same family (`ipc-handlers.ts:1057-1059`, `remote-server.ts:1683-1685`) already append `path.sep` — align the two stragglers. Low severity (paths come from our own UI, not untrusted input), but it's a two-line fix and the inconsistency invites copy-paste of the weak form. -- [x] Artifact pane: a text-extension file that sniffs binary renders a blank pane `bug` `#renderer` `#artifacts` (added 2026-08-12, found by the PR #303 review; **FIXED same day — youcoded PR #308, merge `38a85e95`**: `getViewer` gained a `binaryHint`; registry hits that are text-content viewers (MarkdownView, CodeEditorView, CsvView, and — per the review's finding — HtmlView, whose null-content state was a PERPETUAL "Loading…") route to `BinaryFallback` with its "Open in default app" action. Review verified: UTF-16-BOM files never had a working preview (blank pre-PR → honest fallback now, nothing working taken away), binary files can't reach edit mode, single call site. Known ~1-frame stale-hint window on artifact switch is pre-existing hook architecture, noted for the future.) -- [x] Attachment paperclip opens the file picker biased to images `bug` `#ui` (reported by Destin 2026-08-12, **FIXED same day — youcoded PR #309, merge `f68ad7c3`**) - The real bias lived in the REMOTE/mobile path: `remote-shim.ts`'s picker set an `accept` whitelist that made phone browsers open a media-biased chooser — removed outright (browsers have no filter dropdown, so all-files is the whole fix there). The desktop native dialog was already All-Files-first since `e83c3ede` (v1.1.0) and gained the requested category options (Images / Markdown & Text / PDFs / Spreadsheets / Documents / Code); Android was already `*/*`. **If the desktop picker still LOOKS images-biased after this, the likely cause is KDE's portal opening in the Recent Files view (dominated by previously-attached screenshots), or an Electron→portal `current_filter` quirk — that would be a new portal-layer investigation, not app code.** Downstream verified: non-image attachments degrade gracefully everywhere (filename chip, path text to CC, path-not-pixels on the native harness — extending native embedding beyond images is a possible follow-up). **Superseded same day by PR #310 (merge `de4ab4e8`): a live D-Bus capture + source-level research proved the desktop bias was Electron's — `GetFilterInfo()` strips the wildcard filter and hardcodes `file_type_index=0`, Chromium appends `*.*` last and omits `current_filter`, so KDE preselects the first concrete filter; Windows skips All-Files-as-default BY DESIGN (electron#43491 + #19492, both closed not-planned). The attachment dialog now passes NO filters on any platform — all files everywhere, no type dropdown. The category-options half of the ask is NOT implementable via Electron's dialog API; routes if ever wanted: upstream Electron patch or an in-app picker. Full diagnosis lives in the handler's WHY comment.** -- [ ] Git surface: `core.quotePath` paths (quotes, backslashes, non-ASCII) read as clean for EVERY entry kind `bug` `#git` (added 2026-08-12, found by the PR #304 review — pre-existing, all entry kinds, not introduced by the conflicted-files fix) - git C-quotes such paths in porcelain v2 output (verified: `"quo\"te.txt"`, `"\303\274n\303\257code.txt"`); `porcelain.ts` never unquotes, so the `path === rel` match misses and the file reads clean. Fix direction: migrate to `-z` output (already noted as a future migration in `porcelain.ts:79`) or add C-unquoting. A user with an accented filename gets silent no-status today. -- [ ] Git surface: rewrite-staleness residual from the PR #304 review `bug` `#git` (added 2026-08-12 — advisory-grade; the OTHER residual from that review, the unkeyed TerminalRightSlot drawer, was **FIXED same day — youcoded PR #307, merge `6a97e345`**: `key={sessionId}` matching chat view's per-session isolation; review verified remount cost is ~zero net-new IPC and unsaved drafts survive via the module-level draft store, which was designed for exactly this remount) - After a history REWRITE (amend/rebase) mid-"Show more", stale extraLog entries persist and inflate `--skip` until the review closes — bounded, documented in the WHY comment, but worth a rewrite-detection guard someday. -- [ ] `.claude/rules/sync-spaces.md`'s "sync dots are the ONE sanctioned status-color use" is over-broad — reword or re-scope `bug` `#docs` (added 2026-08-12, found by the PR #304 review) - Shipped primitives already contradict the global phrasing (Callout warning tone, StatusStrip warn, git footer green/red, the new Conflict badge — and `desktop/CLAUDE.md:120` explicitly sanctions hardcoded status colors incl. amber). The sentence means "sync status specifically must come from sync-dot-state.ts", which is true — scope it to say that. Destin's call on wording since it's a rule file. -- [ ] Decide: should `*.tmp` join sync-spaces `DEFAULT_IGNORES`? `task` `#sync` (added 2026-08-12, surfaced by the PR #296 review — **Destin's call, deliberately NOT slipped into that PR**) - Temp-then-rename orphans (any `*.tmp` stranded by a crash between write and rename) are not ignored by `sync-spaces/guards.ts:20-41`, so one stranded in a synced dir transports to every device as junk — `transcript-mirror.ts:17-19` documents the hazard and per-writer sweeps mitigate it (PR #296 adds them to cas-write and the two user-tree writers). Adding `*.tmp` to `DEFAULT_IGNORES` would close the class in one line, BUT `isIgnoredPath` also feeds the backup filter, so a user file genuinely named `something.tmp` would silently stop syncing/backing up. Per-writer sweeps make this optional; decide whether the one-line blanket is worth that edge. -- [x] Remote/Android initial history load pulls the FULL transcript — remote-shim `loadHistory` argument order is scrambled `bug` `#remote` `#android` `#perf` (added 2026-08-12, found by the PR #300 review; **FIXED same day — youcoded PR #301, merge `5527fa27`**: shim reordered to match preload with preload-parity defaults so real number/boolean types hit the wire; server now honors a SAFE_ID_RE-gated client `projectSlug` before falling back to the scan; a parameter-ORDER parity test pins the shim against preload (executed against the old code to prove it catches this class). Android impact was worse than filed: the misplaced boolean coerced to slug `"false"`, so Android remote history loaded EMPTY. Review confirmed stale cached-browser shims degrade to exactly the old behavior, no worse. The fix agent's sweep verified loadHistory was the ONLY order mismatch between shim and preload.) -- [x] `tool-views/ToolBody.tsx` has the same unknown-input crash shape ToolCard just fixed `bug` `#renderer` (added 2026-08-12, found by the PR #295 review; **FIXED same day — youcoded PR #306, merge `22108f52`**: all ~30 `input.` accesses in the expanded views hardened via a shared `asString` (extracted to `utils/tool-input.ts`, ToolCard migrated onto it); the AskUserQuestion deep fields got a `normalizeQuestions()` pass so one malformed member degrades instead of crashing — the review traced the normalized-echo through both the native harness path (never read) and the CC hook path (reconstructs exactly the schema's fields; the pre-PR echo was already a reconstruction) and empirically re-confirmed 11/12 tests red-first. The `transcript-watcher.ts:498,743` main-process lie-casts noted in the original entry remain — cosmetic, main-process, no render surface.) - GrepView (~:694-695, :753) and GlobView (~:765, :773) use `tool.input.path as string | undefined` guarded only by truthiness, so an object `path` crashes `basename()`, and an object `pattern` rendered as a React child throws "Objects are not valid as a React child". ~20 more lie-casts throughout (:242-244, :265-266, :384-386, :510-512, :581-583, :692-695, :764-765, :796-797). Exposure is lower than ToolCard (only on card expand), and a crash is contained by the Chat ErrorBoundary — but it's the identical class; port ToolCard's `asString` idiom. Related main-process instance of the same lie-cast pattern: `transcript-watcher.ts:498,743` (`subagent_type as string || ''`). -- [x] Two remote-server hardening nits from the PR #294 review `bug` `#remote` `#security` (added 2026-08-12, **FIXED same day — youcoded PR #300, merge `7a385879`**: `transcript:read-meta` now mirrors `model:read-last` (non-string → null, resolve inside try — the old throw was an unhandled rejection AND a 30s client hang since no response was ever sent); `session:history` validates the id against `SAFE_ID_RE` (now exported from session-browser as single source of truth) before the fs.access probe; review confirmed no legit id is rejected and the `SAFE_ID_RE.test(undefined)`→"undefined"-string coercion trap is guarded.) - (a) In the remote `transcript:read-meta` case, `path.resolve(transcriptPath)` at `remote-server.ts:1657` sits *outside* the `try`, and `payload.path || payload` can yield a non-string — a malformed WS frame throws into a floating promise (`ws.on('message', …)` at :685 doesn't catch, and `handleMessage` has no outer try around the switch), so one bad frame can surface as an unhandled rejection instead of an error response. (b) `session:history` (`remote-server.ts:1231`) joins a client-supplied `histSessionId` into a path for an `fs.access` probe BEFORE `loadHistory`'s `SAFE_ID_RE` guard runs — at most a file-existence oracle for `*.jsonl` paths (no content leak), but the probe should validate first. Both small; batch with the next remote-server pass. -- [x] Pre-existing vitest flake — parallel workers race on the shared temp home `bug` `#testing` (added 2026-08-06, **FIXED 2026-08-28 — youcoded PR #362** — same fix as the per-run sandbox entry under v1.3; this was the same root cause seen from a different victim file) - Surfaced in three separate tasks during chatsearch phase 1, in a different unrelated file each time (`session-meta-parity.test.ts`, `ipc-handlers.test.ts`), as ENOENT or temp-rename errors. Proven pre-existing by reproducing with all new files stashed. Root cause is parallel vitest workers sharing `/tmp/youcoded-vitest-home` (set in `vitest.config.ts`, wiped once per run by `tests/global-setup.ts`). It passes in isolation and on re-run, so it intermittently fails `verify.sh` and will do the same to unrelated PRs. -- [ ] `remote-server.ts` re-implements the saved-folders store inline — unify it and cover the remote path `bug` `#tech-debt` `#remote` (added 2026-08-06) - `remote-server.ts` does its own read/parse/find/write of `~/.claude/youcoded-folders.json` in each `folders:*` case instead of importing `saved-folders.ts`, which is the real store (`readFolders`/`writeFolders`, atomic temp-then-rename). As of the per-project-description branch there are **three** copies of that logic: the store module, the `folders:rename` case, and the new `folders:set-description` case. Duplicating rather than refactoring was a deliberate call by Destin 2026-08-06 — the refactor changes the shipped `folders:rename` remote path, and that path has **zero** test coverage (verified: `tests/remote-server.test.ts` contains no `folders` reference; `tests/saved-folders.test.ts` covers only the store module). Doing it inside a description branch was the wrong risk. **Do the test first**: cover `folders:rename` over the WebSocket path, then collapse both cases onto `saved-folders.ts`. Note the inline copies also skip the store's atomic write (plain `writeFile`, not temp-then-rename), so a crash mid-write truncates the user's folder list — that is the concrete bug hiding in the duplication, not just style. -- [x] `validate-plugin-pr.yml` has NEVER fired — wrong branch and wrong paths `bug` `#marketplace` `#ci` (added 2026-08-05, **FIXED 2026-08-12 — wecoded-marketplace PR #67, merge `cbaa74d`.** Branch main→master, paths now top-level with exclusions + plugin.json detection; rebuild loop triple-protected (empty-detection gate, `[skip ci]`, GITHUB_TOKEN pushes don't retrigger); five latent bugs fixed that would have broken the first real run (depth-1 diff, multi-line GITHUB_OUTPUT, false-duplicate on update PRs, always-firing API-key warning, rebuild on non-plugin merges); the also-dead `validate-apple-services*` jobs revived after fixing a jq escape compile error, two SC2209s, and an osascript step that EXECUTED scripts instead of compile-checking them. **Adversarially reviewed twice; second pass clean. What this deliberately did NOT do:** regenerate the drifted index files — the first plugin merge will run `sync.js` for the first time and its bot commit will likely carry a large catch-up diff (Destin's call whether to absorb it in a dedicated PR first). **Residual gaps, non-blocking:** a plugin-DELETION merge skips the rebuild (registry serves the stale entry until the next plugin-touching merge); direct `marketplace.json` edits on master don't regenerate the `.claude-plugin/` mirror; multi-commit direct pushes diff only `HEAD~1..HEAD`.) - Verified 2026-08-05 while adding `youcoded-chatsearch`: the workflow triggers on `pull_request`/`push` to branch **`main`** filtered to **`plugins/**`**, but `wecoded-marketplace` uses **`master`** and keeps plugins at the **repo top level** (`wecoded-themes-plugin/`, `wecoded-marketplace-publisher/`, `youcoded-chatsearch/`). Both jobs are therefore dead: (1) the PR **validation** job — so the `ALLOWED_TAGS` / `ALLOWED_LIFE_AREAS` / `ALLOWED_AUDIENCE` enums in `scripts/schema.js` are documented as CI-enforced but are not enforced by anything; (2) the merge **rebuild** job — so `index.json` / `skills/index.json` have only ever been regenerated by someone running `node scripts/sync.js` by hand. Consequence seen in practice: the chatsearch branch's regeneration produced a ~20k-line index diff of accumulated upstream drift (sync reported `Added: 154, Deprecated: 20`, of which exactly one addition was the new plugin). Fix = correct the branch and path filters; then decide whether to absorb the drift in one dedicated regeneration PR so feature PRs stay reviewable. `sync.js` also needs `GITHUB_TOKEN` or it hits the anonymous rate limit. **Worse than documented (found 2026-08-12):** the `paths: plugins/**` filter sits on the top-level `on:` block, so it also disables the `validate-apple-services*` jobs later in the same file — their comment claims their trigger "isn't disturbed", but they are dead too. - **STATUS 2026-07-23 (latest — XWayland attempt executed):** it WORKS. The existing three-window floater runs correctly under XWayland — real positioning, `keepAbove`, clean transparency, sharp at 1.5× on KDE, visually confirmed by Destin. Two flags are required: a **real argv** `electron . --ozone-platform=x11` (`app.commandLine.appendSwitch('ozone-platform',…)` and `ELECTRON_OZONE_PLATFORM_HINT` are both **silently ineffective** on Electron 41 — verified) and `--use-angle=vulkan` to stop a GPU-process SIGSEGV at `EGL_CreateWindowSurface` (default ANGLE GL crashed 3×/launch; vulkan 0). Also found + fixed a real Electron bug: at 1.5× fractional scale `setPosition()` inflates frameless windows every call, ballooning the chat to 1851×1526 and drifting the group apart — fixed by re-asserting fixed size via `setBounds` (`min/max size does NOT clamp it`). **Shelved as EXPERIMENTAL, not shipped** — draft PR itsdestin/youcoded#239, branch `fix/linux-xwayland-floater`. Why: the ozone backend is a whole-process launch-time choice (can't run the main app native-Wayland and only the buddy on XWayland), production enablement is unimplemented (needs self re-exec or launcher flag), XWayland sharpness is compositor-specific (blurry on GNOME), mixed-DPI multi-monitor breaks, and the GPU flag is untested off this hardware. **Decision: prefer finding a native-Wayland path; if XWayland is ever adopted, hide it behind a user toggle rather than forcing it on all Linux users.** Evidence: `docs/active/investigations/2026-07-23-buddy-overlay-wayland-presentation.md`. - **STATUS 2026-07-23 (earlier):** the one-window overlay rewrite was BUILT and merged DORMANT (youcoded PR #214 — BuddyManager seam + overlay behind `YOUCODED_BUDDY_STRATEGY=overlay` + 7 live-found fixes; zero behavior change on all platforms). It cannot be enabled: `setIgnoreMouseEvents` is a probe-verified TOTAL no-op on native Wayland (with and without `forward`), so the fullscreen overlay eats every click — the design's load-bearing primitive doesn't exist. The 2026-07-17 rejection of XWayland below was **falsified** by the workbench (FINDINGS Round 3/5c): KDE's default `XwaylandClientsScale` renders XWayland Electron SHARP at 1.5× fractional scale, and the smear fix shipped in the Electron 41.10.3 bump. Under XWayland the EXISTING three-window floater works (setPosition, always-on-top, transparency all pass) — remaining blocker is a GPU crash in the full app (ANGLE null-deref at EGL_CreateWindowSurface; minimal probes survive) with an untested workaround list (`--use-angle=gl`, `--disable-gpu-sandbox`, `--in-process-gpu`; worktree `worktrees/xwayland-floater` exists). Full evidence + next steps: `docs/active/investigations/2026-07-23-buddy-overlay-wayland-presentation.md`. Original analysis kept below for context. - On native Wayland (Electron ≥38.2 default), the buddy floater is unusable: it can't be dragged (stuck centered), and edge-anchored peek/sink/lean animations misfire inside the fixed 112×112 window. Root cause is architectural, not a code bug — Wayland forbids apps from programmatically positioning (`setPosition` is a no-op, `getPosition` returns `[0,0]`) or introspecting global window coordinates, and the floater's entire drag model is renderer-pointermove → `buddy:move-mascot` IPC → main-process `setPosition` every frame across THREE separate transparent frameless BrowserWindows (mascot + chat + bar, see `buddy-window-manager.ts`, `buddy-bar-geometry.ts`, `BuddyMascot.tsx`). Confirmed against Electron docs + issues #52204/#48833/#40886; repro env: CachyOS, KDE Plasma (KWin 6.7.3), Wayland, single 1.5×-scaled panel. The proper fix (per Wayland devs + Electron maintainers) is to collapse the three windows into ONE fullscreen transparent always-on-top window and position the mascot/chat/bar as absolutely-positioned DOM inside it — dragging becomes a local CSS write (no IPC, no platform permission, dodges the transparency-smear bug class too). Hard parts priced in: rewiring the 7 cross-webContents IPC pushes + session-subscription-by-webContents.id into in-app React state, and hand-managing dynamic click-through regions (`setIgnoreMouseEvents(forward:true)` + hover-region tracking) across the dead transparent area — the cursor-timing-sensitive debugging the current per-window design gets for free. Do NOT do the `--ozone-platform=x11` XWayland stopgap as the fix (considered 2026-07-17, rejected): it blurs the whole app at 1.5× fractional scaling AND still needs an Electron bump for the separate transparent-window smear bug (electron#50541, propagating to 41-x). This rewrite SUBSUMES the "scene-companion follow physics — window padding redesign" item under Features (the padding/click-through problem disappears in a one-window model). Prototype the click-through + hover behavior in a workbench first, like the rig/peek work. - -## Bugs -- [ ] `bug` `#native-runtime` `#renderer` `#ui` **Settings says OpenRouter is "Connected" when it has never once asked OpenRouter — and its Test button cannot fail** (added 2026-08-31, hit live by Destin: every turn 401'd while Settings read Connected) - **Spec, approved and behavior-complete: `docs/active/specs/2026-08-31-openrouter-connection-trust-design.md`.** Not yet planned or implemented — Destin's call to hold. Desktop only (`provider:*` already refuses honestly on Android, `SessionService.kt:4015-4021`). - **Four defects, each verified against `master` and the live API.** (a) "Connected" is `enabled && hasKey` (`provider-registry.ts:69-79`) — a string on disk, never validated. (b) `testConnection`'s openrouter branch probes `GET /api/v1/models`, which is **public**: it returns `200` for a fabricated key *and for no key at all*, so `Test` always says "Connected." (`provider-registry.ts:362-373,424`). The code's own `CAVEAT` at line 365 says not to present this as key validation; the UI does exactly that. (c) The Connect modal runs that same hollow test and flashes green at entry (`ModelProvidersPopup.tsx:340-352`). (d) A real rejection carries **no action** — `AttentionBanner` gates its Open Settings button on the phrase `/Settings → Providers/` (`:40-41`), which only *pre-flight* errors emit, so a 401 from OpenRouter renders as raw jargon in a red pill. The model picker stays full throughout, because `model-catalog.ts:15` reads the same public endpoint unauthenticated. - **OpenRouter is the only provider with a hollow test** — Anthropic (`x-api-key`→401), OpenAI (`Bearer`→401) and Google (`?key=`→400) all probe endpoints that require the credential. The one built-in, first-listed provider is the one that does not. - **Two OpenRouter facts the vendor's own docs get wrong, both verified live 2026-08-31:** `GET /api/v1/credits` works with a plain inference key (the reference claims a Management key is required), so the **account balance is free** — no second key; and keys carry an **`expires_at`**, set only on request, whose lapse returns the identical `User not found.` 401 as a deleted key. Expiry is the leading suspect for the reported key (created 2026-07-15, dead 2026-08-31) but is unprovable after the fact. There is **no purchase API** — adding credit is a link, never an in-app storefront. - **Design:** a persisted verdict (`verified` / `rejected` / `unchecked`) keyed by `secretRef` rather than provider id, written on entry, on Test, on a 30-min main-process refresh, and — the missing link that caused the bug — **on a live turn failure**; real validation via `/api/v1/key` + `/credits`; typed rejection reasons replacing the phrase-match so every failure ends in an action (Open Settings / Add credit); gear-badge warnings; an OpenRouter balance widget in the status bar's Rate Limits category; PKCE OAuth connect (loopback on desktop, code-paste fallback for remote/phone) **alongside**, never replacing, manual key entry; and `setKey` minting a new `secretRef` so a key replaced in one app copy stops silently resolving to a stale one in another. - **Two corrections already folded in from review, both verified:** the gear's blue badge is `remoteClientCount === 0` (`App.tsx:2021-2032`) — a "Set Up Remote Access" nudge lit permanently for anyone without phone access, **not** a sync-mirroring info channel — so a warning routed into it would be invisible; and the status-bar relevance gate cannot express "OpenRouter sessions only" today, because `widgetApplies` takes `'claude' | 'native'` and `native` covers local models and every direct-key provider. - **Every visual decision is reserved** for a workbench build + review deck (spec §5, seven open questions incl. thresholds, chip default, and Settings wording when a replaced key flips other copies to "Needs API key"). -- [ ] `bug` `#games` `#worker` **`game-forfeit` writes a permanent record about another person on one client's say-so, and neither head-to-head socket message is rate limited** (added 2026-08-31, found reviewing wecoded-marketplace#78) - Two halves of one fix, both in `worker/src/social/presence-room.ts`, both **deployed on master as of #78** and both requiring an authenticated account with an accepted friendship — so neither is a stranger attack. - **(a) The forfeit path contradicts the design's own principle.** §6.2's rule is that the client never asserts a result alone; `handleGameForfeit` is exactly that assertion, guarded only by "the opponent has no live socket right now", which the claimant can simply wait for. No proof a match was ever played, and the `match_id` is opaque. So a hand-written client can manufacture "47-0 vs Jake" — a permanent row about someone who never agreed to it. **Verified 2026-08-31: no client on either platform ever sends `game-forfeit`** — re-runnable on master now that the arcade has merged: `git -C youcoded grep -n game-forfeit origin/master -- .` returns nothing; the string exists only in the Worker and its tests. So today the only caller would be a hostile one. Options: delete the message until §6.3 has a real client, or require the DO to have independently observed the opponent leave *during* a match it also saw start. - **(b) No rate limit on `game-result`/`game-forfeit`.** `POST /games/scores` is capped at 300/hour; the two socket messages are uncapped. Each unpaired report parks a `result:` key in DO storage until the ~5-minute alarm sweeps it, and that storage belongs to the **single global presence object** (`idFromName("global")`) that carries everyone's online/offline status — whose sweep does an unbounded `list()` every tick. A loop degrades presence for all users. Forfeits are worse: each one writes a permanent `game_matches` row with no cap at all. Fix is small — a `checkRateLimit` on both message types plus a per-account ceiling on pending slots. - **Not bugs (checked and correct):** the both-players-agree flow is safe against simultaneous reports (Cloudflare's input gates serialise the read-then-write, traced both interleavings), retries are idempotent on both sides of settlement, and the score upsert's tiebreak timestamp only moves on a genuine improvement. -- [ ] `feature` `#marketplace` `#catalog` **314 Docker MCP listings are browsable but not installable — and the blocker is acquisition, not MCP support** (added 2026-08-31) - **Correcting a claim made loosely in conversation:** YouCoded *fully supports MCP servers* — client, manager, registry, per-tool permissions, `safeStorage` secrets and synced `mcp.json` (`desktop/src/main/harness/mcp/`) — and installing a plugin that bundles one wires it up automatically (`skill-provider.ts` calls `reconcileMcp()` after install). The gap is narrower: **the installer only knows how to clone a git repo**, and these rows have none — their payload is a container image (`sourceRef: docker:mcp/brave-search@sha256:…`), so `installPlugin` answers "unknown source type" and Task 21 correctly shows "Open source". - The shape fits: a stored server needs id + label + transport + secret refs; supported transports are `stdio` (command + args) or `http`; `docker run -i mcp/` is a valid stdio command; and the catalog row already knows Brave Search needs `BRAVE_API_KEY` because the scanner derived it. The obstacles are practical — **it needs Docker on the user's machine** (heavy for a non-technical audience, and the honest reason this was deferred), a first-run key prompt rather than a silent failure, and the cards stay "Not checked" because we hold metadata only. Depth and the Home Assistant analysis: `docs/active/investigations/2026-08-31-marketplace-featuring-recommendations.md` §3. -- [ ] `bug` `#marketplace` `#catalog` **The ingest ignores `sourceGitRef`, so a plugin shipping from a non-default branch is scanned against the wrong code** (added 2026-08-31) - The ingest scans a repo's **default branch** and ignores `index.json`'s `sourceGitRef`. Four live entries name something else — `netsuite-ai-companion`, `netsuite-finance-analyst`, `netsuite-suitecloud` (all `ai-plugins-dist`) and `42crunch-api-security-testing` (`v1.5.5`). Their folders do not exist on the default branch, so the fetch returned an empty file list, the scan found nothing suspicious in nothing, and stamped `checked`. That is the exact thing `.claude/rules/catalog.md` forbids — *never `checked` without having read the files* — and it is the worst direction to be wrong in. - **wecoded-marketplace#77 stops NEW ones** (an unreadable tree now forces `unchecked`, guarded by a test), **but cannot clear the existing ones**: rule 1 says an incoming `unchecked` never overwrites a stored `checked`, which is right in general and exactly wrong here. Confirmed after that fix deployed and a full re-ingest ran: **netsuite still 14 of 14 `checked`** in production. `--force-rescan` will not help either — it blanks the skip key, not the merge. - **The false verdicts were cleared 2026-08-31** — 19 rows deleted from production D1 (all 19 were falsely `checked`; the pattern touched nothing else, verified by a `SELECT` first), and the next ingest rebuilt them honestly: **13 netsuite rows now `unchecked`, and 42crunch came back genuinely `checked` with real capabilities** (its files ARE readable on the default branch). So no listing currently claims a safety verdict nobody earned. - **What remains is the root cause:** the ingest still ignores `sourceGitRef`, so those 13 netsuite rows will read "Not checked" forever even though their code is perfectly scannable on `ai-plugins-dist`. 84 live entries record a `sourceGitRef`; 4 name something other than `main`/`master` and are the exposed set. Fix = resolve the ref when present. Note the deletion trick is NOT a general remedy — rule 1 (an incoming `unchecked` never overwrites a stored `checked`) will protect any future false verdict the same way, which is correct in general and wrong in exactly this case. -- [ ] `bug` `#marketplace` `#catalog` **The "What this can do" panel under-reports the most capable plugins** (added 2026-08-31) - Measured on the live catalog: `github`, `playwright`, `serena` and `context7` all declare `hasMcpConfig: true` yet list a single `adds` capability — no shell, no network, no key. Root cause is shared with the scan gap: they are Anthropic `local` rows whose files were never fetched, so neither the scan verdict nor the capabilities could be computed. wecoded-marketplace#77 fixes the fetch for those 126 rows, so **re-check this after the next full rescan** — it may already be resolved. What is NOT covered by that fix: `desktop-commander` is `checked`, wraps a terminal-command server, and still declares only `network` + `adds`. **The failure direction is the dangerous one** — the most powerful plugins read as the most harmless, on the panel users are told to read before installing. -- [x] `bug` `#marketplace` `#catalog` **One plugin emits two listings with the same id, so its TYPE flips between runs** — **FIXED 2026-08-31, wecoded-marketplace#77** (first kind declared wins; duplicate ids 1 → 0 in production) (added 2026-08-31, found while proving the star-churn fix) - `claude-security` lists the name `claude-security` under **both** `components.skills` and `components.agents` in `index.json`, so `sources/wecoded.mjs` emits `claude-security/claude-security` twice — once as `itemType: "skill"`, once as `"specialist"`. Both land in the same upsert and the last one wins, and which is last depends on the 500-row batch boundaries, which shift run to run as the skip set changes. Measured: it is the **only** row still differing across an otherwise-quiet ingest run (`catalog.itemType` was the sole field that moved), and `rg`-checked across all 302 live entries — **exactly one plugin** has a name in two component lists. Small (1 listing of 4,156) but it means a listing that cannot decide what kind of thing it is, plus an occasional pointless catalog-version bump. Fix: dedupe member ids per bundle in `normalise` — first kind wins, or disambiguate the id — and pin it with a fixture that has the collision. -- [ ] `bug` `#themes` `#ui` **Provider brand colours are still unreadable on the four LIGHT community themes** (added 2026-08-31, residue of youcoded 992f7228/13f6e356) - Measured across 11 brand colours x 7 published community themes = 70 combinations: **55 unreadable before the mode fix, 25 after.** The 30 repaired are all the DARK themes, which were being served the light set — the actual bug — and now clear 5.65:1. The 25 remaining are Kuromi Dreamer, Cotton Candy Sky, Meadow Mist and Strawberry Kitty, which were never mis-served: they already got the light set. Those values were tuned against the built-in Light theme's `#EAEAEA`, and a pale tinted panel is simply a different background (Kuromi's worst is `--brand-claude` at 3.19:1 on `#D4C5E6`). Two candidate fixes, neither in scope when this shipped: darken the light set, which **would restyle the built-in Light and Creme themes** and break the "nothing moves on your themes" promise Destin approved on the 2026-08-31 review deck; or derive each brand colour against the live `--panel` at runtime (theme-engine already has `contrastRatio`/`mixHex`), which fixes every present and future theme but makes the colour no longer a fixed brand value. **Guarded meanwhile** by `desktop/tests/brand-colour-modes.test.ts`: dark community themes must clear 4.5:1, the residue may not grow past 25, nothing may drop below 3:1. -- [x] `bug` `#marketplace` **The "Installing…" spinner has never appeared for a plugin install** — **FIXED 2026-08-31, youcoded#368** (both sides now build the key through one `installTrackingKey` helper; the card was the outlier — the footer strip splits on the prefix to pick a registry) (added 2026-08-31, found while wiring the Update button) - `MarketplaceCard.tsx` computes `installKey = kind === "theme" ? \`theme:${item.entry.slug}\` : item.entry.id`, but `marketplace-context.tsx` marks progress under `` `skill:${id}` `` in both `installSkill` and its uninstall counterpart. The two strings can never match, so `mp.installingIds.has(installKey)` is always false and a plugin install shows no progress at all — the card sits inert until the install finishes and the state flips to Installed. **Themes are unaffected** (both sides use `theme:`), which is why this survived: the one path anyone demos looks right. One-character-class fix (prefix the plugin key), but it changes visible behaviour on every card, so it wants its own commit and a look. Not fixed inside the overhaul, which was scoped to the Update action. -- [x] `bug` `#marketplace` `#android` **Android reports a prompt update it never performed** — **FIXED 2026-08-31, youcoded#368** (mirrors desktop; also mints a `user:` id, which Android never did, so share-link prompts were unaddressable) (added 2026-08-31) - `app/src/main/kotlin/com/youcoded/app/skills/LocalSkillProvider.kt` handles a prompt update by calling `configStore.updatePackageVersion(id, …)` and returning success. It never attempts to rewrite the prompt's content, and the install path records no package either. Desktop had the same false success and it was fixed in the overhaul (Task 2: `updatePromptSkill` returns false on a miss and the caller reports the failure); the Kotlin half was outside that task's file list and no task covers it. **This becomes user-visible the moment the catalog ships**, because the catalog introduces **257** cursorrules prompt entries where the registry currently has zero live ones — so a path nothing exercises today becomes a common one. Fix = mirror the desktop shape: keep the marketplace id on install, record a package, and return an honest failure when the row is not there. -- [x] `bug` `#marketplace` `#android` **Android cannot record which commit it installed** — **FIXED 2026-08-31, youcoded#368** (`runGit` returns output; records the sha git reports, never the catalog's) (added 2026-08-31) - Desktop's Task 17 runs `git rev-parse HEAD` after pinning and stores the full sha on `PackageInfo.commit`; the renderer then treats "recorded commit differs from the catalog's `sourceCommit`" as an update being available, alongside the version-string compare. Android's `PluginInstaller.runGit` returns a bare `Boolean` and never surfaces git's output, so there is nothing to record without changing that signature at every call site. **It degrades rather than lies** — with no commit recorded the commit compare contributes nothing and Android falls back to version strings, exactly as the plan designs for. Worth noting the tempting wrong fix: recording the *catalog's* `sourceCommit` instead of the one actually checked out would always compare equal and silently never badge. -- [x] `bug` `#marketplace` `#tooling` **Featuring a theme validated against a stale 2-of-7 registry** — **FIXED 2026-08-31, youcoded-admin#6** (`a4d9e8c`); reads the live theme registry, and a missing index now warns instead of silently dropping a whole category. All 7 themes validate. `wecoded-marketplace/themes/index.json` is now safe to delete, which it was not before (added 2026-08-31) - `youcoded-admin/skills/feature/scripts/edit-featured.js:29,59` builds its set of valid slugs from `wecoded-marketplace/themes/index.json` — a file generated **2026-04-07** holding only `golden-sunbreak` and `halftone-dimension`. The live registry (`wecoded-themes/registry/theme-registry.json`) has 7. So `requireSlug` rejects `cotton-candy-sky`, `devils-garden`, `kuromi-dreamer`, `meadow-mist` and `strawberry-kitty` with "not in skills/themes index". **This is also why the obvious cleanup is a trap:** the 2026-08-30 catalog plan proposed deleting `themes/index.json` as a dead duplicate, and `addEntries` skips a missing file silently — deleting it would drop *every* theme from validation and make featuring any theme fail. Do it in this order: point `edit-featured.js` at the real registry (it needs a second root, since the themes clone is a sibling of the marketplace clone), **then** delete the stale file. -- [x] `bug` `#marketplace` `#catalog` **53 Anthropic plugins can never be scanned** — **FIXED 2026-08-31, wecoded-marketplace#77** (read from Anthropic's own repo; production `checked` 2,697 → 2,823) (added 2026-08-31, measured on the first real ingest dry run) - They are recorded with `sourceType: "local"` and a `sourceRef` like `./plugins/agent-sdk-dev` — a path inside **Anthropic's** repository, not ours. The ingest's `local` branch reads from our own checkout, finds nothing, and correctly reports `unchecked` (it must never stamp `checked` for files it did not read). That is 53 of the 54 unchecked bundles; the 54th is `youcoded-core`, which is mid-deprecation. Fix = teach `fetchFiles` to treat those as `git-subdir` against `anthropics/claude-plugins-public`. Until then the grey shield on those cards is accurate but permanent, which is worth knowing before reading anything into the checked/unchecked ratio. -- [x] `chore` `#worker` **`ratings/routes.ts` parses with `c.req.json()`** — **FIXED 2026-08-31, wecoded-marketplace#77**; the premise was wrong, `reports` and `auth` did too, all three switched (added 2026-08-31) - Malformed input becomes an unhandled throw surfacing as a 500 rather than a clean 400. `installs/routes.ts` was switched during the catalog work; `rg -n "c.req.json" worker/src/` now returns `ratings/routes.ts:33` plus `app/routes.ts`, which is the documented deliberate exception. One-line change, left out only because the catalog plan scoped it to installs. -- [x] `chore` `#worker` `#tooling` **Wrangler is on v3.114.17 and warns on every invocation that v4 is out** (added 2026-08-31) **DONE 2026-09-01** — wecoded-marketplace PR #80 — wrangler ^4.128 (with workers-types 5, vitest 4, pool-workers 0.22); deployed. - Deliberately NOT bundled with the catalog service: upgrading the deploy tool in the same change as a new D1 migration, a new KV binding and a new public route means a failed deploy cannot tell you which one broke it. Its own small change, after the catalog is live and known good. -- [ ] `bug` `#tests` **~100 fixed sleeps still stand in for signals, and one test cannot survive 8 concurrent suites** `#tooling` (added 2026-08-28, the deliberate residual of youcoded#362/#363) - Twelve causes of intermittent failure were fixed and the suite now passes **6 concurrent full runs, 6/6, zero unhandled errors** (Desktop CI green on ubuntu/macOS/Windows at `0371c265` — the first green since 2026-08-16, verified against the run history, not assumed) — but two things were left, on purpose, and both are judgment calls worth revisiting rather than facts to accept. - (1) **~100 `await new Promise((r) => setTimeout(r, N))` remain across the suite** (36 in `native-session-host.test.ts` alone). Not all are waits — some legitimately let time pass, and one in `transcript-watcher.test.ts` is a NEGATIVE assertion whose whole job is a bounded pause (raising it turned 250ms into 15s and made that file 6x slower, so it was reverted). But each one that stands in for a real signal is a latent version of the steer-test bug fixed in #363. Converting them wholesale and blind would be more dangerous than leaving them; convert the ones you touch. The pattern is the first invariant in `.claude/rules/test-suite-hygiene.md`. - (2) **`mcp-startup-wiring.test.ts` exceeds even the 30s budget at EIGHT concurrent full suites** — it `await import()`s all 3,906 lines of `ipc-handlers.ts` inside the test body. The import genuinely cannot be hoisted: the file's `os` mock is a closure over a per-test temp dir, so a static import would evaluate before that dir exists. Eight concurrent suites is far past any real scenario (CI runs one, a developer might run two) and six is green, so this was left rather than restructured on speculation. If it ever fails on real CI, the fix is to restructure the mock so the import can move to module scope — not to raise the number again. - Evidence and measurements for all of it: `docs/testing-under-load.md`. - **New evidence 2026-08-31 (youcoded#366, the marketplace overhaul):** the macOS leg went red **three times on one unchanged tree**, with a different victim each run — `native-session-host.test.ts` (`ENOTEMPTY … rmdir .../.youcoded/sessions`, a teardown race), then `sync-spaces-engine.test.ts > debounces file changes`, then `sync-spaces-engine.test.ts > emits error events`. **Ubuntu and Windows passed all three times**, and the branch touches none of those files. The last two are the *exact pair* `tests/sync-spaces-engine.test.ts:27-32` already names from beta run 29701441150 on 2026-07-19, where the same alternation was diagnosed as a too-tight budget rather than a broken assertion — so that diagnosis is confirmed, and the 2026-07-19 fix did not fully close it. The file was run 5x locally: 19/19 green every time, which is the point — it only fails under a loaded parallel pool. Worth noting the likely trigger: the overhaul adds ~40 tests, and on a budget this marginal any added load tips it. The macOS **Build** step never ran on those three attempts (it is gated behind tests), so macOS packaging is unverified for that PR — Ubuntu and Windows both built. -- [ ] `bug` `#conversation-store` **A metadata-only save keeps a session's OLD last-used model** — `store.upsert({ id, provider, lastUsedModel })` with no `lastActive` (the shape sent after a model swap) leaves the previous model on the record: the overlay carries `lastUsedModel` into `mergeRecords`, but the EPOCH `lastActive` makes the incoming side lose wholesale, and the field is deliberately NOT in the post-merge local-truth re-apply block (`desktop/src/main/conversations/conversation-store.ts` ~:275–300 — the inline comment says it should "compete on activity", which is exactly why it never lands). Red repro on branch `test/last-used-model-pin` → `desktop/tests/conversation-store-last-used-model-upsert.test.ts` (expected `claude-opus-4-7`, got `claude-sonnet-4-5`; rescued 2026-08-27 from an untracked file that sat in the main checkout since 08-16). Fix = decide whether last-used model is local truth (re-apply post-merge like `title`) and flip that test green (added 2026-08-27) -- [ ] `bug` `#native-runtime` `#pricing` **The cost self-check dilutes a per-model error across a model swap** — Task 30 (2026-08-27) accumulates our cost figure and OpenRouter's own reported cost across the whole session and compares the sums, which is what makes the check fire at all on cheap models (a single cheap turn is always below the comparison floor). But the sums are not keyed by model: 100 correctly-priced turns on model A plus 2 badly-priced turns on cheap model B keeps the overall ratio under the 5% threshold, so the fault on B is never reported. The code acknowledges the related NAMING problem (it logs `modelOfLatestTurn`, since the sums can span a swap) but not the dilution. Fix shape: key `SessionCostTotals` by model id and compare per model, keeping the whole-session sum as well. Inherent residual either way, worth recording: a systematic error occurring only inside partially-reporting turns is checked at neither the turn nor the session level, because those turns are dropped from both sides to keep the comparison honest. (added 2026-08-27) -- [ ] `bug` `#native-runtime` `#pricing` **The session-cost chip is systematically LOW once a session starts compacting, and the new self-check cannot see it** — `generateSummary` (`desktop/src/main/harness/harness-session.ts` ~1440) issues its own `streamText` and deliberately never awaits `result.usage` (comment at ~1467, from `52ec8f73`, 2026-07-16 — pre-existing). It is not a `runStreamOnce` step, so its tokens enter neither `turnUsage` nor the provider-cost sum. Both sides of the Task 27 comparison exclude it identically, so the checker stays honest — **but a clean bill from that checker must never be read as "the chip matches the invoice"**, because a real provider bill DOES include the summarize call. Measured shape of the error (2026-08-27 review): compaction fires at 75% of context (`compactionConfig.triggerRatio`) and each summarize sends up to 60% of the window as input, so on a 200k-context Sonnet-class model that is ~120k uncounted input tokens ≈ $0.36 **per summarize event**. Zero error until the first compaction, then a step function — a long session summarizing five times is ~$1.80 low, roughly **25% low on a chip reading $5**. Fix shape: await and accumulate the summarize call's usage into the turn it belongs to (or into a session-level bucket), so both our figure and the provider comparison include it. (added 2026-08-27) -- [x] `chore` `#harness-eval` `#pricing` **Retire `MEASURED_ROSTER_SPEND_USD = 3.46` — the harness evaluator can now report OpenRouter's own per-request cost instead of a hand-copied biller total** — that constant's own comment (`desktop/src/main/harness/eval/estimate.ts` ~202-226) admits it is "a rough CALIBRATION ANCHOR, not an input to the maths" and that "the direction of the error is UNMEASURED", because per-round figures were never recorded separately. Task 27 (2026-08-27) added `openRouterCostExtractor` in `desktop/src/main/harness/pricing.ts`, which reads OpenRouter's per-request `usage.cost` off the wire — but the evaluator deliberately does NOT go through `provider-registry.ts` (it builds its own handle in `eval/openrouter-factory.ts` so the test tool never touches Destin's encrypted keys — live-app-safety). Fix is two small pieces: add `metadataExtractor: openRouterCostExtractor` to that factory (one line), and surface the summed provider cost on `run.metrics` beside the token counts `eval/run-case.ts` already accumulates. Then a roster round reports the provider's own total per model per round — which also answers the question the constant cannot: which direction the estimate errs. Deliberately not done inside Task 27, which was scoped to the app's own pricing path. (added 2026-08-27) **DONE 2026-09-01** — youcoded PR #376 — `openRouterCostExtractor` attached; run facts print the provider-billed total; constants deleted. -- [x] `bug` `#tests` `#tooling` **Nine test files fail intermittently under full-suite load, and the set VARIES BETWEEN RUNS ON AN IDENTICAL TREE** — `subagent-view.test.tsx`, `compacting-status-singleness.test.tsx`, `active-artifact-view.test.tsx` (all three literally `ReferenceError: window is not defined`, from `performWorkOnRootViaSchedulerTask` or a `BrailleSpinner` timer), plus `mcp-startup-wiring.test.ts`, `harness-eval-orchestrator.test.ts` and `harness-review-runner.test.ts` (5s/30s timeouts). Every one passes in isolation. This is not cosmetic: a `verify.sh` run that fails for an unrelated reason trains sessions to wave failures away, and during the 2026-08-27 status-bar work FOUR separate agents each had to re-run suites in isolation to tell a real regression from noise — one of them nearly attributed another agent's genuine breakage to flakiness. Fix shape: the three `window is not defined` cases need the component's timers/renders stopped on unmount (or `vi.useFakeTimers` + explicit cleanup) so nothing is scheduled past teardown; the timeout trio needs its per-test budget raised or its setup moved out of the hot path. A SEVENTH, different in kind: `web-fetch-tool.test.ts` asserts wall-clock duration (`expected 1339 to be less than 1000`), which cannot hold under `verify.sh`'s parallel load — that one needs the timing assertion replaced with a fake clock, not a bigger budget. Two more of the timeout kind surfaced 2026-08-27 — `remote-server.test.ts` and `engine-supervisor.test.ts`, both `Test timed out in 5000ms`, both green in isolation. The clinching evidence that this is load and not code: two full-suite runs on the SAME tree minutes apart failed a DIFFERENT set of files each time. Note also that `tests/statusline-context-remaining.test.ts` (added the same day) spawns 6 short-lived child processes, so it adds a sliver of load to a suite that already times out — the variance predates it, but a fix should account for total process pressure, not just per-test budgets. (added 2026-08-27, during the status-bar session-relevance work) ****FIXED 2026-08-28 — youcoded PR #362** — all five distinct causes, each measured on the tree rather than inferred.** (1) The shared sandbox HOME is now pid-suffixed. (2) Suite-wide `testTimeout`/`hookTimeout` raised 5s→30s; measured in isolation, `remote-server` 4.9s / `engine-supervisor` 4.5s / `mcp-startup-wiring` 2.7s for the WHOLE FILE, so a single heavy test sat within a rounding error of the old per-test budget before any contention. `harness-review-runner` (25s in isolation, the heaviest file in the suite) got a named `HEAVY_RUN_TIMEOUT_MS = 120_000`, since its existing per-test 30s budget was itself measured failing at 30,289ms. (3) The three `window is not defined` cases were React scheduler work outliving jsdom teardown — fixed centrally by running testing-library's `cleanup` after every jsdom test in `setup-dom.ts`, because **18** of 113 `.tsx` files never unmounted, not the three that happened to be caught; all 142 `.tsx` suites pass with it. (4) NOT IN THE ORIGINAL ENTRY: `harness-eval` wrote into the real youcoded-dev workspace (`docs/active/investigations/`), and the tests' snapshot/restore guard could not be correct under concurrency — that was the `ENOENT … run-summary-unit-test-plan.json` failure, and it had also left five empty dated directories in the repo. Added a `YOUCODED_EVAL_RUNS_DIR` override. (5) `web-fetch-tool`'s 15 wall-clock assertions now measure `process.cpuUsage()` instead — the fake clock the entry proposed would have defeated the tests, whose whole subject is real CPU burn (2.8s–108s pre-fix); CPU time is immune to descheduling, so the 1,000ms budgets did not have to be loosened. **Result: 3 concurrent full runs failed 6 files before; 4 concurrent runs now pass 4/4, 7,429 tests each, zero unhandled errors.** Prevention: `.claude/rules/test-suite-hygiene.md`.) -- [ ] `bug` `#native-runtime` `#pricing` **Swapping models mid-turn bills the WHOLE turn at the new model's rate, and labels it with the new model's name** — `setBinding` writes `opts.pricing` / `opts.free` / the model id immediately, but a turn's `costUsd` and `model` are read at the `turn-complete` emit, so every token already generated by the old model is re-priced at the new one. **Measured, not inferred** (2026-08-27, while pinning Task 24 of the status-bar work): swapping `m-cheap` → `m-dear` ~30 ms into a still-streaming turn produced `{"n":1,"model":"m-dear","cost":70}` for a turn that should have cost 7. It also contradicts `setBinding`'s own doc comment ("next turn uses the new binding"). Needs the user to change models while a turn is streaming, and `NATIVE_SET_BINDING` (`ipc-handlers.ts` ~2597) has no guard against that — plausible whenever someone switches away from a slow local model mid-answer. Fix shape: snapshot the price card + model id at turn START and price the emit from the snapshot, rather than reading live `opts`. Deliberately not fixed inside Task 24, which was authorised only to add guards, not to change pricing behaviour. (added 2026-08-27) -- [ ] `bug` `#renderer` `#ui` **Chat panel vanished from a live session — no messages, and new sessions show no "Start a conversation" text** (added 2026-08-27, UNDIAGNOSED — Destin said ignore for now) - Reported alongside the black-glyph terminal bug below, same live app (beta.16, `ebf00c81`). Checked from outside: the chat panel uses no WebGL (only `ThemeEffects`/`SessionStrip` 2D canvases), the Chat `ErrorBoundary` would print "Chat crashed" rather than vanish, and `visible`/`sessionActive` are the same `s.id === sessionId` compare that was demonstrably working for the terminal — so no inspectable code path explains a silent blank panel. Not reproduced. Next step when Destin wants it: a screenshot of chat view on any session, and whether it survived the ~01:56 relaunch (if yes → runtime glitch, add renderer console capture to `~/.claude/desktop.log`; if no → reproduce in a dev instance). Notes in `docs/active/investigations/2026-08-27-terminal-black-glyphs-mipmap-driver.md` → "Open: the chat panel". -- [x] `bug` `#terminal` `#renderer` `#linux` **Terminal text goes solid black (glyph atlas mipmap) — the July heal re-runs the same failing upload** (added 2026-08-27; **FIXED same day — youcoded PR #333, merge `f9f39ee6`**: postinstall patch `desktop/scripts/patch-xterm-webgl-mipmap.js` applies upstream xterm.js #5987 to both addon-webgl builds, pinned by `tests/xterm-webgl-mipmap-patch.test.ts`, which also fails once a stable addon ≥ 0.20.0 is installed as the cue to delete the patch. Runtime confirmation still needs Destin's machine on a build that contains it: `journalctl --user | grep allocateMipmapLevelsForGeneration` should stay empty.) - Destin's live app (beta.16, built from `ebf00c81`, which DOES contain the `619d064a` heal) drew every terminal glyph as a black rectangle; the GPU-process journal shows 106× `GL_INVALID_OPERATION … allocateMipmapLevelsForGeneration … Unexpected driver error` from 2026-08-26 17:06 through the report, and it recurs in a freshly relaunched instance. Root cause: `@xterm/addon-webgl` 0.19.0 calls `gl.generateMipmap` after uploading each atlas page and never sets `TEXTURE_MIN_FILTER`, so a driver that rejects the mip allocation (mesa-git radeonsi here; upstream lists other Linux+Wayland stacks) leaves the texture *incomplete*, which WebGL samples as opaque black. `clearTextureAtlas()` re-uploads → fails again, so the heal cannot recover. **Upstream fixed it in xterm.js PR #5987 (2026-06-03: drop `generateMipmap`, set MIN/MAG filter to `LINEAR`) but no stable release carries it** (0.19.0 is from 2025-12; the fix is only in `0.20.0-beta.*`, which needs a beta core). Recommended: a `patch-node-pty.js`-style postinstall patch of the one call site + a pinning test, removed when 0.20.0 stable ships; optionally `gl.getError()` detect-and-fall-back to the DOM renderer. Full evidence and options: `docs/active/investigations/2026-08-27-terminal-black-glyphs-mipmap-driver.md`. -- [x] `bug` `#tooling` **Two concurrent `run-review.sh` sweeps deadlock each other** — each shard's Chrome gets CDP port `30000 + offset + idx`, so two sessions sweeping at once (both at the default offset 300) overlap and hang for 20+ minutes with no error — the index runs to ~80 per sweep, so offsets 300 and 310 STILL overlap (30300–30378 vs 30310–30388; 2026-08-27: this session's Phase C after-sweep vs another session's download-resume sweep). Fix: probe each CDP port before use and skip busy ones (or derive the base from the pid), and refuse loudly when the range is occupied. Workaround: offsets at least 100 apart (`YOUCODED_PORT_OFFSET=400`). (added 2026-08-27) **DONE 2026-09-01** — youcoded-dev PR #7 — `cdp-ports.sh` picks a free 400-port block per pid; `run-review.sh --dry-run` shows it. -- [ ] `bug` `#docs` **Four knowledge files are over their word budget, so `/audit`'s mechanical pass fails every run** — `.claude/rules/artifacts.md` (741/600), `.claude/rules/chat-reducer.md` (734/600), `.claude/rules/sync-spaces.md` (651/600), `docs/PITFALLS.md` (2855/2500). Three of the four were ALREADY over before 2026-08-28 (`chat-reducer` 678, `artifacts` 741, `sync-spaces` 651); PITFALLS was at 2468 and went over from two independent same-day additions (paged history's "Removing a broadcast", 181 words, and another session's ~206). A failing budget check trains sessions to ignore the audit's output, which is the actual cost. The fix is the documented one — migrate overflow into each rule's lazy depth doc, as paged history's depth was moved to `youcoded/docs/chat-reducer.md` — not raising the limits. Not done in the shipping session because it means editing four subsystems' prose at once, most of it other work's, which deserves its own pass. One anchor also stays broken: `.claude/rules/harness-tools.md` expects `permissionSubject: () => undefined` in `send-user-file.ts` and the code no longer says that — verify which is right before editing either. (The three `engine-dependencies.md` anchors that failed the same way were repo-relative paths and are fixed in youcoded PR #351.) (added 2026-08-28) -- [x] `bug` `#ci` `#native-specialists` **Nine Windows-only test failures on master block every PR from showing green CI** — `task-tool.test.ts` (7), `harness-tool-guards.test.ts` (1), `harness-session-loop.test.ts` (1), all about specialist `work_dir` resolution and permission subjects. Present on master at `62c1f182` (the specialists 1c merge, 2026-08-26) and on every PR branched from it; Linux and macOS are unaffected, which points at path-separator handling (`${charter}:${workDir}` subjects and relative-`work_dir`-against-session-folder resolution both build strings from paths). Found 2026-08-27 while merging youcoded#331, which had to go in with `--admin` because CI cannot go green until these are fixed. **Verified NOT flaky, unlike its neighbours:** the same nine failed identically across three re-runs of one commit, while the rotating extras (`git-service.test.ts`, `native-session-host.test.ts` — both timing-sensitive on the slow Windows runner) appeared in runs 1 and 2 and vanished in run 3. Reproduce by reading the run logs; there is no Windows machine here, so the failing assertions have NOT been traced to a line. **Confirmed master-level by the cleanest possible control 2026-08-28:** youcoded PR #351 changes TWO MARKDOWN FILES and nothing else, and its Windows job failed with the same 9 in the same 3 files (7,204 passed). The same docs-only run also flaked `native-session-host.test.ts` ("steerSpecialist appends the note…", `expected [] to have a length of 1`) on **ubuntu**, not Windows — so the rotating extras are not a Windows-runner property, they are timing-sensitive anywhere under load. (added 2026-08-27) **FIXED 2026-08-28 — youcoded PR #363, merge `0371c265`. Desktop CI is green on all three platforms for the first time since 2026-08-16.** The entry's own hypothesis was right — it was path-separator handling — but the count was low and two of the failures were NOT test bugs. Actual total: **19 failures across 4 files**, because `read-pdf.test.ts` (10) was never in anyone's count. (1) **A REAL PRODUCT BUG: every PDF read fails on Windows.** `pdfjsAssetDirs()` appended `path.sep`; pdf.js validates these as "factory urls" inside `getDocument()` (`if (val.endsWith("/")) return val;` else `Invalid factory url … must include trailing slash`), so on Windows every read threw before parsing. (2) **A SECOND REAL BUG:** `workspaceMatchFor` built its RETURNED path from `canonical`, which `canonicalize()` lowercases on win32 — the "did you mean this file?" recovery answered `roadmap.md` for `ROADMAP.md`. Quiet on a case-insensitive filesystem, but that string reaches the model and the user, and git's index is case-sensitive everywhere; `toPosix`'s own doc comment already warned against exactly this. (3) The 8 remaining were POSIX-only assertions, now resolved the way the tool resolves them. **The lesson, recorded because it is the expensive part:** the guard for the PDF bug had to be a SOURCE SCAN, not a behavioural assertion — `path.sep` *is* `/` on POSIX, so a contract test is vacuous here and the only machine that could ever see the bug was the CI leg being ignored. It sat there twelve days. Full write-up: `docs/testing-under-load.md`. -- [ ] `bug` `#tooling` `#tests` **`workbench-boot-check.mjs` reports "All 12 workbench routes mount cleanly" when NOTHING is serving the port** — so a green boot check does not prove the app mounted. Verified 2026-08-27: with no listener on 5233 (confirmed by `ss -ltnp`), and again against a deliberately dead port (`node scripts/workbench-boot-check.mjs 5999`), it printed `ok` for all 12 routes and exited 0. The script's own premise is that the unit suite passed while the app crashed at boot three times; a check that cannot distinguish "mounted fine" from "server absent" restores exactly that blind spot, and CLAUDE.md calls running it after any mock-shim change non-optional. Likely cause: it counts console errors on each route, and a failed navigation produces no page-level console error to count — so zero errors reads as success. Fix shape: assert the navigation actually resolved (`Page.navigate` frame result / a non-4xx-5xx response) AND that a known root element exists in the DOM before scoring a route `ok`; fail loudly when the port refuses a connection. (added 2026-08-27, hit while building the download-resume UI) -- [ ] `feature` `#ui` `#local-models` **Decide the app-wide GB convention: binary (1024^3) everywhere, or decimal where the source is decimal** — Destin flagged 2026-08-27 that the Local Models row reads `74.2 GB` for a download whose Hugging Face page says `79.7 GB`. Both describe the same 79,674,559,677 bytes; `gb()` in `LocalModelsSection.tsx` divides by 1073741824 (what the OS file manager does) while HF divides by 1e9 (what drive vendors do). Deliberately NOT changed inside the download-resume work: every other size in the app already uses the binary form, so changing one screen would make it disagree with the rest. The decision is app-wide and needs an inventory of every size-rendering site first (`gb(`/`mb(` helpers plus any ad-hoc `/ 1024` maths), then one rule — candidates: (a) keep binary everywhere and never show a vendor number, (b) keep binary but label it `GiB` where a remote source is quoted alongside, (c) show the source's own number when echoing a remote catalogue and binary for on-disk sizes. (a) is the smallest change; (c) is the most honest but means two conventions living side by side, which is how this confusion starts. (added 2026-08-27, deferred out of the download-resume design pass) -- [x] `bug` `#ui` `#desktop` **`