diff --git a/.claude/commands/audit.md b/.claude/commands/audit.md index 7e444a79..a0db35e8 100644 --- a/.claude/commands/audit.md +++ b/.claude/commands/audit.md @@ -87,8 +87,8 @@ Work every finding in the same run: - **Decision-residue** (privacy copy, LICENSE text, deleting user-created content, product-behavior questions) — never auto-edit; goes to the report's `## Residue` with a recommendation. -- Drift genuinely unfixable this session → ROADMAP `bug` line tagged `#docs` AND a - residue entry. +- Drift genuinely unfixable this session → an entry in `docs/roadmap/dev-workspace.md` + under `## knowledge` (`ROADMAP.md` → "Filing an item") AND a residue entry. ### 5. Roadmap verification @@ -109,9 +109,7 @@ under `## Roadmap`. Then: Dedup near-identical items across area files by hand (one entry, one report, keep the older date in the report's history line). Filing rule and grammar: the bottom of -`ROADMAP.md` and the spec, `docs/active/specs/2026-09-01-roadmap-restructure-design.md` §2. -Until the migration lands (`docs/roadmap/` absent) the tool prints one line and exits 0 — -do the old manual check against `ROADMAP.md` in that case. +`ROADMAP.md` and the spec, `docs/archive/specs/2026-09-01-roadmap-restructure-design.md` §2. ### 6. Gardening (the anti-rot pass) diff --git a/.claude/hooks/roadmap-edit-check.mjs b/.claude/hooks/roadmap-edit-check.mjs index a7e067d9..cd951cd5 100644 --- a/.claude/hooks/roadmap-edit-check.mjs +++ b/.claude/hooks/roadmap-edit-check.mjs @@ -36,7 +36,7 @@ const r = spawnSync(process.execPath, [script, '--structure', '--quiet', '--root if (r.status === 0) process.exit(0); process.stderr.write( 'roadmap-check: the roadmap file you just wrote has structure errors — fix them now ' - + '(entry grammar: docs/active/specs/2026-09-01-roadmap-restructure-design.md §2; ' + + '(entry grammar: docs/archive/specs/2026-09-01-roadmap-restructure-design.md §2; ' + 'filing rule: the bottom of ROADMAP.md)\n' + (r.stdout || '') + (r.stderr || ''), ); process.exit(2); diff --git a/.claude/rules/android-runtime.md b/.claude/rules/android-runtime.md index 9d7d75c3..1f9121db 100644 --- a/.claude/rules/android-runtime.md +++ b/.claude/rules/android-runtime.md @@ -1,7 +1,7 @@ --- paths: - "**/app/**" -last_verified: 2026-07-15 +last_verified: 2026-09-01 verify: - path: youcoded/app/src/main/kotlin/com/youcoded/app/runtime/DirectShellBridge.kt contains: "no 600ms Enter-split here" @@ -27,12 +27,12 @@ Claude Code (a Node CLI) runs inside a Termux-derived environment. **Full contex ## Exec permissions & git auth - **`~/.claude-mobile/exec-wrappers/*` must be chmod 0755, not 0700.** Java's `setExecutable(true)` gives 0700 under Android's 0077 umask; shebang exec via `/system/bin/sh` then fails EACCES (stricter than a direct linker64 invoke) — breaks `gh` spawning `git`. Fix: `setReadable(true,false)` + `setExecutable(true,false)` in `deployBashEnv()`. Don't "tighten" back — wider perms stay inside the uid-isolated app sandbox. -- **Git HTTPS auth uses `~/.netrc`, NOT `gh auth setup-git`** (Go's raw-syscall exec can't traverse the exec-wrapper path). The OAuth token is mirrored into `~/.netrc` (mode 0600) by `Bootstrap.syncGhTokenToNetrc()` at session-start + the `gh` wrapper's `_youcoded_sync_gh_netrc` post-hook. Add any new gh-auth-changing command to that hook's case list. **Do NOT reintroduce `gh auth setup-git` anywhere** — it fails silently or EACCES. +- **Git HTTPS auth uses `~/.netrc`, NOT `gh auth setup-git`** (Go's raw-syscall exec can't traverse the exec-wrapper path). The OAuth token is mirrored into `~/.netrc` (mode 0600) by `Bootstrap.syncGhTokenToNetrc()` in first-run `Bootstrap.setup()` + the `gh` wrapper's `_youcoded_sync_gh_netrc` post-hook. Add any new gh-auth-changing command to that hook's case list. **Do NOT reintroduce `gh auth setup-git` anywhere** — it fails silently or EACCES. - **`gh auth login --web` polling is flaky — retry once** if it dies "error connecting to github.com" (Go HTTP/2 on Android's stack, ~1-of-3 success in the wild). Don't wrap a retry in `gh()` (double-prompts a new device code). ## Build-type parity (R8) — guard: `./gradlew :app:assembleReleaseTest` (CI: `android-ci.yml`) - **Release enables R8 minification; debug skips it — they are NOT equivalent.** **Don't use string-based reflection against your own code** (`getMethod`, `Class.forName`, `KClass`, `::declaredMembers`) — R8 obfuscates the name and the lookup throws. The `PluginInstaller.buildEnv()` reflection bug (`912f5ca7`) shipped a stripped env without `LD_PRELOAD` in release — every marketplace install died — while every dev/CI build was debug. Direct calls always; unavoidable reflection needs an explicit `-keep` in `proguard-rules.pro`, never a silent `try{reflection}catch{fallback}`. -- **`Bootstrap` has a defensive `-keep` rule** — don't remove without an audit confirming nothing reflects against it. **`assembleReleaseTest`** (same R8 config, debug keystore, `.releasetest` suffix, port 9961) is the parity check — run it before tagging after touching reflection/annotation/symbol-name-dependent code. Android workflows `setup-node@v4` explicitly so `bundleWebUi` doesn't depend on the runner image's node. +- **`Bootstrap` has a defensive `-keep` rule** — don't remove without an audit confirming nothing reflects against it. **`assembleReleaseTest`** (same R8 config, debug keystore, `.releasetest` suffix, port 9961) is the parity check — run it before tagging after touching reflection/annotation/symbol-name-dependent code. Android workflows `setup-node@v7` explicitly so `bundleWebUi` doesn't depend on the runner image's node. ## PTY writes are NOT symmetric across the two bridges - **`PtyBridge.writeInput` keeps the 600 ms split before Enter; `DirectShellBridge.writeInput` deliberately does NOT — never "parity fix" it.** The split works around Ink's 500 ms `PASTE_TIMEOUT` in Claude Code's TUI; `DirectShellBridge` talks to raw bash, which has no paste-mode timing. The shared-env rule above is about `buildRuntimeEnv`/`deployBashEnv`, not write timing. *Guard:* the WHY comment at `DirectShellBridge.writeInput`. diff --git a/.claude/rules/artifacts.md b/.claude/rules/artifacts.md index a626f229..12557388 100644 --- a/.claude/rules/artifacts.md +++ b/.claude/rules/artifacts.md @@ -8,7 +8,7 @@ paths: - "**/desktop/src/renderer/state/artifact-tool-use-tracker.ts" - "**/desktop/src/renderer/state/ArtifactContext.tsx" - "**/desktop/src/shared/artifacts/**" -last_verified: 2026-08-30 +last_verified: 2026-09-01 verify: - test: youcoded/desktop/tests/artifacts/artifact-tool-use-tracker.test.ts - path: youcoded/desktop/src/main/artifacts/artifact-store.ts @@ -67,7 +67,7 @@ Per-project sidecars + a central index track every file Claude touches; I/O is m ## Paths & counts - **Project list = saved folders (`youcoded-folders.json`), NOT the central index.** `buildSavedFolderProjects` reuses an index entry by canonical path, else synths one whose `id` IS the path (traversal-guarded). -- **Two single-source count helpers:** `countArtifacts` vs `countAllFiles` — never recompute inline (282-vs-1209 drift). Both subtract orphans via ONE cwd-keyed cache (`useMissingArtifacts.ts`), never cleared before its replacement lands. +- **Two single-source count helpers:** `countArtifacts` vs `countAllFiles` — never recompute inline. `countArtifacts` drops orphans (`fs.access`); `countAllFiles` is raw discovery (`artifacts/projects-index.ts`). ## Concurrency - **`casWrite` uses a mkdir-based lock** (bare CAS = TOCTOU data loss); central-index writers use `mutateFileUnderLock`. `appendVersion` retries CAS 5× — never add a second loop. @@ -85,4 +85,4 @@ Per-project sidecars + a central index track every file Claude touches; I/O is m - **Drawer state is per-session keyed by `sessionId`**, labels SESSION-scoped; layout-level, not an overlay. Status glyphs (`●◐○`) BANNED. `.youcoded/` auto-gitignored. - **`showDeletedArtifacts` is SESSION-DRAWER-ONLY — deliberate** (a tombstone, not a recovery path). Cross-device-SYNCED — don't delete the "unused" flag. - **`EXCLUDE` has NO renderer caller** (legacy round-trip only); in-folder files can't be excluded. -- **Android `get`/`save`/`read-binary` are REAL (SessionService.kt), NOT stubs — mirror any new desktop guard in Kotlin.** List/project/check-existence return `not-implemented-on-mobile`; `project:*` is desktop-only. +- **Android `get`/`save`/`read-binary` are REAL (SessionService.kt), NOT stubs — mirror any new desktop guard in Kotlin.** List/project/import-file/search-content/watch-project return `not-implemented-on-mobile`; `check-existence` stubs "nothing missing"; `project:*` is desktop-only. diff --git a/.claude/rules/engine-local-models.md b/.claude/rules/engine-local-models.md index 0c2f6dae..eeebd504 100644 --- a/.claude/rules/engine-local-models.md +++ b/.claude/rules/engine-local-models.md @@ -3,7 +3,8 @@ paths: - "**/desktop/src/main/engine/**" - "**/desktop/src/main/models/**" - "**/desktop/test-engine/**" -last_verified: 2026-08-16 + - "**/desktop/src/main/providers/provider-registry.ts" +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/main/engine/engine-supervisor.ts contains: "models-dir" @@ -21,7 +22,7 @@ verify: # Local llama.cpp engine + model manager (Plans B + C) -A downloaded, SHA-256-verified `llama-server` in router mode + supervised, plus the in-app model manager (curated catalog, HF search, resumable downloads, GPU-aware fit). **READ `youcoded/docs/engine-dependencies.md` first — every fact below is verified there against b9992. Re-run `test-engine/probe-*.mjs` on every engine bump (any new probe MUST pass `--models-dir`).** +A downloaded, SHA-256-verified `llama-server` in router mode + supervised, plus the in-app model manager (curated catalog, HF search, resumable downloads, GPU-aware fit). **READ `youcoded/docs/engine-dependencies.md` first — every fact below is verified there against b10665. Re-run `test-engine/probe-*.mjs` on every engine bump (any new probe MUST pass `--models-dir`).** ## Engine (Plan B, `src/main/engine/`) — guards: `engine-supervisor.test.ts`, `engine-acquisition.test.ts`, `test-engine/probe-{health,models,chat}.mjs` - **`--models-dir ` discovers GGUFs — NOT `LLAMA_CACHE`** (vestigial; only `-hf` auto-downloads). Covers bring-your-own GGUFs AND Plan C downloads. Without it, `GET /models` is empty and every completion is 400 `model not found`. Router id = filename minus `.gguf` (== `cache-scan.ts`). @@ -33,11 +34,11 @@ A downloaded, SHA-256-verified `llama-server` in router mode + supervised, plus ## Model manager (Plan C, `src/main/models/`) — guards: `model-downloader.test.ts`, `test-engine/probe-download.mjs` - **Flat-basename cache naming is a probe-pinned contract, single-file AND multi-part** — `model-downloader.ts` writes each HF file under its BASENAME; `probe-download.mjs` asserts the router lists + serves both ids. NEVER rename downloads or change split-part naming without re-running it. -- **Curated list carries NO baked sizes** — the panel computes size + fit LIVE from `models.quants(hfRepo)` (lazy per tier, per-card `loading|ready|unavailable`). Remote list is `schemaVersion`-gated with a shipped-copy fallback. Don't re-add baked sizes. +- **Curated list carries NO baked sizes** — the panel computes size + fit LIVE from `models.quants(hfRepo)` (lazy per tier, per-card `idle|loading|error`). Remote list is `schemaVersion`-gated with a shipped-copy fallback. Don't re-add baked sizes. - **Fit is GPU-AWARE with a safety bias** — VRAM only UPGRADES a verdict, and only for a confidently-probed DEDICATED GPU; integrated GPUs fall back to RAM-only. Windows uses registry `qwMemorySize` / `nvidia-smi`, NEVER `Win32_VideoController.AdapterRAM` (caps at 4 GB). - **The quant parser DENYLISTS `mmproj*` + `mtp-*` aux files and recognizes `MXFP4(_MOE)`.** Multi-part sets must be COMPLETE before download. Unrecognized tokens drop silently. - **Delete unloads best-effort, then removes every part + `.partial`.** CUDA opt-in is Windows-x64-only. `engine:set-context` restart nulls `supervisorBinary` (else `rebuildSupervisor` dedups on `binaryPath` and keeps the old `-c`). -- **`listModels()`'s K2 union is LISTING ONLY** — it merges a disk scan into the router's `GET /models` (router rows win), so a disk-only row is a selectable model the router CANNOT serve. Serveability is separate: `ensureServable` (rescan once, re-check, **fail OPEN**) at the local-send chokepoint in `provider-registry.ts`, plus `refreshModels()` after every download and delete. +- **`listModels()`'s K2 union is LISTING ONLY** — it merges a disk scan into the router's `GET /models` (router rows win), so a disk-only row is a selectable model the router CANNOT serve. Serveability is separate: `ensureServable` (rescan once, re-check, **fail OPEN**) at the local-send chokepoint in `providers/provider-registry.ts`, plus `refreshModels()` after every download and delete. - **The router re-scans `--models-dir` only when asked: `GET /models?reload=1`** — a post-boot file 400s `model 'X' not found` until then (measured 2026-08-16). **A WRITE, never a poll** — `load_models()` unloads models whose source changed or vanished. Guards: `engine-supervisor.test.ts` → "router rescan" describe, esp. "the background model poll NEVER sends reload=1". -- **Orphaned `.partial`s: `models:orphaned-partials`** lists them (in-flight excluded via `activePartialNames()`); clean via `models:delete`, resume by re-downloading the same repo+quant. Guards: `cache-scan.test.ts`, `model-downloader.test.ts`. Panel UI = ROADMAP follow-up. +- **A cancelled download keeps its `.partial`** — `models:resume` continues it, `models:delete` removes it, the panel shows a partial row (`models:orphaned-partials` is gone). Guards: `cache-scan.test.ts`, `model-downloader.test.ts`. diff --git a/.claude/rules/harness-evaluator.md b/.claude/rules/harness-evaluator.md index 51a4cbc7..a94b6adb 100644 --- a/.claude/rules/harness-evaluator.md +++ b/.claude/rules/harness-evaluator.md @@ -11,7 +11,7 @@ paths: # defects here that 4,500 passing tests missed, because every test drives a scripted # fake model and none of them spend a real turn deciding what to do next. - "**/desktop/src/main/harness/tools/**" -last_verified: 2026-08-13 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/main/harness/eval/run-case.ts contains: "askUser: async" @@ -38,6 +38,8 @@ verify: - test: youcoded/desktop/tests/harness-eval-report.test.ts - test: youcoded/desktop/tests/harness-review-fixture.test.ts - test: youcoded/desktop/tests/harness-review-runner.test.ts + - test: youcoded/desktop/tests/harness-eval-orchestrator.test.ts + - test: youcoded/desktop/tests/harness-eval-estimate.test.ts --- # Harness evaluator (`test-engine/harness-eval.mjs`) @@ -51,7 +53,7 @@ free and needs no key; a real run needs `--key-file`. if `OPENROUTER_API_KEY` is in its environment, and passes worker config over **stdin** — never argv, never env. `delete process.env.X` is `unsetenv`: in-heap only, it never rewrites `/proc//environ`, which every same-uid descendant — including a Bash call - the model makes — can read. **`review-harness.mjs` still has the bug** (ROADMAP). Guard: + the model makes — can read. **`review-harness.mjs` still has the bug** (a `decision` in `docs/roadmap/dev-workspace.md`). Guard: `harness-eval-key-leak.test.ts`, whose negative control must report LEAKED. - **The grader always loads from the orchestrator's own build; only the worker loads the @@ -72,8 +74,9 @@ free and needs no key; a real run needs `--key-file`. - **The fixture jail is held by `askUser`, not `decide`.** `decide` is fully permissive; `askUser` denies every ask that isn't a genuine `AskUserQuestion` — `external_directory`, - `doom_loop`, `max_steps`. **One path is exempt by design:** Bash's spill root - (`tools/spill-paths.ts`) is `ok`, so a model can read back its own truncated output. + `doom_loop`, `max_steps` (allowed once, `STEP_GATE_ALLOWANCE`). **Exempt by design** + (`tools/guards.ts`): Bash's spill root and `internalReadRoots` — a model may read back its + own truncated output. Guard: `harness-review-runner.test.ts` → "denies a Write outside the fixture". - **Uniform step budget, not the app's chat tiers** (25/50 cuts a 40–80-call run short), diff --git a/.claude/rules/landing-page.md b/.claude/rules/landing-page.md index d96ae94a..019f7a29 100644 --- a/.claude/rules/landing-page.md +++ b/.claude/rules/landing-page.md @@ -6,7 +6,7 @@ paths: - "**/docs/gallery/**" - "scripts/ui-review/**" - "**/desktop/src/renderer/dev/workbench/**" -last_verified: 2026-08-28 +last_verified: 2026-09-01 verify: - path: scripts/ui-review/site-assets.sh contains: "docs/media" @@ -15,8 +15,8 @@ verify: - path: scripts/ui-review/README.md contains: "Recording a loop" - path: scripts/ui-review/copy-preview.py - - path: youcoded/desktop/src/renderer/dev/workbench/mock-shim.ts - contains: "__workbenchAppearanceSync" + - path: youcoded/docs/index.html + contains: "Appearance" - path: youcoded/desktop/src/renderer/dev/workbench/reply-script.ts contains: "splitTurns" - path: youcoded/desktop/src/renderer/dev/workbench/fixture-loader.ts @@ -69,9 +69,10 @@ Switches: `?seed=none` (empty chat), `?title=`, `?model=`, `?platform=android`, **Why:** every frame still "verifies" against stale code — filmed the old fixture twice. ## The live embed -**Invariant:** the page's theme button drives the app's real Settings → Appearance; theme -changes go through `__workbenchAppearanceSync` (the app's cross-window sync), never a -reload; the iframe ignores the pointer until the visitor clicks once. +**Invariant:** the page's floating theme button clicks the app's own gear + Appearance row +inside the iframe (the swatch bar and its `__workbenchAppearanceSync` hook went in `8d077dcd`; +the hook survives only for the workbench deck); never a reload; the iframe ignores the +pointer until the visitor clicks once. **Why:** a reload flashed the poster; an interactive iframe under the wheel trapped page scroll ("janky"). @@ -84,5 +85,5 @@ never as a prose description or a still that can't show it. ## Copy and review **Invariant:** page copy is reviewed in place with `scripts/ui-review/copy-preview.py serve … [--media docs/media]` (edit text on a page-shaped preview; per-row loop verdicts) — never -a table, contact sheet, or chat description (all rejected). The never-claim list lives in the -spec's Global Constraints; the disclaimer paragraph is verbatim. +a table, contact sheet, or chat description (all rejected). The never-claim list is the audit's, +referenced from the spec's "Not in scope"; the footer's Anthropic non-affiliation sentence is verbatim. diff --git a/.claude/rules/narrow-viewport.md b/.claude/rules/narrow-viewport.md index 23f8ded2..bae5f24c 100644 --- a/.claude/rules/narrow-viewport.md +++ b/.claude/rules/narrow-viewport.md @@ -1,7 +1,7 @@ --- paths: - "**/desktop/src/renderer/**" -last_verified: 2026-07-20 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/renderer/hooks/use-narrow-viewport.ts contains: "max-width: 639.98px" @@ -23,13 +23,14 @@ features were not merely cramped but **unreachable**. **640px is the breakpoint; `useNarrowViewport()` is the source of truth.** Use the hook when the DOM structure branches, Tailwind's `max-sm:`/`sm:` when -only classes change. Don't introduce a new number. · why: four competing values +only classes change. Don't introduce a new number. (One survivor: the structural +collapse in `globals.css` is still `@media (max-width: 700px)` — pre-pass, not a licence.) · why: four competing values is what produced the unreachable states · guard: `use-narrow-viewport.ts` (`639.98px`), `OverflowMenu.test.tsx`. **Never hide a control as the narrow "fix" unless another entry point exists.** `hidden sm:block` on the gamepad made Connect 4 unreachable below 640px — -`TOGGLE_PANEL` had exactly one caller in the whole renderer, so an incoming +`TOGGLE_PANEL` had one caller in the whole renderer then (three today), so an incoming challenge could never be answered. Collapse into the `|||` menu instead. · guard: `OverflowMenu.test.tsx` (deleting a row fails it). @@ -53,7 +54,7 @@ hung outside an `overflow:hidden` box on a 390px screen. Check the child too. frame border is painted by chrome-glass, NOT by the `.frame-edge` elements (those are flex spacers). Hide the spacers and the pane paints over the border. Inset the pane with **margins**, not by un-hiding spacers: `ChatView`'s -`framed-shell` has `.frame-edge` children, `TerminalRightSlot`'s clone has none, +`framed-shell` has two `.frame-edge` children, `TerminalRightSlot`'s clone has one, so the spacer route fixes chat view and leaves terminal view broken. **Hover-only affordances have no touch path.** `opacity-0 group-hover:` never @@ -65,7 +66,7 @@ never fire on touch — don't put load-bearing copy there. current one. Reads correct either way in source; only obviously wrong in the running app. · guard: `NarrowViewToggle.test.tsx`. -Remote-specific trap: the shim overwrites `__PLATFORM__` with the **host's** -platform, so `isTouchDevice()` is false on a phone. Feature-detect +Remote-specific trap: the server sends `platform: 'desktop'` and the shim adopts it unless +`preservePlatform` is set (`remote-shim.ts`), so `isTouchDevice()` is false in a phone browser. Feature-detect (`matchMedia('(pointer: coarse)')`) rather than trusting the platform string — -see the open ROADMAP bug. +see the open item in `docs/roadmap/remote-access.md` ("the remote shim overwrites the device platform"). diff --git a/.claude/rules/native-runtime.md b/.claude/rules/native-runtime.md index 2d7d43b0..be012804 100644 --- a/.claude/rules/native-runtime.md +++ b/.claude/rules/native-runtime.md @@ -10,7 +10,7 @@ paths: - "**/desktop/src/main/providers/**" - "**/desktop/src/main/native-home.ts" - "**/desktop/src/renderer/components/native-send.ts" -last_verified: 2026-08-16 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/main/harness/harness-session.ts - path: youcoded/desktop/src/main/harness/harness-session.ts @@ -59,13 +59,13 @@ verify: ## Provider seam (Phase 0) — guard: `ipc-channels.test.ts` - **`'gemini'` is GONE** — never reintroduce it. - **`native.supported` is the ONLY gate** — a boolean, not IPC; ON by default, kill switch `YOUCODED_NATIVE=0`; remote-shim hardcodes `false`. -- **`createSession` throws for non-claude providers**; the native branch builds NO PTY worker (guard every `session.worker.X`); needs a `binding` unless resuming. +- **`createSession` throws only for a fresh native session without a binding**; the native branch builds NO PTY worker (guard every `session.worker.X`); needs a `binding` unless resuming. ## Native sessions (Plan A) — guards: `harness-session`/`native-session-host`/`native-send`/`native-home` tests -- **API keys: `safeStorage`-encrypted in `userData/native-secrets.json`, NEVER `~/.youcoded/`** (only a `secretRef`; no plaintext fallback); `~/.youcoded/` writes ride `NativeHome.mutateFileUnderLock` (THROWS on lock exhaustion). +- **API keys: `safeStorage`-encrypted in `userData/native-secrets.json`, NEVER `~/.youcoded/`** (only a `secretRef`; no plaintext fallback); `~/.youcoded/` writes ride `NativeHome.mutateJson` (→ `mutateFileUnderLock`, THROWS on lock exhaustion). - **`SessionStore` coalesces same-`partId` deltas; display-only (`session-error`, payload-less `assistant-thinking`) is NEVER persisted.** Callers serialize per session; re-entrant `send()` throws. - **`send()` never throws — synchronous `NativeSendResult`** (`'sent'|'queued'` FIFO-10 `|'failed'`, real reason); the queue drains ONLY on `send()` settle; **interrupt aborts the current turn only — the queue still drains**; `destroy()` order is load-bearing (destroy → append-chain → dispose → delete). -- **Queued messages are renderer list state, NEVER timeline**; `native:*` calls are invokes with ONE result shape on ALL transports. +- **Queued messages are renderer list state, NEVER timeline**; `native:*` calls have ONE shape on ALL transports (invokes; interrupt/retry are fire-and-forget). - **The renderer native send path skips ALL PTY machinery** (`native-send.ts`); the send string MUST equal `buildOutgoingMessage(...).content`; ESC → `native.interrupt`. ## Tool loop (`harness-session.ts`) — guards: `harness-session-loop`/`harness-history-rebuild`/`harness-sdk-toolcall-contract`/`permission-engine` tests @@ -84,7 +84,7 @@ verify: - **Two-stage compaction FAILS SAFE** — never drops a message, cuts on a USER boundary. ## Stall watchdog & the park — guard: `harness-stall-watchdog.test.ts` -- **The park is a `return` that does NOT resolve the stall race** — stage 2 emits `{stalled:true}` and returns; nothing is torn down, so a chunk arriving minutes later still lands in the loop and continues the turn. That `return` IS the feature. +- **The park is a `return` that does NOT resolve the stall race** — stage 2 emits `{stalled:true}` and returns; nothing is torn down; a late chunk still continues the turn. That `return` IS the feature. - **Check the park guard against the EXPRESSION, never prose — it has been mis-stated five times.** `!isSpecialistChild && (sawFirstChunk || turnEverParked) && !willRetry`, `willRetry = !emittedAny && isFirstAttempt`. `willRetry` tests `emittedAny`, NOT `sawFirstChunk` — tool-argument fragments set only the latter, so a first-attempt tool-args stall still auto-retries silently. - **Clock 1 stays OUT OF SCOPE** — nothing streamed this attempt and the turn never parked → still ends in the prefill `StreamStallError`. `turnEverParked` is per-TURN (cleared only at `send()` entry), so a post-park retry can never die on Clock 1. - **A specialist child must NEVER park** — `SUBAGENT_DISPLAY_TYPES` excludes `assistant-thinking`, so a parked child shows no card, its `send()` never settles, and the parent's `Task` waits forever. diff --git a/.claude/rules/react-renderer.md b/.claude/rules/react-renderer.md index 66856571..8ff8d370 100644 --- a/.claude/rules/react-renderer.md +++ b/.claude/rules/react-renderer.md @@ -1,7 +1,7 @@ --- paths: - "**/desktop/src/renderer/**" -last_verified: 2026-07-17 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/renderer/App.tsx - path: youcoded/desktop/src/renderer/components/HeaderBar.tsx @@ -34,7 +34,7 @@ This code runs in BOTH the Electron renderer AND a bundled Android WebView. **De - **No `process.env`, `require()`, `fs`/`path`/`os`, or direct filesystem access** — the WebView has no Node. Go through `window.claude.*`; use ES `import`, browser APIs, `fetch`. - **Platform detection: `location.protocol === 'file:'` = Android** — use the `remote-shim.ts` helpers, not the check inline. - **Perf:** prefer `content-visibility: auto` over virtualization; memoize every Context value; the reducer preserves `toolCalls`/`toolGroups` Map refs — don't clone them. -- **Render-path chat state goes through a cached selector, never the whole map.** `state/chat-context.ts` is a `useSyncExternalStore` store: `useChatState(id)` for one session; cached-selector hooks (`useSessionAttention`, `useActiveSessionModel`) for derived values. **`useChatStateMap()` is banned on the render path** (sole sanctioned caller: `RemoteSnapshotExporter`); **never `store.getState()` during render** (tears — add a selector). +- **Render-path chat state goes through a cached selector, never the whole map.** `state/chat-context.ts` is a `useSyncExternalStore` store: `useChatState(id)` for one session; cached-selector hooks for derived values. **`useChatStateMap()` is banned on the render path** (sole sanctioned caller: `RemoteSnapshotExporter`); **never `store.getState()` during render** (tears). ## Framed shell & chrome-glass (`globals.css`, `App.tsx`) - **ONE backdrop-filter, ever** — the frame chrome is a single `
` clipped via `clip-path: polygon()`; per-element backdrop-filters seam at non-100% zoom. @@ -44,11 +44,11 @@ This code runs in BOTH the Electron renderer AND a bundled Android WebView. **De - **The right slot holds EITHER the artifact drawer OR the games panel** — both read `var(--right-pane-width)`; `chrome-glass--drawer-open` gates on `activeDrawerOpen || gameState.panelOpen`. Don't hardcode the width. ## Theme color contrast (`desktop/scripts/audit-theme-contrast.mjs`; CI `wecoded-themes/scripts/audit-contrast.mjs`) -- **`panel` vs `canvas` ≥ 1.07:1**; `fg`/`fg-2` ≥4.5, `fg-dim`/`fg-muted` ≥3, `fg-faint` ≥1.8; `on-accent` vs `accent` ≥4.5. +- **`panel` vs `canvas` ≥ 1.07:1**; `fg` ≥8, `fg-2` ≥5.5, `fg-dim` ≥4, `fg-muted` ≥3, `fg-faint` ≥2 on five surfaces (`contrast-rules.js`); `on-accent` vs `accent` ≥4.5. - **chat-pane bg == drawer-pane bg (both `--canvas`)** — change them in the SAME edit; the audit doesn't catch a mismatch. ## Header bar (`HeaderBar.tsx`) -- **No `min-w-0` on the left cluster** (collapses below the gear's `shrink-0`); put it on an individual child. Layout is SPACE-aware (`packSessions()` + ResizeObserver) — no `@media`/`window.innerWidth`. +- **No `min-w-0` on the left cluster** (collapses below the gear's `shrink-0`); put it on an individual child. Layout is SPACE-aware (`packSessions()` + ResizeObserver) — no `@media`/`window.innerWidth`; viewport branches only via `useNarrowViewport()`. - **`showCaptionButtons` must include Linux** — frameless on BOTH; gate window-chrome on "not macOS", NEVER `navigator.platform === 'Win32'`. Announcement lives in StatusBar, not HeaderBar. ## Control primitives (`components/ui/`) @@ -56,12 +56,12 @@ This code runs in BOTH the Electron renderer AND a bundled Android WebView. **De - **Padding groups are per-axis** (`px-`/`py-` independent; `p-N` in ALL groups) — an `px-`-only override must NOT drop `py-` · guard: `Button.test.tsx` if you touch `CONFLICT_GROUPS`. ## Overlays (`components/overlays/Overlay.tsx`) -- **Use `` + ``** (or `.layer-surface` for scrimless popovers) — never hardcode scrim/blur/shadow/radius/z-index; pick a LAYER (L1 drawers / L2 popups / L3 destructive / L4 system). `SessionStrip` at `z-[9000]` is load-bearing. Glassmorphism is var-driven. +- **Use `` + ``** (or `.layer-surface` for scrimless popovers) — never hardcode scrim/blur/shadow/radius/z-index; pick a LAYER (L1–L4). `SessionStrip` `z-[9000]` is load-bearing; glassmorphism is var-driven. - **`.layer-surface` on a REPEATED element (grid tile, list row) is a paint bug** — N tiles = N backdrop-filters, and Windows Electron drops their paint per card (shipped twice: `516411a5`, `1f68a7f0`) · guard: `drawer-card-glass.test.ts`. ## Remote access state sync (`main/remote-server.ts`, `RemoteSnapshotExporter.tsx`) - **Remote clients hydrate via `chat:hydrate` on connect** — no parallel replay buffer; extend `serializeChatState`/`deserializeChatState` instead. `chat:export-snapshot` has a 2s timeout. -- **`attentionState` is authoritative on DESKTOP only** — remote browsers get `attentionMap` via `status:data` and MUST NOT run their own classifier. The shim's `attentionMap` diff is load-bearing. +- **`attentionState` is authoritative on DESKTOP only** — remote browsers get `attentionMap` via `status:data` and MUST NOT run their own classifier. App's `statusData` handler's `attentionMap` diff is load-bearing. ## UI iteration tooling - **Building or redesigning UI? `bash scripts/run-workbench.sh`** (real renderer, fake `window.claude`); `run-dev.sh` only for PTY/main-process behaviour. Unbacked channels → `MOCK_ONLY`; review under `stress`/`empty`. **After ANY shim change: `node scripts/workbench-boot-check.mjs`.** Spec: `docs/archive/specs/2026-07-29-ui-workbench-design.md`. diff --git a/.claude/rules/registries.md b/.claude/rules/registries.md index ecc130c5..609f4486 100644 --- a/.claude/rules/registries.md +++ b/.claude/rules/registries.md @@ -8,7 +8,7 @@ paths: - "**/desktop/src/main/announcement-service.ts" - "**/desktop/src/shared/announcement.ts" - "**/desktop/src/shared/bundled-plugins.ts" -last_verified: 2026-07-15 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/main/claude-code-registry.ts - path: youcoded/desktop/src/main/local-theme-synthesizer.ts @@ -21,10 +21,10 @@ verify: # Registries: themes, marketplace, plugin install, announcements -Both registries are GitHub repos fetched at runtime via `raw.githubusercontent.com` — no *scheduled* rebuild, but both rebuild **on merge** (`validate-plugin-pr.yml` → `rebuild`; themes CI regenerates registry + previews). **Registry-repo depth: workspace `docs/registries.md`. MCP-authoring depth: `wecoded-marketplace/docs/mcp-authoring.md`.** +Both registries are GitHub repos fetched at runtime via `raw.githubusercontent.com` — no *scheduled* rebuild, but both rebuild **on merge** (`validate-plugin-pr.yml` → `rebuild`, only when a `plugin.json` dir changed; themes CI regenerates registry + previews). **Registry-repo depth: workspace `docs/registries.md`. MCP-authoring depth: `wecoded-marketplace/docs/mcp-authoring.md`.** ## Theme & skill registries -- **`wecoded-themes/registry/theme-registry.json` is auto-generated on CI merge** — don't hand-edit. Each theme: `themes/{slug}/manifest.json` + assets. **15 required CSS tokens** and four CSS-safety bans, both CI-enforced and both listed in `docs/registries.md`. Manifest >10MB or duplicate slug fails CI. +- **`wecoded-themes/registry/theme-registry.json` is auto-generated on CI merge** — don't hand-edit. Each theme: `themes/{slug}/manifest.json` + assets. **15 required CSS tokens** and four CSS-safety bans, both CI-enforced and both listed in `docs/registries.md`. Theme dir >10MB or duplicate slug fails CI. - **Any content change to a published theme MUST bump the manifest `version`** — no bump → installed users see a no-op "Installed" forever (depth: `docs/registries.md`). Guard: none — candidate. - **Apps read the Worker's `/catalog` FIRST** (1h TTL, `If-None-Match`/304 **mandatory** — MBs, hourly, mobile data, `*.workers.dev` has no edge cache); `index.json` is the fallback, then stale cache. **`CATALOG_ENABLED="0"` → 503 → silent fallback.** Depth: `wecoded-marketplace/docs/catalog.md`. - **`wecoded-marketplace`:** root `index.json` (bare array — **the fallback both apps fetch**) + `skills/index.json` (same entries wrapped, written first by `sync.js`) + `marketplace.json` (YouCoded-only). Entries with `sourceMarketplace: "youcoded"` are never overwritten by upstream sync. App caches 24h at `~/.claude/youcoded-marketplace-cache/` — **`youcoded-`**, not `wecoded-` (7 code sites). @@ -40,8 +40,8 @@ Both registries are GitHub repos fetched at runtime via `raw.githubusercontent.c - **`BUNDLED_PLUGIN_IDS` is two-way duplicated** — `desktop/src/shared/bundled-plugins.ts` + Kotlin `BundledPlugins.kt` must stay in sync. Intentionally hardcoded (offline-first + no remote force-install authority); changing it requires an app release. ## MCP plugin authoring (marketplace plugins) — **read `wecoded-marketplace/docs/mcp-authoring.md` before shipping a stdio MCP server** -- **`bash` is NOT on the Windows system PATH** — don't write `command:"bash"` in `mcp-manifest.json`; use a real on-PATH binary (`node`/`uvx`/`python`) or bash's 8.3 short name (`C:\PROGRA~1\Git\usr\bin\bash.exe` — spaces break the spawn). MSYS `/c/...` paths work only as ARGS, never `command`. -- **`${PACKAGE_DIR}` is expanded by YouCoded's `reconcileMcp()`, NOT Claude Code** — non-YouCoded CLI users get a literal placeholder. **Pin `mcp>=1.0.0,<2.0.0`** (0.x→1.x is dict→Pydantic breaking). **`claude mcp list` is a liar** (verifies only `initialize`) — verify with in-session `/mcp` (full `tools/list`). Spawn-probe the handshake before submission. Ten more footguns are in that doc. +- **`bash` is NOT on the Windows system PATH** — don't write `command:"bash"` in `mcp-manifest.json`; use a real on-PATH binary (`node`/`uvx`/`python`), `command_windows`, or bash's 8.3 short name (`C:\PROGRA~1\Git\usr\bin\bash.exe` — spaces break the spawn). MSYS `/c/...` paths work only as ARGS, never `command`. +- **The reconciler expands ONLY `{{plugin_root}}`** — `${PACKAGE_DIR}` is expanded by nobody (roadmap `marketplace`). **Pin `mcp>=1.0.0,<2.0.0`** (0.x→1.x is dict→Pydantic breaking). **`claude mcp list` lies** (verifies only `initialize`) — verify with in-session `/mcp` (full `tools/list`). Spawn-probe the handshake first; five more footguns in that doc. ## Announcements (`announcement-service.ts`, `shared/announcement.ts`) - **Source of truth is `youcoded/announcements.txt`** (app repo), NOT youcoded-core. `/announce` writes there; public URL `raw.githubusercontent.com/itsdestin/youcoded/master/announcements.txt`. diff --git a/.claude/rules/status-bar-relevance.md b/.claude/rules/status-bar-relevance.md index 78a3d9e8..93115749 100644 --- a/.claude/rules/status-bar-relevance.md +++ b/.claude/rules/status-bar-relevance.md @@ -81,7 +81,7 @@ number, when there is nothing to compare. as "we checked and it matched". **Guard:** `provider-cost-check.test.ts`, `harness-pricing.test.ts`. -**Known gaps, tracked in `ROADMAP.md`, not defects of this code:** the chip runs ~25% low +**Known gaps, tracked in `docs/roadmap/native-harness.md` → `## cost`, not defects of this code:** the chip runs ~25% low once a session compacts (the summarize call's tokens are counted by neither side, so the comparison stays honest but a clean bill ≠ matching the invoice); a mid-turn model swap re-prices the whole turn; the session sum can average a bad model away across a swap. diff --git a/.claude/rules/sync-spaces.md b/.claude/rules/sync-spaces.md index 920008ed..16634e2b 100644 --- a/.claude/rules/sync-spaces.md +++ b/.claude/rules/sync-spaces.md @@ -10,7 +10,7 @@ paths: - "**/desktop/src/main/github-connect.ts" - "**/desktop/src/main/github-client.ts" - "**/desktop/src/main/github-fork-publish.ts" -last_verified: 2026-07-22 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/src/main/github-client.ts contains: "createGithubClient" @@ -76,16 +76,16 @@ verify: - **Corrupt-repo heal is ONCE per space per launch** (`healedSpaces`, marked BEFORE attempting). **Self device-row recency derives from `lastSyncFor` evidence**, never `.sync-marker`. ## SyncHub (`sync-hub-socket.ts` + `SyncGroupRoom` DO) -- **The DO is per-account, an ACCELERANT not truth** — never drop the 120s poll. **spaceKey = `repoNameForSpace()`, never the local id; signal ONLY on `pushed:true`; the hub send runs LAST in `broadcast()`, isolated.** +- **The DO is per-account, an ACCELERANT not truth** — never drop the 120s poll. **spaceKey = `repoNameForSpace()`, never the local id; signal ONLY on `pushed:true`; the hub send runs LAST of the fan-outs in `broadcast()`, isolated.** - **Per-device recency rides the SAME signal** (`lastSyncByDevice` in DO storage; pure `deviceActivityLabel` renders). **Self reads the LOCAL `lastSyncEpoch`, NOT the map.** ## Import (`sync-spaces/import-project.ts`) - **Import MOVES the folder — never copy-and-keep-both.** The EXDEV branch re-checks `existsSync(dest)` BEFORE cpSync; store remaps degrade to WARNINGS, never silent drops. ## Project UX + discovery -- **Sync dots (green/red/gray) are the ONE sanctioned status-color use** — ALL dot state from pure `sync-dot-state.ts`; labels pinned. +- **Sync status comes ONLY from pure `sync-dot-state.ts`** (every dot's state and label); other status-coloured controls are not sync. - **Project registry at `~/YouCoded/Personal/ProjectSync/.json` — VISIBLE per-file, NEVER under `.youcoded/`.** `state` = `stopped`-dominates monotonic (not LWW); **fold-on-read** blocks resurrection; schema stays 1. -- **Per-field merge: `laterOf` takes `{v, at}` wrappers, NEVER whole entries; `description` is LWW on its OWN `descriptionUpdatedAt`, never `updatedAt`.** A whole-entry `laterOf` tie-breaks on `JSON.stringify` and broke associativity; a shared clock lets a description write revert another device's rename. +- **Per-field merge: `laterOf` takes `{v, at}` wrappers (`description` does; the name dimension passes whole entries); `description` is LWW on its OWN `descriptionUpdatedAt`, never `updatedAt`.** Whole-entry `laterOf` tie-breaks on `JSON.stringify` (broke associativity); a shared clock reverts a peer's rename. ## Device registry - **TWO identities, NEVER merged: `getDeviceIdentity(userData)` = per-INSTALL (leases); `getMachineIdentity(builtAppUserData)` = per-MACHINE (registry), which READS, never mints — `null` ⇒ register NOTHING.** diff --git a/.claude/rules/test-suite-hygiene.md b/.claude/rules/test-suite-hygiene.md index eaec0db8..8d0d1bde 100644 --- a/.claude/rules/test-suite-hygiene.md +++ b/.claude/rules/test-suite-hygiene.md @@ -8,7 +8,7 @@ paths: - "**/desktop/tests/**/*.test.tsx" - "**/desktop/src/**/*.test.ts" - "**/desktop/src/**/*.test.tsx" -last_verified: 2026-08-28 +last_verified: 2026-09-01 verify: - path: youcoded/desktop/vitest.config.ts contains: "youcoded-vitest-home-" diff --git a/.claude/rules/worker-backend.md b/.claude/rules/worker-backend.md index 695d6e6d..65e5700c 100644 --- a/.claude/rules/worker-backend.md +++ b/.claude/rules/worker-backend.md @@ -1,7 +1,7 @@ --- paths: - "**/worker/**" -last_verified: 2026-07-15 +last_verified: 2026-09-01 verify: - path: wecoded-marketplace/worker/src/lib/analytics.ts contains: "writeAppEvent" @@ -33,6 +33,6 @@ Opt-outable anonymous device-hash + DAU/MAU. Current design: `docs/archive/specs - **Country + region are read SERVER-side** (`CF-IPCountry`, `CF-IPRegionCode`, ISO 3166-2), never sent from the client. NO cross-tabulation of region with other dimensions (fingerprint risk at low cell counts) — single-dimension GROUP BY only. - **`adminFilterClause` + `cutoverClause` (`lib/admin-filter.ts`) are the SQL safety boundary** — hex/ISO-only sanitization is mandatory (AE has no parameter binding; we string-interpolate). `KNOWN_DEV_DEVICES` filters Destin's own hashes out by default (`?include_admins=1` bypasses). - **CF Analytics Engine SQL is a narrow subset, NOT full ClickHouse** — `count(DISTINCT)` only, quoted `INTERVAL '30' DAY`, its own narrow-scope token, subqueries unreliable. The four 422 gotchas in full: `docs/worker-backend.md`. -- **Admin auth via `requireAdminAuth`** — cookie session `Bearer` OR the `youcoded-admin` skill's `X-GitHub-PAT` (traded for a platform account id via `identities`, cached 60s in `auth/pat.ts`). The `isAdminAccount()` allowlist (`auth/admin.ts`) stays inline per-route so 401 (not auth'd) vs 403 (not admin) stay distinct. `ADMIN_USER_IDS` = bare GitHub numeric ids (secret `MARKETPLACE_ADMIN_USER_IDS`), NOT `github:`. +- **Admin auth via `requireAdminAuth`** — cookie session `Bearer` OR the `youcoded-admin` skill's `X-GitHub-PAT` (traded for a platform account id via `identities`, cached 60s in `auth/pat.ts`). The `requireAdminAccount(c)` allowlist wrapper (`auth/admin.ts`) is called per-route so 401 (not auth'd) vs 403 (not admin) stay distinct. `ADMIN_USER_IDS` = bare GitHub numeric ids (secret `MARKETPLACE_ADMIN_USER_IDS`), NOT `github:`. Note: SyncHub (`SyncGroupRoom` DO) worker invariants live in `.claude/rules/sync-spaces.md`. diff --git a/.claude/skills/ui-mockup/SKILL.md b/.claude/skills/ui-mockup/SKILL.md index 5becf120..9d4d1cdd 100644 --- a/.claude/skills/ui-mockup/SKILL.md +++ b/.claude/skills/ui-mockup/SKILL.md @@ -83,7 +83,7 @@ Decisions must not live only in chat: 2. Turn the `MOCK_ONLY` entries the approved UI depends on into real handlers (main + `preload.ts` + `remote-shim.ts` + `SessionService.kt`, guarded by `ipc-channels.test.ts`), then drop them from the registry. -3. Add ROADMAP entries for anything deferred, and follow the workspace knowledge rules +3. Add roadmap entries for anything deferred (`docs/roadmap/.md` — `ROADMAP.md` → "Filing an item"), and follow the workspace knowledge rules (pinning test > ast-grep rule > WHY comment > path-scoped rule) for anything durable. Merging cannot shift appearance, because nothing was ever copied. diff --git a/.claude/skills/ui-review/SKILL.md b/.claude/skills/ui-review/SKILL.md index 36d05dc2..163b5bca 100644 --- a/.claude/skills/ui-review/SKILL.md +++ b/.claude/skills/ui-review/SKILL.md @@ -48,7 +48,7 @@ ledger: `docs/active/design/2026-08-25-ui-audit-findings.md`. - Copy the gallery + sheets into `docs/active/design/-ui-audit/` (images git-ignored by the existing rule; README says how to regenerate). - Update the design guide only for rules that changed; new rules get the next `G-n`. -- ROADMAP entry: the whole-UI review item gets an update line pointing at the docs. +- Roadmap entry: the whole-UI review item (in `docs/roadmap/dev-workspace.md` → `## rigs`) gets an update line pointing at the docs. ## 4. Improve (when asked, or as the follow-up) @@ -73,7 +73,7 @@ then a **review page** — never a gallery, never a chat summary: timeout; 3 = another process already serves this spec — neither 2 nor 3 carries answers, do not invent a result) (`wait ` if you lost the process). Never ask him to paste anything. 4. Act on the summary exactly (`Other` + note = change it as described); record decisions in the - findings ledger row, the guide, the ROADMAP entry. Merge, archive, clean up. + findings ledger row, the guide, the roadmap entry. Merge, archive, clean up. ## Red flags diff --git a/.claude/skills/wrap-up/SKILL.md b/.claude/skills/wrap-up/SKILL.md index ef809759..05a95a04 100644 --- a/.claude/skills/wrap-up/SKILL.md +++ b/.claude/skills/wrap-up/SKILL.md @@ -12,7 +12,7 @@ written down, never closed, and independently rediscovered twice. **This is a PROCESS, not a report generator.** A numbered list nobody actions is the failure mode, not the output. Every recommendation ends this session as one of three -things: **applied**, **a dated `ROADMAP.md` entry**, or **dropped with a reason.** +things: **applied**, **a dated roadmap entry** (`docs/roadmap/.md` — `ROADMAP.md` → "Filing an item"), or **dropped with a reason.** ## Step 0 — "wrap up" DOES NOT MEAN "merge" @@ -34,15 +34,16 @@ landed and reports accordingly: - **Pre-merge** (the common case): it skips branch, worktree and dead-name checks, because all three would tell you to delete things still in use. What remains is real: - is it pushed so someone can review it, and the docs/ROADMAP/MAP hygiene below. + is it pushed so someone can review it, and the docs/roadmap/MAP hygiene below. - **Post-merge:** the full cleanup — delete the remote branch, the local branch, remove the worktree, and fix docs that now name a dead branch. **Finish every line it reports.** A `TODO` is yours to do now. A `--` line is a judgement the script deliberately refuses to make — make it: -- Flip the ROADMAP item for this work to `[x]` **if the work actually shipped**; if the - PR is still open, leave it and say so. +- Close the roadmap item for this work **if the work actually shipped** (delete it from its + `docs/roadmap/.md`, append one line to `docs/roadmap/shipped.md`, archive its report, + run `node scripts/roadmap-check.mjs --fix`); if the PR is still open, leave it and say so. - Does the subsystem have a `docs/MAP.md` row and a hot-path entry? "No rule" is an acceptable answer; "no row" is not. - Move docs whose `status:` is now `shipped` to `docs/archive/`, and repoint cross-links. @@ -108,7 +109,7 @@ A numbered list. For each item, three short lines: 1. **What to change** — the concrete edit, file named. 2. **Why, in plain terms** — one or two sentences, no jargon. What went wrong this session that this prevents, and what a future session will experience differently. -3. **Where it lands** — `applied now` / `ROADMAP` / `dropped: `. +3. **Where it lands** — `applied now` / `roadmap` / `dropped: `. Order by how much friction it removes, not by how easy it is. Say "nothing worth changing" if that is the honest answer — a session that found no friction is a real @@ -120,7 +121,8 @@ outcome, and inventing recommendations to fill a list poisons the ones that matt (budgets and anchors are enforced) plus `node --test scripts/*.test.mjs .claude/hooks/*.test.mjs` if you touched either. Never report a change as done on the strength of having written it — run the check and quote what it returned. -- **ROADMAP:** add a typed, tagged, dated entry — and **dedupe by file or symbol name, +- **Roadmap:** add an entry to the area file whose `Filing test:` says yes (`ROADMAP.md` → + "Filing an item"; a symptom in Destin's words, tokens on the last line) — and **dedupe by file or symbol name, not by symptom** first; a 2026-08-31 session filed a duplicate by searching `flaky` instead of `sync-spaces-engine`. - **Dropped:** say so in your reply, with the reason. An unrecorded "we considered and diff --git a/CLAUDE.md b/CLAUDE.md index f9d6b4f7..87d973c4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -16,7 +16,7 @@ YouCoded is an open-source cross-platform AI assistant app built entirely withou **The app is the product.** Everything else — themes, skill marketplace, bundled plugins — supports the app. Documentation and code should reflect that hierarchy. -**One product.** The five sub-repos are components of a single consolidated product. Planning, versioning, and roadmapping happen at the workspace level (`ROADMAP.md`); sub-repo docs exist only for knowledge physically coupled to that repo's code. +**One product.** The five sub-repos are components of a single consolidated product. Planning, versioning, and roadmapping happen at the workspace level (`ROADMAP.md` is the index; the backlogs are `docs/roadmap/.md`); sub-repo docs exist only for knowledge physically coupled to that repo's code. ## Workspace Layout @@ -124,6 +124,10 @@ bash scripts/run-dev.sh --label "Feature Name" When designing new features or making changes to user-facing app interfaces, the first step should always be to visualize and design the UI/UX of the final feature. Planning sessions should prioritize iterative UI design using the workbench and other tooling to help Destin shape the final user experience of the feature before building backend. When Destin provides final sign-off on the UI/UX design for the feature, the UI/UX should be treated as largely final and backend should be designed around the UI/UX accordingly. The standard every new surface is measured against is `docs/active/design/2026-08-25-ui-design-guide.md` (five laws, primitives, per-surface anatomies, checklist); show him the change as a **review deck** (scripts/ui-review/review-cards.py — one point per step: Before | After with the changed region boxed by the rig, a headline and three cards — What changed / You'll notice / Risk — Yes / No / Other, answers saved to a file and handed to Claude on Submit; `serve ` in the background does it all), built from the UI review rig below; never a gallery, a prose page or a chat description (all three were rejected). **For motion, drag or hover, use a LIVE step** — panes of the running app he can actually operate, one authored candidate each out of `youcoded`'s `compare/registry.tsx` (`serve` boots the worktree's workbench for them). A recording is the wrong tool for a 200 ms animation: four clip steps were rejected on 2026-08-31 as "just rough to compare". `scripts/ui-review/README.md` → "Live panes". +### Asking Destin many questions at once + +**Four or more questions that need Destin's input go on a question deck, never in a chat message** — `python3 scripts/questions/serve.py ` (run it in the background; its exit is the submit signal and it prints every answer). A wall of one-liners in chat was rejected on 2026-09-01: it assumes he remembers every item, and some were filed months earlier. Every question on the deck is written for someone with **no context**, in plain words, in four parts the page renders as labelled blocks — **today** (what exists: which part of the app, what it does for the user), **the problem** (what goes wrong, as the user experiences it), **the proposal** (what would change, as the user would notice it), and **options** (each with pros and cons **about the user's experience**, not the code). Yes/No/Don't-know questions say in the proposal what each answer leads to. Fewer than four, or wording-only, still go in chat. Spec format is in the script's header. + ### UI Workbench `bash scripts/run-workbench.sh` boots the **real renderer** in a browser tab (Vite only — no Electron, no PTY) against a fake `window.claude`, on port 5233. Every menu is clickable and stateful, so **new feature UI is built here before its backend exists** — channels with no backend go in `MOCK_ONLY`, which is then the backend to-do list. Toolbar switches scenario (`default`/`empty`/`no-providers`/`refused`/`stress`), fake IPC latency, narrow viewport, and the tool gallery that replaced `?mode=tool-sandbox`. Use `run-dev.sh` instead when you need real event ordering, PTY, or main-process behaviour. **After any change to the mock shim run `node scripts/workbench-boot-check.mjs`** — it loads every registered workbench route headless (12 today) and fails on a console error; the unit suite passed while the app crashed at boot three times running. Rule: `.claude/rules/react-renderer.md`; spec: `docs/archive/specs/2026-07-29-ui-workbench-design.md`. @@ -199,7 +203,7 @@ to replace. It also runs on its own at the end of any substantial session. It replays what the session actually did — what context loaded, what you had to hunt for because it was unwritten, which tooling you used and why, where you took a wrong turn — and turns that friction into workspace changes. Every recommendation ends the session -**applied**, as a dated **`ROADMAP.md`** entry, or **explicitly dropped with a reason**. +**applied**, as a dated roadmap entry (`docs/roadmap/.md` — see `ROADMAP.md` → "Filing an item"), or **explicitly dropped with a reason**. A numbered list nobody actions is the failure mode, not the output. **Why it has to be asked for.** No hook can know when a session is finished: `SessionEnd` @@ -241,16 +245,16 @@ New knowledge goes to, in descending preference: **a pinning test > an ast-grep | Kind of knowledge | Home | |---|---| | Invariant / lesson | The ladder above. Slim `docs/PITFALLS.md` holds only cross-repo items | -| Planned feature / bug / idea | `ROADMAP.md` — capture in the SAME session Destin mentions it (typed, tagged, dated; dedup first) | -| Doc contradicting code | **Fix on sight** (verify against code; cite verification in the commit). Unfixable this session → ROADMAP `bug` tagged `#docs`. There is no drift ledger | +| Planned feature / bug / idea | `docs/roadmap/.md` — the file whose `Filing test:` line says yes (`ROADMAP.md` → "Filing an item" has the grammar). Capture in the SAME session Destin mentions it; dedup first; a symptom in Destin's words, no paths; run `node scripts/roadmap-check.mjs --fix` before committing | +| Doc contradicting code | **Fix on sight** (verify against code; cite verification in the commit). Unfixable this session → an entry in `docs/roadmap/dev-workspace.md` under `## knowledge`. There is no drift ledger | | CC-version watch item | `youcoded/docs/cc-dependencies.md` | | Completed/superseded plans, specs, handoffs | `docs/archive/` (in-flight ones live in `docs/active/`) | | Destin-specific preferences / session feedback | Auto-memory — LAST resort; product planning never lives in memory | -**Document lifecycle:** new specs/plans/handoffs save to `docs/active/{specs,plans,handoffs,investigations,prototypes}/` with `status:` frontmatter (`draft | active | shipped | superseded`). When a feature merges, its docs move to `docs/archive/` and the ROADMAP item flips to `[x]` in the same session — "Merge means merge AND push" extends to "…AND archive the docs AND flip the roadmap item." Searches for live docs exclude `docs/archive/` by default. +**Document lifecycle:** new specs/plans/handoffs save to `docs/active/{specs,plans,handoffs,investigations,prototypes}/` with `status:` frontmatter (`draft | active | shipped | superseded`). When a feature merges, its docs move to `docs/archive/` and the roadmap item closes in the same session (delete it from its area file, append one line to `docs/roadmap/shipped.md`, archive its report) — "Merge means merge AND push" extends to "…AND archive the docs AND close the roadmap item." Searches for live docs exclude `docs/archive/` by default. **A retrospective is closed in the session that acts on it.** Every finding ends as -shipped, dropped, or a dated `ROADMAP.md` entry — then the document moves to +shipped, dropped, or a dated roadmap entry in its area file — then the document moves to `docs/archive/`. Two retrospectives sat unclosed for weeks and their unshipped half was independently rediscovered twice; one of the rediscovered items was the glob migration of 2026-08-31. diff --git a/ROADMAP.md b/ROADMAP.md index 6f96892a..9cb34c9a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,1464 +1,70 @@ -# 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` **`