# The `sl0ppi` CLI — design Status: **agreed, not yet implemented.** This is the contract the MVP is built against. Decisions marked *(deferred)* are deliberately out of the MVP. ## The problem `sl0ppij list-sessions` on the development box returns **65 sessions**, nearly all `EXITED`, named `effulgent-magpie`, `chatty-ocelot`, `rusty-capsicum`. Nothing maps a session to a project, so resuming work means remembering which animal was which repo. The two halves of sl0ppi each solve part of this and do not talk to each other: - **sl0ppij** knows the terminal arrangement, and via `purpose=` can record what each pane and tab is *for* — but it deliberately never interprets that string. - **`pir`** knows the agent history for a directory — but nothing about the terminal. The CLI's job is to own the vocabulary the fork refuses to own, and to make reattaching restore both halves at once. ## Model ``` sl0ppij session = a ROOT dir ~/dev -> session "dev" tab = a PROJECT ~/dev/foo -> purpose "project:foo.a1b2c3" panes = .sl0ppi/layout.kdl -> purposes "agent:main", "service:web", ... ``` One root, one session. One project, one tab. Panes come from the project. ### Identity lives in the tab purpose The tab *name* is a UI string a human renames; the tab *purpose* is the key. This is not a preference, it is what the fork guarantees (see `sl0ppij/sl0p/workspace-hints-handover.md`): - reported by `sl0ppij action list-tabs --json` - layout-declared purposes **outrank in-band ones and cannot be overridden**, so `cat hostile.txt` in a pane cannot relabel a project tab - purposes **survive resurrection**, because restored tabs come back through the layout — so identity is stable across a session restart Slug format: `project:.` where the hash is of the absolute path. Keeps identity unique when two roots both contain `api`, and stays well inside the 64-char limit. The sanitiser accepts `[A-Za-z0-9_.:/-]`, so the format survives verbatim. ## Commands ``` sl0ppi attach the root session. Inside a project? focus its tab sl0ppi up [--root DIR] create/attach, then ensure one tab per discovered project sl0ppi ls projects x (tab? panes? agent alive?) sl0ppi open ensure + focus a tab (creates on demand) sl0ppi close close the tab, leave the session sl0ppi init [dir] scaffold .sl0ppi/layout.kdl sl0ppi agent [project] focus the agent:* pane; --resume picks up its pi history sl0ppi doctor verify install, plugins, config, versions ``` ### `up` is idempotent, and everything rests on that `up` diffs the discovered projects against `list-tabs --json` and creates only what is missing. Running it fifty times produces the same session. If this property ever breaks, the tool is worse than useless, so it is the first thing the tests assert. ## Decided **D1 — Discovery is depth-1.** Direct children of the root containing a `.sl0ppi/` directory. Recursive scanning of `~/dev` is slow and surprising. `sl0ppi open path/to/nested` always works explicitly. **D2 — Directories without `.sl0ppi/` are ignored.** No implicit tabs. Use `sl0ppi init` to opt a project in. **D3 — Tab order is alphabetical.** MVP. A root-level order file is *(deferred)*. **D4 — Stale tabs auto-close.** A tab whose `project:` purpose points at a missing directory is closed by `up`/`ls`. Rationale: the purpose encodes an absolute path, so "the directory is gone" is unambiguous. *Caveat to watch:* a temporarily unmounted path would look identical to a deleted one, so auto-close must skip closing when the **root itself** is missing (that is a mount problem, not a deleted project). **D5 — Every project declares an agent.** The scaffolded layout includes a pane with `purpose="agent:main"` running pi. **D6 — Command panes are `start_suspended` by default.** A root with twelve projects must not become twelve running servers and twelve agents on `up`. The tab and its arrangement exist; nothing executes until the pane is focused and gets a keypress. `--eager` opts in per run. **D7 — `.sl0ppi/layout.kdl` is a fragment**: pane nodes only, no wrapping `layout {}` or `tab {}`. sl0ppi composes the tab around it: ```kdl tab name="foo" purpose="project:foo.a1b2c3" cwd="/home/user/dev/foo" { } ``` Pure string composition, no KDL parser, and sl0ppi keeps sole control of identity and cwd. `sl0ppi print ` emits the full layout for use with plain `sl0ppij --layout`. **D8 — Running `sl0ppi` inside the session focuses, it does not nest.** Detected via `$SL0PPIJ`/`$ZELLIJ`. **D9 — `sl0ppi` is the daily entry point, and it fails open.** Any error — bad layout, unreadable root, unexpected state — warns on stderr and `exec`s plain `sl0ppij` with the original arguments. An orchestrator bug must never leave you without a terminal; when the environment breaks, the terminal is what you debug it with. sl0ppij stays fully usable on its own, and sl0ppi ignores sessions it did not create. **D10 — TypeScript on the bundled bun.** We already ship bun and pi's `./client` SDK, and `pi-ext` is TypeScript, so this adds no runtime. It is mostly JSON and process spawning; a Rust binary would share nothing with the extensions. It also gives `pir` a path off its undeclared `python3` dependency (see below). ## Non-goals - Not a session manager competing with zellij's; ad-hoc `sl0ppij -s foo` sessions are none of its business. - No daemon, no database. All state is derived from `list-tabs --json`, `list-panes --json` and the filesystem, so there is nothing to desync. - Never required. Deleting sl0ppi leaves sl0ppij and pi working exactly as now. - Does not auto-launch agents you did not ask for (hence D6). ## Known problem: tab overflow Twelve projects is twelve tabs in a status bar that shows maybe six before it truncates. At that scale the tab bar is a status display, not navigation — which is presumably why `zj-pane-finder` exists. So `sl0ppi open` should be a fuzzy picker rather than a name argument you must remember. MVP: `sl0ppi open ` with prefix matching. A picker is the first thing to add after, and if tabs turn out to be the wrong container at 20+ projects, that is the design review trigger. ## Known problem: `pir` is not self-contained The shipped `bin/pir` is `#!/usr/bin/env python3` and shells out to `fzf`. Neither is in the tarball; the clean-room test only passed because `/usr/bin` was on `PATH`. So the packaging claim "no system runtime needed" is currently true for `sl0ppij` and `pi`, and false for `pir`. Absorbing `pir` into `sl0ppi agent --resume` (D10) removes the `python3` dependency. `fzf` then becomes either a declared external dependency or an internal picker. ## Fork changes this design required Designing against the fork turned up one real gap, found by integration-testing rather than reading: - **purposes were dropped by runtime `new-tab`** (`sl0p: carry pane and tab purposes over the wire`). `TiledPaneLayout`/`FloatingPaneLayout` had no protobuf `purpose` field, so a layout sent as an Action lost every purpose it declared. Session-creation layouts kept theirs, which is why the feature looked fine until an orchestrator tried to add a tab. Both protobuf schemas now carry it, sanitised on ingest. Also worth recording, because it costs an afternoon otherwise: **KDL nodes on a single line need `;` terminators**. `tab x { pane }` fails to parse; the generator emits multi-line KDL, which sidesteps it entirely. ## MVP scope 1. discovery + slug + `list-tabs --json` diff (the idempotency core) 2. `up`, `ls`, `open`, `close` 3. `init` scaffolding a layout with `agent:main` + `start_suspended` 4. fail-open wrapper (D9) 5. `doctor` Deferred: `agent --resume` (needs the pir port), fuzzy picker, root order file, eager/lazy per project, multi-root listing.