@lumoai/cli 1.58.0 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/assets/skill/SKILL.md +13 -16
  2. package/assets/skill/references/artifacts-figma.md +4 -3
  3. package/assets/skill/references/confirmation.md +131 -0
  4. package/assets/skill/references/criteria.md +12 -21
  5. package/assets/skill/references/doc-editing.md +11 -9
  6. package/assets/skill/references/docs.md +4 -3
  7. package/assets/skill/references/memory.md +4 -2
  8. package/assets/skill/references/milestones.md +3 -2
  9. package/assets/skill/references/outcome.md +1 -14
  10. package/assets/skill/references/plan-runs.md +5 -0
  11. package/assets/skill/references/sessions.md +3 -3
  12. package/assets/skill/references/sprints.md +18 -17
  13. package/assets/skill/references/task-deps.md +4 -3
  14. package/assets/skill/references/tasks.md +34 -2
  15. package/assets/skill/references/verify.md +46 -68
  16. package/assets/skill/references/worktree.md +13 -7
  17. package/dist/cli/src/commands/doc-delete.js +35 -23
  18. package/dist/cli/src/commands/doc-rebuild-source.js +16 -4
  19. package/dist/cli/src/commands/memory-rm.js +68 -10
  20. package/dist/cli/src/commands/milestone-delete.js +13 -10
  21. package/dist/cli/src/commands/outcome.js +0 -77
  22. package/dist/cli/src/commands/session-attach.js +8 -2
  23. package/dist/cli/src/commands/sprint-close.js +29 -9
  24. package/dist/cli/src/commands/sprint-delete.js +13 -10
  25. package/dist/cli/src/commands/sprint-show.js +3 -9
  26. package/dist/cli/src/commands/task-artifact-rm.js +58 -28
  27. package/dist/cli/src/commands/task-criteria-list.js +1 -4
  28. package/dist/cli/src/commands/task-criteria-set.js +3 -12
  29. package/dist/cli/src/commands/task-deps.js +20 -6
  30. package/dist/cli/src/commands/task-status.js +165 -110
  31. package/dist/cli/src/commands/task-update.js +129 -0
  32. package/dist/cli/src/commands/verify.js +22 -13
  33. package/dist/cli/src/commands/worktree-rm.js +35 -7
  34. package/dist/cli/src/index.js +47 -47
  35. package/dist/cli/src/lib/blocked-error.js +178 -0
  36. package/dist/cli/src/lib/confirmation.js +89 -0
  37. package/dist/cli/src/lib/hook-runner.js +23 -11
  38. package/dist/shared/src/referent-kind.js +31 -1
  39. package/dist/shared/src/security-scan.js +29 -0
  40. package/package.json +1 -1
  41. package/assets/skill/references/fidelity.md +0 -32
  42. package/dist/cli/src/commands/fidelity.js +0 -108
  43. package/dist/cli/src/commands/verdict.js +0 -189
@@ -42,15 +42,12 @@ When to suggest: user says "create a sprint", "new sprint", "start a new iterati
42
42
 
43
43
  `<identifier>` accepts a sprint number (e.g. `3`) or a UUID. `--team` is required when using a number in a multi-team workspace.
44
44
 
45
- Output: key:value header (number, name, status, dates, team), then a `Progress:` line, then a **`Health:` line** with the sprint's risk level (`HEALTHY` / `WATCH` / `AT-RISK`), then a task table listing every task in the sprint.
45
+ Output: key:value header (number, name, status, dates, team), then a `Progress:` line, then a `Blockers:` section, then a task table listing every task in the sprint.
46
46
 
47
- The risk level reuses the same engine as project/workspace stats, fed the sprint's task set (thresholds come from the workspace risk config sprints have no per-sprint overrides). When the engine flags reasons they print as `- <detail>` lines under `Health:`, and a `Blockers:` section lists the top offenders per category (`Overdue` / `Stalled` / `Agent fail` / `Stale PRs`). A healthy sprint with no blockers shows just the `Health:` line.
47
+ `Blockers:` lists the top offenders per category (`Overdue` / `Stalled` / `Agent fail` / `Stale PRs`) a task counts as stalled after 7 days untouched, a PR as stale after 3 days awaiting review. A sprint with nothing in any category prints no `Blockers:` section at all. (There is no risk score or `Health:` line LUM-709 removed the risk-assessment layer.)
48
48
 
