@lumoai/cli 1.30.0 → 1.32.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 +1 -0
- package/assets/skill/references/artifacts-figma.md +5 -0
- package/assets/skill/references/criteria.md +91 -0
- package/assets/skill/references/docs.md +45 -13
- package/assets/skill/references/memory.md +18 -1
- package/assets/skill/references/onboarding.md +2 -2
- package/assets/skill/references/task-context.md +5 -0
- package/assets/skill/references/tasks.md +7 -0
- package/assets/skill/references/verify.md +48 -0
- package/assets/skill/references/worktree.md +18 -3
- package/dist/cli/src/commands/verdict.js +194 -0
- package/dist/cli/src/commands/worktree-add.js +17 -0
- package/dist/cli/src/index.js +11 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -80,6 +80,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
80
80
|
|
|
81
81
|
- `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally, report one structured PASS/FAIL verdict per criterion to the server, print next actions. Defaults to the session-bound task. Round cap 3: an all-pass round moves the task to IN_REVIEW (agent stops there); a round-3 fail escalates to a human (stop retrying). **Run this before claiming a task is done.**
|
|
82
82
|
- `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM, milliseconds): the contract with each criterion's latest verdict (REVIEW_ADDED provenance visible), verification history, current round, last round's failure reasons, and `nextActions` = the unmet criteria (the declarative "what's next" — no separate plan). Defaults to the session-bound task; `--json` emits a versioned payload (`version` field). **Run it first when resuming a task in a new session or after a verification round was rejected.**
|
|
83
|
+
- `lumo verdict [task] --pass | --pass-with-followup | --fail` — acceptance verdicts (LUM-422). `--pass` / `--pass-with-followup` open the browser to the human verdict bar focused on the passing action (a deep link — **records nothing**; a passing data row is only ever a human's own click). `--fail --reason <enum> [--note <text>] [--criterion <id>…]` records an **AGENT send-back** (verifierType=AGENT, verdict hard-coded FAIL) and bounces the task to IN_PROGRESS. Defaults to the session-bound task. **An unresolved send-back (machine/AGENT/human FAIL) blocks the agent/CLI DONE transition with 409** — clear it (re-verify) before `task update --status done`.
|
|
83
84
|
|
|
84
85
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
85
86
|
|
|
@@ -96,6 +96,11 @@ cfl_xxx3 Onboarding (file-level) 2026-05-20 ⚠ thumbnail stale
|
|
|
96
96
|
|
|
97
97
|
Accepts a `cfl_*` cuid or the original URL. Idempotent (`Not linked: ...` + exit 0 when no match).
|
|
98
98
|
|
|
99
|
+
```bash
|
|
100
|
+
lumo task figma rm LUM-42 cfl_xxx1
|
|
101
|
+
lumo task figma rm LUM-42 "https://www.figma.com/design/abc123/Onboarding?node-id=1-234"
|
|
102
|
+
```
|
|
103
|
+
|
|
99
104
|
#### `lumo task figma refresh <task>` — manual refresh
|
|
100
105
|
|
|
101
106
|
Re-fetches metadata + thumbnail for every Figma link on the task. Per-link
|
|
@@ -64,9 +64,84 @@ must track the size of the work — it is not a fixed ritual:
|
|
|
64
64
|
HUMAN** — the server rejects checkpointer-less MACHINE criteria rather
|
|
65
65
|
than silently rewriting them. Don't invent a fake checkpointer to keep it
|
|
66
66
|
MACHINE.
|
|
67
|
+
- **Jest-by-name checkpointers must go through the zero-match guard.** A
|
|
68
|
+
bare `npx jest -t '<name>'` exits 0 even when the pattern matches **no**
|
|
69
|
+
test — rename or delete the test and the checkpointer silently fake-PASSes
|
|
70
|
+
(same trap class as the BSD-grep `-P` incident, LUM-401). Write
|
|
71
|
+
`npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` instead:
|
|
72
|
+
it fails unless ≥1 matching test ran and passed. The optional path scopes
|
|
73
|
+
the run to one file (much faster than name-filtering the whole suite).
|
|
67
74
|
- `evidenceRequired: true` marks criteria whose verdict must point at proof
|
|
68
75
|
(a MACHINE PASS always requires evidence regardless of this flag).
|
|
69
76
|
|
|
77
|
+
### Invariant (negative-assertion) criteria
|
|
78
|
+
|
|
79
|
+
Most criteria assert that **something that should happen, happened** ("the
|
|
80
|
+
endpoint returns 409", "the section renders"). An _invariant criterion_ asserts
|
|
81
|
+
the dual: that **something that must not happen, didn't** — the change left an
|
|
82
|
+
explicit don't-touch surface intact. This is the acceptance analogue of
|
|
83
|
+
AppWorld's _collateral-damage_ checks (arXiv 2407.18901) and ToolSandbox's
|
|
84
|
+
_minefield_ states (arXiv 2408.04682): a finished task should be judged not only
|
|
85
|
+
on the intended effect but on the forbidden effects it avoided. `lumo verify`
|
|
86
|
+
already judges final state per round, so an invariant fits the existing
|
|
87
|
+
machinery with no new mechanism — it's a drafting habit, not a feature.
|
|
88
|
+
|
|
89
|
+
**When to write one.** Add a single invariant criterion when the task has a
|
|
90
|
+
concrete, nameable surface the change must not disturb — a data-destruction red
|
|
91
|
+
line, a structure guard, an always-green baseline set, a diff that must stay
|
|
92
|
+
in-bounds, a standing budget. It is decision support, not a ritual: reach for it
|
|
93
|
+
only when you can point at the surface. If the only invariant you can name is
|
|
94
|
+
"don't break the build", that's already the repo's PR baseline (tests / `tsc` /
|
|
95
|
+
lint / i18n parity) — don't spend a slot restating it.
|
|
96
|
+
|
|
97
|
+
**How to phrase it.** State the invariant as _still holding after the change_,
|
|
98
|
+
not as a step you took. The **c-CRAB boundary** is the hard rule: the check
|
|
99
|
+
encodes **"the problem does not (re)occur"**, never **"a specific fix exists"**.
|
|
100
|
+
Write "`prisma/migrations/` has no deleted files vs origin/main" (the bad state
|
|
101
|
+
is absent) — not "the migration-delete guard function is present" (a named fix).
|
|
102
|
+
The first survives a refactor of _how_ the invariant is enforced; the second
|
|
103
|
+
re-fails the moment someone renames the guard, and passes even if the protection
|
|
104
|
+
was gutted some other way.
|
|
105
|
+
|
|
106
|
+
**Pairing with a checkpointer.** An invariant almost always has a runnable check
|
|
107
|
+
— that's its advantage — so prefer MACHINE. The existing `checkpointer` syntax
|
|
108
|
+
carries it as-is: a `git diff` path/filter probe, a full-suite `jest` run, a
|
|
109
|
+
structure-verify script, a parity check. Two real-repo examples:
|
|
110
|
+
|
|
111
|
+
- **`prisma/migrations/` files are never deleted** (the CLAUDE.md red line). The
|
|
112
|
+
bad state is a deleted migration file in the diff:
|
|
113
|
+
|
|
114
|
+
```json
|
|
115
|
+
{
|
|
116
|
+
"statement": "No file under prisma/migrations/ is deleted by this change (vs origin/main)",
|
|
117
|
+
"verifierType": "MACHINE",
|
|
118
|
+
"checkpointer": "bash -c \"test -z \\\"$(git diff --diff-filter=D --name-only origin/main -- prisma/migrations/)\\\"\""
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
- **A live-doc's table structure is not flattened** (the LUM-349 incident: an
|
|
123
|
+
HTML→md round-trip silently collapsed tables to plain text). The verify script
|
|
124
|
+
hard-fails if any table / row / heading count shrank:
|
|
125
|
+
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"statement": "Live-doc keeps its table structure after the edit (no rows/headings dropped)",
|
|
129
|
+
"verifierType": "MACHINE",
|
|
130
|
+
"checkpointer": "npx tsx scripts/verify-live-doc.ts <docId> docs/live-docs/<file>.md"
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
A third lives in a current contract you can copy: **LUM-415**'s "SKILL.md
|
|
135
|
+
frontmatter description stays ≤ 1000 chars" — the standing context-budget
|
|
136
|
+
invariant (the description is resident in every session), checkpointed by the
|
|
137
|
+
`description cap` case in
|
|
138
|
+
`scripts/analysis/lum392-cli-friction/__tests__/doc-examples.test.ts`.
|
|
139
|
+
|
|
140
|
+
One invariant criterion is usually enough — it's the guardrail, not the whole
|
|
141
|
+
contract. Pair it with the positive criteria that say what the change should
|
|
142
|
+
achieve; together they assert _did the right thing_ **and** _touched nothing
|
|
143
|
+
it shouldn't_.
|
|
144
|
+
|
|
70
145
|
## JSON file format
|
|
71
146
|
|
|
72
147
|
`criteria.json` is a JSON array; one object per criterion:
|
|
@@ -104,6 +179,13 @@ Rejected with 409 once **any** criteria exist on the task — the agent lock.
|
|
|
104
179
|
Echoes the stored criteria (with ids) plus the 3–7 soft-cap warning if
|
|
105
180
|
outside range.
|
|
106
181
|
|
|
182
|
+
```bash
|
|
183
|
+
lumo task criteria set LUM-42 --file criteria-lum42.json
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
(`--file` must point inside the current project directory — write the JSON to
|
|
187
|
+
the repo root or a subdirectory, not `/tmp`, and delete it after submission.)
|
|
188
|
+
|
|
107
189
|
### `lumo task criteria set <task> --file <criteria.json> --human`
|
|
108
190
|
|
|
109
191
|
Record a **human contract revision** (HUMAN_EDIT), e.g. when the user changes
|
|
@@ -125,6 +207,11 @@ verdict is never** — human verdicts only enter through human-initiated paths
|
|
|
125
207
|
Only use `--human` for decisions a human actually made (in conversation, in a
|
|
126
208
|
comment, in review). Never use it to work around your own lock.
|
|
127
209
|
|
|
210
|
+
```bash
|
|
211
|
+
lumo task criteria set LUM-42 --file criteria-revision.json --human
|
|
212
|
+
lumo task criteria set LUM-42 --file criteria-revision.json --human --cause SCOPE_CHANGE
|
|
213
|
+
```
|
|
214
|
+
|
|
128
215
|
Optionally annotate **why** the contract drifted with
|
|
129
216
|
`--cause <NEW_INFO|SCOPE_CHANGE|DRAFT_BLIND_SPOT|GRANULARITY|OTHER>` — pick
|
|
130
217
|
the tag from the human's stated reason (new information, scope moved, the
|
|
@@ -139,6 +226,10 @@ Print the contract: `<id> [MACHINE|HUMAN] SOURCE@rN [evidence] statement`
|
|
|
139
226
|
plus an indented `↳ check:` line for checkpointers. Use it to fetch ids
|
|
140
227
|
before a `--human` revision. Empty contract prints a drafting pointer.
|
|
141
228
|
|
|
229
|
+
```bash
|
|
230
|
+
lumo task criteria list LUM-42
|
|
231
|
+
```
|
|
232
|
+
|
|
142
233
|
## Injection behavior
|
|
143
234
|
|
|
144
235
|
- **Session start** (bound task): the contract is the highest-priority
|
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
## Document Management
|
|
4
4
|
|
|
5
|
+
### Content channels: prefer stdin / `--content`
|
|
6
|
+
|
|
7
|
+
`doc create` / `doc update` / `doc patch` / `doc append` all take the body from one of three mutually-exclusive channels. **For agent write-back, prefer stdin or `--content`** — pipe the markdown you already hold in memory, or pass it inline:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
printf '%s' "$body" | lumo doc update cmd_xxx # stdin — best for multi-line / generated content
|
|
11
|
+
lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ 论文" # --content — short inline edits
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`--file` is the **fallback for content that already lives in an authoritative file on disk** (e.g. a repo-tracked `docs/live-docs/<file>.md` source you are editing). It is **not** the default channel: `--file` is sandboxed — the CLI rejects any path outside the current project directory or matching the sensitive-file denylist (`.env*`, keys, `credentials`, …), with no override. So feeding it generated content forces you to first write a scratch file _inside the repo_ just to pass it — an extra step that is the real-world friction that gets `--file` rejected. Pipe via stdin instead and skip the temp file; reach for `--file` only when the file _is_ the source of truth you're editing.
|
|
15
|
+
|
|
16
|
+
### Markdown tables: keep them compact (no alignment padding)
|
|
17
|
+
|
|
18
|
+
When you author or edit a markdown doc that contains tables (live-docs, registers, reports), write the cells **compact — one space around each pipe, no column-alignment padding**:
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
| col | meaning |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| a | first |
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
not the aligned form (`| col | meaning |` padded so columns line up). Two reasons, both about local editability:
|
|
27
|
+
|
|
28
|
+
- **prettier re-pads tables.** A compact table gets re-aligned and a padded one churns the diff on every prettier run — so repo-tracked live-doc sources under `docs/live-docs/` are in `.prettierignore` to stay byte-stable (see the live-docs README).
|
|
29
|
+
- **the Edit tool's exact-match fails on padding.** Incremental edits match on exact cell text; alignment whitespace makes that match fragile (measured to fail). Compact cells edit reliably.
|
|
30
|
+
|
|
31
|
+
This is a pure authoring convention — the server stores your markdown **byte-for-byte** (`sourceMarkdown`), so `doc show --raw` and `doc diff` stay byte-exact (LUM-408); there is **no** server-side table normalization to lean on. The structure guard (LUM-410) compares _rendered_ structure and is whitespace-insensitive, so compactness buys local editability, not a verify pass.
|
|
32
|
+
|
|
5
33
|
### `lumo doc create [title] [flags]` — create a new document
|
|
6
34
|
|
|
7
35
|
Use this when the user wants to write a new document from the terminal. Title is positional and optional. When omitted, the doc title defaults to empty (server renders as "Untitled" via i18n).
|
|
@@ -132,12 +160,12 @@ Use default mode when the user wants to read a doc; use `--raw` whenever the ful
|
|
|
132
160
|
|
|
133
161
|
Replaces the **whole addressed section** (heading line included, subsections included) with the provided content, server-side, leaving every byte outside the section untouched. The new content comes from `--content`, `--file`, or piped stdin (one required; `--file` is sandboxed like `doc update`).
|
|
134
162
|
|
|
135
|
-
| Flag | Type
|
|
136
|
-
| --------------------- |
|
|
137
|
-
| `--section <heading>` | string
|
|
138
|
-
| `--content <text>` | string
|
|
139
|
-
| `--file <path>` | string
|
|
140
|
-
| `--if-revision <n>` | int
|
|
163
|
+
| Flag | Type | Notes |
|
|
164
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
165
|
+
| `--section <heading>` | string | Required. Heading text addressing the section; prefix `#…` pins the depth. |
|
|
166
|
+
| `--content <text>` | string | New section content (include the heading line — the whole section is replaced verbatim). |
|
|
167
|
+
| `--file <path>` | string | New section content from file (project-local sandbox). |
|
|
168
|
+
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`. Recommended whenever the edit base was read earlier. |
|
|
141
169
|
| `--allow-shrink` | boolean | Let the patch through even when it drops tables/rows/headings within the addressed section (422 otherwise). |
|
|
142
170
|
|
|
143
171
|
Concurrency: the splice always commits **conditionally** on the revision the server read the source at — even without `--if-revision`, a concurrent body edit between read and write returns 409 instead of clobbering. On 409 the CLI prints the server reason plus a re-read-and-retry hint and exits 1.
|
|
@@ -164,11 +192,11 @@ lumo doc patch cmd_xxx --section "D 状态表" --file sec.md --if-revision 6
|
|
|
164
192
|
|
|
165
193
|
Inserts the new content at the **end of the addressed section** (just before the next same-or-higher-level heading), or at the end of the document when `--section` is omitted. Pure insertion: no pre-existing byte is modified, which makes it the natural write for running logs, ledgers and queues. Content channels and sandbox are the same as `doc patch`; separator blank lines are added automatically.
|
|
166
194
|
|
|
167
|
-
| Flag | Type | Notes
|
|
168
|
-
| --------------------- | ------ |
|
|
169
|
-
| `--section <heading>` | string | Optional. Omit to append at the document end.
|
|
170
|
-
| `--content <text>` | string | Content to append.
|
|
171
|
-
| `--file <path>` | string | Content from file (project-local sandbox).
|
|
195
|
+
| Flag | Type | Notes |
|
|
196
|
+
| --------------------- | ------ | ---------------------------------------------------------------------------- |
|
|
197
|
+
| `--section <heading>` | string | Optional. Omit to append at the document end. |
|
|
198
|
+
| `--content <text>` | string | Content to append. |
|
|
199
|
+
| `--file <path>` | string | Content from file (project-local sandbox). |
|
|
172
200
|
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
|
|
173
201
|
|
|
174
202
|
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). Requires a stored markdown source.
|
|
@@ -188,8 +216,8 @@ echo "## 2026-06-10\n吸收了 3 篇" | lumo doc append cmd_xxx # document end
|
|
|
188
216
|
|
|
189
217
|
Compares the server-side stored markdown source (the byte-identical last markdown upload) against a local file, making remote/local split-brain visible on demand.
|
|
190
218
|
|
|
191
|
-
| Flag | Type | Notes
|
|
192
|
-
| --------------- | ------ |
|
|
219
|
+
| Flag | Type | Notes |
|
|
220
|
+
| --------------- | ------ | --------------------------------------------------------------------------------- |
|
|
193
221
|
| `--file <path>` | string | Required. Local markdown file; same project-local sandbox as `doc update --file`. |
|
|
194
222
|
|
|
195
223
|
Exit codes: **0** = byte-identical (prints `Clean: …`), **1** = divergent (prints a unified diff, `--- remote/<id> (sourceMarkdown)` vs `+++ local/<file>`) or error. A doc without a stored markdown source errors explicitly (upload a markdown base once, then diff works).
|
|
@@ -278,6 +306,10 @@ lumo doc delete cmd_xxx --yes
|
|
|
278
306
|
|
|
279
307
|
Adds an explicit DocumentMention row. Survives content edits in the Web UI. If a CONTENT-derived mention already exists for the same pair, it's upgraded to EXPLICIT so a later `unbind` can remove it.
|
|
280
308
|
|
|
309
|
+
```bash
|
|
310
|
+
lumo doc bind cmd_xxx LUM-42
|
|
311
|
+
```
|
|
312
|
+
|
|
281
313
|
Output:
|
|
282
314
|
|
|
283
315
|
```
|
|
@@ -19,7 +19,7 @@ lumo task memory add [LUM-N] --category trap --trigger "..." --outcome "..." [--
|
|
|
19
19
|
lumo task memory add [LUM-N] --category decision --what "..." --why "..." [--alternatives "..."] [--implications "..."] [--agent <agent>]
|
|
20
20
|
lumo task memory add [LUM-N] --category convention --rule "..." --applies "..." [--agent <agent>]
|
|
21
21
|
lumo task memory add [LUM-N] --category procedural --workflow "..." --trigger "..." [--step "..." --step "..."] [--agent <agent>]
|
|
22
|
-
lumo project memory add [<project>] --category ... [--agent <agent>] # same flags; records at PROJECT scope
|
|
22
|
+
lumo project memory add [<project>] --category convention --rule "..." --applies "..." [--agent <agent>] # same per-category flags as task memory add; records at PROJECT scope
|
|
23
23
|
|
|
24
24
|
# --agent values: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)
|
|
25
25
|
# Aliases: gemini → gemini-cli, copilot → github-copilot (case-insensitive)
|
|
@@ -34,6 +34,23 @@ When the session is bound (`lumo session attach <LUM-N>`), omit the identifier:
|
|
|
34
34
|
`lumo task memory add --category trap --trigger ... --outcome ...` records onto
|
|
35
35
|
the bound task; `lumo project memory add ...` records onto its project.
|
|
36
36
|
|
|
37
|
+
Real calls (memory fields are structured per category — there is **no**
|
|
38
|
+
`--content` flag):
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Minimal — session bound, identifier omitted
|
|
42
|
+
lumo task memory add --category trap --trigger "npx tsc --noEmit passes locally but CI fails on exactOptionalPropertyTypes" --outcome "PR goes red after merge"
|
|
43
|
+
lumo project memory add --category convention --rule "CLI tag-modify commands print a final 'Tags:' line" --applies "lumo doc/task create and update"
|
|
44
|
+
|
|
45
|
+
# Full form — explicit identifier, all optional fields
|
|
46
|
+
lumo task memory add LUM-42 --category decision --what "Store doc content as Tiptap-serialized HTML" --why "markdown-to-JSON and markdown-to-HTML paths drifted" --alternatives "store Tiptap JSON" --implications "all markdown input goes through markdownToHtml" --agent claude-code
|
|
47
|
+
lumo project memory add lumo --category procedural --workflow "Regenerate the CLI grammar snapshot" --trigger "any cli/src/commands change" --step "npx tsx scripts/analysis/lum392-cli-friction/emit-grammar.ts" --step "npx jest scripts/analysis/lum392-cli-friction" --agent claude-code
|
|
48
|
+
|
|
49
|
+
# Curate
|
|
50
|
+
lumo memory promote cmpi19iqabc123
|
|
51
|
+
lumo memory rm cmpi19iqabc123 --yes
|
|
52
|
+
```
|
|
53
|
+
|
|
37
54
|
### Reconcile-on-write & deduplication
|
|
38
55
|
|
|
39
56
|
`memory add` does **not** unconditionally insert a new row. Before writing it:
|
|
@@ -22,8 +22,8 @@ not touch git hooks.
|
|
|
22
22
|
npx @lumoai/cli setup # interactive: prompts user/project; agent=claude-code
|
|
23
23
|
npx @lumoai/cli setup --user # write to ~/.claude/
|
|
24
24
|
npx @lumoai/cli setup --project # write to ./.claude/
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
lumo setup --project --force # same via global install: overwrite skill files if they differ from bundled
|
|
26
|
+
lumo setup --project --agent codex # bake agent=codex into the hook commands
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
Use when:
|
|
@@ -115,6 +115,11 @@ lumo task pr show LUM-42 128
|
|
|
115
115
|
Read-only audit view over the task's `LineageEdge` rows. Given a task
|
|
116
116
|
identifier (`LUM-N`), prints the causal trail:
|
|
117
117
|
|
|
118
|
+
```bash
|
|
119
|
+
lumo task lineage LUM-42 # per-session causal trail + cost
|
|
120
|
+
lumo task lineage LUM-42 --signal # append workspace-level usage signal-health
|
|
121
|
+
```
|
|
122
|
+
|
|
118
123
|
- **Totals banner** — distinct sessions, fragment count, edge count,
|
|
119
124
|
total tokens (input/output/cache split) and loops, and the outcome
|
|
120
125
|
distribution.
|
|
@@ -154,6 +154,13 @@ LUM-48 TODO MEDIUM backend Investigate slow query
|
|
|
154
154
|
| `-m, --milestone <ref>` | string | Filter to my tasks under this milestone (name or UUID). |
|
|
155
155
|
| `-n, --limit <count>` | integer | Cap output to the first N rows. |
|
|
156
156
|
|
|
157
|
+
```bash
|
|
158
|
+
lumo task list # everything assigned to me
|
|
159
|
+
lumo task list --status in_progress -n 10 # capped, status-filtered
|
|
160
|
+
lumo task list --project backend --status todo
|
|
161
|
+
lumo task list --milestone "Q3 Launch"
|
|
162
|
+
```
|
|
163
|
+
|
|
157
164
|
When `--milestone` is set, the CLI calls `GET /api/milestones/<id>/tasks?assigned=me` instead of `/api/tasks/me`. Other filters (`--status`, `--limit`) still apply client-side to the milestone-scoped result.
|
|
158
165
|
|
|
159
166
|
Filtering is currently client-side — the server returns the full "my tasks" set and the CLI slices it. This is fine for typical workspaces (dozens of tasks); revisit if a workspace has hundreds.
|
|
@@ -146,3 +146,51 @@ major; additive fields don't. Pin on `version` when scripting against it.
|
|
|
146
146
|
`status` reads; `verify` judges. Running status never starts a round, never
|
|
147
147
|
escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
|
|
148
148
|
all-pass, human-only DONE) live entirely in `lumo verify` and the server.
|
|
149
|
+
|
|
150
|
+
## lumo verdict — the three verdict channels (LUM-422)
|
|
151
|
+
|
|
152
|
+
`lumo verify` is the MACHINE channel. `lumo verdict` covers the other two — the
|
|
153
|
+
HUMAN pass and the AGENT send-back — under one red line: **no passing data row
|
|
154
|
+
is ever agent-produced.**
|
|
155
|
+
|
|
156
|
+
```
|
|
157
|
+
lumo verdict --pass
|
|
158
|
+
lumo verdict LUM-42 --pass-with-followup
|
|
159
|
+
lumo verdict --fail --reason CRITERION_UNMET --note "the retry path is still missing"
|
|
160
|
+
lumo verdict LUM-42 --fail --reason scope_mismatch --criterion c-abc123
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### --pass / --pass-with-followup — a deep link, never a write
|
|
164
|
+
|
|
165
|
+
These resolve the task, then open the browser to its verdict bar focused on the
|
|
166
|
+
passing action. **The CLI writes nothing** — PASS / PASS_WITH_FOLLOWUP only ever
|
|
167
|
+
land from a human's own click (Clerk session). Use this to hand a finished task
|
|
168
|
+
to a human for the final pass; it carries them one click from recording it.
|
|
169
|
+
|
|
170
|
+
### --fail — the AGENT send-back
|
|
171
|
+
|
|
172
|
+
`--fail --reason <enum>` records a real verdict row server-side with
|
|
173
|
+
verifierType=AGENT (a channel distinct from MACHINE and HUMAN, so "machine
|
|
174
|
+
all-pass but human FAIL" stays an uncontaminated signal). The verdict is
|
|
175
|
+
hard-coded FAIL — there is no agent path to a passing verdict. It:
|
|
176
|
+
|
|
177
|
+
- requires `--reason` (case-insensitive): `CRITERION_UNMET | EVIDENCE_INSUFFICIENT
|
|
178
|
+
| CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER` — the agent pays the
|
|
179
|
+
structured tax a human send-back is spared;
|
|
180
|
+
- takes an optional `--note`, posted as a task comment (@mentions and images for
|
|
181
|
+
free) and summarized onto the verdict row;
|
|
182
|
+
- takes repeatable `--criterion <id>` to narrow the send-back; omitted, it fans
|
|
183
|
+
out to the whole contract;
|
|
184
|
+
- records at round = the current max (not a new round) and bounces the task back
|
|
185
|
+
to IN_PROGRESS, with the unmet criteria surfacing through `lumo task status`.
|
|
186
|
+
|
|
187
|
+
### The DONE gate
|
|
188
|
+
|
|
189
|
+
Once any criterion's latest verdict is FAIL — machine, AGENT, or human — moving
|
|
190
|
+
the task to DONE on the agent/CLI path is refused with **409** and the
|
|
191
|
+
unresolved items listed. Clear the send-back (fix + re-verify, or a human PASS)
|
|
192
|
+
before `lumo task update <id> --status done`. A task with no criteria, or whose
|
|
193
|
+
criteria were never adjudicated, transitions freely — the gate only blocks an
|
|
194
|
+
actual send-back, never an un-adjudicated criterion. When the machine loop has
|
|
195
|
+
left a task IN_REVIEW with no send-back standing, the agent may move it to DONE
|
|
196
|
+
directly; a human-PASS row is a provable manual override, not a required ticket.
|
|
@@ -10,9 +10,11 @@ there); `list` is read-only and runnable from anywhere.
|
|
|
10
10
|
|
|
11
11
|
Creates `.worktrees/<LUM-N>[-slug]` on branch `lumo/<LUM-N>[-slug]`, branched off
|
|
12
12
|
`origin/main` (after a fetch), and symlinks its `node_modules` to the main
|
|
13
|
-
checkout so jest/tsc work immediately.
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
checkout so jest/tsc work immediately. In the same scaffold step it copies the
|
|
14
|
+
main checkout's `.husky/_` hooks shim into the worktree so git hooks actually
|
|
15
|
+
fire there (see below). With no slug, the dir and branch use the bare `LUM-N`
|
|
16
|
+
form. Prints next-step guidance plus the two gotchas it can't fix for you
|
|
17
|
+
(shared prisma client, jest cwd).
|
|
16
18
|
|
|
17
19
|
```bash
|
|
18
20
|
lumo worktree add LUM-267 worktree-scaffold # → .worktrees/LUM-267-worktree-scaffold
|
|
@@ -36,6 +38,19 @@ do `generate + tsc` atomically once at the end.
|
|
|
36
38
|
in first) — `cli/` has no jest config, and running from the main checkout hits
|
|
37
39
|
the `cli/package.json` haste collision and silently runs the wrong tests.
|
|
38
40
|
|
|
41
|
+
**The husky hooks it copies in:** husky owns git hooks via
|
|
42
|
+
`core.hooksPath = .husky/_`, a relative path git resolves against each
|
|
43
|
+
worktree's own root. That `_` shim dir is **untracked** (husky regenerates it on
|
|
44
|
+
the main checkout's `npm install`/`prepare`), so a fresh worktree would lack it
|
|
45
|
+
and git would **silently skip every hook** — pre-commit (lint-staged) and
|
|
46
|
+
commit-msg (LUM-405 drift-check) included. Since this repo has no GitHub CI,
|
|
47
|
+
husky is the only deterministic quality gate, so `add` copies `.husky/_` into the
|
|
48
|
+
new worktree (copy, not symlink — the shim is tiny and the hooks it invokes
|
|
49
|
+
resolve relative to the worktree). If the main checkout itself has no `.husky/_`
|
|
50
|
+
(husky never installed there), `add` prints a warning telling you to run
|
|
51
|
+
`npm install` in the main checkout to regenerate it, rather than skipping
|
|
52
|
+
silently.
|
|
53
|
+
|
|
39
54
|
**Never run `npm install` / `npm ci` inside a worktree.** The worktree shares
|
|
40
55
|
the main checkout's `node_modules` via a symlink; npm does not respect the link
|
|
41
56
|
— it deletes it and reifies a full standalone `node_modules` (thousands of
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectCriterion = collectCriterion;
|
|
4
|
+
exports.verdict = verdict;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
const browser_1 = require("../lib/browser");
|
|
9
|
+
/**
|
|
10
|
+
* Rejection-reason vocabulary (mirrors the server's VerificationRejectionReason
|
|
11
|
+
* enum). Required on the agent --fail path — the agent pays the structured tax
|
|
12
|
+
* a human is spared (LUM-422 ①).
|
|
13
|
+
*/
|
|
14
|
+
const FAIL_REASONS = [
|
|
15
|
+
'CRITERION_UNMET',
|
|
16
|
+
'EVIDENCE_INSUFFICIENT',
|
|
17
|
+
'CHECK_EXECUTION_ERROR',
|
|
18
|
+
'SCOPE_MISMATCH',
|
|
19
|
+
'OTHER',
|
|
20
|
+
];
|
|
21
|
+
/** Collect repeatable `--criterion <id>` flags into an array (commander idiom). */
|
|
22
|
+
function collectCriterion(value, prev = []) {
|
|
23
|
+
return [...prev, value];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* `lumo verdict [task]` — human + agent acceptance verdicts (LUM-422).
|
|
27
|
+
*
|
|
28
|
+
* Three modes, exactly one required:
|
|
29
|
+
* --pass / --pass-with-followup open the browser to the task's verdict bar,
|
|
30
|
+
* focused on the passing action (a deep link — writes NOTHING; a passing
|
|
31
|
+
* data row is only ever produced by a human's own click, red line).
|
|
32
|
+
* --fail --reason <enum> [--note] [--criterion …] records an AGENT send-back
|
|
33
|
+
* (verdict hard-coded FAIL server-side) and bounces the task to
|
|
34
|
+
* IN_PROGRESS. Bearer-authed.
|
|
35
|
+
*
|
|
36
|
+
* Defaults to the session-bound task; an explicit identifier overrides.
|
|
37
|
+
*/
|
|
38
|
+
async function verdict(identifier, options = {}) {
|
|
39
|
+
const modes = [
|
|
40
|
+
options.pass && 'pass',
|
|
41
|
+
options.passWithFollowup && 'pass-with-followup',
|
|
42
|
+
options.fail && 'fail',
|
|
43
|
+
].filter(Boolean);
|
|
44
|
+
if (modes.length === 0) {
|
|
45
|
+
console.error('Error: choose a verdict mode — --pass, --pass-with-followup, or --fail.');
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
if (modes.length > 1) {
|
|
49
|
+
console.error(`Error: pick exactly one verdict mode (got ${modes.join(', ')}).`);
|
|
50
|
+
return 1;
|
|
51
|
+
}
|
|
52
|
+
const creds = (0, config_1.readCredentials)();
|
|
53
|
+
if (!creds) {
|
|
54
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
58
|
+
const headers = {
|
|
59
|
+
Authorization: `Bearer ${creds.token}`,
|
|
60
|
+
};
|
|
61
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
62
|
+
if (sessionId)
|
|
63
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
64
|
+
// ── Resolve the task: explicit identifier or the session binding ──────────
|
|
65
|
+
let taskId = identifier;
|
|
66
|
+
if (!taskId) {
|
|
67
|
+
if (!sessionId) {
|
|
68
|
+
console.error('Error: no task given and $CLAUDE_CODE_SESSION_ID is not set.\n' +
|
|
69
|
+
'Run `lumo verdict <LUM-N> …` or run inside a session bound via `lumo session attach`.');
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
let res;
|
|
73
|
+
try {
|
|
74
|
+
res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
const data = res.ok
|
|
82
|
+
? (await res.json())
|
|
83
|
+
: null;
|
|
84
|
+
if (!data?.taskIdentifier) {
|
|
85
|
+
console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first, or pass the task explicitly.');
|
|
86
|
+
return 1;
|
|
87
|
+
}
|
|
88
|
+
taskId = data.taskIdentifier;
|
|
89
|
+
}
|
|
90
|
+
if (options.fail) {
|
|
91
|
+
return failVerdict(base, headers, taskId, options, creds.workspaceSlug);
|
|
92
|
+
}
|
|
93
|
+
return passDeepLink(base, headers, taskId, options.passWithFollowup ? 'pass_with_followup' : 'pass', creds.workspaceSlug);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* --pass / --pass-with-followup: open the human's verdict bar pre-focused on the
|
|
97
|
+
* passing action. The CLI never writes the verdict — it only carries the human
|
|
98
|
+
* to the one click that does (red line: no agent-produced passing row).
|
|
99
|
+
*/
|
|
100
|
+
async function passDeepLink(base, headers, taskId, verdictParam, workspaceSlug) {
|
|
101
|
+
let res;
|
|
102
|
+
try {
|
|
103
|
+
res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(taskId)}`, { headers });
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
107
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
if (res.status === 401) {
|
|
111
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
112
|
+
return 1;
|
|
113
|
+
}
|
|
114
|
+
if (res.status === 404) {
|
|
115
|
+
console.error(`Error: task ${taskId} not found in workspace ${workspaceSlug}`);
|
|
116
|
+
return 1;
|
|
117
|
+
}
|
|
118
|
+
if (!res.ok) {
|
|
119
|
+
console.error(`Error: could not load task (HTTP ${res.status})`);
|
|
120
|
+
return 1;
|
|
121
|
+
}
|
|
122
|
+
const { task } = (await res.json());
|
|
123
|
+
if (!task?.url) {
|
|
124
|
+
console.error('Error: server did not return a task URL to open.');
|
|
125
|
+
return 1;
|
|
126
|
+
}
|
|
127
|
+
const sep = task.url.includes('?') ? '&' : '?';
|
|
128
|
+
const deepLink = `${task.url}${sep}verdict=${verdictParam}`;
|
|
129
|
+
const label = verdictParam === 'pass_with_followup' ? 'Pass with follow-up' : 'Pass';
|
|
130
|
+
process.stdout.write(`Opening ${taskId} for a human "${label}" verdict (nothing is recorded until they click):\n` +
|
|
131
|
+
` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
|
|
132
|
+
(0, browser_1.openBrowser)(deepLink);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* --fail: record an AGENT send-back. The server hard-codes verdict=FAIL and
|
|
137
|
+
* verifierType=AGENT — there is no passing verdict the CLI can express.
|
|
138
|
+
*/
|
|
139
|
+
async function failVerdict(base, headers, taskId, options, workspaceSlug) {
|
|
140
|
+
if (!options.reason) {
|
|
141
|
+
console.error(`Error: --fail requires --reason <${FAIL_REASONS.join('|')}>.`);
|
|
142
|
+
return 1;
|
|
143
|
+
}
|
|
144
|
+
// Case-insensitive like every other CLI enum flag (LUM-420): fold to the
|
|
145
|
+
// canonical upper-case enum before validating and sending.
|
|
146
|
+
const reason = options.reason.toUpperCase();
|
|
147
|
+
if (!FAIL_REASONS.includes(reason)) {
|
|
148
|
+
console.error(`Error: invalid --reason "${options.reason}". Allowed: ${FAIL_REASONS.join(', ')}.`);
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
const body = { rejectionReasonEnum: reason };
|
|
152
|
+
if (options.note)
|
|
153
|
+
body.note = options.note;
|
|
154
|
+
if (options.criterion && options.criterion.length > 0) {
|
|
155
|
+
body.criterionIds = options.criterion;
|
|
156
|
+
}
|
|
157
|
+
let res;
|
|
158
|
+
try {
|
|
159
|
+
res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/agent-verdict`, {
|
|
160
|
+
method: 'POST',
|
|
161
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
162
|
+
body: JSON.stringify(body),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
167
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
if (res.status === 401) {
|
|
171
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
if (res.status === 404) {
|
|
175
|
+
console.error(`Error: task ${taskId} not found in workspace ${workspaceSlug}`);
|
|
176
|
+
return 1;
|
|
177
|
+
}
|
|
178
|
+
if (!res.ok) {
|
|
179
|
+
const errBody = (await res.json().catch(() => null));
|
|
180
|
+
const detail = errBody && typeof errBody.error === 'string'
|
|
181
|
+
? (0, sanitize_1.sanitizeField)(errBody.error)
|
|
182
|
+
: '';
|
|
183
|
+
console.error(`Error: send-back rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
|
|
184
|
+
return 1;
|
|
185
|
+
}
|
|
186
|
+
const outcome = (await res.json());
|
|
187
|
+
process.stdout.write(`✗ Sent ${taskId} back (AGENT FAIL, reason ${reason}) — ` +
|
|
188
|
+
`${outcome.criterionIds.length} ${outcome.criterionIds.length === 1 ? 'criterion' : 'criteria'} at round ${outcome.round}; task is now ${outcome.taskStatus}.\n`);
|
|
189
|
+
if (outcome.commentId) {
|
|
190
|
+
process.stdout.write(' A send-back note was posted as a task comment.\n');
|
|
191
|
+
}
|
|
192
|
+
process.stdout.write('Address the unmet criteria (see `lumo task status`), then re-verify.\n');
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
@@ -96,6 +96,23 @@ async function worktreeAdd(rawId, slug, opts) {
|
|
|
96
96
|
else {
|
|
97
97
|
console.log('Warning: main checkout has no node_modules; run `npm install` there, then symlink manually');
|
|
98
98
|
}
|
|
99
|
+
// husky owns git hooks via `core.hooksPath = .husky/_` (a relative path git
|
|
100
|
+
// resolves against each worktree's root). That `_` shim dir is untracked
|
|
101
|
+
// (generated by husky on the main checkout's `npm install`/`prepare`), so a
|
|
102
|
+
// fresh worktree lacks it and git SILENTLY skips every hook — pre-commit
|
|
103
|
+
// (lint-staged) and commit-msg (LUM-405 drift-check) included. This repo has
|
|
104
|
+
// no CI, so husky is the only deterministic gate; copy the shim in so hooks
|
|
105
|
+
// fire. Copy (not symlink) is the stable choice: the shim is tiny, rarely
|
|
106
|
+
// changes, and the hooks it invokes resolve relative to the worktree.
|
|
107
|
+
const srcHusky = path.join(root, '.husky', '_');
|
|
108
|
+
if (fs.existsSync(srcHusky)) {
|
|
109
|
+
fs.cpSync(srcHusky, path.join(dir, '.husky', '_'), { recursive: true });
|
|
110
|
+
console.log(' git hooks: husky shim copied (pre-commit + commit-msg active)');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
console.log('Warning: main checkout has no .husky/_ shim; git hooks will NOT run in this worktree.');
|
|
114
|
+
console.log(' Run `npm install` in the main checkout to (re)generate husky, then re-run `lumo worktree add`.');
|
|
115
|
+
}
|
|
99
116
|
printGuidance(dir, branch);
|
|
100
117
|
if (opts.verify) {
|
|
101
118
|
console.log('Running baseline jest …');
|
package/dist/cli/src/index.js
CHANGED
|
@@ -49,6 +49,7 @@ const session_status_1 = require("./commands/session-status");
|
|
|
49
49
|
const session_wrap_1 = require("./commands/session-wrap");
|
|
50
50
|
const next_1 = require("./commands/next");
|
|
51
51
|
const verify_1 = require("./commands/verify");
|
|
52
|
+
const verdict_1 = require("./commands/verdict");
|
|
52
53
|
const task_context_1 = require("./commands/task-context");
|
|
53
54
|
const task_create_1 = require("./commands/task-create");
|
|
54
55
|
const task_update_1 = require("./commands/task-update");
|
|
@@ -218,6 +219,16 @@ program
|
|
|
218
219
|
.description('Machine verification loop (LUM-343): run every MACHINE criterion checkpointer locally, report structured verdicts to the server (round cap 3), and print next actions. All-pass moves the task to IN_REVIEW. Defaults to the session-bound task.')
|
|
219
220
|
.option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
|
|
220
221
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
222
|
+
program
|
|
223
|
+
.command('verdict [task]')
|
|
224
|
+
.description('Acceptance verdict (LUM-422). --pass / --pass-with-followup open the browser to the human verdict bar focused on the passing action (a deep link — records nothing; a passing row is only ever a human click). --fail --reason <enum> records an AGENT send-back and bounces the task to IN_PROGRESS. Defaults to the session-bound task.')
|
|
225
|
+
.option('--pass', 'Open the verdict bar focused on Pass (human one-click; no write)')
|
|
226
|
+
.option('--pass-with-followup', 'Open the verdict bar focused on Pass with follow-up (human one-click; no write)')
|
|
227
|
+
.option('--fail', 'Record an AGENT send-back (verdict FAIL) — requires --reason')
|
|
228
|
+
.option('--reason <enum>', 'Rejection reason for --fail: CRITERION_UNMET | EVIDENCE_INSUFFICIENT | CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER (case-insensitive)')
|
|
229
|
+
.option('--note <text>', 'Optional send-back narrative, posted as a task comment')
|
|
230
|
+
.option('--criterion <id>', 'Narrow a --fail to specific criteria (repeatable); omitted = the whole contract', verdict_1.collectCriterion)
|
|
231
|
+
.action(wrap((task, options) => (0, verdict_1.verdict)(task, options)));
|
|
221
232
|
program
|
|
222
233
|
.command('next')
|
|
223
234
|
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|