From 9f8186ebc4b6d7f3278ffd0f673e58eeffd9d1d5 Mon Sep 17 00:00:00 2001 From: Alex Clarke Date: Fri, 21 Aug 2026 12:08:00 -0600 Subject: [PATCH] docs: Document macros as first class custom commands --- Home.md | 2 +- Macros.md | 150 ++++++++++++++++++++++++++++++++++++++++++++++++------ REPL.md | 24 +++++++-- 3 files changed, 155 insertions(+), 21 deletions(-) diff --git a/Home.md b/Home.md index 7f204a0..4a50701 100644 --- a/Home.md +++ b/Home.md @@ -36,7 +36,7 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g * [Create Custom Bash Tools](Custom-Bash-Tools) * [Bash Prompt Utilities](Bash-Prompt-Helpers) * [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. * [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]`. diff --git a/Macros.md b/Macros.md index 5926b1c..38ad9c7 100644 --- a/Macros.md +++ b/Macros.md @@ -1,8 +1,15 @@ 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, -RAG, or agent state, and they will not affect your current context. +workflows. They are also Coyote's **custom commands**: any enabled macro can be invoked directly from the REPL as a +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) @@ -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 | +|-------------------------------|----------------------------------------------------------------------------------------------------| +| `. [args...]` | Top-level custom command. Built-in commands always win a name collision (see [Naming rules](#naming-rules)) | +| `.macro [args...]` | Explicit form. Also the escape hatch for macros whose names are shadowed by a built-in command | +| `coyote --macro [args...]` | From the shell, without entering the REPL | + +Tab completion knows about your macros: `.` offers enabled macros (with their `description`) alongside the built-in +commands, and `.macro ` completes macro names, descriptions, and the `enable`/`disable` subcommands. + +Running `.macro ` with a name that doesn't exist (and no arguments) starts the interactive macro creator. + # Macro Definition -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 +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 command: ```shell 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 -users pass in variables to alter the behavior of the macro at runtime. +A macro definition has four parts, all but `steps` optional: + +* `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 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** `macros/generate-commit-message.yaml` ```yaml +description: Generate a git commit message from the current diff steps: - .file `git diff` -- generate git commit message ``` @@ -39,13 +66,17 @@ $ coyote --macro generate-commit-message 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. ## 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. -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. **Example:** @@ -54,12 +85,12 @@ $ coyote --macro example-variable-macro first_argument second_argument ``` 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. -* `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. -* `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 +* `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 `false`. 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. +# 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 ` 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 ` | Add a single macro to the global enabled list | +| `.macro disable ` | Remove a single macro from the global enabled list | +| `.set enabled_macros ` | 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 ` | +| `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 ` | +| `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 -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 +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 looking for more examples. * `generate-commit-message` - Generate a Git commit message based on the staged changes in the current directory diff --git a/REPL.md b/REPL.md index 8188c22..cfaad06 100644 --- a/REPL.md +++ b/REPL.md @@ -12,10 +12,12 @@ things like * **Tab Autocompletion:** Every command in the REPL (i.e. everything that starts with a `.`) has fuzzy search auto completions. * `.` to complete REPL commands + * `.` also offers enabled [macros](Macros) (with their descriptions) as top-level custom commands * `.model ` to complete chat models * `.set ` to complete configuration keys * `.set key ` to complete configuration values * `.mcp auth ` to complete remote MCP server names + * `.macro ` 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 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`) @@ -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). -## `.macro` - Execute a macro -Macros in Coyote are like "scripts" of commands that can be run in isolated environments; that means they do not use any -active settings and use the same settings they had when written. They are created/executed using the `.macro ` -command. +## `.macro` - Execute or manage a macro +Macros in Coyote are like "scripts" of commands and they double as Coyote's **custom commands**: any enabled macro can +be invoked directly as a top-level command (`.my-macro args`), with tab completion alongside the built-ins. By default +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 | +|---------------------------|-----------------------------------------------------------------------------------------| +| `. [args...]` | Run an enabled macro as a top-level custom command (built-in commands win name collisions) | +| `.macro [args...]` | Run a macro explicitly; also works for macros shadowed by a built-in command's name | +| `.macro ` | If `` doesn't exist, starts the interactive macro creator | +| `.macro enable ` | Add a single macro to the global enabled list | +| `.macro disable ` | 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) @@ -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_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_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 | | `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 |