@lumoai/cli 1.66.0 → 1.69.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, ideas, plan runs (the idea→plan converter), projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, priorities, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
3
+ description: 'Use when the user mentions a Lumo task id (LUM-N, or any team prefix like SPEC-12) or the `lumo` CLI, in any language; is starting, resuming, or about to claim completion of a task; asks what to work on next; or works with any Lumo resource — task context, sessions, acceptance criteria, machine verification, tasks, ideas, plan runs (the idea→plan converter), projects, milestones, sprints, docs, artifacts, Figma links, dependencies, team memory, priorities, PR security scans, or worktrees. Key triggers: "LUM-", "lumo", "task context", "session attach", "verify", "task status", "acceptance criteria", "what should I work on", "resume task".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -25,13 +25,14 @@ The command catalog below is a **map**: it lists every command grouped by domain
25
25
  | --------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
26
26
  | `setup`, `auth login/logout`, `whoami`, `update` | [references/onboarding.md](references/onboarding.md) |
27
27
  | `task context`, retrieval (`slack/web/figma context`, `comments list`, `pr show`) | [references/task-context.md](references/task-context.md) |
28
+ | `pr scan <number>` — one PR's security scans by number (layers, findings, hunt detail) | [references/pr-scan.md](references/pr-scan.md) |
28
29
  | `task create/update/list/show/comment`, `next` | [references/tasks.md](references/tasks.md) |
29
30
  | `task deps add/list/confirm/dismiss/rm` (dependency edges) | [references/task-deps.md](references/task-deps.md) |
30
31
  | `idea*` — capture pool, lifecycle, evidence, comments | [references/ideas.md](references/ideas.md) |
31
32
  | `plan`, `plan status` — the idea→plan converter run | [references/plan-runs.md](references/plan-runs.md) |
32
33
  | `initiative create/list` (`LUM-INIT-<n>`) | [references/initiatives.md](references/initiatives.md) |
33
34
  | `task artifact*`, `task figma*` | [references/artifacts-figma.md](references/artifacts-figma.md) |
34
- | `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
35
+ | `task criteria set/list/confirm`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
35
36
  | `verify`, `task status` — machine verification loop, claim-done flow, self-check/resume | [references/verify.md](references/verify.md) |
36
37
  | `cost` — per-operation (per-tool) token cost read-out; `task lineage` Top-5 | [references/task-context.md](references/task-context.md) |
37
38
  | `outcome show/rate` — post-hoc outcome well (falsifier oracle, rejection-only) | [references/outcome.md](references/outcome.md) |
@@ -64,6 +65,10 @@ The command catalog below is a **map**: it lists every command grouped by domain
64
65
  - `lumo task pr show <id> <number>` — synced PR metadata (v1)
65
66
  - `lumo task lineage <id>` — read-only causal-trail audit: fragments that fed the task + each one's disclosure tag/outcome, the run's token/loop cost, a disclosure-funnel summary, and a Top-5 "operations by token cost"; `--signal` appends workspace-level usage signal-health (see [task-context.md](references/task-context.md))
66
67
 
68
+ ### Pull requests by number — see [pr-scan.md](references/pr-scan.md)
69
+
70
+ - `lumo pr scan <number> [--repo owner/repo] [--all] [--full] [--json]` — one PR's security scans by number, linked to a task or not: layer line (same labels as `task status`), every finding incl. dispositioned ones + their source, hunt coverage; `--full` per-task hunt detail, `--all` every head. Read-only.
71
+
67
72
  ### Tasks — see [tasks.md](references/tasks.md)
68
73
 
69
74
  - `lumo task create <title> [flags]` — create a task. **Mid-task** (your session is bound to an in-flight task) it requires `--rework-of <id>` (redirects you to fix the existing task — creates nothing) or `--new-scope` (genuinely new, out-of-scope work). On a send-back, fix in place / amend the contract instead of spinning off a new task — see [verify.md](references/verify.md) and [criteria.md](references/criteria.md).
@@ -104,13 +109,14 @@ The command catalog below is a **map**: it lists every command grouped by domain
104
109
 
105
110
  - `lumo task criteria set <task> --file <criteria.json> [--human] [--cause <tag>]` — submit the whole contract (full-group replace). Default = agent draft (AGENT_DRAFT, editable until DONE); `--human` = a HUMAN_EDIT revision transcribed from the conversation; `--cause` annotates why the contract drifted. See [criteria.md](references/criteria.md)
106
111
  - `lumo task criteria list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
