Decisions
ADR-006 — The limits of cac, and loading the TUI lazily
Two limits found in practice, and why the interactive board must never be in the fast path.
Date: 2026-09-03 · Status: accepted; amends ADR-004 · Scope: stack
Context
ADR-004 chose cac for its 3.3 ms import and predicted one risk: abandonment.
Practice produced two different ones, and both cost time.
cacdoes not match multi-word commands.cli.command('task add <title>')shows up in help, but the action never fires. Only found by running the binary by hand: the tests called command functions directly and saw nothing wrong.cactreats any value with a leading-as a flag.--estimate -5andtask log KAD-1 -30mnever reach our code; they fail withUnknown option.
Separately: a TUI on blessed costs ~28 ms to import, and Ink costs 146 ms out of a 200 ms budget.
Options considered
| Option | For | Against |
|---|---|---|
Move to citty | Supports nested commands | +5 ms; rewriting the whole CLI layer for two workarounds |
| Own parser | Full control | --help, subcommands and error messages are permanent debt |
Keep cac, work around both limits | Zero migration; the workarounds are local and documented | Two places in the code you have to remember |
Decision
Keep cac, with two explicit workarounds:
- Commands are declared as
task <action>and the action is dispatched by aswitch. This also produced better messages: on a missing argumentcacsays "missing required args", which tells a user nothing. - Arguments with a leading
-are intercepted beforecli.parse(). For--estimatethat is an error with an explanation; fortask logit is a legitimate negative value.
The TUI is loaded dynamically and stays out of the main bundle. The 200 ms guardrail is about commands run dozens of times a day; an interactive session starts once and lives for minutes.
Consequences
- Zero migration, and
dist/cli.jsstayed 30 KB with no trace of blessed in it — CI greps for that on every push. - Two non-obvious places in
src/cli/index.ts, both with a comment. A new contributor will not guess them. - What would make us revisit it: a third
caclimit stops being a workaround and becomes a mismatched tool, and thencittyis justified.