@lumoai/cli 1.45.0 → 1.47.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.
@@ -83,7 +83,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
83
83
 
84
84
  **Verification (machine acceptance loop)** — see [verify.md](references/verify.md)
85
85
 
86
- - `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally and report a structured PASS/FAIL verdict per criterion. Round cap 3: all-pass → task to IN_REVIEW (agent stops); round-3 fail → escalate to human. **Run this before claiming a task is done.**
86
+ - `lumo verify [task] --note "<self-report>" [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally and report a structured PASS/FAIL verdict per criterion. **`--note` is required** when a round will be posted: a one-line self-report ("我改了 X,因为 Y 可验收") frozen as your task's claim (source AGENT) the moment the round passes into IN_REVIEW, then checked against the diff for faithfulness. No note → no round posted (none burned). Round cap 3: all-pass → task to IN_REVIEW (agent stops); round-3 fail → escalate to human. **Run this before claiming a task is done.**
87
87
  - `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM): the contract with each criterion's latest verdict, verification history/round, last failure reasons, `nextActions` (the unmet criteria), and any OPEN boundary crossings (fails closed — `null`/`⚠` means "could not confirm", not safe). **Run it first when resuming a task or after a round was rejected.**
88
88
  - `lumo verdict [task] --pass | --fail` — acceptance verdicts. `--pass` deep-links to the human verdict bar (records nothing). `--fail --reason <enum> …` records an AGENT send-back → IN_PROGRESS. **An unresolved send-back blocks the DONE transition with 409.**
89
89
  - `lumo crossing explain <id> --note "<text>"` — append an agent self-explanation ("申辩") to a boundary crossing; append-only, **never clears the crossing or unblocks Done** (disposition stays web + human-only). A review aid, not a self-clear.
@@ -137,7 +137,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
137
137
 
138
138
  - `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
139
139
  - `lumo session status` — show current binding
140
- - End-of-session housekeeping is fully automatic (no command — removed in LUM-544). On DONE the server runs three best-effort silent passes: Layer-1 memory curation, fragment-usage audit, and blocked-tag automation. See [sessions.md](references/sessions.md)
140
+ - End-of-session housekeeping is fully automatic (no command). On DONE the server runs three best-effort silent passes: Layer-1 memory curation, fragment-usage audit, and blocked-tag automation. See [sessions.md](references/sessions.md)
141
141
  - Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
142
142
 
143
143
  **Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
@@ -148,10 +148,10 @@ The command catalog below is a **map**: it lists every command grouped by domain
148
148
 
149
149
  ## Commands & flags that do NOT exist (common mistakes)
150
150
 
151
- Measured from real agent sessions (LUM-392) — don't guess these:
151
+ Don't guess these:
152
152
 
153
153
  - No `lumo session start` — binding is `lumo session attach <LUM-N>`
154
- - No `lumo session detach` — the session↔task binding is a lifetime lock (LUM-459); to work on a different task, start a new Claude Code session
154
+ - No `lumo session detach` — the session↔task binding is a lifetime lock; to work on a different task, start a new Claude Code session
155
155
  - No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
156
156
  - No `lumo task artifact edit` — it's `lumo task artifact update`
157
157
  - No `lumo auth status` — identity check is `lumo whoami`
@@ -175,7 +175,7 @@ Measured from real agent sessions (LUM-392) — don't guess these:
175
175
 
176
176
  **Git-suggest at start:** when unbound, session-start may infer the task from the git branch / recent commits (any team prefix, e.g. `SPEC-12`) and print `Detected LUM-N … Run lumo session attach LUM-N to bind.` — **without** binding. Confirm it's right, then attach yourself. See [sessions.md](references/sessions.md)
177
177
 
178
- ### Golden rules (most-violated — LUM-392)
178
+ ### Golden rules (most-violated)
179
179
 
180
180
  - **Attach + draft criteria before coding.** Skipping attach triggers `SESSION_BINDING_MISSING`; skipping criteria means verification has nothing to check.
181
181
  - **Evidence before "done".** Never claim done from reading code — run `lumo verify`. While a PR is open, the task is IN_REVIEW, not DONE.
@@ -1,6 +1,6 @@
1
1
  # Acceptance criteria (contract)
2
2
 
3
- The acceptance contract is a small set of structured criteria the task's work is verified against (Acceptance v1, LUM-341/342). The agent drafts it; the server validates and stores it; verification rounds (`lumo verify`, Slice 1 task #3) judge against it. Criteria are injected at session start and in `lumo task context` as the `## Acceptance criteria (contract)` section.
3
+ The acceptance contract is a small set of structured criteria the task's work is verified against. The agent drafts it; the server validates and stores it; verification rounds (`lumo verify`) judge against it. Criteria are injected at session start and in `lumo task context` as the `## Acceptance criteria (contract)` section.
4
4
 
5
5
  ## When to draft — the golden rule
6
6
 
@@ -45,7 +45,7 @@ The 3–7 range is calibrated for typical multi-file tasks. Criterion count must
45
45
  - **Outcome-level definition of done, not micro-steps** — a verifiable result (`"lumo task criteria set rejects a second agent draft with 409"`), not a task step ("add a check in the service").
46
46
  - **Repo-wide baselines don't take slots** — tests pass / `tsc --noEmit` clean / i18n locale parity / lint are already required by the repo's PR checklist; never spend one of your 3–7 criteria on them.
47
47
  - `evidenceRequired: true` — marks criteria whose verdict must point at proof (a MACHINE PASS always requires evidence regardless of this flag).
48
- - **Edited a MACHINE checkpointer mid-task? Re-run `lumo verify`.** A prior PASS was recorded against the old command — swapping the checkpointer (or rewording the criterion) makes that pass stale: `lumo task status` / the acceptance tab flag it `⚠ pre-edit version` (LUM-457). The stale pass still counts as met (render-only, doesn't block DONE), but re-verify so the green reflects the current check.
48
+ - **Edited a MACHINE checkpointer mid-task? Re-run `lumo verify`.** A prior PASS was recorded against the old command — swapping the checkpointer (or rewording the criterion) makes that pass stale: `lumo task status` / the acceptance tab flag it `⚠ pre-edit version`. The stale pass still counts as met (render-only, doesn't block DONE), but re-verify so the green reflects the current check.
49
49
 
50
50
  **MACHINE vs HUMAN:**
51
51
 
@@ -55,10 +55,10 @@ The 3–7 range is calibrated for typical multi-file tasks. Criterion count must
55
55
 
56
56
  **Two checkpointer traps:**
57
57
 
58
- - `npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` — jest-by-name checkpointers must go through this zero-match guard. A bare `npx jest -t '<name>'` exits 0 even when the pattern matches **no** test — rename or delete the test and the checkpointer silently fake-PASSes (same trap class as the BSD-grep `-P` incident, LUM-401). `jest-t.ts` fails unless ≥1 matching test ran and passed; the optional path scopes the run to one file (much faster than name-filtering the whole suite).
59
- - `instance-independent, never self-comparing` — a checkpointer is stored once and re-run for the life of the task, so it must not depend on a baseline that shifts under it. The trap is a `vs origin/main` self-comparison ("current file is strictly smaller than origin/main"): passes pre-merge, but the moment the change merges every branch cut from main has `current == base` and the check fails forever (LUM-433 c1, fixed in #528). Anchor to a **fixed, absolute target** — a literal budget (`byteCount ≤ 9500`), a committed fixture, or a stable named test through the zero-match guard. A jest-by-name checkpointer likewise rots when its test is renamed/deleted — often by _another_ task's work, so the stored PASS goes stale invisibly. Keep test names stable; `npx tsx scripts/checkpointer-drift.ts <LUM-N> | --mine` statically sweeps stored contracts for these zero-match checkpointers (detection-only).
58
+ - `npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` — jest-by-name checkpointers must go through this zero-match guard. A bare `npx jest -t '<name>'` exits 0 even when the pattern matches **no** test — rename or delete the test and the checkpointer silently fake-PASSes. `jest-t.ts` fails unless ≥1 matching test ran and passed; the optional path scopes the run to one file (much faster than name-filtering the whole suite).
59
+ - `instance-independent, never self-comparing` — a checkpointer is stored once and re-run for the life of the task, so it must not depend on a baseline that shifts under it. The trap is a `vs origin/main` self-comparison ("current file is strictly smaller than origin/main"): passes pre-merge, but the moment the change merges every branch cut from main has `current == base` and the check fails forever. Anchor to a **fixed, absolute target** — a literal budget (`byteCount ≤ 9500`), a committed fixture, or a stable named test through the zero-match guard. A jest-by-name checkpointer likewise rots when its test is renamed/deleted — often by _another_ task's work, so the stored PASS goes stale invisibly. Keep test names stable; `npx tsx scripts/checkpointer-drift.ts <LUM-N> | --mine` statically sweeps stored contracts for these zero-match checkpointers (detection-only).
60
60
 
61
- ### judgeSteps — agent-drafted judging steps for HUMAN criteria (LUM-465)
61
+ ### judgeSteps — agent-drafted judging steps for HUMAN criteria
62
62
 
63
63
  A HUMAN criterion is judged by a person, not a checkpointer — so don't hand them a bare assertion ("the copy reads naturally") and make them reverse-engineer what to do. Attach **`judgeSteps`**: short, human-readable instructions the adjudication card renders verbatim (light markdown, URLs made clickable). Structured labour is yours; the human just follows the steps.
64
64
 
@@ -68,7 +68,7 @@ A HUMAN criterion is judged by a person, not a checkpointer — so don't hand th
68
68
  2. `What to do` — the concrete action ("open the acceptance tab", "read the `judgeSteps` block aloud", "resize to mobile width").
69
69
  3. `✓ pass / ✗ send-back` — the decision rule, both directions, so the verdict isn't a coin-flip ("pass if every URL opens; send back if any step is vague").
70
70
 
71
- **One criterion = one judgment point.** If a HUMAN criterion bundles two checks, split it — each half gets its own steps. The canonical miss is LUM-397's "additive schema **and** self-credit not implemented": two assertions; the first is even machine-able (below), the second is a separate human read.
71
+ **One criterion = one judgment point.** If a HUMAN criterion bundles two checks, split it — each half gets its own steps. The canonical miss is an "additive schema **and** self-credit not implemented" criterion: two assertions; the first is even machine-able (below), the second is a separate human read.
72
72
 
73
73
  **Machine-able → make it MACHINE, don't burn human attention.** Before writing `judgeSteps`, ask whether a checkpointer could decide it. "The migration is purely additive" → `grep` the `migration.sql` for `DROP`/destructive DDL → MACHINE. "No file under `prisma/migrations/` was deleted" → a `git diff` probe → MACHINE. Reserve HUMAN + `judgeSteps` for genuine taste/feel/fidelity judgments a check can't make.
74
74
 
@@ -90,7 +90,7 @@ Most criteria assert that **something that should happen, happened** ("the endpo
90
90
  - `How to phrase it` — state the invariant as _still holding after the change_, not as a step you took. The **c-CRAB boundary** is the hard rule: encode **"the problem does not (re)occur"**, never **"a specific fix exists"**. Write "`prisma/migrations/` has no deleted files vs origin/main" (the bad state is absent) — not "the migration-delete guard function is present" (a named fix). The first survives a refactor of _how_ the invariant is enforced; the second re-fails the moment someone renames the guard, and passes even if the protection was gutted some other way.
91
91
  - `Pairing with a checkpointer` — an invariant almost always has a runnable check (its advantage), so prefer MACHINE. The existing `checkpointer` syntax carries it as-is: a `git diff` path/filter probe, a full-suite `jest` run, a structure-verify script, a parity check.
92
92
 
93
- Two real-repo invariant examples — `prisma/migrations/` files never deleted (the CLAUDE.md red line), and a live-doc's table structure not flattened (the LUM-349 incident: an HTML→md round-trip silently collapsed tables to plain text):
93
+ Two real-repo invariant examples — `prisma/migrations/` files never deleted (the CLAUDE.md red line), and a live-doc's table structure not flattened (an HTML→md round-trip that silently collapsed tables to plain text):
94
94
 
95
95
  ```json
96
96
  [
@@ -107,7 +107,7 @@ Two real-repo invariant examples — `prisma/migrations/` files never deleted (t
107
107
  ]
108
108
  ```
109
109
 
110
- A third lives in a current contract you can copy: **LUM-415**'s "SKILL.md frontmatter description stays ≤ 1000 chars" — the standing context-budget invariant (the description is resident in every session), checkpointed by the `description cap` case in `scripts/analysis/lum392-cli-friction/__tests__/doc-examples.test.ts`.
110
+ A third example invariant is "SKILL.md frontmatter description stays ≤ 1000 chars" — the standing context-budget invariant (the description is resident in every session), checkpointed by the `description cap` case in `scripts/analysis/lum392-cli-friction/__tests__/doc-examples.test.ts`.
111
111
 
112
112
  One invariant criterion is usually enough — it's the guardrail, not the whole contract. Pair it with the positive criteria that say what the change should achieve; together they assert _did the right thing_ **and** _touched nothing it shouldn't_.
113
113
 
@@ -178,7 +178,7 @@ Optionally annotate **why** the contract drifted with `--cause <NEW_INFO|SCOPE_C
178
178
  - `GRANULARITY` — wrong granularity.
179
179
  - `OTHER` — anything else.
180
180
 
181
- The tag lands in the drift record (TaskActivity payload), feeding the Slice-3 drift-cause distribution. Every criteria add/update/delete is mirrored as a structured `CRITERION_CHANGED` activity automatically; `--cause` just enriches it.
181
+ The tag lands in the drift record (TaskActivity payload). Every criteria add/update/delete is mirrored as a structured `CRITERION_CHANGED` activity automatically; `--cause` just enriches it.
182
182
 
183
183
  ### `lumo task criteria list <task>`
184
184
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Commands for **reading a faithful edit base and writing back surgically** — the safe way to edit live docs (registers, ledgers, reports) without flattening tables or clobbering concurrent edits. For document CRUD, sharing, and import, see [docs.md](docs.md) (it also documents the shared **content channels** — `--content` / `--file` / stdin — and the **compact-tables authoring convention**, which apply equally to `doc patch` / `doc append`).
4
4
 
5
- **The red line (LUM-349):** never treat rendered `doc show` output as a re-uploadable source — HTML→markdown is lossy and flattens tables. The only legal edit base is `doc show --raw` / `--section`, which print the byte-identical stored `sourceMarkdown`.
5
+ **The red line:** never treat rendered `doc show` output as a re-uploadable source — HTML→markdown is lossy and flattens tables. The only legal edit base is `doc show --raw` / `--section`, which print the byte-identical stored `sourceMarkdown`.
6
6
 
7
7
  ## `lumo doc show <doc> [--raw | --section <heading>]` — print one document's detail
8
8
 
@@ -14,19 +14,19 @@ lumo doc show cmd_xxx --raw > base.md # byte-identical edit base (rev
14
14
  lumo doc show cmd_xxx --section "D 状态表" > sec.md # one section only (revision on stderr)
15
15
  ```
16
16
 
17
- **`--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.
17
+ **`--raw`** 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.
18
18
 
19
19
  - 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.
20
- - 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.
20
+ - 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. Rebuild flow: 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.
21
21
  - 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.
22
22
 
23
23
  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.
24
24
 
25
- 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>"` / `--raw`. `--raw` and `--section` are **never** capped — they are byte-faithful edit bases.
25
+ Output budget: **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>"` / `--raw`. `--raw` and `--section` are **never** capped — they are byte-faithful edit bases.
26
26
 
27
- **`--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`.
27
+ **`--section <heading>`** 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`.
28
28
 
29
- - 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. Prefix with `#…` to pin the level when the same text exists at several depths (`--section "## Status"`). Normalization never relaxes the ambiguity guard — multiple matches still give the candidate list + exit 1.
29
+ - Section addressing: pass the heading text (`--section "D 状态表"`), matched in three tiers — exact, then case-insensitive, then full-width↔half-width punctuation + whitespace normalization, so a half-width query (`问题(P4)`) lands on a full-width stored heading (`问题(P4)`) and vice-versa. Prefix with `#…` to pin the level when the same text exists at several depths (`--section "## Status"`). Normalization never relaxes the ambiguity guard — multiple matches still give the candidate list + exit 1.
30
30
  - Missing heading → exit 1 listing the available headings; ambiguous heading → exit 1 with a depth-disambiguation hint.
31
31
  - Requires a stored markdown source — same no-fallback rule and rebuild flow as `--raw`.
32
32
  - Heading detection is markdown-aware: `#` lines inside fenced code blocks or blockquotes are never section boundaries.
@@ -55,7 +55,7 @@ Replaces the **whole addressed section** (heading line included, subsections inc
55
55
 
56
56
  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.
57
57
 
58
- 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. Pass `--allow-shrink` when the deletion is intentional. `doc append` is pure insertion and is never guarded.
58
+ Structure guard, **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. Pass `--allow-shrink` when the deletion is intentional. `doc append` is pure insertion and is never guarded.
59
59
 
60
60
  Requires a stored markdown source (same rule as `--raw`); errors with the rebuild hint otherwise.
61
61
 
@@ -84,7 +84,7 @@ Inserts the new content at the **end of the addressed section** (just before the
84
84
  | `--file <path>` | string | Content from file (project-local sandbox). |
85
85
  | `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
86
86
 
87
- 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.
87
+ 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**: 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.
88
88
 
89
89
  ```bash
90
90
  lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ 论文"
@@ -119,7 +119,7 @@ lumo doc diff cmd_xxx --file docs/live-docs/research-intake-ledger.md
119
119
 
120
120
  ## `lumo doc rebuild-source <doc>` — regenerate the markdown source from the HTML body
121
121
 
122
- 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.
122
+ The **recovery path for a source-less doc**. 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, 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.
123
123
 
124
124
  | Flag | Type | Notes |
125
125
  | ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
@@ -127,7 +127,7 @@ The **recovery path for a source-less doc** (LUM-446). When a doc has no stored
127
127
  | `--force` | boolean | Re-derive even when a source already exists (default: 409 — protects a byte-faithful human source from a downgrade). |
128
128
  | `--if-revision <n>` | int | Only apply if the body is still at this revision (from `doc show`). |
129
129
 
130
- 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`.
130
+ The rebuild is **structure-guarded** (the same guard 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`.
131
131
 
132
132
  ```bash
133
133
  lumo doc rebuild-source cmd_xxx # restore a source-less doc; --raw works after
@@ -28,7 +28,7 @@ not the aligned form (`| col | meaning |` padded so columns line up). Two reas
28
28
  - **prettier re-pads tables.** A compact table gets re-aligned and a padded one churns the diff on every prettier run — so repo-tracked live-doc sources under `docs/live-docs/` are in `.prettierignore` to stay byte-stable (see the live-docs README).
29
29
  - **the Edit tool's exact-match fails on padding.** Incremental edits match on exact cell text; alignment whitespace makes that match fragile (measured to fail). Compact cells edit reliably.
30
30
 
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.
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; there is **no** server-side table normalization to lean on. The structure guard compares _rendered_ structure and is whitespace-insensitive, so compactness buys local editability, not a verify pass.
32
32
 
33
33
  ## `lumo doc create [title] [flags]` — create a new document
34
34
 
@@ -75,27 +75,27 @@ The cuid (`cmd_xxx`) is still printed as a stable identifier you can pass back i
75
75
 
76
76
  `<doc>` accepts a cuid or a case-insensitive title. Ambiguous titles fail with a candidate list — re-run with the cuid.
77
77
 
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). |
93
-
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`).
95
-
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`.
97
-
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.
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. |
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). |
93
+
94
+ A **metadata-only update leaves the body untouched**: 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`).
95
+
96
+ Optimistic concurrency: `--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`.
97
+
98
+ **Structure guard, 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 and stale-base section loss 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.
99
99
 
100
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.
101
101
 
@@ -88,10 +88,9 @@ detail on demand instead of carrying every memory's content in context.
88
88
  ### `lumo memory sync` (downsync to local Claude Code memory)
89
89
 
90
90
  Writes the team's project memory into the dev's local Claude Code memory store so
91
- Claude Code's **native recall** surfaces it — the direction LUM-535/536 chose over
92
- per-session `<untrusted-team-memory>` injection. This is P3 (the sync build); it
93
- runs **alongside** the existing injection (retiring injection is LUM-540), so
94
- during the transition both paths are active and overlap is expected, not a bug.
91
+ Claude Code's **native recall** surfaces it — the direction chosen over
92
+ per-session `<untrusted-team-memory>` injection. This runs **alongside** the
93
+ existing injection, so during the transition both paths are active and overlap is expected, not a bug.
95
94
 
96
95
  - **Where it writes**: `~/.claude/projects/<encoded-cwd>/memory/` — team files as
97
96
  `team/<memoryId>.md` (YAML frontmatter + a `metadata.lumo` ownership marker),
@@ -100,12 +99,12 @@ during the transition both paths are active and overlap is expected, not a bug.
100
99
  - **Only touches what it owns**: a file is owned only if its frontmatter carries
101
100
  `metadata.lumo.source: team`. Your own hand-written memory files and any
102
101
  `MEMORY.md` lines outside the managed block are **never** read or written.
103
- - **Mirrors the whole project (LUM-552)**: the bundle is the project's **full
102
+ - **Mirrors the whole project**: the bundle is the project's **full
104
103
  ACTIVE memory set** — every dev in the project gets the same corpus, regardless
105
104
  of how many active tasks they hold. There is **no** task-level routing/relevance
106
105
  filter and **no** token-budget cap on the corpus at sync time: sync only writes
107
106
  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)
107
+ to Claude Code's native recall, not decided at sync. Judge-used 履历
109
108
  still orders the resident index (so the most-proven memories list first) but
110
109
  never drops anything. Relevance/budget gating remains only on the **injection**
111
110
  path (`lumo task context` / session-start), which does spend tokens.
@@ -118,7 +117,7 @@ during the transition both paths are active and overlap is expected, not a bug.
118
117
  upsync candidate (the reverse direction is a later phase).
119
118
  - **Reversible**: `--dry-run` prints the add/update/remove/drift-skip plan without
120
119
  writing; `--clean` removes every owned file + the managed block (full rollback).
121
- - **Code-anchor staleness check (LUM-547 P4b)**: after downsync, each synced memory's
120
+ - **Code-anchor staleness check**: after downsync, each synced memory's
122
121
  code anchors (file paths, and backtick-wrapped symbols/flags) are checked against
123
122
  this repo — resolved against **git-tracked** files (`git ls-files`) + exact
124
123
  word-boundary grep, never the dirty working tree. A memory whose **every** anchor
@@ -142,7 +141,7 @@ store. Each candidate is a JSON file in `<memory-dir>/outbox/` shaped
142
141
  `{ "category": "convention", "content": { ... } }` (the same per-category content
143
142
  shape as `lumo project memory add`). `push` POSTs each to the **existing**
144
143
  create-project-memory endpoint, so it runs through the same canonicalize → dedup →
145
- **reconcile-on-write** pipeline (LUM-538) — no parallel upsync path — and a
144
+ **reconcile-on-write** pipeline — no parallel upsync path — and a
146
145
  successful push removes the file from the outbox.
147
146
 
148
147
  - **Scope**: structured local memories (the lossless path). A team file you edited
@@ -157,7 +156,7 @@ successful push removes the file from the outbox.
157
156
  shared with the team — drop a `{category, content}` JSON in the memory `outbox/`
158
157
  and run `lumo memory push` (or just use `lumo project memory add` for a one-off).
159
158
 
160
- ### Automatic sync/push triggers (LUM-551)
159
+ ### Automatic sync/push triggers
161
160
 
162
161
  Both directions also fire automatically, best-effort — a failure never blocks the
163
162
  session or command:
@@ -168,7 +167,7 @@ session or command:
168
167
  a non-empty `<memory-dir>/outbox/*.json` is drained via the same
169
168
  create-project-memory pipeline as `lumo memory push`. An **empty outbox does zero
170
169
  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).
170
+ cheap.
172
171
  - **Env vars**: `LUMO_SYNC_THROTTLE_HOURS` (downsync throttle window, default 12),
173
172
  `LUMO_DISABLE_MEMORY_AUTO=1` (disable **both** auto-paths). The `--no-anchor-check`
174
173
  flag on `lumo memory sync` is unchanged.
@@ -4,12 +4,12 @@
4
4
 
5
5
  ### Suggest-on-start from local git (no auto-bind)
6
6
 
7
- When a session starts **without** a bound task, the `session-start` hook infers a task from local git and **suggests** it — it never binds for you (LUM-302).
7
+ When a session starts **without** a bound task, the `session-start` hook infers a task from local git and **suggests** it — it never binds for you.
8
8
 
9
9
  Detection order and rules:
10
10
 
11
11
  - `current branch name` first (e.g. `lumo/LUM-145-...`), then the `most recent commit subjects` (e.g. `... [LUM-145]`), extracting the first task identifier.
12
- - `prefix-agnostic` (LUM-419) — any team prefix matches (`SPEC-12` as much as `LUM-145`), using the same pattern the server uses to link PR branches to tasks.
12
+ - `prefix-agnostic` — any team prefix matches (`SPEC-12` as much as `LUM-145`), using the same pattern the server uses to link PR branches to tasks.
13
13
  - `well-known acronym-number tokens` (`UTF-8`, `SHA-256`, `ISO-8601`, …) are skipped, never suggested.
14
14
 
15
15
  On a hit it prints one suggestion line and stops — the session stays **unbound**, no context injected yet:
@@ -28,7 +28,7 @@ You see a suggestion line. Confirm the inferred task is the one the user wants,
28
28
 
29
29
  ### Layer 2 project-memory review at session start
30
30
 
31
- When the session is bound, session-start may inject a **"🆕 Review needed: project memories auto-consolidated by the previous session"** section alongside the memory / PR-review blocks (LUM-165). It lists the **PROJECT-scope** memories that the member's **immediately-preceding session** auto-consolidated (Layer 2 runs asynchronously when a task is marked `done`). Each item shows its `id`.
31
+ When the session is bound, session-start may inject a **"🆕 Review needed: project memories auto-consolidated by the previous session"** section alongside the memory / PR-review blocks. It lists the **PROJECT-scope** memories that the member's **immediately-preceding session** auto-consolidated (Layer 2 runs asynchronously when a task is marked `done`). Each item shows its `id`.
32
32
 
33
33
  - **Why async / next-session:** Layer 2 promotions land after the task hits DONE, so they surface at the _next_ session-start, when they've definitely landed.
34
34
  - **Show-once:** the section appears only at the session immediately following the one that produced the memories. It does **not** re-nag later, so act now or it scrolls off.
@@ -91,11 +91,11 @@ What it does:
91
91
  - Reads `CLAUDE_CODE_SESSION_ID` from the environment (Claude Code sets it automatically); errors out if unset — must run from inside a Claude Code session.
92
92
  - Calls `POST /api/sessions/<session_id>/bind-task`, which sets the Session row's `taskId` and re-tags previously-untagged HookEvent rows in this session.
93
93
  - The binding lives entirely on the server (`Session.taskId`); subsequent hooks read it back via the session row. The CLI keeps no local sentinel.
94
- - Prints the task's **acceptance contract** (`## Acceptance criteria (contract)`, LUM-342) right after the bind confirmation — or, when a still-open task has none, the draft reminder to draft 3–7 criteria before the first line of code (see [criteria.md](criteria.md)). The same section is auto-injected at session start when already bound (highest priority in the injection budget, ahead of memory).
94
+ - Prints the task's **acceptance contract** (`## Acceptance criteria (contract)`) right after the bind confirmation — or, when a still-open task has none, the draft reminder to draft 3–7 criteria before the first line of code (see [criteria.md](criteria.md)). The same section is auto-injected at session start when already bound (highest priority in the injection budget, ahead of memory).
95
95
 
96
96
  After attaching, always run `lumo task context <identifier>` to load the task background.
97
97
 
98
- #### Auto-downsync on attach (LUM-551)
98
+ #### Auto-downsync on attach
99
99
 
100
100
  A successful `session attach` also runs a **best-effort team-memory downsync** for the bound task's project — the same work as `lumo memory sync` (including the P4b code-anchor staleness check), landing the team's memory in your local Claude Code memory store without a separate command.
101
101
 
@@ -105,7 +105,7 @@ A successful `session attach` also runs a **best-effort team-memory downsync** f
105
105
  - Manual `lumo memory sync` stays unthrottled.
106
106
  - `LUMO_DISABLE_MEMORY_AUTO=1` turns off the auto-downsync (and the hook auto-upsync — see [memory.md](memory.md)) entirely; `--no-anchor-check` on `lumo memory sync` is unchanged.
107
107
 
108
- #### Lifetime lock (LUM-459)
108
+ #### Lifetime lock
109
109
 
110
110
  `Session.taskId` is **write-once**. Re-attaching to the **same** task is always a no-op re-bind (idempotent, re-emits context). Attaching to a **different** task is refused with HTTP 409 — the server returns `{ error, currentTaskIdentifier, currentTaskTitle }` and the CLI prints:
111
111
 
@@ -135,13 +135,13 @@ Prints which task the current Claude Code session is bound to, or "(no task)" if
135
135
 
136
136
  The user asks "which task am I on", "what's this session bound to", or you need to decide whether to suggest `session attach` for a mentioned task ID.
137
137
 
138
- ### Automatic end-of-session housekeeping (no command — LUM-544)
138
+ ### Automatic end-of-session housekeeping (no command)
139
139
 
140
- The old end-of-session command was **removed in LUM-544**. The three passes it ran interactively now happen **automatically server-side** — all evidence-gated, best-effort, and silent. There is nothing for the agent to run or confirm. Two fire when the bound task reaches **DONE** (`lumo task update <id> --status done`, which threads `CLAUDE_CODE_SESSION_ID` so attribution lands); one runs continuously off the failure/progress hooks.
140
+ The old end-of-session command was **removed**. The three passes it ran interactively now happen **automatically server-side** — all evidence-gated, best-effort, and silent. There is nothing for the agent to run or confirm. Two fire when the bound task reaches **DONE** (`lumo task update <id> --status done`, which threads `CLAUDE_CODE_SESSION_ID` so attribution lands); one runs continuously off the failure/progress hooks.
141
141
 
142
142
  **1. Layer-1 memory curation (on DONE).** An LLM judge reviews the Layer-1 memories each of the task's sessions recorded, against that session's event log, and **soft-invalidates only the clearly-wrong / self-contradictory ones**: the row flips to `INVALIDATED` and is excluded from injection but **kept for audit — never hard-deleted**. **Uncertain memories are left untouched** (the judge defaults to keeping). Promotion to project scope is **not** done here — that stays with the Layer-2 flow (surfaced at the next session-start, see above).
143
143
 
144
- **2. Fragment-usage audit (LUM-314, on DONE).** An LLM judge sees the fragments this session consumed (its lineage edges) plus the session's event log and votes which were **actually used**:
144
+ **2. Fragment-usage audit (on DONE).** An LLM judge sees the fragments this session consumed (its lineage edges) plus the session's event log and votes which were **actually used**:
145
145
 
146
146
  - confidently-used edges → `used=true`
147
147
  - confidently-unused → `used=false`
@@ -149,7 +149,7 @@ The old end-of-session command was **removed in LUM-544**. The three passes it r
149
149
 
150
150
  Already-voted sessions are skipped; a cron backstop drains any backlog. **Why:** upgrades the flywheel signal from "co-loaded" (constant) to "actually used" (discriminative); `task context` then prefers each fragment's usage-based merge rate, falling back to the presence rate when usage samples are thin.
151
151
 
152
- **3. Blocked-tag automation (LUM-544 §3, server-side).** When a session crosses the same-tool failure threshold (**≥ 3** same-type failures, aggregated from `POST_TOOL_USE_FAILURE` grouped by tool name + `STOP_FAILURE` turn-level failures), the server **auto-applies the shared `blocked` tag** to the bound task. This **inverts the old LUM-153 manual `y` gate** — no prompt, no human in the loop — and is safe via three safeguards:
152
+ **3. Blocked-tag automation (server-side).** When a session crosses the same-tool failure threshold (**≥ 3** same-type failures, aggregated from `POST_TOOL_USE_FAILURE` grouped by tool name + `STOP_FAILURE` turn-level failures), the server **auto-applies the shared `blocked` tag** to the bound task. This inverts the old manual gate — no prompt, no human in the loop — and is safe via three safeguards:
153
153
 
154
154
  - **idempotent:** at most one active auto-block per task, so re-crossing is a no-op.
155
155
  - **auto-untag on progress:** the next observable progress (a successful tool call or a non-failure turn end) removes the tag.
@@ -13,7 +13,7 @@ lumo task context LUM-42
13
13
  The command prints a markdown document to stdout with these sections, in order:
14
14
 
15
15
  1. **Task header** — identifier, title, status, description.
16
- 2. **`## Acceptance criteria (contract)`** (LUM-342) — shown right after the header. Each line `[MACHINE|HUMAN] statement`, with a `↳ check:` line for MACHINE checkpointers; HUMAN_EDIT / REVIEW_ADDED provenance tagged inline. A still-open task with none shows a draft reminder instead — draft 3–7 criteria **before writing code** (see [criteria.md](criteria.md)).
16
+ 2. **`## Acceptance criteria (contract)`** — shown right after the header. Each line `[MACHINE|HUMAN] statement`, with a `↳ check:` line for MACHINE checkpointers; HUMAN_EDIT / REVIEW_ADDED provenance tagged inline. A still-open task with none shows a draft reminder instead — draft 3–7 criteria **before writing code** (see [criteria.md](criteria.md)).
17
17
  3. **Memory section** — cross-session learnings; trusted background context that persists, so you avoid re-learning decisions/constraints.
18
18
  4. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below).
19
19
  5. **`## PR review todos`** — mirrored PR review comments as a checkbox todo list. Each line-level comment shows `` `file:line` `` + reviewer's ask + GitHub comment link; each `changes_requested` review summary shows "🛑 Changes requested (whole PR)". Present only when the task's PR(s) have review comments. Each unchecked box is a TODO: resolve it, then reply on the PR (a Lumo comment mirrors back to GitHub).
@@ -33,7 +33,7 @@ The command prints a markdown document to stdout with these sections, in order:
33
33
 
34
34
  ## Context Retrieval (full text on demand)
35
35
 
36
- LUM-122 split context injection into tiers: `lumo task context <LUM-N>` emits a **cheap inline card** per source instead of dumping full bodies. Slack/docs/artifacts/comments get an **LLM summary**; web/Figma/PR get **metadata only**. Each card ends with the **retrieval command** for the heavy content.
36
+ Context injection is split into tiers: `lumo task context <LUM-N>` emits a **cheap inline card** per source instead of dumping full bodies. Slack/docs/artifacts/comments get an **LLM summary**; web/Figma/PR get **metadata only**. Each card ends with the **retrieval command** for the heavy content.
37
37
 
38
38
  Run the matching command below when the card isn't enough. Pass the same `LUM-N` plus the id the card shows for that source:
39
39
 
@@ -47,7 +47,7 @@ Run the matching command below when the card isn't enough. Pass the same `LUM-N`
47
47
 
48
48
  All five are **read-only** (no live Slack/GitHub/Figma calls except the web body fetch). Web/Figma/PR are v1 metadata-degraded: they print a `note:` saying live content needs an external integration.
49
49
 
50
- **Output budget (LUM-428):** the whole `task context` handoff is capped to the output-token budget (25,000 tokens). On overflow, output is truncated and ends in a pointer to the precise sub-commands (`lumo task status` / `task comments list --full` / `task lineage` / `doc show`) to pull any dropped section just-in-time.
50
+ **Output budget:** the whole `task context` handoff is capped to the output-token budget (25,000 tokens). On overflow, output is truncated and ends in a pointer to the precise sub-commands (`lumo task status` / `task comments list --full` / `task lineage` / `doc show`) to pull any dropped section just-in-time.
51
51
 
52
52
  ### `lumo task slack show <identifier> <contextId>` — full Slack thread snapshot
53
53
 
@@ -77,7 +77,7 @@ lumo task figma context LUM-42 cfl_abc123
77
77
 
78
78
  Prints the thread: each comment as `author · createdAt` then its plain-text body (bodies stored as HTML, stripped to text). Replies indent two spaces under their parent. Author falls back to `unknown`. No comments prints `(no comments)`.
79
79
 
80
- - `lumo task comments list LUM-42` — capped to the output budget (LUM-428: 25,000 tokens; every printed line spends from your context). On overflow, truncates and ends in a fetch-more pointer: `… +N more comments not shown (output capped at 25,000 tokens) — read the whole thread with: lumo task comments list <id> --full`.
80
+ - `lumo task comments list LUM-42` — capped to the output budget (25,000 tokens; every printed line spends from your context). On overflow, truncates and ends in a fetch-more pointer: `… +N more comments not shown (output capped at 25,000 tokens) — read the whole thread with: lumo task comments list <id> --full`.
81
81
  - `lumo task comments list LUM-42 --full` — every comment, uncapped. Use only when you actually need the whole thread.
82
82
 
83
83
  ```bash
@@ -104,13 +104,13 @@ lumo task lineage LUM-42 # per-session causal trail + cost
104
104
  lumo task lineage LUM-42 --signal # append workspace-level usage signal-health
105
105
  ```
106
106
 
107
- - `lumo task lineage <id> --signal` — appends the workspace-level usage signal-health block. Used-vs-base merge rate uses iteration-taint fold (send-back / reopen / PR-close = negative class even if later merged); shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet. The block ends with a workspace-wide disclosure funnel (same format as the totals funnel below, aggregated over **all** workspace edges, not just this task) including the same `· ~T tokens saved` suffix when token data exists (LUM-522).
107
+ - `lumo task lineage <id> --signal` — appends the workspace-level usage signal-health block. Used-vs-base merge rate uses iteration-taint fold (send-back / reopen / PR-close = negative class even if later merged); shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet. The block ends with a workspace-wide disclosure funnel (same format as the totals funnel below, aggregated over **all** workspace edges, not just this task) including the same `· ~T tokens saved` suffix when token data exists.
108
108
 
109
109
  Output sections:
110
110
 
111
- - **Totals banner** — distinct sessions, fragment count, edge count, total tokens (input/output/cache split), loops, and outcome distribution. After the outcome summary, one funnel line: `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`. Impressions = edge count; INDEX% and used% are over impressions; pull% is over INDEX only (FULL fragments have no pull opportunity). Divide-by-zero guarded (zero impressions or zero INDEX renders `0%`). With per-fragment token weights collected (LUM-522), appends `· ~T tokens saved` = Σ(fullTokens − indexTokens) over un-pulled INDEX edges (the token cost index-only injection avoided); omitted cleanly when no edge carries token data (older edges predate the columns).
111
+ - **Totals banner** — distinct sessions, fragment count, edge count, total tokens (input/output/cache split), loops, and outcome distribution. After the outcome summary, one funnel line: `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`. Impressions = edge count; INDEX% and used% are over impressions; pull% is over INDEX only (FULL fragments have no pull opportunity). Divide-by-zero guarded (zero impressions or zero INDEX renders `0%`). With per-fragment token weights collected, appends `· ~T tokens saved` = Σ(fullTokens − indexTokens) over un-pulled INDEX edges (the token cost index-only injection avoided); omitted cleanly when no edge carries token data (older edges predate the columns).
112
112
  - **One block per session** — the group's cost shown **once** (token/loop), the date it consumed context, then each fragment as `[OUTCOME] TYPE — <source label>` plus a disclosure suffix: `· INDEX pulled` (INDEX, `pulledAt` set) / `· INDEX not-pulled` (INDEX, never pulled) / `· FULL` (injected in full at session-start). Per-group outcome summary follows.
113
- - **Top operations by token cost (LUM-523)** — the totals also append a per-task Top-5 of the most expensive tools by attributed token cost (`<tool> — N tokens`), ending with `(full breakdown: lumo cost --task <id>)`. Omitted when no per-operation cost has been attributed yet.
113
+ - **Top operations by token cost** — the totals also append a per-task Top-5 of the most expensive tools by attributed token cost (`<tool> — N tokens`), ending with `(full breakdown: lumo cost --task <id>)`. Omitted when no per-operation cost has been attributed yet.
114
114
 
115
115
  Cost is attributed once per session (a session that injected many fragments is not double-counted). Fragment ids are canonical — MEMORY fragments survive consolidation drift. **Cold start:** a task with no edges prints a friendly note (lineage is captured when a session-bound run consumes the task's context), not an error.
116
116
 
@@ -99,18 +99,18 @@ The `Tags:` line is omitted when the resulting tag set is empty.
99
99
 
100
100
  The server's transition matrix (`lib/task/state-machine.ts`):
101
101
 
102
- | From | Allowed targets |
103
- | ----------- | --------------------------------------------------- |
104
- | TODO | IN_PROGRESS, IN_REVIEW, DONE |
105
- | IN_PROGRESS | TODO, IN_REVIEW, DONE |
106
- | IN_REVIEW | TODO, IN_PROGRESS, DONE |
107
- | DONE | TODO, IN_PROGRESS (reopen only — **not** IN_REVIEW) |
102
+ | From | Allowed targets |
103
+ | ----------- | ------------------------------------- |
104
+ | TODO | IN_PROGRESS, IN_REVIEW, DONE |
105
+ | IN_PROGRESS | TODO, IN_REVIEW, DONE |
106
+ | IN_REVIEW | TODO, IN_PROGRESS, DONE |
107
+ | DONE | TODO, IN_PROGRESS, IN_REVIEW (reopen) |
108
108
 
109
109
  Practical rules:
110
110
 
111
- - **One call suffices.** `--status done` straight from TODO or IN_PROGRESS is legal — never walk `in_progress → in_review → done` as a ritual (measured in LUM-392: 70 such chains wasted ~75 calls).
111
+ - **One call suffices.** `--status done` straight from TODO or IN_PROGRESS is legal — never walk `in_progress → in_review → done` as a ritual; it just wastes calls.
112
112
  - **Under the verify flow you don't set `in_review`/`done` at all** — `lumo verify` moves the task to IN_REVIEW on all-pass and the DONE adjudication is human-only.
113
- - **DONE IN_REVIEW is rejected (409).** To attach follow-up context to a DONE task, use `lumo task comment` instead of reopening.
113
+ - **A DONE task can be reopened** — to IN_REVIEW, IN_PROGRESS, or TODO. Reopening is a plain status change and does not alter any recorded acceptance verdict; verdict adjudication is human-only and has no CLI path. To attach context without reopening, `lumo task comment` works.
114
114
 
115
115
  ### When to suggest `task update`
116
116
 
@@ -1,7 +1,6 @@
1
1
  # lumo verify — machine verification loop
2
2
 
3
- `lumo verify` is the machine half of the acceptance system (Acceptance v1,
4
- LUM-343): it executes every **MACHINE** criterion's checkpointer in the local
3
+ `lumo verify` is the machine half of the acceptance system: it executes every **MACHINE** criterion's checkpointer in the local
5
4
  repo, POSTs one structured PASS/FAIL verdict per criterion, and prints what to
6
5
  do next. Execution is on the client; adjudication is server-side — round
7
6
  numbering, the **3-round cap**, escalation, and the **IN_REVIEW** transition all
@@ -14,11 +13,22 @@ touching its status — run `lumo verify`.** The loop replaces "I read the code
14
13
  and it looks done" with executed evidence.
15
14
 
16
15
  ```bash
17
- lumo verify # session-bound task
18
- lumo verify LUM-42 # explicit task (overrides the session binding)
19
- lumo verify --timeout 900 # per-checkpointer timeout in seconds (default 600)
16
+ lumo verify --note "implemented X in foo.ts because Y; tests + tsc pass" # session-bound task
17
+ lumo verify LUM-42 --note "…" # explicit task (overrides the session binding)
18
+ lumo verify --note "…" --timeout 900 # per-checkpointer timeout in seconds (default 600)
20
19
  ```
21
20
 
21
+ **`--note` is required (LUM-597)** whenever a round will actually be posted (the
22
+ task has MACHINE criteria). It is your one-line self-report — what you did and
23
+ why it is ready ("我改了 X,因为 Y 可验收"). When the round all-passes and the
24
+ task flips to IN_REVIEW, the note is frozen as the task's **claim** (provenance
25
+ `AGENT` — the汇报者's own voice, not a summarizer paraphrase) and is checked
26
+ against the diff for faithfulness. Capturing the claim is deterministic ("不填不
27
+ 让 post"); whether it is _truthful_ is the faithfulness audit's job — so don't
28
+ under- or over-state it. A missing/blank note is refused **before** the round is
29
+ posted, so no round is burned; just re-run with one. (An old CLI that omits it
30
+ still verifies — the claim degrades to the synthesized run-summary fallback.)
31
+
22
32
  ## What one round does
23
33
 
24
34
  1. Loads the task's acceptance contract and picks out MACHINE criteria.
@@ -29,11 +39,11 @@ lumo verify --timeout 900 # per-checkpointer timeout in seconds (default 600)
29
39
  TaskActivity event.
30
40
  4. Prints the round outcome:
31
41
 
32
- | Round outcome | Effect | What to do |
33
- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
34
- | **All PASS** | Task transitions to **IN_REVIEW** (existing state machine + TASK_IN_REVIEW notification) | **Stop here.** Human adjudication + any HUMAN criteria take over; **never set DONE yourself** |
35
- | **Any FAIL** | Task status untouched; unmet criteria printed as next actions (statement, checkpointer, failure tail) | Fix and re-run |
36
- | **Round 3 still failing** | Loop escalates: a human is notified (AGENT_VERIFY, requires action); further `lumo verify` rounds are rejected with **409** | **Stop retrying**; fix only what the human directs |
42
+ | Round outcome | Effect | What to do |
43
+ | ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
44
+ | **All PASS** | Task transitions to **IN_REVIEW** (existing state machine + TASK_IN_REVIEW notification); the `--note` self-report is frozen as the task's claim (source `AGENT`) | **Stop here.** Human adjudication + any HUMAN criteria take over; **never set DONE yourself** |
45
+ | **Any FAIL** | Task status untouched; unmet criteria printed as next actions (statement, checkpointer, failure tail) | Fix and re-run |
46
+ | **Round 3 still failing** | Loop escalates: a human is notified (AGENT_VERIFY, requires action); further `lumo verify` rounds are rejected with **409** | **Stop retrying**; fix only what the human directs |
37
47
 
38
48
  Exit code 0 = all passed (or nothing to run); 1 = failures, escalation, or
39
49
  errors.
@@ -57,7 +67,7 @@ errors.
57
67
  | **HUMAN-only contract** (zero MACHINE criteria) | Nothing to run; CLI says so and suggests `lumo task update <id> --status in_review` for human review. No server write happens. |
58
68
  | **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds. |
59
69
  | **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round. |
60
- | **Session bound to a different task** (LUM-459) | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
70
+ | **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
61
71
  | **Provably-unbound session** | Response carries `bindingAdvisory: 'unbound'`; prints `⚠ Working unbound — this verify ran from a Claude Code session not attached to the task.` Recorded as a `SESSION_BINDING_MISSING` boundary crossing (visible in `lumo task status` open crossings). Run `lumo session attach <LUM-N>` before the next verify. |
62
72
  | **Unconfirmed session binding** | `bindingAdvisory: 'unconfirmed'` → softer advisory `⚠ Could not confirm this session is attached to the task.` Same remediation: `lumo session attach <LUM-N>`. |
63
73
 
@@ -85,8 +95,7 @@ clauses that can never be found wanting.
85
95
 
86
96
  ## lumo task status — the read half (self-check entry point)
87
97
 
88
- `lumo task status [task] [--json]` is the read-only counterpart of the loop
89
- (LUM-344): pure read, milliseconds, no LLM, never writes — running it costs
98
+ `lumo task status [task] [--json]` is the read-only counterpart of the loop: pure read, milliseconds, no LLM, never writes — running it costs
90
99
  nothing and burns no round. Defaults to the session-bound task; an explicit
91
100
  identifier overrides.
92
101
 
@@ -111,31 +120,35 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
111
120
  ### What it prints
112
121
 
113
122
  - **Header** — task identifier/title/status + `verification round N/3` (round 0 = never verified) + an escalation warning when the machine loop is exhausted.
114
- - **Machine verification rollup** (LUM-470) directly under the `Criteria` header, one line `Machine verification: N machine-verified / M human override (of T MACHINE criteria)` over the active MACHINE criteria, aligned with the web read model (LUM-456). Printed whenever the contract has ≥1 MACHINE criterion, so the terminal rollup never reads as all-human when a checkpointer actually verified the work.
123
+ - **Claim vs verification** — 规律 2 声称vs核验: the headline contrast, printed right after the header (whenever the contract exists), so the report shows **both** columns instead of only the verification one. Two sides:
124
+ - **Claim** — what the agent _says_ it did: an **unverified self-report** (`agent self-report · estimated, not verification`), estimate-tier provenance (估, not 测). Sourced by layering (LUM-597, preference `AGENT > RUN_SUMMARY > DIGEST > null`): if you supplied a `lumo verify --note`, that **own self-report** is the claim (`↳ source: agent self-report (verify --note)`) — the汇报者's voice, what faithfulness judges. Absent a self-report (old CLI / non-verify path) it degrades to the summarizer paraphrase, labelled honestly (`↳ source: synthesized run summary (no self-report)`). **Fail-closed**: when only a raw STOP turn digest exists it prints `generating — the formal run summary is still being synthesized` and **withholds the raw digest**; with no material at all it prints `not generated yet — …`, never a fabricated claim.
125
+ - **Verification** — what was actually _confirmed_ (measured): the machine-verification rollup `N machine-verified / M human override (of T MACHINE criteria)` over the active MACHINE criteria (relocated here from its old standalone line under `Criteria`), plus `X of Y criteria met by their latest verdict`. **Fail-closed**: before any round runs it prints `no verification has run yet — the claim is unconfirmed` rather than implying a pass.
126
+ - **Faithfulness** — the third state over the two columns: whether the CLAIM itself is _true_, read from the persisted faithfulness verdict (no LLM at read time), printed as `▸ Faithfulness — does the claim match the delivery`. One of `faithful` / `overstated` (claim says more than the diff/PR shows) / `under-reported` (claim says less) / `unjudgeable`, with an `· evidence: PR #N · <sha>` pointer when the judge cited one. **Fail-closed**: `not yet judged` (no verdict — distinct from a real `unjudgeable` verdict) and `… (stale — re-checked on the next batch)` (judged against an older claim snapshot, the verdict still shown) are surfaced explicitly. Omitted only against an older server that doesn't emit it.
127
+ - Carried in `--json` as `claim { text, source }` (`source: 'AGENT' | 'RUN_SUMMARY' | 'DIGEST' | null`; `AGENT` = the verify self-report, `RUN_SUMMARY`/`DIGEST` = the synthesized fallback; `text: null` = none generated) and `faithfulness { state, verdict, evidence, diffShas, prNumbers, judgedAt, judgedClaimAt }` (`state` adds `PENDING`/`STALE` over the four verdicts). Omitted only against an older server that doesn't emit the field. The machine-verification rollup is still carried top-level as `machineVerification`.
115
128
  - **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (failure tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
116
- - A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict (LUM-470): `· machine-verified` when a checkpointer actually passed it (even after a human later signs the task off), or `· human override (no machine pass)` when it passes only on a human sign-off with no machine run underneath. This keeps the terminal honest with web — a machine-verified criterion that a human co-signed no longer reads as a plain human pass.
117
- - A verdict's **evidence is drillable** (LUM-563), rendered as an indented `↳ evidence:` line under the verdict (PASS _and_ FAIL) instead of the inert raw pointer that used to ride the verdict line — so a conclusion points at real proof you can act on, not just the `check:` command: a `cmd:` pointer prints the actual command + exit code (`ran \`…\` → exit N · re-run to reproduce`), a `file:`pointer prints a terminal-clickable`path:line`, and a `commit:` pointer prints a navigable web URL (`<repo>/commit/<hash>`, resolved from the local git `origin`remote) or a`git show <hash>`fallback when no remote resolves. A criterion that **requires evidence but has none recorded yet** (e.g. a HUMAN evidence criterion before sign-off) renders an explicit`↳ evidence: pending — no reference recorded yet`(fail-closed) instead of a bare, dead`[evidence]` tag.
118
- - A pass can carry a **`⚠ pre-edit version`** note (LUM-457): the criterion was changed after that verdict (reworded, or its checkpointer was swapped so the recorded evidence ran a different command). The pass still counts as met (a stale pass does not block DONE — render-only signal), but it vouches for an older version — **re-run `lumo verify` to re-confirm against the current criterion.** This is the habit whenever you edit a MACHINE criterion's checkpointer mid-task: change the check, then re-verify so the green is honest.
129
+ - A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict: `· machine-verified` when a checkpointer actually passed it (even after a human later signs the task off), or `· human override (no machine pass)` when it passes only on a human sign-off with no machine run underneath. This keeps the terminal honest with web — a machine-verified criterion that a human co-signed no longer reads as a plain human pass.
130
+ - A verdict's **evidence is drillable**, rendered as an indented `↳ evidence:` line under the verdict (PASS _and_ FAIL) instead of the inert raw pointer that used to ride the verdict line — so a conclusion points at real proof you can act on, not just the `check:` command: a `cmd:` pointer prints the actual command + exit code (`ran \`…\` → exit N · re-run to reproduce`), a `file:`pointer prints a terminal-clickable`path:line`, and a `commit:` pointer prints a navigable web URL (`<repo>/commit/<hash>`, resolved from the local git `origin`remote) or a`git show <hash>`fallback when no remote resolves. A criterion that **requires evidence but has none recorded yet** (e.g. a HUMAN evidence criterion before sign-off) renders an explicit`↳ evidence: pending — no reference recorded yet`(fail-closed) instead of a bare, dead`[evidence]` tag.
131
+ - A pass can carry a **`⚠ pre-edit version`** note: the criterion was changed after that verdict (reworded, or its checkpointer was swapped so the recorded evidence ran a different command). The pass still counts as met (a stale pass does not block DONE — render-only signal), but it vouches for an older version — **re-run `lumo verify` to re-confirm against the current criterion.** This is the habit whenever you edit a MACHINE criterion's checkpointer mid-task: change the check, then re-verify so the green is honest.
119
132
  - **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
120
133
  - **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
121
- - **Cost** (LUM-560) — 规律 1: the costs a human should weigh, on the same report as the verdict instead of scattered across the web delivery card and `task lineage`. Three lines: **Tokens** (total input+output+cache across the task's sessions), **Active time** (non-idle agent seconds — Σ per-turn `STOP − prompt`, LUM-487), and **Rework rounds** (verify rounds that recorded a FAIL). Read from the **same** server-side source the web delivery card consumes (`retrospectiveRepository.loadActuals`), so the two reports cannot drift. Token cost is **fail-closed**: when no session usage was recorded it prints `Tokens: not recorded (no session usage captured)`, kept distinct from a measured `0` (没测到 vs 花了0, aligned with LUM-559). Carried in `--json` as `cost { tokenCost, activeTimeSec, reworkRounds }` (`tokenCost: null` = not measured). Omitted only against an older server that doesn't emit the field.
122
- - **Struggle / rework / outstanding** (LUM-561) — the anti-mum-and-deaf block: **always printed when the contract exists, even on a clean 0-unmet task** so a passing task still shows its scars instead of wiping them to a single PASS count. Lists, when present:
134
+ - **Cost** — 规律 1: the costs a human should weigh, on the same report as the verdict instead of scattered across the web delivery card and `task lineage`. Three lines: **Tokens** (total input+output+cache across the task's sessions), **Active time** (non-idle agent seconds — Σ per-turn `STOP − prompt`), and **Rework rounds** (verify rounds that recorded a FAIL). Read from the **same** server-side source the web delivery card consumes (`retrospectiveRepository.loadActuals`), so the two reports cannot drift. Token cost is **fail-closed**: when no session usage was recorded it prints `Tokens: not recorded (no session usage captured)`, kept distinct from a measured `0` (没测到 vs 花了0). Carried in `--json` as `cost { tokenCost, activeTimeSec, reworkRounds }` (`tokenCost: null` = not measured). Omitted only against an older server that doesn't emit the field.
135
+ - **Struggle / rework / outstanding** — the anti-mum-and-deaf block: **always printed when the contract exists, even on a clean 0-unmet task** so a passing task still shows its scars instead of wiping them to a single PASS count. Lists, when present:
123
136
  - **rework rounds** — verify rounds that had a FAIL;
124
137
  - **send-backs** — criteria sent back by a human/agent verdict (a MACHINE verify-loop FAIL is not a 打回), with their open/resolved lifecycle, preserved even for since-removed criteria;
125
138
  - **leftover follow-ups** — criteria whose latest verdict is `PASS_WITH_FOLLOWUP`;
126
- - **PR iterations** — when the task has >1 PR (the dominant rework signal when the verify loop ran once but the work churned across many follow-up PRs — e.g. LUM-557: ~10 PRs vs 1 verify round); a single PR is the happy path and is not flagged;
139
+ - **PR iterations** — when the task has >1 PR (the dominant rework signal when the verify loop ran once but the work churned across many follow-up PRs); a single PR is the happy path and is not flagged;
127
140
  - **reopens** — backward `IN_REVIEW/DONE → IN_PROGRESS/TODO` transitions (from the `STATUS_CHANGED` log): the task reached review/done and got bounced, a rework that leaves no FAIL verdict.
128
141
 
129
142
  When the trail is genuinely empty it states the **basis** (`None recorded — N rounds run, 0 FAIL, no send-backs, no reopens, no leftover follow-ups`); when nothing has been verified yet it says so (`No verification has run yet — cannot confirm there were no difficulties`) rather than rendering an implicitly-clean slate. Carried in `--json` as `struggleTrail` (incl. `pullRequests` + `reopens`).
130
143
 
131
- - **Trend** (LUM-562) — 规律 7 趋势非快照: the _movement_ of the key quantities across the task's attempts, not a single snapshot. Where History/Cost/Struggle list current values, this shows direction: **Pass rate** across verification rounds (`r1 60% → r2 100% (↑ +40pts)`), **Cost/session** across the task's sessions (`4.2K → 1.1K tokens (↓), 5.3K total` — per-session spend from the same source as the **Cost** total, so the trajectory's points sum to it), and **Rework** accrual (`3 accrued — 1 FAIL round, 1 reopen, +1 PR cycle (↑ from 0)`). **Honest about a single point:** with only one round and one session every quantity is one data point, so it prints `Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions)` rather than drawing a fake arrow off one value (closes the named "1-round pass = single point" gap). When nothing was verified and no cost was measured it says `No verification rounds or measured cost yet — nothing to trend`. Carried in `--json` as `trend { passRate[], cost[], rework{} }`. Omitted only against an older server.
144
+ - **Trend** — 规律 7 趋势非快照: the _movement_ of the key quantities across the task's attempts, not a single snapshot. Where History/Cost/Struggle list current values, this shows direction: **Pass rate** across verification rounds (`r1 60% → r2 100% (↑ +40pts)`), **Cost/session** across the task's sessions (`4.2K → 1.1K tokens (↓), 5.3K total` — per-session spend from the same source as the **Cost** total, so the trajectory's points sum to it), and **Rework** accrual (`3 accrued — 1 FAIL round, 1 reopen, +1 PR cycle (↑ from 0)`). **Honest about a single point:** with only one round and one session every quantity is one data point, so it prints `Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions)` rather than drawing a fake arrow off one value. When nothing was verified and no cost was measured it says `No verification rounds or measured cost yet — nothing to trend`. Carried in `--json` as `trend { passRate[], cost[], rework{} }`. Omitted only against an older server.
132
145
 
133
146
  - **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included). This list IS the plan — recomputed from the event log on every read, never maintained separately. Empty + rounds recorded = awaiting human adjudication.
134
- - **Open boundary crossings** (LUM-448) — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first), each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (LUM-469 — who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
135
- - **Read-only awareness** — this surfaces crossings detected elsewhere (LUM-426/435/442); there is no CLI path to disposition or clear one. Disposition stays web + human-only (LUM-426/435/422): an agent/CLI bearer cannot clear its own crossing from the terminal.
136
- - **The check fails closed (LUM-480):** if the crossings read itself errors (network / server / parse), the block prints `⚠ Boundary-crossing check failed (network/server error) — could not confirm whether any are undispositioned` instead of staying silent. Silence means a successful read with zero open crossings, never a failed check — a hiccup can no longer masquerade as "all clear".
147
+ - **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first), each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
148
+ - **Read-only awareness** — this surfaces crossings detected elsewhere; there is no CLI path to disposition or clear one. Disposition stays web + human-only: an agent/CLI bearer cannot clear its own crossing from the terminal.
149
+ - **The check fails closed:** if the crossings read itself errors (network / server / parse), the block prints `⚠ Boundary-crossing check failed (network/server error) — could not confirm whether any are undispositioned` instead of staying silent. Silence means a successful read with zero open crossings, never a failed check — a hiccup can no longer masquerade as "all clear".
137
150
 
138
- ### Responding to an open crossing — `lumo crossing explain` (LUM-542)
151
+ ### Responding to an open crossing — `lumo crossing explain`
139
152
 
140
153
  When `lumo task status` surfaces an OPEN crossing you believe is a false positive
141
154
  — or you simply want to leave a rationale for the human reviewer — append a
@@ -148,7 +161,7 @@ lumo crossing explain <id> --note "this was a generated fixture, not a hand-edit
148
161
  This is the **inverse** of dispositioning, but it is the agent/CLI path
149
162
  (bearer-only; a clerk/human caller is refused). Behavior:
150
163
 
151
- - it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only, LUM-448);
164
+ - it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only);
152
165
  - the note is shown to the human reviewer at disposition time, kept for later review, and explicitly labeled _agent self-report · unverified_;
153
166
  - `<id>` must be a crossing on the **session-bound task** (resolved from `$CLAUDE_CODE_SESSION_ID`; cross-task targets and unbound/mismatched sessions are rejected);
154
167
  - earlier explanations are immutable — a correction is a new note.
@@ -159,16 +172,16 @@ This is the **inverse** of dispositioning, but it is the agent/CLI path
159
172
  `1`). The schema is versioned: breaking shape changes bump the major; additive
160
173
  fields don't. Pin on `version` when scripting against it.
161
174
 
162
- - each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it; LUM-456/470);
175
+ - each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it);
163
176
  - the payload carries a top-level `machineVerification` aggregate `{ total, machineVerified, humanOverridden }` over the active MACHINE criteria — read these, not `latestVerdict` alone, to tell a machine-verified criterion from a human override;
164
- - open boundary crossings ride along as an additive top-level `openCrossings`, each entry `{ id, category, severity, detail, attribution }` where `attribution` is `{ workspaceMemberId, sessionId, agent, worktreeBranch, model }` with every field nullable — null = unknown, never fabricated (LUM-469); the array length is the count. Same read-only awareness, no write path;
165
- - **`openCrossings` is `null` when the crossings check failed (LUM-480)** — distinct from `[]` (a successful read with zero open crossings). Script consumers must treat `null` as "unknown / could not confirm", **not** "safe".
177
+ - open boundary crossings ride along as an additive top-level `openCrossings`, each entry `{ id, category, severity, detail, attribution }` where `attribution` is `{ workspaceMemberId, sessionId, agent, worktreeBranch, model }` with every field nullable — null = unknown, never fabricated; the array length is the count. Same read-only awareness, no write path;
178
+ - **`openCrossings` is `null` when the crossings check failed** — distinct from `[]` (a successful read with zero open crossings). Script consumers must treat `null` as "unknown / could not confirm", **not** "safe".
166
179
 
167
180
  `status` reads; `verify` judges. Running status never starts a round, never
168
181
  escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
169
182
  all-pass, human-only DONE) live entirely in `lumo verify` and the server.
170
183
 
171
- ## lumo verdict — the three verdict channels (LUM-422)
184
+ ## lumo verdict — the three verdict channels
172
185
 
173
186
  `lumo verify` is the MACHINE channel. `lumo verdict` covers the other two — the
174
187
  HUMAN pass and the AGENT send-back — under one red line: **no passing data row is
@@ -36,7 +36,7 @@ Errors if the target dir already exists; reuses the branch if it already exists
36
36
 
37
37
  - **`prisma generate` clobbers all worktrees.** The generated client lives in the shared (symlinked) `node_modules`, so a `generate` in one worktree overwrites the client every parallel worktree depends on. Verify with jest (SWC mocks Prisma); do `generate + tsc` atomically once at the end.
38
38
  - **Run jest from the worktree root** (`cd` in first). `cli/` has no jest config; running from the main checkout hits the `cli/package.json` haste collision and silently runs the wrong tests.
39
- - **Husky hooks are copied in for you.** Husky owns hooks via `core.hooksPath = .husky/_`, resolved relative to each worktree's root. That `_` shim is **untracked** (regenerated on the main checkout's `npm install`/`prepare`), so a fresh worktree would lack it and git would **silently skip every hook** — pre-commit (lint-staged) and commit-msg (LUM-405 drift-check). With no GitHub CI, husky is the only deterministic quality gate, so `add` copies `.husky/_` in (copy, not symlink). If the main checkout has no `.husky/_`, `add` warns you to `npm install` there rather than skipping silently.
39
+ - **Husky hooks are copied in for you.** Husky owns hooks via `core.hooksPath = .husky/_`, resolved relative to each worktree's root. That `_` shim is **untracked** (regenerated on the main checkout's `npm install`/`prepare`), so a fresh worktree would lack it and git would **silently skip every hook** — pre-commit (lint-staged) and commit-msg (drift-check). With no GitHub CI, husky is the only deterministic quality gate, so `add` copies `.husky/_` in (copy, not symlink). If the main checkout has no `.husky/_`, `add` warns you to `npm install` there rather than skipping silently.
40
40
  - **Never `npm install` / `npm ci` inside a worktree.** npm doesn't respect the `node_modules` symlink — it deletes it and reifies a full standalone tree (~1 min, shared prisma-client gone). Install only in the main checkout, then re-create the symlink if npm replaced it. (Older npm could plant a self-referential `node_modules/node_modules` that hard-panics Turbopack's `next build`; the `prebuild`/`predev`/`preanalyze` guard `scripts/fix-nodemodules-selflink.ts` removes it, but this rule avoids the mess.)
