@lumoai/cli 1.37.0 → 1.39.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 +2 -1
- package/assets/skill/references/task-context.md +5 -2
- package/dist/cli/src/commands/cost.js +8 -4
- package/dist/cli/src/commands/crossing-explain.js +93 -0
- package/dist/cli/src/index.js +9 -0
- package/dist/cli/src/lib/hook-runner.js +12 -4
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -82,10 +82,11 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
82
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.**
|
|
83
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
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
|
+
- `lumo crossing explain <id> --note "<text>"` — append an agent self-explanation ("申辩") to a boundary crossing (LUM-542). The **inverse** of dispositioning: this is the agent/CLI path (bearer-only; a clerk/human caller is refused), but it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only, LUM-448). The note is shown to the human reviewer at disposition time and kept for later review, explicitly labeled _agent self-report · unverified_. Scope: `<id>` must be a crossing on the **session-bound task** (resolved from `$CLAUDE_CODE_SESSION_ID`; cross-task targets and unbound/mismatched sessions are rejected). Earlier explanations are immutable — a correction is a new note. **When to suggest:** after `lumo task status` (or the next pre-tool-use hook's one-time reminder) surfaces an OPEN crossing you believe is a false positive or want to leave a rationale for — record it here; it's a review aid, not a way to self-clear the gate.
|
|
85
86
|
|
|
86
87
|
**Cost (per-operation token read-out)** — see [task-context.md](references/task-context.md)
|
|
87
88
|
|
|
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)
|
|
89
|
+
- `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); each ranking row breaks the cost into output / input / cache_create / cache_read columns plus total, with a per-step coverage line and a "heuristic" note (parallel tools split a step's tokens evenly; output and cache_read attribute cleanly per step while input and cache_create are bursty, so per-tool figures for those two are coarse). 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`.
|
|
89
90
|
|
|
90
91
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
91
92
|
|
|
@@ -187,8 +187,11 @@ Per-operation (per-tool) token cost read-out. Where `task lineage` answers
|
|
|
187
187
|
token delta to the tool(s) that ran in it — **per-step** where the
|
|
188
188
|
`POST_TOOL_BATCH` hook captured the tool list, **per-turn fallback** otherwise
|
|
189
189
|
(a parallel-tool step splits its tokens evenly across the tools, hence the
|
|
190
|
-
"heuristic" note).
|
|
191
|
-
|
|
190
|
+
"heuristic" note). Each ranking row breaks the cost into four columns —
|
|
191
|
+
`output` (generation), `input`, `cache_create`, `cache_read` (~95%, structural —
|
|
192
|
+
turns × context) — plus `total`. `output` and `cache_read` attribute cleanly
|
|
193
|
+
per step; `input` and `cache_create` are bursty (prompt + cache checkpoints), so
|
|
194
|
+
their per-tool figures are coarser.
|
|
192
195
|
|
|
193
196
|
```bash
|
|
194
197
|
lumo cost --task LUM-42
|
|
@@ -10,15 +10,19 @@ function groupThousands(value) {
|
|
|
10
10
|
.toString()
|
|
11
11
|
.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
12
12
|
}
|
|
13
|
+
/** Render one ranking row: label + the four token categories + total, aligned. */
|
|
14
|
+
function rankRow(label, output, input, cacheCreate, cacheRead, total) {
|
|
15
|
+
return ` ${label.slice(0, 20).padEnd(20)} ${output.padStart(10)} ${input.padStart(10)} ${cacheCreate.padStart(12)} ${cacheRead.padStart(12)} ${total.padStart(12)}`;
|
|
16
|
+
}
|
|
13
17
|
function rankTable(title, rows) {
|
|
14
18
|
if (rows.length === 0)
|
|
15
19
|
return '';
|
|
16
20
|
const lines = [
|
|
17
21
|
title,
|
|
18
|
-
'
|
|
22
|
+
rankRow('operation', 'output', 'input', 'cache_create', 'cache_read', 'total'),
|
|
19
23
|
];
|
|
20
24
|
for (const r of rows.slice(0, 20)) {
|
|
21
|
-
lines.push(
|
|
25
|
+
lines.push(rankRow(r.label, groupThousands(r.output), groupThousands(r.input), groupThousands(r.cacheCreation), groupThousands(r.cacheRead), groupThousands(r.total)));
|
|
22
26
|
}
|
|
23
27
|
return lines.join('\n');
|
|
24
28
|
}
|
|
@@ -39,7 +43,7 @@ function formatOperationCost(data, by) {
|
|
|
39
43
|
: `${Math.round(data.coverage.perStepPct * 100)}%`;
|
|
40
44
|
const out = [];
|
|
41
45
|
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`);
|
|
46
|
+
out.push(`Total: output ${groupThousands(data.grandTotal.output)} · input ${groupThousands(data.grandTotal.input)} · cache_create ${groupThousands(data.grandTotal.cacheCreation)} · cache_read ${groupThousands(data.grandTotal.cacheRead)} · all ${groupThousands(data.grandTotal.total)} tokens`);
|
|
43
47
|
out.push(`Per-step attribution: ${pct} of ${data.coverage.toolTurns} tool-using turns (rest fall back to per-turn split)`);
|
|
44
48
|
out.push('');
|
|
45
49
|
out.push(rankTable(`By ${by}:`, primary));
|
|
@@ -52,7 +56,7 @@ function formatOperationCost(data, by) {
|
|
|
52
56
|
if (by !== 'session' && data.bySession.length > 1)
|
|
53
57
|
out.push('\n' + rankTable('By session:', data.bySession));
|
|
54
58
|
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
|
|
59
|
+
out.push('Note: token→tool attribution is heuristic — a model step that fires several tools in parallel splits its tokens evenly across them. output (generation) and cache_read (~95%, structural — turns × context) attribute cleanly per step; input and cache_create are bursty (prompt + cache checkpoints) and per-tool figures for those two are coarse.');
|
|
56
60
|
return out.join('\n');
|
|
57
61
|
}
|
|
58
62
|
async function cost(opts) {
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.crossingExplain = crossingExplain;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
/**
|
|
8
|
+
* `lumo crossing explain <id> --note "…"` — append an agent self-explanation
|
|
9
|
+
* ("申辩") to a boundary crossing (LUM-542).
|
|
10
|
+
*
|
|
11
|
+
* This is the AGENT side of the boundary-crossing review loop and the deliberate
|
|
12
|
+
* inverse of dispositioning: it can only ADD an append-only note for the human
|
|
13
|
+
* reviewer to weigh — it never clears the crossing or unblocks Done (a human
|
|
14
|
+
* dispositions that, in the web acceptance panel). The crossing must belong to
|
|
15
|
+
* the task this session is bound to; the binding is how the target task is
|
|
16
|
+
* resolved, so run it inside a session attached via `lumo session attach`.
|
|
17
|
+
*/
|
|
18
|
+
async function crossingExplain(crossingId, options = {}) {
|
|
19
|
+
if (!crossingId || crossingId.trim() === '') {
|
|
20
|
+
console.error('Error: a crossing id is required: lumo crossing explain <id> --note "…"');
|
|
21
|
+
return 1;
|
|
22
|
+
}
|
|
23
|
+
const note = options.note?.trim();
|
|
24
|
+
if (!note) {
|
|
25
|
+
console.error('Error: --note "<text>" is required (the explanation to record).');
|
|
26
|
+
return 1;
|
|
27
|
+
}
|
|
28
|
+
const creds = (0, config_1.readCredentials)();
|
|
29
|
+
if (!creds) {
|
|
30
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
31
|
+
return 1;
|
|
32
|
+
}
|
|
33
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
34
|
+
const headers = {
|
|
35
|
+
Authorization: `Bearer ${creds.token}`,
|
|
36
|
+
};
|
|
37
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
38
|
+
if (sessionId)
|
|
39
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
40
|
+
// The crossing is addressed by id, but the route is task-scoped — resolve the
|
|
41
|
+
// bound task from the session so the server can verify the crossing is on it.
|
|
42
|
+
if (!sessionId) {
|
|
43
|
+
console.error('Error: $CLAUDE_CODE_SESSION_ID is not set — run inside a session bound via `lumo session attach <LUM-N>`.');
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
let bound;
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
|
|
49
|
+
bound = res.ok
|
|
50
|
+
? (await res.json())
|
|
51
|
+
: null;
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
55
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
if (!bound?.taskIdentifier) {
|
|
59
|
+
console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first.');
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
const taskId = bound.taskIdentifier;
|
|
63
|
+
let res;
|
|
64
|
+
try {
|
|
65
|
+
res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/boundary-crossings/${encodeURIComponent(crossingId)}/explain`, {
|
|
66
|
+
method: 'POST',
|
|
67
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
68
|
+
body: JSON.stringify({ note }),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
if (res.status === 401) {
|
|
77
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const errBody = (await res.json().catch(() => null));
|
|
82
|
+
const detail = errBody && typeof errBody.error === 'string'
|
|
83
|
+
? (0, sanitize_1.sanitizeField)(errBody.error)
|
|
84
|
+
: '';
|
|
85
|
+
console.error(`Error: explanation rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
const outcome = (await res.json());
|
|
89
|
+
process.stdout.write(`✓ Recorded an explanation on crossing ${(0, sanitize_1.sanitizeField)(outcome.crossingId)}.\n` +
|
|
90
|
+
' This is an append-only note for the human reviewer — it does not clear ' +
|
|
91
|
+
'the crossing or unblock Done.\n');
|
|
92
|
+
return;
|
|
93
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -50,6 +50,7 @@ const next_1 = require("./commands/next");
|
|
|
50
50
|
const cost_1 = require("./commands/cost");
|
|
51
51
|
const verify_1 = require("./commands/verify");
|
|
52
52
|
const verdict_1 = require("./commands/verdict");
|
|
53
|
+
const crossing_explain_1 = require("./commands/crossing-explain");
|
|
53
54
|
const task_context_1 = require("./commands/task-context");
|
|
54
55
|
const task_create_1 = require("./commands/task-create");
|
|
55
56
|
const task_update_1 = require("./commands/task-update");
|
|
@@ -230,6 +231,14 @@ program
|
|
|
230
231
|
.option('--note <text>', 'Optional send-back narrative, posted as a task comment')
|
|
231
232
|
.option('--criterion <id>', 'Narrow a --fail to specific criteria (repeatable); omitted = the whole contract', verdict_1.collectCriterion)
|
|
232
233
|
.action(wrap((task, options) => (0, verdict_1.verdict)(task, options)));
|
|
234
|
+
const crossing = program
|
|
235
|
+
.command('crossing')
|
|
236
|
+
.description('Inspect and annotate boundary crossings');
|
|
237
|
+
crossing
|
|
238
|
+
.command('explain <id>')
|
|
239
|
+
.description('Append an agent self-explanation ("申辩") to a boundary crossing (LUM-542). Append-only and for the human reviewer — it never clears the crossing or unblocks Done (a human dispositions that). Targets a crossing on the session-bound task.')
|
|
240
|
+
.requiredOption('--note <text>', 'The explanation to record (the rationale for the action / why it may be a false positive)')
|
|
241
|
+
.action(wrap((id, options) => (0, crossing_explain_1.crossingExplain)(id, options)));
|
|
233
242
|
program
|
|
234
243
|
.command('next')
|
|
235
244
|
.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`.')
|
|
@@ -79,9 +79,17 @@ _now = new Date()) {
|
|
|
79
79
|
if (path === 'pre-tool-use') {
|
|
80
80
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
81
81
|
return [];
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
const body = responseBody;
|
|
83
|
+
// Two independent PreToolUse signals (LUM-150 collision, LUM-542 crossing
|
|
84
|
+
// reminder) share one additionalContext block when both are present.
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (typeof body.collisionWarning === 'string' &&
|
|
87
|
+
body.collisionWarning !== '')
|
|
88
|
+
parts.push(body.collisionWarning);
|
|
89
|
+
if (typeof body.crossingReminder === 'string' &&
|
|
90
|
+
body.crossingReminder !== '')
|
|
91
|
+
parts.push(body.crossingReminder);
|
|
92
|
+
if (parts.length === 0)
|
|
85
93
|
return [];
|
|
86
94
|
return [
|
|
87
95
|
JSON.stringify({
|
|
@@ -89,7 +97,7 @@ _now = new Date()) {
|
|
|
89
97
|
hookEventName: 'PreToolUse',
|
|
90
98
|
// Server-built text routed back to stdout — sanitize untrusted free
|
|
91
99
|
// text before Claude Code consumes it (ANSI/control-char injection).
|
|
92
|
-
additionalContext: (0, sanitize_1.sanitizeField)(
|
|
100
|
+
additionalContext: (0, sanitize_1.sanitizeField)(parts.join('\n\n')),
|
|
93
101
|
},
|
|
94
102
|
}),
|
|
95
103
|
];
|