@lumoai/cli 1.45.0 → 1.47.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 +5 -5
- package/assets/skill/references/criteria.md +9 -9
- package/assets/skill/references/doc-editing.md +10 -10
- package/assets/skill/references/docs.md +22 -22
- package/assets/skill/references/memory.md +9 -10
- package/assets/skill/references/sessions.md +10 -10
- package/assets/skill/references/task-context.md +7 -7
- package/assets/skill/references/tasks.md +8 -8
- package/assets/skill/references/verify.md +43 -30
- package/assets/skill/references/worktree.md +1 -1
- package/dist/cli/src/commands/task-status.js +120 -7
- package/dist/cli/src/commands/verify.js +14 -1
- package/dist/cli/src/index.js +1 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -83,7 +83,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
83
83
|
|
|
84
84
|
**Verification (machine acceptance loop)** — see [verify.md](references/verify.md)
|
|
85
85
|
|
|
86
|
-
- `lumo verify [task] [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally and report a structured PASS/FAIL verdict per criterion. Round cap 3: all-pass → task to IN_REVIEW (agent stops); round-3 fail → escalate to human. **Run this before claiming a task is done.**
|
|
86
|
+
- `lumo verify [task] --note "<self-report>" [--timeout <seconds>]` — run every MACHINE criterion's checkpointer locally and report a structured PASS/FAIL verdict per criterion. **`--note` is required** when a round will be posted: a one-line self-report ("我改了 X,因为 Y 可验收") frozen as your task's claim (source AGENT) the moment the round passes into IN_REVIEW, then checked against the diff for faithfulness. No note → no round posted (none burned). Round cap 3: all-pass → task to IN_REVIEW (agent stops); round-3 fail → escalate to human. **Run this before claiming a task is done.**
|
|
87
87
|
- `lumo task status [task] [--json]` — read-only acceptance self-check (no LLM): the contract with each criterion's latest verdict, verification history/round, last failure reasons, `nextActions` (the unmet criteria), and any OPEN boundary crossings (fails closed — `null`/`⚠` means "could not confirm", not safe). **Run it first when resuming a task or after a round was rejected.**
|
|
88
88
|
- `lumo verdict [task] --pass | --fail` — acceptance verdicts. `--pass` deep-links to the human verdict bar (records nothing). `--fail --reason <enum> …` records an AGENT send-back → IN_PROGRESS. **An unresolved send-back blocks the DONE transition with 409.**
|
|
89
89
|
- `lumo crossing explain <id> --note "<text>"` — append an agent self-explanation ("申辩") to a boundary crossing; append-only, **never clears the crossing or unblocks Done** (disposition stays web + human-only). A review aid, not a self-clear.
|
|
@@ -137,7 +137,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
137
137
|
|
|
138
138
|
- `lumo session attach <id>` — bind this session to a task (then run `task context`). **Lifetime lock**: re-attaching to the same task is a no-op; attaching to a _different_ task is refused with 409 — start a new Claude Code session instead. No `--force`, no `session detach`.
|
|
139
139
|
- `lumo session status` — show current binding
|
|
140
|
-
- End-of-session housekeeping is fully automatic (no command
|
|
140
|
+
- End-of-session housekeeping is fully automatic (no command). On DONE the server runs three best-effort silent passes: Layer-1 memory curation, fragment-usage audit, and blocked-tag automation. See [sessions.md](references/sessions.md)
|
|
141
141
|
- Git-suggest at session start (suggests `session attach`, never auto-binds) + Layer-2 project-memory review — see the reference
|
|
142
142
|
|
|
143
143
|
**Worktrees (local dev tooling)** — see [worktree.md](references/worktree.md)
|
|
@@ -148,10 +148,10 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
148
148
|
|
|
149
149
|
## Commands & flags that do NOT exist (common mistakes)
|
|
150
150
|
|
|
151
|
-
|
|
151
|
+
Don't guess these:
|
|
152
152
|
|
|
153
153
|
- No `lumo session start` — binding is `lumo session attach <LUM-N>`
|
|
154
|
-
- No `lumo session detach` — the session↔task binding is a lifetime lock
|
|
154
|
+
- No `lumo session detach` — the session↔task binding is a lifetime lock; to work on a different task, start a new Claude Code session
|
|
155
155
|
- No `lumo task delete` — tasks can't be deleted from the CLI (web UI only)
|
|
156
156
|
- No `lumo task artifact edit` — it's `lumo task artifact update`
|
|
157
157
|
- No `lumo auth status` — identity check is `lumo whoami`
|
|
@@ -175,7 +175,7 @@ Measured from real agent sessions (LUM-392) — don't guess these:
|
|
|
175
175
|
|
|
176
176
|
**Git-suggest at start:** when unbound, session-start may infer the task from the git branch / recent commits (any team prefix, e.g. `SPEC-12`) and print `Detected LUM-N … Run lumo session attach LUM-N to bind.` — **without** binding. Confirm it's right, then attach yourself. See [sessions.md](references/sessions.md)
|
|
177
177
|
|
|
178
|
-
### Golden rules (most-violated
|
|
178
|
+
### Golden rules (most-violated)
|
|
179
179
|
|
|
180
180
|
- **Attach + draft criteria before coding.** Skipping attach triggers `SESSION_BINDING_MISSING`; skipping criteria means verification has nothing to check.
|
|
181
181
|
- **Evidence before "done".** Never claim done from reading code — run `lumo verify`. While a PR is open, the task is IN_REVIEW, not DONE.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Acceptance criteria (contract)
|
|
2
2
|
|
|
3
|
-
The acceptance contract is a small set of structured criteria the task's work is verified against
|
|
3
|
+
The acceptance contract is a small set of structured criteria the task's work is verified against. The agent drafts it; the server validates and stores it; verification rounds (`lumo verify`) judge against it. Criteria are injected at session start and in `lumo task context` as the `## Acceptance criteria (contract)` section.
|
|
4
4
|
|
|
5
5
|
## When to draft — the golden rule
|
|
6
6
|
|
|
@@ -45,7 +45,7 @@ The 3–7 range is calibrated for typical multi-file tasks. Criterion count must
|
|
|
45
45
|
- **Outcome-level definition of done, not micro-steps** — a verifiable result (`"lumo task criteria set rejects a second agent draft with 409"`), not a task step ("add a check in the service").
|
|
46
46
|
- **Repo-wide baselines don't take slots** — tests pass / `tsc --noEmit` clean / i18n locale parity / lint are already required by the repo's PR checklist; never spend one of your 3–7 criteria on them.
|
|
47
47
|
- `evidenceRequired: true` — marks criteria whose verdict must point at proof (a MACHINE PASS always requires evidence regardless of this flag).
|
|
48
|
-
- **Edited a MACHINE checkpointer mid-task? Re-run `lumo verify`.** A prior PASS was recorded against the old command — swapping the checkpointer (or rewording the criterion) makes that pass stale: `lumo task status` / the acceptance tab flag it `⚠ pre-edit version
|
|
48
|
+
- **Edited a MACHINE checkpointer mid-task? Re-run `lumo verify`.** A prior PASS was recorded against the old command — swapping the checkpointer (or rewording the criterion) makes that pass stale: `lumo task status` / the acceptance tab flag it `⚠ pre-edit version`. The stale pass still counts as met (render-only, doesn't block DONE), but re-verify so the green reflects the current check.
|
|
49
49
|
|
|
50
50
|
**MACHINE vs HUMAN:**
|
|
51
51
|
|
|
@@ -55,10 +55,10 @@ The 3–7 range is calibrated for typical multi-file tasks. Criterion count must
|
|
|
55
55
|
|
|
56
56
|
**Two checkpointer traps:**
|
|
57
57
|
|
|
58
|
-
- `npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` — jest-by-name checkpointers must go through this zero-match guard. A bare `npx jest -t '<name>'` exits 0 even when the pattern matches **no** test — rename or delete the test and the checkpointer silently fake-PASSes
|
|
59
|
-
- `instance-independent, never self-comparing` — a checkpointer is stored once and re-run for the life of the task, so it must not depend on a baseline that shifts under it. The trap is a `vs origin/main` self-comparison ("current file is strictly smaller than origin/main"): passes pre-merge, but the moment the change merges every branch cut from main has `current == base` and the check fails forever
|
|
58
|
+
- `npx tsx scripts/jest-t.ts '<exact test name>' [test file path]` — jest-by-name checkpointers must go through this zero-match guard. A bare `npx jest -t '<name>'` exits 0 even when the pattern matches **no** test — rename or delete the test and the checkpointer silently fake-PASSes. `jest-t.ts` fails unless ≥1 matching test ran and passed; the optional path scopes the run to one file (much faster than name-filtering the whole suite).
|
|
59
|
+
- `instance-independent, never self-comparing` — a checkpointer is stored once and re-run for the life of the task, so it must not depend on a baseline that shifts under it. The trap is a `vs origin/main` self-comparison ("current file is strictly smaller than origin/main"): passes pre-merge, but the moment the change merges every branch cut from main has `current == base` and the check fails forever. Anchor to a **fixed, absolute target** — a literal budget (`byteCount ≤ 9500`), a committed fixture, or a stable named test through the zero-match guard. A jest-by-name checkpointer likewise rots when its test is renamed/deleted — often by _another_ task's work, so the stored PASS goes stale invisibly. Keep test names stable; `npx tsx scripts/checkpointer-drift.ts <LUM-N> | --mine` statically sweeps stored contracts for these zero-match checkpointers (detection-only).
|
|
60
60
|
|
|
61
|
-
### judgeSteps — agent-drafted judging steps for HUMAN criteria
|
|
61
|
+
### judgeSteps — agent-drafted judging steps for HUMAN criteria
|
|
62
62
|
|
|
63
63
|
A HUMAN criterion is judged by a person, not a checkpointer — so don't hand them a bare assertion ("the copy reads naturally") and make them reverse-engineer what to do. Attach **`judgeSteps`**: short, human-readable instructions the adjudication card renders verbatim (light markdown, URLs made clickable). Structured labour is yours; the human just follows the steps.
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ A HUMAN criterion is judged by a person, not a checkpointer — so don't hand th
|
|
|
68
68
|
2. `What to do` — the concrete action ("open the acceptance tab", "read the `judgeSteps` block aloud", "resize to mobile width").
|
|
69
69
|
3. `✓ pass / ✗ send-back` — the decision rule, both directions, so the verdict isn't a coin-flip ("pass if every URL opens; send back if any step is vague").
|
|
70
70
|
|
|
71
|
-
**One criterion = one judgment point.** If a HUMAN criterion bundles two checks, split it — each half gets its own steps. The canonical miss is
|
|
71
|
+
**One criterion = one judgment point.** If a HUMAN criterion bundles two checks, split it — each half gets its own steps. The canonical miss is an "additive schema **and** self-credit not implemented" criterion: two assertions; the first is even machine-able (below), the second is a separate human read.
|
|
72
72
|
|
|
73
73
|
**Machine-able → make it MACHINE, don't burn human attention.** Before writing `judgeSteps`, ask whether a checkpointer could decide it. "The migration is purely additive" → `grep` the `migration.sql` for `DROP`/destructive DDL → MACHINE. "No file under `prisma/migrations/` was deleted" → a `git diff` probe → MACHINE. Reserve HUMAN + `judgeSteps` for genuine taste/feel/fidelity judgments a check can't make.
|
|
74
74
|
|
|
@@ -90,7 +90,7 @@ Most criteria assert that **something that should happen, happened** ("the endpo
|
|
|
90
90
|
- `How to phrase it` — state the invariant as _still holding after the change_, not as a step you took. The **c-CRAB boundary** is the hard rule: encode **"the problem does not (re)occur"**, never **"a specific fix exists"**. Write "`prisma/migrations/` has no deleted files vs origin/main" (the bad state is absent) — not "the migration-delete guard function is present" (a named fix). The first survives a refactor of _how_ the invariant is enforced; the second re-fails the moment someone renames the guard, and passes even if the protection was gutted some other way.
|
|
91
91
|
- `Pairing with a checkpointer` — an invariant almost always has a runnable check (its advantage), so prefer MACHINE. The existing `checkpointer` syntax carries it as-is: a `git diff` path/filter probe, a full-suite `jest` run, a structure-verify script, a parity check.
|
|
92
92
|
|
|
93
|
-
Two real-repo invariant examples — `prisma/migrations/` files never deleted (the CLAUDE.md red line), and a live-doc's table structure not flattened (
|
|
93
|
+
Two real-repo invariant examples — `prisma/migrations/` files never deleted (the CLAUDE.md red line), and a live-doc's table structure not flattened (an HTML→md round-trip that silently collapsed tables to plain text):
|
|
94
94
|
|
|
95
95
|
```json
|
|
96
96
|
[
|
|
@@ -107,7 +107,7 @@ Two real-repo invariant examples — `prisma/migrations/` files never deleted (t
|
|
|
107
107
|
]
|
|
108
108
|
```
|
|
109
109
|
|
|
110
|
-
A third
|
|
110
|
+
A third example invariant is "SKILL.md frontmatter description stays ≤ 1000 chars" — the standing context-budget invariant (the description is resident in every session), checkpointed by the `description cap` case in `scripts/analysis/lum392-cli-friction/__tests__/doc-examples.test.ts`.
|
|
111
111
|
|
|
112
112
|
One invariant criterion is usually enough — it's the guardrail, not the whole contract. Pair it with the positive criteria that say what the change should achieve; together they assert _did the right thing_ **and** _touched nothing it shouldn't_.
|
|
113
113
|
|
|
@@ -178,7 +178,7 @@ Optionally annotate **why** the contract drifted with `--cause <NEW_INFO|SCOPE_C
|
|
|
178
178
|
- `GRANULARITY` — wrong granularity.
|
|
179
179
|
- `OTHER` — anything else.
|
|
180
180
|
|
|
181
|
-
The tag lands in the drift record (TaskActivity payload)
|
|
181
|
+
The tag lands in the drift record (TaskActivity payload). Every criteria add/update/delete is mirrored as a structured `CRITERION_CHANGED` activity automatically; `--cause` just enriches it.
|
|
182
182
|
|
|
183
183
|
### `lumo task criteria list <task>`
|
|
184
184
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Commands for **reading a faithful edit base and writing back surgically** — the safe way to edit live docs (registers, ledgers, reports) without flattening tables or clobbering concurrent edits. For document CRUD, sharing, and import, see [docs.md](docs.md) (it also documents the shared **content channels** — `--content` / `--file` / stdin — and the **compact-tables authoring convention**, which apply equally to `doc patch` / `doc append`).
|
|
4
4
|
|
|
5
|
-
**The red line
|
|
5
|
+
**The red line:** never treat rendered `doc show` output as a re-uploadable source — HTML→markdown is lossy and flattens tables. The only legal edit base is `doc show --raw` / `--section`, which print the byte-identical stored `sourceMarkdown`.
|
|
6
6
|
|
|
7
7
|
## `lumo doc show <doc> [--raw | --section <heading>]` — print one document's detail
|
|
8
8
|
|
|
@@ -14,19 +14,19 @@ lumo doc show cmd_xxx --raw > base.md # byte-identical edit base (rev
|
|
|
14
14
|
lumo doc show cmd_xxx --section "D 状态表" > sec.md # one section only (revision on stderr)
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
**`--raw
|
|
17
|
+
**`--raw`** prints the byte-identical markdown source of the last markdown upload — no header, no trailing newline added. The server stores the raw markdown (`sourceMarkdown`) alongside the rendered HTML on every markdown write (`doc create/update --content/--file/stdin`, gdoc import/sync), so `--raw` output IS a legal edit base: `doc show --raw > base.md`, edit, `doc update --file base.md` round-trips losslessly.
|
|
18
18
|
|
|
19
19
|
- A web-editor (HTML-direct) edit or revision restore **invalidates** the stored source — the doc's markdown source is gone until the next markdown upload.
|
|
20
|
-
- When no source is stored (legacy doc or after an HTML edit), `--raw` **fails with exit 1 and a rebuild hint** — it never silently falls back to the lossy HTML→markdown reverse render
|
|
20
|
+
- When no source is stored (legacy doc or after an HTML edit), `--raw` **fails with exit 1 and a rebuild hint** — it never silently falls back to the lossy HTML→markdown reverse render. Rebuild flow: run **`lumo doc rebuild-source <doc>`** — it regenerates the source from the stored HTML with a lossless serializer (tables round-trip) and a structure guard, so `--raw` works from then on.
|
|
21
21
|
- Raw output is verbatim (unsanitized) by design — redirect it to a file rather than reading it in a terminal when the doc's provenance is uncertain.
|
|
22
22
|
|
|
23
23
|
Note: the markdown rendered by **default-mode** `doc show` is still best-effort (tables flatten). Round-trip via `doc show > tmp.md && doc update --file tmp.md` is NOT a no-op — use `--raw` as the edit base instead.
|
|
24
24
|
|
|
25
|
-
Output budget
|
|
25
|
+
Output budget: **default-mode** `doc show` caps the rendered body to the output-token budget (25,000 tokens) and, when truncated, ends in a pointer to `--section "<heading>"` / `--raw`. `--raw` and `--section` are **never** capped — they are byte-faithful edit bases.
|
|
26
26
|
|
|
27
|
-
**`--section <heading
|
|
27
|
+
**`--section <heading>`** prints just one heading-addressed section of the markdown source — a byte-faithful slice from the heading line through (not including) the next same-or-higher-level heading, subsections included. No header on stdout (the slice is a legal `doc patch` base); the current revision is printed to **stderr** as `Revision: N`. Mutually exclusive with `--raw`.
|
|
28
28
|
|
|
29
|
-
- Section addressing: pass the heading text (`--section "D 状态表"`), matched in three tiers — exact, then case-insensitive, then
|
|
29
|
+
- Section addressing: pass the heading text (`--section "D 状态表"`), matched in three tiers — exact, then case-insensitive, then full-width↔half-width punctuation + whitespace normalization, so a half-width query (`问题(P4)`) lands on a full-width stored heading (`问题(P4)`) and vice-versa. Prefix with `#…` to pin the level when the same text exists at several depths (`--section "## Status"`). Normalization never relaxes the ambiguity guard — multiple matches still give the candidate list + exit 1.
|
|
30
30
|
- Missing heading → exit 1 listing the available headings; ambiguous heading → exit 1 with a depth-disambiguation hint.
|
|
31
31
|
- Requires a stored markdown source — same no-fallback rule and rebuild flow as `--raw`.
|
|
32
32
|
- Heading detection is markdown-aware: `#` lines inside fenced code blocks or blockquotes are never section boundaries.
|
|
@@ -55,7 +55,7 @@ Replaces the **whole addressed section** (heading line included, subsections inc
|
|
|
55
55
|
|
|
56
56
|
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.
|
|
57
57
|
|
|
58
|
-
Structure guard
|
|
58
|
+
Structure guard, **scoped to the addressed section**: a replacement whose render has fewer `table`/`tr`/heading elements than the old section's render is rejected with 422 naming each shrunk category (old→new counts); structure elsewhere in the document never factors in. Dropping the heading line itself trips the guard too. Pass `--allow-shrink` when the deletion is intentional. `doc append` is pure insertion and is never guarded.
|
|
59
59
|
|
|
60
60
|
Requires a stored markdown source (same rule as `--raw`); errors with the rebuild hint otherwise.
|
|
61
61
|
|
|
@@ -84,7 +84,7 @@ Inserts the new content at the **end of the addressed section** (just before the
|
|
|
84
84
|
| `--file <path>` | string | Content from file (project-local sandbox). |
|
|
85
85
|
| `--if-revision <n>` | int | Only apply if the body is still at revision `n`; 409 + retry hint otherwise. |
|
|
86
86
|
|
|
87
|
-
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). **End-of-document append (no `--section`) does NOT require a stored markdown source
|
|
87
|
+
Same concurrency contract as `doc patch` (always a conditional commit; 409 on conflict). **End-of-document append (no `--section`) does NOT require a stored markdown source**: when the source is missing (web HTML edit / revision restore / legacy doc) it renders the new block to HTML and concatenates it onto the stored HTML body — so one web operation can no longer lock the whole doc against the agent write path. The source stays null (the doc is still HTML-only afterward; `--raw`/`--section`/`doc patch` keep erroring with the rebuild hint). **Section-addressed append (`--section`) still requires a stored source** — it needs the markdown to locate the heading boundary.
|
|
88
88
|
|
|
89
89
|
```bash
|
|
90
90
|
lumo doc append cmd_xxx --section "F 待办队列" --content "- [ ] 评估 XYZ 论文"
|
|
@@ -119,7 +119,7 @@ lumo doc diff cmd_xxx --file docs/live-docs/research-intake-ledger.md
|
|
|
119
119
|
|
|
120
120
|
## `lumo doc rebuild-source <doc>` — regenerate the markdown source from the HTML body
|
|
121
121
|
|
|
122
|
-
The **recovery path for a source-less doc
|
|
122
|
+
The **recovery path for a source-less doc**. When a doc has no stored `sourceMarkdown` — a web HTML-direct edit or revision restore nulled it, or the doc predates source storage — every markdown write path (`--raw`, `--section`, `doc patch`, `doc append --section`, `doc diff`) is locked. This regenerates a valid source by serializing the **stored HTML structure model** back to markdown with a **lossless serializer that round-trips tables/rows/headings** (the default `doc show` render flattens tables, so it was never a safe rebuild base). Only the `sourceMarkdown` column is backfilled; the rendered body is untouched, so the doc reads identically and you just regain the edit base.
|
|
123
123
|
|
|
124
124
|
| Flag | Type | Notes |
|
|
125
125
|
| ------------------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
@@ -127,7 +127,7 @@ The **recovery path for a source-less doc** (LUM-446). When a doc has no stored
|
|
|
127
127
|
| `--force` | boolean | Re-derive even when a source already exists (default: 409 — protects a byte-faithful human source from a downgrade). |
|
|
128
128
|
| `--if-revision <n>` | int | Only apply if the body is still at this revision (from `doc show`). |
|
|
129
129
|
|
|
130
|
-
The rebuild is **structure-guarded** (the same
|
|
130
|
+
The rebuild is **structure-guarded** (the same guard as `doc update`/`doc patch`): if the serializer would drop any table/row/heading, it is rejected with **422** rather than silently committing a flattened source — `--allow-shrink` is the explicit escape hatch. A doc that already has a source is refused **409** unless `--force`.
|
|
131
131
|
|
|
132
132
|
```bash
|
|
133
133
|
lumo doc rebuild-source cmd_xxx # restore a source-less doc; --raw works after
|
|
@@ -28,7 +28,7 @@ not the aligned form (`| col | meaning |` padded so columns line up). Two reas
|
|
|
28
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
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
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
|
|
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; there is **no** server-side table normalization to lean on. The structure guard compares _rendered_ structure and is whitespace-insensitive, so compactness buys local editability, not a verify pass.
|
|
32
32
|
|
|
33
33
|
## `lumo doc create [title] [flags]` — create a new document
|
|
34
34
|
|
|
@@ -75,27 +75,27 @@ The cuid (`cmd_xxx`) is still printed as a stable identifier you can pass back i
|
|
|
75
75
|
|
|
76
76
|
`<doc>` accepts a cuid or a case-insensitive title. Ambiguous titles fail with a candidate list — re-run with the cuid.
|
|
77
77
|
|
|
78
|
-
| Flag | Type | Notes
|
|
79
|
-
| ------------------------ | ------------------- |
|
|
80
|
-
| `--title <text>` | string | New title (cannot be empty).
|
|
81
|
-
| `--content <text>` | string | Replace content (inline).
|
|
82
|
-
| `--file <path>` | string | Replace content from file.
|
|
83
|
-
| (stdin) | — | Pipe to replace content. Empty / whitespace-only stdin (a non-TTY shell with nothing piped — the common agent case) is treated as **no content channel**, not a body clear
|
|
84
|
-
| `--scope <scope>` | enum | `personal` / `workspace`.
|
|
85
|
-
| `--project <ref>` | string | Project name/slug. `--project ""` clears the filing.
|
|
86
|
-
| `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`.
|
|
87
|
-
| `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`.
|
|
88
|
-
| `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20.
|
|
89
|
-
| `--add-tag-id <cuid>` | string (repeatable) | Attach tag by id. Max 20.
|
|
90
|
-
| `--remove-tag <name>` | string (repeatable) | Detach tag by name (find-or-create; an unknown name creates an orphan Tag row before the no-op detach — use `--remove-tag-id` to avoid orphans). Max 20.
|
|
91
|
-
| `--remove-tag-id <cuid>` | string (repeatable) | Detach tag by id. Unknown ids are a no-op. Max 20.
|
|
92
|
-
| `--allow-shrink` | boolean | Let a body update through even when it drops tables/rows/headings versus the stored body (see structure guard below).
|
|
93
|
-
|
|
94
|
-
A **metadata-only update leaves the body untouched
|
|
95
|
-
|
|
96
|
-
Optimistic concurrency
|
|
97
|
-
|
|
98
|
-
**Structure guard
|
|
78
|
+
| Flag | Type | Notes |
|
|
79
|
+
| ------------------------ | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
80
|
+
| `--title <text>` | string | New title (cannot be empty). |
|
|
81
|
+
| `--content <text>` | string | Replace content (inline). |
|
|
82
|
+
| `--file <path>` | string | Replace content from file. |
|
|
83
|
+
| (stdin) | — | Pipe to replace content. Empty / whitespace-only stdin (a non-TTY shell with nothing piped — the common agent case) is treated as **no content channel**, not a body clear. |
|
|
84
|
+
| `--scope <scope>` | enum | `personal` / `workspace`. |
|
|
85
|
+
| `--project <ref>` | string | Project name/slug. `--project ""` clears the filing. |
|
|
86
|
+
| `--tag <name>` | string (repeatable) | **Bulk replace** the tag set by name. Creates tag if missing. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
|
|
87
|
+
| `--tag-id <cuid>` | string (repeatable) | **Bulk replace** the tag set by id. Max 20. Mutually exclusive with `--add-tag*` / `--remove-tag*`. |
|
|
88
|
+
| `--add-tag <name>` | string (repeatable) | Attach tag by name (find-or-create). Max 20. |
|
|
89
|
+
| `--add-tag-id <cuid>` | string (repeatable) | Attach tag by id. Max 20. |
|
|
90
|
+
| `--remove-tag <name>` | string (repeatable) | Detach tag by name (find-or-create; an unknown name creates an orphan Tag row before the no-op detach — use `--remove-tag-id` to avoid orphans). Max 20. |
|
|
91
|
+
| `--remove-tag-id <cuid>` | string (repeatable) | Detach tag by id. Unknown ids are a no-op. Max 20. |
|
|
92
|
+
| `--allow-shrink` | boolean | Let a body update through even when it drops tables/rows/headings versus the stored body (see structure guard below). |
|
|
93
|
+
|
|
94
|
+
A **metadata-only update leaves the body untouched**: when no content channel is supplied (`--title`/`--scope`/`--project`/tag flags only), the body is omitted from the PATCH — it does not get blanked and the structure guard cannot fire. To deliberately clear or replace the body you must supply a content channel explicitly (`--content ""` to clear, which then hits the structure guard as a shrink → pair with `--allow-shrink`).
|
|
95
|
+
|
|
96
|
+
Optimistic concurrency: `--if-revision <n>` only applies the update if the doc body is still at revision `n` (from `doc show`). Mismatch → 409 conflict, nothing written — re-read, rebase, retry. `--if-revision` alone is not an update (still errors "no fields to update"); same for `--allow-shrink`.
|
|
97
|
+
|
|
98
|
+
**Structure guard, built into the server:** a body update whose new render has **fewer `table` / `tr` / heading elements than the stored body** is rejected with **422** before anything is written — the error names each shrunk category with old→new counts (e.g. `table 1→0, tr 4→0`). This is the `verify-live-doc.ts` reconciliation moved into the write path, so table flattening and stale-base section loss fail loudly. When the deletion is intentional, re-run with `--allow-shrink`. On a 422: don't reach for `--allow-shrink` reflexively — first check whether your edit base is stale (`doc show <doc> --raw`) and rebase. Only markdown-path writes are guarded; web-editor edits and `doc sync` (Google authority) are not.
|
|
99
99
|
|
|
100
100
|
`--tag` / `--tag-id` (bulk replace) are mutually exclusive with `--add-tag` / `--add-tag-id` / `--remove-tag` / `--remove-tag-id`. The CLI errors before any network call if both families are mixed. Like `doc create`, `--file` is sandboxed.
|
|
101
101
|
|
|
@@ -88,10 +88,9 @@ detail on demand instead of carrying every memory's content in context.
|
|
|
88
88
|
### `lumo memory sync` (downsync to local Claude Code memory)
|
|
89
89
|
|
|
90
90
|
Writes the team's project memory into the dev's local Claude Code memory store so
|
|
91
|
-
Claude Code's **native recall** surfaces it — the direction
|
|
92
|
-
per-session `<untrusted-team-memory>` injection. This
|
|
93
|
-
|
|
94
|
-
during the transition both paths are active and overlap is expected, not a bug.
|
|
91
|
+
Claude Code's **native recall** surfaces it — the direction chosen over
|
|
92
|
+
per-session `<untrusted-team-memory>` injection. This runs **alongside** the
|
|
93
|
+
existing injection, so during the transition both paths are active and overlap is expected, not a bug.
|
|
95
94
|
|
|
96
95
|
- **Where it writes**: `~/.claude/projects/<encoded-cwd>/memory/` — team files as
|
|
97
96
|
`team/<memoryId>.md` (YAML frontmatter + a `metadata.lumo` ownership marker),
|
|
@@ -100,12 +99,12 @@ during the transition both paths are active and overlap is expected, not a bug.
|
|
|
100
99
|
- **Only touches what it owns**: a file is owned only if its frontmatter carries
|
|
101
100
|
`metadata.lumo.source: team`. Your own hand-written memory files and any
|
|
102
101
|
`MEMORY.md` lines outside the managed block are **never** read or written.
|
|
103
|
-
- **Mirrors the whole project
|
|
102
|
+
- **Mirrors the whole project**: the bundle is the project's **full
|
|
104
103
|
ACTIVE memory set** — every dev in the project gets the same corpus, regardless
|
|
105
104
|
of how many active tasks they hold. There is **no** task-level routing/relevance
|
|
106
105
|
filter and **no** token-budget cap on the corpus at sync time: sync only writes
|
|
107
106
|
local files (no tokens, no conversation), so "which memory is relevant" is left
|
|
108
|
-
to Claude Code's native recall, not decided at sync.
|
|
107
|
+
to Claude Code's native recall, not decided at sync. Judge-used 履历
|
|
109
108
|
still orders the resident index (so the most-proven memories list first) but
|
|
110
109
|
never drops anything. Relevance/budget gating remains only on the **injection**
|
|
111
110
|
path (`lumo task context` / session-start), which does spend tokens.
|
|
@@ -118,7 +117,7 @@ during the transition both paths are active and overlap is expected, not a bug.
|
|
|
118
117
|
upsync candidate (the reverse direction is a later phase).
|
|
119
118
|
- **Reversible**: `--dry-run` prints the add/update/remove/drift-skip plan without
|
|
120
119
|
writing; `--clean` removes every owned file + the managed block (full rollback).
|
|
121
|
-
- **Code-anchor staleness check
|
|
120
|
+
- **Code-anchor staleness check**: after downsync, each synced memory's
|
|
122
121
|
code anchors (file paths, and backtick-wrapped symbols/flags) are checked against
|
|
123
122
|
this repo — resolved against **git-tracked** files (`git ls-files`) + exact
|
|
124
123
|
word-boundary grep, never the dirty working tree. A memory whose **every** anchor
|
|
@@ -142,7 +141,7 @@ store. Each candidate is a JSON file in `<memory-dir>/outbox/` shaped
|
|
|
142
141
|
`{ "category": "convention", "content": { ... } }` (the same per-category content
|
|
143
142
|
shape as `lumo project memory add`). `push` POSTs each to the **existing**
|
|
144
143
|
create-project-memory endpoint, so it runs through the same canonicalize → dedup →
|
|
145
|
-
**reconcile-on-write** pipeline
|
|
144
|
+
**reconcile-on-write** pipeline — no parallel upsync path — and a
|
|
146
145
|
successful push removes the file from the outbox.
|
|
147
146
|
|
|
148
147
|
- **Scope**: structured local memories (the lossless path). A team file you edited
|
|
@@ -157,7 +156,7 @@ successful push removes the file from the outbox.
|
|
|
157
156
|
shared with the team — drop a `{category, content}` JSON in the memory `outbox/`
|
|
158
157
|
and run `lumo memory push` (or just use `lumo project memory add` for a one-off).
|
|
159
158
|
|
|
160
|
-
### Automatic sync/push triggers
|
|
159
|
+
### Automatic sync/push triggers
|
|
161
160
|
|
|
162
161
|
Both directions also fire automatically, best-effort — a failure never blocks the
|
|
163
162
|
session or command:
|
|
@@ -168,7 +167,7 @@ session or command:
|
|
|
168
167
|
a non-empty `<memory-dir>/outbox/*.json` is drained via the same
|
|
169
168
|
create-project-memory pipeline as `lumo memory push`. An **empty outbox does zero
|
|
170
169
|
network** (the fast path checked first), so the high-frequency `stop` hook stays
|
|
171
|
-
cheap.
|
|
170
|
+
cheap.
|
|
172
171
|
- **Env vars**: `LUMO_SYNC_THROTTLE_HOURS` (downsync throttle window, default 12),
|
|
173
172
|
`LUMO_DISABLE_MEMORY_AUTO=1` (disable **both** auto-paths). The `--no-anchor-check`
|
|
174
173
|
flag on `lumo memory sync` is unchanged.
|
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
### Suggest-on-start from local git (no auto-bind)
|
|
6
6
|
|
|
7
|
-
When a session starts **without** a bound task, the `session-start` hook infers a task from local git and **suggests** it — it never binds for you
|
|
7
|
+
When a session starts **without** a bound task, the `session-start` hook infers a task from local git and **suggests** it — it never binds for you.
|
|
8
8
|
|
|
9
9
|
Detection order and rules:
|
|
10
10
|
|
|
11
11
|
- `current branch name` first (e.g. `lumo/LUM-145-...`), then the `most recent commit subjects` (e.g. `... [LUM-145]`), extracting the first task identifier.
|
|
12
|
-
- `prefix-agnostic`
|
|
12
|
+
- `prefix-agnostic` — any team prefix matches (`SPEC-12` as much as `LUM-145`), using the same pattern the server uses to link PR branches to tasks.
|
|
13
13
|
- `well-known acronym-number tokens` (`UTF-8`, `SHA-256`, `ISO-8601`, …) are skipped, never suggested.
|
|
14
14
|
|
|
15
15
|
On a hit it prints one suggestion line and stops — the session stays **unbound**, no context injected yet:
|
|
@@ -28,7 +28,7 @@ You see a suggestion line. Confirm the inferred task is the one the user wants,
|
|
|
28
28
|
|
|
29
29
|
### Layer 2 project-memory review at session start
|
|
30
30
|
|
|
31
|
-
When the session is bound, session-start may inject a **"🆕 Review needed: project memories auto-consolidated by the previous session"** section alongside the memory / PR-review blocks
|
|
31
|
+
When the session is bound, session-start may inject a **"🆕 Review needed: project memories auto-consolidated by the previous session"** section alongside the memory / PR-review blocks. It lists the **PROJECT-scope** memories that the member's **immediately-preceding session** auto-consolidated (Layer 2 runs asynchronously when a task is marked `done`). Each item shows its `id`.
|
|
32
32
|
|
|
33
33
|
- **Why async / next-session:** Layer 2 promotions land after the task hits DONE, so they surface at the _next_ session-start, when they've definitely landed.
|
|
34
34
|
- **Show-once:** the section appears only at the session immediately following the one that produced the memories. It does **not** re-nag later, so act now or it scrolls off.
|
|
@@ -91,11 +91,11 @@ What it does:
|
|
|
91
91
|
- Reads `CLAUDE_CODE_SESSION_ID` from the environment (Claude Code sets it automatically); errors out if unset — must run from inside a Claude Code session.
|
|
92
92
|
- Calls `POST /api/sessions/<session_id>/bind-task`, which sets the Session row's `taskId` and re-tags previously-untagged HookEvent rows in this session.
|
|
93
93
|
- The binding lives entirely on the server (`Session.taskId`); subsequent hooks read it back via the session row. The CLI keeps no local sentinel.
|
|
94
|
-
- Prints the task's **acceptance contract** (`## Acceptance criteria (contract)
|
|
94
|
+
- Prints the task's **acceptance contract** (`## Acceptance criteria (contract)`) right after the bind confirmation — or, when a still-open task has none, the draft reminder to draft 3–7 criteria before the first line of code (see [criteria.md](criteria.md)). The same section is auto-injected at session start when already bound (highest priority in the injection budget, ahead of memory).
|
|
95
95
|
|
|
96
96
|
After attaching, always run `lumo task context <identifier>` to load the task background.
|
|
97
97
|
|
|
98
|
-
#### Auto-downsync on attach
|
|
98
|
+
#### Auto-downsync on attach
|
|
99
99
|
|
|
100
100
|
A successful `session attach` also runs a **best-effort team-memory downsync** for the bound task's project — the same work as `lumo memory sync` (including the P4b code-anchor staleness check), landing the team's memory in your local Claude Code memory store without a separate command.
|
|
101
101
|
|
|
@@ -105,7 +105,7 @@ A successful `session attach` also runs a **best-effort team-memory downsync** f
|
|
|
105
105
|
- Manual `lumo memory sync` stays unthrottled.
|
|
106
106
|
- `LUMO_DISABLE_MEMORY_AUTO=1` turns off the auto-downsync (and the hook auto-upsync — see [memory.md](memory.md)) entirely; `--no-anchor-check` on `lumo memory sync` is unchanged.
|
|
107
107
|
|
|
108
|
-
#### Lifetime lock
|
|
108
|
+
#### Lifetime lock
|
|
109
109
|
|
|
110
110
|
`Session.taskId` is **write-once**. Re-attaching to the **same** task is always a no-op re-bind (idempotent, re-emits context). Attaching to a **different** task is refused with HTTP 409 — the server returns `{ error, currentTaskIdentifier, currentTaskTitle }` and the CLI prints:
|
|
111
111
|
|
|
@@ -135,13 +135,13 @@ Prints which task the current Claude Code session is bound to, or "(no task)" if
|
|
|
135
135
|
|
|
136
136
|
The user asks "which task am I on", "what's this session bound to", or you need to decide whether to suggest `session attach` for a mentioned task ID.
|
|
137
137
|
|
|
138
|
-
### Automatic end-of-session housekeeping (no command
|
|
138
|
+
### Automatic end-of-session housekeeping (no command)
|
|
139
139
|
|
|
140
|
-
The old end-of-session command was **removed
|
|
140
|
+
The old end-of-session command was **removed**. The three passes it ran interactively now happen **automatically server-side** — all evidence-gated, best-effort, and silent. There is nothing for the agent to run or confirm. Two fire when the bound task reaches **DONE** (`lumo task update <id> --status done`, which threads `CLAUDE_CODE_SESSION_ID` so attribution lands); one runs continuously off the failure/progress hooks.
|
|
141
141
|
|
|
142
142
|
**1. Layer-1 memory curation (on DONE).** An LLM judge reviews the Layer-1 memories each of the task's sessions recorded, against that session's event log, and **soft-invalidates only the clearly-wrong / self-contradictory ones**: the row flips to `INVALIDATED` and is excluded from injection but **kept for audit — never hard-deleted**. **Uncertain memories are left untouched** (the judge defaults to keeping). Promotion to project scope is **not** done here — that stays with the Layer-2 flow (surfaced at the next session-start, see above).
|
|
143
143
|
|
|
144
|
-
**2. Fragment-usage audit (
|
|
144
|
+
**2. Fragment-usage audit (on DONE).** An LLM judge sees the fragments this session consumed (its lineage edges) plus the session's event log and votes which were **actually used**:
|
|
145
145
|
|
|
146
146
|
- confidently-used edges → `used=true`
|
|
147
147
|
- confidently-unused → `used=false`
|
|
@@ -149,7 +149,7 @@ The old end-of-session command was **removed in LUM-544**. The three passes it r
|
|
|
149
149
|
|
|
150
150
|
Already-voted sessions are skipped; a cron backstop drains any backlog. **Why:** upgrades the flywheel signal from "co-loaded" (constant) to "actually used" (discriminative); `task context` then prefers each fragment's usage-based merge rate, falling back to the presence rate when usage samples are thin.
|
|
151
151
|
|
|
152
|
-
**3. Blocked-tag automation (
|
|
152
|
+
**3. Blocked-tag automation (server-side).** When a session crosses the same-tool failure threshold (**≥ 3** same-type failures, aggregated from `POST_TOOL_USE_FAILURE` grouped by tool name + `STOP_FAILURE` turn-level failures), the server **auto-applies the shared `blocked` tag** to the bound task. This inverts the old manual gate — no prompt, no human in the loop — and is safe via three safeguards:
|
|
153
153
|
|
|
154
154
|
- **idempotent:** at most one active auto-block per task, so re-crossing is a no-op.
|
|
155
155
|
- **auto-untag on progress:** the next observable progress (a successful tool call or a non-failure turn end) removes the tag.
|
|
@@ -13,7 +13,7 @@ lumo task context LUM-42
|
|
|
13
13
|
The command prints a markdown document to stdout with these sections, in order:
|
|
14
14
|
|
|
15
15
|
1. **Task header** — identifier, title, status, description.
|
|
16
|
-
2. **`## Acceptance criteria (contract)`**
|
|
16
|
+
2. **`## Acceptance criteria (contract)`** — shown right after the header. Each line `[MACHINE|HUMAN] statement`, with a `↳ check:` line for MACHINE checkpointers; HUMAN_EDIT / REVIEW_ADDED provenance tagged inline. A still-open task with none shows a draft reminder instead — draft 3–7 criteria **before writing code** (see [criteria.md](criteria.md)).
|
|
17
17
|
3. **Memory section** — cross-session learnings; trusted background context that persists, so you avoid re-learning decisions/constraints.
|
|
18
18
|
4. **Inline source cards** — Slack / web / Figma / artifacts / documents / comments / Pull Requests (see "Context Retrieval" below).
|
|
19
19
|
5. **`## PR review todos`** — mirrored PR review comments as a checkbox todo list. Each line-level comment shows `` `file:line` `` + reviewer's ask + GitHub comment link; each `changes_requested` review summary shows "🛑 Changes requested (whole PR)". Present only when the task's PR(s) have review comments. Each unchecked box is a TODO: resolve it, then reply on the PR (a Lumo comment mirrors back to GitHub).
|
|
@@ -33,7 +33,7 @@ The command prints a markdown document to stdout with these sections, in order:
|
|
|
33
33
|
|
|
34
34
|
## Context Retrieval (full text on demand)
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
Context injection is split into tiers: `lumo task context <LUM-N>` emits a **cheap inline card** per source instead of dumping full bodies. Slack/docs/artifacts/comments get an **LLM summary**; web/Figma/PR get **metadata only**. Each card ends with the **retrieval command** for the heavy content.
|
|
37
37
|
|
|
38
38
|
Run the matching command below when the card isn't enough. Pass the same `LUM-N` plus the id the card shows for that source:
|
|
39
39
|
|
|
@@ -47,7 +47,7 @@ Run the matching command below when the card isn't enough. Pass the same `LUM-N`
|
|
|
47
47
|
|
|
48
48
|
All five are **read-only** (no live Slack/GitHub/Figma calls except the web body fetch). Web/Figma/PR are v1 metadata-degraded: they print a `note:` saying live content needs an external integration.
|
|
49
49
|
|
|
50
|
-
**Output budget
|
|
50
|
+
**Output budget:** the whole `task context` handoff is capped to the output-token budget (25,000 tokens). On overflow, output is truncated and ends in a pointer to the precise sub-commands (`lumo task status` / `task comments list --full` / `task lineage` / `doc show`) to pull any dropped section just-in-time.
|
|
51
51
|
|
|
52
52
|
### `lumo task slack show <identifier> <contextId>` — full Slack thread snapshot
|
|
53
53
|
|
|
@@ -77,7 +77,7 @@ lumo task figma context LUM-42 cfl_abc123
|
|
|
77
77
|
|
|
78
78
|
Prints the thread: each comment as `author · createdAt` then its plain-text body (bodies stored as HTML, stripped to text). Replies indent two spaces under their parent. Author falls back to `unknown`. No comments prints `(no comments)`.
|
|
79
79
|
|
|
80
|
-
- `lumo task comments list LUM-42` — capped to the output budget (
|
|
80
|
+
- `lumo task comments list LUM-42` — capped to the output budget (25,000 tokens; every printed line spends from your context). On overflow, truncates and ends in a fetch-more pointer: `… +N more comments not shown (output capped at 25,000 tokens) — read the whole thread with: lumo task comments list <id> --full`.
|
|
81
81
|
- `lumo task comments list LUM-42 --full` — every comment, uncapped. Use only when you actually need the whole thread.
|
|
82
82
|
|
|
83
83
|
```bash
|
|
@@ -104,13 +104,13 @@ lumo task lineage LUM-42 # per-session causal trail + cost
|
|
|
104
104
|
lumo task lineage LUM-42 --signal # append workspace-level usage signal-health
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
-
- `lumo task lineage <id> --signal` — appends the workspace-level usage signal-health block. Used-vs-base merge rate uses iteration-taint fold (send-back / reopen / PR-close = negative class even if later merged); shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet. The block ends with a workspace-wide disclosure funnel (same format as the totals funnel below, aggregated over **all** workspace edges, not just this task) including the same `· ~T tokens saved` suffix when token data exists
|
|
107
|
+
- `lumo task lineage <id> --signal` — appends the workspace-level usage signal-health block. Used-vs-base merge rate uses iteration-taint fold (send-back / reopen / PR-close = negative class even if later merged); shows negative-class size per side; prints "metric cannot discriminate" when no failure outcomes exist yet. The block ends with a workspace-wide disclosure funnel (same format as the totals funnel below, aggregated over **all** workspace edges, not just this task) including the same `· ~T tokens saved` suffix when token data exists.
|
|
108
108
|
|
|
109
109
|
Output sections:
|
|
110
110
|
|
|
111
|
-
- **Totals banner** — distinct sessions, fragment count, edge count, total tokens (input/output/cache split), loops, and outcome distribution. After the outcome summary, one funnel line: `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`. Impressions = edge count; INDEX% and used% are over impressions; pull% is over INDEX only (FULL fragments have no pull opportunity). Divide-by-zero guarded (zero impressions or zero INDEX renders `0%`). With per-fragment token weights collected
|
|
111
|
+
- **Totals banner** — distinct sessions, fragment count, edge count, total tokens (input/output/cache split), loops, and outcome distribution. After the outcome summary, one funnel line: `- Disclosure funnel: N impressions · M INDEX (X%) · K pulled (Y% of INDEX) · J used (Z%)`. Impressions = edge count; INDEX% and used% are over impressions; pull% is over INDEX only (FULL fragments have no pull opportunity). Divide-by-zero guarded (zero impressions or zero INDEX renders `0%`). With per-fragment token weights collected, appends `· ~T tokens saved` = Σ(fullTokens − indexTokens) over un-pulled INDEX edges (the token cost index-only injection avoided); omitted cleanly when no edge carries token data (older edges predate the columns).
|
|
112
112
|
- **One block per session** — the group's cost shown **once** (token/loop), the date it consumed context, then each fragment as `[OUTCOME] TYPE — <source label>` plus a disclosure suffix: `· INDEX pulled` (INDEX, `pulledAt` set) / `· INDEX not-pulled` (INDEX, never pulled) / `· FULL` (injected in full at session-start). Per-group outcome summary follows.
|
|
113
|
-
- **Top operations by token cost
|
|
113
|
+
- **Top operations by token cost** — the totals also append a per-task Top-5 of the most expensive tools by attributed token cost (`<tool> — N tokens`), ending with `(full breakdown: lumo cost --task <id>)`. Omitted when no per-operation cost has been attributed yet.
|
|
114
114
|
|
|
115
115
|
Cost is attributed once per session (a session that injected many fragments is not double-counted). Fragment ids are canonical — MEMORY fragments survive consolidation drift. **Cold start:** a task with no edges prints a friendly note (lineage is captured when a session-bound run consumes the task's context), not an error.
|
|
116
116
|
|
|
@@ -99,18 +99,18 @@ The `Tags:` line is omitted when the resulting tag set is empty.
|
|
|
99
99
|
|
|
100
100
|
The server's transition matrix (`lib/task/state-machine.ts`):
|
|
101
101
|
|
|
102
|
-
| From | Allowed targets
|
|
103
|
-
| ----------- |
|
|
104
|
-
| TODO | IN_PROGRESS, IN_REVIEW, DONE
|
|
105
|
-
| IN_PROGRESS | TODO, IN_REVIEW, DONE
|
|
106
|
-
| IN_REVIEW | TODO, IN_PROGRESS, DONE
|
|
107
|
-
| DONE | TODO, IN_PROGRESS (reopen
|
|
102
|
+
| From | Allowed targets |
|
|
103
|
+
| ----------- | ------------------------------------- |
|
|
104
|
+
| TODO | IN_PROGRESS, IN_REVIEW, DONE |
|
|
105
|
+
| IN_PROGRESS | TODO, IN_REVIEW, DONE |
|
|
106
|
+
| IN_REVIEW | TODO, IN_PROGRESS, DONE |
|
|
107
|
+
| DONE | TODO, IN_PROGRESS, IN_REVIEW (reopen) |
|
|
108
108
|
|
|
109
109
|
Practical rules:
|
|
110
110
|
|
|
111
|
-
- **One call suffices.** `--status done` straight from TODO or IN_PROGRESS is legal — never walk `in_progress → in_review → done` as a ritual
|
|
111
|
+
- **One call suffices.** `--status done` straight from TODO or IN_PROGRESS is legal — never walk `in_progress → in_review → done` as a ritual; it just wastes calls.
|
|
112
112
|
- **Under the verify flow you don't set `in_review`/`done` at all** — `lumo verify` moves the task to IN_REVIEW on all-pass and the DONE adjudication is human-only.
|
|
113
|
-
- **DONE
|
|
113
|
+
- **A DONE task can be reopened** — to IN_REVIEW, IN_PROGRESS, or TODO. Reopening is a plain status change and does not alter any recorded acceptance verdict; verdict adjudication is human-only and has no CLI path. To attach context without reopening, `lumo task comment` works.
|
|
114
114
|
|
|
115
115
|
### When to suggest `task update`
|
|
116
116
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# lumo verify — machine verification loop
|
|
2
2
|
|
|
3
|
-
`lumo verify` is the machine half of the acceptance system
|
|
4
|
-
LUM-343): it executes every **MACHINE** criterion's checkpointer in the local
|
|
3
|
+
`lumo verify` is the machine half of the acceptance system: it executes every **MACHINE** criterion's checkpointer in the local
|
|
5
4
|
repo, POSTs one structured PASS/FAIL verdict per criterion, and prints what to
|
|
6
5
|
do next. Execution is on the client; adjudication is server-side — round
|
|
7
6
|
numbering, the **3-round cap**, escalation, and the **IN_REVIEW** transition all
|
|
@@ -14,11 +13,22 @@ touching its status — run `lumo verify`.** The loop replaces "I read the code
|
|
|
14
13
|
and it looks done" with executed evidence.
|
|
15
14
|
|
|
16
15
|
```bash
|
|
17
|
-
lumo verify
|
|
18
|
-
lumo verify LUM-42
|
|
19
|
-
lumo verify --timeout 900
|
|
16
|
+
lumo verify --note "implemented X in foo.ts because Y; tests + tsc pass" # session-bound task
|
|
17
|
+
lumo verify LUM-42 --note "…" # explicit task (overrides the session binding)
|
|
18
|
+
lumo verify --note "…" --timeout 900 # per-checkpointer timeout in seconds (default 600)
|
|
20
19
|
```
|
|
21
20
|
|
|
21
|
+
**`--note` is required (LUM-597)** whenever a round will actually be posted (the
|
|
22
|
+
task has MACHINE criteria). It is your one-line self-report — what you did and
|
|
23
|
+
why it is ready ("我改了 X,因为 Y 可验收"). When the round all-passes and the
|
|
24
|
+
task flips to IN_REVIEW, the note is frozen as the task's **claim** (provenance
|
|
25
|
+
`AGENT` — the汇报者's own voice, not a summarizer paraphrase) and is checked
|
|
26
|
+
against the diff for faithfulness. Capturing the claim is deterministic ("不填不
|
|
27
|
+
让 post"); whether it is _truthful_ is the faithfulness audit's job — so don't
|
|
28
|
+
under- or over-state it. A missing/blank note is refused **before** the round is
|
|
29
|
+
posted, so no round is burned; just re-run with one. (An old CLI that omits it
|
|
30
|
+
still verifies — the claim degrades to the synthesized run-summary fallback.)
|
|
31
|
+
|
|
22
32
|
## What one round does
|
|
23
33
|
|
|
24
34
|
1. Loads the task's acceptance contract and picks out MACHINE criteria.
|
|
@@ -29,11 +39,11 @@ lumo verify --timeout 900 # per-checkpointer timeout in seconds (default 600)
|
|
|
29
39
|
TaskActivity event.
|
|
30
40
|
4. Prints the round outcome:
|
|
31
41
|
|
|
32
|
-
| Round outcome | Effect
|
|
33
|
-
| ------------------------- |
|
|
34
|
-
| **All PASS** | Task transitions to **IN_REVIEW** (existing state machine + TASK_IN_REVIEW notification)
|
|
35
|
-
| **Any FAIL** | Task status untouched; unmet criteria printed as next actions (statement, checkpointer, failure tail)
|
|
36
|
-
| **Round 3 still failing** | Loop escalates: a human is notified (AGENT_VERIFY, requires action); further `lumo verify` rounds are rejected with **409**
|
|
42
|
+
| Round outcome | Effect | What to do |
|
|
43
|
+
| ------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
|
|
44
|
+
| **All PASS** | Task transitions to **IN_REVIEW** (existing state machine + TASK_IN_REVIEW notification); the `--note` self-report is frozen as the task's claim (source `AGENT`) | **Stop here.** Human adjudication + any HUMAN criteria take over; **never set DONE yourself** |
|
|
45
|
+
| **Any FAIL** | Task status untouched; unmet criteria printed as next actions (statement, checkpointer, failure tail) | Fix and re-run |
|
|
46
|
+
| **Round 3 still failing** | Loop escalates: a human is notified (AGENT_VERIFY, requires action); further `lumo verify` rounds are rejected with **409** | **Stop retrying**; fix only what the human directs |
|
|
37
47
|
|
|
38
48
|
Exit code 0 = all passed (or nothing to run); 1 = failures, escalation, or
|
|
39
49
|
errors.
|
|
@@ -57,7 +67,7 @@ errors.
|
|
|
57
67
|
| **HUMAN-only contract** (zero MACHINE criteria) | Nothing to run; CLI says so and suggests `lumo task update <id> --status in_review` for human review. No server write happens. |
|
|
58
68
|
| **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds. |
|
|
59
69
|
| **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round. |
|
|
60
|
-
| **Session bound to a different task**
|
|
70
|
+
| **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
|
|
61
71
|
| **Provably-unbound session** | Response carries `bindingAdvisory: 'unbound'`; prints `⚠ Working unbound — this verify ran from a Claude Code session not attached to the task.` Recorded as a `SESSION_BINDING_MISSING` boundary crossing (visible in `lumo task status` open crossings). Run `lumo session attach <LUM-N>` before the next verify. |
|
|
62
72
|
| **Unconfirmed session binding** | `bindingAdvisory: 'unconfirmed'` → softer advisory `⚠ Could not confirm this session is attached to the task.` Same remediation: `lumo session attach <LUM-N>`. |
|
|
63
73
|
|
|
@@ -85,8 +95,7 @@ clauses that can never be found wanting.
|
|
|
85
95
|
|
|
86
96
|
## lumo task status — the read half (self-check entry point)
|
|
87
97
|
|
|
88
|
-
`lumo task status [task] [--json]` is the read-only counterpart of the loop
|
|
89
|
-
(LUM-344): pure read, milliseconds, no LLM, never writes — running it costs
|
|
98
|
+
`lumo task status [task] [--json]` is the read-only counterpart of the loop: pure read, milliseconds, no LLM, never writes — running it costs
|
|
90
99
|
nothing and burns no round. Defaults to the session-bound task; an explicit
|
|
91
100
|
identifier overrides.
|
|
92
101
|
|
|
@@ -111,31 +120,35 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
111
120
|
### What it prints
|
|
112
121
|
|
|
113
122
|
- **Header** — task identifier/title/status + `verification round N/3` (round 0 = never verified) + an escalation warning when the machine loop is exhausted.
|
|
114
|
-
- **
|
|
123
|
+
- **Claim vs verification** — 规律 2 声称vs核验: the headline contrast, printed right after the header (whenever the contract exists), so the report shows **both** columns instead of only the verification one. Two sides:
|
|
124
|
+
- **Claim** — what the agent _says_ it did: an **unverified self-report** (`agent self-report · estimated, not verification`), estimate-tier provenance (估, not 测). Sourced by layering (LUM-597, preference `AGENT > RUN_SUMMARY > DIGEST > null`): if you supplied a `lumo verify --note`, that **own self-report** is the claim (`↳ source: agent self-report (verify --note)`) — the汇报者's voice, what faithfulness judges. Absent a self-report (old CLI / non-verify path) it degrades to the summarizer paraphrase, labelled honestly (`↳ source: synthesized run summary (no self-report)`). **Fail-closed**: when only a raw STOP turn digest exists it prints `generating — the formal run summary is still being synthesized` and **withholds the raw digest**; with no material at all it prints `not generated yet — …`, never a fabricated claim.
|
|
125
|
+
- **Verification** — what was actually _confirmed_ (measured): the machine-verification rollup `N machine-verified / M human override (of T MACHINE criteria)` over the active MACHINE criteria (relocated here from its old standalone line under `Criteria`), plus `X of Y criteria met by their latest verdict`. **Fail-closed**: before any round runs it prints `no verification has run yet — the claim is unconfirmed` rather than implying a pass.
|
|
126
|
+
- **Faithfulness** — the third state over the two columns: whether the CLAIM itself is _true_, read from the persisted faithfulness verdict (no LLM at read time), printed as `▸ Faithfulness — does the claim match the delivery`. One of `faithful` / `overstated` (claim says more than the diff/PR shows) / `under-reported` (claim says less) / `unjudgeable`, with an `· evidence: PR #N · <sha>` pointer when the judge cited one. **Fail-closed**: `not yet judged` (no verdict — distinct from a real `unjudgeable` verdict) and `… (stale — re-checked on the next batch)` (judged against an older claim snapshot, the verdict still shown) are surfaced explicitly. Omitted only against an older server that doesn't emit it.
|
|
127
|
+
- Carried in `--json` as `claim { text, source }` (`source: 'AGENT' | 'RUN_SUMMARY' | 'DIGEST' | null`; `AGENT` = the verify self-report, `RUN_SUMMARY`/`DIGEST` = the synthesized fallback; `text: null` = none generated) and `faithfulness { state, verdict, evidence, diffShas, prNumbers, judgedAt, judgedClaimAt }` (`state` adds `PENDING`/`STALE` over the four verdicts). Omitted only against an older server that doesn't emit the field. The machine-verification rollup is still carried top-level as `machineVerification`.
|
|
115
128
|
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (failure tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
116
|
-
- A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict
|
|
117
|
-
- A verdict's **evidence is drillable
|
|
118
|
-
- A pass can carry a **`⚠ pre-edit version`** note
|
|
129
|
+
- A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict: `· machine-verified` when a checkpointer actually passed it (even after a human later signs the task off), or `· human override (no machine pass)` when it passes only on a human sign-off with no machine run underneath. This keeps the terminal honest with web — a machine-verified criterion that a human co-signed no longer reads as a plain human pass.
|
|
130
|
+
- A verdict's **evidence is drillable**, rendered as an indented `↳ evidence:` line under the verdict (PASS _and_ FAIL) instead of the inert raw pointer that used to ride the verdict line — so a conclusion points at real proof you can act on, not just the `check:` command: a `cmd:` pointer prints the actual command + exit code (`ran \`…\` → exit N · re-run to reproduce`), a `file:`pointer prints a terminal-clickable`path:line`, and a `commit:` pointer prints a navigable web URL (`<repo>/commit/<hash>`, resolved from the local git `origin`remote) or a`git show <hash>`fallback when no remote resolves. A criterion that **requires evidence but has none recorded yet** (e.g. a HUMAN evidence criterion before sign-off) renders an explicit`↳ evidence: pending — no reference recorded yet`(fail-closed) instead of a bare, dead`[evidence]` tag.
|
|
131
|
+
- A pass can carry a **`⚠ pre-edit version`** note: the criterion was changed after that verdict (reworded, or its checkpointer was swapped so the recorded evidence ran a different command). The pass still counts as met (a stale pass does not block DONE — render-only signal), but it vouches for an older version — **re-run `lumo verify` to re-confirm against the current criterion.** This is the habit whenever you edit a MACHINE criterion's checkpointer mid-task: change the check, then re-verify so the green is honest.
|
|
119
132
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
120
133
|
- **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
|
|
121
|
-
- **Cost**
|
|
122
|
-
- **Struggle / rework / outstanding**
|
|
134
|
+
- **Cost** — 规律 1: the costs a human should weigh, on the same report as the verdict instead of scattered across the web delivery card and `task lineage`. Three lines: **Tokens** (total input+output+cache across the task's sessions), **Active time** (non-idle agent seconds — Σ per-turn `STOP − prompt`), and **Rework rounds** (verify rounds that recorded a FAIL). Read from the **same** server-side source the web delivery card consumes (`retrospectiveRepository.loadActuals`), so the two reports cannot drift. Token cost is **fail-closed**: when no session usage was recorded it prints `Tokens: not recorded (no session usage captured)`, kept distinct from a measured `0` (没测到 vs 花了0). Carried in `--json` as `cost { tokenCost, activeTimeSec, reworkRounds }` (`tokenCost: null` = not measured). Omitted only against an older server that doesn't emit the field.
|
|
135
|
+
- **Struggle / rework / outstanding** — the anti-mum-and-deaf block: **always printed when the contract exists, even on a clean 0-unmet task** so a passing task still shows its scars instead of wiping them to a single PASS count. Lists, when present:
|
|
123
136
|
- **rework rounds** — verify rounds that had a FAIL;
|
|
124
137
|
- **send-backs** — criteria sent back by a human/agent verdict (a MACHINE verify-loop FAIL is not a 打回), with their open/resolved lifecycle, preserved even for since-removed criteria;
|
|
125
138
|
- **leftover follow-ups** — criteria whose latest verdict is `PASS_WITH_FOLLOWUP`;
|
|
126
|
-
- **PR iterations** — when the task has >1 PR (the dominant rework signal when the verify loop ran once but the work churned across many follow-up PRs
|
|
139
|
+
- **PR iterations** — when the task has >1 PR (the dominant rework signal when the verify loop ran once but the work churned across many follow-up PRs); a single PR is the happy path and is not flagged;
|
|
127
140
|
- **reopens** — backward `IN_REVIEW/DONE → IN_PROGRESS/TODO` transitions (from the `STATUS_CHANGED` log): the task reached review/done and got bounced, a rework that leaves no FAIL verdict.
|
|
128
141
|
|
|
129
142
|
When the trail is genuinely empty it states the **basis** (`None recorded — N rounds run, 0 FAIL, no send-backs, no reopens, no leftover follow-ups`); when nothing has been verified yet it says so (`No verification has run yet — cannot confirm there were no difficulties`) rather than rendering an implicitly-clean slate. Carried in `--json` as `struggleTrail` (incl. `pullRequests` + `reopens`).
|
|
130
143
|
|
|
131
|
-
- **Trend**
|
|
144
|
+
- **Trend** — 规律 7 趋势非快照: the _movement_ of the key quantities across the task's attempts, not a single snapshot. Where History/Cost/Struggle list current values, this shows direction: **Pass rate** across verification rounds (`r1 60% → r2 100% (↑ +40pts)`), **Cost/session** across the task's sessions (`4.2K → 1.1K tokens (↓), 5.3K total` — per-session spend from the same source as the **Cost** total, so the trajectory's points sum to it), and **Rework** accrual (`3 accrued — 1 FAIL round, 1 reopen, +1 PR cycle (↑ from 0)`). **Honest about a single point:** with only one round and one session every quantity is one data point, so it prints `Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions)` rather than drawing a fake arrow off one value. When nothing was verified and no cost was measured it says `No verification rounds or measured cost yet — nothing to trend`. Carried in `--json` as `trend { passRate[], cost[], rework{} }`. Omitted only against an older server.
|
|
132
145
|
|
|
133
146
|
- **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included). This list IS the plan — recomputed from the event log on every read, never maintained separately. Empty + rounds recorded = awaiting human adjudication.
|
|
134
|
-
- **Open boundary crossings**
|
|
135
|
-
- **Read-only awareness** — this surfaces crossings detected elsewhere
|
|
136
|
-
- **The check fails closed
|
|
147
|
+
- **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count, then one line per crossing `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first), each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
|
|
148
|
+
- **Read-only awareness** — this surfaces crossings detected elsewhere; there is no CLI path to disposition or clear one. Disposition stays web + human-only: an agent/CLI bearer cannot clear its own crossing from the terminal.
|
|
149
|
+
- **The check fails closed:** if the crossings read itself errors (network / server / parse), the block prints `⚠ Boundary-crossing check failed (network/server error) — could not confirm whether any are undispositioned` instead of staying silent. Silence means a successful read with zero open crossings, never a failed check — a hiccup can no longer masquerade as "all clear".
|
|
137
150
|
|
|
138
|
-
### Responding to an open crossing — `lumo crossing explain`
|
|
151
|
+
### Responding to an open crossing — `lumo crossing explain`
|
|
139
152
|
|
|
140
153
|
When `lumo task status` surfaces an OPEN crossing you believe is a false positive
|
|
141
154
|
— or you simply want to leave a rationale for the human reviewer — append a
|
|
@@ -148,7 +161,7 @@ lumo crossing explain <id> --note "this was a generated fixture, not a hand-edit
|
|
|
148
161
|
This is the **inverse** of dispositioning, but it is the agent/CLI path
|
|
149
162
|
(bearer-only; a clerk/human caller is refused). Behavior:
|
|
150
163
|
|
|
151
|
-
- it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only
|
|
164
|
+
- it can **only append** an append-only note — it **never clears the crossing or unblocks Done** (disposition stays web + human-only);
|
|
152
165
|
- the note is shown to the human reviewer at disposition time, kept for later review, and explicitly labeled _agent self-report · unverified_;
|
|
153
166
|
- `<id>` must be a crossing on the **session-bound task** (resolved from `$CLAUDE_CODE_SESSION_ID`; cross-task targets and unbound/mismatched sessions are rejected);
|
|
154
167
|
- earlier explanations are immutable — a correction is a new note.
|
|
@@ -159,16 +172,16 @@ This is the **inverse** of dispositioning, but it is the agent/CLI path
|
|
|
159
172
|
`1`). The schema is versioned: breaking shape changes bump the major; additive
|
|
160
173
|
fields don't. Pin on `version` when scripting against it.
|
|
161
174
|
|
|
162
|
-
- each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it
|
|
175
|
+
- each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it);
|
|
163
176
|
- the payload carries a top-level `machineVerification` aggregate `{ total, machineVerified, humanOverridden }` over the active MACHINE criteria — read these, not `latestVerdict` alone, to tell a machine-verified criterion from a human override;
|
|
164
|
-
- open boundary crossings ride along as an additive top-level `openCrossings`, each entry `{ id, category, severity, detail, attribution }` where `attribution` is `{ workspaceMemberId, sessionId, agent, worktreeBranch, model }` with every field nullable — null = unknown, never fabricated
|
|
165
|
-
- **`openCrossings` is `null` when the crossings check failed
|
|
177
|
+
- open boundary crossings ride along as an additive top-level `openCrossings`, each entry `{ id, category, severity, detail, attribution }` where `attribution` is `{ workspaceMemberId, sessionId, agent, worktreeBranch, model }` with every field nullable — null = unknown, never fabricated; the array length is the count. Same read-only awareness, no write path;
|
|
178
|
+
- **`openCrossings` is `null` when the crossings check failed** — distinct from `[]` (a successful read with zero open crossings). Script consumers must treat `null` as "unknown / could not confirm", **not** "safe".
|
|
166
179
|
|
|
167
180
|
`status` reads; `verify` judges. Running status never starts a round, never
|
|
168
181
|
escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
|
|
169
182
|
all-pass, human-only DONE) live entirely in `lumo verify` and the server.
|
|
170
183
|
|
|
171
|
-
## lumo verdict — the three verdict channels
|
|
184
|
+
## lumo verdict — the three verdict channels
|
|
172
185
|
|
|
173
186
|
`lumo verify` is the MACHINE channel. `lumo verdict` covers the other two — the
|
|
174
187
|
HUMAN pass and the AGENT send-back — under one red line: **no passing data row is
|
|
@@ -36,7 +36,7 @@ Errors if the target dir already exists; reuses the branch if it already exists
|
|
|
36
36
|
|
|
37
37
|
- **`prisma generate` clobbers all worktrees.** The generated client lives in the shared (symlinked) `node_modules`, so a `generate` in one worktree overwrites the client every parallel worktree depends on. Verify with jest (SWC mocks Prisma); do `generate + tsc` atomically once at the end.
|
|
38
38
|
- **Run jest from the worktree root** (`cd` in first). `cli/` has no jest config; running from the main checkout hits the `cli/package.json` haste collision and silently runs the wrong tests.
|
|
39
|
-
- **Husky hooks are copied in for you.** Husky owns hooks via `core.hooksPath = .husky/_`, resolved relative to each worktree's root. That `_` shim is **untracked** (regenerated on the main checkout's `npm install`/`prepare`), so a fresh worktree would lack it and git would **silently skip every hook** — pre-commit (lint-staged) and commit-msg (
|
|
39
|
+
- **Husky hooks are copied in for you.** Husky owns hooks via `core.hooksPath = .husky/_`, resolved relative to each worktree's root. That `_` shim is **untracked** (regenerated on the main checkout's `npm install`/`prepare`), so a fresh worktree would lack it and git would **silently skip every hook** — pre-commit (lint-staged) and commit-msg (drift-check). With no GitHub CI, husky is the only deterministic quality gate, so `add` copies `.husky/_` in (copy, not symlink). If the main checkout has no `.husky/_`, `add` warns you to `npm install` there rather than skipping silently.
|
|
40
40
|
- **Never `npm install` / `npm ci` inside a worktree.** npm doesn't respect the `node_modules` symlink — it deletes it and reifies a full standalone tree (~1 min, shared prisma-client gone). Install only in the main checkout, then re-create the symlink if npm replaced it. (Older npm could plant a self-referential `node_modules/node_modules` that hard-panics Turbopack's `next build`; the `prebuild`/`predev`/`preanalyze` guard `scripts/fix-nodemodules-selflink.ts` removes it, but this rule avoids the mess.)
|
|
41
41
|
|
|
42
42
|
## `lumo worktree rm <LUM-N>`
|
|
@@ -45,15 +45,14 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
45
45
|
pushOpenCrossings(lines, extras);
|
|
46
46
|
return lines.join('\n') + '\n';
|
|
47
47
|
}
|
|
48
|
+
// LUM-564 声称 vs 核验: the agent's CLAIM (what it says it did) paired with
|
|
49
|
+
// the measured verification conclusion (what was actually confirmed) — the
|
|
50
|
+
// two columns regular 規律2 wants, instead of only the verification one. The
|
|
51
|
+
// machine-verification rollup (LUM-470) lives on the verification side here
|
|
52
|
+
// rather than as a standalone line, so the contrast is in one place.
|
|
53
|
+
pushClaimVsVerification(lines, data);
|
|
48
54
|
lines.push('');
|
|
49
55
|
lines.push(`Criteria (${data.criteria.length} total, ${data.nextActions.length} unmet):`);
|
|
50
|
-
// LUM-470: honest machine-verification rollup over the active MACHINE criteria
|
|
51
|
-
// (same read model as web, LUM-456) — so the terminal rollup never reads as
|
|
52
|
-
// all-human when a checkpointer actually verified the work.
|
|
53
|
-
const mv = data.machineVerification;
|
|
54
|
-
if (mv.total > 0) {
|
|
55
|
-
lines.push(`Machine verification: ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
|
|
56
|
-
}
|
|
57
56
|
for (const c of data.criteria) {
|
|
58
57
|
const glyph = c.latestVerdict == null
|
|
59
58
|
? '○'
|
|
@@ -208,6 +207,120 @@ function fmtDuration(totalSec) {
|
|
|
208
207
|
parts.push(`${s}s`);
|
|
209
208
|
return parts.join(' ');
|
|
210
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Append the "Claim vs verification" pairing (LUM-564) — 規律2 声称vs核验. The
|
|
212
|
+
* verification conclusion (machine-verified vs human-override, LUM-470) was the
|
|
213
|
+
* whole-system template, but the agent's run summary — its CLAIM of what it did
|
|
214
|
+
* — lived on a different surface, so the "two-column" contrast was really one
|
|
215
|
+
* column. This puts them side by side: the unverified self-report (estimate-
|
|
216
|
+
* tier — 估, not 测) against the measured verdict (测), so a reader can weigh
|
|
217
|
+
* 声称 against 核验 at a glance.
|
|
218
|
+
*
|
|
219
|
+
* Fail-closed on every axis: a missing claim renders an explicit "not generated
|
|
220
|
+
* yet" line (never a fabricated claim); a not-yet-verified task renders the
|
|
221
|
+
* verification side as explicitly unconfirmed (never an implied pass). The
|
|
222
|
+
* claim text is same-source with the web card (latest session's LLM run
|
|
223
|
+
* summary, else its STOP turn digest) so the two surfaces cannot drift.
|
|
224
|
+
*/
|
|
225
|
+
function pushClaimVsVerification(lines, data) {
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push('Claim vs verification:');
|
|
228
|
+
// ── Claim (声称): the agent's self-report — estimate-tier, never measured.
|
|
229
|
+
lines.push(' ▸ Claim — what the agent says it did');
|
|
230
|
+
lines.push(' (agent self-report · estimated, not verification):');
|
|
231
|
+
const claim = data.claim;
|
|
232
|
+
if (claim && claim.source === 'AGENT' && claim.text) {
|
|
233
|
+
// LUM-597: the agent's OWN self-report (attached to verify) — the
|
|
234
|
+
// authoritative claim faithfulness judges, not a summarizer paraphrase.
|
|
235
|
+
for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
|
|
236
|
+
lines.push(` ${cl}`);
|
|
237
|
+
}
|
|
238
|
+
lines.push(' ↳ source: agent self-report (verify --note)');
|
|
239
|
+
}
|
|
240
|
+
else if (claim && claim.source === 'RUN_SUMMARY' && claim.text) {
|
|
241
|
+
for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
|
|
242
|
+
lines.push(` ${cl}`);
|
|
243
|
+
}
|
|
244
|
+
lines.push(' ↳ source: synthesized run summary (no self-report)');
|
|
245
|
+
}
|
|
246
|
+
else if (claim && claim.source === 'DIGEST') {
|
|
247
|
+
// LUM-583 ③ / LUM-574: a raw turn digest is not a claim — withhold it and
|
|
248
|
+
// say the formal summary is still generating, same-source with the web card.
|
|
249
|
+
lines.push(' generating — the formal run summary is still being synthesized');
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
// Fail-closed: no run summary → say so, never invent a claim. Run summaries
|
|
253
|
+
// are synthesized when the bound task reaches DONE (LUM-481).
|
|
254
|
+
lines.push(' not generated yet — the agent run summary is synthesized when the task reaches DONE');
|
|
255
|
+
}
|
|
256
|
+
// ── Faithfulness (LUM-583): the third state — is the claim itself true? Read
|
|
257
|
+
// from the persisted LUM-582 verdict; PENDING (not yet judged) is distinct
|
|
258
|
+
// from UNJUDGEABLE (a real verdict), STALE keeps the verdict but flags a
|
|
259
|
+
// pending re-check. Skipped only when the server didn't emit it (older server).
|
|
260
|
+
pushFaithfulness(lines, data.faithfulness);
|
|
261
|
+
// ── Verification (核验): the measured verdict — machine-verified vs override.
|
|
262
|
+
lines.push(' ▸ Verification — what was actually confirmed (measured):');
|
|
263
|
+
const mv = data.machineVerification;
|
|
264
|
+
if (data.currentRound === 0) {
|
|
265
|
+
// Nothing verified yet — the claim stands unconfirmed. Don't imply a pass.
|
|
266
|
+
lines.push(' no verification has run yet — the claim is unconfirmed');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
if (mv.total > 0) {
|
|
270
|
+
lines.push(` ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
|
|
271
|
+
}
|
|
272
|
+
const met = data.criteria.length - data.nextActions.length;
|
|
273
|
+
lines.push(` ${met} of ${data.criteria.length} criteria met by their latest verdict`);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Append the "Faithfulness" conclusion (LUM-583) — the third state over the
|
|
278
|
+
* 声称/核验 columns: whether the agent's CLAIM is itself TRUE, judged
|
|
279
|
+
* independently against the delivery (LUM-582). Same read model the web honest
|
|
280
|
+
* report renders, so the two surfaces cannot drift. Fail-closed: PENDING (not
|
|
281
|
+
* yet judged) reads distinctly from UNJUDGEABLE (a real verdict), and STALE
|
|
282
|
+
* keeps the prior verdict while flagging a pending re-check. Skipped only when
|
|
283
|
+
* the server didn't emit the field (older server) — never fabricated.
|
|
284
|
+
*/
|
|
285
|
+
function pushFaithfulness(lines, f) {
|
|
286
|
+
if (!f)
|
|
287
|
+
return; // older server: field absent → don't fabricate a conclusion.
|
|
288
|
+
const verdictPhrase = (v) => {
|
|
289
|
+
switch (v) {
|
|
290
|
+
case 'FAITHFUL':
|
|
291
|
+
return 'faithful — the claim matches the delivery';
|
|
292
|
+
case 'OVERSTATED':
|
|
293
|
+
return 'overstated — the claim says more than the diff/PR shows';
|
|
294
|
+
case 'UNDERREPORT':
|
|
295
|
+
return 'under-reported — the claim says less than the diff/PR shows';
|
|
296
|
+
default:
|
|
297
|
+
return "unjudgeable — couldn't decide from the diff/PR";
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
let body;
|
|
301
|
+
if (f.state === 'PENDING') {
|
|
302
|
+
body =
|
|
303
|
+
'not yet judged — the faithfulness check runs in a batch after delivery';
|
|
304
|
+
}
|
|
305
|
+
else if (f.state === 'STALE') {
|
|
306
|
+
body = `${f.verdict ? verdictPhrase(f.verdict) : 'judged'} (stale — re-checked on the next batch)`;
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
body = verdictPhrase(f.state);
|
|
310
|
+
}
|
|
311
|
+
const ev = [];
|
|
312
|
+
if (f.prNumbers.length > 0) {
|
|
313
|
+
ev.push(`PR ${f.prNumbers.map(n => `#${n}`).join(', ')}`);
|
|
314
|
+
}
|
|
315
|
+
if (f.diffShas.length > 0) {
|
|
316
|
+
ev.push(f.diffShas
|
|
317
|
+
.slice(0, 3)
|
|
318
|
+
.map(s => (0, sanitize_1.sanitizeField)(s).slice(0, 7))
|
|
319
|
+
.join(' '));
|
|
320
|
+
}
|
|
321
|
+
lines.push(' ▸ Faithfulness — does the claim match the delivery (LUM-583):');
|
|
322
|
+
lines.push(` ${body}${ev.length > 0 ? ` · evidence: ${ev.join(' · ')}` : ''}`);
|
|
323
|
+
}
|
|
211
324
|
/**
|
|
212
325
|
* Append the honest "Cost" section (LUM-560) — 规律 1: surface the costs a human
|
|
213
326
|
* should weigh (token spend, active time, machine rework) on the same report as
|
|
@@ -144,6 +144,19 @@ async function verify(identifier, options = {}) {
|
|
|
144
144
|
`The contract is HUMAN-only; finish your work and hand off for human review (lumo task update ${taskId} --status in_review).\n`);
|
|
145
145
|
return;
|
|
146
146
|
}
|
|
147
|
+
// ── Require the self-report (LUM-597) ────────────────────────────────────
|
|
148
|
+
// Requesting acceptance is a structured action; `--note` is the agent's
|
|
149
|
+
// mandatory self-report. It is captured deterministically here (the agent
|
|
150
|
+
// can't be made to phrase it well, but it can be made to provide one); its
|
|
151
|
+
// truthfulness is the faithfulness audit's job downstream. No note → no round
|
|
152
|
+
// is posted (no round burned), so re-running with one is free.
|
|
153
|
+
const note = options.note?.trim();
|
|
154
|
+
if (!note) {
|
|
155
|
+
console.error('Error: --note is required — state what you did and why it is ready, e.g.\n' +
|
|
156
|
+
` lumo verify ${taskId} --note "implemented X in foo.ts because Y; tests + tsc pass"\n` +
|
|
157
|
+
'This self-report is recorded as your claim and checked against the diff for faithfulness.');
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
147
160
|
// ── Execute every checkpointer locally ───────────────────────────────────
|
|
148
161
|
process.stdout.write(`Verifying ${taskId} — ${machine.length} MACHINE criteria\n`);
|
|
149
162
|
const results = [];
|
|
@@ -165,7 +178,7 @@ async function verify(identifier, options = {}) {
|
|
|
165
178
|
res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/verify`, {
|
|
166
179
|
method: 'POST',
|
|
167
180
|
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
168
|
-
body: JSON.stringify({ results }),
|
|
181
|
+
body: JSON.stringify({ results, note }),
|
|
169
182
|
});
|
|
170
183
|
}
|
|
171
184
|
catch (err) {
|
package/dist/cli/src/index.js
CHANGED
|
@@ -223,6 +223,7 @@ program
|
|
|
223
223
|
.command('verify [task]')
|
|
224
224
|
.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.')
|
|
225
225
|
.option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
|
|
226
|
+
.option('--note <text>', 'Required self-report — what you did and why it is ready (LUM-597). Recorded as your claim (source AGENT) when the round passes into IN_REVIEW, and checked against the diff for faithfulness.')
|
|
226
227
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
227
228
|
program
|
|
228
229
|
.command('verdict [task]')
|