Skip to Content
文档CLI 设计

CLI Substrate — design principles

Audience: anyone adding, removing, or renaming a CLI command in runex. Goal: every change keeps runex --help working 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 --help first, not source. Treat it as the primary API doc.
  • Every sub-cmd must be reachable from a single runex --help walk.
  • 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 directly

L3 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.

TypeSub-cmdsWhat it does
Lifecycleinit · doctor · upgradesetup / health / version
Long-runtray · listenforeground UI / daemon entry points

Static engine surface vs dynamic consumer. ontology is the L4 facade — single-shot, returns Result. tray / listen are the long-run consumers of that facade — they sit in a loop and call ontology.dispatch / ontology.load repeatedly. A common confusion is “why is listen at top level, not under ontology?”: because ontology is a question, tray/listen are a worker. runex ontology listen would imply “run a one-shot ontology thing”; runex listen says “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) | uses engine.dispatch under 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 · vault

arch 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:

  1. Is this a new user concept? If yes, it’s an Application sub-cmd (Kind 3), named after the concept.
  2. Is this engine-layer work? If yes, it goes at L2 or L4 (Kind 1). L3 is reached through the L4 facade, not directly.
  3. 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 load
    • connection pull for a new source).
  • Enumeration: “do this for every active one” (e.g. connection pause --all pauses every active connection).
  • Composition: combine multiple applications or engine ops (e.g. scm add = find the .scm + load it via ontology 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).
  • Proposal operations (list / approve / reject) live under proposal, not under ontology.
  • The engine sub-app ontology exposes 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 in tests/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?
    • scm composes ontology (Kind 1 L4) only.

5. Naming follows the architecture

  • The CLI uses the architecture’s vocabulary verbatim. Arch says WorkItem → the CLI says workitem, not work. Arch says IngestConnection → the CLI says connection, not ingest or sync.
  • 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, not agent 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=True on 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 --help walk.
  • 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 runex from a release artifact. They do not have PYTHONPATH=src set.
  • The binary is the surface. Source-only tests prove behavior; they do not prove the binary still works.
  • bash scripts/runex/build.sh is 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-patternExample we didLesson
Application concept in engine sub-apprunex ontology proposals / runex ontology proposal-approveEngine sub-app carries only its own verbs. Application verbs go to the concept’s sub-app.
Alias with no valuerunex 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 usersproposed agent add (alias for agent register)Aliases for nonexistent users are technical debt. Don’t add them.
Pure passthrough in umbrellaremoved sync umbrella (sync list / status / pull / remove)Engine / application command already exists. Umbrella adds value, not names.
Renamed concept, unrenamed CLIwork (arch says WorkItem)CLI is the user’s vocabulary. Match the arch.
Vocabulary collision, no helprunex inbox (looks like email)First 5 words of help must say what the verb is for.
Two names for one functionagent 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.py
    • runex --help at any commit.
  • Architectural layer definitions. See docs/architecture.md.
  • The build / install / distribution pipeline. See scripts/runex/build.sh and scripts/runex/install.sh.
Last updated on