49
49
  ```
50
50
  Progress: 4 / 10
51
- Health: AT-RISK
52
- - 4/10 tasks overdue (40%)
53
- - 3 tasks with no progress in 7 days
54
51
  Blockers:
55
52
  Overdue: LUM-1 Fix login, LUM-8 Wire API
56
53
  Stalled: LUM-5 Add tests
@@ -63,7 +60,7 @@ lumo sprint show 3 --team backend
63
60
  lumo sprint show 11111111-2222-3333-4444-555555555555
64
61
  ```
65
62
 
66
- When to suggest: user asks "what's in sprint 3", "show me the current sprint", "what tasks are in this sprint", "is this sprint at risk", "sprint health".
63
+ When to suggest: user asks "what's in sprint 3", "show me the current sprint", "what tasks are in this sprint", "what's blocking this sprint".
67
64
 
68
65
  ### `lumo sprint update <identifier> [flags]` — patch a sprint
69
66
 
@@ -83,12 +80,13 @@ lumo sprint update 3 --end 2026-06-21
83
80
 
84
81
  When to suggest: user wants to rename a sprint, extend dates, or fix sprint metadata.
85
82
 
86
- ### `lumo sprint delete <identifier> --yes` — delete a sprint (DRAFT only)
83
+ ### `lumo sprint delete <identifier> --confirm` — delete a sprint (DRAFT only)
87
84
 
88
- Requires `--yes`; no interactive prompt (agent-friendly). Server rejects with an error if the sprint is ACTIVE or CLOSED.
85
+ Without `--confirm` the CLI exits **4** with a confirmation envelope (`Will delete sprint #3 "Sprint 3"` · `N tasks under it keep their data; only sprintId is cleared`) and deletes nothing — relay it to the user, re-run `confirmCommand` on approval ([confirmation.md](confirmation.md)). `--yes` is a legacy alias. Server rejects with an error if the sprint is ACTIVE or CLOSED.
89
86
 
90
87
  ```bash
91
- lumo sprint delete 3 --yes
88
+ lumo sprint delete 3 # exit 4 + envelope
89
+ lumo sprint delete 3 --confirm # after the user approved
92
90
  ```
93
91
 
94
92
  When to suggest: user wants to remove a draft sprint that was created by mistake.
@@ -107,18 +105,21 @@ When to suggest: user says "start the sprint", "kick off sprint 3", "activate sp
107
105
 
108
106
  Handles unfinished tasks based on flags. Without flags: closes only if all tasks are done; otherwise prints a list of unfinished tasks and refuses.
109
107
 
110
- | Flag | Type | Notes |
111
- | --------------- | ------- | -------------------------------------------------------------------------------- |
112
- | `--move-all` | boolean | Move all unfinished tasks to the next sprint. Requires `--yes`. |
113
- | `--backlog-all` | boolean | Remove all unfinished tasks from the sprint (send to backlog). Requires `--yes`. |
114
- | `--yes` | boolean | Required when `--move-all` or `--backlog-all` is given. |
108
+ | Flag | Type | Notes |
109
+ | --------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
+ | `--move-all` | boolean | Move all unfinished tasks to the next sprint. Gated by `--confirm`. |
111
+ | `--backlog-all` | boolean | Remove all unfinished tasks from the sprint (send to backlog). Gated by `--confirm`. |
112
+ | `--confirm` | boolean | The user approved the envelope. Without it a mode flag exits 4 with a confirmation envelope listing every task that would move (`--yes` is a legacy alias). |
115
113
 
116
114
  ```bash
117
- lumo sprint close 3 # fails if unfinished tasks exist
118
- lumo sprint close 3 --move-all --yes # move unfinished to next sprint
119
- lumo sprint close 3 --backlog-all --yes # send unfinished to backlog
115
+ lumo sprint close 3 # fails (exit 1) if unfinished tasks exist — pick a mode
116
+ lumo sprint close 3 --move-all # exit 4 + envelope: "2 unfinished tasks move to the next sprint: LUM-3, LUM-4"
117
+ lumo sprint close 3 --move-all --confirm # move unfinished to next sprint (after the user approved)
118
+ lumo sprint close 3 --backlog-all --confirm # send unfinished to backlog
120
119
  ```
