@lumoai/cli 1.21.0 → 1.23.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 详情", "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".'
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,20 @@ 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
+ **Task dependencies**
65
+
66
+ - `lumo task deps list <id>` — list dependency edges in both directions, grouped CONFIRMED / SUGGESTED(待确认) / DISMISSED; each row shows a short edge id `[xxxxxxxx]`, the other task, and detected evidence (`shared_files(N 个共享文件: …)` / `task_mention(…)`); SUGGESTED rows include a copy-pasteable confirm hint
67
+ - `lumo task deps add <id> --blocked-by <LUM-N>` — declare a manual hard dependency (created CONFIRMED, source MANUAL; if a SUGGESTED/DISMISSED edge already exists in the same direction, it is confirmed in place rather than creating a new row)
68
+ - `lumo task deps confirm <id> <edge> [--reverse]` — confirm a detected candidate; `<edge>` is a short edge-id prefix (≥6 chars) or the other task's identifier (case-insensitive); `--reverse` flips the direction when the detector guessed it backwards
69
+ - `lumo task deps dismiss <id> <edge>` — dismiss a candidate (never re-suggested)
70
+ - `lumo task deps rm <id> <edge> --yes` — delete an edge (refuses without `--yes`)
71
+ - On an ambiguous/unknown `<edge>` selector the CLI prints all candidates with short ids and exits 1 — retry with one of them
72
+
73
+ **Acceptance criteria(验收合约)** — see [criteria.md](references/criteria.md)
74
+
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 list <task>` — print the contract (id, MACHINE/HUMAN, provenance source@round, checkpointer)
77
+
63
78
  **Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
64
79
 
65
80
  - `lumo task artifact add/update/list/show/rm` — record spec/plan products on a task
@@ -114,6 +129,7 @@ Typical flow when a user says "help me with LUM-42":
114
129
  1. `lumo session attach LUM-42` — bind this session
115
130
  2. `lumo task context LUM-42` — load background
116
131
  3. Review unresolved items, PR-review todos, and the task description
117
- 4. Begin working on the task
132
+ 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
+ 5. Begin working on the task
118
134
 
119
135
  **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`.
@@ -35,6 +35,46 @@ When the session is bound, session-start may inject a **"🆕 待核对:上次
35
35
 
36
36
  Attribution requires the CC session id to reach the server: `lumo task update <id> --status done` automatically sends `CLAUDE_CODE_SESSION_ID` (via an `X-Lumo-Session-Id` header) so the resulting Layer 2 memories are attributed to the session. Marking a task done from the **web UI** leaves them unattributed (they won't surface for review) — that's expected.
37
37
 
38
+ ### Blocker alert injected at `session attach` / session-start
39
+
40
+ When the bound task has live blockers or SUGGESTED dependency candidates, the attach output and session-start hook context inject a dependency warning block. The warning is built by `buildBlockerWarningSectionForTask` and is **omitted entirely** (empty string) when nothing is actionable — no output appears when there are no live blockers and no SUGGESTED candidates.
41
+
42
+ **Trigger conditions (OR — either or both may appear):**
43
+ 1. At least one CONFIRMED "blocked by" edge where the blocking task's status is not DONE.
44
+ 2. At least one SUGGESTED (unconfirmed) dependency candidate on the task.
45
+
46
+ **Output form A — live blockers exist (with or without SUGGESTED candidates):**
47
+
48
+ Starts with the `## ⚠ 依赖告警` header, followed by one line per live blocker, then the advice line. If there are also SUGGESTED candidates the candidate-hint line is appended after the advice line.
49
+
50
+ ```
51
+ ## ⚠ 依赖告警
52
+
53
+ - 本任务被 LUM-9「Fix auth token expiry」阻塞(状态 IN_PROGRESS)。
54
+ - 本任务被 LUM-15「Upgrade Postgres driver」阻塞(状态 IN_REVIEW,PR #42 未 merge)。
55
+
56
+ 建议先等 blocker 合并再开工,避免白跑 run;如边已过时,用 `lumo task deps rm/dismiss` 清理。
57
+ 检测到 3 条候选依赖待确认:运行 `lumo task deps list LUM-42`。
58
+ ```
59
+
60
+ **Output form B — NO live blockers, but SUGGESTED candidates exist:**
61
+
62
+ Output is **only** the hint line — no `## ⚠ 依赖告警` header, no advice line. Design rationale: 纯候选是提示不是告警,不戴 ⚠ 头,避免稀释真告警。
63
+
64
+ ```
65
+ 检测到 3 条候选依赖待确认:运行 `lumo task deps list LUM-42`。
66
+ ```
67
+
68
+ - Each blocker line shows: identifier, title, current status. If the blocking task has an open pull request, a `,PR #N` note is appended (no space after the comma) — `,PR #N (draft) 未 merge` for draft PRs.
69
+ - The guidance line ("建议先等 blocker 合并…") appears only when there is at least one live blocker (form A).
70
+ - The function never throws — if it fails it silently returns an empty string, leaving the session start unaffected.
71
+
72
+ **Agent guidance — watch for EITHER the `## ⚠ 依赖告警` header (form A) OR the standalone hint line (form B):**
73
+ - **Form A — live blockers:** Evaluate whether to wait. If the blocking task's work overlaps with yours (same files, same API surface), starting immediately risks rework. Read the blocker's status and open PR note before deciding.
74
+ - **Stale or wrong edge?** Run `lumo task deps list <LUM-N>` to inspect the full edge list, then `lumo task deps rm <LUM-N> <edge> --yes` (if manually added and now obsolete) or `lumo task deps dismiss <LUM-N> <edge>` (if a false positive from detection).
75
+ - **Candidate hint present (form A or B)?** Run `lumo task deps list <LUM-N>` and review each SUGGESTED edge — confirm real ones, dismiss false positives. Leaving SUGGESTED edges unreviewed means repeated hints every session.
76
+ - Do **not** blindly start work on a task whose live blocker is still IN_PROGRESS or IN_REVIEW unless the user explicitly decides to proceed in parallel.
77
+
38
78
  ### `lumo session attach <identifier>` — bind the current session to a task
39
79
 
40
80
  Use this whenever the user mentions a task ID. The command is the only way to bind a session to a task.
@@ -49,6 +89,8 @@ What it does:
49
89
  - 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
90
  - 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
91
 
92
+ - 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).
93
+
52
94
  **After attaching, always run `lumo task context <identifier>` to load the task background.**
