feat: add lazy macro resolver with two-dir discovery and per-macro states

Adds src/config/macro_policy.rs: MacroPolicy::effective computes the
visible macro set on demand from the discovered definition files, the
four-level enabled_macros allowlists, and the built-in command names.

- Discovery scans workspace (.coyote/macros/) then global macros dirs on
  every resolution; workspace shadows global by name, and the shadowed
  global entry is retained and flagged so both stay listable (plan
  custom-commands-design.md §5). Workspace scanning is gated on a bool
  parameter so the future --no-workspace-macros flag wires in one line.
- Allowlist precedence is session > agent > role > global, first Some
  wins, no merging; None falls through, an empty list is an explicit
  zero, all-None enables everything (mirrors SkillPolicy).
- Per-macro states per plan §6: enabled, disabled (runtime, global-level
  exclusions only), locked (role/agent/session exclusions, recording the
  owning level), missing (unknown allowlist names warn instead of
  bailing — deliberate divergence from skills), shadowed (built-in name
  collisions), and invalid (parse failures and the reserved names
  enable/disable). Invalid beats allowlist exclusion beats shadowing.
- Adds enabled_macros() accessors on Role, Session, and Agent alongside
  their enabled_skills() counterparts, plus paths::workspace_macros_dir.
- 37 tests: state matrix, pairwise precedence, explicit-zero pinned at
  every level, workspace shadowing, reserved names, builtin collisions,
  missing rows, invalid YAML, and env-gated discovery (#[serial]).
This commit is contained in:
2026-08-21 11:55:13 -06:00
parent f39381aa9d
commit e8ddb61518
6 changed files with 951 additions and 0 deletions
+4
View File
@@ -214,6 +214,10 @@ pub fn workspace_skill_file(name: &str) -> PathBuf {
workspace_skills_dir().join(name).join("SKILL.md")
}
pub fn workspace_macros_dir() -> PathBuf {
workspace_config_dir().join(MACROS_DIR_NAME)
}
pub fn workspace_mcp_config_file() -> Option<PathBuf> {
workspace_mcp_config_file_in(&env::current_dir().unwrap_or_default())
}