# Design: Macros as First-Class Custom Commands Status: DRAFT v2 — grounded in code (src/config/macros.rs, src/repl/mod.rs, src/config/install_remote.rs, src/config/paths.rs). Supersedes v1, which proposed a new markdown "commands" artifact before discovering macros already cover ~80% of the feature. ## 0. Decision record - **Naming: keep "macros"** (user decision). No rename, no new artifact type, no new install filter — `--install-from --filter macros` already exists in both the CLI enum (`InstallFilter`, config/mod.rs:403) and the REPL parser (`install_remote_from_repl_args`). (Note: the flag may be renamed to `--install` per plans/bundle-manifest-design.md §5; the filter is unaffected.) Docs will state plainly: *macros are coyote's custom commands* (README + config examples + `.help`). "Macro" is also the more accurate term: these are replayable scripts of REPL commands with variables, not just prompt templates. - **Keep the `.macro` subcommand** alongside top-level invocation (user decision): it hosts the interactive creator (which top-level must never trigger), it is the escape hatch for macros shadowed by built-ins (incl. future built-ins landing on existing macro names), and removing it breaks muscle memory and macros whose steps invoke `.macro`. Cost of keeping: ~0. - **Rejected**: separate markdown command files (redundant — a single-step macro with one `rest` variable IS a prompt command, and macros additionally do multi-step, role switching, and `` .file `cmd` `` shell embedding); rename to "commands" (churn, dir migration, and the less accurate word); an `alias` field (complexity without payoff — the file name is the command name, matching the existing naming structure); command-side agent/role binding (inverts contexts-curate-artifacts; lets installed bundles mutate existing contexts). - **Bundle manifest / provenance tracking is NOT in this plan.** It is an orthogonal, separate design (see §10); this feature neither depends on nor blocks it. ## 1. What exists today (verified) - `Macro { variables: Vec, steps: Vec }`, YAML at `macros_dir()/.yaml` (global only; no workspace dir, unlike skills). - `.macro [args]` executes; nonexistent name + no args opens the interactive macro creator (`ctx.new_macro`). - `macro_execute` forks a fresh `RequestContext` from the current role (inherits role's model/temperature/enabled_tools/enabled_mcp_servers), copies `last_message` in (discontinuous), sets `macro_flag`, runs each `{{var}}`-interpolated step via `run_repl_command`. **No state flows back**: the macro's exchanges are not recorded in the active session and do not update the caller's `last_message`. - Auto-generated usage strings; positional vars with defaults + rest-capture. - `.list macros`, `.delete macro`, built-ins embedded from `assets/macros/`. - Unknown dot-command → `unknown_command()` at repl/mod.rs:1521: `Error: Unknown command. Type ".help" for additional help.` ## 2. Gaps this design closes 1. Top-level invocation: `.review-work args`, not just `.macro review-work args`. 2. Discoverability: `description` field → listings, completion, `.help`. 3. Context scoping: `enabled_macros` on role / agent (non-graph) / session / global config, mirroring `enabled_skills`. 4. Runtime toggles: `.macro enable|disable `, shorthand for `.set enabled_macros` (see §6). 5. Conversation-integrated macros: `isolated: false`. 6. Workspace-local macros: `.coyote/macros/` shadowing global, mirroring workspace skills/MCP conventions (see §5). ## 3. New Macro fields ```yaml description: Review WIP against a base branch # optional; shown in listings/completion/.help isolated: false # optional; default TRUE (current behavior) variables: - name: base default: main - name: instructions rest: true default: "" steps: - "Review the diff against {{base}}. {{instructions}}" ``` Both new fields are `#[serde(default)]`-style optional → every existing macro file remains valid; default `isolated: true` preserves current behavior exactly. ### `isolated` semantics - `true` (default, today's behavior): forked context as described in §1. Right for utility macros (e.g. generate-commit-message) whose chatter should not pollute the session. - `false`: steps run via `run_repl_command(ctx, ...)` against the **live** context — exactly as if the user had typed each step at the prompt themselves. Prompts continue the actual conversation: recorded in the active session, `last_message` updated, agent context preserved. Consequence to document loudly: mutating steps (`.role x`, `.model y`) **persist after the macro ends** — that is the meaning of non-isolation, not a bug. - **VERIFIED: the session-recording chain has no macro_flag check anywhere** (`after_chat_completion` request_context.rs:1528-1544 → `save_message` :1464-1474 → `Session::add_message` session.rs:724-763; disk save deferred to `Session::exit` :626-631). Today's "macros aren't recorded" behavior comes ENTIRELY from the fork (`session: None` in the fresh context), not from the flag — so non-isolated execution records normally with zero changes to the recording path. - Footnotes to today's isolation (documented, not changed): (a) the forked ctx still appends exchanges to the flat `messages.md` file via the `save_message` fallthrough (request_context.rs:1518-1526) — only *session* recording is skipped; (b) Arc-backed state copied into the fork (supervisor, inbox, escalation_queue) is shared, so mutations through those handles already reach the parent. - Both modes: `macro_flag` is set for the duration (temporarily on the live ctx for non-isolated, restored on exit **including the error path** — RAII guard) so the existing forbidden-op guards (repl/mod.rs:890, 979, 1310) apply, and nested `.macro`/top-level macro invocation inside a macro is rejected in non-isolated mode (isolated mode already recurses safely via `#[async_recursion]`; keep as-is). Precise predicate (oracle note): reject when `macro_flag` is set AND the CURRENT execution mode is non-isolated. An **isolated** macro's step invoking a **non-isolated** macro runs it inline on the FORKED ctx (harmless — the fork has no session); pin this behavior in the step-5 test matrix. Implementation footguns (oracle): (a) the RAII guard cannot hold `&mut ctx.macro_flag` while `&mut ctx` is passed to `run_repl_command` — wrap the whole `&mut RequestContext` (Drop restores prior mode) or use save/restore around a closure; (b) prefer a COMPANION FIELD for the mode over changing `macro_flag` to `Option` (the enum touches all 13 sites incl. both fork-propagation sites; the companion field is the smaller diff); (c) a separate `macro_execute_inline` fn creates a new type-level recursion cycle with `run_repl_command` and needs its own `#[async_recursion]`/boxing — a branch inside the already-boxed `macro_execute` is free. Semantics caveat to document (§8): steps are FAIL-FAST — a mid-macro error aborts remaining steps while completed steps' mutations persist (slightly stronger than "as if typed", where a human would continue past errors); and a `.exit` step's exit signal is swallowed inside macros today (bool discarded at macros.rs:67) — unchanged, but say so. - **VERIFIED guard inventory** (13 `macro_flag` sites: 4 decl/init, 2 propagation — `fork_for_branch` request_context.rs:276, `new_for_child` :315 — and 7 behavioral). Under non-isolated execution: - Keep as-is (desirable in a macro even on the live ctx): `.update` bail (repl/mod.rs:890), `.edit` bail (:979 — no `$EDITOR` mid-macro), blank-line suppression (:1310, cosmetic), `new_role` prompt→bail (request_context.rs:2259), `new_macro` prompt→bail (:2360). - Non-issue: `apply_prelude` skip (:3966) — on a live REPL ctx the prelude already ran and `state()` is non-empty. - **RULED (user, 2026-08-20): condition on isolation.** `use_agent` (:3743-3749) suppresses the agent's default `agent_session` when `macro_flag` is set; under `isolated: false` the suppression is LIFTED — a non-isolated `.agent foo` step inherits foo's default session exactly as if typed. Mechanism: the RAII guard records the mode (companion field or `Option` replacing the bare bool) so use_agent can distinguish isolated from non-isolated; isolated mode keeps today's suppression verbatim. - Nested-macro *execution* is currently unguarded (`new_macro` only blocks the interactive creator) — the non-isolated nesting rejection is NEW code, not a reuse of an existing check. ## 4. Top-level invocation - Dispatch order in the REPL command match: **built-ins first**, then — where `unknown_command()` fires today — look up visible macros by name (file stem). Hit → execute exactly as `.macro ` would (honoring `isolated`). Miss → existing `unknown_command()` error, verbatim, unchanged. **VERIFIED insertion point**: the top-level catch-all `_ => unknown_command()?` at repl/mod.rs:1297. The other `unknown_command()` call sites (:630, :740, :763, :1236, :1259) are sub-argument mismatches inside known commands and must NOT dispatch macros. - `.macro` subcommand is kept verbatim for back-compat, including the interactive-creation flow (creation stays ONLY under `.macro`; a top-level typo like `.hi` must error, never open the creator). - Both entry points (`.name` and `.macro name`) go through the same §5 visibility check — otherwise `.macro` trivially bypasses `enabled_macros`. - Collision rules: - Macro name colliding with a **built-in** command: built-in always wins; macro still invokable via `.macro `; flagged `shadowed (built-in)` in `.list macros`. Built-in list sourced from the existing `ReplCommand` registry, not a hardcoded copy. **VERIFIED shape**: `static REPL_COMMANDS: LazyLock<[ReplCommand; 60]>` (repl/mod.rs:56-330), `ReplCommand { name: &'static str, description, state: AssertState }` (:554-571), with a test hardcoding the count (:1732). Macros must NOT be added to this array (static, `&'static str`, fixed count). - Tab completion: on `.`, visible macros appear alongside the usual built-ins, with their `description` shown when available — joining the built-in completer — as a SEPARATE dynamic source queried at completion time: the completer clones REPL_COMMANDS at construction (completer.rs:93-98) but already holds `Arc>` (completer.rs:84), so it can query visible macros live. **Shadowed macros are excluded from completion entirely** (RULED): a macro whose name collides with a built-in is neither dispatchable via `.` (built-in always wins) nor listed in `.` completions — it surfaces only in `.list macros` as `shadowed (built-in)` and stays invokable via `.macro `. - **`.macro ` argument completion (RULED)**: upgraded to match `.` presentation — macro names WITH descriptions when available. VERIFIED today: request_context.rs:3004 completes `.macro` args via `map_completion_values(paths::list_macros())` (names only, no descriptions), while the plumbing already supports described suggestions (`repl_complete` returns `(String, Option)`; `.model`/`.agent` arms use it, rendered at completer.rs:58-60). Change the `.macro` arm to the same resolver source as top-level completion. Differences from `.`: shadowed macros ARE listed here (`.macro` is their escape hatch), and the `enable`/`disable` subcommands appear alongside macro names. Second-arg completion: `.macro enable ` / `.macro disable ` complete toggle-eligible macro names. - Completion entries for macros carry the same `AssertState` stance as the `.macro` built-in (the completer filters on `cmd.is_valid(state)`, completer.rs:46). - `enable`/`disable` are sub-args of the existing `.macro` entry, NOT new `REPL_COMMANDS` array entries (the count-asserting test at repl/mod.rs:1732 stays untouched). `.help` gains a "custom commands (macros)" section and a line stating macros = custom commands. ## 5. Scoping: `enabled_macros` New optional field, mirroring `enabled_skills` semantics **verbatim** — same parser (`parse_string_or_array`: YAML list or comma-separated string), same null/absent/empty behavior. **VERIFIED semantics** (resolver: `SkillPolicy::effective_with`, skill_policy.rs:40-132; precedence :78-82; regression test :388-404 pinning CHANGELOG:320): **Scope of "mirror" (explicit):** the mirroring covers ONLY the allowlist resolution semantics (None/empty/populated meanings, first-`Some`-wins precedence) and the config plumbing (where the field lives, how each level parses it). It does NOT copy any LLM-facing skills machinery: skills feed instruction injection and tool-scope refresh into the model payload — macros have no analog of any of that. The LLM never learns macros exist or ran (an isolated macro's exchanges arrive as ordinary messages on a fork; a non-isolated macro's steps are indistinguishable from typed input). `enabled_macros` gets its own small resolver over the discovered-files set — it is NOT wired into `SkillPolicy`, prompt building, or context startup (see "Lazy resolution" below). - `None`/absent = "no opinion" → fall through to the next level; all-`None` → everything visible. - `Some([])` (empty list, incl. empty string via the parsers) = **explicit ZERO** — nothing enabled. Empty ≠ all; that was the regression. - Populated = exactly those names. **Deliberate DIVERGENCE from skills on unknown names**: skills hard-bail the whole resolution (skill_policy.rs:94-103 — there is no warn path). For macros, hard validation happens ONLY at `.set enabled_macros` time (the request_context.rs:2709-2727 pattern: bail on a name that exists in neither workspace nor global macros dir); CONFIG-FILE lists are validated gracefully at resolution time — warn + `missing` row in `.list macros`, never a bail (a stale name in a role file must not brick that role). There is no `visible_macros` concept in v1. - Precedence: `.or_else()` chain — session → agent → role → global, **first `Some` wins outright**, no merging. Matches the ruling below. - Plumbing gotcha: the global level is TWO structs (`Config` mod.rs:222 AND `AppConfig` app_config.rs:44/:212) plus an env-override arm (app_config.rs:532); role parses via frontmatter `parse_string_or_array` (role.rs:131), session via plain serde — three parse paths to mirror. - Global config (`config.example.yaml`, alongside `enabled_skills`): default when no role/agent/session is active. Absent/null = all macros visible. - Role (`config.role.example.md` frontmatter), agent config (`config.agent.example.yaml`), session config: allowlist for that context. - **Graph-based agents — CORRECTED BY VERIFICATION, then RULED (user, 2026-08-20): silently ignored, option (a).** The prior ruling ("graph configs reject the field") is not implementable as specified: coyote uses `deny_unknown_fields` nowhere except mcp/mod.rs:79, so unknown fields in graph.yaml are silently ignored — and `enabled_skills` is in fact SUPPORTED at graph level (`AgentConfig:: from_graph` copies it at agent.rs:811; graph/llm.rs:195-226 swaps per-node values with save/restore; validator enforces node⊆graph at graph/validator.rs:1156,:1235). Ruling: `enabled_macros` is simply omitted from the `Graph` struct — graph.yaml ignores it like any other unknown field (zero code, consistent behavior); the omission is documented in the wiki/agent docs. Full skills-style graph support was rejected as meaningless (graph nodes never dispatch REPL commands); a bespoke validator warning was considered and declined. - Precedence: most-specific active context that defines the field wins — session > agent > role > global. No merging/intersection. - Allowlist entries are macro **names** (the file stem — the same identifier used for invocation). ### Workspace-local macros (RULED: in scope) Workspace macro definitions are allowed, mirroring the existing workspace-artifact conventions (VERIFIED mechanics): - Discovery: `.coyote/macros/*.yaml` under `workspace_config_dir()` (paths.rs:198-207 — CWD only, no ancestor walk-up, dir name overridable via `COYOTE_WORKSPACE_CONFIG_DIR`), exactly like workspace skills (`workspace_skills_dir()`, paths.rs:209-215). - Collision rule: **workspace shadows global by name** — same as workspace skills (`list_skills` iterates [workspace, global] with shadowing, paths.rs:478-501; `has_skill` checks workspace first, :503-505) and workspace MCP (HashMap insert, workspace wins, mcp/mod.rs:239). - Opt-out mirrors MCP: new `--no-workspace-macros` CLI flag, modeled on `--no-workspace-mcp` (cli/mod.rs:96-98 → main.rs:222-223). IMPLEMENTATION-VERIFIED CORRECTION (T6): `no_workspace_mcp` is CLI-flag-only — AppConfig field exists but there is NO Config-struct key and NO env arm, so a config.yaml entry is non-functional. The exact-mirror ruling therefore makes `no_workspace_macros` CLI-flag-only too. (Pre-existing bug, out of scope: config.example.yaml:140 documents `no_workspace_mcp:` as a yaml key even though it does nothing — follow-up.) - Trust model: no confirmation prompt — consistent with workspace MCP and skills, which load with no gate (mcp/mod.rs:225-268 merely eprintlns); macros are strictly lower-risk since they run only on explicit user invocation, never automatically. `.list macros` gains a source column (`workspace` | `global`) so provenance is always visible. - `enabled_macros` existence validation consults workspace-then-global (the `has_skill` pattern). - The interactive creator (`.macro `) continues to write to the GLOBAL macros dir; workspace macros are authored by hand / committed to the repo. Remote installs (`--filter macros`) also target global only — bundles never write into a workspace. - Agent plumbing (VERIFIED): `AgentConfig` agent.rs:694-764 (getter/setter pattern :399-412), reached lazily from `RequestContext.agent` (request_context.rs:147, set in `use_agent` :3670+). Policy is resolved lazily at enforcement sites from `ctx.role/agent/session` — no activation-time snapshot exists, which confirms the lazy-resolution ruling fits the existing architecture exactly. - Session-level note: `enabled_skills` has NO REPL setter today (session file serde only). Runtime toggles do NOT touch session state — `.macro enable|disable` edits the global-level in-memory list via `update_app_config` (see §6), reusing the `.set` machinery. - Unknown names in a config-file list: warning + `missing` row in `.list macros` (per the divergence ruling above — resolution never bails). ### Lazy resolution `enabled_macros` never touches the LLM payload (macros are invisible to the model), so the visible set is computed on demand — at top-level dispatch fallback, completion, `.list macros`, `.macro enable|disable` — from: discovered files x active context's allowlist x runtime toggles. Zero context-startup work; no new startup-ordering surface. Rescan both macro dirs (workspace + global) on each resolution — two `read_dir`s; matches `paths::list_macros()` cost today. ## 6. REPL management surface - `.list macros` (enriched): name, description, isolated?, state: `enabled` | `disabled (runtime)` | `locked` | `missing` | `shadowed (built-in)` | `invalid`. `locked` names the restricting config (e.g. `agent:oracle enabled_macros`). Plus a source column (`workspace` | `global`). - **`.macro enable ` / `.macro disable `** (RULED — replaces the earlier `.command` proposal; user prefers no separate command at the cost of reserving two names): shorthand for editing `.set enabled_macros`. Consequently **`enable` and `disable` are reserved macro names**: the creator rejects them, discovery marks such a file `invalid` (with a warning), and remote installs warn. - **`.set enabled_macros `** — new key in the `.set` match (`RequestContext::update`, request_context.rs:2644-2929), mirroring `enabled_skills` (:2706-2730) verbatim: `csv_to_vec` parsing (space-free, comma-separated), `null` clears back to None, per-name existence validation (workspace-then-global), then `update_app_config(|app| app.enabled_macros = ...)`. - **Mechanism makes the no-override rule structural** (VERIFIED): `.set` writes the in-memory GLOBAL AppConfig level — the LOWEST-precedence rung of the session > agent > role > global chain — in-memory only, never persisted (`update_app_config` clone-and-swaps the Arc, request_context.rs:347-354). So `.macro enable|disable` physically cannot override a role/agent/session `enabled_macros` allowlist. When such an allowlist is active, the toggle ERRORS (naming the owning config) instead of silently writing a shadowed value. - Toggle semantics over the global-level list: `disable X` with list=None (all visible) materializes the list as all-discovered-minus-X; `enable X` appends if absent (None → already visible → no-op notice); `disable X` removes (absent → no-op notice). Lifetime: process runtime, like every other `.set` key. - Drive-by fix bundled here: `enabled_skills` is missing from the `.set` key completion list (request_context.rs:3018-3047) although its setter works — add both `enabled_skills` and `enabled_macros` to completion. States per macro per context: 1. permitted + enabled (default within allowlist) 2. permitted + disabled (runtime toggle) 3. locked (outside a ROLE/AGENT/SESSION allowlist) — not enableable from the REPL; the error says to edit `enabled_macros` in the owning config. No REPL override, ever: those configs stay the single source of truth. Boundary rule: `locked` applies ONLY to role/agent/session allowlists. A global-level exclusion (whether from config.yaml or a prior toggle — they occupy the same in-memory list and are indistinguishable) is always toggleable via `.macro enable` and displays as `disabled (runtime)`. ### Error handling matrix | Action | Condition | Behavior | |---|---|---| | `.name` | no built-in, no macro | existing error verbatim: `Error: Unknown command. Type ".help" for additional help.` | | `.name` / `.macro name` | locked | error naming the restricting context/config | | `.name` / `.macro name` | runtime-disabled | error: re-enable with `.macro enable ` | | `.macro enable X` | locked (more-specific allowlist active) | error: "restricted by ; edit `enabled_macros` there" | | `.macro enable X` | unknown | existing unknown-style error | | `.macro enable X` | already enabled | no-op notice | | `.macro create/creator` | name is `enable` or `disable` | error: reserved name | | discovery | file named `enable`/`disable`.yaml | `invalid` in `.list macros` + warning | | config `enabled_macros` | unknown name | warning + `missing` in `.list macros` | | discovery | name shadows built-in | works via `.macro` only; `shadowed` in `.list macros` | | discovery | workspace + global same name | workspace shadows global; both visible in `.list macros` source column | | non-isolated macro | step invokes another macro | error: nested macros not allowed in non-isolated mode | ## 7. Install & distribution Unchanged: `--install-from --filter macros` (CLI + REPL; flag rename to `--install` tracked in plans/bundle-manifest-design.md §5). Installing macros never modifies any `enabled_macros` list — a context with an allowlist is unaffected by new installs until the user edits it (the security property motivating context-side scoping). Existing overwrite/skip behavior applies; install-time warning if an installed macro's name shadows a built-in. ## 8. Docs - README + `.help`: "macros are coyote's custom commands" framing; top-level invocation; `isolated` semantics with the role-switch-persists warning. - `config.example.yaml`, `config.role.example.md`, `config.agent.example.yaml`: `enabled_macros` entries mirroring the existing `enabled_skills` doc comments. Do NOT add a `no_workspace_macros` yaml entry — the flag is CLI-only (see §5 correction); the existing `no_workspace_mcp` yaml line documents a dead key (pre-existing, follow-up). - New `macro.example.yaml` (or extend existing docs) showing all fields incl. description/isolated. - Workspace macros: document `.coyote/macros/` alongside the existing workspace skills/MCP conventions. ## 9. Implementation sketch 1. **Macro struct**: add `description`, `isolated` (serde defaults); deser tests for back-compat with field-less YAML. 2. **`enabled_macros` field**: global AppConfig + role + agent (non-graph) + session structs (both `Config` AND `AppConfig` at the global level + env arm), via `parse_string_or_array` like role.rs:131; graph.yaml silently ignores it (field omitted from the Graph struct, per §5 ruling); precedence resolver + tests (incl. the empty-list-means-zero regression case). 3. **Resolver**: discovered x allowlist x runtime toggles (global-level in-memory) → visible set + per-macro state; table-driven tests over the §6 matrix. OWNS the two-dir discovery (`workspace_macros_dir()` + shadowing, the has_skill/list_skills pattern) so steps 4/5/6 are genuinely independent. 4. **REPL dispatch**: macro fallback immediately before `unknown_command()`; `.macro enable|disable` + `.set enabled_macros` key (+ completion drive-by); enriched `.list macros`; dynamic macro completion with shadowed-name exclusion; `.macro ` arg completion upgraded to descriptions + subcommands (request_context.rs:3004); `.help`. Enforce visibility on the `.macro` path too. 5. **Non-isolated execution**: `macro_execute_inline(ctx, ...)` variant (or branch) running steps on the live ctx with RAII macro_flag guard + nested-macro rejection; tests for flag restore on error, plus two mock-free session tests (oracle note — no mock-client harness needed): (i) the non-isolated path passes the LIVE ctx (session `Some`) into `run_repl_command`, not a fork; (ii) a mutating step (`.model x`) persists on the live ctx after the macro returns. Also pin isolated→non-isolated nesting (§3). 6. **Workspace macros (flag + docs only; discovery lives in step 3)**: `--no-workspace-macros` flag + `no_workspace_macros` config key, source column in `.list macros`. 7. **Docs** (§8). Order: 1 → 2 → 3 → {4, 5, 6 in parallel} → 7. ## 10. Follow-ups (out of scope) - Persisted toggles (surviving restart) — `.set` state is process-lifetime by design; persistence would be new machinery for all `.set` keys, not just macros. - Bundle manifest & provenance layer — **separate design doc**, now written: `plans/bundle-manifest-design.md` (per-file provenance recording at install time, `--list-bundles` / `--update-bundle` / `--uninstall`, optional author-shipped `coyote-bundle.yaml` manifest). Once it exists, `.list macros` gains a "source bundle" column for free. This plan neither depends on nor blocks it. - "Did you mean" suggestions on unknown command (only if built-ins get it too). ## 11. VERIFY before task materialization All items resolved 2026-08-20 (findings folded into §3/§4/§5 above): - [x] `enabled_skills` semantics: None=fall-through, empty=ZERO, populated= exact+hard-bail validation; first-`Some`-wins precedence (`SkillPolicy::effective_with`). See §5. - [x] Agent config plumbing: lazy resolution from `ctx.agent`, no snapshot; new field = AgentConfig field + accessor + resolver read. See §5. - [x] `ReplCommand` registry: static fixed array, count-asserting test; macros go in a separate dynamic completer source. See §4. - [x] `macro_flag` guards: 13 sites inventoried; none suppress session recording; one OPEN DECISION (`use_agent` agent_session suppression) + nested-rejection is new code. See §3. - [x] Session recording: `after_chat_completion` → `save_message` → `Session::add_message`, no macro conditions; isolation lives in the fork's `session: None`. See §3. Both open decisions ruled by the user 2026-08-20: §3 `use_agent` suppression is conditioned on isolation (lifted for isolated:false); §5 graph.yaml silently ignores `enabled_macros`. No open questions remain. Tooling warning for implementers: `fs_grep`/plain grep tools silently skip src/config/request_context.rs (file-size exclusion) — audit that file with ast_grep or targeted reads only. ### Second verification round (2026-08-20, after user review) - [x] `.set` mechanics: fixed hand-written key match in `RequestContext::update` (request_context.rs:2644-2929); `enabled_skills` IS settable (:2706-2730) and always writes the in-memory global AppConfig via `update_app_config` (:347-354) — lowest precedence, never persisted, no role/agent/session setter. Values: `csv_to_vec` comma lists (space-free; whitespace rejected for all but two keys, :2654-2662), `null` clears. `enabled_skills` missing from `.set` completion (:3018-3047) — oversight, fixed as drive-by. - [x] Workspace discovery precedents: MCP = CWD-only probe `.coyote/mcp.json` → `.coyote/.mcp.json` → `.mcp.json` (paths.rs:217-230), workspace wins collisions via HashMap insert (mcp/mod.rs:239), gated by `no_workspace_mcp` only, no trust prompt; skills = `.coyote/skills/` shadowing global by name (paths.rs:478-505); memory walks ancestors but MCP/skills do NOT. Workspace macros copy the skills model + an MCP-style opt-out flag.