121
120
 
121
+ See [confirmation.md](confirmation.md) for the envelope shape and the relay rules.
122
+
122
123
  When to suggest: user says "close the sprint", "end sprint 3", "wrap up the sprint". If they haven't decided what to do with unfinished tasks, ask before adding `--move-all` or `--backlog-all`.
123
124
 
124
125
  ### `lumo sprint summary <identifier> [--retry]` — fetch AI-generated sprint retro
@@ -82,12 +82,13 @@ lumo task deps dismiss LUM-42 e5f6a7b8
82
82
 
83
83
  Use `dismiss` for false positives. Use `rm` only when you want the pair eligible for re-detection again.
84
84
 
85
- ## `lumo task deps rm <LUM-N> <edge> --yes` — delete an edge
85
+ ## `lumo task deps rm <LUM-N> <edge> --confirm` — delete an edge
86
86
 
87
- Hard-deletes the edge row. **Requires `--yes`** the CLI refuses without it (no interactive prompt exists).
87
+ Hard-deletes the edge row. Without `--confirm` it resolves the edge, then exits **4** with a confirmation envelope describing it (`Will delete dependency edge [a1b2c3d4] from LUM-42: blocked by LUM-9 "…" (CONFIRMED, manual)`) and sends nothing — relay `changes` to the user and re-run the envelope's `confirmCommand` on approval (see [confirmation.md](confirmation.md)). `--yes` is a legacy alias of `--confirm`.
88
88
 
89
89
  ```bash
90
- lumo task deps rm LUM-42 a1b2c3d4 --yes
90
+ lumo task deps rm LUM-42 a1b2c3d4 # exit 4 + envelope, nothing deleted
91
+ lumo task deps rm LUM-42 a1b2c3d4 --confirm # after the user approved
91
92
  # Removed [a1b2c3d4] from LUM-42
92
93
  ```
93
94
 
@@ -120,14 +120,46 @@ The server's transition matrix (`lib/task/state-machine.ts`):
120
120
 
121
121
  Practical rules:
122
122
 
123
- - **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.
123
+ - **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. That one call **walks the confirmation protocol** (see below): without `--confirm` it exits 4 with an envelope, never a prompt.
124
124
  - **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.
125
125
  - **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.
126
126
 
127
+ ### `--status done` requires `--confirm` — the confirmation envelope (LUM-755)
128
+
129
+ DONE is the one status move that needs a human's sign-off. It no longer prompts (LUM-731's interactive yes/no question is gone): run without `--confirm`, the CLI looks the task up, prints a **confirmation envelope** on stdout and exits **4** with nothing sent — no PATCH, and no tag resolution either. The envelope's `changes` carry everything the old prompt showed, including a trailing `⚠` line when a linked PR has not merged (that line replaces the old second prompt — the protocol is one step, and you relay the whole block). Full protocol: [confirmation.md](confirmation.md).
130
+
131
+ ```bash
132
+ lumo task update LUM-48 --status done
133
+ # exit 4, stdout (JSON when piped; the same lines as text on a TTY):
134
+ # {
135
+ # "status": "confirmation_required",
136
+ # "command": "task update",
137
+ # "changes": [
138
+ # "Will move LUM-48 \"Title\" to DONE",
139
+ # "Status: IN_REVIEW → DONE",
140
+ # "#926 merged refactor: panel https://github.com/o/r/pull/926",
141
+ # "#930 open feat: wip https://github.com/o/r/pull/930",
142
+ # "⚠ 1 linked pull request not merged: #930 open"
143
+ # ],
144
+ # "confirmCommand": "lumo task update LUM-48 --status done --confirm"
145
+ # }
146
+ ```
147
+
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
+
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.
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
+ - A failed lookup (404, network) exits 1 before any envelope.
154
+
155
+ ```bash
156
+ lumo task update LUM-48 --status done --confirm # only after the user approved the envelope
157
+ ```
158
+
127
159
  ### When to suggest `task update`
