@wirecat/cli-base 0.63.0 → 0.65.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +232 -0
  2. package/dist/codegen/discovery.d.ts +3 -3
  3. package/dist/codegen/discovery.js +18 -9
  4. package/dist/codegen/index.d.ts +3 -1
  5. package/dist/codegen/index.js +3 -1
  6. package/dist/codegen/json-schema.d.ts +8 -0
  7. package/dist/codegen/json-schema.js +78 -0
  8. package/dist/commands/index.d.ts +1 -1
  9. package/dist/commands/index.js +1 -1
  10. package/dist/credentials.d.ts +1 -1
  11. package/dist/dev/bundle.d.ts +31 -0
  12. package/dist/dev/bundle.js +280 -0
  13. package/dist/dev/main.js +48 -1
  14. package/dist/skill/index.d.ts +1 -1
  15. package/dist/skill/index.js +2 -2
  16. package/dist/standards/audit.js +8 -0
  17. package/dist/standards/flags-calendar.d.ts +2 -0
  18. package/dist/standards/flags-calendar.js +72 -0
  19. package/dist/standards/flags-how.js +7 -1
  20. package/dist/standards/flags-what.js +7 -0
  21. package/dist/standards/vocabulary.js +13 -2
  22. package/docs/standards/README.md +5 -5
  23. package/docs/standards/accounts.md +44 -11
  24. package/docs/standards/architecture.md +19 -12
  25. package/docs/standards/ci.md +33 -12
  26. package/docs/standards/commands.md +35 -10
  27. package/docs/standards/data-model.md +5 -1
  28. package/docs/standards/docs.md +4 -1
  29. package/docs/standards/kit-surface.md +6 -5
  30. package/docs/standards/layout.md +32 -0
  31. package/docs/standards/lifecycle.md +69 -8
  32. package/docs/standards/mcp.md +48 -24
  33. package/docs/standards/new-cli.md +30 -16
  34. package/docs/standards/output.md +13 -2
  35. package/docs/standards/permissions.md +46 -3
  36. package/docs/standards/releases.md +21 -5
  37. package/docs/standards/security.md +16 -8
  38. package/docs/standards/store.md +4 -3
  39. package/docs/standards/testing.md +19 -8
  40. package/docs/standards/time.md +8 -1
  41. package/docs/standards/toolchain.md +24 -18
  42. package/package.json +6 -1
@@ -3,7 +3,7 @@
3
3
  ## Scope
4
4
 
5
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.
6
+ scope flags work the same everywhere, and sign-in has the same four commands in every CLI.
7
7
 
8
8
  A **profile** is the name of one login of a CLI on one computer, for example `personal` or `work`. One profile can
9
9
  hold a personal session, a bot token, or both.
@@ -16,7 +16,8 @@ Rule ids on this page are `WC-ACCT-<n>`. How to read a rule, how ids work and wh
16
16
  cli-core resolves the profile for tg, max and wa. WC-ACCT-1 needs cli-core 0.319.0 or newer. memo has no messenger profile: its shared commands run without one, and they pick accounts with `--provider`
17
17
  and `--account` (WC-ACCT-7). zoom resolves its own profile word by the same rules (`zm <profile> …`, `ZM_PROFILE`,
18
18
  `ZM_PROFILE_LOCK`) from zoom-cli 0.35.0, with cli-core's `liftProfile` and `usableProfileName`. drive still has its own
19
- profile code: it registers its own profiles. That is a known gap, not an exception.
19
+ profile code: it registers its own profiles. That is a known gap, not an exception. gcal resolves the profile through
20
+ cli-core's `run()`, like tg, max and wa. Which CLIs lack a sign-in command of WC-ACCT-12 is listed in that rule.
20
21
 
21
22
  ## Rules
22
23
 
@@ -31,9 +32,11 @@ on the computer.
31
32
  - **Example:** `tg personal chats list`, or `TG_PROFILE=personal tg chats list`. When no profile is named and the
32
33
  config file still has `default_profile`, cli-core stops and says which profile to type. `config set default_profile`
33
34
  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.
