@lumoai/cli 1.36.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.
- package/assets/skill/SKILL.md +10 -4
- package/assets/skill/references/criteria.md +13 -0
- package/assets/skill/references/docs.md +3 -1
- package/assets/skill/references/memory.md +20 -0
- package/assets/skill/references/task-context.md +65 -2
- package/assets/skill/references/verify.md +45 -6
- package/dist/cli/src/commands/cost.js +107 -0
- package/dist/cli/src/commands/memory-show.js +57 -0
- package/dist/cli/src/commands/task-create.js +23 -6
- package/dist/cli/src/commands/task-figma-context.js +4 -0
- package/dist/cli/src/commands/task-lineage.js +31 -1
- package/dist/cli/src/commands/task-slack-show.js +9 -4
- package/dist/cli/src/commands/task-status.js +12 -0
- package/dist/cli/src/commands/task-web-show.js +7 -2
- package/dist/cli/src/commands/verdict.js +13 -18
- package/dist/cli/src/index.js +18 -2
- package/dist/cli/src/lib/doc-input.js +7 -0
- package/dist/cli/src/lib/hook-runner.js +27 -46
- package/dist/cli/src/lib/report-pull.js +49 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -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 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)
|
|
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 | --
|
|
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,6 +125,7 @@ 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)
|
|
@@ -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:
|
|
@@ -137,15 +137,33 @@ lumo task lineage LUM-42 --signal # append workspace-level usage signal-health
|
|
|
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
|
|
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.
|
|
@@ -217,17 +217,17 @@ is ever agent-produced.**
|
|
|
217
217
|
|
|
218
218
|
```
|
|
219
219
|
lumo verdict --pass
|
|
220
|
-
lumo verdict LUM-42 --pass
|
|
220
|
+
lumo verdict LUM-42 --pass
|
|
221
221
|
lumo verdict --fail --reason CRITERION_UNMET --note "the retry path is still missing"
|
|
222
222
|
lumo verdict LUM-42 --fail --reason scope_mismatch --criterion c-abc123
|
|
223
223
|
```
|
|
224
224
|
|
|
225
|
-
### --pass
|
|
225
|
+
### --pass — a deep link, never a write
|
|
226
226
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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.
|
|
231
231
|
|
|
232
232
|
### --fail — the AGENT send-back
|
|
233
233
|
|
|
@@ -256,3 +256,42 @@ criteria were never adjudicated, transitions freely — the gate only blocks an
|
|
|
256
256
|
actual send-back, never an un-adjudicated criterion. When the machine loop has
|
|
257
257
|
left a task IN_REVIEW with no send-back standing, the agent may move it to DONE
|
|
258
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
|
+
}
|
|
@@ -38,7 +38,12 @@ async function bindTaskToSprint(base, token, workspaceSlug, sprintRef, task) {
|
|
|
38
38
|
throw new Error(`sprint lookup failed (HTTP ${sprintRes.status})`);
|
|
39
39
|
}
|
|
40
40
|
const { sprint: full } = (await sprintRes.json());
|
|
41
|
-
sprint = {
|
|
41
|
+
sprint = {
|
|
42
|
+
id: full.id,
|
|
43
|
+
number: full.number,
|
|
44
|
+
name: full.name,
|
|
45
|
+
teamId: full.teamId,
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
48
|
assertSameTeam(task, sprint);
|
|
44
49
|
const bindRes = await fetch(`${base}/api/sprints/${sprint.id}/tasks`, {
|
|
@@ -137,14 +142,22 @@ async function taskCreate(title, opts) {
|
|
|
137
142
|
body.milestoneRef = opts.milestone;
|
|
138
143
|
if (tagIds && tagIds.length > 0)
|
|
139
144
|
body.tagIds = tagIds;
|
|
145
|
+
if (opts.reworkOf !== undefined)
|
|
146
|
+
body.reworkOfRef = opts.reworkOf;
|
|
147
|
+
if (opts.newScope)
|
|
148
|
+
body.newScope = true;
|
|
149
|
+
const headers = {
|
|
150
|
+
Authorization: `Bearer ${creds.token}`,
|
|
151
|
+
'Content-Type': 'application/json',
|
|
152
|
+
};
|
|
153
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
154
|
+
if (sessionId)
|
|
155
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
140
156
|
let res;
|
|
141
157
|
try {
|
|
142
158
|
res = await fetch(url, {
|
|
143
159
|
method: 'POST',
|
|
144
|
-
headers
|
|
145
|
-
Authorization: `Bearer ${creds.token}`,
|
|
146
|
-
'Content-Type': 'application/json',
|
|
147
|
-
},
|
|
160
|
+
headers,
|
|
148
161
|
body: JSON.stringify(body),
|
|
149
162
|
});
|
|
150
163
|
}
|
|
@@ -165,7 +178,11 @@ async function taskCreate(title, opts) {
|
|
|
165
178
|
if (opts.sprint) {
|
|
166
179
|
const workspaceSlug = creds.workspaceSlug ?? '';
|
|
167
180
|
try {
|
|
168
|
-
const sprint = await bindTaskToSprint(base, creds.token, workspaceSlug, opts.sprint, {
|
|
181
|
+
const sprint = await bindTaskToSprint(base, creds.token, workspaceSlug, opts.sprint, {
|
|
182
|
+
id: data.task.id,
|
|
183
|
+
teamId: data.task.teamId,
|
|
184
|
+
identifier: data.task.identifier,
|
|
185
|
+
});
|
|
169
186
|
process.stdout.write(`Sprint: #${sprint.number} "${(0, sanitize_1.sanitizeField)(sprint.name)}"\n`);
|
|
170
187
|
}
|
|
171
188
|
catch (err) {
|
|
@@ -4,6 +4,7 @@ exports.taskFigmaContext = taskFigmaContext;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task figma context <LUM-N> <link-id>`
|
|
9
10
|
*
|
|
@@ -58,4 +59,7 @@ async function taskFigmaContext(identifier, linkId) {
|
|
|
58
59
|
console.log(`syncError: ${(0, sanitize_1.sanitizeField)(metadata.lastSyncError)}`);
|
|
59
60
|
if (note)
|
|
60
61
|
console.log(`\nnote: ${(0, sanitize_1.sanitizeField)(note)}`);
|
|
62
|
+
// LUM-500: stamp the disclosure funnel. The linkId arg == lineage FIGMA
|
|
63
|
+
// fragmentId. Fire-and-forget — never blocks output, swallows failures.
|
|
64
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'FIGMA', fragmentId: linkId });
|
|
61
65
|
}
|
|
@@ -68,6 +68,10 @@ async function taskLineage(identifier, opts) {
|
|
|
68
68
|
function groupThousands(n) {
|
|
69
69
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
70
70
|
}
|
|
71
|
+
/** Integer percentage, 0 when the denominator is 0 (never NaN). */
|
|
72
|
+
function pct(num, den) {
|
|
73
|
+
return den === 0 ? 0 : Math.round((num / den) * 100);
|
|
74
|
+
}
|
|
71
75
|
const OUTCOME_ORDER = ['MERGED', 'REWORKED', 'REJECTED', 'UNKNOWN'];
|
|
72
76
|
/** "2 MERGED · 1 UNKNOWN", fixed order, zeros omitted. */
|
|
73
77
|
function outcomeSummary(counts) {
|
|
@@ -117,7 +121,21 @@ function formatLineageMarkdown(data) {
|
|
|
117
121
|
const totalsOutcome = outcomeSummary(t.outcomes);
|
|
118
122
|
if (totalsOutcome)
|
|
119
123
|
lines.push(`- Outcomes: ${totalsOutcome}`);
|
|
124
|
+
const f = t.funnel;
|
|
125
|
+
const fSavedSuffix = f.tokensSaved != null ? ` · ~${f.tokensSaved} tokens saved` : '';
|
|
126
|
+
lines.push(`- Disclosure funnel: ${f.impressions} impressions · ` +
|
|
127
|
+
`${f.index} INDEX (${pct(f.index, f.impressions)}%) · ` +
|
|
128
|
+
`${f.pulled} pulled (${pct(f.pulled, f.index)}% of INDEX) · ` +
|
|
129
|
+
`${f.used} used (${pct(f.used, f.impressions)}%)${fSavedSuffix}`);
|
|
120
130
|
lines.push('');
|
|
131
|
+
if (data.totals.topOperations.length > 0) {
|
|
132
|
+
lines.push('');
|
|
133
|
+
lines.push('Top operations by token cost:');
|
|
134
|
+
for (const op of data.totals.topOperations) {
|
|
135
|
+
lines.push(` ${op.tool} — ${op.total.toLocaleString('en-US')} tokens`);
|
|
136
|
+
}
|
|
137
|
+
lines.push(' (full breakdown: lumo cost --task <id>)');
|
|
138
|
+
}
|
|
121
139
|
for (const g of data.groups) {
|
|
122
140
|
lines.push(`## ${(0, sanitize_1.sanitizeField)(g.label)} · ${g.includedAt.slice(0, 10)}`);
|
|
123
141
|
if (g.cost) {
|
|
@@ -130,7 +148,12 @@ function formatLineageMarkdown(data) {
|
|
|
130
148
|
lines.push(`**Fragments** (${g.fragments.length}${summary ? `: ${summary}` : ''}):`);
|
|
131
149
|
lines.push('_✓ used · · abstained · ✗ unused (manual)_');
|
|
132
150
|
for (const f of g.fragments) {
|
|
133
|
-
|
|
151
|
+
const tag = f.disclosure === 'INDEX'
|
|
152
|
+
? f.pulled
|
|
153
|
+
? 'INDEX pulled'
|
|
154
|
+
: 'INDEX not-pulled'
|
|
155
|
+
: 'FULL';
|
|
156
|
+
lines.push(`- ${usageMarker(f.used)} [${f.outcome}] ${f.fragmentType} — ${(0, sanitize_1.sanitizeField)(f.sourceLabel)} · ${tag}`);
|
|
134
157
|
}
|
|
135
158
|
lines.push('');
|
|
136
159
|
}
|
|
@@ -153,5 +176,12 @@ function formatSignalHealth(h) {
|
|
|
153
176
|
else {
|
|
154
177
|
lines.push('- Used × outcome: insufficient resolved tasks');
|
|
155
178
|
}
|
|
179
|
+
lines.push(`- Spinoffs during in-flight work: ${h.spinoffsDuringInFlight} (recorded, not yet judged)`);
|
|
180
|
+
const f = h.disclosureFunnel;
|
|
181
|
+
const fSavedSuffix = f.tokensSaved != null ? ` · ~${f.tokensSaved} tokens saved` : '';
|
|
182
|
+
lines.push(`- Disclosure funnel: ${f.impressions} impressions · ` +
|
|
183
|
+
`${f.index} INDEX (${pct(f.index, f.impressions)}%) · ` +
|
|
184
|
+
`${f.pulled} pulled (${pct(f.pulled, f.index)}% of INDEX) · ` +
|
|
185
|
+
`${f.used} used (${pct(f.used, f.impressions)}%)${fSavedSuffix}`);
|
|
156
186
|
return lines.join('\n');
|
|
157
187
|
}
|
|
@@ -4,6 +4,7 @@ exports.taskSlackShow = taskSlackShow;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task slack show <LUM-N> <context-id>`
|
|
9
10
|
*
|
|
@@ -50,10 +51,14 @@ async function taskSlackShow(identifier, contextId) {
|
|
|
50
51
|
const messages = snapshot?.messages ?? [];
|
|
51
52
|
if (messages.length === 0) {
|
|
52
53
|
console.log('(no messages in stored snapshot)');
|
|
53
|
-
return;
|
|
54
54
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
else {
|
|
56
|
+
for (const m of messages) {
|
|
57
|
+
const author = (0, sanitize_1.sanitizeField)(m.userName ?? '@' + m.userId);
|
|
58
|
+
console.log(`${author}: ${(0, sanitize_1.sanitizeField)(m.text)}`);
|
|
59
|
+
}
|
|
58
60
|
}
|
|
61
|
+
// LUM-500: stamp the disclosure funnel. The contextId arg == lineage
|
|
62
|
+
// SLACK_CONTEXT fragmentId. Fire-and-forget — never blocks, swallows failures.
|
|
63
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'SLACK_CONTEXT', fragmentId: contextId });
|
|
59
64
|
}
|
|
@@ -104,6 +104,18 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
104
104
|
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
// LUM-511 Phase 5: send-back lifecycle (was this criterion's send-back
|
|
108
|
+
// resolved, and by which PR).
|
|
109
|
+
const sb = c.sendBackResolution;
|
|
110
|
+
if (sb) {
|
|
111
|
+
if (sb.status === 'resolved') {
|
|
112
|
+
const pr = sb.closingPrNumber ? ` · PR #${sb.closingPrNumber}` : '';
|
|
113
|
+
lines.push(` ↳ send-back (r${sb.failedAtRound}) resolved in r${sb.resolvedAtRound}${pr}`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(` ↳ send-back (r${sb.failedAtRound}) open`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
107
119
|
}
|
|
108
120
|
if (data.verificationHistory.length > 0) {
|
|
109
121
|
lines.push('');
|
|
@@ -4,6 +4,7 @@ exports.taskWebShow = taskWebShow;
|
|
|
4
4
|
const config_1 = require("../lib/config");
|
|
5
5
|
const api_1 = require("../lib/api");
|
|
6
6
|
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
const report_pull_1 = require("../lib/report-pull");
|
|
7
8
|
/**
|
|
8
9
|
* `lumo task web show <LUM-N> <link-id>`
|
|
9
10
|
*
|
|
@@ -58,7 +59,11 @@ async function taskWebShow(identifier, linkId) {
|
|
|
58
59
|
const { body } = (await res.json());
|
|
59
60
|
if (!body || body.trim().length === 0) {
|
|
60
61
|
console.log('(empty body)');
|
|
61
|
-
return;
|
|
62
62
|
}
|
|
63
|
-
|
|
63
|
+
else {
|
|
64
|
+
console.log((0, sanitize_1.sanitizeField)(body));
|
|
65
|
+
}
|
|
66
|
+
// LUM-500: stamp the disclosure funnel. The linkId arg == lineage WEB_LINK
|
|
67
|
+
// fragmentId. Fire-and-forget — never blocks output, swallows failures.
|
|
68
|
+
await (0, report_pull_1.reportPull)({ fragmentType: 'WEB_LINK', fragmentId: linkId });
|
|
64
69
|
}
|
|
@@ -25,10 +25,10 @@ function collectCriterion(value, prev = []) {
|
|
|
25
25
|
/**
|
|
26
26
|
* `lumo verdict [task]` — human + agent acceptance verdicts (LUM-422).
|
|
27
27
|
*
|
|
28
|
-
*
|
|
29
|
-
* --pass
|
|
30
|
-
*
|
|
31
|
-
*
|
|
28
|
+
* Two modes, exactly one required:
|
|
29
|
+
* --pass opens the browser to the task's verdict bar, focused on Pass (a deep
|
|
30
|
+
* link — writes NOTHING; a passing data row is only ever produced by a
|
|
31
|
+
* human's own click, red line).
|
|
32
32
|
* --fail --reason <enum> [--note] [--criterion …] records an AGENT send-back
|
|
33
33
|
* (verdict hard-coded FAIL server-side) and bounces the task to
|
|
34
34
|
* IN_PROGRESS. Bearer-authed.
|
|
@@ -36,13 +36,9 @@ function collectCriterion(value, prev = []) {
|
|
|
36
36
|
* Defaults to the session-bound task; an explicit identifier overrides.
|
|
37
37
|
*/
|
|
38
38
|
async function verdict(identifier, options = {}) {
|
|
39
|
-
const modes = [
|
|
40
|
-
options.pass && 'pass',
|
|
41
|
-
options.passWithFollowup && 'pass-with-followup',
|
|
42
|
-
options.fail && 'fail',
|
|
43
|
-
].filter(Boolean);
|
|
39
|
+
const modes = [options.pass && 'pass', options.fail && 'fail'].filter(Boolean);
|
|
44
40
|
if (modes.length === 0) {
|
|
45
|
-
console.error('Error: choose a verdict mode — --pass
|
|
41
|
+
console.error('Error: choose a verdict mode — --pass or --fail.');
|
|
46
42
|
return 1;
|
|
47
43
|
}
|
|
48
44
|
if (modes.length > 1) {
|
|
@@ -90,14 +86,14 @@ async function verdict(identifier, options = {}) {
|
|
|
90
86
|
if (options.fail) {
|
|
91
87
|
return failVerdict(base, headers, taskId, options, creds.workspaceSlug);
|
|
92
88
|
}
|
|
93
|
-
return passDeepLink(base, headers, taskId,
|
|
89
|
+
return passDeepLink(base, headers, taskId, creds.workspaceSlug);
|
|
94
90
|
}
|
|
95
91
|
/**
|
|
96
|
-
* --pass
|
|
97
|
-
*
|
|
98
|
-
*
|
|
92
|
+
* --pass: open the human's verdict bar pre-focused on Pass. The CLI never writes
|
|
93
|
+
* the verdict — it only carries the human to the one click that does (red line:
|
|
94
|
+
* no agent-produced passing row).
|
|
99
95
|
*/
|
|
100
|
-
async function passDeepLink(base, headers, taskId,
|
|
96
|
+
async function passDeepLink(base, headers, taskId, workspaceSlug) {
|
|
101
97
|
let res;
|
|
102
98
|
try {
|
|
103
99
|
res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(taskId)}`, { headers });
|
|
@@ -125,9 +121,8 @@ async function passDeepLink(base, headers, taskId, verdictParam, workspaceSlug)
|
|
|
125
121
|
return 1;
|
|
126
122
|
}
|
|
127
123
|
const sep = task.url.includes('?') ? '&' : '?';
|
|
128
|
-
const deepLink = `${task.url}${sep}verdict
|
|
129
|
-
|
|
130
|
-
process.stdout.write(`Opening ${taskId} for a human "${label}" verdict (nothing is recorded until they click):\n` +
|
|
124
|
+
const deepLink = `${task.url}${sep}verdict=pass`;
|
|
125
|
+
process.stdout.write(`Opening ${taskId} for a human "Pass" verdict (nothing is recorded until they click):\n` +
|
|
131
126
|
` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
|
|
132
127
|
(0, browser_1.openBrowser)(deepLink);
|
|
133
128
|
return;
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,6 +47,7 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
const session_status_1 = require("./commands/session-status");
|
|
48
48
|
const session_wrap_1 = require("./commands/session-wrap");
|
|
49
49
|
const next_1 = require("./commands/next");
|
|
50
|
+
const cost_1 = require("./commands/cost");
|
|
50
51
|
const verify_1 = require("./commands/verify");
|
|
51
52
|
const verdict_1 = require("./commands/verdict");
|
|
52
53
|
const task_context_1 = require("./commands/task-context");
|
|
@@ -66,6 +67,7 @@ const memory_project_list_1 = require("./commands/memory-project-list");
|
|
|
66
67
|
const memory_project_add_1 = require("./commands/memory-project-add");
|
|
67
68
|
const memory_promote_1 = require("./commands/memory-promote");
|
|
68
69
|
const memory_rm_1 = require("./commands/memory-rm");
|
|
70
|
+
const memory_show_1 = require("./commands/memory-show");
|
|
69
71
|
const task_artifact_add_1 = require("./commands/task-artifact-add");
|
|
70
72
|
const task_criteria_set_1 = require("./commands/task-criteria-set");
|
|
71
73
|
const task_criteria_list_1 = require("./commands/task-criteria-list");
|
|
@@ -221,9 +223,8 @@ program
|
|
|
221
223
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
222
224
|
program
|
|
223
225
|
.command('verdict [task]')
|
|
224
|
-
.description('Acceptance verdict (LUM-422). --pass
|
|
226
|
+
.description('Acceptance verdict (LUM-422). --pass opens the browser to the human verdict bar focused on Pass (a deep link — records nothing; a passing row is only ever a human click). --fail --reason <enum> records an AGENT send-back and bounces the task to IN_PROGRESS. Defaults to the session-bound task.')
|
|
225
227
|
.option('--pass', 'Open the verdict bar focused on Pass (human one-click; no write)')
|
|
226
|
-
.option('--pass-with-followup', 'Open the verdict bar focused on Pass with follow-up (human one-click; no write)')
|
|
227
228
|
.option('--fail', 'Record an AGENT send-back (verdict FAIL) — requires --reason')
|
|
228
229
|
.option('--reason <enum>', 'Rejection reason for --fail: CRITERION_UNMET | EVIDENCE_INSUFFICIENT | CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER (case-insensitive)')
|
|
229
230
|
.option('--note <text>', 'Optional send-back narrative, posted as a task comment')
|
|
@@ -234,6 +235,15 @@ program
|
|
|
234
235
|
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|
|
235
236
|
.option('-n, --count <N>', 'Number of tasks to recommend (default 3)')
|
|
236
237
|
.action(wrap(options => (0, next_1.nextCommand)(options)));
|
|
238
|
+
program
|
|
239
|
+
.command('cost')
|
|
240
|
+
.description('Show per-operation (per-tool) token cost. Defaults to a workspace 30-day window; scope with --task / --session')
|
|
241
|
+
.option('--task <id>', 'Aggregate a single task (e.g. LUM-42)')
|
|
242
|
+
.option('--session <id>', 'Aggregate a single Claude Code session')
|
|
243
|
+
.option('--since <date>', 'Workspace window lower bound (ISO date); default last 30 days')
|
|
244
|
+
.option('--by <dim>', 'Headline grouping: tool | model | member | session (case-insensitive; default tool)')
|
|
245
|
+
.option('--json', 'Emit the versioned payload as JSON')
|
|
246
|
+
.action(wrap(options => (0, cost_1.cost)(options)));
|
|
237
247
|
const session = program
|
|
238
248
|
.command('session')
|
|
239
249
|
.description('Manage per-terminal coding-session context');
|
|
@@ -291,6 +301,8 @@ task
|
|
|
291
301
|
.option('--tag <name>', 'Attach tag by name (repeatable)', collect, [])
|
|
292
302
|
.option('--tag-id <cuid>', 'Attach tag by id (repeatable)', collect, [])
|
|
293
303
|
.option('--sprint <ref>', 'Sprint number or UUID to add the task to after creation')
|
|
304
|
+
.option('--rework-of <id>', 'Declare this is rework of an existing task (redirects you to fix it; creates nothing)')
|
|
305
|
+
.option('--new-scope', 'Declare this is genuinely new work, outside your current task’s scope')
|
|
294
306
|
.action(wrap((title, options) => (0, task_create_1.taskCreate)(title, options)));
|
|
295
307
|
const taskFigma = task
|
|
296
308
|
.command('figma')
|
|
@@ -489,6 +501,10 @@ projectMemory
|
|
|
489
501
|
const memoryCmd = program
|
|
490
502
|
.command('memory')
|
|
491
503
|
.description('Operate on a single memory by id (see `lumo task memory` / `lumo project memory` to list/add)');
|
|
504
|
+
memoryCmd
|
|
505
|
+
.command('show <memoryId>')
|
|
506
|
+
.description("Show one memory's full card by id (category + content). Use to pull the body of a memory you saw as a one-line index entry at session start.")
|
|
507
|
+
.action(wrap((id) => (0, memory_show_1.memoryShow)(id)));
|
|
492
508
|
memoryCmd
|
|
493
509
|
.command('promote <memoryId>')
|
|
494
510
|
.description('Promote a TASK memory to PROJECT scope. Only when the lesson recurs across 2+ tasks.')
|
|
@@ -81,6 +81,13 @@ async function resolveDocContent(args) {
|
|
|
81
81
|
}
|
|
82
82
|
if (!args.stdinIsTTY) {
|
|
83
83
|
const text = await args.readStdin();
|
|
84
|
+
// A non-TTY shell with nothing piped (the common agent/CI case) yields an
|
|
85
|
+
// empty read. Treat empty/whitespace-only stdin as "no content channel"
|
|
86
|
+
// so a title-only `doc update` doesn't ship an empty body and trip the
|
|
87
|
+
// LUM-410 structure guard / blank the document (LUM-505). To clear a body
|
|
88
|
+
// deliberately, pass an explicit `--content ""` (handled above).
|
|
89
|
+
if (text.trim().length === 0)
|
|
90
|
+
return { kind: 'none' };
|
|
84
91
|
return { kind: 'ok', markdown: text };
|
|
85
92
|
}
|
|
86
93
|
return { kind: 'none' };
|
|
@@ -12,7 +12,6 @@ const hook_log_1 = require("./hook-log");
|
|
|
12
12
|
const sanitize_1 = require("./sanitize");
|
|
13
13
|
const agent_1 = require("./agent");
|
|
14
14
|
const git_task_1 = require("./git-task");
|
|
15
|
-
const format_1 = require("./format");
|
|
16
15
|
const transcript_usage_1 = require("./transcript-usage");
|
|
17
16
|
/**
|
|
18
17
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
@@ -72,7 +71,11 @@ function readStdin() {
|
|
|
72
71
|
* The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
|
|
73
72
|
* runtime injects additionalContext into the conversation automatically.
|
|
74
73
|
*/
|
|
75
|
-
function formatHookStdoutLines(path, responseBody,
|
|
74
|
+
function formatHookStdoutLines(path, responseBody,
|
|
75
|
+
// Retained for signature stability (callers/tests still pass it). LUM-500
|
|
76
|
+
// removed the only time-dependent rendering (the recovery card), so it is no
|
|
77
|
+
// longer read.
|
|
78
|
+
_now = new Date()) {
|
|
76
79
|
if (path === 'pre-tool-use') {
|
|
77
80
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
78
81
|
return [];
|
|
@@ -107,20 +110,27 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
107
110
|
else if (tb && tb.bound === false) {
|
|
108
111
|
lines.push(unboundPromptLine(sessionId));
|
|
109
112
|
}
|
|
110
|
-
//
|
|
111
|
-
// todos share one additionalContext block so Claude Code injects a
|
|
112
|
-
// coherent context payload at session start. The
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
|
|
113
|
+
// Blocker warning + criteria + progress + memory + linked resources +
|
|
114
|
+
// PR-review todos share one additionalContext block so Claude Code injects a
|
|
115
|
+
// single coherent context payload at session start. The dependency blocker
|
|
116
|
+
// warning (LUM-172) slots in first so it stays prominent — it can preempt the
|
|
117
|
+
// session's work entirely. Then the progressive-disclosure tiers (LUM-500):
|
|
118
|
+
// Tier-0 acceptance contract → Tier-1 prior-session progress → Tier-2 memory
|
|
119
|
+
// index → linked resources.
|
|
120
|
+
//
|
|
121
|
+
// LUM-500: the prior-session recovery is now the server-rendered Tier-1
|
|
122
|
+
// `progressSection`, which REPLACES the CLI-rendered recovery card so progress
|
|
123
|
+
// isn't injected twice. `body.previousSession` is still sent (it drives the
|
|
124
|
+
// server's lineage/metrics) but is no longer rendered here.
|
|
116
125
|
const envelope = sessionContextEnvelope([
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
// work entirely (wait for the blocker instead of starting) — LUM-172.
|
|
126
|
+
// Blocker warning first: it can preempt the session's work entirely (wait
|
|
127
|
+
// for the blocker instead of starting) — LUM-172.
|
|
120
128
|
body.blockerWarningSection,
|
|
121
|
-
//
|
|
122
|
-
//
|
|
129
|
+
// Tier-0 acceptance contract: what the session's work is judged against
|
|
130
|
+
// (LUM-342).
|
|
123
131
|
body.criteriaSection,
|
|
132
|
+
// Tier-1 prior-session progress (LUM-500), server-rendered.
|
|
133
|
+
body.progressSection,
|
|
124
134
|
body.memorySection,
|
|
125
135
|
body.linkedResourcesSection,
|
|
126
136
|
body.reviewTodosSection,
|
|
@@ -137,37 +147,6 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
|
|
|
137
147
|
function unboundPromptLine(sessionId) {
|
|
138
148
|
return `[Lumo] session_id=${sessionId} | No task bound. Tell me the task you want to work on (e.g. LUM-42), or say "skip".`;
|
|
139
149
|
}
|
|
140
|
-
const MAX_UNRESOLVED = 5;
|
|
141
|
-
/**
|
|
142
|
-
* Render the "resuming previous session" recovery card from the structured previousSession
|
|
143
|
-
* payload. Returns undefined when there's nothing to show (null payload or an
|
|
144
|
-
* empty headline). Free text (headline / unresolved) is sanitized here — it's
|
|
145
|
-
* LLM-generated and routed to Claude Code stdout. unresolved is capped at
|
|
146
|
-
* MAX_UNRESOLVED with a "+M more" pointer to `lumo task context`.
|
|
147
|
-
*/
|
|
148
|
-
function renderRecoveryCard(prev, taskIdentifier, now) {
|
|
149
|
-
if (!prev || typeof prev.headline !== 'string' || prev.headline === '') {
|
|
150
|
-
return undefined;
|
|
151
|
-
}
|
|
152
|
-
const ago = (0, format_1.relativeTime)(new Date(prev.lastActivityAt), now);
|
|
153
|
-
const dur = (0, format_1.formatDuration)(prev.durationMs);
|
|
154
|
-
const lines = [
|
|
155
|
-
`## Resuming previous session (${ago} · ${dur})`,
|
|
156
|
-
`Last stopped at: ${(0, sanitize_1.sanitizeField)(prev.headline)}`,
|
|
157
|
-
];
|
|
158
|
-
const unresolved = Array.isArray(prev.unresolved) ? prev.unresolved : [];
|
|
159
|
-
if (unresolved.length > 0) {
|
|
160
|
-
lines.push('Unfinished:');
|
|
161
|
-
const shown = unresolved.slice(0, MAX_UNRESOLVED);
|
|
162
|
-
for (const u of shown)
|
|
163
|
-
lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
|
|
164
|
-
const extra = unresolved.length - shown.length;
|
|
165
|
-
if (extra > 0) {
|
|
166
|
-
lines.push(`- … (+${extra} more — run \`lumo task context ${taskIdentifier}\` for the full list)`);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return lines.join('\n');
|
|
170
|
-
}
|
|
171
150
|
/**
|
|
172
151
|
* Wrap any non-empty context parts into a single SessionStart
|
|
173
152
|
* hookSpecificOutput envelope so Claude Code injects one coherent
|
|
@@ -204,7 +183,9 @@ function formatSuggestLine(sessionId, match) {
|
|
|
204
183
|
* detect-and-suggest, never auto-bind). No match falls back to the generic
|
|
205
184
|
* unbound prompt.
|
|
206
185
|
*/
|
|
207
|
-
function resolveSessionStartStdout(responseBody, deps,
|
|
186
|
+
function resolveSessionStartStdout(responseBody, deps,
|
|
187
|
+
// Retained for signature stability; no longer read (see formatHookStdoutLines).
|
|
188
|
+
_now = new Date()) {
|
|
208
189
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
209
190
|
return [];
|
|
210
191
|
const body = responseBody;
|
|
@@ -213,7 +194,7 @@ function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
|
213
194
|
const sessionId = body.sessionId;
|
|
214
195
|
const tb = body.taskBinding;
|
|
215
196
|
if (tb && tb.bound === true) {
|
|
216
|
-
return formatHookStdoutLines('session-start', responseBody
|
|
197
|
+
return formatHookStdoutLines('session-start', responseBody);
|
|
217
198
|
}
|
|
218
199
|
if (!tb || tb.bound !== false)
|
|
219
200
|
return [];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.reportPull = reportPull;
|
|
4
|
+
const config_1 = require("./config");
|
|
5
|
+
const api_1 = require("./api");
|
|
6
|
+
/**
|
|
7
|
+
* Fire-and-forget telemetry for progressive disclosure (LUM-500).
|
|
8
|
+
*
|
|
9
|
+
* When an agent pulls a fragment's body via a `lumo … show <id>` command, this
|
|
10
|
+
* reports the pull to the server, which stamps `pulledAt` on the session-start
|
|
11
|
+
* lineage edge — the middle of the disclosure funnel (saw → pulled → used).
|
|
12
|
+
*
|
|
13
|
+
* Contract:
|
|
14
|
+
* - NO-OP silently when there is no bound session (nothing is sent). The bound
|
|
15
|
+
* session id is `CLAUDE_CODE_SESSION_ID` — the same source other commands use
|
|
16
|
+
* for `X-Lumo-Session-Id` provenance.
|
|
17
|
+
* - Fire-and-forget: never blocks the command's main output and never surfaces
|
|
18
|
+
* an error. All failures (no creds, network, non-ok) are swallowed. Returns
|
|
19
|
+
* void so callers can `await` it without affecting their exit code.
|
|
20
|
+
*
|
|
21
|
+
* `fragmentId` must be the DB row id the lineage edge stored — callers are
|
|
22
|
+
* responsible for passing the id that matches (see each command's wiring).
|
|
23
|
+
*/
|
|
24
|
+
async function reportPull(ref) {
|
|
25
|
+
try {
|
|
26
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
27
|
+
if (!sessionId)
|
|
28
|
+
return;
|
|
29
|
+
const creds = (0, config_1.readCredentials)();
|
|
30
|
+
if (!creds)
|
|
31
|
+
return;
|
|
32
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
33
|
+
await fetch(`${base}/api/lineage/pulls`, {
|
|
34
|
+
method: 'POST',
|
|
35
|
+
headers: {
|
|
36
|
+
Authorization: `Bearer ${creds.token}`,
|
|
37
|
+
'Content-Type': 'application/json',
|
|
38
|
+
},
|
|
39
|
+
body: JSON.stringify({
|
|
40
|
+
sessionId,
|
|
41
|
+
fragmentType: ref.fragmentType,
|
|
42
|
+
fragmentId: ref.fragmentId,
|
|
43
|
+
}),
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// fire-and-forget: never surface telemetry failures
|
|
48
|
+
}
|
|
49
|
+
}
|