@lumoai/cli 1.23.0 → 1.24.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.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "task deps", "dependency", "dependencies", "依赖", "依赖边", "blocked by", "blocker", "confirm dependency", "dismiss dependency", "确认依赖", "忽略依赖", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task criteria", "criteria set", "criteria list", "acceptance criteria", "验收标准", "验收合约", "draft criteria", "草拟验收", "definition of done", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context", "--signal", "usage signal health", "auto usage audit", "自动使用审计", "signal-health".'
3
+ description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "task deps", "dependency", "dependencies", "依赖", "依赖边", "blocked by", "blocker", "confirm dependency", "dismiss dependency", "确认依赖", "忽略依赖", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task criteria", "criteria set", "criteria list", "acceptance criteria", "验收标准", "验收合约", "draft criteria", "草拟验收", "definition of done", "lumo verify", "verify task", "machine verification", "verification round", "机器验收", "自验", "验收轮", "claim done", "宣称完成", "--cause", "contract drift", "合约漂移", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context", "--signal", "usage signal health", "auto usage audit", "自动使用审计", "signal-health".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -26,6 +26,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
26
26
  | `task create/update/list/show/comment`, `next` | [references/tasks.md](references/tasks.md) |
27
27
  | `task artifact*`, `task figma*` | [references/artifacts-figma.md](references/artifacts-figma.md) |
28
28
  | `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
29
+ | `verify` — machine verification loop, claim-done flow | [references/verify.md](references/verify.md) |
29
30
  | `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
30
31
  | `doc*` | [references/docs.md](references/docs.md) |
31
32
  | `sprint*` | [references/sprints.md](references/sprints.md) |
@@ -72,9 +73,13 @@ The command catalog below is a **map**: it lists every command grouped by domain
72
73
 
73
74
  **Acceptance criteria(验收合约)** — see [criteria.md](references/criteria.md)
74
75
 
75
- - `lumo task criteria set <task> --file <criteria.json> [--human]` — submit the whole contract: default = initial agent draft (AGENT_DRAFT, locked once submitted); `--human` = a HUMAN_EDIT revision transcribed from the conversation (desired final list; items with `id` keep/update, missing ones are deleted)
76
+ - `lumo task criteria set <task> --file <criteria.json> [--human] [--cause <tag>]` — submit the whole contract: default = initial agent draft (AGENT_DRAFT, locked once submitted); `--human` = a HUMAN_EDIT revision transcribed from the conversation (desired final list; items with `id` keep/update, missing ones are deleted); `--cause` (with `--human`) annotates why the contract drifted: `NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER`
76
77
  - `lumo task criteria list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
77
78
 
79
+ **Verification(机器验收循环)** — see [verify.md](references/verify.md)
80
+
81
+ - `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally, report one structured PASS/FAIL verdict per criterion to the server, print next actions. Defaults to the session-bound task. Round cap 3: an all-pass round moves the task to IN_REVIEW (agent stops there); a round-3 fail escalates to a human (stop retrying). **Run this before claiming a task is done.**
82
+
78
83
  **Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
79
84
 
80
85
  - `lumo task artifact add/update/list/show/rm` — record spec/plan products on a task
@@ -131,5 +136,6 @@ Typical flow when a user says "help me with LUM-42":
131
136
  3. Review unresolved items, PR-review todos, and the task description
132
137
  4. **If the task has no acceptance criteria** (context shows the 草拟提醒 instead of a contract): draft 3–7 outcome-level criteria and submit them with `lumo task criteria set` **before writing the first line of code** — see [criteria.md](references/criteria.md) for the drafting guide
133
138
  5. Begin working on the task
139
+ 6. **Before claiming the work is done: run `lumo verify`** — the machine half of the acceptance loop. Fix failures and re-run (round cap 3). On all-pass the task moves to IN_REVIEW and you stop; never set DONE yourself after a verify loop — that adjudication is human-only. See [verify.md](references/verify.md)
134
140
 
135
141
  **Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits and print a suggestion — `检测到 LUM-N … 运行 lumo session attach LUM-N 绑定。` — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
