Design doc for macros as first-class custom commands: top-level .name invocation, description/isolated fields, enabled_macros scoping, workspace-local macros, completion integration. Gatekeeper: sealed (3 findings fixed). Oracle plan-review: approved.
29 KiB
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 <url> --filter macrosalready 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--installper 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
.macrosubcommand 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
restvariable 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); analiasfield (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<MacroVariable{name, rest, default}>, steps: Vec<String> }, YAML atmacros_dir()/<name>.yaml(global only; no workspace dir, unlike skills)..macro <name> [args]executes; nonexistent name + no args opens the interactive macro creator (ctx.new_macro).macro_executeforks a freshRequestContextfrom the current role (inherits role's model/temperature/enabled_tools/enabled_mcp_servers), copieslast_messagein (discontinuous), setsmacro_flag, runs each{{var}}-interpolated step viarun_repl_command. No state flows back: the macro's exchanges are not recorded in the active session and do not update the caller'slast_message.- Auto-generated usage strings; positional vars with defaults + rest-capture.
.list macros,.delete macro, built-ins embedded fromassets/macros/.- Unknown dot-command →
unknown_command()at repl/mod.rs:1521:Error: Unknown command. Type ".help" for additional help.
2. Gaps this design closes
- Top-level invocation:
.review-work args, not just.macro review-work args. - Discoverability:
descriptionfield → listings, completion,.help. - Context scoping:
enabled_macroson role / agent (non-graph) / session / global config, mirroringenabled_skills. - Runtime toggles:
.macro enable|disable <name>, shorthand for.set enabled_macros(see §6). - Conversation-integrated macros:
isolated: false. - Workspace-local macros:
.coyote/macros/shadowing global, mirroring workspace skills/MCP conventions (see §5).
3. New Macro fields
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 viarun_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_messageupdated, 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_completionrequest_context.rs:1528-1544 →save_message:1464-1474 →Session::add_messagesession.rs:724-763; disk save deferred toSession::exit:626-631). Today's "macros aren't recorded" behavior comes ENTIRELY from the fork (session: Nonein 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.mdfile via thesave_messagefallthrough (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_flagis 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 whenmacro_flagis 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_flagwhile&mut ctxis passed torun_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 changingmacro_flagtoOption<MacroMode>(the enum touches all 13 sites incl. both fork-propagation sites; the companion field is the smaller diff); (c) a separatemacro_execute_inlinefn creates a new type-level recursion cycle withrun_repl_commandand needs its own#[async_recursion]/boxing — a branch inside the already-boxedmacro_executeis 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.exitstep's exit signal is swallowed inside macros today (bool discarded at macros.rs:67) — unchanged, but say so. - VERIFIED guard inventory (13
macro_flagsites: 4 decl/init, 2 propagation —fork_for_branchrequest_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):
.updatebail (repl/mod.rs:890),.editbail (:979 — no$EDITORmid-macro), blank-line suppression (:1310, cosmetic),new_roleprompt→bail (request_context.rs:2259),new_macroprompt→bail (:2360). - Non-issue:
apply_preludeskip (:3966) — on a live REPL ctx the prelude already ran andstate()is non-empty. - RULED (user, 2026-08-20): condition on isolation.
use_agent(:3743-3749) suppresses the agent's defaultagent_sessionwhenmacro_flagis set; underisolated: falsethe suppression is LIFTED — a non-isolated.agent foostep inherits foo's default session exactly as if typed. Mechanism: the RAII guard records the mode (companion field orOption<MacroMode>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_macroonly blocks the interactive creator) — the non-isolated nesting rejection is NEW code, not a reuse of an existing check.
- Keep as-is (desirable in a macro even on the live ctx):
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 <name> <rest>would (honoringisolated). Miss → existingunknown_command()error, verbatim, unchanged. VERIFIED insertion point: the top-level catch-all_ => unknown_command()?at repl/mod.rs:1297. The otherunknown_command()call sites (:630, :740, :763, :1236, :1259) are sub-argument mismatches inside known commands and must NOT dispatch macros. .macrosubcommand is kept verbatim for back-compat, including the interactive-creation flow (creation stays ONLY under.macro; a top-level typo like.himust error, never open the creator).- Both entry points (
.nameand.macro name) go through the same §5 visibility check — otherwise.macrotrivially bypassesenabled_macros. - Collision rules:
- Macro name colliding with a built-in command: built-in always
wins; macro still invokable via
.macro <name>; flaggedshadowed (built-in)in.list macros. Built-in list sourced from the existingReplCommandregistry, 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).
- Macro name colliding with a built-in command: built-in always
wins; macro still invokable via
- Tab completion: on
.<TAB>, visible macros appear alongside the usual built-ins, with theirdescriptionshown 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 holdsArc<RwLock<RequestContext>>(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.<name>(built-in always wins) nor listed in.<TAB>completions — it surfaces only in.list macrosasshadowed (built-in)and stays invokable via.macro <name>. .macro <TAB>argument completion (RULED): upgraded to match.<TAB>presentation — macro names WITH descriptions when available. VERIFIED today: request_context.rs:3004 completes.macroargs viamap_completion_values(paths::list_macros())(names only, no descriptions), while the plumbing already supports described suggestions (repl_completereturns(String, Option<String>);.model/.agentarms use it, rendered at completer.rs:58-60). Change the.macroarm to the same resolver source as top-level completion. Differences from.<TAB>: shadowed macros ARE listed here (.macrois their escape hatch), and theenable/disablesubcommands appear alongside macro names. Second-arg completion:.macro enable <TAB>/.macro disable <TAB>complete toggle-eligible macro names.- Completion entries for macros carry the same
AssertStatestance as the.macrobuilt-in (the completer filters oncmd.is_valid(state), completer.rs:46). enable/disableare sub-args of the existing.macroentry, NOT newREPL_COMMANDSarray entries (the count-asserting test at repl/mod.rs:1732 stays untouched)..helpgains 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_macrostime (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 +missingrow in.list macros, never a bail (a stale name in a role file must not brick that role). There is novisible_macrosconcept in v1. -
Precedence:
.or_else()chain — session → agent → role → global, firstSomewins outright, no merging. Matches the ruling below. -
Plumbing gotcha: the global level is TWO structs (
Configmod.rs:222 ANDAppConfigapp_config.rs:44/:212) plus an env-override arm (app_config.rs:532); role parses via frontmatterparse_string_or_array(role.rs:131), session via plain serde — three parse paths to mirror. -
Global config (
config.example.yaml, alongsideenabled_skills): default when no role/agent/session is active. Absent/null = all macros visible. -
Role (
config.role.example.mdfrontmatter), 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_fieldsnowhere except mcp/mod.rs:79, so unknown fields in graph.yaml are silently ignored — andenabled_skillsis in fact SUPPORTED at graph level (AgentConfig:: from_graphcopies 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_macrosis simply omitted from theGraphstruct — 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/*.yamlunderworkspace_config_dir()(paths.rs:198-207 — CWD only, no ancestor walk-up, dir name overridable viaCOYOTE_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_skillsiterates [workspace, global] with shadowing, paths.rs:478-501;has_skillchecks workspace first, :503-505) and workspace MCP (HashMap insert, workspace wins, mcp/mod.rs:239). - Opt-out mirrors MCP: new
--no-workspace-macrosCLI flag +no_workspace_macrosconfig key (default false), modeled on--no-workspace-mcp(cli/mod.rs:96-98 → main.rs:222-223, app_config.rs:98). - 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 macrosgains a source column (workspace|global) so provenance is always visible. enabled_macrosexistence validation consults workspace-then-global (thehas_skillpattern).- The interactive creator (
.macro <new-name>) 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):
AgentConfigagent.rs:694-764 (getter/setter pattern :399-412), reached lazily fromRequestContext.agent(request_context.rs:147, set inuse_agent:3670+). Policy is resolved lazily at enforcement sites fromctx.role/agent/session— no activation-time snapshot exists, which confirms the lazy-resolution ruling fits the existing architecture exactly. - Session-level note:
enabled_skillshas NO REPL setter today (session file serde only). Runtime toggles do NOT touch session state —.macro enable|disableedits the global-level in-memory list viaupdate_app_config(see §6), reusing the.setmachinery. - Unknown names in a config-file list: warning +
missingrow 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_dirs; 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.lockednames the restricting config (e.g.agent:oracle enabled_macros). Plus a source column (workspace|global)..macro enable <name>/.macro disable <name>(RULED — replaces the earlier.commandproposal; user prefers no separate command at the cost of reserving two names): shorthand for editing.set enabled_macros. Consequentlyenableanddisableare reserved macro names: the creator rejects them, discovery marks such a fileinvalid(with a warning), and remote installs warn..set enabled_macros <csv|null>— new key in the.setmatch (RequestContext::update, request_context.rs:2644-2929), mirroringenabled_skills(:2706-2730) verbatim:csv_to_vecparsing (space-free, comma-separated),nullclears back to None, per-name existence validation (workspace-then-global), thenupdate_app_config(|app| app.enabled_macros = ...).- Mechanism makes the no-override rule structural (VERIFIED):
.setwrites the in-memory GLOBAL AppConfig level — the LOWEST-precedence rung of the session > agent > role > global chain — in-memory only, never persisted (update_app_configclone-and-swaps the Arc, request_context.rs:347-354). So.macro enable|disablephysically cannot override a role/agent/sessionenabled_macrosallowlist. 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 Xwith list=None (all visible) materializes the list as all-discovered-minus-X;enable Xappends if absent (None → already visible → no-op notice);disable Xremoves (absent → no-op notice). Lifetime: process runtime, like every other.setkey. - Drive-by fix bundled here:
enabled_skillsis missing from the.setkey completion list (request_context.rs:3018-3047) although its setter works — add bothenabled_skillsandenabled_macrosto completion.
States per macro per context:
- permitted + enabled (default within allowlist)
- permitted + disabled (runtime toggle)
- locked (outside a ROLE/AGENT/SESSION allowlist) — not enableable from the
REPL; the error says to edit
enabled_macrosin 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 <name> |
.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;isolatedsemantics with the role-switch-persists warning. config.example.yaml,config.role.example.md,config.agent.example.yaml:enabled_macrosentries mirroring the existingenabled_skillsdoc comments;no_workspace_macrosentry alongside the existingno_workspace_mcpone (config.example.yaml:140-145).- 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
- Macro struct: add
description,isolated(serde defaults); deser tests for back-compat with field-less YAML. enabled_macrosfield: global AppConfig + role + agent (non-graph) + session structs (bothConfigANDAppConfigat the global level + env arm), viaparse_string_or_arraylike 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).- 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. - REPL dispatch: macro fallback immediately before
unknown_command();.macro enable|disable+.set enabled_macroskey (+ completion drive-by); enriched.list macros; dynamic macro completion with shadowed-name exclusion;.macro <TAB>arg completion upgraded to descriptions + subcommands (request_context.rs:3004);.help. Enforce visibility on the.macropath too. - 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 (sessionSome) intorun_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). - Workspace macros (flag + docs only; discovery lives in step 3):
--no-workspace-macrosflag +no_workspace_macrosconfig key, source column in.list macros. - Docs (§8).
Order: 1 → 2 → 3 → {4, 5, 6 in parallel} → 7.
10. Follow-ups (out of scope)
- Persisted toggles (surviving restart) —
.setstate is process-lifetime by design; persistence would be new machinery for all.setkeys, 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-shippedcoyote-bundle.yamlmanifest). Once it exists,.list macrosgains 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):
enabled_skillssemantics: None=fall-through, empty=ZERO, populated= exact+hard-bail validation; first-Some-wins precedence (SkillPolicy::effective_with). See §5.- Agent config plumbing: lazy resolution from
ctx.agent, no snapshot; new field = AgentConfig field + accessor + resolver read. See §5. ReplCommandregistry: static fixed array, count-asserting test; macros go in a separate dynamic completer source. See §4.macro_flagguards: 13 sites inventoried; none suppress session recording; one OPEN DECISION (use_agentagent_session suppression) + nested-rejection is new code. See §3.- Session recording:
after_chat_completion→save_message→Session::add_message, no macro conditions; isolation lives in the fork'ssession: 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)
.setmechanics: fixed hand-written key match inRequestContext::update(request_context.rs:2644-2929);enabled_skillsIS settable (:2706-2730) and always writes the in-memory global AppConfig viaupdate_app_config(:347-354) — lowest precedence, never persisted, no role/agent/session setter. Values:csv_to_veccomma lists (space-free; whitespace rejected for all but two keys, :2654-2662),nullclears.enabled_skillsmissing from.setcompletion (:3018-3047) — oversight, fixed as drive-by.- 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 byno_workspace_mcponly, 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.