@wirecat/cli-base 0.40.4 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,7 @@ the command tree as data — the parts every CLI needs and nobody should write t
24
24
  | [Shared tooling](docs/TOOLING.md) | The TypeScript, Biome, lefthook, docs and CI setup a CLI takes from here, and `cli-dev` |
25
25
  | [Testing](docs/dev/TESTING.md) | The checks, and what each one is for |
26
26
  | [Conventions](docs/dev/CONVENTIONS.md) | How code here is written |
27
+ | [Standards handbook](docs/standards/README.md) | The rules every WireCat CLI follows, one page per area (a skeleton for now) |
27
28
  | [CLI standard](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/STANDARD.md) | The rules every WireCat CLI follows; this package holds them as data |
28
29
  | [Changelog](CHANGELOG.md) | What each version changed, and what may break callers |
29
30
 
@@ -0,0 +1,111 @@
1
+ # WireCat CLI standards
2
+
3
+ This handbook holds the rules every WireCat CLI follows: tools, layout, commands, output, MCP, accounts,
4
+ permissions, architecture, tests, security, documents, releases and CI. Read this page first. Then read the page
5
+ for the area you are about to change.
6
+
7
+ It ships in the `@wirecat/cli-base` package. A CLI that depends on cli-base has it at
8
+ `node_modules/@wirecat/cli-base/docs/standards/README.md`, at the same version as the `cli-dev lint` it runs.
9
+
10
+ ## How to use this for a new CLI
11
+
12
+ Start with the [New CLI](new-cli.md) checklist. Each step names the check that proves it and the area page whose
13
+ rules it keeps. A new CLI is ready for its first release when every check under "Done when" passes.
14
+
15
+ ## Which rule wins
16
+
17
+ 1. **The handbook wins over a repository's own documents.** When a `CONVENTIONS.md`, `CLAUDE.md` or `AGENTS.md`
18
+ disagrees with a rule here, the rule here applies, and the repository document is the one to fix.
19
+ 2. **A repository may be stricter, never looser.** For example, a smaller file size cap is fine. Turning off a
20
+ rule is not.
21
+ 3. **Product rules stay local.** A rule that only makes sense for one CLI lives in that CLI's `CLAUDE.md`, under
22
+ "Local rules", below a link to this page.
23
+
24
+ ## Areas
25
+
26
+ | Page | Area id | Rules | What it covers |
27
+ |---|---|---:|---|
28
+ | [Toolchain](toolchain.md) | `TOOL` | 19 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown |
29
+ | [Layout](layout.md) | `LAY` | 9 | Repository layout, entry points, exports, file size, one job per file, splitting a unit |
30
+ | [Commands](commands.md) | `CMD` | 27 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help |
31
+ | [Output](output.md) | `OUT` | 22 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
32
+ | [MCP](mcp.md) | `MCP` | 12 | The MCP tools, how they come from commands, what each permission shows |
33
+ | [Accounts](accounts.md) | `ACCT` | 10 | The explicit profile, reads across accounts, scope flags |
34
+ | [Permissions](permissions.md) | `PERM` | 21 | Levels, keys, defaults, hard limits, the write guard and its journal |
35
+ | [Architecture](architecture.md) | `ARCH` | 16 | Layers, dependency direction, vendor types, what goes in which package |
36
+ | [Lifecycle](lifecycle.md) | `LIFE` | 10 | Opening late, who closes, one-shot exits, long-running commands, the `serve` relay |
37
+ | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
38
+ | [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
39
+ | [Security](security.md) | `SECR` | 19 | Secrets, logs, keyring, argv, error text, scanners, workflow hardening, reporting |
40
+ | [Documents](docs.md) | `DOC` | 20 | User pages and developer pages, generated pages, current facts, changelog |
41
+ | [Releases](releases.md) | `REL` | 16 | `bin/release`, the release lock, versions, release reports, trusted publishing |
42
+ | [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
43
+ | [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
44
+ | [New CLI](new-cli.md) | — | — | The checklist for starting a new CLI |
45
+
46
+ Each count leaves out the ids that moved or merged (see "How ids work").
47
+
48
+ ## How a page is laid out
49
+
50
+ Every area page has the same parts, in this order:
51
+
52
+ 1. **Scope:** what the page covers, its rule id prefix, and links to the pages that own nearby rules. It may add
53
+ "Words used here" for terms the page defines, and "Who follows these rules today" for known gaps in a CLI.
54
+ 2. **The rules:** under one `## Rules` heading, or under a few `##` headings that group them by theme.
55
+ 3. **Planned — not built yet** (only on a page that needs it): an approved design that is not true yet. It has no
56
+ rule ids.
57
+ 4. **Sources:** a short list of the documents the rules came from. Where one of them disagrees with the page, the
58
+ page holds.
59
+
60
+ ## How a rule is written
61
+
62
+ Every rule has the same shape. A rule may add a sentence or a short list under its heading, before the bullets.
63
+
64
+ ```markdown
65
+ ### WC-<AREA>-<n> · <the rule, in one short sentence>
66
+
67
+ - **Why:** <one line>.
68
+ - **Enforced by:** <check names in backticks, joined by " · ">, or the words "review only".
69
+ - **Example:** <a command or a piece of code that keeps the rule>.
70
+ - **Exceptions:** <when the rule does not apply>, or "none".
71
+ - **Applies to:** <all CLIs, or which ones>. **Since:** cli-base <version>. **Rulings:** <ids>, or "none".
72
+ ```
73
+
74
+ - **Enforced by** names checks in one of these forms:
75
+ - a check in this repository: a `cli-dev lint` or `cli-dev audit` rule (`flag-synonym`), a workflow (`ci.yml`, or
76
+ `ci.yml:ci` for one job), a `pnpm` script (`pnpm standards:check`) or a file by its path (`src/keyring.test.ts`);
77
+ - a check in another repository, as `<repo>: <check>`, such as `cli-core: pnpm parity:check` or
78
+ `cli-core: biome noRestrictedImports`. `<repo>` is the repository's name on GitHub: `cli-core`, `cli-testing`,
79
+ `cli-docs`, `community`, `tg-cli`, `max-cli`, `cli-memo`, `drive-cli`, `zoom-cli` or `whatsapp-cli`. `<app>`
80
+ stands for each CLI the rule applies to, when every one of them runs that check in its own repository.
81
+ - **Since** is the cli-base release whose handbook first carried the rule. A feature can be older than its rule.
82
+
83
+ ## How ids work
84
+
85
+ - A rule id is `WC-<AREA>-<n>`, for example `WC-OUT-3`. The area id is in the table above.
86
+ - The `WC-` prefix keeps rule ids apart from other ids in the WireCat repositories.
87
+ - Numbers go up within a page and are never reused or renumbered. A new rule takes the next number and goes last on
88
+ its page.
89
+ - A rule that moves to another page, or merges into another rule, keeps its id as a one-line entry, so a citation of
90
+ the old id still leads somewhere:
91
+
92
+ ```markdown
93
+ ### WC-CI-11 · Moved to [WC-SECR-20](security.md#wc-secr-20--pin-each-third-party-action-to-a-commit-and-give-each-workflow-read-only-defaults)
94
+ ### WC-ARCH-12 · Merged into [WC-LIFE-6](lifecycle.md#wc-life-6--open-nothing-until-an-action-needs-it)
95
+ ```
96
+ - A `cli-dev lint` or `cli-dev audit` rule keeps its own kebab-case name, such as `flag-synonym`. The handbook rule
97
+ names it under "Enforced by".
98
+
99
+ ## What `pnpm docs:check` checks here
100
+
101
+ [`scripts/standards-ids.ts`](https://github.com/WireCatLabs/cli-base/blob/main/scripts/standards-ids.ts) reads every
102
+ page and fails when:
103
+
104
+ - a rule id appears twice, belongs to another page's area, or is out of order on its page;
105
+ - a rule has no "Enforced by" line;
106
+ - a name in an "Enforced by" line, on any of its lines, is not a known check. A check in this repository must exist.
107
+ A check in another repository cannot be read from here, so only its form is checked: a known repository name, a
108
+ colon and a space, and a check name. Anything else says "review only";
109
+ - a "Moved to" or "Merged into" entry points at an id that is not a rule;
110
+ - the rule count in the table above differs from the page;
111
+ - a page in this folder is not linked from this index.
@@ -0,0 +1,148 @@
1
+ # Accounts
2
+
3
+ ## Scope
4
+
5
+ How a CLI chooses an account: the profile is always explicit, a read across accounts covers all of them and says so,
6
+ and scope flags work the same everywhere.
7
+
8
+ A **profile** is the name of one login of a CLI on one computer, for example `personal` or `work`. One profile can
9
+ hold a personal session, a bot token, or both.
10
+
11
+ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and which rule wins are in the
12
+ [index](README.md).
13
+
14
+ ### Who follows these rules today
15
+
16
+ cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer, and all three pin
17
+ 0.321.0. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
18
+ and `--account` (WC-ACCT-7). drive and zoom still have their own profile code: drive registers its own profiles, and
19
+ zoom keeps one saved profile in its config file. These are known gaps, not exceptions.
20
+
21
+ ## Rules
22
+
23
+ ### WC-ACCT-1 · Name the profile on every command that acts for an account
24
+
25
+ The profile is the first word on the command line, or the `<PREFIX>_PROFILE` variable. Nothing picks one for the
26
+ owner: not a config file, not the first profile in a list, not a profile called `default`, and not the only profile
27
+ on the computer.
28
+
29
+ - **Why:** which account a command speaks for is the owner's choice; a guess sends from the wrong account.
30
+ - **Enforced by:** `cli-core: src/cli/settings.test.ts`.
31
+ - **Example:** `tg personal chats list`, or `TG_PROFILE=personal tg chats list`. When no profile is named and the
32
+ config file still has `default_profile`, cli-core stops and says which profile to type. `config set default_profile`
33
+ is refused.
34
+ - **Exceptions:** a command that touches no account runs without a profile, for example `account list`,
35
+ `config show` and `commands`.
36
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84.
37
+
38
+ ### WC-ACCT-2 · The profile is a word, never a flag
39
+
40
+ There is no `--profile` option. The first word is the profile unless it names a command, and an option is never a
41
+ profile.
42
+
43
+ - **Why:** one place for the profile keeps every command line readable the same way, in every CLI.
44
+ - **Enforced by:** `cli-core: src/cli/profile.test.ts`.
45
+ - **Example:** `max work bot messages send …` uses the same first word as `max work chats list`. `tg --json chats list`
46
+ still works, because `--json` is an option.
47
+ - **Exceptions:** none.
48
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-294.
49
+
50
+ ### WC-ACCT-3 · A missing profile is an error that lists the choices
51
+
52
+ When a command needs a profile and none is named, it fails with `validation_error`. The message and
53
+ `details.profiles` list the profiles on this computer. With no profile at all, the message says how to create one by
54
+ name.
55
+
56
+ - **Why:** the owner, or an agent, can fix the command from the error alone.
57
+ - **Enforced by:** `cli-core: src/cli/settings.test.ts`.
58
+ - **Example:** ``name a profile: `tg <profile> …` — profiles here: personal, work (or set TG_PROFILE)``. With none,
59
+ the message says to ``create one with `tg personal setup` (any name in place of personal)``.
60
+ - **Exceptions:** none.
61
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
62
+
63
+ ### WC-ACCT-4 · A profile name is a plain word and never a command word
64
+
65
+ A name has letters, digits, dot, dash and underscore only, and starts with a letter or a digit. A name that is also a
66
+ top-level command, such as `chats` or `bot`, is refused when the profile is created. `default` is an ordinary name.
67
+
68
+ - **Why:** the name becomes a file name and a keyring account, and a profile named after a command could never be
69
+ selected.
70
+ - **Enforced by:** `cli-core: src/cli/profile.test.ts` · `cli-core: src/cli/settings.test.ts`.
71
+ - **Example:** `tg work setup` works. `tg chats setup` is refused with the list of command words to avoid.
72
+ `tg a/../b setup` is refused as a name.
73
+ - **Exceptions:** none.
74
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-294.
75
+
76
+ ### WC-ACCT-5 · A profile lock pins a process to one profile
77
+
78
+ When `<PREFIX>_PROFILE_LOCK` is set, a first word or `<PREFIX>_PROFILE` that names any other profile is refused with
79
+ `permission_error`.
80
+
81
+ - **Why:** an agent must not escape its guards by naming a profile with fewer of them.
82
+ - **Enforced by:** `cli-core: src/cli/settings.test.ts`.
83
+ - **Example:** with `MAX_PROFILE_LOCK=agent`, `max personal messages send …` is refused, and `max messages list` runs
84
+ as `agent`.
85
+ - **Exceptions:** none.
86
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
87
+
88
+ ### WC-ACCT-6 · The most specific setting wins
89
+
90
+ A setting is read from the nearest of these sections: this profile's `personal` or `bot` entry, then the profile,
91
+ then every personal account or every bot (`personal.defaults`, `bot.defaults`), then `defaults`.
92
+
93
+ - **Why:** naming one account says more than naming all of them, so it must not be undone by a broader section.
94
+ - **Enforced by:** review only.
95
+ - **Example:** `defaults.sends_per_hour: 30` and `profiles.work.sends_per_hour: 10` give `work` a limit of 10.
96
+ - **Exceptions:** permissions layer the same way, with one more rule: see [WC-PERM-7](permissions.md).
97
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-355.
98
+
99
+ ### WC-ACCT-7 · Never pick an account inside a provider for the owner
100
+
101
+ When a provider has several accounts, the command takes `--account <account>` or reads all of them. It never takes the
102
+ first account in a list.
103
+
104
+ - **Why:** the same reason as WC-ACCT-1, one level down.
105
+ - **Enforced by:** review only.
106
+ - **Example:** `memo search mail "invoice"` reads every mailbox; `--source email:work` reads one.
107
+ - **Exceptions:** none.
108
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
109
+
110
+ ### WC-ACCT-8 · Scope flags have one meaning in every CLI
111
+
112
+ `--account` names one account. `--provider` reads every account of one provider. `--source` takes a comma-separated
113
+ list of `provider:account`, a provider, a kind, or `group:<name>`. `--all-bots` also reads the other bots' copies that
114
+ the config allows.
115
+
116
+ - **Why:** an agent that learned a flag in one CLI must not be surprised by it in another.
117
+ - **Enforced by:** `flag-two-meanings` · `flag-synonym` · `option-role` · `same-flag-meaning`.
118
+ - **Example:** `memo search messages "invoice" --source telegram,max`.
119
+ - **Exceptions:** `same-flag-meaning` runs weekly and does not block a pull request.
120
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
121
+
122
+ ### WC-ACCT-9 · A read across accounts covers all of them and says which it read
123
+
124
+ A read that is not limited to one account reads every account in its scope, and its answer names them, for example in
125
+ `coverage.accounts`.
126
+
127
+ - **Why:** an answer from part of the data must not look like an answer from all of it.
128
+ - **Enforced by:** review only.
129
+ - **Example:** `search messages` answers `coverage.accounts`; `search mail` names the mailboxes it read.
130
+ - **Exceptions:** a meaning search never reads wider than the running account unless `--source` says so. `search all`
131
+ reads meetings only when they are named. Not every cross-account read reports its accounts yet.
132
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** ss:8.
133
+
134
+ ### WC-ACCT-10 · A bot reads another bot's copy only when the config and the command both say so
135
+
136
+ - **Why:** each bot's local copy belongs to that bot; reading it is a choice, not a default.
137
+ - **Enforced by:** review only.
138
+ - **Example:** `read_other_bots: ["support"]` in the bot's section, and `--all-bots` or `--bots support` on the command.
139
+ - **Exceptions:** none.
140
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** bots:12.
141
+
142
+ ## Sources
143
+
144
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
145
+ holds.
146
+
147
+ - [cli-core `STANDARD.md`, "Command names"](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/STANDARD.md#command-names), rule 1
148
+ - [cli-core `ARCHITECTURE.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/ARCHITECTURE.md), on the account-bound store
@@ -0,0 +1,214 @@
1
+ # Architecture
2
+
3
+ ## Scope
4
+
5
+ How the code is split: the layers and the direction of dependencies, no vendor type above the adapter, and what
6
+ belongs in cli-base, in cli-core or in the CLI itself.
7
+
8
+ Rule ids on this page are `WC-ARCH-<n>`. How to read a rule, how ids work and which rule wins are in the
9
+ [index](README.md).
10
+
11
+ Related pages: file size, one job per file and how a large unit is split are in [Layout](layout.md); import cycles
12
+ and the lint that holds the layers are in [Toolchain](toolchain.md); who opens and closes a resource, what may open
13
+ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's own rules are in [Store](store.md).
14
+
15
+ ### Words used here
16
+
17
+ - **Adapter:** the code that talks to one provider (Telegram, MAX, WhatsApp, Google Drive, Zoom) and translates its
18
+ objects into our own types.
19
+ - **Port:** an interface that says what a service needs, for example `MessengerAdapter` or a store area.
20
+ - **Service:** a use case, written once, that both a command and an MCP tool call.
21
+ - **Composition root:** the place in a CLI where real implementations are chosen and connected, usually
22
+ `program.ts` and the command context.
23
+ - **Vendor type:** a type from a provider library, such as `@mtcute/*` or `ws`.
24
+
25
+ ## Rules
26
+
27
+ ### WC-ARCH-1 · Call only the layers below you: interface, then service, then port, then adapter
28
+
29
+ - **Why:** when a command calls an adapter directly, the MCP tool for the same job does something different.
30
+ - **Enforced by:** review only.
31
+ - **Example:** cli-core has five layers: domain (`src/domain/`), adapters, ports (`port.ts`, the store), services
32
+ (`src/services/`) and the interface (commands and MCP tools). Its `biome.json` is meant to refuse an import of
33
+ `commander` or of `src/cli/` from the services and the layers below them, and an import of a command file from
34
+ `src/mcp/`. What a
35
+ command and a service share lives below the command.
36
+ - **Exceptions:** none. A new adapter method goes into a named group in `port.ts`, never into the domain. Today the
37
+ last `noRestrictedImports` override in cli-core's `biome.json` replaces the earlier ones, so cli-core's layer,
38
+ provider, module and store import rules (WC-ARCH-2, WC-ARCH-8, WC-STORE-4) catch nothing until it is fixed.
39
+ - **Applies to:** all CLIs. Each repository's `biome.json` holds its own import rules. **Since:** cli-base 0.41.0.
40
+ **Rulings:** none.
41
+
42
+ ### WC-ARCH-2 · Dependencies point one way: a CLI depends on cli-core, cli-core on cli-base, never back
43
+
44
+ - **Why:** a shared package that imports an app cannot be shared.
45
+ - **Enforced by:** review only.
46
+ - **Example:** cli-base has no dependency on cli-core or on any CLI. cli-core's `biome.json` is meant to refuse an import
47
+ of a messenger library or an adapter directory ("cli-core speaks no provider"); today it does not (WC-ARCH-1).
48
+ - **Exceptions:** none.
49
+ - **Applies to:** cli-base, cli-core and all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
50
+
51
+ ### WC-ARCH-3 · Only the adapter imports a vendor library; nothing above it sees a vendor type
52
+
53
+ - **Why:** the provider library underneath must be replaceable, and the layers above must not change when it is.
54
+ - **Enforced by:** `max-cli: biome noRestrictedImports` · `tg-cli: biome noRestrictedImports` ·
55
+ `whatsapp-cli: biome noRestrictedImports`.
56
+ - **Example:** in max-cli, `biome.json` lets only `src/client.ts` and `src/client/` reach the protocol, and only
57
+ `src/adapter/` maps the client onto the shared messenger port. A field that only one provider has travels in
58
+ `providerMetadata`.
59
+ - **Exceptions:** a bot slice that talks to an official HTTP API keeps its own transport, beside the personal
60
+ account's adapter, never above it.
61
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-12.
62
+
63
+ ### WC-ARCH-4 · Put code in the lowest shared package that can hold it
64
+
65
+ - **Why:** the CLIs keep to one standard by sharing code, not by copying text.
66
+ - **Enforced by:** review only.
67
+ - **Example:**
68
+ - cli-base: general CLI infrastructure that needs no domain knowledge. This includes the streams, the renderer,
69
+ errors and exit codes, the keyring, config and paths, clocks, retry, the command registry and format-neutral code
70
+ generation.
71
+ - cli-core: provider-neutral models, ports and use cases, even when only one CLI uses them today. Meeting discovery
72
+ is in cli-core although Zoom was its first user.
73
+ - The CLI: vendor translation, profile binding, resource lifetimes and rules that only make sense for that product.
74
+ - **Exceptions:** share an invariant or a real capability. Lines that only look alike are not a reason for a new
75
+ shared abstraction.
76
+ - **Applies to:** cli-base, cli-core and all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-292, NEED-440.
77
+
78
+ ### WC-ARCH-5 · Build a feature that another provider could have as a shared command behind a capability
79
+
80
+ - **Why:** a command written once in cli-core gives every messenger the same path, flags and output.
81
+ - **Enforced by:** review only.
82
+ - **Example:** topics and polls are port groups and shared commands in cli-core. A messenger that lacks a capability
83
+ refuses the command; it does not silently do less. A bot is another messenger with fewer capabilities, and tg and
84
+ max share the bot code.
85
+ - **Exceptions:** a feature stays in one CLI only when the other providers cannot have it, such as MAX's socket server
86
+ or Telegram's app registration.
87
+ - **Applies to:** messenger CLIs (tg, max, wa). **Since:** cli-base 0.41.0. **Rulings:** NEED-820, NEED-440,
88
+ NEED-301, NEED-444.
89
+
90
+ ### WC-ARCH-6 · Replace a use case, not a command
91
+
92
+ - **Why:** when a CLI replaces the service method, its command and its MCP tool both get the change.
93
+ - **Enforced by:** review only.
94
+ - **Example:** `Messenger.services` receives the shared services and returns only the ones it changes:
95
+ `services: (base) => ({ messages: { ...base.messages, list: maxList } })`. To add a subcommand, a CLI calls
96
+ `addCommand` on the command a factory returns.
97
+ - **Exceptions:** none.
98
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
99
+
100
+ ### WC-ARCH-7 · Keep policy in the service; a command or MCP handler only parses input and presents the result
101
+
102
+ - **Why:** two transports that each interpret a query or a write policy drift apart.
103
+ - **Enforced by:** review only.
104
+ - **Example:** query interpretation, cursor binding, evidence checks and write policy live in the service. Each
105
+ caller parses its own input, so an error names `--since` in a command and `since` in a tool, but both run the same
106
+ method and give the same error for the same input.
107
+ - **Exceptions:** none.
108
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
109
+
110
+ ### WC-ARCH-8 · Cross a module boundary only through the module's entry point
111
+
112
+ - **Why:** a deep import ties the caller to another module's internals.
113
+ - **Enforced by:** `cli-memo: biome noRestrictedImports` · `drive-cli: biome noRestrictedImports` ·
114
+ `zoom-cli: biome noRestrictedImports`.
115
+ - **Example:** in cli-core, `src/tasks/` and `src/meetings/` are entered only through their `index.ts`, `cli/`, `mcp/`
116
+ and `testing/` entries; its `biome.json` is meant to refuse the rest, and today does not (WC-ARCH-1). memo, drive and zoom have a "public exports only" rule. A
117
+ package's own entry points are its `exports`
118
+ ([WC-LAY-5](layout.md#wc-lay-5--add-an-entry-point-as-an-exports-entry-built-from-srcindexts)).
119
+ - **Exceptions:** tests may import what they test.
120
+ - **Applies to:** all CLIs. tg, max and wa do not have the import rule yet. **Since:** cli-base 0.41.0.
121
+ **Rulings:** none.
122
+
123
+ ### WC-ARCH-9 · Pass the narrow port or callback a use case needs, not a whole store or container
124
+
125
+ - **Why:** a narrow dependency shows what the use case touches and is easy to replace in a test.
126
+ - **Enforced by:** review only.
127
+ - **Example:** a service sees the messenger as `MessengerTraits`, only the fields it reads. The CLI's `Messenger`
128
+ adds how commands connect.
129
+ - **Exceptions:** none. Add a factory, inheritance or a dependency-injection framework only for a concrete benefit.
130
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
131
+
132
+ ### WC-ARCH-10 · Pass every effect on the machine as an argument, with the real one only as the default
133
+
134
+ - **Why:** a test then replaces the clock, the keyring or the network, and never touches the real machine.
135
+ - **Enforced by:** review only.
136
+ - **Example:** clock, sleep, keyring, `env`, `fetch`, streams, stdin and the TTY check are parameters or fields of the
137
+ command environment. Production code uses `environment.sleep ?? realSleep`. A new effect gets a new field, not a
138
+ module mock.
139
+ - **Exceptions:** none. Watch the defaults: `new Credentials({...})` without `keyring` is the owner's real keychain,
140
+ and `resolvePaths` without `env` reads `process.env`.
141
+ - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** none.
142
+
143
+ ### WC-ARCH-11 · Keep one command environment per program, cli-core's
144
+
145
+ - **Why:** two registries of the same environment must both be filled on every run, and one is always forgotten.
146
+ - **Enforced by:** review only.
147
+ - **Example:** a CLI extends `BaseEnvironment`, hands it to cli-core's `provide(program, environment)`, and its
148
+ commands read it with `environmentOf<Environment>(command)`, never with `process.*`.
149
+ - **Exceptions:** a CLI that does not use cli-core's command skeleton keeps one environment of its own.
150
+ - **Applies to:** CLIs built on cli-core's command skeleton. **Since:** cli-base 0.41.0. **Rulings:** none.
151
+
152
+ ### WC-ARCH-12 · Merged into [WC-LIFE-6](lifecycle.md#wc-life-6--open-nothing-until-an-action-needs-it)
153
+
154
+ ### WC-ARCH-13 · Merged into [WC-LIFE-1](lifecycle.md#wc-life-1--the-code-that-opens-a-resource-closes-it-so-a-one-shot-command-exits-when-it-has-answered)
155
+
156
+ ### WC-ARCH-14 · Merged into [WC-LIFE-11](lifecycle.md#wc-life-11--run-commands-through-serve-when-the-messenger-allows-one-connection-per-account)
157
+
158
+ ### WC-ARCH-15 · Each CLI runs its own background server, built on cli-core's process library
159
+
160
+ - **Why:** a server shared between CLIs couples their releases and their failures.
161
+ - **Enforced by:** review only.
162
+ - **Example:** cli-core's `./background` export holds the lock per app and profile, the PID check and the systemd and
163
+ launchd units. `tg serve` and `max serve` are each built on it.
164
+ - **Exceptions:** none.
165
+ - **Applies to:** CLIs with a background process. **Since:** cli-base 0.41.0. **Rulings:** NEED-492.
166
+
167
+ ### WC-ARCH-16 · Layers below the commands never name a command
168
+
169
+ - **Why:** an adapter that prints "check `tg polls show`" breaks when the command is renamed and cannot be shared.
170
+ - **Enforced by:** review only.
171
+ - **Example:** a tg adapter error carries a typed reason, such as `{ check: "polls.show" }`. The command layer turns
172
+ the reason into the words `tg polls show`.
173
+ - **Exceptions:** none.
174
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
175
+
176
+ ### WC-ARCH-17 · Merged into [WC-LAY-8](layout.md#wc-lay-8--give-each-module-one-reason-to-change)
177
+
178
+ ### WC-ARCH-18 · An optional capability that is missing fails out loud
179
+
180
+ - **Why:** a request that quietly becomes a smaller one gives the caller a wrong answer that looks right.
181
+ - **Enforced by:** review only.
182
+ - **Example:** a new adapter method is optional and reached with `capability()`. When it is absent, the command
183
+ refuses with a reason; it does not drop the filter or the scope it could not apply.
184
+ - **Exceptions:** none.
185
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-820.
186
+
187
+ ### WC-ARCH-19 · Shared packages print nothing and log nothing on their own
188
+
189
+ - **Why:** what is written, and where, is the host's decision; a stray line breaks machine output.
190
+ - **Enforced by:** review only.
191
+ - **Example:** cli-base hands the host a `Logger` interface, a Pino adapter and a renderer. It never calls
192
+ `console.log`, nor `console.error`, which the lint allows elsewhere
193
+ ([WC-TOOL-9](toolchain.md#wc-tool-9--write-to-the-console-only-with-consoleerror-except-in-scripts)).
194
+ - **Exceptions:** none.
195
+ - **Applies to:** cli-base and cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
196
+
197
+ ### WC-ARCH-20 · Keep HTTP out of cli-base's root export
198
+
199
+ - **Why:** a CLI that only speaks a socket must not depend on an HTTP stack.
200
+ - **Enforced by:** review only.
201
+ - **Example:** a helper that reads a status code or a header goes to `@wirecat/cli-base/http`. How an entry point
202
+ is added is [WC-LAY-5](layout.md#wc-lay-5--add-an-entry-point-as-an-exports-entry-built-from-srcindexts).
203
+ - **Exceptions:** none.
204
+ - **Applies to:** cli-base. **Since:** cli-base 0.41.0. **Rulings:** NEED-15.
205
+
206
+ ## Sources
207
+
208
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
209
+ holds.
210
+
211
+ - [cli-base `MODULARITY.md`](https://github.com/WireCatLabs/cli-base/blob/main/docs/dev/MODULARITY.md)
212
+ - [cli-base `ARCHITECTURE.md`](https://github.com/WireCatLabs/cli-base/blob/main/docs/dev/ARCHITECTURE.md)
213
+ - [cli-core `ARCHITECTURE.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/ARCHITECTURE.md)
214
+ - [cli-core `ADAPTERS.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/ADAPTERS.md)
@@ -0,0 +1,159 @@
1
+ # CI
2
+
3
+ ## Scope
4
+
5
+ What runs where: the local hooks, the checks on every pull request and their time budget, full validation on each
6
+ merge to `main`, and the monthly runs.
7
+
8
+ Rule ids on this page are `WC-CI-<n>`. How to read a rule, how ids work and which rule wins are in the
9
+ [index](README.md). What each tool checks is on the [Toolchain page](toolchain.md); the release workflow is on the
10
+ [Releases page](releases.md); which scanners must exist and how workflows are hardened is on the
11
+ [Security page](security.md).
12
+
13
+ "The ten repositories" on this page means the six CLIs (tg, max, memo, drive, zoom, wa), cli-base, cli-core,
14
+ cli-testing, cli-docs, plus community itself.
15
+
16
+ ## Rules
17
+
18
+ ### WC-CI-1 · Run only staged lint and a staged secret scan before a commit
19
+
20
+ - **Why:** a hook that takes more than a second makes people skip it, which is worse than a small hook.
21
+ - **Enforced by:** `pnpm standards:check` · `standards.yml:standards`.
22
+ - **Example:** the shared `lefthook` config runs, in parallel, Biome on the staged files and
23
+ `gitleaks git --staged --redact`. A clone without gitleaks gets a warning and can still commit; CI scans anyway.
24
+ The `prepare` script installs the hooks; `LEFTHOOK=0 git commit` skips them for one commit.
25
+ - **Exceptions:** none.
26
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
27
+
28
+ ### WC-CI-2 · Add no push hook, and no hook that installs, builds, checks types, generates or tests
29
+
30
+ - **Why:** every check in a hook runs again in CI, so a slow hook only costs time and teaches people to bypass hooks.
31
+ - **Enforced by:** review only.
32
+ - **Example:** a repository's `lefthook.yml` holds only the `extends` line for `config/wirecat/lefthook.yml`. An agent
33
+ does not run `standards:check`, the full lint, spelling, a build or the test suite on each commit or push; it runs a
34
+ targeted check when it needs one.
35
+ - **Exceptions:** none. cli-base `config/lefthook.yml` still has a pre-push typecheck, but no repository extends it.
36
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
37
+
38
+ ### WC-CI-3 · Run the fast standards job on every pull request and every push to `main`
39
+
40
+ - **Why:** it catches broken presets, lint, Markdown, types and new secrets in well under a minute, before review.
41
+ - **Enforced by:** `standards.yml:standards`.
42
+ - **Example:** the job does one cached install with lifecycle scripts off, then runs `pnpm standards:check`,
43
+ `pnpm lint`, `pnpm docs:lint`, `pnpm run --if-present ci:quick` and a gitleaks scan of the new commits only. A new
44
+ commit cancels the run it replaces. The job times out after five minutes, a ceiling for a stalled setup, not an
45
+ expected time.
46
+ - **Exceptions:** none. Runner queues and cold downloads are outside the budget.
47
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
48
+
49
+ ### WC-CI-4 · Keep `ci:quick` to early bug checks that finish within two minutes
50
+
51
+ - **Why:** it is the only build-level check on a pull request, so it must stay fast enough that nobody wants it gone.
52
+ - **Enforced by:** `standards.yml:standards` · `pnpm ci:quick`.
53
+ - **Example:** what each repository runs today:
54
+ - tg, max, memo and wa: `pnpm typecheck && pnpm build && pnpm lint:commands`;
55
+ - drive: `pnpm typecheck && pnpm lint:commands`, which builds first;
56
+ - zoom, cli-base and cli-testing: `pnpm typecheck`;
57
+ - cli-core: `pnpm typecheck` and its command lint test;
58
+ - cli-docs: its unit tests, without the one that needs generated pages;
59
+ - community: its script tests.
60
+ - **Exceptions:** never put live-account suites, agent suites, full tests, coverage or platform jobs here.
61
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
62
+
63
+ ### WC-CI-5 · Keep full tests, builds of documents, parity and platform suites out of pull request CI
64
+
65
+ - **Why:** pull request CI is the step people wait on; the heavy suites run once on `main`, where a failure still
66
+ shows within minutes of the merge.
67
+ - **Enforced by:** review only.
68
+ - **Example:** on a pull request, only the standards job, the changelog guard and small path-scoped jobs run. Coverage,
69
+ contracts, the test matrix, `docs:check`, parity, Windows and macOS run in full validation (WC-CI-7).
70
+ - **Exceptions:** a path-scoped job that tests one platform-specific file may run on pull requests, such as cli-base
71
+ `macos.yml` for the path code. cli-docs and community run `security-pr.yml`, a new-commit secret scan. Third-party
72
+ security apps may post their own checks.
73
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
74
+
75
+ ### WC-CI-6 · Run the changelog guard on every pull request
76
+
77
+ - **Why:** the guard is the only check that reads the changelog on a pull request.
78
+ - **Enforced by:** `changelog-guard.yml:guard`.
79
+ - **Example:** each repository's `changelog-guard.yml` calls
80
+ `WireCatLabs/cli-base/.github/workflows/changelog-guard.yml` at a pinned commit, on `opened`, `edited`,
81
+ `synchronize` and `reopened`. The changelog rules it applies are on the [Documents page](docs.md).
82
+ - **Exceptions:** cli-testing, cli-docs and community have no changelog guard yet.
83
+ - **Applies to:** the CLIs, cli-base and cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
84
+
85
+ ### WC-CI-7 · Run full validation once on each merge to `main`, on Linux, without publishing
86
+
87
+ - **Why:** a merge that breaks the full suite shows up the same hour, not at the next release.
88
+ - **Enforced by:** `main.yml:validation` · `node-ci.yml:check` · `node-ci.yml:bun` · `node-ci.yml:secrets`.
89
+ - **Example:** `main.yml` calls the repository's own `ci.yml`, which calls cli-base `node-ci.yml`. That runs
90
+ `pnpm lint`, `pnpm typecheck`, `pnpm test:coverage`, `pnpm build`, then the project's `checks` lines (for max:
91
+ contracts, the no-leak scan, the version check, `pnpm generate` and `git diff --exit-code`, the test matrix,
92
+ `docs:check` and parity), a Bun job, and a gitleaks scan of the whole history.
93
+ - **Exceptions:** platform suites (Windows, macOS, install checks) are skipped here and run at release, monthly or on
94
+ request. Nothing in this run publishes or tags. cli-docs runs its full validation before each deployment and monthly
95
+ instead; community has no build to validate.
96
+ - **Applies to:** the CLIs, cli-base, cli-core and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
97
+
98
+ ### WC-CI-8 · Call cli-base `node-ci.yml` from `ci.yml`, pinned to a tag or commit
99
+
100
+ - **Why:** one shared workflow keeps the full suite the same everywhere, and a pin keeps a cli-base change from
101
+ breaking ten repositories at once.
102
+ - **Enforced by:** `ci.yml:ci`.
103
+ - **Example:** `uses: WireCatLabs/cli-base/.github/workflows/node-ci.yml@<commit> # v0.31.0`, with the project's own
104
+ checks in the `checks` input and `bun-run` for the Bun job. A job that only one repository needs stays beside it in
105
+ that repository's `ci.yml`.
106
+ - **Exceptions:** never pin to `main`. cli-base itself calls `./.github/workflows/node-ci.yml`. cli-docs has its own
107
+ full workflow for the site, and community has none.
108
+ - **Applies to:** the CLIs, cli-core and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
109
+
110
+ ### WC-CI-9 · Run full validation and release security on the first day of each month, without publishing
111
+
112
+ - **Why:** a repository with no merges for weeks still meets new advisories, new runner images and expired pins.
113
+ - **Enforced by:** `monthly.yml:validation` · `monthly.yml:security`.
114
+ - **Example:** `monthly.yml`, from the community template, calls the repository's `ci.yml` and the pinned cli-base
115
+ `release-checks.yml`. Each repository has its own minute, between 02:17 and 03:21 UTC, so runs do not overlap.
116
+ - **Exceptions:** community has no build, so it runs only the release security checks, at 03:47 UTC. A monthly run
117
+ never publishes a package or deploys the site.
118
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
119
+
120
+ ### WC-CI-10 · Run heavy security checks only at release, monthly or on request
121
+
122
+ - **Why:** a pull request needs a fast secret check; source scanners take minutes and find nothing new on most
123
+ changes.
124
+ - **Enforced by:** `release.yml:checks` · `monthly.yml:security`.
125
+ - **Example:** cli-base `release-checks.yml` runs the production dependency audit, the workflow scan (`zizmor`),
126
+ CodeQL and a full-history gitleaks scan. A release depends on it. A pull request runs only the new-commit gitleaks
127
+ scan in the standards job.
128
+ - **Exceptions:** none. Do not add a heavy scanner to an editor hook, a stop hook or an ordinary pull request job.
129
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** sc:9.
130
+
131
+ ### WC-CI-11 · Moved to [WC-SECR-20](security.md#wc-secr-20--pin-each-third-party-action-to-a-commit-and-give-each-workflow-read-only-defaults)
132
+
133
+ ### WC-CI-12 · Merge to `main` only through a pull request whose checks all passed
134
+
135
+ - **Why:** a red merge breaks `main` for every parallel branch, and an admin merge skips the only gate there is.
136
+ - **Enforced by:** review only.
137
+ - **Example:** `gh pr checks <number>` shows every line as pass before `gh pr merge`. Never merge with `--admin`. A
138
+ release changes `main` the same way
139
+ ([WC-REL-2](releases.md#wc-rel-2--main-changes-only-through-a-pull-request-also-during-a-release)).
140
+ - **Exceptions:** none.
141
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** NEED-381.
142
+
143
+ ### WC-CI-13 · Start no live messenger or account test from a workflow
144
+
145
+ - **Why:** a live test acts on a real account; only a person who chose to run it may start it.
146
+ - **Enforced by:** review only.
147
+ - **Example:** the offline contracts run in full validation. The live scenarios run only by hand, through the CLI's
148
+ live-test procedure, on test accounts.
149
+ - **Exceptions:** none.
150
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
151
+
152
+ ## Sources
153
+
154
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
155
+ holds. The procedures stay in their own homes.
156
+
157
+ - [community check schedule](https://github.com/WireCatLabs/community/blob/main/standards/CHECKS.md): the rough timings
158
+ - [cli-testing CI folder](https://github.com/WireCatLabs/cli-testing/tree/main/docs/ci): the cross-project timing
159
+ table, the monthly runbook and the contract runner