112
+ - `lumo task criteria confirm <task> [--confirm]` — record the **user's** confirmation of the contract; without `--confirm` it exits 4 with an envelope listing every criterion (writes nothing). Never blocks DONE. See [criteria.md](references/criteria.md)
107
113
 
108
114
  ### Verification (machine acceptance loop) — see [verify.md](references/verify.md)
109
115
 
110
116
  - `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
117
  - `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
118
  - `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)
119
+ - `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** read-then-rule confirmation handshake the server enforces (LUM-769); never supply a step flag (`--confirm-read` / `--confirm`) yourself each is the user's approval of that step. Either ruling clears that crossing's block on DONE. Reverting to OPEN, repository suppression and steward crossings stay web-only. See [verify.md](references/verify.md)
114
120
 
115
121
  ### Cost (per-operation token read-out) — see [task-context.md](references/task-context.md)
116
122
 
@@ -207,6 +213,7 @@ Don't guess these:
207
213
  - No `lumo session detach` — the session↔task binding is a lifetime lock; to work on a different task, start a new Claude Code session
208
214
  - No steward "role switch" or un-steward — `session attach --steward` is the same lifetime lock on a milestone; a steward session never converts to an implementation session (and there is no CLI path to clear its boundary crossings)
209
215
  - No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
216
+ - No `lumo pr show` / `lumo scan` — the by-number security read-out is `lumo pr scan <number>`; `lumo task pr show` is the task-scoped PR card
210
217
  - No `lumo task artifact edit` — it's `lumo task artifact update`
211
218
  - No `lumo auth status` — identity check is `lumo whoami`
212
219
  - No `--body` on `lumo task comment` — the body is a positional arg: `lumo task comment LUM-N "text"`
@@ -1,6 +1,6 @@
1
1
  # Confirmation protocol (`--confirm`, exit code 4)
2
2
 
3
- Some `lumo` commands change or destroy state that a human should sign off on first — deleting a sprint, milestone, document, memory, artifact or dependency edge; closing a sprint with unfinished tasks; removing a worktree; re-deriving a doc's markdown source; and moving a task to **DONE**. Since LUM-755 they all speak one protocol instead of prompting or hard-refusing:
3
+ Some `lumo` commands change or destroy state that a human should sign off on first — deleting a sprint, milestone, document, memory, artifact or dependency edge; closing a sprint with unfinished tasks; removing a worktree; re-deriving a doc's markdown source; moving a task to **DONE**; and, since LUM-789, recording a human's confirmation of the acceptance contract. Since LUM-755 they all speak one protocol instead of prompting or hard-refusing:
4
4
 
5
5
  1. You run the command **without `--confirm`**.
6
6
  2. The CLI computes, from real server/local state, what it would do — and **does not do it**. It prints a **confirmation envelope on stdout** and exits **4**.
@@ -48,7 +48,7 @@ Nothing goes to stderr; the envelope is the command's result, not an error. **Ex
48
48
  ## Flags
49
49
 
50
50
  - `--confirm` — the user approved. Present on every command in the table below.
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.
51
+ - `--yes` — legacy alias of `--confirm`, carried by the commands that took it before LUM-755 plus `task criteria confirm` (LUM-789). 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
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.
54
54
 
@@ -57,6 +57,7 @@ Nothing goes to stderr; the envelope is the command's result, not an error. **Ex
57
57
  | Command | `changes` describes | Notes |
58
58
  | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
59
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
+ | `task criteria confirm <task>` | `Will record YOUR confirmation of the acceptance contract on LUM-N` · `N of M criteria are not yet confirmed` · one ` [MACHINE\|HUMAN] statement` line per criterion (already-confirmed ones annotated) · a reminder that confirming never closes the task or blocks DONE | LUM-789. Never blocks DONE either way — an unconfirmed contract closes normally. See [criteria.md](criteria.md). |
60
61
  | `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
62
  | `task deps rm <id> <edge>` | the resolved edge (short id, direction, other task, status, source) | [task-deps.md](task-deps.md) |
62
63
  | `task artifact rm <task> <artifact-id>` | artifact id, title, kind; "irreversible" | [artifacts-figma.md](artifacts-figma.md) |
@@ -230,6 +230,113 @@ Print the contract: `<id> [MACHINE|HUMAN] SOURCE@rN [evidence] statement` pl
230
230
  lumo task criteria list LUM-42
231
231
  ```
