@lumoai/cli 1.40.0 → 1.42.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.
@@ -1,8 +1,8 @@
1
1
  # Document Management
2
2
 
3
- ## Document Management
3
+ Document **CRUD, listing, tree/move, sharing, and Google import/sync**. For the surgical live-doc edit path — `doc show --raw/--section`, `doc patch`, `doc append`, `doc diff`, `doc rebuild-source` — see [doc-editing.md](doc-editing.md). The content-channels and table-authoring conventions below apply to those editing commands too.
4
4
 
5
- ### Content channels: prefer stdin / `--content`
5
+ ## Content channels: prefer stdin / `--content`
6
6
 
7
7
  `doc create` / `doc update` / `doc patch` / `doc append` all take the body from one of three mutually-exclusive channels. **For agent write-back, prefer stdin or `--content`** — pipe the markdown you already hold in memory, or pass it inline:
8
8
 
@@ -13,7 +13,7 @@ lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ
13
13
 
14
14
  `--file` is the **fallback for content that already lives in an authoritative file on disk** (e.g. a repo-tracked `docs/live-docs/<file>.md` source you are editing). It is **not** the default channel: `--file` is sandboxed — the CLI rejects any path outside the current project directory or matching the sensitive-file denylist (`.env*`, keys, `credentials`, …), with no override. So feeding it generated content forces you to first write a scratch file _inside the repo_ just to pass it — an extra step that is the real-world friction that gets `--file` rejected. Pipe via stdin instead and skip the temp file; reach for `--file` only when the file _is_ the source of truth you're editing.
15
15
 
16
- ### Markdown tables: keep them compact (no alignment padding)
16
+ ## Markdown tables: keep them compact (no alignment padding)
17
17
 
18
18
  When you author or edit a markdown doc that contains tables (live-docs, registers, reports), write the cells **compact — one space around each pipe, no column-alignment padding**:
19
19
 
@@ -30,7 +30,7 @@ not the aligned form (`| col | meaning |` padded so columns line up). Two reas
30
30
 
31
31
  This is a pure authoring convention — the server stores your markdown **byte-for-byte** (`sourceMarkdown`), so `doc show --raw` and `doc diff` stay byte-exact (LUM-408); there is **no** server-side table normalization to lean on. The structure guard (LUM-410) compares _rendered_ structure and is whitespace-insensitive, so compactness buys local editability, not a verify pass.
32
32
 
33
- ### `lumo doc create [title] [flags]` — create a new document
33
+ ## `lumo doc create [title] [flags]` — create a new document
34
34
 
35
35
  Use this when the user wants to write a new document from the terminal. Title is positional and optional. When omitted, the doc title defaults to empty (server renders as "Untitled" via i18n).
36
36
 
@@ -64,44 +64,40 @@ Created cmd_xxx "RFC: doc CLI" https://www.uselumo.ai/workspace/lumo/documents/
64
64
  Tags: rfc, draft
