Define custom slash commands

Drop a markdown file into .cantrip/commands/ and the filename becomes a new slash verb.

The idea

Some prompts come up again and again — "triage this issue", "walk the relation between these two charms", "generate a release note from the last five commits". Typing the same 200 characters every time gets old fast. Custom slash commands let a charm team ship those prompts as ordinary markdown files, discoverable by every session that opens the charm.

Where Cantrip looks

Two locations, read on every agent startup; the repo wins when verbs collide:

  1. ~/.config/cantrip/commands/*.md — your personal commands, available across every charm.
  2. <charm>/.cantrip/commands/*.md — per-charm commands, committed alongside the code so every team member gets them automatically.

The filename becomes the slash verb: relation-check.md registers /relation-check. Filenames must match [a-z0-9][a-z0-9_-]*; anything else is rejected with a clear error.

A working example

Save this as .cantrip/commands/relation-check.md in a charm:

---
description: Inspect a relation on a deployed charm
agent: primary
---
Inspect the ``$1`` relation on my deployed charms.  Current
relations according to Juju:

!`juju show-unit $1/0 2>&1 | head -80`

Walk through what the relation is carrying, whether the
endpoint is bound, and whether the remote side looks healthy.

Then, in any Cantrip session for that charm:

> /relation-check redis

Cantrip substitutes $1 with redis, runs juju show-unit redis/0, folds the output into the prompt, and hands the whole thing to the agent. The agent sees a fully-hydrated message — same tools, same context, no special case.

Frontmatter schema

---
description: <one-line summary; shown in /help and autocomplete>
agent: primary | research | build | deploy | test | debug | infra
model: <optional model override>
subtask: <optional bool; default false>
retry: <optional retry block; see "Retry blocks" below>
---
<markdown prompt template>
description
One-liner shown in /help and the autocomplete popup. Optional; Cantrip falls back to "User command from <filename>" if you omit it.
agent
Which agent receives the expanded prompt. primary (the default) means the conversation loop, exactly as if you had typed the expansion at the prompt. Any subagent category name (research, build, deploy, test, debug, infra) queues the expansion as a work-queue task and a subagent of that category picks it up.
model
Override the model used for this command. Optional.
subtask
Force work-queue dispatch even when agent is primary. Useful when you want a command to run in the background without pausing the current conversation.
retry
Optional declarative retry block. When set, Cantrip runs the command, evaluates a list of checks against the result, and retries with a corrective prompt if any check fails. See Retry blocks below. Only supported on primary commands today; setting retry alongside subtask: true or a non-primary agent is rejected at load time.

Unknown frontmatter keys raise a clear error on load — no silent "typed a key wrong, command gets wrong defaults".

Placeholders

The body is plain markdown with four substitution shapes:

Evaluation order is fixed: arguments first, then @path, then !`cmd` ``. This means a shell command can carry a filled-in positional argument, and a file reference can include a path built from arguments. It also means an earlier expansion cannot accidentally leak content that gets re-expanded as a placeholder.

Interaction with permissions

Shell expansion runs through the same policy the subagent uses. That has two practical consequences:

Dispatch semantics

When you type a custom verb Cantrip:

  1. Looks up the command in the registry loaded at session start.
  2. Expands the body (arguments → files → shell).
  3. For agent: primary and subtask: false — feeds the expansion to agent.process_message, exactly as if you'd typed it. The answer lands in chat.
  4. For a subagent category or subtask: true — creates a work-queue task of that category. Progress shows up in the task panel; the agent continues the foreground conversation unchanged.

The catalogue (/help, slash-command autocomplete, the CLI startup banner) picks up every loaded command automatically. No config entry required.

Retry blocks

A retry block declares a success predicate for the command: a list of checks that must all pass before Cantrip considers the run done. If any check fails, Cantrip re-runs the command with a corrective prompt that quotes the failed checks and the previous response. The block is the user-specified counterpart to Ralph Loop — Ralph stops when the agent says STOP; retry stops when your shell command says yes.

---
description: Build the charm and pass unit tests
retry:
  max_retries: 3
  timeout_seconds: 600
  checks:
    - type: shell
      command: "uv run pytest tests/unit -q"
    - type: file_exists
      path: src/charm.py
  on_failure: "echo 'rolled back'"
---
Build the charm and run the full unit suite. Fix any failures.
max_retries
Number of retries on top of the initial attempt. Default 1; capped at 50. The total attempt budget is max_retries + 1.
timeout_seconds
Total wall-clock budget across every attempt. Default 600 (ten minutes). When the deadline is reached the runner returns without starting another attempt.
checks
Ordered list of checks. Every check must pass for the run to converge. Three types ship today:
  • shell — runs command in the repo root and passes when the command exits with status 0. Routes through the permission policy: deny records a failure with the policy reason; ask parks on the CONFIRM surface and refuses if the user declines. Optional per-check timeout_seconds defaults to 60 s.
  • file_exists — passes when path resolves to an existing regular file. Path safety mirrors @path: absolute paths and traversals outside the repo root are rejected at load time.
  • json_schema — validates the task's final response against the JSON Schema in schema. Markdown ```json fences are stripped before parsing.
on_failure
Optional shell command run once if the run exits without converging. Useful for cleanup ("git stash drop", "rm -rf build"). Goes through the same permission gate as shell checks; a refused or failed launch is logged but does not raise.

Each retry attempt receives the original goal verbatim plus a short summary of which checks failed and an excerpt of the previous response. The original prompt is preserved so a long-context model does not drift toward the failure summary. The chat surface reports the attempt count and any unresolved failures in a one-paragraph summary at the bottom of the final response.

Troubleshooting