232
232
 
233
+ ### `lumo task criteria confirm <task> [--confirm]` — LUM-789
234
+
235
+ Record the **user's** one-time confirmation that they've seen and agreed to
236
+ the acceptance contract. This is the plan's one structural human touchpoint:
237
+ confirming right after drafting is cheap — the contract is small and fresh in
238
+ the human's head — confirming after implementation is done is expensive
239
+ rework by comparison. Confirming is **read-only with respect to DONE**: it
240
+ never closes the task and there is no gate anywhere that requires it. An
241
+ unconfirmed contract closes exactly like a confirmed one.
242
+
243
+ **Flags:**
244
+
245
+ | Flag | Meaning |
246
+ | ----------- | -------------------------------------------------------------------------------------- |
247
+ | `--confirm` | The user approved, after seeing every criterion. Required to write; absent by default. |
248
+ | `--yes` | Legacy alias of `--confirm`. Prefer `--confirm` in new invocations. |
249
+
250
+ It follows the [confirmation protocol](confirmation.md) exactly — two calls,
251
+ never a prompt:
252
+
253
+ **Step 1 — without `--confirm` (writes nothing):**
254
+
255
+ ```bash
256
+ lumo task criteria confirm LUM-42
257
+ ```
258
+
259
+ ```json
260
+ {
261
+ "status": "confirmation_required",
262
+ "command": "task criteria confirm",
263
+ "changes": [
264
+ "Will record YOUR confirmation of the acceptance contract on LUM-42",
265
+ "1 of 2 criteria are not yet confirmed",
266
+ " [HUMAN] 登录页在移动端可用 (already confirmed)",
267
+ " [MACHINE] npx tsc --noEmit 通过",
268
+ "Confirming does NOT close the task and never blocks DONE — it records that a human agreed to what \"done\" means here",
269
+ "To change the contract instead, run `lumo task criteria set <task> --file <criteria.json> --human`"
270
+ ],
271
+ "confirmCommand": "lumo task criteria confirm LUM-42 --confirm"
272
+ }
273
+ ```
274
+
275
+ Every criterion in the contract is listed as ` [MACHINE|HUMAN] statement`,
276
+ with `(already confirmed)` appended to rows that already carry a
277
+ `confirmedAt`. Read the `N of M criteria are not yet confirmed` line and
278
+ every criterion line to the user — this envelope IS the "read the contract
279
+ aloud" step. A task with no criteria yet refuses (exit 1): draft one first.
280
+
281
+ **Step 2 — after the user approves, re-run with `--confirm`:**
282
+
283
+ ```bash
284
+ lumo task criteria confirm LUM-42 --confirm
285
+ # Confirmed the acceptance contract on LUM-42 (2 criteria newly confirmed)
286
+ ```
287
+
288
+ `confirmedCount` counts only rows that were **previously unconfirmed** and
289
+ just received their first stamp — re-running on an already-fully-confirmed
290
+ contract is an idempotent no-op (`0 criteria newly confirmed`), never an
291
+ error, and never overwrites who confirmed it or when.
292
+
293
+ ### When to suggest it
294
+
295
+ Right after you finish drafting the contract with `lumo task criteria set`
296
+ and have read every criterion aloud to the user in chat — that's the moment
297
+ their agreement is cheapest to capture. If the user instead says "change X"
298
+ or otherwise wants the contract itself different, that's **not** a
299
+ confirmation — run `lumo task criteria set <task> --file <criteria.json>
300
+ --human` to transcribe their edit (see above); only offer `criteria confirm`
301
+ once the contract reads back the way they actually want it.
302
+
303
+ ### Two things a future reader will get wrong
304
+
305
+ - **Unconfirmed is the default, not a deviation.** Every fresh `AGENT_DRAFT`
306
+ contract starts unconfirmed, and there is deliberately **no flag to skip
307
+ confirmation** — there's nothing to skip. `lumo task status` prints a
308
+ one-line nudge (`Contract unconfirmed (N criteria) — lumo task criteria
309
+ confirm LUM-42`) when criteria are unconfirmed **and the task isn't DONE**,
310
+ but it's advisory, not a blocker: absence of confirmation is not an error
311
+ state anywhere in this system.
312
+ - **DONE is a terminal lock on confirming, same as on editing.** The
313
+ migration that added `confirmedAt` is deliberately backfill-free, so every
314
+ contract drafted before LUM-789 reads unconfirmed forever — without a DONE
315
+ guard the nudge above would sit on every historical, already-shipped task.
316
+ `lumo task status` suppresses the nudge once the task is DONE, and
317
+ `lumo task criteria confirm` itself pre-checks the task's status (carried
318
+ on the same `GET /api/tasks/[id]/criteria` read used to build the
319
+ confirmation envelope — no extra round trip) and refuses with exit 1
320
+ _before_ printing the envelope, so it never promises a write it can't
321
+ deliver. The server backs this with its own 409
322
+ (`taskCriteriaConfirmService.confirmContract`, same terminal-lock message
323
+ family as `criteria set --human`'s DONE 409) in case an older CLI skips the
324
+ pre-check. Confirming after delivery would record a human "agreeing" to
325
+ the contract after the fact — the exact inversion LUM-789 exists to
326
+ prevent (the touchpoint is supposed to be at draft time, not after DONE).
327
+ - **`--human` self-confirms by construction — this is accepted, not a bug.**
328
+ A `criteria set --human` revision stamps every row it touches as
329
+ `confirmedAt`/`confirmedByMemberId` in the same write (see "Record a human
330
+ contract revision" above) — `HUMAN_EDIT` is the source tag the **caller**
331
+ declares on the request, not verified proof of human authorship. An agent
332
+ that calls `--human` on its own initiative therefore confirms its own
333
+ contract with nobody in the loop. This is a known, deliberate trade-off the
334
+ user ruled on for LUM-789, not something to "fix" by adding a separate
335
+ confirm step to the `--human` path — `--human` is already reserved for
336
+ transcribing a human's own stated decision (never for the agent's own
337
+ edits), so the self-confirmation only ever fires on a caller that already
338
+ violated that rule.
339
+
233
340
  ## Injection behavior
234
341
 
235
342
  - **Session start** (bound task): the contract is the highest-priority section in the injection budget — ahead of memory/PR/Slack/Figma/web. If a still-open task has no criteria, the draft reminder is injected instead.
@@ -226,6 +226,35 @@ Content is **always normalized to English** before storing — the memory store
226
226
  a single canonical language. If you supply text in another language the CLI
227
227
  translates it automatically; the stored memory will be in English.
228
228
 
229
+ ### The write gate — what gets refused (LUM-793)
230
+
231
+ Memory is the one agent-writable store that is re-injected into **every** future
232
+ session, so the content checks run at **write** time, not only at injection
233
+ time. Every path — `memory add`, `memory push`, the web form, `PATCH
234
+ /api/memories/<id>`, and the LLM curation / fold passes — goes through the same
235
+ gate. A refusal is a **422** with the matched sentence quoted; nothing is
236
+ stored. Three reasons:
237
+
238
+ | Reason | What trips it |
239
+ | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
240
+ | `INJECTION_SIGNAL` | An imperative aimed at the model, text hidden in a comment, or a long encoded blob. |
241
+ | `AGENT_DIRECTIVE` | A **standing order to future agents** — "from now on you must …", 「以后每次…都要…」 — rather than a description. |
242
+ | `POLICY_CONFLICT` | The text permits what a deterministic, blocking CLAUDE.md rule forbids; the message names the rule id. |
243
+
244
+ What does **not** trip it: ordinary imperative PROCEDURAL steps ("Run `lumo
245
+ verify` when done", "All tests go in `__tests__/`"), or invisible characters /
246
+ `javascript:` links — those are still stored and marked on the row, exactly as
247
+ LUM-783 does for every other source card.
248
+
249
+ **If a write is refused, rewrite the memory as a description of what happens**
250
+ (what the trap is, what the convention is) instead of an instruction to whoever
251
+ reads it next. Do not try to smuggle the same sentence past the gate.
252
+
253
+ `lumo memory rm` on a **TRAP** memory that other tasks were already judged to
254
+ have _used_ records a `USED_TRAP_MEMORY_DELETED` boundary crossing against the
255
+ bound task (it blocks DONE until dispositioned). Correct or retire such a memory
256
+ rather than deleting it; deleting a never-used memory records nothing.
257
+
229
258
  ### Lumo memory vs the harness memory tool
230
259
 
231
260
  Claude Code / the Claude API may expose a file-based **memory tool** (a
@@ -0,0 +1,129 @@
1
+ # Pull requests by number — `lumo pr scan`
2
+
3
+ `lumo pr scan <number>` prints the **security scans of one PR** in the
4
+ workspace, by PR number — whether or not a task is linked to it (LUM-788).
5
+ Until now the only CLI window onto a scan was `lumo task status` (one summary
6
+ line per _linked_ PR, undispositioned findings in Next actions); a PR whose
7
+ title carries no `[LUM-N]` and whose branch is not `lumo/LUM-N-…` was invisible,
8
+ and none of the L3 hunt's stored audit (per-task commands, step trace, stop
9
+ arithmetic, sites, model, diff sizing) had any entry point outside a
10
+ prod-database script.
11
+
12
+ Read-only, no LLM. **Disposition stays web-only** (the LUM-750 CLI path was
13
+ rolled back) — this command reports dispositions, it never records one.
14
+
15
+ ## Usage
16
+
17
+ ```bash
18
+ lumo pr scan 1013 # latest head's scan, prose
19
+ lumo pr scan 1013 --full # + per-task hunt detail
20
+ lumo pr scan 1013 --all # every pushed head, newest first
21
+ lumo pr scan 1013 --json # structured payload (version 1)
22
+ lumo pr scan 1013 --repo Lumo-Workspace/lumo # same number in several repos
23
+ ```
24
+
25
+ | Flag | Notes |
26
+ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
27
+ | `--repo <owner/repo>` | Narrow to one repo. Only needed when the workspace has the same PR number in several repos — the command then exits 1 and lists one `--repo` line per candidate. |
28
+ | `--all` | Every head's scan (one row per pushed sha), newest first. Default = the latest head only. |
29
+ | `--full` | Append the hunt header (plan, diff sizing, gate counts) and the per-task detail: commands (✗ = the gate refused it, with its reason), step trace, `stop:` arithmetic, wrap-up, sites, model. |
30
+ | `--json` | Emit the payload as JSON (`version` field; breaking changes bump it). No prose, no redaction notice line. |
31
+
32
+ The PR is resolved **inside the caller's workspace** (bearer API key or
33
+ browser session). A number that belongs to another workspace is
34
+ indistinguishable from one that never existed — `Error: PR #N not found in
35
+ workspace <slug>` — so the command never leaks whether a PR exists elsewhere.
36
+
37
+ ## Reading the output
38
+
39
+ ```
40
+ PR #1013 · Lumo-Workspace/lumo · merged · task LUM-787
41
+ feat: size the hunt by diff [LUM-787]
42
+ https://github.com/Lumo-Workspace/lumo/pull/1013
43
+
44
+ Scan abcdef0 · FINDINGS · started 2026-09-15T07:00:00.000Z · finished … · trigger PR_EVENT · 61,000 tokens
45
+ Secrets: checked · Code scan: ✗ failed (1 external finding) · Dependencies: checked · AI review: checked · Exploit paths: checked — gitleaks exit 2
46
+ Exploit paths 2/3 done · t1 idor 41.2s done · t2 injection 72.0s done (2 attempts) · t3 race skipped
47
+ Findings: 6 (2 open · 1 blocks DONE · 3 dispositioned · 1 already on main)
48
+ [HIGH] DETERMINISTIC secret:aws-access-key-id — src/cfg.ts:12 — AWS access key id (blocks DONE)
49
+ [LOW] DETERMINISTIC secret:generic-api-key — src/cfg.ts:12 — … — placeholder value, downgraded (FALSE_POSITIVE · human · "doc example")
50
+ [MEDIUM] EXTERNAL_TOOL ext:codeql:js/sql-injection — src/cfg.ts — … (ACCEPTED_RISK · inherited from an earlier scan of this PR)
51
+ [LOW] LLM_JUDGE vuln:xss — src/cfg.ts:12 — … (merged unreviewed · AI suggests FALSE_POSITIVE)
52
+ ```
53
+
54
+ - **Header** — repo, PR state, and `task LUM-N` or `no linked task`.
55
+ - **Scan line** — short head sha, scan status (`PENDING` / `RUNNING` / `CLEAN` / `FINDINGS` / `FAILED`), timestamps, trigger, and the hunt's token spend (`tokens not metered` when the row carries null — null is never printed as 0).
56
+ - **Layer line** — the same five-layer grammar and display labels as `lumo task status` and the GitHub PR summary (`Secrets` · `Code scan` · `Dependencies` · `AI review` · `Exploit paths`; states `checked` / `✗ failed` / `skipped` / `off` / `scanning` / `⚠ incomplete` / `⚠ superseded`). `checked` means the layer completed, not that it found nothing. A trailing `· partial` means a layer hit its cap; `· N left the queue (…)` explains where a merged/closed PR's undispositioned rows went (LUM-775); an external-scanner reason prints only in its scrubbed form.
57
+ - **Coverage line** — `Exploit paths done/total` plus one `t<n> <category> <seconds> <stop>` cell per planned hunt task (LUM-762). Absent when the scan stored no usable audit.
58
+ - **Findings** — every row of the scan, dispositioned ones included, one tag each in parentheses:
59
+ - `blocks DONE` / `advisory` — open (undispositioned, not on main, still in the queue); the split follows the workspace policy.
60
+ - `<DISPOSITION> · human` / `· inherited from an earlier scan of this PR` / `· AI-suggested, human-confirmed` — ruled rows, with the human's note when one was left. A disposition is never an AI verdict on its own: an unconfirmed suggestion shows as `AI suggests <DISPOSITION>` on an open row.
61
+ - `already on main` — the fingerprint is on the default-branch baseline (LUM-738); not introduced by this PR.
62
+ - `resolved on main` / `merged unreviewed` / `PR closed` — the row left the queue when the PR merged or closed (LUM-775).
63
+ - `falsifier INCONCLUSIVE` rides along on hunt rows — the falsifier never says PASS.
64
+ - **No scan on record** prints `⚠ No security scan has run for PR #N — could not confirm it is clean.` — the fail-closed wording of `task status`, never an implicit clean.
65
+ - A `⚠ N suspected credentials were redacted before injection …` line above the read-out means the server masked registry secrets in free text (finding titles, hunt commands, model step text) before it reached your terminal (LUM-784).
66
+
67
+ ### `--full` — the hunt's per-task detail
68
+
69
+ ```
70
+ Hunt: planned by model — two sinks lit · diff 14 source files · 9 sites · task cap 4 · candidates 2 · mechanically rejected 1 · falsifier rejects 1
71
+ t1 done · alibaba/qwen3-coder-plus · 3 sites · 2 cmds (gate refused 1, sandbox failed 0, not found 0, empty 0)
72
+ rg -n queryRaw lib/ [900 chars]
73
+ ✗ curl https://evil.test ← segment is not read-only
74
+ #1 tool=run_shell in=1900 out=300
75
+ stop: submitted · spent 9800/30000 · reserve 4400 · last step 4300 · next 4400 · 3 steps
76
+ wrap-up: not made (submitted)
77
+ t2 steps · alibaba/qwen3-coder-plus · 6 sites · no shell commands
78
+ stop: steps · spent 19000/30000 · reserve 0 · last step ? · 4 steps
79
+ ```
80
+
81
+ Same printer as the lum739 audit report (`shared/src/hunt-audit.ts`), so the
82
+ two read the same. Per task: the header (task id, stop reason, model, sites,
83
+ shell counts — `no shell commands` for a task that never asked the sandbox
84
+ for anything), every command with its output size (`✗` = refused by the gate,
85
+ with the gate's reason), the step trace (`#i tool= in= out= reasoning= elided=
86
+ finish= "text"`), the `stop:` arithmetic (spent/budget, reserve held for the
87
+ wrap-up, last step's cost, the price the next step was charged at, steps),
88
+ and the wrap-up outcome. Fields a row was written without are simply not
89
+ printed — an older scan prints exactly what it recorded. `oversized` on the
90
+ diff line means the PR exceeded the hunt's source-file ceiling and the tasks
91
+ covered a corner of it.
92
+
93
+ ### `--json` shape
94
+
95
+ ```
96
+ { version: 1,
97
+ pullRequest: { repo, number, title, url, state, task: { identifier } | null },
98
+ all: boolean,
99
+ redactedSecrets: number,
100
+ scans: [ { id, headSha, status, trigger, judgeStatus, truncated,
101
+ stages: { secrets?, external?, supplyChain?, judge?, hunt? },
102
+ partial, externalFailureReason, startedAt, finishedAt, tokenSpent,
103
+ counts: { findings, open, blocking, dispositioned, persisting, externalFindings, dependencyFindings },
104
+ queueExits: { RESOLVED_ON_MAIN?, MERGED_UNREVIEWED?, PR_CLOSED? },
105
+ findings: [ { id, kind, ruleId, severity, provenance, filePath, line, title, detail,
106
+ disposition, dispositionSource, dispositionNote, dispositionedAt,
107
+ blocking, open, persisting, queueExit, falsifierVerdict,
108
+ suggestedDisposition, suggestionOutcome, secretDowngrade, cwe, reachability, externalRef } ],
109
+ hunt: { coverage: { total, done, categories[] } | null, plannedBy, plannedReason,
110
+ diff: { sourceFiles, sites, taskCap, oversized } | null,
111
+ candidates, falsifierRejects, mechanicallyRejected, error,
112
+ tasks: [ { taskId, stopped, sites?, model?, shell?, trace?, stop?, wrapUp? } ] } | null } ] }
113
+ ```
114
+
115
+ Finding `evidence` (the redacted triggering line) is **not** in the payload
116
+ by construction — the server never loads the column for this read.
117
+
118
+ ## Errors
119
+
120
+ - `Error: PR #N not found in workspace <slug>` — unknown number, or a PR of another workspace (same message on purpose).
121
+ - `Error: PR #N exists in several repos — pass --repo <owner/repo>` followed by one ready-to-run `lumo pr scan N --repo …` line per candidate.
122
+ - `Error: --repo must be owner/repo` — malformed flag, no request made.
123
+
124
+ ## When to suggest
125
+
126
+ - The user asks what the security scan found on a PR ("what did the scan say on #1013", "why is PR 1013 blocked", "show me the scan") — especially a PR **without** a linked task, which `lumo task status` cannot see.
127
+ - `lumo task status` shows a `PR #N · scan …` line and the user wants the findings behind it, the dispositioned history, or why the hunt layer read `⚠ incomplete` / `N/M done` — `lumo pr scan N` then `--full` for the per-task trace.
128
+ - Auditing the L3 hunt on a real scan (which commands the gate refused, how many steps a task got, which model ran) without a prod-database script.
129
+ - **Not** for changing a finding's standing: disposition is human-only in the web delivery panel. **Not** `lumo task pr show` — that is the task-scoped PR card (title, state, CI, branches) and carries no scan detail.
@@ -123,8 +123,8 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
123
123
 
124
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.
125
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`.
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.
126
+ - **Verification** (`--full`) — the measured rollup `N machine-verified / M human override (of T MACHINE criteria)` plus `X of Y criteria met`; before any round it prints `no verification has run yet`. (Relocation six, whole-branch review third pass: this used to say `X of Y criteria met by their latest verdict` — the `X` count is `isUnmetForDisplay`-based same as everywhere else post-LUM-789, so it counts a tacit-pass HUMAN criterion as met even though that row carries no verdict at all; dropped the verdict claim rather than invent a third vocabulary for "met" alongside the Criteria section's glyphs and the closing line's judged/tacit split.)
127
+ - **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet / `~` a **HUMAN** criterion with no verdict — a **tacit pass**, settled but never individually judged, distinct from both ✓ and the still-open ○) with its checkpointer and latest verdict line (failure tail on fail; a `~` row's line reads `(tacit pass — no objection raised, not individually judged)` instead of `(no verdict yet)`). `REVIEW_ADDED@rN` provenance is visible per row.
128
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.
129
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.
130
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.
@@ -142,7 +142,7 @@ 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[ — <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.
145
+ - **Next actions** — the unmet criteria followed by any **undispositioned PR security findings** (LUM-737) as `• [SECURITY] [SEVERITY] PROVENANCE ruleId — file:line — title (PR #n · blocks DONE | advisory[ — <cause>, downgraded])`. **"Unmet" (LUM-789):** a **MACHINE** criterion is unmet whenever its latest verdict is not a pass — failed or never verified. A **HUMAN** criterion with no verdict yet is **not** unmet — it's a **tacit pass**: no one ever has to hand it off for a human to explicitly PASS it, and it never blocked DONE (the server's DONE gate, `assertNoUnresolvedSendBack`, already only ever refused on an explicit FAIL — never on a merely-unadjudicated criterion, tacit pass is not a new leniency). Only an explicit send-back (a human FAIL) makes a HUMAN criterion unmet, and it stays unmet until re-verified. This is the render rule for what shows up here (`lib/acceptance/unmet-display.ts`) — a separate mechanism (a human submitting a web verdict, which recomputes the task's own status) still requires every selected criterion to carry an explicit PASS before that action moves the task to DONE; the two are not the same gate and must not be conflated. 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 (Finding 2, second whole-branch review):** the closing line is honest about _whether anyone actually judged_ the HUMAN criteria, not a blanket "awaiting human adjudication" — that claim is false the moment even one HUMAN criterion settled as a tacit pass (nothing is being "awaited"), and it's _also_ false once every criterion already carries an explicit verdict (the adjudication already happened, nothing is pending). Four cases: no HUMAN criteria in the contract at all → `All criteria met by their latest verdicts.`; every HUMAN criterion carries an explicit verdict → `All criteria met by their latest verdicts — human-judged.`; none of them do (a fresh, never-adjudicated contract) → `All criteria met — no objection raised on the HUMAN criteria; not individually judged.`; some do and some don't → `All criteria met — N/M HUMAN criteria explicitly judged, the rest accepted as drafted.` Mirrors the web hero banner's own judged/tacit/mixed split (`VerdictDecision.tsx`'s `allPassed` vs the tacit-aware `machineAwaitingHuman` kind) — same rule, two surfaces.
146
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
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.
148
148
  - **Queue exit** (LUM-775): a merged / closed PR's line ends with ` · N left the queue (a resolved on main, b merged unreviewed, c PR closed)` when any undispositioned row has left the queue — derived at read time, never stored. `resolved on main` = an external-tool finding whose fingerprint is absent from a default-branch baseline that can testify (landed, whole, requested after the merge, ran that tool); `merged unreviewed` = an advisory row whose PR merged without a decision (a policy-blocking row never exits this way — it stays a next action and keeps blocking DONE until a human decides, or until a fresh baseline shows it gone); `PR closed` = closed without merging, nothing landed (reopening the PR puts the rows back). Exited rows are not next actions, are not counted in `openFindings`, and show muted with the reason in the web panel. Absent on an older server. The workspace-wide read-out with per-row reasons is `scripts/analysis/lum775-finding-queue/report.ts`.
@@ -49,7 +49,15 @@ async function memoryRm(memoryId, options) {
49
49
  const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
50
50
  const base = (0, api_1.trimTrailingSlash)(apiUrl);
51
51
  const url = `${base}/api/memories/${encodeURIComponent(memoryId)}`;
52
- const headers = { Authorization: `Bearer ${creds.token}` };
52
+ const headers = {
53
+ Authorization: `Bearer ${creds.token}`,
54
+ };
55
+ // LUM-793: the server attributes a used-TRAP deletion to the task this
56
+ // session is attached to — without the header it can prove no binding and
57
+ // records nothing.
58
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
59
+ if (sessionId)
60
+ headers['X-Lumo-Session-Id'] = sessionId;
53
61
  const notFound = `Error: memory ${memoryId} not found — pass the full memory id (cuid) from \`lumo task memory list\` / \`lumo project memory list\`; truncated id prefixes are not resolved`;
54
62
  // LUM-755: without --confirm, fetch the card so the envelope shows what
55
63
  // would be deleted, then stop — no DELETE is sent.