feat: Custom command aliases via [aliases] TOML config (F2.2) #25

Open
opened 2026-04-25 04:01:43 +02:00 by product-owner · 0 comments

Problem Statement

Spec F2.2 (terraphim-agent-session-search-spec.md, lines 167-184) requires both built-in AND user-defined custom command aliases:

[aliases]
ss = "sessions search"
si = "sessions import"

Built-in aliases ship today via DEFAULT_ALIASES in crates/terraphim_agent/src/forgiving/aliases.rs:8, and #4 wires ForgivingParser into REPL dispatch. But there is no path to load the [aliases] TOML section into AliasRegistry. AliasRegistry::add() exists but no caller ever populates it from config, so users cannot define ss = "sessions search" -- the headline F2.2 example.

This blocks the "make CLI usable by AI agents and power users" goal (G2). Custom aliases also enable multi-token expansions (ss -> sessions search) that the current expand() only partially supports (it returns &str for a single token; multi-word aliases need different handling).

Acceptance Criteria

  • Define AliasConfig in terraphim_agent/src/forgiving/aliases.rs:
    #[derive(Debug, Clone, Default, serde::Deserialize)]
    pub struct AliasConfig { pub aliases: std::collections::HashMap<String, String> }
    
  • Add AliasRegistry::load_from_toml(path: &Path) -> Result<Self, AliasError> that:
    • Reads TOML, deserialises [aliases] table
    • Starts from DEFAULT_ALIASES, merges user values (user wins on conflict)
    • Validates each alias key is a single token (no whitespace, no leading /); rejects with AliasError::InvalidKey { alias, reason }
    • Validates each alias value is non-empty
  • Multi-token expansion: when an alias maps to e.g. "sessions search", the parser must split into ["sessions", "search", ...rest_of_args]; update ForgivingParser::expand_alias accordingly with a unit test for /ss "auth" -> sessions search "auth"
  • Config file location: load from <terraphim_data_dir>/agent.toml if present; absence is non-fatal (DEFAULT_ALIASES still load). Document the path in module-level rustdoc.
  • Conflict logging: when a user alias overrides a default, log via log::info!("alias override: {} (default '{}' -> user '{}')", k, default_v, user_v)
  • AliasError enum with thiserror: Io, TomlParse, InvalidKey, InvalidValue
  • Wire AliasRegistry::load_from_toml into REPL startup (ReplHandler::new or builder), behind a try_load that falls back to AliasRegistry::new() on any error with a warn-level log
  • Robot mode capabilities output exposes the loaded alias count: "alias_count": 17
  • Unit tests: load valid TOML, load malformed TOML, missing file, invalid key (with whitespace), conflict with default, multi-token expansion roundtrip
  • cargo test -p terraphim_agent --lib forgiving::aliases::tests passes
  • cargo clippy -p terraphim_agent -- -D warnings passes

Proposed Approach

  • Crates touched: terraphim_agent (forgiving/aliases.rs, forgiving/parser.rs, repl/handler.rs, robot/docs.rs)
  • Reuse existing toml and serde deps -- no new crates
  • expand_alias returns Option<Vec<String>> (tokens) instead of Option<&str> to support multi-word; old single-word callers wrap with .first()
  • Pair with #4 (which wires the parser into REPL) so this issue completes F2.2 fully

Dependencies

None as a hard blocker. Lands cleanest after #4 (ForgivingParser into REPL dispatch); without #4 the multi-token expansion won't be observable from the REPL but the loader and unit tests still verify behaviour. Recommend implementation-swarm picks this after #4.

Verification

cargo test -p terraphim_agent --lib forgiving::aliases
cargo test -p terraphim_agent --lib forgiving::parser::tests::expand_multi_token
cargo clippy -p terraphim_agent -- -D warnings
echo '[aliases]\nss = "sessions search"\nsi = "sessions import"' > /tmp/agent.toml
TERRAPHIM_DATA_DIR=/tmp ./target/debug/terraphim-agent --robot capabilities | jq '.data.alias_count'

Scope

Single PR, ~250-320 lines (loader ~80, parser tweak ~40, error type ~20, REPL wiring ~30, robot capabilities ~10, tests ~100). Closes spec F2.2 fully alongside #4.

