@lumoai/cli 1.25.0 → 1.26.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 +21 -7
- package/assets/skill/references/artifacts-figma.md +1 -1
- package/assets/skill/references/criteria.md +10 -9
- package/assets/skill/references/docs.md +5 -5
- package/assets/skill/references/milestones.md +6 -6
- package/assets/skill/references/sessions.md +30 -30
- package/assets/skill/references/sprints.md +6 -6
- package/assets/skill/references/task-context.md +5 -5
- package/assets/skill/references/tasks.md +37 -15
- package/assets/skill/references/verify.md +3 -3
- 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/milestone-show.js +1 -1
- package/dist/cli/src/commands/next.js +1 -1
- package/dist/cli/src/commands/session-attach.js +5 -4
- package/dist/cli/src/commands/task-artifact-add.js +2 -2
- package/dist/cli/src/commands/task-criteria-set.js +3 -3
- package/dist/cli/src/commands/task-deps.js +12 -12
- package/dist/cli/src/commands/verify.js +1 -1
- package/dist/cli/src/commands/wrap/blocked-prompt-section.js +9 -9
- package/dist/cli/src/commands/wrap/fragment-usage-section.js +6 -6
- package/dist/cli/src/commands/wrap/memory-review-section.js +6 -6
- package/dist/cli/src/commands/wrap/progress-comment-section.js +11 -11
- package/dist/cli/src/index.js +7 -2
- package/dist/cli/src/lib/doc-input.js +10 -1
- package/dist/cli/src/lib/hook-runner.js +9 -9
- package/dist/cli/src/lib/wrap-panel.js +1 -1
- package/package.json +1 -1
|
@@ -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").
|
|
@@ -182,7 +199,7 @@ Top 3 recommended tasks (of 12 open):
|
|
|
182
199
|
Next: lumo session attach LUM-42 && lumo task context LUM-42
|
|
183
200
|
```
|
|
184
201
|
|
|
185
|
-
When to suggest: the user asks "what should I work on", "what's next", "
|
|
202
|
+
When to suggest: the user asks "what should I work on", "what's next", "recommend my next task" (in any language), "pick my next task", or starts a session without a task in mind. After they choose, run `session attach` + `task context` for the picked task.
|
|
186
203
|
|
|
187
204
|
### `lumo task show <identifier>` — print one task's detail
|
|
188
205
|
|
|
@@ -210,7 +227,7 @@ The CLI does not support @-mention chip syntax. If the user wants to ping someon
|
|
|
210
227
|
|
|
211
228
|
### `lumo task deps list <LUM-N>` — show all dependency edges
|
|
212
229
|
|
|
213
|
-
Prints the task's dependency edges grouped into three sections: **CONFIRMED**, **SUGGESTED(
|
|
230
|
+
Prints the task's dependency edges grouped into three sections: **CONFIRMED**, **SUGGESTED (pending confirmation)**, and **DISMISSED**. Each row includes a short 8-character edge id in square brackets, the direction (`blocked by` / `blocks`), the other task's identifier and title, the other task's current status, the source (`MANUAL` or `DETECTED`), and inline evidence for detected edges.
|
|
214
231
|
|
|
215
232
|
```bash
|
|
216
233
|
lumo task deps list LUM-42
|
|
@@ -222,27 +239,29 @@ Example output:
|
|
|
222
239
|
Dependencies for LUM-42 (3)
|
|
223
240
|
|
|
224
241
|
CONFIRMED
|
|
225
|
-
[a1b2c3d4] blocked by LUM-9
|
|
242
|
+
[a1b2c3d4] blocked by LUM-9 "Fix auth token expiry" IN_PROGRESS · MANUAL
|
|
226
243
|
|
|
227
|
-
SUGGESTED(
|
|
228
|
-
[e5f6a7b8] blocks LUM-55
|
|
229
|
-
|
|
230
|
-
[c9d0e1f2] blocked by LUM-38
|
|
231
|
-
|
|
244
|
+
SUGGESTED (pending confirmation)
|
|
245
|
+
[e5f6a7b8] blocks LUM-55 "Migrate DB schema" TODO · shared_files(4 shared files: src/db/schema.ts, src/db/migrate.ts, ...)
|
|
246
|
+
confirm: lumo task deps confirm LUM-42 e5f6a7b8 (add --reverse if direction is flipped; false positive: dismiss)
|
|
247
|
+
[c9d0e1f2] blocked by LUM-38 "Add OAuth scopes" IN_REVIEW · task_mention(description)
|
|
248
|
+
confirm: lumo task deps confirm LUM-42 c9d0e1f2 (add --reverse if direction is flipped; false positive: dismiss)
|
|
232
249
|
|
|
233
250
|
DISMISSED
|
|
234
|
-
[b3c4d5e6] blocks LUM-12 ·
|
|
251
|
+
[b3c4d5e6] blocks LUM-12 · dismissed
|
|
235
252
|
```
|
|
236
253
|
|
|
237
|
-
CONFIRMED and SUGGESTED rows show the other task's identifier, title, current status, and source/evidence. DISMISSED rows render as `[shortId] <direction> <identifier> ·
|
|
254
|
+
CONFIRMED and SUGGESTED rows show the other task's identifier, title, current status, and source/evidence. DISMISSED rows render as `[shortId] <direction> <identifier> · dismissed` only — no title, status, or source.
|
|
238
255
|
|
|
239
256
|
When there are no edges at all the output is:
|
|
257
|
+
|
|
240
258
|
```
|
|
241
|
-
Dependencies for LUM-42:
|
|
259
|
+
Dependencies for LUM-42: no dependency edges.
|
|
242
260
|
```
|
|
243
261
|
|
|
244
262
|
**Evidence fields by detection signal:**
|
|
245
|
-
|
|
263
|
+
|
|
264
|
+
- `shared_files` — `shared_files(N shared files: 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
|
|
|
248
267
|
CONFIRMED rows also show `source`: `MANUAL` (user-declared via `deps add`) or `DETECTED` (auto-found then confirmed via `deps confirm`).
|
|
@@ -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
|
|
|
@@ -292,7 +314,7 @@ lumo task deps dismiss LUM-42 e5f6a7b8
|
|
|
292
314
|
lumo task deps dismiss LUM-42 LUM-38
|
|
293
315
|
```
|
|
294
316
|
|
|
295
|
-
Output: `Dismissed: [e5f6a7b8] LUM-38
|
|
317
|
+
Output: `Dismissed: [e5f6a7b8] LUM-38 "Add OAuth scopes" (won't be suggested again)`
|
|
296
318
|
|
|
297
319
|
Use `dismiss` for false positives. Use `rm` only when you want the pair to be eligible for re-detection in the future (the detection service can re-suggest pairs with no existing row).
|
|
298
320
|
|
|
@@ -330,6 +352,6 @@ Output: `Removed [a1b2c3d4] from LUM-42`
|
|
|
330
352
|
|
|
331
353
|
- **After `session attach` output shows a blocker warning or candidate-count hint** → run `lumo task deps list <LUM-N>` to review the full edge list, then `confirm` or `dismiss` each SUGGESTED candidate.
|
|
332
354
|
- **User says "X needs to wait for Y" or "LUM-42 is blocked by LUM-9"** → run `lumo task deps add LUM-42 --blocked-by LUM-9`.
|
|
333
|
-
- **Agent sees a `## ⚠
|
|
334
|
-
- **Agent sees only a standalone hint line
|
|
355
|
+
- **Agent sees a `## ⚠ Dependency alerts` block (form A — live blockers) at session-start** → evaluate whether to wait for the blocker to merge before starting work; if the edge is stale or wrong, clean it with `deps rm` or `deps dismiss`.
|
|
356
|
+
- **Agent sees only a standalone hint line `Detected N candidate dependencies awaiting confirmation…` (form B — no live blockers)** → no immediate blocker, but run `lumo task deps list <LUM-N>` to review and confirm/dismiss SUGGESTED candidates. See [sessions.md](sessions.md) for the full alert format.
|
|
335
357
|
- **User reports a false positive dependency suggestion** → `lumo task deps dismiss <LUM-N> <edge>` to permanently suppress it for this pair.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
# lumo verify — machine verification loop
|
|
1
|
+
# lumo verify — machine verification loop
|
|
2
2
|
|
|
3
3
|
`lumo verify` is the machine half of the acceptance system (Acceptance v1,
|
|
4
4
|
LUM-343). It executes every **MACHINE** criterion's checkpointer in the local
|
|
5
5
|
repo, reports one structured PASS/FAIL verdict per criterion to the server,
|
|
6
6
|
and prints what to do next. The judge lives server-side: round numbering, the
|
|
7
7
|
3-round cap, escalation, and the IN_REVIEW transition all happen there
|
|
8
|
-
(
|
|
8
|
+
(execution on the client, adjudication on the server).
|
|
9
9
|
|
|
10
10
|
## The claim-done rule
|
|
11
11
|
|
|
@@ -71,7 +71,7 @@ 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
73
|
|
|
74
|
-
## lumo task status — the read half
|
|
74
|
+
## lumo task status — the read half (self-check entry point)
|
|
75
75
|
|
|
76
76
|
`lumo task status [task] [--json]` is the read-only counterpart of the loop
|
|
77
77
|
(LUM-344): pure read, milliseconds, no LLM, never writes — running it costs
|
|
@@ -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) {
|
|
@@ -34,7 +34,7 @@ function sprintCoverageLines(coverage) {
|
|
|
34
34
|
return ` ${num} ${status} ${name} ${s.doneCount}/${s.taskCount} done`;
|
|
35
35
|
});
|
|
36
36
|
if (coverage.unsprinted > 0) {
|
|
37
|
-
const label = '
|
|
37
|
+
const label = 'Unscheduled'.padEnd(numW + 2 + statusW + 2 + nameW);
|
|
38
38
|
rows.push(` ${label} ${coverage.unsprinted} task${coverage.unsprinted === 1 ? '' : 's'}`);
|
|
39
39
|
}
|
|
40
40
|
return ['', 'Sprint coverage:', ...rows];
|
|
@@ -97,7 +97,7 @@ function formatNextOutput(ranked, totalOpen) {
|
|
|
97
97
|
lines.push('');
|
|
98
98
|
lines.push(`Next: lumo session attach ${first.identifier} && lumo task context ${first.identifier}`);
|
|
99
99
|
if (ranked.length > 1) {
|
|
100
|
-
lines.push('
|
|
100
|
+
lines.push('(or pick any other LUM-N from the list)');
|
|
101
101
|
}
|
|
102
102
|
return lines.join('\n');
|
|
103
103
|
}
|
|
@@ -20,7 +20,7 @@ const line_prompt_1 = require("../lib/line-prompt");
|
|
|
20
20
|
* longer silently clobbers `Session.taskId` (LUM-266): the server returns
|
|
21
21
|
* the current binding and we confirm before overwriting —
|
|
22
22
|
* - `--force` skips the prompt and overwrites directly;
|
|
23
|
-
* - on a TTY we ask
|
|
23
|
+
* - on a TTY we ask `Already bound to LUM-X. Rebind to LUM-Y? [y/N]`;
|
|
24
24
|
* - off a TTY (the usual agent case) we refuse and point at `--force`.
|
|
25
25
|
*/
|
|
26
26
|
async function sessionAttach(identifier, options = {}) {
|
|
@@ -95,9 +95,9 @@ async function sessionAttach(identifier, options = {}) {
|
|
|
95
95
|
'(or run `lumo session detach` first).');
|
|
96
96
|
return 0;
|
|
97
97
|
}
|
|
98
|
-
const answer = await (0, line_prompt_1.promptLine)(
|
|
98
|
+
const answer = await (0, line_prompt_1.promptLine)(`Already bound to ${body.currentTaskIdentifier}. Rebind to ${identifier}? [y/N] `);
|
|
99
99
|
if (!/^y(es)?$/i.test(answer)) {
|
|
100
|
-
console.log(
|
|
100
|
+
console.log(`Cancelled — still bound to ${body.currentTaskIdentifier}.`);
|
|
101
101
|
return 0;
|
|
102
102
|
}
|
|
103
103
|
const second = await bind(true);
|
|
@@ -112,7 +112,8 @@ async function sessionAttach(identifier, options = {}) {
|
|
|
112
112
|
}
|
|
113
113
|
console.log(`Attached session ${sessionId} to ${body.taskIdentifier} "${(0, sanitize_1.sanitizeField)(body.taskTitle)}"`);
|
|
114
114
|
console.log(`Re-tagged ${body.retaggedEventCount} previously-untagged event${body.retaggedEventCount === 1 ? '' : 's'} in this session.`);
|
|
115
|
-
//
|
|
115
|
+
// Warnings come before contract/memory: matches the hook injection order —
|
|
116
|
+
// short, actionable information first.
|
|
116
117
|
if (body.blockerWarningSection) {
|
|
117
118
|
console.log('');
|
|
118
119
|
console.log((0, sanitize_1.sanitizeField)(body.blockerWarningSection));
|
|
@@ -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);
|
|
@@ -99,7 +99,7 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
99
99
|
Authorization: `Bearer ${creds.token}`,
|
|
100
100
|
'Content-Type': 'application/json',
|
|
101
101
|
};
|
|
102
|
-
// Session
|
|
102
|
+
// Session provenance for HUMAN_EDIT transcriptions — the server records the
|
|
103
103
|
// revision (with this session id) as a task comment.
|
|
104
104
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
105
105
|
if (sessionId)
|
|
@@ -10,7 +10,7 @@ exports.taskDepsRm = taskDepsRm;
|
|
|
10
10
|
const config_1 = require("../lib/config");
|
|
11
11
|
const api_1 = require("../lib/api");
|
|
12
12
|
const sanitize_1 = require("../lib/sanitize");
|
|
13
|
-
/**
|
|
13
|
+
/** Short id = first 8 chars; used for both display and selectors. */
|
|
14
14
|
const shortId = (id) => id.slice(0, 8);
|
|
15
15
|
/**
|
|
16
16
|
* Resolve a user-supplied edge selector against the task's edge list. Accepts
|
|
@@ -35,7 +35,7 @@ function resolveEdgeSelector(edges, selector) {
|
|
|
35
35
|
*/
|
|
36
36
|
function formatDepsList(identifier, edges) {
|
|
37
37
|
if (edges.length === 0)
|
|
38
|
-
return `Dependencies for ${identifier}:
|
|
38
|
+
return `Dependencies for ${identifier}: no dependency edges.`;
|
|
39
39
|
const lines = [
|
|
40
40
|
`Dependencies for ${identifier} (${edges.length})`,
|
|
41
41
|
'',
|
|
@@ -43,7 +43,7 @@ function formatDepsList(identifier, edges) {
|
|
|
43
43
|
const dirLabel = (e) => e.direction === 'BLOCKED_BY' ? 'blocked by' : 'blocks';
|
|
44
44
|
const evidence = (e) => {
|
|
45
45
|
if (e.reason === 'shared_files')
|
|
46
|
-
return ` · shared_files(${e.detail?.count ?? '?'}
|
|
46
|
+
return ` · shared_files(${e.detail?.count ?? '?'} shared files${e.detail?.sample?.length ? ': ' + e.detail.sample.join(', ') : ''})`;
|
|
47
47
|
if (e.reason === 'task_mention')
|
|
48
48
|
return ` · task_mention(${e.detail?.surface ?? ''})`;
|
|
49
49
|
return '';
|
|
@@ -60,14 +60,14 @@ function formatDepsList(identifier, edges) {
|
|
|
60
60
|
const suggested = edges.filter(e => e.status === 'SUGGESTED');
|
|
61
61
|
const dismissed = edges.filter(e => e.status === 'DISMISSED');
|
|
62
62
|
section('CONFIRMED', confirmed, e => [
|
|
63
|
-
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier}
|
|
63
|
+
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} "${e.other.title}" ${e.other.status} · ${e.source}${evidence(e)}`,
|
|
64
64
|
]);
|
|
65
|
-
section('SUGGESTED(
|
|
66
|
-
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier}
|
|
67
|
-
`
|
|
65
|
+
section('SUGGESTED (pending confirmation)', suggested, e => [
|
|
66
|
+
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} "${e.other.title}" ${e.other.status}${evidence(e)}`,
|
|
67
|
+
` confirm: lumo task deps confirm ${identifier} ${shortId(e.id)} (add --reverse if direction is flipped; false positive: dismiss)`,
|
|
68
68
|
]);
|
|
69
69
|
section('DISMISSED', dismissed, e => [
|
|
70
|
-
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} ·
|
|
70
|
+
` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} · dismissed`,
|
|
71
71
|
]);
|
|
72
72
|
return lines.join('\n').trimEnd();
|
|
73
73
|
}
|
|
@@ -160,11 +160,11 @@ async function fetchDeps(ctx, taskDbId) {
|
|
|
160
160
|
function printSelectorCandidates(edges, selector) {
|
|
161
161
|
console.error(`Error: no unique dependency edge matches "${selector}". Candidates:`);
|
|
162
162
|
if (edges.length === 0) {
|
|
163
|
-
console.error(' (
|
|
163
|
+
console.error(' (no dependency edges)');
|
|
164
164
|
return;
|
|
165
165
|
}
|
|
166
166
|
for (const e of edges) {
|
|
167
|
-
console.error((0, sanitize_1.sanitizeField)(` [${shortId(e.id)}] ${e.status} ${e.other.identifier}
|
|
167
|
+
console.error((0, sanitize_1.sanitizeField)(` [${shortId(e.id)}] ${e.status} ${e.other.identifier} "${e.other.title}"`));
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
/**
|
|
@@ -247,7 +247,7 @@ async function taskDepsConfirm(identifier, selector, opts) {
|
|
|
247
247
|
}, 'dependency confirm');
|
|
248
248
|
if (typeof res === 'number')
|
|
249
249
|
return res;
|
|
250
|
-
console.log((0, sanitize_1.sanitizeField)(`Confirmed: [${shortId(edge.id)}] ${task.identifier} ${edge.direction === 'BLOCKED_BY' ? 'blocked by' : 'blocks'} ${edge.other.identifier}
|
|
250
|
+
console.log((0, sanitize_1.sanitizeField)(`Confirmed: [${shortId(edge.id)}] ${task.identifier} ${edge.direction === 'BLOCKED_BY' ? 'blocked by' : 'blocks'} ${edge.other.identifier} "${edge.other.title}"${opts.reverse ? ' (reversed)' : ''}`));
|
|
251
251
|
}
|
|
252
252
|
/** `lumo task deps dismiss <LUM-N> <edge>` */
|
|
253
253
|
async function taskDepsDismiss(identifier, selector) {
|
|
@@ -262,7 +262,7 @@ async function taskDepsDismiss(identifier, selector) {
|
|
|
262
262
|
const res = await depsFetch(ctx, `/api/tasks/${encodeURIComponent(task.id)}/dependencies/${encodeURIComponent(edge.id)}`, { method: 'PATCH', body: JSON.stringify({ action: 'dismiss' }) }, 'dependency dismiss');
|
|
263
263
|
if (typeof res === 'number')
|
|
264
264
|
return res;
|
|
265
|
-
console.log((0, sanitize_1.sanitizeField)(`Dismissed: [${shortId(edge.id)}] ${edge.other.identifier}
|
|
265
|
+
console.log((0, sanitize_1.sanitizeField)(`Dismissed: [${shortId(edge.id)}] ${edge.other.identifier} "${edge.other.title}" (won't be suggested again)`));
|
|
266
266
|
}
|
|
267
267
|
/** `lumo task deps rm <LUM-N> <edge> --yes` */
|
|
268
268
|
async function taskDepsRm(identifier, selector, opts) {
|
|
@@ -14,7 +14,7 @@ function tail(s, max) {
|
|
|
14
14
|
return s.length > max ? `…${s.slice(-max)}` : s;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
* Execute one MACHINE checkpointer in the local repo (
|
|
17
|
+
* Execute one MACHINE checkpointer in the local repo (runs client-side — the
|
|
18
18
|
* server can't run repo tests) and fold the result into a structured verdict.
|
|
19
19
|
* Exit 0 = PASS with a `cmd:` evidence pointer; non-zero = CRITERION_UNMET;
|
|
20
20
|
* spawn failure or timeout = CHECK_EXECUTION_ERROR.
|
|
@@ -14,7 +14,7 @@ const failure_summary_api_1 = require("../../lib/failure-summary-api");
|
|
|
14
14
|
*/
|
|
15
15
|
class BlockedPromptSection {
|
|
16
16
|
deps;
|
|
17
|
-
title = '
|
|
17
|
+
title = 'Blocked check';
|
|
18
18
|
draft = null;
|
|
19
19
|
constructor(deps) {
|
|
20
20
|
this.deps = deps;
|
|
@@ -28,14 +28,14 @@ class BlockedPromptSection {
|
|
|
28
28
|
if (!draft || !draft.shouldPrompt || !draft.taskIdentifier)
|
|
29
29
|
return;
|
|
30
30
|
const top = draft.topFailure;
|
|
31
|
-
const where = top ? (0, sanitize_1.sanitizeField)(top.label) : '
|
|
31
|
+
const where = top ? (0, sanitize_1.sanitizeField)(top.label) : 'an operation';
|
|
32
32
|
const count = top ? top.count : 0;
|
|
33
|
-
process.stdout.write(
|
|
33
|
+
process.stdout.write(`This session looks repeatedly stuck on ${where} (${count} failures).\n`);
|
|
34
34
|
if (top?.lastErrorSummary) {
|
|
35
|
-
process.stdout.write(
|
|
35
|
+
process.stdout.write(`Last error: ${(0, sanitize_1.sanitizeField)(top.lastErrorSummary)}\n`);
|
|
36
36
|
}
|
|
37
37
|
if (opts.dryRun) {
|
|
38
|
-
process.stdout.write(`(dry-run
|
|
38
|
+
process.stdout.write(`(dry-run, no changes; confirming would tag ${draft.taskIdentifier} blocked)\n`);
|
|
39
39
|
return;
|
|
40
40
|
}
|
|
41
41
|
// Tagging the shared board is opt-in: it requires an explicit interactive
|
|
@@ -43,10 +43,10 @@ class BlockedPromptSection {
|
|
|
43
43
|
// does NOT auto-tag — silently flipping shared board state is exactly what
|
|
44
44
|
// LUM-153 set out to avoid. We surface the suggestion and move on.
|
|
45
45
|
if (opts.yes) {
|
|
46
|
-
process.stdout.write(`(--yes
|
|
46
|
+
process.stdout.write(`(--yes does not auto-tag; answer y interactively, or run \`lumo task update ${draft.taskIdentifier} --add-tag blocked\` manually)\n`);
|
|
47
47
|
return;
|
|
48
48
|
}
|
|
49
|
-
const choice = (await (0, line_prompt_1.promptLine)(
|
|
49
|
+
const choice = (await (0, line_prompt_1.promptLine)(`Tag ${draft.taskIdentifier} as blocked? [y] tag [s] skip > `))
|
|
50
50
|
.trim()
|
|
51
51
|
.toLowerCase();
|
|
52
52
|
if (choice === 'y') {
|
|
@@ -54,11 +54,11 @@ class BlockedPromptSection {
|
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
// Empty / 's' / anything else → do nothing. Tagging is opt-in.
|
|
57
|
-
process.stdout.write('
|
|
57
|
+
process.stdout.write('Skipped — not tagged.\n');
|
|
58
58
|
}
|
|
59
59
|
async mark() {
|
|
60
60
|
const { taskIdentifier, tag } = await (0, failure_summary_api_1.markTaskBlocked)(this.deps.creds, this.deps.sessionId);
|
|
61
|
-
process.stdout.write(
|
|
61
|
+
process.stdout.write(`Tagged ${taskIdentifier} with ${tag}.\n`);
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
exports.BlockedPromptSection = BlockedPromptSection;
|
|
@@ -24,7 +24,7 @@ function parseUsedHandles(spec) {
|
|
|
24
24
|
*/
|
|
25
25
|
class FragmentUsageSection {
|
|
26
26
|
deps;
|
|
27
|
-
title = '
|
|
27
|
+
title = 'Fragment-usage vote';
|
|
28
28
|
draft = null;
|
|
29
29
|
constructor(deps) {
|
|
30
30
|
this.deps = deps;
|
|
@@ -38,19 +38,19 @@ class FragmentUsageSection {
|
|
|
38
38
|
if (!draft || draft.candidates.length === 0)
|
|
39
39
|
return;
|
|
40
40
|
if (draft.alreadyVoted) {
|
|
41
|
-
process.stdout.write('
|
|
41
|
+
process.stdout.write('This session already voted — skipping.\n');
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
-
process.stdout.write('
|
|
44
|
+
process.stdout.write('Context fragments injected in this session:\n');
|
|
45
45
|
draft.candidates.forEach((c, i) => {
|
|
46
46
|
process.stdout.write(` [${i + 1}] ${(0, sanitize_1.sanitizeField)(c.label)}\n`);
|
|
47
47
|
});
|
|
48
48
|
if (opts.dryRun) {
|
|
49
|
-
process.stdout.write('(dry-run
|
|
49
|
+
process.stdout.write('(dry-run, no changes)\n');
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
if (this.deps.used === undefined) {
|
|
53
|
-
process.stdout.write('
|
|
53
|
+
process.stdout.write('Use `lumo session wrap --used <indices>` (or `--used none`) to record which fragments you actually used.\n');
|
|
54
54
|
return;
|
|
55
55
|
}
|
|
56
56
|
const idx = parseUsedHandles(this.deps.used);
|
|
@@ -60,7 +60,7 @@ class FragmentUsageSection {
|
|
|
60
60
|
fragmentId: draft.candidates[i].fragmentId,
|
|
61
61
|
}));
|
|
62
62
|
const { used, unused } = await (0, fragment_usage_api_1.applyFragmentUsage)(this.deps.creds, this.deps.sessionId, { usedRefs });
|
|
63
|
-
process.stdout.write(
|
|
63
|
+
process.stdout.write(`Recorded: ${used} used, ${unused} unused.\n`);
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
66
|
exports.FragmentUsageSection = FragmentUsageSection;
|
|
@@ -32,7 +32,7 @@ function parseReviewInstruction(line) {
|
|
|
32
32
|
*/
|
|
33
33
|
class MemoryReviewSection {
|
|
34
34
|
deps;
|
|
35
|
-
title = '
|
|
35
|
+
title = 'Memory review';
|
|
36
36
|
draft = null;
|
|
37
37
|
constructor(deps) {
|
|
38
38
|
this.deps = deps;
|
|
@@ -45,19 +45,19 @@ class MemoryReviewSection {
|
|
|
45
45
|
const draft = this.draft;
|
|
46
46
|
if (!draft || !draft.watermark || draft.memories.length === 0)
|
|
47
47
|
return;
|
|
48
|
-
process.stdout.write(
|
|
48
|
+
process.stdout.write(`This session recorded ${draft.memories.length} new memories:\n`);
|
|
49
49
|
process.stdout.write(`${(0, sanitize_1.sanitizeField)((0, memory_content_1.formatMemoryReviewList)(draft.memories))}\n`);
|
|
50
50
|
if (opts.dryRun) {
|
|
51
|
-
process.stdout.write('(dry-run
|
|
51
|
+
process.stdout.write('(dry-run, no changes)\n');
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
if (opts.yes) {
|
|
55
55
|
await this.apply(draft.watermark, [], []);
|
|
56
56
|
return;
|
|
57
57
|
}
|
|
58
|
-
const line = (await (0, line_prompt_1.promptLine)('[
|
|
58
|
+
const line = (await (0, line_prompt_1.promptLine)('[Enter] keep all [d 1,3] delete [p 2] promote to project [s] skip > ')).trim();
|
|
59
59
|
if (line.toLowerCase() === 's') {
|
|
60
|
-
process.stdout.write('
|
|
60
|
+
process.stdout.write('Section skipped.\n');
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
if (line === '') {
|
|
@@ -75,7 +75,7 @@ class MemoryReviewSection {
|
|
|
75
75
|
}
|
|
76
76
|
async apply(watermark, deleteIds, promoteIds) {
|
|
77
77
|
const { deleted, promoted } = await (0, session_memory_api_1.applyMemoryReview)(this.deps.creds, this.deps.sessionId, { watermark, deleteIds, promoteIds });
|
|
78
|
-
process.stdout.write(
|
|
78
|
+
process.stdout.write(`Deleted ${deleted}, promoted ${promoted} to project scope.\n`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
exports.MemoryReviewSection = MemoryReviewSection;
|
|
@@ -6,7 +6,7 @@ const sanitize_1 = require("../../lib/sanitize");
|
|
|
6
6
|
const line_prompt_1 = require("../../lib/line-prompt");
|
|
7
7
|
const editor_1 = require("../../lib/editor");
|
|
8
8
|
const progress_comment_api_1 = require("../../lib/progress-comment-api");
|
|
9
|
-
const HEADER = '
|
|
9
|
+
const HEADER = 'Session progress';
|
|
10
10
|
/** Join turn summaries into a bulleted progress comment body under a header. */
|
|
11
11
|
function formatProgressBody(summaries) {
|
|
12
12
|
return [HEADER, ...summaries.map(s => `- ${s}`)].join('\n');
|
|
@@ -18,7 +18,7 @@ function formatProgressBody(summaries) {
|
|
|
18
18
|
*/
|
|
19
19
|
class ProgressCommentSection {
|
|
20
20
|
deps;
|
|
21
|
-
title = '
|
|
21
|
+
title = 'Progress comment';
|
|
22
22
|
draft = null;
|
|
23
23
|
body = '';
|
|
24
24
|
constructor(deps) {
|
|
@@ -37,31 +37,31 @@ class ProgressCommentSection {
|
|
|
37
37
|
if (!draft || !draft.watermark)
|
|
38
38
|
return;
|
|
39
39
|
// Preview: sanitize the server free-text before it hits the terminal.
|
|
40
|
-
process.stdout.write(
|
|
40
|
+
process.stdout.write(`Will post to ${draft.taskIdentifier} "${(0, sanitize_1.sanitizeField)(draft.taskTitle ?? '')}":\n`);
|
|
41
41
|
process.stdout.write(`${(0, sanitize_1.sanitizeField)(this.body)}\n`);
|
|
42
42
|
if (opts.dryRun) {
|
|
43
|
-
process.stdout.write('(dry-run
|
|
43
|
+
process.stdout.write('(dry-run, not posted)\n');
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
if (opts.yes) {
|
|
47
47
|
await this.post(draft.watermark, this.body);
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
const choice = (await (0, line_prompt_1.promptLine)('[y]
|
|
50
|
+
const choice = (await (0, line_prompt_1.promptLine)('[y] post [e] edit [s] skip > ')).toLowerCase();
|
|
51
51
|
if (choice === 's' || choice === '') {
|
|
52
|
-
process.stdout.write('
|
|
52
|
+
process.stdout.write('Skipped.\n');
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
if (choice === 'e') {
|
|
56
56
|
const edited = (await (0, editor_1.editInEditor)(this.body)).trim();
|
|
57
57
|
if (edited.length === 0) {
|
|
58
|
-
process.stdout.write('
|
|
58
|
+
process.stdout.write('Empty body — skipped.\n');
|
|
59
59
|
return;
|
|
60
60
|
}
|
|
61
61
|
process.stdout.write(`${(0, sanitize_1.sanitizeField)(edited)}\n`);
|
|
62
|
-
const confirm = (await (0, line_prompt_1.promptLine)('[y]
|
|
62
|
+
const confirm = (await (0, line_prompt_1.promptLine)('[y] post [s] skip > ')).toLowerCase();
|
|
63
63
|
if (confirm !== 'y') {
|
|
64
|
-
process.stdout.write('
|
|
64
|
+
process.stdout.write('Skipped.\n');
|
|
65
65
|
return;
|
|
66
66
|
}
|
|
67
67
|
await this.post(draft.watermark, edited);
|
|
@@ -71,11 +71,11 @@ class ProgressCommentSection {
|
|
|
71
71
|
await this.post(draft.watermark, this.body);
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
process.stdout.write('
|
|
74
|
+
process.stdout.write('Unrecognized choice — skipped.\n');
|
|
75
75
|
}
|
|
76
76
|
async post(watermark, body) {
|
|
77
77
|
const { commentId } = await (0, progress_comment_api_1.postProgressComment)(this.deps.creds, this.deps.sessionId, { body, watermark });
|
|
78
|
-
process.stdout.write(
|
|
78
|
+
process.stdout.write(`Posted progress comment (comment ${commentId})\n`);
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
exports.ProgressCommentSection = ProgressCommentSection;
|
package/dist/cli/src/index.js
CHANGED
|
@@ -172,7 +172,12 @@ function wrap(fn) {
|
|
|
172
172
|
const program = new commander_1.Command()
|
|
173
173
|
.name('lumo')
|
|
174
174
|
.description('Lumo CLI — manage tasks and sessions from the terminal')
|
|
175
|
-
.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)');
|
|
176
181
|
const auth = program.command('auth').description('Manage Lumo authentication');
|
|
177
182
|
auth
|
|
178
183
|
.command('login')
|
|
@@ -391,7 +396,7 @@ taskCriteria
|
|
|
391
396
|
.command('set <task>')
|
|
392
397
|
.description('Submit the whole acceptance contract from a JSON file. Default = initial agent draft (locked once submitted); --human records a HUMAN_EDIT revision (desired final list; items with "id" keep/update, missing ones are deleted).')
|
|
393
398
|
.requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
|
|
394
|
-
.option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session
|
|
399
|
+
.option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session provenance')
|
|
395
400
|
.option('--cause <tag>', 'Why the contract drifted (with --human): NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER')
|
|
396
401
|
.action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
|
|
397
402
|
taskCriteria
|