@lumoai/cli 1.35.0 → 1.37.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.
@@ -27,6 +27,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
27
27
  | `task artifact*`, `task figma*` | [references/artifacts-figma.md](references/artifacts-figma.md) |
28
28
  | `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
29
29
  | `verify`, `task status` — machine verification loop, claim-done flow, self-check/resume | [references/verify.md](references/verify.md) |
30
+ | `cost` — per-operation (per-tool) token cost read-out; `task lineage` Top-5 | [references/task-context.md](references/task-context.md) |
30
31
  | `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
31
32
  | `doc*` | [references/docs.md](references/docs.md) |
32
33
  | `sprint*` | [references/sprints.md](references/sprints.md) |
@@ -51,11 +52,11 @@ The command catalog below is a **map**: it lists every command grouped by domain
51
52
  - `lumo task figma context <id> <linkId>` — Figma link metadata (v1)
52
53
  - `lumo task comments list <id>` — comment thread, capped to the output budget (`--full` prints every comment; read-only; ≠ `task comment`)
53
54
  - `lumo task pr show <id> <number>` — synced PR metadata (v1)
54
- - `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome + the run's token/loop cost (read-only audit view); `lumo task lineage <id> --signal` also appends workspace-level usage signal-health (used distribution, per-session variance, used-vs-base merge rate)
55
+ - `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome (each fragment line shows its disclosure tag: `· INDEX pulled` / `· INDEX not-pulled` / `· FULL`) + the run's token/loop cost (read-only audit view); totals include a single-task disclosure-funnel summary (`- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`) and a per-task **"Top operations by token cost"** Top-5 — the most expensive tools by attributed token cost, with a pointer to the full breakdown via `lumo cost --task <id>`; `lumo task lineage <id> --signal` also appends workspace-level usage signal-health (used distribution, per-session variance, used-vs-base merge rate via iteration-taint fold — tasks with a send-back/reopen/PR-close count as the negative class even if later merged; shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet; also prints a raw count of mid-flight `--new-scope` spinoffs (recorded, not yet judged — LUM-511 Phase 3)) and a workspace-wide disclosure funnel in the same shape as the single-task line
55
56
 
56
57
  **Tasks** — see [tasks.md](references/tasks.md)
57
58
 
58
- - `lumo task create <title> [flags]` — create a task
59
+ - `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).
59
60
  - `lumo task update <id> [flags]` — patch status/title/priority/assignee/milestone/sprint/tags
60
61
  - `lumo task list [flags]` — list tasks assigned to you
61
62
  - `lumo next [--count N]` — recommend the next task to work on (read-only)
@@ -79,8 +80,12 @@ The command catalog below is a **map**: it lists every command grouped by domain
79
80
  **Verification (machine acceptance loop)** — see [verify.md](references/verify.md)
80
81
 
81
82
  - `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally, report one structured PASS/FAIL verdict per criterion to the server, print next actions. Defaults to the session-bound task. Round cap 3: an all-pass round moves the task to IN_REVIEW (agent stops there); a round-3 fail escalates to a human (stop retrying). **Run this before claiming a task is done.**
