@lumoai/cli 1.11.0 → 1.15.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.
package/assets/skill.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lumo
3
- description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "milestone summary", "milestone retro", "summarize milestone", "里程碑总结", "里程碑复盘", "milestone add", "milestone remove", "add tasks to milestone", "remove tasks from milestone", "batch milestone", "bulk milestone", "挂任务到里程碑", "批量挂里程碑", "从里程碑移除任务", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化", "task artifact", "artifact add", "artifact list", "artifact show", "artifact rm", "artifact delete", "artifact update", "update artifact", "edit artifact", "change artifact kind", "change artifact source", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "编辑 artifact", "修改 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task memory", "project memory", "lumo memory", "retrieval", "取全文", "load full content", "拉全文", "task slack show", "看 thread", "看 slack thread", "show slack thread", "slack 全文", "task web show", "show web link body", "web 正文", "抓网页正文", "task figma context", "figma metadata", "figma 元数据", "figma 设计上下文", "task comments list", "list comments", "看评论", "评论流", "task pr show", "查看 PR", "show pr", "PR 详情", "pr metadata", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入 google 文档", "同步 google 文档", "session wrap", "wrap up session", "收尾", "post progress", "把进度发出去", "progress comment", "进度评论", "lumo next", "next task", "what''s next", "what should I work on", "recommend a task", "推荐下一个任务", "pick my next task".'
3
+ description: 'Use the Lumo CLI to load task context, manage session bindings, inspect projects and milestones, and create/update/list/show/comment on tasks from the terminal. Activate when: user mentions a Lumo task identifier (LUM-42, LUM-12, etc.), asks to load task background or context, wants to bind, check, or detach a Claude Code session''s task binding, is about to start development work on a specific task, wants to create a new task, list their tasks, view a task, comment on a task, list projects, list milestones, attach a task to a milestone, or update a task''s status/title/description/priority/assignee/milestone. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "bind session", "unbind session", "which task", "what task am I on", "work on LUM", "create task", "new task", "add task", "file a task", "log a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "leave a comment", "list projects", "what projects", "update task", "change task status", "rename task", "reassign task", "mark task as done", "milestone", "里程碑", "list milestones", "set milestone", "挂到 milestone", "attach milestone", "unbind milestone", "create milestone", "new milestone", "update milestone", "change milestone status", "delete milestone", "show milestone", "view milestone", "tasks in milestone", "milestone tasks", "search milestones", "find milestone", "filter milestones", "milestone search", "搜索里程碑", "查找里程碑", "milestone health", "milestone risk", "risk light", "on-track", "at-risk", "overdue", "里程碑健康度", "健康度", "风险灯", "延期风险", "新建里程碑", "更新里程碑", "删除里程碑", "查看里程碑", "archive milestone", "unarchive milestone", "restore milestone", "archived milestones", "归档里程碑", "恢复里程碑", "取消归档", "milestone summary", "milestone retro", "summarize milestone", "里程碑总结", "里程碑复盘", "milestone reorder", "milestone move", "reorder milestones", "move milestone", "排序里程碑", "调整里程碑顺序", "里程碑调序", "move milestone before", "move milestone after", "milestone add", "milestone remove", "add tasks to milestone", "remove tasks from milestone", "batch milestone", "bulk milestone", "挂任务到里程碑", "批量挂里程碑", "从里程碑移除任务", "auth login", "log in", "login", "auth logout", "log out", "logout", "sign out", "switch account", "switch identity", "whoami", "who am I", "current identity", "current user", "current workspace", "登录", "登出", "切换账号", "当前身份", "create doc", "new doc", "new document", "write doc", "写文档", "新建文档", "update doc", "edit doc", "修改文档", "更新文档", "list docs", "my docs", "我的文档", "show doc", "view doc", "查看文档", "delete doc", "删除文档", "bind doc", "attach doc to task", "把文档关联到任务", "文档绑定到任务", "unbind doc", "解绑文档", "personal doc", "workspace doc", "个人文档", "workspace 文档", "doc scope", "tag", "add tag", "remove tag", "标签", "添加标签", "移除标签", "doc tag", "task tag", "share doc", "doc share", "share document", "分享文档", "文档分享", "unshare doc", "remove share", "取消分享", "share list", "list doc shares", "who has access", "viewer", "editor", "manager", "shared with", "doc tree", "doc move", "move doc", "reparent doc", "文档树", "移动文档", "sprint", "start sprint", "close sprint", "add to sprint", "active sprints", "冲刺", "迭代", "开始冲刺", "关闭冲刺", "create sprint", "new sprint", "list sprints", "show sprint", "update sprint", "delete sprint", "sprint summary", "sprint retro", "把任务挂到冲刺", "冲刺里有什么", "lumo update", "update cli", "upgrade lumo", "update lumo", "upgrade cli", "升级 lumo", "更新 cli", "new lumo version", "是否有新版本", "lumo setup", "install lumo skill", "install lumo hooks", "wire up lumo", "set up lumo", "onboard lumo", "npx @lumoai/cli", "安装 lumo", "配置 lumo", "lumo 初始化", "task artifact", "artifact add", "artifact list", "artifact show", "artifact rm", "artifact delete", "artifact update", "update artifact", "edit artifact", "change artifact kind", "change artifact source", "remove artifact", "delete artifact", "spec artifact", "record spec", "attach spec", "attach plan", "记录 spec", "挂 spec", "查看 artifact", "编辑 artifact", "修改 artifact", "删除 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task memory", "project memory", "lumo memory", "retrieval", "取全文", "load full content", "拉全文", "task slack show", "看 thread", "看 slack thread", "show slack thread", "slack 全文", "task web show", "show web link body", "web 正文", "抓网页正文", "task figma context", "figma metadata", "figma 元数据", "figma 设计上下文", "task comments list", "list comments", "看评论", "评论流", "task pr show", "查看 PR", "show pr", "PR 详情", "pr metadata", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入 google 文档", "同步 google 文档", "session wrap", "wrap up session", "收尾", "post progress", "把进度发出去", "progress comment", "进度评论", "mark blocked", "blocked tag", "标 blocked", "标记 blocked", "卡住检测", "反复失败", "stuck", "repeatedly failing", "lumo next", "next task", "what''s next", "what should I work on", "recommend a task", "推荐下一个任务", "pick my next task".'
4
4
  ---
5
5
 
6
6
  ## Prerequisites
@@ -343,7 +343,7 @@ Task LUM-48 has no sprint binding # noop (already unbound)
343
343
 
344
344
  The CLI does **not** currently update due date or parent task. Those need to be edited in the web UI.
345
345
 
346
- Milestone updates (`--milestone`) and sprint binding (`--sprint`) both work. Full milestone CRUD is available via `lumo milestone create / show / update / delete`, and tasks can be bound/unbound in bulk via `lumo milestone add / remove <identifier> <task...>` (see below). Full sprint CRUD is available via `lumo sprint create / show / update / delete / start / close / add / remove` (see below).
346
+ Milestone updates (`--milestone`) and sprint binding (`--sprint`) both work. Full milestone CRUD is available via `lumo milestone create / show / update / delete`, tasks can be bound/unbound in bulk via `lumo milestone add / remove <identifier> <task...>`, and milestones can be manually reordered via `lumo milestone reorder <ref...>` / `lumo milestone move <ref> --before|--after <ref>` (see below). Full sprint CRUD is available via `lumo sprint create / show / update / delete / start / close / add / remove` (see below).
347
347
 
348
348
  ### `lumo task list [flags]` — list tasks assigned to you