## Problem Statement Spec F2.2 (terraphim-agent-session-search-spec.md, lines 167-184) requires both built-in AND user-defined custom command aliases: ```toml [aliases] ss = "sessions search" si = "sessions import" ``` Built-in aliases ship today via `DEFAULT_ALIASES` in `crates/terraphim_agent/src/forgiving/aliases.rs:8`, and #4 wires `ForgivingParser` into REPL dispatch. But there is no path to load the `[aliases]` TOML section into `AliasRegistry`. `AliasRegistry::add()` exists but no caller ever populates it from config, so users cannot define `ss = "sessions search"` -- the headline F2.2 example. This blocks the "make CLI usable by AI agents and power users" goal (G2). Custom aliases also enable multi-token expansions (`ss` -> `sessions search`) that the current `expand()` only partially supports (it returns `&str` for a single token; multi-word aliases need different handling). ## Acceptance Criteria - [ ] Define `AliasConfig` in `terraphim_agent/src/forgiving/aliases.rs`: ```rust #[derive(Debug, Clone, Default, serde::Deserialize)] pub struct AliasConfig { pub aliases: std::collections::HashMap<String, String> } ``` - [ ] Add `AliasRegistry::load_from_toml(path: &Path) -> Result<Self, AliasError>` that: - Reads TOML, deserialises `[aliases]` table - Starts from `DEFAULT_ALIASES`, merges user values (user wins on conflict) - Validates each alias key is a single token (no whitespace, no leading `/`); rejects with `AliasError::InvalidKey { alias, reason }` - Validates each alias value is non-empty - [ ] Multi-token expansion: when an alias maps to e.g. `"sessions search"`, the parser must split into `["sessions", "search", ...rest_of_args]`; update `ForgivingParser::expand_alias` accordingly with a unit test for `/ss "auth"` -> `sessions search "auth"` - [ ] Config file location: load from `<terraphim_data_dir>/agent.toml` if present; absence is non-fatal (DEFAULT_ALIASES still load). Document the path in module-level rustdoc. - [ ] Conflict logging: when a user alias overrides a default, log via `log::info!("alias override: {} (default '{}' -> user '{}')", k, default_v, user_v)` - [ ] `AliasError` enum with `thiserror`: `Io`, `TomlParse`, `InvalidKey`, `InvalidValue` - [ ] Wire `AliasRegistry::load_from_toml` into REPL startup (`ReplHandler::new` or builder), behind a `try_load` that falls back to `AliasRegistry::new()` on any error with a warn-level log - [ ] Robot mode `capabilities` output exposes the loaded alias count: `"alias_count": 17` - [ ] Unit tests: load valid TOML, load malformed TOML, missing file, invalid key (with whitespace), conflict with default, multi-token expansion roundtrip - [ ] `cargo test -p terraphim_agent --lib forgiving::aliases::tests` passes - [ ] `cargo clippy -p terraphim_agent -- -D warnings` passes ## Proposed Approach - Crates touched: `terraphim_agent` (`forgiving/aliases.rs`, `forgiving/parser.rs`, `repl/handler.rs`, `robot/docs.rs`) - Reuse existing `toml` and `serde` deps -- no new crates - `expand_alias` returns `Option<Vec<String>>` (tokens) instead of `Option<&str>` to support multi-word; old single-word callers wrap with `.first()` - Pair with #4 (which wires the parser into REPL) so this issue completes F2.2 fully ## Dependencies None as a hard blocker. Lands cleanest after #4 (ForgivingParser into REPL dispatch); without #4 the multi-token expansion won't be observable from the REPL but the loader and unit tests still verify behaviour. Recommend implementation-swarm picks this after #4. ## Verification ``` cargo test -p terraphim_agent --lib forgiving::aliases cargo test -p terraphim_agent --lib forgiving::parser::tests::expand_multi_token cargo clippy -p terraphim_agent -- -D warnings echo '[aliases]\nss = "sessions search"\nsi = "sessions import"' > /tmp/agent.toml TERRAPHIM_DATA_DIR=/tmp ./target/debug/terraphim-agent --robot capabilities | jq '.data.alias_count' ``` ## Scope Single PR, ~250-320 lines (loader ~80, parser tweak ~40, error type ~20, REPL wiring ~30, robot capabilities ~10, tests ~100). Closes spec F2.2 fully alongside #4.
product-owner added a new dependency 2026-04-25 04:01:47 +02:00
Sign in to join this conversation.