@lumoai/cli 1.20.2 → 1.22.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 详情", "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 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 详情", "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".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -25,6 +25,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
25
25
  | `task context`, retrieval (`slack/web/figma context`, `comments list`, `pr show`) | [references/task-context.md](references/task-context.md) |
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
+ | `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
28
29
  | `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
29
30
  | `doc*` | [references/docs.md](references/docs.md) |
30
31
  | `sprint*` | [references/sprints.md](references/sprints.md) |
@@ -60,6 +61,11 @@ The command catalog below is a **map**: it lists every command grouped by domain
60
61
  - `lumo task show <id>` — print one task's detail
61
62
  - `lumo task comment <id> <body>` — leave a comment
62
63
 
64
+ **Acceptance criteria(验收合约)** — see [criteria.md](references/criteria.md)
65
+
66
+ - `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)
67
+ - `lumo task criteria list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
68
+
63
69
  **Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
64
70
 
65
71
  - `lumo task artifact add/update/list/show/rm` — record spec/plan products on a task
@@ -114,6 +120,7 @@ Typical flow when a user says "help me with LUM-42":
114
120
  1. `lumo session attach LUM-42` — bind this session
115
121
  2. `lumo task context LUM-42` — load background
116
122
  3. Review unresolved items, PR-review todos, and the task description
117
- 4. Begin working on the task
123
+ 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
124
+ 5. Begin working on the task
118
125
 
119
126
  **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.
