@lumoai/cli 1.47.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 +30 -0
- package/assets/skill/references/outcome.md +65 -0
- 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/index.js +29 -0
- package/dist/shared/src/referent-kind.js +168 -0
- package/package.json +1 -1
package/assets/skill/SKILL.md
CHANGED
|
@@ -31,6 +31,7 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
31
31
|
| `task criteria set/list`, drafting the acceptance contract | [references/criteria.md](references/criteria.md) |
|
|
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
|
+
| `outcome show/record/rate` — post-hoc outcome well (falsifier oracle, rejection-only) | [references/outcome.md](references/outcome.md) |
|
|
34
35
|
| `project list`, `milestone*` | [references/milestones.md](references/milestones.md) |
|
|
35
36
|
| `doc create/update/list/move/bind/share/import` (CRUD) | [references/docs.md](references/docs.md) |
|
|
36
37
|
| `doc show --raw/--section`, `doc patch/append/diff/rebuild-source` (editing live docs) | [references/doc-editing.md](references/doc-editing.md) |
|
|
@@ -92,6 +93,16 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
92
93
|
|
|
93
94
|
- `lumo cost [--task <id>|--session <id>|--since <date>] [--by tool|model|member|session] [--json]` — per-operation token cost read-out, attributing each model step's token delta to the tool(s) it ran. Scope is mutually exclusive (default = workspace last-30-days). For the per-task Top-5 inline, see `lumo task lineage`.
|
|
94
95
|
|
|
96
|
+
**Criteria audit** — workspace-level referent-kind analytics
|
|
97
|
+
|
|
98
|
+
- `lumo criteria audit` — workspace-level read-out: count of acceptance criteria by effective referent kind (EXTERNAL_FACT / AGENT_CONSTRUCTED_STATE / PENDING_OUTCOME / UNVERIFIED_ASSERTION / UNCLASSIFIED) + the self-confirming-green ratio
|
|
99
|
+
|
|
100
|
+
**Outcome well (post-hoc oracle)** — see [outcome.md](references/outcome.md)
|
|
101
|
+
|
|
102
|
+
- `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
|
+
- `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 forecast-confidence bracket (high/medium/low), the post-hoc REJECTED rate from the well. Honest by construction — thin brackets read `insufficient` (no fabricated rate), the high-vs-low comparison stays `inconclusive` until CIs separate, and the rate is a **lower bound** (no signal = INCONCLUSIVE, never satisfied). Under-sampled today by design — the skeleton produces real strata once the well fills.
|
|
105
|
+
|
|
95
106
|
**Artifacts & Figma** — see [artifacts-figma.md](references/artifacts-figma.md)
|
|
96
107
|
|
|
97
108
|
- `lumo task artifact add/update/list/show/rm` — record spec/plan products on a task
|
|
@@ -146,6 +157,25 @@ The command catalog below is a **map**: it lists every command grouped by domain
|
|
|
146
157
|
- `lumo worktree rm <LUM-N> --yes` — remove a worktree (keeps the branch unless `--delete-branch`)
|
|
147
158
|
- `lumo worktree list` — list `.worktrees/` worktrees (task id, branch, dirty, node_modules link)
|
|
148
159
|
|
|
160
|
+
## Criteria audit
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
lumo criteria audit
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`lumo criteria audit` — workspace-level acceptance-criteria analytics. Calls `GET /api/criteria/audit` and prints two things:
|
|
167
|
+
|
|
168
|
+
1. **Declared-kind distribution** — count of criteria by effective referent kind:
|
|
169
|
+
- `EXTERNAL_FACT` — declared as externally observable (git SHA, CI status, DB row) and the checkpointer invokes real external tooling
|
|
170
|
+
- `AGENT_CONSTRUCTED_STATE` — passes by checking output the agent itself produced
|
|
171
|
+
- `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 by the audit
|
|
173
|
+
- `UNCLASSIFIED` — legacy rows predating the `referentKind` field (null in DB)
|
|
174
|
+
|
|
175
|
+
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.
|
|
176
|
+
|
|
177
|
+
**When to suggest**: when the user wants to audit how much of the workspace's "green" criteria are self-confirming vs externally anchored; when diagnosing whether acceptance criteria have real external hooks or are circular.
|
|
178
|
+
|
|
149
179
|
## Commands & flags that do NOT exist (common mistakes)
|
|
150
180
|
|
|
151
181
|
Don't guess these:
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Outcome well (post-hoc oracle) — `lumo outcome`
|
|
2
|
+
|
|
3
|
+
The **post-hoc outcome well** (LUM-598) is the single external oracle for correctness + fidelity. It records the real-world fate a delivery met **after** it shipped — facts the agent cannot construct and that occur post-delivery (a revert, a rollback, CI turning red, a reopen, a downstream redirect of the design).
|
|
4
|
+
|
|
5
|
+
## The one invariant: falsifier, not verifier
|
|
6
|
+
|
|
7
|
+
The well **only** ever produces a rejection. Its verdict space is exactly:
|
|
8
|
+
|
|
9
|
+
- `REJECTED` — the well holds ≥1 rejection signal (reality revoked / redid / bypassed the delivery).
|
|
10
|
+
- `INCONCLUSIVE` — no rejection on record.
|
|
11
|
+
|
|
12
|
+
There is **no `SATISFIED` / `PASS`**. "N sprints and nobody touched it" is the _absence_ of a rejection — satisfied, merely-tolerated, and never-noticed are externally indistinguishable — so it reads `INCONCLUSIVE`, never a pass. Treat `INCONCLUSIVE` as "could not confirm a rejection", not as approval.
|
|
13
|
+
|
|
14
|
+
## Collection (where signals come from)
|
|
15
|
+
|
|
16
|
+
- **Automatic** — a DONE task reopened (the lineage `REWORKED` transition) is folded into the well as a `REOPENED` signal. No action needed; it happens server-side.
|
|
17
|
+
- **Manual** — a human who observed reality reject a delivery records it with `lumo outcome record`. The `--note` is mandatory: it is the observed referent, frozen as evidence.
|
|
18
|
+
|
|
19
|
+
## `lumo outcome show <task>`
|
|
20
|
+
|
|
21
|
+
Read the well for a task — the verdict plus its backing signals.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
lumo outcome show LUM-42
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
An empty well prints `INCONCLUSIVE` with the reminder that silence is not a pass. A non-empty well prints `REJECTED` and lists each signal (date, kind, source, note).
|
|
28
|
+
|
|
29
|
+
## `lumo outcome record <task> --note "<what reality did>" [--kind <kind>]`
|
|
30
|
+
|
|
31
|
+
Record one human-observed post-hoc rejection. Append-only — every call inserts a new signal; nothing is ever mutated or cleared.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
lumo outcome record LUM-42 --note "reverted in #812 after a prod incident" --kind reverted
|
|
35
|
+
lumo outcome record LUM-42 --note "downstream rewrote the API shape this task introduced" --kind downstream_redirect
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
- `--kind` (case-insensitive, default `manual`): `reverted | rolled_back | ci_regression | downstream_redirect | bypassed | manual`. `reopened` is **not** a manual kind — it is owned by the automatic lineage fold.
|
|
39
|
+
- `--occurred-at <iso>` — when the event actually happened (defaults to now).
|
|
40
|
+
|
|
41
|
+
## `lumo outcome rate [--min N] [--json]`
|
|
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 forecast-confidence** (high / medium / low — the only trust axis frozen at delivery, recomputed from the snapshot's `cohortSize` / `tokenCostBasis`) and reports each bracket's post-hoc **REJECTED** rate from the well. The question it answers once data is sufficient: _do high-trust deliveries get rejected by reality less often than low-trust ones?_
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
lumo outcome rate
|
|
47
|
+
lumo outcome rate --min 20 --json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
This is the falsifier discipline applied at the **query layer** — it refuses to manufacture a finding off a thin sample:
|
|
51
|
+
|
|
52
|
+
- A bracket 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-bracket 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 headline **high-vs-low** comparison asserts a direction **only** when both brackets are measured _and_ their 95% confidence intervals are disjoint; otherwise it stays **`inconclusive`**.
|
|
55
|
+
|
|
56
|
+
**Expect `inconclusive` today.** The well is freshly wired and under-sampled — that is the designed state, not a bug. The same command produces real strata once enough post-hoc signals accumulate (the deferred outcome the skeleton exists to read).
|
|
57
|
+
|
|
58
|
+
> Scope note (debt-a): the trust axis is **task-level** forecast confidence only. Fragment-level trust is not frozen at delivery and drifts, so it is deliberately **not** joined here — doing so would read a post-hoc value against a past delivery. Freezing fragment trust is a separate follow-up.
|
|
59
|
+
|
|
60
|
+
## When to suggest
|
|
61
|
+
|
|
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
|
+
- Auditing whether a "done" task actually held up — `lumo outcome show <task>` reads the well.
|
|
64
|
+
- Asking whether **higher-confidence deliveries actually fail less** (does delivery-time trust predict post-hoc fate?) — `lumo outcome rate`. Expect `inconclusive` until the well fills; it will never print a correlation the sample can't support.
|
|
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.
|
|
@@ -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
|
}
|
package/dist/cli/src/index.js
CHANGED
|
@@ -47,9 +47,11 @@ const session_attach_1 = require("./commands/session-attach");
|
|
|
47
47
|
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
|
+
const criteria_audit_1 = require("./commands/criteria-audit");
|
|
50
51
|
const verify_1 = require("./commands/verify");
|
|
51
52
|
const verdict_1 = require("./commands/verdict");
|
|
52
53
|
const crossing_explain_1 = require("./commands/crossing-explain");
|
|
54
|
+
const outcome_1 = require("./commands/outcome");
|
|
53
55
|
const task_context_1 = require("./commands/task-context");
|
|
54
56
|
const task_create_1 = require("./commands/task-create");
|
|
55
57
|
const task_update_1 = require("./commands/task-update");
|
|
@@ -242,6 +244,26 @@ crossing
|
|
|
242
244
|
.description('Append an agent self-explanation ("申辩") to a boundary crossing (LUM-542). Append-only and for the human reviewer — it never clears the crossing or unblocks Done (a human dispositions that). Targets a crossing on the session-bound task.')
|
|
243
245
|
.requiredOption('--note <text>', 'The explanation to record (the rationale for the action / why it may be a false positive)')
|
|
244
246
|
.action(wrap((id, options) => (0, crossing_explain_1.crossingExplain)(id, options)));
|
|
247
|
+
const outcome = program
|
|
248
|
+
.command('outcome')
|
|
249
|
+
.description('Read / record the post-hoc outcome well (LUM-598)');
|
|
250
|
+
outcome
|
|
251
|
+
.command('show <task>')
|
|
252
|
+
.description('Read the post-hoc outcome well for a task: the falsifier verdict (REJECTED | INCONCLUSIVE — never a "pass") plus its backing rejection signals. INCONCLUSIVE means no rejection on record, NOT satisfied.')
|
|
253
|
+
.action(wrap((task) => (0, outcome_1.outcomeShow)(task)));
|
|
254
|
+
outcome
|
|
255
|
+
.command('record <task>')
|
|
256
|
+
.description('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.')
|
|
257
|
+
.requiredOption('--note <text>', 'What reality did (the observed referent), e.g. "reverted in #812 after a prod incident"')
|
|
258
|
+
.option('--kind <kind>', 'reverted | rolled_back | ci_regression | downstream_redirect | bypassed | manual (case-insensitive; default: manual)')
|
|
259
|
+
.option('--occurred-at <iso>', 'When the event happened (ISO 8601; defaults to now)')
|
|
260
|
+
.action(wrap((task, options) => (0, outcome_1.outcomeRecord)(task, options)));
|
|
261
|
+
outcome
|
|
262
|
+
.command('rate')
|
|
263
|
+
.description('Trust × post-hoc fate: per delivery-time forecast-confidence bracket, the post-hoc REJECTED rate from the outcome well. Honest by construction — thin brackets read "insufficient" and the high-vs-low comparison stays "inconclusive" until the well has enough signal. The rate is a LOWER BOUND (no signal = INCONCLUSIVE, never satisfied).')
|
|
264
|
+
.option('--min <N>', 'Per-bracket sample floor below which a rate is withheld (default 10)')
|
|
265
|
+
.option('--json', 'Emit the report as JSON')
|
|
266
|
+
.action(wrap(options => (0, outcome_1.outcomeRate)(options)));
|
|
245
267
|
program
|
|
246
268
|
.command('next')
|
|
247
269
|
.description('Recommend the next task(s) to work on, ranked by priority, active sprint, and due date. Prints top N (default 3); pick one and run `session attach` + `task context`.')
|
|
@@ -256,6 +278,13 @@ program
|
|
|
256
278
|
.option('--by <dim>', 'Headline grouping: tool | model | member | session (case-insensitive; default tool)')
|
|
257
279
|
.option('--json', 'Emit the versioned payload as JSON')
|
|
258
280
|
.action(wrap(options => (0, cost_1.cost)(options)));
|
|
281
|
+
const criteria = program
|
|
282
|
+
.command('criteria')
|
|
283
|
+
.description('Workspace-level acceptance-criteria analytics');
|
|
284
|
+
criteria
|
|
285
|
+
.command('audit')
|
|
286
|
+
.description('Show the workspace referent-kind distribution + self-confirming-green ratio (GET /api/criteria/audit)')
|
|
287
|
+
.action(wrap(() => (0, criteria_audit_1.criteriaAudit)()));
|
|
259
288
|
const session = program
|
|
260
289
|
.command('session')
|
|
261
290
|
.description('Manage per-terminal coding-session context');
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.REFERENT_KIND_DECLARABLE = void 0;
|
|
4
|
+
exports.classifyCheckpointerGrounding = classifyCheckpointerGrounding;
|
|
5
|
+
exports.effectiveReferentKind = effectiveReferentKind;
|
|
6
|
+
/**
|
|
7
|
+
* referentKind (LUM-602): what a criterion anchors — the only dimension that
|
|
8
|
+
* determines independence. Dependency-light (no @prisma/client) so both the
|
|
9
|
+
* Next.js server and the CLI can import it; the Prisma `ReferentKind` enum
|
|
10
|
+
* mirrors REFERENT_KIND_DECLARABLE exactly.
|
|
11
|
+
*/
|
|
12
|
+
exports.REFERENT_KIND_DECLARABLE = [
|
|
13
|
+
'EXTERNAL_FACT',
|
|
14
|
+
'AGENT_CONSTRUCTED_STATE',
|
|
15
|
+
'PENDING_OUTCOME',
|
|
16
|
+
];
|
|
17
|
+
/** Tools whose output the agent cannot author — external facts. */
|
|
18
|
+
const EXTERNAL_TOOLS = [
|
|
19
|
+
'git',
|
|
20
|
+
'gh',
|
|
21
|
+
'curl',
|
|
22
|
+
'wget',
|
|
23
|
+
'http',
|
|
24
|
+
'https',
|
|
25
|
+
'psql',
|
|
26
|
+
'dig',
|
|
27
|
+
'nc',
|
|
28
|
+
];
|
|
29
|
+
/** Agent's own test/type/build harness — green here is self-confirming. */
|
|
30
|
+
const HARNESS_TOOLS = [
|
|
31
|
+
'jest',
|
|
32
|
+
'vitest',
|
|
33
|
+
'mocha',
|
|
34
|
+
'pytest',
|
|
35
|
+
'tsc',
|
|
36
|
+
'node',
|
|
37
|
+
'ts-node',
|
|
38
|
+
'tsx',
|
|
39
|
+
];
|
|
40
|
+
/**
|
|
41
|
+
* `prisma migrate status` is EXTERNAL (queries DB/migration history); a bare
|
|
42
|
+
* `prisma generate` is harness. Matched as a two-word phrase before the
|
|
43
|
+
* single-word scan.
|
|
44
|
+
*/
|
|
45
|
+
const EXTERNAL_PHRASES = ['prisma migrate status'];
|
|
46
|
+
function wordMatches(word, tools) {
|
|
47
|
+
// exact tool, or a tool-prefixed filename like jest-t.ts / vitest.config
|
|
48
|
+
return tools.some(t => word === t || word.startsWith(`${t}-`) || word.startsWith(`${t}.`));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Given a raw command word (possibly `npx jest`, `npm run build`, etc.),
|
|
52
|
+
* normalize to the effective tool name. Returns null for pure wrapper tokens
|
|
53
|
+
* (npx, sudo, command) that should cause us to advance to the next word.
|
|
54
|
+
*/
|
|
55
|
+
function normalizeCommandWord(word) {
|
|
56
|
+
// strip a leading path: scripts/jest-t.ts → jest-t.ts (keep basename)
|
|
57
|
+
const slash = word.lastIndexOf('/');
|
|
58
|
+
return slash >= 0 ? word.slice(slash + 1) : word;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract command-position words from a shell script string.
|
|
62
|
+
*
|
|
63
|
+
* Algorithm:
|
|
64
|
+
* 1. Strip `#` comments (everything from # to end of line).
|
|
65
|
+
* 2. Extract `$(...)` and backtick substitutions — they are command
|
|
66
|
+
* lists; recurse into them and add their command-position words.
|
|
67
|
+
* 3. For `bash -c '...'` / `sh -c '...'` tokens, extract the quoted
|
|
68
|
+
* payload and recurse into it as a command list.
|
|
69
|
+
* 4. Split the remaining text on command separators (|, &&, ||, ;, \n)
|
|
70
|
+
* to get individual command segments.
|
|
71
|
+
* 5. From each segment, take the first meaningful word as the command.
|
|
72
|
+
* Skip wrapper words (npx, sudo, command) and "npm run" pairs.
|
|
73
|
+
*/
|
|
74
|
+
function collectCommandWords(script) {
|
|
75
|
+
const result = [];
|
|
76
|
+
// Step 1: strip # comments (not inside quotes — simple heuristic: strip
|
|
77
|
+
// from # that is preceded by whitespace or start-of-line to end of line)
|
|
78
|
+
const noComments = script.replace(/(^|\s)#[^\n]*/gm, ' ');
|
|
79
|
+
// Step 2: recurse into $(...) substitutions
|
|
80
|
+
// We use a simple balanced-paren extractor.
|
|
81
|
+
let remaining = noComments;
|
|
82
|
+
remaining = remaining.replace(/\$\(([^)]*)\)/g, (_match, inner) => {
|
|
83
|
+
result.push(...collectCommandWords(inner));
|
|
84
|
+
return ' ';
|
|
85
|
+
});
|
|
86
|
+
// Recurse into backtick substitutions
|
|
87
|
+
remaining = remaining.replace(/`([^`]*)`/g, (_match, inner) => {
|
|
88
|
+
result.push(...collectCommandWords(inner));
|
|
89
|
+
return ' ';
|
|
90
|
+
});
|
|
91
|
+
// Step 3: handle bash -c / sh -c with a single-quoted payload.
|
|
92
|
+
// Pattern: (bash|sh) ... -c '...' or (bash|sh) ... -c "..."
|
|
93
|
+
// We do this BEFORE splitting on separators so the payload content
|
|
94
|
+
// doesn't pollute the segment split.
|
|
95
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+'([^']*)'/g, (_match, payload) => {
|
|
96
|
+
result.push(...collectCommandWords(payload));
|
|
97
|
+
return ' ';
|
|
98
|
+
});
|
|
99
|
+
remaining = remaining.replace(/\b(?:bash|sh)\b[^'"]*-c\s+"([^"]*)"/g, (_match, payload) => {
|
|
100
|
+
result.push(...collectCommandWords(payload));
|
|
101
|
+
return ' ';
|
|
102
|
+
});
|
|
103
|
+
// Step 4: split on command separators to get segments.
|
|
104
|
+
// Split on |, &&, ||, ;, newlines. Note: || must be checked before |.
|
|
105
|
+
const segments = remaining.split(/\|\||&&|[|;\n]/);
|
|
106
|
+
// Step 5: from each segment extract the command word.
|
|
107
|
+
for (const seg of segments) {
|
|
108
|
+
// Strip leading/trailing whitespace and any residual shell chars
|
|
109
|
+
// (quotes, $, brackets not already consumed).
|
|
110
|
+
const cleaned = seg
|
|
111
|
+
.replace(/['"]/g, ' ') // remove remaining quotes (string args)
|
|
112
|
+
.replace(/\[|\]/g, ' ') // remove [ ] test brackets
|
|
113
|
+
.replace(/\$\{?[^}]*}?/g, ' ') // remove remaining $VAR references
|
|
114
|
+
.trim();
|
|
115
|
+
if (!cleaned)
|
|
116
|
+
continue;
|
|
117
|
+
const tokens = cleaned.split(/\s+/).filter(Boolean);
|
|
118
|
+
// Walk tokens to find the effective command, skipping wrappers.
|
|
119
|
+
let i = 0;
|
|
120
|
+
while (i < tokens.length) {
|
|
121
|
+
const tok = tokens[i];
|
|
122
|
+
const lower = tok.toLowerCase();
|
|
123
|
+
if (lower === 'npx' || lower === 'sudo' || lower === 'command') {
|
|
124
|
+
i++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (lower === 'npm' && tokens[i + 1]?.toLowerCase() === 'run') {
|
|
128
|
+
// "npm run <script>" — script name is the command
|
|
129
|
+
i += 2;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// This is the effective command
|
|
133
|
+
const word = normalizeCommandWord(lower);
|
|
134
|
+
if (word)
|
|
135
|
+
result.push(word);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return result;
|
|
140
|
+
}
|
|
141
|
+
function classifyCheckpointerGrounding(checkpointer) {
|
|
142
|
+
if (!checkpointer || checkpointer.trim() === '')
|
|
143
|
+
return 'NONE';
|
|
144
|
+
const lower = checkpointer.toLowerCase();
|
|
145
|
+
// Fast path for known two-word external phrases (e.g. prisma migrate status)
|
|
146
|
+
if (EXTERNAL_PHRASES.some(p => lower.includes(p)))
|
|
147
|
+
return 'EXTERNAL';
|
|
148
|
+
const words = collectCommandWords(checkpointer.toLowerCase());
|
|
149
|
+
if (words.length === 0)
|
|
150
|
+
return 'NONE';
|
|
151
|
+
// Strongest signal wins: EXTERNAL > AGENT_HARNESS > ASSERTION_ONLY
|
|
152
|
+
if (words.some(w => wordMatches(w, EXTERNAL_TOOLS)))
|
|
153
|
+
return 'EXTERNAL';
|
|
154
|
+
if (words.some(w => wordMatches(w, HARNESS_TOOLS)))
|
|
155
|
+
return 'AGENT_HARNESS';
|
|
156
|
+
return 'ASSERTION_ONLY';
|
|
157
|
+
}
|
|
158
|
+
function effectiveReferentKind(args) {
|
|
159
|
+
const { declared, verifierType, checkpointer } = args;
|
|
160
|
+
if (declared === null)
|
|
161
|
+
return 'UNCLASSIFIED';
|
|
162
|
+
if (declared === 'EXTERNAL_FACT' && verifierType === 'MACHINE') {
|
|
163
|
+
if (classifyCheckpointerGrounding(checkpointer) !== 'EXTERNAL') {
|
|
164
|
+
return 'UNVERIFIED_ASSERTION';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return declared;
|
|
168
|
+
}
|