349
349
 
@@ -382,8 +382,8 @@ membership**, then **due date** (earlier first), then in-flight status
382
382
  sprint lookup is best-effort — if it fails the command still recommends, just
383
383
  without the sprint boost.
384
384
 
385
- | Flag | Type | Notes |
386
- | ---- | ---- | ----- |
385
+ | Flag | Type | Notes |
386
+ | ----------------- | ------- | ----------------------------------------------------------------------- |
387
387
  | `-n, --count <N>` | integer | How many tasks to recommend. Defaults to 3. Must be a positive integer. |
388
388
 
389
389
  ```bash
@@ -559,13 +559,35 @@ Prints `<slug> <Display Name>` lines. The slug column matches the `--project <r
559
559
  lumo project list
560
560
  ```
561
561
 
562
- ### `lumo milestone list [--project <ref>]` — list milestones in a project
562
+ ### `lumo milestone list [--project <ref>] [--archived] [--all] [--search <text>]` — list milestones in a project
563
563
 
564
- Prints fixed-width rows: `<STATUS> <target-date or -> <name>`, sorted by target date asc (nulls last) then created asc.
564
+ Prints fixed-width rows: `<STATUS> <HEALTH> <target-date or -> <name>`, sorted by target date asc (nulls last) then created asc.
565
+
566
+ By default, only **non-archived** milestones are listed. Use `--archived` to show **only** archived milestones, or `--all` to show **both** archived and non-archived. Archived rows are marked with a ` (archived)` suffix on the name.
567
+
568
+ Use `--search <text>` to filter to milestones whose **name or description** contains the text (case-insensitive substring match). It applies **on top of** the archive filter (e.g. `--all --search q3`). A blank/whitespace-only value is ignored (no filtering). The matched text is bounded to 120 chars server-side.
569
+
570
+ | Flag | Type | Notes |
571
+ | ----------------- | ------- | -------------------------------------------------------------------------------------- |
572
+ | `--project <ref>` | string | Required when the workspace has more than one project. Name or slug, case-insensitive. |
573
+ | `--archived` | boolean | Show **only** archived milestones (instead of the default non-archived set). |
574
+ | `--all` | boolean | Show **both** archived and non-archived milestones. |
575
+ | `--search <text>` | string | Filter by **name/description** case-insensitive substring. Combines with the archive filter; blank value ignored. |
576
+
577
+ `HEALTH` is the target-date risk light, server-computed from the milestone's target date + task progress:
578
+
579
+ - `ON-TRACK` — on schedule (or all tasks done)
580
+ - `AT-RISK` — completion lags elapsed time, or (no start date) the target is within ~7 days with work remaining
581
+ - `OVERDUE` — past the target date with tasks still open
582
+ - `-` — no light applies (status `COMPLETED`/`CANCELLED`, or no target date)
565
583
 
566
584
  ```bash
567
- lumo milestone list # one-project workspace
585
+ lumo milestone list # one-project workspace (non-archived only)
568
586
  lumo milestone list --project lumo # multi-project workspace
587
+ lumo milestone list --archived # only archived milestones
588
+ lumo milestone list --all # archived + non-archived (archived marked "(archived)")
589
+ lumo milestone list --search q3 # name/description contains "q3" (case-insensitive)
590
+ lumo milestone list --all --search launch # search across archived + non-archived
569
591
  ```
570
592
 
571
593
  `--project <ref>` is required when the workspace has more than one project (consistent with `task create --project`). Match is by project name or slug, case-insensitive.
@@ -574,6 +596,7 @@ lumo milestone list --project lumo # multi-project workspace
574
596
 
575
597
  - Before suggesting `task create --milestone <ref>` or `task update --milestone <ref>` to confirm the milestone exists under the expected name.
576
598
  - When the user asks "what milestones do we have", "what's on v1.0", or similar.
599
+ - When the user wants to **find/search milestones by keyword** ("find the launch milestone", "搜索里程碑", "which milestones mention X") — use `--search <text>` rather than listing all and eyeballing.
577
600
 
578
601
  When to suggest: before `task create --project <ref>` when the workspace has more than one project and the user hasn't specified which one.
579
602
 
@@ -599,7 +622,7 @@ On success: `Created milestone "Q3 Launch" <id>`.
599
622
 
600
623
  Accepts UUID or name. With a name, `--project <ref>` is required when the workspace has >1 project.
601
624
 
602
- Prints a key:value header (name, status, dates, project, description), task counts, and the full task table under the milestone.
625
+ Prints a key:value header (name, status, **health**, dates, project, description), task counts, and the full task table under the milestone. The `Health:` line shows the same target-date risk light as `milestone list` (`ON-TRACK` / `AT-RISK` / `OVERDUE`, or `-` when none applies).
603
626
 
604
627
  ```bash
605
628
  lumo milestone show "Q3 Launch"
@@ -633,6 +656,23 @@ Requires `--yes`. No interactive prompt — CLI is agent-friendly. Tasks under t
633
656
  lumo milestone delete "Q3 Launch" --yes
634
657
  ```
635
658
 
659
+ ### `lumo milestone archive <identifier>` — soft-archive a milestone
660
+
661
+ Soft-archives a milestone by setting `archivedAt`. The milestone is **hidden from `milestone list` by default** (use `--archived` or `--all` to see it), but its **history and task links are preserved**, and the action is **reversible** via `milestone unarchive`. This is distinct from `milestone delete`, which is a **hard delete**. While archived, the milestone **rejects edits** (`milestone update`) and **new task bindings** (`task --milestone`, `milestone add`) with a 409 until it is restored. `<identifier>` accepts a UUID or name; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
662
+
663
+ ```bash
664
+ lumo milestone archive "Q3 Launch"
665
+ lumo milestone archive 11111111-2222-3333-4444-555555555555
666
+ ```
667
+
668
+ ### `lumo milestone unarchive <identifier>` — restore an archived milestone
669
+
670
+ Restores an archived milestone by clearing `archivedAt`. It reappears in `milestone list` and can be edited and bound to tasks again. Idempotent — unarchiving an already-active milestone is a no-op. Same identifier / `--project` rules as `milestone archive`: `<identifier>` accepts a UUID or name; `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
671
+
672
+ ```bash
673
+ lumo milestone unarchive "Q3 Launch"
674
+ ```
675
+
636
676
  ### `lumo milestone add <identifier> <task...>` — bind tasks to a milestone (batch)
637
677
 
638
678
  Binds **one or more** tasks to a milestone in a single call — the batch counterpart of `task update --milestone <ref>` (which only takes one task at a time). `<identifier>` accepts a milestone name or UUID; each `<task>` accepts `LUM-N` or a task UUID. `--project <ref>` is required when the identifier is a name and the workspace has >1 project.
