@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.
Files changed (29) hide show
  1. package/assets/skill/SKILL.md +21 -7
  2. package/assets/skill/references/artifacts-figma.md +1 -1
  3. package/assets/skill/references/criteria.md +10 -9
  4. package/assets/skill/references/docs.md +5 -5
  5. package/assets/skill/references/milestones.md +6 -6
  6. package/assets/skill/references/sessions.md +30 -30
  7. package/assets/skill/references/sprints.md +6 -6
  8. package/assets/skill/references/task-context.md +5 -5
  9. package/assets/skill/references/tasks.md +37 -15
  10. package/assets/skill/references/verify.md +3 -3
  11. package/dist/cli/src/commands/doc-list.js +4 -0
  12. package/dist/cli/src/commands/memory-promote.js +11 -4
  13. package/dist/cli/src/commands/memory-rm.js +1 -1
  14. package/dist/cli/src/commands/milestone-show.js +1 -1
  15. package/dist/cli/src/commands/next.js +1 -1
  16. package/dist/cli/src/commands/session-attach.js +5 -4
  17. package/dist/cli/src/commands/task-artifact-add.js +2 -2
  18. package/dist/cli/src/commands/task-criteria-set.js +3 -3
  19. package/dist/cli/src/commands/task-deps.js +12 -12
  20. package/dist/cli/src/commands/verify.js +1 -1
  21. package/dist/cli/src/commands/wrap/blocked-prompt-section.js +9 -9
  22. package/dist/cli/src/commands/wrap/fragment-usage-section.js +6 -6
  23. package/dist/cli/src/commands/wrap/memory-review-section.js +6 -6
  24. package/dist/cli/src/commands/wrap/progress-comment-section.js +11 -11
  25. package/dist/cli/src/index.js +7 -2
  26. package/dist/cli/src/lib/doc-input.js +10 -1
  27. package/dist/cli/src/lib/hook-runner.js +9 -9
  28. package/dist/cli/src/lib/wrap-panel.js +1 -1
  29. 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", "推荐下一个任务", "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.
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(待确认)**, 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.
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-9Fix auth token expiry IN_PROGRESS · MANUAL
242
+ [a1b2c3d4] blocked by LUM-9 "Fix auth token expiry" IN_PROGRESS · MANUAL
226
243
 