65
65
  ```
66
66
 
67
- The cuid (`cmd_xxx`) is still printed as a stable identifier you can pass back into other `lumo doc ...` commands; the URL switched to the per-workspace slug shape (`slugify(title)-<number>`) and the cuid is no longer a valid web URL.
68
-
69
- The `Tags:` line is omitted when no tags were attached.
67
+ The cuid (`cmd_xxx`) is still printed as a stable identifier you can pass back into other `lumo doc ...` commands; the URL switched to the per-workspace slug shape (`slugify(title)-<number>`) and the cuid is no longer a valid web URL. The `Tags:` line is omitted when no tags were attached.
70
68
 
71
69
  ### When to suggest `doc create`
72
70
 
73
71
  - User says "write a doc", "draft an RFC", "new document" (in any language), or describes a deliverable that should live as a document.
74
72
  - After a discussion that needs a write-up, offer to create the doc with a title and the right scope.
75
73
 
76
- ### `lumo doc update <doc> [flags]` — update a document
74
+ ## `lumo doc update <doc> [flags]` — update a document
77
75
 
78
76
  `<doc>` accepts a cuid or a case-insensitive title. Ambiguous titles fail with a candidate list — re-run with the cuid.
79
77
 
80
- | Flag | Type | Notes |
81
- | ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | `--title <text>` | string | New title (cannot be empty). |
83
- | `--content <text>` | string | Replace content (inline). |
84
- | `--file <path>` | string | Replace content from file. |
85
- | (stdin) | — | Pipe to replace content. Empty / whitespace-only stdin (a non-TTY shell with nothing piped — the common agent case) is treated as **no content channel**, not a body clear (LUM-505). |
86
- | `--scope <scope>` | enum | `personal` / `workspace`. |
87
- | `--project <ref>` | string | Project name/slug. `--project ""` clears the filing. |
88
- | `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
89
- | `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
90
- | `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20. |
91
- | `--add-tag-id <cuid>` | string (repeatable) | Attach tag by id. Max 20. |
92
- | `--remove-tag <name>` | string (repeatable) | Detach tag by name. `--remove-tag <name>` resolves the name via find-or-create on the workspace. If the name was unknown, a new Tag row is created (orphan, no attachments) before the detach runs as a no-op. Use `--remove-tag-id <cuid>` to avoid orphans. Max 20. |
93
- | `--remove-tag-id <cuid>` | string (repeatable) | Detach tag by id. Unknown ids are a no-op (no side effects). Max 20. |
94
- | `--allow-shrink` | boolean | Let a body update through even when it drops tables/rows/headings versus the stored body (see structure guard below). |
78
+ | Flag | Type | Notes |
79
+ | ------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
80
+ | `--title <text>` | string | New title (cannot be empty). |
81
+ | `--content <text>` | string | Replace content (inline). |
82
+ | `--file <path>` | string | Replace content from file. |
83
+ | (stdin) | — | Pipe to replace content. Empty / whitespace-only stdin (a non-TTY shell with nothing piped — the common agent case) is treated as **no content channel**, not a body clear (LUM-505). |
84
+ | `--scope <scope>` | enum | `personal` / `workspace`. |
85
+ | `--project <ref>` | string | Project name/slug. `--project ""` clears the filing. |
86
+ | `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
87
+ | `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
88
+ | `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20. |
89
+ | `--add-tag-id <cuid>` | string (repeatable) | Attach tag by id. Max 20. |
90
+ | `--remove-tag <name>` | string (repeatable) | Detach tag by name (find-or-create; an unknown name creates an orphan Tag row before the no-op detach — use `--remove-tag-id` to avoid orphans). Max 20. |
91
+ | `--remove-tag-id <cuid>` | string (repeatable) | Detach tag by id. Unknown ids are a no-op. Max 20. |
92
+ | `--allow-shrink` | boolean | Let a body update through even when it drops tables/rows/headings versus the stored body (see structure guard below). |
95
93
 
96
94
  A **metadata-only update leaves the body untouched** (LUM-505): when no content channel is supplied (`--title`/`--scope`/`--project`/tag flags only), the body is omitted from the PATCH — it does not get blanked and the structure guard cannot fire. To deliberately clear or replace the body you must supply a content channel explicitly (`--content ""` to clear, which then hits the structure guard as a shrink → pair with `--allow-shrink`).
97
95
 
98
96
  Optimistic concurrency (LUM-409): `--if-revision <n>` only applies the update if the doc body is still at revision `n` (from `doc show`). Mismatch → 409 conflict, nothing written — re-read, rebase, retry. `--if-revision` alone is not an update (still errors "no fields to update"); same for `--allow-shrink`.
99
97
 
100
- **Structure guard (LUM-410), built into the server:** a body update whose new render has **fewer `table` / `tr` / heading elements than the stored body** is rejected with **422** before anything is written — the error names each shrunk category with old→new counts (e.g. `table 1→0, tr 4→0`). This is the `verify-live-doc.ts` reconciliation moved into the write path, so table flattening (LUM-349) and stale-base section loss (#460) fail loudly even when nobody remembers to run the script. When the deletion is intentional (you really are removing a section/table), re-run with `--allow-shrink`. On a 422: don't reach for `--allow-shrink` reflexively — first check whether your edit base is stale (`doc show <doc> --raw`) and rebase. Only markdown-path writes are guarded; web-editor edits and `doc sync` (Google authority) are not.
101
-
102
- `--tag` / `--tag-id` (bulk replace) are mutually exclusive with `--add-tag` / `--add-tag-id` / `--remove-tag` / `--remove-tag-id`. The CLI errors before any network call if both families are mixed.
98
+ **Structure guard (LUM-410), built into the server:** a body update whose new render has **fewer `table` / `tr` / heading elements than the stored body** is rejected with **422** before anything is written — the error names each shrunk category with old→new counts (e.g. `table 1→0, tr 4→0`). This is the `verify-live-doc.ts` reconciliation moved into the write path, so table flattening (LUM-349) and stale-base section loss (#460) fail loudly. When the deletion is intentional, re-run with `--allow-shrink`. On a 422: don't reach for `--allow-shrink` reflexively — first check whether your edit base is stale (`doc show <doc> --raw`) and rebase. Only markdown-path writes are guarded; web-editor edits and `doc sync` (Google authority) are not.
103
99
 
104
- Like `doc create`, `--file` is sandboxed: the CLI rejects paths that resolve outside the project directory or match the sensitive-file denylist (`.env*`, private keys, `credentials`, …). No override flag.
100
+ `--tag` / `--tag-id` (bulk replace) are mutually exclusive with `--add-tag` / `--add-tag-id` / `--remove-tag` / `--remove-tag-id`. The CLI errors before any network call if both families are mixed. Like `doc create`, `--file` is sandboxed.
105
101
 
106
102
  Examples:
107
103
 
@@ -118,148 +114,11 @@ lumo doc update RFC --tag final --add-tag oops
118
114
  ### When to suggest `doc update`
119
115
 
120
116
  - User wants to revise an existing doc **as a whole** (title, scope, or a full-body rewrite).
121
- - After running `lumo doc list` or `doc show`, if the user wants to change a doc's scope, title, or content.
122
- - For a small edit inside one section, prefer `doc patch` / `doc append` (below) — full `doc update` has the maximum clobber radius.
117
+ - For a small edit inside one section, prefer `doc patch` / `doc append` ([doc-editing.md](doc-editing.md)) full `doc update` has the maximum clobber radius.
123
118
  - When replacing the body from a previously fetched base, pass `--if-revision` so a concurrent edit fails loudly (409) instead of being overwritten.
124
119
  - When the update intentionally deletes a section or table, pass `--allow-shrink` up front; otherwise expect (and want) the 422 structure guard on accidental structure loss.
125
120
 
126
- ### `lumo doc show <doc> [--raw | --section <heading>]` — print one document's detail
127
-
128
- Default mode prints a key:value header (id, title, scope, project, created/updated timestamps, **revision**, mentioned tasks) and the content rendered back to markdown. `Revision:` is the body's optimistic-concurrency counter — feed it back as `--if-revision` on `doc update` / `doc patch` / `doc append`.
129
-
130
- ```bash
131
- lumo doc show "RFC: doc CLI"
132
- lumo doc show cmd_xxx --raw > base.md # byte-identical edit base (revision on stderr)
133
- lumo doc show cmd_xxx --section "D 状态表" > sec.md # one section only (revision on stderr)
134
- ```
135
-
136
- **`--raw` (LUM-408)** prints the byte-identical markdown source of the last markdown upload — no header, no trailing newline added. The server stores the raw markdown (`sourceMarkdown`) alongside the rendered HTML on every markdown write (`doc create/update --content/--file/stdin`, gdoc import/sync), so `--raw` output IS a legal edit base: `doc show --raw > base.md`, edit, `doc update --file base.md` round-trips losslessly.
137
-
138
- - A web-editor (HTML-direct) edit or revision restore **invalidates** the stored source — the doc's markdown source is gone until the next markdown upload.
139
- - When no source is stored (legacy doc or after an HTML edit), `--raw` **fails with exit 1 and a rebuild hint** — it never silently falls back to the lossy HTML→markdown reverse render (that fallback flattened tables: LUM-349). Rebuild flow (LUM-446): run **`lumo doc rebuild-source <doc>`** — it regenerates the source from the stored HTML with a lossless serializer (tables round-trip) and a structure guard, so `--raw` works from then on. (The old manual flow — `doc show` rendered → hand-reconstruct → `doc update --file` — flattened tables and is superseded.)
140
- - Raw output is verbatim (unsanitized) by design — redirect it to a file rather than reading it in a terminal when the doc's provenance is uncertain.
141
-
142
- Note: the markdown rendered by **default-mode** `doc show` is still best-effort (tables flatten). Round-trip via `doc show > tmp.md && doc update --file tmp.md` is NOT a no-op — use `--raw` as the edit base instead.
143
-
144
- Output budget (LUM-428): **default-mode** `doc show` caps the rendered body to the output-token budget (25,000 tokens) and, when truncated, ends in a pointer to `--section "<heading>"` (just-in-time slice) / `--raw` (full source). `--raw` and `--section` are **never** capped — they are byte-faithful edit bases.
145
-
146
- **`--section <heading>` (LUM-409)** prints just one heading-addressed section of the markdown source — a byte-faithful slice from the heading line through (not including) the next same-or-higher-level heading, subsections included. No header on stdout (the slice is a legal `doc patch` base); the current revision is printed to **stderr** as `Revision: N`. Mutually exclusive with `--raw`.
147
-
148
- - Section addressing: pass the heading text (`--section "D 状态表"`), matched in three tiers — exact, then case-insensitive, then (LUM-447) full-width↔half-width punctuation + whitespace normalization, so a half-width query (`问题(P4)`) lands on a full-width stored heading (`问题(P4)`) and vice-versa without copying the raw bytes. Prefix with `#…` to pin the level when the same text exists at several depths (`--section "## Status"`). Normalization never relaxes the ambiguity guard — if it matches more than one heading you still get the candidate list + exit 1.
149
- - Missing heading → exit 1 listing the available headings; ambiguous heading → exit 1 with a depth-disambiguation hint.
150
- - Requires a stored markdown source — same no-fallback rule and rebuild flow as `--raw`.
151
- - Heading detection is markdown-aware: `#` lines inside fenced code blocks or blockquotes are never section boundaries.
152
-
153
- Use default mode when the user wants to read a doc; use `--raw` whenever the full output will be edited and uploaded back; use `--section` when only one part matters (keeps the context window small and the patch radius smaller).
154
-
155
- ### When to suggest `doc show --raw` / `--section`
156
-
157
- - Before any `doc update --file` that starts from existing remote content — fetch the base with `--raw`, never from rendered `doc show` output.
158
- - Before a `doc patch` — read the section with `--section` first, note the `Revision:` line, edit, patch back with `--if-revision`.
159
- - User asks "what's the exact source of this doc", or an agent needs a faithful local copy of a live doc.
160
- - User asks "what's in section X" of a long doc — `--section` avoids pulling the whole body into context.
161
- - If `--raw`/`--section` errors (no stored source), run `lumo doc rebuild-source <doc>` to regenerate it losslessly, then retry — don't edit the rendered output.
162
-
163
- ### `lumo doc patch <doc> --section <heading>` — replace one section
164
-
165
- Replaces the **whole addressed section** (heading line included, subsections included) with the provided content, server-side, leaving every byte outside the section untouched. The new content comes from `--content`, `--file`, or piped stdin (one required; `--file` is sandboxed like `doc update`).
166
-
167
- | Flag | Type | Notes |
168
- | --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
169
- | `--section <heading>` | string | Required. Heading text addressing the section; prefix `#…` pins the depth. |
170
- | `--content <text>` | string | New section content (include the heading line — the whole section is replaced verbatim). |
171
- | `--file <path>` | string | New section content from file (project-local sandbox). |
172
- | `--if-revision <n>` | int | Only apply if the body is still at revision `n`. Recommended whenever the edit base was read earlier. |
173
- | `--allow-shrink` | boolean | Let the patch through even when it drops tables/rows/headings within the addressed section (422 otherwise). |
174
-
175
- Concurrency: the splice always commits **conditionally** on the revision the server read the source at — even without `--if-revision`, a concurrent body edit between read and write returns 409 instead of clobbering. On 409 the CLI prints the server reason plus a re-read-and-retry hint and exits 1.
176
-
177
- Structure guard (LUM-410), **scoped to the addressed section**: a replacement whose render has fewer `table`/`tr`/heading elements than the old section's render is rejected with 422 naming each shrunk category (old→new counts); structure elsewhere in the document never factors in. Dropping the heading line itself trips the guard too (heading count shrinks). Pass `--allow-shrink` when the deletion is intentional. `doc append` is pure insertion and is never guarded.
178
-
179
- Requires a stored markdown source (same rule as `--raw`); errors with the rebuild hint otherwise. Replacement is verbatim — if the new content omits the heading line, the heading is gone (an explicit choice the guard makes you confirm with `--allow-shrink`).
180
-
181
- ```bash
182
- lumo doc show cmd_xxx --section "D 状态表" > sec.md # stderr: Revision: 6
183
- # … edit sec.md …
184
- lumo doc patch cmd_xxx --section "D 状态表" --file sec.md --if-revision 6
185
- # → Patched cmd_xxx "登记册" § "D 状态表" revision 7
186
- ```
187
-
188
- ### When to suggest `doc patch`
189
-
190
- - User wants to change one section of a live doc ("update the status table", "rewrite section D") — patch beats full `doc update` on clobber radius and context size.
191
- - Live-doc status updates that used to be read-whole/edit/upload-whole round-trips.
192
- - If the patch 409s: re-read the section, rebase the edit, retry — never fall back to a full-body upload from the stale base.
193
- - If the patch 422s (structure guard): the replacement drops tables/rows/headings the section has. Re-check the edit base first; add `--allow-shrink` only when the deletion is what the user wants.
194
-
195
- ### `lumo doc append <doc> [--section <heading>]` — append to a section (or the doc)
196
-
197
- Inserts the new content at the **end of the addressed section** (just before the next same-or-higher-level heading), or at the end of the document when `--section` is omitted. Pure insertion: no pre-existing byte is modified, which makes it the natural write for running logs, ledgers and queues. Content channels and sandbox are the same as `doc patch`; separator blank lines are added automatically.
198
-
199
- | Flag | Type | Notes |
200
- | --------------------- | ------ | ---------------------------------------------------------------------------- |
201
- | `--section <heading>` | string | Optional. Omit to append at the document end. |
202
- | `--content <text>` | string | Content to append. |
203
- | `--file <path>` | string | Content from file (project-local sandbox). |
204
- | `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
205
-
206
- Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). **End-of-document append (no `--section`) does NOT require a stored markdown source** (LUM-444): when the source is missing (web HTML edit / revision restore / legacy doc) it renders the new block to HTML and concatenates it onto the stored HTML body — so one web operation can no longer lock the whole doc against the agent write path. The source stays null (the doc is still HTML-only afterward; `--raw`/`--section`/`doc patch` keep erroring with the rebuild hint). **Section-addressed append (`--section`) still requires a stored source** — it needs the markdown to locate the heading boundary.
207
-
208
- ```bash
209
- lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ 论文"
210
- # → Appended to cmd_xxx "登记册" § "F 待办队列" revision 8
211
- echo "## 2026-06-10\n吸收了 3 篇" | lumo doc append cmd_xxx # document end
212
- ```
213
-
214
- ### When to suggest `doc append`
215
-
216
- - User wants to add an entry/row/log line to a section ("把 X 加进待办队列", "append today's notes") — this is the killer op for ledger-style live docs: zero clobber risk by construction.
217
- - Whenever the alternative would be re-uploading the whole doc just to add lines at the end of one section.
218
-
219
- ### `lumo doc diff <doc> --file <local.md>` — compare remote markdown source vs a local file
220
-
221
- Compares the server-side stored markdown source (the byte-identical last markdown upload) against a local file, making remote/local split-brain visible on demand.
222
-
223
- | Flag | Type | Notes |
224
- | --------------- | ------ | --------------------------------------------------------------------------------- |
225
- | `--file <path>` | string | Required. Local markdown file; same project-local sandbox as `doc update --file`. |
226
-
227
- Exit codes: **0** = byte-identical (prints `Clean: …`), **1** = divergent (prints a unified diff, `--- remote/<id> (sourceMarkdown)` vs `+++ local/<file>`) or error. A doc without a stored markdown source errors explicitly (upload a markdown base once, then diff works).
228
-
229
- ```bash
230
- lumo doc diff cmd_xxx --file docs/live-docs/research-intake-ledger.md
231
- ```
232
-
233
- ### When to suggest `doc diff`
234
-
235
- - Before uploading a locally edited file with `doc update --file` — check whether the remote source moved since the local copy was taken (prevents stale-upload clobbering, the #460 incident class).
236
- - When a repo-tracked markdown source (e.g. `docs/live-docs/`) and the live doc may have drifted and the user asks which is current.
237
- - After a suspected concurrent edit: a clean diff (exit 0) proves remote and local are in sync.
238
-
239
- ### `lumo doc rebuild-source <doc>` — regenerate the markdown source from the HTML body
240
-
241
- The **recovery path for a source-less doc** (LUM-446). When a doc has no stored `sourceMarkdown` — a web HTML-direct edit or revision restore nulled it, or the doc predates source storage — every markdown write path (`--raw`, `--section`, `doc patch`, `doc append --section`, `doc diff`) is locked. This regenerates a valid source by serializing the **stored HTML structure model** back to markdown with a **lossless serializer that round-trips tables/rows/headings** (the default `doc show` render flattens tables — LUM-349 — so it was never a safe rebuild base). Only the `sourceMarkdown` column is backfilled; the rendered body is untouched, so the doc reads identically and you just regain the edit base.
242
-
243
- | Flag | Type | Notes |
244
- | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
245
- | `--allow-shrink` | boolean | Commit even if the rebuilt source re-renders with fewer tables/rows/headings than the stored body (default: 422 reject). |
246
- | `--force` | boolean | Re-derive even when a source already exists (default: 409 — protects a byte-faithful human source from a downgrade). |
247
- | `--if-revision <n>` | int | Only apply if the body is still at this revision (from `doc show`). |
248
-
249
- The rebuild is **structure-guarded** (the same LUM-410 口径 as `doc update`/`doc patch`): if the serializer would drop any table/row/heading, it is rejected with **422** rather than silently committing a flattened source — `--allow-shrink` is the explicit escape hatch. A doc that already has a source is refused **409** unless `--force`.
250
-
251
- ```bash
252
- lumo doc rebuild-source cmd_xxx # restore a source-less doc; --raw works after
253
- lumo doc rebuild-source cmd_xxx --force # re-derive even if a source already exists
254
- ```
255
-
256
- ### When to suggest `doc rebuild-source`
257
-
258
- - A `--raw` / `--section` / `doc patch` / `doc diff` call errored with "no stored markdown source" — rebuild, then retry. This is the first thing to try, not a manual reconstruction.
259
- - A table-heavy live doc (e.g. a `docs/live-docs/` registry) lost its source after a web operation and the markdown write path is locked.
260
- - Do **not** run it on a doc that already has a good source unless the user explicitly wants to re-derive it (then pass `--force`) — it replaces the byte-faithful source with a serializer-derived one.
261
-
262
- ### `lumo doc list [flags]` — list documents
121
+ ## `lumo doc list [flags]` — list documents
263
122
 
264
123
  Default behavior: lists every document the current user can see, as fixed-width rows: `<cuid> <SCOPE> <project|-> <title>`.
265
124
 
@@ -271,11 +130,7 @@ Default behavior: lists every document the current user can see, as fixed-width
271
130
  | `--limit <n>` | int | Cap output to the first N rows. |
272
131
  | `--tree` | boolean | Render as an indented tree (two spaces per depth level) instead of the flat row table. |
273
132
 
274
- `--tree` plus the existing filters work together: the CLI fetches with the filters applied, then builds the tree from whatever rows came back. Docs whose parent is **not** in the result (e.g. filtered out by `--task LUM-N`) render as top-level rows. `--limit N --tree` truncates the flat list to N rows _before_ the tree is built, so a deeply nested subtree may not appear contiguously.
275
-
276
- When `--task` is combined with `--scope` / `--project`, those become client-side filters on the task-scoped result.
277
-
278
- Examples:
133
+ `--tree` plus filters work together: the CLI fetches with filters applied, then builds the tree from whatever rows came back. Docs whose parent is **not** in the result render as top-level rows. `--limit N --tree` truncates the flat list to N rows _before_ the tree is built. When `--task` is combined with `--scope` / `--project`, those become client-side filters on the task-scoped result.
279
134
 
280
135
  ```bash
