@lumoai/cli 1.43.0 → 1.45.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.
|
@@ -112,11 +112,13 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
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
114
|
- **Machine verification rollup** (LUM-470) — directly under the `Criteria` header, one line `Machine verification: N machine-verified / M human override (of T MACHINE criteria)` over the active MACHINE criteria, aligned with the web read model (LUM-456). Printed whenever the contract has ≥1 MACHINE criterion, so the terminal rollup never reads as all-human when a checkpointer actually verified the work.
|
|
115
|
-
- **Criteria** — every criterion as `<glyph> <id> [TYPE] SOURCE@rN statement` (✓ latest verdict passed / ✗ failed / ○ no verdict yet) with its checkpointer and latest verdict line (
|
|
115
|
+
- **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
116
|
- A passing **MACHINE** criterion's verdict line carries a machine-state tag derived from the read model's `machinePassed` flag, NOT the latest verdict (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.
|
|
117
|
+
- 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
118
|
- 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
119
|
- **History** — one line per recorded round: `rN · timestamp · X PASS / Y FAIL`.
|
|
119
120
|
- **Last round failures** — the most recent round's FAIL verdicts with their rejection reasons (why the last round bounced).
|
|
121
|
+
- **Cost** (LUM-560) — 规律 1: the costs a human should weigh, on the same report as the verdict instead of scattered across the web delivery card and `task lineage`. Three lines: **Tokens** (total input+output+cache across the task's sessions), **Active time** (non-idle agent seconds — Σ per-turn `STOP − prompt`, LUM-487), and **Rework rounds** (verify rounds that recorded a FAIL). Read from the **same** server-side source the web delivery card consumes (`retrospectiveRepository.loadActuals`), so the two reports cannot drift. Token cost is **fail-closed**: when no session usage was recorded it prints `Tokens: not recorded (no session usage captured)`, kept distinct from a measured `0` (没测到 vs 花了0, aligned with LUM-559). Carried in `--json` as `cost { tokenCost, activeTimeSec, reworkRounds }` (`tokenCost: null` = not measured). Omitted only against an older server that doesn't emit the field.
|
|
120
122
|
- **Struggle / rework / outstanding** (LUM-561) — the anti-mum-and-deaf block: **always printed when the contract exists, even on a clean 0-unmet task** so a passing task still shows its scars instead of wiping them to a single PASS count. Lists, when present:
|
|
121
123
|
- **rework rounds** — verify rounds that had a FAIL;
|
|
122
124
|
- **send-backs** — criteria sent back by a human/agent verdict (a MACHINE verify-loop FAIL is not a 打回), with their open/resolved lifecycle, preserved even for since-removed criteria;
|
|
@@ -126,6 +128,8 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
126
128
|
|
|
127
129
|
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`).
|
|
128
130
|
|
|
131
|
+
- **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.
|
|
132
|
+
|
|
129
133
|
- **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.
|
|
130
134
|
- **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.
|
|
131
135
|
- **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;
|
|
@@ -85,9 +86,6 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
85
86
|
lines.push(` ✗ FAIL@r${v.round}${why}`);
|
|
86
87
|
}
|
|
87
88
|
else {
|
|
88
|
-
const evidencePart = v.evidencePointer
|
|
89
|
-
? ` · ${(0, sanitize_1.sanitizeField)(v.evidencePointer)}`
|
|
90
|
-
: '';
|
|
91
89
|
// LUM-470: tag a passing MACHINE criterion by the read model's
|
|
92
90
|
// machinePassed flag, not the latest verdict — a criterion a checkpointer
|
|
93
91
|
// verified reads as machine-verified even after a human signs off, and a
|
|
@@ -97,13 +95,26 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
97
95
|
? ' · machine-verified'
|
|
98
96
|
: ' · human override (no machine pass)'
|
|
99
97
|
: '';
|
|
100
|
-
lines.push(` ✓ ${v.verdict}@r${v.round}${
|
|
98
|
+
lines.push(` ✓ ${v.verdict}@r${v.round}${machineTag}`);
|
|
101
99
|
// LUM-457: a pass that vouches for a pre-edit version of the criterion —
|
|
102
100
|
// render-only downgrade, the criterion still counts met.
|
|
103
101
|
if (c.verdictStale || c.checkMismatch) {
|
|
104
102
|
lines.push(' ⚠ pre-edit version — criterion changed since this check; re-run `lumo verify` to re-confirm');
|
|
105
103
|
}
|
|
106
104
|
}
|
|
105
|
+
// LUM-563: drill the verdict's evidence into a navigable / re-runnable
|
|
106
|
+
// reference instead of the inert raw pointer that used to ride the verdict
|
|
107
|
+
// line — a commit URL (or `git show` fallback), a clickable path:line, or
|
|
108
|
+
// the actual command + exit. Rendered for PASS and FAIL alike so a failure
|
|
109
|
+
// is just as reproducible. When a criterion requires evidence but none is
|
|
110
|
+
// recorded yet, say so explicitly (fail-closed) rather than leaving the
|
|
111
|
+
// bare `[evidence]` tag pointing at nothing.
|
|
112
|
+
if (v?.evidencePointer) {
|
|
113
|
+
lines.push(` ↳ evidence: ${(0, sanitize_1.sanitizeField)((0, evidence_display_1.formatEvidenceDrilldown)(v.evidencePointer, extras.repoWebUrl ?? null))}`);
|
|
114
|
+
}
|
|
115
|
+
else if (c.evidenceRequired) {
|
|
116
|
+
lines.push(' ↳ evidence: pending — no reference recorded yet');
|
|
117
|
+
}
|
|
107
118
|
// LUM-511 Phase 5: send-back lifecycle (was this criterion's send-back
|
|
108
119
|
// resolved, and by which PR).
|
|
109
120
|
const sb = c.sendBackResolution;
|
|
@@ -134,7 +145,9 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
134
145
|
}
|
|
135
146
|
}
|
|
136
147
|
}
|
|
148
|
+
pushCost(lines, data);
|
|
137
149
|
pushStruggleTrail(lines, data);
|
|
150
|
+
pushTrend(lines, data);
|
|
138
151
|
lines.push('');
|
|
139
152
|
if (data.nextActions.length === 0) {
|
|
140
153
|
lines.push(data.currentRound > 0
|
|
@@ -166,6 +179,57 @@ function formatTaskStatus(data, extras = {}) {
|
|
|
166
179
|
pushOpenCrossings(lines, extras);
|
|
167
180
|
return lines.join('\n') + '\n';
|
|
168
181
|
}
|
|
182
|
+
/** Compact a token count for the terminal — 1_200_000 → "1.2M", 850_000 →
|
|
183
|
+
* "850K", 0 → "0". Uses the SAME Intl compact formatter the web card's
|
|
184
|
+
* fmtCompact uses (notation:'compact', maximumFractionDigits:1) so the same
|
|
185
|
+
* number reads identically in both surfaces — no presentation drift (LUM-560). */
|
|
186
|
+
const TOKEN_FMT = new Intl.NumberFormat('en-US', {
|
|
187
|
+
notation: 'compact',
|
|
188
|
+
maximumFractionDigits: 1,
|
|
189
|
+
});
|
|
190
|
+
function fmtTokens(n) {
|
|
191
|
+
return TOKEN_FMT.format(n);
|
|
192
|
+
}
|
|
193
|
+
/** Active (non-idle) seconds → a compact "2h 14m" / "3m 5s" / "12s". 0 → "0s". */
|
|
194
|
+
function fmtDuration(totalSec) {
|
|
195
|
+
const sec = Math.max(0, Math.round(totalSec));
|
|
196
|
+
if (sec === 0)
|
|
197
|
+
return '0s';
|
|
198
|
+
const h = Math.floor(sec / 3600);
|
|
199
|
+
const m = Math.floor((sec % 3600) / 60);
|
|
200
|
+
const s = sec % 60;
|
|
201
|
+
const parts = [];
|
|
202
|
+
if (h > 0)
|
|
203
|
+
parts.push(`${h}h`);
|
|
204
|
+
if (m > 0)
|
|
205
|
+
parts.push(`${m}m`);
|
|
206
|
+
// Show seconds only when the duration is under an hour (keeps long runs tidy).
|
|
207
|
+
if (s > 0 && h === 0)
|
|
208
|
+
parts.push(`${s}s`);
|
|
209
|
+
return parts.join(' ');
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Append the honest "Cost" section (LUM-560) — 规律 1: surface the costs a human
|
|
213
|
+
* should weigh (token spend, active time, machine rework) on the same report as
|
|
214
|
+
* the acceptance verdict, instead of leaving them scattered across the web
|
|
215
|
+
* delivery card and `task lineage`. Every number is the server's, read from the
|
|
216
|
+
* same loadActuals source as the web card (no drift). Token cost is fail-closed:
|
|
217
|
+
* a null reads as an explicit "not recorded" line, never a silent or fake 0, so
|
|
218
|
+
* 没测到 (no session usage) stays distinct from a measured 花了0. Skipped only
|
|
219
|
+
* when the server didn't emit the field (older server) — never fabricated.
|
|
220
|
+
*/
|
|
221
|
+
function pushCost(lines, data) {
|
|
222
|
+
const cost = data.cost;
|
|
223
|
+
if (!cost)
|
|
224
|
+
return; // older server: can't fabricate cost, so don't claim any.
|
|
225
|
+
lines.push('');
|
|
226
|
+
lines.push('Cost:');
|
|
227
|
+
lines.push(cost.tokenCost == null
|
|
228
|
+
? ' Tokens: not recorded (no session usage captured)'
|
|
229
|
+
: ` Tokens: ${fmtTokens(cost.tokenCost)}`);
|
|
230
|
+
lines.push(` Active time: ${fmtDuration(cost.activeTimeSec)} (non-idle)`);
|
|
231
|
+
lines.push(` Rework rounds: ${cost.reworkRounds}${cost.reworkRounds === 0 ? ' (no machine rework)' : ''}`);
|
|
232
|
+
}
|
|
169
233
|
/**
|
|
170
234
|
* Append the honest "Struggle / rework / outstanding" section (LUM-561) — the
|
|
171
235
|
* anti-mum-and-deaf block (kills a silent "Nothing outstanding"). It is ALWAYS
|
|
@@ -239,6 +303,95 @@ function pushStruggleTrail(lines, data) {
|
|
|
239
303
|
}
|
|
240
304
|
}
|
|
241
305
|
}
|
|
306
|
+
/** Direction glyph for a delta — ↑ up, ↓ down, → flat. */
|
|
307
|
+
function arrow(delta) {
|
|
308
|
+
return delta > 0 ? '↑' : delta < 0 ? '↓' : '→';
|
|
309
|
+
}
|
|
310
|
+
/** Pass rate of one round as an integer percent (0 verdicts → 0%). */
|
|
311
|
+
function passPct(p) {
|
|
312
|
+
const total = p.passed + p.failed;
|
|
313
|
+
return total === 0 ? 0 : Math.round((p.passed / total) * 100);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Append the "Trend" section (LUM-562, 規律7 趨勢非快照) — the *movement* of the
|
|
317
|
+
* task's key quantities across its attempts, not a single-point snapshot. Where
|
|
318
|
+
* History lists each round's PASS/FAIL, Cost shows the running total, and
|
|
319
|
+
* Struggle lists the scars, this shows direction: pass rate climbing, cost per
|
|
320
|
+
* session rising/falling, rework accruing. The honesty rule (mirrors the
|
|
321
|
+
* struggle trail): a single data point is NOT a trajectory — with one round and
|
|
322
|
+
* one session the section says so outright rather than drawing a fake arrow off
|
|
323
|
+
* a single value, closing the named "1-round pass = single point" gap. The
|
|
324
|
+
* per-session cost reuses the same source as the Cost section, so the
|
|
325
|
+
* trajectory's points sum to the Cost total (no in-report drift). Skipped only
|
|
326
|
+
* when the server didn't emit the field (older server) — never fabricated.
|
|
327
|
+
*/
|
|
328
|
+
function pushTrend(lines, data) {
|
|
329
|
+
const trend = data.trend;
|
|
330
|
+
if (!trend)
|
|
331
|
+
return; // older server: no trend data — don't invent one.
|
|
332
|
+
const rounds = trend.passRate;
|
|
333
|
+
const cost = trend.cost;
|
|
334
|
+
const rw = trend.rework;
|
|
335
|
+
lines.push('');
|
|
336
|
+
// Nothing to trend at all — no verification, no measured cost.
|
|
337
|
+
if (rounds.length === 0 && cost.length === 0) {
|
|
338
|
+
lines.push('Trend:');
|
|
339
|
+
lines.push(' No verification rounds or measured cost yet — nothing to trend. Run `lumo verify`.');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// A trajectory needs ≥2 points in at least one dimension. With a single
|
|
343
|
+
// round AND a single session, every quantity is one data point — say so
|
|
344
|
+
// plainly instead of implying a flat/healthy trend (the named gap).
|
|
345
|
+
const hasTrajectory = rounds.length >= 2 || cost.length >= 2;
|
|
346
|
+
if (!hasTrajectory) {
|
|
347
|
+
lines.push('Trend:');
|
|
348
|
+
lines.push(' Single attempt so far — no trajectory yet (a trend needs ≥2 rounds or sessions).');
|
|
349
|
+
const bits = [];
|
|
350
|
+
if (rounds.length === 1)
|
|
351
|
+
bits.push(`pass rate r${rounds[0].round} ${passPct(rounds[0])}%`);
|
|
352
|
+
if (cost.length === 1)
|
|
353
|
+
bits.push(`cost ${fmtTokens(cost[0].total)} tokens`);
|
|
354
|
+
bits.push(rw.total === 0 ? 'rework none' : `rework ${rw.total}`);
|
|
355
|
+
lines.push(` ${bits.join(' · ')}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const sessionWord = cost.length === 1 ? 'session' : 'sessions';
|
|
359
|
+
lines.push(`Trend (${rounds.length} rounds · ${cost.length} ${sessionWord}):`);
|
|
360
|
+
// Pass rate across rounds — the climb (or fall) of the verify loop.
|
|
361
|
+
if (rounds.length >= 2) {
|
|
362
|
+
const seq = rounds.map(r => `r${r.round} ${passPct(r)}%`).join(' → ');
|
|
363
|
+
const delta = passPct(rounds[rounds.length - 1]) - passPct(rounds[0]);
|
|
364
|
+
const sign = delta > 0 ? `+${delta}` : `${delta}`;
|
|
365
|
+
lines.push(` Pass rate: ${seq} (${arrow(delta)} ${sign}pts)`);
|
|
366
|
+
}
|
|
367
|
+
else if (rounds.length === 1) {
|
|
368
|
+
lines.push(` Pass rate: r${rounds[0].round} ${passPct(rounds[0])}% (1 round — single point)`);
|
|
369
|
+
}
|
|
370
|
+
// Cost per session across the task's sessions — the spend trajectory.
|
|
371
|
+
if (cost.length >= 2) {
|
|
372
|
+
const first = cost[0].total;
|
|
373
|
+
const last = cost[cost.length - 1].total;
|
|
374
|
+
const total = cost.reduce((a, c) => a + c.total, 0);
|
|
375
|
+
lines.push(` Cost/session: ${fmtTokens(first)} → ${fmtTokens(last)} tokens (${arrow(last - first)}), ${fmtTokens(total)} total over ${cost.length} sessions`);
|
|
376
|
+
}
|
|
377
|
+
else if (cost.length === 1) {
|
|
378
|
+
lines.push(` Cost: ${fmtTokens(cost[0].total)} tokens (1 session — single point)`);
|
|
379
|
+
}
|
|
380
|
+
// Rework accrual — monotonic from a clean baseline of 0.
|
|
381
|
+
if (rw.total === 0) {
|
|
382
|
+
lines.push(' Rework: none accrued (0) — clean across every attempt');
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
const parts = [];
|
|
386
|
+
if (rw.reworkRounds > 0)
|
|
387
|
+
parts.push(`${rw.reworkRounds} FAIL round${rw.reworkRounds === 1 ? '' : 's'}`);
|
|
388
|
+
if (rw.reopens > 0)
|
|
389
|
+
parts.push(`${rw.reopens} reopen${rw.reopens === 1 ? '' : 's'}`);
|
|
390
|
+
if (rw.extraPrCycles > 0)
|
|
391
|
+
parts.push(`+${rw.extraPrCycles} PR cycle${rw.extraPrCycles === 1 ? '' : 's'}`);
|
|
392
|
+
lines.push(` Rework: ${rw.total} accrued — ${parts.join(', ')} (↑ from 0)`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
242
395
|
/**
|
|
243
396
|
* Append the OPEN boundary-crossings safety block (LUM-448) — a count, one line
|
|
244
397
|
* per crossing with its severity + category + clipped detail, and a pointer to
|
|
@@ -355,5 +508,9 @@ async function taskStatus(identifier, options = {}) {
|
|
|
355
508
|
process.stdout.write(formatTaskStatus(data, {
|
|
356
509
|
openCrossings: crossingsResult,
|
|
357
510
|
dispositionUrl: (0, open_crossings_1.dispositionUrl)(base, creds.workspaceSlug ?? 'lumo', data.task.identifier),
|
|
511
|
+
// LUM-563: resolve the local repo's web base so a `commit:` evidence
|
|
512
|
+
// pointer drills to a navigable URL; null (not a git repo / no origin)
|
|
513
|
+
// falls back to a `git show` hint in the renderer.
|
|
514
|
+
repoWebUrl: (0, evidence_display_1.resolveRepoWebUrl)(),
|
|
358
515
|
}));
|
|
359
516
|
}
|
|
@@ -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 —
|