128
160
 
129
161
  - The user describes a state change in natural language (e.g. "mark LUM-48 as in progress", "rename LUM-12 to ...", "assign LUM-30 to me", "bump the priority on LUM-7").
130
- - After the agent finishes a task and the user confirms — offer to move it to `done`.
162
+ - After the agent finishes a task and the user confirms — tell the user to run `lumo task update LUM-N --status done` in their terminal (it prompts for confirmation; an agent shell is non-TTY and is refused).
131
163
  - Multiple status changes in a row should each be a separate `update` invocation rather than batched.
132
164
 
133
165
  ### Sprint output format
@@ -21,22 +21,19 @@ lumo verify LUM-42 --note "…" # explicit task (overrides the sessio
21
21
  lumo verify --note "…" --timeout 900 # per-checkpointer timeout in seconds (default 600)
22
22
  ```
23
23
 
24
- **`--note` is required (LUM-597)** whenever a round will actually be posted (the
25
- task has MACHINE criteria). It is your one-line self-report what you did and
26
- why it is ready ("我改了 X,因为 Y 可验收"). When the round all-passes and the
27
- task flips to IN*REVIEW, the note is frozen as the task's **claim** (provenance
28
- `AGENT` the汇报者's own voice, not a summarizer paraphrase) and is checked
29
- against the diff for faithfulness. Capturing the claim is deterministic ("不填不
30
- 让 post"); whether it is \_truthful* is the faithfulness audit's job — so don't
31
- under- or over-state it. A missing/blank note is refused **before** the round is
32
- posted, so no round is burned; just re-run with one. (An old CLI that omits it
33
- still verifies — the claim degrades to the synthesized run-summary fallback.)
24
+ **`--note` is optional (LUM-733).** It is your one-line self-report what
25
+ you did and why it is ready ("我改了 X,因为 Y 可验收"). When given and the round
26
+ all-passes into IN_REVIEW, it is frozen as the task's claim (source `AGENT`) and
27
+ judged for faithfulness in the background; when omitted the round still posts
28
+ and the claim degrades to the synthesized run summary. It is never a
29
+ precondition for verifying don't hold a round back to word it.
34
30
 
35
31
  ## What one round does
36
32
 
37
33
  1. Loads the task's acceptance contract and picks out MACHINE criteria.
38
- 2. Runs each checkpointer locally (shell, cwd = current directory), one at a
39
- time, echoing PASS/FAIL as it goes.
34
+ 2. Runs each checkpointer locally (shell, cwd = current directory, env = the
35
+ CLI's own minus its private `LUMO_NO_HINTS` mute — `--no-hints` never
36
+ reaches a checkpointer), one at a time, echoing PASS/FAIL as it goes.
40
37
  3. POSTs the structured verdicts; the server records one VerificationRun per
41
38
  criterion at round = previous max + 1 and mirrors each verdict as a
42
39
  TaskActivity event.
@@ -64,16 +61,15 @@ errors.
64
61
 
65
62
  ## Edge cases
66
63
 
67
- | Case | Behavior |
68
- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
69
- | **No contract yet** | Error pointing at `lumo task criteria set`; draft the contract first (criteria.md golden rule). |
70
- | **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. |
71
- | **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds. |
72
- | **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round. |
73
- | **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
74
- | **Ungrounded EXTERNAL_FACT PASS** (LUM-605) | A reported PASS on a criterion declared `EXTERNAL_FACT` whose checkpointer doesn't invoke external tooling (e.g. `npx jest …`) is blocked with **422 no round burned**; the task does **not** move to IN_REVIEW. Fix it for free: re-ground the checkpointer against a real external fact, or honestly re-declare `referentKind` as `AGENT_CONSTRUCTED_STATE` (`lumo task criteria set`), then re-run. See criteria.md "referentKind". |
75
- | **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. |
76
- | **Unconfirmed session binding** | `bindingAdvisory: 'unconfirmed'` → softer advisory `⚠ Could not confirm this session is attached to the task.` Same remediation: `lumo session attach <LUM-N>`. |
64
+ | Case | Behavior |
65
+ | ----------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
66
+ | **No contract yet** | Error pointing at `lumo task criteria set`; draft the contract first (criteria.md golden rule). |
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. |
68
+ | **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds. |
69
+ | **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round. |
70
+ | **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
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. |
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>`. |
77
73
 