281
136
  lumo doc list --scope workspace
@@ -288,40 +143,28 @@ lumo doc list --scope workspace --tree
288
143
  - The user asks "what docs do I have", "show me workspace docs", "what's on LUM-42".
289
144
  - Before suggesting `doc update` or `doc delete` when no doc ID is in context — run `doc list` first to surface candidates.
290
145
 
291
- ### `lumo doc move <doc> [flags]` — move a doc under a different parent or to root
146
+ ## `lumo doc move <doc> [flags]` — move a doc under a different parent or to root
292
147
 
293
- Reparents a doc. `<doc>` accepts a cuid or a case-insensitive title; ambiguous titles fail with a candidate list — re-run with the cuid.
148
+ Reparents a doc. `<doc>` accepts a cuid or a case-insensitive title; ambiguous titles fail with a candidate list.
294
149
 
295
150
  | Flag | Type | Notes |
296
151
  | ---------------- | ------- | ------------------------------------------------ |
297
152
  | `--parent <doc>` | string | New parent doc (cuid or case-insensitive title). |
298
153
  | `--root` | boolean | Move to top level (`parentId = null`). |
299
154
 
300
- `--parent` and `--root` are **mutually exclusive** and one of them is **required**. CLI errors before any network call if both or neither is supplied.
301
-
302
- The new `sortOrder` is computed CLI-side as `max(sibling.sortOrder) + 1` — the moved doc lands at the end of its new sibling list. Reordering among siblings (`--before` / `--after`) is not yet supported; for now use the Web UI to reorder.
303
-
304
- Examples:
155
+ `--parent` and `--root` are **mutually exclusive** and one is **required** (CLI errors before any network call otherwise). The new `sortOrder` is computed CLI-side as `max(sibling.sortOrder) + 1` — the moved doc lands at the end of its new sibling list. Reordering among siblings (`--before` / `--after`) is not yet supported; use the Web UI.
305
156
 