35
+ - **Exceptions:** a command that touches no account runs without a profile, for example `session list`
36
+ ([WC-ACCT-12](#wc-acct-12--sign-in-is-session-start-status-and-end-and-session-list-names-the-profiles)),
37
+ `config show` and `commands`; tg, max and wa still name their list `account list`. `mcp serve` may name its profiles with `--profiles <names|all>` instead
38
+ ([WC-MCP-9](mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)); `all` is the owner's explicit choice, never a fallback.
39
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-1881.
37
40
 
38
41
  ### WC-ACCT-2 · The profile is a word, never a flag
39
42
 
@@ -44,8 +47,9 @@ profile.
44
47
  - **Enforced by:** `cli-core: src/cli/profile.test.ts`.
45
48
  - **Example:** `max work bot messages send …` uses the same first word as `max work chats list`. `tg --json chats list`
46
49
  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.
50
+ - **Exceptions:** `mcp serve --profiles <names|all>` names the profiles one server serves ([WC-MCP-9](mcp.md#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)). It is used in
51
+ place of the first word, never with it.
52
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-84, NEED-294, NEED-1881.
49
53
 
50
54
  ### WC-ACCT-3 · A missing profile is an error that lists the choices
51
55
 
@@ -57,8 +61,8 @@ CLI with a `setup` command suggests it, and any other CLI suggests the command t
57
61
  - **Enforced by:** `cli-core: src/cli/settings.test.ts` · `cli-core: src/cli/program.test.ts`.
58
62
  - **Example:** ``name a profile: `tg <profile> …` — profiles here: personal, work (or set TG_PROFILE)``. With none,
59
63
  tg, which has `setup`, says to ``create one with `tg personal setup` (any name in place of personal)``. A CLI
60
- without `setup`, such as one made from cli-template, answers `cal events list` with ``name a profile: there is none
61
- on this computer yet — any name works, such as `cal personal events list` (or set CAL_PROFILE)`` (cli-core 0.424.0
64
+ without `setup`, such as one made from cli-template, answers `gcal events list` with ``name a profile: there is none
65
+ on this computer yet — any name works, such as `gcal personal events list` (or set GCAL_PROFILE)`` (cli-core 0.424.0
62
66
  and later).
63
67
  - **Exceptions:** none.
64
68
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
@@ -78,8 +82,8 @@ top-level command, such as `chats` or `bot`, is refused when the profile is crea
78
82
 
79
83
  ### WC-ACCT-5 · A profile lock pins a process to one profile
80
84
 
81
- When `<PREFIX>_PROFILE_LOCK` is set, a first word or `<PREFIX>_PROFILE` that names any other profile is refused with
82
- `permission_error`.
85
+ When `<PREFIX>_PROFILE_LOCK` is set, a first word, `<PREFIX>_PROFILE` or `mcp serve --profiles` that names any other
86
+ profile is refused with `permission_error`. `--profiles all` names every profile, so it is refused too.
83
87
 
84
88
  - **Why:** an agent must not escape its guards by naming a profile with fewer of them.
85
89
  - **Enforced by:** `cli-core: src/cli/settings.test.ts`.
@@ -155,6 +159,35 @@ inferred from a matching name, and `unlink` undoes it.
155
159
  - **Exceptions:** none.
156
160
  - **Applies to:** all CLIs. **Since:** cli-base 0.43.1. **Rulings:** none.
157
161
 
162
+ ### WC-ACCT-12 · Sign-in is `session start`, `status` and `end`, and `session list` names the profiles
163
+
164
+ Every CLI that signs in has four commands under `session`:
165
+
166
+ - `<cli> <profile> session start` signs the profile in.
167
+ - `<cli> <profile> session status` says whether this profile is signed in, and as whom.
168
+ - `<cli> <profile> session end` signs the profile out and forgets its login.
169
+ - `<cli> session list` takes no profile and lists the profiles on this computer.
170
+
171
+ `status` answers about one profile; `list` answers which profiles exist. Neither one connects to more than it must:
172
+ `list` reads only what the computer holds.
173
+
174
+ - **Why:** the owner and an agent look for sign-in in the same place in every CLI. Today each CLI has a different
175
+ part of the set, so an agent that learned one CLI guesses wrong in the next.
176
+ - **Enforced by:** review only.
177
+ - **Example:** `gcal personal session status` answers whether the `personal` profile holds a Google login;
178
+ `gcal session list` lists `personal` and `work` with no profile named.
179
+ - **Exceptions:** known gaps, as of 2026-10-27:
180
+ - tg, max and wa have `session start` and `session end`, no `session status`, and list their profiles with
181
+ `account list` instead of `session list`.
182
+ - drive has `session start`, `session list` and `session end`, and no `session status`.
183
+ - gcal has `session start`, `session status` and `session end`, and no `session list`.
184
+ - zm has `session start` and `session status`, and no `session end` or `session list`.
185
+ - memo signs in per mail account with `mail add` and has no `session` group.
186
+
187
+ A CLI that adds a missing command adds it under `session`; a renamed path keeps no alias
188
+ ([WC-CMD-8](commands.md#wc-cmd-8--keep-no-aliases-a-renamed-path-stops-working-in-the-release-that-renames-it)).
189
+ - **Applies to:** every CLI that signs in. **Since:** cli-base 0.65.0. **Rulings:** NEED-1884.
190
+
158
191
  ## Sources
159
192
 
160
193
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
@@ -20,7 +20,9 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
20
20
  - **Service:** a use case, written once, that both a command and an MCP tool call.
21
21
  - **Composition root:** the place in a CLI where real implementations are chosen and connected, usually
22
22
  `program.ts` and the command context.
23
- - **Vendor type:** a type from a provider library, such as `@mtcute/*` or `ws`.
23
+ - **Vendor type:** a type from a provider library, such as `@mtcute/*` or `ws`, and a type generated from a
24
+ provider's API description, such as gcal's and drive's types under `src/generated/` from a Google Discovery
25
+ document: both change when the provider changes, not when we do.
24
26
 
25
27
  ## Rules
26
28
 
@@ -57,7 +59,9 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
57
59
  `src/adapter/` maps the client onto the shared messenger port. A field that only one provider has travels in
58
60
  `providerMetadata`.
59
61
  - **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.
62
+ account's adapter, never above it. Generated API types may also be used by the generated `api <method>` commands,
63
+ which are the provider's own API by design; the shared ports and services never see them. In gcal,
64
+ `calendar-source.ts` and `calendar-writer.ts` translate the generated event type into cli-core's calendar entry.
61
65
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-12.
62
66
 
63
67
  ### WC-ARCH-4 · Put code in the lowest shared package that can hold it
@@ -77,14 +81,16 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
77
81
 
78
82
  ### WC-ARCH-5 · Build a feature that another provider could have as a shared command behind a capability
79
83
 
80
- - **Why:** a command written once in cli-core gives every messenger the same path, flags and output.
84
+ - **Why:** a command written once in cli-core gives every provider of a kind the same path, flags and output.
81
85
  - **Enforced by:** review only.
82
86
  - **Example:** topics and polls are port groups and shared commands in cli-core. A messenger that lacks a capability
83
87
  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
+ max share the bot code. Calendars have the same shape: cli-core's `addCalendarCommands` builds `calendars`,
89
+ `sync`, `status`, `events` and `search` over the `CalendarSource` and `CalendarWriter` ports, and mounts
90
+ `calendars busy` or the event writes only when the provider has them.
91
+ - **Exceptions:** a feature stays in one CLI only when the other providers cannot have it, such as MAX's socket server,
92
+ Telegram's app registration, or gcal's generated `api` commands.
93
+ - **Applies to:** messenger CLIs (tg, max, wa) and calendar CLIs (gcal). **Since:** cli-base 0.41.0. **Rulings:** NEED-820, NEED-440,
88
94
  NEED-301, NEED-444.
89
95
 
90
96
  ### WC-ARCH-6 · Replace a use case, not a command
@@ -93,9 +99,10 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
93
99
  - **Enforced by:** review only.
94
100
  - **Example:** `Messenger.services` receives the shared services and returns only the ones it changes:
95
101
  `services: (base) => ({ messages: { ...base.messages, list: maxList } })`. To add a subcommand, a CLI calls
96
- `addCommand` on the command a factory returns.
102
+ `addCommand` on the command a factory returns. A calendar CLI changes what the shared calendar commands do through
103
+ the source and writer it passes to `addCalendarCommands`, never by writing its own `sync` or `events list`.
97
104
  - **Exceptions:** none.
98
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
105
+ - **Applies to:** messenger CLIs and calendar CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
99
106
 
100
107
  ### WC-ARCH-7 · Keep policy in the service; a command or MCP handler only parses input and presents the result
101
108
 
@@ -215,9 +222,9 @@ out of search and listed under `skipped`, with the hint to run that CLI once.
215
222
  - **Why:** a new source joins search and memo without editing cli-core, and a provider nobody described is never read
216
223
  as something it is not (until this rule, an unlisted provider was searched as a messenger).
217
224
  - **Enforced by:** `cli-core: src/store/sqlite/providers.test.ts`.
218
- - **Example:** cal registers `{ name: "google-calendar", kind: "events", aliases: ["cal"], command: "cal" }` each
219
- time it starts. On a machine where cal has never run, a search across accounts lists cal's accounts under
220
- `skipped` instead of reading their rows as messages.
225
+ - **Example:** gcal registers `{ name: "google-calendar", kind: "events", aliases: ["gcal", "calendar"], command:
226
+ "gcal" }` each time it opens the store. On a machine where gcal has never run, a search across accounts lists
227
+ gcal's accounts under `skipped` instead of reading their rows as messages.
221
228
  - **Exceptions:** the built-in providers of cli-core need no entry. Two entries that disagree on a kind are an error
222
229
  naming both.
223
230
  - **Applies to:** cli-core and every CLI with a provider of its own. **Since:** cli-base 0.53.0. **Rulings:** none
@@ -10,8 +10,9 @@ Rule ids on this page are `WC-CI-<n>`. How to read a rule, how ids work and whic
10
10
  [Releases page](releases.md); which scanners must exist and how workflows are hardened is on the
11
11
  [Security page](security.md).
12
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. cli-template follows every rule here as a CLI does
13
+ "The WireCat repositories" on this page means every WireCat CLI (today tg, max, memo, drive, zoom, wa and gcal; a
14
+ CLI made with `npx @wirecat/cli-template new` counts from its first commit), cli-base, cli-core, cli-testing, cli-docs,
15
+ plus community itself. cli-template follows every rule here as a CLI does
15
16
  ([New CLI](new-cli.md#the-template-repository-itself)).
16
17
 
17
18
  ## Rules
@@ -24,7 +25,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
24
25
  `gitleaks git --staged --redact`. A clone without gitleaks gets a warning and can still commit; CI scans anyway.
25
26
  The `prepare` script installs the hooks; `LEFTHOOK=0 git commit` skips them for one commit.
26
27
  - **Exceptions:** none.
27
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
28
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
28
29
 
29
30
  ### WC-CI-2 · Add no push hook, and no hook that installs, builds, checks types, generates or tests
30
31
 
@@ -34,7 +35,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
34
35
  does not run `standards:check`, the full lint, spelling, a build or the test suite on each commit or push; it runs a
35
36
  targeted check when it needs one.
36
37
  - **Exceptions:** none. cli-base `config/lefthook.yml` still has a pre-push typecheck, but no repository extends it.
37
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
38
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
38
39
 
39
40
  ### WC-CI-3 · Run the fast standards job on every pull request and every push to `main`
40
41
 
@@ -45,7 +46,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
45
46
  commit cancels the run it replaces. The job times out after five minutes, a ceiling for a stalled setup, not an
46
47
  expected time.
47
48
  - **Exceptions:** none. Runner queues and cold downloads are outside the budget.
48
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
49
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
49
50
 
50
51
  ### WC-CI-4 · Keep `ci:quick` to early bug checks that finish within two minutes
51
52
 
@@ -61,7 +62,7 @@ cli-testing, cli-docs, plus community itself. cli-template follows every rule he
61
62
  - **Exceptions:** never put live-account suites, agent suites or platform jobs here. Full tests and coverage run in
62
63
  the pull request validation job ([WC-CI-5](#wc-ci-5--run-the-full-linux-validation-on-every-pull-request-and-keep-platform-suites-out)),
63
64
  not in `ci:quick`.
64
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
65
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
65
66
 
66
67
  ### WC-CI-5 · Run the full Linux validation on every pull request, and keep platform suites out
67
68
 
@@ -139,10 +140,13 @@ cannot pass yet; the `skip` input names it.
139
140
 
140
141
  The shared workflows are not in the npm package, because CI runs the pinned commit, not the installed copy. To see
141
142
  what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/…@<commit>` lines in `ci.yml`,
142
- `release.yml` and `monthly.yml`, and opens each workflow file at that commit in cli-base.
143
+ `release.yml` and `monthly.yml`, and opens each workflow file at that commit in cli-base. The fresh-agent audit on the
144
+ [New CLI](new-cli.md#done-when) page is given these files for this reason, beside the handbook and the repository;
145
+ its prompt in cli-template says so from cli-template 0.8.0. A CLI on an older template takes the prompt with
146
+ `upgrade`.
143
147
 
144
148
  - **Why:** one shared workflow keeps the full suite the same everywhere, and a pin keeps a cli-base change from
145
- breaking ten repositories at once.
149
+ breaking every repository at once.
146
150
  - **Enforced by:** `ci.yml:ci`.
147
151
  - **Example:** `uses: WireCatLabs/cli-base/.github/workflows/node-ci.yml@<commit> # v0.31.0`, with `cli: true` for
148
152
  the CLI steps (WC-CI-7), the project's extra checks in the `checks` input and `bun-run` for the Bun job. A job that only one repository needs stays beside it in
@@ -158,12 +162,29 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
158
162
  - **Example:** `monthly.yml`, from the community template, calls the repository's `ci.yml` and the pinned cli-base
159
163
  `release-checks.yml`. Each repository has its own minute, between 02:17 and 03:20 UTC, so runs do not overlap. No
160
164
  check compares the minutes across repositories; review keeps them apart. A CLI made from cli-template starts with
161
- the template's own minute and changes it (see [New CLI](new-cli.md)).
165
+ the template's own minute and changes it (see [New CLI](new-cli.md)). Taken minutes, from each `monthly.yml` on
166
+ the default branch:
167
+
168
+ | UTC | Repository |
169
+ |---|---|
170
+ | 02:17 | cli-base |
171
+ | 02:24 | whatsapp-cli |
172
+ | 02:27 | gcal-cli |
173
+ | 02:31 | cli-memo |
174
+ | 02:38 | cli-core |
175
+ | 02:45 | drive-cli |
176
+ | 02:52 | cli-testing |
177
+ | 02:59 | max-cli |
178
+ | 03:02 | cli-template |
179
+ | 03:06 | tg-cli |
180
+ | 03:13 | zoom-cli |
181
+ | 03:20 | cli-docs |
182
+
162
183
  - **Exceptions:** cli-core also builds and probes its native packages in their own workflows, after the window:
163
184
  `onnx.yml` at 03:27 and `sqlite.yml` at 03:37 UTC. A scheduled run of either never publishes; only a manual run
164
185
  with `publish` does. community has no build, so it runs only the release security checks, at 03:47 UTC. A monthly
165
186
  run never publishes a package or deploys the site.
166
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
187
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
167
188
 
168
189
  ### WC-CI-10 · Run heavy security checks only at release, monthly or on request
169
190
 
@@ -177,7 +198,7 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
177
198
  job ([WC-CI-5](#wc-ci-5--run-the-full-linux-validation-on-every-pull-request-and-keep-platform-suites-out)); it
178
199
  takes seconds. The dependency audit, `zizmor` and CodeQL stay out. Do not add a heavy scanner to an editor hook, a
179
200
  stop hook or an ordinary pull request job.
180
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** sc:9.
201
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** sc:9.
181
202
 
182
203
  ### 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)
183
204
 
@@ -195,7 +216,7 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
195
216
  release changes `main` the same way
196
217
  ([WC-REL-2](releases.md#wc-rel-2--main-changes-only-through-a-pull-request-also-during-a-release)).
197
218
  - **Exceptions:** none.
198
- - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** NEED-381.
219
+ - **Applies to:** the WireCat repositories. **Since:** cli-base 0.41.0. **Rulings:** NEED-381.
199
220
 
200
221
  ### WC-CI-13 · Start no live messenger or account test from a workflow
201
222
 
@@ -75,10 +75,12 @@ catalogue and parity manifest stay in cli-core
75
75
  - **Why:** an option that turns a command into a different report hides that report from anyone reading the tree.
76
76
  - **Enforced by:** review only.
77
77
  - **Example:** ranking is `stats messages top`, not a ranking mode on `search messages`. Options choose the scope,
78
- measure, order or format of the same action. No command both shows and changes something.
78
+ measure, order or format of the same action. No command both shows and changes something; saving what it read
79
+ into the local store is not a change ([WC-CMD-17](#wc-cmd-17--declare-effects-on-every-command)).
79
80
  - **Exceptions:** a documented preparatory fetch such as `--sync-first`. It keeps its own permissions and reports
80
81
  its outcome.
81
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
82
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1880 (`mcp revoke` is its own command; it
83
+ replaces NEED-1864, which kept `--revoke` as an option on `mcp`).
82
84
 
83
85
  ### WC-CMD-7 · Never let two commands overlap
84
86
 
@@ -116,9 +118,16 @@ catalogue and parity manifest stay in cli-core
116
118
  - **Why:** `--limit` must mean "how many items" in every command of every CLI.
117
119
  - **Enforced by:** `flag-synonym` · `flag-two-meanings` · `same-flag-meaning` (weekly audit; it does not block a pull
118
120
  request yet).
119
- - **Example:** `--limit <n>`, never `--max`, `--take` or `--count`. `--cursor <cursor>`, never `--offset`. How much
120
- to read is `--limit`, `--page` and `--page-size`. A count of pages is never an option.
121
- - **Exceptions:** generated commands (`bot api`) are exempt from the naming rules.
121
+ - **Example:** `--limit <n>`, never `--max`, `--take` or `--count`. `--cursor <cursor>`, never `--offset`. How big
122
+ the answer is: `--limit`, `--page` and `--page-size`; a count of pages is never a paging option. How much work a
123
+ command may do is a `budget` option: `--max-pages <pages>` caps the pages a sync reads from the source, and
124
+ `--max-output-bytes` the size of the answer. A budget changes how far the command gets, never which items one page
125
+ of the answer holds. A retry after `outcome_unknown` is `--operation-id`: the attempt in the send journal. The
126
+ calendar vocabulary's `--id <id>` is a different value: the provider's own id for a thing being created, chosen
127
+ before the create, so a retry finds it at the provider. A CLI whose provider takes such an id may offer both.
128
+ - **Exceptions:** generated commands (`bot api`) are exempt from the naming rules. Known gap, as of 2026-10-27: gcal's
129
+ `events create` offers `--id` and no `--operation-id`, so its `outcome_unknown` names the event id instead of an
130
+ `operation_id` (WC-LIFE-9, WC-OUT-10).
122
131
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
123
132
 
124
133
  ### WC-CMD-11 · Name an option in plain words, with the kind of value in the name
@@ -128,7 +137,10 @@ catalogue and parity manifest stay in cli-core
128
137
  - **Example:** `--operation-id`, not `--cid`. `--before-id` takes a message id, `--after-time` a time, `--before-n` a
129
138
  count: two kinds are two options. A length of time is a `<duration>` with a unit (`500ms`, `30s`, `2m`, `4h`, `1d`),
130
139
  never a bare number.
131
- - **Exceptions:** none.
140
+ - **Exceptions:** `--duration-minutes <minutes>` (zm's meeting length, gcal's `calendars busy`) takes a bare count of
141
+ minutes. It is a known gap in the vocabulary, as of 2026-10-27: it stays until the vocabulary has
142
+ `--duration <duration>`, and then moves in the release that adds it, under "Changed — may break callers". No new
143
+ option follows it.
132
144
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
133
145
 
134
146
  ### WC-CMD-12 · Give every option a role from the vocabulary, and filter a list with its query
@@ -187,13 +199,25 @@ Each of these is set with `annotate(command, { … })` from `@wirecat/cli-base/c
187
199
 
188
200
  ### WC-CMD-17 · Declare `effects` on every command
189
201
 
202
+ A command **writes** when it changes something on the service (`remote: "write"`), or when it changes the local
203
+ store or files without reading the service first (`remote: "none"`, `local: "write"`, such as `store reindex` or
204
+ `config set`). A command that reads the service and only saves what it read into the owner's local store
205
+ (`{ remote: "read", local: "write" }`, such as `sync` or `calendars list`) is a **read**: on the command line, in
206
+ [WC-CMD-6](#wc-cmd-6--give-one-action-or-one-report-one-leaf-command), over MCP
207
+ ([WC-MCP-4](mcp.md#wc-mcp-4--read-runs-only-reading-commands-and-write-only-writing-ones)) and as a next step
208
+ ([WC-OUT-9](output.md#wc-out-9--offer-at-most-two-next-steps-and-mark-a-step-safe-only-when-it-changes-nothing-on-the-service)).
209
+
190
210
  - **Why:** retries and unasked next steps are judged on effects, and a command without them counts as a write.
191
211
  - **Enforced by:** `missing-command-effects` · `cli-core: src/cli/effects.test.ts` · `cli-memo: src/effects.test.ts`.
192
212
  - **Example:** `annotate(send, { effects: { remote: "write", local: "write" } })`. Each side is `none`, `read` or
193
213
  `write`. A sync that reads the service and writes the local store is `{ remote: "read", local: "write" }`.
194
214
  - **Exceptions:** none. Without `effects`, the error handler never marks a failure safe to retry, and a next step
195
- that runs the command is never safe to run unasked.
196
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
215
+ that runs the command is never safe to run unasked. Known gap, as of 2026-10-27: cli-base's `writes()`
216
+ (`src/standards/effects.ts`) still counts every local write as a write, so cli-core's MCP router serves `sync` and
217
+ `calendars list` through the `write` tool. The follow-up makes `writes()` true for `remote: "write"`, or for
218
+ `local: "write"` when `remote` is `none`.
219
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
220
+ owner's ruling on what counts as a read over MCP (2026-10-27).
197
221
 
198
222
  ### WC-CMD-18 · Declare `lifetime` and `cooperative` instead of matching command names
199
223
 
@@ -327,8 +351,9 @@ The references, each for its own part:
327
351
  [WC-OUT-18](output.md#wc-out-18--exit-with-cli-bases-code-for-each-kind-of-failure), prompts follow
328
352
  [WC-CMD-15](#wc-cmd-15--never-prompt-when-no-person-can-answer), and a renamed path keeps no alias
329
353
  ([WC-CMD-8](#wc-cmd-8--keep-no-aliases-a-renamed-path-stops-working-in-the-release-that-renames-it)), unlike the
330
- deprecation advice of the Command Line Interface Guidelines. A read may have documented local effects, such as storing what it read or recording a failed
331
- run, so a failed command does not promise that it wrote nothing. A write is never retried on its own, and a tool's
354
+ deprecation advice of the Command Line Interface Guidelines. A read may have documented local effects, such as
355
+ storing what it read or recording a failed run, so a failed command does not promise that it wrote nothing; what
356
+ counts as a read is in [WC-CMD-17](#wc-cmd-17--declare-effects-on-every-command). A write is never retried on its own, and a tool's
332
357
  whole payload is never logged. The `stats` root and its resources are the project's own choice.
333
358
  - **Exceptions:** the CLI Agent Spec's rule that help goes to stderr when stdout is not a terminal is not adopted. The
334
359
  Agent Client Protocol links an editor to a coding agent; a WireCat CLI is a tool an agent calls, so it adds no
@@ -37,6 +37,8 @@ is rewritten only to follow it:
37
37
  entity's own fields (WC-DATA-2).
38
38
  - `meeting_participants` is unique on the meeting and the contact; its `external_id` is in no unique key (WC-DATA-3).
39
39
 
40
+ The calendar tables that gcal fills through cli-core (`calendars`, `calendar_entries`) follow them from the start.
41
+
40
42
  ## Rules
41
43
 
42
44
  ### WC-DATA-1 · A record more than one tool needs is a core entity in cli-core; a CLI's own tables stay private
@@ -73,7 +75,9 @@ to its entity. One entity may combine source records from several sources and ac
73
75
  ### WC-DATA-3 · A provider's id goes in `external_id`
74
76
 
75
77
  The id a provider gives a record is stored in `external_id`, unique with `account_id` (or with `provider` where the
76
- record has no account). Never in a column named after the provider (`google_id`, `file_id`) and never only in
78
+ record has no account). Where the provider's id is unique only inside a container the record belongs to, it is unique
79
+ with that container instead: a calendar entry's `external_id` is unique with its `calendar_id`, because the same event
80
+ id appears in each calendar that holds a copy of the event, and the calendar is unique with `account_id`. Never in a column named after the provider (`google_id`, `file_id`) and never only in
77
81
  `metadata`. A second id the provider also uses gets a named column of its own (`ical_uid`).
78
82
 
79
83
  - **Why:** one place to look for every provider's id, and one unique key to update a record by.
@@ -37,7 +37,10 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
37
37
  - **Enforced by:** review only.
38
38
  - **Example:** a claim that turns out wrong is rewritten to the current fact, with no "correction" note and no dated
39
39
  "measured on" trail. A finished plan or handoff is deleted. A done backlog or cleanup line is deleted.
40
- - **Exceptions:** `CHANGELOG.md` is the one place with version history.
40
+ - **Exceptions:** `CHANGELOG.md` is the one place with version history. A template-owned file (`AGENTS.md`,
41
+ `docs/dev/TESTING.md`, the skills under `docs/dev/skills/` and the others
42
+ [New CLI](new-cli.md) lists) that has turned false is fixed in cli-template, never only in the CLI: `upgrade` brings
43
+ the fix to every CLI, and a local edit would make `upgrade` ask about it each time.
41
44
  - **Applies to:** all CLIs, user and developer pages. **Since:** cli-base 0.41.0. **Rulings:** none.
42
45
 
43
46
  ### WC-DOC-4 · A generated page is never edited by hand
@@ -12,17 +12,18 @@ This page is a reference, not a rule page: it assigns no rule ids. How to start
12
12
 
13
13
  ## What the template imports
14
14
 
15
- The list is what the template's `src/` and `scripts/` import today, type imports and re-exports included. `kit/` imports
15
+ The list is what the template's `src/` and `scripts/` import today, type imports, re-exports and imports loaded on
16
+ demand included. `kit/` imports
16
17
  nothing from either package.
17
18
 
18
19
  | Package and path | Exports |
19
20
  |---|---|
20
21
  | `@wirecat/cli-base` | `catchCrashes`, `CliError`, `configFilePath`, `mcpErrorResult`, `resolvePaths` |
21
- | `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `describeProgram` |
22
+ | `@wirecat/cli-base/commands` | `annotate`, `CommandInfo`, `COMMANDS_PAGE_LABELS` (in `scripts/`), `commandsPage` (in `scripts/`), `describeOptions` (in `scripts/`), `describeProgram` |
22
23
  | `@wirecat/cli-base/codegen` | `writeArtifacts` (in `scripts/`) |
23
- | `@wirecat/cli-base/release` | `DocsRules`, `JOURNAL_IDS`, `markdownFiles`, `packContents` (in `scripts/`) |
24
- | `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `itemOf`, `LOCAL_READ`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
25
- | `@wirecat/cli-core/mcp` | `McpCommand`, `registerCommands`, `runCommandForAgent`, `toolsFromCommands` |
24
+ | `@wirecat/cli-base/release` | `DocsRules`, `JOURNAL_IDS`, `markdownFiles`, `packContents` (all in `scripts/`) |
25
+ | `@wirecat/cli-core/cli` | `AppIdentity`, `changesAuthorityAt`, `commandHelpMarkdown` (in `scripts/`), `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `httpTokenFile`, `itemOf`, `LOCAL_READ`, `LOCAL_WRITE`, `MCP_HTTP_DEFAULT_PORT`, `mcpServerCommand`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `refuseUnderReadOnly`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
26
+ | `@wirecat/cli-core/mcp` | `httpLockFile`, `MCP_PATH`, `McpCommand`, `portOf`, `publicUrlOf`, `registerCommands`, `revokeAll`, `runCommandForAgent`, `serveOverHttp`, `toolsFromCommands` |
26
27
  | `@wirecat/cli-core/sends` | `Level` |
27
28
  | `@wirecat/cli-core/update` | `appUpdate` |
28
29
  | `@wirecat/cli-core/testing` | `ARGV_LOG`, `argvLogSetup` |
@@ -122,6 +122,38 @@ Rule ids on this page are `WC-LAY-<n>`. How to read a rule, how ids work and whi
122
122
  - **Exceptions:** none.
123
123
  - **Applies to:** all CLIs and libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
124
124
 
125
+ ### WC-LAY-10 · Ship the bin as a bundle that `cli-dev bundle` builds
126
+
127
+ A command that imports about a thousand module files spends most of its start resolving and reading them. tg's
128
+ bundle halved `--help` and every other short command
129
+ ([measurements](https://github.com/WireCatLabs/cli-testing/blob/main/performance/startup/tg-bundle/README.md)).
130
+
131
+ - **Why:** one shared build keeps what tg learned in one place: lazy imports stay lazy, files and packages are found
132
+ where they were installed, and a running server survives an in-place upgrade.
133
+ - **Enforced by:** `<app>: pnpm build` · `<app>: pnpm smoke:node` · `<app>: pnpm smoke:bun`.
134
+ - **Example:** `"build": "tsc --build --force && cli-dev bundle --bin dist/bin/tg.js --external '@mtcute/*' --external undici"`,
135
+ with `esbuild` as a devDependency in cli-base's peer range. `dist/bundle/` holds the bundle, split so every lazy
136
+ `import()` stays a chunk of its own; `dist/bin/tg.js` stays the path `"bin"` names and becomes a loader. What it
137
+ does and checks:
138
+ - The loader answers `--version` from the version built into it, turns the compile cache on, and imports the
139
+ bundle. Under `serve`, `watch` and `mcp` it loads every chunk 2 s after start and resolves imports between chunks
140
+ without looking on disk: an upgrade deletes the old chunks, whose names carry a hash.
141
+ - In the CLI's own modules and in cli-core's and cli-base's (`--anchor` adds a package), `import.meta.url` is the
142
+ module's place before bundling, so skills, fonts, scenarios, migrations and child scripts are found. A package
143
+ they load at run time resolves from that place too (a resolve hook under Node, `Bun.resolveSync` under Bun):
144
+ pnpm installs cli-core's dependencies beside cli-core.
145
+ - A dynamic import of CommonJS (`yaml`) keeps its named exports. Names are kept (`keepNames`), so `commands --json`
146
+ and errors read as before.
147
+ - Native modules, Bun's SQLite and the packages cli-core loads by name stay outside; `--external` adds the CLI's
148
+ own (`*` matches anything).
149
+ - The build fails when a bundled module would look for a file or package from the chunk's place, or when the loader
150
+ names a chunk that was not built. A third-party file read and found harmless is named with `--harmless`.
151
+ - The CLI's smoke test runs the built bin under Node and Bun and checks that the store opens and the document
152
+ readers resolve.
153
+ - **Exceptions:** a CLI moves when its outputs are byte-identical before and after and its start gains 15% or more;
154
+ until then it ships `tsc`'s output. cli-base, cli-core and cli-testing ship libraries, not a bin.
155
+ - **Applies to:** all CLIs. **Since:** cli-base 0.64.0. **Rulings:** none.
156
+
125
157
  ## Sources
126
158
 
127
159
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
@@ -20,9 +20,13 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
20
20
 
21
21
  ### Who follows these rules today
22
22
 
23
- - **WC-LIFE-15:** tg, max, wa and the template. drive, memo and zm do not keep runs yet; each is adding them on a
23
+ - **WC-LIFE-15:** tg, max, wa, gcal and the template. drive, memo and zm do not keep runs yet; each is adding them on a
24
24
  `feat/runs` branch (2026-10-25). zm's `resolveSettings` also returns `keepFailedRuns: false`, which turns off the
25
25
  failure log cli-core's `run` would keep.
26
+ - **WC-LIFE-16:** cli-core's shared calendar `sync`, from cli-core 0.464.0 (cli-core #2118); gcal gets it when it
27
+ moves up from cli-core 0.459.0. The deadlines of WC-LIFE-5 come with the same release. The other syncs
28
+ (`messages sync` in tg, max and wa, drive's `sync`, memo's mail import, zm's `recordings sync`) are cooperative but
29
+ spend no request budget yet, and gcal sets no `requestMs` of its own, so it uses the 2 s default.
26
30
 
27
31
  ## One-shot commands
28
32
 
@@ -74,15 +78,31 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
74
78
 
75
79
  ### WC-LIFE-5 · Bound every one-shot command by a deadline
76
80
 
77
- - **Why:** a command that waits for ever on a dead connection never fails, so nobody learns why it hangs.
81
+ | Command | At a terminal or in a script | Called over MCP |
82
+ |---|---|---|
83
+ | one-shot | 30 s | 30 s |
84
+ | `cooperative` ([WC-LIFE-10](#wc-life-10--mark-a-long-command-cooperative-when-it-should-stop-at-its-own-pace-and-keep-its-work)) | 5 min | 45 s |
85
+
86
+ `--timeout 2m` or the `<PREFIX>_TIMEOUT` variable sets the deadline, higher or lower. Past the deadline a one-shot
87
+ command fails with `timeout` (exit 9).
88
+
89
+ - **Why:** a command that waits for ever on a dead connection never fails, so nobody learns why it hangs. Over MCP
90
+ the client cuts a tool call at about 60 s: the MCP TypeScript SDK's `DEFAULT_REQUEST_TIMEOUT_MSEC` is 60000
91
+ (`@modelcontextprotocol/sdk` 1.32.1, `dist/esm/shared/protocol.js:8`), and users report the same for Claude Desktop
92
+ and ChatGPT. A cooperative command answers at 45 s, so the agent gets the answer and the way to continue instead of
93
+ a cut call. Claude Code waits longer ([its MCP page](https://code.claude.com/docs/en/mcp)), but a CLI does not know
94
+ its client, so it takes the shortest.
78
95
  - **Enforced by:** `cli-core: src/cli/deadline.test.ts` · `cli-core: src/cli/program.test.ts`.
79
- - **Example:** the default is 30 seconds. `--timeout 2m` or the `<PREFIX>_TIMEOUT` variable changes it. Past the
80
- deadline the command fails with `timeout` (exit 9).
96
+ - **Example:** from cli-core 0.464.0, a calendar `sync` in a terminal may run 5 minutes; the same command called over
97
+ MCP stops at 45 seconds with what it saved. cli-core's `DEFAULT_COMMAND_MS`, `DEFAULT_COOPERATIVE_MS` and
98
+ `DEFAULT_AGENT_COOPERATIVE_MS` (`src/cli/execution.ts`) hold the three values, and `run()` picks one when it starts
99
+ the command.
81
100
  - **Exceptions:** a `persistent` command has no deadline and ignores `--timeout`. An `interactive` command has none
82
101
  while a person is at the terminal; run headless, it keeps the deadline. `--help` and `--version` need none. A
83
102
  background fetch started with `<PREFIX>_BACKFILL_JOB` is bounded only by an explicit `--timeout`.
84
103
  - **Applies to:** CLIs that run on cli-core's `run()`; other CLIs use cli-base's `createDeadline`. **Since:** cli-base
85
- 0.41.0. **Rulings:** none.
104
+ 0.41.0 (the cooperative deadlines: cli-base 0.65.0, cli-core 0.464.0). **Rulings:** the owner's deadlines and
105
+ request budgets (2026-10-27).
86
106
 
87
107
  ### WC-LIFE-6 · Open nothing until an action needs it
88
108
 
@@ -131,7 +151,9 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
131
151
  - **Enforced by:** review only.
132
152
  - **Example:** `annotate(fetch, { cooperative: true })`. The runner then does not cut the command when the run is
133
153
  aborted from outside. The command passes the signal to every operation that can be cancelled, checks it before
134
- each write, and stops after the current step. `persistent` commands count as cooperative.
154
+ each write, and stops after the current step. `persistent` commands count as cooperative. A cooperative command
155
+ gets the longer deadline of WC-LIFE-5, and a sync among them spends a request budget (WC-LIFE-16), so it stops
156
+ before the deadline instead of at it.
135
157
  - **Exceptions:** none.
136
158
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
137
159
 
@@ -147,14 +169,15 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
147
169
  a CLI's own commands included, connects through `connectFor`: a relayed run gets serve's own adapter, and a run for
148
170
  another profile is refused. With no live `serve`, the command connects on its own. A `serve` that offers the socket
149
171
  but does not answer gives `provider_unavailable`, never a second connection.
150
- - **Exceptions:** `serve`, `server`, `bot`, logins, `--offline`, `--dry-run` and `mcp --http` are not relayed.
172
+ - **Exceptions:** `serve`, `server`, `bot`, logins, `--offline`, `--dry-run` and `mcp serve --http` are not relayed.
151
173
  - **Applies to:** messenger CLIs with an exclusive connection (wa today). **Since:** cli-base 0.41.0.
152
174
  **Rulings:** NEED-1389.
153
175
 
154
176
  ## Resumable syncs
155
177
 
156
178
  A long sync — a mailbox, a chat history, a Drive folder, a recording archive — is stopped by a kill, a reboot or a
157
- provider's limit as often as it ends. These three rules make the next run carry on.
179
+ provider's limit as often as it ends. These three rules make the next run carry on, and WC-LIFE-16 stops a run
180
+ before its deadline.
158
181
 
159
182
  ### WC-LIFE-12 · Save a long sync's work per item or per page, so a kill loses nothing saved
160
183
 
@@ -221,6 +244,44 @@ every value that may be content
221
244
  - **Exceptions:** none.
222
245
  - **Applies to:** all CLIs. **Since:** cli-base 0.56.0. **Rulings:** none.
223
246
 
247
+ ## Request budgets
248
+
249
+ A sync that resumes (WC-LIFE-12 to WC-LIFE-14) also stops on its own before its deadline.
250
+
251
+ ### WC-LIFE-16 · A sync spends a request budget sized to its deadline, and its answer says how to continue
252
+
253
+ Before each request to the provider, a sync asks its budget whether one more may go. The budget is
254
+
255
+ `requests = floor((deadline − margin) / requestMs)`, at least one,
256
+
257
+ where `margin` is the larger of 5 s and 15 % of the deadline, and `requestMs` is how long one request of this CLI is
258
+ expected to take: one constant per CLI, `AppIdentity.requestMs` (2 s when it is not set). `--timeout` changes the
259
+ deadline, and the budget is computed again from it; `--max-pages <pages>` can only lower it. The budget also stops
260
+ when the deadline is closer than the margin plus the slowest request so far, and when the run is cancelled.
261
+
262
+ When the budget stops the sync before its end, the sync saves where it is (WC-LIFE-12), exits 0 as a declared
263
+ `partial` answer ([WC-OUT-8](output.md#wc-out-8--exit-0-on-a-partial-answer-only-when-the-command-declares-partial))
264
+ and says in the answer why it stopped and how far it got. Its `next` holds the exact command that continues it, the
265
+ same `sync` with the same budget and timeout, marked safe to run unasked; over MCP the step carries that command and
266
+ its arguments for the `read` tool. A run that moved nothing on offers no step, so an agent cannot loop. The sync's
267
+ status command answers how far it is
268
+ ([WC-LIFE-14](#wc-life-14--a-syncs-status-command-says-how-far-it-is-in-one-shape)).
269
+
270
+ - **Why:** a first sync of a large account does not fit one deadline. Without a budget the deadline cuts it mid-page
271
+ and the run loses its round; with one, every run ends with an answer, saves its work and says how to go on. One
272
+ `requestMs` per CLI, in one place, keeps the arithmetic the same in every sync of that CLI.
273
+ - **Enforced by:** `cli-core: src/background/budget.test.ts` · `cli-core: src/calendar/testing/sync-contract.ts`.
274
+ - **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with the 2 s default about 19 requests. It
275
+ answers `state: "partial"`, `stopped: "requests"`, `progress: { calendars, calendarsDone, requests, saved }`, and
276
+ `next` with `gcal personal sync`; `gcal personal status` shows each calendar's progress. cli-core builds the budget
277
+ with `runBudgetOf(command, maxPages)` from `@wirecat/cli-core/cli` over `runBudget` in
278
+ `@wirecat/cli-core/background`.
279
+ - **Exceptions:** known gaps, as of 2026-10-27: only the calendar sync spends a budget (see "Who follows"). The
280
+ continuing step names the command, not the MCP tool, and until the WC-CMD-17 follow-up the router sends that
281
+ command to `write`, not `read`.
282
+ - **Applies to:** every CLI with a sync. **Since:** cli-base 0.65.0. **Rulings:** the owner's deadlines and request
283
+ budgets (2026-10-27).
284
+
224
285
  ## Sources
225
286
 
226
287
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page