@@ -0,0 +1,124 @@
1
+ # Acceptance criteria(验收合约)
2
+
3
+ The acceptance contract is a small set of structured criteria the task's work
4
+ will be verified against (Acceptance v1, LUM-341/342). The agent drafts it;
5
+ the server validates and stores it; verification rounds (`lumo verify`,
6
+ Slice 1 任务 ③) judge against it. Criteria are injected at session start and
7
+ in `lumo task context` as the `## 验收标准(合约)` section.
8
+
9
+ ## When to draft — the golden rule
10
+
11
+ **Attach → read context → draft criteria → THEN write the first line of code.**
12
+
13
+ If `lumo task context` / session-start shows 该任务尚无验收标准 instead of a
14
+ contract, you MUST draft and submit criteria before starting implementation:
15
+
16
+ 1. Read the task description, comments, linked resources, and memory first —
17
+ the contract distills what "done" means, so understand the task before
18
+ writing it.
19
+ 2. Draft **3–7 criteria** (soft range — the server warns outside it but never
20
+ rejects; if you genuinely need more, merge related checks instead).
21
+ 3. Submit with `lumo task criteria set <task> --file <criteria.json>`.
22
+ Submission **locks the contract for agent edits** — get it right before
23
+ submitting, because afterwards only human revisions (`--human`) can change it.
24
+
25
+ Once you (the agent) have started work, you can NOT change your own contract.
26
+ That's by design: the contract is what your work gets judged against, not a
27
+ to-do list you trim to fit what you built.
28
+
29
+ ## How to write good criteria
30
+
31
+ - **Outcome-level definition of done, not micro-steps.** A criterion states a
32
+ verifiable result ("`lumo task criteria set` rejects a second agent draft
33
+ with 409"), not a task step ("add a check in the service").
34
+ - **Repo-wide baselines don't take slots.** Tests pass / `tsc --noEmit` clean /
35
+ i18n locale parity / lint are already required by the repo's PR checklist —
36
+ never spend one of your 3–7 criteria on them.
37
+ - **MACHINE vs HUMAN:**
38
+ - `MACHINE` = an executable check exists. It **must** carry a `checkpointer`
39
+ (the runnable check: a test command, script, probe…). The checkpointer
40
+ runs on the agent's machine; the structured verdict is reported back to
41
+ the server (执行在客户端,裁判在服务端).
42
+ - `HUMAN` = needs human judgment (UX feel, copy tone, design fidelity).
43
+ - **If you can't attach a checkpointer to a MACHINE criterion, demote it to
44
+ HUMAN** — the server rejects checkpointer-less MACHINE criteria rather
45
+ than silently rewriting them. Don't invent a fake checkpointer to keep it
46
+ MACHINE.
47
+ - `evidenceRequired: true` marks criteria whose verdict must point at proof
48
+ (a MACHINE PASS always requires evidence regardless of this flag).
49
+
50
+ ## JSON file format
51
+
52
+ `criteria.json` is a JSON array; one object per criterion:
53
+
54
+ ```json
55
+ [
56
+ {
57
+ "statement": "PUT /api/tasks/[id]/criteria rejects a second AGENT_DRAFT submission with 409",
58
+ "verifierType": "MACHINE",
59
+ "checkpointer": "npx jest __tests__/task-criteria.service.test.ts -t 'agent lock'"
60
+ },
61
+ {
62
+ "statement": "The criteria section reads naturally as part of the task statement",
63
+ "verifierType": "HUMAN"
64
+ },
65
+ {
66
+ "statement": "Session-start injection shows the contract ahead of memory",
67
+ "verifierType": "MACHINE",
68
+ "checkpointer": "npx jest __tests__/cli/hook-runner-session-start-stdout.test.ts",
69
+ "evidenceRequired": true
70
+ }
71
+ ]
72
+ ```
73
+
74
+ Fields: `statement` (required, ≤2000 chars), `verifierType` (`"MACHINE"` |
75
+ `"HUMAN"`), `checkpointer` (required for MACHINE), `evidenceRequired`
76
+ (optional, default false), `id` (only in `--human` revisions — see below).
77
+
78
+ ## Commands
79
+
80
+ ### `lumo task criteria set <task> --file <criteria.json>`
81
+
82
+ Initial agent draft. Creates the whole contract as `AGENT_DRAFT` at round 0.
83
+ Rejected with 409 once **any** criteria exist on the task — the agent lock.
84
+ Echoes the stored criteria (with ids) plus the 3–7 soft-cap warning if
85
+ outside range.
86
+
87
+ ### `lumo task criteria set <task> --file <criteria.json> --human`
88
+
89
+ Record a **human contract revision** (HUMAN_EDIT), e.g. when the user changes
90
+ the contract in conversation — you transcribe their decision. Allowed at any
91
+ time, even after the agent lock. The file is the **desired final list**:
92
+
93
+ - items carrying an `id` (from `criteria list`) keep that criterion — changed
94
+ fields update it and stamp it `HUMAN_EDIT`; identical fields leave it (and
95
+ its provenance) untouched
96
+ - items without `id` are created as `HUMAN_EDIT`
97
+ - existing criteria missing from the file are **deleted** — refused with 409
98
+ if the criterion already has verification runs (reword it instead)
99
+
100
+ The revision is auto-recorded as a task comment with the session 出处
101
+ (`X-Lumo-Session-Id`). **Transcribing the contract is allowed; transcribing a
102
+ verdict is never** — human verdicts only enter through human-initiated paths
103
+ (web UI / Slack action), and the agent has no write path to them.
104
+
105
+ Only use `--human` for decisions a human actually made (in conversation, in a
106
+ comment, in review). Never use it to work around your own lock.
107
+
108
+ ### `lumo task criteria list <task>`
109
+
110
+ Print the contract: `<id> [MACHINE|HUMAN] SOURCE@rN [evidence] statement`
111
+ plus an indented `↳ check:` line for checkpointers. Use it to fetch ids
112
+ before a `--human` revision. Empty contract prints a drafting pointer.
113
+
114
+ ## Injection behavior
115
+
116
+ - **Session start** (bound task): the contract is the highest-priority
117
+ section in the injection budget — ahead of memory/PR/Slack/Figma/web. If a
118
+ still-open task has no criteria, a 草拟提醒 is injected instead.
119
+ - **`lumo session attach`**: prints the contract (or the 草拟提醒) right after
120
+ binding.
121
+ - **`lumo task context`**: the `## 验收标准(合约)` section appears after the
122
+ task description, before memory.
123
+ - Review-time gap findings (`REVIEW_ADDED`, appended at the round they
124
+ surface) arrive via the verification loop (任务 ③), not via `criteria set`.
@@ -49,6 +49,8 @@ What it does:
49
49
  - Calls `POST /api/sessions/<session_id>/bind-task` on the Lumo server, which sets the Session row's `taskId` and re-tags previously-untagged HookEvent rows in this session.
50
50
  - The binding lives entirely on the server (`Session.taskId`); subsequent hooks read it back via the session row. The CLI keeps no local sentinel.
51
51
 
52
+ - Prints the task's **验收标准(合约)** (acceptance contract, LUM-342) right after the bind confirmation — or, when a still-open task has none, the 草拟提醒 telling you to draft 3–7 criteria before the first line of code (see [criteria.md](criteria.md)). The same section is auto-injected at session start when the session is already bound (highest priority in the injection budget, ahead of memory).
53
+
52
54
  **After attaching, always run `lumo task context <identifier>` to load the task background.**
53
55
 
54
56
  #### Overwriting an existing binding (LUM-266)
@@ -15,14 +15,15 @@ Example: `lumo task context LUM-42`
15
15
  The command prints a markdown document to stdout containing:
16
16
 
17
17
  1. **Task header** — identifier, title, status, description
18
- 2. **Memory section** cross-session learnings accumulated over prior sessions; treat as trusted background context
19
- 3. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below)
20
- 4. **PR Review 待办**mirrored PR review comments as a checkbox todo list: each line-level reviewer comment (shown as `` `file:line` `` + the reviewer's ask + a link to the GitHub comment) and each `changes_requested` review summary (shown as "🛑 整体要求改动"). Present only when the task's PR(s) have review comments. This same block is **auto-injected at session start** (alongside the memory section) when the session is bound to a task — so reviewer asks surface without re-running `task context`.
21
- - The **inline source cards** (Slack / web / Figma / PR) are likewise **auto-injected at session start** when the session is bound, under a single global token budget shared with the memory section (priority: memory > PR > Slack > Figma > web). Cards that don't fit the budget are degraded to a one-line manifest carrying just the title and its `lumo task show` retrieval command so you still know they exist and can pull the full content on demand.
22
- 5. **Previous sessions** ordered newest-first, each with:
18
+ 2. **验收标准(合约)**the task's acceptance criteria (LUM-342), shown right after the header. Each line: `[MACHINE|HUMAN] statement` with a `↳ check:` line for MACHINE checkpointers; HUMAN_EDIT / REVIEW_ADDED provenance is tagged inline. If a still-open task has none, a 草拟提醒 appears instead — draft 3–7 criteria **before writing code** (see [criteria.md](criteria.md))
19
+ 3. **Memory section** — cross-session learnings accumulated over prior sessions; treat as trusted background context
20
+ 4. **Inline source cards**Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below)
21
+ 5. **PR Review 待办** mirrored PR review comments as a checkbox todo list: each line-level reviewer comment (shown as `` `file:line` `` + the reviewer's ask + a link to the GitHub comment) and each `changes_requested` review summary (shown as "🛑 整体要求改动"). Present only when the task's PR(s) have review comments. This same block is **auto-injected at session start** (alongside the memory section) when the session is bound to a task so reviewer asks surface without re-running `task context`.
22
+ - The **inline source cards** (Slack / web / Figma / PR) are likewise **auto-injected at session start** when the session is bound, under a single global token budget shared with the memory section (priority: criteria > memory > PR > Slack > Figma > web). Cards that don't fit the budget are degraded to a one-line manifest carrying just the title and its `lumo task … show` retrieval command — so you still know they exist and can pull the full content on demand.
23
+ 6. **Previous sessions** — ordered newest-first, each with:
23
24
  - A headline summary of what was done
24
25
  - Unresolved items (carry-over TODOs from that session)
25
- 6. **飞轮信号 · 历史 merge 贡献** — appended at the very end. For each context fragment with enough history, a one-line historical merge-contribution signal (e.g. `出现在 5 个已闭合任务·4 merged (80%)`), computed from accumulated lineage edges. Denominator = distinct tasks where the fragment appeared with a resolved (non-UNKNOWN) outcome; only fragments with ≥3 such tasks are shown (cold-start gate), so the block is often absent early on. This is **historical correlation, not causation** — don't read it as a prediction.
26
+ 7. **飞轮信号 · 历史 merge 贡献** — appended at the very end. For each context fragment with enough history, a one-line historical merge-contribution signal (e.g. `出现在 5 个已闭合任务·4 merged (80%)`), computed from accumulated lineage edges. Denominator = distinct tasks where the fragment appeared with a resolved (non-UNKNOWN) outcome; only fragments with ≥3 such tasks are shown (cold-start gate), so the block is often absent early on. This is **historical correlation, not causation** — don't read it as a prediction.
26
27
 
27
28
  ### How to use the context
28
29
 
@@ -112,6 +112,11 @@ 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 first: it's what the upcoming work is judged against (LUM-342).
116
+ if (body.criteriaSection) {
117
+ console.log('');
118
+ console.log((0, sanitize_1.sanitizeField)(body.criteriaSection));
119
+ }
115
120
  if (body.memorySection !== '') {
116
121
  console.log('');
117
122
  console.log((0, sanitize_1.sanitizeField)(body.memorySection));
@@ -70,6 +70,12 @@ function formatTaskContextMarkdown(data, now) {
70
70
  lines.push(`**Description**: ${(0, sanitize_1.sanitizeField)(body)}`);
71
71
  }
72
72
  lines.push('');
73
+ // The acceptance contract leads (LUM-342): it's what the work is judged
74
+ // against, so it reads as part of the task statement itself.
75
+ if (data.criteriaSection && data.criteriaSection.trim().length > 0) {
76
+ lines.push((0, sanitize_1.sanitizeField)(data.criteriaSection.trimEnd()));
77
+ lines.push('');
78
+ }
73
79
  // Frontload memory before sessions: it's cold context the agent should see
74
80
  // first. Server returns "" when empty, in which case we skip the section.
75
81
  if (data.memorySection && data.memorySection.trim().length > 0) {
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatCriteriaRows = formatCriteriaRows;
4
+ exports.taskCriteriaList = taskCriteriaList;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ /**
9
+ * Render criteria rows for stdout. One line per criterion —
10
+ * `<id> [TYPE] SOURCE@rN statement` — plus an indented checkpointer line
11
+ * when present. Shared by `criteria list` and the `criteria set` echo so the
12
+ * agent always sees the ids it needs for a later `--human` revision.
13
+ */
14
+ function formatCriteriaRows(criteria) {
15
+ const lines = [];
16
+ for (const c of criteria) {
17
+ const provenance = `${c.source}@r${c.addedAtRound}`;
18
+ const evidence = c.evidenceRequired ? ' [evidence]' : '';
19
+ lines.push(`${c.id} [${c.verifierType}] ${provenance}${evidence} ${(0, sanitize_1.sanitizeField)(c.statement)}`);
20
+ if (c.checkpointer) {
21
+ lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
22
+ }
23
+ }
24
+ return lines.length > 0 ? lines.join('\n') + '\n' : '';
25
+ }
26
+ /** `lumo task criteria list <task>` — print a task's acceptance contract. */
27
+ async function taskCriteriaList(identifier) {
28
+ if (!identifier) {
29
+ console.error('Error: missing <task>. Usage: lumo task criteria list <LUM-42>');
30
+ return 1;
31
+ }
32
+ const creds = (0, config_1.readCredentials)();
33
+ if (!creds) {
34
+ console.error('Error: not logged in. Run `lumo auth login` first.');
35
+ return 1;
36
+ }
37
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
38
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
39
+ let res;
40
+ try {
41
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, { headers: { Authorization: `Bearer ${creds.token}` } });
42
+ }
43
+ catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
46
+ return 1;
47
+ }
48
+ if (res.status === 401) {
49
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
50
+ return 1;
51
+ }
52
+ if (res.status === 404) {
53
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
54
+ return 1;
55
+ }
56
+ if (!res.ok) {
57
+ console.error(`Error: criteria list failed (HTTP ${res.status})`);
58
+ return 1;
59
+ }
60
+ const data = (await res.json());
61
+ if (data.criteria.length === 0) {
62
+ process.stdout.write(`No acceptance criteria on ${identifier} — draft 3–7 and submit with lumo task criteria set ${identifier} --file <criteria.json>\n`);
63
+ return;
64
+ }
65
+ process.stdout.write(formatCriteriaRows(data.criteria));
66
+ if (data.warning) {
67
+ process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.warning)}\n`);
68
+ }
69
+ }
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.taskCriteriaSet = taskCriteriaSet;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const doc_input_1 = require("../lib/doc-input");
7
+ const path_guard_1 = require("../lib/path-guard");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ const task_criteria_list_1 = require("./task-criteria-list");
10
+ /** Client-side shape gate: fail fast on obviously malformed JSON before the
11
+ * round-trip. Full validation (statement length, MACHINE→checkpointer …)
12
+ * stays server-side. */
13
+ function parseCriteriaJson(raw) {
14
+ let parsed;
15
+ try {
16
+ parsed = JSON.parse(raw);
17
+ }
18
+ catch {
19
+ return { ok: false, error: 'file is not valid JSON' };
20
+ }
21
+ if (!Array.isArray(parsed) || parsed.length === 0) {
22
+ return {
23
+ ok: false,
24
+ error: 'expected a non-empty JSON array of criteria, e.g. [{"statement":"…","verifierType":"MACHINE","checkpointer":"npx jest …"}]',
25
+ };
26
+ }
27
+ return { ok: true, items: parsed };
28
+ }
29
+ /**
30
+ * `lumo task criteria set <task> --file <criteria.json> [--human]`
31
+ *
32
+ * Submit a task's whole acceptance contract. Default = the agent's initial
33
+ * draft (AGENT_DRAFT, locked once submitted — the server rejects a second
34
+ * agent draft). `--human` records a HUMAN_EDIT revision: the file is the
35
+ * desired final list, items carrying an `id` keep/update that criterion,
36
+ * items without `id` are new, and existing criteria missing from the file
37
+ * are deleted (refused for criteria with recorded verification runs).
38
+ */
39
+ async function taskCriteriaSet(identifier, options) {
40
+ if (!identifier) {
41
+ console.error('Error: missing <task>. Usage: lumo task criteria set <LUM-42> --file criteria.json');
42
+ return 1;
43
+ }
44
+ if (!options.file) {
45
+ console.error('Error: --file <path> is required (a JSON array of criteria).');
46
+ return 1;
47
+ }
48
+ const verdict = (0, path_guard_1.checkArtifactFilePath)(options.file);
49
+ if (!verdict.ok) {
50
+ if (verdict.reason === 'unreadable') {
51
+ console.error(`Error: could not read file ${options.file}`);
52
+ }
53
+ else {
54
+ console.error(`Error: refusing to read ${options.file} — ${verdict.detail}. ` +
55
+ `criteria --file must be a non-sensitive path inside the project directory.`);
56
+ }
57
+ return 1;
58
+ }
59
+ let raw;
60
+ try {
61
+ raw = await (0, doc_input_1.readFileUtf8)(verdict.resolved);
62
+ }
63
+ catch {
64
+ console.error(`Error: could not read file ${options.file}`);
65
+ return 1;
66
+ }
67
+ const parsed = parseCriteriaJson(raw);
68
+ if (!parsed.ok) {
69
+ console.error(`Error: ${parsed.error}`);
70
+ return 1;
71
+ }
72
+ const creds = (0, config_1.readCredentials)();
73
+ if (!creds) {
74
+ console.error('Error: not logged in. Run `lumo auth login` first.');
75
+ return 1;
76
+ }
77
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
78
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
79
+ const headers = {
80
+ Authorization: `Bearer ${creds.token}`,
81
+ 'Content-Type': 'application/json',
82
+ };
83
+ // Session 出处 for HUMAN_EDIT transcriptions — the server records the
84
+ // revision (with this session id) as a task comment.
85
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
86
+ if (sessionId)
87
+ headers['X-Lumo-Session-Id'] = sessionId;
88
+ let res;
89
+ try {
90
+ res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/criteria`, {
91
+ method: 'PUT',
92
+ headers,
93
+ body: JSON.stringify({
94
+ source: options.human ? 'HUMAN_EDIT' : 'AGENT_DRAFT',
95
+ criteria: parsed.items,
96
+ }),
97
+ });
98
+ }
99
+ catch (err) {
100
+ const msg = err instanceof Error ? err.message : String(err);
101
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
102
+ return 1;
103
+ }
104
+ if (res.status === 401) {
105
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
106
+ return 1;
107
+ }
108
+ if (res.status === 404) {
109
+ console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
110
+ return 1;
111
+ }
112
+ if (!res.ok) {
113
+ const body = (await res.json().catch(() => null));
114
+ const detail = body && typeof body.error === 'string' ? (0, sanitize_1.sanitizeField)(body.error) : '';
115
+ console.error(`Error: criteria set failed (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
116
+ return 1;
117
+ }
118
+ const data = (await res.json());
119
+ process.stdout.write(options.human
120
+ ? `Recorded HUMAN_EDIT contract revision on ${identifier} (${data.criteria.length} criteria)\n`
121
+ : `Acceptance contract drafted on ${identifier} (${data.criteria.length} criteria) — locked for agent edits; human revisions via --human\n`);
122
+ process.stdout.write((0, task_criteria_list_1.formatCriteriaRows)(data.criteria));
123
+ if (data.warning) {
124
+ process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.warning)}\n`);
125
+ }
126
+ }
@@ -64,6 +64,8 @@ const memory_project_add_1 = require("./commands/memory-project-add");
64
64
  const memory_promote_1 = require("./commands/memory-promote");