306
157
  ```bash
307
158
  lumo doc move "Sub-doc" --parent "Roadmap"
308
159
  lumo doc move "Sub-doc" --root
309
- lumo doc move cmd_xxx --parent cmd_yyy
310
- ```
311
-
312
- Output:
313
-
314
- ```
315
- Moved cmd_xxx "Sub-doc" → "Roadmap"
316
- Moved cmd_xxx "Sub-doc" → root
317
160
  ```
318
161
 
319
162
  ### When to suggest `doc move`
320
163
 
321
164
  - User says "move doc X under Y", "reparent X to root", "promote X to top level".
322
- - After `doc create`, if the user realizes the new doc should live elsewhere in the tree — suggest `doc move` rather than recreating.
165
+ - After `doc create`, if the user realizes the new doc should live elsewhere — suggest `doc move` rather than recreating.
323
166
 
324
- ### `lumo doc delete <doc> --yes` — delete a document
167
+ ## `lumo doc delete <doc> --yes` — delete a document
325
168
 
326
169
  Requires `--yes`; no interactive prompt (agent-friendly).
327
170
 
@@ -329,80 +172,40 @@ Requires `--yes`; no interactive prompt (agent-friendly).
329
172
  lumo doc delete cmd_xxx --yes
330
173
  ```
331
174
 
332
- ### `lumo doc bind <doc> <task>` bind a doc to a task
175
+ ## `lumo doc bind <doc> <task>` / `lumo doc unbind <doc> <task>` task linkage
333
176
 
334
- Adds an explicit DocumentMention row. Survives content edits in the Web UI. If a CONTENT-derived mention already exists for the same pair, it's upgraded to EXPLICIT so a later `unbind` can remove it.
177
+ `bind` adds an explicit DocumentMention row (survives Web UI content edits). If a CONTENT-derived mention already exists for the same pair, it's upgraded to EXPLICIT so a later `unbind` can remove it. `unbind` removes EXPLICIT mentions only — a purely CONTENT-derived mention can't be unbound from CLI (fails 409, instructing the user to remove the `@LUM-N` from the doc body in the Web UI).
335
178
 
336
179
  ```bash