@@ -681,9 +721,9 @@ Prints the AI-generated retrospective summary for a milestone (mirrors `sprint s
681
721
 
682
722
  A summary is generated automatically when a milestone transitions to `COMPLETED` (e.g. via `lumo milestone update <id> --status completed`). The generated report has sections `## Summary`, `## Delivered`, `## Outstanding` plus a one-line `tldr`. Use `--retry` to queue regeneration (e.g. after a failed generation) before fetching — regeneration is async, so the printed result may still be the previous summary or `(no summary generated yet)`.
683
723
 
684
- | Flag | Type | Notes |
685
- | ----------------- | ------- | ---------------------------------------------------------------- |
686
- | `--project <ref>` | string | Project name or slug. Required when identifier is a name and the workspace has >1 project. |
724
+ | Flag | Type | Notes |
725
+ | ----------------- | ------- | ------------------------------------------------------------------------------------------------------ |
726
+ | `--project <ref>` | string | Project name or slug. Required when identifier is a name and the workspace has >1 project. |
687
727
  | `--retry` | boolean | Queue a regeneration (async, server returns 202) before fetching. Only valid on a COMPLETED milestone. |
688
728
 
689
729
  ```bash
@@ -694,6 +734,50 @@ lumo milestone summary 11111111-2222-3333-4444-555555555555
694
734
 
695
735
  When to suggest: user asks "summarize the milestone", "milestone retro", "give me a summary of the Q3 milestone", "里程碑总结", "里程碑复盘".
696
736
 
737
+ ### `lumo milestone reorder <ref...> [--project <ref>]` — set the full milestone order
738
+
739
+ Reorders a project's milestones. Pass **every** milestone in the project (by name, case-insensitive, or its cuid) in the desired order — the command rewrites each milestone's `sortOrder` to match. An incomplete list (not every milestone named), an unknown ref, or a duplicate is rejected **before any network mutation**, with a message naming the offending / missing milestones.
740
+
741
+ ```bash
742
+ lumo milestone reorder "Q3 Launch" "Beta" "Alpha"
743
+ lumo milestone reorder "Q3 Launch" "Beta" "Alpha" --project backend
744
+ ```
745
+
746
+ `--project <ref>` is required when the workspace has more than one project.
747
+
748
+ Output:
749
+
750
+ ```
751
+ Reordered 3 milestones:
752
+ 1. Q3 Launch
753
+ 2. Beta
754
+ 3. Alpha
755
+ ```
756
+
757
+ ### `lumo milestone move <ref> --before <ref> | --after <ref> [--project <ref>]` — move one milestone
758
+
759
+ Moves a single milestone immediately before or after a target milestone, leaving the rest in their current relative order. `--before` and `--after` are **mutually exclusive and exactly one is required** (the CLI errors before any network call if both or neither is given). Refs resolve by cuid or case-insensitive name.
760
+
761
+ ```bash
762
+ lumo milestone move "Alpha" --before "Q3 Launch"
763
+ lumo milestone move "Alpha" --after "Beta" --project backend
764
+ ```
765
+
766
+ Output:
767
+
768
+ ```
769
+ Moved "Alpha" before "Q3 Launch". New order:
770
+ 1. Alpha
771
+ 2. Q3 Launch
772
+ 3. Beta
773
+ ```
774
+
775
+ Both commands resolve to a full ordered list client-side and call the same `PATCH /api/projects/<id>/milestones/reorder` endpoint (which requires the list to name every milestone exactly once). New milestones created via `milestone create` are appended to the bottom of the order.
776
+
777
+ **Ambiguous names:** milestone names are not unique within a project (only the slug is). A ref whose name matches more than one milestone is **rejected** with an `ambiguous milestone name … re-run with the id` error listing the candidate cuids — pass the cuid instead. This applies to every name-based milestone ref (`reorder`, `move`, and also `show` / `update` / `delete` / `summary` / `add` / `remove`).
778
+
779
+ When to suggest: user asks to "reorder milestones", "排序里程碑", "调整里程碑顺序", "move milestone X before/after Y", "把里程碑 X 移到 Y 前面/后面", "put this milestone first".
780
+
697
781
  ## Document Management
698
782
 
699
783
  ### `lumo doc create [title] [flags]` — create a new document
@@ -1233,6 +1317,16 @@ auto-bind line, run `lumo session detach` to clear the binding (then `session
1233
1317
  attach <LUM-N>` if they name the right one). No detach is needed when the
1234
1318
  auto-bound task is correct.
1235
1319
 
1320
+ ### Layer 2 project-memory review at session start
1321
+
1322
+ When the session is bound, session-start may inject a **"🆕 待核对:上次会话自动合并的项目级记忆"** section alongside the memory / PR-review blocks (LUM-165). It lists the **PROJECT-scope** memories that the member's **immediately-preceding session** auto-consolidated (Layer 2 runs asynchronously when a task is marked `done`). Each item shows its `id`.
1323
+
1324
+ - **Why it's here:** Layer 2 promotions land async, so they can't be reviewed in the synchronous `session wrap` panel — they're surfaced at the _next_ session-start instead, when they've definitely landed.
1325
+ - **Show-once:** the section appears only at the session that immediately follows the one that produced the memories. It does **not** re-nag on later sessions, so act on it now or it scrolls off.
1326
+ - **Agent guidance:** briefly sanity-check each listed memory against the codebase/context. If one is wrong or over-generalized, remove it with `lumo memory rm <id> --yes` (ideally confirm with the user first). If they all look right, ignore the section and continue.
1327
+
1328
+ 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.
1329
+
1236
1330
  ### `lumo session attach <identifier>` — bind the current session to a task
1237
1331
 
1238
1332
  Use this whenever the user mentions a task ID. The command is the only way to bind a session to a task.
@@ -1273,9 +1367,9 @@ lumo session detach
1273
1367
 
1274
1368
  When to suggest: the user wants to stop tagging the current session with the active task (e.g., switching to unrelated exploratory work without binding to a different task).
1275
1369
 
1276
- ### `lumo session wrap [--yes] [--dry-run]` — wrap-up panel: progress comment + memory review
1370
+ ### `lumo session wrap [--yes] [--dry-run]` — wrap-up panel: progress comment + memory review + blocked-tag prompt
1277
1371
 
1278
- Session-end wrap-up panel with **two sections, run in order**:
1372
+ Session-end wrap-up panel with **three sections, run in order**:
1279
1373
 
1280
1374
  **1. 进度评论** — reads back the current Claude Code session's per-turn
1281
1375
  `turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
@@ -1295,10 +1389,27 @@ Out-of-range indices are ignored. Deletes/promotes run server-side, scoped to
1295
1389
  memories this session created (you can't touch other sessions' memories through
1296
1390
  this panel). With no new memories the section prints "(无内容)" and does nothing.
1297
1391
 
1392
+ **3. 卡住检测 (blocked-tag prompt, LUM-153)** — if the **same kind of failure
1393
+ recurred ≥ 3 times** in this session (server-aggregated from
1394
+ `POST_TOOL_USE_FAILURE` events grouped by tool name, plus `STOP_FAILURE`
1395
+ turn-level failures), the section surfaces the dominant failure (`卡在 <tool>
1396
+ (N 次失败)` + last error summary) and prompts `[y] 标记 / [s] 跳过` whether to
1397
+ flag the bound task with a **`blocked` tag**. **Prompt-only — never auto-flips
1398
+ status.** It uses a plain tag (no `TaskStatus` enum, no board column, **no
1399
+ schema migration**). The prompt is **suppressed** when: there's no bound task,
1400
+ the threshold isn't met, or the task **already** carries a `blocked` tag (the
1401
+ idempotent gate — there's no watermark, the existing tag is what prevents
1402
+ re-nagging). The default on empty input / `s` is **do nothing** (tagging is
1403
+ opt-in), so a stray Enter never tags the task. Confirming with an explicit `y`
1404
+ attaches the tag idempotently. **`--yes` does NOT auto-tag** — tagging the
1405
+ shared board requires an interactive `y`, so `--yes` (and non-TTY) prints the
1406
+ suggestion and moves on rather than silently flipping board state. When there's
1407
+ nothing to prompt, the section prints "(无内容)".
1408
+
1298
1409
  ```bash
1299
1410
  lumo session wrap # interactive: preview each section, choose per-section
1300
- lumo session wrap --yes # progress comment posted + memories all kept, no prompting (agent-friendly)
1301
- lumo session wrap --dry-run # print both drafts only; never posts, never mutates, never advances watermarks
1411
+ lumo session wrap --yes # progress posted + memories kept; blocked tag NOT auto-applied (needs interactive y)
1412
+ lumo session wrap --dry-run # print all drafts only; never posts, never mutates, never advances watermarks
1302
1413
  ```
