@wirecat/cli-base 0.64.0 → 0.65.1

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.
@@ -75,11 +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
- its outcome. `mcp --revoke` forgets every login given to a browser app; it stays an option on `mcp` because it
81
- belongs with the HTTP server's login state.
82
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1864.
81
+ its outcome.
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`).
83
84
 
84
85
  ### WC-CMD-7 · Never let two commands overlap
85
86
 
@@ -117,9 +118,16 @@ catalogue and parity manifest stay in cli-core
117
118
  - **Why:** `--limit` must mean "how many items" in every command of every CLI.
118
119
  - **Enforced by:** `flag-synonym` · `flag-two-meanings` · `same-flag-meaning` (weekly audit; it does not block a pull
119
120
  request yet).
120
- - **Example:** `--limit <n>`, never `--max`, `--take` or `--count`. `--cursor <cursor>`, never `--offset`. How much
121
- to read is `--limit`, `--page` and `--page-size`. A count of pages is never an option.
122
- - **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).
123
131
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
124
132
 
125
133
  ### WC-CMD-11 · Name an option in plain words, with the kind of value in the name
@@ -129,7 +137,10 @@ catalogue and parity manifest stay in cli-core
129
137
  - **Example:** `--operation-id`, not `--cid`. `--before-id` takes a message id, `--after-time` a time, `--before-n` a
130
138
  count: two kinds are two options. A length of time is a `<duration>` with a unit (`500ms`, `30s`, `2m`, `4h`, `1d`),
131
139
  never a bare number.
132
- - **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.
133
144
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
134
145
 
135
146
  ### WC-CMD-12 · Give every option a role from the vocabulary, and filter a list with its query
@@ -188,13 +199,25 @@ Each of these is set with `annotate(command, { … })` from `@wirecat/cli-base/c
188
199
 
189
200
  ### WC-CMD-17 · Declare `effects` on every command
190
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
+
191
210
  - **Why:** retries and unasked next steps are judged on effects, and a command without them counts as a write.
192
211
  - **Enforced by:** `missing-command-effects` · `cli-core: src/cli/effects.test.ts` · `cli-memo: src/effects.test.ts`.
193
212
  - **Example:** `annotate(send, { effects: { remote: "write", local: "write" } })`. Each side is `none`, `read` or
194
213
  `write`. A sync that reads the service and writes the local store is `{ remote: "read", local: "write" }`.
195
214
  - **Exceptions:** none. Without `effects`, the error handler never marks a failure safe to retry, and a next step
196
- that runs the command is never safe to run unasked.
197
- - **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).
198
221
 
199
222
  ### WC-CMD-18 · Declare `lifetime` and `cooperative` instead of matching command names
200
223
 