53
95
 
54
96
  #### 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
 
@@ -203,3 +203,133 @@ lumo task comment LUM-42 "Reproduced the redirect bug on staging — Safari only
203
203
  ```
204
204
 
205
205
  The CLI does not support @-mention chip syntax. If the user wants to ping someone, they should comment from the web UI.
206
+
207
+ ---
208
+
209
+ ## Task Dependencies (`lumo task deps …`)
210
+
211
+ ### `lumo task deps list <LUM-N>` — show all dependency edges
212
+
213
+ Prints the task's dependency edges grouped into three sections: **CONFIRMED**, **SUGGESTED(待确认)**, and **DISMISSED**. Each row includes a short 8-character edge id in square brackets, the direction (`blocked by` / `blocks`), the other task's identifier and title, the other task's current status, the source (`MANUAL` or `DETECTED`), and inline evidence for detected edges.
214
+
215
+ ```bash
216
+ lumo task deps list LUM-42
217
+ ```
218
+
219
+ Example output:
220
+
221
+ ```
222
+ Dependencies for LUM-42 (3)
223
+
224
+ CONFIRMED
225
+ [a1b2c3d4] blocked by LUM-9「Fix auth token expiry」 IN_PROGRESS · MANUAL
226
+
227
+ SUGGESTED(待确认)
228
+ [e5f6a7b8] blocks LUM-55「Migrate DB schema」 TODO · shared_files(4 个共享文件: src/db/schema.ts, src/db/migrate.ts, ...)
229
+ 确认: lumo task deps confirm LUM-42 e5f6a7b8(方向反了加 --reverse;误报: dismiss)
230
+ [c9d0e1f2] blocked by LUM-38「Add OAuth scopes」 IN_REVIEW · task_mention(description)
231
+ 确认: lumo task deps confirm LUM-42 c9d0e1f2(方向反了加 --reverse;误报: dismiss)
232
+
233
+ DISMISSED
234
+ [b3c4d5e6] blocks LUM-12 · 已忽略
235
+ ```
236
+
237
+ CONFIRMED and SUGGESTED rows show the other task's identifier, title, current status, and source/evidence. DISMISSED rows render as `[shortId] <direction> <identifier> · 已忽略` only — no title, status, or source.
238
+
239
+ When there are no edges at all the output is:
240
+ ```
241
+ Dependencies for LUM-42: 无依赖边。
242
+ ```
243
+
244
+ **Evidence fields by detection signal:**
245
+ - `shared_files` — `shared_files(N 个共享文件: path1, path2, …)` — number of shared write-touched files in the 14-day window, plus up to 5 sample paths.
246
+ - `task_mention` — `task_mention(description)` or `task_mention(comment)` — the surface where the mention appeared.
247
+
248
+ CONFIRMED rows also show `source`: `MANUAL` (user-declared via `deps add`) or `DETECTED` (auto-found then confirmed via `deps confirm`).
249
+
250
+ ### `lumo task deps add <LUM-N> --blocked-by <LUM-M>` — declare a manual hard dependency
251
+
252
+ Asserts that LUM-N is blocked by LUM-M. The edge is created as CONFIRMED + MANUAL, meaning it is immediately in effect (no confirmation step required).
253
+
254
+ ```bash
255
+ lumo task deps add LUM-42 --blocked-by LUM-9
256
+ ```
257
+
258
+ Both `<LUM-N>` and `--blocked-by` are required. The command errors on usage if either is missing.
259
+
260
+ **Service semantics (read before using):**
261
+ - **Self-edge** → 400 ("A task cannot depend on itself").
262
+ - **CONFIRMED edge in the same direction already exists** → 409 ("Dependency already exists").
263
+ - **CONFIRMED edge in the reverse direction already exists** → the cycle guard fires and returns 409 ("Dependency would create a cycle").
264
+ - **Pair has a SUGGESTED or DISMISSED edge** in the same direction → `add` confirms it in place, preserving the original DETECTED source (so the edge transitions to CONFIRMED while keeping its auto-detected provenance). No new row is created.
265
+ - **Cycle guard** — the service runs a DFS over all CONFIRMED BLOCKS edges in the workspace before writing. If adding the edge would create a cycle → 409 ("Dependency would create a cycle").
266
+
267
+ ### `lumo task deps confirm <LUM-N> <edge> [--reverse]` — confirm a detected candidate
268
+
269
+ Promotes a SUGGESTED edge to CONFIRMED. `<edge>` is a selector for the specific edge on LUM-N's edge list.
270
+
271
+ ```bash
272
+ lumo task deps confirm LUM-42 e5f6a7b8 # confirm as detected direction
273
+ lumo task deps confirm LUM-42 LUM-55 # selector by other task's identifier
274
+ lumo task deps confirm LUM-42 e5f6a7b8 --reverse # flip direction before confirming
275
+ ```
276
+
277
+ **Edge selector semantics** (shared by `confirm`, `dismiss`, `rm`):
278
+ - **Other task's identifier** (e.g., `LUM-55`) — case-insensitive exact match against the edge's other-task identifier. Resolves unambiguously when there is exactly one edge to that task.
279
+ - **Edge-id prefix** — at least 6 characters of the short id (e.g., `e5f6a7`). Must match exactly one edge.
280
+ - If zero or more than one edge matches → prints all candidate edges with short ids and exits 1. Retry with a more specific selector.
281
+
282
+ **`--reverse` semantics:**
283
+ - The detector's direction heuristic is best-effort. If the suggested direction is backwards (e.g., the detector says "LUM-42 blocks LUM-55" but actually LUM-55 blocks LUM-42), confirm with `--reverse` to flip before writing.
284
+ - The service checks that the reversed pair does not already have an edge (→ 409), and re-runs the cycle guard with the flipped direction.
285
+
286
+ ### `lumo task deps dismiss <LUM-N> <edge>` — dismiss a candidate (immune to re-detection)
287
+
288
+ Marks the edge DISMISSED. The row is kept in the database — this is the key difference from `rm`. Because the row exists (in either direction), the detection service will never re-suggest this pair.
289
+
290
+ ```bash
291
+ lumo task deps dismiss LUM-42 e5f6a7b8
292
+ lumo task deps dismiss LUM-42 LUM-38
293
+ ```
294
+
295
+ Output: `Dismissed: [e5f6a7b8] LUM-38「Add OAuth scopes」(不再建议)`
296
+
297
+ Use `dismiss` for false positives. Use `rm` only when you want the pair to be eligible for re-detection in the future (the detection service can re-suggest pairs with no existing row).
298
+
299
+ ### `lumo task deps rm <LUM-N> <edge> --yes` — delete an edge
300
+
301
+ Hard-deletes the edge row. **Requires `--yes`** — the CLI refuses without it (no interactive prompt exists).
302
+
303
+ ```bash
304
+ lumo task deps rm LUM-42 a1b2c3d4 --yes
305
+ ```
306
+
307
+ Output: `Removed [a1b2c3d4] from LUM-42`
308
+
309
+ **`rm` vs `dismiss`:** deleting removes the immunity — the detection service may re-suggest this pair the next time a shared-files sweep or task-mention event fires. Prefer `dismiss` for detection false positives; use `rm` to remove an incorrectly-declared MANUAL edge that you want fully erased.
310
+
311
+ ---
312
+
313
+ ### Detection red lines
314
+
315
+ - The detection service **never creates CONFIRMED edges automatically**. All auto-detected candidates are SUGGESTED; a human must `confirm` or `add` for an edge to become CONFIRMED.
316
+ - **Dismiss is pair-wise immunity**: once any edge exists between task A and task B (in either direction, regardless of status including DISMISSED), the detection service will not create a new candidate for that pair.
317
+ - **`rm` lifts the immunity**: after deleting the only edge between A and B, the detector may re-suggest them on the next event or sweep.
318
+
319
+ ---
320
+
321
+ ### Detection signals
322
+
323
+ **Signal 1 — `task_mention`**: fires when a task's description is updated or a comment is created. If the updated HTML contains @-mentions of other tasks, the mentioning task is recorded as depending on the mentioned task (direction heuristic: mentioner blocked by mentioned). Triggers immediately on write events; no cron needed.
324
+
325
+ **Signal 2 — `shared_files`**: hourly cron sweep. Looks at write-tool hook events (file edits, creates, etc.) in the past 14 days. For every pair of open (non-DONE) tasks that share **≥ 3 written files**, a SUGGESTED edge is created. Direction heuristic: older task blocks newer task. Parent–child task pairs are skipped (they share files by design). Edges are not created across different workspaces.
326
+
327
+ ---
328
+
329
+ ### When to suggest `task deps` commands
330
+
331
+ - **After `session attach` output shows a blocker warning or candidate-count hint** → run `lumo task deps list <LUM-N>` to review the full edge list, then `confirm` or `dismiss` each SUGGESTED candidate.
332
+ - **User says "X needs to wait for Y" or "LUM-42 is blocked by LUM-9"** → run `lumo task deps add LUM-42 --blocked-by LUM-9`.
333
+ - **Agent sees a `## ⚠ 依赖告警` block (form A — live blockers) at session-start** → evaluate whether to wait for the blocker to merge before starting work; if the edge is stale or wrong, clean it with `deps rm` or `deps dismiss`.
334
+ - **Agent sees only a standalone hint line `检测到 N 条候选依赖待确认…` (form B — no live blockers)** → no immediate blocker, but run `lumo task deps list <LUM-N>` to review and confirm/dismiss SUGGESTED candidates. See [sessions.md](sessions.md) for the full alert format.
335
+ - **User reports a false positive dependency suggestion** → `lumo task deps dismiss <LUM-N> <edge>` to permanently suppress it for this pair.
@@ -112,6 +112,16 @@ 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 注入顺序一致,短且可操作的信息优先。
116
+ if (body.blockerWarningSection) {
117
+ console.log('');
118
+ console.log((0, sanitize_1.sanitizeField)(body.blockerWarningSection));
119
+ }
120
+ // Contract next: it's what the upcoming work is judged against (LUM-342).
121
+ if (body.criteriaSection) {
122
+ console.log('');
123
+ console.log((0, sanitize_1.sanitizeField)(body.criteriaSection));
124
+ }
115
125
  if (body.memorySection !== '') {
116
126
  console.log('');
117
127
  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
+ }
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveEdgeSelector = resolveEdgeSelector;
4
+ exports.formatDepsList = formatDepsList;
5
+ exports.taskDepsList = taskDepsList;
6
+ exports.taskDepsAdd = taskDepsAdd;
7
+ exports.taskDepsConfirm = taskDepsConfirm;
8
+ exports.taskDepsDismiss = taskDepsDismiss;
9
+ exports.taskDepsRm = taskDepsRm;
10
+ const config_1 = require("../lib/config");
11
+ const api_1 = require("../lib/api");
12
+ const sanitize_1 = require("../lib/sanitize");
13
+ /** 短 id = 前 8 位,展示与 selector 都用它。 */
14
+ const shortId = (id) => id.slice(0, 8);
15
+ /**
16
+ * Resolve a user-supplied edge selector against the task's edge list. Accepts
17
+ * either the other task's identifier (case-insensitive, exact) or an edge-id
18
+ * prefix of at least 6 chars. Returns null when nothing (or more than one
19
+ * edge) matches — callers print the candidate list and bail.
20
+ */
21
+ function resolveEdgeSelector(edges, selector) {
22
+ const s = selector.trim();
23
+ const byIdent = edges.filter(e => e.other.identifier.toUpperCase() === s.toUpperCase());
24
+ if (byIdent.length === 1)
25
+ return byIdent[0] ?? null;
26
+ const byPrefix = edges.filter(e => e.id.startsWith(s));
27
+ if (s.length >= 6 && byPrefix.length === 1)
28
+ return byPrefix[0] ?? null;
29
+ return null;
30
+ }
31
+ /**
32
+ * Render the dependency edges of a task grouped by status. SUGGESTED rows get
33
+ * a copy-pasteable confirm hint with the short edge id; detected evidence
34
+ * (shared files / task mention) is summarized inline.
35
+ */
36
+ function formatDepsList(identifier, edges) {
37
+ if (edges.length === 0)
38
+ return `Dependencies for ${identifier}: 无依赖边。`;
39
+ const lines = [
40
+ `Dependencies for ${identifier} (${edges.length})`,
41
+ '',
42
+ ];
43
+ const dirLabel = (e) => e.direction === 'BLOCKED_BY' ? 'blocked by' : 'blocks';
44
+ const evidence = (e) => {
45
+ if (e.reason === 'shared_files')
46
+ return ` · shared_files(${e.detail?.count ?? '?'} 个共享文件${e.detail?.sample?.length ? ': ' + e.detail.sample.join(', ') : ''})`;
47
+ if (e.reason === 'task_mention')
48
+ return ` · task_mention(${e.detail?.surface ?? ''})`;
49
+ return '';
50
+ };
51
+ const section = (title, rows, render) => {
52
+ if (rows.length === 0)
53
+ return;
54
+ lines.push(title);
55
+ for (const e of rows)
56
+ lines.push(...render(e));
57
+ lines.push('');
58
+ };
59
+ const confirmed = edges.filter(e => e.status === 'CONFIRMED');
60
+ const suggested = edges.filter(e => e.status === 'SUGGESTED');
61
+ const dismissed = edges.filter(e => e.status === 'DISMISSED');
62
+ section('CONFIRMED', confirmed, e => [
63
+ ` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier}「${e.other.title}」 ${e.other.status} · ${e.source}${evidence(e)}`,
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)`,
68
+ ]);
69
+ section('DISMISSED', dismissed, e => [
70
+ ` [${shortId(e.id)}] ${dirLabel(e)} ${e.other.identifier} · 已忽略`,
71
+ ]);
72
+ return lines.join('\n').trimEnd();
73
+ }
74
+ function getCtx() {
75
+ const creds = (0, config_1.readCredentials)();
76
+ if (!creds) {
77
+ console.error('Error: not logged in. Run `lumo auth login` first.');
78
+ return 1;
79
+ }
80
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
81
+ return {
82
+ apiUrl,
83
+ base: (0, api_1.trimTrailingSlash)(apiUrl),
84
+ token: creds.token,
85
+ workspaceSlug: creds.workspaceSlug,
86
+ };
87
+ }
88
+ /** Resolve LUM-N → DB id (the dependencies endpoints take the DB id). */
89
+ async function resolveTask(ctx, identifier) {
90
+ let res;
91
+ try {
92
+ res = await fetch(`${ctx.base}/api/tasks/resolve/${encodeURIComponent(identifier)}`, { headers: { Authorization: `Bearer ${ctx.token}` } });
93
+ }
94
+ catch (err) {
95
+ const msg = err instanceof Error ? err.message : String(err);
96
+ console.error(`Error: could not reach Lumo API at ${ctx.apiUrl} (${msg})`);
97
+ return 1;
98
+ }
99
+ if (res.status === 401) {
100
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
101
+ return 1;
102
+ }
103
+ if (res.status === 404) {
104
+ console.error(`Error: task ${identifier} not found in workspace ${ctx.workspaceSlug}`);
105
+ return 1;
106
+ }
107
+ if (!res.ok) {
108
+ console.error(`Error: resolve failed (HTTP ${res.status})`);
109
+ return 1;
110
+ }
111
+ return (await res.json());
112
+ }
113
+ /** Best-effort `{ error }` body extraction for non-2xx responses. */
114
+ async function readErrorMessage(res) {
115
+ try {
116
+ const body = (await res.json());
117
+ return typeof body.error === 'string' ? body.error : null;
118
+ }
119
+ catch {
120
+ return null;
121
+ }
122
+ }
123
+ async function depsFetch(ctx, path, init, label) {
124
+ let res;
125
+ try {
126
+ res = await fetch(`${ctx.base}${path}`, {
127
+ ...init,
128
+ headers: {
129
+ Authorization: `Bearer ${ctx.token}`,
130
+ ...(init.body ? { 'Content-Type': 'application/json' } : {}),
131
+ },
132
+ });
133
+ }
134
+ catch (err) {
135
+ const msg = err instanceof Error ? err.message : String(err);
136
+ console.error(`Error: could not reach Lumo API at ${ctx.apiUrl} (${msg})`);
137
+ return 1;
138
+ }
139
+ if (res.status === 401) {
140
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
141
+ return 1;
142
+ }
143
+ if (!res.ok) {
144
+ const serverMsg = await readErrorMessage(res);
145
+ console.error(serverMsg
146
+ ? `Error: ${(0, sanitize_1.sanitizeField)(serverMsg)}`
147
+ : `Error: ${label} failed (HTTP ${res.status})`);
148
+ return 1;
149
+ }
150
+ return res;
151
+ }
152
+ async function fetchDeps(ctx, taskDbId) {
153
+ const res = await depsFetch(ctx, `/api/tasks/${encodeURIComponent(taskDbId)}/dependencies`, {}, 'dependencies list');
154
+ if (typeof res === 'number')
155
+ return res;
156
+ const { dependencies } = (await res.json());
157
+ return dependencies ?? [];
158
+ }
159
+ /** Selector miss: print all candidates with short ids so the user can retry. */
160
+ function printSelectorCandidates(edges, selector) {
161
+ console.error(`Error: no unique dependency edge matches "${selector}". Candidates:`);
162
+ if (edges.length === 0) {
163
+ console.error(' (无依赖边)');
164
+ return;
165
+ }
166
+ for (const e of edges) {
167
+ console.error((0, sanitize_1.sanitizeField)(` [${shortId(e.id)}] ${e.status} ${e.other.identifier}「${e.other.title}」`));
168
+ }
169
+ }
170
+ /**
171
+ * Shared front half of confirm/dismiss/rm: resolve the task, fetch its edge
172
+ * list, and resolve the selector. Returns the task + edge on success, or an
173
+ * exit code after printing the candidate list.
174
+ */
175
+ async function resolveTaskAndEdge(identifier, selector) {
176
+ const ctx = getCtx();
177
+ if (typeof ctx === 'number')
178
+ return ctx;
179
+ const task = await resolveTask(ctx, identifier);
180
+ if (typeof task === 'number')
181
+ return task;
182
+ const edges = await fetchDeps(ctx, task.id);
183
+ if (typeof edges === 'number')
184
+ return edges;
185
+ const edge = resolveEdgeSelector(edges, selector);
186
+ if (!edge) {
187
+ printSelectorCandidates(edges, selector);
188
+ return 1;
189
+ }
190
+ return { ctx, task, edge };
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Command actions
194
+ // ---------------------------------------------------------------------------
195
+ /** `lumo task deps list <LUM-N>` */
196
+ async function taskDepsList(identifier) {
197
+ if (!identifier) {
198
+ console.error('Error: usage: lumo task deps list <LUM-42>');
199
+ return 1;
200
+ }
201
+ const ctx = getCtx();
202
+ if (typeof ctx === 'number')
203
+ return ctx;
204
+ const task = await resolveTask(ctx, identifier);
205
+ if (typeof task === 'number')
206
+ return task;
207
+ const edges = await fetchDeps(ctx, task.id);
208
+ if (typeof edges === 'number')
209
+ return edges;
210
+ // Whole-block sanitization (session-attach style): edge titles, reasons and
211
+ // sample paths are server-controlled free text.
212
+ console.log((0, sanitize_1.sanitizeField)(formatDepsList(task.identifier, edges)));
213
+ }
214
+ /** `lumo task deps add <LUM-N> --blocked-by <LUM-M>` */
215
+ async function taskDepsAdd(identifier, opts) {
216
+ if (!identifier || !opts.blockedBy) {
217
+ console.error('Error: usage: lumo task deps add <LUM-42> --blocked-by <LUM-9>');
218
+ return 1;
219
+ }
220
+ const ctx = getCtx();
221
+ if (typeof ctx === 'number')
222
+ return ctx;
223
+ const task = await resolveTask(ctx, identifier);
224
+ if (typeof task === 'number')
225
+ return task;
226
+ const res = await depsFetch(ctx, `/api/tasks/${encodeURIComponent(task.id)}/dependencies`, { method: 'POST', body: JSON.stringify({ blockedBy: opts.blockedBy }) }, 'dependency add');
227
+ if (typeof res === 'number')
228
+ return res;
229
+ console.log(`Added: ${task.identifier} blocked by ${opts.blockedBy}`);
230
+ }
231
+ /** `lumo task deps confirm <LUM-N> <edge> [--reverse]` */
232
+ async function taskDepsConfirm(identifier, selector, opts) {
233
+ if (!identifier || !selector) {
234
+ console.error('Error: usage: lumo task deps confirm <LUM-42> <edge> [--reverse]');
235
+ return 1;
236
+ }
237
+ const resolved = await resolveTaskAndEdge(identifier, selector);
238
+ if (typeof resolved === 'number')
239
+ return resolved;
240
+ const { ctx, task, edge } = resolved;
241
+ const res = await depsFetch(ctx, `/api/tasks/${encodeURIComponent(task.id)}/dependencies/${encodeURIComponent(edge.id)}`, {
242
+ method: 'PATCH',
243
+ body: JSON.stringify({
244
+ action: 'confirm',
245
+ reverse: opts.reverse ?? false,
246
+ }),
247
+ }, 'dependency confirm');
248
+ if (typeof res === 'number')
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)' : ''}`));
251
+ }
252
+ /** `lumo task deps dismiss <LUM-N> <edge>` */
253
+ async function taskDepsDismiss(identifier, selector) {
254
+ if (!identifier || !selector) {
255
+ console.error('Error: usage: lumo task deps dismiss <LUM-42> <edge>');
256
+ return 1;
257
+ }
258
+ const resolved = await resolveTaskAndEdge(identifier, selector);
259
+ if (typeof resolved === 'number')
260
+ return resolved;
261
+ const { ctx, task, edge } = resolved;
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
+ if (typeof res === 'number')
264
+ return res;
265
+ console.log((0, sanitize_1.sanitizeField)(`Dismissed: [${shortId(edge.id)}] ${edge.other.identifier}「${edge.other.title}」(不再建议)`));
266
+ }
267
+ /** `lumo task deps rm <LUM-N> <edge> --yes` */
268
+ async function taskDepsRm(identifier, selector, opts) {
269
+ if (!identifier || !selector) {
270
+ console.error('Error: usage: lumo task deps rm <LUM-42> <edge> --yes');
271
+ return 1;
272
+ }
273
+ if (!opts.yes) {
274
+ console.error('Error: refusing to delete without --yes. Re-run with --yes to confirm.');
275
+ return 1;
276
+ }
277
+ const resolved = await resolveTaskAndEdge(identifier, selector);
278
+ if (typeof resolved === 'number')
279
+ return resolved;
280
+ const { ctx, task, edge } = resolved;
281
+ const res = await depsFetch(ctx, `/api/tasks/${encodeURIComponent(task.id)}/dependencies/${encodeURIComponent(edge.id)}`, { method: 'DELETE' }, 'dependency delete');
282
+ if (typeof res === 'number')
283
+ return res;
284
+ console.log(`Removed [${shortId(edge.id)}] from ${task.identifier}`);
285
+ }
@@ -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");
@@ -72,6 +74,7 @@ const task_slack_show_1 = require("./commands/task-slack-show");
72
74
  const task_web_show_1 = require("./commands/task-web-show");