227
- SUGGESTED(待确认)
228
- [e5f6a7b8] blocks LUM-55Migrate DB schema TODO · shared_files(4 个共享文件: src/db/schema.ts, src/db/migrate.ts, ...)
229
- 确认: lumo task deps confirm LUM-42 e5f6a7b8(方向反了加 --reverse;误报: dismiss)
230
- [c9d0e1f2] blocked by LUM-38Add OAuth scopes IN_REVIEW · task_mention(description)
231
- 确认: lumo task deps confirm LUM-42 c9d0e1f2(方向反了加 --reverse;误报: dismiss)
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> · 已忽略` only — no title, status, or source.
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
- - `shared_files` — `shared_files(N 个共享文件: path1, path2, …)` — number of shared write-touched files in the 14-day window, plus up to 5 sample paths.
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-38Add OAuth scopes(不再建议)`
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 `## ⚠ 依赖告警` 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`.
334
- - **Agent sees only a standalone hint line `检测到 N 条候选依赖待确认…` (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.
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: { Authorization: `Bearer ${creds.token}`, 'Content-Type': 'application/json' },
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
- console.error(m ? `Error: ${(0, sanitize_1.sanitizeField)(m)}` : `Error: promote failed (HTTP ${res.status})`);
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 = '未排期'.padEnd(numW + 2 + statusW + 2 + nameW);
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('(也可换成列表里任意一个 LUM-N');
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 `已绑定 LUM-X,覆盖为 LUM-Y? [y/N]`;
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)(`已绑定 ${body.currentTaskIdentifier},覆盖为 ${identifier}? [y/N] `);
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(`已取消,仍绑定 ${body.currentTaskIdentifier}。`);
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
- // 告警先于 contract/memory:与 hook 注入顺序一致,短且可操作的信息优先。
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: could not read file ${options.file}`);
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: could not read file ${options.file}`);
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: could not read file ${options.file}`);
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: could not read file ${options.file}`);
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 出处 for HUMAN_EDIT transcriptions — the server records the
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
- /** id = 8 位,展示与 selector 都用它。 */
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 ?? '?'} 个共享文件${e.detail?.sample?.length ? ': ' + e.detail.sample.join(', ') : ''})`;
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}「${e.other.title} ${e.other.status} · ${e.source}${evidence(e)}`,
63
+ ` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} "${e.other.title}" ${e.other.status} · ${e.source}${evidence(e)}`,
64
64
  ]);
65
- section('SUGGESTED(待确认)', suggested, e => [
66
- ` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier}「${e.other.title} ${e.other.status}${evidence(e)}`,
67
- ` 确认: lumo task deps confirm ${identifier} ${shortId(e.id)}(方向反了加 --reverse;误报: dismiss)`,
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}「${e.other.title}」`));
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}「${edge.other.title}」${opts.reverse ? ' (reversed)' : ''}`));
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}「${edge.other.title}(不再建议)`));
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 (执行在客户端 — the
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(`看起来本次会话反复卡在 ${where}(${count} 次失败)。\n`);
33
+ process.stdout.write(`This session looks repeatedly stuck on ${where} (${count} failures).\n`);
34
34
  if (top?.lastErrorSummary) {
35
- process.stdout.write(`最后错误:${(0, sanitize_1.sanitizeField)(top.lastErrorSummary)}\n`);
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,未改动;确认后会给 ${draft.taskIdentifier} blocked)\n`);
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 不自动标记;如确认请交互式回答 y,或手动 \`lumo task update ${draft.taskIdentifier} --add-tag blocked\`)\n`);
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)(`要在 ${draft.taskIdentifier} blocked 吗?[y] 标记 [s] 跳过 > `))
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('已跳过,未标记。\n');
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(`已给 ${taskIdentifier} ${tag}。\n`);
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(' session 已投票,跳过。\n');
41
+ process.stdout.write('This session already voted — skipping.\n');
42
42
  return;
43
43
  }
44
- process.stdout.write('本次会话注入的 context fragment:\n');
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,未改动)\n');
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(' `lumo session wrap --used <序号>`( `--used none`)记录你实际用到的 fragment。\n');
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(`已记录:用过 ${used} 个,未用 ${unused} 个。\n`);
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(`本次会话新增了这 ${draft.memories.length} memory:\n`);
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,未改动)\n');
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)('[回车] 全部保留 [d 1,3] 删除 [p 2] 提升到项目级 [s] 跳过 > ')).trim();
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('已跳过本节。\n');
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(`已删除 ${deleted} 条,提升 ${promoted} 条到项目级。\n`);
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(`将发到 ${draft.taskIdentifier} "${(0, sanitize_1.sanitizeField)(draft.taskTitle ?? '')}":\n`);
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,未发送)\n');
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] 发送 [e] 编辑 [s] 跳过 > ')).toLowerCase();
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('已跳过。\n');
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('正文为空,已跳过。\n');
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] 发送 [s] 跳过 > ')).toLowerCase();
62
+ const confirm = (await (0, line_prompt_1.promptLine)('[y] post [s] skip > ')).toLowerCase();
63
63
  if (confirm !== 'y') {
64
- process.stdout.write('已跳过。\n');
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('无法识别的选择,已跳过。\n');
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(`已发送进度评论 (comment ${commentId})\n`);
78
+ process.stdout.write(`Posted progress comment (comment ${commentId})\n`);
79
79
  }
80
80
  }
81
81
  exports.ProgressCommentSection = ProgressCommentSection;
@@ -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