1303
1414
 
1304
1415
  - Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
@@ -1307,12 +1418,13 @@ lumo session wrap --dry-run # print both drafts only; never posts, never mutate
1307
1418
  - `[e] 编辑` (进度评论) opens `$EDITOR` (fallback vi/nano) on the drafted body;
1308
1419
  the edited text is posted and the watermark still advances to the turns the
1309
1420
  draft covered.
1310
- - `--yes` applies to both sections: posts the progress comment AND keeps all
1311
- memories (no deletes/promotes) while advancing the memory-review watermark.
1312
- - `--dry-run` prints both drafts; never posts, never mutates memories, never
1421
+ - `--yes` posts the progress comment AND keeps all memories (no
1422
+ deletes/promotes) while advancing the memory-review watermark; for the
1423
+ blocked-tag section it prints the suggestion but does **not** apply the tag.
1424
+ - `--dry-run` prints all drafts; never posts, never mutates memories/tags, never
1313
1425
  advances either watermark.
1314
- - Non-TTY without `--yes`: prints the drafts and does **not** post or mutate
1315
- (safe default).
1426
+ - Non-TTY without `--yes`: prints the drafts and does **not** post, mutate, or
1427
+ tag (safe default).
1316
1428
 
1317
1429
  When to suggest: at the end of a working session on a bound task, to record what
