@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.
@@ -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 | 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. |
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
- npx @lumoai/cli setup --force # overwrite skill files if they differ from bundled
26
- npx @lumoai/cli setup --agent codex # bake agent=codex into the hook commands
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
+ }
@@ -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`.')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.30.0",
3
+ "version": "1.31.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",