78
74
  ## Round discipline
79
75
 
@@ -105,8 +101,9 @@ nothing and burns no round. Defaults to the session-bound task; an explicit
105
101
  identifier overrides.
106
102
 
107
103
  ```bash
108
- lumo task status # session-bound task
104
+ lumo task status # session-bound task (core: criteria / next actions / open crossings)
109
105
  lumo task status LUM-42 # explicit task
106
+ lumo task status --full # + verification rollup, history, cost, struggle trail, trend
110
107
  lumo task status --json # versioned machine-readable payload
111
108
  ```
112
109
 
@@ -125,19 +122,16 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
125
122
  ### What it prints
126
123
 
127
124
  - **Header** — task identifier/title/status + `verification round N/M` (`M` = the workspace's `verificationMaxRounds`; round 0 = never verified) + an escalation warning when the machine loop is exhausted.
128
- - **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:
129
- - **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.
130
- - **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.
131
- - **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.
132
- - 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`.
125
+ - **Default = the self-check core (LUM-733):** header, Criteria, Last round failures, Next actions, Open boundary crossings. The dashboard sections below (**Verification**, **History**, **Cost**, **Struggle**, **Trend**) print only with `--full` they are human-dashboard material and cost context. The claim text and faithfulness verdict are no longer rendered in the terminal (`--json` still carries `claim` / `faithfulness` for scripts).
126
+ - **Verification** (`--full`) the measured rollup `N machine-verified / M human override (of T MACHINE criteria)` plus `X of Y criteria met by their latest verdict`; before any round it prints `no verification has run yet`.
133
127
  - **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.
134
128
  - 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.
135
129
  - 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.
136
130
  - 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.
137
- - **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
131
+ - **History** (`--full`) — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
138
132
  - **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
139
- - **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.
140
- - **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:
133
+ - **Cost** (`--full`) — 规律 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.
134
+ - **Struggle / rework / outstanding** (`--full`) — 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:
141
135
  - **rework rounds** — verify rounds that had a FAIL;
142
136
  - **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;
143
137
  - **leftover follow-ups** — criteria whose latest verdict is `PASS_WITH_FOLLOWUP`;
@@ -146,9 +140,12 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
146
140
 
147
141
  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`).
148
142
 
149
- - **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.
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.
150
144
 
151
- - **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.
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.
147
+ - **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
+ - **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.
152
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.
153
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.
154
151
  - **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".
@@ -181,50 +178,33 @@ fields don't. Pin on `version` when scripting against it.
181
178
  - 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;
182
179
  - 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;
