Files
clide/governance/decisions/architecture.md
T
jpmschweitzerandClaude Opus 4.8 da61af810d decide D-87 (output/log dock) + resolve Q-28; file T-258 (terminal home)
D-87 — T-54's "output and log panel" is a bottom output dock: read-only,
two tabs (Output = the Logger stream, filterable + auto-scroll; Problems =
diagnostics moved out of the sidebar). Toggled by a single status-bar widget
that replaces the app-status indicator (merged health+log: green check when
clean, warn/error counts otherwise) via click or Cmd/Ctrl+J. Needs a bounded
ring sink on the Logger (no history today). Amends D-47: the dock is the one
surface allowed to push Claude up, capped so Claude stays >=50%.

Resolves Q-28 by splitting on interaction: read-only output (logs/problems)
goes in the dock; the terminal does NOT — it stays a first-class editor-pane
surface, tracked by new T-258 (swap-vs-split, with Q-27). Refines T-54.

Wireframe under docs/design/wireframes/output-dock/.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 22:57:18 +02:00

109 KiB
Raw Blame History

Architecture Decisions

Core, rendering, IPC, kernel, panel manager.


D-1: CLI-first, not MCP

  • Date: 2026-04-20 (was ADR 0001; ported from the claudian lineage)
  • Amendment (2026-05-15): D-1's intent — the CLI is the primary agent-facing surface, with the same contract as pql — stands. An additional /ide-compatible MCP surface is added per D-68; both wrap the same in-process dispatcher. The escape-hatch line in this record's Cost ("nothing here precludes adding [MCP] later that shells out to the same CLI") is realised — the MCP server does not bypass the CLI's surface, it offers a second transport to it.
  • Decision: Claude talks to clide exclusively via Bash (clide …). No MCP server. No protocol layer in Claude's face. The CLI uses the same exit-code + stderr-JSON contract as pql.
  • Context: The two mainstream options for the agent-facing surface were an MCP server or a plain Bash CLI matching pql's contract.
  • Rationale: Same mental model as pql for the agent — one tool-use pattern covers both. No MCP runtime to host, authenticate, or keep in sync with client versions. User/Claude parity is easier to enforce: every CLI subcommand must have a UI affordance in the Flutter app and vice versa (D-6). Claude Code's Bash(clide *) allow rule is the only configuration clide needs on the agent side.
  • Cost: If an MCP-only integration becomes compelling later (e.g. a multi-agent scenario), nothing here precludes adding one that shells out to the same CLI.
  • Raised by: Ported from the claudian lineage.

D-3: pql as supporter tool; clide wraps, never duplicates

  • Date: 2026-04-20 (was ADR 0003; ported from the claudian lineage)
  • Decision: Two complementary rules. (1) Wrap, don't duplicate. Clide never re-implements backlinks, ranking, frontmatter parsing, or wikilink resolution for query purposes. If a capability is missing in pql, it is added upstream in pql's repo and clide bumps the dependency. The only place clide contains pql logic is lib/src/pql/ — pure shell-outs to the pql binary. (2) pql is a clide subsystem when clide is present in the repo. On load, clide writes its current state into .pql/config.yaml — no conditional sync. Clide only stomps keys it manages (starting with ignore_files: — see D-4). Other pql config keys are left alone. Clide does not touch pql's index/cache data under <repo>/.pql/ — that stays pql's private store.
  • Context: pql is a pre-existing Go CLI that indexes a markdown-bearing directory tree into SQLite and exposes frontmatter, wikilinks, tags, headings, and bases through a query surface. Clide needs those capabilities for its Query panel, canvas drivers, graph view, and any feature that needs to know structure.
  • Rationale: One source of truth for markdown semantics. Any new query capability the UI wants goes through a pql upstream PR, not a local workaround. Users never have to learn pql's config file to get consistent behaviour — clide manages it. The arrow clide → pql is never inverted: pql stays ignorant of its wrapper.
  • Cost: Clide's lib/src/pql/ package is deliberately thin. pql is also the only query engine — Obsidian-style inline "bases" are explicitly not supported; queries live at the repo level. In repos without clide, pql works standalone unaffected.
  • Raised by: Ported from the claudian lineage. Load-bearing for D-39.

