feat(palette): your own slash commands, declared in commands.toml (ADR 0018) - #112
Merged
Conversation
…ours The shipped catalogs in web/src/lib/agent-commands.ts are limited, by their own sourcing rules, to commands every user of that harness has. A command registered by a PLUGIN or by the user exists on one machine only, so it can never be vouched for there — and it is exactly the command worth one tap: omp's /fork-in-herdr, a Claude Code custom command, your own /deploy. COLLIE_COMMANDS declares them, per pane or globally: COLLIE_COMMANDS=omp:/fork-in-herdr=Fork this conversation into a herdr tab A pane your rows address shows YOUR rows and nothing else. A pane none of them address keeps the shipped catalog; declare nothing and every pane is as shipped. Replacing rather than merging is the point: this surface is a handful of one-thumb shortcuts, and the value of the shipped ten is that someone chose them — a list half-chosen by you and half-guessed for you is worse than either whole one. Nothing is lost by it. The agent's own `/` completion renders in the mirrored pane, live and complete, which no list on this side could stay: the set is registered at runtime inside the agent process, so there is nothing to enumerate from out here. Discovery belongs to the harness; this pane is for the handful you reach for without looking. Four rules, all so that adding a row narrows or clarifies, never widens: 1. Your list is the palette, on the panes it addresses. 2. A pane you did not address keeps its catalog. 3. Danger is inherited, not reset — naming a shipped command takes over its description but keeps its confirm, so re-wording a session wipe cannot make it one-tap. 4. The narrowest scope wins and one /name is one row: exact beats family beats unscoped. A family scope is only ever the catalog's own name for the family, so `claude-local:` addresses claude-local and not the family the catalog lookup would fold it onto. The grammar reuses the existing comma-separated list style, so a description cannot contain a comma and an arg hint cannot contain `=`; both are documented in .env.example and pinned by tests. Per CLAUDE.md, a fork PR leaves the version files and CHANGELOG alone. (cherry picked from commit 3021479)
…ands.toml The rows land as declared, in a file next to the operator's .env: [[commands]] scope = "omp" command = "/fork-in-herdr" description = "Fork this conversation into a new herdr tab" The env grammar packed a list into one variable, so it had to spend separators the rows then could not contain: a description could not hold a comma, an arg hint could not hold an "=". And .env is dual-parsed — bash sources it, systemd reads it as an EnvironmentFile — so prose in it needs quoting that differs between its two readers. Nothing had shipped, so the grammar goes rather than gaining a compat path; TOML gives every field its own key and the whole hazard class dissolves with it. Three things follow: - confirm = true is now spellable per row, so an operator can put their own /deploy behind the same two-tap a shipped dangerous command gets. Inheriting stays a FLOOR, not a default: confirm = false on a row naming a shipped dangerous command still confirms. - The file is read at request time behind an mtime check, the same posture as web/dist and the build id — editing it is live, with no restart. A file that stops parsing keeps serving the last good rows, warned once per change, never a 500 on /api/config. - The path is resolved from HERDR_PLUGIN_CONFIG_DIR (the dir the launcher passes into the unit) exactly as scripts/collie-ctl.sh resolves it, minus the herdr shell-out the launcher exists to spare us. Bun.TOML.parse does the parsing — no new dependency. Validation is pure and fs-free; only the mtime/read shell touches disk, and it is injected in the reader's tests. The four resolution rules, the web store and the palette are unchanged from the contributor's work; the parser's tests are ported onto the validator. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ADR 0018 records the decision with the blast radius — the operator's rows REPLACE the shipped catalog on panes they address rather than merging into it — because "why not merge?" is the question that gets reopened, and the answer is a refusal rather than a deferral. commands.toml.example ships every row commented out: copying it verbatim must leave every pane exactly as shipped. .env.example points at it instead of describing a grammar that no longer exists. README gains a Configure subsection (where the file goes, the shape of a row, replace-not-merge in one sentence, confirm, and the live pickup — with the palette itself as the checkpoint). CLAUDE.md and ARCHITECTURE.md get one line each, both linking the ADR rather than restating it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #109 — the feature, the palette resolution rules, and the web-side store are @enieuwy's (their commit is cherry-picked here with authorship preserved). The follow-ups change where rows are declared and add the maintainer decisions that were blocking merge.
What changed relative to #109:
commands.toml, not an env var.COLLIE_COMMANDSis gone before it ever shipped:.envis dual-parsed (bash sources it, systemd reads it), and the packed-string grammar spent separators a description then couldn't contain. TOML gives every field its own key — a description may contain any character. A commented-outcommands.toml.exampleships at the repo root as the starting move.confirm = truemarks an operator row dangerous — same two-tap as a destructive shipped command. Inheritance stays a floor: naming a shipped dangerous command keeps its confirm, andconfirm = falsecannot lift it. A non-booleanconfirmdrops the row (fail closed) — reading it as false would make exactly the row the operator tried to brake one-tap.web/dist), so edits need no restart. A file that stops parsing keeps serving the last good rows, warned once per change —/api/confignever breaks mid-edit..env.exampleall point at it rather than restating it.No version bump — release deferred; the bump rides the next
chore(release):commit.Tests: root 635 pass, web 2314 pass, tsc clean both packages.
🤖 Generated with Claude Code