@lumoai/cli 1.25.1 → 1.27.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.
@@ -199,7 +199,7 @@ Top 3 recommended tasks (of 12 open):
199
199
  Next: lumo session attach LUM-42 && lumo task context LUM-42
200
200
  ```
201
201
 
202
- 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.
203
203
 
204
204
  ### `lumo task show <identifier>` — print one task's detail
205
205
 
@@ -227,7 +227,7 @@ The CLI does not support @-mention chip syntax. If the user wants to ping someon
227
227
 
228
228
  ### `lumo task deps list <LUM-N>` — show all dependency edges
229
229
 
230
- 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.
231
231
 
232
232
  ```bash
233
233
  lumo task deps list LUM-42
@@ -239,29 +239,29 @@ Example output:
239
239
  Dependencies for LUM-42 (3)
240
240
 
241
241
  CONFIRMED
242
- [a1b2c3d4] blocked by LUM-9Fix auth token expiry IN_PROGRESS · MANUAL
242
+ [a1b2c3d4] blocked by LUM-9 "Fix auth token expiry" IN_PROGRESS · MANUAL
243
243
 
244
- SUGGESTED(待确认)
245
- [e5f6a7b8] blocks LUM-55Migrate DB schema TODO · shared_files(4 个共享文件: src/db/schema.ts, src/db/migrate.ts, ...)
246
- 确认: lumo task deps confirm LUM-42 e5f6a7b8(方向反了加 --reverse;误报: dismiss)
247
- [c9d0e1f2] blocked by LUM-38Add OAuth scopes IN_REVIEW · task_mention(description)
248
- 确认: 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)
249
249
 
250
250
  DISMISSED
251
- [b3c4d5e6] blocks LUM-12 · 已忽略
251
+ [b3c4d5e6] blocks LUM-12 · dismissed
252
252
  ```
253
253
 
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.
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.
255
255
 
256
256
  When there are no edges at all the output is:
257
257
 
258
258
  ```
259
- Dependencies for LUM-42: 无依赖边。
259
+ Dependencies for LUM-42: no dependency edges.
260
260
  ```
261
261
 
262
262
  **Evidence fields by detection signal:**
263
263
 
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.
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.
265
265
  - `task_mention` — `task_mention(description)` or `task_mention(comment)` — the surface where the mention appeared.
266
266
 
267
267
  CONFIRMED rows also show `source`: `MANUAL` (user-declared via `deps add`) or `DETECTED` (auto-found then confirmed via `deps confirm`).
@@ -314,7 +314,7 @@ lumo task deps dismiss LUM-42 e5f6a7b8
314
314
  lumo task deps dismiss LUM-42 LUM-38
315
315
  ```
316
316
 
317
- Output: `Dismissed: [e5f6a7b8] LUM-38Add OAuth scopes(不再建议)`
317
+ Output: `Dismissed: [e5f6a7b8] LUM-38 "Add OAuth scopes" (won't be suggested again)`
318
318
 
319
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).
320
320
 
@@ -352,6 +352,6 @@ Output: `Removed [a1b2c3d4] from LUM-42`
352
352
 