337
180
  lumo doc bind cmd_xxx LUM-42
338
- ```
339
-
340
- Output:
341
-
342
- ```
343
- Bound cmd_xxx ↔ LUM-42
344
- Bound cmd_xxx ↔ LUM-42 (upgraded from content) # when upgrading a CONTENT row
345
- Already bound cmd_xxx ↔ LUM-42 # idempotent
346
- ```
347
-
348
- ### `lumo doc unbind <doc> <task>` — unbind a doc from a task
349
-
350
- Removes EXPLICIT mentions only. A purely CONTENT-derived mention can't be unbound from CLI — fails with 409 and a message instructing the user to remove the @LUM-N from the doc body in the Web UI.
351
-
352
- ```bash
181
+ # Bound cmd_xxx ↔ LUM-42
182
+ # Bound cmd_xxx ↔ LUM-42 (upgraded from content) # when upgrading a CONTENT row
183
+ # Already bound cmd_xxx ↔ LUM-42 # idempotent
353
184
  lumo doc unbind cmd_xxx LUM-42
354
185
  ```
355
186
 
356
- ### `lumo doc share <doc> <member> [--role viewer|editor|manager]` — share with a workspace member
187
+ ## `lumo doc share / unshare / share-list` — member sharing
357
188
 
358
- Adds (or updates) a DocumentShare row for the given member. `<member>` resolves the same way as `task --assignee`: `me`, an email, or a display name (case-insensitive). Role defaults to `viewer`.
189
+ `share <doc> <member> [--role viewer|editor|manager]` adds (or updates) a DocumentShare row. `<member>` resolves the same way as `task --assignee`: `me`, an email, or a display name (case-insensitive). Role defaults to `viewer`. **Auto-promotion**: invoked on a PRIVATE doc, the server transactionally flips visibility to SHARED before upserting the share row (sharing = intent that it should be SHARED). A second invocation with a different role updates the row in place.
359
190
 
360
- **Auto-promotion**: when invoked on a PRIVATE doc, server-side transactionally flips the doc's visibility to SHARED before upserting the share row. No flag needed sharing a doc is treated as expressing the intent that it should be SHARED.
191
+ `unshare <doc> <member>` is idempotent (prints `Not shared with <name>`, exit 0 if no row). It does **not** flip visibility back to PRIVATE do that explicitly via `doc update --scope personal`.
361
192
 
362
- Examples:
193
+ `share-list <doc>` prints fixed-width rows `<displayName> <ROLE>`; empty output = no shares (may still be WORKSPACE-visible).
363
194
 
364
195
  ```bash
