@lumoai/cli 1.65.0 → 1.68.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.
@@ -92,6 +92,7 @@ const memory_fold_1 = require("./commands/memory-fold");
92
92
  const task_artifact_add_1 = require("./commands/task-artifact-add");
93
93
  const task_criteria_set_1 = require("./commands/task-criteria-set");
94
94
  const task_criteria_list_1 = require("./commands/task-criteria-list");
95
+ const task_criteria_confirm_1 = require("./commands/task-criteria-confirm");
95
96
  const task_artifact_list_1 = require("./commands/task-artifact-list");
96
97
  const task_artifact_show_1 = require("./commands/task-artifact-show");
97
98
  const task_artifact_rm_1 = require("./commands/task-artifact-rm");
@@ -102,6 +103,7 @@ const task_figma_context_1 = require("./commands/task-figma-context");
102
103
  const task_comment_list_1 = require("./commands/task-comment-list");
103
104
  const task_deps_1 = require("./commands/task-deps");
104
105
  const task_pr_show_1 = require("./commands/task-pr-show");
106
+ const pr_scan_1 = require("./commands/pr-scan");
105
107
  const task_lineage_1 = require("./commands/task-lineage");
106
108
  const project_list_1 = require("./commands/project-list");
107
109
  const milestone_list_1 = require("./commands/milestone-list");
@@ -434,6 +436,16 @@ priorityCmd
434
436
  .command('list')
435
437
  .description('Print the team priority append history, newest-first (the current declaration is marked with *).')
436
438
  .action(wrap(() => (0, priority_1.priorityList)()));
439
+ const pr = program
440
+ .command('pr')
441
+ .description('Inspect pull requests by number — linked to a task or not');
442
+ pr.command('scan <number>')
443
+ .description('Security scans of one PR in the workspace, by PR number (LUM-788): the latest head by default — layer states (same labels as task status / the GitHub summary), every finding incl. dispositioned ones with disposition + source, and the hunt coverage line. Read-only; disposition is web-only.')
444
+ .option('--repo <owner/repo>', 'Narrow to one repo when the same PR number exists in several (the error lists the candidates)')
445
+ .option('--all', "Every pushed head's scan, newest first, instead of the latest only")
446
+ .option('--full', 'Append the per-task hunt detail: commands (with gate refusals), step trace, stop arithmetic, wrap-up, sites, model, diff sizing')
447
+ .option('--json', 'Emit the versioned machine-readable payload ({ pullRequest, scans[].stages / findings[] / hunt.tasks[] })')
448
+ .action(wrap((number, options) => (0, pr_scan_1.prScan)(number, options)));
437
449
  const criteria = program
438
450
  .command('criteria')
439
451
  .description('Workspace-level acceptance-criteria analytics');
@@ -625,6 +637,12 @@ taskCriteria
625
637
  .command('list <task>')
626
638
  .description('List a task\u2019s acceptance criteria (id, type, provenance, checkpointer)')
627
639
  .action(wrap((taskId) => (0, task_criteria_list_1.taskCriteriaList)(taskId)));
640
+ taskCriteria
641
+ .command('confirm <task>')
642
+ .description('Record the USER\u2019s confirmation of the acceptance contract (LUM-789). Gated: without --confirm it prints a confirmation envelope listing every criterion and exits 4, writing nothing. Confirming never blocks DONE \u2014 an unconfirmed contract still closes normally.')
643
+ .option('--confirm', 'The user has approved this contract')
644
+ .option('--yes', 'Legacy alias of --confirm')
645
+ .action(wrap((taskId, options) => (0, task_criteria_confirm_1.taskCriteriaConfirm)(taskId, options)));
628
646
  const taskArtifact = task
629
647
  .command('artifact')
