@lumoai/cli 1.48.0 → 1.49.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 +12 -2
- package/assets/skill/references/criteria.md +25 -3
- package/assets/skill/references/fidelity.md +32 -0
- package/assets/skill/references/outcome.md +7 -7
- package/assets/skill/references/verify.md +12 -11
- package/dist/cli/src/commands/fidelity.js +108 -0
- package/dist/cli/src/commands/outcome.js +11 -7
- package/dist/cli/src/commands/task-criteria-set.js +5 -0
- package/dist/cli/src/index.js +9 -0
- package/dist/shared/src/referent-kind.js +156 -3
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -32,6 +32,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
32
32
|
| `verify`, `task status` — machine verification loop, claim-done flow, self-check/resume | [references/verify.md](references/verify.md) |
|
|
33
33
|
| `cost` — per-operation (per-tool) token cost read-out; `task lineage` Top-5 | [references/task-context.md](references/task-context.md) |
|
|
34
34
|
| `outcome show/record/rate` — post-hoc outcome well (falsifier oracle, rejection-only) | [references/outcome.md](references/outcome.md) |
|
|
35
|
+
| `fidelity show` — per-delivery grounding + independence change-pattern read-out | [references/fidelity.md](references/fidelity.md) |
|
|
35
36
|
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
36
37
|
| `doc create/update/list/move/bind/share/import` (CRUD) | [references/docs.md](references/docs.md) |
|
|
37
38
|
| `doc show --raw/--section`, `doc patch/append/diff/rebuild-source` (editing live docs) | [references/doc-editing.md](references/doc-editing.md) |
|
|
@@ -101,7 +102,16 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
101
102
|
|
|
102
103
|
- `lumo outcome show <task>` — read the post-hoc outcome well: the **falsifier** verdict (`REJECTED` | `INCONCLUSIVE` — never a "pass") plus its backing rejection signals. `INCONCLUSIVE` = no rejection on record, **not** "satisfied".
|
|
103
104
|
- `lumo outcome record <task> --note "<what reality did>" [--kind <kind>]` — record a human-observed post-hoc **rejection** of a delivery (revert / rollback / CI regression / downstream redirect / bypass). Append-only; **there is no "mark satisfied" counterpart** — the well only asserts rejection. `--note` is required (the observed referent).
|
|
104
|
-
- `lumo outcome rate [--min N] [--json]` — workspace **trust × post-hoc-fate** join (LUM-603): per delivery-time
|
|
105
|
+
- `lumo outcome rate [--min N] [--json]` — workspace **trust × post-hoc-fate** join (LUM-603, LUM-606): per delivery-time **verification-friction** bucket (`no-verification` / `single-round` / `multi-round-or-sendback` — two raw counts frozen at delivery, replacing the saturating forecast-confidence axis), the post-hoc REJECTED rate from the well. Honest by construction — thin buckets read `insufficient` (no fabricated rate), the single-round-vs-multi comparison stays `inconclusive` until CIs separate, and the rate is a **lower bound** (no signal = INCONCLUSIVE, never satisfied). Bucket names are value-neutral — direction is decided by the correlation, never welded into the label. Under-sampled today by design — the skeleton produces real strata once the well fills.
|
|
106
|
+
|
|
107
|
+
**Fidelity read-out (mechanical change-pattern)** — see [fidelity.md](references/fidelity.md)
|
|
108
|
+
|
|
109
|
+
- `lumo fidelity show <task> [--json]` — per delivery snapshot (LUM-609), a **composition table, not a single score**: (1) the **grounding** of the frozen contract — grounded (`EXTERNAL_FACT`, a machine-checkable upper bound) / self-confirming (`AGENT_CONSTRUCTED_STATE` + `UNVERIFIED_ASSERTION`) / inconclusive (`PENDING_OUTCOME` + `UNCLASSIFIED`, surfaced not swallowed) + the full effective-kind tally; (2) the **independence** signal — criteria `ADDED`/`UPDATED` at round>0 (after work started) that the delivery rode on = backward-inference suspects. Purely mechanical (timing + op + grounding) — **no direction verdict**. A no-snapshot task is reported explicitly (not a pass). Zero new storage — reads the snapshot + `CRITERION_CHANGED` trail.
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
lumo fidelity show LUM-42
|
|
113
|
+
lumo fidelity show LUM-42 --json
|
|
114
|
+
```
|
|
105
115
|
|
|
106
116
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
107
117
|
|
|
@@ -169,7 +179,7 @@ lumo criteria audit
|
|
|
169
179
|
- `EXTERNAL_FACT` — declared as externally observable (git SHA, CI status, DB row) and the checkpointer invokes real external tooling
|
|
170
180
|
- `AGENT_CONSTRUCTED_STATE` — passes by checking output the agent itself produced
|
|
171
181
|
- `PENDING_OUTCOME` — deferred to post-hoc falsifier
|
|
172
|
-
- `UNVERIFIED_ASSERTION` — declared `EXTERNAL_FACT` but the checkpointer does **not** actually invoke external tooling (git/CI/HTTP); downgraded
|
|
182
|
+
- `UNVERIFIED_ASSERTION` — declared `EXTERNAL_FACT` but the checkpointer does **not** actually invoke external tooling (git/CI/HTTP); auto-downgraded. As of LUM-605 this is an **enforced gate, not just a label**: `lumo task criteria set` warns on it, and `lumo verify` blocks a PASS on it with **422 (no round burned)** until it's re-grounded or honestly re-declared `AGENT_CONSTRUCTED_STATE` (see references/criteria.md "referentKind")
|
|
173
183
|
- `UNCLASSIFIED` — legacy rows predating the `referentKind` field (null in DB)
|
|
174
184
|
|
|
175
185
|
2. **Self-confirming-green headline ratio** — `(AGENT_CONSTRUCTED_STATE + UNVERIFIED_ASSERTION) / classified`. Criteria in these two buckets pass by confirming only the agent's own work, not an independent external state.
|
|
@@ -60,6 +60,21 @@ The 3–7 range is calibrated for typical multi-file tasks. Criterion count must
|
|
|
60
60
|
|
|
61
61
|
### judgeSteps — agent-drafted judging steps for HUMAN criteria
|
|
62
62
|
|
|
63
|
+
**`referentKind` — what the criterion anchors (required, and a real gate as of LUM-605):**
|
|
64
|
+
|
|
65
|
+
Every criterion declares one `referentKind` — the only dimension that decides whether a PASS is independent or self-confirming. Required on every item (the API rejects a missing one with 400):
|
|
66
|
+
|
|
67
|
+
- `EXTERNAL_FACT` — anchored on a fact the agent cannot author: a git SHA / diff, CI status, an HTTP probe, a DB row, `prisma migrate status`. **Only honest when the checkpointer actually invokes external tooling** (`git`/`gh`/`curl`/`psql`/…) AND its output or exit status reaches the verdict (piped into an assertion, captured in `$(…)`, or the terminal command whose exit IS the verdict). A no-op or output-discarded external command (`git --version && cat frozen.txt | grep`) does **not** count.
|
|
68
|
+
- `AGENT_CONSTRUCTED_STATE` — passes by checking output the agent itself produced: **your own jest/vitest/tsc/node/tsx tests, build, type-check**. This is the honest kind for almost every code task's checks. Green here is self-confirming — that's fine, just label it truthfully.
|
|
69
|
+
- `PENDING_OUTCOME` — truth lands post-delivery (a downstream regression, a revert). Has no check today: it **cannot** be MACHINE (the API rejects PENDING_OUTCOME + MACHINE). Draft it HUMAN or leave it out of the encoded contract.
|
|
70
|
+
|
|
71
|
+
**The gate (LUM-605):** declaring `EXTERNAL_FACT` + MACHINE on a checkpointer that doesn't ground external (e.g. `npx jest …`) is the LUM-601 fakeGit shape — it would launder a self-confirming green into a "verified external fact". The system now downgrades it to `UNVERIFIED_ASSERTION` and:
|
|
72
|
+
|
|
73
|
+
- at **`criteria set`** — stores it but returns a non-rejecting ⚠ warning naming the criterion (re-ground or re-declare);
|
|
74
|
+
- at **`lumo verify`** — a PASS on it is **blocked with 422 (no round burned)**; the round does not land and the task does not move to IN_REVIEW until you either re-ground the checkpointer against a real external fact, or honestly re-declare it `AGENT_CONSTRUCTED_STATE`. Both fixes are free (no round consumed).
|
|
75
|
+
|
|
76
|
+
Don't reach for `EXTERNAL_FACT` to look stronger — a jest test is `AGENT_CONSTRUCTED_STATE`, full stop. Reserve `EXTERNAL_FACT` for checks that truly read external state, or use a HUMAN criterion for "a person ran the real system and observed X" (HUMAN is never mechanically downgraded).
|
|
77
|
+
|
|
63
78
|
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
79
|
|
|
65
80
|
**Shape — 1–3 steps, always in this order:**
|
|
@@ -78,6 +93,7 @@ A HUMAN criterion is judged by a person, not a checkpointer — so don't hand th
|
|
|
78
93
|
{
|
|
79
94
|
"statement": "The acceptance tab reads as plain operational instructions, not agent-voice assertions",
|
|
80
95
|
"verifierType": "HUMAN",
|
|
96
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE",
|
|
81
97
|
"judgeSteps": "Open the task's Acceptance tab (the IN_REVIEW card). For each HUMAN criterion, read its 'How to judge' block. Pass if each one names where to look, what to do, and what counts as pass vs send-back; send back if any reads as a bare assertion with no steps."
|
|
82
98
|
}
|
|
83
99
|
```
|
|
@@ -97,12 +113,14 @@ Two real-repo invariant examples — `prisma/migrations/` files never deleted (t
|
|
|
97
113
|
{
|
|
98
114
|
"statement": "No file under prisma/migrations/ is deleted by this change (vs origin/main)",
|
|
99
115
|
"verifierType": "MACHINE",
|
|
100
|
-
"checkpointer": "bash -c \"test -z \\\"$(git diff --diff-filter=D --name-only origin/main -- prisma/migrations/)\\\"\""
|
|
116
|
+
"checkpointer": "bash -c \"test -z \\\"$(git diff --diff-filter=D --name-only origin/main -- prisma/migrations/)\\\"\"",
|
|
117
|
+
"referentKind": "EXTERNAL_FACT"
|
|
101
118
|
},
|
|
102
119
|
{
|
|
103
120
|
"statement": "Live-doc keeps its table structure after the edit (no rows/headings dropped)",
|
|
104
121
|
"verifierType": "MACHINE",
|
|
105
|
-
"checkpointer": "npx tsx scripts/verify-live-doc.ts <docId> docs/live-docs/<file>.md"
|
|
122
|
+
"checkpointer": "npx tsx scripts/verify-live-doc.ts <docId> docs/live-docs/<file>.md",
|
|
123
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE"
|
|
106
124
|
}
|
|
107
125
|
]
|
|
108
126
|
```
|
|
@@ -120,17 +138,20 @@ One invariant criterion is usually enough — it's the guardrail, not the whole
|
|
|
120
138
|
{
|
|
121
139
|
"statement": "PUT /api/tasks/[id]/criteria rejects a second AGENT_DRAFT submission with 409",
|
|
122
140
|
"verifierType": "MACHINE",
|
|
123
|
-
"checkpointer": "npx jest __tests__/task-criteria.service.test.ts -t 'agent lock'"
|
|
141
|
+
"checkpointer": "npx jest __tests__/task-criteria.service.test.ts -t 'agent lock'",
|
|
142
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE"
|
|
124
143
|
},
|
|
125
144
|
{
|
|
126
145
|
"statement": "The criteria section reads naturally as part of the task statement",
|
|
127
146
|
"verifierType": "HUMAN",
|
|
147
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE",
|
|
128
148
|
"judgeSteps": "Open the task's Acceptance tab. Read the contract top to bottom. Pass if it scans as one coherent definition of done; send back if a criterion reads as boilerplate or contradicts another."
|
|
129
149
|
},
|
|
130
150
|
{
|
|
131
151
|
"statement": "Session-start injection shows the contract ahead of memory",
|
|
132
152
|
"verifierType": "MACHINE",
|
|
133
153
|
"checkpointer": "npx jest __tests__/cli/hook-runner-session-start-stdout.test.ts",
|
|
154
|
+
"referentKind": "AGENT_CONSTRUCTED_STATE",
|
|
134
155
|
"evidenceRequired": true
|
|
135
156
|
}
|
|
136
157
|
]
|
|
@@ -141,6 +162,7 @@ Fields:
|
|
|
141
162
|
- `statement` — required, ≤2000 chars.
|
|
142
163
|
- `verifierType` — `"MACHINE"` | `"HUMAN"`.
|
|
143
164
|
- `checkpointer` — required for MACHINE.
|
|
165
|
+
- `referentKind` — **required**, `"EXTERNAL_FACT"` | `"AGENT_CONSTRUCTED_STATE"` | `"PENDING_OUTCOME"` (see "referentKind" above). Your own tests/build/type-check are `AGENT_CONSTRUCTED_STATE`; `EXTERNAL_FACT` is gated (warned at set, 422 at verify) unless the checkpointer truly grounds external; `PENDING_OUTCOME` can't be MACHINE.
|
|
144
166
|
- `evidenceRequired` — optional, default false.
|
|
145
167
|
- `judgeSteps` — optional, ≤2000 chars; agent-drafted human-judging steps for a HUMAN criterion (see "judgeSteps" above).
|
|
146
168
|
- `id` — only in `--human` revisions (see below).
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Fidelity read-outs (mechanical change-pattern)
|
|
2
|
+
|
|
3
|
+
`lumo fidelity` is the fidelity-axis read surface — pure, read-only analytics over what the acceptance loop already records, in the same family as `lumo criteria audit` and `lumo outcome rate`. It adds **no storage and no write path**; it only reads the LUM-609 delivery snapshots and the `CRITERION_CHANGED` audit trail.
|
|
4
|
+
|
|
5
|
+
## `lumo fidelity show <task>`
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
lumo fidelity show LUM-42
|
|
9
|
+
lumo fidelity show LUM-42 --json
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
For **each delivery** of the task (one append-only `TaskCriteriaSnapshot` per fresh IN_REVIEW entry — a reopen→re-deliver appends another), it discloses two compositions and deliberately stops there. There is **no single fidelity score and no good/bad direction verdict** — the read-out is purely mechanical (timing + op + checkpointer grounding; it never reads statement or diff semantics).
|
|
13
|
+
|
|
14
|
+
### 1. Grounding composition
|
|
15
|
+
|
|
16
|
+
Runs the same `effectiveReferentKind` classifier the LUM-605 verify gate uses over the **frozen** contract (delivery-time truth, not the since-edited `latest`), rolled into three buckets plus the full per-kind tally:
|
|
17
|
+
|
|
18
|
+
- **grounded** — `EXTERNAL_FACT` (anchored on a fact the agent cannot author: a git SHA/diff, CI status, a DB row). This count is a **machine-checkable _upper bound_** — the checkpointer heuristic can over-count external grounding, never under-count it.
|
|
19
|
+
- **self-confirming** — `AGENT_CONSTRUCTED_STATE` + `UNVERIFIED_ASSERTION` (passes by checking the agent's own output, incl. a declared-EXTERNAL_FACT checkpointer that doesn't truly ground external).
|
|
20
|
+
- **inconclusive** — `PENDING_OUTCOME` + `UNCLASSIFIED` (not mechanically resolvable here). **Surfaced, never silently swallowed.**
|
|
21
|
+
|
|
22
|
+
### 2. Independence signal
|
|
23
|
+
|
|
24
|
+
A criterion is a **backward-inference suspect** when it is both (a) in this delivery's frozen contract and (b) was `ADDED`/`UPDATED` at **round>0** (after work started) at or before the freeze — i.e. the contract the delivery rode on was bent after the work began rather than fixed up front. round-0 (initial-draft) criteria are clean; `DELETED` ops and edits recorded after the freeze (a later cycle) don't count. Each suspect shows its `op`, `round`, and `causeTag`. The read-out **counts and discloses — it does not judge** whether a given edit was legitimate sharpening or tampering (that direction call is left to a human / a later axis).
|
|
25
|
+
|
|
26
|
+
### Caveat (printed on every report)
|
|
27
|
+
|
|
28
|
+
The time anchor is the **work-start boundary** (`Task.workStartedAt`), so the independence signal detects a contract edited _after work started_ — not specifically _after output was produced_. A task with **no delivery snapshot** (never entered IN_REVIEW, or predates the LUM-609 freeze) is reported explicitly — "nothing to read yet", which is **not** a pass.
|
|
29
|
+
|
|
30
|
+
`--json` emits the full report (`taskId`, `workStartedAt`, `hasSnapshots`, `deliveries[]` with `grounding`/`independence`, `caveat`) for scripting.
|
|
31
|
+
|
|
32
|
+
**When to suggest**: when the user wants to see how externally-grounded a task's delivered acceptance contract actually was, or whether criteria were added/changed after work began (independence). It's a disclosure tool, not a gate — it never blocks DONE and emits no verdict.
|
|
@@ -40,7 +40,7 @@ lumo outcome record LUM-42 --note "downstream rewrote the API shape this task in
|
|
|
40
40
|
|
|
41
41
|
## `lumo outcome rate [--min N] [--json]`
|
|
42
42
|
|
|
43
|
-
The workspace-level **trust × post-hoc-fate** join (LUM-603). For every delivery that has a frozen forecast snapshot, it buckets the delivery by its **delivery-time
|
|
43
|
+
The workspace-level **trust × post-hoc-fate** join (LUM-603, LUM-606). For every delivery that has a frozen forecast snapshot, it buckets the delivery by its **delivery-time verification friction** — two raw counts (`verificationRounds`, `sendBackCount`) frozen at delivery from the append-only `VerificationRun` log, cut at `capturedAt`, then bucketed value-neutrally into `no-verification` / `single-round` / `multi-round-or-sendback` — and reports each bucket's post-hoc **REJECTED** rate from the well. The friction axis replaced the original sample-size forecast-confidence axis (LUM-606), which saturated: every mature-project delivery landed in `high`, so `high`-vs-`low` could never compare. The question it answers once data is sufficient: _do deliveries that took more verification friction get rejected by reality more (fragility) or less (scrutiny) often?_ The bucket names carry **no** good/bad direction — the correlation, not the label, decides.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
lumo outcome rate
|
|
@@ -49,17 +49,17 @@ lumo outcome rate --min 20 --json
|
|
|
49
49
|
|
|
50
50
|
This is the falsifier discipline applied at the **query layer** — it refuses to manufacture a finding off a thin sample:
|
|
51
51
|
|
|
52
|
-
- A
|
|
53
|
-
- The per-
|
|
54
|
-
- The
|
|
52
|
+
- A bucket with fewer than `--min` (default 10) deliveries reads **`insufficient`** with the rate withheld — the raw `rejected/delivered` counts are still shown, but never divided into a believable proportion.
|
|
53
|
+
- The per-bucket rate is an explicit **lower bound** on true failure: the well only records rejections, so a zero-signal delivery is `INCONCLUSIVE` (counted in the denominator), never "satisfied".
|
|
54
|
+
- The **`single-round` vs `multi-round-or-sendback`** comparison asserts a direction **only** when both buckets are measured _and_ their 95% confidence intervals are disjoint; otherwise it stays **`inconclusive`**. `no-verification` is never a comparison arm (silence is not a friction level).
|
|
55
55
|
|
|
56
|
-
**Expect `inconclusive` today.** The well is freshly wired and
|
|
56
|
+
**Expect `inconclusive` today.** The well is freshly wired and the discriminating end of the friction axis is sparse (multi-round/send-back deliveries are rare) — that is the designed state, not a bug. The same command produces real strata once enough post-hoc signals accumulate against the populated friction buckets (the deferred outcome the skeleton exists to read).
|
|
57
57
|
|
|
58
|
-
> Scope note
|
|
58
|
+
> Scope note: friction is frozen **at delivery** and cut at `capturedAt`, so it is replayable and never reads a drifted current value. A delivery with no `VerificationRun` before capture is its own `no-verification` bucket — absence of verification is never folded into `single-round` (silence ≠ a clean pass).
|
|
59
59
|
|
|
60
60
|
## When to suggest
|
|
61
61
|
|
|
62
62
|
- A teammate says a shipped task **was reverted / rolled back / broke prod / got redone / was bypassed** — record it with `lumo outcome record` so the well carries reality's verdict.
|
|
63
63
|
- Auditing whether a "done" task actually held up — `lumo outcome show <task>` reads the well.
|
|
64
|
-
- Asking whether **
|
|
64
|
+
- Asking whether **verification friction predicts post-hoc fate** (do harder-to-verify deliveries fail more, or less?) — `lumo outcome rate`. Expect `inconclusive` until the well fills; it will never print a correlation the sample can't support.
|
|
65
65
|
- **Never** use it to mark something good: the well has no positive verdict by design. To assert a delivery met its contract, that is the acceptance loop (`lumo verify` / `lumo task status`), not the well.
|
|
@@ -21,10 +21,10 @@ lumo verify --note "…" --timeout 900 # per-checkpointer timeout in seconds
|
|
|
21
21
|
**`--note` is required (LUM-597)** whenever a round will actually be posted (the
|
|
22
22
|
task has MACHINE criteria). It is your one-line self-report — what you did and
|
|
23
23
|
why it is ready ("我改了 X,因为 Y 可验收"). When the round all-passes and the
|
|
24
|
-
task flips to
|
|
24
|
+
task flips to IN*REVIEW, the note is frozen as the task's **claim** (provenance
|
|
25
25
|
`AGENT` — the汇报者's own voice, not a summarizer paraphrase) and is checked
|
|
26
26
|
against the diff for faithfulness. Capturing the claim is deterministic ("不填不
|
|
27
|
-
让 post"); whether it is
|
|
27
|
+
让 post"); whether it is \_truthful* is the faithfulness audit's job — so don't
|
|
28
28
|
under- or over-state it. A missing/blank note is refused **before** the round is
|
|
29
29
|
posted, so no round is burned; just re-run with one. (An old CLI that omits it
|
|
30
30
|
still verifies — the claim degrades to the synthesized run-summary fallback.)
|
|
@@ -61,15 +61,16 @@ errors.
|
|
|
61
61
|
|
|
62
62
|
## Edge cases
|
|
63
63
|
|
|
64
|
-
| Case | Behavior
|
|
65
|
-
| ----------------------------------------------- |
|
|
66
|
-
| **No contract yet** | Error pointing at `lumo task criteria set`; draft the contract first (criteria.md golden rule).
|
|
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.
|
|
68
|
-
| **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds.
|
|
69
|
-
| **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round.
|
|
70
|
-
| **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright.
|
|
71
|
-
| **
|
|
72
|
-
| **
|
|
64
|
+
| Case | Behavior |
|
|
65
|
+
| ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
66
|
+
| **No contract yet** | Error pointing at `lumo task criteria set`; draft the contract first (criteria.md golden rule). |
|
|
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. |
|
|
68
|
+
| **Partial round** | A round must cover every MACHINE criterion; the CLI always runs all of them and the server rejects partial rounds. |
|
|
69
|
+
| **`REVIEW_ADDED` criteria** | Criteria added during review appear in the contract and are picked up automatically by the next round. |
|
|
70
|
+
| **Session bound to a different task** | Server returns 409, surfaced as an error. No advisory printed; the verify round is rejected outright. |
|
|
71
|
+
| **Ungrounded EXTERNAL_FACT PASS** (LUM-605) | A reported PASS on a criterion declared `EXTERNAL_FACT` whose checkpointer doesn't invoke external tooling (e.g. `npx jest …`) is blocked with **422 — no round burned**; the task does **not** move to IN_REVIEW. Fix it for free: re-ground the checkpointer against a real external fact, or honestly re-declare `referentKind` as `AGENT_CONSTRUCTED_STATE` (`lumo task criteria set`), then re-run. See criteria.md "referentKind". |
|
|
72
|
+
| **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. |
|
|
73
|
+
| **Unconfirmed session binding** | `bindingAdvisory: 'unconfirmed'` → softer advisory `⚠ Could not confirm this session is attached to the task.` Same remediation: `lumo session attach <LUM-N>`. |
|
|
73
74
|
|
|
74
75
|
## Round discipline
|
|
75
76
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fidelityShow = fidelityShow;
|
|
4
|
+
const config_1 = require("../lib/config");
|
|
5
|
+
const api_1 = require("../lib/api");
|
|
6
|
+
const sanitize_1 = require("../lib/sanitize");
|
|
7
|
+
/**
|
|
8
|
+
* `lumo fidelity show <task>` — the LUM-610 mechanical change-pattern read-out
|
|
9
|
+
* (fidelity axis, second block, on the LUM-609 delivery-snapshot foundation).
|
|
10
|
+
*
|
|
11
|
+
* Per delivery snapshot it discloses two compositions and NOTHING else — no
|
|
12
|
+
* single score, no good/bad verdict (approach (a): purely mechanical):
|
|
13
|
+
* • grounding — how much of the FROZEN contract is externally grounded
|
|
14
|
+
* (EXTERNAL_FACT) vs self-confirming (agent-authored) vs inconclusive.
|
|
15
|
+
* `grounded` is a machine-checkable UPPER BOUND.
|
|
16
|
+
* • independence — criteria ADDED/UPDATED after work started (round>0) that
|
|
17
|
+
* the delivery rode on: backward-inference suspects. Direction left blank.
|
|
18
|
+
*/
|
|
19
|
+
const EFFECTIVE_KINDS = [
|
|
20
|
+
'EXTERNAL_FACT',
|
|
21
|
+
'AGENT_CONSTRUCTED_STATE',
|
|
22
|
+
'UNVERIFIED_ASSERTION',
|
|
23
|
+
'PENDING_OUTCOME',
|
|
24
|
+
'UNCLASSIFIED',
|
|
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
|
+
function formatReport(report, taskId) {
|
|
40
|
+
const lines = [];
|
|
41
|
+
lines.push(`Fidelity change-pattern — ${(0, sanitize_1.sanitizeField)(taskId)}`);
|
|
42
|
+
if (!report.hasSnapshots) {
|
|
43
|
+
lines.push(' No delivery snapshot on record (task never entered IN_REVIEW, or ' +
|
|
44
|
+
'predates the LUM-609 freeze). Nothing to read yet — not a pass.');
|
|
45
|
+
lines.push('');
|
|
46
|
+
lines.push(` ${report.caveat}`);
|
|
47
|
+
return lines.join('\n') + '\n';
|
|
48
|
+
}
|
|
49
|
+
report.deliveries.forEach((d, i) => {
|
|
50
|
+
const when = d.frozenAt.slice(0, 19).replace('T', ' ');
|
|
51
|
+
lines.push('');
|
|
52
|
+
lines.push(` Delivery ${i + 1}/${report.deliveries.length} · frozen ${when}Z · ${(0, sanitize_1.sanitizeField)(d.trigger)} · ${d.total} criteria`);
|
|
53
|
+
const g = d.grounding;
|
|
54
|
+
lines.push(` grounding: grounded ${g.grounded} · self-confirming ${g.selfConfirming} · inconclusive ${g.inconclusive} (grounded = machine-checkable upper bound)`);
|
|
55
|
+
const kinds = EFFECTIVE_KINDS.filter(k => g.byEffectiveKind[k] > 0)
|
|
56
|
+
.map(k => `${k} ${g.byEffectiveKind[k]}`)
|
|
57
|
+
.join(', ');
|
|
58
|
+
lines.push(` effective kinds: ${kinds || '(none)'}`);
|
|
59
|
+
const ind = d.independence;
|
|
60
|
+
lines.push(` independence: ${ind.postStartChanges}/${ind.total} criteria ADDED/UPDATED after work started (backward-inference suspects; direction not judged)`);
|
|
61
|
+
for (const s of ind.suspects) {
|
|
62
|
+
const cause = s.causeTag ? ` · ${(0, sanitize_1.sanitizeField)(s.causeTag)}` : '';
|
|
63
|
+
lines.push(` ↳ ${(0, sanitize_1.sanitizeField)(s.criterionId)} ${s.op}@round${s.round}${cause} (${s.at.slice(0, 10)})`);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
lines.push('');
|
|
67
|
+
lines.push(` ${report.caveat}`);
|
|
68
|
+
return lines.join('\n') + '\n';
|
|
69
|
+
}
|
|
70
|
+
async function fidelityShow(taskId, options = {}) {
|
|
71
|
+
if (!taskId || taskId.trim() === '') {
|
|
72
|
+
console.error('Error: a task is required: lumo fidelity show <task>');
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
const auth = authBase();
|
|
76
|
+
if ('error' in auth) {
|
|
77
|
+
console.error(`Error: ${auth.error}`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
let res;
|
|
81
|
+
try {
|
|
82
|
+
res = await fetch(`${auth.base}/api/tasks/${encodeURIComponent(taskId)}/fidelity`, { headers: auth.headers });
|
|
83
|
+
}
|
|
84
|
+
catch (err) {
|
|
85
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
86
|
+
console.error(`Error: could not reach Lumo API (${msg})`);
|
|
87
|
+
return 1;
|
|
88
|
+
}
|
|
89
|
+
if (res.status === 401) {
|
|
90
|
+
console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
|
|
91
|
+
return 1;
|
|
92
|
+
}
|
|
93
|
+
if (res.status === 404) {
|
|
94
|
+
console.error(`Error: task ${(0, sanitize_1.sanitizeField)(taskId)} not found.`);
|
|
95
|
+
return 1;
|
|
96
|
+
}
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
console.error(`Error: could not read the fidelity change-pattern (HTTP ${res.status}).`);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
const report = (await res.json());
|
|
102
|
+
if (options.json) {
|
|
103
|
+
process.stdout.write(JSON.stringify(report, null, 2) + '\n');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
process.stdout.write(formatReport(report, taskId));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
@@ -141,10 +141,12 @@ async function outcomeShow(taskId) {
|
|
|
141
141
|
}
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
|
+
/** Width of the widest bucket label, for aligned columns. */
|
|
145
|
+
const BUCKET_LABEL_WIDTH = 'multi-round-or-sendback'.length;
|
|
144
146
|
/** Render the report client-side (same pattern as `lumo cost` / `outcome show`). */
|
|
145
147
|
function formatRate(report) {
|
|
146
148
|
const lines = [];
|
|
147
|
-
lines.push('Trust × post-hoc fate — delivery-time
|
|
149
|
+
lines.push('Trust × post-hoc fate — delivery-time verification friction vs the outcome well');
|
|
148
150
|
lines.push(' (rejection rate is a LOWER BOUND: the well only records rejections; ' +
|
|
149
151
|
'no signal = INCONCLUSIVE, never "satisfied")');
|
|
150
152
|
lines.push(` ${report.totalDeliveries} snapshotted deliveries · min ${report.minSamples} per bracket`);
|
|
@@ -157,10 +159,10 @@ function formatRate(report) {
|
|
|
157
159
|
const pct = ((b.rejectionRate ?? 0) * 100).toFixed(1);
|
|
158
160
|
const lo = ((b.interval?.lower ?? 0) * 100).toFixed(1);
|
|
159
161
|
const hi = ((b.interval?.upper ?? 0) * 100).toFixed(1);
|
|
160
|
-
lines.push(` ${b.bracket.padEnd(
|
|
162
|
+
lines.push(` ${b.bracket.padEnd(BUCKET_LABEL_WIDTH)} ≥${pct}% rejected (${b.rejected}/${b.delivered}, 95% CI ${lo}–${hi}%)`);
|
|
161
163
|
}
|
|
162
164
|
else {
|
|
163
|
-
lines.push(` ${b.bracket.padEnd(
|
|
165
|
+
lines.push(` ${b.bracket.padEnd(BUCKET_LABEL_WIDTH)} insufficient (${b.rejected}/${b.delivered} deliveries < ${report.minSamples} — rate withheld)`);
|
|
164
166
|
}
|
|
165
167
|
}
|
|
166
168
|
const verdict = report.comparison.status === 'measured'
|
|
@@ -171,10 +173,12 @@ function formatRate(report) {
|
|
|
171
173
|
}
|
|
172
174
|
/**
|
|
173
175
|
* `lumo outcome rate [--min N] [--json]` — read the workspace trust × post-hoc-
|
|
174
|
-
* fate join: per delivery-time
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* `
|
|
176
|
+
* fate join: per delivery-time verification-friction bucket (LUM-606:
|
|
177
|
+
* no-verification / single-round / multi-round-or-sendback, frozen at delivery),
|
|
178
|
+
* the post-hoc REJECTED rate from the outcome well. Honest by construction — thin
|
|
179
|
+
* buckets read `insufficient` (no fabricated rate) and the single-round
|
|
180
|
+
* vs multi-round-or-sendback comparison stays `inconclusive` until the well has
|
|
181
|
+
* enough signal to tell the buckets apart.
|
|
178
182
|
*/
|
|
179
183
|
async function outcomeRate(options = {}) {
|
|
180
184
|
const auth = authBase();
|
|
@@ -197,6 +197,11 @@ async function taskCriteriaSet(identifier, options) {
|
|
|
197
197
|
if (data.judgeStepsWarning) {
|
|
198
198
|
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.judgeStepsWarning)}\n`);
|
|
199
199
|
}
|
|
200
|
+
// LUM-605: EXTERNAL_FACT criteria whose checkpointer doesn't ground external
|
|
201
|
+
// — stored, but the verify loop will refuse a PASS on them until re-grounded.
|
|
202
|
+
if (data.referentWarning) {
|
|
203
|
+
process.stdout.write(`⚠ ${(0, sanitize_1.sanitizeField)(data.referentWarning)}\n`);
|
|
204
|
+
}
|
|
200
205
|
const readbackIssues = verifyReadback(criteriaItems, data.criteria);
|
|
201
206
|
if (readbackIssues.length > 0) {
|
|
202
207
|
for (const issue of readbackIssues) {
|
package/dist/cli/src/index.js
CHANGED
|
@@ -48,6 +48,7 @@ const session_status_1 = require("./commands/session-status");
|
|
|
48
48
|
const next_1 = require("./commands/next");
|
|
49
49
|
const cost_1 = require("./commands/cost");
|
|
50
50
|
const criteria_audit_1 = require("./commands/criteria-audit");
|
|
51
|
+
const fidelity_1 = require("./commands/fidelity");
|
|
51
52
|
const verify_1 = require("./commands/verify");
|
|
52
53
|
const verdict_1 = require("./commands/verdict");
|
|
53
54
|
const crossing_explain_1 = require("./commands/crossing-explain");
|
|
@@ -285,6 +286,14 @@ criteria
|
|
|
285
286
|
.command('audit')
|
|
286
287
|
.description('Show the workspace referent-kind distribution + self-confirming-green ratio (GET /api/criteria/audit)')
|
|
287
288
|
.action(wrap(() => (0, criteria_audit_1.criteriaAudit)()));
|
|
289
|
+
const fidelity = program
|
|
290
|
+
.command('fidelity')
|
|
291
|
+
.description('Mechanical fidelity read-outs (LUM-610)');
|
|
292
|
+
fidelity
|
|
293
|
+
.command('show <task>')
|
|
294
|
+
.description('Per delivery snapshot (LUM-609): the grounding composition of the frozen contract (grounded / self-confirming / inconclusive) and the independence signal (criteria ADDED/UPDATED after work started = backward-inference suspects). A composition table, not a single score — direction is never judged. inconclusive is surfaced, never swallowed.')
|
|
295
|
+
.option('--json', 'Emit the report as JSON')
|
|
296
|
+
.action(wrap((task, options) => (0, fidelity_1.fidelityShow)(task, options)));
|
|
288
297
|
const session = program
|
|
289
298
|
.command('session')
|
|
290
299
|
.description('Manage per-terminal coding-session context');
|
|
@@ -47,6 +47,20 @@ function wordMatches(word, tools) {
|
|
|
47
47
|
// exact tool, or a tool-prefixed filename like jest-t.ts / vitest.config
|
|
48
48
|
return tools.some(t => word === t || word.startsWith(`${t}-`) || word.startsWith(`${t}.`));
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* No-op external invocations (LUM-605): forms whose exit code and output are
|
|
52
|
+
* constant w.r.t. external state (`git --version`, `gh --help`, …). An external
|
|
53
|
+
* tool invoked this way establishes no external fact, so it must never ground
|
|
54
|
+
* EXTERNAL — this is half of the C-hole fix (the other half is the consumption
|
|
55
|
+
* requirement below). Kept deliberately small/conservative: a smaller set means
|
|
56
|
+
* fewer real external checks get mistaken for no-ops (we'd rather under-flag a
|
|
57
|
+
* decoy than false-downgrade a genuine check, which enforcement now penalizes).
|
|
58
|
+
*/
|
|
59
|
+
const NOOP_FLAG_TOKENS = new Set(['--version', '--help']);
|
|
60
|
+
/** No-op SUBCOMMAND forms — only when they are the first word after the tool
|
|
61
|
+
* (`git version`, `gh help`). Not matched as a bare flag-anywhere, so we don't
|
|
62
|
+
* trip on an unrelated positional like a psql `-h <host>` (host, not help). */
|
|
63
|
+
const NOOP_SUBCOMMANDS = new Set(['version', 'help']);
|
|
50
64
|
/**
|
|
51
65
|
* Given a raw command word (possibly `npx jest`, `npm run build`, etc.),
|
|
52
66
|
* normalize to the effective tool name. Returns null for pure wrapper tokens
|
|
@@ -138,6 +152,143 @@ function collectCommandWords(script) {
|
|
|
138
152
|
}
|
|
139
153
|
return result;
|
|
140
154
|
}
|
|
155
|
+
function isExternalTool(word) {
|
|
156
|
+
return wordMatches(word, EXTERNAL_TOOLS);
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Parse one shell command segment (already split off sequencing/pipe operators)
|
|
160
|
+
* into its effective tool + whether it is a no-op invocation. Mirrors the
|
|
161
|
+
* wrapper-skipping of {@link collectCommandWords} (npx / sudo / command / npm
|
|
162
|
+
* run), then reads the post-tool tokens to flag no-op forms (LUM-605).
|
|
163
|
+
*/
|
|
164
|
+
function parseInvocation(segment) {
|
|
165
|
+
const cleaned = segment
|
|
166
|
+
.replace(/['"]/g, ' ')
|
|
167
|
+
.replace(/\[|\]/g, ' ')
|
|
168
|
+
.replace(/\$\{?[^}]*}?/g, ' ')
|
|
169
|
+
.trim();
|
|
170
|
+
if (!cleaned)
|
|
171
|
+
return null;
|
|
172
|
+
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
|
173
|
+
let i = 0;
|
|
174
|
+
while (i < tokens.length) {
|
|
175
|
+
const lower = tokens[i].toLowerCase();
|
|
176
|
+
if (lower === 'npx' || lower === 'sudo' || lower === 'command') {
|
|
177
|
+
i++;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (lower === 'npm' && tokens[i + 1]?.toLowerCase() === 'run') {
|
|
181
|
+
i += 2;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
if (i >= tokens.length)
|
|
187
|
+
return null;
|
|
188
|
+
const tool = normalizeCommandWord(tokens[i].toLowerCase());
|
|
189
|
+
const rest = tokens.slice(i + 1).map(t => t.toLowerCase());
|
|
190
|
+
const isNoop = rest.some(t => NOOP_FLAG_TOKENS.has(t)) ||
|
|
191
|
+
(rest.length > 0 && NOOP_SUBCOMMANDS.has(rest[0]));
|
|
192
|
+
return { tool, isNoop };
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Does this script contain a CONSUMED, non-no-op external command (LUM-605)?
|
|
196
|
+
* "Consumed" — the discriminator that closes the C hole — means the external
|
|
197
|
+
* command's output or exit actually reaches the check's verdict:
|
|
198
|
+
* - captured in a `$(...)` / backtick substitution (output → data → assertion);
|
|
199
|
+
* - piped into a downstream stage (`ext | …`);
|
|
200
|
+
* - the terminal command of the whole script (its exit status IS the verdict).
|
|
201
|
+
* A non-terminal external word whose output is discarded (the decoy
|
|
202
|
+
* `git --version && cat frozen.txt | grep`), or any no-op invocation, does NOT
|
|
203
|
+
* ground EXTERNAL.
|
|
204
|
+
*/
|
|
205
|
+
function groundsExternal(script) {
|
|
206
|
+
const noComments = script.replace(/(^|\s)#[^\n]*/gm, ' ');
|
|
207
|
+
return scanScript(noComments);
|
|
208
|
+
}
|
|
209
|
+
/** Pull out substitutions / `bash -c` payloads (recursing into them) then walk
|
|
210
|
+
* statements → `&&`/`||` segments → pipeline stages to test each consumed slot. */
|
|
211
|
+
function scanScript(text) {
|
|
212
|
+
let remaining = text;
|
|
213
|
+
let foundInNested = false;
|
|
214
|
+
remaining = remaining.replace(/\$\(([^)]*)\)/g, (_m, inner) => {
|
|
215
|
+
if (scanConsumed(inner))
|
|
216
|
+
foundInNested = true;
|
|
217
|
+
return ' ';
|
|
218
|
+
});
|
|
219
|
+
remaining = remaining.replace(/`([^`]*)`/g, (_m, inner) => {
|
|
220
|
+
if (scanConsumed(inner))
|
|
221
|
+
foundInNested = true;
|
|
222
|
+
return ' ';
|
|
223
|
+
});
|
|
224
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+'([^']*)'/g, (_m, payload) => {
|
|
225
|
+
if (scanScript(payload))
|
|
226
|
+
foundInNested = true;
|
|
227
|
+
return ' ';
|
|
228
|
+
});
|
|
229
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+"([^"]*)"/g, (_m, payload) => {
|
|
230
|
+
if (scanScript(payload))
|
|
231
|
+
foundInNested = true;
|
|
232
|
+
return ' ';
|
|
233
|
+
});
|
|
234
|
+
if (foundInNested)
|
|
235
|
+
return true;
|
|
236
|
+
const statements = remaining
|
|
237
|
+
.split(/;|\n/)
|
|
238
|
+
.map(s => s.trim())
|
|
239
|
+
.filter(Boolean);
|
|
240
|
+
for (let si = 0; si < statements.length; si++) {
|
|
241
|
+
const terminalStatement = si === statements.length - 1;
|
|
242
|
+
const segments = statements[si].split(/&&|\|\|/)
|
|
243
|
+
.map(s => s.trim())
|
|
244
|
+
.filter(Boolean);
|
|
245
|
+
for (let gi = 0; gi < segments.length; gi++) {
|
|
246
|
+
const terminalSegment = gi === segments.length - 1;
|
|
247
|
+
const stages = segments[gi].split(/\|/)
|
|
248
|
+
.map(s => s.trim())
|
|
249
|
+
.filter(Boolean);
|
|
250
|
+
for (let pi = 0; pi < stages.length; pi++) {
|
|
251
|
+
const finalStage = pi === stages.length - 1;
|
|
252
|
+
const inv = parseInvocation(stages[pi]);
|
|
253
|
+
if (!inv || !isExternalTool(inv.tool) || inv.isNoop)
|
|
254
|
+
continue;
|
|
255
|
+
const pipedIntoConsumer = !finalStage;
|
|
256
|
+
const exitIsVerdict = finalStage && terminalSegment && terminalStatement;
|
|
257
|
+
if (pipedIntoConsumer || exitIsVerdict)
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
/** Substitution context: the captured output is data fed to the surrounding
|
|
265
|
+
* assertion, so a non-no-op external command word in ANY position is consumed. */
|
|
266
|
+
function scanConsumed(inner) {
|
|
267
|
+
let remaining = inner;
|
|
268
|
+
let found = false;
|
|
269
|
+
remaining = remaining.replace(/\$\(([^)]*)\)/g, (_m, x) => {
|
|
270
|
+
if (scanConsumed(x))
|
|
271
|
+
found = true;
|
|
272
|
+
return ' ';
|
|
273
|
+
});
|
|
274
|
+
remaining = remaining.replace(/`([^`]*)`/g, (_m, x) => {
|
|
275
|
+
if (scanConsumed(x))
|
|
276
|
+
found = true;
|
|
277
|
+
return ' ';
|
|
278
|
+
});
|
|
279
|
+
if (found)
|
|
280
|
+
return true;
|
|
281
|
+
const segments = remaining
|
|
282
|
+
.split(/&&|\|\||;|\||\n/)
|
|
283
|
+
.map(s => s.trim())
|
|
284
|
+
.filter(Boolean);
|
|
285
|
+
for (const seg of segments) {
|
|
286
|
+
const inv = parseInvocation(seg);
|
|
287
|
+
if (inv && isExternalTool(inv.tool) && !inv.isNoop)
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
141
292
|
function classifyCheckpointerGrounding(checkpointer) {
|
|
142
293
|
if (!checkpointer || checkpointer.trim() === '')
|
|
143
294
|
return 'NONE';
|
|
@@ -145,11 +296,13 @@ function classifyCheckpointerGrounding(checkpointer) {
|
|
|
145
296
|
// Fast path for known two-word external phrases (e.g. prisma migrate status)
|
|
146
297
|
if (EXTERNAL_PHRASES.some(p => lower.includes(p)))
|
|
147
298
|
return 'EXTERNAL';
|
|
148
|
-
const words = collectCommandWords(
|
|
299
|
+
const words = collectCommandWords(lower);
|
|
149
300
|
if (words.length === 0)
|
|
150
301
|
return 'NONE';
|
|
151
|
-
// Strongest signal wins: EXTERNAL > AGENT_HARNESS > ASSERTION_ONLY
|
|
152
|
-
|
|
302
|
+
// Strongest signal wins: EXTERNAL > AGENT_HARNESS > ASSERTION_ONLY. EXTERNAL
|
|
303
|
+
// is gated on a CONSUMED, non-no-op external command (LUM-605) — a no-op or
|
|
304
|
+
// output-discarded external word falls through to harness/assertion.
|
|
305
|
+
if (groundsExternal(lower))
|
|
153
306
|
return 'EXTERNAL';
|
|
154
307
|
if (words.some(w => wordMatches(w, HARNESS_TOOLS)))
|
|
155
308
|
return 'AGENT_HARNESS';
|