183
180
  - **`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".
181
+ - `nextActions` entries carry a `kind` discriminator (LUM-737): `CRITERION` (the pre-737 shape: `criterionId`, `statement`, `verifierType`, `checkpointer`, `judgeSteps`, `source`, `addedAtRound`, `rejectionReason`) or `SECURITY_FINDING` (`findingId`, `statement`, `severity`, `provenance`, `blocking`, `prNumber`, `ruleId`, `filePath`, `line`). Additive — a missing `kind` means `CRITERION`; do not index every entry by `criterionId`;
182
+ - a top-level `securityFindings` summary `{ open, blocking, unconfirmedPrs: [{ number, reason: NONE|FAILED|PENDING|RUNNING }], scans?: [{ prNumber, status, stages, partial, error, externalFindings, openFindings }] }`; **`null` means the scan read failed** — treat as "unknown / could not confirm", not "safe". `scans` is additive (LUM-735) — absent against an older server, one entry per linked PR with a latest scan otherwise.
184
183
 
185
184
  `status` reads; `verify` judges. Running status never starts a round, never
186
185
  escalates, and never changes task state — loop rules (the workspace round cap,
187
186
  IN_REVIEW on all-pass, human-only DONE) live entirely in `lumo verify` and the
188
187
  server.
189
188
 
190
- ## lumo verdict — the three verdict channels
189
+ ## Verdict channels
191
190
 
192
- `lumo verify` is the MACHINE channel. `lumo verdict` covers the other two the
193
- HUMAN pass and the AGENT send-back under one red line: **no passing data row is
194
- ever agent-produced.**
195
-
196
- ```bash
197
- lumo verdict --pass
198
- lumo verdict LUM-42 --pass
199
- lumo verdict --fail --reason CRITERION_UNMET --note "the retry path is still missing"
200
- lumo verdict LUM-42 --fail --reason scope_mismatch --criterion c-abc123
201
- ```
202
-
203
- ### --pass — a deep link, never a write
204
-
205
- This resolves the task, then opens the browser to its verdict bar focused on
206
- Pass. **The CLI writes nothing** — PASS only ever lands from a human's own click
207
- (Clerk session). Use this to hand a finished task to a human for the final pass;
208
- it carries them one click from recording it.
209
-
210
- ### --fail — the AGENT send-back
211
-
212
- `--fail --reason <enum>` records a real verdict row server-side with
213
- verifierType=AGENT (a channel distinct from MACHINE and HUMAN, so "machine
214
- all-pass but human FAIL" stays an uncontaminated signal). The verdict is
215
- hard-coded FAIL — there is no agent path to a passing verdict. It:
216
-
217
- - `--reason <enum>` required (case-insensitive): `CRITERION_UNMET | EVIDENCE_INSUFFICIENT | CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER` — the agent pays the structured tax a human send-back is spared;
218
- - `--note <text>` optional, posted as a task comment (@mentions and images for free) and summarized onto the verdict row;
219
- - `--criterion <id>` repeatable, narrows the send-back; omitted, it fans out to the whole contract;
220
- - `round` = the current max (not a new round); bounces the task back to IN_PROGRESS, with the unmet criteria surfacing through `lumo task status`.
191
+ `lumo verify` is the MACHINE channel. The only other channel is the **human**
192
+ one: a person records PASS or a send-back (FAIL) in the web verdict bar (Clerk
193
+ session). There is no agent-facing verdict command — the former
194
+ `lumo verdict --fail` AGENT send-back was removed in LUM-733 (17 rows all-time,
195
+ none in the last 30 days); when the machine loop runs out of rounds the task is
196
+ escalated to a human instead. **No passing data row is ever agent-produced.**
221
197
 
222
198
  ### The DONE gate
223
199
 
224
- Once any criterion's latest verdict is FAIL — machine, AGENT, or human — moving
200
+ Once any criterion's latest verdict is FAIL — machine or human — moving
225
201
  the task to DONE on the agent/CLI path is refused with **409** and the unresolved
226
202
  items listed. Clear the send-back (fix + re-verify, or a human PASS) before
227
- `lumo task update <id> --status done`.
203
+ `lumo task update <id> --status done` — which itself walks the confirmation
204
+ protocol: without `--confirm` it exits 4 with an envelope for the user to
205
+ approve, and when the server gate still refuses it exits 5 with a structured
206
+ `DONE_BLOCKED` error listing every blocker (LUM-755, see [tasks.md](tasks.md)
207
+ and [confirmation.md](confirmation.md)).
228
208
 
229
209
  - A task with no criteria, or whose criteria were never adjudicated, transitions freely — **the gate only blocks an actual send-back, never an un-adjudicated criterion.**
230
210
  - When the machine loop has left a task IN_REVIEW with no send-back standing, the agent may move it to DONE directly; a human-PASS row is a provable manual override, not a required ticket.
@@ -259,9 +239,7 @@ When someone reports a defect in conversation, your action depends on whether th
259
239
  task has **ever entered IN_REVIEW**:
260
240
 
261
241
  - **Not yet** (still your first working pass) → just fix it and continue. No verdict needed — nothing was claimed complete, so there's nothing to contradict.
262
- - **Already submitted** (entered IN_REVIEW / DONE / merged) → **do not silently fix and re-pass.** Either:
263
- - record your own send-back `lumo verdict --fail` (noting it was human-reported — this is _your_ honest concurrence, not a forged human verdict), or
264
- - ask the reporter to record a human FAIL via the web UI / Slack (the only channel that can attribute it to a human).
242
+ - **Already submitted** (entered IN_REVIEW / DONE / merged) → **do not silently fix and re-pass.** Ask the reporter to record a human send-back (FAIL) via the web verdict bar / Slack — the only channel that can attribute it to a human — then fix in place and re-run `lumo verify` so the fix lands as a fresh machine round on the record.
265
243
 
266
244
  If the defect is a **new requirement** not covered by any criterion, first
267
245
  transcribe it with `lumo task criteria set --human`, then proceed. You can never
@@ -55,15 +55,21 @@ Errors if the target dir already exists; reuses the branch if it already exists
55
55
 
56
56
  ## `lumo worktree rm <LUM-N>`
57
57
 
58
- Removes the worktree for a task. Requires `--yes`. Refuses a dirty worktree
59
- unless `--force`. Keeps the branch by default (it may hold unpushed work / an
60
- open PR); `--delete-branch` removes it with `git branch -d` (which itself
61
- refuses an unmerged branch).
58
+ Removes the worktree for a task. Gated by the confirmation protocol
59
+ ([confirmation.md](confirmation.md)): without `--confirm` or with
60
+ uncommitted changes and no `--force` it exits **4** with an envelope naming
61
+ the path and branch, a `⚠ … uncommitted changes … discarded` line when dirty,
62
+ and whether the branch is kept or deleted; nothing is removed. A dirty tree's
63
+ `confirmCommand` carries `--force --confirm`. `--yes` is a legacy alias of
64
+ `--confirm`. Keeps the branch by default (it may hold unpushed work / an open
65
+ PR); `--delete-branch` removes it with `git branch -d` (which itself refuses
66
+ an unmerged branch).
62
67
 
63
68
  ```bash
