@lumoai/cli 1.59.0 → 1.61.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.
@@ -108,8 +108,9 @@ The command catalog below is a **map**: it lists every command grouped by domain
108
108
  ### Verification (machine acceptance loop) — see [verify.md](references/verify.md)
109
109
 
110
110
  - `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 an optional one-line self-report ("我改了 X,因为 Y 可验收"), frozen as the task's claim (source AGENT) when the round passes into IN_REVIEW. The round cap is the workspace's `verificationMaxRounds` (default 3) — read the actual budget off the `Round N/M` line the command prints, never assume a number. All-pass → task to IN_REVIEW (agent stops); a fail on the last round → escalate to human. **Run this before claiming a task is done.**
111
- - `lumo task status [task] [--full] [--json]` — read-only acceptance self-check (no LLM): the contract with each criterion's latest verdict, the current round, last failure reasons, `nextActions` (the unmet criteria, plus any undispositioned PR **security findings** as `SECURITY_FINDING` entries — `blocks DONE` vs `advisory`), a per-linked-PR **scan status line** (stage states incl. the L3 `hunt` layer, external-finding count, partial flag, external failure reason — LUM-735/739), and any OPEN boundary crossings; `--full` adds the dashboard sections (verification rollup, history, cost, struggle trail, trend) (fails closed — `null`/`⚠` means "could not confirm", not safe). **Run it first when resuming a task or after a round was rejected.**
112
- - `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.
111
+ - `lumo task status [task] [--full] [--json]` — read-only acceptance self-check (no LLM): the contract with each criterion's latest verdict, the current round, last failure reasons, `nextActions` (the unmet criteria, plus any undispositioned PR **security findings** as `SECURITY_FINDING` entries — `blocks DONE` vs `advisory`), a per-linked-PR **scan status line** (stage states incl. the L3 `hunt` layer, external-finding count, partial flag, external failure reason — LUM-735/739) plus the hunt's per-category coverage line (LUM-762), and any OPEN boundary crossings; `--full` adds the dashboard sections (verification rollup, history, cost, struggle trail, trend) (fails closed — `null`/`⚠` means "could not confirm", not safe). **Run it first when resuming a task or after a round was rejected.**
112
+ - `lumo crossing explain <id> --note "<text>"` — append an agent self-explanation ("申辩") to a boundary crossing; append-only, **never clears the crossing or unblocks Done**. A review aid, not a self-clear.
113
+ - `lumo crossing disposition <id> --false-positive | --confirmed [--note "<text>"] [--task <LUM-N>]` — record the **user's** ruling on a task crossing through a **three-step** confirmation handshake the server enforces (LUM-769): (1) no step flag — prints the crossing (severity, detail, recurrence, your explanations) as an exit-4 envelope with a stage-1 **read receipt**, writes nothing; relay it and ask the user to confirm they have **read** it, then run its `confirmCommand` (`--receipt <r1> --confirm-read`); (2) that prints a second exit-4 envelope for the ruling with the stage-2 receipt; relay it and ask the user to **approve**, then run its `confirmCommand` (`--receipt <r2> --confirm`); (3) the write. Confirming the ruling before the read is acknowledged is refused and told to confirm the read first; never supply either step flag yourself. Either ruling clears that crossing's block on DONE and is audited as a CLI-channel disposition. Reverting to OPEN, repository suppression rules and steward (milestone) crossings stay web-only. See [verify.md](references/verify.md)
113
114
 
114
115
  ### Cost (per-operation token read-out) — see [task-context.md](references/task-context.md)
115
116
 
@@ -144,7 +145,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
144
145
  - `lumo milestone summary [--retry]` — AI retro
145
146
  - `lumo milestone changelog <id> [--json]` — structured changelog from DONE tasks + merged PRs + frozen claimSnapshot (pure assembly, no LLM; runs on any status). Headline counts carry a baseline vs prior completed milestones (LUM-508); each claim shows provenance, no-PR/no-claim rendered explicitly (LUM-649)
146
147
  - `lumo milestone reorder/move` — manual ordering
147
- - `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) block `--status completed` **and** the LUM-624 auto-rollup with 409/fail-closed; human override is web-only (`?force=1`, stamped as a persistent trail). `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
148
+ - `lumo milestone criteria set/list/verdict` — milestone-level **exit criteria** (LUM-643): the close contract, drafted & adjudicated (裁定) by the steward (LUM-640) or a human. Unresolved criteria (pending or NOT_MET) hold the LUM-714 derived completion fail-closed (no `completedAt`, no retro); human override is web-only, stamped as a persistent trail. `verdict --met` on `EXTERNAL_FACT` requires `--evidence` (422); `PENDING_OUTCOME` can never be MET
148
149
 
149
150
  ### Documents — CRUD/sharing → [docs.md](references/docs.md); **editing live docs** (raw/section/patch/append/diff/rebuild) → [doc-editing.md](references/doc-editing.md)
150
151
 
@@ -235,5 +236,5 @@ Don't guess these:
235
236
  - **Evidence before "done".** Never claim done from reading code — run `lumo verify`. While a PR is open, the task is IN_REVIEW, not DONE.
236
237
  - **Never set DONE yourself after a verify loop** — that adjudication is human-only. An unresolved send-back blocks the DONE transition with 409.
237
238
  - **Status updates are one direct call** (`task update --status done`), not a step-by-step walk through `in_progress → in_review → done`.