82
- - `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM, milliseconds): the contract with each criterion's latest verdict (REVIEW_ADDED provenance visible), verification history, current round, last round's failure reasons, `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan), and any OPEN (undispositioned) boundary crossings (count + per crossing category/severity/detail + a read-only attribution line `↳ by model=…·agent=…·session=…` naming who/what crossed, `unknown` when unresolved — LUM-469; `--json` adds an `openCrossings` field, each entry carrying an `attribution` object) — read-only awareness, disposition stays web + human-only (LUM-448). The crossings check fails closed (LUM-480): if the read errors, the block prints `⚠ Boundary-crossing check failed` instead of staying silent, and `--json` sets `openCrossings: null` (distinct from `[]` = a successful read with zero open — treat `null` as "could not confirm", not "safe"). Defaults to the session-bound task; `--json` emits a versioned payload (`version` field). **Run it first when resuming a task in a new session or after a verification round was rejected.**
83
- - `lumo verdict [task] --pass | --pass-with-followup | --fail` — acceptance verdicts (LUM-422). `--pass` / `--pass-with-followup` open the browser to the human verdict bar focused on the passing action (a deep link — **records nothing**; a passing data row is only ever a human's own click). `--fail --reason <enum> [--note <text>] [--criterion <id>…]` records an **AGENT send-back** (verifierType=AGENT, verdict hard-coded FAIL) and bounces the task to IN_PROGRESS. Defaults to the session-bound task. **An unresolved send-back (machine/AGENT/human FAIL) blocks the agent/CLI DONE transition with 409** — clear it (re-verify) before `task update --status done`.
83
+ - `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM, milliseconds): the contract with each criterion's latest verdict (REVIEW_ADDED provenance visible), verification history, current round, last round's failure reasons, a per-criterion **send-back lifecycle** line when applicable (`↳ send-back (rN) resolved in rM · PR #K` / `… open` — LUM-511 Phase 5), `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan), and any OPEN (undispositioned) boundary crossings (count + per crossing category/severity/detail + a read-only attribution line `↳ by model=…·agent=…·session=…` naming who/what crossed, `unknown` when unresolved — LUM-469; `--json` adds an `openCrossings` field, each entry carrying an `attribution` object) — read-only awareness, disposition stays web + human-only (LUM-448). The crossings check fails closed (LUM-480): if the read errors, the block prints `⚠ Boundary-crossing check failed` instead of staying silent, and `--json` sets `openCrossings: null` (distinct from `[]` = a successful read with zero open — treat `null` as "could not confirm", not "safe"). Defaults to the session-bound task; `--json` emits a versioned payload (`version` field). **Run it first when resuming a task in a new session or after a verification round was rejected.**
84
+ - `lumo verdict [task] --pass | --fail` — acceptance verdicts (LUM-422). `--pass` opens the browser to the human verdict bar focused on Pass (a deep link — **records nothing**; a passing data row is only ever a human's own click). `--fail --reason <enum> [--note <text>] [--criterion <id>…]` records an **AGENT send-back** (verifierType=AGENT, verdict hard-coded FAIL) and bounces the task to IN_PROGRESS. Defaults to the session-bound task. **An unresolved send-back (machine/AGENT/human FAIL) blocks the agent/CLI DONE transition with 409** — clear it (re-verify) before `task update --status done`.
85
+
86
+ **Cost (per-operation token read-out)** — see [task-context.md](references/task-context.md)
87
+
88
+ - `lumo cost [--task <id>|--session <id>|--since <date>] [--by tool|model|member|session] [--json]` — per-operation (per-tool) token cost read-out. Attributes each model step's token delta to the tool(s) it ran (per-step where POST_TOOL_BATCH data exists, per-turn fallback otherwise), output vs cache_read shown separately, plus a per-step coverage line and a "heuristic" note (parallel tools split a step's tokens evenly). Scope is mutually exclusive: `--task` (one task) / `--session` (one Claude Code session) / `--since` (workspace window); default = workspace last-30-days. `--by` only changes which grouping is the headline (the others are still printed when non-trivial). For the per-task Top-5 inline, see `lumo task lineage`.
84
89
 
85
90
  **Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
86
91
 
@@ -120,13 +125,14 @@ The command catalog below is a **map**: it lists every command grouped by domain
120
125
  **Memory** — see [memory.md](references/memory.md)
121
126
 
122
127
  - `lumo task memory add/list` · `lumo project memory add/list` — record/curate Memory (TASK vs PROJECT)
128
+ - `lumo memory show <id>` — show one memory's full card (category + content) by id (progressive disclosure from a one-line index entry)
123
129
  - `lumo memory promote <id>` / `lumo memory rm <id> --yes` — TASK→PROJECT / delete
124
130
 
125
131
  **Sessions** — see [sessions.md](references/sessions.md)
126
132
 
127
133
  - `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
128
134
  - `lumo session status` — show current binding
129
- - `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt, then a read-only reminder when the bound task has ≥1 OPEN boundary crossing still undispositioned (silent only on a genuine empty read — no wrap-up noise; a crossings-check failure prints a "could not confirm" warning instead of staying silent, LUM-480; pointer is web + human-only, LUM-448). Usage is now also audited automatically when a task reaches DONE (evidence-gated, true-only — confident fragments marked used, the rest left NULL); `session wrap --used` remains the manual override and takes precedence for a session.
135
+ - `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt, then a read-only reminder when the bound task has ≥1 OPEN boundary crossing still undispositioned (silent only on a genuine empty read — no wrap-up noise; a crossings-check failure prints a "could not confirm" warning instead of staying silent, LUM-480; pointer is web + human-only, LUM-448). Usage is now also audited automatically when a task reaches DONE (evidence-gated, true-only — confident fragments marked used, the rest left NULL); `session wrap --used` remains the manual override and takes precedence for a session.
130
136
  - Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
131
137
 
132
138
  **Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
@@ -49,6 +49,19 @@ across all stages: a criterion that already has verification runs is
49
49
  run-free criterion is hard-deleted. Either way, reword rather than delete when
50
50
  possible.
51
51
 
52
+ ### A send-back may mean the contract was wrong — amend it, don't spin off
53
+
54
+ If a send-back reveals the **contract itself** was wrong or incomplete, the right
55
+ move is to **amend the criteria on this task** (`lumo task criteria set <id>`,
56
+ and with `--human` annotate the reason via `--cause NEW_INFO | SCOPE_CHANGE |
57
+ DRAFT_BLIND_SPOT`) and re-verify — **not** to open a new task. Sharpening the
58
+ contract as understanding grows is expected; that's what the drift trail is
59
+ _for_, not something to avoid. **The line:** amending to reflect reality is
60
+ legitimate; **weakening a criterion you were just FAILed on, purely to make it
61
+ pass, is tampering** — it's audited and counts as a failure either way. Deleting
62
+ a failed criterion doesn't help: the DONE gate still blocks on its standing FAIL
63
+ (the run is soft-deleted, the verdict survives).
64
+
52
65
  ## Scale the contract to the task size
53
66
 
54
67
  The 3–7 range is calibrated for typical multi-file tasks. The criterion count
@@ -82,7 +82,7 @@ The `Tags:` line is omitted when no tags were attached.
82
82
  | `--title <text>` | string | New title (cannot be empty). |
83
83
  | `--content <text>` | string | Replace content (inline). |
84
84
  | `--file <path>` | string | Replace content from file. |
85
- | (stdin) | — | Pipe to replace content. |
85
+ | (stdin) | — | Pipe to replace content. Empty / whitespace-only stdin (a non-TTY shell with nothing piped — the common agent case) is treated as **no content channel**, not a body clear (LUM-505). |
86
86
  | `--scope <scope>` | enum | `personal` / `workspace`. |
87
87
  | `--project <ref>` | string | Project name/slug. `--project ""` clears the filing. |
88
88
  | `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
@@ -93,6 +93,8 @@ The `Tags:` line is omitted when no tags were attached.
93
93
  | `--remove-tag-id <cuid>` | string (repeatable) | Detach tag by id. Unknown ids are a no-op (no side effects). Max 20. |
94
94
  | `--allow-shrink` | boolean | Let a body update through even when it drops tables/rows/headings versus the stored body (see structure guard below). |
95
95
 
96
+ A **metadata-only update leaves the body untouched** (LUM-505): when no content channel is supplied (`--title`/`--scope`/`--project`/tag flags only), the body is omitted from the PATCH — it does not get blanked and the structure guard cannot fire. To deliberately clear or replace the body you must supply a content channel explicitly (`--content ""` to clear, which then hits the structure guard as a shrink → pair with `--allow-shrink`).
97
+
96
98
  Optimistic concurrency (LUM-409): `--if-revision <n>` only applies the update if the doc body is still at revision `n` (from `doc show`). Mismatch → 409 conflict, nothing written — re-read, rebase, retry. `--if-revision` alone is not an update (still errors "no fields to update"); same for `--allow-shrink`.
97
99
 
98
100
  **Structure guard (LUM-410), built into the server:** a body update whose new render has **fewer `table` / `tr` / heading elements than the stored body** is rejected with **422** before anything is written — the error names each shrunk category with old→new counts (e.g. `table 1→0, tr 4→0`). This is the `verify-live-doc.ts` reconciliation moved into the write path, so table flattening (LUM-349) and stale-base section loss (#460) fail loudly even when nobody remembers to run the script. When the deletion is intentional (you really are removing a section/table), re-run with `--allow-shrink`. On a 422: don't reach for `--allow-shrink` reflexively — first check whether your edit base is stale (`doc show <doc> --raw`) and rebase. Only markdown-path writes are guarded; web-editor edits and `doc sync` (Google authority) are not.
@@ -26,6 +26,7 @@ lumo project memory add [<project>] --category convention --rule "..." --applies
26
26
  # Omitting --agent records the memory as produced by Claude Code.
27
27
 
28
28
  # Single-memory ops (memoryId from `... memory list` column 1)
29
+ lumo memory show <memoryId> # show one memory's full card by id
29
30
  lumo memory promote <memoryId> # TASK → PROJECT
30
31
  lumo memory rm <memoryId> --yes # hard delete
31
32
  ```
@@ -47,10 +48,29 @@ lumo task memory add LUM-42 --category decision --what "Store doc content as Tip
47
48
  lumo project memory add lumo --category procedural --workflow "Regenerate the CLI grammar snapshot" --trigger "any cli/src/commands change" --step "npx tsx scripts/analysis/lum392-cli-friction/emit-grammar.ts" --step "npx jest scripts/analysis/lum392-cli-friction" --agent claude-code
48
49
 
49
50
  # Curate
51
+ lumo memory show cmpi19iqabc123
50
52
  lumo memory promote cmpi19iqabc123
51
53
  lumo memory rm cmpi19iqabc123 --yes
52
54
  ```
53
55
 
56
+ ### `lumo memory show <id>` (progressive disclosure)
57
+
58
+ Fetches one memory's full card by id from the server and prints its category tag
59
+ (`[TRAP]`, `[DECISION]`, `[CONVENTION]`, `[WORKFLOW]`, …) followed by the
60
+ structured `content` body (pretty-printed JSON).
61
+
62
+ - **Arg**: `<memoryId>` (required) — the id you saw in `... memory list` column 1
63
+ or as a one-line index entry at session start. Without it the command errors
64
+ with a usage line and exits 1.
65
+ - **Errors**: not-logged-in (run `lumo auth login`), `401` invalid/revoked key,
66
+ `404` memory not found in your workspace (cross-workspace ids 404 too), other
67
+ non-2xx → generic HTTP error; all exit 1.
68
+
69
+ **When to suggest**: at session start memories arrive as one-line index entries.
70
+ When a specific entry looks relevant to the task at hand, run
71
+ `lumo memory show <id>` to pull its full body before acting on it — read the
72
+ detail on demand instead of carrying every memory's content in context.
73
+
54
74
  ### Reconcile-on-write & deduplication
55
75
 
56
76
  `memory add` does **not** unconditionally insert a new row. Before writing it:
@@ -125,18 +125,11 @@ lumo session status
125
125
 
126
126
  When to suggest: the user asks "which task am I on", "what's this session bound to", or you need to decide whether to suggest `session attach` for a mentioned task ID.
127
127
 
128
- ### `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — wrap-up panel: progress comment + memory review + fragment-usage vote + blocked-tag prompt
128
+ ### `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — wrap-up panel: memory review + fragment-usage vote + blocked-tag prompt
129
129
 
130
- Session-end wrap-up panel with **four sections, run in order**:
130
+ Session-end wrap-up panel with **three sections, run in order**:
131
131
 
132
- **1. Progress comment** — reads back the current Claude Code session's per-turn
133
- `turnSummary` rows (the one-line summaries written each STOP), aggregates
134
- every turn **since the last progress comment** into one bulleted body, and — after
135
- a `[y] post / [e] edit / [s] skip` confirmation — posts it as a comment on the
136
- session's bound task. A server-side watermark (`Session.lastProgressCommentAt`)
137
- means re-running never re-posts the same turns.
138
-
139
- **2. Memory review** — lists the Layer1 memories this session sedimented since the
132
+ **1. Memory review** — lists the Layer1 memories this session sedimented since the
140
133
  last review (deduped by a per-session watermark `Session.lastMemoryReviewAt`).
141
134
  Each new memory is shown as `[SCOPE] CATEGORY headline`, numbered from 1. You
142
135
  curate with a single line: `d 1,3` deletes rows 1 and 3, `p 2` promotes row 2 to
@@ -147,7 +140,7 @@ Out-of-range indices are ignored. Deletes/promotes run server-side, scoped to
147
140
  memories this session created (you can't touch other sessions' memories through
148
141
  this panel). With no new memories the section prints "(no content)" and does nothing.
149
142
 
150
- **3. Fragment-usage vote (LUM-300)** — lists the context
143
+ **2. Fragment-usage vote (LUM-300)** — lists the context
151
144
  fragments this session **consumed** (its lineage edges: memory / slack / web /
152
145
  figma / PR / review-todo / session), numbered from 1 with a content snippet
153
146
  label. The agent records which it **actually used** via
@@ -161,7 +154,7 @@ upgrades the flywheel signal from "co-loaded" (constant, no information) to
161
154
  fragment's usage-based merge rate, falling back to the weaker presence rate when
162
155
  usage samples are thin. With no consumed fragments the section prints "(no content)".
163
156
 
164
- **4. Blocked check (blocked-tag prompt, LUM-153)** — if the **same kind of failure
157
+ **3. Blocked check (blocked-tag prompt, LUM-153)** — if the **same kind of failure
165
158
  recurred ≥ 3 times** in this session (server-aggregated from
166
159
  `POST_TOOL_USE_FAILURE` events grouped by tool name, plus `STOP_FAILURE`
167
160
  turn-level failures), the section surfaces the dominant failure (`This session looks repeatedly stuck on <tool> (N failures).` + last error summary) and prompts `[y] tag / [s] skip` whether to
@@ -177,7 +170,7 @@ shared board requires an interactive `y`, so `--yes` (and non-TTY) prints the
177
170
  suggestion and moves on rather than silently flipping board state. When there's
178
171
  nothing to prompt, the section prints "(no content)".
179
172
 
180
- **After the panel — open-crossings reminder (LUM-448).** Once the four sections
173
+ **After the panel — open-crossings reminder (LUM-448).** Once the three sections
181
174
  finish, `session wrap` prints a one-shot read-only reminder **if** the bound
182
175
  task has ≥1 OPEN (undispositioned) boundary crossing: `⚠ N open boundary
183
176
  crossing(s) on LUM-N still undispositioned:` then a line per crossing `• [SEVERITY]
@@ -194,31 +187,27 @@ clear its own crossing from the terminal.
194
187
 
195
188
  ```bash
196
189
  lumo session wrap # interactive: preview each section, choose per-section
197
- lumo session wrap --yes # progress posted + memories kept; blocked tag NOT auto-applied (needs interactive y)
190
+ lumo session wrap --yes # memories kept; blocked tag NOT auto-applied (needs interactive y)
198
191
  lumo session wrap --yes --used 1,3 # also record fragments 1 & 3 as used (the rest used=false)
199
192
  lumo session wrap --used none # record that none of the injected fragments were used
200
- lumo session wrap --dry-run # print all drafts only; never posts, never mutates, never advances watermarks
193
+ lumo session wrap --dry-run # print all drafts only; never mutates, never advances watermarks
201
194
  ```
202
195
 
203
196
  The usage vote is a two-step flow for agents: run `lumo session wrap` once to
204
197
  see the numbered fragment list, decide which you actually used, then re-run with
205
198
  `--used <indices>`. Re-running is safe — the other sections are watermark-guarded
206
- (progress won't double-post, reviewed memories won't re-list).
199
+ (reviewed memories won't re-list).
207
200
 
208
201
  - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
209
- task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
210
- summaries, the Progress comment section prints "(no content)" and posts nothing.
211
- - `[e] edit` (Progress comment) opens `$EDITOR` (fallback vi/nano) on the drafted body;
212
- the edited text is posted and the watermark still advances to the turns the
213
- draft covered.
214
- - `--yes` posts the progress comment AND keeps all memories (no
215
- deletes/promotes) while advancing the memory-review watermark; for the
216
- blocked-tag section it prints the suggestion but does **not** apply the tag.
217
- - `--dry-run` prints all drafts; never posts, never mutates memories/tags, never
218
- advances either watermark.
219
- - Non-TTY without `--yes`: prints the drafts and does **not** post, mutate, or
220
- tag (safe default).
221
-
222
- When to suggest: at the end of a working session on a bound task, to record what
223
- was done as a progress comment — offer `lumo session wrap` rather than composing
224
- a `task comment` by hand.
202
+ task (`lumo session attach <LUM-N>` first).
203
+ - `--yes` keeps all memories (no deletes/promotes) while advancing the
204
+ memory-review watermark; for the blocked-tag section it prints the suggestion
205
+ but does **not** apply the tag.
206
+ - `--dry-run` prints all drafts; never mutates memories/tags, never advances the
207
+ memory-review watermark.
208
+ - Non-TTY without `--yes`: prints the drafts and does **not** mutate or tag (safe
209
+ default).
210
+
211
+ When to suggest: at the end of a working session on a bound task, to review the
212
+ memories it sedimented, vote which injected fragments were actually used, and
213
+ flag the task `blocked` if it got repeatedly stuck — offer `lumo session wrap`.
@@ -132,20 +132,38 @@ identifier (`LUM-N`), prints the causal trail:
132
132
 
133
133
  ```bash
134
134
  lumo task lineage LUM-42 # per-session causal trail + cost
135
- lumo task lineage LUM-42 --signal # append workspace-level usage signal-health
135
+ lumo task lineage LUM-42 --signal # append workspace-level usage signal-health; used-vs-base merge rate uses iteration-taint fold (send-back/reopen/PR-close = negative class even if later merged); shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet
136
136
  ```
137
137
 
138
138
  - **Totals banner** — distinct sessions, fragment count, edge count,
139
139
  total tokens (input/output/cache split) and loops, and the outcome
140
- distribution.
140
+ distribution. After the outcome summary, one funnel line is appended:
141
+ `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`
142
+ where impressions = edge count, INDEX% and used% are over impressions,
143
+ pull% is over INDEX only (FULL fragments have no pull opportunity).
144
+ Divide-by-zero is guarded (zero impressions or zero INDEX renders `0%`).
145
+ When per-fragment token weights have been collected (LUM-522), the line also
146
+ appends `· ~T tokens saved` = Σ(fullTokens − indexTokens) over un-pulled INDEX
147
+ edges (the token cost the index-only injection avoided); the suffix is omitted
148
+ cleanly when no edge carries token data yet (older edges predate the columns).
141
149
  - **One block per session** — the group's cost shown **once** (token/loop),
142
150
  the date it consumed context, then each context fragment as
143
- `[OUTCOME] TYPE — <source label>`, plus a per-group outcome summary.
151
+ `[OUTCOME] TYPE — <source label>`, plus a disclosure annotation suffix:
152
+ `· INDEX pulled` (INDEX fragment, `pulledAt` is set) /
153
+ `· INDEX not-pulled` (INDEX fragment, never pulled) /
154
+ `· FULL` (injected in full at session-start).
155
+ Per-group outcome summary follows.
144
156
 
145
157
  Cost is attributed once per session (a session that injected many fragments is
146
158
  not double-counted). Fragment ids are canonical — MEMORY fragments survive
147
159
  consolidation drift.
148
160
 
161
+ **`--signal` workspace funnel:** the workspace-level usage signal-health block
162
+ appended by `--signal` ends with a workspace-wide disclosure funnel in the
163
+ same format: `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`
164
+ aggregated over all edges in the workspace (not just this task) — including the
165
+ same `· ~T tokens saved` suffix when token data exists (LUM-522).
166
+
149
167
  **Cold start:** a task with no edges prints a friendly note (lineage is captured
150
168
  when a session-bound run consumes the task's context), not an error.
151
169
 
@@ -154,3 +172,48 @@ use, and what did it cost" for a task / merged PR — CFO / compliance / trust
154
172
  narratives.
155
173
 
156
174
  Entry point is the task identifier only; PR-number lookup is a future addition.
175
+
176
+ **Top operations by token cost (LUM-523):** the lineage totals also append a
177
+ per-task **"Top operations by token cost"** Top-5 — the most expensive tools by
178
+ attributed token cost (`<tool> — N tokens`), ending with the pointer
179
+ `(full breakdown: lumo cost --task <id>)`. The block is omitted when no
180
+ per-operation cost has been attributed yet.
181
+
182
+ ## `lumo cost`
183
+
184
+ Per-operation (per-tool) token cost read-out. Where `task lineage` answers
185
+ "what context fed this task and what did the run cost," `lumo cost` answers
186
+ "which _operations_ (tools) burned the tokens." It attributes each model step's
187
+ token delta to the tool(s) that ran in it — **per-step** where the
188
+ `POST_TOOL_BATCH` hook captured the tool list, **per-turn fallback** otherwise
189
+ (a parallel-tool step splits its tokens evenly across the tools, hence the
190
+ "heuristic" note). `output` (generation) and `cache_read` (~95%, structural —
191
+ turns × context) are shown in separate columns.
192
+
193
+ ```bash
194
+ lumo cost --task LUM-42
195
+ lumo cost --session <session-id> --by model
196
+ lumo cost --since 2026-06-01 --by member --json
197
+ ```
198
+
199
+ - **Scope (mutually exclusive)** — `--task <id>` scopes to one task,
200
+ `--session <id>` to one Claude Code session, `--since <ISO-date>` to a
201
+ workspace window from that date. With none given, the default is a
202
+ **workspace last-30-days** window. (If more than one is passed, the CLI
203
+ picks task > session > since.)
204
+ - **`--by tool|model|member|session`** (default `tool`) — only changes which
205
+ grouping is the **headline** table; the other groupings are still printed
206
+ below when non-trivial (member/session tables appear only when there is more
207
+ than one). Case-insensitive.
208
+ - **`--json`** — emit the versioned payload (`version: 1`, scope, grandTotal,
209
+ coverage, and `byTool` / `byModel` / `byMember` / `bySession` row arrays)
210
+ instead of the rendered tables.
211
+ - **Coverage line** — `Per-step attribution: X% of N tool-using turns` tells
212
+ you how much of the report is precise per-step attribution vs the per-turn
213
+ fallback. `n/a` when there were no tool-using turns.
214
+
215
+ **When to suggest:** the user asks where their tokens went _by operation_ —
216
+ "which tools are most expensive," cost attribution by model / teammate /
217
+ session, or a workspace cost window. For the quick per-task Top-5 inline, point
218
+ them at `lumo task lineage <id>` instead; reach for `lumo cost` for the full
219
+ breakdown or any non-task scope.
@@ -63,6 +63,18 @@ only).
63
63
  them; the server rejects partial rounds.
64
64
  - Criteria added during review (`REVIEW_ADDED`) appear in the contract and
65
65
  are picked up automatically by the next round.
66
+ - **Session bound to a different task (LUM-459)** → the server returns 409,
67
+ which the command surfaces as an error. No advisory is printed; the verify
68
+ round is rejected outright.
69
+ - **Provably-unbound session** → the server includes `bindingAdvisory: 'unbound'`
70
+ in the round response, and the command prints:
71
+ `⚠ Working unbound — this verify ran from a Claude Code session not attached to the task.`
72
+ The run is recorded as a `SESSION_BINDING_MISSING` boundary crossing visible in
73
+ `lumo task status` open crossings. Run `lumo session attach <LUM-N>` before the
74
+ next verify to bind the session.
75
+ - **Unconfirmed session binding** → `bindingAdvisory: 'unconfirmed'` causes a
76
+ softer advisory: `⚠ Could not confirm this session is attached to the task.`
77
+ Same remediation: `lumo session attach <LUM-N>`.
66
78
 
67
79
  ## Round discipline
68
80
 
@@ -205,17 +217,17 @@ is ever agent-produced.**
205
217
 
206
218
  ```
207
219
  lumo verdict --pass
208
- lumo verdict LUM-42 --pass-with-followup
220
+ lumo verdict LUM-42 --pass
209
221
  lumo verdict --fail --reason CRITERION_UNMET --note "the retry path is still missing"
210
222
  lumo verdict LUM-42 --fail --reason scope_mismatch --criterion c-abc123
211
223
  ```
212
224
 
213
- ### --pass / --pass-with-followup — a deep link, never a write
225
+ ### --pass — a deep link, never a write
214
226
 
215
- These resolve the task, then open the browser to its verdict bar focused on the
216
- passing action. **The CLI writes nothing** — PASS / PASS_WITH_FOLLOWUP only ever
217
- land from a human's own click (Clerk session). Use this to hand a finished task
218
- to a human for the final pass; it carries them one click from recording it.
227
+ This resolves the task, then opens the browser to its verdict bar focused on
228
+ Pass. **The CLI writes nothing** — PASS only ever lands from a human's own click
229
+ (Clerk session). Use this to hand a finished task to a human for the final pass;
230
+ it carries them one click from recording it.
219
231
 
220
232
  ### --fail — the AGENT send-back
221
233
 
@@ -244,3 +256,42 @@ criteria were never adjudicated, transitions freely — the gate only blocks an
244
256
  actual send-back, never an un-adjudicated criterion. When the machine loop has
245
257
  left a task IN_REVIEW with no send-back standing, the agent may move it to DONE
246
258
  directly; a human-PASS row is a provable manual override, not a required ticket.
259
+
260
+ ## When a defect appears — fix in place, don't spin off a new task
261
+
262
+ On a send-back **or** a self-review finding: if the issue falls under any
263
+ existing acceptance criterion of **this** task, fix it in place and re-run
264
+ `lumo verify`. **Do not** `lumo task create` for it. New tasks are only for work
265
+ genuinely _outside_ this task's acceptance contract. Creating a task (and PR)
266
+ for in-scope rework launders a first-attempt failure — it bypasses the DONE
267
+ gate's send-back protection and corrupts the flywheel signal.
268
+
269
+ This is now enforced: when you're mid-task, `lumo task create` refuses the bare
270
+ form and makes you declare intent — `--rework-of <id>` (it redirects you back to
271
+ fix the existing task and creates nothing) or `--new-scope` (genuinely new,
272
+ separate work). If the send-back reveals the **contract itself** was wrong,
273
+ amend it on this task (`lumo task criteria set`) rather than opening a new
274
+ task — see criteria.md.
275
+
276
+ **Hard rule:** while THIS task has an unresolved send-back (any criterion's
277
+ latest verdict is FAIL — the same condition that blocks DONE), `lumo task create`
278
+ is refused with 409 **even with `--new-scope`**. A standing send-back means the
279
+ task can't be completed yet; resolve it (fix + `lumo verify`, or amend the
280
+ contract) before opening any new work. `--rework-of` still redirects you to it.
281
+
282
+ ## Human-reported defects, once a task is submitted
283
+
284
+ When someone reports a defect in conversation, your action depends on whether the
285
+ task has **ever entered IN_REVIEW**:
286
+
287
+ - **Not yet** (still your first working pass) → just fix it and continue. No
288
+ verdict needed — nothing was claimed complete, so there's nothing to contradict.
289
+ - **Already submitted** (entered IN*REVIEW / DONE / merged) → **do not silently
290
+ fix and re-pass.** Either record your own send-back (`lumo verdict --fail`,
291
+ noting it was human-reported — this is \_your* honest concurrence, not a forged
292
+ human verdict), or ask the reporter to record a human FAIL via the web UI /
293
+ Slack (the only channel that can attribute it to a human). If the defect is a
294
+ **new requirement** not covered by any criterion, first transcribe it with
295
+ `lumo task criteria set --human`, then proceed. You can never write a human
296
+ _verdict_ — the terminal can't prove a human is behind the command
297
+ (attribution integrity, not anti-forgery).
@@ -0,0 +1,107 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatOperationCost = formatOperationCost;
4
+ exports.cost = cost;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ /** Deterministic thousands separator (no locale dependency, test-stable). */
8
+ function groupThousands(value) {
9
+ return Math.round(value)
10
+ .toString()
11
+ .replace(/\B(?=(\d{3})+(?!\d))/g, ',');
12
+ }
13
+ function rankTable(title, rows) {
14
+ if (rows.length === 0)
15
+ return '';
16
+ const lines = [
17
+ title,
18
+ ' operation output cache_read total',
19
+ ];
20
+ for (const r of rows.slice(0, 20)) {
21
+ lines.push(` ${r.label.slice(0, 20).padEnd(20)} ${groupThousands(r.output).padStart(10)} ${groupThousands(r.cacheRead).padStart(12)} ${groupThousands(r.total).padStart(12)}`);
22
+ }
23
+ return lines.join('\n');
24
+ }
25
+ /**
26
+ * Render an OperationCostResponse as the per-operation cost report. Pure
27
+ * function (no clock / env / network) so it is deterministic and unit-testable.
28
+ */
29
+ function formatOperationCost(data, by) {
30
+ const primary = by === 'model'
31
+ ? data.byModel
32
+ : by === 'member'
33
+ ? data.byMember
34
+ : by === 'session'
35
+ ? data.bySession
36
+ : data.byTool;
37
+ const pct = data.coverage.perStepPct == null
38
+ ? 'n/a'
39
+ : `${Math.round(data.coverage.perStepPct * 100)}%`;
40
+ const out = [];
41
+ out.push(`Per-operation cost — ${data.scope.kind} ${data.scope.label}`);
42
+ out.push(`Total: output ${groupThousands(data.grandTotal.output)} · cache_read ${groupThousands(data.grandTotal.cacheRead)} · all ${groupThousands(data.grandTotal.total)} tokens`);
43
+ out.push(`Per-step attribution: ${pct} of ${data.coverage.toolTurns} tool-using turns (rest fall back to per-turn split)`);
44
+ out.push('');
45
+ out.push(rankTable(`By ${by}:`, primary));
46
+ if (by !== 'tool')
47
+ out.push('\n' + rankTable('By tool:', data.byTool));
48
+ if (by !== 'model')
49
+ out.push('\n' + rankTable('By model:', data.byModel));
50
+ if (by !== 'member' && data.byMember.length > 1)
51
+ out.push('\n' + rankTable('By member:', data.byMember));
52
+ if (by !== 'session' && data.bySession.length > 1)
53
+ out.push('\n' + rankTable('By session:', data.bySession));
54
+ out.push('');
55
+ out.push('Note: token→tool attribution is heuristic — a model step that fires several tools in parallel splits its tokens evenly across them; cache_read (~95%) is structural (turns × context), shown alongside output (generation).');
56
+ return out.join('\n');
57
+ }
58
+ async function cost(opts) {
59
+ const creds = (0, config_1.readCredentials)();
60
+ if (!creds) {
61
+ console.error('Error: not logged in. Run `lumo auth login` first.');
62
+ return 1;
63
+ }
64
+ const byArg = opts.by?.toLowerCase();
65
+ const by = byArg === 'model' || byArg === 'member' || byArg === 'session'
66
+ ? byArg
67
+ : 'tool';
68
+ const params = new URLSearchParams();
69
+ if (opts.task)
70
+ params.set('task', opts.task);
71
+ else if (opts.session)
72
+ params.set('session', opts.session);
73
+ else if (opts.since)
74
+ params.set('since', opts.since);
75
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
76
+ const qs = params.toString();
77
+ const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/cost${qs ? `?${qs}` : ''}`;
78
+ let res;
79
+ try {
80
+ res = await fetch(url, {
81
+ headers: { Authorization: `Bearer ${creds.token}` },
82
+ });
83
+ }
84
+ catch (err) {
85
+ const msg = err instanceof Error ? err.message : String(err);
86
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
87
+ return 1;
88
+ }
89
+ if (res.status === 401) {
90
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
91
+ return 1;
92
+ }
93
+ if (res.status === 404) {
94
+ console.error(`Error: task ${opts.task ?? ''} not found in workspace ${creds.workspaceSlug}`);
95
+ return 1;
96
+ }
97
+ if (!res.ok) {
98
+ console.error(`Error: server returned HTTP ${res.status}`);
99
+ return 1;
100
+ }
101
+ const data = (await res.json());
102
+ if (opts.json) {
103
+ console.log(JSON.stringify(data, null, 2));
104
+ return;
105
+ }
106
+ console.log(formatOperationCost(data, by));
107
+ }
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.memoryShow = memoryShow;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const sanitize_1 = require("../lib/sanitize");
7
+ const report_pull_1 = require("../lib/report-pull");
8
+ /**
9
+ * `lumo memory show <memory-id>`
10
+ *
11
+ * Progressive disclosure: fetch one memory's full card by id from
12
+ * `/api/memories/:id`. The agent sees memories as one-line index entries at
13
+ * session start; this pulls the body of a specific one on demand. Prints the
14
+ * category tag and the structured content.
15
+ */
16
+ async function memoryShow(id) {
17
+ if (!id) {
18
+ console.error('Error: usage: lumo memory show <memory-id>');
19
+ return 1;
20
+ }
21
+ const creds = (0, config_1.readCredentials)();
22
+ if (!creds) {
23
+ console.error('Error: not logged in. Run `lumo auth login` first.');
24
+ return 1;
25
+ }
26
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
27
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
28
+ let res;
29
+ try {
30
+ res = await fetch(`${base}/api/memories/${encodeURIComponent(id)}`, {
31
+ headers: { Authorization: `Bearer ${creds.token}` },
32
+ });
33
+ }
34
+ catch (err) {
35
+ const msg = err instanceof Error ? err.message : String(err);
36
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
37
+ return 1;
38
+ }
39
+ if (res.status === 401) {
40
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
41
+ return 1;
42
+ }
43
+ if (res.status === 404) {
44
+ console.error(`Error: memory ${id} not found in workspace ${creds.workspaceSlug}`);
45
+ return 1;
46
+ }
47
+ if (!res.ok) {
48
+ console.error(`Error: memory show failed (HTTP ${res.status})`);
49
+ return 1;
50
+ }
51
+ const { memory } = (await res.json());
52
+ console.log((0, sanitize_1.sanitizeField)(`[${memory.category}]`));
53
+ console.log((0, sanitize_1.sanitizeField)(JSON.stringify(memory.content, null, 2)));
54
+ // LUM-500: stamp the disclosure funnel. The arg id == lineage MEMORY
55
+ // fragmentId. Fire-and-forget — never blocks output, swallows failures.
56
+ await (0, report_pull_1.reportPull)({ fragmentType: 'MEMORY', fragmentId: id });
57
+ }
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sessionWrap = sessionWrap;
4
4
  const config_1 = require("../lib/config");
5
5
  const wrap_panel_1 = require("../lib/wrap-panel");
6
- const progress_comment_section_1 = require("./wrap/progress-comment-section");
7
6
  const memory_review_section_1 = require("./wrap/memory-review-section");
8
7
  const fragment_usage_section_1 = require("./wrap/fragment-usage-section");
9
8
  const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
@@ -11,13 +10,12 @@ const crossings_reminder_1 = require("./wrap/crossings-reminder");
11
10
  /**
12
11
  * `lumo session wrap [--yes] [--dry-run]`
13
12
  *
14
- * Session-end wrap-up panel with four sections, run in order: (1) draft a
15
- * progress comment from this session's unposted turnSummaries and post it
16
- * (after y/e/s confirmation) to the bound task; (2) review the Layer1 memories
17
- * this session sedimented keep/delete/promote, deduped by a per-session
18
- * watermark; (3) vote which injected context fragments were actually used
19
- * (LUM-300, via `--used`); (4) if the session repeatedly hit the same failure,
20
- * prompt whether to flag the bound task with a `blocked` tag (LUM-153).
13
+ * Session-end wrap-up panel with three sections, run in order: (1) review the
14
+ * Layer1 memories this session sedimented keep/delete/promote, deduped by a
15
+ * per-session watermark; (2) vote which injected context fragments were
16
+ * actually used (LUM-300, via `--used`); (3) if the session repeatedly hit the
17
+ * same failure, prompt whether to flag the bound task with a `blocked` tag
18
+ * (LUM-153).
21
19
  */
22
20
  async function sessionWrap(options) {
23
21
  const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
@@ -32,7 +30,6 @@ async function sessionWrap(options) {
32
30
  return 1;
33
31
  }
34
32
  const sections = [
35
- new progress_comment_section_1.ProgressCommentSection({ creds, sessionId }),
36
33
  new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
37
34
  new fragment_usage_section_1.FragmentUsageSection({ creds, sessionId, used: options.used }),
38
35
  new blocked_prompt_section_1.BlockedPromptSection({ creds, sessionId }),