64
- lumo worktree rm LUM-267 --yes
65
- lumo worktree rm LUM-267 --yes --force # discard uncommitted changes
66
- lumo worktree rm LUM-267 --yes --delete-branch # also delete lumo/LUM-267…
69
+ lumo worktree rm LUM-267 # exit 4 + envelope
70
+ lumo worktree rm LUM-267 --confirm # after the user approved (clean tree)
71
+ lumo worktree rm LUM-267 --force --confirm # dirty tree: discard uncommitted changes
72
+ lumo worktree rm LUM-267 --confirm --delete-branch # also delete lumo/LUM-267…
67
73
  ```
68
74
 
69
75
  ## `lumo worktree list`
@@ -1,18 +1,23 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeDocDelete = describeDocDelete;
3
4
  exports.docDelete = docDelete;
4
5
  const config_1 = require("../lib/config");
5
6
  const api_1 = require("../lib/api");
6
- const resolve_doc_1 = require("../lib/resolve-doc");
7
7
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
8
8
  const sanitize_1 = require("../lib/sanitize");
9
+ const confirmation_1 = require("../lib/confirmation");
10
+ /** The changes[] block for the confirmation envelope. */
11
+ function describeDocDelete(id, title) {
12
+ const escaped = (0, sanitize_1.sanitizeField)(title).replace(/"/g, '\\"');
13
+ return [
14
+ `Will delete document ${id} "${escaped}"`,
15
+ 'Every document nested under it is deleted with it; task bindings and shares are dropped',
16
+ ];
17
+ }
9
18
  async function docDelete(reference, opts) {
10
19
  if (!reference) {
11
- console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --yes');
12
- return 1;
13
- }
14
- if (!opts.yes) {
15
- console.error('Error: Refusing to delete without --yes');
20
+ console.error('Error: missing <doc>. Usage: lumo doc delete <doc> --confirm');
16
21
  return 1;
17
22
  }
18
23
  const creds = (0, config_1.readCredentials)();
@@ -21,26 +26,33 @@ async function docDelete(reference, opts) {
21
26
  return 1;
22
27
  }
23
28
  const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
24
- // For a cuid reference we don't have the title; for a title we look up id+title at once.
25
- let id;
26
- let title = '';
27
- if ((0, resolve_doc_1.isLikelyCuid)(reference)) {
28
- id = reference;
29
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
30
+ const headers = { Authorization: `Bearer ${creds.token}` };
31
+ const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
32
+ if (!id) {
33
+ console.error(`Error: Document not found: ${reference}`);
34
+ return 1;
35
+ }
36
+ // Always fetch the doc first: the envelope needs its real title, and the
37
+ // same GET confirms it exists before anything destructive happens.
38
+ const showRes = await fetch(`${base}/api/documents/${id}`, { headers });
39
+ if (!showRes.ok) {
40
+ const text = await showRes.text();
41
+ console.error(`Error: ${showRes.status} ${showRes.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
42
+ return 1;
29
43
  }