41
41
 
42
42
  ## `lumo worktree rm <LUM-N>`
@@ -45,15 +45,14 @@ function formatTaskStatus(data, extras = {}) {
45
45
  pushOpenCrossings(lines, extras);
46
46
  return lines.join('\n') + '\n';
47
47
  }
48
+ // LUM-564 声称 vs 核验: the agent's CLAIM (what it says it did) paired with
49
+ // the measured verification conclusion (what was actually confirmed) — the
50
+ // two columns regular 規律2 wants, instead of only the verification one. The
51
+ // machine-verification rollup (LUM-470) lives on the verification side here
52
+ // rather than as a standalone line, so the contrast is in one place.
53
+ pushClaimVsVerification(lines, data);
48
54
  lines.push('');
49
55
  lines.push(`Criteria (${data.criteria.length} total, ${data.nextActions.length} unmet):`);
50
- // LUM-470: honest machine-verification rollup over the active MACHINE criteria
51
- // (same read model as web, LUM-456) — so the terminal rollup never reads as
52
- // all-human when a checkpointer actually verified the work.
53
- const mv = data.machineVerification;
54
- if (mv.total > 0) {
55
- lines.push(`Machine verification: ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
56
- }
57
56
  for (const c of data.criteria) {
58
57
  const glyph = c.latestVerdict == null
59
58
  ? '○'
@@ -208,6 +207,120 @@ function fmtDuration(totalSec) {
208
207
  parts.push(`${s}s`);
209
208
  return parts.join(' ');
210
209
  }
210
+ /**
211
+ * Append the "Claim vs verification" pairing (LUM-564) — 規律2 声称vs核验. The
212
+ * verification conclusion (machine-verified vs human-override, LUM-470) was the
213
+ * whole-system template, but the agent's run summary — its CLAIM of what it did
214
+ * — lived on a different surface, so the "two-column" contrast was really one
215
+ * column. This puts them side by side: the unverified self-report (estimate-
216
+ * tier — 估, not 测) against the measured verdict (测), so a reader can weigh
217
+ * 声称 against 核验 at a glance.
218
+ *
219
+ * Fail-closed on every axis: a missing claim renders an explicit "not generated
220
+ * yet" line (never a fabricated claim); a not-yet-verified task renders the
221
+ * verification side as explicitly unconfirmed (never an implied pass). The
222
+ * claim text is same-source with the web card (latest session's LLM run
223
+ * summary, else its STOP turn digest) so the two surfaces cannot drift.
224
+ */
225
+ function pushClaimVsVerification(lines, data) {
226
+ lines.push('');
227
+ lines.push('Claim vs verification:');
228
+ // ── Claim (声称): the agent's self-report — estimate-tier, never measured.
229
+ lines.push(' ▸ Claim — what the agent says it did');
230
+ lines.push(' (agent self-report · estimated, not verification):');
231
+ const claim = data.claim;
232
+ if (claim && claim.source === 'AGENT' && claim.text) {
233
+ // LUM-597: the agent's OWN self-report (attached to verify) — the
234
+ // authoritative claim faithfulness judges, not a summarizer paraphrase.
235
+ for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
236
+ lines.push(` ${cl}`);
237
+ }
238
+ lines.push(' ↳ source: agent self-report (verify --note)');
239
+ }
240
+ else if (claim && claim.source === 'RUN_SUMMARY' && claim.text) {
241
+ for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
242
+ lines.push(` ${cl}`);
243
+ }
244
+ lines.push(' ↳ source: synthesized run summary (no self-report)');
245
+ }
246
+ else if (claim && claim.source === 'DIGEST') {
247
+ // LUM-583 ③ / LUM-574: a raw turn digest is not a claim — withhold it and
248
+ // say the formal summary is still generating, same-source with the web card.
249
+ lines.push(' generating — the formal run summary is still being synthesized');
250
+ }
251
+ else {
252
+ // Fail-closed: no run summary → say so, never invent a claim. Run summaries
253
+ // are synthesized when the bound task reaches DONE (LUM-481).
254
+ lines.push(' not generated yet — the agent run summary is synthesized when the task reaches DONE');
255
+ }
256
+ // ── Faithfulness (LUM-583): the third state — is the claim itself true? Read
257
+ // from the persisted LUM-582 verdict; PENDING (not yet judged) is distinct
258
+ // from UNJUDGEABLE (a real verdict), STALE keeps the verdict but flags a
259
+ // pending re-check. Skipped only when the server didn't emit it (older server).
260
+ pushFaithfulness(lines, data.faithfulness);
261
+ // ── Verification (核验): the measured verdict — machine-verified vs override.
262
+ lines.push(' ▸ Verification — what was actually confirmed (measured):');
263
+ const mv = data.machineVerification;
264
+ if (data.currentRound === 0) {
265
+ // Nothing verified yet — the claim stands unconfirmed. Don't imply a pass.
266
+ lines.push(' no verification has run yet — the claim is unconfirmed');
267
+ }
268
+ else {
269
+ if (mv.total > 0) {
270
+ lines.push(` ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
271
+ }
272
+ const met = data.criteria.length - data.nextActions.length;
273
+ lines.push(` ${met} of ${data.criteria.length} criteria met by their latest verdict`);
274
+ }
275
+ }
276
+ /**
277
+ * Append the "Faithfulness" conclusion (LUM-583) — the third state over the
278
+ * 声称/核验 columns: whether the agent's CLAIM is itself TRUE, judged
279
+ * independently against the delivery (LUM-582). Same read model the web honest
280
+ * report renders, so the two surfaces cannot drift. Fail-closed: PENDING (not
281
+ * yet judged) reads distinctly from UNJUDGEABLE (a real verdict), and STALE
282
+ * keeps the prior verdict while flagging a pending re-check. Skipped only when
283
+ * the server didn't emit the field (older server) — never fabricated.
284
+ */
285
+ function pushFaithfulness(lines, f) {
286
+ if (!f)
287
+ return; // older server: field absent → don't fabricate a conclusion.
288
+ const verdictPhrase = (v) => {
289
+ switch (v) {
290
+ case 'FAITHFUL':
291
+ return 'faithful — the claim matches the delivery';
292
+ case 'OVERSTATED':
293
+ return 'overstated — the claim says more than the diff/PR shows';
294
+ case 'UNDERREPORT':
295
+ return 'under-reported — the claim says less than the diff/PR shows';
296
+ default:
297
+ return "unjudgeable — couldn't decide from the diff/PR";
298
+ }
299
+ };
300
+ let body;
301
+ if (f.state === 'PENDING') {
302
+ body =
303
+ 'not yet judged — the faithfulness check runs in a batch after delivery';
304
+ }
305
+ else if (f.state === 'STALE') {
306
+ body = `${f.verdict ? verdictPhrase(f.verdict) : 'judged'} (stale — re-checked on the next batch)`;
307
+ }
308
+ else {
309
+ body = verdictPhrase(f.state);
310
+ }
311
+ const ev = [];
312
+ if (f.prNumbers.length > 0) {
313
+ ev.push(`PR ${f.prNumbers.map(n => `#${n}`).join(', ')}`);
314
+ }
315
+ if (f.diffShas.length > 0) {
316
+ ev.push(f.diffShas
317
+ .slice(0, 3)
318
+ .map(s => (0, sanitize_1.sanitizeField)(s).slice(0, 7))
319
+ .join(' '));
320
+ }
321
+ lines.push(' ▸ Faithfulness — does the claim match the delivery (LUM-583):');
322
+ lines.push(` ${body}${ev.length > 0 ? ` · evidence: ${ev.join(' · ')}` : ''}`);
323
+ }
211
324
  /**
212
325
  * Append the honest "Cost" section (LUM-560) — 规律 1: surface the costs a human
213
326
  * should weigh (token spend, active time, machine rework) on the same report as
@@ -144,6 +144,19 @@ async function verify(identifier, options = {}) {
144
144
  `The contract is HUMAN-only; finish your work and hand off for human review (lumo task update ${taskId} --status in_review).\n`);
145
145
  return;
146
146
  }
147
+ // ── Require the self-report (LUM-597) ────────────────────────────────────
148
+ // Requesting acceptance is a structured action; `--note` is the agent's
149
+ // mandatory self-report. It is captured deterministically here (the agent
150
+ // can't be made to phrase it well, but it can be made to provide one); its
151
+ // truthfulness is the faithfulness audit's job downstream. No note → no round
152
+ // is posted (no round burned), so re-running with one is free.
153
+ const note = options.note?.trim();
154
+ if (!note) {
155
+ console.error('Error: --note is required — state what you did and why it is ready, e.g.\n' +
156
+ ` lumo verify ${taskId} --note "implemented X in foo.ts because Y; tests + tsc pass"\n` +
157
+ 'This self-report is recorded as your claim and checked against the diff for faithfulness.');
158
+ return 1;
159
+ }
147
160
  // ── Execute every checkpointer locally ───────────────────────────────────
148
161
  process.stdout.write(`Verifying ${taskId} — ${machine.length} MACHINE criteria\n`);
149
162
  const results = [];
@@ -165,7 +178,7 @@ async function verify(identifier, options = {}) {
165
178
  res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/verify`, {
166
179
  method: 'POST',
167
180
  headers: { ...headers, 'Content-Type': 'application/json' },
168
- body: JSON.stringify({ results }),
181
+ body: JSON.stringify({ results, note }),
169
182
  });
170
183
  }
171
184
  catch (err) {
@@ -223,6 +223,7 @@ program
223
223
  .command('verify [task]')
224
224
  .description('Machine verification loop (LUM-343): run every MACHINE criterion checkpointer locally, report structured verdicts to the server (round cap 3), and print next actions. All-pass moves the task to IN_REVIEW. Defaults to the session-bound task.')
225
225
  .option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
226
+ .option('--note <text>', 'Required self-report — what you did and why it is ready (LUM-597). Recorded as your claim (source AGENT) when the round passes into IN_REVIEW, and checked against the diff for faithfulness.')
226
227
  .action(wrap((task, options) => (0, verify_1.verify)(task, options)));
227
228
  program
228
229
  .command('verdict [task]')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.45.0",
3
+ "version": "1.47.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",