365
196
  lumo doc share "RFC" alice@example.com --role editor
366
197
  lumo doc share cmd_xxx "Alice Wong" --role manager
367
- lumo doc share cmd_xxx me # share with yourself (rare; mostly for testing)
368
- ```
369
-
370
- Output:
371
-
372
- ```
373
- Shared cmd_xxx ↔ Alice Wong (EDITOR)
374
- ```
375
-
376
- A second invocation with a different role updates the existing row in place (upsert semantics).
377
-
378
- ### `lumo doc unshare <doc> <member>` — remove a member's share
379
-
380
- Idempotent. If the member has no share row, prints `Not shared with <name>` and exits 0.
381
-
382
- ```bash
383
198
  lumo doc unshare "RFC" alice@example.com
384
- # → Unshared cmd_xxx ↔ Alice Wong
385
- ```
386
-
387
- Unshare does **not** flip visibility back to PRIVATE — that has to be done explicitly via `lumo doc update --scope personal`.
388
-
389
- ### `lumo doc share-list <doc>` — list current shares
390
-
391
- Prints fixed-width rows: `<displayName> <ROLE>`. Empty output means the doc has no shares (it may still be WORKSPACE-visible).
392
-
393
- ```bash
394
199
  lumo doc share-list "RFC"
395
- # Alice Wong EDITOR
396
- # Bob Chan VIEWER
397
200
  ```
398
201
 
399
202
  ### When to suggest the doc share commands
400
203
 
401
- - User says "share doc X with Y", "give Y access to X"
402
- - After `doc create --scope personal`, if the user mentions teammates needing access, suggest `doc share` rather than `doc update --scope workspace` when only specific members should see it
403
- - Before `doc unshare`, run `doc share-list` if the user hasn't named a specific member
204
+ - User says "share doc X with Y", "give Y access to X".
205
+ - After `doc create --scope personal`, if teammates need access, suggest `doc share` rather than `doc update --scope workspace` when only specific members should see it.
206
+ - Before `doc unshare`, run `doc share-list` if the user hasn't named a specific member.
404
207
 
405
- ### `lumo doc import-gdoc <url> [--scope personal|workspace] [--task LUM-N]` — import a Google Doc
208
+ ## `lumo doc import-gdoc <url> [--scope ] [--task LUM-N]` — import a Google Doc
406
209
 
407
210
  One-way import of a Google Doc into Lumo. The doc is exported from Google as markdown and turned into a native Lumo document (markdown → HTML), storing the source `googleDocId` and importer so it can be re-synced later (`lumo doc sync`). `<url>` accepts a Google Doc URL or a bare doc id.
408
211
 
@@ -411,62 +214,30 @@ One-way import of a Google Doc into Lumo. The doc is exported from Google as mar
411
214
  | `--scope <scope>` | enum | `personal` (→ PRIVATE) or `workspace` (→ WORKSPACE). Omit to use the server default scope. |
412
215
  | `--task <LUM-N>` | string | Bind the imported doc to this task immediately after import. |
413
216
 
414
- **Over-share note:** once imported, the content follows **Lumo's** sharing model (PRIVATE / SHARED / WORKSPACE) and is **no longer gated by Google permissions**. Importing a `workspace`-scoped doc can therefore expose it to everyone in the workspace even if the Google Doc was restricted — the command prints this reminder on success.
415
-
416
- Requires a connected Google Drive integration; connect it in the Web UI at `/settings/integrations`. There is no CLI `google auth` command.
217
+ **Over-share note:** once imported, the content follows **Lumo's** sharing model (PRIVATE / SHARED / WORKSPACE) and is **no longer gated by Google permissions**. Importing a `workspace`-scoped doc can expose it to everyone in the workspace even if the Google Doc was restricted — the command prints this reminder on success. Requires a connected Google Drive integration (connect in the Web UI at `/settings/integrations`; there is no CLI `google auth` command).
417
218
 
418
219
  ```bash
