@lumoai/cli 1.30.0 → 1.31.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 +84 -0
- package/assets/skill/references/docs.md +17 -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/dist/cli/src/commands/verdict.js +194 -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
|
|
@@ -67,6 +67,74 @@ must track the size of the work — it is not a fixed ritual:
|
|
|
67
67
|
- `evidenceRequired: true` marks criteria whose verdict must point at proof
|
|
68
68
|
(a MACHINE PASS always requires evidence regardless of this flag).
|
|
69
69
|
|
|
70
|
+
### Invariant (negative-assertion) criteria
|
|
71
|
+
|
|
72
|
+
Most criteria assert that **something that should happen, happened** ("the
|
|
73
|
+
endpoint returns 409", "the section renders"). An _invariant criterion_ asserts
|
|
74
|
+
the dual: that **something that must not happen, didn't** — the change left an
|
|
75
|
+
explicit don't-touch surface intact. This is the acceptance analogue of
|
|
76
|
+
AppWorld's _collateral-damage_ checks (arXiv 2407.18901) and ToolSandbox's
|
|
77
|
+
_minefield_ states (arXiv 2408.04682): a finished task should be judged not only
|
|
78
|
+
on the intended effect but on the forbidden effects it avoided. `lumo verify`
|
|
79
|
+
already judges final state per round, so an invariant fits the existing
|
|
80
|
+
machinery with no new mechanism — it's a drafting habit, not a feature.
|
|
81
|
+
|
|
82
|
+
**When to write one.** Add a single invariant criterion when the task has a
|
|
83
|
+
concrete, nameable surface the change must not disturb — a data-destruction red
|
|
84
|
+
line, a structure guard, an always-green baseline set, a diff that must stay
|
|
85
|
+
in-bounds, a standing budget. It is decision support, not a ritual: reach for it
|
|
86
|
+
only when you can point at the surface. If the only invariant you can name is
|
|
87
|
+
"don't break the build", that's already the repo's PR baseline (tests / `tsc` /
|
|
88
|
+
lint / i18n parity) — don't spend a slot restating it.
|
|
89
|
+
|
|
90
|
+
**How to phrase it.** State the invariant as _still holding after the change_,
|
|
91
|
+
not as a step you took. The **c-CRAB boundary** is the hard rule: the check
|
|
92
|
+
encodes **"the problem does not (re)occur"**, never **"a specific fix exists"**.
|
|
93
|
+
Write "`prisma/migrations/` has no deleted files vs origin/main" (the bad state
|
|
94
|
+
is absent) — not "the migration-delete guard function is present" (a named fix).
|
|
95
|
+
The first survives a refactor of _how_ the invariant is enforced; the second
|
|
96
|
+
re-fails the moment someone renames the guard, and passes even if the protection
|
|
97
|
+
was gutted some other way.
|
|
98
|
+
|
|
99
|
+
**Pairing with a checkpointer.** An invariant almost always has a runnable check
|
|
100
|
+
— that's its advantage — so prefer MACHINE. The existing `checkpointer` syntax
|
|
101
|
+
carries it as-is: a `git diff` path/filter probe, a full-suite `jest` run, a
|
|
102
|
+
structure-verify script, a parity check. Two real-repo examples:
|
|
103
|
+
|
|
104
|
+
- **`prisma/migrations/` files are never deleted** (the CLAUDE.md red line). The
|
|
105
|
+
bad state is a deleted migration file in the diff:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{
|
|
109
|
+
"statement": "No file under prisma/migrations/ is deleted by this change (vs origin/main)",
|
|
110
|
+
"verifierType": "MACHINE",
|
|
111
|
+
"checkpointer": "bash -c \"test -z \\\"$(git diff --diff-filter=D --name-only origin/main -- prisma/migrations/)\\\"\""
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
- **A live-doc's table structure is not flattened** (the LUM-349 incident: an
|
|
116
|
+
HTML→md round-trip silently collapsed tables to plain text). The verify script
|
|
117
|
+
hard-fails if any table / row / heading count shrank:
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"statement": "Live-doc keeps its table structure after the edit (no rows/headings dropped)",
|
|
122
|
+
"verifierType": "MACHINE",
|
|
123
|
+
"checkpointer": "npx tsx scripts/verify-live-doc.ts <docId> docs/live-docs/<file>.md"
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
A third lives in a current contract you can copy: **LUM-415**'s "SKILL.md
|
|
128
|
+
frontmatter description stays ≤ 1000 chars" — the standing context-budget
|
|
129
|
+
invariant (the description is resident in every session), checkpointed by the
|
|
130
|
+
`description cap` case in
|
|
131
|
+
`scripts/analysis/lum392-cli-friction/__tests__/doc-examples.test.ts`.
|
|
132
|
+
|
|
133
|
+
One invariant criterion is usually enough — it's the guardrail, not the whole
|
|
134
|
+
contract. Pair it with the positive criteria that say what the change should
|
|
135
|
+
achieve; together they assert _did the right thing_ **and** _touched nothing
|
|
136
|
+
it shouldn't_.
|
|
137
|
+
|
|
70
138
|
## JSON file format
|
|
71
139
|
|
|
72
140
|
`criteria.json` is a JSON array; one object per criterion:
|
|
@@ -104,6 +172,13 @@ Rejected with 409 once **any** criteria exist on the task — the agent lock.
|
|
|
104
172
|
Echoes the stored criteria (with ids) plus the 3–7 soft-cap warning if
|
|
105
173
|
outside range.
|
|
106
174
|
|
|
175
|
+
```bash
|
|
176
|
+
lumo task criteria set LUM-42 --file criteria-lum42.json
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
(`--file` must point inside the current project directory — write the JSON to
|
|
180
|
+
the repo root or a subdirectory, not `/tmp`, and delete it after submission.)
|
|
181
|
+
|
|
107
182
|
### `lumo task criteria set <task> --file <criteria.json> --human`
|
|
108
183
|
|
|
109
184
|
Record a **human contract revision** (HUMAN_EDIT), e.g. when the user changes
|
|
@@ -125,6 +200,11 @@ verdict is never** — human verdicts only enter through human-initiated paths
|
|
|
125
200
|
Only use `--human` for decisions a human actually made (in conversation, in a
|
|
126
201
|
comment, in review). Never use it to work around your own lock.
|
|
127
202
|
|
|
203
|
+
```bash
|
|
204
|
+
lumo task criteria set LUM-42 --file criteria-revision.json --human
|
|
205
|
+
lumo task criteria set LUM-42 --file criteria-revision.json --human --cause SCOPE_CHANGE
|
|
206
|
+
```
|
|
207
|
+
|
|
128
208
|
Optionally annotate **why** the contract drifted with
|
|
129
209
|
`--cause <NEW_INFO|SCOPE_CHANGE|DRAFT_BLIND_SPOT|GRANULARITY|OTHER>` — pick
|
|
130
210
|
the tag from the human's stated reason (new information, scope moved, the
|
|
@@ -139,6 +219,10 @@ Print the contract: `<id> [MACHINE|HUMAN] SOURCE@rN [evidence] statement`
|
|
|
139
219
|
plus an indented `↳ check:` line for checkpointers. Use it to fetch ids
|
|
140
220
|
before a `--human` revision. Empty contract prints a drafting pointer.
|
|
141
221
|
|
|
222
|
+
```bash
|
|
223
|
+
lumo task criteria list LUM-42
|
|
224
|
+
```
|
|
225
|
+
|
|
142
226
|
## Injection behavior
|
|
143
227
|
|
|
144
228
|
- **Session start** (bound task): the contract is the highest-priority
|
|
@@ -132,12 +132,12 @@ Use default mode when the user wants to read a doc; use `--raw` whenever the ful
|
|
|
132
132
|
|
|
133
133
|
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
134
|
|
|
135
|
-
| Flag | Type
|
|
136
|
-
| --------------------- |
|
|
137
|
-
| `--section <heading>` | string
|
|
138
|
-
| `--content <text>` | string
|
|
139
|
-
| `--file <path>` | string
|
|
140
|
-
| `--if-revision <n>` | int
|
|
135
|
+
| Flag | Type | Notes |
|
|
136
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| `--section <heading>` | string | Required. Heading text addressing the section; prefix `#…` pins the depth. |
|
|
138
|
+
| `--content <text>` | string | New section content (include the heading line — the whole section is replaced verbatim). |
|
|
139
|
+
| `--file <path>` | string | New section content from file (project-local sandbox). |
|
|
140
|
+
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`. Recommended whenever the edit base was read earlier. |
|
|
141
141
|
| `--allow-shrink` | boolean | Let the patch through even when it drops tables/rows/headings within the addressed section (422 otherwise). |
|
|
142
142
|
|
|
143
143
|
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 +164,11 @@ lumo doc patch cmd_xxx --section "D 状态表" --file sec.md --if-revision 6
|
|
|
164
164
|
|
|
165
165
|
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
166
|
|
|
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).
|
|
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). |
|
|
172
172
|
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
|
|
173
173
|
|
|
174
174
|
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). Requires a stored markdown source.
|
|
@@ -188,8 +188,8 @@ echo "## 2026-06-10\n吸收了 3 篇" | lumo doc append cmd_xxx # document end
|
|
|
188
188
|
|
|
189
189
|
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
190
|
|
|
191
|
-
| Flag | Type | Notes
|
|
192
|
-
| --------------- | ------ |
|
|
191
|
+
| Flag | Type | Notes |
|
|
192
|
+
| --------------- | ------ | --------------------------------------------------------------------------------- |
|
|
193
193
|
| `--file <path>` | string | Required. Local markdown file; same project-local sandbox as `doc update --file`. |
|
|
194
194
|
|
|
195
195
|
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 +278,10 @@ lumo doc delete cmd_xxx --yes
|
|
|
278
278
|
|
|
279
279
|
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
280
|
|
|
281
|
+
```bash
|
|
282
|
+
lumo doc bind cmd_xxx LUM-42
|
|
283
|
+
```
|
|
284
|
+
|
|
281
285
|
Output:
|
|
282
286
|
|
|
283
287
|
```
|
|
@@ -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.
|
|
@@ -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
|
+
}
|
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`.')
|