@@ -105,6 +105,14 @@ verdict is never** — human verdicts only enter through human-initiated paths
105
105
  Only use `--human` for decisions a human actually made (in conversation, in a
106
106
  comment, in review). Never use it to work around your own lock.
107
107
 
108
+ Optionally annotate **why** the contract drifted with
109
+ `--cause <NEW_INFO|SCOPE_CHANGE|DRAFT_BLIND_SPOT|GRANULARITY|OTHER>` — pick
110
+ the tag from the human's stated reason (new information, scope moved, the
111
+ draft missed it, wrong granularity). The tag lands in the drift record
112
+ (TaskActivity payload), feeding the Slice-3 drift-cause distribution. Every
113
+ criteria add/update/delete is mirrored as a structured `CRITERION_CHANGED`
114
+ activity automatically; `--cause` just enriches it.
115
+
108
116
  ### `lumo task criteria list <task>`
109
117
 
110
118
  Print the contract: `<id> [MACHINE|HUMAN] SOURCE@rN [evidence] statement`
@@ -121,4 +129,10 @@ before a `--human` revision. Empty contract prints a drafting pointer.
121
129
  - **`lumo task context`**: the `## 验收标准(合约)` section appears after the
122
130
  task description, before memory.
123
131
  - Review-time gap findings (`REVIEW_ADDED`, appended at the round they
124
- surface) arrive via the verification loop (任务 ③), not via `criteria set`.
132
+ surface) arrive via the verification loop, not via `criteria set`.
133
+
134
+ ## After the contract: the verification loop
135
+
136
+ The contract is judged by `lumo verify` — run it before claiming the task is
137
+ done. See [verify.md](verify.md) for the loop (round cap 3, IN_REVIEW on
138
+ all-pass, escalation on a round-3 fail).
@@ -0,0 +1,72 @@
1
+ # lumo verify — machine verification loop(机器验收循环)
2
+
3
+ `lumo verify` is the machine half of the acceptance system (Acceptance v1,
4
+ LUM-343). It executes every **MACHINE** criterion's checkpointer in the local
5
+ repo, reports one structured PASS/FAIL verdict per criterion to the server,
6
+ and prints what to do next. The judge lives server-side: round numbering, the
7
+ 3-round cap, escalation, and the IN_REVIEW transition all happen there
8
+ (执行在客户端,裁判在服务端).
9
+
10
+ ## The claim-done rule
11
+
12
+ **Before claiming a task is complete — in conversation, in a wrap-up, or by
13
+ touching its status — run `lumo verify`.** The loop replaces "I read the code
14
+ and it looks done" with executed evidence.
15
+
16
+ ```
17
+ lumo verify # session-bound task
18
+ lumo verify LUM-42 # explicit task (overrides the session binding)
19
+ lumo verify --timeout 900 # per-checkpointer timeout in seconds (default 600)
20
+ ```
21
+
22
+ ## What one round does
23
+
24
+ 1. Loads the task's acceptance contract and picks out MACHINE criteria.
25
+ 2. Runs each checkpointer locally (shell, cwd = current directory), one at a
26
+ time, echoing PASS/FAIL as it goes.
27
+ 3. POSTs the structured verdicts; the server records one VerificationRun per
28
+ criterion at round = previous max + 1 and mirrors each verdict as a
29
+ TaskActivity event.
30
+ 4. Prints the round outcome:
31
+ - **All PASS** → the task transitions to **IN_REVIEW** (existing state
32
+ machine + TASK_IN_REVIEW notification). **Stop here.** Human
33
+ adjudication and any HUMAN criteria take over; never set DONE yourself.
34
+ - **Any FAIL** → task status is untouched; the unmet criteria are printed
35
+ as next actions (statement, checkpointer, failure tail). Fix and re-run.
36
+ - **Round 3 still failing** → the loop escalates: a human is notified
37
+ (AGENT_VERIFY, requires action) and further `lumo verify` rounds are
38
+ rejected with 409. **Stop retrying**; fix only what the human directs.
39
+
40
+ Exit code 0 = all passed (or nothing to run); 1 = failures, escalation, or
41
+ errors.
42
+
43
+ ## Verdict semantics (what the CLI sends)
44
+
45
+ - checkpointer exits 0 → `PASS` with evidence `cmd:<command>#exit=0`
46
+ - non-zero exit → `FAIL`, reason = output tail, enum `CRITERION_UNMET`
47
+ - spawn failure / timeout → `FAIL`, enum `CHECK_EXECUTION_ERROR`
48
+
49
+ evidencePointer is **not free text** — the server only accepts
50
+ `commit:<hash>`, `file:<path>:<line>`, or `cmd:<command>#exit=<code>`.
51
+ Verdicts are PASS|FAIL only; the agent path cannot write HUMAN verdicts or
52
+ `PASS_WITH_FOLLOWUP` (red line — those enter via human-initiated UI paths
53
+ only).
54
+
55
+ ## Edge cases
56
+
57
+ - **No contract yet** → error pointing at `lumo task criteria set`; draft the
58
+ contract first (criteria.md golden rule).
59
+ - **HUMAN-only contract (zero MACHINE criteria)** → nothing to run; the CLI
60
+ says so and suggests handing off for human review
61
+ (`lumo task update <id> --status in_review`). No server write happens.
62
+ - **A round must cover every MACHINE criterion** — the CLI always runs all of
63
+ them; the server rejects partial rounds.
64
+ - Criteria added during review (`REVIEW_ADDED`) appear in the contract and
65
+ are picked up automatically by the next round.
66
+
67
+ ## Round discipline
68
+
69
+ Rounds are a hard budget of 3, not a retry loop. Between rounds, actually fix
70
+ the failures — re-running without changes burns a round and (at round 3)
71
+ pages a human. A FAIL round never changes task status; only an all-pass round
72
+ moves it (to IN_REVIEW, never further).
@@ -7,6 +7,13 @@ const doc_input_1 = require("../lib/doc-input");
7
7
  const path_guard_1 = require("../lib/path-guard");