419
- lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit"
420
220
  lumo doc import-gdoc "https://docs.google.com/document/d/<id>/edit" --scope workspace --task LUM-127
421
221
  ```
422
222
 
423
- Output:
424
-
425
- ```
426
- Imported cmd_xxx "Quarterly Plan" https://www.uselumo.ai/workspace/lumo/documents/quarterly-plan-42
427
- Note: imported content follows Lumo sharing and is no longer gated by Google permissions.
428
- Bound cmd_xxx ↔ LUM-127
429
- ```
430
-
431
- The `Bound ... ↔ LUM-N` line appears only when `--task` is supplied.
432
-
433
223
  ### When to suggest `doc import-gdoc`
434
224
 
435
225
  - User pastes a Google Doc URL or says "import this Google Doc", "pull this gdoc into Lumo".
436
226
  - User wants a Google Doc tracked alongside Lumo tasks/docs — suggest import (with `--task LUM-N` if a task is in context) and remind them about the over-share semantics for `--scope workspace`.
437
227
 
438
- ### `lumo doc sync <doc>` — re-sync an imported doc from Google
228
+ ## `lumo doc sync <doc>` — re-sync an imported doc from Google
439
229
 
440
- Re-imports a previously imported Google Doc and **overwrites the Lumo body** with the current Google content. **One-way and destructive** — any edits made to the doc inside Lumo are discarded; Google is the source of truth for synced docs.
441
-
442
- Sync always runs as the **importer** (owner model): it re-exports using the importer's stored Google token, not the token of whoever runs the command. If the importer has lost access to the Google Doc, sync fails with a clear error.
443
-
444
- `<doc>` accepts a doc cuid or a case-insensitive title; ambiguous titles fail with a candidate list — re-run with the cuid.
230
+ Re-imports a previously imported Google Doc and **overwrites the Lumo body** with the current Google content. **One-way and destructive** — any edits made inside Lumo are discarded; Google is the source of truth for synced docs. Sync always runs as the **importer** (owner model): it re-exports using the importer's stored Google token, not the caller's. If the importer has lost access, sync fails with a clear error. `<doc>` accepts a cuid or case-insensitive title.
445
231
 
446
232
  ```bash
447
- lumo doc sync cmd_xxx
448
233
  lumo doc sync "Quarterly Plan"
449
234
  ```
450
235
 
451
- Output:
452
-
453
- ```
454
- Synced cmd_xxx "Quarterly Plan" from Google
455
- ```
456
-
457
236
  ### When to suggest `doc sync`
458
237
 
459
238
  - User says "re-sync the Google Doc", "pull the latest from Google", "refresh the imported doc".
460
239
  - After the user mentions the Google Doc changed upstream. Warn first that local Lumo edits to that doc will be overwritten (one-way, destructive).
461
240
 
462
- ### Out of scope (CLI v1)
463
-
464
- The CLI does **not** currently support:
465
-
466
- - `--from-editor` (interactive $EDITOR).
467
- - Lossless markdown round-trip from **rendered** `doc show` output (use `doc show --raw` — lossless whenever a markdown source is stored).
468
- - Reordering siblings within the same parent (`--before` / `--after`); use the Web UI for that.
469
-
470
- ### When to suggest session binding for docs
241
+ ## When to suggest session binding for docs
471
242
 
472
- If user creates a doc with `--task LUM-N` and the current Claude Code session is not bound, suggest `lumo session attach LUM-N` so subsequent hook events also tag that task.
243
+ If the user creates a doc with `--task LUM-N` and the current Claude Code session is not bound, suggest `lumo session attach LUM-N` so subsequent hook events also tag that task.
@@ -40,6 +40,9 @@ lumo memory sync --no-anchor-check # skip the post-sync code-anchor staleness ch
40
40
  # Upsync locally-authored memories to the team (reverse direction)
41
41
  lumo memory push # promote <memory-dir>/outbox/*.json to the team
42
42
  lumo memory push --dry-run # list what would be pushed without sending
43
+
44
+ # Topic folding — autonomous (daily cron); the CLI is the read-only preview only
45
+ lumo memory fold --dry-run # preview the next auto-fold pass; writes nothing
43
46
  ```
44
47
 
45
48
  When the session is bound (`lumo session attach <LUM-N>`), omit the identifier:
@@ -97,13 +100,18 @@ during the transition both paths are active and overlap is expected, not a bug.
97
100
  - **Only touches what it owns**: a file is owned only if its frontmatter carries
98
101
  `metadata.lumo.source: team`. Your own hand-written memory files and any
99
102
  `MEMORY.md` lines outside the managed block are **never** read or written.
100
- - **Selection**: judge-used 履历 (LUM-539) ranks first, with relevance/cold-start
101
- grace as backfill, capped to the resident-index budget so the local index
102
- stays compact.
103
- - **Routing (per recipient)**: the bundle is scoped to the calling dev — memories
104
- are filtered by relevance to that dev's active (assigned, not-DONE) tasks in the
105
- project, so different devs get differentiated sets rather than the whole corpus.
106
- A dev with no active tasks falls back to the full budget-capped set.
103
+ - **Mirrors the whole project (LUM-552)**: the bundle is the project's **full
104
+ ACTIVE memory set** every dev in the project gets the same corpus, regardless
105
+ of how many active tasks they hold. There is **no** task-level routing/relevance
106
+ filter and **no** token-budget cap on the corpus at sync time: sync only writes
107
+ local files (no tokens, no conversation), so "which memory is relevant" is left
108
+ to Claude Code's native recall, not decided at sync. judge-used 履历 (LUM-539)
109
+ still orders the resident index (so the most-proven memories list first) but
110
+ never drops anything. Relevance/budget gating remains only on the **injection**
111
+ path (`lumo task context` / session-start), which does spend tokens.
112
+ - **Project-level routing only**: you still don't sync a project you're not a
113
+ member of — that's the API route's workspace authorization. Within a project,
114
+ everyone gets the full set.
107
115
  - **Reconcile / drift**: re-running is idempotent. A team file you edited locally