353
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.
354
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`.
355
- - **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`.
356
- - **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.
357
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
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.docDiff = docDiff;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const resolve_doc_id_1 = require("../lib/resolve-doc-id");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const doc_input_1 = require("../lib/doc-input");
9
+ const path_guard_1 = require("../lib/path-guard");
10
+ const unified_diff_1 = require("../lib/unified-diff");
11
+ /**
12
+ * `lumo doc diff <doc> --file <local.md>` (LUM-408).
13
+ *
14
+ * Compares the server-side markdown source (Document.sourceMarkdown — the
15
+ * byte-identical last markdown upload) against a local file, so a
16
+ * remote/local split-brain is visible on demand instead of discovered from
17
+ * memory after a stale upload clobbers someone's edit.
18
+ *
19
+ * Exit codes: 0 = sources byte-identical, 1 = divergent (or error).
20
+ */
21
+ async function docDiff(reference, opts) {
22
+ if (!reference) {
23
+ console.error('Error: missing <doc>. Usage: lumo doc diff <doc> --file <local.md>');
24
+ return 1;
25
+ }
26
+ if (!opts.file) {
27
+ console.error('Error: --file <local.md> is required for doc diff');
28
+ return 1;
29
+ }
30
+ const creds = (0, config_1.readCredentials)();
31
+ if (!creds) {
32
+ console.error('Error: not logged in. Run `lumo auth login` first.');
33
+ return 1;
34
+ }
35
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
36
+ // Same sandbox as doc create/update --file: project-local, non-sensitive.
37
+ const check = (0, path_guard_1.checkArtifactFilePath)(opts.file);
38
+ if (!check.ok) {
39
+ console.error(check.reason === 'unreadable'
40
+ ? `Error: ${(0, doc_input_1.unreadableFileMessage)(opts.file)}`
41
+ : `Error: refusing to read ${opts.file} — ${check.detail}. ` +
42
+ `--file must be a non-sensitive path inside the project directory.`);
43
+ return 1;
44
+ }
45
+ const localText = await (0, doc_input_1.readFileUtf8)(check.resolved);
46
+ const id = await (0, resolve_doc_id_1.lookupDocId)(apiUrl, creds.token, reference);
47
+ if (!id) {
48
+ console.error(`Error: Document not found: ${reference}`);
49
+ return 1;
50
+ }
51
+ const res = await fetch(`${(0, api_1.trimTrailingSlash)(apiUrl)}/api/documents/${id}`, {
52
+ headers: { Authorization: `Bearer ${creds.token}` },
53
+ });
54
+ if (!res.ok) {
55
+ const text = await res.text();
56
+ console.error(`Error: ${res.status} ${res.statusText}: ${(0, sanitize_1.sanitizeField)(text)}`);
57
+ return 1;
58
+ }
59
+ const data = (await res.json());
60
+ const d = data.document;
61
+ if (!d) {
62
+ console.error('Error: server returned an empty document response');
63
+ return 1;
64
+ }
65
+ if (typeof d.sourceMarkdown !== 'string') {
66
+ console.error(`Error: ${d.id} has no stored markdown source to diff against (last edit ` +
67
+ `was HTML-direct or predates markdown source storage). Upload a markdown ` +
68
+ `base once (\`lumo doc update ${d.id} --file <base.md>\`) and diff from then on.`);
69
+ return 1;
70
+ }
71
+ if (d.sourceMarkdown === localText) {
72
+ console.log(`Clean: remote markdown source of ${d.id} matches ${opts.file} (${Buffer.byteLength(localText, 'utf8')} bytes)`);
73
+ return;
74
+ }
75
+ const diff = (0, unified_diff_1.formatUnifiedDiff)(d.sourceMarkdown, localText, `remote/${d.id} (sourceMarkdown)`, `local/${opts.file}`);
76
+ // Byte-level divergence with identical line sequences (e.g. trailing
77
+ // newline only) still counts as divergent — say so explicitly.
78
+ console.log(diff !== ''
79
+ ? (0, sanitize_1.sanitizeField)(diff)
80
+ : `Divergent: byte-level difference only (e.g. trailing newline) between remote ${d.id} and ${opts.file}`);
81
+ return 1;
82
+ }
@@ -27,9 +27,9 @@ function formatShowOutput(vm) {
27
27
  const header = lines.join('\n');
28
28
  return vm.bodyMarkdown ? `${header}\n\n${(0, sanitize_1.sanitizeField)(vm.bodyMarkdown)}` : header;
29
29
  }
