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
+7
View File
@@ -246,6 +246,9 @@ impl Session {
if let Some(enabled_skills) = self.enabled_skills() {
data["enabled_skills"] = json!(enabled_skills);
}
if let Some(enabled_macros) = self.enabled_macros() {
data["enabled_macros"] = json!(enabled_macros);
}
if let Some(save_session) = self.save_session() {
data["save_session"] = save_session.into();
}
@@ -325,6 +328,10 @@ impl Session {
items.push(("enabled_skills", enabled_skills.join(",")));
}
if let Some(enabled_macros) = self.enabled_macros() {
items.push(("enabled_macros", enabled_macros.join(",")));
}
if let Some(save_session) = self.save_session() {
items.push(("save_session", save_session.to_string()));
}