file the 2026-06-11 Fable review under epic T-359

Commit fable-ous.md (13-reviewer multi-agent assessment of the whole
tree) and the ticket tree it produced: epic T-359 with 26 children
covering the dragon bugs (PTY fd leak, undrained claude stderr,
unauthenticated MCP HTTP, path-confinement gaps, dispose-path leaks,
SGR crash, a11y semantics), the medium scorpions, a dead-code sweep,
the systemic-pattern work, and split plans for app.dart and the claude
meta sidebar. Root causes appended to existing T-274, T-283 context,
and the parser split plan to T-123. One review claim (ColumnHat
duplicated in app.dart) was refuted during verification and is
annotated on T-385/T-394.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-12 00:13:52 +02:00
co-authored by Claude Fable 5
parent 662325d5db
commit 3ed90fc318
3 changed files with 1035 additions and 0 deletions
+428
View File
@@ -0,0 +1,428 @@
# fable-ous.md
*A fable about clide, as told by Fable.*
*Produced by 13 parallel subsystem reviewers reading ~58k LOC of Dart, ~100 raw
findings put through 70 adversarial verification passes (exactly one finding was
refuted — it had missed D-14), 5 feature-ideation lenses, and a full read of the
pql planning vault: 94 confirmed decisions, 24 open questions, 11 rejected
alternatives, 358 tickets. Everything below cites real file:line evidence that a
skeptical second agent re-read and failed to knock down.*
---
## TL;DR scoreboard
| Dimension | Verdict |
|---|---|
| Build health | **Green.** `make analyze` 0 issues, 1383 tests pass, format clean, coverage 95.30% over the 95 floor |
| Documentation discipline | **Best-in-class.** Near-every file cites its D-NNN/T-NNN; zero TODO/FIXME debt in 58k LOC |
| Honesty | **High.** CHANGELOG claims verified against code; stubs self-describe as stubs |
| Real bugs found | ~14 distinct high-severity, ~35 medium — mostly in process lifecycle, a11y, and terminal conformance |
| Systemic risks | Broadcast-streams-without-replay, silent `catch (_)`, kernel lookup in `dispose()`, copy-paste drift |
| Release process | **Stalled.** No git tag since v2.1.0 despite five CHANGELOG releases; `ci/release.sh` is a stub |
| Killer-feature headroom | Enormous — the moats (owned agent loop, owned renderer, pql vault) are real and barely exploited |
The fable in one sentence: **a castle with exceptional masonry, a few unlocked
side doors, and a dragon hoard of features in the basement nobody has spent yet.**
---
## Part I — The state of the realm (what's genuinely excellent)
Credit where due, because this codebase does several things better than most
production repos:
- **Governance traceability is real, not ceremonial.** Nearly every non-trivial
declaration carries its ticket/decision id. Multiple reviewers independently
called it "the best I've seen at this scale." Code-to-decision drift is
*auditable from the code itself* — and indeed most drift findings below were
found exactly that way.
- **Schema-validated IPC dispatch (D-74)** is beautifully executed: schemas
co-registered with handlers, the MCP tool surface and `clide capabilities`
both generated from the same registry (`lib/src/daemon/dispatcher.dart:90-137`)
so three surfaces cannot drift apart.
- **The keymap subsystem (D-82)** — layered precedence, headless
`SequenceMatcher`, clock-injected `ModifierTapTracker` — is exemplary,
near-fully test-mirrored design.
- **`ClideTappable`, the anchored-overlay/menu family (D-88), reduced-motion
honoring in every animated primitive** — the owned widget layer is coherent
and disciplined.
- **Battle scars are encoded where they happened.** `pumpAsync` documents why
`pumpAndSettle` wedges; `KernelFixture` encodes the T-280 teardown-hang fix;
flake postmortems live as comments in the test that almost shipped them.
- **Security instincts**: 0600 socket + live-listener probe before unlink, git
argv hardening with `--` terminators, `path_safety.dart` documenting its
threat models inline, workspace-relative binary resolution forbidden (T-98).
- **Zero TODO/FIXME/HACK** across the tree. The clean-board policy is lived.
Patterns worth *extending* (the reviewers kept wishing other code did this):
the coalesced-notify `Timer(Duration.zero)` trick in `ConversationController`,
the pure-Dart-core/thin-Flutter-shell split, and `RecordingEventSink`-style
event-driven test waits.
---
## Part II — The bestiary (bugs I would fix, in order)
### 🐉 Dragons (high severity, verified, fix this week)
1. **Every naturally-exited PTY leaks its master fd — forever.**
`lib/src/pty/native_pty.dart:444-450` — on child EOF, `_reap()` sets
`_dead = true` but never closes `_fd`; a later `close()` short-circuits at
`if (_dead) return;` (line 460) so `_nativeClose(_fd)` (line 477) never runs.
Two reviewers found this independently. Every terminal/Claude pane whose
child exits on its own leaks an fd and a pty device for the life of the app.
2. **The Claude child process is observed only via stdout.** Three reviewers
converged here. `lib/builtin/claude/src/stream_json_session.dart:43-78`
stderr is *never drained* (≥64KB of `--verbose` spew = pipe fills = child
blocks mid-turn = the flagship pane wedges with zero diagnostics), and
nothing watches `exitCode` or `onDone` (line 303), so a crashed/dead
session just looks… thoughtful. Drain stderr into a ring buffer, surface a
terminal `SessionEnded` state. This also intersects T-283 (no resume
timeout/fallback).
3. **The MCP HTTP server exposes the entire dispatcher with zero auth.**
`lib/src/ipc/mcp_server.dart:138-195`, started unconditionally at boot
(`lib/main.dart:174-180`). D-71's threat model ("another user on the same
host should not drive my IDE") is enforced with 0600 on the unix socket —
and then bypassed wholesale by an unauthenticated localhost HTTP port that,
since D-86, serves *every* clide verb as a tool. Generate a token in the
lock file (Claude Code's own `/ide` lock format has a slot for it), require
the header.
4. **`editor.open`/`editor.save` skip path confinement entirely.**
`lib/src/editor/registry.dart:215-219` returns absolute paths verbatim, no
`..` normalization, no `path_safety` call — an unconfined read *and write*
primitive over IPC while `files.read` is carefully guarded. Same family:
**`search.replace` silently ignores its include/exclude globs**
(`lib/src/search/replace_engine.dart:124-143`) and will happily rewrite
files outside the filter the user typed; and **`listDir`'s symlink
detection is dead code** (`lib/src/files/listing.dart:46-54``stat()`
follows links, so `isSymlink` is always false) which means `walkFiles`
descends symlinked dirs the docs claim it skips.
5. **Closing a terminal pane never closes the shell.**
`lib/builtin/terminal/src/terminal_pane.dart:131-137` calls
`ClideKernel.of(context)` from `dispose()` — illegal ancestor lookup,
swallowed by `catch (_)` — so `pane.close` is never sent and the backend
PTY + daemon pane leak. The same idiom leaks the settings listener in every
disposed `ClaudePane` (`claude_pane.dart:460-466`). Combined with dragon #1
this is a two-stage leak pipeline. Cache the kernel ref in
`didChangeDependencies`, delete the catch-all.
6. **Project switch leaks the entire previous workspace.**
`lib/main.dart:335-344` — a new dispatcher gets fresh `PaneRegistry`,
`FilesService`, `EditorRegistry`, etc., but nothing calls the old set's
`shutdown()` methods (which exist and have zero callers). Old watchers keep
emitting into the new workspace's bus.
7. **T-274's root cause, found and verified:** the status bar is empty because
`statusStream` is a plain broadcast controller — the `system/init` event
fires while `spawn()` is still awaiting a 256KB transcript-tail read, before
the pane ever subscribes (`claude_pane.dart:322`,
`session_orchestrator.dart:222-225`). Seed from `session.status` on bind, or
make it replay-latest. (The broadcast-without-replay shape is a recurring
bug factory — see Part III.)
8. **Auto-scroll yanks a scrolled-up reader to the bottom on every streamed
token.** `conversation_view.dart:268-277` — the `_atBottom` pin exists but
is only consulted on viewport *resize*, not on new items. Anyone reading
earlier output during a long streaming reply is dragged to the bottom
continuously. One-line gate + the missing twin test.
9. **The terminal can crash on garbled output.** SGR 38/48 extended-color
parsing does unguarded `params[i + 1]` lookahead
(`lib/src/terminal/src/core/escape/parser.dart:501-516`) — `printf '\e[38m'`
throws RangeError inside `Terminal.write`. An emulator must never throw on
hostile bytes. While in there: colon-form SGR sub-parameters are mangled
into bogus params.
10. **A11y has drifted despite being a Tier-0 contract.** Two independent
verified findings: `ClideCollapserCard`'s `excludeSemantics: true` wipes
*every expanded child* from the a11y tree
(`lib/widgets/src/clide_collapser_card.dart:92-101`) — a screen-reader user
can expand a run and hear nothing; and the three a11y gate tests
hand-enumerate their subjects and have measurably fallen behind `lib/`
(contrast checks fewer themes than `main.dart:443-454` loads; i18n checks 4
of 8 namespaces). The gates stay green while covering less. Make `lib/`
export the canonical lists and iterate them in the gates.
### 🦂 Scorpions (medium — real, will sting eventually)
- **D-72's "serial dispatch" isn't.** `lib/src/ipc/server.dart:151-180` uses an
`async` onData without pausing the subscription — pipelined requests
interleave, and the shared `StringBuffer` framing can drop/double lines.
`client.cast<List<int>>().transform(utf8.decoder).transform(LineSplitter())`
+ `await for` fixes framing, UTF-8 split chunks, and serialization at once.
- **Split-chunk UTF-8 corruption is endemic at byte→String seams**: the
terminal's only ingestion API is `write(String)` (`terminal.dart:218`) so
both consumers decode per-chunk; `FileTailFollower` starts mid-character by
construction. Add `writeBytes()` with a persistent chunked decoder.
- **`Orchestrator.spawn()` races itself** — check-then-act across two awaits;
concurrent spawns for one id leak a live `claude` process
(`session_orchestrator.dart:191-249`). Hold a `Map<String, Future<ManagedSession>>`.
- **Fork panes misbehave on `/clear`, `/resume`, `/fork`** —
`widget.forkSourceId` wins forever, so `/clear` *re-forks the original
conversation* instead of clearing (`claude_pane.dart:266-281`).
- **Settings persistence corrupts maps-inside-lists on write**
(`settings.dart:199-219` emits `toString()`), breaking the documented keymap
overlay across restarts; writes are non-atomic and a parse failure silently
resets all settings.
- **Extension activation isn't transactional** — a throw mid-contribution
leaves contributions mounted while the extension records as failed; retry
double-applies (`extensions_manager.dart:133-192`). Plus: disabling an
extension ignores dependents, and registries clobber silently on id
collision — fine among curated builtins, hazardous the day Tier-6 Lua lands.
- **Terminal conformance debt** (the fork fixes what it trips over but has no
vttest-style suite): HTS is a no-op (`isSetAt` instead of `setAt`,
`terminal.dart:423`), DECCKM is tracked but never consumed, legacy mouse rows
are off-by-one *and the test enshrines the bug*, CPR replies 0-based where
every real terminal is 1-based, scrollback is maintained but structurally
unreachable (`ViewportOffset.zero()` pinned every build,
`terminal_view.dart:224`).
- **Markdown renderer**: hard breaks and images render as empty text (words
glue together; `clide_markdown.dart:408-410`), and the whole document
re-parses with sync `existsSync()` calls *inside build* on every streaming
delta — multiplied by the conversation view re-deriving everything O(n) per
notification and token streaming re-encoding the full reply per delta
(O(n²) churn, `stream_json_session.dart:410-431`).
- **Terminal panes spawn in `Directory.current`**, not the open project root
(`terminal_pane.dart:69`) — desktop launches get `$HOME` shells.
- **The Notifications service renders nowhere** — `notify.dart` has zero widget
consumers; cli_install's dogfood warnings vanish into an unrendered list
while `ToastService` sits right there.
- **Welcome screen no-ops**: "Clone from git…" and "Start a Claude session"
advertise shortcuts that don't exist and do nothing on tap
(`welcome_view.dart:173-174`).
- **`make test-e2e`/`ui-dev`/`ui-smoke` are dead** — `tools/ui/*.sh` still `cd`
into the removed `app/` directory; the staged Gitea CI workflow would fail in
three independent ways on activation, while D-32 calls it "ready."
### 🐀 Rats (low, but they breed)
Dead code worth a one-day extermination sweep: the entire legacy free-function
git API (~250 LOC duplicating `GitClient`, kept alive only by tests, *with its
own latent pipe-deadlock bug*), `ToolCheck`, ~60% of `ffi/libc.dart` (fd-passing
era), `GraphView` (unreachable placeholder), `ColumnHat` (duplicated
line-for-line in app.dart, kept alive by a zero-coverage test), the tmux-era
team pipeline (`TranscriptPublisher`, `TeamMemberJoined` — *nothing emits these
events*, yet the team roster UI still listens to them exclusively, meaning team
tiles are populated by ghosts), the dead `ptyc` binary still committed in
`native/linux-x64/` against D-62/D-63, and `mocktail` — pinned, documented in
D-25 as the IO-mocking strategy, and imported by exactly zero files.
---
## Part III — Patterns I would change (the systemic stuff)
1. **Broadcast streams that carry state need replay-latest.** This one shape
caused T-274, the meta sidebar's manual compensation, and the
prompt-stream's `initialData` workaround. Write a tiny `ValueStream` wrapper
once; retrofit `statusStream`, `busyStream`, `pendingPromptStream`.
2. **Ban `catch (_) {}` on I/O and lifecycle paths.** The silent-swallow idiom
turned an illegal-lookup-in-dispose into two resource leaks and turned
process-spawn failures into blank panes. Cleanup paths may swallow; spawn,
read, and dispose paths must log through the kernel Logger they already have.
3. **Sync I/O in async handlers on the single isolate.** `files.read` does a
sync 10MB read; the replace engine reads and rewrites the workspace
synchronously *while grep right next to it fans out to isolates per D-79*.
Decide the rule (offload above N KB), write it into a D-record, apply it.
4. **Path confinement belongs at the dispatch layer, not per-verb.** files.read
remembered, search.replace half-remembered, editor.* forgot. A confinement
check keyed off the co-registered schema (the registry already knows which
params are paths) ends the per-verb lottery.
5. **Copy-paste is the repo's main duplication tax.** The welcome screen clones
FileActions' entire open-folder flow verbatim; palette and quick-open are
~230-line near-twins; three private "tail a growing file" implementations in
the claude builtin alone; five hand-rolled `_userErr` helpers; five
copy-pasted git test sandboxes (none isolating host git config); two
parallel ANSI flag enums that already drifted (strikethrough is stored but
never painted). Each is small; together they're how a solo-dev repo rots.
6. **Hand-enumerated lists drift; export the truth.** Bundled themes (already
drifted between `main.dart` and the testmode harness — catppuccin is
silently unvalidated), a11y gate subjects, i18n namespaces. One exported
const each, consumed by both sides.
7. **The claude builtin returns `ok` with an `error` payload in 16 handlers**,
drifting from the D-6 exit-code contract every other subsystem honors. A
scripted `clide claude.agent.set-permission-mode bogus` exits 0 today.
8. **God-files**: `app.dart` (1187 LOC, five concerns — split plan is in the
findings), `claude_meta_sidebar.dart` (1192), `parser.dart` (1139, T-123
already exists — and the split should also fix `_consumeCsi` discarding
intermediate bytes, which permanently blocks DECSCUSR/DECSTR).
9. **Docs drift at the front door**: CLAUDE.md and README still say "tmux owns
Claude session persistence (D-41)" — superseded by D-75/D-77 per
`docs/architecture.md`; README says "Pre-v2.0 (2.0.0-dev)" at v2.3.3 and
headlines "canvas and graph surfaces" that are a 17-line stub and a flat
ListView respectively. clide's honesty is its brand; the README is the one
place currently off-brand.
10. **Close the release loop.** Five CHANGELOG releases since the last git tag;
`ci/release.sh` exits 64 and references the dissolved sidecar; the pre-push
fast path's safety argument cites "release CI on tagged versions" that
doesn't exist; and the fast path skips ALL tests for pushes touching
`test/`, `ci/`, or the hook itself. Back-tag 2.2.02.3.3, add tagging to
the git-commit skill ritual, widen the fast-path regex. (This is also the
blocking prerequisite your own T-47 refinement identified for self-update.)
---
## Part IV — Killer features (the dragon hoard)
Five ideation lenses, 27 proposals, deduplicated and ranked. The convergence
test mattered: **two lenses independently invented the flight recorder, and two
independently invented the visual canvas round-trip** — when separate agents
with different briefs land on the same feature, that's the market talking.
Clide's structural moats, verified against code: it *spawns and owns* the agent
process (D-77/D-78) where competitors are sandboxed extension guests; it owns
every pixel (terminal, markdown, canvas); everything is local-and-committed
(transcripts, costs, decisions, tickets) where competitors' business models
require cloud custody; and the pql vault is structured planning data no
mainstream IDE has an analogue for.
### Tier 1 — do these (high leverage, mostly M-effort, plumbing exists)
1. **Agent Blame + Session Flight Recorder** `[L]` — gutter action on any line:
*which session, which turn, which prompt, which permission grant, what it
cost* — opening the native conversation at the exact `tool_use`. Timeline
scrubber to replay a session. The transcript pipeline
(`transcript_reader.dart`, `session_index.dart`) already parses everything
needed. Cursor/Copilot cannot ship this: their logs live server-side by
business design. *Two lenses converged here.*
2. **Context X-ray** `[M]` — per-card token attribution ("this 40KB Bash tail
is 12% of your window") + a real compaction indicator. `stream_json_session.dart`
already parses usage and contextWindow per event; the renderer owns the
cards. Fixes T-244 (invisible compaction) as a side effect. Context is the
scarcest resource in agent pairing and every tool renders it as one opaque
percentage.
3. **Trust Ledger + decision-aware permission prompts** `[M]` — every
permission rule with provenance (which prompt, which session, which ticket),
ticket-scoped expiry; and when a `can_use_tool` request arrives, chip the
relevant D-record onto the card (edit touching pubspec.yaml → D-31
prefer-zero-deps, one keystroke to deny *with the decision cited*).
Governance stops being documentation and becomes live agent policy. No
competitor has a queryable in-repo decision system to even attempt this.
4. **Agent Activity HUD** `[M]` — four backlog tickets and one open question
are secretly one feature: build T-59's `OperationsRegistry` once and feed it
git/pql progress (T-59), sidebar badges (T-58), compaction state (T-244),
the status strip (T-274), with Q-34's budget slot reserved. Fix the T-274
plumbing bug first or the HUD inherits blank-slot syndrome.
5. **Active Ticket Context** `[S]`*cheapest win in the whole list* — picking
up a ticket binds it to the session: a "working on T-244" chip, auto
`in_progress` flip, `(T-NNN)` pre-suggested in commit messages, changelog
reminder on done. Makes kanban ambient instead of homework, and makes every
trail/ledger feature below reliable.
### Tier 2 — the differentiators (L/XL, each could headline a release)
6. **Twin-timeline rewind** `[L]` — snapshot the worktree as hidden git refs
(`git write-tree``refs/clide/checkpoints`) at every turn boundary, keyed
to turn uuid; every user-message card gains "restore files to before this."
Claude's `/rewind` only restores what Claude itself edited; clide owns both
timelines.
7. **Visual Dialog** `[L]` — one bidirectional scene schema: Claude draws
(D-91 canvas cards), the user annotates in the interaction zone (T-260), and
the annotations return as *structured geometry + flattened PNG*, not prose.
Merge the T-317/T-318 and T-260 specs into one protocol before they become
two dialects. *Two lenses converged here.*
8. **Immortal terminals** `[M]` — T-258 (terminal as editor-mode peer) fused
with T-325 live-tails: any long process — user shell *or* agent-spawned
build — promotes to a full tmux-backed surface that survives restart.
"The build that never dies" is structural for clide, a plugin fantasy for
Electron. Resolve Q-27 (swap vs split) as part of it, as T-258 already notes.
9. **Local cost ledger** `[M]` — per-turn cost/tokens persisted against the
active pql ticket; the board shows what each feature actually cost.
Flat-subscription opacity is the competitors' business model; turn-level
local cost data is clide's birthright. (Tokens primary, dollars advisory —
subscription auth reports notional costs.)
10. **Label-routed work queues / ticket dispatch** `[M→XL]` — T-277 labels +
the shipped pick-up path turn the board into an agent control surface;
the XL extension dispatches a ticket to a teammate session in an isolated
git worktree (SpawnSpec.cwd already exists). Local, no-telemetry
background agents with the work item, isolation, review surface, and audit
trail all in-repo.
### Tier 3 — moonshots (XL, pick one per quarter, they compound)
11. **Semantic terminal** — OSC 133 markers (clide spawns the shell, injection
is trivial) lift scrollback into foldable command regions with recognizers
for test runners and stack traces; real widgets between rows is something
xterm.js structurally cannot do. *Note: fix the scrollback-unreachable bug
first — a semantic scrollback you can't scroll is a koan.*
12. **Living codebase map** — tree-sitter imports + pql links + git churn on
the owned canvas, with live agent heat from `tool_use` events: watch Claude
*move through your codebase* in real time.
13. **Live mixed documents** — fenced blocks in the owned markdown renderer
become live embeds (canvas scenes, pql query results, decision cards,
confirm-gated command buttons). Notebook-grade, zero webview. The
`ClideMarkdownHooks` seam already exists.
14. **Remote Claude over SSH** — T-329 is fully ticketed and undersold: agent
on the buildbox, permission prompts rendering natively local. VS Code
Remote moves the editor; nobody remotes the *agent control channel*.
15. **Sealed-workspace mode** — an egress-audit proxy around the whole agent
stack, operationalizing D-60/D-64 into a provable property. The one
feature in this list competitors *cannot* copy without breaking their own
products.
### Honorable mentions
Plan-to-Board bridge (ExitPlanMode approval files the plan as a ticket tree),
Release Cockpit (renders `[Unreleased]` with word-count badges + one-action
release cut — would also unstall Part III #10), Daily Helm ("since you were
last here" pulse on the welcome screen, computed from data the repo already
commits), Total-recall conversation search (D-79 grep over the transcript
corpus + fork-from-here), Shared Gaze (attach editor selection/scroll context
to each outgoing turn), Speakable Layouts (`clide layout apply review.yaml`
the agent stages your workspace), Agent fleet tray (enumerate per-workspace
sockets, show every repo's agent state — *requires fixing T-247's stale-socket
litter first*), Governance Graph (the D/Q/R/T web as a navigable map — gives
the placeholder graph view a flagship dataset).
---
## Part V — If I were you, Monday morning
1. **One leak-fix commit**: PTY fd on natural exit + kernel-lookup-in-dispose
(terminal & claude panes) + project-switch service disposal. Three findings,
one theme, one afternoon.
2. **One Claude-resilience commit**: drain stderr, watch exitCode, seed status
on bind, gate auto-scroll on `_atBottom`. The flagship pane stops having
silent failure modes.
3. **One security commit**: MCP auth token + editor.* path confinement +
search.replace glob filter + symlink-walk fix.
4. **One rat-extermination day**: dead git API, ToolCheck, libc bindings,
ColumnHat, GraphView, tmux-era team pipeline, ptyc binary, mocktail. The
diff is gloriously red and the coverage denominator thanks you.
5. **Tag your releases.** Five releases of honest changelog work are currently
unaddressable commits.
6. Then go build the **Active Ticket chip** (S!) and the **Context X-ray**, and
let clide start showing people things no other IDE can.
---
*Findings methodology: every medium/high claim above survived an independent
adversarial re-read of the cited lines (one claim did not — the proposed
"can't-disable core extensions" guard, which D-14 deliberately rejects, so it
stays out of this report). The full per-finding evidence, severities, and
suggested fixes live in the review transcripts; ~34 additional low-severity
findings were verified by citation only.*
*— Fable, 2026-06-11*