238
- - **Exit code 4 = the user must confirm — you may not.** A gated mutation (`--status done`, `sprint/milestone/doc delete`, `memory rm`, `task deps/artifact rm`, `sprint close --move-all/--backlog-all`, `worktree rm`, `doc rebuild-source` on an existing source) run without `--confirm` prints a `confirmation_required` envelope on stdout and does nothing. Show every line of its `changes` to the user, wait for their explicit approval, then run its `confirmCommand` verbatim. **Never add `--confirm` (or `--yes` / `--force`) yourself** — the flag means "the user has confirmed", and adding it on your own initiative is the exact failure the protocol exists to stop. **Exit code 5 = blocked by a human-only gate** (`DONE_BLOCKED`: a send-back, an undispositioned boundary crossing, a blocking security finding): relay its `blockers` and `remediation` to the user; nothing on the CLI clears it — no retry, no flag, no rewording a failed criterion. Details: [confirmation.md](references/confirmation.md)
239
+ - **Exit code 4 = the user must confirm — you may not.** A gated mutation (`--status done`, `sprint/milestone/doc delete`, `memory rm`, `task deps/artifact rm`, `sprint close --move-all/--backlog-all`, `worktree rm`, `doc rebuild-source` on an existing source) run without `--confirm` prints a `confirmation_required` envelope on stdout and does nothing. Show every line of its `changes` to the user, wait for their explicit approval, then run its `confirmCommand` verbatim. **Never add `--confirm` (or `--yes` / `--force`) yourself** — the flag means "the user has confirmed", and adding it on your own initiative is the exact failure the protocol exists to stop. **Exit code 5 = blocked by a human-only gate** (`DONE_BLOCKED`: a send-back, an undispositioned boundary crossing, a blocking security finding): relay its `blockers` and `remediation` to the user; nothing on `task update` clears it — no retry, no flag, no rewording a failed criterion. A boundary crossing is the one blocker with a terminal path, and it is the user's: `lumo crossing disposition <id> --false-positive | --confirmed` prints an exit-4 envelope you relay exactly like `--status done` — never self-approve. Details: [confirmation.md](references/confirmation.md)
239
240
  - **Read the reference before composing a command** whose flags/edge-cases matter — don't run from memory.
@@ -50,25 +50,27 @@ Nothing goes to stderr; the envelope is the command's result, not an error. **Ex
50
50
  - `--confirm` — the user approved. Present on every command in the table below.
51
51
  - `--yes` — legacy alias of `--confirm` on the commands that took it before LUM-755. Prefer `--confirm` in new invocations. `task update` never had `--yes` and does not gain it.
52
52
  - `--force` — a **second gate** that stays separate from `--confirm`: `worktree rm` uses it to acknowledge discarding uncommitted changes; `doc rebuild-source` uses it to acknowledge replacing an existing source. Missing `--force` also yields an envelope (exit 4) whose `confirmCommand` carries `--force --confirm`.
53
+ - `--confirm-read` + `--receipt <token>` — the **read-acknowledgement handshake** (LUM-769) on `crossing disposition`, a three-step protocol the _server_ enforces, the way a web form is loaded, "I have read this" is ticked, and only then submit works. **Step 1** (no step flag) reads the crossing and obtains a stage-1 receipt bound to the crossing, the ruling, your session/member and the crossing's current state (ruling, recurrence, explanations); its envelope's `confirmCommand` carries `--receipt <r1> --confirm-read`. **Step 2** (`--confirm-read`, after the user says they have read it) has the server verify r1 and return the stage-2 receipt; the CLI prints a second envelope for the ruling whose `confirmCommand` carries `--receipt <r2> --confirm`. **Step 3** (`--confirm`, after the user approves the ruling) writes. Confirming the ruling with the stage-1 receipt is refused — locally before any request, and by the server with 409 — and told to confirm the read first; a receipt that is missing, forged, expired (10-minute TTL), for another reader/ruling, or older than the crossing's latest change (a recurrence, a new explanation, a ruling) is refused too, nothing written — re-run without a step flag for a fresh envelope. Each envelope lists its receipt under `receipt: { token, expiresAt }`. The audit row records shown / read-acknowledged / confirmed times. This makes the order un-skippable; it does not, and cannot, prove the user personally ticked the box — you relay each step — so the ruling is recorded as a CLI-channel disposition.
53
54
 
54
55
  ## Commands that use the protocol
55
56
 
