CLI Substrate — design principles
Audience: anyone adding, removing, or renaming a CLI command in runex. Goal: every change keeps
runex --helpworking as a navigation index for an agent or workflow automator who already knows what an ontology is, and just needs to find the right sub-cmd fast.
This document states the principles. The system layers themselves
are defined in docs/architecture.md; we do not
re-derive them here. We state only what they imply for the CLI.
Note (2026-07). This doc is working-draft. The CLI surface is pre-release. Terms and categories may move without notice; pinned in
tests/test_cli_surface.py.
1. runex --help is the surface
The CLI is what users see. What --help prints is what they can do.
- The user reads
--helpfirst, not source. Treat it as the primary API doc. - Every sub-cmd must be reachable from a single
runex --helpwalk. - If a command isn’t worth listing in
--help, it isn’t worth shipping.
2. Three sub-cmd kinds: engine-layer, system, and Application
The CLI has three kinds of sub-cmds with different placement rules.
Confusing them is what produced the L-leaks in the current surface
(runex ontology proposals, runex ontology listen, etc.).
Kind 1 — Engine-layer sub-cmds (mirror the L hierarchy)
These sub-cmds sit at a specific engine layer. The L-layer they reach is where their work lives.
L2 (graph store) node · query · search · inspect · state
↑ reach L2 directly
(store.create_node / store.tag /
store.conn.execute / FTS5)
↑ NO engine dispatch, NO L4 facade
L4 (engine facade) ontology (list / manifest / primitives /
check / load / describe / run /
scan / preview / trace / events)
↑ the L4 facade per arch line 19–22
↑ returns Result envelope, exposes manifest
↑ L3 internals (DSL, engine, bus) are
reached through this facade, not directlyL3 is not directly surfaced. The engine’s internals (DSL eval, Engine, Kernels, ReactiveBus) are reached only via the L4 facade. A sub-cmd that uses L3 (e.g. dispatches an action through the bus) is not at L3 — it’s a system facility, see Kind 2.
Kind 2 — System sub-cmds (independent, use the engine but aren’t at an L)
These are CLI-level facilities that exist because the system needs them. They are not at any L-layer; they reach into the engine as needed.
| Type | Sub-cmds | What it does |
|---|---|---|
| Lifecycle | init · doctor · upgrade | setup / health / version |
| Long-run | tray · listen | foreground UI / daemon entry points |
Static engine surface vs dynamic consumer.
ontologyis the L4 facade — single-shot, returnsResult.tray/listenare the long-run consumers of that facade — they sit in a loop and callontology.dispatch/ontology.loadrepeatedly. A common confusion is “why islistenat top level, not underontology?”: becauseontologyis a question,tray/listenare a worker.runex ontology listenwould imply “run a one-shot ontology thing”;runex listensays “this is a daemon.” | Cross-wizard |scm| multi-step convenience over the L4 facade | | Engine-dispatch facility |connection(configure / list / status / pull / pause / resume / delete) | usesengine.dispatchunder the hood; is a system concept (IngestConnection) not an L-layer |
A sub-cmd is in this kind when it does useful CLI work but the work doesn’t live at any single L-layer. The right test: “can I classify this by an L-number?” If the answer is “I have to stretch”, it’s a system sub-cmd.
Kind 3 — L5 Applications (user-concept sub-cmds, one per concept)
L5 per arch line 383 is “Outside core” = user-concept applications
built on top of the engine. Each is its own sub-cmd, named after
the concept it serves. The mapping is not 1:1 with a single supertag:
an inbox application may span InboxMessage + AgentMessage.
Per the arch’s “every durable object is a node” rule (line 425-444), any node-tagged concept can be an Application. The set we plan to surface as Applications (from arch’s 6 explicit ones + the ones the CLI already references):
task · workitem · proposal · approval · conflict · rule · assertion ·
inbox · session · agent · connection · vaultarch 425-444 lists Task / WorkItem / Proposal / Approval / Assertion /
Conflict / BusinessRule explicitly. The remaining 5 (inbox, session,
agent, connection, vault) are node-tagged concepts the CLI
already references or will reference — same rule, not in the arch
table.
A new Application sub-cmd answers: which user concept does this serve? If the answer is “none — this is engine work”, it’s not an Application (it’s a system sub-cmd instead).
A new command answers three questions before any code is written:
- Is this a new user concept? If yes, it’s an Application sub-cmd (Kind 3), named after the concept.
- Is this engine-layer work? If yes, it goes at L2 or L4 (Kind 1). L3 is reached through the L4 facade, not directly.
- Is this a system-level facility? If yes, it’s a system sub-cmd (Kind 2), categorised by what kind of facility (lifecycle / long-run / cross-wizard / engine-dispatch).
If none of the three fit, the command probably doesn’t belong here.
Current leaks in the surface
(Moved to a live log; see CHANGELOG-CLI-SURFACE.md
once we set it up. As of the CLI-substrate spec, the historical leaks
were: runex ontology listen/proposals/approve/reject/rules/approve.
All are now resolved — see the test file for the current contract.)
3. Umbrella ≠ passthrough
An umbrella is the user-facing wrapper for one or more applications. Justified iff it adds value beyond 1:1 forwarding:
- Wizard: a multi-step flow where users would otherwise have to
know internal sequencing (e.g.
connection configure+ontology loadconnection pullfor a new source).
- Enumeration: “do this for every active one” (e.g.
connection pause --allpauses every active connection). - Composition: combine multiple applications or engine ops
(e.g.
scm add= find the .scm + load it viaontology load).
A 1:1 passthrough is forbidden in an umbrella. If
runex connection list already exists, do not add a sync list that
merely forwards — the user can do the former. Cut it. The application
always exists; the umbrella is the value-add on top.
4. One concept = one application
- An application is owned by one architectural concept (which may span one or more scm supertags).
Proposaloperations (list/approve/reject) live underproposal, not underontology.- The engine sub-app
ontologyexposes only the engine’s own verbs. Today those are:list,manifest,primitives,check,load,describe,run,scan,preview,trace,events. The exact list is pinned intests/test_cli_surface.py. The sub-app does not carry lifecycle verbs for any supertag type. - Cross-concept operations go in a higher-level umbrella, not in
either application. Verify before adding: which sub-cmds does
the umbrella actually compose?
scmcomposesontology(Kind 1 L4) only.
5. Naming follows the architecture
- The CLI uses the architecture’s vocabulary verbatim.
Arch says
WorkItem→ the CLI saysworkitem, notwork. Arch saysIngestConnection→ the CLI saysconnection, notingestorsync. - Old names are deprecated with a warning, then removed at the next minor version. No silent compat aliases — we do not have users who depend on the old name. The migration debt is owned by the next reader, not by a forever-alias.
- Two sub-cmd names that do the same thing is a bug. Pick one
(
agent register, notagent add).
6. Bare sub-app invocation must show help
runex <sub-app> (no sub-cmd) prints the sub-app’s help. It does
not print “Missing command”.
- Enforced by Typer’s
no_args_is_help=Trueon every sub-app. - Enforced by
tests/test_cli_surface.py::test_sub_app_no_args_shows_help. - A sub-app with no sub-cmds either gets one (fix the gap) or is removed.
7. The reader is the entry point, not the doc
The reader of runex --help is an agent or a person building
automation workflows over the ontology. They already have a
working concept of nodes, types, and lifecycles. They do not need
the CLI to teach them what an ontology is.
What they need is navigation — the path from “I want to do X”
to “the sub-cmd that does X”. --help is the navigation, not the
tutorial.
- Every command name and one-liner help text is the navigation
index. The reader should land on the right sub-cmd in one
--helpwalk. - One-line command descriptions are mandatory. Multi-sentence descriptions are a code-review failure.
- Command names that collide with general vocabulary
(
inbox,vault,task,session) must make their domain clear in the first 5 words of--help— because the reader may also be a person, and “inbox” reads as email if not contextualized. - A command whose meaning requires reading the architecture to understand is not ready to ship — rename or document better.
8. Test the surface, not just the behavior
tests/test_cli_surface.py pins the surface. It fails the suite
if:
- A top-level command is added or removed.
- A sub-cmd is added to or removed from a sub-app.
- A removed alias is re-introduced.
- A sub-app’s bare invocation stops showing help.
- A pure-passthrough sub-cmd is re-added to an umbrella.
Pin the names, not just the runtime behavior. “I tested it manually” is not a substitute.
9. The build / install loop is part of the surface
- A user runs
runexfrom a release artifact. They do not havePYTHONPATH=srcset. - The binary is the surface. Source-only tests prove behavior; they do not prove the binary still works.
bash scripts/runex/build.shis part of the change cycle, not a one-off before release.- The feedback loop is: **source test (3 s) → pytest (1 min) → build
- install (5 min) only when shipping**. Skip the build for iteration.
Anti-patterns (we did these; document the lesson)
| Anti-pattern | Example we did | Lesson |
|---|---|---|
| Application concept in engine sub-app | runex ontology proposals / runex ontology proposal-approve | Engine sub-app carries only its own verbs. Application verbs go to the concept’s sub-app. |
| Alias with no value | runex listen (alias for runex ontology listen) | A shorter path is not a feature. Either promote (with a real reason) or cut. |
| Compat alias for no users | proposed agent add (alias for agent register) | Aliases for nonexistent users are technical debt. Don’t add them. |
| Pure passthrough in umbrella | removed sync umbrella (sync list / status / pull / remove) | Engine / application command already exists. Umbrella adds value, not names. |
| Renamed concept, unrenamed CLI | work (arch says WorkItem) | CLI is the user’s vocabulary. Match the arch. |
| Vocabulary collision, no help | runex inbox (looks like email) | First 5 words of help must say what the verb is for. |
| Two names for one function | agent register vs agent add (proposed) | Pick one. Don’t punt. |
What the doc does NOT cover
- The specific layout of every command. That’s
tests/test_cli_surface.pyrunex --helpat any commit.
- Architectural layer definitions. See
docs/architecture.md. - The build / install / distribution pipeline. See
scripts/runex/build.shandscripts/runex/install.sh.