@lumoai/cli 1.46.0 → 1.48.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 +35 -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/outcome.md +65 -0
- 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 -33
- package/assets/skill/references/worktree.md +1 -1
- package/dist/cli/src/commands/criteria-audit.js +52 -0
- package/dist/cli/src/commands/outcome.js +221 -0
- package/dist/cli/src/commands/task-criteria-list.js +8 -1
- package/dist/cli/src/commands/task-criteria-set.js +35 -1
- package/dist/cli/src/commands/task-status.js +68 -4
- package/dist/cli/src/commands/verify.js +14 -1
- package/dist/cli/src/index.js +30 -0
- package/dist/shared/src/referent-kind.js +168 -0
- package/package.json +1 -1
|
@@ -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,34 +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
|
-
- **Claim vs verification**
|
|
115
|
-
- **Claim** — what the agent _says_ it did:
|
|
116
|
-
- **Verification** — what was actually _confirmed_ (measured): the machine-verification rollup
|
|
117
|
-
-
|
|
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`.
|
|
118
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.
|
|
119
|
-
- A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict
|
|
120
|
-
- A verdict's **evidence is drillable
|
|
121
|
-
- 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.
|
|
122
132
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
123
133
|
- **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
|
|
124
|
-
- **Cost**
|
|
125
|
-
- **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:
|
|
126
136
|
- **rework rounds** — verify rounds that had a FAIL;
|
|
127
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;
|
|
128
138
|
- **leftover follow-ups** — criteria whose latest verdict is `PASS_WITH_FOLLOWUP`;
|
|
129
|
-
- **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;
|
|
130
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.
|
|
131
141
|
|
|
132
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`).
|
|
133
143
|
|
|
134
|
-
- **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.
|
|
135
145
|
|
|
136
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.
|
|
137
|
-
- **Open boundary crossings**
|
|
138
|
-
- **Read-only awareness** — this surfaces crossings detected elsewhere
|
|
139
|
-
- **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".
|
|
140
150
|
|
|
141
|
-
### Responding to an open crossing — `lumo crossing explain`
|
|
151
|
+
### Responding to an open crossing — `lumo crossing explain`
|
|
142
152
|
|
|
143
153
|
When `lumo task status` surfaces an OPEN crossing you believe is a false positive
|
|
144
154
|
— or you simply want to leave a rationale for the human reviewer — append a
|
|
@@ -151,7 +161,7 @@ lumo crossing explain <id> --note "this was a generated fixture, not a hand-edit
|
|
|
151
161
|
This is the **inverse** of dispositioning, but it is the agent/CLI path
|
|
152
162
|
(bearer-only; a clerk/human caller is refused). Behavior:
|
|
153
163
|
|
|
154
|
-
- 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);
|
|
155
165
|
- the note is shown to the human reviewer at disposition time, kept for later review, and explicitly labeled _agent self-report · unverified_;
|
|
156
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);
|
|
157
167
|
- earlier explanations are immutable — a correction is a new note.
|
|
@@ -162,16 +172,16 @@ This is the **inverse** of dispositioning, but it is the agent/CLI path
|
|
|
162
172
|
`1`). The schema is versioned: breaking shape changes bump the major; additive
|
|
163
173
|
fields don't. Pin on `version` when scripting against it.
|
|
164
174
|
|
|
165
|
-
- each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it
|
|
175
|
+
- each criterion carries `machinePassed` (boolean — a checkpointer currently vouches for it);
|
|
166
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;
|
|
167
|
-
- 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
|
|
168
|
-
- **`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".
|
|
169
179
|
|
|
170
180
|
`status` reads; `verify` judges. Running status never starts a round, never
|
|
171
181
|
escalates, and never changes task state — loop rules (cap 3, IN_REVIEW on
|
|
172
182
|
all-pass, human-only DONE) live entirely in `lumo verify` and the server.
|
|
173
183
|
|
|
174
|
-
## lumo verdict — the three verdict channels
|
|
184
|
+
## lumo verdict — the three verdict channels
|
|
175
185
|
|
|
176
186
|
`lumo verify` is the MACHINE channel. `lumo verdict` covers the other two — the
|
|
177
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>`
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatAuditReport = formatAuditReport;
|
|
4
|
+
exports.criteriaAudit = criteriaAudit;
|
|
5
|
+
const config_1 = require("../lib/config");
|
|
6
|
+
const api_1 = require("../lib/api");
|
|
7
|
+
const KIND_ORDER = [
|
|
8
|
+
'EXTERNAL_FACT',
|
|
9
|
+
'AGENT_CONSTRUCTED_STATE',
|
|
10
|
+
'PENDING_OUTCOME',
|
|
11
|
+
'UNVERIFIED_ASSERTION',
|
|
12
|
+
'UNCLASSIFIED',
|
|
13
|
+
];
|
|
14
|
+
function formatAuditReport(r) {
|
|
15
|
+
const lines = ['Criteria referent-kind audit (workspace)', ''];
|
|
16
|
+
for (const k of KIND_ORDER) {
|
|
17
|
+
const n = r.byEffectiveKind[k] ?? 0;
|
|
18
|
+
const pct = r.total === 0 ? 0 : Math.round((n / r.total) * 100);
|
|
19
|
+
lines.push(` ${k.padEnd(24)} ${String(n).padStart(4)} (${pct}%)`);
|
|
20
|
+
}
|
|
21
|
+
lines.push('');
|
|
22
|
+
const pct = Math.round(r.selfConfirmingGreenRatio * 100);
|
|
23
|
+
lines.push(` self-confirming green: ${pct}% of ${r.classified} classified criteria`);
|
|
24
|
+
lines.push(` (AGENT_CONSTRUCTED_STATE + UNVERIFIED_ASSERTION — green that confirms only the agent's own work)`);
|
|
25
|
+
return lines.join('\n') + '\n';
|
|
26
|
+
}
|
|
27
|
+
/** `lumo criteria audit` — workspace-level referent-kind distribution. */
|
|
28
|
+
async function criteriaAudit() {
|
|
29
|
+
const creds = (0, config_1.readCredentials)();
|
|
30
|
+
if (!creds) {
|
|
31
|
+
console.error('Error: not logged in. Run `lumo auth login` first.');
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
35
|
+
let res;
|
|
36
|
+
try {
|
|
37
|
+
res = await fetch(`${base}/api/criteria/audit`, {
|
|
38
|
+
headers: { Authorization: `Bearer ${creds.token}` },
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
43
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
44
|
+
return 1;
|
|
45
|
+
}
|
|
46
|
+
if (!res.ok) {
|
|
47
|
+
console.error(`Error: criteria audit failed (HTTP ${res.status})`);
|
|
48
|
+
return 1;
|
|
49
|
+
}
|
|
50
|
+
const report = (await res.json());
|
|
51
|
+
process.stdout.write(formatAuditReport(report));
|
|
52
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.outcomeRecord = outcomeRecord;
|
|
4
|
+
exports.outcomeShow = outcomeShow;
|
|
5
|
+
exports.outcomeRate = outcomeRate;
|
|
6
|
+
const config_1 = require("../lib/config");
|
|
7
|
+
const api_1 = require("../lib/api");
|
|
8
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
9
|
+
/**
|
|
10
|
+
* `lumo outcome` — the post-hoc outcome well (LUM-598).
|
|
11
|
+
*
|
|
12
|
+
* The well is the single external oracle for correctness + fidelity: it records
|
|
13
|
+
* the real-world fate a delivery met AFTER it shipped. It is a FALSIFIER, not a
|
|
14
|
+
* verifier — it only ever reads REJECTED (reality revoked/redid/bypassed the
|
|
15
|
+
* work) or INCONCLUSIVE (no rejection on record). There is deliberately no
|
|
16
|
+
* "mark satisfied": the absence of a rejection is not a pass.
|
|
17
|
+
*/
|
|
18
|
+
const MANUAL_KINDS = [
|
|
19
|
+
'REVERTED',
|
|
20
|
+
'ROLLED_BACK',
|
|
21
|
+
'CI_REGRESSION',
|
|
22
|
+
'DOWNSTREAM_REDIRECT',
|
|
23
|
+
'BYPASSED',
|
|
24
|
+
'MANUAL',
|
|
25
|
+
];
|
|
26
|
+
function authBase() {
|
|
27
|
+
const creds = (0, config_1.readCredentials)();
|
|
28
|
+
if (!creds)
|
|
29
|
+
return { error: 'not logged in. Run `lumo auth login` first.' };
|
|
30
|
+
const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
|
|
31
|
+
const headers = {
|
|
32
|
+
Authorization: `Bearer ${creds.token}`,
|
|
33
|
+
};
|
|
34
|
+
const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
|
|
35
|
+
if (sessionId)
|
|
36
|
+
headers['X-Lumo-Session-Id'] = sessionId;
|
|
37
|
+
return { base, headers };
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* `lumo outcome record <task> --note "<what reality did>" [--kind <kind>]` —
|
|
41
|
+
* record a human-observed post-hoc REJECTION of a delivery. Append-only. The
|
|
42
|
+
* note is mandatory (the observed referent); the default kind is MANUAL.
|
|
43
|
+
*/
|
|
44
|
+
async function outcomeRecord(taskId, options = {}) {
|
|
45
|
+
if (!taskId || taskId.trim() === '') {
|
|
46
|
+
console.error('Error: a task is required: lumo outcome record <task> --note "…"');
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const note = options.note?.trim();
|
|
50
|
+
if (!note) {
|
|
51
|
+
console.error('Error: --note "<what reality did>" is required (the observed rejection, e.g. "reverted in #812 after prod incident").');
|
|
52
|
+
return 1;
|
|
53
|
+
}
|
|
54
|
+
const kind = (options.kind?.trim().toUpperCase() ||
|
|
55
|
+
'MANUAL');
|
|
56
|
+
if (!MANUAL_KINDS.includes(kind)) {
|
|
57
|
+
console.error(`Error: --kind must be one of: ${MANUAL_KINDS.map(k => k.toLowerCase()).join(', ')}`);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const auth = authBase();
|
|
61
|
+
if ('error' in auth) {
|
|
62
|
+
console.error(`Error: ${auth.error}`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
const payload = { kind, note };
|
|
66
|
+
if (options.occurredAt?.trim())
|
|
67
|
+
payload.occurredAt = options.occurredAt.trim();
|
|
68
|
+
let res;
|
|
69
|
+
try {
|
|
70
|
+
res = await fetch(`${auth.base}/api/tasks/${encodeURIComponent(taskId)}/outcome-signals`, {
|
|
71
|
+
method: 'POST',
|
|
72
|
+
headers: { ...auth.headers, 'Content-Type': 'application/json' },
|
|
73
|
+
body: JSON.stringify(payload),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
78
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
if (res.status === 401) {
|
|
82
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
const errBody = (await res.json().catch(() => null));
|
|
87
|
+
const detail = errBody && typeof errBody.error === 'string'
|
|
88
|
+
? (0, sanitize_1.sanitizeField)(errBody.error)
|
|
89
|
+
: '';
|
|
90
|
+
console.error(`Error: outcome not recorded (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write(`✓ Recorded a ${(0, sanitize_1.sanitizeField)(kind.toLowerCase())} rejection on ${(0, sanitize_1.sanitizeField)(taskId)}.\n` +
|
|
94
|
+
' The well is a falsifier — this marks the delivery REJECTED by reality; ' +
|
|
95
|
+
'it is append-only and there is no "satisfied" counterpart.\n');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* `lumo outcome show <task>` — read the well for a task: the falsifier verdict
|
|
100
|
+
* plus its backing rejection signals.
|
|
101
|
+
*/
|
|
102
|
+
async function outcomeShow(taskId) {
|
|
103
|
+
if (!taskId || taskId.trim() === '') {
|
|
104
|
+
console.error('Error: a task is required: lumo outcome show <task>');
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
const auth = authBase();
|
|
108
|
+
if ('error' in auth) {
|
|
109
|
+
console.error(`Error: ${auth.error}`);
|
|
110
|
+
return 1;
|
|
111
|
+
}
|
|
112
|
+
let res;
|
|
113
|
+
try {
|
|
114
|
+
res = await fetch(`${auth.base}/api/tasks/${encodeURIComponent(taskId)}/outcome-signals`, { headers: auth.headers });
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
118
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
if (res.status === 401) {
|
|
122
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
123
|
+
return 1;
|
|
124
|
+
}
|
|
125
|
+
if (!res.ok) {
|
|
126
|
+
console.error(`Error: could not read the well (HTTP ${res.status}).`);
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
const well = (await res.json());
|
|
130
|
+
const mark = well.verdict === 'REJECTED' ? '✗' : '·';
|
|
131
|
+
process.stdout.write(`${mark} Outcome well — ${(0, sanitize_1.sanitizeField)(taskId)}: ${well.verdict}\n`);
|
|
132
|
+
if (well.verdict === 'INCONCLUSIVE') {
|
|
133
|
+
process.stdout.write(' No rejection on record. INCONCLUSIVE ≠ satisfied — the well only ' +
|
|
134
|
+
'asserts rejection; silence is not a pass.\n');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
for (const s of well.signals) {
|
|
138
|
+
const when = (s.occurredAt ?? s.detectedAt).slice(0, 10);
|
|
139
|
+
const note = s.evidence?.note ? ` — ${(0, sanitize_1.sanitizeField)(s.evidence.note)}` : '';
|
|
140
|
+
process.stdout.write(` ${when} ${(0, sanitize_1.sanitizeField)(s.kind)} (${(0, sanitize_1.sanitizeField)(s.source)})${note}\n`);
|
|
141
|
+
}
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
/** Render the report client-side (same pattern as `lumo cost` / `outcome show`). */
|
|
145
|
+
function formatRate(report) {
|
|
146
|
+
const lines = [];
|
|
147
|
+
lines.push('Trust × post-hoc fate — delivery-time forecast confidence vs the outcome well');
|
|
148
|
+
lines.push(' (rejection rate is a LOWER BOUND: the well only records rejections; ' +
|
|
149
|
+
'no signal = INCONCLUSIVE, never "satisfied")');
|
|
150
|
+
lines.push(` ${report.totalDeliveries} snapshotted deliveries · min ${report.minSamples} per bracket`);
|
|
151
|
+
if (report.totalDeliveries === 0) {
|
|
152
|
+
lines.push(' (no delivery has a forecast snapshot yet — nothing to join)');
|
|
153
|
+
return lines.join('\n') + '\n';
|
|
154
|
+
}
|
|
155
|
+
for (const b of report.brackets) {
|
|
156
|
+
if (b.status === 'measured') {
|
|
157
|
+
const pct = ((b.rejectionRate ?? 0) * 100).toFixed(1);
|
|
158
|
+
const lo = ((b.interval?.lower ?? 0) * 100).toFixed(1);
|
|
159
|
+
const hi = ((b.interval?.upper ?? 0) * 100).toFixed(1);
|
|
160
|
+
lines.push(` ${b.bracket.padEnd(6)} ≥${pct}% rejected (${b.rejected}/${b.delivered}, 95% CI ${lo}–${hi}%)`);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
lines.push(` ${b.bracket.padEnd(6)} insufficient (${b.rejected}/${b.delivered} deliveries < ${report.minSamples} — rate withheld)`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const verdict = report.comparison.status === 'measured'
|
|
167
|
+
? `→ ${(0, sanitize_1.sanitizeField)(report.comparison.reason)}`
|
|
168
|
+
: `→ inconclusive: ${(0, sanitize_1.sanitizeField)(report.comparison.reason)}`;
|
|
169
|
+
lines.push(` ${verdict}`);
|
|
170
|
+
return lines.join('\n') + '\n';
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* `lumo outcome rate [--min N] [--json]` — read the workspace trust × post-hoc-
|
|
174
|
+
* fate join: per delivery-time forecast-confidence bracket, the post-hoc
|
|
175
|
+
* REJECTED rate from the outcome well. Honest by construction — thin brackets
|
|
176
|
+
* read `insufficient` (no fabricated rate) and the high-vs-low comparison stays
|
|
177
|
+
* `inconclusive` until the well has enough signal to tell the brackets apart.
|
|
178
|
+
*/
|
|
179
|
+
async function outcomeRate(options = {}) {
|
|
180
|
+
const auth = authBase();
|
|
181
|
+
if ('error' in auth) {
|
|
182
|
+
console.error(`Error: ${auth.error}`);
|
|
183
|
+
return 1;
|
|
184
|
+
}
|
|
185
|
+
let min;
|
|
186
|
+
if (options.min !== undefined && options.min.trim() !== '') {
|
|
187
|
+
const n = Number(options.min);
|
|
188
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
189
|
+
console.error('Error: --min must be a positive integer');
|
|
190
|
+
return 1;
|
|
191
|
+
}
|
|
192
|
+
min = n;
|
|
193
|
+
}
|
|
194
|
+
const qs = min !== undefined ? `?min=${min}` : '';
|
|
195
|
+
let res;
|
|
196
|
+
try {
|
|
197
|
+
res = await fetch(`${auth.base}/api/outcome/rate${qs}`, {
|
|
198
|
+
headers: auth.headers,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
catch (err) {
|
|
202
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
203
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
if (res.status === 401) {
|
|
207
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
if (!res.ok) {
|
|
211
|
+
console.error(`Error: could not read the outcome rate (HTTP ${res.status}).`);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
const report = (await res.json());
|
|
215
|
+
if (options.json) {
|
|
216
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
process.stdout.write(formatRate(report));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
@@ -5,6 +5,7 @@ exports.taskCriteriaList = taskCriteriaList;
|
|
|
5
5
|
const config_1 = require("../lib/config");
|
|
6
6
|
const api_1 = require("../lib/api");
|
|
7
7
|
const sanitize_1 = require("../lib/sanitize");
|
|
8
|
+
const referent_kind_1 = require("../../../shared/src/referent-kind");
|
|
8
9
|
/**
|
|
9
10
|
* Render criteria rows for stdout. One line per criterion —
|
|
10
11
|
* `<id> [TYPE] SOURCE@rN statement` — plus an indented checkpointer line
|
|
@@ -16,7 +17,13 @@ function formatCriteriaRows(criteria) {
|
|
|
16
17
|
for (const c of criteria) {
|
|
17
18
|
const provenance = `${c.source}@r${c.addedAtRound}`;
|
|
18
19
|
const evidence = c.evidenceRequired ? ' [evidence]' : '';
|
|
19
|
-
|
|
20
|
+
const eff = (0, referent_kind_1.effectiveReferentKind)({
|
|
21
|
+
declared: c.referentKind ?? null,
|
|
22
|
+
verifierType: c.verifierType,
|
|
23
|
+
checkpointer: c.checkpointer,
|
|
24
|
+
});
|
|
25
|
+
const kindTag = ` ⟨${eff}⟩`;
|
|
26
|
+
lines.push(`${c.id} [${c.verifierType}] ${provenance}${evidence}${kindTag} ${(0, sanitize_1.sanitizeField)(c.statement)}`);
|
|
20
27
|
if (c.checkpointer) {
|
|
21
28
|
lines.push(` ↳ check: ${(0, sanitize_1.sanitizeField)(c.checkpointer)}`);
|
|
22
29
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.verifyReadback = verifyReadback;
|
|
3
4
|
exports.taskCriteriaSet = taskCriteriaSet;
|
|
4
5
|
const config_1 = require("../lib/config");
|
|
5
6
|
const api_1 = require("../lib/api");
|
|
@@ -7,6 +8,31 @@ const doc_input_1 = require("../lib/doc-input");
|
|
|
7
8
|
const path_guard_1 = require("../lib/path-guard");
|
|
8
9
|
const sanitize_1 = require("../lib/sanitize");
|
|
9
10
|
const task_criteria_list_1 = require("./task-criteria-list");
|
|
11
|
+
/**
|
|
12
|
+
* Landing-integrity read-back (LUM-602): a 200 OK proves absence of error,
|
|
13
|
+
* not presence of correct content. Compare what the server stored (echoed in
|
|
14
|
+
* the PUT response) field-by-field against what we submitted; any divergence
|
|
15
|
+
* means the value was corrupted in transport/landing.
|
|
16
|
+
*/
|
|
17
|
+
function verifyReadback(submitted, stored) {
|
|
18
|
+
const issues = [];
|
|
19
|
+
const storedByStatement = new Map(stored.map(c => [c.statement, c]));
|
|
20
|
+
for (const s of submitted) {
|
|
21
|
+
const got = storedByStatement.get(s.statement);
|
|
22
|
+
if (!got) {
|
|
23
|
+
issues.push(`criterion not found after write: "${s.statement.slice(0, 60)}"`);
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const wantCk = s.checkpointer ?? null;
|
|
27
|
+
if ((got.checkpointer ?? null) !== wantCk) {
|
|
28
|
+
issues.push(`checkpointer corrupted in landing for "${s.statement.slice(0, 40)}": sent ${JSON.stringify(wantCk)}, stored ${JSON.stringify(got.checkpointer ?? null)}`);
|
|
29
|
+
}
|
|
30
|
+
if (s.referentKind != null && got.referentKind !== s.referentKind) {
|
|
31
|
+
issues.push(`referentKind corrupted in landing for "${s.statement.slice(0, 40)}": sent ${s.referentKind}, stored ${got.referentKind}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return issues;
|
|
35
|
+
}
|
|
10
36
|
const CAUSE_TAGS = [
|
|
11
37
|
'NEW_INFO',
|
|
12
38
|
'SCOPE_CHANGE',
|
|
@@ -28,7 +54,7 @@ function parseCriteriaJson(raw) {
|
|
|
28
54
|
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
29
55
|
return {
|
|
30
56
|
ok: false,
|
|
31
|
-
error: 'expected a non-empty JSON array of criteria, e.g. [{"statement":"…","verifierType":"MACHINE","checkpointer":"npx jest …"}]',
|
|
57
|
+
error: 'expected a non-empty JSON array of criteria, e.g. [{"statement":"…","verifierType":"MACHINE","checkpointer":"npx jest …","referentKind":"AGENT_CONSTRUCTED_STATE"}]',
|
|
32
58
|
};
|
|
33
59
|
}
|
|
34
60
|
return { ok: true, items: parsed };
|
|
@@ -171,4 +197,12 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
171
197
|
if (data.judgeStepsWarning) {
|
|
172
198
|
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.judgeStepsWarning)}\n`);
|
|
173
199
|
}
|
|
200
|
+
const readbackIssues = verifyReadback(criteriaItems, data.criteria);
|
|
201
|
+
if (readbackIssues.length > 0) {
|
|
202
|
+
for (const issue of readbackIssues) {
|
|
203
|
+
console.error(`⚠ landing-integrity: ${(0, sanitize_1.sanitizeField)(issue)}`);
|
|
204
|
+
}
|
|
205
|
+
console.error('Error: stored contract does not match what was submitted — do not trust the success receipt. Re-run and verify.');
|
|
206
|
+
return 1;
|
|
207
|
+
}
|
|
174
208
|
}
|