8
8
  const sanitize_1 = require("../lib/sanitize");
9
9
  const task_criteria_list_1 = require("./task-criteria-list");
10
+ const CAUSE_TAGS = [
11
+ 'NEW_INFO',
12
+ 'SCOPE_CHANGE',
13
+ 'DRAFT_BLIND_SPOT',
14
+ 'GRANULARITY',
15
+ 'OTHER',
16
+ ];
10
17
  /** Client-side shape gate: fail fast on obviously malformed JSON before the
11
18
  * round-trip. Full validation (statement length, MACHINE→checkpointer …)
12
19
  * stays server-side. */
@@ -69,6 +76,18 @@ async function taskCriteriaSet(identifier, options) {
69
76
  console.error(`Error: ${parsed.error}`);
70
77
  return 1;
71
78
  }
79
+ let causeTag;
80
+ if (options.cause !== undefined) {
81
+ if (!options.human) {
82
+ console.error('Error: --cause annotates contract drift and requires --human.');
83
+ return 1;
84
+ }
85
+ causeTag = options.cause.toUpperCase();
86
+ if (!CAUSE_TAGS.includes(causeTag)) {
87
+ console.error(`Error: invalid --cause "${options.cause}". One of: ${CAUSE_TAGS.join(' | ')}`);
88
+ return 1;
89
+ }
90
+ }
72
91
  const creds = (0, config_1.readCredentials)();
73
92
  if (!creds) {
74
93
  console.error('Error: not logged in. Run `lumo auth login` first.');
@@ -93,6 +112,7 @@ async function taskCriteriaSet(identifier, options) {
93
112
  body: JSON.stringify({
94
113
  source: options.human ? 'HUMAN_EDIT' : 'AGENT_DRAFT',
95
114
  criteria: parsed.items,
115
+ ...(causeTag ? { causeTag } : {}),
96
116
  }),
97
117
  });
98
118
  }
