@lumoai/cli 1.29.0 → 1.29.1

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.
@@ -0,0 +1,148 @@
1
+ # lumo verify — machine verification loop
2
+
3
+ `lumo verify` is the machine half of the acceptance system (Acceptance v1,
4
+ LUM-343). It executes every **MACHINE** criterion's checkpointer in the local
5
+ repo, reports one structured PASS/FAIL verdict per criterion to the server,
6
+ and prints what to do next. The judge lives server-side: round numbering, the
7
+ 3-round cap, escalation, and the IN_REVIEW transition all happen there
8
+ (execution on the client, adjudication on the server).
9
+
10
+ ## The claim-done rule
11
+
12
+ **Before claiming a task is complete — in conversation, in a wrap-up, or by
13
+ touching its status — run `lumo verify`.** The loop replaces "I read the code
14
+ and it looks done" with executed evidence.
15
+
16
+ ```
17
+ lumo verify # session-bound task
18
+ lumo verify LUM-42 # explicit task (overrides the session binding)
19
+ lumo verify --timeout 900 # per-checkpointer timeout in seconds (default 600)
20
+ ```
21
+
22
+ ## What one round does
23
+
24
+ 1. Loads the task's acceptance contract and picks out MACHINE criteria.
25
+ 2. Runs each checkpointer locally (shell, cwd = current directory), one at a
26
+ time, echoing PASS/FAIL as it goes.
27
+ 3. POSTs the structured verdicts; the server records one VerificationRun per
28
+ criterion at round = previous max + 1 and mirrors each verdict as a
29
+ TaskActivity event.
30
+ 4. Prints the round outcome:
31
+ - **All PASS** → the task transitions to **IN_REVIEW** (existing state
32
+ machine + TASK_IN_REVIEW notification). **Stop here.** Human
33
+ adjudication and any HUMAN criteria take over; never set DONE yourself.
34
+ - **Any FAIL** → task status is untouched; the unmet criteria are printed
35
+ as next actions (statement, checkpointer, failure tail). Fix and re-run.
36
+ - **Round 3 still failing** → the loop escalates: a human is notified
37
+ (AGENT_VERIFY, requires action) and further `lumo verify` rounds are
38
+ rejected with 409. **Stop retrying**; fix only what the human directs.
39
+
40
+ Exit code 0 = all passed (or nothing to run); 1 = failures, escalation, or
41
+ errors.
42
+
43
+ ## Verdict semantics (what the CLI sends)
44
+
45
+ - checkpointer exits 0 → `PASS` with evidence `cmd:<command>#exit=0`
46
+ - non-zero exit → `FAIL`, reason = output tail, enum `CRITERION_UNMET`
47
+ - spawn failure / timeout → `FAIL`, enum `CHECK_EXECUTION_ERROR`
48
+
49
+ evidencePointer is **not free text** — the server only accepts
50
+ `commit:<hash>`, `file:<path>:<line>`, or `cmd:<command>#exit=<code>`.
51
+ Verdicts are PASS|FAIL only; the agent path cannot write HUMAN verdicts or
52
+ `PASS_WITH_FOLLOWUP` (red line — those enter via human-initiated UI paths
53
+ only).
54
+
55
+ ## Edge cases
56
+
57
+ - **No contract yet** → error pointing at `lumo task criteria set`; draft the
58
+ contract first (criteria.md golden rule).
59
+ - **HUMAN-only contract (zero MACHINE criteria)** → nothing to run; the CLI
60
+ says so and suggests handing off for human review
61
+ (`lumo task update <id> --status in_review`). No server write happens.
62
+ - **A round must cover every MACHINE criterion** — the CLI always runs all of
63
+ them; the server rejects partial rounds.
64
+ - Criteria added during review (`REVIEW_ADDED`) appear in the contract and
65
+ are picked up automatically by the next round.
66
+
67
+ ## Round discipline
68
+
69
+ Rounds are a hard budget of 3, not a retry loop. Between rounds, actually fix
70
+ the failures — re-running without changes burns a round and (at round 3)
71
+ pages a human. A FAIL round never changes task status; only an all-pass round
72
+ moves it (to IN_REVIEW, never further).
73
+
74
+ ## Review-time drift habits (gap findings)
75
+
76
+ A problem discovered during acceptance/review that the contract does NOT
77
+ cover is a **gap finding** — record it in the contract, never just fix it
78
+ silently:
79
+
80
+ 1. **Append it on the spot.** Transcribe the human's finding as a criterion
81
+ via `lumo task criteria set <task> --file <desired-final-list> --human` —
82
+ the review-added semantics: the gap surfaced at review time, at the
83
+ current round.
84
+ 2. **Tag why the contract drifted** with `--cause
85
+ <NEW_INFO|SCOPE_CHANGE|DRAFT_BLIND_SPOT|GRANULARITY|OTHER>`. Gap findings
86
+ are usually `DRAFT_BLIND_SPOT` (the draft missed it) or `NEW_INFO`
87
+ (information that didn't exist at drafting time).
88
+ 3. **Then bounce.** The appended criterion shows up in `lumo task status`
89
+ nextActions and the next verify round picks it up automatically — no
90
+ side-channel to-do list.
91
+
92
+ How to read drift: information-lag and requirement-movement drift
93
+ (`NEW_INFO`, `SCOPE_CHANGE`) is healthy — don't optimize it away.
94
+ `DRAFT_BLIND_SPOT` clusters feed back into the drafting guide. **Zero drift
95
+ across many tasks is a red flag, not a trophy** — it usually means contracts
96
+ are too thin or state only sure-win clauses that can never be found wanting.
97
+
98
+ ## lumo task status — the read half (self-check entry point)
99
+
100
+ `lumo task status [task] [--json]` is the read-only counterpart of the loop
101
+ (LUM-344): pure read, milliseconds, no LLM, never writes — running it costs
102
+ nothing and burns no round. Defaults to the session-bound task; an explicit
103
+ identifier overrides.
104
+
105
+ ```
106
+ lumo task status # session-bound task
107
+ lumo task status LUM-42 # explicit task
108
+ lumo task status --json # versioned machine-readable payload
109
+ ```
110
+
111
+ ### When to run it
112
+
113
+ **Status-first recovery:** run it FIRST — before re-reading code or
114
+ planning — whenever you:
115
+
116
+ - resume a task in a new session (yours or another agent's earlier work);
117
+ - come back after a verification round was rejected (`lumo verify` failed);
118
+ - were told the task bounced in review (REVIEW_ADDED criteria may have been
119
+ appended at the round they surfaced — they show up here automatically).
120
+
121
+ It answers "where does the loop stand": what already passed (don't redo it),
122
+ what's unmet and why (the exact failure tails), and how many rounds are left.
123
+
124
+ ### What it prints
125
+
126
+ - Header: task identifier/title/status + `verification round N/3` (round 0 =
127
+ never verified) + an escalation warning when the machine loop is exhausted.
128
+ - **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN
129
+ statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its
130
+ checkpointer and latest verdict line (evidence pointer on pass, failure
131
+ tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
132
+ - **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
133
+ - **Last round failures** — the most recent round's FAIL verdicts with their
134
+ rejection reasons (why the last round bounced).
135
+ - **Next actions** — the unmet criteria (latest verdict is not a pass:
136
+ failed or never verified, HUMAN ones included). This list IS the plan —
137
+ it is recomputed from the event log on every read, never maintained
138
+ separately. Empty + rounds recorded = awaiting human adjudication.
139
+
140
+ ### --json contract
141
+
142
+ `--json` emits the full read model with a top-level `version` field
143
+ (currently `1`). The schema is versioned: breaking shape changes bump the
144
+ major; additive fields don't. Pin on `version` when scripting against it.
145
+
146
+ `status` reads; `verify` judges. Running status never starts a round, never
147
+ escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
148
+ all-pass, human-only DONE) live entirely in `lumo verify` and the server.
@@ -51,7 +51,7 @@ async function docList(opts) {
51
51
  }
52
52
  else {
53
53
  const params = new URLSearchParams();
54
- if (opts.scope && opts.scope !== 'all') {
54
+ if (opts.scope && opts.scope.toLowerCase() !== 'all') {
55
55
  const v = (0, doc_create_1.normalizeScope)(opts.scope);
56
56
  if (!v) {
57
57
  console.error(`Error: invalid scope "${opts.scope}". Allowed: personal, workspace, all`);
@@ -74,7 +74,7 @@ async function docList(opts) {
74
74
  }
75
75
  const { documents } = (await res.json());
76
76
  let rows = documents;
77
- if (opts.task && opts.scope && opts.scope !== 'all') {
77
+ if (opts.task && opts.scope && opts.scope.toLowerCase() !== 'all') {
78
78
  const v = (0, doc_create_1.normalizeScope)(opts.scope);
79
79
  if (v)
80
80
  rows = rows.filter(r => r.visibility === v);
@@ -256,7 +256,7 @@ task
256
256
  task
257
257
  .command('list')
258
258
  .description('List tasks assigned to you. Filter with --status, --project, --milestone, --limit.')
259
- .option('-s, --status <value>', 'Filter by status: todo | in_progress | in_review | done')
259
+ .option('-s, --status <value>', 'Filter by status: todo | in_progress | in_review | done (case-insensitive)')
260
260
  .option('-p, --project <ref>', 'Filter by project name (case-insensitive)')
261
261
  .option('-n, --limit <count>', 'Show only the first N tasks')
262
262
  .option('-m, --milestone <ref>', 'Filter by milestone name or UUID (scopes to my tasks under that milestone)')
@@ -278,7 +278,7 @@ task
278
278
  .command('create <title>')
279
279
  .description('Create a task. --project required when workspace has >1 project; defaults: priority=low, assignee=me.')
280
280
  .option('-d, --description <text>', 'Task description')
281
- .option('-p, --priority <level>', 'Priority: low | medium | high | urgent (default: low)')
281
+ .option('-p, --priority <level>', 'Priority: low | medium | high | urgent (case-insensitive; default: low)')
282
282
  .option('-a, --assignee <ref>', 'Assignee email, name, or "me" (default: me)')
283
283
  .option('--project <ref>', 'Project name or slug')
284
284
  .option('--milestone <ref>', 'Milestone name (case-insensitive)')
@@ -377,7 +377,7 @@ const taskMemory = task
377
377
  taskMemory
378
378
  .command('list [task]')
379
379
  .description("List a task's memories. <task> defaults to the session-bound task.")
380
- .option('--category <cat>', 'Filter by trap|decision|convention|procedural')
380
+ .option('--category <cat>', 'Filter by trap|decision|convention|procedural (case-insensitive)')
381
381
  .option('-n, --limit <count>', 'Cap output to the first N rows')
382
382
  .action(wrap((taskArg, opts) => (0, memory_task_list_1.memoryTaskList)(taskArg, opts)));
383
383
  taskMemory
@@ -385,7 +385,7 @@ taskMemory
385
385
  .description('Record a memory on a task (<task> defaults to the bound session). ' +
386
386
  'Record only knowledge invisible in the codebase (the why, a runtime gotcha, a non-obvious failure, a non-trivial workflow); skip routine work. ' +
387
387
  'Pick --category then its fields.')
388
- .requiredOption('--category <cat>', 'trap | decision | convention | procedural')
388
+ .requiredOption('--category <cat>', 'trap | decision | convention | procedural (case-insensitive)')
389
389
  .option('--trigger <text>', 'trap/procedural: the situation that triggers it')
390
390
  .option('--outcome <text>', 'trap: what goes wrong')
391
391
  .option('--workaround <text>', 'trap: optional fix')
@@ -397,7 +397,7 @@ taskMemory
397
397
  .option('--applies <text>', 'convention: where the rule applies')
398
398
  .option('--workflow <text>', 'procedural: the workflow name')
399
399
  .option('--step <text>', 'procedural: a step (repeatable)', collect, [])
400
- .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
400
+ .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (case-insensitive; default claude-code)')
401
401
  .action(wrap((taskArg, opts) => (0, memory_task_add_1.memoryTaskAdd)(taskArg, opts)));
402
402
  const taskCriteria = task
403
403
  .command('criteria')
@@ -407,7 +407,7 @@ taskCriteria
407
407
  .description('Submit the whole acceptance contract from a JSON file. Default = initial agent draft (locked once submitted); --human records a HUMAN_EDIT revision (desired final list; items with "id" keep/update, missing ones are deleted).')
408
408
  .requiredOption('--file <path>', 'JSON array of criteria: [{"statement","verifierType":"MACHINE"|"HUMAN","checkpointer?","evidenceRequired?","id?"}]')
409
409
  .option('--human', 'Record a human contract revision (HUMAN_EDIT) transcribed from the conversation, with session provenance')
410
- .option('--cause <tag>', 'Why the contract drifted (with --human): NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER')
410
+ .option('--cause <tag>', 'Why the contract drifted (with --human): NEW_INFO | SCOPE_CHANGE | DRAFT_BLIND_SPOT | GRANULARITY | OTHER (case-insensitive)')
411
411
  .action(wrap((taskId, options) => (0, task_criteria_set_1.taskCriteriaSet)(taskId, options)));
412
412
  taskCriteria
413
413
  .command('list <task>')
@@ -423,7 +423,7 @@ taskArtifact
423
423
  .requiredOption('--title <title>', 'Artifact title')
424
424
  .requiredOption('--file <path>', 'File whose contents become the artifact body')
425
425
  .requiredOption('--source <source>', 'Spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (opaque; quote names with spaces)')
426
- .requiredOption('--agent <agent>', 'Coding tool that produced the artifact: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
426
+ .requiredOption('--agent <agent>', 'Coding tool that produced the artifact: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (case-insensitive)')
427
427
  .action(wrap((taskId, options) => (0, task_artifact_add_1.taskArtifactAdd)(taskId, options)));
428
428
  taskArtifact
429
429
  .command('update <task> <artifact-id>')
@@ -431,7 +431,7 @@ taskArtifact
431
431
  .option('--kind <kind>', 'New artifact kind (opaque)')
432
432
  .option('--title <title>', 'New artifact title')
433
433
  .option('--source <source>', 'New spec-gen framework, formal name e.g. Superpowers | "Spec Kit" | BMad | OpenSpec | GSD (quote names with spaces)')
434
- .option('--agent <agent>', 'New coding tool: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf')
434
+ .option('--agent <agent>', 'New coding tool: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (case-insensitive)')
435
435
  .action(wrap((taskId, artifactId, options) => (0, task_artifact_update_1.taskArtifactUpdate)(taskId, artifactId, options)));
436
436
  taskArtifact
437
437
  .command('list <task>')
@@ -459,13 +459,13 @@ const projectMemory = projectCmd
459
459
  projectMemory
460
460
  .command('list [project]')
461
461
  .description("List a project's PROJECT-scope memories. <project> defaults to the bound task's project.")
462
- .option('--category <cat>', 'Filter by trap|decision|convention|procedural')
462
+ .option('--category <cat>', 'Filter by trap|decision|convention|procedural (case-insensitive)')
463
463
  .option('-n, --limit <count>', 'Cap output to the first N rows')
464
464
  .action(wrap((p, opts) => (0, memory_project_list_1.memoryProjectList)(p, opts)));
465
465
  projectMemory
466
466
  .command('add [project]')
467
467
  .description("Record a PROJECT-scope memory. Use PROJECT scope only when the lesson helps any task in the project. <project> defaults to the bound task's project.")
468
- .requiredOption('--category <cat>', 'trap | decision | convention | procedural')
468
+ .requiredOption('--category <cat>', 'trap | decision | convention | procedural (case-insensitive)')
469
469
  .option('--trigger <text>', 'trap/procedural trigger')
470
470
  .option('--outcome <text>', 'trap outcome')
471
471
  .option('--workaround <text>', 'trap optional workaround')
@@ -477,7 +477,7 @@ projectMemory
477
477
  .option('--applies <text>', 'convention: where it applies')
478
478
  .option('--workflow <text>', 'procedural workflow')
479
479
  .option('--step <text>', 'procedural step (repeatable)', collect, [])
480
- .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (default claude-code)')
480
+ .option('--agent <agent>', 'Producing agent: claude-code | codex | cursor | gemini-cli | github-copilot | windsurf (case-insensitive; default claude-code)')
481
481
  .action(wrap((p, opts) => (0, memory_project_add_1.memoryProjectAdd)(p, opts)));
482
482
  const memoryCmd = program
483
483
  .command('memory')
@@ -521,7 +521,7 @@ milestoneCmd
521
521
  .option('--project <ref>', 'Project name or slug (when identifier is a name)')
522
522
  .option('-n, --name <text>', 'New name')
523
523
  .option('-d, --description <text>', 'New description (empty string to clear)')
524
- .option('-s, --status <value>', 'New status: planned | active | completed | cancelled')
524
+ .option('-s, --status <value>', 'New status: planned | active | completed | cancelled (case-insensitive)')
525
525
  .option('--start <date>', 'Start date YYYY-MM-DD (empty string to clear)')
526
526
  .option('--target <date>', 'Target date YYYY-MM-DD (empty string to clear)')
527
527
  .action(wrap((identifier, options) => (0, milestone_update_1.milestoneUpdate)(identifier, options)));
@@ -641,7 +641,7 @@ doc
641
641
  .description('Create a new document. Body comes from --content, --file, or piped stdin (pick one). --scope defaults to personal. Use --task LUM-N to bind the new doc to a task, --parent to file it under another doc.')
642
642
  .option('--content <text>', 'Inline markdown content')
643
643
  .option('--file <path>', 'Read markdown content from file')
644
- .option('--scope <scope>', 'personal | workspace (default: personal)')
644
+ .option('--scope <scope>', 'personal | workspace (case-insensitive; default: personal)')
645
645
  .option('--project <ref>', 'Project name or slug to file under')
646
646
  .option('--task <LUM-N>', 'Bind to this task after creation')
647
647
  .option('--parent <doc>', 'File under this parent doc (cuid or title)')
@@ -651,7 +651,7 @@ doc
651
651
  doc
652
652
  .command('import-gdoc <url>')
653
653
  .description('Import a Google Doc as a Lumo doc (one-way Google → Lumo)')
654
- .option('--scope <scope>', 'personal | workspace (default: personal)')
654
+ .option('--scope <scope>', 'personal | workspace (case-insensitive; default: personal)')
655
655
  .option('--task <LUM-N>', 'Bind the imported doc to this task')
656
656
  .action(wrap((url, opts) => (0, doc_import_gdoc_1.docImportGdoc)(url, opts)));
657
657
  doc
@@ -664,7 +664,7 @@ doc
664
664
  .option('--title <text>', 'New title')
665
665
  .option('--content <text>', 'Replace content (inline)')
666
666
  .option('--file <path>', 'Replace content from file')
667
- .option('--scope <scope>', 'personal | workspace')
667
+ .option('--scope <scope>', 'personal | workspace (case-insensitive)')
668
668
  .option('--project <ref>', 'Project name/slug; pass "" to clear')
669
669
  .option('--tag <name>', 'Set tags by name (bulk replace, repeatable)', collect, [])
670
670
  .option('--tag-id <cuid>', 'Set tags by id (bulk replace, repeatable)', collect, [])
@@ -704,7 +704,7 @@ doc
704
704
  doc
705
705
  .command('list')
706
706
  .description('List documents visible to the current user')
707
- .option('--scope <scope>', 'personal | workspace | all (default: all)')
707
+ .option('--scope <scope>', 'personal | workspace | all (case-insensitive; default: all)')
708
708
  .option('--project <ref>', 'Filter by project name/slug')
709
709
  .option('--task <LUM-N>', 'Only docs bound to this task')
710
710
  .option('--limit <n>', 'Cap output to first N rows')
@@ -732,7 +732,7 @@ doc
732
732
  doc
733
733
  .command('share <doc> <member>')
734
734
  .description('Share a document with a workspace member. PRIVATE docs are auto-promoted to SHARED. <member> accepts "me", an email, or a display name.')
735
- .option('--role <role>', 'viewer | editor | manager (default: viewer)')
735
+ .option('--role <role>', 'viewer | editor | manager (case-insensitive; default: viewer)')
736
736
  .action(wrap((docRef, member, opts) => (0, doc_share_1.docShare)(docRef, member, opts)));
737
737
  doc
738
738
  .command('unshare <doc> <member>')
@@ -747,8 +747,8 @@ task
747
747
  .description('Update a task. Provide at least one of --title, --description, --status, --priority, --assignee, --milestone, --sprint, or tag flags. Use "" to clear description, assignee, milestone, or sprint binding. --tag/--tag-id (bulk replace) cannot be combined with --add-tag/--add-tag-id/--remove-tag/--remove-tag-id.')
748
748
  .option('-t, --title <text>', 'New title')
749
749
  .option('-d, --description <text>', 'New description (empty string to clear)')
750
- .option('-s, --status <value>', 'New status: todo | in_progress | in_review | done')
751
- .option('-p, --priority <level>', 'New priority: low | medium | high | urgent')
750
+ .option('-s, --status <value>', 'New status: todo | in_progress | in_review | done (case-insensitive)')
751
+ .option('-p, --priority <level>', 'New priority: low | medium | high | urgent (case-insensitive)')
752
752
  .option('-a, --assignee <ref>', 'New assignee: email, name, or "me" (empty string to clear)')
753
753
  .option('--milestone <ref>', 'Milestone name (case-insensitive); empty string to unbind')
754
754
  .option('--sprint <ref>', 'Sprint number or UUID to bind the task to; empty string to unbind from current sprint')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.29.0",
3
+ "version": "1.29.1",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",