30
- async function docShow(reference) {
30
+ async function docShow(reference, opts = {}) {
31
31
  if (!reference) {
32
- console.error('Error: missing <doc>. Usage: lumo doc show <doc>');
32
+ console.error('Error: missing <doc>. Usage: lumo doc show <doc> [--raw]');
33
33
  return 1;
34
34
  }
35
35
  const creds = (0, config_1.readCredentials)();
@@ -58,6 +58,23 @@ async function docShow(reference) {
58
58
  console.error('Error: server returned an empty document response');
59
59
  return 1;
60
60
  }
61
+ if (opts.raw) {
62
+ // Byte-identical markdown source of the last markdown upload (LUM-408).
63
+ // Written verbatim (no header, no sanitization, no added newline) so the
64
+ // output is a legal edit base: `doc show --raw > base.md` round-trips.
65
+ // No silent fallback to the lossy HTML→markdown reverse render — that
66
+ // fallback is exactly what flattened tables in LUM-349.
67
+ if (typeof d.sourceMarkdown !== 'string') {
68
+ console.error(`Error: ${d.id} has no stored markdown source (last edit was HTML-direct ` +
69
+ `or predates markdown source storage). --raw refuses to fall back to the ` +
70
+ `lossy HTML→markdown render. Rebuild a base instead: run \`lumo doc show ${d.id}\`, ` +
71
+ `reconstruct the markdown faithfully, then \`lumo doc update ${d.id} --file <rebuilt.md>\` ` +
72
+ `— from then on --raw works.`);
73
+ return 1;
74
+ }
75
+ process.stdout.write(d.sourceMarkdown);
76
+ return;
77
+ }
61
78
  // Server returns `contentMarkdown` derived from the HTML body (LUM-83+).
62
79
  // Fall back to parsing the raw content as legacy Tiptap JSON for docs
63
80
  // written before the storage shape changed.
@@ -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));
@@ -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;
@@ -107,6 +107,7 @@ const doc_import_gdoc_1 = require("./commands/doc-import-gdoc");
107
107
  const doc_sync_1 = require("./commands/doc-sync");
108
108
  const doc_update_1 = require("./commands/doc-update");
109
109
  const doc_show_1 = require("./commands/doc-show");
110
+ const doc_diff_1 = require("./commands/doc-diff");
110
111
  const doc_list_1 = require("./commands/doc-list");
111
112
  const doc_delete_1 = require("./commands/doc-delete");
112
113
  const doc_bind_1 = require("./commands/doc-bind");
@@ -396,7 +397,7 @@ taskCriteria
396
397
  .command('set <task>')
397
398
  .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).')
398
399
  .requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
399
- .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session 出处')
400
+ .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session provenance')
400
401
  .option('--cause <tag>', 'Why the contract drifted (with --human): NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER')
401
402
  .action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
402
403
  taskCriteria
@@ -665,8 +666,14 @@ doc
665
666
  .action(wrap((reference, opts) => (0, doc_update_1.docUpdate)(reference, opts)));
666
667
  doc
667
668
  .command('show <doc>')
668
- .description('Show document header and body (doc = title or cuid)')
669
- .action(wrap(reference => (0, doc_show_1.docShow)(reference)));
669
+ .description('Show document header and body (doc = title or cuid). --raw prints the byte-identical markdown source of the last markdown upload (a legal edit base); it errors when no markdown source is stored instead of falling back to the lossy HTML→markdown render.')
670
+ .option('--raw', 'Print the stored markdown source verbatim (no header); errors if absent')
671
+ .action(wrap((reference, opts) => (0, doc_show_1.docShow)(reference, opts)));
672
+ doc
673
+ .command('diff <doc>')
674
+ .description('Compare the server-side markdown source against a local file. Exit 0 when byte-identical, 1 with a unified diff when divergent. Requires the doc to have a stored markdown source.')
675
+ .requiredOption('--file <path>', 'Local markdown file to compare against')
676
+ .action(wrap((reference, opts) => (0, doc_diff_1.docDiff)(reference, opts)));
670
677
  doc
671
678
  .command('list')
672
679
  .description('List documents visible to the current user')