@lumoai/cli 1.38.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
CHANGED
|
@@ -82,6 +82,7 @@ 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
|
|
|
@@ -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
|
];
|