@lumoai/cli 1.61.0 → 1.62.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/references/verify.md +1 -0
- package/dist/cli/src/commands/task-status.js +9 -1
- package/dist/cli/src/lib/git-deleted-test-files.js +99 -0
- package/dist/cli/src/lib/hook-runner.js +52 -1
- package/dist/shared/src/security-scan.js +56 -1
- package/dist/shared/src/test-file-path.js +18 -0
- package/package.json +1 -1
|
@@ -145,6 +145,7 @@ what's unmet and why (the exact failure tails), and how many rounds are left.
|
|
|
145
145
|
- **Next actions** — the unmet criteria (latest verdict is not a pass: failed or never verified, HUMAN ones included) followed by any **undispositioned PR security findings** (LUM-737) as `• [SECURITY] [SEVERITY] PROVENANCE ruleId — file:line — title (PR #n · blocks DONE | advisory[ — <cause>, downgraded])`. The trailing **downgrade cause** appears only on deterministic `secret:*` rows the scanner lowered to LOW — `placeholder value` (the matched string looks like `…EXAMPLE`, `YOUR_…`, `CHANGEME`…) or `test/fixture/doc path`. Downgrading is **not** suppression: the row is still reported so a human decides, and the cause is what lets you tell a recognised test fixture from a real leak without opening the file. Absent on rows scanned before the cause was recorded, and on any row that was not downgraded — silence means "not downgraded", never "cause unknown". This list IS the plan — recomputed from the event log + the latest scan per linked PR on every read, never maintained separately. The header counts them separately: `Next actions (N unmet · M security findings)`. A finding is **not** a criterion: fix it and push (a fixed fingerprint disappears from the next scan) or a human dispositions it in the web delivery panel — there is no CLI path to clear one, and a `blocks DONE` finding refuses DONE with 409. Empty + rounds recorded = awaiting human adjudication.
|
|
146
146
|
- **Per-PR scan status** (LUM-735): before the unconfirmed-PR lines, one line per linked PR that has a latest scan (open or closed alike): `PR #945 · scan CLEAN · Secrets: checked · Code scan: checked (2 external findings) · Dependencies: checked (1 dependency finding, 1 already on main) · AI review: checked · Exploit paths: ⚠ incomplete` — stage segments only for keys present, in secrets/external/supplyChain/judge/hunt order. **The stage keys are internal; what prints is their display label** (LUM-763, `shared/src/security-scan.ts`, shared with the GitHub PR summary so the two cannot drift): `secrets`→Secrets, `external`→Code scan, `supplyChain`→Dependencies, `judge`→AI review, `hunt`→Exploit paths; states render as `RAN`→checked, `SKIPPED`→skipped, `NOT_CONFIGURED`→off, `PENDING`→scanning, `PARTIAL`→⚠ incomplete, `SUPERSEDED`→⚠ superseded, `FAILED`→✗ failed. "checked" means the layer completed, **not** that it found nothing — findings are counted in their own segments and in the next-actions list (`hunt` = the L3 vulnerability hunt, LUM-739: absent when `LUMO_SECURITY_HUNT=off`, `SKIPPED` when no trigger fired, `PENDING` while the workflow runs, `RAN` / `PARTIAL` (budget hit, a planned task that never finished, or more surviving candidates than the falsifier cap) / `FAILED`; its findings are `LLM_JUDGE` advisory, never `blocks DONE`); `(N external findings)` (singular at 1) decorates only the Code scan segment, and only when N > 0; `(N dependency findings[, M already on main])` (LUM-738; singular at 1) decorates only the Dependencies segment, only when N > 0, and prints bare `Dependencies: <state>` against an older server without the counts; ` · partial` appended when the scan is partial; and, when `stages.external === 'FAILED'` and a **scrubbed scanner reason** was recorded (the `error` column carries the `external: ` provenance prefix), an appended ` — <reason without the prefix, tail 200 chars>` — an unprefixed `error` (stage A's own raw crash text) is never printed here, same P8 rule as the web panel and the PR summary (LUM-756). Fed by the additive `securityFindings.scans` array; omitted entirely (no lines) against an older server that doesn't send it.
|
|
147
147
|
- **Hunt coverage line** (LUM-762): indented under its scan line whenever the server sends the L3 hunt's per-category audit — `Exploit paths 2/3 done · t1 idor 41.2s done · t2 injection 72.0s done (2 attempts) · t3 race skipped`. `<n>/N done` is how many planned tasks finished (where N is the number of tasks the planning step selected for this scan, typically 0–3). Rows persisted before LUM-758 lack a planned-task count and fall back to the historical 6-category denominator (reading as `/6` instead). Each segment is `[taskId ]<category> <elapsed>s <stop>` — the `taskId` prefix (`t1`, `t2`, …) appears only on current rows where the task graph is present (post-LUM-758), and is omitted on pre-task-graph audit rows, so they render exactly as they always did. Elapsed is summed over the task's attempts; `(N attempts)` when the workflow retried it; no elapsed for a `skipped` task (the scan's budget ran out before its turn). Stop values: `done` · `steps` · `budget` · `timeout` · `error` · `skipped`. Headed by the same display label as the layer's own segment (LUM-763), so the detail line and the line above it never name the layer two different ways. This is the only surface that reads `huntAudit`, so it is where you answer "how long does a healthy task take" and "which tasks never ran" without paging through deploy logs. Absent on an older server and on scans with no usable audit (pre-LUM-739 rows, or a hunt that failed before its first batch) — no line, never a guess.
|
|
148
|
+
- **Queue exit** (LUM-775): a merged / closed PR's line ends with ` · N left the queue (a resolved on main, b merged unreviewed, c PR closed)` when any undispositioned row has left the queue — derived at read time, never stored. `resolved on main` = an external-tool finding whose fingerprint is absent from a default-branch baseline that can testify (landed, whole, requested after the merge, ran that tool); `merged unreviewed` = an advisory row whose PR merged without a decision (a policy-blocking row never exits this way — it stays a next action and keeps blocking DONE until a human decides, or until a fresh baseline shows it gone); `PR closed` = closed without merging, nothing landed (reopening the PR puts the rows back). Exited rows are not next actions, are not counted in `openFindings`, and show muted with the reason in the web panel. Absent on an older server. The workspace-wide read-out with per-row reasons is `scripts/analysis/lum775-finding-queue/report.ts`.
|
|
148
149
|
- **Persisting findings** (LUM-738): a finding whose fingerprint is already on the repository's default-branch baseline (`persisting`) is not this PR's — it never appears as a next action, is not counted in `openFindings`, and shows muted (_already on the default branch_) in the web panel. Dependency findings (`kind=DEPENDENCY`, from osv-scanner) are advisory: they never block DONE.
|
|
149
150
|
- **Fails closed:** `⚠ Security-scan check failed — could not confirm …` when the scan read errored, and one `⚠ PR #n: … — could not confirm it is clean.` line per open PR whose latest scan is missing / FAILED / still running. Silence means a successful read with nothing open, never a failed check.
|
|
150
151
|
- **Open boundary crossings** — a trailing safety block when the task has ≥1 OPEN (undispositioned) forbidden-action crossing: a count split the way the server DONE gate counts it — `(N undispositioned: B blocking DONE, A advisory / non-blocking)` (LUM-771) — then one line per crossing, blocking rows first as `• [SEVERITY] CATEGORY — <clipped detail>` (highest-severity first) and non-blocking rows as `◦ [SEVERITY] CATEGORY (advisory — does not block DONE)` / `(below the workspace gate — does not block DONE)`; only the `•` rows need a ruling before DONE, each followed by a read-only **attribution** line `↳ by model=<m> · agent=<type>[/branch] · session=<8-char prefix>` (who/what crossed; any dimension that couldn't be resolved server-side prints `unknown`, never a fabricated value), then a pointer to `lumo crossing disposition` and the web acceptance panel. Silent when there are none, so it never overshadows the criteria.
|
|
@@ -65,7 +65,9 @@ const SCAN_STAGE_ORDER = security_scan_1.SCAN_STAGE_KEYS;
|
|
|
65
65
|
/** LUM-735 — one line per linked PR's latest scan: `PR #945 · scan CLEAN ·
|
|
66
66
|
* Secrets: checked · Code scan: checked (2 external findings) ·
|
|
67
67
|
* Dependencies: checked (1 dependency finding, 1 already on main) ·
|
|
68
|
-
* AI review: checked · Exploit paths: checked
|
|
68
|
+
* AI review: checked · Exploit paths: checked`, plus, on a merged / closed
|
|
69
|
+
* PR, ` · 3 left the queue (2 resolved on main, 1 merged unreviewed)`
|
|
70
|
+
* (LUM-775).
|
|
69
71
|
* Stage segments only for keys present, in
|
|
70
72
|
* secrets/external/supplyChain/judge/hunt order (LUM-739 added the hunt
|
|
71
73
|
* layer); the `(N external findings)` count only decorates the Code scan
|
|
@@ -101,6 +103,12 @@ function formatScanSummaryLine(s) {
|
|
|
101
103
|
line += ` · ${segments.join(' · ')}`;
|
|
102
104
|
if (s.partial)
|
|
103
105
|
line += ' · partial';
|
|
106
|
+
// LUM-775: what left the queue because the PR merged / closed, and why —
|
|
107
|
+
// so a merged PR's line explains where its findings went instead of
|
|
108
|
+
// silently listing fewer next actions than the scan found.
|
|
109
|
+
const exits = (0, security_scan_1.formatQueueExits)(s.queueExits);
|
|
110
|
+
if (exits)
|
|
111
|
+
line += ` · ${exits}`;
|
|
104
112
|
// LUM-756 (P8): `error` has two writers — only the prefixed, scrubbed
|
|
105
113
|
// scanner reason may be printed, and only the text after the prefix. Stage
|
|
106
114
|
// A's raw crash text carries no prefix and is never shown here (same rule
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.collectDeletedTestFiles = collectDeletedTestFiles;
|
|
4
|
+
exports.looksLikeDeleteCommand = looksLikeDeleteCommand;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const test_file_path_1 = require("../../../shared/src/test-file-path");
|
|
7
|
+
/**
|
|
8
|
+
* LUM-779 — the local half of the TEST_FILE_DELETED net-deletion verdict.
|
|
9
|
+
*
|
|
10
|
+
* The server's matcher only sees command TEXT, and text lies: a restore from
|
|
11
|
+
* HEAD, a `cp` back from a .bak, or an `rm` of a scratch probe the agent wrote
|
|
12
|
+
* seconds earlier all look like "deleted a test file" and none of them did.
|
|
13
|
+
* What settles it is repo state — is a tracked test file missing now — and
|
|
14
|
+
* only the hook, running on the agent's machine, can ask git that question.
|
|
15
|
+
*
|
|
16
|
+
* `collectDeletedTestFiles` answers with the tracked test files that are gone:
|
|
17
|
+
* - working tree + index vs HEAD (`git diff HEAD --diff-filter=D`) — a plain
|
|
18
|
+
* `rm` or a `git rm` of a tracked file, not yet committed
|
|
19
|
+
* - commits on top of the base branch (`git log --diff-filter=D base..HEAD`)
|
|
20
|
+
* — an `rm … && git commit` chained in one command would otherwise be
|
|
21
|
+
* invisible to the first probe
|
|
22
|
+
* Filtered by the shared test-path pattern, sorted, de-duplicated.
|
|
23
|
+
*
|
|
24
|
+
* `null` means the probe could not answer (not a repository, git missing, a
|
|
25
|
+
* timeout) — the caller omits the field and the server keeps its fail-closed
|
|
26
|
+
* blocking behaviour. `[]` is a verdict: the probe ran and nothing is missing.
|
|
27
|
+
*/
|
|
28
|
+
/** Base-branch candidates, in order. The first that resolves wins. */
|
|
29
|
+
const BASE_REF_CANDIDATES = [
|
|
30
|
+
'refs/remotes/origin/HEAD',
|
|
31
|
+
'refs/remotes/origin/main',
|
|
32
|
+
'refs/remotes/origin/master',
|
|
33
|
+
'refs/remotes/origin/develop',
|
|
34
|
+
];
|
|
35
|
+
const GIT_TIMEOUT_MS = 2000;
|
|
36
|
+
function runGit(args, cwd) {
|
|
37
|
+
try {
|
|
38
|
+
const r = (0, child_process_1.spawnSync)('git', args, {
|
|
39
|
+
cwd,
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
timeout: GIT_TIMEOUT_MS,
|
|
42
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
43
|
+
});
|
|
44
|
+
if (r.error || r.status !== 0)
|
|
45
|
+
return { ok: false, stdout: '' };
|
|
46
|
+
return { ok: true, stdout: r.stdout ?? '' };
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return { ok: false, stdout: '' };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** NUL-separated path list → paths (empty entries dropped). */
|
|
53
|
+
function splitPaths(stdoutZ) {
|
|
54
|
+
return stdoutZ.split('\0').filter(p => p.length > 0);
|
|
55
|
+
}
|
|
56
|
+
function resolveBaseRef(cwd) {
|
|
57
|
+
for (const ref of BASE_REF_CANDIDATES) {
|
|
58
|
+
if (runGit(['rev-parse', '--verify', '-q', ref], cwd).ok)
|
|
59
|
+
return ref;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function collectDeletedTestFiles(cwd) {
|
|
64
|
+
// Working tree + index vs HEAD. Also the "is this a repo with a HEAD"
|
|
65
|
+
// probe: a non-repository, or git itself missing, fails here → null.
|
|
66
|
+
const uncommitted = runGit(['diff', 'HEAD', '--diff-filter=D', '--name-only', '-z'], cwd);
|
|
67
|
+
if (!uncommitted.ok)
|
|
68
|
+
return null;
|
|
69
|
+
const missing = new Set(splitPaths(uncommitted.stdout));
|
|
70
|
+
// Deletions already committed on top of the base branch. Best-effort: no
|
|
71
|
+
// resolvable base (a repo with no remote) just skips this half.
|
|
72
|
+
const base = resolveBaseRef(cwd);
|
|
73
|
+
if (base) {
|
|
74
|
+
const committed = runGit([
|
|
75
|
+
'log',
|
|
76
|
+
'--diff-filter=D',
|
|
77
|
+
'--name-only',
|
|
78
|
+
'--format=',
|
|
79
|
+
'-z',
|
|
80
|
+
`${base}..HEAD`,
|
|
81
|
+
], cwd);
|
|
82
|
+
if (committed.ok) {
|
|
83
|
+
for (const p of splitPaths(committed.stdout))
|
|
84
|
+
missing.add(p);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return [...missing].filter(test_file_path_1.isTestFilePath).sort();
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Cheap pre-filter deciding whether the probe is worth running at all: an
|
|
91
|
+
* `rm` / `git rm` token anywhere in the command, including inside a `bash -c`
|
|
92
|
+
* wrapper string. Deliberately a SUPERSET of the server's matcher — a probe
|
|
93
|
+
* that runs for an `rm` of a non-test file costs two git calls and nothing
|
|
94
|
+
* else, whereas a probe that is skipped for a command the server does flag
|
|
95
|
+
* leaves the server fail-closed on a possible false positive.
|
|
96
|
+
*/
|
|
97
|
+
function looksLikeDeleteCommand(command) {
|
|
98
|
+
return /(?:^|[\s(;&|"'`])(?:rm|git\s+rm)\b/.test(command);
|
|
99
|
+
}
|
|
@@ -4,6 +4,7 @@ exports.formatHookStdoutLines = formatHookStdoutLines;
|
|
|
4
4
|
exports.formatSuggestLine = formatSuggestLine;
|
|
5
5
|
exports.resolveSessionStartStdout = resolveSessionStartStdout;
|
|
6
6
|
exports.augmentBodyWithUsage = augmentBodyWithUsage;
|
|
7
|
+
exports.augmentBodyWithDeletedTestFiles = augmentBodyWithDeletedTestFiles;
|
|
7
8
|
exports.runHook = runHook;
|
|
8
9
|
exports.runHookWithBody = runHookWithBody;
|
|
9
10
|
const config_1 = require("./config");
|
|
@@ -16,6 +17,7 @@ const transcript_usage_1 = require("./transcript-usage");
|
|
|
16
17
|
const resolve_project_1 = require("./resolve-project");
|
|
17
18
|
const resolve_bound_task_1 = require("./resolve-bound-task");
|
|
18
19
|
const memory_auto_1 = require("./memory-auto");
|
|
20
|
+
const git_deleted_test_files_1 = require("./git-deleted-test-files");
|
|
19
21
|
/**
|
|
20
22
|
* Hard timeout for the hook POST. On timeout the request is aborted,
|
|
21
23
|
* logged, and `runHook` exits 0 — Claude Code is never blocked beyond
|
|
@@ -261,6 +263,54 @@ function augmentBodyWithUsage(path, body) {
|
|
|
261
263
|
_lumo_usage: { ...usage.total, byModel: usage.byModel },
|
|
262
264
|
});
|
|
263
265
|
}
|
|
266
|
+
/**
|
|
267
|
+
* LUM-779: for a post-tool-use Bash event whose command carries an `rm` /
|
|
268
|
+
* `git rm`, ask local git which tracked test files are actually missing now
|
|
269
|
+
* and fold the answer into the body under `_lumo_deleted_test_files`. The
|
|
270
|
+
* server judges TEST_FILE_DELETED by this list — not by the command text —
|
|
271
|
+
* so a restore or an `rm` of an untracked scratch probe is recorded advisory
|
|
272
|
+
* while a real deletion of a tracked test still blocks.
|
|
273
|
+
*
|
|
274
|
+
* The three wire states matter: a non-empty list (blocking), an EMPTY list
|
|
275
|
+
* (probe ran, nothing missing → advisory), and NO field (probe skipped or
|
|
276
|
+
* git could not answer → the server fails closed). So the field is only ever
|
|
277
|
+
* attached when the probe returned an array, and a probe that throws leaves
|
|
278
|
+
* the body untouched. Never blocks or fails the hook.
|
|
279
|
+
*/
|
|
280
|
+
function augmentBodyWithDeletedTestFiles(path, body, probe = git_deleted_test_files_1.collectDeletedTestFiles) {
|
|
281
|
+
if (path !== 'post-tool-use')
|
|
282
|
+
return body;
|
|
283
|
+
let parsed;
|
|
284
|
+
try {
|
|
285
|
+
parsed = JSON.parse(body || '{}');
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return body;
|
|
289
|
+
}
|
|
290
|
+
if (!parsed || typeof parsed !== 'object')
|
|
291
|
+
return body;
|
|
292
|
+
if (parsed['tool_name'] !== 'Bash')
|
|
293
|
+
return body;
|
|
294
|
+
const input = parsed['tool_input'];
|
|
295
|
+
const command = input && typeof input === 'object'
|
|
296
|
+
? input['command']
|
|
297
|
+
: undefined;
|
|
298
|
+
if (typeof command !== 'string' || !(0, git_deleted_test_files_1.looksLikeDeleteCommand)(command))
|
|
299
|
+
return body;
|
|
300
|
+
const cwd = typeof parsed['cwd'] === 'string' && parsed['cwd']
|
|
301
|
+
? parsed['cwd']
|
|
302
|
+
: process.cwd();
|
|
303
|
+
let deleted;
|
|
304
|
+
try {
|
|
305
|
+
deleted = probe(cwd);
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
return body;
|
|
309
|
+
}
|
|
310
|
+
if (deleted === null)
|
|
311
|
+
return body;
|
|
312
|
+
return JSON.stringify({ ...parsed, _lumo_deleted_test_files: deleted });
|
|
313
|
+
}
|
|
264
314
|
/**
|
|
265
315
|
* POST the hook body to /api/hooks/<path> with a short timeout. All errors
|
|
266
316
|
* — credential missing, network failure, timeout, non-2xx — are routed to
|
|
@@ -309,7 +359,8 @@ async function runHookWithBody(path, body, agentToken) {
|
|
|
309
359
|
const agentEnum = agentToken ? (0, agent_1.normalizeAgent)(agentToken) : null;
|
|
310
360
|
if (agentEnum)
|
|
311
361
|
headers['X-Lumo-Agent'] = agentEnum;
|
|
312
|
-
const outgoingBody = augmentBodyWithUsage(path, body) ||
|
|
362
|
+
const outgoingBody = augmentBodyWithDeletedTestFiles(path, augmentBodyWithUsage(path, body)) ||
|
|
363
|
+
'{}';
|
|
313
364
|
try {
|
|
314
365
|
const res = await fetch(url, {
|
|
315
366
|
method: 'POST',
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SECRET_DOWNGRADE_LABELS = exports.SCAN_STAGE_STATE_LABELS = exports.SCAN_STAGE_LABELS = exports.SCAN_STAGE_STATES = exports.SCAN_STAGE_KEYS = exports.EXTERNAL_ERROR_PREFIX = void 0;
|
|
3
|
+
exports.QUEUE_EXIT_LABELS = exports.QUEUE_EXIT_REASONS = exports.SECRET_DOWNGRADE_LABELS = exports.SCAN_STAGE_STATE_LABELS = exports.SCAN_STAGE_LABELS = exports.SCAN_STAGE_STATES = exports.SCAN_STAGE_KEYS = exports.EXTERNAL_ERROR_PREFIX = void 0;
|
|
4
4
|
exports.externalFailureReason = externalFailureReason;
|
|
5
5
|
exports.scanStageLabel = scanStageLabel;
|
|
6
6
|
exports.scanStageStateLabel = scanStageStateLabel;
|
|
7
7
|
exports.secretDowngradeNote = secretDowngradeNote;
|
|
8
|
+
exports.formatQueueExits = formatQueueExits;
|
|
8
9
|
/**
|
|
9
10
|
* PR security scan — the one `PrSecurityScan.error` shape that may be shown
|
|
10
11
|
* as an external-scanner reason (spec P8). Shared by the web panel, the PR
|
|
@@ -123,3 +124,57 @@ function secretDowngradeNote(reason) {
|
|
|
123
124
|
return '';
|
|
124
125
|
return ` — ${exports.SECRET_DOWNGRADE_LABELS[reason] ?? reason}, downgraded`;
|
|
125
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* LUM-775 — why an undispositioned finding is no longer in the queue.
|
|
129
|
+
*
|
|
130
|
+
* A PR finding's review window is the PR. Once the PR is merged or closed
|
|
131
|
+
* the question changes from "what did this change introduce" to "what is on
|
|
132
|
+
* main now", and only the default-branch baseline (LUM-738) may answer that.
|
|
133
|
+
* The exit is DERIVED at read time from PR state × baseline × policy — never
|
|
134
|
+
* written to the row, so `disposition` stays a human-only column.
|
|
135
|
+
*
|
|
136
|
+
* - `RESOLVED_ON_MAIN`: an external-tool finding whose fingerprint is absent
|
|
137
|
+
* from a baseline that can testify (landed, whole, requested after the
|
|
138
|
+
* merge, ran that tool). The one exit with evidence.
|
|
139
|
+
* - `MERGED_UNREVIEWED`: the window closed without a decision. Advisory rows
|
|
140
|
+
* only — a policy-blocking row stays until a human decides.
|
|
141
|
+
* - `PR_CLOSED`: closed without merging; nothing landed. Reopening the PR
|
|
142
|
+
* puts the row straight back (derived, not stored).
|
|
143
|
+
*/
|
|
144
|
+
exports.QUEUE_EXIT_REASONS = [
|
|
145
|
+
'RESOLVED_ON_MAIN',
|
|
146
|
+
'MERGED_UNREVIEWED',
|
|
147
|
+
'PR_CLOSED',
|
|
148
|
+
];
|
|
149
|
+
/** One wording per exit on every surface (CLI scan line, analysis report). */
|
|
150
|
+
exports.QUEUE_EXIT_LABELS = {
|
|
151
|
+
RESOLVED_ON_MAIN: 'resolved on main',
|
|
152
|
+
MERGED_UNREVIEWED: 'merged unreviewed',
|
|
153
|
+
PR_CLOSED: 'PR closed',
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* `3 left the queue (2 resolved on main, 1 merged unreviewed)`, or null when
|
|
157
|
+
* nothing left — the caller omits the segment rather than printing a zero.
|
|
158
|
+
* Reasons print in the declared order; an unknown key (a newer server) is
|
|
159
|
+
* counted in the total and printed as itself, never dropped.
|
|
160
|
+
*/
|
|
161
|
+
function formatQueueExits(counts) {
|
|
162
|
+
if (!counts)
|
|
163
|
+
return null;
|
|
164
|
+
const parts = [];
|
|
165
|
+
let total = 0;
|
|
166
|
+
const keys = [
|
|
167
|
+
...exports.QUEUE_EXIT_REASONS.filter(k => k in counts),
|
|
168
|
+
...Object.keys(counts).filter(k => !exports.QUEUE_EXIT_REASONS.includes(k)),
|
|
169
|
+
];
|
|
170
|
+
for (const key of keys) {
|
|
171
|
+
const n = counts[key] ?? 0;
|
|
172
|
+
if (n <= 0)
|
|
173
|
+
continue;
|
|
174
|
+
total += n;
|
|
175
|
+
parts.push(`${n} ${exports.QUEUE_EXIT_LABELS[key] ?? key}`);
|
|
176
|
+
}
|
|
177
|
+
if (total === 0)
|
|
178
|
+
return null;
|
|
179
|
+
return `${total} left the queue (${parts.join(', ')})`;
|
|
180
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TEST_FILE_TARGET = void 0;
|
|
4
|
+
exports.isTestFilePath = isTestFilePath;
|
|
5
|
+
/**
|
|
6
|
+
* What counts as a test file / test tree for the TEST_FILE_DELETED session
|
|
7
|
+
* guard (LUM-736): `*.test.*`, `*.spec.*`, or anything under a `__tests__/`
|
|
8
|
+
* segment. Single-sourced here (LUM-779) because two sides must agree on it:
|
|
9
|
+
* the server's command matcher (lib/services/boundary-detector.service.ts)
|
|
10
|
+
* and the CLI's local-git probe (cli/src/lib/git-deleted-test-files.ts) that
|
|
11
|
+
* reports which tracked test files are actually missing. A pattern that
|
|
12
|
+
* drifted between the two would let a real deletion slip through as "nothing
|
|
13
|
+
* missing" — so there is exactly one pattern.
|
|
14
|
+
*/
|
|
15
|
+
exports.TEST_FILE_TARGET = /(?:\.(?:test|spec)\.|(?:^|\/)__tests__(?:\/|$))/;
|
|
16
|
+
function isTestFilePath(path) {
|
|
17
|
+
return exports.TEST_FILE_TARGET.test(path);
|
|
18
|
+
}
|