@@ -0,0 +1,206 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runCheckpointer = runCheckpointer;
4
+ exports.verify = verify;
5
+ const child_process_1 = require("child_process");
6
+ const config_1 = require("../lib/config");
7
+ const api_1 = require("../lib/api");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ const acceptance_evidence_1 = require("../../../shared/src/acceptance-evidence");
10
+ const DEFAULT_TIMEOUT_SECONDS = 600;
11
+ const OUTPUT_TAIL_CHARS = 1_500;
12
+ const MAX_OUTPUT_BUFFER = 10 * 1024 * 1024;
13
+ function tail(s, max) {
14
+ return s.length > max ? `…${s.slice(-max)}` : s;
15
+ }
16
+ /**
17
+ * Execute one MACHINE checkpointer in the local repo (执行在客户端 — the
18
+ * server can't run repo tests) and fold the result into a structured verdict.
19
+ * Exit 0 = PASS with a `cmd:` evidence pointer; non-zero = CRITERION_UNMET;
20
+ * spawn failure or timeout = CHECK_EXECUTION_ERROR.
21
+ */
22
+ function runCheckpointer(criterionId, checkpointer, timeoutMs) {
23
+ const r = (0, child_process_1.spawnSync)(checkpointer, {
24
+ shell: true,
25
+ encoding: 'utf8',
26
+ timeout: timeoutMs,
27
+ maxBuffer: MAX_OUTPUT_BUFFER,
28
+ cwd: process.cwd(),
29
+ });
30
+ if (r.error) {
31
+ const timedOut = r.error.code === 'ETIMEDOUT' ||
32
+ r.signal === 'SIGTERM';
33
+ return {
34
+ criterionId,
35
+ verdict: 'FAIL',
36
+ rejectionReason: timedOut
37
+ ? `checkpointer timed out after ${Math.round(timeoutMs / 1000)}s: ${checkpointer}`
38
+ : `checkpointer failed to execute: ${r.error.message}`,
39
+ rejectionReasonEnum: 'CHECK_EXECUTION_ERROR',
40
+ };
41
+ }
42
+ const exitCode = r.status ?? 1;
43
+ if (exitCode === 0) {
44
+ return {
45
+ criterionId,
46
+ verdict: 'PASS',
47
+ evidencePointer: (0, acceptance_evidence_1.buildCmdEvidencePointer)(checkpointer, 0),
48
+ };
49
+ }
50
+ const output = `${r.stdout ?? ''}\n${r.stderr ?? ''}`.trim();
51
+ return {
52
+ criterionId,
53
+ verdict: 'FAIL',
54
+ evidencePointer: (0, acceptance_evidence_1.buildCmdEvidencePointer)(checkpointer, exitCode),
55
+ rejectionReason: tail(output, OUTPUT_TAIL_CHARS) || `exit code ${exitCode}`,
56
+ rejectionReasonEnum: 'CRITERION_UNMET',
57
+ };
58
+ }
59
+ /**
60
+ * `lumo verify [task]` — the machine half of the acceptance loop.
61
+ *
62
+ * Runs every MACHINE criterion's checkpointer locally, reports one structured
63
+ * verdict per criterion to the server (the judge: round numbering, the
64
+ * 3-round cap, escalation, and the IN_REVIEW transition all live there), and
65
+ * prints what to do next. Defaults to the session-bound task; an explicit
66
+ * identifier overrides.
67
+ */
68
+ async function verify(identifier, options = {}) {
69
+ const creds = (0, config_1.readCredentials)();
70
+ if (!creds) {
71
+ console.error('Error: not logged in. Run `lumo auth login` first.');
72
+ return 1;
73
+ }
74
+ const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
75
+ const headers = {
76
+ Authorization: `Bearer ${creds.token}`,
77
+ };
78
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
79
+ if (sessionId)
80
+ headers['X-Lumo-Session-Id'] = sessionId;
81
+ const timeoutSeconds = options.timeout
82
+ ? parseInt(options.timeout, 10)
83
+ : DEFAULT_TIMEOUT_SECONDS;
84
+ if (!Number.isFinite(timeoutSeconds) || timeoutSeconds <= 0) {
85
+ console.error('Error: --timeout must be a positive number of seconds.');
86
+ return 1;
87
+ }
88
+ // ── Resolve the task: explicit identifier or the session binding ─────────
89
+ let taskId = identifier;
90
+ if (!taskId) {
91
+ if (!sessionId) {
92
+ console.error('Error: no task given and $CLAUDE_CODE_SESSION_ID is not set.\n' +
93
+ 'Run `lumo verify <LUM-N>` or run inside a Claude Code session bound via `lumo session attach`.');
94
+ return 1;
95
+ }
96
+ let res;
97
+ try {
98
+ res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
99
+ }
100
+ catch (err) {
101
+ const msg = err instanceof Error ? err.message : String(err);
102
+ console.error(`Error: could not reach Lumo API (${msg})`);
103
+ return 1;
104
+ }
105
+ const data = res.ok
106
+ ? (await res.json())
107
+ : null;
108
+ if (!data?.taskIdentifier) {
109
+ console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first, or pass the task explicitly: `lumo verify <LUM-N>`.');
110
+ return 1;
111
+ }
112
+ taskId = data.taskIdentifier;
113
+ }
114
+ // ── Load the contract and pick out the MACHINE criteria ─────────────────
115
+ let criteriaRes;
116
+ try {
117
+ criteriaRes = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/criteria`, { headers });
118
+ }
119
+ catch (err) {
120
+ const msg = err instanceof Error ? err.message : String(err);
121
+ console.error(`Error: could not reach Lumo API (${msg})`);
122
+ return 1;
123
+ }
124
+ if (criteriaRes.status === 401) {
125
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
126
+ return 1;
127
+ }
128
+ if (criteriaRes.status === 404) {
129
+ console.error(`Error: task ${taskId} not found in workspace ${creds.workspaceSlug}`);
130
+ return 1;
131
+ }
132
+ if (!criteriaRes.ok) {
133
+ console.error(`Error: could not load criteria (HTTP ${criteriaRes.status})`);
134
+ return 1;
135
+ }
136
+ const { criteria } = (await criteriaRes.json());
137
+ const machine = criteria.filter(c => c.verifierType === 'MACHINE' && c.checkpointer);
138
+ if (criteria.length === 0) {
139
+ process.stdout.write(`${taskId} has no acceptance contract yet — draft one with \`lumo task criteria set\` before verifying.\n`);
140
+ return 1;
141
+ }
142
+ if (machine.length === 0) {
143
+ process.stdout.write(`${taskId} has no MACHINE criteria — nothing for the machine loop to run.\n` +
144
+ `The contract is HUMAN-only; finish your work and hand off for human review (lumo task update ${taskId} --status in_review).\n`);
145
+ return;
146
+ }
147
+ // ── Execute every checkpointer locally ───────────────────────────────────
148
+ process.stdout.write(`Verifying ${taskId} — ${machine.length} MACHINE criteria\n`);
149
+ const results = [];
150
+ for (const [i, c] of machine.entries()) {
151
+ process.stdout.write(`\n[${i + 1}/${machine.length}] ${(0, sanitize_1.sanitizeField)(c.statement)}\n` +
152
+ ` $ ${(0, sanitize_1.sanitizeField)(c.checkpointer)}\n`);
153
+ const verdict = runCheckpointer(c.id, c.checkpointer, timeoutSeconds * 1000);
154
+ results.push(verdict);
155
+ if (verdict.verdict === 'PASS') {
156
+ process.stdout.write(' ✓ PASS\n');
157
+ }
158
+ else {
159
+ process.stdout.write(` ✗ FAIL${verdict.rejectionReason ? ` — ${(0, sanitize_1.sanitizeField)(tail(verdict.rejectionReason, 400))}` : ''}\n`);
160
+ }
161
+ }
162
+ // ── Report the round to the judge ─────────────────────────────────────────
163
+ let res;
164
+ try {
165
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/verify`, {
166
+ method: 'POST',
167
+ headers: { ...headers, 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({ results }),
169
+ });
170
+ }
171
+ catch (err) {
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ console.error(`Error: could not report the round to Lumo (${msg})`);
174
+ return 1;
175
+ }
176
+ if (!res.ok) {
177
+ const body = (await res.json().catch(() => null));
178
+ const detail = body && typeof body.error === 'string' ? (0, sanitize_1.sanitizeField)(body.error) : '';
179
+ console.error(`Error: verification round rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
180
+ return 1;
181
+ }
182
+ const outcome = (await res.json());
183
+ process.stdout.write(`\nRound ${outcome.round}/${outcome.maxRounds} recorded.\n`);
184
+ if (outcome.allPassed) {
185
+ process.stdout.write(`✓ All MACHINE criteria passed — task is now ${outcome.taskStatus}.\n` +
186
+ `Stop here: human adjudication (and any HUMAN criteria) take over from this point.\n`);
187
+ return;
188
+ }
189
+ if (outcome.escalated) {
190
+ process.stdout.write(`✗ Round ${outcome.round} still has failures — the machine loop is exhausted (cap ${outcome.maxRounds}).\n` +
191
+ `A human has been notified to take over. STOP retrying lumo verify; fix only what they direct.\n`);
192
+ return 1;
193
+ }
194
+ process.stdout.write(`✗ ${outcome.nextActions.length} criteria unmet — task stays ${outcome.taskStatus}. Next actions:\n`);
195
+ for (const a of outcome.nextActions) {
196
+ process.stdout.write(` • ${(0, sanitize_1.sanitizeField)(a.statement)}\n`);
197
+ if (a.checkpointer) {
198
+ process.stdout.write(` check: ${(0, sanitize_1.sanitizeField)(a.checkpointer)}\n`);
199
+ }
200
+ if (a.rejectionReason) {
201
+ process.stdout.write(` why: ${(0, sanitize_1.sanitizeField)(tail(a.rejectionReason, 400))}\n`);
202
+ }
203
+ }
204
+ process.stdout.write(`Fix the failures, then re-run \`lumo verify\` (${outcome.maxRounds - outcome.round} round${outcome.maxRounds - outcome.round === 1 ? '' : 's'} left).\n`);
205
+ return 1;
206
+ }
@@ -47,6 +47,7 @@ const session_detach_1 = require("./commands/session-detach");
47
47
  const session_status_1 = require("./commands/session-status");
