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>
24 KiB
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 capabilitiesboth 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-injectedModifierTapTracker— 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.
pumpAsyncdocuments whypumpAndSettlewedges;KernelFixtureencodes 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.dartdocumenting 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)
-
Every naturally-exited PTY leaks its master fd — forever.
lib/src/pty/native_pty.dart:444-450— on child EOF,_reap()sets_dead = truebut never closes_fd; a laterclose()short-circuits atif (_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. -
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--verbosespew = pipe fills = child blocks mid-turn = the flagship pane wedges with zero diagnostics), and nothing watchesexitCodeoronDone(line 303), so a crashed/dead session just looks… thoughtful. Drain stderr into a ring buffer, surface a terminalSessionEndedstate. This also intersects T-283 (no resume timeout/fallback). -
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/idelock format has a slot for it), require the header. -
editor.open/editor.saveskip path confinement entirely.lib/src/editor/registry.dart:215-219returns absolute paths verbatim, no..normalization, nopath_safetycall — an unconfined read and write primitive over IPC whilefiles.readis carefully guarded. Same family:search.replacesilently ignores its include/exclude globs (lib/src/search/replace_engine.dart:124-143) and will happily rewrite files outside the filter the user typed; andlistDir's symlink detection is dead code (lib/src/files/listing.dart:46-54—stat()follows links, soisSymlinkis always false) which meanswalkFilesdescends symlinked dirs the docs claim it skips. -
Closing a terminal pane never closes the shell.
lib/builtin/terminal/src/terminal_pane.dart:131-137callsClideKernel.of(context)fromdispose()— illegal ancestor lookup, swallowed bycatch (_)— sopane.closeis never sent and the backend PTY + daemon pane leak. The same idiom leaks the settings listener in every disposedClaudePane(claude_pane.dart:460-466). Combined with dragon #1 this is a two-stage leak pipeline. Cache the kernel ref indidChangeDependencies, delete the catch-all. -
Project switch leaks the entire previous workspace.
lib/main.dart:335-344— a new dispatcher gets freshPaneRegistry,FilesService,EditorRegistry, etc., but nothing calls the old set'sshutdown()methods (which exist and have zero callers). Old watchers keep emitting into the new workspace's bus. -
T-274's root cause, found and verified: the status bar is empty because
statusStreamis a plain broadcast controller — thesystem/initevent fires whilespawn()is still awaiting a 256KB transcript-tail read, before the pane ever subscribes (claude_pane.dart:322,session_orchestrator.dart:222-225). Seed fromsession.statuson bind, or make it replay-latest. (The broadcast-without-replay shape is a recurring bug factory — see Part III.) -
Auto-scroll yanks a scrolled-up reader to the bottom on every streamed token.
conversation_view.dart:268-277— the_atBottompin 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. -
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 insideTerminal.write. An emulator must never throw on hostile bytes. While in there: colon-form SGR sub-parameters are mangled into bogus params. -
A11y has drifted despite being a Tier-0 contract. Two independent verified findings:
ClideCollapserCard'sexcludeSemantics: truewipes 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 behindlib/(contrast checks fewer themes thanmain.dart:443-454loads; i18n checks 4 of 8 namespaces). The gates stay green while covering less. Makelib/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-180uses anasynconData without pausing the subscription — pipelined requests interleave, and the sharedStringBufferframing can drop/double lines.client.cast<List<int>>().transform(utf8.decoder).transform(LineSplitter())await forfixes 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;FileTailFollowerstarts mid-character by construction. AddwriteBytes()with a persistent chunked decoder. Orchestrator.spawn()races itself — check-then-act across two awaits; concurrent spawns for one id leak a liveclaudeprocess (session_orchestrator.dart:191-249). Hold aMap<String, Future<ManagedSession>>.- Fork panes misbehave on
/clear,/resume,/fork—widget.forkSourceIdwins forever, so/clearre-forks the original conversation instead of clearing (claude_pane.dart:266-281). - Settings persistence corrupts maps-inside-lists on write
(
settings.dart:199-219emitstoString()), 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 (
isSetAtinstead ofsetAt,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 syncexistsSync()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$HOMEshells. - The Notifications service renders nowhere —
notify.darthas zero widget consumers; cli_install's dogfood warnings vanish into an unrendered list whileToastServicesits 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-smokeare dead —tools/ui/*.shstillcdinto the removedapp/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)
-
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
initialDataworkaround. Write a tinyValueStreamwrapper once; retrofitstatusStream,busyStream,pendingPromptStream. -
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. -
Sync I/O in async handlers on the single isolate.
files.readdoes 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. -
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.
-
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
_userErrhelpers; 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. -
Hand-enumerated lists drift; export the truth. Bundled themes (already drifted between
main.dartand the testmode harness — catppuccin is silently unvalidated), a11y gate subjects, i18n namespaces. One exported const each, consumed by both sides. -
The claude builtin returns
okwith anerrorpayload in 16 handlers, drifting from the D-6 exit-code contract every other subsystem honors. A scriptedclide claude.agent.set-permission-mode bogusexits 0 today. -
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_consumeCsidiscarding intermediate bytes, which permanently blocks DECSCUSR/DECSTR). -
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. -
Close the release loop. Five CHANGELOG releases since the last git tag;
ci/release.shexits 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 touchingtest/,ci/, or the hook itself. Back-tag 2.2.0–2.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)
-
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 exacttool_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. -
Context X-ray
[M]— per-card token attribution ("this 40KB Bash tail is 12% of your window") + a real compaction indicator.stream_json_session.dartalready 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. -
Trust Ledger + decision-aware permission prompts
[M]— every permission rule with provenance (which prompt, which session, which ticket), ticket-scoped expiry; and when acan_use_toolrequest 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. -
Agent Activity HUD
[M]— four backlog tickets and one open question are secretly one feature: build T-59'sOperationsRegistryonce 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. -
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, autoin_progressflip,(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)
-
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/rewindonly restores what Claude itself edited; clide owns both timelines. -
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. -
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. -
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.) -
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)
- 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.
- Living codebase map — tree-sitter imports + pql links + git churn on
the owned canvas, with live agent heat from
tool_useevents: watch Claude move through your codebase in real time. - 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
ClideMarkdownHooksseam already exists. - 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.
- 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
- 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.
- 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. - One security commit: MCP auth token + editor.* path confinement + search.replace glob filter + symlink-walk fix.
- 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.
- Tag your releases. Five releases of honest changelog work are currently unaddressable commits.
- 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