56
- | Command | `changes` describes | Notes |
57
- | --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
58
- | `task update <id> --status done` | `Will move LUM-N "title" to DONE` · `Status: <current> → DONE` · one line per linked PR (`#926 merged title url`) or `Pull requests: none linked` · a trailing `⚠ N linked pull request(s) not merged: …` when any PR is open/closed/draft | Only `--status done` is gated; other statuses PATCH directly. The server's own DONE gates (unresolved send-back, open boundary crossing → 409) still apply after `--confirm`. See [tasks.md](tasks.md). |
59
- | `task deps rm <id> <edge>` | the resolved edge (short id, direction, other task, status, source) | [task-deps.md](task-deps.md) |
60
- | `task artifact rm <task> <artifact-id>` | artifact id, title, kind; "irreversible" | [artifacts-figma.md](artifacts-figma.md) |
61
- | `memory rm <memoryId>` | memory id, `[CATEGORY]`, a one-line content summary | [memory.md](memory.md) |
62
- | `doc delete <doc>` | doc id + title; nested documents are deleted with it | [docs.md](docs.md) |
63
- | `sprint delete <ref>` | sprint number + name; task count (tasks keep their data) | [sprints.md](sprints.md) |
64
- | `sprint close <ref> --move-all` / `--backlog-all` | sprint; every unfinished task and where it goes | Without a mode flag and with unfinished tasks the command is a plain exit-1 refusal (pick a mode). [sprints.md](sprints.md) |
65
- | `milestone delete <ref>` | milestone name; task count (tasks keep their data) | [milestones.md](milestones.md) |
66
- | `worktree rm <LUM-N>` | path + branch; `⚠ uncommitted changes discarded` when dirty; whether the branch is kept or deleted | Dirty tree → `confirmCommand` carries `--force --confirm`. [worktree.md](worktree.md) |
67
- | `doc rebuild-source <doc>` (when a source already exists) | the server's 409 reason; what `--force` would replace | `confirmCommand` carries `--force --confirm`. [doc-editing.md](doc-editing.md) |
57
+ | Command | `changes` describes | Notes |
58
+ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
+ | `task update <id> --status done` | `Will move LUM-N "title" to DONE` · `Status: <current> → DONE` · one line per linked PR (`#926 merged title url`) or `Pull requests: none linked` · a trailing `⚠ N linked pull request(s) not merged: …` when any PR is open/closed/draft | Only `--status done` is gated; other statuses PATCH directly. The server's own DONE gates (unresolved send-back, open boundary crossing → 409) still apply after `--confirm`. See [tasks.md](tasks.md). |
60
+ | `crossing disposition <id> --false-positive` / `--confirmed` | **Envelope 1 (read):** `Will disposition crossing <id> on LUM-N as FALSE_POSITIVE` · `Disposition: OPEN → FALSE_POSITIVE` · `[SEVERITY] CATEGORY — detail` · `Seen ×N while open` when it recurred · every agent explanation (`agent self-report · unverified`) or `Agent explanations: none recorded` · `Note to record: …` · `⚠ Approving clears this crossing's block on moving LUM-N to DONE` (or `Advisory crossing — it never blocked DONE…`) · `⚠ HIGH severity …` / `⚠ Irreversible category …` when they apply · `Step 1 of 2 — read receipt issued … valid until <time>`. **Envelope 2 (ruling, after `--confirm-read`):** `Read acknowledged for crossing <id> (shown <t1>, acknowledged <t2>)` · `Step 2 of 2 — record the ruling: OPEN → FALSE_POSITIVE on LUM-N` · the note · the ⚠ consequence line | LUM-769. Two envelopes, two approvals: envelope 1's `confirmCommand` ends in `--confirm-read`, envelope 2's in `--confirm` (see Flags). The crossing is read first on every path and the command fails closed (exit 1, no write) when that read or a receipt request errors; a step flag without its receipt, `--confirm` with the stage-1 receipt, or both step flags at once are refused before any request; an already-matching ruling is a no-op (exit 0). Reverting to OPEN and repository suppression rules stay web-only; steward (milestone) crossings are not reachable. See [verify.md](verify.md). |
61
+ | `task deps rm <id> <edge>` | the resolved edge (short id, direction, other task, status, source) | [task-deps.md](task-deps.md) |
62
+ | `task artifact rm <task> <artifact-id>` | artifact id, title, kind; "irreversible" | [artifacts-figma.md](artifacts-figma.md) |
63
+ | `memory rm <memoryId>` | memory id, `[CATEGORY]`, a one-line content summary | [memory.md](memory.md) |
64
+ | `doc delete <doc>` | doc id + title; nested documents are deleted with it | [docs.md](docs.md) |
65
+ | `sprint delete <ref>` | sprint number + name; task count (tasks keep their data) | [sprints.md](sprints.md) |
66
+ | `sprint close <ref> --move-all` / `--backlog-all` | sprint; every unfinished task and where it goes | Without a mode flag and with unfinished tasks the command is a plain exit-1 refusal (pick a mode). [sprints.md](sprints.md) |
67
+ | `milestone delete <ref>` | milestone name; task count (tasks keep their data) | [milestones.md](milestones.md) |
68
+ | `worktree rm <LUM-N>` | path + branch; `⚠ uncommitted changes discarded` when dirty; whether the branch is kept or deleted | Dirty tree → `confirmCommand` carries `--force --confirm`. [worktree.md](worktree.md) |
69
+ | `doc rebuild-source <doc>` (when a source already exists) | the server's 409 reason; what `--force` would replace | `confirmCommand` carries `--force --confirm`. [doc-editing.md](doc-editing.md) |
68
70
 
69
71
  ## When the gate is human-only: `DONE_BLOCKED`, exit code 5
70
72
 
71
- Not every refusal is something the user can approve. The server's DONE gates — an **unresolved send-back**, an **undispositioned boundary crossing**, a **blocking security finding** — are verdicts on the agent's own work, and by design there is no CLI path to clear them (the interested party may not adjudicate itself). They are therefore **not** part of the confirmation protocol: no `--confirm`, no `--force`, nothing you can pass.
73
+ Not every refusal is something the user can approve on the spot. The server's DONE gates — an **unresolved send-back**, an **undispositioned boundary crossing**, a **blocking security finding** — are verdicts on the agent's own work, and the interested party may not adjudicate itself: nothing on `task update` clears them no `--confirm`, no `--force`, nothing you can pass. Since LUM-769 a **boundary crossing** does have a terminal path, but it is a separate command and it is the user's: `lumo crossing disposition <id> --false-positive | --confirmed` walks the exit-4 protocol above (you relay the crossing, the user rules, you re-run the `confirmCommand`). Send-backs and security findings still have no CLI path at all.
72
74
 
73
75
  What the CLI does instead: when `task update <id> --status done --confirm` is refused by one of these gates, it re-reads the task's acceptance status and open crossings, prints a structured error on **stdout** and exits **5**:
74
76
 
@@ -90,20 +92,21 @@ What the CLI does instead: when `task update <id> --status done --confirm` is re
90
92
  ],
91
93
  "unconfirmed": [],
92
94
  "remediation": [
93
- "BOUNDARY_CROSSING: leave a rationale with `lumo crossing explain cmtw… --note \"…\"` (append-only; it does not clear anything). A human dispositions each crossing (false positive / confirmed) at https://…/my-tasks/LUM-48#boundary-crossings"
95
+ "BOUNDARY_CROSSING: leave a rationale with `lumo crossing explain cmtw… --note \"…\"` (append-only; it does not clear anything), then relay each crossing to the user. Once they rule, record it with `lumo crossing disposition cmtw… --false-positive` or `--confirmed` it prints an exit-4 envelope for the user to approve; never self-approve. The web panel also works: https://…/my-tasks/LUM-48#boundary-crossings"
94
96
  ],
95
97
  "dispositionUrl": "https://…/my-tasks/LUM-48#boundary-crossings"
96
98
  }