1318
1430
  was done as a progress comment — offer `lumo session wrap` rather than composing
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatArchiveResult = formatArchiveResult;
4
+ exports.milestoneArchive = milestoneArchive;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ function formatArchiveResult(name) {
10
+ return `Archived "${(0, sanitize_1.sanitizeField)(name)}"`;
11
+ }
12
+ async function milestoneArchive(identifier, opts) {
13
+ const creds = (0, config_1.readCredentials)();
14
+ if (!creds) {
15
+ console.error('Error: not logged in. Run `lumo auth login` first.');
16
+ return 1;
17
+ }
18
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
19
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
20
+ let milestoneId;
21
+ try {
22
+ const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
23
+ milestoneId = resolved.id;
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ let res;
30
+ try {
31
+ res = await fetch(`${base}/api/milestones/${milestoneId}/archive`, {
32
+ method: 'POST',
33
+ headers: { Authorization: `Bearer ${creds.token}` },
34
+ });
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
39
+ return 1;
40
+ }
41
+ if (res.status === 401) {
42
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ let errMsg = `milestone archive failed (HTTP ${res.status})`;
47
+ try {
48
+ const body = (await res.json());
49
+ if (body.error)
50
+ errMsg = body.error;
51
+ }
52
+ catch {
53
+ // non-JSON body; keep the status-only message
54
+ }
55
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
56
+ return 1;
57
+ }
58
+ const { milestone } = (await res.json());
59
+ process.stdout.write(formatArchiveResult(milestone.name) + '\n');
60
+ }
@@ -6,6 +6,14 @@ const config_1 = require("../lib/config");
6
6
  const api_1 = require("../lib/api");
7
7
  const resolve_1 = require("../lib/resolve");
8
8
  const sanitize_1 = require("../lib/sanitize");
9
+ const HEALTH_LABEL = {
10
+ 'on-track': 'ON-TRACK',
11
+ 'at-risk': 'AT-RISK',
12
+ overdue: 'OVERDUE',
13
+ };
14
+ function formatHealth(health) {
15
+ return health ? HEALTH_LABEL[health] : '-';
16
+ }
9
17
  function formatDate(iso) {
10
18
  if (!iso)
11
19
  return '-';
@@ -13,7 +21,10 @@ function formatDate(iso) {
13
21
  }
14
22
  /**
15
23
  * Render milestones as fixed-width rows:
16
- * <STATUS> <target-date or -> <name>
24
+ * <STATUS> <HEALTH> <target-date or -> <name>
25
+ *
26
+ * HEALTH is the target-date risk light (ON-TRACK / AT-RISK / OVERDUE), or `-`
27
+ * when no light applies (terminal status or no target date).
17
28
  *
18
29
  * Sorted server-side by targetDate asc nulls last, createdAt asc.
19
30
  */
@@ -21,9 +32,10 @@ function formatMilestoneList(rows) {
21
32
  if (rows.length === 0)
22
33
  return 'No milestones.';
23
34
  const statusW = Math.max(...rows.map(r => r.status.length));
35
+ const healthW = Math.max(...rows.map(r => formatHealth(r.health).length));
24
36
  const dateW = Math.max(...rows.map(r => formatDate(r.targetDate).length));
25
37
  return rows
26
- .map(r => `${r.status.padEnd(statusW)} ${formatDate(r.targetDate).padEnd(dateW)} ${(0, sanitize_1.sanitizeField)(r.name)}`)
38
+ .map(r => `${r.status.padEnd(statusW)} ${formatHealth(r.health).padEnd(healthW)} ${formatDate(r.targetDate).padEnd(dateW)} ${r.archivedAt ? `${(0, sanitize_1.sanitizeField)(r.name)} (archived)` : (0, sanitize_1.sanitizeField)(r.name)}`)
27
39
  .join('\n');
28
40
  }
29
41
  async function milestoneList(options) {
@@ -43,9 +55,16 @@ async function milestoneList(options) {
43
55
  console.error(`Error: ${msg}`);
44
56
  return 1;
45
57
  }
46
- const res = await fetch(`${base}/api/projects/${projectId}/milestones`, {
47
- headers: { Authorization: `Bearer ${creds.token}` },
48
- });
58
+ if (options.archived && options.all) {
59
+ console.error('Error: --archived and --all are mutually exclusive.');
60
+ return 1;
61
+ }
62
+ const filter = options.all ? 'all' : options.archived ? 'archived' : 'active';
63
+ const query = new URLSearchParams({ filter });
64
+ const term = options.search?.trim();
65
+ if (term)
66
+ query.set('search', term);
67
+ const res = await fetch(`${base}/api/projects/${projectId}/milestones?${query.toString()}`, { headers: { Authorization: `Bearer ${creds.token}` } });
49
68
  if (!res.ok) {
50
69
  console.error(`Error: milestone list failed (HTTP ${res.status})`);
51
70
  return 1;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.milestoneMove = milestoneMove;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const resolve_1 = require("../lib/resolve");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const milestone_reorder_1 = require("../lib/milestone-reorder");
9
+ async function milestoneMove(reference, opts) {
10
+ if (!reference) {
11
+ console.error('Error: usage: lumo milestone move <ref> --before <ref> | --after <ref>');
12
+ return 1;
13
+ }
14
+ const hasBefore = typeof opts.before === 'string' && opts.before.length > 0;
15
+ const hasAfter = typeof opts.after === 'string' && opts.after.length > 0;
16
+ if (hasBefore && hasAfter) {
17
+ console.error('Error: --before and --after are mutually exclusive');
18
+ return 1;
19
+ }
20
+ if (!hasBefore && !hasAfter) {
21
+ console.error('Error: specify --before <ref> or --after <ref>');
22
+ return 1;
23
+ }
24
+ const creds = (0, config_1.readCredentials)();
25
+ if (!creds) {
26
+ console.error('Error: not logged in. Run `lumo auth login` first.');
27
+ return 1;
28
+ }
29
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
30
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
31
+ let projectId;
32
+ try {
33
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, opts.project);
34
+ }
35
+ catch (err) {
36
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
37
+ return 1;
38
+ }
39
+ const listRes = await fetch(`${base}/api/projects/${projectId}/milestones`, {
40
+ headers: { Authorization: `Bearer ${creds.token}` },
41
+ });
42
+ if (!listRes.ok) {
43
+ console.error(`Error: milestone list failed (HTTP ${listRes.status})`);
44
+ return 1;
45
+ }
46
+ const { milestones } = (await listRes.json());
47
+ const refRows = milestones.map(m => ({
48
+ id: m.id,
49
+ name: m.name,
50
+ sortOrder: m.sortOrder,
51
+ }));
52
+ const position = hasBefore ? 'before' : 'after';
53
+ const targetRef = (hasBefore ? opts.before : opts.after);
54
+ const resolved = (0, milestone_reorder_1.computeMoveOrder)(reference, targetRef, position, refRows);
55
+ if (!resolved.ok) {
56
+ console.error(`Error: ${resolved.error}`);
57
+ return 1;
58
+ }
59
+ const patchRes = await fetch(`${base}/api/projects/${projectId}/milestones/reorder`, {
60
+ method: 'PATCH',
61
+ headers: {
62
+ Authorization: `Bearer ${creds.token}`,
63
+ 'Content-Type': 'application/json',
64
+ },
65
+ body: JSON.stringify({ orderedIds: resolved.orderedIds }),
66
+ });
67
+ if (!patchRes.ok) {
68
+ const text = await patchRes.text();
69
+ let msg = text;
70
+ try {
71
+ const json = JSON.parse(text);
72
+ if (json.error)
73
+ msg = json.error;
74
+ }
75
+ catch { }
76
+ console.error(`Error: ${patchRes.status} ${patchRes.statusText}: ${(0, sanitize_1.sanitizeField)(msg)}`);
77
+ return 1;
78
+ }
79
+ const byId = new Map(refRows.map(m => [m.id, m.name]));
80
+ console.log(`Moved "${(0, sanitize_1.sanitizeField)(reference)}" ${position} "${(0, sanitize_1.sanitizeField)(targetRef)}". New order:`);
81
+ resolved.orderedIds.forEach((id, i) => {
82
+ console.log(` ${i + 1}. ${(0, sanitize_1.sanitizeField)(byId.get(id) ?? id)}`);
83
+ });
84
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.milestoneReorder = milestoneReorder;
4
+ const config_1 = require("../lib/config");
5
+ const api_1 = require("../lib/api");
6
+ const resolve_1 = require("../lib/resolve");
7
+ const sanitize_1 = require("../lib/sanitize");
8
+ const milestone_reorder_1 = require("../lib/milestone-reorder");
9
+ async function milestoneReorder(refs, opts) {
10
+ if (!refs || refs.length === 0) {
11
+ console.error('Error: usage: lumo milestone reorder <ref...> [--project <ref>]');
12
+ return 1;
13
+ }
14
+ const creds = (0, config_1.readCredentials)();
15
+ if (!creds) {
16
+ console.error('Error: not logged in. Run `lumo auth login` first.');
17
+ return 1;
18
+ }
19
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
20
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
21
+ let projectId;
22
+ try {
23
+ projectId = await (0, resolve_1.resolveProjectId)(base, creds.token, opts.project);
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ const listRes = await fetch(`${base}/api/projects/${projectId}/milestones`, {
30
+ headers: { Authorization: `Bearer ${creds.token}` },
31
+ });
32
+ if (!listRes.ok) {
33
+ console.error(`Error: milestone list failed (HTTP ${listRes.status})`);
34
+ return 1;
35
+ }
36
+ const { milestones } = (await listRes.json());
37
+ const refRows = milestones.map(m => ({
38
+ id: m.id,
39
+ name: m.name,
40
+ sortOrder: m.sortOrder,
41
+ }));
42
+ const resolved = (0, milestone_reorder_1.resolveOrderedIds)(refs, refRows);
43
+ if (!resolved.ok) {
44
+ console.error(`Error: ${resolved.error}`);
45
+ return 1;
46
+ }
47
+ const patchRes = await fetch(`${base}/api/projects/${projectId}/milestones/reorder`, {
48
+ method: 'PATCH',
49
+ headers: {
50
+ Authorization: `Bearer ${creds.token}`,
51
+ 'Content-Type': 'application/json',
52
+ },
53
+ body: JSON.stringify({ orderedIds: resolved.orderedIds }),
54
+ });
55
+ if (!patchRes.ok) {
56
+ const text = await patchRes.text();
57
+ let msg = text;
58
+ try {
59
+ const json = JSON.parse(text);
60
+ if (json.error)
61
+ msg = json.error;
62
+ }
63
+ catch { }
64
+ console.error(`Error: ${patchRes.status} ${patchRes.statusText}: ${(0, sanitize_1.sanitizeField)(msg)}`);
65
+ return 1;
66
+ }
67
+ const byId = new Map(refRows.map(m => [m.id, m.name]));
68
+ console.log(`Reordered ${resolved.orderedIds.length} milestones:`);
69
+ resolved.orderedIds.forEach((id, i) => {
70
+ console.log(` ${i + 1}. ${(0, sanitize_1.sanitizeField)(byId.get(id) ?? id)}`);
71
+ });
72
+ }
@@ -7,9 +7,17 @@ const api_1 = require("../lib/api");
7
7
  const resolve_1 = require("../lib/resolve");
8
8
  const format_1 = require("../lib/format");
9
9
  const sanitize_1 = require("../lib/sanitize");
10
+ const HEALTH_LABEL = {
11
+ 'on-track': 'ON-TRACK',
12
+ 'at-risk': 'AT-RISK',
13
+ overdue: 'OVERDUE',
14
+ };
10
15
  function fmtDate(iso) {
11
16
  return iso ? iso.slice(0, 10) : '-';
12
17
  }
18
+ function fmtHealth(health) {
19
+ return health ? HEALTH_LABEL[health] : '-';
20
+ }
13
21
  function formatMilestoneShow(m, tasks) {
14
22
  const total = m.taskCounts.TODO +
15
23
  m.taskCounts.IN_PROGRESS +
@@ -18,6 +26,8 @@ function formatMilestoneShow(m, tasks) {
18
26
  const lines = [
19
27
  `Milestone: ${(0, sanitize_1.sanitizeField)(m.name)}`,
20
28
  `Status: ${m.status}`,
29
+ `Archived: ${m.archivedAt ? m.archivedAt.slice(0, 10) : 'no'}`,
30
+ `Health: ${fmtHealth(m.health)}`,
21
31
  `Start: ${fmtDate(m.startDate)}`,
22
32
  `Target: ${fmtDate(m.targetDate)}`,
23
33
  `Project: ${(0, sanitize_1.sanitizeField)(m.projectName)}`,
@@ -99,8 +109,10 @@ async function milestoneShow(identifier, opts) {
99
109
  status: milestone.status,
100
110
  startDate: milestone.startDate,
101
111
  targetDate: milestone.targetDate,
112
+ archivedAt: milestone.archivedAt,
102
113
  description: milestone.description,
103
114
  projectName,
104
115
  taskCounts: milestone.taskCounts,
116
+ health: milestone.health,
105
117
  }, tasks) + '\n');
106
118
  }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatUnarchiveResult = formatUnarchiveResult;
4
+ exports.milestoneUnarchive = milestoneUnarchive;
5
+ const config_1 = require("../lib/config");
6
+ const api_1 = require("../lib/api");
7
+ const resolve_1 = require("../lib/resolve");
8
+ const sanitize_1 = require("../lib/sanitize");
9
+ function formatUnarchiveResult(name) {
10
+ return `Unarchived "${(0, sanitize_1.sanitizeField)(name)}"`;
11
+ }
12
+ async function milestoneUnarchive(identifier, opts) {
13
+ const creds = (0, config_1.readCredentials)();
14
+ if (!creds) {
15
+ console.error('Error: not logged in. Run `lumo auth login` first.');
16
+ return 1;
17
+ }
18
+ const apiUrl = (0, api_1.resolveAuthedApiUrl)(creds.apiUrl);
19
+ const base = (0, api_1.trimTrailingSlash)(apiUrl);
20
+ let milestoneId;
21
+ try {
22
+ const resolved = await (0, resolve_1.resolveMilestoneId)(base, creds.token, identifier, opts.project);
23
+ milestoneId = resolved.id;
24
+ }
25
+ catch (err) {
26
+ console.error(`Error: ${err instanceof Error ? err.message : String(err)}`);
27
+ return 1;
28
+ }
29
+ let res;
30
+ try {
31
+ res = await fetch(`${base}/api/milestones/${milestoneId}/unarchive`, {
32
+ method: 'POST',
33
+ headers: { Authorization: `Bearer ${creds.token}` },
34
+ });
35
+ }
36
+ catch (err) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
39
+ return 1;
40
+ }
41
+ if (res.status === 401) {
42
+ console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
43
+ return 1;
44
+ }
45
+ if (!res.ok) {
46
+ let errMsg = `milestone unarchive failed (HTTP ${res.status})`;
47
+ try {
48
+ const body = (await res.json());
49
+ if (body.error)
50
+ errMsg = body.error;
51
+ }
52
+ catch {
53
+ // non-JSON body; keep the status-only message
54
+ }
55
+ console.error(`Error: ${(0, sanitize_1.sanitizeField)(errMsg)}`);
56
+ return 1;
57
+ }
58
+ const { milestone } = (await res.json());
59
+ process.stdout.write(formatUnarchiveResult(milestone.name) + '\n');
60
+ }
@@ -5,14 +5,16 @@ const config_1 = require("../lib/config");
5
5
  const wrap_panel_1 = require("../lib/wrap-panel");
6
6
  const progress_comment_section_1 = require("./wrap/progress-comment-section");
7
7
  const memory_review_section_1 = require("./wrap/memory-review-section");
8
+ const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
8
9
  /**
9
10
  * `lumo session wrap [--yes] [--dry-run]`
10
11
  *
11
- * Session-end wrap-up panel with two sections, run in order: (1) draft a
12
+ * Session-end wrap-up panel with three sections, run in order: (1) draft a
12
13
  * progress comment from this session's unposted turnSummaries and post it
13
14
  * (after y/e/s confirmation) to the bound task; (2) review the Layer1 memories
14
15
  * this session sedimented — keep/delete/promote, deduped by a per-session
15
- * watermark.
16
+ * watermark; (3) if the session repeatedly hit the same failure, prompt whether
17
+ * to flag the bound task with a `blocked` tag (LUM-153, prompt-only).
16
18
  */
17
19
  async function sessionWrap(options) {
18
20
  const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
@@ -29,6 +31,7 @@ async function sessionWrap(options) {
29
31
  const sections = [
30
32
  new progress_comment_section_1.ProgressCommentSection({ creds, sessionId }),
31
33
  new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
34
+ new blocked_prompt_section_1.BlockedPromptSection({ creds, sessionId }),
32
35
  ];
33
36
  await (0, wrap_panel_1.runWrapPanel)(sections, {
34
37
  yes: options.yes === true,
@@ -60,6 +60,10 @@ function formatTaskContextMarkdown(data, now) {
60
60
  ? `, target ${data.task.milestone.targetDate.slice(0, 10)}`
61
61
  : '';
62
62
  lines.push(`**Milestone**: ${(0, sanitize_1.sanitizeField)(data.task.milestone.name)} (${data.task.milestone.status}${target})`);
63
+ const milestoneGoal = data.task.milestone.description;
64
+ if (milestoneGoal && milestoneGoal.trim().length > 0) {
65
+ lines.push(`**Milestone goal**: ${(0, sanitize_1.sanitizeField)(milestoneGoal)}`);
66
+ }
63
67
  }
64
68
  const body = data.task.descriptionMarkdown ?? data.task.description;
65
69
  if (body && body.trim().length > 0) {
@@ -186,14 +186,22 @@ async function taskUpdate(identifier, opts) {
186
186
  const hasPatchFields = flagsGiven.length > 0 || hasTagFields;
187
187
  if (hasPatchFields) {
188
188
  const patchUrl = `${base}/api/tasks/by-identifier/${encodeURIComponent(identifier)}`;
189
+ // When run inside a Claude Code session, pass its id so a status→DONE
190
+ // update attributes the resulting Layer 2 PROJECT memories to this
191
+ // session — the next session-start surfaces them for review (LUM-165).
192
+ // Absent outside Claude Code; the server treats the header as optional.
193
+ const headers = {
194
+ Authorization: `Bearer ${creds.token}`,
195
+ 'Content-Type': 'application/json',
196
+ };
197
+ const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
198
+ if (sessionId)
199
+ headers['X-Lumo-Session-Id'] = sessionId;
189
200
  let res;
190
201
  try {
191
202
  res = await fetch(patchUrl, {
192
203
  method: 'PATCH',
193
- headers: {
194
- Authorization: `Bearer ${creds.token}`,
195
- 'Content-Type': 'application/json',
196
- },
204
+ headers,
197
205
  body: JSON.stringify(payload),
198
206
  });
199
207
  }
@@ -0,0 +1,64 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.BlockedPromptSection = void 0;
4
+ const sanitize_1 = require("../../lib/sanitize");
5
+ const line_prompt_1 = require("../../lib/line-prompt");
6
+ const failure_summary_api_1 = require("../../lib/failure-summary-api");
7
+ /**
8
+ * Wrap-panel section (LUM-153) that detects repeated same-type failures in this
9
+ * session and *prompts* whether to flag the bound task with a `blocked` tag.
10
+ * Prompt-only by design — it never flips status automatically, and it only
11
+ * shows up when the server says `shouldPrompt` (≥ threshold failures, bound
12
+ * task, not already blocked). Confirming attaches the tag; the empty/`s`
13
+ * default does nothing, so a stray Enter never tags the task.
14
+ */
15
+ class BlockedPromptSection {
16
+ deps;
17
+ title = '卡住检测';
18
+ draft = null;
19
+ constructor(deps) {
20
+ this.deps = deps;
21
+ }
22
+ async prepare() {
23
+ this.draft = await (0, failure_summary_api_1.fetchFailureSummary)(this.deps.creds, this.deps.sessionId);
24
+ return this.draft.shouldPrompt;
25
+ }
26
+ async run(opts) {
27
+ const draft = this.draft;
28
+ if (!draft || !draft.shouldPrompt || !draft.taskIdentifier)
29
+ return;
30
+ const top = draft.topFailure;
31
+ const where = top ? (0, sanitize_1.sanitizeField)(top.label) : '某个操作';
32
+ const count = top ? top.count : 0;
33
+ process.stdout.write(`看起来本次会话反复卡在 ${where}(${count} 次失败)。\n`);
34
+ if (top?.lastErrorSummary) {
35
+ process.stdout.write(`最后错误:${(0, sanitize_1.sanitizeField)(top.lastErrorSummary)}\n`);
36
+ }
37
+ if (opts.dryRun) {
38
+ process.stdout.write(`(dry-run,未改动;确认后会给 ${draft.taskIdentifier} 标 blocked)\n`);
39
+ return;
40
+ }
41
+ // Tagging the shared board is opt-in: it requires an explicit interactive
42
+ // `y`. `--yes` (and non-TTY, where promptLine returns empty) deliberately
43
+ // does NOT auto-tag — silently flipping shared board state is exactly what
44
+ // LUM-153 set out to avoid. We surface the suggestion and move on.
45
+ if (opts.yes) {
46
+ process.stdout.write(`(--yes 不自动标记;如确认请交互式回答 y,或手动 \`lumo task update ${draft.taskIdentifier} --add-tag blocked\`)\n`);
47
+ return;
48
+ }
49
+ const choice = (await (0, line_prompt_1.promptLine)(`要在 ${draft.taskIdentifier} 标 blocked 吗?[y] 标记 [s] 跳过 > `))
50
+ .trim()
51
+ .toLowerCase();
52
+ if (choice === 'y') {
53
+ await this.mark();
54
+ return;
55
+ }
56
+ // Empty / 's' / anything else → do nothing. Tagging is opt-in.
57
+ process.stdout.write('已跳过,未标记。\n');
58
+ }
59
+ async mark() {
60
+ const { taskIdentifier, tag } = await (0, failure_summary_api_1.markTaskBlocked)(this.deps.creds, this.deps.sessionId);
61
+ process.stdout.write(`已给 ${taskIdentifier} 标 ${tag}。\n`);
62
+ }
63
+ }
64
+ exports.BlockedPromptSection = BlockedPromptSection;
@@ -79,9 +79,13 @@ const milestone_create_1 = require("./commands/milestone-create");
79
79
  const milestone_show_1 = require("./commands/milestone-show");
80
80
  const milestone_update_1 = require("./commands/milestone-update");
81
81
  const milestone_delete_1 = require("./commands/milestone-delete");
82
+ const milestone_archive_1 = require("./commands/milestone-archive");
83
+ const milestone_unarchive_1 = require("./commands/milestone-unarchive");
82
84
  const milestone_add_1 = require("./commands/milestone-add");
83
85
  const milestone_remove_1 = require("./commands/milestone-remove");
84
86
  const milestone_summary_1 = require("./commands/milestone-summary");
87
+ const milestone_reorder_1 = require("./commands/milestone-reorder");
88
+ const milestone_move_1 = require("./commands/milestone-move");
85
89
  const sprint_create_1 = require("./commands/sprint-create");
86
90
  const sprint_list_1 = require("./commands/sprint-list");
87
91
  const sprint_show_1 = require("./commands/sprint-show");
@@ -411,8 +415,11 @@ const milestoneCmd = program
411
415
  .description('Inspect milestones from the terminal');
412
416
  milestoneCmd
413
417
  .command('list')
414
- .description('List milestones for a project. --project required when workspace has >1 project.')
418
+ .description('List milestones for a project. --project required when workspace has >1 project. By default only non-archived milestones are shown.')
415
419
  .option('--project <ref>', 'Project name or slug')
420
+ .option('--archived', 'Show only archived milestones')
421
+ .option('--all', 'Show both archived and non-archived milestones')
422
+ .option('--search <text>', 'Filter by name/description substring (case-insensitive)')
416
423
  .action(wrap(options => (0, milestone_list_1.milestoneList)(options)));
417
424
  milestoneCmd
418
425
  .command('create <name>')
@@ -443,6 +450,16 @@ milestoneCmd
443
450
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
444
451
  .option('--yes', 'Required: confirm deletion without TTY prompt')
445
452
  .action(wrap((identifier, options) => (0, milestone_delete_1.milestoneDelete)(identifier, options)));
453
+ milestoneCmd
454
+ .command('archive <identifier>')
455
+ .description('Archive a milestone: hidden from `milestone list` by default, history and task links preserved, reversible with `milestone unarchive`. Identifier accepts UUID or name.')
456
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
457
+ .action(wrap((identifier, options) => (0, milestone_archive_1.milestoneArchive)(identifier, options)));
458
+ milestoneCmd
459
+ .command('unarchive <identifier>')
460
+ .description('Restore an archived milestone so it shows in `milestone list` again. Identifier accepts UUID or name.')
461
+ .option('--project <ref>', 'Project name or slug (when identifier is a name)')
462
+ .action(wrap((identifier, options) => (0, milestone_unarchive_1.milestoneUnarchive)(identifier, options)));
446
463
  milestoneCmd
447
464
  .command('add <identifier> <tasks...>')
448
465
  .description('Bind one or more tasks to a milestone in one call. <identifier> accepts a name or UUID; each <task> accepts LUM-N or UUID. Per-task result with a tally; partial failures do not roll back.')
@@ -459,6 +476,18 @@ milestoneCmd
459
476
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
460
477
  .option('--retry', 'Trigger summary regeneration before fetching')
461
478
  .action(wrap((identifier, options) => (0, milestone_summary_1.milestoneSummary)(identifier, options)));
479
+ milestoneCmd
480
+ .command('reorder <refs...>')
481
+ .description("Reorder a project's milestones. Pass every milestone (name or UUID) in the desired order. --project required when workspace has >1 project.")
482
+ .option('--project <ref>', 'Project name or slug')
483
+ .action(wrap((refs, options) => (0, milestone_reorder_1.milestoneReorder)(refs, options)));
484
+ milestoneCmd
485
+ .command('move <ref>')
486
+ .description('Move one milestone before or after another. --before and --after are mutually exclusive; exactly one is required. --project required when workspace has >1 project.')
487
+ .option('--project <ref>', 'Project name or slug')
488
+ .option('--before <ref>', 'Place <ref> immediately before this milestone')
489
+ .option('--after <ref>', 'Place <ref> immediately after this milestone')
490
+ .action(wrap((ref, options) => (0, milestone_move_1.milestoneMove)(ref, options)));
462
491
  const sprintCmd = program
463
492
  .command('sprint')
464
493
  .description('Inspect sprints from the terminal');
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchFailureSummary = fetchFailureSummary;
4
+ exports.markTaskBlocked = markTaskBlocked;
5
+ const api_1 = require("./api");
6
+ function base(creds) {
7
+ return (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
8
+ }
9
+ /** GET the blocked-tag prompt draft for the session. Throws on transport / non-200. */
10
+ async function fetchFailureSummary(creds, sessionId) {
11
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/failure-summary`;
12
+ const res = await fetch(url, {
13
+ headers: { Authorization: `Bearer ${creds.token}` },
14
+ });
15
+ if (res.status === 401)
16
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
17
+ if (!res.ok)
18
+ throw new Error(`failure summary fetch failed (HTTP ${res.status})`);
19
+ return (await res.json());
20
+ }
21
+ /** POST to attach the `blocked` tag to the session's bound task. Throws the server message on non-201. */
22
+ async function markTaskBlocked(creds, sessionId) {
23
+ const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/mark-blocked`;
24
+ const res = await fetch(url, {
25
+ method: 'POST',
26
+ headers: { Authorization: `Bearer ${creds.token}` },
27
+ });
28
+ if (res.status === 401)
29
+ throw new Error('API key invalid or revoked. Run `lumo auth login`.');
30
+ if (res.status !== 201) {
31
+ let serverMsg = null;
32
+ try {
33
+ const errBody = (await res.json());
34
+ if (typeof errBody.error === 'string')
35
+ serverMsg = errBody.error;
36
+ }
37
+ catch {
38
+ // body wasn't JSON
39
+ }
40
+ throw new Error(serverMsg ?? `mark blocked failed (HTTP ${res.status})`);
41
+ }
42
+ return (await res.json());
43
+ }
@@ -115,6 +115,7 @@ function formatHookStdoutLines(path, responseBody, now = new Date()) {
115
115
  body.memorySection,
116
116
  body.linkedResourcesSection,
117
117
  body.reviewTodosSection,
118
+ body.layer2ReviewSection,
118
119
  ]);
119
120
  if (envelope)
120
121
  lines.push(envelope);
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveOrderedIds = resolveOrderedIds;
4
+ exports.computeMoveOrder = computeMoveOrder;
5
+ function findRef(ref, milestones) {
6
+ // Try by id first (exact match — milestone ids are cuids, unique).
7
+ const byId = milestones.find(m => m.id === ref);
8
+ if (byId)
9
+ return { kind: 'found', ref: byId };
10
+ // Then by name (case-insensitive). Names are NOT unique within a project
11
+ // (only slug is), so a name can match more than one milestone.
12
+ const needle = ref.trim().toLowerCase();
13
+ const hits = milestones.filter(m => m.name.toLowerCase() === needle);
14
+ if (hits.length === 0)
15
+ return { kind: 'not-found' };
16
+ if (hits.length === 1)
17
+ return { kind: 'found', ref: hits[0] };
18
+ return { kind: 'ambiguous', candidates: hits };
19
+ }
20
+ function ambiguousError(ref, candidates) {
21
+ const ids = candidates.map(c => c.id).join(', ');
22
+ return `ambiguous milestone name "${ref}" matches ${candidates.length} milestones; re-run with the id: ${ids}`;
23
+ }
24
+ /** Milestones in current display order (sortOrder asc, stable). */
25
+ function sorted(milestones) {
26
+ return [...milestones].sort((a, b) => a.sortOrder - b.sortOrder);
27
+ }
28
+ /**
29
+ * Resolve a full list of milestone refs (name or UUID) to ids in the given
30
+ * order. The list must name EVERY milestone in the project exactly once.
31
+ */
32
+ function resolveOrderedIds(refs, milestones) {
33
+ const ids = [];
34
+ const seen = new Set();
35
+ for (const ref of refs) {
36
+ const found = findRef(ref, milestones);
37
+ if (found.kind === 'not-found') {
38
+ return { ok: false, error: `unknown milestone: "${ref}"` };
39
+ }
40
+ if (found.kind === 'ambiguous') {
41
+ return { ok: false, error: ambiguousError(ref, found.candidates) };
42
+ }
43
+ const match = found.ref;
44
+ if (seen.has(match.id)) {
45
+ return { ok: false, error: `duplicate milestone: "${ref}"` };
46
+ }
47
+ seen.add(match.id);
48
+ ids.push(match.id);
49
+ }
50
+ if (ids.length !== milestones.length) {
51
+ const missing = milestones
52
+ .filter(m => !seen.has(m.id))
53
+ .map(m => m.name);
54
+ return {
55
+ ok: false,
56
+ error: `incomplete order: list every milestone. Missing: ${missing.join(', ')}`,
57
+ };
58
+ }
59
+ return { ok: true, orderedIds: ids };
60
+ }
61
+ /**
62
+ * Compute the full orderedIds after moving `moveRef` immediately before/after
63
+ * `targetRef` in the project's current order.
64
+ */
65
+ function computeMoveOrder(moveRef, targetRef, position, milestones) {
66
+ const moveFound = findRef(moveRef, milestones);
67
+ if (moveFound.kind === 'not-found') {
68
+ return { ok: false, error: `unknown milestone: "${moveRef}"` };
69
+ }
70
+ if (moveFound.kind === 'ambiguous') {
71
+ return { ok: false, error: ambiguousError(moveRef, moveFound.candidates) };
72
+ }
73
+ const targetFound = findRef(targetRef, milestones);
74
+ if (targetFound.kind === 'not-found') {
75
+ return { ok: false, error: `unknown milestone: "${targetRef}"` };
76
+ }
77
+ if (targetFound.kind === 'ambiguous') {
78
+ return { ok: false, error: ambiguousError(targetRef, targetFound.candidates) };
79
+ }
80
+ const move = moveFound.ref;
81
+ const target = targetFound.ref;
82
+ if (move.id === target.id) {
83
+ return { ok: false, error: 'cannot move a milestone relative to itself' };
84
+ }
85
+ const order = sorted(milestones)
86
+ .map(m => m.id)
87
+ .filter(id => id !== move.id);
88
+ const targetIndex = order.indexOf(target.id);
89
+ const insertAt = position === 'before' ? targetIndex : targetIndex + 1;
90
+ order.splice(insertAt, 0, move.id);
91
+ return { ok: true, orderedIds: order };
92
+ }
@@ -86,8 +86,7 @@ async function resolveTeamId(base, token, ref) {
86
86
  throw new Error('workspace has multiple teams; pass --team <slug>.');
87
87
  }
88
88
  const needle = ref.trim().toLowerCase();
89
- const match = teams.find(t => t.name.toLowerCase() === needle ||
90
- t.identifier.toLowerCase() === needle);
89
+ const match = teams.find(t => t.name.toLowerCase() === needle || t.identifier.toLowerCase() === needle);
91
90
  if (!match) {
92
91
  throw new Error(`team "${ref}" not found. Try \`lumo team list\`.`);
93
92
  }
@@ -160,11 +159,23 @@ async function resolveMilestoneId(base, token, identifier, projectRef) {
160
159
  return { id: identifier, name: '', projectId: '' };
161
160
  }
162
161
  const projectId = await resolveProjectId(base, token, projectRef);
163
- const { milestones } = await fetchJson(base, token, `/api/projects/${projectId}/milestones`);
162
+ const { milestones } = await fetchJson(base, token, `/api/projects/${projectId}/milestones?filter=all`);
163
+ // Exact id match first (milestone ids are cuids, unique).
164
+ const byId = milestones.find(m => m.id === identifier);
165
+ if (byId) {
166
+ return { id: byId.id, name: byId.name, projectId };
167
+ }
168
+ // Then by name (case-insensitive). Names are NOT unique within a project
169
+ // (only slug is), so reject an ambiguous name rather than silently picking
170
+ // the first match.
164
171
  const needle = identifier.trim().toLowerCase();
165
- const match = milestones.find(m => m.name.toLowerCase() === needle);
166
- if (!match) {
172
+ const hits = milestones.filter(m => m.name.toLowerCase() === needle);
173
+ if (hits.length === 0) {
167
174
  throw new Error(`no milestone matches "${identifier}" in this project. Try \`lumo milestone list\`.`);
168
175
  }
169
- return { id: match.id, name: match.name, projectId };
176
+ if (hits.length > 1) {
177
+ const ids = hits.map(h => h.id).join(', ');
178
+ throw new Error(`ambiguous milestone name "${identifier}" matches ${hits.length} milestones; re-run with the id: ${ids}`);
179
+ }
180
+ return { id: hits[0].id, name: hits[0].name, projectId };
170
181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.11.0",
3
+ "version": "1.15.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",