Files
clide/fable-ous.md
T
jpmschweitzerandClaude Fable 5 3ed90fc318 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>
2026-06-12 00:13:52 +02:00

24 KiB
Raw Blame History

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-54stat() 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, /forkwidget.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 nowherenotify.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 deadtools/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, TeamMemberJoinednothing 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)

  1. Twin-timeline rewind [L] — snapshot the worktree as hidden git refs (git write-treerefs/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.

  2. 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.

  3. 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.

  4. 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.)

  5. 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)

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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