Skip to content

feat(palette): your own slash commands, declared in commands.toml (ADR 0018) - #112

Merged
AltanS merged 3 commits into
mainfrom
commands/operator-rows
Aug 17, 2026
Merged

feat(palette): your own slash commands, declared in commands.toml (ADR 0018)#112
AltanS merged 3 commits into
mainfrom
commands/operator-rows

Conversation

@AltanS

@AltanS AltanS commented Aug 17, 2026

Copy link
Copy Markdown
Owner

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:

  • Rows live in commands.toml, not an env var. COLLIE_COMMANDS is gone before it ever shipped: .env is 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-out commands.toml.example ships at the repo root as the starting move.
  • confirm = true marks an operator row dangerous — same two-tap as a destructive shipped command. Inheritance stays a floor: naming a shipped dangerous command keeps its confirm, and confirm = false cannot lift it. A non-boolean confirm drops the row (fail closed) — reading it as false would make exactly the row the operator tried to brake one-tap.
  • Live pickup. The bridge re-reads the file behind an mtime check (same posture as web/dist), so edits need no restart. A file that stops parsing keeps serving the last good rows, warned once per change — /api/config never breaks mid-edit.
  • ADR 0018 records the replace-not-merge decision; CLAUDE.md, README (operator how-to + verify checkpoint), ARCHITECTURE.md, and .env.example all 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

enieuwy and others added 3 commits August 17, 2026 23:44
…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>
@AltanS
AltanS merged commit 59df2a9 into main Aug 17, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants