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