48
48
  const session_wrap_1 = require("./commands/session-wrap");
49
49
  const next_1 = require("./commands/next");
50
+ const verify_1 = require("./commands/verify");
50
51
  const task_context_1 = require("./commands/task-context");
51
52
  const task_create_1 = require("./commands/task-create");
52
53
  const task_update_1 = require("./commands/task-update");
@@ -196,6 +197,11 @@ program
196
197
  .option('--force', 'Overwrite existing skill files (SKILL.md + references/) when they differ from the bundled version')
197
198
  .option('--agent <token>', 'Coding agent these hooks run under (claude-code, codex, cursor, gemini-cli, github-copilot, windsurf). Baked into every hook command. Defaults to claude-code.')
198
199
  .action(wrap(options => (0, setup_1.setup)(options)));
200
+ program
201
+ .command('verify [task]')
202
+ .description('Machine verification loop (LUM-343): run every MACHINE criterion checkpointer locally, report structured verdicts to the server (round cap 3), and print next actions. All-pass moves the task to IN_REVIEW. Defaults to the session-bound task.')
203
+ .option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
204
+ .action(wrap((task, options) => (0, verify_1.verify)(task, options)));
199
205
  program
200
206
  .command('next')
201
207
  .description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
@@ -380,6 +386,7 @@ taskCriteria
380
386
  .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).')
