@lumoai/cli 1.44.0 → 1.46.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.
|
@@ -111,9 +111,13 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
111
111
|
### What it prints
|
|
112
112
|
|
|
113
113
|
- **Header** — task identifier/title/status + `verification round N/3` (round 0 = never verified) + an escalation warning when the machine loop is exhausted.
|
|
114
|
-
- **
|
|
115
|
-
- **
|
|
114
|
+
- **Claim vs verification** (LUM-564) — 规律 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:
|
|
115
|
+
- **Claim** — what the agent _says_ it did: the latest session's run summary, **same-source** with the web delivery card's `latestRunSummary` (the LLM run summary, else the raw STOP turn digest). Explicitly tagged an **unverified self-report** (`agent self-report · estimated, not verification`) — estimate-tier provenance (估, not 测), with a `↳ source:` line distinguishing an `LLM run summary` from a `raw turn digest — no LLM summary yet`. **Fail-closed**: with no run summary it prints `not generated yet — the agent run summary is synthesized when the task reaches DONE`, never a fabricated claim.
|
|
116
|
+
- **Verification** — what was actually _confirmed_ (measured): the machine-verification rollup (LUM-470/456) `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.
|
|
117
|
+
- Carried in `--json` as `claim { text, source }` (`source: 'RUN_SUMMARY' | 'DIGEST' | null`; `text: null` = none generated). Omitted only against an older server that doesn't emit the field. The machine-verification rollup is still carried top-level as `machineVerification`.
|
|
118
|
+
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (failure tail on fail). `REVIEW_ADDED@rN` provenance is visible per row.
|
|
116
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 (LUM-470): `· 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.
|
|
120
|
+
- A verdict's **evidence is drillable** (LUM-563), 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.
|
|
117
121
|
- A pass can carry a **`⚠ pre-edit version`** note (LUM-457): 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.
|
|
118
122
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
119
123
|
- **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
|
|
@@ -127,6 +131,8 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
127
131
|
|
|
128
132
|
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`).
|
|
129
133
|
|
|
134
|
+
- **Trend** (LUM-562) — 规律 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 (closes the named "1-round pass = single point" gap). 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
|
+
|
|
130
136
|
- **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.
|
|
131
137
|
- **Open boundary crossings** (LUM-448) — 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>` (LUM-469 — 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.
|
|
132
138
|
- **Read-only awareness** — this surfaces crossings detected elsewhere (LUM-426/435/442); there is no CLI path to disposition or clear one. Disposition stays web + human-only (LUM-426/435/422): an agent/CLI bearer cannot clear its own crossing from the terminal.
|
|
@@ -7,6 +7,7 @@ const api_1 = require("../lib/api");
|
|
|
7
7
|
const resolve_bound_task_1 = require("../lib/resolve-bound-task");
|
|
8
8
|
const sanitize_1 = require("../lib/sanitize");
|
|
9
9
|
const open_crossings_1 = require("../lib/open-crossings");
|
|
10
|
+
const evidence_display_1 = require("../lib/evidence-display");
|
|
10
11
|
/** One-line a possibly-multiline crossing detail and cap it so the safety block
|
|
11
12
|
* stays a glance, never a wall (it must not overshadow the criteria). */
|
|
12
13
|
const CROSSING_DETAIL_CAP = 160;
|
|
@@ -44,15 +45,14 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
44
45
|
pushOpenCrossings(lines, extras);
|
|
45
46
|
return lines.join('\n') + '\n';
|
|
46
47
|
}
|
|
48
|
+
// LUM-564 声称 vs 核验: the agent's CLAIM (what it says it did) paired with
|
|
49
|
+
// the measured verification conclusion (what was actually confirmed) — the
|
|
50
|
+
// two columns regular 規律2 wants, instead of only the verification one. The
|
|
51
|
+
// machine-verification rollup (LUM-470) lives on the verification side here
|
|
52
|
+
// rather than as a standalone line, so the contrast is in one place.
|
|
53
|
+
pushClaimVsVerification(lines, data);
|
|
47
54
|
lines.push('');
|
|
48
55
|
lines.push(`Criteria (${data.criteria.length} total, ${data.nextActions.length} unmet):`);
|
|
49
|
-
// LUM-470: honest machine-verification rollup over the active MACHINE criteria
|
|
50
|
-
// (same read model as web, LUM-456) — so the terminal rollup never reads as
|
|
51
|
-
// all-human when a checkpointer actually verified the work.
|
|
52
|
-
const mv = data.machineVerification;
|
|
53
|
-
if (mv.total > 0) {
|
|
54
|
-
lines.push(`Machine verification: ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
|
|
55
|
-
}
|
|
56
56
|
for (const c of data.criteria) {
|
|
57
57
|
const glyph = c.latestVerdict == null
|
|
58
58
|
? '○'
|
|
@@ -85,9 +85,6 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
85
85
|
lines.push(` ✗ FAIL@r${v.round}${why}`);
|
|
86
86
|
}
|
|
87
87
|
else {
|
|
88
|
-
const evidencePart = v.evidencePointer
|
|
89
|
-
? ` · ${(0, sanitize_1.sanitizeField)(v.evidencePointer)}`
|
|
90
|
-
: '';
|
|
91
88
|
// LUM-470: tag a passing MACHINE criterion by the read model's
|
|
92
89
|
// machinePassed flag, not the latest verdict — a criterion a checkpointer
|
|
93
90
|
// verified reads as machine-verified even after a human signs off, and a
|
|
@@ -97,13 +94,26 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
97
94
|
? ' · machine-verified'
|
|
98
95
|
: ' · human override (no machine pass)'
|
|
99
96
|
: '';
|
|
100
|
-
lines.push(` ✓ ${v.verdict}@r${v.round}${
|
|
97
|
+
lines.push(` ✓ ${v.verdict}@r${v.round}${machineTag}`);
|
|
101
98
|
// LUM-457: a pass that vouches for a pre-edit version of the criterion —
|
|
102
99
|
// render-only downgrade, the criterion still counts met.
|
|
103
100
|
if (c.verdictStale || c.checkMismatch) {
|
|
104
101
|
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
105
102
|
}
|
|
106
103
|
}
|
|
104
|
+
// LUM-563: drill the verdict's evidence into a navigable / re-runnable
|
|
105
|
+
// reference instead of the inert raw pointer that used to ride the verdict
|
|
106
|
+
// line — a commit URL (or `git show` fallback), a clickable path:line, or
|
|
107
|
+
// the actual command + exit. Rendered for PASS and FAIL alike so a failure
|
|
108
|
+
// is just as reproducible. When a criterion requires evidence but none is
|
|
109
|
+
// recorded yet, say so explicitly (fail-closed) rather than leaving the
|
|
110
|
+
// bare `[evidence]` tag pointing at nothing.
|
|
111
|
+
if (v?.evidencePointer) {
|
|
112
|
+
lines.push(` ↳ evidence: ${(0, sanitize_1.sanitizeField)((0, evidence_display_1.formatEvidenceDrilldown)(v.evidencePointer, extras.repoWebUrl ?? null))}`);
|
|
113
|
+
}
|
|
114
|
+
else if (c.evidenceRequired) {
|
|
115
|
+
lines.push(' ↳ evidence: pending — no reference recorded yet');
|
|
116
|
+
}
|
|
107
117
|
// LUM-511 Phase 5: send-back lifecycle (was this criterion's send-back
|
|
108
118
|
// resolved, and by which PR).
|
|
109
119
|
const sb = c.sendBackResolution;
|
|
@@ -136,6 +146,7 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
136
146
|
}
|
|
137
147
|
pushCost(lines, data);
|
|
138
148
|
pushStruggleTrail(lines, data);
|
|
149
|
+
pushTrend(lines, data);
|
|
139
150
|
lines.push('');
|
|
140
151
|
if (data.nextActions.length === 0) {
|
|
141
152
|
lines.push(data.currentRound > 0
|
|
@@ -196,6 +207,56 @@ function fmtDuration(totalSec) {
|
|
|
196
207
|
parts.push(`${s}s`);
|
|
197
208
|
return parts.join(' ');
|
|
198
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Append the "Claim vs verification" pairing (LUM-564) — 規律2 声称vs核验. The
|
|
212
|
+
* verification conclusion (machine-verified vs human-override, LUM-470) was the
|
|
213
|
+
* whole-system template, but the agent's run summary — its CLAIM of what it did
|
|
214
|
+
* — lived on a different surface, so the "two-column" contrast was really one
|
|
215
|
+
* column. This puts them side by side: the unverified self-report (estimate-
|
|
216
|
+
* tier — 估, not 测) against the measured verdict (测), so a reader can weigh
|
|
217
|
+
* 声称 against 核验 at a glance.
|
|
218
|
+
*
|
|
219
|
+
* Fail-closed on every axis: a missing claim renders an explicit "not generated
|
|
220
|
+
* yet" line (never a fabricated claim); a not-yet-verified task renders the
|
|
221
|
+
* verification side as explicitly unconfirmed (never an implied pass). The
|
|
222
|
+
* claim text is same-source with the web card (latest session's LLM run
|
|
223
|
+
* summary, else its STOP turn digest) so the two surfaces cannot drift.
|
|
224
|
+
*/
|
|
225
|
+
function pushClaimVsVerification(lines, data) {
|
|
226
|
+
lines.push('');
|
|
227
|
+
lines.push('Claim vs verification:');
|
|
228
|
+
// ── Claim (声称): the agent's self-report — estimate-tier, never measured.
|
|
229
|
+
lines.push(' ▸ Claim — what the agent says it did');
|
|
230
|
+
lines.push(' (agent self-report · estimated, not verification):');
|
|
231
|
+
const claim = data.claim;
|
|
232
|
+
if (claim && claim.text) {
|
|
233
|
+
for (const cl of (0, sanitize_1.sanitizeField)(claim.text).split('\n')) {
|
|
234
|
+
lines.push(` ${cl}`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(claim.source === 'RUN_SUMMARY'
|
|
237
|
+
? ' ↳ source: LLM run summary'
|
|
238
|
+
: ' ↳ source: raw turn digest — no LLM summary yet');
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
// Fail-closed: no run summary → say so, never invent a claim. Run summaries
|
|
242
|
+
// are synthesized when the bound task reaches DONE (LUM-481).
|
|
243
|
+
lines.push(' not generated yet — the agent run summary is synthesized when the task reaches DONE');
|
|
244
|
+
}
|
|
245
|
+
// ── Verification (核验): the measured verdict — machine-verified vs override.
|
|
246
|
+
lines.push(' ▸ Verification — what was actually confirmed (measured):');
|
|
247
|
+
const mv = data.machineVerification;
|
|
248
|
+
if (data.currentRound === 0) {
|
|
249
|
+
// Nothing verified yet — the claim stands unconfirmed. Don't imply a pass.
|
|
250
|
+
lines.push(' no verification has run yet — the claim is unconfirmed');
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
if (mv.total > 0) {
|
|
254
|
+
lines.push(` ${mv.machineVerified} machine-verified / ${mv.humanOverridden} human override (of ${mv.total} MACHINE criteria)`);
|
|
255
|
+
}
|
|
256
|
+
const met = data.criteria.length - data.nextActions.length;
|
|
257
|
+
lines.push(` ${met} of ${data.criteria.length} criteria met by their latest verdict`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
199
260
|
/**
|
|
200
261
|
* Append the honest "Cost" section (LUM-560) — 规律 1: surface the costs a human
|
|
201
262
|
* should weigh (token spend, active time, machine rework) on the same report as
|
|
@@ -291,6 +352,95 @@ function pushStruggleTrail(lines, data) {
|
|
|
291
352
|
}
|
|
292
353
|
}
|
|
293
354
|
}
|
|
355
|
+
/** Direction glyph for a delta — ↑ up, ↓ down, → flat. */
|
|
356
|
+
function arrow(delta) {
|
|
357
|
+
return delta > 0 ? '↑' : delta < 0 ? '↓' : '→';
|
|
358
|
+
}
|
|
359
|
+
/** Pass rate of one round as an integer percent (0 verdicts → 0%). */
|
|
360
|
+
function passPct(p) {
|
|
361
|
+
const total = p.passed + p.failed;
|
|
362
|
+
return total === 0 ? 0 : Math.round((p.passed / total) * 100);
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Append the "Trend" section (LUM-562, 規律7 趨勢非快照) — the *movement* of the
|
|
366
|
+
* task's key quantities across its attempts, not a single-point snapshot. Where
|
|
367
|
+
* History lists each round's PASS/FAIL, Cost shows the running total, and
|
|
368
|
+
* Struggle lists the scars, this shows direction: pass rate climbing, cost per
|
|
369
|
+
* session rising/falling, rework accruing. The honesty rule (mirrors the
|
|
370
|
+
* struggle trail): a single data point is NOT a trajectory — with one round and
|
|
371
|
+
* one session the section says so outright rather than drawing a fake arrow off
|
|
372
|
+
* a single value, closing the named "1-round pass = single point" gap. The
|
|
373
|
+
* per-session cost reuses the same source as the Cost section, so the
|
|
374
|
+
* trajectory's points sum to the Cost total (no in-report drift). Skipped only
|
|
375
|
+
* when the server didn't emit the field (older server) — never fabricated.
|
|
376
|
+
*/
|
|
377
|
+
function pushTrend(lines, data) {
|
|
378
|
+
const trend = data.trend;
|
|
379
|
+
if (!trend)
|
|
380
|
+
return; // older server: no trend data — don't invent one.
|
|
381
|
+
const rounds = trend.passRate;
|
|
382
|
+
const cost = trend.cost;
|
|
383
|
+
const rw = trend.rework;
|
|
384
|
+
lines.push('');
|
|
385
|
+
// Nothing to trend at all — no verification, no measured cost.
|
|
386
|
+
if (rounds.length === 0 && cost.length === 0) {
|
|
387
|
+
lines.push('Trend:');
|
|
388
|
+
lines.push(' No verification rounds or measured cost yet — nothing to trend. Run `lumo verify`.');
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
// A trajectory needs ≥2 points in at least one dimension. With a single
|
|
392
|
+
// round AND a single session, every quantity is one data point — say so
|
|
393
|
+
// plainly instead of implying a flat/healthy trend (the named gap).
|
|
394
|
+
const hasTrajectory = rounds.length >= 2 || cost.length >= 2;
|
|
395
|
+
if (!hasTrajectory) {
|
|
396
|
+
lines.push('Trend:');
|
|
397
|
+
lines.push(' Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions).');
|
|
398
|
+
const bits = [];
|
|
399
|
+
if (rounds.length === 1)
|
|
400
|
+
bits.push(`pass rate r${rounds[0].round} ${passPct(rounds[0])}%`);
|
|
401
|
+
if (cost.length === 1)
|
|
402
|
+
bits.push(`cost ${fmtTokens(cost[0].total)} tokens`);
|
|
403
|
+
bits.push(rw.total === 0 ? 'rework none' : `rework ${rw.total}`);
|
|
404
|
+
lines.push(` ${bits.join(' · ')}`);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const sessionWord = cost.length === 1 ? 'session' : 'sessions';
|
|
408
|
+
lines.push(`Trend (${rounds.length} rounds · ${cost.length} ${sessionWord}):`);
|
|
409
|
+
// Pass rate across rounds — the climb (or fall) of the verify loop.
|
|
410
|
+
if (rounds.length >= 2) {
|
|
411
|
+
const seq = rounds.map(r => `r${r.round} ${passPct(r)}%`).join(' → ');
|
|
412
|
+
const delta = passPct(rounds[rounds.length - 1]) - passPct(rounds[0]);
|
|
413
|
+
const sign = delta > 0 ? `+${delta}` : `${delta}`;
|
|
414
|
+
lines.push(` Pass rate: ${seq} (${arrow(delta)} ${sign}pts)`);
|
|
415
|
+
}
|
|
416
|
+
else if (rounds.length === 1) {
|
|
417
|
+
lines.push(` Pass rate: r${rounds[0].round} ${passPct(rounds[0])}% (1 round — single point)`);
|
|
418
|
+
}
|
|
419
|
+
// Cost per session across the task's sessions — the spend trajectory.
|
|
420
|
+
if (cost.length >= 2) {
|
|
421
|
+
const first = cost[0].total;
|
|
422
|
+
const last = cost[cost.length - 1].total;
|
|
423
|
+
const total = cost.reduce((a, c) => a + c.total, 0);
|
|
424
|
+
lines.push(` Cost/session: ${fmtTokens(first)} → ${fmtTokens(last)} tokens (${arrow(last - first)}), ${fmtTokens(total)} total over ${cost.length} sessions`);
|
|
425
|
+
}
|
|
426
|
+
else if (cost.length === 1) {
|
|
427
|
+
lines.push(` Cost: ${fmtTokens(cost[0].total)} tokens (1 session — single point)`);
|
|
428
|
+
}
|
|
429
|
+
// Rework accrual — monotonic from a clean baseline of 0.
|
|
430
|
+
if (rw.total === 0) {
|
|
431
|
+
lines.push(' Rework: none accrued (0) — clean across every attempt');
|
|
432
|
+
}
|
|
433
|
+
else {
|
|
434
|
+
const parts = [];
|
|
435
|
+
if (rw.reworkRounds > 0)
|
|
436
|
+
parts.push(`${rw.reworkRounds} FAIL round${rw.reworkRounds === 1 ? '' : 's'}`);
|
|
437
|
+
if (rw.reopens > 0)
|
|
438
|
+
parts.push(`${rw.reopens} reopen${rw.reopens === 1 ? '' : 's'}`);
|
|
439
|
+
if (rw.extraPrCycles > 0)
|
|
440
|
+
parts.push(`+${rw.extraPrCycles} PR cycle${rw.extraPrCycles === 1 ? '' : 's'}`);
|
|
441
|
+
lines.push(` Rework: ${rw.total} accrued — ${parts.join(', ')} (↑ from 0)`);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
294
444
|
/**
|
|
295
445
|
* Append the OPEN boundary-crossings safety block (LUM-448) — a count, one line
|
|
296
446
|
* per crossing with its severity + category + clipped detail, and a pointer to
|
|
@@ -407,5 +557,9 @@ async function taskStatus(identifier, options = {}) {
|
|
|
407
557
|
process.stdout.write(formatTaskStatus(data, {
|
|
408
558
|
openCrossings: crossingsResult,
|
|
409
559
|
dispositionUrl: (0, open_crossings_1.dispositionUrl)(base, creds.workspaceSlug ?? 'lumo', data.task.identifier),
|
|
560
|
+
// LUM-563: resolve the local repo's web base so a `commit:` evidence
|
|
561
|
+
// pointer drills to a navigable URL; null (not a git repo / no origin)
|
|
562
|
+
// falls back to a `git show` hint in the renderer.
|
|
563
|
+
repoWebUrl: (0, evidence_display_1.resolveRepoWebUrl)(),
|
|
410
564
|
}));
|
|
411
565
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeRepoWebUrl = normalizeRepoWebUrl;
|
|
4
|
+
exports.resolveRepoWebUrl = resolveRepoWebUrl;
|
|
5
|
+
exports.formatEvidenceDrilldown = formatEvidenceDrilldown;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
const acceptance_evidence_1 = require("../../../shared/src/acceptance-evidence");
|
|
8
|
+
/**
|
|
9
|
+
* Drill-down rendering for acceptance evidence pointers (LUM-563).
|
|
10
|
+
*
|
|
11
|
+
* A verdict's evidencePointer is a structured token (`cmd:…#exit=N`,
|
|
12
|
+
* `file:path:line`, `commit:hash`). Printed raw it satisfies Norman's gulf of
|
|
13
|
+
* evaluation only halfway: you can read it but not act on it. These helpers
|
|
14
|
+
* turn each shape into a navigable / re-runnable line — a web commit URL when
|
|
15
|
+
* the git remote resolves (else a `git show` fallback), a terminal-clickable
|
|
16
|
+
* `path:line`, or the actual command + exit you can re-run to reproduce.
|
|
17
|
+
*
|
|
18
|
+
* No third-party deps; sanitization is applied by the caller (task-status.ts)
|
|
19
|
+
* before anything reaches the terminal.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* Normalize a git remote URL to its https web base (no trailing `.git`), or
|
|
23
|
+
* null when it doesn't look like a remote we can navigate. Fails closed: a
|
|
24
|
+
* shape we can't confidently map yields null so the caller drops to a
|
|
25
|
+
* non-URL fallback rather than printing a broken link.
|
|
26
|
+
*
|
|
27
|
+
* Handles the three remote forms git emits:
|
|
28
|
+
* - scp-style `git@host:owner/repo.git`
|
|
29
|
+
* - https `https://host/owner/repo.git`
|
|
30
|
+
* - ssh url `ssh://git@host/owner/repo.git`
|
|
31
|
+
*/
|
|
32
|
+
function normalizeRepoWebUrl(remote) {
|
|
33
|
+
const raw = remote.trim();
|
|
34
|
+
if (!raw)
|
|
35
|
+
return null;
|
|
36
|
+
let host;
|
|
37
|
+
let path;
|
|
38
|
+
const scp = /^[\w.-]+@([\w.-]+):(.+)$/.exec(raw);
|
|
39
|
+
if (scp) {
|
|
40
|
+
host = scp[1];
|
|
41
|
+
path = scp[2];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
let url;
|
|
45
|
+
try {
|
|
46
|
+
url = new URL(raw);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
if (url.protocol !== 'https:' && url.protocol !== 'ssh:')
|
|
52
|
+
return null;
|
|
53
|
+
host = url.host;
|
|
54
|
+
path = url.pathname;
|
|
55
|
+
}
|
|
56
|
+
const cleanPath = path
|
|
57
|
+
.replace(/^\/+/, '')
|
|
58
|
+
.replace(/\.git$/, '')
|
|
59
|
+
.replace(/\/+$/, '');
|
|
60
|
+
// A navigable repo path is at minimum owner/repo — reject a bare host.
|
|
61
|
+
if (!host || !cleanPath || !cleanPath.includes('/'))
|
|
62
|
+
return null;
|
|
63
|
+
return `https://${host}/${cleanPath}`;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve the current repo's web base by reading `origin`'s remote URL. Best
|
|
67
|
+
* effort and side-effecting (runs git in the cwd), so it lives outside the pure
|
|
68
|
+
* formatter and is injected via TaskStatusExtras. Null when not a git repo /
|
|
69
|
+
* no origin / unparseable — the renderer then uses the `git show` fallback.
|
|
70
|
+
*/
|
|
71
|
+
function resolveRepoWebUrl(cwd = process.cwd()) {
|
|
72
|
+
try {
|
|
73
|
+
const r = (0, child_process_1.spawnSync)('git', ['remote', 'get-url', 'origin'], {
|
|
74
|
+
cwd,
|
|
75
|
+
encoding: 'utf8',
|
|
76
|
+
timeout: 5_000,
|
|
77
|
+
});
|
|
78
|
+
if (r.status !== 0 || !r.stdout)
|
|
79
|
+
return null;
|
|
80
|
+
return normalizeRepoWebUrl(r.stdout);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Render the actionable evidence body for one pointer (the text after
|
|
88
|
+
* `↳ evidence: `). Unrecognised shapes fall back to the raw pointer so data is
|
|
89
|
+
* never hidden, only upgraded when we know how.
|
|
90
|
+
*/
|
|
91
|
+
function formatEvidenceDrilldown(pointer, repoWebUrl) {
|
|
92
|
+
const parsed = (0, acceptance_evidence_1.parseEvidencePointer)(pointer);
|
|
93
|
+
if (!parsed)
|
|
94
|
+
return pointer;
|
|
95
|
+
if (parsed.kind === 'cmd') {
|
|
96
|
+
return `ran \`${parsed.command}\` → exit ${parsed.exitCode} · re-run to reproduce`;
|
|
97
|
+
}
|
|
98
|
+
if (parsed.kind === 'file') {
|
|
99
|
+
return `${parsed.path}:${parsed.line}`;
|
|
100
|
+
}
|
|
101
|
+
// commit
|
|
102
|
+
const short = parsed.hash.slice(0, 10);
|
|
103
|
+
return repoWebUrl
|
|
104
|
+
? `commit ${short} · ${repoWebUrl}/commit/${parsed.hash}`
|
|
105
|
+
: `commit ${short} · view with \`git show ${parsed.hash}\``;
|
|
106
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.EVIDENCE_POINTER_MAX = exports.EVIDENCE_POINTER_FORMAT_HINT = exports.EVIDENCE_POINTER_PATTERNS = void 0;
|
|
4
4
|
exports.isValidEvidencePointer = isValidEvidencePointer;
|
|
5
|
+
exports.parseEvidencePointer = parseEvidencePointer;
|
|
5
6
|
exports.buildCmdEvidencePointer = buildCmdEvidencePointer;
|
|
6
7
|
/**
|
|
7
8
|
* Evidence-pointer grammar for acceptance verification (LUM-343).
|
|
@@ -29,6 +30,24 @@ function isValidEvidencePointer(value) {
|
|
|
29
30
|
}
|
|
30
31
|
/** Max stored pointer length — mirrors the column-level cap in validation. */
|
|
31
32
|
exports.EVIDENCE_POINTER_MAX = 2_000;
|
|
33
|
+
/**
|
|
34
|
+
* Decompose an evidence pointer into its parts, or null if it doesn't parse.
|
|
35
|
+
* The grammar is the single source of truth (EVIDENCE_POINTER_PATTERNS); this
|
|
36
|
+
* mirrors those exact shapes — a `#` may appear inside a `cmd:` command, so the
|
|
37
|
+
* exit marker is matched greedily from the END, never the first `#`.
|
|
38
|
+
*/
|
|
39
|
+
function parseEvidencePointer(value) {
|
|
40
|
+
const commit = /^commit:([0-9a-f]{7,40})$/.exec(value);
|
|
41
|
+
if (commit)
|
|
42
|
+
return { kind: 'commit', hash: commit[1] };
|
|
43
|
+
const file = /^file:([^\s]+):(\d+(?:-\d+)?)$/.exec(value);
|
|
44
|
+
if (file)
|
|
45
|
+
return { kind: 'file', path: file[1], line: file[2] };
|
|
46
|
+
const cmd = /^cmd:([\s\S]+)#exit=(\d+)$/.exec(value);
|
|
47
|
+
if (cmd)
|
|
48
|
+
return { kind: 'cmd', command: cmd[1], exitCode: Number(cmd[2]) };
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
32
51
|
/**
|
|
33
52
|
* Build a `cmd:` evidence pointer for an executed checkpointer. The command
|
|
34
53
|
* is truncated so the suffix (`#exit=N`) always survives the length cap —
|