97
99
  ```
98
100
 
99
- | Field | Meaning |
100
- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
101
- | `message` | The server's 409 text, verbatim. |
102
- | `blockers` | One entry per item holding the gate: `SEND_BACK` (criterionId, statement, failedAtRound, rejectionReason), `BOUNDARY_CROSSING` (id, severity, category, detail), `SECURITY_FINDING` (findingId, severity, ruleId, filePath, line, prNumber, statement). Built from the same read models as `lumo task status`. |
103
- | `unconfirmed` | Enrichment reads that failed (`boundary-crossings read failed: HTTP 500`). Non-empty means the blocker list may be incomplete it never silently shrinks. |
104
- | `remediation` | One line per blocker kind present, naming only human-side or append-only paths: re-run `lumo verify`, `lumo crossing explain`, fix-and-push, the web disposition panel. |
101
+ | Field | Meaning |
102
+ | ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
103
+ | `message` | The server's 409 text, verbatim. |
104
+ | `blockers` | One entry per item holding the gate: `SEND_BACK` (criterionId, statement, failedAtRound, rejectionReason), `BOUNDARY_CROSSING` (id, severity, category, detail), `SECURITY_FINDING` (findingId, severity, ruleId, filePath, line, prNumber, statement). Built from the same read models as `lumo task status`. A `BOUNDARY_CROSSING` entry appears only for crossings the server gate counts (LUM-771): advisory crossings never, and with the workspace gate off only HIGH ones. |
105
+ | `nonBlocking` | Open crossings the gate does NOT count (id, severity, category, detail, advisory) awareness only; do not ask the user to disposition them to get past DONE_BLOCKED. Rendered as a separate `Also open, not blocking DONE` list in the terminal (LUM-771). |
106
+ | `unconfirmed` | Enrichment reads that failed (`boundary-crossings read failed: HTTP 500`). Non-empty means the blocker list may be incomplete it never silently shrinks. |
107
+ | `remediation` | One line per blocker kind present, naming only human-side, append-only or user-approved paths: re-run `lumo verify`, `lumo crossing explain`, `lumo crossing disposition` (exit-4 envelope, the user rules), fix-and-push, the web disposition panel. Never a ready-to-run approval flag. |
105
108
 
106
- **Exit 4 vs exit 5:** 4 means "the user must approve, then you can proceed"; 5 means "a human must adjudicate elsewhere — nothing on the CLI moves this". On exit 5, relay `blockers` and `remediation` to the user. Do not retry, do not look for a flag, and do not reword or delete a failed criterion to get past a send-back (that is tampering and is audited). A 409 that is not a DONE gate (e.g. a permission refusal) keeps the plain exit-1 text on stderr.
109
+ **Exit 4 vs exit 5:** 4 means "the user must approve, then you can proceed"; 5 means "a human must adjudicate — nothing on this command moves it". On exit 5, relay `blockers` and `remediation` to the user. Do not retry `task update`, do not look for a flag on it, and do not reword or delete a failed criterion to get past a send-back (that is tampering and is audited). For a `BOUNDARY_CROSSING` blocker the adjudication can happen in the terminal — `lumo crossing disposition <id> …` is itself an exit-4 command, so the user's ruling still comes through the protocol, never from you. Once every blocker is cleared, re-run the original `task update … --confirm` (its envelope was already approved). A 409 that is not a DONE gate (e.g. a permission refusal) keeps the plain exit-1 text on stderr.
107
110
 
108
111
  ## Example exchange
109
112
 
@@ -28,7 +28,7 @@ Use `--search <text>` to filter to milestones whose **name or description** cont
28
28
  - `ON-TRACK` — on schedule (or all tasks done)
29
29
  - `AT-RISK` — completion lags elapsed time, or (no start date) the target is within ~7 days with work remaining
30
30
  - `OVERDUE` — past the target date with tasks still open
31
- - `-` — no light applies (status `COMPLETED`/`CANCELLED`, or no target date)
31
+ - `-` — no light applies (the milestone is completed or archived, or has no target date)
32
32
 
33
33
  ```bash
34
34
  lumo milestone list # one-project workspace (non-archived only)
@@ -71,9 +71,9 @@ On success: `Created milestone "Q3 Launch" <id>`.
71
71
 
72
72
  Accepts UUID or name. With a name, `--project <ref>` is required when the workspace has >1 project.
73
73
 