D-4: Ignore file strategy

  • Date: 2026-04-20 (was ADR 0004; ported from the claudian lineage)
  • Decision: One mechanism everywhere: the ignore_files: list in .pql/config.yaml. Ordered list of gitignore-shaped files; later entries win on per-pattern conflicts. pql defaults to ignore_files: [.gitignore]. Per D-3, clide writes the list on load — [.gitignore, .clideignore] if .clideignore exists, else [.gitignore]. .clideignore carries only the clide-specific deviations from .gitignore (supports !pattern negations); never duplicate gitignore's contents. Walker magic: none except .git/ — every other tool-owned dir (.pql/, .clide/) is added to .gitignore at install time; exclusion flows through the normal ignore_files: chain.
  • Context: Every file-enumerating surface in clide (pql query panels, canvas drivers, graph view, file watchers, pane lists, file tree) needs to skip the obvious junk — vendor/, node_modules/, dist/, build artifacts — or results drown in noise. Clide's working assumption is that the git repo is the workspace — no separate "vault" concept.
  • Rationale: Users get one config knob, in a file they might already know (pql users) or never need to touch (clide-only users). .clideignore is short by design — it's deltas, not a full list. Sidecar consumers read the same key and apply identical precedence, so Claude and the user always see the same filtered surface.
  • Cost: Removing clide from a repo leaves pql working with vanilla defaults (clide's last-written ignore_files: stays until pql or the user rewrites it; worth reconsidering during uninstall design).
  • Raised by: Ported from the claudian lineage.

D-5: Dart core; sidecar dissolved; ptyc as pql-peer

  • Date: 2026-04-20 (was ADR 0005; supersedes R-2)
  • Amendment (2026-04-23): The separate daemon process and two-package layout are dissolved per D-56. Dart-core principle survives; the daemon binary does not.
  • Amendment (2026-05-07): ptyc retired. PTY spawning moved to Dart FFI forkpty() (lib/src/pty/native_pty.dart). The ptyc/ source tree, PtySession, and scm_rights.dart are removed. pql remains the sole external supporter tool.
  • Amendment (2026-05-17): forkpty() replaced with posix_openpt() + posix_spawn() (T-96). forkpty calls fork() underneath, which is unsafe in the multithreaded Dart VM: ~5% of spawns deadlocked in the child before execve due to libc locks held by ghost-threads at fork time. posix_spawn uses vfork under glibc/musl/macOS, keeping the parent suspended until execve completes — no Dart code runs in the child. Side benefit: dropped the libutil.so.1 dynamic dependency; PTY now resolves entirely against libc via DynamicLibrary.process().
  • Decision: Three moves. (1) Dart is the core language. Everything that used to live under sidecar/ — IPC server, CLI dispatch, process management, file watching, git shell-outs, pql wrapper — is written in Dart. Two execution modes of one Dart AOT binary: clide <subcommand> (one-shot, pql-style) and clide --daemon (long-running, owns PTYs and subprocesses, survives app restarts). The Flutter app imports the Dart core as a library and connects to the daemon over IPC. (2) The sidecar directory dissolves. Layout is app/ (Flutter UI), lib/ (Dart core), bin/clide.dart (AOT entry), ptyc/ (C helper), no sidecar/, no Go module. (3) ptyc is a pql-peer supporter tool. Small C binary that does posix_openpt + fork + exec + fd-passing via SCM_RIGHTS; clide wraps it the same way it wraps pql. Shells out for every PTY (terminal pane, tmux session, Claude, LSP server, debug adapter — one code path). Consumers other than clide can use ptyc standalone.
  • Context: R-2 picked Go for the sidecar/CLI on two premises: (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so the muscle memory transfers. On reassessment, both premises broke: the "heavy work" is I/O-bound glue that dart:io covers cleanly — the real choice was separate process vs shared language, and separate-process is what matters. PTY is the one place Dart is genuinely weak (multi-threaded VM can't safely fork()), and once you accept a small native helper, nothing else needs to be in the same language.
  • Rationale: One toolchain for the IDE proper (Flutter + Dart). C toolchain needed only to build ptyc — tiny, rarely-changing. Session persistence stays because PTY master fds live in the Dart daemon process, not the app. ptyc naming: p for project (parallel to pql's project query language), ptyc reads as both "PTY + child" (domain vocabulary) and "PTY + C" (implementation language). Usable from Dart, Python, Go, shell — anywhere a subprocess can be spawned and a fd received.
  • Cost: Rust remains an escape hatch, not a plan. If a Dart limit later forces a second native helper (file-watching at scale on macOS, a tree-sitter host, etc.), the precedent is: new native need → new supporter tool, peer of pql and ptyc. Never a second "core language." Supply-chain gates stay, shape changes — Go govulncheck removed, Dart advisories review + exact-pin stays, ptyc gets a "read the 150 lines" review checklist (see make security).
  • Raised by: 2026-04-20 reassessment. See also the ptyc naming note in the original ADR (read as Project Terminal Controller / PTY+C / PTY+child).

D-6: CLI and event surface contract

  • Date: 2026-04-20 (was ADR 0006)
  • Decision: The CLI is organised into subsystems. Each subsystem owns a noun, a set of verbs, and a set of events. The set is closed at any point in time (documented); growth is additive (new verbs, new events — never renaming existing ones without a version bump). Initial subsystems (by tier): pane, tab, open, editor, panel, tree, git, pql, canvas, graph, theme, settings, project. Two umbrella entry points sit outside any subsystem: clide tail --events [--filter <subsystem>[:<id>]] and clide status. Command shape: clide <subsystem> <verb> [<positional>...] [--flag ...] [-- argv...]. Exit codes parity with pql (0/1/2/3/4 + 64-78 sysexits reserved); diagnostic JSON on stderr on non-zero exit; stdout stays machine-parseable on success. Events are JSON objects, one per line, with v, ts, type (<subsystem>.<verb_past|noun_changed>), subsystem, id, and payload; binary payloads base64. Every state-changing command emits at least one event; read-only commands emit nothing. Replay buffer per subsystem (default depth 16) so late subscribers still see recent effects. Parity rule: every UI affordance has a matching CLI verb (or a follow-up task naming the verb); every CLI verb surfaces in the UI (or documents why it's Claude-only).
  • Context: D-1 established that Claude drives clide via a Bash CLI. That decided the channel — it did not define the surface. CLAUDE.md stated the rule colloquially ("every CLI subcommand has a UI affordance … if you add one side without the other, the feature is incomplete"); this record restates it as an implementable contract that satisfies user/Claude parity, daemon-as-authoritative-state, and pql-style ergonomics at once.
  • Rationale: Surface is enumerable — adding a subsystem means adding a row and specifying verbs + events. Wire schema is versioned (v: 1 starting point; compatibility breaks bump the major and land alongside a pubspec.yaml schema_version: bump — see Q-5). Events are the only UI→app state channel; the Flutter app does not poll. Extensions inherit this — a Dart extension publishes a subsystem; the same registration pipeline exposes it to Claude via the CLI.
  • Cost: Replay-buffer memory per subsystem (cheap — most emit seldom). Back-pressure on firehose streams (Q-2), authorisation granularity (Q-1), and event persistence (Q-3) are all deferred until Tier 1 is in real use.
  • Raised by: 2026-04-20 planning.

D-7: App root is bare WidgetsApp

  • Date: 2026-04-21
  • Decision: The Flutter app root is WidgetsApp, not MaterialApp or CupertinoApp. Clide's look is fully custom; the Material/Cupertino shells would drag in opinionated theming, default icons, and platform chrome we'd then have to fight.
  • Rationale: Clide is a Linux-primary desktop IDE with a custom theme pipeline and custom primitives (panels, tabs, panes, canvas). Material's implicit theming collides with D-9; Cupertino is iOS-flavoured. WidgetsApp gives us routing, locale, focus traversal, semantics, and Directionality without aesthetic baggage.
  • Cost: We build and own every primitive; no ElevatedButton fallback. See R-3 and R-7.
  • Raised by: 2026-04-21 planning.

D-8: Feature-first folder layout

  • Date: 2026-04-21
  • Decision: Under lib/, organise by feature (kernel/, extension/, widgets/, builtin/<name>/) rather than by layer (models/, views/, controllers/). Private implementation lives under each feature's src/; the feature's public surface is a barrel file at the feature root (e.g. lib/kernel/kernel.dart).
  • Rationale: Features grow and get deleted as units; layer-first layouts fragment a feature across three directories and make deletions risky. Matches extensions-as-features (every extension already has its own folder).
  • Cost: Imports cross features only via the barrel — enforce by review, no automated check yet.
  • Raised by: 2026-04-21 planning.

D-9: Three-tier theme pipeline

  • Date: 2026-04-21
  • Decision: Themes resolve through three layers: (1) palette — raw named colours per theme YAML; (2) semantic — roles like surface.background, text.primary, accent.focus; (3) surface — component-scoped tokens derived from semantic roles (button bg/fg/border hover/pressed/disabled states).
  • Rationale: Direct palette-to-component binding collapses under multi-theme work; VS Code's 600-token surface map is the proof. The semantic layer is where a11y contrast gates apply; the surface layer is where components bind.
  • Cost: Three layers to keep coherent per theme. Contrast gate (D-22) enforces the semantic layer on every bundled theme.
  • Raised by: 2026-04-21 planning.

D-10: State management — ChangeNotifier + ListenableBuilder

  • Date: 2026-04-21
  • Decision: Per-feature state uses ChangeNotifier exposed through a feature facade (singleton-per-kernel); widgets subscribe via ListenableBuilder. No Riverpod, Provider, BLoC, or Redux.
  • Rationale: SDK-shipped, zero deps, trivial to fake in tests (hand-rolled fakes in D-25). Violates D-31 prefer-zero-deps otherwise. See R-8.
  • Cost: No codegen ergonomics; manual notifyListeners() discipline. The ListenableBuilder.listenable contract rejects rebuilds outside the subscribed notifier — intentional.
  • Raised by: 2026-04-21 planning.

D-11: Panel manager is kernel; layout is data; three-column is a preset

  • Date: 2026-04-21
  • Decision: The kernel owns a panel manager that treats layout as declarative data (tree of splits + leaves). The default "three-column IDE" (sidebar / editor / assistant) is one preset; alternative presets (writer-focus single-column, debugger four-pane) ship as data, not code forks.
  • Rationale: Hard-coded three-column layouts paint us into corners when future tiers add canvas, graph, terminal-grid. Data-driven layout also lets extensions contribute presets without patching the panel manager.
  • Cost: More kernel surface up-front; pays back at Tier 5 (canvas) and Tier 6 (extension-contributed layouts).
  • Raised by: 2026-04-21 planning.

D-12: Kernel admission rule — mandatory shared singletons only

  • Date: 2026-04-21
  • Decision: A service joins the kernel only if it is (a) mandatory for app boot and (b) a shared singleton across features. Everything else is an extension or a feature-local service.
  • Rationale: Keeps the kernel auditable. Previous drafts piled "useful globals" into the kernel; result was a 40-service god-object. The admission rule forced 18 services out of 31 candidates.
  • Cost: Some legitimate cross-cutting concerns (telemetry, crash reporter when they land) must pass the test; we expect a few more admissions as Tiers 3-6 land.
  • Raised by: 2026-04-21 planning.

D-13: Git hardcoded in kernel project-loader

  • Date: 2026-04-21
  • Decision: The kernel's project loader treats "repo root" as a git concept — runs git rev-parse --show-toplevel to find workspace root, subscribes to filesystem events, and shells out to git for status/diff/stage. No VCS abstraction layer.
  • Rationale: Option B (VCS abstraction) is premature generalisation — we have one VCS today, Mercurial/Fossil/Sapling users are a rounding error on the Linux desktop IDE market, and the abstraction adds a seam that has to be tested against nothing. When a second VCS shows up we refactor.
  • Cost: Adding Mercurial support later costs a real refactor, not just a plugin. Acceptable.
  • Raised by: 2026-04-21 planning.

D-14: Two-tier disable — kernel locked, everything else extension-shaped

  • Date: 2026-04-21
  • Decision: Kernel services cannot be disabled at runtime. Extensions (including every bundled built-in) can be toggled via the extension manager. This creates exactly two disable tiers: kernel (always on) and extension (toggleable).
  • Rationale: A three-tier system (kernel / bundled-cannot-disable / user-can-disable) is dishonest — if a "bundled built-in" can't be disabled, it's kernel and belongs in kernel admission review. Forcing every bundled feature to pass the extension contract is also the best test we have that the contract is actually usable.
  • Cost: Disabling builtin.default_layout by mistake produces an empty window. Mitigated by the kernel's first-boot defaults and a "reset extensions" action.
  • Raised by: 2026-04-21 planning.

D-41: Claude panes — one primary per repo, tmux-backed

  • Date: 2026-04-22
  • Decision: Every repo (keyed on the git root) hosts exactly one primary Claude pane plus zero or more secondary Claude panes. The primary persists across clide restarts; secondaries are ephemeral. Persistence layer is tmux: the daemon spawns the primary as tmux new-session -A -s clide-claude-<repohash> -- claude, which re-attaches to the running session if the app restarts. Secondaries spawn as tmux new-session -A -s clide-claude-<repohash>-N -- claude with N incrementing. Close semantics: closing a secondary kills that tmux session and focus collapses back to the primary (or to the next-most-recent secondary); the primary has no close affordance — close-gestures on it hide it / minimise to a dock, they don't kill the session. Daemon is the owner; the UI doesn't track tmux session state directly, it just asks the pane subsystem to spawn/close and observes events. General-purpose terminal panes (builtin.terminal) do not get tmux wrapping or persistence — they're per-app-lifetime.
  • Context: 2026-04-22 planning. The user workflow is "open repo → Claude is already there, with my last conversation intact." A cold session-restart every time clide re-launches defeats the premise. tmux already solves "reattach to a shell-like session across disconnects"; layering our own persistence protocol on top of ptyc would duplicate it.
  • Rationale: (1) tmux is battle-tested — no new persistence code to review. (2) The pane subsystem stays neutral; Claude-specific behaviour lives in builtin.claude. (3) Keying by git root means the user doesn't manage session names manually — opening a repo is enough. (4) "Always one primary" removes a failure mode: there's never "no Claude to talk to." (5) Secondaries stay frictionless — the user spawns and closes them at will without breaking the primary.
  • Cost: Requires tmux on the PATH of the daemon's runtime environment (reasonable for Linux + macOS; Windows support via WSL or a separate approach). Killing a primary (via the daemon on shutdown) still leaves the detached tmux session around until the next clide start re-attaches; acceptable but worth documenting for support. Secondary numbering (-1, -2, …) resets between clide runs since ephemeral state is lost — also acceptable.
  • Raised by: 2026-04-22 planning, Tier 1 implementation.
  • Cross-reference: D-5 (ptyc as the spawn primitive tmux runs under), D-6 (pane.* IPC surface), R-9 (why per-repo scoping via git root matches the wrap-don't-duplicate theme).

D-43: Design handoff — adopt token palettes, reject Material wrapper

  • Date: 2026-04-22
  • Decision: The claude.ai/design handoff (docs/claude-design/) delivers hi-fi mockups, interaction flows, a design system, and four theme palettes (clide, midnight, paper, terminal) as Dart files using MaterialApp/ThemeData. We adopt the colour tokens, layout annotations, typography direction, and syntax highlighting palettes. We reject the MaterialApp wrapper — tokens are translated into our existing YAML theme pipeline and SurfaceTokens (per D-7). The design files stay in docs/claude-design/ as reference; they are not runtime assets.
  • Rationale: The design's value is in the palette + layout + component vocabulary, not in the delivery format. Material's ThemeData fights our bare-WidgetsApp + CustomPaint stance. Translating tokens preserves design intent without absorbing Material's widget opinions.
  • Cost: Manual translation of four theme files into YAML. Ongoing: any design refresh needs the same translation pass.
  • Cross-reference: D-7, D-9, R-12.
  • Raised by: 2026-04-22 design handoff review.

D-44: Four bundled themes — clide, midnight, paper, terminal

  • Date: 2026-04-22
  • Decision: Ship four bundled themes replacing the single summer-night preset. clide (cool near-black + periwinkle, default), midnight (VS Code-adjacent muted dark), paper (drafting-sheet light), terminal (near-black + amber). All share the same semantic token names. Source palettes in docs/claude-design/themes/; runtime YAML under lib/kernel/src/theme/themes/.
  • Rationale: Summer-night was a placeholder carried from the legacy TUI. The design system delivers a coherent set of four that covers dark, muted-dark, light, and monochrome workflows.
  • Cost: Summer-night users lose their theme (acceptable — it was dev-only). Four YAML files to maintain.
  • Cross-reference: D-43, D-22.
  • Raised by: 2026-04-22 design handoff review.

D-45: Syntax highlighting tokens in the theme pipeline

  • Date: 2026-04-22
  • Decision: Add syntax-role colour tokens to SurfaceTokens: keyword, type, string, number, comment, method, punctuation. Each bundled theme defines these. The editor and diff views consume them; tree-sitter (when it lands per Q-15) maps grammar scopes to these tokens.
  • Rationale: The design system ships syntax palettes per theme. Adding them now means the token surface is ready when syntax highlighting lands.
  • Cost: Seven new fields on SurfaceTokens. Default resolution falls back to semantic roles (keyword → accent, comment → textMuted, etc.) so themes that don't declare syntax tokens still compile.
  • Raised by: 2026-04-22 design handoff review.

D-47: Interaction model — Claude-is-home layout

  • Date: 2026-04-22
  • Amendment (2026-06-06): Rule (1) (prompt-bar Y invariant across all states) gains one documented exception: the bottom output dock (D-87) may re-baseline the prompt bar by pushing Claude up when it opens, capped so Claude stays the largest surface (≥ 50% of the middle column). Closed dock → original baseline. The dock is the only surface permitted to move the prompt-bar Y; everything else still obeys rule (1).
  • Decision: The prompt bar is pinned to a fixed Y-position in the middle column; every other surface makes room around Claude — never on top, never pushing the prompt off-Y. Three hard rules: (1) prompt bar Y-position is invariant across all states (open, collapsed, focus, editor, viewer); (2) the three bottom strips (left icon rail, app strip, right icon rail) align to one continuous horizontal line; (3) Claude is always the largest surface when present. The three-column layout from D-11 is refined: left = overview (tickets, decisions, files, git, PRs), middle = Claude (+ optional editor above), right = context (viewer, pql graph, links, images). Both side panels have a bottom icon rail for section switching; keyboard: ⌥15 (left), context-type switcher (right).
  • Rationale: "Claude is home" means the prompt never moves, regardless of what opens or closes around it. Every layout mutation respects this invariant. The three-column refinement assigns purpose to columns rather than leaving them generic.
  • Cost: The prompt bar invariant constrains future layout presets — any preset that repositions Claude must explicitly break this rule. Editor mode (see D-49) is the only case where another surface shares the middle column, and it opens above Claude rather than displacing it.
  • Cross-reference: D-11, D-48, D-49.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-48: Chrome budget — no tabs, no breadcrumbs, keyboard-first

  • Date: 2026-04-22
  • Decision: Clide deletes classic IDE chrome: no buffer tabs, no breadcrumbs, no VS Code-style activity bar, no separate status bar row (merged into app strip). Total persistent chrome: 2 edge arrows (collapse toggles), 1 hover-only glyph per panel (focus mode), 0 always-visible buttons beyond icon rails. ⌘P overlay is the fuzzy finder — no layout shift. Keyboard is the primary interaction surface; icons are escape hatches. Files open individually; opening a second file closes the first (split on explicit command — deferred, see Q-27).
  • Rationale: Every pixel of chrome that isn't Claude is a tax on the "Claude is home" principle. Tabs and breadcrumbs are navigation affordances for a multi-buffer editor; clide's editor is a secondary surface (viewer ↔ editor swap per D-49), not a primary one. The fuzzy finder (⌘P) replaces all navigation chrome.
  • Cost: Users accustomed to VS Code/IntelliJ tab workflows have no tabs to fall back on. Mitigated by ⌘P fuzzy find being the universal navigation path. Resolves T-22 (multi-buffer editor tabs) as rejected in favour of this approach.
  • Cross-reference: D-47, D-49.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-49: Editor mode — inline above Claude, viewer swap

  • Date: 2026-04-22
  • Decision: Editor invoked via ⌘E on a file or icon in a viewer. Editor lifts above Claude in the middle column, occupying 3040% of vertical space; Claude keeps the remainder; prompt bar Y unchanged. Close with ⌘W. Draggable divider between editor and Claude. The viewer (👁) and editor () are mutually exclusive for the same file — a click on a viewer promotes the file to editor in the middle column and snaps the right panel back to nav; a 👁 click on an editor demotes the file to viewer in the right panel and closes the editor. Different files can coexist (editor on main.dart + viewer on README.md). When editor is open on .md, the viewer auto-opens with live sync to editor content; no auto-viewer for non-renderable files (.dart, .yaml, etc.).
  • Rationale: The editor is not a primary surface — it's a temporary intervention. Claude's prompt bar must never move (D-47), so the editor opens above, not replacing. The viewer ↔ editor swap prevents two surfaces showing the same file simultaneously, which simplifies state management and avoids confusion about which surface is authoritative.
  • Cost: Only one file in the editor at a time (no tabs per D-48). Power users wanting two files side-by-side must wait for split (see Q-27).
  • Cross-reference: D-47, D-48.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-50: Context auto-behavior — right panel reacts to Claude

  • Date: 2026-04-22
  • Decision: The right panel responds to Claude's content references automatically: (1) right open + empty → panel holds footprint, stays empty; (2) right open + viewer loaded + Claude links foo.md → swap in, replaces current viewer; (3) right collapsed + Claude links foo.md → badge on spine ("2"), no layout shift; (4) editor open on .md → viewer auto-opens with live sync; (5) editor on non-renderable file → no auto-viewer.
  • Rationale: Claude is the driver; the context panel is reactive. Auto-swapping when the panel is open reduces user clicks. Badging when collapsed respects the user's decision to collapse — no involuntary layout shifts.
  • Cost: The auto-swap requires the daemon (or Claude integration) to emit structured content references, not just terminal text. This implies a lightweight parser or event that identifies file references in Claude's output — deferred to implementation.
  • Cross-reference: D-47, D-51.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-51: Panel collapse — 12px spine with badge

  • Date: 2026-04-22
  • Decision: When collapsed, a panel becomes a 12px spine: vertically rotated label ("tickets" / "context"), no icon rail, paper-2 background (slightly darker than main paper), border on inner edge only. Click anywhere on spine to expand. If a context badge is pending (e.g. Claude linked a file while collapsed): small filled dot with count at top of spine. Edge arrow on outer boundary toggles collapse; keyboard: ⌘⇧1 (left) / ⌘⇧3 (right). Expand restores prior size and section state.
  • Rationale: Collapsed panels must not consume significant horizontal space (12px = 1 icon-width) but must remain discoverable and able to signal pending content. The badge-on-spine avoids involuntary expand while still communicating that something arrived.
  • Cost: The spine replaces the current simple setVisible(false) toggle with a real collapsed-state widget. Collapse state must be persisted across sessions (see D-53).
  • Cross-reference: D-47, D-50.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-52: Focus mode — full-window takeover

  • Date: 2026-04-22
  • Decision: Focus mode entered via double-click on panel header, hover-visible glyph in header, or ⌘.. Active panel takes the full window; all others hidden. Header shows "Esc" hint. Esc restores the exact prior layout (collapse state, divider positions, active sections). Focus mode is per-panel, not per-tab.
  • Rationale: When the user wants to concentrate on a single surface — Claude conversation, file tree, diff view — they shouldn't have to manually collapse both side panels. Focus mode is a single-action "maximise and restore" with no state loss.
  • Cost: Must snapshot and restore full LayoutArrangement state on enter/exit. Interacts with responsive behaviour — focus mode at narrow widths should work identically.
  • Cross-reference: D-47, D-53.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-53: State persistence across sessions

  • Date: 2026-04-22
  • Decision: The following layout state is persisted across app restarts: collapse state of left and right panels, active left section (tickets/decisions/files/git/pr), active right context type, pql pane expanded/collapsed, editor split ratio when open, fuzzy find recent picks. Stored via SettingsStore in project-scoped settings (.clide/settings.yaml).
  • Rationale: Users expect their workspace layout to survive restarts. Without persistence, every launch starts at the default layout preset, which is disorienting when the user has customised their column widths and panel states.
  • Cost: Adds write-on-change to several layout operations. Must handle migration if the setting keys evolve. .clide/settings.yaml is already gitignored, so personal layout state stays personal.
  • Cross-reference: D-47, D-51, D-52.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-54: Keyboard map — canonical shortcuts

  • Date: 2026-04-22
  • Decision: Canonical keyboard shortcuts (cross-platform, = Ctrl on Linux): ⌘P fuzzy find overlay; ⌘⇧1 / ⌘⇧3 collapse/expand left / right panel; ⌘1 / ⌘2 / ⌘3 focus left / middle / right panel; ⌘. toggle focus mode on focused panel; ⌥1–⌥5 left-panel section switch (tickets, decisions, files, git, pr); ⌘E open current file in editor; ⌘W close editor / dismiss viewer; Esc exit focus mode / close fuzzy finder / dismiss viewer. Responsive breakpoints: ≥ 1600px splits relax toward 30%; 12001600px default (L 200px, R 220px, middle flex); < 1200px splits toward 40%, consider auto-collapse right; < 1000px deferred (see Q-26).
  • Rationale: These shortcuts follow the "keyboard is the primary surface" principle from D-48. The set is minimal and covers all layout operations. ⌘. for focus mode follows VS Code precedent (quick-fix → general "do the thing").
  • Cost: Some shortcuts may conflict with OS-level bindings on specific Linux desktops; the keybinding resolver (D-17) allows user override.
  • Cross-reference: D-47, D-48, D-52.
  • Raised by: 2026-04-22 interaction model spec (Wireframe — Flows v3).

D-55: Claude pane internal tabs for multi-session

  • Date: 2026-04-23
  • Decision: Multiple Claude sessions share the workspace as internal tabs inside the Claude pane header — not as workspace-level tabs (which would violate D-48). The primary session tab has no close affordance (per D-41). Secondary session tabs show a close ×. A small + button sits at the right end of the tab row to spawn a new secondary. Double-clicking empty space in the tab row also spawns a new secondary. When a secondary is closed, focus collapses to the most-recently-active remaining tab (primary or another secondary). The tab row is hidden when only the primary exists — it appears on first secondary spawn and disappears when the last secondary closes. Session names in the tab row use the tmux session name slug (readable path, per the session naming convention).
  • Amendment to D-41: D-41 defined the lifecycle (primary persists, secondaries are ephemeral, close semantics) but left the multi-session UI unspecified. This record fills that gap. The claude.new-secondary command (already registered but not wired) is the spawn mechanism; the tab row is the UI surface.
  • Rationale: The workspace is Claude's space (D-47). Multiple Claude sessions are a Claude concern, not a workspace concern. Internal tabs keep the multiplicity contained — the workspace slot doesn't know how many sessions exist, it just renders the Claude pane. The hide-when-one rule keeps the common case (single primary) chrome-free.
  • Cost: The Claude pane grows its own tab model (lightweight — just a list of session IDs + which is active). The builtin.claude extension owns this; no kernel changes needed.
  • Cross-reference: D-41, D-47, D-48.
  • Raised by: 2026-04-23 interaction model refinement.

D-56: Dissolve daemon process; Flutter app hosts IPC server

  • Date: 2026-04-23
  • Decision: The separate Dart daemon process (clide --daemon) and the two-package repo layout (lib/ core + app/ Flutter) are dissolved. The Flutter app moves to the repo root (one pubspec.yaml) and hosts the IPC server in-process. All subsystem handlers (pane, files, editor, git, pql) run inside the Flutter process. The bin/clide.dart AOT binary is removed. The CLI surface for Claude (clide <command>) becomes a thin C client — either a new peer of ptyc or a mode within ptyc itself — that connects to the app's unix socket, sends a JSON-lines request, prints the response, and exits. tmux owns session persistence (it already did per D-41); the daemon's PTY ownership was redundant.
  • Amendment to D-5: D-5's "two execution modes of one Dart AOT binary" premise assumed the daemon needed to outlive the app to preserve PTY sessions. tmux already solves this — tmux new-session -A re-attaches regardless of which process originally spawned it. The daemon process added complexity (two packages, two build targets, IPC client/server split, process lifecycle management) without a benefit tmux doesn't already provide. D-5's other principles survive: Dart is the core language, ptyc is a C peer of pql, one language for the IDE proper.
  • Repo layout after dissolution:
    • /pubspec.yaml — single Flutter package (was app/pubspec.yaml)
    • /lib/ — all Dart code: kernel, extensions, widgets, subsystem handlers
    • /bin/ — empty or removed (CLI is a C binary now)
    • /test/ — all tests
    • /assets/ — fonts, themes, grammars, licenses
    • /ptyc/ — C PTY helper (unchanged)
    • /native/ — vendored native libs (libtree-sitter.so)
    • /decisions/, /docs/, /legacy/ — unchanged
  • Rationale: One package means one pubspec.yaml, one flutter analyze, one flutter test, no cd gymnastics, no cross-package import barriers. The IPC server running in-process eliminates the daemon lifecycle (start, stop, reconnect, pid file). If the app crashes, tmux sessions survive; the app re-attaches on restart. The CLI client in C is ~100 lines (socket connect + JSON exchange) with the same contract as pql.
  • Cost: If the Flutter app is not running, Claude's clide commands fail. In practice this is acceptable — the IDE being closed means the user isn't working. A future "headless mode" could start the Flutter engine without a window if needed.
  • Amendment (2026-05-19): Implemented in T-99 across eight slices (T-124 server, T-125 argv translator, T-126 C client, T-127 socket loopback replacing InProcessClient, T-128 legacy-IPC cleanup, T-129 event streaming, T-130 MCP companion, T-131 this wrap-up). Per-workspace unix socket at the D-70 path; D-71 chmod gate; D-72 multi-connection serial dispatch; D-73 MCP transport. The C clide client lives at native/clide-cli/clide.c and ships with make clide-cli. Only the socket IPC model survives — InProcessClient + IsolateClient + Backend gone.
  • Cross-reference: D-5 (amended), D-41 (tmux persistence), D-1 (CLI-first surface preserved via C client), D-70 / D-71 / D-72 (implementation contracts).
  • Raised by: 2026-04-23 architectural simplification.

D-57: Frameless custom chrome with per-column 24px hats

  • Date: 2026-04-23
  • Decision: The OS-native title bar is hidden. Each of the three columns wears its own 24px "hat" that serves as both a drag region and a host for window controls. Left hat: macOS traffic lights (Linux/Windows: plain drag). Center hat: clide > branch label, always present. Right hat: minimize/maximize/close glyph buttons on Linux/Windows (macOS: plain drag). Entire hat surface is draggable; buttons opt out of hit testing. When a column collapses to a 12px spine, its hat shrinks to a 12px drag cap — no buttons, still draggable. The center hat never collapses. Three ChromeStyle variants: seam (default desktop — full hats), prompt (center hat only — presentations/focus), inline (web/wasm — no hats, browser owns window controls). Persisted in settings as app.chromeStyle. Platform bridge via MethodChannel('clide/window') — custom GTK C and Cocoa Swift handlers, no third-party package.
  • Resolves: Q-6.
  • Rationale: The GTK headerbar wastes 30+ vertical pixels and clashes with the custom theme. Per-column hats add zero net rows — they reuse the space each column header already occupied. Custom FFI avoids a window_manager dependency (D-31). The ChromeStyle enum keeps web builds clean and allows user override.
  • Cost: ~150 lines C (GTK) + ~100 lines Swift (Cocoa) for the platform channel. Window controls become unreachable when their column collapses — mitigated by keyboard shortcuts (⌘Q to close, ⌘1/⌘3 to expand).
  • Cross-reference: D-47 (center hat always visible), D-51 (spine-cap behavior).
  • Raised by: 2026-04-23 interaction model refinement.

D-64: No telemetry — architectural commitment

  • Date: 2026-05-03
  • Decision: clide does not phone home. No analytics SDKs (Firebase, Sentry, Mixpanel, hand-rolled). No crash reporters that upload automatically — crashes produce local logs the user can read and optionally attach to a manual bug report. No auto-update checks without user action. No license validation calls. No feature flags fetched from a server. No A/B testing, experiments, remote config, or "anonymous usage statistics." This is not a "default off" setting; it is an architectural commitment. Proposals to add telemetry under any framing — opt-in, anonymized, debug-only, "just errors" — are out of scope for this project, full stop.
  • Rationale: clide is a space to think, not a surface for data collection. Users installing clide are choosing a tool that does not watch them. That promise is worth more than any data we could collect. The architectural commitment is the feature.
  • Cost: No usage data for product decisions; no automated crash triage. Accepted — user trust is the product decision.
  • Cross-reference: D-60, POLICY.md.
  • Raised by: 2026-05-03 policy-to-decision migration (T-28).

D-68: Dual integration surface — Bash CLI primary, MCP secondary

  • Date: 2026-05-15
  • Amendment (2026-05-19): Both surfaces implemented in T-99. CLI lands as IpcServer over the unix socket (T-124, per D-70/71/72) with the C client at native/clide-cli/clide.c (T-126); the argv grammar lives in lib/src/cli/argv_to_request.dart (T-125). MCP lands as McpServer over HTTP+SSE (T-130, per D-73) — the transport choice closed Q-33. The two /ide minimum tools ship as stubs; real implementations follow as Q-32 resolves the broader tool-surface question.
  • Decision: clide exposes two integration surfaces over the same in-process DaemonDispatcher. (1) Bash CLI over Unix socket — primary. Per D-1 and D-56, a thin C client (clide …) connects to a per-user Unix socket served in-process by the Flutter app, exchanges JSON-lines, and exits. This is the surface Claude-Code-in-a-pane uses; it is also the surface for human shell use, scripts, and external editor integrations. Full action surface — pane.*, files.*, editor.*, git.*, pql.*, …. (2) /ide-compatible MCP server — secondary. clide additionally serves an MCP endpoint compatible with Claude Code's /ide integration (the same protocol VS Code and JetBrains plugins serve). Minimum tools: mcp__ide__getDiagnostics, mcp__ide__executeCode. Optional mcp__clide__* namespace exposing high-leverage clide tools is deferred to Q-32. Transport closed by D-73 — SSE over HTTP. The MCP server wraps the same DaemonDispatcher; there is no second source of truth.
  • Context: D-1 chose CLI-first over MCP-only because MCP alone doesn't cover the action surface clide needs — Claude Code's /ide MCP exposes only two narrow tools (getDiagnostics, executeCode), enough for Claude to read diagnostics and run Jupyter cells but not enough to drive an IDE. The CLI surface gives full reach. But for users who run Claude Code outside clide and connect via /ide, MCP is the only path Claude Code knows; not serving it means clide is invisible to that workflow. The two surfaces are complementary, not alternatives. Reinforced by the 2026-05-14 consultant review: the architect flagged the absent socket server as the most critical drift; user confirmed the socket server (D-56 path a) plus an MCP companion.
  • Rationale: Both surfaces wrap the same dispatcher, so neither becomes a second source of truth. CLI remains the contract user/Claude parity (D-6) is enforced against. MCP is added because the /ide ecosystem is real and growing — VS Code, JetBrains, Cursor, Windsurf all serve compatible MCP — and clide should be a peer there. The implementation cost is a protocol adapter + tool definitions, not duplicate business logic.
  • Cost: Two transports to maintain. Mitigated by both wrapping the same dispatcher: the MCP adapter is the only thing that has to track /ide protocol evolution. If mcp__clide__* tools are added (pending Q-32), surface bloat is the obvious risk — every CLI verb invites an MCP twin; resist by default, justify on user need.
  • Cross-reference: D-1 (amended — see amendment line there), D-6, D-56, Q-32, Q-33.
  • Raised by: 2026-05-15 — consultant review (consultants.md) flagged the absent socket server (D-56 unimplemented) as the highest architectural drift; user chose option (a) "implement the server" and asked for MCP coverage alongside.

D-70: IPC socket path is per-workspace, deterministic

  • Date: 2026-05-18
  • Decision: The Unix-domain IPC socket served by the Flutter app (per D-56 / D-68) lives at $XDG_RUNTIME_DIR/clide/<hash(workspace-root)>.sock on Linux and $HOME/Library/Caches/clide/<hash(workspace-root)>.sock on macOS. The workspace root is the git toplevel (the same path the Flutter app resolved on boot). The hash is FNV-1a 64-bit, lower-case hex (16 chars) — deterministic, dependency-free (no package:crypto), matches the existing session_naming.dart _hash shape so users see one hashing pattern across clide's process boundaries. No env override. The C client (T-126) and any other consumer resolves its target socket by walking CWD up to the git toplevel and computing the same hash.
  • Rationale: "Repo-is-the-workspace" (CLAUDE.md guardrail) means clide instances are per-repo, so the socket must be too — a per-user-global socket would force one running clide per user and break the multi-repo workflow. The same hash on both sides ensures the shell client + the running app always agree without configuration. No env override because the deterministic path is the contract; the only reason to override is a test fixture, and tests can set XDG_RUNTIME_DIR to a tempdir directly. FNV-1a over a crypto hash: collision resistance isn't a security need (workspace paths are user-supplied; the path is 0600-readable only by that user anyway); 64 bits is overkill for the cardinality (a user with 65k workspaces would still see negligible birthday collisions). Aligns with D-41's tmux-socket-per-repo convention so users see one consistent pattern.
  • Cost: The 16-char hex prefix means socket paths aren't human-readable at a glance — ls $XDG_RUNTIME_DIR/clide/ won't tell you which one is which repo. Acceptable; the C client never asks the user to type the path, and debugging can use a sibling .path file next to each socket if it becomes painful.
  • Cross-reference: D-41, D-56, D-68, lib/kernel/src/files.dart (workspace root resolution).
  • Raised by: 2026-05-18 — T-99 design pass; locked in before T-124 starts so the server + client agree on path strategy.

D-71: IPC socket access gated by chmod 0600 on socket + parent

  • Date: 2026-05-18
  • Decision: The IPC socket file and its parent directory (D-70) are created with permissions 0600 (owner read/write only) and 0700 respectively. No token-based auth at the IPC layer; the Unix file-permission check is the only gate. Capability-scoped auth for third-party Lua/Dart extensions is a separate concern and remains tracked by Q-1 for when extensions actually need it.
  • Rationale: clide's threat model on a developer workstation is "another user on the same host should not be able to drive my IDE." File perms cover this exhaustively — the kernel enforces the check on every connect(2), no userspace token comparison can match that. Adding a session token on top would be belt-and-suspenders without expanding the threat model. Capability tokens become useful when extensions can publish their own dispatcher routes and we want to gate which third-party code can reach which subsystem — but that's a Tier-6 concern.
  • Cost: Doesn't defend against same-uid attacks (a malicious process running as the user can connect). Accepted — same-uid is outside this layer's threat model; that's a sandboxing / capability concern that lives further out. Doesn't work on shared multi-user dev hosts where one socket needs to be reachable by multiple uids — clide isn't targeted at that workflow today.
  • Cross-reference: D-70, Q-1.
  • Raised by: 2026-05-18 — T-99 design pass.

D-72: IPC server is multi-connection with serial dispatch on the main isolate

  • Date: 2026-05-18
  • Decision: The IPC server (D-70) is a multi-connection accept loop over ServerSocket.listen. Multiple clients (one-shot clide <verb> calls, a long-lived clide tail --events subscriber, the MCP adapter) can hold simultaneous connections. Each connection reads its own JSON-line stream asynchronously. Dispatch through the existing DaemonDispatcher is serial on the main Flutter isolate — the dispatcher walks one request at a time. Individual handlers are free to offload heavy or blocking work to short-lived worker isolates (the pattern NativePty and SchedulerService already use); the IPC layer doesn't impose that choice.
  • Rationale: Multi-connection at the socket layer is what every other concurrent operation in the codebase needs — clide tail --events (T-129) is structurally a long-lived subscription, the MCP adapter (T-130) lives on a separate connection from the CLI's one-shot requests, and parallel CLI invocations from a developer's shell shouldn't serialise on the I/O level. Serial dispatch on the main isolate is forced by the architecture, not chosen: subsystem handlers (PaneRegistry, FilesService, EditorRegistry, GitClient, PqlClient) hold mutable Dart objects + ChangeNotifiers the UI rebuilds from, and Dart isolates don't share heaps. A worker-isolate-per-connection design would have to ferry every request back to the main isolate via SendPort to actually execute — pure overhead with no parallel-dispatch gain. Per-handler isolate offload solves the only real problem (a slow handler janking the UI) without paying the isolate-safety tax across every subsystem.
  • Cost: A genuinely slow handler that doesn't offload to an isolate blocks the dispatch queue for all other connections until it returns. Mitigated by the per-handler offload pattern already in the codebase. Doesn't fit a future world where clide hosts headless workers (CI, batch jobs) that want true parallel dispatch — that's a different product shape and would warrant rethinking this decision.
  • Cross-reference: D-56, D-70, lib/src/pty/native_pty.dart (per-handler isolate offload example), lib/kernel/src/scheduler.dart (same pattern).
  • Raised by: 2026-05-18 — T-99 design pass; user explicitly considered worker-isolate-per-connection and confirmed serial dispatch on main is the right shape given the shared-state architecture.

D-73: MCP transport for /ide is SSE over HTTP

  • Date: 2026-05-19
  • Decision: The /ide-compatible MCP server clide ships per D-68 uses HTTP + Server-Sent Events as its transport. The Flutter app binds an HTTP server on a random localhost port at startup, advertises itself via a discovery file at $HOME/.claude/ide/<pid>.lock (the format Claude Code's /ide command discovers), and serves: a GET /sse endpoint that opens a long-lived SSE stream for server-to-client JSON-RPC responses + events, and a POST /messages endpoint that accepts client-to-server JSON-RPC requests. Closes Q-33.
  • Rationale: clide's Flutter app is always-running and user-launched — the agent connects to it, not the other way around. That rules out stdio (which assumes the agent spawns the server as a subprocess). Between SSE and WebSocket, SSE wins on three counts: (a) Claude Code's existing /ide discovery already uses HTTP servers advertised via lock files, (b) SSE is trivially implementable on dart:io's HttpServer (long-lived response + data: <json>\n\n per message — no upgrade dance, no framing), (c) JSON-RPC is fundamentally client-pushes-requests / server-pushes-responses-and-events, which maps cleanly to "POST in / SSE out." WebSocket buys bidirectional symmetry we don't need. Per D-72 the MCP layer is just another transport that wraps the same DaemonDispatcher — no second source of truth.
  • Cost: SSE requires a long-lived HTTP response. Browsers cap concurrent SSE connections per origin at 6, but the consumers here are Claude Code instances (not browsers) and one-per-workspace is the expected fan-out. Adds an HTTP listener alongside the unix socket — a small surface increase, but the alternatives are worse. Each running clide grabs a random localhost port; no contention.
  • Cross-reference: D-68, D-72, lib/src/ipc/mcp_server.dart (this transport's implementation lands in T-130).
  • Raised by: 2026-05-19 — T-130 design pass; user picked SSE over HTTP after weighing against WebSocket and stdio.

D-74: IPC command schema is co-registered with the handler, validated at dispatch

  • Date: 2026-05-20
  • Decision: Each IPC command may declare a typed argument schema (per-arg charset/regex/range constraints, required/optional, kind). The schema is registered alongside the handler, not authored in a central file: the DaemonDispatcher registration API carries an optional schema per command, and the dispatcher accumulates a cmd → schema registry as commands register. DaemonDispatcher.dispatch validates req.args against the command's schema before invoking the handler (lib/src/daemon/dispatcher.dart), returning a userError (sysexit 64) on violation so no handler sees malformed input. Built-in command modules (registerPaneCommands, registerGitCommands, …) supply their schemas at registration; extension-contributed commands (CommandContribution, Tier 6 / D-46) carry their own. The pre-existing T-104 spot-checks — validateGitRef in lib/src/git/operations.dart and the count/path caps in lib/src/daemon/git_commands.dart — stay in place as defense-in-depth below the dispatcher, because the git client is also callable directly from the Flutter UI (not only through the dispatcher). MCP tools/list generation from this registry is out of scope here — deferred to the T-130 track.
  • Rationale: clide's command surface is open, not fixed: every register*Commands() module already contributes a set of commands, and the extension framework lets plugins contribute more at runtime. A central static schema map (command_schema.dart keyed by every known cmd) cannot see extension-contributed commands and would fight the plugin model — it was considered and rejected for exactly that reason. Co-registration keeps the schema next to the code that owns the command's meaning, supports any number of independent registrants (the normal case, not an edge case), and still yields a single validation lookup at dispatch time. Validating at the dispatcher (rather than per-handler) is what makes the constraint a contract instead of a convention: a new command can't forget to validate, and the _ResizeArgs-style hand-lifts scattered through handlers (see panel_commands.dart, T-119) collapse into the schema. No third-party validation library — the constraint vocabulary is hand-rolled per the prefer-zero-deps guardrail.
  • Cost: A schema must be authored for each command (build-fresh — the argv parser at lib/src/cli/argv_to_request.dart only knows syntactic shape, never per-command argument names, so there is nothing to "lift"). Keeping validateGitRef + caps as well as the schema is deliberate redundancy on the git path; the alternative (single gate at the dispatcher) would leave the UI→client call path unvalidated. Until every command has a schema, validation is opt-in per command — commands with no registered schema dispatch unvalidated, same as today.
  • Cross-reference: D-6 (CLI/Claude parity the schema enforces), D-68 / D-72 (the dispatch path being gated), D-46 (extension model the central-map alternative would have broken), lib/src/daemon/dispatcher.dart (the validation hook), lib/src/git/operations.dart + lib/src/daemon/git_commands.dart (the T-104 checks kept as defense-in-depth).
  • Raised by: 2026-05-20 — T-120 design pass; user rejected the central registry as misaligned with the extensions/plugins model and confirmed co-registration with multiple command-set registrants.

D-75: Claude rendered natively from transcripts; terminal retained as general tool only

  • Date: 2026-05-22
  • Decision: clide renders the Claude conversation as native Flutter widgets driven by Claude Code's transcript JSONL (~/.claude/projects/<munged-cwd>/<session-id>.jsonl; teammate agents under <session-id>/subagents/agent-<id>.jsonl). The PTY/terminal emulator is not used to scrape or render Claude's TUI output. Claude still runs under tmux (-L clide) for process and session persistence per D-41, but its content is sourced from the transcript file, not from the PTY stream. The terminal emulator (builtin.terminal) is retained and fully functional as a general-purpose IDE tool — shell sessions, build output, REPL — but it is not the Claude rendering surface. All transcript parsing is isolated behind a single reader/observer module (lib/builtin/claude/src/transcript/) keyed off the transcript version field; coupling to Claude Code internal contracts (transcript JSONL schema, ~/.claude/teams/*/config.json, tmux control mode) is isolated and version-pinned (initially claude 2.1.148). Principle ordering: Claude-centric first; CLI-first (D-1, D-6) is a strong second. D-6 CLI/event surfaces are preserved where sensible; CLI-first no longer blocks the Claude integration.
  • Rationale: (a) Native Flutter rendering gives cross-widget text selection and copy — the one meaningful advantage of a terminal emulator for plain-text output, recovered in a more flexible form. (b) Removes OS-variant PTY/TUI rendering fragility (xterm escape sequences, tmux passthrough, font-metric alignment) from the Claude display path. (c) It is the substrate for surfacing tmux agent teams as real GUI panels: once the transcript reader exists, each subagent's .jsonl file feeds its own panel with no extra IPC. (d) Structured transcript data enables features that scraped terminal text cannot: message-level copy, per-message actions, cost/token annotation, search, diff highlighting. Accepting version-drifting coupling to CC internals is mitigated by isolation + version-pin + fixture tests: the reader module is the only file that knows the JSONL shape; bumping Claude means updating one module and running the fixture suite.
  • Cost: Depends on undocumented and potentially version-drifting Claude Code internals (transcript JSONL schema, subagent path conventions, ~/.claude/teams/*/config.json). Mitigated by: (1) isolation — all parsing behind lib/builtin/claude/src/transcript/, nothing else touches the schema; (2) version-pinning — initial target claude 2.1.148, bumps are deliberate and tested; (3) fixture tests — golden JSONL snapshots at each pinned version exercise the reader. If the schema drifts beyond the reader's tolerance, the Claude pane degrades gracefully (shows a parse-error banner with the raw transcript path) rather than crashing.
  • Cross-reference: D-1 (CLI-first preserved as strong second), D-6 (CLI/event surfaces preserved), D-41 (tmux process persistence unchanged), D-56 (Flutter app hosts in-process). Implemented by epic T-132.
  • Raised by: 2026-05-22 — native Claude integration epic (T-132); user set Claude-centric > CLI-first ordering.

D-76: ClaudeConfig — Claude's config surface is clide's app settings (builtin-owned, watched, probe-cached per version)

  • Date: 2026-05-23
  • Decision: A single ClaudeConfig service in lib/builtin/claude/ is the app-wide source of truth for Claude Code's environment: skills (<scope>/skills/*/SKILL.md), custom slash commands (<scope>/commands/*.md), settings.json, and permission rules (allow/deny/ask) — read from both the global scope (~/.claude/) and the local repo scope (.claude/), layered local-over-global (same ordering discipline as D-4's ignore_files:). It exposes typed, listenable views and is refreshed by FileWatcher (lib/src/files/watcher.dart) on both directories plus an explicit refresh. Built-in slash commands (not on disk) are obtained by a one-shot claude --output-format stream-json probe whose result is cached keyed on the resolved claude version id, so additions/deprecations are re-captured on upgrade; a small static list is the fallback when the probe is unavailable. The service is builtin-owned, not a kernel service — Claude is a non-disableable extension but still an extension, so the kernel stays Claude-agnostic and there is one ownership pattern. Consumers (composer slash typeahead, the status pane, later the team/permission surfaces) read from ClaudeConfig; none re-scan the filesystem or re-derive the command list.
  • Rationale: (a) Claude is clide's primary citizen, so its on-disk config is clide's settings surface — centralizing it removes per-feature filesystem scans and keeps one cache + one watcher. (b) Reuse: the slash list, skills, and permission/model defaults are needed by more than one surface (typeahead + status pane at minimum); a singleton avoids divergent copies. (c) Probe-and-cache-per-version honors "the CLI owns command/skill discovery" (the IDE enumerates from the CLI rather than reimplementing resolution) while bounding the cost of the extra process to once per version. (d) Builtin ownership preserves the kernel/extension boundary — the kernel does not learn about Claude — at the cost of Claude not being a kernel-level "setting", which is acceptable since nothing generic needs it.
  • Cost: Extends the version-drifting CC coupling already accepted in D-75 from the transcript/team schema to the config layout (skills/commands dir conventions, settings.json shape, permission-rule shape, the stream-json init slash_commands payload). Mitigated the same way: all of it lives behind ClaudeConfig; the slash-command probe is version-keyed; on a parse miss the service degrades to whatever it could read (and the static built-in fallback) rather than failing. The probe assumes claude --output-format stream-json emits an init message listing slash_commands — to be confirmed empirically against the pinned version before the typeahead depends on it.
  • Cross-reference: D-75 (native rendering + accepted CC-internals coupling — this extends it to config), D-1 (CLI owns command/skill resolution; clide enumerates, never reimplements), D-4 (global/local layering discipline), D-3 (wrap-don't-duplicate analogue for Claude config).
  • Raised by: 2026-05-23 — slash-command typeahead work surfaced that clide has no command/skill enumeration today; user directed a top-level builtin-owned config object reused across surfaces, probe-cached per claude version.

D-77: Drive Claude via the stream-json control protocol; teams become a clide-owned coordination layer

  • Date: 2026-05-24
  • Status: accepted — phased (direction confirmed 2026-05-24; phase-1 single-agent first, phase-2 team-coordination scope refined per its own tickets)
  • Decision: Drive the Claude pane through Claude Code's stream-json control protocol (claude --input-format stream-json --output-format stream-json --verbose, with --permission-prompt-tool / SDK-style canUseTool handling) instead of running an interactive TUI inside tmux -L clide. Conversation content comes from the structured event stream (assistant / tool_use / tool_result / result), not from tailing the transcript JSONL. Session continuity is via --resume <session-id> (persistence through the transcript files, not tmux). Consequence: Claude Code's built-in experimental agent-team mode is tmux/interactive-only and not usable headless, so clide stops observing a Claude-run tmux team and instead orchestrates its own team — N independent stream-json Claude processes that clide spawns, renders natively, and coordinates through a clide-hosted MCP server that supplies the messaging/task tooling Claude's tmux mode provided for free.
  • Rationale: (a) Closes the interactive-prompt gap that the TUI model can't: permission requests and AskUserQuestion arrive as structured canUseTool callbacks (toolName == 'AskUserQuestion', input carries the questions); clide renders a native prompt and returns the answer — exactly the native-rendering control clide wants, with no capture-pane text-scraping. (b) Structured events replace fragile transcript-file tailing and the "is this the active session?" guessing. (c) --resume persistence is documented to be identical to the TUI and retires the --session-id-already-in-use / session-fork class of bugs (T-156, T-161) and the tmux-session lifecycle entirely. (d) Owning team orchestration is more in line with "own the stack" than wrapping an undocumented, drift-prone tmux feature.
  • Team-awareness + messaging (the load-bearing design question): Claude has no documented way for an independent session to be a teammate — the team config/SendMessage/task-list are undocumented and coupled to the tmux runtime. So clide manufactures team membership:
    • Awareness — inject roster + role into each agent via --append-system-prompt (and/or --agents <json>): "you are <name>, a teammate on <team>; members are …; use the team tools to message them." The agent doesn't know it's solo; it's told it's a member and given tools that behave like membership.
    • Messaging/tasks — clide hosts a small MCP server (attached to every agent via --mcp-config) exposing send_message(to, text), broadcast(text), list_teammates(), inbox() / a shared claim_task/task_status. clide is the broker: a message from agent A's tool call is delivered into agent B's next turn (as a user/tool message on B's stream-json stdin). The built-in SendMessage + auto-delivery + task list do not work outside tmux mode, so clide reimplements them — which means clide fully controls routing, ordering, and what the UI shows.
    • Orchestration — there is no SDK/CLI "multi-agent controller" primitive; clide spawns and multiplexes the N processes itself. The existing team UI (T-140 tiles, T-141/T-157 sidebar) is re-pointed from "observe tmux team" to "render clide-managed agents"; the T-139 observer becomes an orchestrator.
  • What it unlocks (unified session model): With clide owning spawn + I/O + the broker, four things that are distinct today collapse into one primitive — a clide-managed Claude session rendered as a pane: a teammate, a secondary tab, a forked branch, and an inline subagent become the same thing. Consequences: (a) the sidebar becomes the cockpit — the task list and inter-agent messages are local data clide owns, so the sidebar doesn't just display them, it lets the user act (reassign a task, inject/redirect a message, mute/spawn/show/hide an agent); (b) panel-vs-inline dissolves — every session is always live, and showing it as a pane is just a visibility toggle on the roster; (c) fork-into-a-pane — "branch this conversation" is simply spawning a managed session seeded from another's context (--resume + --fork-session) through the same plumbing, surfaced as a slash action or a sidebar button. This reframes phase 2 from "reimplement Claude's tmux teams" to "build the unified model the tmux teams only approximated," and is a primary argument for the pivot.
  • Cost / risk: Large. It reworks the Claude input/render path (stream-json multiplexer + canUseTool prompt UI), replaces D-41's tmux persistence with --resume, and rebuilds teams as a clide-owned coordination layer + MCP broker — building messaging/task-sync that the tmux mode gave for free. Undocumented surfaces (team-awareness mechanism, SendMessage schema, AskUserQuestion-over-stream-json specifics) mean clide reimplements rather than wraps, accepting drift risk isolated behind the orchestrator + MCP server. The interactive terminal builtin remains for general shell use (unchanged). Mitigation / phasing: ship the single-agent pivot first (stream-json events + canUseTool permissions + AskUserQuestion UI + --resume), which alone fixes the prompt gap and the session-lifecycle bugs; treat clide-owned teams as a separate follow-on epic, decided on its own once the single-agent path is proven.
  • Cross-reference: amends D-41 (tmux-for-persistence → --resume; tmux retained only for the general terminal, not Claude), evolves D-75 (native rendering kept, but sourced from the stream-json event stream rather than the transcript file), and supersedes the alternative "keep the tmux TUI + a capture-pane/send-keys prompt bridge" (rejected as fragile text-scraping that still can't give structured permissions). Relates to D-1 (clide now also hosts an MCP server for inter-agent tooling), D-76.
  • Raised by: 2026-05-24 — user testing found the UI cannot handle AskUserQuestion / permission prompts (they live in the TUI, never hit the transcript). Spike (docs + SDK) confirmed stream-json surfaces prompts via canUseTool but agent teams are tmux-only; user chose the stream-json direction, accepting that teams must become clide-orchestrated.

D-78: Claude permission/prompt transport is the stdio control channel

  • Date: 2026-05-25
  • Status: accepted
  • Decision: Carry Claude's conversation, permission prompts, and AskUserQuestion over the stream-json stdio control channel — spawn with --permission-prompt-tool stdio, receive can_use_tool control_requests, reply with a control_response. Do not route these through MCP. MCP is reserved for the orthogonal job of giving agents tools/capabilities (IDE context; the team messaging/task broker per D-77/T-170). This refines D-77's abstract "canUseTool handling" into the concrete transport choice.
  • Empirically confirmed (claude 2.1.150; full shapes + fixtures in docs/spikes/cc-stream-json-control-protocol-2.1.150.md): stdio is mandatory — without it, "ask" tools are silently auto-denied and no prompt reaches the client; an allow decision must echo back updatedInput (a bare {behavior:"allow"} is rejected); deny requires message; AskUserQuestion is permission-gated through the same can_use_tool channel and answered by injecting updatedInput.answers (question-text → chosen label), not via a tool_result.
  • Rationale: Directness — no extra process or loopback "networking", and Claude hands us structured prompt metadata (display_name / description / permission_suggestions) for free. It is exactly what the SDK's canUseTool maps to. Routing permissions through MCP instead is strictly more machinery (the broker must be up before the first prompt; the structured fields must be hand-rolled) for no gain.
  • Cost / risk: The control channel is an undocumented internal contract pinned to a claude version — Anthropic may change or remove it on any bump. Mitigation: pin claude_code_version; the captured spike logs serve as regression fixtures / canaries; all protocol framing lives behind one module (per D-77) so the transport is a one-seam swap. Documented fallback menu so a shift never lands us at a blank slate (preference order): (1) MCP permission tool--permission-prompt-tool mcp__clide__approve against the T-170 broker (strongest; MCP is a public/stable protocol and the broker will already exist); (2) degrade to --permission-mode acceptEdits / dontAsk / bypassPermissions as a reduced-fidelity stopgap; (3) the Agent SDK if a stable public canUseTool surface ships; (4) ACP (session/request_permission) if the ecosystem converges on it. Detection symptoms + detail in the spike doc.
  • Cross-reference: refines D-77; relates to D-1 (clide hosts MCP for capabilities, never for the conversation/permission transport) and D-76. Implemented by T-166.
  • Raised by: 2026-05-25 — during T-165/T-166 work, an empirical spike against claude 2.1.150 (driving the real CLI + reading the shipped binary's zod schemas) nailed the control-protocol shapes. User weighed stdio vs MCP for permissions, chose stdio for directness, and asked that the brittleness and researched alternatives be documented so a future Anthropic change doesn't leave clide without options.

D-79: Workspace content search is a pure-Dart in-process engine, outside pql

  • Date: 2026-05-31
  • Status: accepted
  • Decision: Find-in-files / search-and-replace (T-52/T-53) run as a pure-Dart, in-process grep engine — an isolate worker pool fans non-ignored files out across cores, matches with RegExp (with a literal indexOf fast-path when the regex toggle is off), and streams matches back over a single engine-agnostic IPC verb (search.grep) with cancellation when the query changes. It does not shell out to ripgrep/grep, and it does not route through pql.
  • Rationale / D-3 boundary: D-3 says clide wraps pql for query surfaces — but pql search is a ranked full-text document index (returns path/score/connections, no line numbers, snippets, regex, case, or glob). Content-grep ("match-in-context, click-to-line, regex/case/glob") is a code-navigation primitive pql does not offer, so it is explicitly outside pql's query surface and is clide's to own (consistent with the "own the rendering/tooling stack" guardrail). A shell-out to ripgrep was rejected as the default: it adds an unvendored external binary that isn't guaranteed present (esp. cross-platform), against prefer-zero-deps and single-process.
  • Performance: the I/O floor (walk + read) is shared by every engine. ripgrep's edge is multithreading + SIMD literal prefilters + a non-backtracking DFA; the Dart engine recovers the dominant win (parallelism) via an isolate pool, sidesteps the regex-engine gap for the common case via the literal fast-path, and hides the rest behind streaming + cancellation. Net: interactive (sub-second) on realistic repos including this one; ripgrep only pulls visibly ahead at monorepo scale clide is not targeting.
  • Escape hatch (de-risk): the search.grep request/result contract is engine-agnostic. If a giant-repo benchmark ever demands it, an optional "use rg when on PATH, else the built-in engine" accelerator can slot in behind the same verb with no caller changes — recorded as future work on T-52, not built now.
  • Distinct from structural search: this is text grep. Structural/semantic search ("find usages", "go to definition") is tree-sitter's job (clide already vendors libtree-sitter.so for highlighting) and is a separate future feature, not part of T-52.
  • Cross-reference: clarifies D-3 (pql wrap boundary) and the "own the stack" guardrail; relates to D-4 (the engine honors the full ignore_files: layering — see T-52, which closes the never-filed ignore-layering placeholder, a (to be recorded) comment in files_commands.dart). Implemented by T-52 (engine + find-in-files) and T-53 (replace).
  • Raised by: 2026-05-31 — during /whats-next refinement of the search/nav batch (T-51/T-52/T-53). Refinement surfaced that pql search structurally can't satisfy find-in-files; the user probed tree-sitter (ruled out — it's a parser, not a grepper) and the performance ceiling, then chose the fastest reasonable Dart option (isolate pool + literal fast-path + streaming) over a ripgrep dependency.

D-80: files.read allows trusted Claude config roots beyond the workspace

  • Date: 2026-06-01
  • Status: accepted
  • Decision: files.read accepts an allow-list of read roots: the workspace root (as before) plus the resolved Claude config directories — the global ~/.claude and the repo-local <repo>/.claude (the latter already lives under the workspace). A path is readable when it is contained by any allowed root; everything else is still rejected with path outside workspace. This widens reads only — writes (search.replace, future files.write) stay confined to the workspace root.
  • Rationale: Per D-76 Claude's config surface (skills/agents/commands under ~/.claude + the repo .claude) is clide-managed and surfaced in the Config tab. Opening a surfaced skill's SKILL.md in the markdown reader is a legitimate, expected action, but ~/.claude is global and outside the repo — the original [T-102] confinement rejected it (path outside workspace). Extending the allow-list to exactly the config roots the app already reads is the user's chosen model ("the .claude dir is in the workspace") and is simpler than a separate trusted-read verb.
  • Security boundary: This is a bounded widening, not a hole. Only the explicitly-listed config roots are added; arbitrary off-repo paths and .. traversal are still rejected, and the symlink re-check ([resolveUnderRootsFollowingSymlinks]) re-verifies the real path is contained by one of the allowed roots (so a symlink under a config root pointing to /etc/shadow is still refused). The roots are the user's own trusted Claude config (same trust level as pql's data per D-3), and writes are unaffected.
  • Cross-reference: amends the read side of D-4/T-102's "repo-is-the-workspace" confinement; builds on T-194 (absolute-under-root reads). Implemented by T-195 — FilesService.extraReadRoots, wired in main.dart to ~/.claude when present.
  • Raised by: 2026-06-01 — after T-194 fixed repo-scope skill reads, the user hit path outside workspace opening a user-scope skill (~/.claude/skills/peon-ping-toggle/SKILL.md) and said the .claude dir should be in the workspace. Chose extending the read allow-list over a separate trusted-read verb.

D-81: Right-pane reader load is driven by a retained ReaderNav, not per-view state or bus retention

  • Date: 2026-06-01
  • Status: accepted
  • Decision: The right-pane readers (markdown, decisions) load content from a retained per-reader ReaderNav (a kernel ChangeNotifier held in a ReaderNavRegistry), not from per-view State and not from a "retained/replay-latest" MessageBus channel. ReaderNav owns the back/forward history + pin, subscribes to its reader's selection channel to record entries, and (re-)emits load — the single channel a reader displays from. A reader grabs nav.current on mount (so a selection that revealed its tab before it subscribed isn't lost), and loads from load while mounted. Back/forward/pin navigate the nav and re-emit load, so every load flows through one path.
  • Rationale: The bug (T-196): a reader subscribes in didChangeDependencies, which runs only after the tab is revealed, so the broadcast selection that triggered the reveal is already gone — first click did nothing. A post-frame re-publish "fixed" it but is timing-fragile (hard to test deterministically). Two clean options remained: make the MessageBus retain-and-replay the last value to new subscribers, or hold the retained state in the nav-history helper. The nav-history is the better home — the reader's "what am I showing" is its current history entry, the helper already exists for back/forward, and it keeps the MessageBus a dumb pipe (no per-channel retention semantics, no unbounded memory question). Grab-on-mount is a pull; the bus stays push-only.
  • Cost / alternatives: Rejected bus retention (BehaviorSubject-style replay) as the wrong layer — it would bake stateful replay into every channel and surprise non-reader subscribers. Rejected per-view ReaderHistory (the old ReaderHistoryMixin, now deleted) because it dies with the widget, which is the whole bug. The cost is a small kernel service (ReaderNavRegistry) on KernelServices + the extension context.
  • Cross-reference: fixes T-196; the readers reveal their tab on selection (extension) then pull nav.current. Relates to D-78-era reader work (T-187/188/189). Implemented in lib/kernel/src/reader_nav.dart + the markdown/decisions readers.
  • Raised by: 2026-06-01 — the user reported decisions opening only on the second click, diagnosed the lost-on-mount race, and explicitly chose a "right-pane nav history helper" over a MessageBus fix ("leaving them in the messagebus is the wrong shape").

D-82: Keymap sequences are space-separated; matching is a reusable matcher consumed at the interception point

  • Date: 2026-06-01
  • Status: accepted
  • Decision: A keymap binding's keys: may be a multi-chord sequence written as a space-separated string ('d d', 'g g', 'ctrl+k ctrl+s'): space means "then". The existing forms are unchanged — + joins modifiers within one chord, and a YAML list ([ctrl+p, meta+p]) still means alternation ("or"). A leading digit run in normal mode is captured as a repeat count and applied by firing the resolved intent N times (not threaded into the intent payload). Sequence matching is a reusable two-part facility: Keymap answers a stateless prefix/exact/none query over its bindings, and a small stateful SequenceMatcher (pending buffer + count + timeout) wraps it. Interception lives at the consumer, not in the global key handler.
  • Rationale: Real Vim needs dd, gg, dw, ciw, 5j — impossible under single-chord resolution (T-205). Of the candidate separators, every punctuation option (, ; >) is itself a bindable key (Vim leader, repeat-find, indent), so each would force an escape rule (\>). A literal space never appears as a key spec — the space key is always spelled space — so it separates with zero collisions and no escaping, and matches Vim-doc / VS Code convention (ctrl+k ctrl+s). Count-by-repeat keeps intents const and payload-free. The interception split is forced by the host: the global dispatch is a passive KeyboardListener (returns void, cannot swallow events), so normal-mode keys can't be intercepted there before EditableText types them — the editor's Focus.onKeyEvent (returns KeyEventResult) is the only place that can consume them. Putting the matcher there (T-206) keeps T-205 pure, headless, and unit-testable, and avoids the global handler buffering keys it has no power to swallow.
  • Cost / alternatives: Rejected a full Vim grammar engine (operator × motion × text-object × count combinatorics) for the first pass — common operator+motion combos are enumerated as explicit sequence bindings in vim.yaml instead, covering the demo surface without a parser. Rejected comma/>/semicolon separators (escape wart). Rejected count-in-intent-payload (would de-const every intent and bloat the bridge). Rejected buffering in the global resolveEvent (it can't swallow, so it would double-handle with the editor). Cost: KeymapBinding generalises chord → an ordered chord list; a new SequenceMatcher; the editor owns interception.
  • Cross-reference: T-205 (matcher + notation), T-206 (editor interception + motions), T-65 (vim.yaml). Builds on the T-117 keymap layer; vim.* scope flags from D-81-era work are set by the Vim mode service (T-207).
  • Raised by: 2026-06-01 — scoping the Vim preset for a Vim-power-user demo; the user weighed ,/;/> separators and flagged the escaping problem, which made space the collision-free choice.

D-83: Dogfood agent model — hosted stream-json session primary, external CLI driver secondary

  • Date: 2026-06-03
  • Status: accepted
  • Decision: There are two distinct "Claude inside clide" agents, and clide commits to both with an explicit primary. (A) The clide-hosted stream-json session is the primary dogfood target. Per D-77/D-78 clide spawns the Claude process, so it owns that child's environment — this is the agent clide can fully bootstrap (inject CLIDE_SOCK/CLIDE_WORKSPACE, guarantee clide on its PATH, inject a context note, seed a Bash(clide *) allow rule — Epic B / T-214) and the agent the rest of the product is built around. (B) An external Claude Code harness driving via clide … is a first-class but secondary, best-effort integration — the same surface D-68 already opens to "human shell use, scripts, and external editor integrations." clide serves the per-workspace socket (D-70) to anyone who can reach it and offers a manual install affordance (T-212), but makes no promise to observe an external agent's non-clide tool use (plain file reads, make test, git) — those bypass the socket and are outside clide's view by construction.
  • Parity scope (how this frames Epic C): the D-6 "agent sees what the user sees" contract is scoped to clide's own surfaces, reflected through the CLI in both directions — the live UI panes/buffers/active-file become readable via pane list / editor active / clide status (T-219/T-220/T-221), and an external agent's clide … mutations are observable on the event bus. An external agent's side-channel tool use (reads/tests/git issued in its own shell) is explicitly out of parity scope: clide reflects what flows through clide, not what an unowned process does elsewhere.
  • Rationale: (a) clide can only bootstrap a process whose launch it controls — env, PATH, permission rules, and context injection are all things you set when you spawn, not things you can push into a shell you didn't start; so the hosted session (A) is where Epic B's leverage actually exists, which makes it the right primary. (b) Naming (A) primary aligns the dogfood target with the product's main interaction surface (the native Claude pane, D-77). (c) Refusing to drop (B) keeps faith with D-68 (the CLI is deliberately an external-integration surface) and with reality — the self-analysis probe and this very initiative are being driven by an external harness; declaring it out of scope would make clide unable to describe its own development. (d) Scoping parity to "what flows through clide" keeps the contract honest and testable: clide can verify it mirrors its own UI state, but cannot truthfully promise to observe tool calls that never touch its socket.
  • Cost / risk: Two supported models means two bootstrap stories. Mitigated by the asymmetry being the point — (A) gets the full automated bootstrap; (B) gets a documented manual path (install clide, the deterministic socket resolves the rest) and best-effort observability, not a second full implementation. The honest limit on (B) — clide is blind to an external agent's non-clide work — is a stated boundary, not a bug to chase; an external agent that wants clide to see an action runs it through clide ….
  • Cross-reference: answers Gap 5 of docs/self-analysis.md; gates D-77/D-78 (the hosted session this names primary), D-68 (the external CLI surface kept as secondary), D-6 (parity scoped here), D-70 (the socket an external agent reaches). Frames Epic B (T-214) bootstrap target and Epic C (T-218) observability scope under initiative T-208 "Give Claude hands". Implemented/decided by T-224.
  • Raised by: 2026-06-03 — T-224, processing the self-analysis.md dogfood probe from inside clide. The probe itself ran as an external harness (B) while the product is built around the hosted session (A); user chose "both, primary = hosted (A)" so Epic B optimizes the bootstrap clide can actually control while keeping the external driver a supported, best-effort integration.

D-84: Diff view placement — editor-mode inline above Claude, spawned from the git sidebar

  • Date: 2026-06-05
  • Status: accepted
  • Decision: The diff view is not a workspace tab (its current placement violates D-48 "no buffer/workspace tabs"). It opens as an editor-mode surface — lifting above Claude in the middle column, the same mechanism as the inline editor (D-49) — and is spawned from the git sidebar (selecting a changed file opens its diff there). It is not a context-panel viewer.
  • Rationale: A diff is a review and intervention surface, not just passive inspection: to be fully functional it must host hunk-level stage/discard and (later) conflict-resolution widgets, which need horizontal room. The context panel (≈420px, D-50 viewer semantics) is too narrow to comfortably hold those controls; the middle column above Claude gives the space while keeping Claude's prompt bar fixed (D-47). Spawning from the git sidebar matches the user's mental model (left = change list, the diff opens in the work area). This also partially serves the side-by-side-compare desire behind Q-27 without a true editor split.
  • Cost / risk: The editor-mode surface currently hosts the single inline editor; it must generalize to host a diff too (editor and diff are mutually exclusive in that slot, like the viewer↔editor swap in D-49). Implementation is a follow-up story, not part of this decision ticket (T-42).
  • Cross-reference: D-48 (no workspace tabs — the rule the old placement broke), D-49 (the editor-mode mechanism reused), D-50 (the viewer model explicitly rejected here), D-47 (prompt bar fixed), Q-27 (partially served). Decides T-42.
  • Raised by: 2026-06-05 — T-42 (decide diff view placement). User chose editor-mode over the context-panel viewer: a diff needs space for resolution widgets and spawns from the git sidebar, so it's too cramped to compress into the right panel.

D-85: Event bus delivery — bounded ring-buffer back-pressure; in-memory cursor retention, bus-owned if persisted

  • Date: 2026-06-06
  • Status: accepted
  • Decision: The in-memory event bus (D-6) gets explicit delivery + retention semantics, resolving Q-2 (back-pressure) and Q-3 (persistence). (1) Back-pressure — bounded per-subscriber ring buffer, drop-oldest. Each subscriber gets a bounded ring; when a slow or absent reader fills it, the oldest events are dropped and a per-subscriber dropped-count is incremented. The producer never blocks and subscribers are never force-disconnected. (2) Cursor retention — in-memory, monotonic cursor. The pull API (clide events --since <cursor>, T-223) is served from a bounded in-memory ring keyed by a monotonically increasing cursor; --since returns the events after the cursor plus a next-cursor, and reports a gap marker (the dropped-count) when the requested cursor has already aged out of the ring — so a caller detects loss instead of silently missing events. No on-disk persistence in v1. (3) If persistence is ever needed (audit log, undo history) it is owned by the bus itself, not a separate subscriber-subsystem. ADR 0006 carried the opposite as a bare one-line position in its open-questions footer ("a subsystem that subscribes and writes — not a property of the bus") with no rationale — which is exactly why it migrated to Q-3 rather than a decision; this resolves that open question the other way, on the reasoning below.
  • Rationale: drop-oldest plus a visible gap marker is the only policy that keeps an interactive IDE responsive — no producer stall from a wedged reader (the main-isolate dispatch of D-72 must never block on a subscriber) — while still letting a request/response agent loop know it fell behind. Blocking the producer risks janking the PTY/UI; killing subscribers pushes resync onto every client. The ring doubles as the cursor store, so back-pressure and pull-retention are one mechanism, not two. Bus-owned persistence (if it ever lands) keeps a single authoritative ordering + cursor space; a side-subscriber writer would have to reconstruct ordering and could itself fall behind under the very back-pressure policy this decides.
  • Cost / risk: ring size is a tuning knob — too small and slow agents see frequent gaps, too large and memory grows under a firehose; sized per channel and revisited if a real consumer hits gaps (the Q-2 triage trigger). A caller that ignores the gap marker silently misses events — mitigated by making next-cursor + gap explicit in the --since response shape. Naming bus-owned persistence as the eventual shape pre-commits against the subscriber-writer pattern; acceptable because nothing persists yet and the call is revisitable when the first real audit/undo requirement lands.
  • Cross-reference: D-6 (the event surface), D-72 (serial main-isolate dispatch the producer must not block), Q-2 + Q-3 (resolved here). Unblocks T-223 (cursor pull events); amends ADR 0006 (persistence lean).
  • Raised by: 2026-06-06 — walking Q-2/Q-3 during the T-208 "give Claude hands" wind-down; user chose the drop-oldest ring buffer, in-memory retention now, and bus-owned persistence if it ever becomes durable.

D-86: MCP tool surface — full clide namespace generated from the co-registered command registry

  • Date: 2026-06-06
  • Status: accepted
  • Decision: Resolves Q-32. clide exposes the full mcp__clide__* tool namespace — not just the /ide minimum (getDiagnostics + executeCode) — but every surface (CLI argv, MCP tools/list, command palette) is generated from the one co-registered command registry established by D-74, never hand-authored per-surface. A command registers once (handler + typed arg schema); the MCP adapter derives its tool definition (name, description, JSON-Schema input) from that same registry entry, exactly as the CLI argv grammar and palette entry already do. There is no separately-maintained MCP tool list. A registry entry may carry an MCP opt-out flag so a command that is a poor tool (long-lived streams, UI-side-effecting verbs) can register without exposing a twin. This extends D-68 (which deferred the mcp__clide__* namespace to Q-32) and consumes the registry whose MCP generation D-74 had marked deferred to the T-130 track.
  • Rationale: the maintenance objection to a broad MCP surface (D-68's "every CLI verb invites an MCP twin; surface bloat") only bites if the twin is authored by hand. Generated from the registry, breadth is nearly free: the same source of truth that already feeds CLI + palette feeds MCP, so adding a command lights up all three surfaces at once and they cannot drift. The extensions-first model (D-46) requires registry-driven surfacing anyway — extension-contributed commands must appear in every surface without the core editing a central list. Full breadth makes clide a real backend for non-Claude-Code MCP clients (Cursor, Windsurf, Copilot), the growing /ide ecosystem D-68 already says clide should be a peer in. Claude-in-a-clide-pane still uses the CLI; the MCP breadth is for external clients.
  • Cost / risk: the adapter must map D-74's arg-schema vocabulary to MCP's JSON-Schema tool-input shape — one adapter, written once, not per command. Exposing the full action surface to any connected MCP client widens what a client can drive — gated by the same transport boundary as today (MCP reaches the dispatcher over the SSE transport of D-73; no new auth surface, same dispatcher as the socket). More tools in tools/list is more for a client to reason about; the opt-out flag + good per-command descriptions keep it sane.
  • Cross-reference: D-68 (dual surface; deferred mcp__clide__* to Q-32 — resolved here), D-74 (the co-registered command+schema registry this generates from), D-73 (SSE transport it serves over — unchanged; Q-33 stays closed by D-73), D-46 (extensions-first, which already requires registry-driven surfacing), D-6 (CLI/parity the registry enforces). Defines the tool surface for T-225. Resolves Q-32.
  • Raised by: 2026-06-06 — walking Q-32 during the T-208 wind-down; user chose the full namespace but constrained it to single-registry generation to avoid a second maintenance front, noting the extensions-first pattern needs that anyway.

D-87: Output/log dock — bottom, toggled, read-only (logs + problems)

  • Date: 2026-06-06
  • Status: accepted
  • Decision: clide gains a bottom output dock — a toggled, read-only panel hosting two tabs: Output (the kernel log stream) and Problems (diagnostics). Resolves Q-28: the bottom strip hosts logs/problems, not the terminal.
    • Toggle = one merged status-bar widget. Clicking it (or ⌘J / Ctrl+J) opens/closes the dock. The widget replaces the separate app-status indicator rather than adding a segment (chrome budget, D-48): it shows green when clean, / diagnostic counts when not, and a / chevron for dock state. The badge means problem counts are visible without opening the dock.
    • Output tab: the Logger stream (subsystems: ipc, mcp, extensions, pql, git, pane, …), filterable by source + level + text, auto-scrolling (follow tail; scrolling up pauses follow and shows "jump to latest").
    • Problems tab: diagnostics, moved out of the left sidebar — no duplication, and the dock's full width suits severity · file:line · message rows the 180400px sidebar truncated. The status badge replaces the sidebar panel's always-visible glance.
    • Retention: add a bounded in-memory ring sink to the Logger (drop-oldest, fixed cap). The logger today only live-broadcasts with no history, so late openers would see nothing; the ring serves "the last N records" on open. Same drop-oldest shape as the D-85 event ring.
    • Layout: the dock occupies the bottom of the workspace column and pushes Claude up; the prompt bar rides up with it. Height is resizable (top edge) and open/closed + height persist per workspace. This is an explicit exception to D-47 rule (1) — see its amendment.
  • Rationale: Q-28 asked whether the bottom strip should host logs/errors/tests. The answer splits by interaction: the dock is passive (you skim it) so it earns the always-present-but-collapsed bottom slot; the terminal is active (you work in it) so it stays a first-class surface in the editor pane (D-49), never demoted into a log strip. Merging health into the toggle widget delivers a new capability with zero new persistent chrome and keeps the at-a-glance count alive while the dock is closed. Problems moves in because the width fits and the badge covers the glance it used to provide from the sidebar.
  • Cost / risk: Breaks D-47's literal prompt-bar Y-invariance — mitigated by the explicit amendment, the ≥50% cap (Claude stays the largest surface), and the dock only moving on a deliberate user toggle. Adds bounded log-buffer memory. Relocating Problems costs muscle memory + the slot move. Small-screen behaviour interacts with Q-26: under a short window the dock + ≥50% cap may force a smaller dock or a modal — deferred to Q-26.
  • Cross-reference: D-47 (amended — prompt-Y exception), D-48 (merged widget adds no segment), D-49 (where the terminal lives instead), D-51 (collapse-with-badge pattern the toggle echoes), D-85 (drop-oldest ring reused), Q-26. Resolves Q-28. Implemented by T-54; the terminal's editor-pane home is tracked separately.
  • Raised by: 2026-06-06 — T-54 UX design session (Frame0 wireframe under docs/design/wireframes/output-dock/). User chose a status-bar-toggled bottom dock scoped to read-only output, Problems folded in, terminal kept first-class in the editor pane.