73
75
  const task_figma_context_1 = require("./commands/task-figma-context");
74
76
  const task_comment_list_1 = require("./commands/task-comment-list");
77
+ const task_deps_1 = require("./commands/task-deps");
75
78
  const task_pr_show_1 = require("./commands/task-pr-show");
76
79
  const task_lineage_1 = require("./commands/task-lineage");
77
80
  const project_list_1 = require("./commands/project-list");
@@ -305,6 +308,32 @@ taskComments
305
308
  .command('list <identifier>')
306
309
  .description('List the full task comment thread')
307
310
  .action(wrap(id => (0, task_comment_list_1.taskCommentList)(id)));
311
+ const taskDeps = task
312
+ .command('deps')
313
+ .description('Task dependency edges — detected candidates + confirmed blockers');
314
+ taskDeps
315
+ .command('list <identifier>')
316
+ .description('List dependency edges (both directions)')
317
+ .action(wrap(id => (0, task_deps_1.taskDepsList)(id)));
318
+ taskDeps
319
+ .command('add <identifier>')
320
+ .requiredOption('--blocked-by <blocker>', 'blocker task (LUM-N)')
321
+ .description('Declare a manual hard dependency (CONFIRMED)')
322
+ .action(wrap((id, opts) => (0, task_deps_1.taskDepsAdd)(id, opts)));
323
+ taskDeps
324
+ .command('confirm <identifier> <edge>')
325
+ .option('--reverse', 'flip direction when confirming')
326
+ .description('Confirm a detected candidate edge')
327
+ .action(wrap((id, edge, opts) => (0, task_deps_1.taskDepsConfirm)(id, edge, opts)));
328
+ taskDeps
329
+ .command('dismiss <identifier> <edge>')
330
+ .description('Dismiss a candidate (never re-suggested)')
331
+ .action(wrap((id, edge) => (0, task_deps_1.taskDepsDismiss)(id, edge)));
332
+ taskDeps
333
+ .command('rm <identifier> <edge>')
334
+ .option('--yes', 'skip confirmation')
335
+ .description('Delete a dependency edge')
336
+ .action(wrap((id, edge, opts) => (0, task_deps_1.taskDepsRm)(id, edge, opts)));
308
337
  const taskPr = task.command('pr').description('Inspect linked PRs');