65
65
  const memory_rm_1 = require("./commands/memory-rm");
66
66
  const task_artifact_add_1 = require("./commands/task-artifact-add");
67
+ const task_criteria_set_1 = require("./commands/task-criteria-set");
68
+ const task_criteria_list_1 = require("./commands/task-criteria-list");
67
69
  const task_artifact_list_1 = require("./commands/task-artifact-list");
68
70
  const task_artifact_show_1 = require("./commands/task-artifact-show");
69
71
  const task_artifact_rm_1 = require("./commands/task-artifact-rm");
@@ -343,6 +345,19 @@ taskMemory
343
345
  .option('--step <text>', 'procedural: a step (repeatable)', collect, [])
344
346
  .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
345
347
  .action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
348
+ const taskCriteria = task
349
+ .command('criteria')
350
+ .description('Acceptance criteria — the task\u2019s verification contract (LUM-342)');
351
+ taskCriteria
352
+ .command('set <task>')
353
+ .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).')
354
+ .requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
355
+ .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session 出处')
356
+ .action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
357
+ taskCriteria
358
+ .command('list <task>')
359
+ .description('List a task\u2019s acceptance criteria (id, type, provenance, checkpointer)')
360
+ .action(wrap((taskId) => (0, task_criteria_list_1.taskCriteriaList)(taskId)));
346
361
  const taskArtifact = task
