diff --git a/.pql/changelog/ticket_history/2026-05.sql b/.pql/changelog/ticket_history/2026-05.sql index fa03be0b..aa96c59e 100644 --- a/.pql/changelog/ticket_history/2026-05.sql +++ b/.pql/changelog/ticket_history/2026-05.sql @@ -1709,3 +1709,7 @@ Why not always? Lock state at fork() time is timing-dependent. ~95% of the time INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-96', 'status', 'backlog', 'done', NULL, '2026-05-17 18:11:18', '2026-05-17 18:11:18', '2026-05-17 18:11:18', NULL, '3215fc763c4f1dce75ea9e0bb2b9bbb6', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-98', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:51:25', '2026-05-17 18:51:25', '2026-05-17 18:51:25', NULL, '0369c8264ec2c67b3bf3f28057404ec0', 1) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-98', 'status', 'in_progress', 'done', NULL, '2026-05-17 18:55:57', '2026-05-17 18:55:57', '2026-05-17 18:55:57', NULL, 'cb0ae539d5665bd9720b327d60a2cbed', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-101', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '55d14b4493a159695d4b40bbd19fff23', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-102', 'status', 'backlog', 'in_progress', NULL, '2026-05-17 18:58:56', '2026-05-17 18:58:56', '2026-05-17 18:58:56', NULL, '7fa075d7232df5302ffdfb5d1c3d79f2', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-101', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '56e8da656c5fc53af241900e6313805e', 1) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-102', 'status', 'in_progress', 'done', NULL, '2026-05-17 19:01:39', '2026-05-17 19:01:39', '2026-05-17 19:01:39', NULL, '7f09d2a1501d3d072e071822c191f253', 1) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-05.sql b/.pql/changelog/tickets/2026-05.sql index 5c79fcee..54898d89 100644 --- a/.pql/changelog/tickets/2026-05.sql +++ b/.pql/changelog/tickets/2026-05.sql @@ -1607,3 +1607,19 @@ INSERT INTO tickets (id, type, parent_id, title, description, status, priority, 3. Existing dugite-in-install-dir behavior preserved. Source: consultants.md "Security — Findings — [Critical]".', 'done', 'critical', NULL, NULL, NULL, '2026-05-17 18:47:13', '2026-05-17 18:55:57', NULL, '40e1ed24c3cce28e2420150f1fac8fbd', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-102', 'bug', 'T-97', 'add symlink re-resolution + containment re-check to files.read / files.ls', '`path_safety.dart:35-51` explicitly does not resolve symlinks; the filesystem layer (`files_commands.dart:81-85`) doesn''t either. A repo symlink `config -> /etc/shadow` passes the containment check (the link path is under root) and clide reads the target. + +**Fix:** after `resolveUnderRoot`, call `resolveSymbolicLinksSync()` and re-verify containment against the workspace root. + +**Acceptance:** test that a symlink pointing outside the workspace is rejected by both `files.read` and `files.ls`. + +Source: consultants.md "Security — Findings — [Major] Path-safety does not defend against symlink escape".', 'done', 'high', NULL, NULL, NULL, '2026-05-17 18:47:33', '2026-05-17 19:01:39', NULL, '937111bd0e00620e7faf68859018619a', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); +INSERT INTO tickets (id, type, parent_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('T-101', 'task', 'T-97', 'rewrite README + docs/initial-plan to match current architecture', '`README.md` and `docs/initial-plan.md` describe an architecture that no longer exists. README documents `ptyc/` as a live component (`README.md:10`) and `make ptyc-build` (`:36`) — `ptyc/` is gone (D-5 amendment 2026-05-07) and the Makefile has no such target. `README.md:44` links to a `decisions/` directory that has moved to `governance/`. `docs/initial-plan.md` still describes a Go sidecar, separate `clide --daemon` long-running process, `app/` subdirectory, and `project.yaml` — all contradicted by D-5, D-56, and the single-package-at-root reality. A new contributor''s first read builds a wrong mental model. + +**Fix:** +1. Rewrite `README.md`''s `ptyc/` sections; remove `make ptyc-build` references. +2. Fix the `decisions/` link → `governance/decisions/`. +3. Either banner `docs/initial-plan.md` as historical (preserved as a snapshot) or extract a current `docs/architecture.md` that matches today''s code. +4. Spot-check other doc references to dissolved components. + +Source: consultants.md "Docs — Findings — [Critical]".', 'done', 'high', NULL, NULL, NULL, '2026-05-17 18:47:29', '2026-05-17 19:01:39', NULL, 'a7d378267119361f2b2188977881a666', 1) ON CONFLICT(id) DO UPDATE SET type=excluded.type, parent_id=excluded.parent_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > tickets.updated_at OR (excluded.updated_at = tickets.updated_at AND excluded.hash > tickets.hash); diff --git a/README.md b/README.md index 6ab439ff..43fec4f8 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,14 @@ An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-po ## Architecture -Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence. +Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence (D-41). -- **`lib/`** — all Dart code. Kernel services (theme, i18n, settings, panels, commands, focus), UI widgets, built-in extensions, and the extension contract. -- **`ptyc/`** — small C helper. Spawns a PTY + child and hands the master fd back over `SCM_RIGHTS`. Every pane (shell, tmux, claude, LSP, debug adapter) goes through it. +- **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`). +- **PTY** — `lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary. +- **`native/`** — vendored native libraries (`libtree-sitter.so` with wasmtime embedded). Linux only today. - **[pql](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it. -Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent. +Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent (D-6). ## Built-in extensions @@ -30,19 +31,24 @@ Then: make run # launch the desktop app make test # fast suite: analyze + format + unit + widget + golden make test-core # core subsystem tests (IPC, PTY, git, pane registry) +make test-a11y # accessibility contract tests make test-integration # real app boot integration tests make build-linux # flutter build linux make build-macos # flutter build macos -make ptyc-build # build the ptyc PTY-spawn helper -make push-check # pre-push gate: decisions + core + fast tests +make push-check # pre-push gate: decisions + core + fast + a11y + coverage + changelog ``` ## Status Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy). -Design doc: [`docs/initial-plan.md`](https://github.com/postmeridiem/clide/blob/main/docs/initial-plan.md). Architectural decisions: [`decisions/`](https://github.com/postmeridiem/clide/tree/main/decisions). +## Documentation + +- [`docs/architecture.md`](docs/architecture.md) — current architecture (read this first). +- [`docs/initial-plan.md`](docs/initial-plan.md) — historical design doc; preserved as a snapshot of the 2026-04 plan, much of it now superseded. +- [`governance/decisions/`](governance/decisions/) — confirmed decisions (`D-NNN`), open questions (`Q-NNN`), rejected alternatives (`R-NNN`). +- [`CLAUDE.md`](CLAUDE.md) — Claude-addressed working notes (guardrails, repo layout). ## License -MIT. See [`LICENSE`](https://github.com/postmeridiem/clide/blob/main/LICENSE). +MIT. See [`LICENSE`](LICENSE). diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 00000000..54af3fb2 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,129 @@ +# clide — Architecture + +Current as of 2026-05-17. Tracks the code on `main`. For the original +design plan (much of it now superseded), see +[`docs/initial-plan.md`](initial-plan.md). For decisions, see +[`governance/decisions/`](../governance/decisions/). + +## Shape + +clide is a **single Flutter package at the repo root**. There is no +sidecar process, no separate daemon, no Go binary. One `flutter run` +boots the whole IDE. + +``` +flutter desktop app (lib/main.dart) + ├── kernel services (lib/kernel/) — theme, i18n, settings, panels, + │ commands, focus, scheduler, … + ├── core subsystems (lib/src/) — ipc, daemon dispatch, panes, + │ │ pty, editor, files, git, pql + │ └── PTY via Dart FFI posix_openpt + posix_spawn (T-96) + ├── widget primitives (lib/widgets/) — ClideButton, ClideText, … no + │ Material/Cupertino + ├── built-in extensions (lib/builtin/) — claude, editor, files, git, terminal, + │ welcome, … each a ClideExtension + └── extension framework (lib/extension/) — contract + dependency-gated activation +``` + +### Process model + +One OS process. The Flutter app hosts: + +- the IPC dispatcher (`DaemonDispatcher` in `lib/src/daemon/`), +- every subsystem handler (pane/files/editor/git/pql), +- the extension manager and all built-in extensions. + +`tmux` is the only external long-lived process — it owns Claude +session persistence so panes survive app restarts (D-41). The app +re-attaches via `tmux new-session -A` on boot. + +PTYs are spawned natively from Dart. `lib/src/pty/native_pty.dart` +calls `posix_openpt()` + `posix_spawn()` via FFI; the child inherits +the slave PTY as stdin/stdout/stderr. No C helper binary. + +### Native dependencies + +Vendored under [`native/`](../native/) with per-platform subdirectories +(`linux-x64/` today). Currently: + +- `libtree-sitter.so` — wasmtime-embedded tree-sitter for syntax + highlighting. Loaded via `dart:ffi`. See + `lib/kernel/src/syntax/tree_sitter_ffi.dart`. + +Each entry is listed in [`assets/licenses.yaml`](../assets/licenses.yaml) +with version + SHA expectation (D-42). + +### External tools + +clide shells out to two binaries at runtime: + +- **`git`** — vendored as `dugite-native` if present at the install + directory; otherwise `PATH` `git`. Resolution happens at + `lib/kernel/src/toolchain_paths.dart`. **Never resolves against the + open workspace** (T-98). +- **`pql`** — the [pql](https://github.com/postmeridiem/pql) project + query language; supporter tool, wrapped in `lib/src/pql/`. Clide + never re-implements pql features (D-3). + +## Surfaces + +### Claude-facing — `clide` CLI + +Per D-1 and D-6, Claude talks to clide exclusively via Bash. Every +state-changing command emits one or more events on a long-lived +event stream; every UI affordance has a matching CLI verb. See D-6 +for the subsystem/verb/event contract. + +> **Caveat (2026-05):** the Unix-socket server that exposes the +> dispatcher to a thin `clide` C client is currently unimplemented. +> Today's working path is in-process direct dispatch. See **T-99** +> (IPC server implementation) and **D-68** (dual integration surface +> — Bash CLI primary, MCP secondary). + +### User-facing — Flutter desktop + +Three-column layout (sidebar / workspace / context) with collapsible +panels, a custom title bar, and per-panel "hats" for branding + +window controls. The interaction model is documented in D-47 and +neighboring decisions. + +## Subsystems at a glance + +| Subsystem | Location | Owns | +|---|---|---| +| IPC envelope + dispatcher | `lib/src/ipc/`, `lib/src/daemon/` | request/response framing, command registration, broadcast events | +| Pane registry | `lib/src/panes/` | spawn/list/write/resize/close, event emission | +| PTY | `lib/src/pty/` | `posix_openpt` + `posix_spawn`, reader isolate, signal forwarding | +| Editor | `lib/src/editor/` | buffer registry, open/save/setContent | +| Files | `lib/src/files/` | ls / read / watch, path-safety containment check | +| Git | `lib/src/git/` | client (no shell), status/diff/operations parsing | +| Pql wrapper | `lib/src/pql/` | shell-out only; never re-implements pql | +| Kernel services | `lib/kernel/src/` | theme, i18n, settings, panels, commands, focus, syntax | +| Extensions | `lib/extension/`, `lib/builtin/` | dependency-gated activation, contribution points | + +## Build + test + +``` +make hooks && flutter pub get # one-time setup +make run # launch app +make test # analyze + format + unit + widget + golden +make test-core # IPC / PTY / git / pane registry +make test-a11y # accessibility contract +make test-integration # real-app boot tests +make push-check # the full pre-push gate +``` + +The pre-push gate enforces a 95% line-coverage floor (D-66), a +40-word soft / 60-word hard CHANGELOG bullet cap, all unit/widget +tests, and the a11y contract. + +## Governance + +All architectural choices live in +[`governance/decisions/.md`](../governance/decisions/) as +`D-NNN` records, open questions as `Q-NNN`, rejected alternatives as +`R-NNN`. Claim new IDs via `pql decisions claim D "title"`. +The governance index lists everything: [`governance/README.md`](../governance/README.md). + +The pql ticket backlog tracks in-flight work; `pql ticket board +--pretty` for the live view. diff --git a/docs/initial-plan.md b/docs/initial-plan.md index ce0db5ee..002e23a3 100644 --- a/docs/initial-plan.md +++ b/docs/initial-plan.md @@ -1,5 +1,24 @@ # Clide — Initial Plan (Flutter rebuild) +> **⚠ HISTORICAL — preserved as a snapshot of the 2026-04 plan.** +> +> Several load-bearing choices in this document have since been +> superseded by formal decisions: +> +> - **No Go sidecar.** Dart is the sole core language (D-5). +> - **No separate daemon process.** The Flutter app hosts the IPC +> server in-process (D-56). +> - **No `ptyc/` C helper.** PTY spawning uses Dart FFI +> `posix_openpt()` + `posix_spawn()` (D-5 amendments, T-96). +> - **Single package at the repo root**, not `app/` + `lib/` + `bin/`. +> - **No `project.yaml`.** Project metadata lives in `pubspec.yaml`. +> +> For the current architecture, see [`docs/architecture.md`](architecture.md). +> For the decision trail, see [`governance/decisions/`](../governance/decisions/). +> The text below is left intact for anyone tracing the design history. + +--- + **Working name:** clide (unchanged from the Python era). Repo root: `/var/mnt/data/projects/clide`. Flutter desktop app + Go sidecar/CLI. diff --git a/docs/macos-pty-problem.md b/docs/macos-pty-problem.md index 21ce48b8..17a75728 100644 --- a/docs/macos-pty-problem.md +++ b/docs/macos-pty-problem.md @@ -1,5 +1,11 @@ # macOS PTY Problem — Diagnosis Complete +> **⚠ HISTORICAL — `ptyc` is retired.** PTY spawning moved to +> Dart FFI `posix_openpt()` + `posix_spawn()` (D-5 amendments, +> T-96). The race described below was specific to the old C helper's +> SCM_RIGHTS hand-off and no longer applies. + + ## Status: Root cause found The PTY master fd is valid (`isatty=1`), SCM_RIGHTS transfer is correct, all struct layouts are correct. The problem is **timing**: the reader isolate starts too late and the shell has already exited by the time `read()` is called on the master fd. macOS returns EOF (n=0) immediately when the slave side is closed — unlike Linux which buffers data. diff --git a/docs/pty-proposition.md b/docs/pty-proposition.md index 0e4794eb..7a0aabbe 100644 --- a/docs/pty-proposition.md +++ b/docs/pty-proposition.md @@ -1,5 +1,12 @@ # PTY on macOS: A New Diagnostic and Resolution Plan +> **⚠ HISTORICAL — `ptyc` is retired.** PTY spawning moved to +> Dart FFI `posix_openpt()` + `posix_spawn()` (D-5 amendments, +> T-96). This document captures the forensic investigation of the +> SCM_RIGHTS control-message mismatch in the old C helper. Kept for +> the diagnostic technique; the code it discusses no longer exists. + + ## 1. Executive Summary Previous attempts to fix the PTY functionality on macOS have failed, even after correcting a deadlock in the Dart code. The core of the problem appears to be a fundamental mismatch in how the C helper (`ptyc`) constructs a control message and how the Dart FFI layer is trying to parse it.