migrate ADRs into D/R records; delete docs/ADRs/
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -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`.
|
||||
@@ -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 `<repo>/.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.
|
||||
@@ -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]`).
|
||||
@@ -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 <subcommand>` — 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).
|
||||
@@ -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 <path>`)* | — (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 <subsystem>[:<id>]]` — 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 <subsystem> <verb> [<positional>...] [--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 <path>` → `clide editor open <path>`).
|
||||
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`: `<subsystem>.<verb_past|noun_changed>`. 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 <subsystem> --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.
|
||||
Reference in New Issue
Block a user