630
648
  .description('Record spec-engineering artifacts (spec / plan / design …) on a task');
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.printRedactionNotice = printRedactionNotice;
4
+ /**
5
+ * LUM-784 — the tier-2 retrieval commands (`task|idea slack show`,
6
+ * `task|idea web show`) print stored bodies straight into the agent's
7
+ * context. The server redacts every registry secret on the way out and
8
+ * reports how many; this prints the one-line notice so the agent knows the
9
+ * `AKIA…(len 20)` tokens it sees are redactions, not the source text.
10
+ *
11
+ * Silent when the count is absent (older server) or zero — output for clean
12
+ * content is byte-identical to before.
13
+ */
14
+ function printRedactionNotice(redactedSecrets) {
15
+ if (!redactedSecrets || redactedSecrets <= 0)
16
+ return;
17
+ const noun = redactedSecrets === 1 ? 'credential' : 'credentials';
18
+ const verb = redactedSecrets === 1 ? 'was' : 'were';
19
+ console.log(`⚠ ${redactedSecrets} suspected ${noun} ${verb} redacted before injection (shown as first 4 chars + length; the original never reached this context)`);
20
+ }
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.formatScanLayerTail = formatScanLayerTail;
4
+ exports.formatHuntCoverageLine = formatHuntCoverageLine;
5
+ const sanitize_1 = require("./sanitize");
6
+ const security_scan_1 = require("../../../shared/src/security-scan");
7
+ const EXTERNAL_ERROR_TAIL = 200;
8
+ function tail(s, max) {
9
+ return s.length > max ? `…${s.slice(-max)}` : s;
10
+ }
11
+ /**
12
+ * ` · Secrets: checked · Code scan: checked (2 external findings) ·
13
+ * Dependencies: checked (1 dependency finding, 1 already on main) ·
14
+ * AI review: checked · Exploit paths: checked · partial · 3 left the queue
15
+ * (2 resolved on main, 1 merged unreviewed) — <scrubbed reason>`, or `''`
16
+ * when the scan carries no stage at all. The caller prefixes its own head
17
+ * (` PR #945 · scan CLEAN` / `Scan abc1234 · FINDINGS`).
18
+ *
19
+ * Stage segments only for keys present, in secrets/external/supplyChain/
20
+ * judge/hunt order; the `(N external findings)` count only decorates the
21
+ * Code scan segment, the dependency count (with its already-on-main share)
22
+ * only the Dependencies segment, and each only when N > 0.
23
+ */
24
+ function formatScanLayerTail(s) {
25
+ const segments = [];
26
+ for (const key of security_scan_1.SCAN_STAGE_KEYS) {
27
+ const state = s.stages[key];
28
+ if (!state)
29
+ continue;
30
+ const head = `${(0, security_scan_1.scanStageLabel)(key)}: ${(0, security_scan_1.scanStageStateLabel)(state)}`;
31
+ if (key === 'external' && s.externalFindings > 0) {
32
+ const noun = s.externalFindings === 1 ? 'finding' : 'findings';
33
+ segments.push(`${head} (${s.externalFindings} external ${noun})`);
34
+ }
35
+ else if (key === 'supplyChain' && (s.dependencyFindings ?? 0) > 0) {
36
+ const n = s.dependencyFindings ?? 0;
37
+ const noun = n === 1 ? 'finding' : 'findings';
38
+ const persisting = s.persistingFindings ?? 0;
39
+ const tail = persisting > 0 ? `, ${persisting} already on main` : '';
40
+ segments.push(`${head} (${n} dependency ${noun}${tail})`);
41
+ }
42
+ else {
43
+ segments.push(head);
44
+ }
45
+ }
46
+ let line = '';
47
+ if (segments.length > 0)
48
+ line += ` · ${segments.join(' · ')}`;
49
+ if (s.partial)
50
+ line += ' · partial';
51
+ // LUM-775: what left the queue because the PR merged / closed, and why —
52
+ // so a merged PR's line explains where its findings went instead of
53
+ // silently listing fewer open rows than the scan found.
54
+ const exits = (0, security_scan_1.formatQueueExits)(s.queueExits);
55
+ if (exits)
56
+ line += ` · ${exits}`;
57
+ // LUM-756 (P8): only the prefixed, scrubbed scanner reason is printed, and
58
+ // only the text after the prefix (the caller has already applied the rule).
59
+ if (s.externalFailureReason) {
60
+ line += ` — ${(0, sanitize_1.sanitizeField)(tail(s.externalFailureReason, EXTERNAL_ERROR_TAIL))}`;
61
+ }
62
+ return line;
63
+ }
64
+ /**
65
+ * LUM-762 — the hunt's per-task read-out, one line under its scan:
66
+ * `Exploit paths 2/3 done · t1 idor 41.2s done · t2 injection 72.0s done
67
+ * (2 attempts) · t3 race skipped`. Headed by the same display label as the
68
+ * layer's own segment (LUM-763) — the detail line and the line above it
69
+ * must not name the layer two different ways. Elapsed time is summed over a
70
+ * task's attempts; a skipped task has none to print.
71
+ *
72
+ * LUM-758: a plan may hold two tasks of the same category, so the row is led
73
+ * by its task id. Pre-task-graph audits carry no id — the row then prints
74
+ * exactly as it always did.
75
+ */
76
+ function formatHuntCoverageLine(hunt) {
77
+ const parts = hunt.categories.map(c => {
78
+ const attempts = c.attempts > 1 ? ` (${c.attempts} attempts)` : '';
79
+ const spent = c.stopped === 'skipped' ? '' : `${(c.elapsedMs / 1000).toFixed(1)}s `;
80
+ const id = c.taskId === undefined ? '' : `${(0, sanitize_1.sanitizeField)(c.taskId)} `;
81
+ return `${id}${(0, sanitize_1.sanitizeField)(c.category)} ${spent}${(0, sanitize_1.sanitizeField)(c.stopped)}${attempts}`;
82
+ });
83
+ return `${(0, security_scan_1.scanStageLabel)('hunt')} ${hunt.done}/${hunt.total} done · ${parts.join(' · ')}`;
84
+ }
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isUnmetForDisplay = isUnmetForDisplay;
4
+ /** PASS 与 PASS_WITH_FOLLOWUP 都算通过(与 verification-run 仓库的
5
+ * isPassVerdict 同义;这里本地实现以保持本模块零依赖)。 */
6
+ function isPass(verdict) {
7
+ return verdict === 'PASS' || verdict === 'PASS_WITH_FOLLOWUP';
8
+ }
9
+ /**
10
+ * LUM-789 渲染口径:一条 criterion 是否该呈现为「未满足」。
11
+ *
12
+ * 未裁定的 HUMAN 项是 **tacit pass** —— 没人送回就是默认通过,与 DONE 闸
13
+ * 早已采用的口径一致(task-state.service.ts 的 assertNoUnresolvedSendBack
14
+ * 只对明确 FAIL 返回 409)。把它呈现成待办是纯摩擦:一个总会通过的闸产出
15
+ * 的不是信号,是伪装成信号的噪音。
16
+ *
17
+ * ⚠️ 这是 **渲染** 口径,不是裁决口径。裁决用的是
18
+ * task-human-verdict.service.ts 的 `allMet`(every criterion isPassVerdict),
19
+ * 它必须保持「未裁定 ≠ 通过」,否则在 web 上点一条 PASS 会把整个任务推成
20
+ * DONE。两者不得合并。
21
+ */
22
+ function isUnmetForDisplay(criterion, latest) {
23
+ if (isPass(latest?.verdict))
24
+ return false;
25
+ if (latest === undefined && criterion.verifierType === 'HUMAN')
26
+ return false;
27
+ return true;
28
+ }
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * LUM-788 — the per-task hunt read-out, shared by the CLI (`lumo pr scan
4
+ * --full`) and the lum739 analysis script. One implementation: the CLI is a
5
+ * separate package that cannot import `lib/` or `scripts/`, and a second copy
6
+ * of this printer is exactly how the audit lost four fields to "computed,
7
+ * carried, never read" before LUM-758.
8
+ *
9
+ * Pure formatting over the raw `PrSecurityScan.huntAudit.tasks[]` rows. Every
10
+ * field is optional by construction: the column is Json written by past
11
+ * versions of the workflow, and a row written before a field existed prints
12
+ * exactly as it always did — nothing is guessed, nothing is thrown over.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.formatHuntTaskDetail = formatHuntTaskDetail;
16
+ exports.formatShellDetail = formatShellDetail;
17
+ /** One line of prose, flattened, for a fixed-width read-out. */
18
+ function oneLine(text, max) {
19
+ const flat = text.replace(/\s+/g, ' ').trim();
20
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
21
+ }
22
+ /**
23
+ * The per-task listing for EVERY task handed in, idle ones included — the
24
+ * `lumo pr scan --full` shape. A task that never asked the shell for anything
25
+ * still has a stop line, a model and the sites it was briefed on, and "this
26
+ * task ran two steps and submitted nothing" is a fact the reader came for.
27
+ *
28
+ * Lines per task: a header (`t1 done · <model> · 3 sites · 2 cmds (gate
29
+ * refused …)`), one line per command (`✗` = the gate refused it, with its
30
+ * reason), one line per trace step, then the stop arithmetic and the
31
+ * wrap-up outcome — each only when the row carries it.
32
+ */
33
+ function formatHuntTaskDetail(tasks, maxTasks) {
34
+ const out = [];
35
+ for (const t of tasks.slice(0, maxTasks)) {
36
+ const sh = t.shell;
37
+ // LUM-787 — the model and the sites print only when the row carries them;
38
+ // the clip count likewise: an older row, and a row from a search that
39
+ // never hit the ceiling, read as before.
40
+ const cmds = sh?.calls
41
+ ? `${sh.calls} cmds (gate refused ${sh.refused ?? 0}, sandbox failed ${sh.failed ?? 0}, not found ${sh.notFound ?? 0}, empty ${sh.empty ?? 0}${sh.stepClipped ? `, step-clipped ${sh.stepClipped}` : ''})`
42
+ : 'no shell commands';
43
+ out.push(`${String(t.taskId)} ${String(t.stopped)}${t.model ? ` · ${t.model}` : ''}${typeof t.sites === 'number' ? ` · ${t.sites} sites` : ''} · ${cmds}`);
44
+ for (const c of sh?.commands ?? []) {
45
+ const cost = typeof c.chars === 'number'
46
+ ? ` [${c.chars} chars${c.compressed ? ', compressed' : ''}${c.clipped ? ', clipped' : ''}]`
47
+ : '';
48
+ out.push(` ${c.denied ? '✗' : ' '} ${c.cmd ?? ''}${cost}${c.reason ? ` ← ${c.reason}` : ''}`);
49
+ }
50
+ // LUM-781 — the model's side, step by step, then how it ended. Only when
51
+ // the row carries them: an older row prints exactly as before.
52
+ for (const st of t.trace ?? []) {
53
+ out.push(` #${st.i ?? '?'} tool=${st.tool ?? '(none)'} in=${st.inputTokens ?? '?'} out=${st.outputTokens ?? '?'}${st.reasoningTokens ? ` reasoning=${st.reasoningTokens}` : ''}${st.elidedChars ? ` elided=${st.elidedChars}` : ''}${st.finishReason ? ` finish=${st.finishReason}` : ''}${st.text ? ` "${oneLine(st.text, 160)}"` : ''}`);
54
+ }
55
+ if (t.stop) {
56
+ const s = t.stop;
57
+ out.push(` stop: ${s.by ?? '?'} · spent ${s.spent ?? '?'}/${s.budget ?? '?'} · reserve ${s.reserve ?? 0} · last step ${s.lastStep ?? '?'}${typeof s.next === 'number' ? ` · next ${s.next}` : ''} · ${s.steps ?? '?'} steps`);
58
+ }
59
+ if (t.wrapUp) {
60
+ const w = t.wrapUp;
61
+ out.push(w.ran
62
+ ? ` wrap-up: ran · submitted ${w.submitted ?? 0} · ${w.tokens ?? '?'} tokens${w.finishReason ? ` · finish=${w.finishReason}` : ''}${w.text ? ` · "${oneLine(w.text, 200)}"` : ''}`
63
+ : w.error
64
+ ? ` wrap-up: attempted, failed — ${oneLine(w.error, 160)}`
65
+ : ` wrap-up: not made (${w.skipped ?? 'unknown'})`);
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /**
71
+ * The per-task, command-by-command listing of the lum739 audit report —
72
+ * only tasks that actually asked the shell for something.
73
+ *
74
+ * The run-level `shell:` line says HOW MUCH; only this says WHAT — and telling
75
+ * "mapped the repo and ran out" from "the gate rejected everything" from
76
+ * "looked and found nothing" is the entire reason the field exists. Extracted
77
+ * as a pure function so it can be tested: a read-out nothing exercises is the
78
+ * same "computed, carried, never read" shape this layer has already lost four
79
+ * fields to.
80
+ */
81
+ function formatShellDetail(tasks, maxTasks) {
82
+ return formatHuntTaskDetail(tasks.filter(x => x.shell?.calls), maxTasks);
83
+ }