381
387
  .requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
382
388
  .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session 出处')
389
+ .option('--cause <tag>', 'Why the contract drifted (with --human): NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER')
383
390
  .action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
384
391
  taskCriteria
385
392
  .command('list <task>')
@@ -0,0 +1,42 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = void 0;
4
+ exports.isValidEvidencePointer = isValidEvidencePointer;
5
+ exports.buildCmdEvidencePointer = buildCmdEvidencePointer;
6
+ /**
7
+ * Evidence-pointer grammar for acceptance verification (LUM-343).
8
+ *
9
+ * A VerificationRun's evidencePointer is NOT free text — free-text evidence
10
+ * from an agent-controlled path would reopen the side channel the verdict
11
+ * red line closed (agent-containment absorption, LUM-343 review note). The
12
+ * pointer must take one of the enumerated shapes below. Single source of
13
+ * truth shared by the CLI (which builds pointers) and the server validation
14
+ * layer (which rejects everything else).
15
+ *
16
+ * Shapes:
17
+ * - `commit:<7-40 hex>` — a commit the evidence lives in
18
+ * - `file:<path>:<line>[-<line>]` — a file location (path:line, no spaces)
19
+ * - `cmd:<command>#exit=<code>` — an executed check plus its exit status
20
+ */
21
+ exports.EVIDENCE_POINTER_PATTERNS = [
22
+ /^commit:[0-9a-f]{7,40}$/,
23
+ /^file:[^\s]+:\d+(-\d+)?$/,
24
+ /^cmd:[\s\S]+#exit=\d+$/,
25
+ ];
26
+ exports.EVIDENCE_POINTER_FORMAT_HINT = 'evidencePointer must be one of: commit:<hash>, file:<path>:<line>, cmd:<command>#exit=<code>';
27
+ function isValidEvidencePointer(value) {
28
+ return exports.EVIDENCE_POINTER_PATTERNS.some(p => p.test(value));
29
+ }
30
+ /** Max stored pointer length — mirrors the column-level cap in validation. */
31
+ exports.EVIDENCE_POINTER_MAX = 2_000;
32
+ /**
33
+ * Build a `cmd:` evidence pointer for an executed checkpointer. The command
34
+ * is truncated so the suffix (`#exit=N`) always survives the length cap —
35
+ * a pointer that loses its exit marker would no longer parse as evidence.
36
+ */
37
+ function buildCmdEvidencePointer(command, exitCode) {
38
+ const suffix = `#exit=${exitCode}`;
39
+ const budget = exports.EVIDENCE_POINTER_MAX - 'cmd:'.length - suffix.length;
40
+ const cmd = command.length > budget ? command.slice(0, budget) : command;
41
+ return `cmd:${cmd}${suffix}`;
42
+ }
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // ── Agent Error types ────────────────────────────────────────────────────────
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.sanitizeField = exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
4
+ exports.buildCmdEvidencePointer = exports.isValidEvidencePointer = exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = exports.sanitizeField = exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
5
5
  exports.userFriendlyError = userFriendlyError;