30
- else {
31
- const fetched = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
32
- if (!fetched) {
33
- console.error(`Error: Document not found: ${reference}`);
34
- return 1;
35
- }
36
- id = fetched;
37
- // Pick up the title from the lookup payload by repeating the GET — small extra cost.
38
- // (Alternative: change lookupDocId to return the full DocLike. For now keep it simple.)
39
- title = reference;
44
+ const { document } = (await showRes.json());
45
+ const title = document.title ?? '';
46
+ // LUM-755: no --confirm → envelope, no DELETE.
47
+ if (!(0, confirmation_1.isConfirmed)(opts)) {
48
+ return (0, confirmation_1.emitConfirmation)({
49
+ command: 'doc delete',
50
+ changes: describeDocDelete(id, title),
51
+ });
40
52
  }
41
- const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
53
+ const res = await fetch(`${base}/api/documents/${id}`, {
42
54
  method: 'DELETE',
43
- headers: { Authorization: `Bearer ${creds.token}` },
55
+ headers,
44
56
  });
45
57
  if (!res.ok) {
46
58
  const text = await res.text();
@@ -5,6 +5,7 @@ const config_1 = require("../lib/config");
5
5
  const api_1 = require("../lib/api");
6
6
  const resolve_doc_id_1 = require("../lib/resolve-doc-id");
7
7
  const sanitize_1 = require("../lib/sanitize");
8
+ const confirmation_1 = require("../lib/confirmation");
8
9
  /**
9
10
  * `lumo doc rebuild-source <doc>` (LUM-446).
10
11
  *
@@ -13,7 +14,9 @@ const sanitize_1 = require("../lib/sanitize");
13
14
  * re-enabling `doc show --raw` / `diff` / `patch` / `append`. The rebuilt source
14
15
  * is structure-guarded server-side: any table/tr/heading shrink is rejected 422
15
16
  * (zero silent flattening — LUM-410 口径) unless --allow-shrink is passed. A doc
16
- * that already has a source is refused 409 unless --force re-derives it.
17
+ * that already has a source is refused 409 unless --force re-derives it
18
+ * that refusal is surfaced as a confirmation envelope (LUM-755): exit 4 with
19
+ * the server's reason and a confirmCommand carrying `--force --confirm`.
17
20
  */
18
21
  async function docRebuildSource(reference, opts) {
19
22
  if (!reference) {
@@ -57,11 +60,20 @@ async function docRebuildSource(reference, opts) {
57
60
  if (!res.ok) {
58
61
  const text = await res.text();
59
62
  const message = (0, api_1.extractErrorMessage)(text);
63
+ if (res.status === 409 && !opts.force) {
64
+ // The --force gate: nothing was written. Hand back the protocol
65
+ // envelope so the agent shows the user what --force would replace.
66
+ return (0, confirmation_1.emitConfirmation)({
67
+ command: 'doc rebuild-source',
68
+ changes: [
69
+ (0, sanitize_1.sanitizeField)(message),
70
+ `Will replace the existing (byte-faithful) markdown source of ${id} with a freshly serialized one — inspect it first with \`lumo doc show ${reference} --raw\``,
71
+ ],
72
+ extraFlags: ['--force'],
73
+ });
74
+ }
60
75
  if (res.status === 409) {
61
76
  console.error(`Error: ${(0, sanitize_1.sanitizeField)(message)}`);
62
- console.error('Hint: the doc already has a markdown source. Inspect it with ' +
63
- `\`lumo doc show ${reference} --raw\`; pass --force only if you intend ` +
64
- 'to replace it with a freshly serialized one.');
65
77
  return 1;
66
78
  }
67
79
  if (res.status === 422) {