108
116
  is detected (its on-disk hash diverged from the recorded `contentHash`) and
109
117
  **skipped, never overwritten** — your edit is preserved and flagged as an
@@ -149,6 +157,48 @@ successful push removes the file from the outbox.
149
157
  shared with the team — drop a `{category, content}` JSON in the memory `outbox/`
150
158
  and run `lumo memory push` (or just use `lumo project memory add` for a one-off).
151
159
 
160
+ ### Automatic sync/push triggers (LUM-551)
161
+
162
+ Both directions also fire automatically, best-effort — a failure never blocks the
163
+ session or command:
164
+
165
+ - **Downsync** runs on `lumo session attach` (throttle-gated, default 12h; see
166
+ [sessions.md](sessions.md)). Manual `lumo memory sync` stays unthrottled.
167
+ - **Upsync** runs on the `session-end` / `stop` / `task-completed` lifecycle hooks:
168
+ a non-empty `<memory-dir>/outbox/*.json` is drained via the same
169
+ create-project-memory pipeline as `lumo memory push`. An **empty outbox does zero
170
+ network** (the fast path checked first), so the high-frequency `stop` hook stays
171
+ cheap. This fills the gap left by the deleted `lumo session wrap` (LUM-544).
172
+ - **Env vars**: `LUMO_SYNC_THROTTLE_HOURS` (downsync throttle window, default 12),
173
+ `LUMO_DISABLE_MEMORY_AUTO=1` (disable **both** auto-paths). The `--no-anchor-check`
174
+ flag on `lumo memory sync` is unchanged.
175
+
176
+ ### `lumo memory fold [project-ref] --dry-run` (topic folding — read-only preview)
177
+
178
+ Topic folding is the **third** memory-consolidation operation (orthogonal to P5
179
+ near-dup merge and P4 retire): it clusters a project's ACTIVE PROJECT memories by
180
+ **subsystem/theme** (related-but-not-duplicate fine cards — e.g. several Prisma
181
+ gotchas, several jest gotchas), synthesizes **one coarse "subsystem card"** per
182
+ cluster that preserves every source card's invariant/gotcha, and supersedes the
183
+ covered sources into it (失效非删, reversible). Folding **runs autonomously** on a
184
+ daily cron with **no human review** — a fail-closed machine coverage gate decides
185
+ which sources are safe to fold (a source is only superseded if a judge confirms,
186
+ with a verbatim quote, that its actionable detail is present in the coarse card).
187
+ There is **no manual apply and no `unfold`** — to correct a coarse card, edit it in
188
+ the web Memory tab; superseded sources are preserved.
189
+
190
+ The CLI exposes only a **read-only `--dry-run` preview** (bare `lumo memory fold`
191
+ without `--dry-run` just prints that folding is automatic). It prints each proposed
192
+ coarse card + how many fine cards it would fold / leave ACTIVE. Defaults to the
193
+ session-bound project; pass `[project-ref]` for another.
194
+
195
+ - `--dry-run` is required for the preview (writes nothing; the actual folding only
196
+ ever runs server-side in the daily cron).
197
+
198
+ **When to suggest**: when a project's PROJECT memory is "又多又细" (many fine-grained
199
+ but non-duplicate cards bloating the resident MEMORY.md index) and you want to see
200
+ how the next auto-fold pass would collapse it.
201
+
152
202
  ### Reconcile-on-write & deduplication
153
203
 
154
204
  `memory add` does **not** unconditionally insert a new row. Before writing it:
@@ -26,7 +26,7 @@ lumo setup --project --force # same via global install: overwrite sk
26
26
  lumo setup --project --agent codex # bake agent=codex into the hook commands
27
27
  ```
28
28
 
29
- Use when:
29
+ #### When to suggest
30
30
 
31
31
  - The user asks "how do I set up lumo", "install the lumo skill", "wire up lumo hooks"
32
32
  - A fresh checkout of a project that uses Lumo doesn't have `.claude/skills/lumo/SKILL.md` yet
@@ -45,7 +45,7 @@ Walks the user through creating an API key in the web app and pasting it back. O
45
45
  lumo auth login
46
46
  ```
47
47
 
48
- Use when:
48
+ #### When to suggest
49
49
 
50
50
  - `lumo whoami` reports "Not logged in"
51
51
  - A bearer call returns 401 (`API key invalid or revoked`) and the user wants to re-auth
@@ -75,7 +75,7 @@ lumo whoami
75
75
  # API: https://www.uselumo.ai
76
76
  ```
77
77
 
78
- Use when:
78
+ #### When to suggest
79
79
 
80
80
  - The user asks "who am I", "which workspace am I in", "what account is this"
81
81
  - You need to disambiguate whether `lumo task list` would hit the user's expected workspace
@@ -93,7 +93,7 @@ lumo update
93
93
 
94
94
  The CLI also performs a passive check at most once every 24 hours (a detached child process refreshes a cache at `~/.lumo/update-check.json`), and prints a one-line "Update available" notice on subsequent invocations when a newer version is in the cache.
95
95
 
96
- Use when:
96
+ #### When to suggest
97
97
 
98
98
  - The user asks "how do I update", "upgrade lumo", "is there a new version"
99
99
  - A bug-fix or feature mentioned in conversation requires a newer CLI than the user is running