feat: surface macros as first-class custom commands in the REPL

Implements the invocation and management surfaces from
plans/custom-commands-design.md §4 and §6:

- Top-level dispatch: an enabled macro <name> now runs as ".<name> [args]"
  from the command catch-all; runtime-disabled macros point at
  ".macro enable <name>", locked macros name the owning config, and
  unknown commands keep the existing error verbatim
- .macro enable|disable <name>: runtime toggles over the in-memory
  global-level enabled_macros list (disable with no list materializes
  all-active-minus-name); toggles error when a role/agent/session
  allowlist owns the field
- .set enabled_macros <csv|null> with workspace-then-global existence
  validation; .set key completion gains enabled_macros and the
  previously missing enabled_skills
- Dynamic completion: enabled macros (with descriptions) join built-ins
  on ".<TAB>" without touching the static command registry;
  ".macro <TAB>" lists invocable macros (incl. built-in-shadowed ones)
  plus the enable/disable subcommands; second-arg completion offers
  toggle-eligible names
- .list macros: enriched table (name, source, isolated, state,
  description) covering every resolver state incl. missing and
  shadowed rows; .help gains a custom-commands section
- Session info/render and sysinfo display enabled_macros; Macro::load
  resolves workspace-then-global; enable/disable rejected as macro
  names in the creator
This commit is contained in:
2026-08-21 11:55:13 -06:00
parent e8ddb61518
commit e94bd450cd
8 changed files with 537 additions and 53 deletions
+18 -1
View File
@@ -74,7 +74,24 @@ impl Completer for ReplCompleter {
format!("{name} ")
};
create_suggestion(&name, description, span)
}))
}));
let macros: Vec<(String, Option<String>)> = ctx
.visible_macro_completions()
.into_iter()
.map(|(name, description)| (format!(".{name}"), description))
.filter(|(name, _)| {
command_filter.len() == 1 || name.starts_with(&command_filter[..2])
})
.collect();
let macros = fuzzy_filter(macros, |(name, _)| name.as_str(), &command_filter);
suggestions.extend(macros.iter().map(|(name, description)| {
create_suggestion(
&format!("{name} "),
description.as_deref().unwrap_or_default(),
span,
)
}));
}
suggestions
}