@lumoai/cli 1.22.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 criteria", "criteria set", "criteria list", "acceptance criteria", "验收标准", "验收合约", "draft criteria", "草拟验收", "definition of done", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context", "--signal", "usage signal health", "auto usage audit", "自动使用审计", "signal-health".'
3
+ description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "task deps", "dependency", "dependencies", "依赖", "依赖边", "blocked by", "blocker", "confirm dependency", "dismiss dependency", "确认依赖", "忽略依赖", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task criteria", "criteria set", "criteria list", "acceptance criteria", "验收标准", "验收合约", "draft criteria", "草拟验收", "definition of done", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context", "--signal", "usage signal health", "auto usage audit", "自动使用审计", "signal-health".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -61,6 +61,15 @@ The command catalog below is a **map**: it lists every command grouped by domain
61
61
  - `lumo task show <id>` — print one task's detail
62
62
  - `lumo task comment <id> <body>` — leave a comment
63
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
+
64
73
  **Acceptance criteria(验收合约)** — see [criteria.md](references/criteria.md)
65
74
 
66
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)
@@ -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.
@@ -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,7 +112,12 @@ 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).
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).
116
121
  if (body.criteriaSection) {
117
122
  console.log('');
118
123
  console.log((0, sanitize_1.sanitizeField)(body.criteriaSection));
@@ -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
+ }
@@ -74,6 +74,7 @@ const task_slack_show_1 = require("./commands/task-slack-show");
74
74
  const task_web_show_1 = require("./commands/task-web-show");
75
75
  const task_figma_context_1 = require("./commands/task-figma-context");
76
76
  const task_comment_list_1 = require("./commands/task-comment-list");
77
+ const task_deps_1 = require("./commands/task-deps");
77
78
  const task_pr_show_1 = require("./commands/task-pr-show");
78
79
  const task_lineage_1 = require("./commands/task-lineage");
79
80
  const project_list_1 = require("./commands/project-list");
@@ -307,6 +308,32 @@ taskComments
307
308
  .command('list <identifier>')
308
309
  .description('List the full task comment thread')
309
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)));
310
337
  const taskPr = task.command('pr').description('Inspect linked PRs');
311
338
  taskPr
312
339
  .command('show <identifier> <number>')
@@ -107,15 +107,19 @@ 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,
117
- // Acceptance contract right after the recovery card: it's what the
118
- // session's work is judged against (LUM-342).
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).
119
123
  body.criteriaSection,
120
124
  body.memorySection,
121
125
  body.linkedResourcesSection,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.22.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",