@lumoai/cli 1.24.0 → 1.25.1
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 +19 -2
- package/assets/skill/references/tasks.md +22 -0
- package/assets/skill/references/verify.md +52 -0
- package/dist/cli/src/commands/doc-list.js +4 -0
- package/dist/cli/src/commands/memory-promote.js +11 -4
- package/dist/cli/src/commands/memory-rm.js +1 -1
- package/dist/cli/src/commands/task-artifact-add.js +2 -2
- package/dist/cli/src/commands/task-criteria-set.js +2 -2
- package/dist/cli/src/commands/task-status.js +166 -0
- package/dist/cli/src/index.js +12 -1
- package/dist/cli/src/lib/doc-input.js +10 -1
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: lumo
|
|
3
|
-
description: 'Use the Lumo CLI to
|
|
3
|
+
description: 'Use the Lumo CLI to work with Lumo (project management for dev teams) from the terminal: load task context, bind/wrap Claude Code sessions, and create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, dependencies, and team memory — plus acceptance criteria, machine verification (verify / task status), lineage audit, worktree scaffolding, and CLI setup/auth/update. Activate when the user mentions a Lumo task identifier (LUM-N) or the lumo CLI; asks to load task background or bind/check/wrap a session; manages any of the resources above in Lumo; is starting, resuming, or about to claim completion of a task; or asks what to work on next. Key triggers: "LUM-", "lumo", "task context", "session attach", "session wrap", "验收", "verify", "task status", "milestone", "里程碑", "sprint", "冲刺", "文档", "记忆", "memory", "依赖", "deps", "lineage", "worktree", "下一个任务", "what should I work on", "设计稿", "验收标准", "机器验收", "恢复任务".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Prerequisites
|
|
@@ -26,7 +26,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
26
26
|
| `task create/update/list/show/comment`, `next` | [references/tasks.md](references/tasks.md) |
|
|
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
|
-
| `verify` — machine verification loop, claim-done flow
|
|
29
|
+
| `verify`, `task status` — machine verification loop, claim-done flow, 自查/恢复 | [references/verify.md](references/verify.md) |
|
|
30
30
|
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
31
31
|
| `doc*` | [references/docs.md](references/docs.md) |
|
|
32
32
|
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
@@ -79,6 +79,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
79
79
|
**Verification(机器验收循环)** — see [verify.md](references/verify.md)
|
|
80
80
|
|
|
81
81
|
- `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, and `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan). 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.**
|
|
82
83
|
|
|
83
84
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
84
85
|
|
|
@@ -127,6 +128,20 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
127
128
|
- `lumo worktree rm <LUM-N> --yes` — remove a worktree (keeps the branch unless `--delete-branch`)
|
|
128
129
|
- `lumo worktree list` — list `.worktrees/` worktrees (task id, branch, dirty, node_modules link)
|
|
129
130
|
|
|
131
|
+
## Commands & flags that do NOT exist (common mistakes)
|
|
132
|
+
|
|
133
|
+
Measured from real agent sessions (LUM-392) — don't guess these:
|
|
134
|
+
|
|
135
|
+
- No `lumo session start` — binding is `lumo session attach <LUM-N>`
|
|
136
|
+
- No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
|
|
137
|
+
- No `lumo task artifact edit` — it's `lumo task artifact update`
|
|
138
|
+
- No `lumo auth status` — identity check is `lumo whoami`
|
|
139
|
+
- No `--body` on `lumo task comment` — the body is a positional arg: `lumo task comment LUM-N "text"`
|
|
140
|
+
- No `--content` on `task/project memory add` — memory is structured fields (`--category` + per-category flags), see [memory.md](references/memory.md)
|
|
141
|
+
- No global `--verbose` flag on any command
|
|
142
|
+
- `lumo task comments list` (plural) **reads** the thread; `lumo task comment` (singular) **writes** one
|
|
143
|
+
- Status updates: `lumo task update LUM-N --status done` is one direct call — do **not** walk `in_progress → in_review → done` step by step (see [tasks.md](references/tasks.md) for the transition matrix; under the verify flow you shouldn't be setting `in_review`/`done` yourself at all)
|
|
144
|
+
|
|
130
145
|
## Core workflow
|
|
131
146
|
|
|
132
147
|
Typical flow when a user says "help me with LUM-42":
|
|
@@ -138,4 +153,6 @@ Typical flow when a user says "help me with LUM-42":
|
|
|
138
153
|
5. Begin working on the task
|
|
139
154
|
6. **Before claiming the work is done: run `lumo verify`** — the machine half of the acceptance loop. Fix failures and re-run (round cap 3). On all-pass the task moves to IN_REVIEW and you stop; never set DONE yourself after a verify loop — that adjudication is human-only. See [verify.md](references/verify.md)
|
|
140
155
|
|
|
156
|
+
**Status-first recovery:** when you pick a task back up — a new session resuming earlier work, or a task that came back after a rejected verification round / review findings — run `lumo task status` **before** re-reading code or planning. It tells you where the loop stands (current round, what passed, what's unmet and why, any REVIEW_ADDED criteria appended during review) so you don't redo finished work or miss the reason it bounced. See [verify.md](references/verify.md)
|
|
157
|
+
|
|
141
158
|
**Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits and print a suggestion — `检测到 LUM-N … 运行 lumo session attach LUM-N 绑定。` — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
|
|
@@ -95,6 +95,23 @@ Tags: urgent
|
|
|
95
95
|
|
|
96
96
|
The `Tags:` line is omitted when the resulting tag set is empty.
|
|
97
97
|
|
|
98
|
+
### Status transitions — direct moves are legal
|
|
99
|
+
|
|
100
|
+
The server's transition matrix (`lib/task/state-machine.ts`):
|
|
101
|
+
|
|
102
|
+
| From | Allowed targets |
|
|
103
|
+
| ----------- | --------------------------------------------------- |
|
|
104
|
+
| TODO | IN_PROGRESS, IN_REVIEW, DONE |
|
|
105
|
+
| IN_PROGRESS | TODO, IN_REVIEW, DONE |
|
|
106
|
+
| IN_REVIEW | TODO, IN_PROGRESS, DONE |
|
|
107
|
+
| DONE | TODO, IN_PROGRESS (reopen only — **not** IN_REVIEW) |
|
|
108
|
+
|
|
109
|
+
Practical rules:
|
|
110
|
+
|
|
111
|
+
- **One call suffices.** `--status done` straight from TODO or IN_PROGRESS is legal — never walk `in_progress → in_review → done` as a ritual (measured in LUM-392: 70 such chains wasted ~75 calls).
|
|
112
|
+
- **Under the verify flow you don't set `in_review`/`done` at all** — `lumo verify` moves the task to IN_REVIEW on all-pass and the DONE adjudication is human-only.
|
|
113
|
+
- **DONE → IN_REVIEW is rejected (409).** To attach follow-up context to a DONE task, use `lumo task comment` instead of reopening.
|
|
114
|
+
|
|
98
115
|
### When to suggest `task update`
|
|
99
116
|
|
|
100
117
|
- The user describes a state change in natural language (e.g. "mark LUM-48 as in progress", "rename LUM-12 to ...", "assign LUM-30 to me", "bump the priority on LUM-7").
|
|
@@ -237,11 +254,13 @@ DISMISSED
|
|
|
237
254
|
CONFIRMED and SUGGESTED rows show the other task's identifier, title, current status, and source/evidence. DISMISSED rows render as `[shortId] <direction> <identifier> · 已忽略` only — no title, status, or source.
|
|
238
255
|
|
|
239
256
|
When there are no edges at all the output is:
|
|
257
|
+
|
|
240
258
|
```
|
|
241
259
|
Dependencies for LUM-42: 无依赖边。
|
|
242
260
|
```
|
|
243
261
|
|
|
244
262
|
**Evidence fields by detection signal:**
|
|
263
|
+
|
|
245
264
|
- `shared_files` — `shared_files(N 个共享文件: path1, path2, …)` — number of shared write-touched files in the 14-day window, plus up to 5 sample paths.
|
|
246
265
|
- `task_mention` — `task_mention(description)` or `task_mention(comment)` — the surface where the mention appeared.
|
|
247
266
|
|
|
@@ -258,6 +277,7 @@ lumo task deps add LUM-42 --blocked-by LUM-9
|
|
|
258
277
|
Both `<LUM-N>` and `--blocked-by` are required. The command errors on usage if either is missing.
|
|
259
278
|
|
|
260
279
|
**Service semantics (read before using):**
|
|
280
|
+
|
|
261
281
|
- **Self-edge** → 400 ("A task cannot depend on itself").
|
|
262
282
|
- **CONFIRMED edge in the same direction already exists** → 409 ("Dependency already exists").
|
|
263
283
|
- **CONFIRMED edge in the reverse direction already exists** → the cycle guard fires and returns 409 ("Dependency would create a cycle").
|
|
@@ -275,11 +295,13 @@ lumo task deps confirm LUM-42 e5f6a7b8 --reverse # flip direction before confir
|
|
|
275
295
|
```
|
|
276
296
|
|
|
277
297
|
**Edge selector semantics** (shared by `confirm`, `dismiss`, `rm`):
|
|
298
|
+
|
|
278
299
|
- **Other task's identifier** (e.g., `LUM-55`) — case-insensitive exact match against the edge's other-task identifier. Resolves unambiguously when there is exactly one edge to that task.
|
|
279
300
|
- **Edge-id prefix** — at least 6 characters of the short id (e.g., `e5f6a7`). Must match exactly one edge.
|
|
280
301
|
- If zero or more than one edge matches → prints all candidate edges with short ids and exits 1. Retry with a more specific selector.
|
|
281
302
|
|
|
282
303
|
**`--reverse` semantics:**
|
|
304
|
+
|
|
283
305
|
- The detector's direction heuristic is best-effort. If the suggested direction is backwards (e.g., the detector says "LUM-42 blocks LUM-55" but actually LUM-55 blocks LUM-42), confirm with `--reverse` to flip before writing.
|
|
284
306
|
- The service checks that the reversed pair does not already have an edge (→ 409), and re-runs the cycle guard with the flipped direction.
|
|
285
307
|
|
|
@@ -70,3 +70,55 @@ Rounds are a hard budget of 3, not a retry loop. Between rounds, actually fix
|
|
|
70
70
|
the failures — re-running without changes burns a round and (at round 3)
|
|
71
71
|
pages a human. A FAIL round never changes task status; only an all-pass round
|
|
72
72
|
moves it (to IN_REVIEW, never further).
|
|
73
|
+
|
|
74
|
+
## lumo task status — the read half(自查入口)
|
|
75
|
+
|
|
76
|
+
`lumo task status [task] [--json]` is the read-only counterpart of the loop
|
|
77
|
+
(LUM-344): pure read, milliseconds, no LLM, never writes — running it costs
|
|
78
|
+
nothing and burns no round. Defaults to the session-bound task; an explicit
|
|
79
|
+
identifier overrides.
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
lumo task status # session-bound task
|
|
83
|
+
lumo task status LUM-42 # explicit task
|
|
84
|
+
lumo task status --json # versioned machine-readable payload
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### When to run it
|
|
88
|
+
|
|
89
|
+
**Status-first recovery:** run it FIRST — before re-reading code or
|
|
90
|
+
planning — whenever you:
|
|
91
|
+
|
|
92
|
+
- resume a task in a new session (yours or another agent's earlier work);
|
|
93
|
+
- come back after a verification round was rejected (`lumo verify` failed);
|
|
94
|
+
- were told the task bounced in review (REVIEW_ADDED criteria may have been
|
|
95
|
+
appended at the round they surfaced — they show up here automatically).
|
|
96
|
+
|
|
97
|
+
It answers "where does the loop stand": what already passed (don't redo it),
|
|
98
|
+
what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
99
|
+
|
|
100
|
+
### What it prints
|
|
101
|
+
|
|
102
|
+
- Header: task identifier/title/status + `verification round N/3` (round 0 =
|
|
103
|
+
never verified) + an escalation warning when the machine loop is exhausted.
|
|
104
|
+
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN
|
|
105
|
+
statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its
|
|
106
|
+
checkpointer and latest verdict line (evidence pointer on pass, failure
|
|
107
|
+
tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
108
|
+
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
109
|
+
- **Last round failures** — the most recent round's FAIL verdicts with their
|
|
110
|
+
rejection reasons (why the last round bounced).
|
|
111
|
+
- **Next actions** — the unmet criteria (latest verdict is not a pass:
|
|
112
|
+
failed or never verified, HUMAN ones included). This list IS the plan —
|
|
113
|
+
it is recomputed from the event log on every read, never maintained
|
|
114
|
+
separately. Empty + rounds recorded = awaiting human adjudication.
|
|
115
|
+
|
|
116
|
+
### --json contract
|
|
117
|
+
|
|
118
|
+
`--json` emits the full read model with a top-level `version` field
|
|
119
|
+
(currently `1`). The schema is versioned: breaking shape changes bump the
|
|
120
|
+
major; additive fields don't. Pin on `version` when scripting against it.
|
|
121
|
+
|
|
122
|
+
`status` reads; `verify` judges. Running status never starts a round, never
|
|
123
|
+
escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
|
|
124
|
+
all-pass, human-only DONE) live entirely in `lumo verify` and the server.
|
|
@@ -9,6 +9,10 @@ const doc_create_1 = require("./doc-create");
|
|
|
9
9
|
const doc_tree_1 = require("../lib/doc-tree");
|
|
10
10
|
const sanitize_1 = require("../lib/sanitize");
|
|
11
11
|
function visibilityLabel(v) {
|
|
12
|
+
// Some routes (e.g. /api/tasks/<id>/documents) have returned rows without a
|
|
13
|
+
// visibility field at runtime — fall back instead of crashing on .padEnd.
|
|
14
|
+
if (!v)
|
|
15
|
+
return '-';
|
|
12
16
|
if (v === 'PRIVATE')
|
|
13
17
|
return 'PERSONAL';
|
|
14
18
|
return v;
|
|
@@ -20,7 +20,10 @@ async function memoryPromote(memoryId) {
|
|
|
20
20
|
try {
|
|
21
21
|
res = await fetch(`${base}/api/memories/${encodeURIComponent(memoryId)}`, {
|
|
22
22
|
method: 'PATCH',
|
|
23
|
-
headers: {
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: `Bearer ${creds.token}`,
|
|
25
|
+
'Content-Type': 'application/json',
|
|
26
|
+
},
|
|
24
27
|
body: JSON.stringify({ scope: 'PROJECT' }),
|
|
25
28
|
});
|
|
26
29
|
}
|
|
@@ -29,7 +32,7 @@ async function memoryPromote(memoryId) {
|
|
|
29
32
|
return 1;
|
|
30
33
|
}
|
|
31
34
|
if (res.status === 404) {
|
|
32
|
-
console.error(`Error: memory ${memoryId} not found`);
|
|
35
|
+
console.error(`Error: memory ${memoryId} not found — pass the full memory id (cuid) from \`lumo task memory list\` / \`lumo project memory list\`; truncated id prefixes are not resolved`);
|
|
33
36
|
return 1;
|
|
34
37
|
}
|
|
35
38
|
if (res.status === 409) {
|
|
@@ -43,8 +46,12 @@ async function memoryPromote(memoryId) {
|
|
|
43
46
|
if (typeof b.error === 'string')
|
|
44
47
|
m = b.error;
|
|
45
48
|
}
|
|
46
|
-
catch {
|
|
47
|
-
|
|
49
|
+
catch {
|
|
50
|
+
/* */
|
|
51
|
+
}
|
|
52
|
+
console.error(m
|
|
53
|
+
? `Error: ${(0, sanitize_1.sanitizeField)(m)}`
|
|
54
|
+
: `Error: promote failed (HTTP ${res.status})`);
|
|
48
55
|
return 1;
|
|
49
56
|
}
|
|
50
57
|
process.stdout.write(`Promoted ${memoryId} to PROJECT — every agent on this project now sees it.\n` +
|
|
@@ -31,7 +31,7 @@ async function memoryRm(memoryId, options) {
|
|
|
31
31
|
return 1;
|
|
32
32
|
}
|
|
33
33
|
if (res.status === 404) {
|
|
34
|
-
console.error(`Error: memory ${memoryId} not found`);
|
|
34
|
+
console.error(`Error: memory ${memoryId} not found — pass the full memory id (cuid) from \`lumo task memory list\` / \`lumo project memory list\`; truncated id prefixes are not resolved`);
|
|
35
35
|
return 1;
|
|
36
36
|
}
|
|
37
37
|
if (res.status !== 204) {
|
|
@@ -44,7 +44,7 @@ async function taskArtifactAdd(identifier, options) {
|
|
|
44
44
|
const verdict = (0, path_guard_1.checkArtifactFilePath)(options.file);
|
|
45
45
|
if (!verdict.ok) {
|
|
46
46
|
if (verdict.reason === 'unreadable') {
|
|
47
|
-
console.error(`Error:
|
|
47
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(options.file)}`);
|
|
48
48
|
}
|
|
49
49
|
else {
|
|
50
50
|
console.error(`Error: refusing to read ${options.file} — ${verdict.detail}. ` +
|
|
@@ -57,7 +57,7 @@ async function taskArtifactAdd(identifier, options) {
|
|
|
57
57
|
content = await (0, doc_input_1.readFileUtf8)(verdict.resolved);
|
|
58
58
|
}
|
|
59
59
|
catch {
|
|
60
|
-
console.error(`Error:
|
|
60
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(options.file)}`);
|
|
61
61
|
return 1;
|
|
62
62
|
}
|
|
63
63
|
if (content.trim().length === 0) {
|
|
@@ -55,7 +55,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
55
55
|
const verdict = (0, path_guard_1.checkArtifactFilePath)(options.file);
|
|
56
56
|
if (!verdict.ok) {
|
|
57
57
|
if (verdict.reason === 'unreadable') {
|
|
58
|
-
console.error(`Error:
|
|
58
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(options.file)}`);
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
61
61
|
console.error(`Error: refusing to read ${options.file} — ${verdict.detail}. ` +
|
|
@@ -68,7 +68,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
68
68
|
raw = await (0, doc_input_1.readFileUtf8)(verdict.resolved);
|
|
69
69
|
}
|
|
70
70
|
catch {
|
|
71
|
-
console.error(`Error:
|
|
71
|
+
console.error(`Error: ${(0, doc_input_1.unreadableFileMessage)(options.file)}`);
|
|
72
72
|
return 1;
|
|
73
73
|
}
|
|
74
74
|
const parsed = parseCriteriaJson(raw);
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatTaskStatus = formatTaskStatus;
|
|
4
|
+
exports.taskStatus = taskStatus;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
const REASON_TAIL = 400;
|
|
10
|
+
function tail(s, max) {
|
|
11
|
+
return s.length > max ? `…${s.slice(-max)}` : s;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Render the acceptance status as prose. Same row grammar as
|
|
15
|
+
* `criteria list` (`<id> [TYPE] SOURCE@rN statement`) with a verdict
|
|
16
|
+
* glyph in front — ✓ pass, ✗ fail, ○ no verdict yet — so REVIEW_ADDED
|
|
17
|
+
* provenance stays explicitly visible in every row.
|
|
18
|
+
*/
|
|
19
|
+
function formatTaskStatus(data) {
|
|
20
|
+
const lines = [];
|
|
21
|
+
const t = data.task;
|
|
22
|
+
lines.push(`${t.identifier} ${(0, sanitize_1.sanitizeField)(t.title)}`);
|
|
23
|
+
const roundLabel = data.currentRound === 0
|
|
24
|
+
? 'no verification rounds yet'
|
|
25
|
+
: `verification round ${data.currentRound}/${data.maxRounds}`;
|
|
26
|
+
lines.push(`Status: ${t.status} · ${roundLabel}`);
|
|
27
|
+
if (data.escalated) {
|
|
28
|
+
lines.push('⚠ Escalated: the machine loop is exhausted — a human has been paged. Stop retrying lumo verify.');
|
|
29
|
+
}
|
|
30
|
+
if (data.criteria.length === 0) {
|
|
31
|
+
lines.push('');
|
|
32
|
+
lines.push(`No acceptance criteria on ${t.identifier} — draft 3–7 and submit with lumo task criteria set ${t.identifier} --file <criteria.json>`);
|
|
33
|
+
return lines.join('\n') + '\n';
|
|
34
|
+
}
|
|
35
|
+
lines.push('');
|
|
36
|
+
lines.push(`Criteria (${data.criteria.length} total, ${data.nextActions.length} unmet):`);
|
|
37
|
+
for (const c of data.criteria) {
|
|
38
|
+
const glyph = c.latestVerdict == null
|
|
39
|
+
? '○'
|
|
40
|
+
: c.latestVerdict.verdict === 'FAIL'
|
|
41
|
+
? '✗'
|
|
42
|
+
: '✓';
|
|
43
|
+
const provenance = `${c.source}@r${c.addedAtRound}`;
|
|
44
|
+
const evidence = c.evidenceRequired ? ' [evidence]' : '';
|
|
45
|
+
lines.push(` ${glyph} ${c.id} [${c.verifierType}] ${provenance}${evidence} ${(0, sanitize_1.sanitizeField)(c.statement)}`);
|
|
46
|
+
if (c.checkpointer) {
|
|
47
|
+
lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
|
|
48
|
+
}
|
|
49
|
+
const v = c.latestVerdict;
|
|
50
|
+
if (v == null) {
|
|
51
|
+
lines.push(' (no verdict yet)');
|
|
52
|
+
}
|
|
53
|
+
else if (v.verdict === 'FAIL') {
|
|
54
|
+
const why = v.rejectionReason
|
|
55
|
+
? ` — ${(0, sanitize_1.sanitizeField)(tail(v.rejectionReason, REASON_TAIL))}`
|
|
56
|
+
: '';
|
|
57
|
+
lines.push(` ✗ FAIL@r${v.round}${why}`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
const evidencePart = v.evidencePointer
|
|
61
|
+
? ` · ${(0, sanitize_1.sanitizeField)(v.evidencePointer)}`
|
|
62
|
+
: '';
|
|
63
|
+
lines.push(` ✓ ${v.verdict}@r${v.round}${evidencePart}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (data.verificationHistory.length > 0) {
|
|
67
|
+
lines.push('');
|
|
68
|
+
lines.push('History:');
|
|
69
|
+
for (const h of data.verificationHistory) {
|
|
70
|
+
lines.push(` r${h.round} · ${h.recordedAt} · ${h.passed} PASS / ${h.failed} FAIL`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (data.lastRoundFailures.length > 0) {
|
|
74
|
+
lines.push('');
|
|
75
|
+
lines.push(`Last round (r${data.currentRound}) failures:`);
|
|
76
|
+
for (const f of data.lastRoundFailures) {
|
|
77
|
+
lines.push(` • ${(0, sanitize_1.sanitizeField)(f.statement)}`);
|
|
78
|
+
if (f.rejectionReason) {
|
|
79
|
+
lines.push(` why: ${(0, sanitize_1.sanitizeField)(tail(f.rejectionReason, REASON_TAIL))}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
lines.push('');
|
|
84
|
+
if (data.nextActions.length === 0) {
|
|
85
|
+
lines.push(data.currentRound > 0
|
|
86
|
+
? 'All criteria met by their latest verdicts — awaiting human adjudication.'
|
|
87
|
+
: 'Nothing unmet — but no verification has run; run `lumo verify` to judge the contract.');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
lines.push(`Next actions (${data.nextActions.length} unmet):`);
|
|
91
|
+
for (const a of data.nextActions) {
|
|
92
|
+
lines.push(` • [${a.verifierType}] ${(0, sanitize_1.sanitizeField)(a.statement)}`);
|
|
93
|
+
if (a.checkpointer) {
|
|
94
|
+
lines.push(` check: ${(0, sanitize_1.sanitizeField)(a.checkpointer)}`);
|
|
95
|
+
}
|
|
96
|
+
if (a.rejectionReason) {
|
|
97
|
+
lines.push(` why: ${(0, sanitize_1.sanitizeField)(tail(a.rejectionReason, REASON_TAIL))}`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (data.escalated) {
|
|
101
|
+
lines.push('Wait for human direction before touching these.');
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const hasMachine = data.nextActions.some(a => a.verifierType === 'MACHINE');
|
|
105
|
+
const left = data.maxRounds - data.currentRound;
|
|
106
|
+
lines.push(hasMachine
|
|
107
|
+
? `Fix the unmet criteria, then run \`lumo verify\` (${left} round${left === 1 ? '' : 's'} left).`
|
|
108
|
+
: 'Remaining criteria are HUMAN-only — finish the work and hand off for human review.');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return lines.join('\n') + '\n';
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* `lumo task status [task] [--json]` — the agent's read-only self-check
|
|
115
|
+
* (LUM-344): contract + latest verdicts + verification history + next
|
|
116
|
+
* actions, straight from the server's derived read model. Run it first when
|
|
117
|
+
* resuming a task in a new session or after a round was rejected. Defaults
|
|
118
|
+
* to the session-bound task; an explicit identifier overrides.
|
|
119
|
+
*/
|
|
120
|
+
async function taskStatus(identifier, options = {}) {
|
|
121
|
+
const creds = (0, config_1.readCredentials)();
|
|
122
|
+
if (!creds) {
|
|
123
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
124
|
+
return 1;
|
|
125
|
+
}
|
|
126
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
127
|
+
let taskId = identifier;
|
|
128
|
+
if (!taskId) {
|
|
129
|
+
taskId =
|
|
130
|
+
(await (0, resolve_bound_task_1.resolveBoundTaskIdentifier)(base, creds.token)) ?? undefined;
|
|
131
|
+
if (!taskId) {
|
|
132
|
+
console.error('Error: no task given and this session is not bound to one.\n' +
|
|
133
|
+
'Run `lumo task status <LUM-N>`, or bind first with `lumo session attach <LUM-N>`.');
|
|
134
|
+
return 1;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
let res;
|
|
138
|
+
try {
|
|
139
|
+
res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/status`, { headers: { Authorization: `Bearer ${creds.token}` } });
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
143
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
144
|
+
return 1;
|
|
145
|
+
}
|
|
146
|
+
if (res.status === 401) {
|
|
147
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
148
|
+
return 1;
|
|
149
|
+
}
|
|
150
|
+
if (res.status === 404) {
|
|
151
|
+
console.error(`Error: task ${taskId} not found in workspace ${creds.workspaceSlug}`);
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
if (!res.ok) {
|
|
155
|
+
console.error(`Error: task status failed (HTTP ${res.status})`);
|
|
156
|
+
return 1;
|
|
157
|
+
}
|
|
158
|
+
const data = (await res.json());
|
|
159
|
+
if (options.json) {
|
|
160
|
+
// JSON.stringify escapes control chars (…), so the payload is safe
|
|
161
|
+
// to emit raw — and consumers get byte-faithful server data.
|
|
162
|
+
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
process.stdout.write(formatTaskStatus(data));
|
|
166
|
+
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -53,6 +53,7 @@ const task_create_1 = require("./commands/task-create");
|
|
|
53
53
|
const task_update_1 = require("./commands/task-update");
|
|
54
54
|
const task_list_1 = require("./commands/task-list");
|
|
55
55
|
const task_show_1 = require("./commands/task-show");
|
|
56
|
+
const task_status_1 = require("./commands/task-status");
|
|
56
57
|
const task_comment_1 = require("./commands/task-comment");
|
|
57
58
|
const task_figma_add_1 = require("./commands/task-figma-add");
|
|
58
59
|
const task_figma_list_1 = require("./commands/task-figma-list");
|
|
@@ -171,7 +172,12 @@ function wrap(fn) {
|
|
|
171
172
|
const program = new commander_1.Command()
|
|
172
173
|
.name('lumo')
|
|
173
174
|
.description('Lumo CLI — manage tasks and sessions from the terminal')
|
|
174
|
-
.version(pkg.version)
|
|
175
|
+
.version(pkg.version)
|
|
176
|
+
// Make usage errors actionable for agents: suggest near-miss spellings and
|
|
177
|
+
// point at --help instead of dead-ending on "unknown option". Subcommands
|
|
178
|
+
// created via .command() inherit these settings.
|
|
179
|
+
.showSuggestionAfterError(true)
|
|
180
|
+
.showHelpAfterError('(run the command with --help to list its valid flags and arguments)');
|
|
175
181
|
const auth = program.command('auth').description('Manage Lumo authentication');
|
|
176
182
|
auth
|
|
177
183
|
.command('login')
|
|
@@ -249,6 +255,11 @@ task
|
|
|
249
255
|
.command('show <identifier>')
|
|
250
256
|
.description('Show a single task: title, status, priority, assignee, project, URL, description.')
|
|
251
257
|
.action(wrap(identifier => (0, task_show_1.taskShow)(identifier)));
|
|
258
|
+
task
|
|
259
|
+
.command('status [task]')
|
|
260
|
+
.description('Acceptance self-check (LUM-344): contract with latest verdicts, verification history, current round, and nextActions (unmet criteria). Read-only, no LLM. Defaults to the session-bound task. Run it first when resuming a task or after a verification round was rejected.')
|
|
261
|
+
.option('--json', 'Emit the versioned machine-readable payload (version field; breaking changes bump it)')
|
|
262
|
+
.action(wrap((taskArg, options) => (0, task_status_1.taskStatus)(taskArg, options)));
|
|
252
263
|
task
|
|
253
264
|
.command('comment <identifier> <body>')
|
|
254
265
|
.description('Add a comment to a task. Body is plain text — quote it to pass spaces or newlines.')
|
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.unreadableFileMessage = unreadableFileMessage;
|
|
36
37
|
exports.resolveDocContent = resolveDocContent;
|
|
37
38
|
exports.readStdinToString = readStdinToString;
|
|
38
39
|
exports.readFileUtf8 = readFileUtf8;
|
|
@@ -45,6 +46,14 @@ const path_guard_1 = require("./path-guard");
|
|
|
45
46
|
* mutually exclusive. stdin is only consulted when neither flag is set
|
|
46
47
|
* and the shell is non-TTY.
|
|
47
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* Actionable message for an unreadable --file path: agents most often pass a
|
|
51
|
+
* path that doesn't exist relative to the current working directory.
|
|
52
|
+
*/
|
|
53
|
+
function unreadableFileMessage(file) {
|
|
54
|
+
return (`could not read file ${file} (cwd: ${process.cwd()}) — ` +
|
|
55
|
+
`check the path exists; pass a project-local relative path`);
|
|
56
|
+
}
|
|
48
57
|
async function resolveDocContent(args) {
|
|
49
58
|
const hasContent = args.content !== undefined;
|
|
50
59
|
const hasFile = args.file !== undefined && args.file.length > 0;
|
|
@@ -62,7 +71,7 @@ async function resolveDocContent(args) {
|
|
|
62
71
|
return {
|
|
63
72
|
kind: 'error',
|
|
64
73
|
message: check.reason === 'unreadable'
|
|
65
|
-
?
|
|
74
|
+
? unreadableFileMessage(args.file)
|
|
66
75
|
: `refusing to read ${args.file} — ${check.detail}. ` +
|
|
67
76
|
`--file must be a non-sensitive path inside the project directory.`,
|
|
68
77
|
};
|