feat(cli): unify the CLI as noun-verb commands (#96) - #111
Conversation
Adds red coverage for sessions/tools/events as noun-verb commands, updates every existing CLI test (integration, e2e, snapshot, unit) to the new forms, and adds two new suites: create-cli.test.ts (a boolean flag ahead of a positional must not swallow it) and noun-verb-cli.integration.test.ts (a removed top-level word names its replacement; a bare or unknown verb names its noun's verbs; events ls stays reserved for #95). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
Every command is now appduct <noun> <verb> [selector] [args], the shape daemon run|start|stop|status already used: - routes/sessions/, routes/tools/ and routes/events/ are routers exactly like routes/daemon/, one route module per verb (ls, revoke, link; ls, describe, call; tail, since). - create-cli.ts's sessions/tools/events commands each declare every flag any of their verbs use, so cac's boolean/string flag table never lets a flag swallow a positional (tools ls --full <selector>). - dispatch.ts's root router drops ls, revoke, link and invoke entirely (no aliases, pre-1.0); a removed word or any other unmatched command is a usage error, and REMOVED_COMMANDS additionally names the replacement for the four that used to exist. - command-options.ts's splitOptionalSelectorAndTarget is deleted: the old ambiguity it resolved (is a single positional the selector, or the name with the selector omitted?) no longer exists once ls and describe are separate commands, so commands/tools.ts's handleToolsCommand drops the probing branch that used to guess between them. - events since <cursor> takes the cursor as a required positional (splitSelectorAndRequiredTarget), not the removed --since flag. - output.ts's renderSuccessData switches on the new command names, and its human hints (and commands/init.ts's) point at the new forms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
Every reference to a removed command (ls, revoke, link, tools <name>, invoke, events --since) now reads as its noun-verb replacement: the appduct package README, ARCHITECTURE.md/PROTOCOL.md/SECURITY.md/TOOLS.md, the react-native/native/playground READMEs, the website's CLI/agents/ install/quick-start pages, and the shipped skill (skills/appduct/). Adds the Unreleased changelog entry for the breaking rename. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
V3RON
left a comment
There was a problem hiding this comment.
Request changes (GitHub won't let this account request changes on its own PR, so this is posted as a comment): 2 blockers, 2 should-fix. The spec is issue #96's rewritten body; the first ## Design comment is superseded.
Fix first: the website landing page still shows appduct invoke, which now exits 64.
Two findings are in files this PR doesn't touch, so they have no inline comment:
- Blocker (criterion 6):
website/src/pages/index.astro:21(invokeCode = "appduct invoke seed_cart ...") andwebsite/src/components/Hero.astro:65(appduct invoke) still use the removed form. A visitor who copies the homepage example gets exit 64. Switch both toappduct tools call. - Should-fix:
.claude/skills/e2e-device/SKILL.mdlines 46, 47, 60, 75-81 and 108 runpnpm playground:appduct -- link|ls|invoke. The pending E2E stage for this PR follows that skill, so its pairing loop and every smoke step will exit 64. Update it tosessions link,sessions lsandtools call.
Generated by Claude Code
sessions/tools/events --help exited 0 without naming any verb: cac's per-command help never prints a command's description, only usage and flags. Red for that reason. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
sessions/tools/events --help exited 0 but never named a verb, since cac's per-command help shows usage and flags, never a command's description. Each noun now sets a `.usage(...)` naming its verbs, so `appduct events --help` names `since` and `appduct tools --help` names `describe`, matching criterion 3 as the issue states it. 1 failing -> 0 failing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
The landing page's terminal code sample and the hero diagram's caller label still read "appduct invoke", which now exits 64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
The Unreleased entry said "tools" was gone, which was false — only its bare and "tools <name>" forms went — and it didn't say what any old form became. Rewritten as explicit pairs for ls, revoke, link, tools, tools <name>, invoke, events and events --since <cursor>. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
e2e-device's smoke pass and connect step still used link, ls and invoke, the forms the E2E stage on this PR would have run against. writing-changelog's tool-groups example still showed the old `tools --group` form. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
V3RON
left a comment
There was a problem hiding this comment.
Verdict: request-changes (round 2). 1 blocker, 0 should-fix.
Spec: issue #96 body. All four round-1 findings are fixed; I checked the <noun> --help output against the built CLI.
Fix first: the website playground page, playground/README.md and the playground app's on-screen hints still use playground:appduct -- link/ls/tools/invoke/events --follow, and every one of those now exits 64 (criterion 6).
Generated by Claude Code
Round-2 review found the sweep missed `playground:appduct -- <verb>` forms: website/start/playground.mdx, playground/README.md and the playground app's on-screen hints still told readers to run `link`, `ls`, `tools`, `invoke` and `events --follow`, each exiting 64. Also fixed the same stale bare forms turned up by a wider grep in the native playgrounds (Info.plist, PlaygroundTools.swift, PlaygroundApplication.kt) and a comment in command-options.ts that described `events --since` as a flag after it became the `<cursor>` positional. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
V3RON
left a comment
There was a problem hiding this comment.
Verdict: request changes. 2 blockers, 0 should-fix, 1 nit. The round-2 blocker is fixed.
Spec: issue #96 body; the first ## Design comment is superseded.
Fix first: the appduct events since cursor hint in output.ts:754 still says pass --since <cursor>, a removed flag.
Generated by Claude Code
output.test.ts pinned the removed --since flag in the human-mode cursor line; expect the noun-verb command instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
The human-mode trailing cursor line named the removed --since flag. It now names appduct events since <cursor>, which actually resumes. 1 failing -> 0 failing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
) README.md and ARCHITECTURE.md still named the removed `invoke` command outside the earlier diff. Also swept source comments, test titles and docblocks repo-wide for the same staleness: `invoke`, bare `ls`/`link`/ `revoke`, `tools <name>`/`tools [selector]`, and `--since` as an example flag, replacing each with its noun-verb equivalent. No behavior change; comments, test names, a stale snapshot key and doc prose only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
V3RON
left a comment
There was a problem hiding this comment.
Verdict: approve. 0 blockers, 0 should-fix, 2 nits. Ready for the device test. The spec is issue #96's body.
All three round-3 fixes hold. On the built CLI, events since now prints run "appduct events since N". A repo-wide search, including commands split across line breaks, finds one removed form left: appduct link at docs/ARCHITECTURE.md:578-579. Fix that first, before merge.
Generated by Claude Code
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
1 failing -> 0 failing Copying the hint after `appduct events since pixel-8 0` dropped the device selector, so with two devices connected the suggested command resolved to ambiguous_session instead of resuming. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
The removed `appduct link` form survived the noun-verb sweep because it wrapped across two lines, with "appduct" ending one line and "link" starting the next. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
Resolves the .agents/memory/INBOX.md conflict by keeping main's notes and appending this PR's note. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt
Closes #96
What changed
Every CLI command is now
appduct <noun> <verb> [selector] [args], the shapedaemon run|start|stop|statusalready used:sessions ls|revoke|linkreplacesls,revokeandlink.tools ls|describe|callreplacestools,tools <name>andinvoke.events tail|since <cursor>replaceseventsandevents --since.This is a clean break with no aliases, since we are pre-1.0. A removed top-level word, or a missing or unrecognized verb, is a usage error that names the fix. Every place that documented or hinted at an old form now uses only the new ones: the shipped skill, the READMEs, the website,
docs/, and the CLI's own output hints.Acceptance criteria
--jsonas the command it replaces; SIGINT still cancelstools call;events sincestill prints the trailing cursor linecli-v2.integration.test.ts,events.integration.test.ts,exit-codes.integration.test.ts,e2e/invoke-cancel.e2e.test.tsls,revoke,linkandinvokeexit 64 naming their replacementnoun-verb-cli.integration.test.ts("removed top-level commands...")sessions/tools/eventsexits 64 naming that noun's verbs, likedaemon;--helpon the noun itself names every verb (via.usage(...)), and so does the global--helpnoun-verb-cli.integration.test.ts("a bare noun with no verb...")events lsexits 64 namingtailandsinceuntil #95 landsnoun-verb-cli.integration.test.ts("events ls is reserved for #95...")tools ls --full <selector>,sessions link --qr --open ios-sim) doesn't swallow itcreate-cli.test.tswebsite/,docs/(includingARCHITECTURE.md§10) and the CLI's own hints (output.ts,commands/init.ts) use only the new forms; the CLI integration tests run against themcli.integration.test.ts,output.test.ts(hint text), and each listed doc checked by hand withpnpm check:linksCHANGELOG.mdunder## UnreleasedE2E evidence
Not run; waived by the maintainer (decision on #96). The iOS row the
e2e-deviceskill requires couldn't run in the cloud container, which has no Xcode, no simulator and no KVM.The change is CLI routing, output hints and docs only. The RPC, the daemon and the app side are untouched. The CLI integration and e2e suites cover the new commands: 69 files in
appduct, all green on the merged head.Checklist
CHANGELOG.mdhas an entry underUnreleasedindex.ts; no new directnode:*I/O outside an adapterarchitectureskill applied (see below)docs/ARCHITECTURE.mdupdated (§10, plus the §9 MCP cross-references)Simplification notes:
command-options.ts'ssplitOptionalSelectorAndTargetis deleted. It decided whether a single positional was a selector or a tool name with the selector omitted, and that can't happen oncetools lsandtools describeare separate commands.commands/tools.ts'shandleToolsCommanddrops the branch that used to guess between them.tools/shared-options.tsparses the listing flags for bothtools lsandtools describe, so it has the two callers the "simplest thing" rule asks for.Out of scope
sessions describe,--nameonevents tail/since(#94), and a workingevents ls(#95) are all non-goals in the issue. Nothing else came up in passing.Status
Implement: done (7/7 green) Review: round 4, approve (both nits fixed) E2E: waived (no macOS host; maintainer decision on #96) Ready: yes
🤖 Generated with Claude Code
https://claude.ai/code/session_014wk7yigkPHWoWLy9nvocyt