347
362
  .command('artifact')
348
363
  .description('Record spec-engineering artifacts (spec / plan / design …) on a task');
@@ -114,6 +114,9 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
114
114
  const card = renderRecoveryCard(body.previousSession, tb?.taskIdentifier ?? '', now);
115
115
  const envelope = sessionContextEnvelope([
116
116
  card,
117
+ // Acceptance contract right after the recovery card: it's what the
118
+ // session's work is judged against (LUM-342).
119
+ body.criteriaSection,
117
120
  body.memorySection,
118
121
  body.linkedResourcesSection,
119
122
  body.reviewTodosSection,
@@ -216,14 +219,17 @@ function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
216
219
  return [formatSuggestLine(sessionId, match)];
217
220
  }
218
221
  /**
219
- * For stop/stop-failure hooks, read the transcript named in the payload and
220
- * fold cumulative token totals into the body under `_lumo_usage` so the server
221
- * can persist them on the Session. Best-effort: returns the body unchanged on
222
- * any failure (no transcript_path, unreadable, unparseable, no usage). The
223
- * hook is never blocked or failed by this.
222
+ * For stop/stop-failure/post-tool-batch hooks, read the transcript named in
223
+ * the payload and fold cumulative token totals into the body under
224
+ * `_lumo_usage` so the server can persist them on the Session. post-tool-batch
225
+ * carries usage so long single turns surface live burn before the first STOP
226
+ * (LUM-345); the server treats it as a flat-columns-only overwrite. Best-
227
+ * effort: returns the body unchanged on any failure (no transcript_path,
228
+ * unreadable, unparseable, no usage). The hook is never blocked or failed by
229
+ * this.
224
230
  */
225
231
  function augmentBodyWithUsage(path, body) {
226
- if (path !== 'stop' && path !== 'stop-failure')
232
+ if (path !== 'stop' && path !== 'stop-failure' && path !== 'post-tool-batch')
227
233
  return body;
228
234
  let parsed;
229
235
  try {
@@ -1,17 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.sanitizeField = sanitizeField;
4
- // Matches C0 control chars EXCEPT tab (\x09) and newline (\x0a), DEL (\x7f),
5
- // and the C1 control range (\x80-\x9f). Includes ESC (\x1b) and the 8-bit CSI
6
- // introducer (\x9b) — both ANSI escape-sequence injection vectors. U+0080-U+009F
7
- // are never legitimate visible text, so stripping them is safe.
8
- const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
9
- /**
10
- * Strip terminal control characters from untrusted, server-returned fields
11
- * before printing them, to prevent ANSI escape-sequence injection. Tab and
12
- * newline are preserved so fixed-width tables and multi-line bodies render
13
- * correctly. Callers handle optional values, e.g. `sanitizeField(name ?? '')`.
14
- */
15
- function sanitizeField(value) {
16
- return value.replace(CONTROL_CHARS, '');
17
- }
3
+ exports.sanitizeField = void 0;
4
+ // Single source of truth lives in shared/src/sanitize.ts (LUM-346) the
5
+ // server applies the same character class at its untrusted render boundary.
6
+ var sanitize_1 = require("../../../shared/src/sanitize");
7
+ Object.defineProperty(exports, "sanitizeField", { enumerable: true, get: function () { return sanitize_1.sanitizeField; } });
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  // ── Agent Error types ────────────────────────────────────────────────────────
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
- exports.parseStreamJsonUsage = exports.tiptapToMarkdown = exports.markdownToTiptap = exports.AgentError = void 0;
4
+ 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;
@@ -36,3 +36,6 @@ Object.defineProperty(exports, "tiptapToMarkdown", { enumerable: true, get: func
36
36
  // ── Stream-json run usage parser ──────────────────────────────────────────────
37
37
  var stream_json_usage_1 = require("./stream-json-usage");
38
38
  Object.defineProperty(exports, "parseStreamJsonUsage", { enumerable: true, get: function () { return stream_json_usage_1.parseStreamJsonUsage; } });
39
+ // ── Untrusted free-text sanitization ─────────────────────────────────────────
40
+ var sanitize_1 = require("./sanitize");
41
+ Object.defineProperty(exports, "sanitizeField", { enumerable: true, get: function () { return sanitize_1.sanitizeField; } });
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sanitizeField = sanitizeField;
4
+ // Matches C0 control chars EXCEPT tab (\x09) and newline (\x0a), DEL (\x7f),
5
+ // and the C1 control range (\x80-\x9f). Includes ESC (\x1b) and the 8-bit CSI
6
+ // introducer (\x9b) — both ANSI escape-sequence injection vectors. U+0080-U+009F
7
+ // are never legitimate visible text, so stripping them is safe.
8
+ const CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f-\x9f]/g;
9
+ /**
10
+ * Strip terminal control characters from untrusted free text before it is
11
+ * printed (CLI) or injected into an agent's context (server render boundary),
12
+ * to prevent ANSI escape-sequence injection. Tab and newline are preserved so
13
+ * fixed-width tables and multi-line bodies render correctly. ids / urls /
14
+ * enums don't need this — only free text does. Callers handle optional
15
+ * values, e.g. `sanitizeField(name ?? '')`.
16
+ *
17
+ * Single source of truth shared by the CLI (cli/src/lib/sanitize.ts) and the
18
+ * server (lib/utils/sanitize.ts) so the two sides can never drift (LUM-346).
19
+ */
20
+ function sanitizeField(value) {
21
+ return value.replace(CONTROL_CHARS, '');
22
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.20.2",
3
+ "version": "1.22.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",