From cede971646e5a92755dd7689b60efc2d7f51a766 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Tue, 21 Apr 2026 17:13:37 +0200 Subject: [PATCH] migrate ADRs into D/R records; delete docs/ADRs/ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lift docs/ADRs/0001-0006 into decisions/: 0001 → D-001 (CLI-first, not MCP) 0002 → R-002 (Go sidecar; superseded by D-005) 0003 → D-003 (pql as supporter tool; wrap, don't duplicate) 0004 → D-004 (ignore file strategy) 0005 → D-005 (Dart core; sidecar dissolved; ptyc as pql-peer) 0006 → D-006 (CLI and event surface contract) ADR 0006's trailing open-questions footer migrates to questions-architecture.md as Q-001 / Q-002 / Q-003 (already landed in the scaffold commit). Titles preserved verbatim; cross-references updated to the new anchor-link style. docs/ADRs/ deleted — originals remain in git history. Co-Authored-By: Claude --- CHANGELOG.md | 10 ++ decisions/architecture.md | 44 +++++- decisions/rejected.md | 5 + docs/ADRs/0001-cli-first-not-mcp.md | 34 ----- docs/ADRs/0002-sidecar-language-go.md | 45 ------ docs/ADRs/0003-pql-as-supporter-tool.md | 61 -------- docs/ADRs/0004-ignore-file-strategy.md | 73 --------- docs/ADRs/0005-dart-core-ptyc-peer.md | 163 -------------------- docs/ADRs/0006-cli-and-event-surface.md | 190 ------------------------ 9 files changed, 56 insertions(+), 569 deletions(-) delete mode 100644 docs/ADRs/0001-cli-first-not-mcp.md delete mode 100644 docs/ADRs/0002-sidecar-language-go.md delete mode 100644 docs/ADRs/0003-pql-as-supporter-tool.md delete mode 100644 docs/ADRs/0004-ignore-file-strategy.md delete mode 100644 docs/ADRs/0005-dart-core-ptyc-peer.md delete mode 100644 docs/ADRs/0006-cli-and-event-surface.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e932a038..0d3b301d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,13 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit. ### Added +- Migrated the `docs/ADRs/` content into `decisions/` as D/R records: + ADR 0001 → `D-001`, ADR 0002 → `R-002` (superseded by `D-005`), ADR + 0003 → `D-003`, ADR 0004 → `D-004`, ADR 0005 → `D-005`, ADR 0006 → + `D-006`. Titles preserved; ADR 0006's trailing open questions moved + to `questions-architecture.md` as `Q-001` / `Q-002` / `Q-003`. The + originals are preserved in git history. + - `decisions/` at the repo root — Q&D record system ported from settled-reach and adapted for clide's domains. Confirmed decisions (`D-NNN`) live under domain files (`architecture.md`, `extensions.md`, @@ -32,6 +39,9 @@ heading, and (b) bumping `project.yaml` `version:` in the same commit. ### Removed +- `docs/ADRs/` directory — content lifted into `decisions/` as D/R + records (see Added above). Originals preserved in git history. + - Go sidecar skeleton under `sidecar/` — `cmd/clide/main.go`, `go.mod`, and the `internal/*` packages (`cli`, `daemon`, `diag`, `git`, `ipc`, `pql`, `proc`, `pty`, `version`). Deleted wholesale per [ADR 0005](docs/ADRs/0005-dart-core-ptyc-peer.md): the "sidecar language: Go" premise no longer holds once the core is Dart. All functionality listed for those packages will be reimplemented under `lib/` as part of Tier 0. - Go-specific Makefile targets (`lint`, `vuln`, `test-race`, `fmt`, `tidy`, `snapshot`, `tools`, `install` via Go), the `govulncheck`/`goimports`/`golangci-lint` version pins, and the pre-push hook's `GOBIN` PATH injection. Replaced with Dart/Flutter equivalents (`analyze`, `format`, `test`, `test-integration`, `build` via `dart compile exe`). - `module:` and `go_version:` from `project.yaml` — single-language core means no Go module path to track. diff --git a/decisions/architecture.md b/decisions/architecture.md index bc271921..e20cf87d 100644 --- a/decisions/architecture.md +++ b/decisions/architecture.md @@ -62,6 +62,44 @@ Core, rendering, IPC, kernel, panel manager. --- -*Architectural backlog from the claudian lineage lives below; ADR -migrations (D-001, D-003, D-004, D-005, D-006) follow when commit #2 -runs.* +### D-001: CLI-first, not MCP +- **Date:** 2026-04-20 (was ADR 0001; ported from the claudian lineage) +- **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-006](#d-006-cli-and-event-surface-contract)). 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-003: 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-004](#d-004-ignore-file-strategy)). Other pql config keys are left alone. Clide does **not** touch pql's index/cache data under `/.pql/` — that stays pql's private store. +- **Context:** [`pql`](https://github.com/postmeridiem/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-039](process.md#d-039-planning-tooling-lives-in-pql). + +### D-004: 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-003](#d-003-pql-as-supporter-tool), 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-005: Dart core; sidecar dissolved; `ptyc` as pql-peer +- **Date:** 2026-04-20 (was ADR 0005; supersedes [R-002](rejected.md#r-002-go-sidecar)) +- **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 ` (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-002](rejected.md#r-002-go-sidecar) 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-006: 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 [:]]` and `clide status`. Command shape: `clide [...] [--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`, `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-001](#d-001-cli-first-not-mcp) 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 `project.yaml` `schema_version:` bump — see [Q-005](questions-architecture.md#q-005-ipc-wire-format-stability)). 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-002](questions-architecture.md#q-002-back-pressure-on-event-streams)), authorisation granularity ([Q-001](questions-architecture.md#q-001-authorisation-granularity)), and event persistence ([Q-003](questions-architecture.md#q-003-event-persistence-audit-undo)) are all deferred until Tier 1 is in real use. +- **Raised by:** 2026-04-20 planning. + +--- diff --git a/decisions/rejected.md b/decisions/rejected.md index 1c8027da..115d5a63 100644 --- a/decisions/rejected.md +++ b/decisions/rejected.md @@ -5,6 +5,11 @@ future reference. --- +### R-002: Go sidecar +- **Rejected:** 2026-04-20 (was ADR 0002; superseded by [D-005](architecture.md#d-005-dart-core-ptyc-peer)) +- **Reason:** The ADR picked Go on two premises — (a) the heavy work belongs in a language separate from the UI layer, and (b) pql is Go so muscle memory transfers. Both broke on reassessment. The sidecar stripped of PTY is I/O-bound glue that `dart:io` covers cleanly (unix sockets, JSON-lines framing, process tables, shell-outs). The real axis was *separate process vs shared language*, not Go vs Rust, and separate-process is what matters (session persistence needs the daemon to outlive the app), not language. PTY is the one place Dart is genuinely weak — Dart's multi-threaded VM can't safely `fork()` — and that single constraint forces a native helper regardless, independent of whether the rest of the core is Dart. Once a small native helper is accepted, the question "does *everything else* need to be in that same native language" answers itself: no. Go sidecar directory dissolved; `ptyc` (C, PTY-only, pql-peer) is the surviving native supporter tool. +- **Cross-reference:** [D-005](architecture.md#d-005-dart-core-ptyc-peer) + ### R-003: `MaterialApp` root - **Rejected:** 2026-04-21 - **Reason:** Dragged in Material theming, default icons, and platform chrome that fought the custom three-tier theme pipeline ([D-009](architecture.md#d-009-three-tier-theme-pipeline)). Every bundled theme had to override Material defaults to look like clide; the overrides were visible in widget tests as "why is this `ElevatedButton` colored this way." diff --git a/docs/ADRs/0001-cli-first-not-mcp.md b/docs/ADRs/0001-cli-first-not-mcp.md deleted file mode 100644 index 15b39300..00000000 --- a/docs/ADRs/0001-cli-first-not-mcp.md +++ /dev/null @@ -1,34 +0,0 @@ -# ADR 0001 — CLI-first, not MCP - -**Status:** accepted -**Date:** 2026-04-20 (ported from the claudian lineage) - -## Context - -Clide exposes capabilities to Claude Code (panes, terminals, git, -pql queries, canvas, graph). The two mainstream options for that -interface are: - -1. A Model Context Protocol (MCP) server the agent connects to. -2. A plain Bash CLI the agent calls from its shell, matching the - contract `pql` already follows. - -## 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. - -## Consequences - -- 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. -- Claude Code's `Bash(clide *)` allow rule is the only configuration - Clide needs on the agent side. -- 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. diff --git a/docs/ADRs/0002-sidecar-language-go.md b/docs/ADRs/0002-sidecar-language-go.md deleted file mode 100644 index d696792a..00000000 --- a/docs/ADRs/0002-sidecar-language-go.md +++ /dev/null @@ -1,45 +0,0 @@ -# ADR 0002 — Sidecar language: Go - -**Status:** superseded by [ADR 0005](0005-dart-core-ptyc-peer.md) -**Date:** 2026-04-20 (ported from the claudian lineage) -**Superseded:** 2026-04-20 — the "separate language" premise dissolved once we chose Dart for the core; see 0005 for the rationale. - -## Context - -The Clide sidecar owns PTYs, subprocesses, file watchers, git -shelling-out, and the IPC server. It ships as a single static binary -that also serves as the `clide` CLI in one-shot mode. The Flutter -desktop app talks to it over IPC; Claude talks to it via the CLI. -Language candidates were Go and Rust. - -Related hard constraint: **no heavy lifting in the UI layer.** The -Flutter app stays focused on rendering and interaction. Everything -heavy (PTYs, subprocesses, file watching, git, pql invocations) -lives in the sidecar. Reason: keep the UI layer thin and the -security-sensitive surface auditable in one language. - -## Decision - -The sidecar/CLI is written in Go. - -Rationale: - -- **Matches pql.** pql is Go; Clide wraps pql and reaches into its - idioms constantly. Shared toolchain and shared patterns cut - cognitive overhead. -- **Static binary.** Single artifact, trivial cross-compile, no - runtime dependencies on the user's machine. -- **PTY story is fine.** `creack/pty` covers what we need; Rust's - crates are marginally nicer but not decisive. -- **Muscle memory.** Build pipeline, `project.yaml` conventions, - goreleaser setup, exit-code contract, diagnostic format — all - already established in pql and portable one-to-one. - -## Consequences - -- Module path: `git.schweitz.net/jpmschweitzer/clide/sidecar`. -- Layout mirrors pql: `cmd/clide/main.go`, `internal/cli`, - `internal/version` (ldflag-stamped `Version`, `Commit`, `Date`), - `internal/diag` (exit codes + stderr-JSON diagnostics). -- Same Makefile shape: version read from `project.yaml` via awk, - stamped via `-ldflags -X`. diff --git a/docs/ADRs/0003-pql-as-supporter-tool.md b/docs/ADRs/0003-pql-as-supporter-tool.md deleted file mode 100644 index 801841bc..00000000 --- a/docs/ADRs/0003-pql-as-supporter-tool.md +++ /dev/null @@ -1,61 +0,0 @@ -# ADR 0003 — pql as supporter tool; Clide wraps, never duplicates - -**Status:** accepted -**Date:** 2026-04-20 (ported from the claudian lineage) - -## Context - -[`pql`](https://github.com/postmeridiem/pql) is a pre-existing Go -CLI that indexes a markdown-bearing directory tree into SQLite and -exposes its semantics (frontmatter, wikilinks, tags, headings, -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. - -## 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 -`sidecar/internal/pql/` — pure shell-outs to the `pql` binary, no -logic beyond invocation and result rendering. - -### 2. pql is a Clide subsystem when Clide is present in the repo. - -Broader than "wrap, don't duplicate." When Clide is loaded in a -repo, it owns pql's lifecycle and the config keys it cares about. -On load, Clide writes its current state into `.pql/config.yaml` — -no conditional sync, no "did anything change" logic. - -Clide only stomps the keys it manages (starting with `ignore_files:` -— see ADR 0004). Other pql config keys are left alone so pql's -config surface can grow independently. - -Clide does **not** touch pql's index/cache data under `/.pql/` -— that stays pql's private store. Only the config file is Clide's -to edit. - -In repos without Clide, pql works standalone, unaffected. The rule: -direct-pql users get vanilla pql; Clide users get pql managed by -Clide. - -## Consequences - -- One source of truth for markdown semantics (pql). -- Clide's `sidecar/internal/pql/` package is deliberately thin. -- Any new query capability the UI wants goes through a pql upstream - PR, not a local workaround. -- User never has to learn pql's config file to get consistent - behavior — Clide manages it. -- The arrow Clide → pql is never inverted: pql stays ignorant of - its wrapper, never hardcodes Clide filenames. -- pql is also the **only** query engine. Obsidian-style inline - "bases" (YAML query tables embedded in markdown) are explicitly - not supported; queries live at the repo level where they belong. diff --git a/docs/ADRs/0004-ignore-file-strategy.md b/docs/ADRs/0004-ignore-file-strategy.md deleted file mode 100644 index a1a1525d..00000000 --- a/docs/ADRs/0004-ignore-file-strategy.md +++ /dev/null @@ -1,73 +0,0 @@ -# ADR 0004 — Ignore file strategy - -**Status:** accepted -**Date:** 2026-04-20 (ported from the claudian lineage) - -## Context - -Clide's working assumption is that the git repo *is* the workspace -— no separate "vault" concept layered on top. Every file-enumerating -surface in Clide (pql query panels, canvas drivers, graph view, -sidecar file watchers, pane lists, file tree) needs to skip the -obvious junk — `vendor/`, `node_modules/`, `dist/`, build artifacts -— or results drown in noise. - -## Decision - -One mechanism everywhere: the `ignore_files:` list in -`.pql/config.yaml`. An ordered list of gitignore-shaped files, later -entries win on per-pattern conflicts. - -### Default - -pql defaults to `ignore_files: [.gitignore]`. Most repos already -keep exclusions there, so zero config in a code repo; in a -notes-only directory `.gitignore` doesn't exist and the default is -a safe no-op. - -### Clide sync - -Per ADR 0003's "pql is a Clide subsystem when present" rule, Clide -writes the list on load: - -- If `.clideignore` exists in the repo: - `ignore_files: [.gitignore, .clideignore]`. Clide-specific deltas - (including `!pattern` negations) layer on top of gitignore. -- Otherwise: `ignore_files: [.gitignore]` (matches the pql default). - -No conditional sync. Clide only stomps `ignore_files:`; other pql -config keys are left alone. - -### `.clideignore` semantics - -- Carries **only** the Clide-specific deviations from `.gitignore`. - Never duplicate gitignore's contents. -- Supports `!pattern` negations to un-ignore specific entries (e.g. - `!.github/` to expose workflow docs in query results). - -### Walker magic: none except `.git/` - -Git self-hides `.git/` — that's the only invisible exclusion in the -stack. Every other tool is explicit: pql adds `.pql/` to -`.gitignore` at install time, and Clide adds any private dirs it -introduces (e.g. `.clide/`) to `.gitignore` on install. Exclusion -flows through the normal `ignore_files:` chain; no hardcoded walker -exceptions for tool-owned dirs. - -### Same list, same rules, everywhere - -Sidecar consumers (watchers, canvas, pane list, file tree, graph -view) read the same key from `.pql/config.yaml` and apply identical -precedence, so Claude and the user always see the same filtered -surface. - -## Consequences - -- 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. -- 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). -- Upstream pql work: the `ignore_files:` list is already the shape - pql has landed on (plural, ordered, defaults to `[.gitignore]`). diff --git a/docs/ADRs/0005-dart-core-ptyc-peer.md b/docs/ADRs/0005-dart-core-ptyc-peer.md deleted file mode 100644 index 709d74e8..00000000 --- a/docs/ADRs/0005-dart-core-ptyc-peer.md +++ /dev/null @@ -1,163 +0,0 @@ -# ADR 0005 — Dart core; sidecar directory dissolved; `ptyc` as pql-peer - -**Status:** accepted -**Date:** 2026-04-20 -**Supersedes:** [ADR 0002](0002-sidecar-language-go.md) - -## Context - -ADR 0002 picked Go for the sidecar/CLI on the reasoning that (a) the -heavy work (PTYs, subprocesses, file watchers, git, IPC) belongs in -a language separate from the UI layer, and (b) pql is Go so the -muscle memory transfers. The Flutter app would talk to a Go binary -over a unix socket. - -On reassessment, two facts broke that reasoning: - -1. **What the "heavy work" actually is.** Stripped of the PTY layer, - the sidecar is I/O-bound glue around shell-outs (`git`, `claude`, - `pql`), a unix-socket server, JSON-lines framing, and a process - table. `dart:io` covers all of this cleanly. The Go-versus-Rust - debate implicit in 0002 was the wrong axis — the real choice was - **separate process vs shared language**, and separate-process is - what matters (session persistence needs the daemon to outlive the - app), not language. - -2. **PTY is the one place Dart is genuinely weak** — and not because - of ecosystem, but because Dart's multi-threaded VM can't safely - `fork()`. That single constraint forces a native helper regardless - of what language wraps it. Once you accept a small native helper, - the question is whether *everything else* needs to be in that same - native language. It doesn't. - -So the "sidecar" directory stopped carrying weight. It existed to -justify the Go/Dart split. With the split gone, the directory is -ceremony. - -## 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 ` — one-shot, pql-style. Parses args, connects - to the running daemon socket, sends a request, prints JSON on - stdout, exits with the pql exit-code contract. -- `clide --daemon` — long-running. Owns PTYs, subscriptions, file - watchers, subprocess lifecycles. Started by the app on load; - survives app restarts so Claude sessions persist. - -The Flutter app imports the Dart core as a library for in-process -state (views, widgets, models) *and* connects to the daemon over the -same IPC the CLI uses. One protocol, two clients. - -### 2. The sidecar directory dissolves. - -``` -app/ # Flutter UI (Linux / macOS primary) -lib/ # Dart core shared by app + CLI + daemon: - # ipc/, pty/, proc/, git/, pql/, events/, panes/ -bin/clide.dart # Dart AOT entry: subcommand dispatch + --daemon -tool/ # Dart scripts used by the Makefile -tests/ # integration tests that span app + daemon -``` - -No `sidecar/`. No Go module. `project.yaml` drops `module:`; the -Dart package name replaces it. - -### 3. `ptyc` is a pql-peer supporter tool. - -The PTY helper — a small C binary that does `posix_openpt` + `fork` -+ `exec` + fd-passing via `SCM_RIGHTS` — graduates to the same -status as pql: single-purpose, language-appropriate, standalone, -reusable outside Clide. It lives in its own directory (eventually -its own repo) and Clide wraps it the same way it wraps pql. Working -name: **`ptyc`**. - -- Clide shells out to `ptyc` to spawn every PTY (terminal pane, - tmux session, claude, LSP server, debug adapter — all one code - path). -- `ptyc` writes only what it needs to write (a PTY + forked child) - and does nothing else. No IPC protocol of its own, no long-running - state. One-shot per pane. -- Consumers other than Clide (a Python script, another Dart app, a - Go tool) can use `ptyc` standalone with no Clide dependency. - -This mirrors ADR 0003's pql contract: **wrap, don't duplicate**; -supporter tools stay independent and reusable. - -## Consequences - -- **ADR 0002 is superseded.** Go sidecar removed. Existing - `sidecar/` contents (Go skeleton — `cmd/clide/`, `internal/*`, - `go.mod`) are deleted; the ideas it encoded (exit-code contract, - ldflag-stamped version, JSON diagnostics) are reimplemented in - Dart. The supersession note stays in 0002 so the history reads - correctly. -- **One toolchain for the IDE proper.** Flutter + Dart. The C - toolchain is needed only to build `ptyc` — a tiny, rarely-changing - artifact. -- **`project.yaml` simplifies.** `module:` and `go_version:` go - away. A `ptyc_version:` pin joins the existing `dart_sdk:` and - `flutter_channel:` keys. -- **Supply-chain gates stay, shape changes.** The Go gate - (`govulncheck`) is removed. The Dart gate stays (advisories review - + exact-pin `pubspec.yaml`). `ptyc` gets its own tiny gate: it has - no deps beyond libc, so the review is "read the 150 lines before - every bump." `make security` becomes `make security` = Dart - advisories + `ptyc` review checklist. -- **IPC stays.** The daemon / app / CLI split is unchanged — unix - socket, token auth, JSON-lines. It was never about language. -- **Session persistence stays.** PTY master fds live in the Dart - daemon process, not the app process. App restart does not kill - Claude. -- **Pql continues as-is.** Wrapped via shell-out from - `lib/src/pql/` (the Dart equivalent of the deleted - `sidecar/internal/pql/`). No protocol change to pql. -- **CLAUDE.md and the Makefile need updates.** Commands, directory - references, and the "sidecar language: Go" guardrail all shift. -- **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 set here is: new - native need → new supporter tool, peer of pql and `ptyc`. Never a - second "core language." - -## Notes - -- **Name: `ptyc`** (pronounced "p-tic"). Three honest readings, all - pointing at the same tool: - 1. **Project Terminal Controller** — parallel to pql's **Project - Query Language**. Clide's supporter tools follow a `p*` pattern - where `p` = *project*: pql handles project queries, ptyc - handles project terminals. Future supporter tools that fit the - "small single-purpose peer of pql" slot should follow the same - pattern when the fit is natural. - 2. **PTY + child** — domain vocabulary (PTY parent/child pair). - This is what a reader seeing the name on a command line will - decode it as, and it's exactly what the tool does: run a child - process under a PTY. - 3. **PTY + C** — the implementation language. Accurate and - non-limiting; Unix has a long tradition of tools advertising - their implementation (`gcc`, `libc`, `musl`). If we ever - rewrote it in another language it would become a new tool with - a new name, same as pql would if rewritten. - - Crucially, none of the readings tie the **caller** to any - ecosystem — `ptyc` is usable from Dart, Python, Go, shell, - anywhere a subprocess can be spawned and a fd can be received. - Alternatives considered and rejected: `clide-pty-spawn` (too - clide-specific for a peer tool), `dpty` (already taken on - crates.io), `ptyspawn` (verbose), `dartmx` (falsely signals - caller-ecosystem + implies multiplex), `ptyx` (arbitrary suffix, - no domain reading), `ptyc` as read-only "PTY C" (works but sells - the name short). -- The helper's wire contract (stdin JSON → stdout JSON + SCM_RIGHTS - fd transfer) is intentionally small so wrapping it is trivial from - any language. -- This ADR does not define the Clide CLI / event surface itself — - that is [ADR 0006](0006-cli-and-event-surface.md). diff --git a/docs/ADRs/0006-cli-and-event-surface.md b/docs/ADRs/0006-cli-and-event-surface.md deleted file mode 100644 index 2adb97ac..00000000 --- a/docs/ADRs/0006-cli-and-event-surface.md +++ /dev/null @@ -1,190 +0,0 @@ -# ADR 0006 — CLI and event surface contract - -**Status:** accepted -**Date:** 2026-04-20 - -## Context - -[ADR 0001](0001-cli-first-not-mcp.md) established that Claude drives -Clide via a Bash CLI, not MCP. That decided the *channel*. It did -not define the *surface* — which subsystems exist, how commands -relate to events, what the JSON looks like on the wire, how -subscribers discover state changes. - -CLAUDE.md states the rule colloquially: "every CLI subcommand has a -UI affordance in the app, and every UI action has a CLI. If you add -one side without the other, the feature is incomplete." This ADR -restates that as an implementable contract. - -The shape needs to satisfy three things at once: - -1. **User/Claude parity.** Anything the user can do with a mouse, - Claude can do with `clide <...>`. Anything the user can observe - in the UI, Claude can observe via events. -2. **Daemon as authoritative state.** The app and the CLI are both - clients. State lives in the `clide --daemon` process; commands - mutate it; events broadcast changes to all subscribers. -3. **pql-style ergonomics.** One tool-use pattern for Claude across - pql and clide — same exit codes, same JSON-on-stdout habit, same - stderr-for-diagnostics rule. - -## 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). - -### Subsystem list (initial, by tier) - -| Subsystem | Tier | Nouns | Representative verbs | Representative events | -|---|---|---|---|---| -| `pane` | 1 | terminal pane | spawn, list, focus, close, write, resize, tail | `pane.spawned`, `pane.output`, `pane.exit`, `pane.resized` | -| `tab` | 2 | workspace tab | new, switch, close, list | `tab.opened`, `tab.switched`, `tab.closed` | -| `open` | 2 | editor shortcut | *(verb-only: `clide open `)* | — (emits `editor.opened`) | -| `editor` | 2 | active editor buffer | goto, highlight, insert, replace-selection, save | `editor.opened`, `editor.selection_changed`, `editor.saved` | -| `panel` | 2 | sidebar/context panels | show, hide, toggle, list | `panel.visibility_changed` | -| `tree` | 2 | file tree | reveal, refresh | `tree.node_expanded`, `file.changed` | -| `git` | 3 | working tree | status, stage, unstage, stage-hunk, commit, stash, pull, push | `git.status_changed`, `git.branch_changed` | -| `pql` | 4 | queries | run, tags, backlinks | `pql.result` | -| `canvas` | 5 | canvas surface | open, node add/move/connect, save | `canvas.node_added`, `canvas.node_moved`, `canvas.connection_added` | -| `graph` | 5 | graph view | open, focus, filter | `graph.focused`, `graph.filter_changed` | -| `theme` | 6 | theme/palette | set, list, get | `theme.changed` | -| `settings` | 6 | settings store | get, set, list | `settings.changed` | -| `project` | — | whole-workspace | status, reload, events | `project.ready`, `project.reloaded` | - -Two umbrella entry points sit outside any subsystem: - -- `clide tail --events [--filter [:]]` — subscribe to - the event stream. Bare `tail --events` gets everything; filtered - forms narrow by subsystem or by subsystem+id (e.g. - `--filter pane:p_7`, `--filter git`). -- `clide status` — one-shot daemon snapshot: connected clients, - live panes, open tabs, workspace root, daemon version, uptime. - -### Command shape - -``` -clide [...] [--flag ...] [-- argv...] -``` - -- Positionals are nouns/ids; flags are modifiers. -- `--` separates Clide's args from an inner argv passed through - (e.g. `clide pane spawn --cwd X -- tmux new-session -A -s foo`). -- Verbs are imperative (`spawn`, not `create-pane`). -- Where it reads naturally, single-word shortcuts exist for the - hottest paths (`clide open ` → `clide editor open `). - Shortcuts alias; they do not fork. - -### Exit-code contract (parity with pql) - -| Code | Meaning | -|---|---| -| `0` | Success | -| `1` | User error (bad args, unknown id, precondition failed) | -| `2` | Tool error (daemon unreachable, IPC failure, internal panic) | -| `3` | Not-found (id or path doesn't resolve) | -| `4` | Conflict (state busy, already-running, concurrent-modify) | -| `64`–`78` | Reserved, per `sysexits.h`, for future specific cases | - -Diagnostic JSON on **stderr** (not stdout) on any non-zero exit: -```json -{"code":1,"kind":"user_error","subsystem":"pane","message":"pane id p_99 not found","hint":"clide pane list"} -``` - -Stdout stays machine-parseable on success. This matches pql. - -### Event schema - -Events are JSON objects, one per line, on the `--events` stream. -Every event: - -```json -{ - "v": 1, - "ts": "2026-04-20T21:00:00.123Z", - "type": "pane.output", - "subsystem": "pane", - "id": "p_7", - "payload": { "bytes_b64": "…", "seq": 412 } -} -``` - -- `v`: schema version. Bumped only for breaking changes. Old - subscribers pin `v`. -- `ts`: ISO-8601 UTC with millisecond precision. -- `type`: `.`. Past-tense for - things that happened; `_changed` suffix for state transitions. -- `subsystem` + `id`: redundant with `type`, but makes - filtering cheap and future-flexible. -- `payload`: subsystem-defined; documented per subsystem. - -Binary payloads (PTY output, file contents) are base64. The ergonomic -cost is worth the "entire stream is line-delimited JSON" invariant. - -### Command ↔ event duality - -Every state-changing command emits at least one event. Subscribers -see the same mutation whether they triggered it or not, and the -issuing client gets the event back (so `clide pane spawn` followed -by a `tail --events` subscription sees `pane.spawned` regardless of -subscribe order, via a short replay buffer per subsystem). - -Read-only commands (`list`, `get`, `status`) emit nothing. - -### User/Claude parity as a check - -Every merge to `main` that adds a UI affordance must either: -- add the matching CLI verb, or -- include a linked follow-up task naming the verb to add next. - -Every merge that adds a CLI verb must either: -- surface it in the UI, or -- document why the verb is Claude-only (rare; mostly diagnostics - like `clide status`). - -Events have the symmetric rule: any UI surface that reacts to state -must react to the corresponding event; any new event must be -consumable both by the UI and by `clide tail --events`. - -## Consequences - -- **Surface is enumerable.** Adding a subsystem means adding a row - to the table above and specifying its verbs + events in a short - doc under `docs/cli/`. The daemon registers it; the CLI dispatcher - picks it up; `clide --help` and `clide --help` stay - accurate by construction. -- **Wire schema is versioned.** `v: 1` is the starting point. - Compatibility breaks bump the major and land alongside a - `project.yaml` `schema_version:` bump. -- **Replay buffer per subsystem.** Cheap (most subsystems emit - seldom); needed so a subscriber that connects after a command - still sees that command's effect. Buffer depth per subsystem is a - tuning parameter; defaults to 16 events. -- **Events are the only UI→app state channel.** The Flutter app - does not poll; it subscribes. Panels render from the last event - for their subsystem + current snapshot from `project status`. -- **pql events fit naturally.** Long-running `pql` queries stream - rows as `pql.result` events keyed by a query id, letting the - Query panel render incrementally. -- **Testability.** Every subsystem can be integration-tested by: - start a daemon → open a `tail --events` subscriber → issue - commands over the CLI → assert events. No UI needed for - protocol-level coverage. -- **Extension API (Tier 6) inherits this.** A Dart extension - publishes a subsystem; the same registration pipeline exposes - it to Claude via the CLI. Extensions don't get a second-class - channel. - -## Open questions - -- **Authorisation granularity.** The daemon's token auth is coarse - (allow all / deny all). Later, per-subsystem grants may matter - (e.g. restrict `git push`). Out of scope here. -- **Back-pressure on event streams.** A subscriber that falls behind - on `pane.output` (a firehose) needs a policy: drop oldest, block - producer, coalesce, or kill subscriber. Defer until Tier 1 is in - real use. -- **Event persistence.** Events are in-memory only in v1. If a - future need (audit log, undo) wants persistence, it becomes a - subsystem that subscribes and writes — not a property of the bus.