docs: Document macros as first class custom commands

2026-08-21 12:08:00 -06:00
parent f3d766007e
commit 9f8186ebc4
3 changed files with 155 additions and 21 deletions
+1 -1
@@ -36,7 +36,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
* [Create Custom Bash Tools](Custom-Bash-Tools) * [Create Custom Bash Tools](Custom-Bash-Tools)
* [Bash Prompt Utilities](Bash-Prompt-Helpers) * [Bash Prompt Utilities](Bash-Prompt-Helpers)
* [First-Class MCP Server Support](MCP-Servers): Easily connect and interact with MCP servers for advanced functionality. * [First-Class MCP Server Support](MCP-Servers): Easily connect and interact with MCP servers for advanced functionality.
* [Macros](Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros). * [Macros](Macros): Automate repetitive tasks and workflows with Coyote "scripts" (macros), and invoke them as your own custom REPL commands.
* [RAG](RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation. * [RAG](RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
* [Sessions](Sessions): Manage and persist conversational contexts and settings across multiple interactions. * [Sessions](Sessions): Manage and persist conversational contexts and settings across multiple interactions.
* [Memory](Memory): Persistent file-based memory that survives across sessions. Bootstrap with `coyote --init-memory [global|workspace]`. * [Memory](Memory): Persistent file-based memory that survives across sessions. Bootstrap with `coyote --init-memory [global|workspace]`.
+134 -16
@@ -1,8 +1,15 @@
Macros are essentially Coyote "scripts"; that is, a predefined sequence of REPL commands that automate repetitive tasks or Macros are essentially Coyote "scripts"; that is, a predefined sequence of REPL commands that automate repetitive tasks or
workflows. Macros run in isolated environments, ensuring that the macros don't inherit any pre-existing role, session, workflows. They are also Coyote's **custom commands**: any enabled macro can be invoked directly from the REPL as a
RAG, or agent state, and they will not affect your current context. top-level dot-command, exactly like a built-in.
This isolation ensures that your workspace remains clean and unaffected by macro operations. ```shell
ollama:gemma4:26b)> .generate-commit-message
```
By default, macros run in isolated environments, ensuring that the macros don't inherit any pre-existing role, session,
RAG, or agent state, and they will not affect your current context. This isolation ensures that your workspace remains
clean and unaffected by macro operations. Macros that *should* affect your context can opt out of isolation with
[`isolated: false`](#isolation).
![Macro Example](./images/macros/macros-example.gif) ![Macro Example](./images/macros/macros-example.gif)
@@ -10,25 +17,45 @@ For more information on Coyote's REPL, refer to the [REPL](REPL) documentation.
--- ---
# Invoking Macros
There are three equivalent ways to run a macro:
| Invocation | Notes |
|-------------------------------|----------------------------------------------------------------------------------------------------|
| `.<name> [args...]` | Top-level custom command. Built-in commands always win a name collision (see [Naming rules](#naming-rules)) |
| `.macro <name> [args...]` | Explicit form. Also the escape hatch for macros whose names are shadowed by a built-in command |
| `coyote --macro <name> [args...]` | From the shell, without entering the REPL |
Tab completion knows about your macros: `.<tab>` offers enabled macros (with their `description`) alongside the built-in
commands, and `.macro <tab>` completes macro names, descriptions, and the `enable`/`disable` subcommands.
Running `.macro <name>` with a name that doesn't exist (and no arguments) starts the interactive macro creator.
# Macro Definition # Macro Definition
Macros are defined as YAML files in the `macros` subdirectory of your Coyote configuration directory. The Coyote configuration Macros are defined as YAML files in the `macros` subdirectory of your Coyote configuration directory. The Coyote configuration
directory can vary between systems, so to find the location of your macros config directory, you can use the following directory can vary between systems, so to find the location of your macros config directory, you can use the following
command: command:
```shell ```shell
coyote --info | grep 'macros_dir' | awk '{print $2}' coyote --info | grep 'macros_dir' | awk '{print $2}'
``` ```
Macro definitions are broken into two parts: the `steps` of the macro, and an optional `variables` section that lets A macro definition has four parts, all but `steps` optional:
users pass in variables to alter the behavior of the macro at runtime.
* `description` (Optional): A short description of what the macro does. Shown in `.list macros` and in tab completion.
* `isolated` (Optional, Boolean): Whether the macro runs on a forked, throwaway context (`true`, the default) or directly
on your live context (`false`). See [Isolation](#isolation).
* `variables` (Optional): Positional runtime arguments. See [Macro Variables](#macro-variables).
* `steps` (Required): The commands to run.
## Step Definitions ## Step Definitions
The step definitions for a macro are straightforward: They are simply the exact commands you would otherwise type in the The step definitions for a macro are straightforward: They are simply the exact commands you would otherwise type in the
REPL. REPL.
**Example: Macro to generate a git commit message** **Example: Macro to generate a git commit message**
`macros/generate-commit-message.yaml` `macros/generate-commit-message.yaml`
```yaml ```yaml
description: Generate a git commit message from the current diff
steps: steps:
- .file `git diff` -- generate git commit message - .file `git diff` -- generate git commit message
``` ```
@@ -39,13 +66,17 @@ $ coyote --macro generate-commit-message
Add documentation on macros Add documentation on macros
``` ```
> **Note:** A step is a plain YAML string. If a step contains a colon followed by a space (`: `), YAML will parse the
> list item as a map and the macro will fail to load (it shows as `invalid` in `.list macros`). Quote the step or use a
> block scalar (`- >-`) to avoid this.
For a full example configuration, refer to the [example macro configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.macro.example.yaml) in the root of this project. For a full example configuration, refer to the [example macro configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.macro.example.yaml) in the root of this project.
## Macro Variables ## Macro Variables
Sometimes it's useful to be able to modify the behavior of a macro at runtime. This is achieved with the `variables` Sometimes it's useful to be able to modify the behavior of a macro at runtime. This is achieved with the `variables`
array of the macro definition. array of the macro definition.
To pass variables to a macro, since they are just Coyote scripts, the syntax is the same as it is for any other scripting To pass variables to a macro, since they are just Coyote scripts, the syntax is the same as it is for any other scripting
language: You just pass them alongside your invocation. language: You just pass them alongside your invocation.
**Example:** **Example:**
@@ -54,12 +85,12 @@ $ coyote --macro example-variable-macro first_argument second_argument
``` ```
Each variable in the `variables` array has the following properties: Each variable in the `variables` array has the following properties:
* `name` (Required): the name of the variable, which can be referenced in the actual steps of the macro using the * `name` (Required): the name of the variable, which can be referenced in the actual steps of the macro using the
`{{name}}` syntax. `{{name}}` syntax.
* `default` (Optional): A default value for the variable if no value is specified. If no default value is defined, and * `default` (Optional): A default value for the variable if no value is specified. If no default value is defined, and
no value is provided for the variable at runtime, Coyote will error out. no value is provided for the variable at runtime, Coyote will error out.
* `rest` (Optional, Boolean): When set to `true`, this variable will collect all remaining arguments passed to the * `rest` (Optional, Boolean): When set to `true`, this variable will collect all remaining arguments passed to the
macro. This behavior is only applicable when the variable is the last variable in the list. By default, this is macro. This behavior is only applicable when the variable is the last variable in the list. By default, this is
`false`. `false`.
The `variables` array is order-dependent; that is to say that all arguments passed to the macro are positional. So be The `variables` array is order-dependent; that is to say that all arguments passed to the macro are positional. So be
@@ -86,9 +117,96 @@ $ coyote --macro invoke-agent sql What tables are available?
For a full example configuration, refer to the [example macro configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.macro.example.yaml) in the root of this project. For a full example configuration, refer to the [example macro configuration file](https://github.com/Dark-Alex-17/coyote/blob/main/config.macro.example.yaml) in the root of this project.
# Isolation
The `isolated` field controls where a macro's steps execute:
* **`isolated: true` (default):** Steps run on a forked, throwaway copy of your context. Nothing the macro does (e.g. role
switches, model changes, RAG activation, etc.) leaks back into your live context, and the macro's exchanges are not
recorded in your active session.
* **`isolated: false`:** Steps run directly on your **live** context, exactly as if you had typed each command yourself.
The exchanges are recorded in your active session and the conversation continues from them.
Non-isolated macros are the right tool when the *point* of the macro is to change your context; e.g. a `.standup` macro
that switches roles, loads a RAG, and asks a kickoff question. Be aware of the consequences:
* **Mutations persist by design.** If a non-isolated macro runs `.role reviewer`, you are still in the `reviewer` role
after it finishes.
* **Steps are fail-fast.** If a step errors, the remaining steps are skipped, but the effects of the steps that already
ran remain.
* **Nested macros are rejected.** A macro invoked by a non-isolated macro's step is an error. (An *isolated* macro's
step may invoke a non-isolated macro; it simply runs on the fork and cannot touch your live context.)
* **Agent sessions engage normally.** In an isolated macro, an agent's `use_agent` session is suppressed (the forked
context has nothing to return to). In a non-isolated macro, `.agent <name>` behaves exactly as if you typed it.
* `.exit` in a macro step does not exit the REPL (it applies to the macro's own scope).
# Workspace Macros
In addition to your global macros directory, Coyote loads macros from `.coyote/macros/` in the current working
directory. This lets a project ship its own custom commands, (e.g. `.review-work` macro tailored to one repo),
that are only available when you run Coyote from that project.
* A workspace macro with the same name as a global macro **shadows** it; `.list macros` shows both, with a `source`
column telling you which one is the invocation target.
* The `--no-workspace-macros` CLI flag disables workspace macro loading entirely (this is a CLI flag only, not a config
file key, mirroring `--no-workspace-mcp`).
# Scoping With `enabled_macros`
Which macros are available can be controlled per context with the `enabled_macros` setting, mirroring
[`enabled_skills`](Skills#scoping). It can be set at four levels:
* **Global** (`config.yaml`): the default-active set. Also settable via the `COYOTE_ENABLED_MACROS` environment variable
and adjustable at runtime with `.set enabled_macros` or `.macro enable|disable`.
* **Role** (frontmatter), **Agent** (agent config), and **Session**: restrict the set for that context.
The rules:
* **Most-specific wins:** `session > agent > role > global`. The first level that sets `enabled_macros` decides the
entire set; the lists are not merged.
* **Unset means all:** A level that doesn't set `enabled_macros` falls through to the next. If no level sets it, every
installed macro is enabled.
* **An empty list means none:** `enabled_macros: []` disables all macros for that context.
* Global config, role frontmatter, and sessions accept either a YAML list or a comma-separated string
(`enabled_macros: standup,review-work`); agent configs accept the list form.
* Graph agents ignore `enabled_macros` (a `graph.yaml` entry is silently ignored, like other unknown keys).
* Unlike `enabled_skills`, an `enabled_macros` name that doesn't match any installed macro is **not** an error, but rather is
logged and shown as `missing` in `.list macros`. Macros are meant to be freely shared and edited, so a dangling name
shouldn't stop Coyote from starting.
# Managing Macros at Runtime
| Command | Description |
|------------------------------|----------------------------------------------------------------------------------------------|
| `.macro enable <name>` | Add a single macro to the global enabled list |
| `.macro disable <name>` | Remove a single macro from the global enabled list |
| `.set enabled_macros <csv\|null>` | Replace the whole global list (`null` clears it, re-enabling everything) |
| `.list macros` | List every macro with its source, isolation, state, and description |
Like `.set`, these adjust the in-memory global configuration; update `config.yaml` to persist them. Because they edit the
*global* level, they cannot override a role/agent/session `enabled_macros` allowlist. If one is active, the toggle
errors and names the config you'd need to edit instead. Disabling a macro when no global list is set materializes the
list as "every installed macro except this one."
The `state` column of `.list macros` tells you exactly why a macro is or isn't invocable:
| State | Meaning |
|----------------------|------------------------------------------------------------------------------------------------------|
| `enabled` | Visible and invocable |
| `disabled (runtime)` | Excluded at the global level; re-enable with `.macro enable <name>` |
| `locked` | Excluded by a role/agent/session `enabled_macros` list (the owning config is shown); edit that config |
| `missing` | Named in an `enabled_macros` list but not installed |
| `shadowed (built-in)`| The name collides with a built-in command; invocable only via `.macro <name>` |
| `invalid` | The definition file failed to parse (the reason is shown), or the name is reserved |
# Naming Rules
The macro's file name is its command name. Two constraints:
* **`enable` and `disable` are reserved** (they are `.macro` subcommands); a macro with either name shows as `invalid`.
* **Built-in commands always win.** A macro named after a built-in command (e.g. `model.yaml`) is never dispatched at
the top level; `.model` runs the built-in. The macro remains reachable via `.macro model`, and `.list macros` shows
it as `shadowed (built-in)`.
# Built-In Macros # Built-In Macros
Coyote comes packaged with some useful built-in macros. These are also good examples if you're looking for more examples Coyote comes packaged with some useful built-in macros. These are also good examples if you're looking for more examples
on how to make your own macros, so be sure to check out the [built-in macro definitions](https://github.com/Dark-Alex-17/coyote/blob/main/assets/macros) if you're on how to make your own macros, so be sure to check out the [built-in macro definitions](https://github.com/Dark-Alex-17/coyote/blob/main/assets/macros) if you're
looking for more examples. looking for more examples.
* `generate-commit-message` - Generate a Git commit message based on the staged changes in the current directory * `generate-commit-message` - Generate a Git commit message based on the staged changes in the current directory
+20 -4
@@ -12,10 +12,12 @@ things like
* **Tab Autocompletion:** Every command in the REPL (i.e. everything that starts with a `.`) has fuzzy search auto * **Tab Autocompletion:** Every command in the REPL (i.e. everything that starts with a `.`) has fuzzy search auto
completions. completions.
* `.<tab>` to complete REPL commands * `.<tab>` to complete REPL commands
* `.<tab>` also offers enabled [macros](Macros) (with their descriptions) as top-level custom commands
* `.model <tab>` to complete chat models * `.model <tab>` to complete chat models
* `.set <tab>` to complete configuration keys * `.set <tab>` to complete configuration keys
* `.set key <tab>` to complete configuration values * `.set key <tab>` to complete configuration values
* `.mcp auth <tab>` to complete remote MCP server names * `.mcp auth <tab>` to complete remote MCP server names
* `.macro <tab>` to complete macro names and the `enable`/`disable` subcommands
* **Multi-Line Prompts:** You can also type prompts that span more than one line to help organize your thoughts. This * **Multi-Line Prompts:** You can also type prompts that span more than one line to help organize your thoughts. This
can be done in the following ways: can be done in the following ways:
* `Ctrl-o` to open the current input buffer in your preferred editor (either the value of `editor` or `$EDITOR`) * `Ctrl-o` to open the current input buffer in your preferred editor (either the value of `editor` or `$EDITOR`)
@@ -159,10 +161,23 @@ complete tasks using the documents as additional context.
For more information about RAG in Coyote and how to utilize it, refer to the [rag documentation](RAG). For more information about RAG in Coyote and how to utilize it, refer to the [rag documentation](RAG).
## `.macro` - Execute a macro ## `.macro` - Execute or manage a macro
Macros in Coyote are like "scripts" of commands that can be run in isolated environments; that means they do not use any Macros in Coyote are like "scripts" of commands and they double as Coyote's **custom commands**: any enabled macro can
active settings and use the same settings they had when written. They are created/executed using the `.macro <name>` be invoked directly as a top-level command (`.my-macro args`), with tab completion alongside the built-ins. By default
command. macros run in isolated environments (they don't use any active settings and can't affect your current context), but a
macro can opt onto your live context with `isolated: false` in its definition.
| Command | Description |
|---------------------------|-----------------------------------------------------------------------------------------|
| `.<name> [args...]` | Run an enabled macro as a top-level custom command (built-in commands win name collisions) |
| `.macro <name> [args...]` | Run a macro explicitly; also works for macros shadowed by a built-in command's name |
| `.macro <name>` | If `<name>` doesn't exist, starts the interactive macro creator |
| `.macro enable <name>` | Add a single macro to the global enabled list |
| `.macro disable <name>` | Remove a single macro from the global enabled list |
Availability is scoped with `enabled_macros` (global/role/agent/session, most-specific wins) and macros can also be
loaded per-project from `.coyote/macros/` in the working directory. `.list macros` shows every macro's source, isolation,
and state.
![macro](./images/macros/macros-example.gif) ![macro](./images/macros/macros-example.gif)
@@ -303,6 +318,7 @@ The following settings can be adjusted at runtime:
| `enabled_tools` | string | Comma-separated list of enabled tools (e.g. `fs_ls,fs_cat` or `all`); the saved YAML config also accepts a list form | | `enabled_tools` | string | Comma-separated list of enabled tools (e.g. `fs_ls,fs_cat` or `all`); the saved YAML config also accepts a list form |
| `enabled_mcp_servers` | string | Comma-separated list of enabled MCP servers (e.g. `github,slack` or `all`); the saved YAML config also accepts a list form | | `enabled_mcp_servers` | string | Comma-separated list of enabled MCP servers (e.g. `github,slack` or `all`); the saved YAML config also accepts a list form |
| `enabled_skills` | string | Comma-separated list of enabled [skills](Skills) (e.g. `git-master,ai-slop-remover`); `null` clears the override | | `enabled_skills` | string | Comma-separated list of enabled [skills](Skills) (e.g. `git-master,ai-slop-remover`); `null` clears the override |
| `enabled_macros` | string | Comma-separated list of enabled [macros](Macros) at the global level (e.g. `standup,review-work`); `null` clears the list, re-enabling all macros |
| `save_session` | boolean | Whether to auto-save sessions | | `save_session` | boolean | Whether to auto-save sessions |
| `compression_threshold` | integer | Token threshold for session compression | | `compression_threshold` | integer | Token threshold for session compression |
| `compression_keep_last` | integer | Number of recent messages kept visible after compression; `0` (default) compresses all messages | | `compression_keep_last` | integer | Number of recent messages kept visible after compression; `0` (default) compresses all messages |