@@ -328,8 +351,9 @@ The references, each for its own part:
328
351
  [WC-OUT-18](output.md#wc-out-18--exit-with-cli-bases-code-for-each-kind-of-failure), prompts follow
329
352
  [WC-CMD-15](#wc-cmd-15--never-prompt-when-no-person-can-answer), and a renamed path keeps no alias
330
353
  ([WC-CMD-8](#wc-cmd-8--keep-no-aliases-a-renamed-path-stops-working-in-the-release-that-renames-it)), unlike the
331
- 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
332
- 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
333
357
  whole payload is never logged. The `stats` root and its resources are the project's own choice.
334
358
  - **Exceptions:** the CLI Agent Spec's rule that help goes to stderr when stdout is not a terminal is not adopted. The
335
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,7 +12,8 @@ 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 |
@@ -20,9 +21,9 @@ nothing from either package.
20
21
  | `@wirecat/cli-base` | `catchCrashes`, `CliError`, `configFilePath`, `mcpErrorResult`, `resolvePaths` |
21
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`, `commandHelpMarkdown` (in `scripts/`), `commandsCommand`, `configCommand`, `Configuration`, `createProgram`, `environmentOf`, `GlobalFlags`, `httpTokenFile`, `itemOf`, `LOCAL_READ`, `MCP_HTTP_DEFAULT_PORT`, `mcpServerCommand`, `outputFor`, `privateFiles`, `profileFree`, `ProgramDefinition`, `ProgramOptions`, `readOnlyLockOf`, `refuseUnderReadOnly`, `REMOTE_WRITE`, `renderAnswer`, `rootOf`, `run`, `RunOptions`, `runsCommand`, `settingsFor`, `skillCommand`, `upgradeCommand` |
25
- | `@wirecat/cli-core/mcp` | `httpLockFile`, `MCP_PATH`, `McpCommand`, `portOf`, `publicUrlOf`, `registerCommands`, `runCommandForAgent`, `serveOverHttp`, `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` |
@@ -131,7 +131,7 @@ bundle halved `--help` and every other short command
131
131
  - **Why:** one shared build keeps what tg learned in one place: lazy imports stay lazy, files and packages are found
132
132
  where they were installed, and a running server survives an in-place upgrade.
133
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"`,
134
+ - **Example:** `"build": "tsc --build --force && cli-dev bundle --bin dist/bin/tg.js --external \"@mtcute/*\" --external undici"`,
135
135
  with `esbuild` as a devDependency in cli-base's peer range. `dist/bundle/` holds the bundle, split so every lazy
136
136
  `import()` stays a chunk of its own; `dist/bin/tg.js` stays the path `"bin"` names and becomes a loader. What it
137
137
  does and checks:
@@ -145,7 +145,8 @@ bundle halved `--help` and every other short command
145
145
  - A dynamic import of CommonJS (`yaml`) keeps its named exports. Names are kept (`keepNames`), so `commands --json`
146
146
  and errors read as before.
147
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).
148
+ own (`*` matches anything). Quote a pattern with double quotes: pnpm runs scripts through `cmd.exe` on Windows,
149
+ which keeps single quotes, so `'@mtcute/*'` would match nothing there.
149
150
  - The build fails when a bundled module would look for a file or package from the chunk's place, or when the loader
150
151
  names a chunk that was not built. A third-party file read and found harmless is named with `--harmless`.
151
152
  - The CLI's smoke test runs the built bin under Node and Bun and checks that the store opens and the document
@@ -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
@@ -15,9 +15,12 @@ Rule ids on this page are `WC-MCP-<n>`. How to read a rule, how ids work and whi
15
15
 
16
16
  ### Who follows these rules today
17
17
 
18
- tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp`) and bot server (`<cli> <profile> bot mcp`).
19
- zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp`, cli-core's
20
- `toolsFromCommands` and `runCommandForAgent`). memo and drive have their own servers: memo has hand-written tools, and
18
+ tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) and bot server (`<cli> <profile> bot mcp serve`).
19
+ `mcp serve --profiles` ([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) is not
20
+ built yet: cli-core serves one named profile.
21
+ zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
22
+ `toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp`; it moves to `mcp serve`
23
+ when it takes cli-core 0.463.0). memo and drive have their own servers: memo has hand-written tools, and
21
24
  drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
22
25
  gaps against these rules.
23
26
 
@@ -66,12 +69,22 @@ Every entry in the catalogue must be a command the CLI has.
66
69
 
67
70
  Every command says whether it writes. `read` refuses a command that writes, and `write` refuses one that only reads.
68
71
  Both declare the answer envelope (a list `{ items, page, … }` or one thing `{ item, … }`) as their `outputSchema`.
69
-
70
- - **Why:** an agent host can let `read` run freely and ask a person before `write`.
72
+ A command that reads the service and only saves what it read into the owner's local store, such as `sync` or
73
+ `calendars list`, is a read and runs through `read`
74
+ ([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command) defines it).
75
+
76
+ - **Why:** an agent host can let `read` run freely and ask a person before `write`. `read` carries the MCP
77
+ `readOnlyHint`, which the [MCP schema](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations)
78
+ describes as "the tool does not modify its environment". A local copy of what the service already holds changes
79
+ nothing the owner would have to undo, so a sync stays under it; sending it through `write` would make a host ask a
80
+ person before every refresh.
71
81
  - **Enforced by:** `cli-core: src/mcp/mcp.test.ts` · `cli-core: src/mcp/output-schema.test.ts`.
72
- - **Example:** `max_read { "command": "messages send", … }` is refused.
73
- - **Exceptions:** none.
74
- - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
82
+ - **Example:** `max_read { "command": "messages send", … }` is refused. Once the gap below is closed,
83
+ `gcal_read { "command": "sync" }` runs.
84
+ - **Exceptions:** none. Known gap, as of 2026-10-27: cli-core decides with cli-base's `writes()`, which still counts
85
+ the local write, so `sync` runs through `write` until that follow-up lands (WC-CMD-17).
86
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0 (what counts as a read: cli-base 0.65.0). **Rulings:** the
87
+ owner's ruling on what counts as a read over MCP (2026-10-27).
75
88
 
76
89
  ### WC-MCP-5 · Unknown arguments are refused before anything runs
77
90
 
@@ -116,29 +129,36 @@ profile's permissions, the recipient list and the hourly limit decide.
116
129
 
117
130
  ### WC-MCP-8 · No `mcp` flag grants access
118
131
 
119
- `mcp --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
132
+ `mcp serve --permission <key=level>` may only make a level stricter for that server. An override that would loosen the
120
133
  profile is refused at start-up with `validation_error` naming the key. This includes a parent key that would hide a
121
- stricter key under it, and lifting a built-in `ask`. `mcp setup --allow-writes` only confirms, at install time, that
134
+ stricter key under it, and lifting a built-in `ask`. `mcp add <client> --allow-writes` only confirms, at install time, that
122
135
  the owner saw the profile offers writes.
123
136
 
124
137
  - **Why:** whoever writes the agent's MCP config must not get more than the profile allows.
125
138
  - **Enforced by:** `cli-core: src/cli/messenger/mcp-command-http.test.ts`.
126
- - **Example:** `max work mcp --permission messages=readonly` works. `max work mcp --permission messages.delete=allow`
139
+ - **Example:** `max work mcp serve --permission messages=readonly` works. `max work mcp serve --permission messages.delete=allow`
127
140
  is refused, because the built-in level is `ask`.
128
- - **Exceptions:** `bot mcp` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
141
+ - **Exceptions:** `bot mcp serve` takes no `--permission` at all. cli-base's flag registry still describes `--allow-writes`
129
142
  as "permit writing"; that wording is a gap.
130
143
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1423.
131
144
 
132
- ### WC-MCP-9 · A server speaks for one named profile over one connection
145
+ ### WC-MCP-9 · A server serves the profiles it is told, with one connection for each
133
146
 
134
- `mcp` needs a profile, like every command ([WC-ACCT-1](accounts.md)). The server opens one messenger connection and
135
- every call shares it.
147
+ `mcp serve` serves one named profile (`<cli> <profile> mcp serve`), or the profiles that `--profiles` lists:
148
+ `--profiles work,personal`, or `--profiles all` for every profile on this computer. With neither, it refuses with
149
+ `validation_error` and lists the profiles, like any missing profile ([WC-ACCT-3](accounts.md)). It never serves every
150
+ profile by default. The server opens one messenger connection for each profile it serves, and every call for that
151
+ profile shares it. When it serves more than one profile, each call names its profile; a call without one is refused
152
+ and lists the profiles served. Each call follows its own profile's permissions.
136
153
 
137
- - **Why:** a connection per call would break the messenger's pace limits and look unlike the official client.
154
+ - **Why:** which accounts an agent can reach is the owner's choice, written in the agent's config. A connection per
155
+ call would break the messenger's pace limits and look unlike the official client.
138
156
  - **Enforced by:** review only.
139
- - **Example:** `max work mcp`. Plain `max mcp` stops and lists the profiles.
157
+ - **Example:** `max work mcp serve`, `max mcp serve --profiles work,personal` or `max mcp serve --profiles all`.
158
+ Under the last two, a call reads `max_read { "profile": "work", "command": "inbox", "arguments": {} }`.
159
+ `max mcp serve` alone is refused, and its message lists `personal` and `work`.
140
160
  - **Exceptions:** the bot server needs no held connection: each Bot API call is plain HTTPS.
141
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
161
+ - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-1881.
142
162
 
143
163
  ### WC-MCP-10 · A tool and its command give the same answer and the same error
144
164
 
@@ -170,15 +190,16 @@ it carries.
170
190
 
171
191
  ### WC-MCP-12 · An MCP server over HTTP follows the same permissions and short-lived logins
172
192
 
173
- `mcp --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
193
+ `mcp serve --http` serves on `127.0.0.1` for agents in a browser, behind the owner's tunnel. Writes follow the profile's
174
194
  permissions. An access token lives one hour and a refresh token 30 days, replaced on each use. To log in, a browser app
175
- needs a one-time code that the owner reads in a terminal. `--revoke` ends every login.
195
+ needs a one-time code that the owner reads in a terminal. `mcp revoke` ends every login.
176
196
 
177
197
  - **Why:** a server reachable from a browser must not be a softer way in than the local one.
178
198
  - **Enforced by:** review only.
179
- - **Example:** `max work mcp --http --public-url https://<name>.ts.net`.
199
+ - **Example:** `max work mcp serve --http --public-url https://<name>.ts.net`.
180
200
  - **Exceptions:** none.
181
- - **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-594, NEED-774.
201
+ - **Applies to:** every CLI that offers `mcp serve --http`: the messenger CLIs, cli-template and every CLI made from
202
+ it. **Since:** cli-base 0.41.0. **Rulings:** NEED-594, NEED-774.
182
203
 
183
204
  ### WC-MCP-13 · A tool's hints and descriptions never stand in for the permission check
184
205
 
@@ -213,8 +234,11 @@ cli-core builds its personal and bot MCP catalogues from the command tree with `
213
234
  - **One server per subtree.** The personal server is built from the program without its `bot` group; the bot server
214
235
  from the `bot` group. Both keep three tools (WC-MCP-1).
215
236
  - **The profile still decides at run time** what is listed (WC-MCP-6).
216
- - **The whole catalogue is pinned** in each CLI's `contracts/mcp-tools.json`, so a command that becomes visible to
217
- agents shows up as a diff in its pull request.
237
+ - **Two snapshots pin what agents see.** `contracts/mcp-tools.json` pins the tools themselves: with three tools
238
+ (WC-MCP-1) that is three names and their schemas, which do not change when a command is added. So each CLI also
239
+ pins the command catalogue behind the tools, every command `tools_search` can return and whether it runs through
240
+ `read` or `write`, in `contracts/mcp-catalogue.json`. A command that becomes visible to agents then shows up as a diff in
241
+ its pull request. tg and max keep both; wa, zm, gcal and drive pin only the tools, a known gap as of 2026-10-27.
218
242
 
219
243
  ## Sources
220
244
 
@@ -51,8 +51,8 @@ The reference CLI is the one to compare with when a step is unclear.
51
51
 
52
52
  The copy already holds every file this handbook asks for: the required root commands (`commands`, `doctor`,
53
53
  `skill show`, `skill install`, `mcp`, `upgrade`) and `config` from cli-core, the run log and its `runs` commands
54
- ([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with a tool for every
55
- command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
54
+ ([WC-LIFE-15](lifecycle.md#wc-life-15--keep-a-run-log-through-cli-core-and-read-it-back-with-runs)), the MCP server with its three tools
55
+ ([WC-MCP-1](mcp.md#wc-mcp-1--a-server-offers-three-tools-not-one-per-command)) that reach every command, writes included, the community presets in `config/wirecat/`, the TypeScript, Biome and vitest setup,
56
56
  `contracts/`, `lint/`, the test matrix, the user pages, `CHANGELOG.md`, `SECURITY.md`, `bin/release`, and the
57
57
  `pr.yml`, `ci.yml`, `main.yml`, `standards.yml`, `changelog-guard.yml`, `monthly.yml` and `release.yml` workflows.
58
58
  The rules they keep are on the area pages: [Commands](commands.md), [MCP](mcp.md), [Accounts](accounts.md),
@@ -93,7 +93,11 @@ JSON Schema 2020-12.
93
93
  - **Why:** a caller that checks only the exit code must not take half an answer for a whole one.
94
94
  - **Enforced by:** review only.
95
95
  - **Example:** a search across accounts declares `annotate(search, { partial: true })` and exits 0 with
96
- `meta.complete: false`. Any other command prints what it read, then exits with the code of its first issue.
96
+ `meta.complete: false`. Any other command prints the whole envelope of what it read on stdout, with
97
+ `meta.complete: false` and its `issues`, then exits with the code of its first issue. Such an answer is an answer,
98
+ not a failure, so [WC-OUT-14](#wc-out-14--print-a-failure-in-machine-mode-as-one-json-object-on-stderr-with-stdout-empty)
99
+ does not apply to it: a caller reads stdout whenever it is not empty, also after a non-zero exit. cli-core's
100
+ `renderAnswer` does this.
97
101
  - **Exceptions:** none.
98
102
  - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
99
103
 
@@ -103,7 +107,8 @@ JSON Schema 2020-12.
103
107
  - **Enforced by:** review only.
104
108
  - **Example:** `next` holds the step to take, then one alternative. Each has `kind` (`command`, or `user_action` with
105
109
  no command) and `safe_to_auto_execute`. A step that reads the service and writes only the local store (a sync) is
106
- safe. A step that reads nothing remotely and writes locally (`store reindex`) is not: it waits to be asked. A step
110
+ safe; it counts as a read, also over MCP
111
+ ([WC-CMD-17](commands.md#wc-cmd-17--declare-effects-on-every-command)). A step that reads nothing remotely and writes locally (`store reindex`) is not: it waits to be asked. A step
107
112
  is built from ids, counts and cursors the CLI holds, never from text a message, mail or document carried. A CLI
108
113
  answer's command step also has `line`, the whole command with the profile (`tg personal mcp config`), because
109
114
  `command` is the bare path an MCP router takes and a copied path without the profile fails.
@@ -160,6 +165,12 @@ JSON Schema 2020-12.
160
165
 
161
166
  ### WC-OUT-14 · Print a failure in machine mode as one JSON object on stderr, with stdout empty
162
167
 
168
+ A failure is a run that has no answer: the command threw. A command that answers with what it could read and
169
+ `meta.complete: false` has not failed, even when it exits non-zero; that case is
170
+ [WC-OUT-8](#wc-out-8--exit-0-on-a-partial-answer-only-when-the-command-declares-partial). So a non-zero exit does not
171
+ mean stdout is empty: stdout empty and one JSON object on stderr is a failure; an envelope on stdout is an answer,
172
+ whole or partial.
173
+
163
174
  - **Why:** a caller tells success from failure without parsing text, and stdout never holds half an answer and an
164
175
  error.
165
176
  - **Enforced by:** `src/failure.test.ts` · `cli-core: src/cli/agent-contract.test.ts`.
@@ -320,9 +320,15 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
320
320
  - **Keys.** Each command is checked against its own path (`events.create`), the default `keyOf`. Housekeeping has no
321
321
  key (WC-PERM-5). `extra` adds the keys a command checks inside itself, such as one per generated API method
322
322
  (`api.calendar.events.insert`). A key that names no command is refused when it is set or saved.
323
- - **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7). A
324
- CLI picks them for what its writes do, not by WC-PERM-6's messenger list: they ask before a write people other
325
- than the owner see or cannot easily undo, and deny what an agent must never do unasked.
323
+ - **Defaults.** `defaults` holds the CLI's built-in levels, beside cli-core's. They only ever tighten (WC-PERM-7).
324
+ Like WC-PERM-6 for the messengers, and as tg and drive do: an ordinary change is `allow`; a delete is `ask`; a
325
+ write that mails or notifies people other than the owner, or deletes a whole series from their calendars, is a key
326
+ of its own, `deny` until the owner allows it.
327
+ Sending a message is not this kind of write; a messenger CLI keeps WC-PERM-6.
328
+ - **Shared keys on every path.** A key a shared command checks inside itself, such as `events.notify` or
329
+ `events.delete.series`, is checked by every path that can do the same thing, the CLI's own generic API commands
330
+ included. A gcal `api` call that deletes an event with `sendUpdates: "all"` checks `events.notify` as well as
331
+ its own `api.calendar.events.delete`; otherwise the generic path is a way round the shared key.
326
332
  - **Read-only lock and guard.** `<PREFIX>_READ_ONLY` refuses every write to the service, as in WC-PERM-12. Every
327
333
  write runs through cli-core's write guard with `permissionDefaults: configuration.permissions.defaults`
328
334
  (cli-core 0.455.0), so `ask` is a y/N question at a terminal (WC-PERM-9) and every attempt goes into the send journal
@@ -331,14 +337,21 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
331
337
  - **Why:** one permission model for every CLI; a CLI-own layer drifts from the shared one and is audited by nobody.
332
338
  - **Enforced by:** `cli-core: src/cli/command-permissions.test.ts` · `cli-core: src/calendar/cli/writes.test.ts`.
333
339
  - **Example:** a calendar CLI takes cli-core's `CALENDAR_WRITE_DEFAULTS` and `CALENDAR_WRITE_KEYS`
334
- (`@wirecat/cli-core/calendar`, cli-core 0.458.0): `events create`, `update`, `respond` and `delete` ask, since each
335
- changes a calendar other people may share or be invited from; `events.delete.series` (a whole repeating series) and
336
- `events.notify` (mail the attendees) are `deny` until the owner allows them. gcal adds its own: a generated
337
- `api <method>` that writes asks, and one that shares, empties or gives away a calendar is `deny`.
338
- - **Exceptions:** known gaps: drive keeps its own policy and `--read-only` flag, zoom reads its own
339
- `remote-meetings` key with `levelFor`, and memo uses cli-core's permissions for the shared commands only. None of
340
- them declares `commandPermissions` yet.
341
- - **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0. **Rulings:** none.
340
+ (`@wirecat/cli-core/calendar`, cli-core 0.458.0). By this rule `events create`, `update` and `respond` are `allow`
341
+ and `events delete` is `ask`. `events.delete.series` (every occurrence of a repeating series, in every attendee's
342
+ calendar) and `events.notify` (the provider mails the attendees) are `deny` until the owner allows them: over MCP
343
+ `ask` runs without a question, and each reaches people other than the owner. gcal adds its own keys for its generated
344
+ `api <method>` commands: sharing a calendar is `ask` (NEED-1857), and emptying the main calendar
345
+ (`api.calendar.calendars.clear`) is `deny`.
346
+ - **Exceptions:** known gaps, as of 2026-10-27:
347
+ - cli-core's `CALENDAR_WRITE_DEFAULTS` (`src/calendar/writer.ts`) still has `events create`, `update` and
348
+ `respond` at `ask`.
349
+ - gcal's generated `api` commands default every write to `ask`, ordinary changes included, and check neither
350
+ `events.notify` nor `events.delete.series`.
351
+ - drive keeps its own policy and `--read-only` flag, zoom reads its own `remote-meetings` key with `levelFor`, and
352
+ memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
353
+ - **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
354
+ keys on every path: cli-base 0.65.0). **Rulings:** NEED-1857, NEED-1883.
342
355
 
343
356
  ## Sources
344
357
 
@@ -192,8 +192,14 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
192
192
 
193
193
  - **Why:** a stray file in the package can leak local data and grows every install.
194
194
  - **Enforced by:** `node-ci.yml:check` · `<app>: pnpm package:check` · `whatsapp-cli: pnpm release:check`.
195
- - **Example:** `package.json` `files` lists what ships. The CLI's `pnpm release:check` runs `packContents` from
196
- `@wirecat/cli-base/release` and fails on any other path in `npm pack --dry-run`.
195
+ - **Example:** `package.json` `files` lists what ships. `packContents(root, allowed, said)` from
196
+ `@wirecat/cli-base/release` fails on any path in `npm pack --dry-run` outside `allowed`. It checks the pack against
197
+ the list it is given, not against this rule: a CLI that passes its own `files` list as `allowed`, as the template's
198
+ `scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
199
+ `npm pack --dry-run` in gcal-cli (2026-10-27) packs 256 files, `src/`, `scripts/`, `.github/`, `config/`,
200
+ `contracts/`, `lint/` and `template.json` among them, because its `files` list names them; its check cannot see it. The check this rule needs
201
+ refuses the forbidden paths above whatever `files` says; it belongs in cli-template, so every CLI gets it by
202
+ `upgrade` ([cli-template#77](https://github.com/WireCatLabs/cli-template/issues/77)).
197
203
  - **Exceptions:** cli-template's own package ships the skeleton's sources and configs, since `new` copies them; the
198
204
  `files` list `new` writes into a CLI follows this rule. Each CLI's `package:check` runs `packContents`; tg and max
199
205
  as `release-check.ts --package-only`.
@@ -310,16 +310,40 @@ store and runs no search. A CLI declares its provider and may replace any step.
310
310
  printed. A CLI that signs in another way (a bot token, a session) does not use the module.
311
311
  - **Applies to:** every CLI that signs in with OAuth. **Since:** cli-base 0.57.1. **Rulings:** NEED-1734.
312
312
 
313
- ### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store
313
+ ### WC-SECR-23 · Meeting passcodes and the query of a join link never reach the store, the output or an agent
314
314
 
315
- - **Why:** a passcode, PIN or join-link query lets anyone who reads the store join the meeting.
315
+ - **Why:** a passcode, PIN or join-link query lets anyone who reads the store, a terminal log or an agent's
316
+ transcript join the meeting.
316
317
  - **Enforced by:** `cli-core: src/store/calendar.test.ts`.
317
318
  - **Example:** a calendar entry's description loses each line that carries a passcode, password or PIN, and every
318
319
  link loses its query string, so `join_url` holds `https://zoom.us/j/123` without `?pwd=`. A meeting is matched by
319
320
  its room (`join_key`, such as `zoom:<number>`), never by a link that carries the passcode.
320
- - **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name.
321
- - **Applies to:** cli-core and every CLI that stores meetings or calendar entries. **Since:** cli-base 0.57.1.
322
- **Rulings:** none (Wave 3 requirements 3.4).
321
+ The same holds for what a command prints and what an MCP tool answers, also when the answer comes straight from
322
+ the provider and never touched the store: a generic API command removes the provider's passcode fields (Google's
323
+ `conferenceData.entryPoints[].passcode`, `pin`, `accessCode`, `password`), the query of every join link and the
324
+ dial sequence after a phone number, and the passcode lines of a description or a location.
325
+ - **Exceptions:** a title that names a PIN or a password is kept: it is the meeting's name. Known gap, as of
326
+ 2026-10-27: gcal's `api` answers drop the conference entry points' secrets (`src/conference.ts`) but keep the
327
+ description and location as Google sent them.
328
+ - **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
329
+ (output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
330
+
331
+ ### WC-SECR-24 · A run with moved folders never writes the owner's system keyring
332
+
333
+ - **Why:** with its folders moved (`<PREFIX>_CONFIG_DIR`, `<PREFIX>_STATE_DIR`, a throwaway `HOME` or `XDG_*`),
334
+ `Credentials` keys the keyring by `<app>:<config folder>` (`keyringService`, `isolated`). Each test run, agent check
335
+ or worktree that signs in then leaves its own copy of a sign-in in the owner's keyring, and nothing removes it.
336
+ - **Enforced by:** `src/credentials.test.ts`.
337
+ - **Example:** a test passes `memoryKeyring()`. An agent checking a built CLI signs in with file storage inside its
338
+ temp folder (`session start --credential-storage file`), or does not sign in. A run against the owner's real data
339
+ keeps the default folders, so it reads the owner's existing entry (`<app>` / `oauth:<provider>:<profile>`) instead
340
+ of making a new one. A throwaway `HOME` does not hide the keyring, which is reached over D-Bus: a run that must not
341
+ see it also runs with `DBUS_SESSION_BUS_ADDRESS` removed. With `isolated` and the system keyring, `Credentials`
342
+ saves new secrets to the private file in the moved config folder and only reads the keyring, for a secret saved
343
+ there before.
344
+ - **Exceptions:** none.
345
+ - **Applies to:** every CLI, its tests and every agent that runs a CLI. **Since:** the next cli-base release. **Rulings:** owner,
346
+ 2026-10-29 (sign-ins copied per work folder should have reused the existing entry).
323
347
 
324
348
  ## Sources
325
349