6
6
  class AgentError extends Error {
7
7
  code;
@@ -39,3 +39,10 @@ Object.defineProperty(exports, "parseStreamJsonUsage", { enumerable: true, get:
39
39
  // ── Untrusted free-text sanitization ─────────────────────────────────────────
40
40
  var sanitize_1 = require("./sanitize");
41
41
  Object.defineProperty(exports, "sanitizeField", { enumerable: true, get: function () { return sanitize_1.sanitizeField; } });
42
+ // ── Acceptance verification evidence-pointer grammar ─────────────────────────
43
+ var acceptance_evidence_1 = require("./acceptance-evidence");
44
+ Object.defineProperty(exports, "EVIDENCE_POINTER_PATTERNS", { enumerable: true, get: function () { return acceptance_evidence_1.EVIDENCE_POINTER_PATTERNS; } });
45
+ Object.defineProperty(exports, "EVIDENCE_POINTER_FORMAT_HINT", { enumerable: true, get: function () { return acceptance_evidence_1.EVIDENCE_POINTER_FORMAT_HINT; } });
46
+ Object.defineProperty(exports, "EVIDENCE_POINTER_MAX", { enumerable: true, get: function () { return acceptance_evidence_1.EVIDENCE_POINTER_MAX; } });
47
+ Object.defineProperty(exports, "isValidEvidencePointer", { enumerable: true, get: function () { return acceptance_evidence_1.isValidEvidencePointer; } });
48
+ Object.defineProperty(exports, "buildCmdEvidencePointer", { enumerable: true, get: function () { return acceptance_evidence_1.buildCmdEvidencePointer; } });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.23.0",
3
+ "version": "1.24.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",