Skip to content

Migrate the CLI from argh to cw - #42

Merged
thorwhalen merged 1 commit into
mainfrom
migrate-argh-to-cw
Aug 30, 2026
Merged

thorwhalen merged 1 commit into
mainfrom
migrate-argh-to-cw

Conversation

@thorwhalen

Copy link
Copy Markdown
Owner

Closes #39.

argh is LGPL-3.0-or-later. cw is MIT, has no runtime dependencies, and reproduces argh's grammar by default — so this changes nothing a user sees.

The CLI surface: no diff

Recorded from the argh console script before any edit, replayed against cw after, with cw.testing:

19 characterized cases -> Counter({'identical': 19})
assert_replay(golden, strict_help=True)  ->  clean

strict_help=True asserts the full --help body, not just the usage: line — so a moved help column or a reflowed description would have failed. Nothing moved.

The 19 cases: no-args, top-level --help, --help for all eleven subcommands, and six error/edge vectors (estimate with no args, inventory --nosuchflag, scaffold, publish, plan, nosuchcommand).

python -m coact --help was checked separately and still prints usage: __main__.py — see "prog" below.

What changed

The three @argh.arg declarations became a config dict. This was the modern form, not the compat shim:

CONFIG = {
    "inventory": {"project": dict(nargs="?", default=".", help="Project root (default: current dir)")},
    "scaffold":  {"agents":  dict(nargs="+", help="Agent .md files / skill sources to wire")},
    "publish":   {"source":  dict(nargs="+", help="Tool refs ('module:function'), a skill dir/SKILL.md, or both")},
}

def main() -> None:
    raise SystemExit(cw.dispatch(COMMANDS, config=CONFIG))

The eleven verbs are now plain annotated functions with nothing CLI-specific attached — the CLI's particulars live in one table next to the command list instead of being scattered across three decorators.

estimate(*agents: str) needed no entry: VAR_POSITIONAL infers nargs='*' on its own, and the replay confirms the help is byte-identical.

prog is deliberately not passed. Hardcoding prog='coact' would have been an improvement to python -m coact --help (which currently reports usage: __main__.py) — and therefore a change to the surface this PR promised not to touch. Left to argparse.

raise SystemExit(...) is load-bearing. argh.dispatch_commands called sys.exit itself; cw.dispatch returns the code. Dropping the re-raise would silently turn every usage error into exit 0. There is now a test that runs main() and asserts SystemExit(2), so it cannot be dropped by accident.

The test that had to be rewritten

test_main_wires_every_verb monkeypatched cli.argh.dispatch_commands. That monkeypatch existed because argh genuinely could not be observed any other way — its output file is bound at import and its return value is discarded, so the only way to see what got registered was to replace the dispatcher.

cw builds a plain argparse.ArgumentParser, so the wiring is inspectable directly. The monkeypatch is gone, replaced by five real tests:

  • the parser's subcommand set is exactly the eleven documented verbs;
  • the four positional nargs shapes (? with default='.', +, +, *) asserted on the real actions, parametrized;
  • two end-to-end dispatches through cw.dispatch(..., out=buf) — including inventory with no positional, which is what nargs='?' is for;
  • the exit-code test above.

Net: the CLI is better covered after the migration than before, and none of the new coverage depends on the dispatcher's identity.

Also

  • pyproject.toml: argh -> cw.
  • The ai-assistant-command-mcp skill carried an explicit argh-migration stage 2 marker — "a house CLI adapter is being built ... this section will name it and show its idiom once it ships." It has shipped, so the marker is retired and Emitter C now shows the cw dispatch-table idiom.
  • Stale argh mentions updated in COACT_SPEC.md, REUSE.md, and the coact-dev skill. CHANGELOG.md left alone — it is history.

Tests

417 passed, 4 skipped.

argh is LGPL-3.0-or-later; cw is MIT with no runtime dependencies and
reproduces argh's grammar by default, so the command line does not move.

- `coact/__main__.py`: `import argh` -> `import cw`. The three `@argh.arg`
  declarations become a `CONFIG` dict keyed like `COMMANDS` (command name ->
  parameter name -> `add_argument` kwargs), so the verbs are now plain
  annotated functions with nothing CLI-specific on them.
- `main()` is `raise SystemExit(cw.dispatch(COMMANDS, config=CONFIG))`.
  `cw.dispatch` returns an exit code rather than exiting, so the re-raise is
  load-bearing: without it a usage error would exit 0.
- `prog` is deliberately left to argparse, preserving `python -m coact`'s
  current `usage: __main__.py` line.
- pyproject: `argh` -> `cw`.

Tests: `test_main_wires_every_verb` monkeypatched `cli.argh.dispatch_commands`
because argh could not be observed any other way (output file bound at import,
return value discarded). cw builds a plain `argparse.ArgumentParser`, so that
is replaced by real assertions: the subcommand set, the four positional nargs
shapes (`?`/`+`/`+`/`*`), two end-to-end dispatches, and an exit-code test that
runs `main()` itself.

Verified with `cw.testing`: 19 characterized cases (top-level help, all eleven
subcommand helps, six error/edge cases) recorded from the argh console script
and replayed against cw with `strict_help=True` -- 19/19 identical, help bodies
included.

Docs: named cw as the house CLI adapter in the ai-assistant-command-mcp skill,
retiring its `argh-migration stage 2` marker.
@thorwhalen
thorwhalen merged commit e0157b8 into main Aug 30, 2026
12 checks passed
@thorwhalen
thorwhalen deleted the migrate-argh-to-cw branch August 30, 2026 22:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ACCEPTANCE GATE: migrate t/coact to cw

1 participant