309
338
  taskPr
310
339
  .command('show <identifier> <number>')
@@ -343,6 +372,19 @@ taskMemory
343
372
  .option('--step <text>', 'procedural: a step (repeatable)', collect, [])
344
373
  .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
345
374
  .action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
375
+ const taskCriteria = task
376
+ .command('criteria')
377
+ .description('Acceptance criteria — the task\u2019s verification contract (LUM-342)');
378
+ taskCriteria
379
+ .command('set <task>')
380
+ .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
+ .requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
382
+ .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session 出处')
383
+ .action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
384
+ taskCriteria
385
+ .command('list <task>')
386
+ .description('List a task\u2019s acceptance criteria (id, type, provenance, checkpointer)')
387
+ .action(wrap((taskId) => (0, task_criteria_list_1.taskCriteriaList)(taskId)));
346
388
  const taskArtifact = task
347
389
  .command('artifact')
348
390
  .description('Record spec-engineering artifacts (spec / plan / design …) on a task');
@@ -107,13 +107,20 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
107
107
  else if (tb && tb.bound === false) {
108
108
  lines.push(unboundPromptLine(sessionId));
109
109
  }
110
- // Recovery card + memory + linked resources + PR-review todos share one
111
- // additionalContext block so Claude Code injects a single coherent context
112
- // payload at session start. The card slots in first so it's the first thing
113
- // the model reads.
110
+ // Recovery card + blocker warning + memory + linked resources + PR-review
111
+ // todos share one additionalContext block so Claude Code injects a single
112
+ // coherent context payload at session start. The card slots in first so it's
113
+ // the first thing the model reads; the dependency blocker warning (LUM-172)
114
+ // comes right after so it stays prominent, ahead of the memory section.
114
115
  const card = renderRecoveryCard(body.previousSession, tb?.taskIdentifier ?? '', now);
115
116
  const envelope = sessionContextEnvelope([
116
117
  card,
118
+ // Blocker warning right after the card: it can preempt the session's
119
+ // work entirely (wait for the blocker instead of starting) — LUM-172.
120
+ body.blockerWarningSection,
121
+ // Acceptance contract next: it's what the session's work is judged
122
+ // against (LUM-342).
123
+ body.criteriaSection,
117
124
  body.memorySection,
118
125
  body.linkedResourcesSection,
119
126
  body.reviewTodosSection,
@@ -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.21.0",
3
+ "version": "1.23.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",