74
- Prints a key:value header (name, status, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
74
+ Prints a key:value header (name, state, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
75
75
 
76
- **Staleness hints (LUM-624):** the `Status:` line is annotated `(stale — task progress indicates <STATUS>; auto-updating)` when the stored status lags the task-derived one (the server self-heals the drift in the background — a re-read shows the corrected status), and the `Target:` line is annotated `(no target date — schedule health unavailable)` for an in-flight milestone with no target date.
76
+ **Staleness hints (LUM-624/LUM-714):** the `State:` line is annotated `(stale — task progress indicates completed|active; auto-updating)` when the stored `completedAt` disagrees with the task counts (the server self-heals the drift in the background — a re-read shows the corrected state), and the `Target:` line is annotated `(no target date — schedule health unavailable)` for an in-flight milestone with no target date.
77
77
 
78
78
  It also prints a **Sprint coverage** section (above the task table) listing which
79
79
  sprints the milestone's tasks span — each row shows the sprint number, status, name,
@@ -111,7 +111,6 @@ lumo milestone show 11111111-2222-3333-4444-555555555555
111
111
  | `--project <ref>` | string | Required when identifier is a name and workspace has >1 project. |
112
112
  | `-n, --name <text>` | string | Cannot be empty. |
113
113
  | `-d, --description <>` | string | `--description ""` clears. |
114
- | `-s, --status <value>` | enum | `planned \| active \| completed \| cancelled`. |
115
114
  | `--start <date>` | string | `--start ""` clears. |
116
115
  | `--target <date>` | string | `--target ""` clears. |
117
116
  | `--token-budget <n>` | int | Advisory token budget (LUM-644), positive integer; `--token-budget ""` clears. |
@@ -122,12 +121,16 @@ At least one field required.
122
121
 
123
122
  **When to suggest**: when the user wants to cap/track token spend on a milestone ("给这个 milestone 设 token 预算 / budget"), or asks why the Overview shows an over-budget alert.
124
123
 
125
- **Status rolls up automatically (LUM-624):** milestone status follows task completion PLANNED → ACTIVE when any task starts, → COMPLETED when every task is DONE (stamps `completedAt`, triggers the retro summary), and COMPLETED → ACTIVE when a non-DONE task (re)appears. A manual `--status planned/active/completed` that contradicts task reality gets reconciled back on the next task event or detail read; `--status cancelled` and archived milestones are human terminal choices the rollup never touches. You normally don't need `--status completed` by hand anymore.
124
+ **There is no status flagstate is derived (LUM-714).** A milestone is `active`, `completed`, or `archived`, and only one of those is a choice:
126
125
 
127
- **The close is gated by exit criteria (LUM-643):** when the milestone has exit criteria (see `milestone criteria` below), any row not adjudicated MET — unadjudicated (PENDING) **or** NOT_MET refuses `--status completed` with **409**, and holds the LUM-624 auto-rollup fail-closed (all tasks DONE keeps the milestone ACTIVE, no retro fires). There is **no CLI/agent override**: the only escape hatch is a human closing via the web with an explicit override (`?force=1` + optional note), stamped on the milestone as a persistent trail (`completedOverride*` — who/when/note, never auto-cleared). To open the gate legitimately, adjudicate the contract with `lumo milestone criteria verdict`.
126
+ - **completed** follows task completion. Every task DONE the rollup stamps `completedAt` and triggers the retro summary; a non-DONE task (re)appearing clears the stamp. Nothing you can pass to `update` moves it.
127
+ - **archived** is the one explicit human state (`milestone archive` / `unarchive`). Archived milestones are left alone by the rollup and hidden from the default list.
128
+
129
+ The old `PLANNED` / `ACTIVE` split is gone — a milestone whose tasks have not started is simply active — and `CANCELLED` is expressed by archiving (a milestone archived while incomplete reads as dropped, not delivered).
130
+
131
+ **The close is gated by exit criteria (LUM-643):** when the milestone has exit criteria (see `milestone criteria` below), any row not adjudicated MET — unadjudicated (PENDING) **or** NOT_MET — holds the LUM-714 derived completion fail-closed: all tasks DONE leaves the milestone active, `completedAt` unstamped, no retro. There is **no CLI/agent override**: the only escape hatch is a human overriding via the web (`POST /api/milestones/<id>/close-override`, Clerk identity only), stamped on the milestone as a persistent trail (`completedOverride*` — who/when/note, never auto-cleared). To open the gate legitimately, adjudicate the contract with `lumo milestone criteria verdict`.
128
132
 
129
133
  ```bash
130
- lumo milestone update "Q3 Launch" --status active
131
134
  lumo milestone update "Q3 Launch" --target 2026-09-15
132
135
  lumo milestone update "Q3 Launch" --description ""
133
136
  lumo milestone update "Q3 Launch" --token-budget 50000000
@@ -162,7 +165,7 @@ lumo milestone criteria set "Q3 Launch" --file exit-criteria.json --human
162
165
 
163
166
  ### `lumo milestone criteria list <identifier>` — print the exit contract
164
167
 
165
- One line per criterion — `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an indented `↳` detail line (date · evidence · note) for adjudicated rows, and a trailing gate-state line: either `All exit criteria MET — the close gate is open.` or `N unresolved (pending or NOT_MET) — the close gate refuses status→COMPLETED until adjudicated (human override only via web).`
168
+ One line per criterion — `<id> [MET|NOT_MET|PENDING] SOURCE ⟨REFERENT⟩ statement` — plus an indented `↳` detail line (date · evidence · note) for adjudicated rows, and a trailing gate-state line: either `All exit criteria MET — the close gate is open.` or `N unresolved (pending or NOT_MET) — the close gate withholds the derived completion until adjudicated (human override only via web).`
166
169
 
167
170
  ```bash
168
171
  lumo milestone criteria list "Q3 Launch"
@@ -189,7 +192,7 @@ lumo milestone criteria verdict "Q3 Launch" cmexit0002 --unmet --note "read mode
189
192
  ### When to suggest `milestone criteria`
190
193
 
191
194
  - A steward session planning a milestone ("what does done mean for this milestone", "draft exit criteria") → `criteria set`.
192
- - Before recommending `milestone update --status completed`, or when it just bounced with 409 → `criteria list` to see what holds the gate, then `verdict` per criterion.
195
+ - When a milestone's tasks are all DONE but it has not completed → `criteria list` to see what holds the gate, then `verdict` per criterion.
193
196
  - User asks "can we close this milestone / what's blocking the close" → `criteria list` (the gate-state line answers it).
194
197
 
195
198
  ### `lumo milestone delete <identifier>` — delete a milestone
@@ -287,7 +290,7 @@ Agent-Native Milestones: 1 marked claimable, 1 skipped
287
290
 
288
291
  Prints the AI-generated retrospective summary for a milestone (mirrors `sprint summary`). `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has more than one project. When no summary exists yet the command prints `(no summary generated yet)`.
289
292
 
290
- A summary is generated automatically when a milestone transitions to `COMPLETED` (e.g. via `lumo milestone update <id> --status completed`). The generated report has sections `## Summary`, `## Delivered`, `## Outstanding` plus a one-line `tldr`. Use `--retry` to queue regeneration (e.g. after a failed generation) before fetching — regeneration is async, so the printed result may still be the previous summary or `(no summary generated yet)`.
293
+ A summary is generated automatically when a milestone completes (the LUM-714 derived transition: its last open task turns DONE, with the exit-criteria gate open). The generated report has sections `## Summary`, `## Delivered`, `## Outstanding` plus a one-line `tldr`. Use `--retry` to queue regeneration (e.g. after a failed generation) before fetching — regeneration is async, so the printed result may still be the previous summary or `(no summary generated yet)`.
291
294
 
292
295
  | Flag | Type | Notes |
293
296
  | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
@@ -304,7 +307,7 @@ When to suggest: user asks "summarize the milestone", "milestone retro", "give m
304
307
 
305
308
  ### `lumo milestone changelog <identifier> [--project <ref>] [--json]` — structured changelog (LUM-649)
306
309
 
307
- Assembles a **structured changelog** from the milestone's **DONE tasks + merged PRs + frozen claimSnapshot**. Pure deterministic assembly — **no LLM**, and (unlike `summary`) **no COMPLETED gate**: it runs on a milestone of any status, so an ACTIVE milestone reports "what has shipped so far". Only DONE tasks ever appear. `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
310
+ Assembles a **structured changelog** from the milestone's **DONE tasks + merged PRs + frozen claimSnapshot**. Pure deterministic assembly — **no LLM**, and (unlike `summary`) **no completion gate**: it runs on a milestone in any state, so an active milestone reports "what has shipped so far". Only DONE tasks ever appear. `<identifier>` accepts a milestone name or UUID; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
308
311
 
309
312
  Honest-reporting contract (why this is more than a task list):
310
313
 
@@ -142,7 +142,7 @@ lumo session attach --steward cmxyz123... # by milestone id
142
142
  - Moving a task to `in_progress` with **yourself as the effective assignee** (whether already assigned to you or assigned in the same call) → 409. Dispatching to _someone else_ is planning and passes.
143
143
  - Red line unchanged: a steward writes no PASS of any kind; DONE stays human-only.
144
144
 
145
- **Backstop (second layer, detector):** commits / PR creations produced during a steward session anyway (the CLI can't stop local git) are recorded as **`STEWARD_IMPLEMENTATION_ARTIFACT` boundary crossings against the milestone** — fail-closed (an unparseable Bash payload records an _unconfirmed_ crossing rather than nothing), disposition stays **web + human-only**, and there is **no CLI clear path** (`crossing explain` remains an appeal, never a clear).
145
+ **Backstop (second layer, detector):** commits / PR creations produced during a steward session anyway (the CLI can't stop local git) are recorded as **`STEWARD_IMPLEMENTATION_ARTIFACT` boundary crossings against the milestone** — fail-closed (an unparseable Bash payload records an _unconfirmed_ crossing rather than nothing), disposition stays **web + human-only** — `lumo crossing disposition` (LUM-769) is task-scoped and cannot reach a milestone crossing — and there is **no CLI clear path** (`crossing explain` remains an appeal, never a clear).
146
146
 
147
147
  #### When to suggest
148
148
 
@@ -148,7 +148,7 @@ lumo task update LUM-48 --status done
148
148
  With no linked PR the PR lines collapse to `Pull requests: none linked` and there is no `⚠` line. Other flags on the same call (`--title`, `--add-tag`…) are preserved in `confirmCommand`.
149
149
 
150
150
  - **Show `changes` to the user and wait for an explicit yes**, then run `confirmCommand` unchanged. `--confirm` means the user confirmed — never add it on your own.
151
- - With `--confirm` the PATCH goes out directly. The server's own DONE gates still apply on top — an unresolved send-back, an undispositioned boundary crossing or a blocking security finding refuses with **409**, which the CLI turns into a structured **`DONE_BLOCKED` error on stdout with exit 5**: the 409 text verbatim, a `blockers[]` with ids (built from `task status` + the crossings read model), and `remediation` lines that only name human-side paths. There is no CLI path to clear any of them — relay, don't retry. See [confirmation.md](confirmation.md) "When the gate is human-only". Any other 409 stays plain text, exit 1.
151
+ - With `--confirm` the PATCH goes out directly. The server's own DONE gates still apply on top — an unresolved send-back, an undispositioned boundary crossing or a blocking security finding refuses with **409**, which the CLI turns into a structured **`DONE_BLOCKED` error on stdout with exit 5**: the 409 text verbatim, a `blockers[]` with ids (built from `task status` + the crossings read model), and `remediation` lines that only name human-side or user-approved paths. Nothing on `task update` clears any of them — relay, don't retry. A boundary crossing is the one blocker with a terminal path, `lumo crossing disposition <id> …` (itself exit-4: the user rules, never you); once every blocker is cleared, re-run this `--confirm` call. See [confirmation.md](confirmation.md) "When the gate is human-only". Any other 409 stays plain text, exit 1.
152
152
  - `--confirm` is the only flag: there is no `--yes` or `--force` on `task update`. Every other status (`todo` / `in_progress` / `in_review`) PATCHes directly and ignores `--confirm`.
153
153
  - A failed lookup (404, network) exits 1 before any envelope.
154
154
 
@@ -142,12 +142,13 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
142
142
 
143
143
  - **Trend** (`--full`) — 规律 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.
144
144
 
145
- - **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included) followed by any **undispositioned PR security findings** (LUM-737) as `• [SECURITY] [SEVERITY] PROVENANCE ruleId — file:line — title (PR #n · blocks DONE | advisory)`. This list IS the plan — recomputed from the event log + the latest scan per linked PR on every read, never maintained separately. The header counts them separately: `Next actions (N unmet · M security findings)`. A finding is **not** a criterion: fix it and push (a fixed fingerprint disappears from the next scan) or a human dispositions it in the web delivery panel — there is no CLI path to clear one, and a `blocks DONE` finding refuses DONE with 409. Empty + rounds recorded = awaiting human adjudication.
146
- - **Per-PR scan status** (LUM-735): before the unconfirmed-PR lines, one line per linked PR that has a latest scan (open or closed alike): `PR #945 · scan CLEAN · secrets RAN · external RAN (2 external findings) · supplyChain RAN (1 dependency finding, 1 already on main) · judge RAN · hunt PARTIAL` — stage segments only for keys present, in secrets/external/supplyChain/judge/hunt order (`hunt` = the L3 vulnerability hunt, LUM-739: absent when `LUMO_SECURITY_HUNT=off`, `SKIPPED` when no trigger fired, `PENDING` while the workflow runs, `RAN` / `PARTIAL` (budget or subgraph cap hit) / `FAILED`; its findings are `LLM_JUDGE` advisory, never `blocks DONE`); `(N external findings)` (singular at 1) decorates only the external segment, and only when N > 0; `(N dependency findings[, M already on main])` (LUM-738; singular at 1) decorates only the supplyChain segment, only when N > 0, and prints bare `supplyChain <state>` against an older server without the counts; ` · partial` appended when the scan is partial; and, when `stages.external === 'FAILED'` and a **scrubbed scanner reason** was recorded (the `error` column carries the `external: ` provenance prefix), an appended ` — <reason without the prefix, tail 200 chars>` — an unprefixed `error` (stage A's own raw crash text) is never printed here, same P8 rule as the web panel and the PR summary (LUM-756). Fed by the additive `securityFindings.scans` array; omitted entirely (no lines) against an older server that doesn't send it.
145
+ - **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included) followed by any **undispositioned PR security findings** (LUM-737) as `• [SECURITY] [SEVERITY] PROVENANCE ruleId — file:line — title (PR #n · blocks DONE | advisory[ — <cause>, downgraded])`. The trailing **downgrade cause** appears only on deterministic `secret:*` rows the scanner lowered to LOW — `placeholder value` (the matched string looks like `…EXAMPLE`, `YOUR_…`, `CHANGEME`…) or `test/fixture/doc path`. Downgrading is **not** suppression: the row is still reported so a human decides, and the cause is what lets you tell a recognised test fixture from a real leak without opening the file. Absent on rows scanned before the cause was recorded, and on any row that was not downgraded — silence means "not downgraded", never "cause unknown". This list IS the plan — recomputed from the event log + the latest scan per linked PR on every read, never maintained separately. The header counts them separately: `Next actions (N unmet · M security findings)`. A finding is **not** a criterion: fix it and push (a fixed fingerprint disappears from the next scan) or a human dispositions it in the web delivery panel — there is no CLI path to clear one, and a `blocks DONE` finding refuses DONE with 409. Empty + rounds recorded = awaiting human adjudication.
146
+ - **Per-PR scan status** (LUM-735): before the unconfirmed-PR lines, one line per linked PR that has a latest scan (open or closed alike): `PR #945 · scan CLEAN · Secrets: checked · Code scan: checked (2 external findings) · Dependencies: checked (1 dependency finding, 1 already on main) · AI review: checked · Exploit paths: ⚠ incomplete` — stage segments only for keys present, in secrets/external/supplyChain/judge/hunt order. **The stage keys are internal; what prints is their display label** (LUM-763, `shared/src/security-scan.ts`, shared with the GitHub PR summary so the two cannot drift): `secrets`→Secrets, `external`→Code scan, `supplyChain`→Dependencies, `judge`→AI review, `hunt`→Exploit paths; states render as `RAN`→checked, `SKIPPED`→skipped, `NOT_CONFIGURED`→off, `PENDING`→scanning, `PARTIAL`→⚠ incomplete, `SUPERSEDED`→⚠ superseded, `FAILED`→✗ failed. "checked" means the layer completed, **not** that it found nothing — findings are counted in their own segments and in the next-actions list (`hunt` = the L3 vulnerability hunt, LUM-739: absent when `LUMO_SECURITY_HUNT=off`, `SKIPPED` when no trigger fired, `PENDING` while the workflow runs, `RAN` / `PARTIAL` (budget hit, a planned task that never finished, or more surviving candidates than the falsifier cap) / `FAILED`; its findings are `LLM_JUDGE` advisory, never `blocks DONE`); `(N external findings)` (singular at 1) decorates only the Code scan segment, and only when N > 0; `(N dependency findings[, M already on main])` (LUM-738; singular at 1) decorates only the Dependencies segment, only when N > 0, and prints bare `Dependencies: <state>` against an older server without the counts; ` · partial` appended when the scan is partial; and, when `stages.external === 'FAILED'` and a **scrubbed scanner reason** was recorded (the `error` column carries the `external: ` provenance prefix), an appended ` — <reason without the prefix, tail 200 chars>` — an unprefixed `error` (stage A's own raw crash text) is never printed here, same P8 rule as the web panel and the PR summary (LUM-756). Fed by the additive `securityFindings.scans` array; omitted entirely (no lines) against an older server that doesn't send it.
147
+ - **Hunt coverage line** (LUM-762): indented under its scan line whenever the server sends the L3 hunt's per-category audit — `Exploit paths 2/3 done · t1 idor 41.2s done · t2 injection 72.0s done (2 attempts) · t3 race skipped`. `<n>/N done` is how many planned tasks finished (where N is the number of tasks the planning step selected for this scan, typically 0–3). Rows persisted before LUM-758 lack a planned-task count and fall back to the historical 6-category denominator (reading as `/6` instead). Each segment is `[taskId ]<category> <elapsed>s <stop>` — the `taskId` prefix (`t1`, `t2`, …) appears only on current rows where the task graph is present (post-LUM-758), and is omitted on pre-task-graph audit rows, so they render exactly as they always did. Elapsed is summed over the task's attempts; `(N attempts)` when the workflow retried it; no elapsed for a `skipped` task (the scan's budget ran out before its turn). Stop values: `done` · `steps` · `budget` · `timeout` · `error` · `skipped`. Headed by the same display label as the layer's own segment (LUM-763), so the detail line and the line above it never name the layer two different ways. This is the only surface that reads `huntAudit`, so it is where you answer "how long does a healthy task take" and "which tasks never ran" without paging through deploy logs. Absent on an older server and on scans with no usable audit (pre-LUM-739 rows, or a hunt that failed before its first batch) — no line, never a guess.
147
148
  - **Persisting findings** (LUM-738): a finding whose fingerprint is already on the repository's default-branch baseline (`persisting`) is not this PR's — it never appears as a next action, is not counted in `openFindings`, and shows muted (_already on the default branch_) in the web panel. Dependency findings (`kind=DEPENDENCY`, from osv-scanner) are advisory: they never block DONE.
148
149
  - **Fails closed:** `⚠ Security-scan check failed — could not confirm …` when the scan read errored, and one `⚠ PR #n: … — could not confirm it is clean.` line per open PR whose latest scan is missing / FAILED / still running. Silence means a successful read with nothing open, never a failed check.
149
- - **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.
150
- - **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.
150
+ - **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count split the way the server DONE gate counts it — `(N undispositioned: B blocking DONE, A advisory / non-blocking)` (LUM-771) — then one line per crossing, blocking rows first as `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first) and non-blocking rows as `◦ [SEVERITY] CATEGORY (advisory — does not block DONE)` / `(below the workspace gate — does not block DONE)`; only the `•` rows need a ruling before DONE, 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 `lumo crossing disposition` and the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
151
+ - **Read-only awareness** — this block surfaces crossings detected elsewhere and clears nothing itself. The ruling is the user's: `lumo crossing disposition <id> --false-positive | --confirmed` (exit-4 envelope you relay, LUM-769 see below) or the web panel. You never self-approve.
151
152
  - **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".
152
153
 
153
154
  ### Responding to an open crossing — `lumo crossing explain`
@@ -163,11 +164,32 @@ lumo crossing explain <id> --note "this was a generated fixture, not a hand-edit
163
164
  This is the **inverse** of dispositioning, but it is the agent/CLI path
164
165
  (bearer-only; a clerk/human caller is refused). Behavior:
165
166
 
166
- - it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only);
167
+ - it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (the user rules, via `lumo crossing disposition` or the web panel);
167
168
  - the note is shown to the human reviewer at disposition time, kept for later review, and explicitly labeled _agent self-report · unverified_;
168
169
  - `<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);
169
170
  - earlier explanations are immutable — a correction is a new note.
170
171
 
172
+ ### Recording the user's ruling — `lumo crossing disposition` (LUM-769)
173
+
174
+ When the user has ruled on a crossing — in chat, after you relayed it — record
175
+ that ruling from the terminal instead of sending them to the web panel:
176
+
177
+ ```bash
178
+ lumo crossing disposition <id> --false-positive --note "covered by the run-outbound tests"
179
+ ```
180
+
181
+ It is a gated mutation on the [confirmation protocol](confirmation.md), as a **three-step** handshake — two envelopes, two separate approvals from the user, and the server enforces the order:
182
+
183
+ - **Step 1 — no step flag.** Reads the crossing, obtains a server-signed stage-1 **read receipt**, and prints an exit-4 envelope — severity, category, detail, `Seen ×N` when it recurred, every explanation you left (labelled _agent self-report · unverified_), the note, a `⚠` line saying that approving clears this crossing's block on DONE (plus `⚠ HIGH severity` / `⚠ Irreversible category` when they apply), and `Step 1 of 2 — read receipt issued … valid until <time>`. Nothing is written. Relay every line and **ask the user to confirm they have read it** — this envelope's `confirmCommand` ends in `--receipt <r1> --confirm-read`, not `--confirm`;
184
+ - **Step 2 — `--confirm-read --receipt <r1>`**, only after the user says they read it. The server verifies r1 and returns the stage-2 receipt; the CLI prints a second exit-4 envelope for the ruling itself — `Read acknowledged … (shown <t1>, acknowledged <t2>)`, `Step 2 of 2 — record the ruling: OPEN → FALSE_POSITIVE`, the note, the ⚠ consequence line. Still nothing written. Relay it and **ask the user to approve the ruling** — its `confirmCommand` ends in `--receipt <r2> --confirm`;
185
+ - **Step 3 — `--confirm --receipt <r2>`**, only after the user approves. POSTs the ruling (`FALSE_POSITIVE` | `CONFIRMED`, optional note, the stage-2 receipt); either value clears that crossing's DONE block, and the audit row records the shown / read-acknowledged / confirmed times. The web panel can still correct it later. **Never add `--confirm-read` or `--confirm` yourself** — each is the user's approval of that step, and the server records the ruling as a CLI-channel disposition (shown as _via CLI_ in the panel) precisely because it cannot verify the in-chat approvals;
186
+ - **the receipts are the server's ordering check** (LUM-769): each is bound to the crossing, the ruling, your session/member and the crossing's state at read time, and expires after 10 minutes. Confirming the ruling with the stage-1 receipt (read not yet acknowledged) is refused — locally before any request, and by the server with 409 — and told to run `--confirm-read` first; a step flag without its receipt, or both step flags at once, is refused locally; a receipt that is forged, expired, for another reader/ruling, or older than the crossing's latest change (a recurrence, a new explanation, a ruling) is refused by the server with 409 and nothing is written — re-run without a step flag for a fresh envelope. If the server cannot issue receipts (no signing secret configured) step 1 fails closed and no envelope is printed;
187
+ - the crossing is read first on both paths: a read that errors **fails closed** (exit 1, nothing written), an id not on the task is exit 1, and an already-matching ruling is a no-op (exit 0);
188
+ - `<id>` resolves against the **session-bound task**; `--task <LUM-N>` overrides the binding (e.g. from a terminal with no session, or after DONE unbinds you);
189
+ - **still web-only:** reverting a ruling to OPEN, repository suppression rules (LUM-751), and steward milestone crossings (`STEWARD_IMPLEMENTATION_ARTIFACT` has no task).
190
+
191
+ After the last blocker is cleared, re-run the original `lumo task update <id> --status done --confirm` — its envelope was already approved.
192
+
171
193
  ### --json contract
172
194
 
173
195
  `--json` emits the full read model with a top-level `version` field (currently