@lumoai/cli 1.19.0 → 1.20.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/SKILL.md +6 -5
- package/assets/skill/references/sessions.md +42 -19
- package/assets/skill/references/task-context.md +26 -0
- package/dist/cli/src/commands/session-wrap.js +6 -3
- package/dist/cli/src/commands/task-context.js +20 -13
- package/dist/cli/src/commands/task-lineage.js +113 -0
- package/dist/cli/src/commands/wrap/fragment-usage-section.js +66 -0
- package/dist/cli/src/index.js +6 -0
- package/dist/cli/src/lib/fragment-usage-api.js +47 -0
- package/dist/cli/src/lib/hook-runner.js +24 -69
- package/dist/cli/src/lib/transcript-usage.js +30 -7
- package/package.json +1 -1
package/assets/skill/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, 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", "进度评论", "卡住检测", "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".'
|
|
3
|
+
description: 'Use the Lumo CLI to load task context, manage session bindings, and run tasks / projects / milestones / sprints / docs / memory from the terminal. Activate when: the user mentions a Lumo task identifier (LUM-42 etc.), asks to load task background/context, wants to bind/check/detach a Claude Code session''s task, is about to start work on a task, or wants to create/update/list/show/comment on tasks, projects, milestones, sprints, documents, artifacts, Figma links, or memory. Triggers on: "LUM-", "task context", "load context", "session start", "session attach", "session status", "session detach", "which task", "what task am I on", "work on LUM", "session wrap", "wrap up session", "进度评论", "卡住检测", "fragment usage vote", "mark used fragments", "which fragments did I use", "--used", "上下文使用投票", "标记用过的记忆", "create task", "new task", "file a task", "list tasks", "my tasks", "show task", "view task", "comment on task", "update task", "change task status", "rename task", "reassign task", "mark task as done", "lumo next", "next task", "what should I work on", "推荐下一个任务", "list projects", "what projects", "milestone", "里程碑", "list/create/update/delete/show milestone", "set milestone", "attach/unbind milestone", "tasks in milestone", "search milestones", "find milestone", "milestone health", "at-risk", "overdue", "archive/unarchive milestone", "归档里程碑", "milestone summary", "里程碑复盘", "reorder/move milestone", "排序里程碑", "milestone add/remove", "挂任务到里程碑", "auth login", "log in", "logout", "sign out", "switch account", "whoami", "who am I", "current workspace", "登录", "切换账号", "create/update/list/show/delete doc", "write doc", "写文档", "新建文档", "修改文档", "查看文档", "bind/unbind doc", "把文档关联到任务", "doc scope", "personal/workspace doc", "tag", "add/remove tag", "标签", "share/unshare doc", "分享文档", "doc share-list", "viewer/editor/manager", "doc tree", "doc move", "move/reparent doc", "移动文档", "sprint", "冲刺", "迭代", "create/list/show/update/delete sprint", "start/close sprint", "开始/关闭冲刺", "add to sprint", "active sprints", "sprint summary", "冲刺总结", "把任务挂到冲刺", "sprint health", "sprint risk", "is this sprint at risk", "冲刺风险", "冲刺健康度", "sprint blockers", "冲刺阻塞", "lumo update", "upgrade lumo", "升级 lumo", "new lumo version", "lumo setup", "install lumo skill/hooks", "wire up lumo", "set up lumo", "安装 lumo", "配置 lumo", "task artifact", "artifact add/list/show/update/rm", "spec artifact", "record/attach spec", "attach plan", "记录 spec", "查看 artifact", figma, attach figma, figma link, 关联 figma, 设计稿, figma design, "memory", "记忆", "remember", "record a memory", "记一条", "promote memory", "promote to project", "沉淀", "task/project memory", "retrieval", "取全文", "拉全文", "task slack show", "看 thread", "show slack thread", "task web show", "web 正文", "task figma context", "figma metadata", "task comments list", "list comments", "看评论", "task pr show", "查看 PR", "show pr", "PR 详情", "import google doc", "sync google doc", "google drive", "doc import-gdoc", "doc sync", "导入/同步 google 文档", "mark blocked", "blocked tag", "标记 blocked", "stuck", "repeatedly failing", "worktree", "git worktree", "并行 worktree", "scaffold worktree", "新建 worktree", "node_modules 软链", "worktree 隔离", "lumo worktree add/rm/list", "task lineage", "lineage", "causal trail", "审计", "因果链", "成本归因", "trace context".'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
## Prerequisites
|
|
@@ -29,7 +29,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
29
29
|
| `doc*` | [references/docs.md](references/docs.md) |
|
|
30
30
|
| `sprint*` | [references/sprints.md](references/sprints.md) |
|
|
31
31
|
| `task/project memory`, `memory promote/rm` | [references/memory.md](references/memory.md) |
|
|
32
|
-
| `session attach/status/detach/wrap`,
|
|
32
|
+
| `session attach/status/detach/wrap`, git-suggest on start, Layer-2 review | [references/sessions.md](references/sessions.md) |
|
|
33
33
|
| `worktree add/rm/list` (local dev tooling) | [references/worktree.md](references/worktree.md) |
|
|
34
34
|
|
|
35
35
|
## Command catalog
|
|
@@ -49,6 +49,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
49
49
|
- `lumo task figma context <id> <linkId>` — Figma link metadata (v1)
|
|
50
50
|
- `lumo task comments list <id>` — full comment thread (read-only; ≠ `task comment`)
|
|
51
51
|
- `lumo task pr show <id> <number>` — synced PR metadata (v1)
|
|
52
|
+
- `lumo task lineage <id>` — show the causal trail: fragments that fed the task + each one's outcome + the run's token/loop cost (read-only audit view)
|
|
52
53
|
|
|
53
54
|
**Tasks** — see [tasks.md](references/tasks.md)
|
|
54
55
|
|
|
@@ -97,8 +98,8 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
97
98
|
|
|
98
99
|
- `lumo session attach <id>` — bind this session to a task (then run `task context`)
|
|
99
100
|
- `lumo session status` / `lumo session detach` — show / clear binding
|
|
100
|
-
- `lumo session wrap [--yes] [--dry-run]` — end-of-session panel: progress comment + memory review + blocked-tag prompt
|
|
101
|
-
-
|
|
101
|
+
- `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — end-of-session panel: progress comment + memory review + fragment-usage vote (`--used`, LUM-300) + blocked-tag prompt
|
|
102
|
+
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
102
103
|
|
|
103
104
|
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
104
105
|
|
|
@@ -115,4 +116,4 @@ Typical flow when a user says "help me with LUM-42":
|
|
|
115
116
|
3. Review unresolved items, PR-review todos, and the task description
|
|
116
117
|
4. Begin working on the task
|
|
117
118
|
|
|
118
|
-
**
|
|
119
|
+
**Git-suggest at start:** when the session is unbound, session-start may infer the task from the git branch / recent commits and print a suggestion — `检测到 LUM-N … 运行 lumo session attach LUM-N 绑定。` — **without** binding. Confirm it's the right task, then run `lumo session attach <LUM-N>` yourself (binding only happens on an explicit attach). See [sessions.md](references/sessions.md) for the full session-start behavior.
|
|
@@ -2,26 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
## Session Management
|
|
4
4
|
|
|
5
|
-
###
|
|
5
|
+
### Suggest-on-start from local git (no auto-bind)
|
|
6
6
|
|
|
7
7
|
When a session starts **without** a bound task, the `session-start` hook tries to
|
|
8
|
-
infer the task from local git
|
|
8
|
+
infer the task from local git so it can **suggest** one — it never binds for you
|
|
9
|
+
(LUM-302):
|
|
9
10
|
|
|
10
11
|
- It reads the **current branch name** first (e.g. `lumo/LUM-145-...`), then the
|
|
11
12
|
**most recent commit subjects** (e.g. `... [LUM-145]`), extracting the first
|
|
12
13
|
`LUM-<n>`.
|
|
13
|
-
- On a hit it
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
- On a hit it prints a single suggestion line and stops — the session stays
|
|
15
|
+
**unbound** and no context is injected yet:
|
|
16
|
+
`检测到 LUM-145(依据分支名/最近 commit)。运行 lumo session attach LUM-145 绑定。`
|
|
17
|
+
No task title is shown here because nothing was fetched; the title, memory,
|
|
18
|
+
and PR-review todos appear only once you actually attach.
|
|
17
19
|
- No match (detached HEAD, a non-lumo branch with no tagged commits, not a git
|
|
18
|
-
repo)
|
|
19
|
-
unbound prompt.
|
|
20
|
+
repo) → it degrades to the normal unbound prompt.
|
|
20
21
|
|
|
21
|
-
**Agent guidance:**
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
**Agent guidance:** when you see a suggestion line, confirm the inferred task is
|
|
23
|
+
the one the user wants, then run `lumo session attach <LUM-N>` (followed by
|
|
24
|
+
`lumo task context <LUM-N>` to load the background). `session attach` is the
|
|
25
|
+
**only** path that binds — there is nothing to "undo" if the inference is wrong;
|
|
26
|
+
just attach the correct task instead.
|
|
25
27
|
|
|
26
28
|
### Layer 2 project-memory review at session start
|
|
27
29
|
|
|
@@ -62,7 +64,7 @@ lumo session attach LUM-42 # already on LUM-7 → prompts (TTY) / ref
|
|
|
62
64
|
lumo session attach LUM-42 --force # overwrite without confirmation
|
|
63
65
|
```
|
|
64
66
|
|
|
65
|
-
**Agent guidance:** when `session attach` reports the session is already bound to a different task, **ask the user** whether to switch before re-running with `--force`. Don't auto-`--force` — the existing binding may be intentional (e.g.
|
|
67
|
+
**Agent guidance:** when `session attach` reports the session is already bound to a different task, **ask the user** whether to switch before re-running with `--force`. Don't auto-`--force` — the existing binding may be intentional (e.g. a manual attach the user ran earlier). Alternatively run `lumo session detach` first, then a clean `session attach`.
|
|
66
68
|
|
|
67
69
|
### Parallel sessions
|
|
68
70
|
|
|
@@ -88,9 +90,9 @@ lumo session detach
|
|
|
88
90
|
|
|
89
91
|
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).
|
|
90
92
|
|
|
91
|
-
### `lumo session wrap [--yes] [--dry-run]` — wrap-up panel: progress comment + memory review + blocked-tag prompt
|
|
93
|
+
### `lumo session wrap [--yes] [--dry-run] [--used <indices>]` — wrap-up panel: progress comment + memory review + fragment-usage vote + blocked-tag prompt
|
|
92
94
|
|
|
93
|
-
Session-end wrap-up panel with **
|
|
95
|
+
Session-end wrap-up panel with **four sections, run in order**:
|
|
94
96
|
|
|
95
97
|
**1. 进度评论** — reads back the current Claude Code session's per-turn
|
|
96
98
|
`turnSummary` rows (the one-line Chinese summaries written each STOP), aggregates
|
|
@@ -110,7 +112,21 @@ Out-of-range indices are ignored. Deletes/promotes run server-side, scoped to
|
|
|
110
112
|
memories this session created (you can't touch other sessions' memories through
|
|
111
113
|
this panel). With no new memories the section prints "(无内容)" and does nothing.
|
|
112
114
|
|
|
113
|
-
**3.
|
|
115
|
+
**3. 上下文使用投票 (fragment-usage vote, LUM-300)** — lists the context
|
|
116
|
+
fragments this session **consumed** (its lineage edges: memory / slack / web /
|
|
117
|
+
figma / PR / review-todo / session), numbered from 1 with a content snippet
|
|
118
|
+
label. The agent records which it **actually used** via
|
|
119
|
+
`lumo session wrap --used <indices>` (1-based, comma/space separated; `--used
|
|
120
|
+
none` = used nothing). Voted fragments get `used=true`, the rest of the
|
|
121
|
+
session's fragments `used=false`. **Without `--used` the section only lists the
|
|
122
|
+
candidates and writes nothing** (edges stay `null` = not voted — honest, not
|
|
123
|
+
"unused"). A session that already voted (`usedAt` set) is skipped. **Why:** it
|
|
124
|
+
upgrades the flywheel signal from "co-loaded" (constant, no information) to
|
|
125
|
+
"actually used" (varies → discriminative); `task context` then prefers each
|
|
126
|
+
fragment's usage-based merge rate, falling back to the weaker presence rate when
|
|
127
|
+
usage samples are thin. With no consumed fragments the section prints "(无内容)".
|
|
128
|
+
|
|
129
|
+
**4. 卡住检测 (blocked-tag prompt, LUM-153)** — if the **same kind of failure
|
|
114
130
|
recurred ≥ 3 times** in this session (server-aggregated from
|
|
115
131
|
`POST_TOOL_USE_FAILURE` events grouped by tool name, plus `STOP_FAILURE`
|
|
116
132
|
turn-level failures), the section surfaces the dominant failure (`卡在 <tool>
|
|
@@ -128,11 +144,18 @@ suggestion and moves on rather than silently flipping board state. When there's
|
|
|
128
144
|
nothing to prompt, the section prints "(无内容)".
|
|
129
145
|
|
|
130
146
|
```bash
|
|
131
|
-
lumo session wrap
|
|
132
|
-
lumo session wrap --yes
|
|
133
|
-
lumo session wrap --
|
|
147
|
+
lumo session wrap # interactive: preview each section, choose per-section
|
|
148
|
+
lumo session wrap --yes # progress posted + memories kept; blocked tag NOT auto-applied (needs interactive y)
|
|
149
|
+
lumo session wrap --yes --used 1,3 # also record fragments 1 & 3 as used (the rest used=false)
|
|
150
|
+
lumo session wrap --used none # record that none of the injected fragments were used
|
|
151
|
+
lumo session wrap --dry-run # print all drafts only; never posts, never mutates, never advances watermarks
|
|
134
152
|
```
|
|
135
153
|
|
|
154
|
+
The usage vote is a two-step flow for agents: run `lumo session wrap` once to
|
|
155
|
+
see the numbered fragment list, decide which you actually used, then re-run with
|
|
156
|
+
`--used <indices>`. Re-running is safe — the other sections are watermark-guarded
|
|
157
|
+
(progress won't double-post, reviewed memories won't re-list).
|
|
158
|
+
|
|
136
159
|
- Requires `$CLAUDE_CODE_SESSION_ID` (must run inside Claude Code) and a bound
|
|
137
160
|
task (`lumo session attach <LUM-N>` first). With no bound task or no new turn
|
|
138
161
|
summaries, the 进度评论 section prints "(无内容)" and posts nothing.
|
|
@@ -22,6 +22,7 @@ The command prints a markdown document to stdout containing:
|
|
|
22
22
|
5. **Previous sessions** — ordered newest-first, each with:
|
|
23
23
|
- A headline summary of what was done
|
|
24
24
|
- Unresolved items (carry-over TODOs from that session)
|
|
25
|
+
6. **飞轮信号 · 历史 merge 贡献** — appended at the very end. For each context fragment with enough history, a one-line historical merge-contribution signal (e.g. `出现在 5 个已闭合任务·4 merged (80%)`), computed from accumulated lineage edges. Denominator = distinct tasks where the fragment appeared with a resolved (non-UNKNOWN) outcome; only fragments with ≥3 such tasks are shown (cold-start gate), so the block is often absent early on. This is **historical correlation, not causation** — don't read it as a prediction.
|
|
25
26
|
|
|
26
27
|
### How to use the context
|
|
27
28
|
|
|
@@ -107,3 +108,28 @@ require the GitHub integration, so the command ends with a `note:` saying so.
|
|
|
107
108
|
```bash
|
|
108
109
|
lumo task pr show LUM-42 128
|
|
109
110
|
```
|
|
111
|
+
|
|
112
|
+
## `lumo task lineage <id>`
|
|
113
|
+
|
|
114
|
+
Read-only audit view over the task's `LineageEdge` rows. Given a task
|
|
115
|
+
identifier (`LUM-N`), prints the causal trail:
|
|
116
|
+
|
|
117
|
+
- **Totals banner** — distinct runs/sessions, fragment count, edge count,
|
|
118
|
+
total tokens (input/output/cache split) and loops, and the outcome
|
|
119
|
+
distribution.
|
|
120
|
+
- **One block per run/session** — the group's cost shown **once** (token/loop),
|
|
121
|
+
the date it consumed context, then each context fragment as
|
|
122
|
+
`[OUTCOME] TYPE — <source label>`, plus a per-group outcome summary.
|
|
123
|
+
|
|
124
|
+
Cost is attributed once per run/session (a run that injected many fragments is
|
|
125
|
+
not double-counted). Fragment ids are canonical — MEMORY fragments survive
|
|
126
|
+
consolidation drift.
|
|
127
|
+
|
|
128
|
+
**Cold start:** a task with no edges prints a friendly note (lineage is captured
|
|
129
|
+
when a session-bound run consumes the task's context), not an error.
|
|
130
|
+
|
|
131
|
+
**When to suggest:** the user wants to audit "what context did the AI actually
|
|
132
|
+
use, and what did it cost" for a task / merged PR — CFO / compliance / trust
|
|
133
|
+
narratives.
|
|
134
|
+
|
|
135
|
+
Entry point is the task identifier only; PR-number lookup is a future addition.
|
|
@@ -5,16 +5,18 @@ 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 fragment_usage_section_1 = require("./wrap/fragment-usage-section");
|
|
8
9
|
const blocked_prompt_section_1 = require("./wrap/blocked-prompt-section");
|
|
9
10
|
/**
|
|
10
11
|
* `lumo session wrap [--yes] [--dry-run]`
|
|
11
12
|
*
|
|
12
|
-
* Session-end wrap-up panel with
|
|
13
|
+
* Session-end wrap-up panel with four sections, run in order: (1) draft a
|
|
13
14
|
* progress comment from this session's unposted turnSummaries and post it
|
|
14
15
|
* (after y/e/s confirmation) to the bound task; (2) review the Layer1 memories
|
|
15
16
|
* this session sedimented — keep/delete/promote, deduped by a per-session
|
|
16
|
-
* watermark; (3)
|
|
17
|
-
*
|
|
17
|
+
* watermark; (3) vote which injected context fragments were actually used
|
|
18
|
+
* (LUM-300, via `--used`); (4) if the session repeatedly hit the same failure,
|
|
19
|
+
* prompt whether to flag the bound task with a `blocked` tag (LUM-153).
|
|
18
20
|
*/
|
|
19
21
|
async function sessionWrap(options) {
|
|
20
22
|
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
@@ -31,6 +33,7 @@ async function sessionWrap(options) {
|
|
|
31
33
|
const sections = [
|
|
32
34
|
new progress_comment_section_1.ProgressCommentSection({ creds, sessionId }),
|
|
33
35
|
new memory_review_section_1.MemoryReviewSection({ creds, sessionId }),
|
|
36
|
+
new fragment_usage_section_1.FragmentUsageSection({ creds, sessionId, used: options.used }),
|
|
34
37
|
new blocked_prompt_section_1.BlockedPromptSection({ creds, sessionId }),
|
|
35
38
|
];
|
|
36
39
|
await (0, wrap_panel_1.runWrapPanel)(sections, {
|
|
@@ -114,21 +114,28 @@ function formatTaskContextMarkdown(data, now) {
|
|
|
114
114
|
lines.push('');
|
|
115
115
|
lines.push('_No prior coding sessions for this task._');
|
|
116
116
|
lines.push('');
|
|
117
|
-
return lines.join('\n');
|
|
118
117
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
else {
|
|
119
|
+
lines.push(`## Previous Sessions (${data.sessions.length})`);
|
|
120
|
+
lines.push('');
|
|
121
|
+
for (const s of data.sessions) {
|
|
122
|
+
const shortId = s.id.slice(0, 8);
|
|
123
|
+
const ago = (0, format_1.relativeTime)(new Date(s.lastActivityAt), now);
|
|
124
|
+
const dur = (0, format_1.formatDuration)(s.durationMs);
|
|
125
|
+
lines.push(`### Session ${shortId} · ${ago} · ${dur}`);
|
|
126
|
+
lines.push(`**Summary**: ${(0, sanitize_1.sanitizeField)(s.headline)}`);
|
|
127
|
+
if (s.unresolved.length > 0) {
|
|
128
|
+
lines.push('**Unresolved**:');
|
|
129
|
+
for (const u of s.unresolved)
|
|
130
|
+
lines.push(`- ${(0, sanitize_1.sanitizeField)(u)}`);
|
|
131
|
+
}
|
|
132
|
+
lines.push('');
|
|
131
133
|
}
|
|
134
|
+
}
|
|
135
|
+
// Flywheel signal goes last so it reads as a closing "historical payoff"
|
|
136
|
+
// footer (LUM-278). Server returns "" below the cold-start floor.
|
|
137
|
+
if (data.lineageSection && data.lineageSection.trim().length > 0) {
|
|
138
|
+
lines.push((0, sanitize_1.sanitizeField)(data.lineageSection.trimEnd()));
|
|
132
139
|
lines.push('');
|
|
133
140
|
}
|
|
134
141
|
return lines.join('\n');
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.taskLineage = taskLineage;
|
|
4
|
+
exports.formatLineageMarkdown = formatLineageMarkdown;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
async function taskLineage(identifier) {
|
|
9
|
+
if (!identifier) {
|
|
10
|
+
console.error('Error: missing <identifier>. Usage: lumo task lineage <LUM-42>');
|
|
11
|
+
return 1;
|
|
12
|
+
}
|
|
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 url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/tasks/lineage/${encodeURIComponent(identifier)}`;
|
|
20
|
+
let res;
|
|
21
|
+
try {
|
|
22
|
+
res = await fetch(url, {
|
|
23
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
28
|
+
console.error(`Error: could not reach Lumo API at ${apiUrl} (${msg})`);
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
if (res.status === 401) {
|
|
32
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
if (res.status === 404) {
|
|
36
|
+
console.error(`Error: task ${identifier} not found in workspace ${creds.workspaceSlug}`);
|
|
37
|
+
return 1;
|
|
38
|
+
}
|
|
39
|
+
if (!res.ok) {
|
|
40
|
+
console.error(`Error: lineage fetch failed (HTTP ${res.status})`);
|
|
41
|
+
return 1;
|
|
42
|
+
}
|
|
43
|
+
const data = (await res.json());
|
|
44
|
+
process.stdout.write(formatLineageMarkdown(data));
|
|
45
|
+
}
|
|
46
|
+
/** Deterministic thousands separator (no locale dependency, test-stable). */
|
|
47
|
+
function groupThousands(n) {
|
|
48
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
|
49
|
+
}
|
|
50
|
+
const OUTCOME_ORDER = ['MERGED', 'REWORKED', 'REJECTED', 'UNKNOWN'];
|
|
51
|
+
/** "2 MERGED · 1 UNKNOWN", fixed order, zeros omitted. */
|
|
52
|
+
function outcomeSummary(counts) {
|
|
53
|
+
const parts = OUTCOME_ORDER.filter(o => counts[o] > 0).map(o => `${counts[o]} ${o}`);
|
|
54
|
+
return parts.join(' · ');
|
|
55
|
+
}
|
|
56
|
+
function fragmentOutcomeCounts(fragments) {
|
|
57
|
+
const c = {
|
|
58
|
+
MERGED: 0,
|
|
59
|
+
REJECTED: 0,
|
|
60
|
+
REWORKED: 0,
|
|
61
|
+
UNKNOWN: 0,
|
|
62
|
+
};
|
|
63
|
+
for (const f of fragments)
|
|
64
|
+
c[f.outcome] += 1;
|
|
65
|
+
return c;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Render a LineageResponse as the audit-facing markdown trail. Pure function
|
|
69
|
+
* (no clock / env) so the CLI output is deterministic and unit-testable.
|
|
70
|
+
*/
|
|
71
|
+
function formatLineageMarkdown(data) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push(`# Lineage: ${data.task.identifier} — ${(0, sanitize_1.sanitizeField)(data.task.title)}`);
|
|
74
|
+
lines.push(`**Status**: ${data.task.status}`);
|
|
75
|
+
lines.push('');
|
|
76
|
+
if (data.groups.length === 0) {
|
|
77
|
+
lines.push('_No lineage edges recorded yet. Lineage is captured when a ' +
|
|
78
|
+
"session-bound run consumes this task's context; once that happens " +
|
|
79
|
+
'(and a PR merges / the task closes), the causal trail and its cost ' +
|
|
80
|
+
'will appear here._');
|
|
81
|
+
lines.push('');
|
|
82
|
+
return lines.join('\n');
|
|
83
|
+
}
|
|
84
|
+
const t = data.totals;
|
|
85
|
+
lines.push('## Totals');
|
|
86
|
+
lines.push(`- Runs/sessions: ${t.runCount}`);
|
|
87
|
+
lines.push(`- Fragments: ${t.fragmentCount}`);
|
|
88
|
+
lines.push(`- Edges: ${t.edgeCount}`);
|
|
89
|
+
lines.push(`- Tokens: ${groupThousands(t.tokens.total)} ` +
|
|
90
|
+
`(in ${groupThousands(t.tokens.input)} · out ${groupThousands(t.tokens.output)} · ` +
|
|
91
|
+
`cache-read ${groupThousands(t.tokens.cacheRead)} · cache-create ${groupThousands(t.tokens.cacheCreation)})`);
|
|
92
|
+
lines.push(`- Loops: ${t.loopCount}`);
|
|
93
|
+
const totalsOutcome = outcomeSummary(t.outcomes);
|
|
94
|
+
if (totalsOutcome)
|
|
95
|
+
lines.push(`- Outcomes: ${totalsOutcome}`);
|
|
96
|
+
lines.push('');
|
|
97
|
+
for (const g of data.groups) {
|
|
98
|
+
lines.push(`## ${(0, sanitize_1.sanitizeField)(g.label)} · ${g.includedAt.slice(0, 10)}`);
|
|
99
|
+
if (g.cost) {
|
|
100
|
+
lines.push(`**Cost**: ${groupThousands(g.cost.total)} tokens · ${g.cost.loopCount} loops`);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
lines.push('**Cost**: (no captured cost)');
|
|
104
|
+
}
|
|
105
|
+
const summary = outcomeSummary(fragmentOutcomeCounts(g.fragments));
|
|
106
|
+
lines.push(`**Fragments** (${g.fragments.length}${summary ? `: ${summary}` : ''}):`);
|
|
107
|
+
for (const f of g.fragments) {
|
|
108
|
+
lines.push(`- [${f.outcome}] ${f.fragmentType} — ${(0, sanitize_1.sanitizeField)(f.sourceLabel)}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('');
|
|
111
|
+
}
|
|
112
|
+
return lines.join('\n');
|
|
113
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FragmentUsageSection = void 0;
|
|
4
|
+
exports.parseUsedHandles = parseUsedHandles;
|
|
5
|
+
const sanitize_1 = require("../../lib/sanitize");
|
|
6
|
+
const fragment_usage_api_1 = require("../../lib/fragment-usage-api");
|
|
7
|
+
/** Parse "1,3 5" → 0-based indices. "none" → []. */
|
|
8
|
+
function parseUsedHandles(spec) {
|
|
9
|
+
if (spec.trim().toLowerCase() === 'none')
|
|
10
|
+
return [];
|
|
11
|
+
return spec
|
|
12
|
+
.split(/[\s,]+/)
|
|
13
|
+
.map(s => s.trim())
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.map(s => parseInt(s, 10) - 1)
|
|
16
|
+
.filter(n => Number.isInteger(n) && n >= 0);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Wrap-panel section (LUM-300) that lists the context fragments this session
|
|
20
|
+
* consumed and records the agent's vote on which it actually used. Voting is
|
|
21
|
+
* non-interactive: the agent passes `lumo session wrap --used <indices>` (or
|
|
22
|
+
* `--used none`). Without `--used`, the section just lists candidates and writes
|
|
23
|
+
* nothing — edges stay null (honest "not voted"). Already-voted sessions skip.
|
|
24
|
+
*/
|
|
25
|
+
class FragmentUsageSection {
|
|
26
|
+
deps;
|
|
27
|
+
title = '上下文使用投票';
|
|
28
|
+
draft = null;
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
}
|
|
32
|
+
async prepare() {
|
|
33
|
+
this.draft = await (0, fragment_usage_api_1.fetchUsageDraft)(this.deps.creds, this.deps.sessionId);
|
|
34
|
+
return this.draft.candidates.length > 0;
|
|
35
|
+
}
|
|
36
|
+
async run(opts) {
|
|
37
|
+
const draft = this.draft;
|
|
38
|
+
if (!draft || draft.candidates.length === 0)
|
|
39
|
+
return;
|
|
40
|
+
if (draft.alreadyVoted) {
|
|
41
|
+
process.stdout.write('本 session 已投票,跳过。\n');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write('本次会话注入的 context fragment:\n');
|
|
45
|
+
draft.candidates.forEach((c, i) => {
|
|
46
|
+
process.stdout.write(` [${i + 1}] ${(0, sanitize_1.sanitizeField)(c.label)}\n`);
|
|
47
|
+
});
|
|
48
|
+
if (opts.dryRun) {
|
|
49
|
+
process.stdout.write('(dry-run,未改动)\n');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (this.deps.used === undefined) {
|
|
53
|
+
process.stdout.write('用 `lumo session wrap --used <序号>`(或 `--used none`)记录你实际用到的 fragment。\n');
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const idx = parseUsedHandles(this.deps.used);
|
|
57
|
+
const inRange = (n) => n >= 0 && n < draft.candidates.length;
|
|
58
|
+
const usedRefs = idx.filter(inRange).map(i => ({
|
|
59
|
+
fragmentType: draft.candidates[i].fragmentType,
|
|
60
|
+
fragmentId: draft.candidates[i].fragmentId,
|
|
61
|
+
}));
|
|
62
|
+
const { used, unused } = await (0, fragment_usage_api_1.applyFragmentUsage)(this.deps.creds, this.deps.sessionId, { usedRefs });
|
|
63
|
+
process.stdout.write(`已记录:用过 ${used} 个,未用 ${unused} 个。\n`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
exports.FragmentUsageSection = FragmentUsageSection;
|
package/dist/cli/src/index.js
CHANGED
|
@@ -73,6 +73,7 @@ const task_web_show_1 = require("./commands/task-web-show");
|
|
|
73
73
|
const task_figma_context_1 = require("./commands/task-figma-context");
|
|
74
74
|
const task_comment_list_1 = require("./commands/task-comment-list");
|
|
75
75
|
const task_pr_show_1 = require("./commands/task-pr-show");
|
|
76
|
+
const task_lineage_1 = require("./commands/task-lineage");
|
|
76
77
|
const project_list_1 = require("./commands/project-list");
|
|
77
78
|
const milestone_list_1 = require("./commands/milestone-list");
|
|
78
79
|
const milestone_create_1 = require("./commands/milestone-create");
|
|
@@ -218,6 +219,7 @@ session
|
|
|
218
219
|
.description("Session-end wrap-up: draft a progress comment from this session's turn summaries and post it to the bound task after confirmation.")
|
|
219
220
|
.option('-y, --yes', 'Post the drafted comment without prompting (agent-friendly)')
|
|
220
221
|
.option('--dry-run', 'Print the draft but do not post or advance the watermark')
|
|
222
|
+
.option('--used <indices>', 'Mark which injected context fragments you actually used (1-based indices, comma/space separated; "none" for all-unused). Omit to skip recording.')
|
|
221
223
|
.action(wrap(options => (0, session_wrap_1.sessionWrap)(options)));
|
|
222
224
|
const task = program
|
|
223
225
|
.command('task')
|
|
@@ -308,6 +310,10 @@ taskPr
|
|
|
308
310
|
.command('show <identifier> <number>')
|
|
309
311
|
.description('Show the synced PR record (diff/review comments when available)')
|
|
310
312
|
.action(wrap((id, num) => (0, task_pr_show_1.taskPrShow)(id, num)));
|
|
313
|
+
task
|
|
314
|
+
.command('lineage <identifier>')
|
|
315
|
+
.description("Show the causal trail: which context fragments fed this task, each fragment's outcome, and the token/loop cost of the run that consumed it")
|
|
316
|
+
.action(wrap(id => (0, task_lineage_1.taskLineage)(id)));
|
|
311
317
|
const taskMemory = task
|
|
312
318
|
.command('memory')
|
|
313
319
|
.description('View and record memories scoped to a task');
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fetchUsageDraft = fetchUsageDraft;
|
|
4
|
+
exports.applyFragmentUsage = applyFragmentUsage;
|
|
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 fragment-usage draft (this session's injected fragments). */
|
|
10
|
+
async function fetchUsageDraft(creds, sessionId) {
|
|
11
|
+
const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/fragment-usage`;
|
|
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(`usage draft fetch failed (HTTP ${res.status})`);
|
|
19
|
+
return (await res.json());
|
|
20
|
+
}
|
|
21
|
+
/** POST the usage vote (used refs). Throws the server message on non-2xx. */
|
|
22
|
+
async function applyFragmentUsage(creds, sessionId, payload) {
|
|
23
|
+
const url = `${base(creds)}/api/sessions/${encodeURIComponent(sessionId)}/fragment-usage`;
|
|
24
|
+
const res = await fetch(url, {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${creds.token}`,
|
|
28
|
+
'Content-Type': 'application/json',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(payload),
|
|
31
|
+
});
|
|
32
|
+
if (res.status === 401)
|
|
33
|
+
throw new Error('API key invalid or revoked. Run `lumo auth login`.');
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
let serverMsg = null;
|
|
36
|
+
try {
|
|
37
|
+
const errBody = (await res.json());
|
|
38
|
+
if (typeof errBody.error === 'string')
|
|
39
|
+
serverMsg = errBody.error;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// body wasn't JSON
|
|
43
|
+
}
|
|
44
|
+
throw new Error(serverMsg ?? `usage vote failed (HTTP ${res.status})`);
|
|
45
|
+
}
|
|
46
|
+
return (await res.json());
|
|
47
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatHookStdoutLines = formatHookStdoutLines;
|
|
4
|
-
exports.
|
|
4
|
+
exports.formatSuggestLine = formatSuggestLine;
|
|
5
5
|
exports.resolveSessionStartStdout = resolveSessionStartStdout;
|
|
6
6
|
exports.augmentBodyWithUsage = augmentBodyWithUsage;
|
|
7
7
|
exports.runHook = runHook;
|
|
@@ -178,22 +178,26 @@ function sessionContextEnvelope(parts) {
|
|
|
178
178
|
});
|
|
179
179
|
}
|
|
180
180
|
/**
|
|
181
|
-
* The single
|
|
182
|
-
*
|
|
181
|
+
* The single line printed when a task is inferred from local git at session
|
|
182
|
+
* start. We deliberately do NOT bind — we point the user at the explicit
|
|
183
|
+
* `lumo session attach` so binding stays a confirmed, manual action. The basis
|
|
184
|
+
* (branch name vs recent commit) is shown so the inference is auditable. No
|
|
185
|
+
* task title is available here because nothing was fetched; the title and
|
|
186
|
+
* context surface only once the user actually attaches.
|
|
183
187
|
*/
|
|
184
|
-
function
|
|
188
|
+
function formatSuggestLine(sessionId, match) {
|
|
185
189
|
const basis = match.source === 'branch' ? '分支名' : '最近 commit';
|
|
186
|
-
|
|
187
|
-
return `[Lumo] session_id=${sessionId} | 已自动绑定 ${identifier} - ${(0, sanitize_1.sanitizeField)(result.taskTitle ?? '')}(依据${basis})。如果不对,回复"不是"我就帮你解绑。`;
|
|
190
|
+
return `[Lumo] session_id=${sessionId} | 检测到 ${match.identifier}(依据${basis})。运行 lumo session attach ${match.identifier} 绑定。`;
|
|
188
191
|
}
|
|
189
192
|
/**
|
|
190
|
-
* Build the stdout lines for a session-start response
|
|
191
|
-
*
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
193
|
+
* Build the stdout lines for a session-start response. When the server reports
|
|
194
|
+
* the session is already bound, reuse the bound formatting (status line +
|
|
195
|
+
* injected context). When it's unbound, infer the task from local git and — on
|
|
196
|
+
* a hit — print a one-line *suggestion* to run `lumo session attach` (LUM-302:
|
|
197
|
+
* detect-and-suggest, never auto-bind). No match falls back to the generic
|
|
198
|
+
* unbound prompt.
|
|
195
199
|
*/
|
|
196
|
-
|
|
200
|
+
function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
197
201
|
if (responseBody == null || typeof responseBody !== 'object')
|
|
198
202
|
return [];
|
|
199
203
|
const body = responseBody;
|
|
@@ -209,58 +213,7 @@ async function resolveSessionStartStdout(responseBody, deps, now = new Date()) {
|
|
|
209
213
|
const match = deps.extractTask();
|
|
210
214
|
if (!match)
|
|
211
215
|
return [unboundPromptLine(sessionId)];
|
|
212
|
-
|
|
213
|
-
if (!result.ok)
|
|
214
|
-
return [unboundPromptLine(sessionId)];
|
|
215
|
-
const lines = [formatAutoBindLine(sessionId, match, result)];
|
|
216
|
-
const card = renderRecoveryCard(result.previousSession, result.taskIdentifier ?? match.identifier, now);
|
|
217
|
-
// bind-task only returns memory + previousSession (not reviewTodosSection) —
|
|
218
|
-
// the auto-bind endpoint never built PR-review todos. Keep that scope here;
|
|
219
|
-
// surfacing review todos on the auto-bind path is a separate change.
|
|
220
|
-
const envelope = sessionContextEnvelope([card, result.memorySection]);
|
|
221
|
-
if (envelope)
|
|
222
|
-
lines.push(envelope);
|
|
223
|
-
return lines;
|
|
224
|
-
}
|
|
225
|
-
/**
|
|
226
|
-
* POST the inferred task binding to the same `bind-task` endpoint that
|
|
227
|
-
* `lumo session attach` uses. Guarded by the same short timeout as the hook
|
|
228
|
-
* POST so the extra round trip can never make session-start hang. Any
|
|
229
|
-
* failure (404 unknown task, network, timeout, non-2xx) returns
|
|
230
|
-
* `{ ok: false }`, which routes the caller back to the unbound prompt.
|
|
231
|
-
*/
|
|
232
|
-
async function postBindTask(sessionId, identifier, token, apiUrl) {
|
|
233
|
-
const controller = new AbortController();
|
|
234
|
-
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
235
|
-
try {
|
|
236
|
-
const url = `${(0, api_1.trimTrailingSlash)(apiUrl)}/api/sessions/${encodeURIComponent(sessionId)}/bind-task`;
|
|
237
|
-
const res = await fetch(url, {
|
|
238
|
-
method: 'POST',
|
|
239
|
-
headers: {
|
|
240
|
-
'Content-Type': 'application/json',
|
|
241
|
-
Authorization: `Bearer ${token}`,
|
|
242
|
-
},
|
|
243
|
-
body: JSON.stringify({ taskIdentifier: identifier }),
|
|
244
|
-
signal: controller.signal,
|
|
245
|
-
});
|
|
246
|
-
if (!res.ok)
|
|
247
|
-
return { ok: false };
|
|
248
|
-
const body = (await res.json());
|
|
249
|
-
return {
|
|
250
|
-
ok: true,
|
|
251
|
-
taskIdentifier: body.taskIdentifier,
|
|
252
|
-
taskTitle: body.taskTitle,
|
|
253
|
-
memorySection: body.memorySection,
|
|
254
|
-
previousSession: body.previousSession ?? undefined,
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
catch (err) {
|
|
258
|
-
(0, hook_log_1.logHookError)('[session-start] auto-bind', err);
|
|
259
|
-
return { ok: false };
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
clearTimeout(timer);
|
|
263
|
-
}
|
|
216
|
+
return [formatSuggestLine(sessionId, match)];
|
|
264
217
|
}
|
|
265
218
|
/**
|
|
266
219
|
* For stop/stop-failure hooks, read the transcript named in the payload and
|
|
@@ -285,7 +238,10 @@ function augmentBodyWithUsage(path, body) {
|
|
|
285
238
|
const usage = (0, transcript_usage_1.sumTranscriptUsage)(transcriptPath);
|
|
286
239
|
if (!usage)
|
|
287
240
|
return body;
|
|
288
|
-
return JSON.stringify({
|
|
241
|
+
return JSON.stringify({
|
|
242
|
+
...parsed,
|
|
243
|
+
_lumo_usage: { ...usage.total, byModel: usage.byModel },
|
|
244
|
+
});
|
|
289
245
|
}
|
|
290
246
|
/**
|
|
291
247
|
* POST the hook body to /api/hooks/<path> with a short timeout. All errors
|
|
@@ -348,16 +304,15 @@ async function runHookWithBody(path, body, agentToken) {
|
|
|
348
304
|
}
|
|
349
305
|
else if (path === 'session-start' || path === 'pre-tool-use') {
|
|
350
306
|
// Paths that turn the response body into stdout for Claude Code:
|
|
351
|
-
// session-start → bind status + injected context (
|
|
352
|
-
//
|
|
307
|
+
// session-start → bind status + injected context, or (when unbound)
|
|
308
|
+
// a LUM-302 suggest line inferred from local git
|
|
353
309
|
// pre-tool-use → parallel-edit collision warning (LUM-150 ③)
|
|
354
310
|
// Only after a 2xx so a transient server failure emits nothing.
|
|
355
311
|
try {
|
|
356
312
|
const responseBody = await res.json();
|
|
357
313
|
const lines = path === 'session-start'
|
|
358
|
-
?
|
|
314
|
+
? resolveSessionStartStdout(responseBody, {
|
|
359
315
|
extractTask: () => (0, git_task_1.extractTaskFromGit)(),
|
|
360
|
-
bindTask: (sessionId, identifier) => postBindTask(sessionId, identifier, creds.token, apiUrl),
|
|
361
316
|
})
|
|
362
317
|
: formatHookStdoutLines(path, responseBody);
|
|
363
318
|
for (const line of lines) {
|
|
@@ -7,8 +7,10 @@ function asNumber(v) {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Read a Claude Code transcript JSONL and sum `message.usage` across all
|
|
10
|
-
* assistant entries -> cumulative session
|
|
11
|
-
*
|
|
10
|
+
* assistant entries, grouped by `message.model` -> cumulative session totals.
|
|
11
|
+
* `<synthetic>` entries (local injections with no API cost) are skipped.
|
|
12
|
+
* Entries with no model string bucket under "unknown". Best-effort: returns
|
|
13
|
+
* null if the file can't be read or has no real assistant usage. Never throws.
|
|
12
14
|
*/
|
|
13
15
|
function sumTranscriptUsage(transcriptPath) {
|
|
14
16
|
let text;
|
|
@@ -18,6 +20,7 @@ function sumTranscriptUsage(transcriptPath) {
|
|
|
18
20
|
catch {
|
|
19
21
|
return null;
|
|
20
22
|
}
|
|
23
|
+
const byModel = {};
|
|
21
24
|
const total = {
|
|
22
25
|
inputTokens: 0,
|
|
23
26
|
outputTokens: 0,
|
|
@@ -44,11 +47,31 @@ function sumTranscriptUsage(transcriptPath) {
|
|
|
44
47
|
const usage = o.message?.usage;
|
|
45
48
|
if (!usage)
|
|
46
49
|
continue;
|
|
50
|
+
const model = typeof o.message?.model === 'string' && o.message.model.length > 0
|
|
51
|
+
? o.message.model
|
|
52
|
+
: 'unknown';
|
|
53
|
+
if (model === '<synthetic>')
|
|
54
|
+
continue;
|
|
47
55
|
seen = true;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
const input = asNumber(usage.input_tokens);
|
|
57
|
+
const output = asNumber(usage.output_tokens);
|
|
58
|
+
const cacheRead = asNumber(usage.cache_read_input_tokens);
|
|
59
|
+
const cacheCreation = asNumber(usage.cache_creation_input_tokens);
|
|
60
|
+
total.inputTokens += input;
|
|
61
|
+
total.outputTokens += output;
|
|
62
|
+
total.cacheReadTokens += cacheRead;
|
|
63
|
+
total.cacheCreationTokens += cacheCreation;
|
|
64
|
+
const m = byModel[model] ?? {
|
|
65
|
+
input: 0,
|
|
66
|
+
output: 0,
|
|
67
|
+
cacheRead: 0,
|
|
68
|
+
cacheCreation: 0,
|
|
69
|
+
};
|
|
70
|
+
m.input += input;
|
|
71
|
+
m.output += output;
|
|
72
|
+
m.cacheRead += cacheRead;
|
|
73
|
+
m.cacheCreation += cacheCreation;
|
|
74
|
+
byModel[model] = m;
|
|
52
75
|
}
|
|
53
|
-
return seen ? total : null;
|
|
76
|
+
return seen ? { total, byModel } : null;
|
|
54
77
|
}
|