@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.
- package/assets/skill/SKILL.md +10 -3
- package/assets/skill/references/confirmation.md +3 -2
- package/assets/skill/references/criteria.md +107 -0
- package/assets/skill/references/ideas.md +1 -1
- package/assets/skill/references/pr-scan.md +129 -0
- package/assets/skill/references/task-context.md +2 -2
- package/assets/skill/references/verify.md +3 -3
- package/dist/cli/src/commands/idea-slack-show.js +3 -1
- package/dist/cli/src/commands/idea-web-show.js +3 -1
- package/dist/cli/src/commands/pr-scan.js +224 -0
- package/dist/cli/src/commands/task-criteria-confirm.js +99 -0
- package/dist/cli/src/commands/task-slack-show.js +3 -1
- package/dist/cli/src/commands/task-status.js +84 -89
- package/dist/cli/src/commands/task-web-show.js +3 -1
- package/dist/cli/src/commands/verify.js +4 -1
- package/dist/cli/src/index.js +18 -0
- package/dist/cli/src/lib/redaction-notice.js +20 -0
- package/dist/cli/src/lib/scan-format.js +84 -0
- package/dist/lib/acceptance/unmet-display.js +28 -0
- package/dist/shared/src/hunt-audit.js +83 -0
- package/dist/shared/src/untrusted-content.js +512 -0
- package/package.json +1 -1
package/dist/cli/src/index.js
CHANGED
|
@@ -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
|
+
}
|