@lumoai/cli 1.58.0 → 1.60.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.
Files changed (47) hide show
  1. package/assets/skill/SKILL.md +15 -17
  2. package/assets/skill/references/artifacts-figma.md +4 -3
  3. package/assets/skill/references/confirmation.md +133 -0
  4. package/assets/skill/references/criteria.md +12 -21
  5. package/assets/skill/references/doc-editing.md +11 -9
  6. package/assets/skill/references/docs.md +4 -3
  7. package/assets/skill/references/memory.md +4 -2
  8. package/assets/skill/references/milestones.md +3 -2
  9. package/assets/skill/references/outcome.md +1 -14
  10. package/assets/skill/references/plan-runs.md +5 -0
  11. package/assets/skill/references/sessions.md +4 -4
  12. package/assets/skill/references/sprints.md +18 -17
  13. package/assets/skill/references/task-deps.md +4 -3
  14. package/assets/skill/references/tasks.md +34 -2
  15. package/assets/skill/references/verify.md +71 -71
  16. package/assets/skill/references/worktree.md +13 -7
  17. package/dist/cli/src/commands/crossing-disposition.js +342 -0
  18. package/dist/cli/src/commands/crossing-explain.js +10 -21
  19. package/dist/cli/src/commands/doc-delete.js +35 -23
  20. package/dist/cli/src/commands/doc-rebuild-source.js +16 -4
  21. package/dist/cli/src/commands/memory-rm.js +68 -10
  22. package/dist/cli/src/commands/milestone-delete.js +13 -10
  23. package/dist/cli/src/commands/outcome.js +0 -77
  24. package/dist/cli/src/commands/session-attach.js +8 -2
  25. package/dist/cli/src/commands/sprint-close.js +29 -9
  26. package/dist/cli/src/commands/sprint-delete.js +13 -10
  27. package/dist/cli/src/commands/sprint-show.js +3 -9
  28. package/dist/cli/src/commands/task-artifact-rm.js +58 -28
  29. package/dist/cli/src/commands/task-criteria-list.js +1 -4
  30. package/dist/cli/src/commands/task-criteria-set.js +3 -12
  31. package/dist/cli/src/commands/task-deps.js +20 -6
  32. package/dist/cli/src/commands/task-status.js +196 -111
  33. package/dist/cli/src/commands/task-update.js +129 -0
  34. package/dist/cli/src/commands/verify.js +22 -13
  35. package/dist/cli/src/commands/worktree-rm.js +35 -7
  36. package/dist/cli/src/index.js +60 -48
  37. package/dist/cli/src/lib/blocked-error.js +183 -0
  38. package/dist/cli/src/lib/bound-task.js +32 -0
  39. package/dist/cli/src/lib/confirmation.js +119 -0
  40. package/dist/cli/src/lib/hook-runner.js +23 -11
  41. package/dist/cli/src/lib/open-crossings.js +6 -6
  42. package/dist/shared/src/referent-kind.js +31 -1
  43. package/dist/shared/src/security-scan.js +125 -0
  44. package/package.json +1 -1
  45. package/assets/skill/references/fidelity.md +0 -32
  46. package/dist/cli/src/commands/fidelity.js +0 -108
  47. package/dist/cli/src/commands/verdict.js +0 -189
@@ -68,8 +68,11 @@ function readStdin() {
68
68
  *
69
69
  * For 'pre-tool-use' the array contains:
70
70
  * [0] (optional) a PreToolUse hookSpecificOutput JSON carrying the parallel-
71
- * edit collision warning as additionalContext, when the server returned a
72
- * `collisionWarning` (LUM-150 step ③). Empty otherwise.
71
+ * edit collision warning (LUM-150 step ③), the one-shot boundary-
72
+ * crossing reminder (LUM-542), and the one-shot PR security-scan
73
+ * reminder (LUM-737) as additionalContext, when the server returned any
74
+ * of `collisionWarning`, `crossingReminder`, `securityReminder`. Empty
75
+ * otherwise.
73
76
  *
74
77
  * The JSON lines conform to Claude Code's hookSpecificOutput envelope so the
75
78
  * runtime injects additionalContext into the conversation automatically.
@@ -83,8 +86,9 @@ _now = new Date()) {
83
86
  if (responseBody == null || typeof responseBody !== 'object')
84
87
  return [];
85
88
  const body = responseBody;
86
- // Two independent PreToolUse signals (LUM-150 collision, LUM-542 crossing
87
- // reminder) share one additionalContext block when both are present.
89
+ // Three independent PreToolUse signals (LUM-150 collision, LUM-542
90
+ // crossing reminder, LUM-737 security reminder) share one
91
+ // additionalContext block when more than one is present.
88
92
  const parts = [];
89
93
  if (typeof body.collisionWarning === 'string' &&
90
94
  body.collisionWarning !== '')
@@ -92,6 +96,9 @@ _now = new Date()) {
92
96
  if (typeof body.crossingReminder === 'string' &&
93
97
  body.crossingReminder !== '')
94
98
  parts.push(body.crossingReminder);
99
+ if (typeof body.securityReminder === 'string' &&
100
+ body.securityReminder !== '')
101
+ parts.push(body.securityReminder);
95
102
  if (parts.length === 0)
96
103
  return [];
97
104
  return [
@@ -121,13 +128,14 @@ _now = new Date()) {
121
128
  else if (tb && tb.bound === false) {
122
129
  lines.push(unboundPromptLine(sessionId));
123
130
  }
124
- // Blocker warning + criteria + progress + memory + linked resources +
125
- // PR-review todos share one additionalContext block so Claude Code injects a
126
- // single coherent context payload at session start. The dependency blocker
127
- // warning (LUM-172) slots in first so it stays prominent — it can preempt the
128
- // session's work entirely. Then the progressive-disclosure tiers (LUM-500):
129
- // Tier-0 acceptance contract → Tier-1 prior-session progress Tier-2 memory
130
- // index linked resources.
131
+ // Blocker warning + security findings + criteria + progress + memory +
132
+ // linked resources + PR-review todos share one additionalContext block so
133
+ // Claude Code injects a single coherent context payload at session start.
134
+ // The dependency blocker warning (LUM-172) slots in first so it stays
135
+ // prominent — it can preempt the session's work entirely. The undispositioned
136
+ // security findings warning (LUM-737) follows next. Then the
137
+ // progressive-disclosure tiers (LUM-500): Tier-0 acceptance contract →
138
+ // Tier-1 prior-session progress → Tier-2 memory index → linked resources.
131
139
  //
132
140
  // LUM-500: the prior-session recovery is now the server-rendered Tier-1
133
141
  // `progressSection`, which REPLACES the CLI-rendered recovery card so progress
@@ -137,6 +145,10 @@ _now = new Date()) {
137
145
  // Blocker warning first: it can preempt the session's work entirely (wait
138
146
  // for the blocker instead of starting) — LUM-172.
139
147
  body.blockerWarningSection,
148
+ // LUM-737: undispositioned security findings — second, before the
149
+ // contract: it changes what "done" needs (fix + push, or a human
150
+ // disposition) but unlike a blocker does not preempt the session.
151
+ body.securityFindingsSection,
140
152
  // Tier-0 acceptance contract: what the session's work is judged against
141
153
  // (LUM-342).
142
154
  body.criteriaSection,
@@ -28,9 +28,9 @@ function normalizeSeverity(s) {
28
28
  * first, via the EXISTING LUM-435 read endpoint — `GET …/boundary-crossings`
29
29
  * returns every crossing (open and dispositioned); we keep only the
30
30
  * undispositioned ones (`disposition == null`). This is the **read/awareness**
31
- * half of the acceptance loop: there is no new query and, by construction, no
32
- * way to clear a crossing disposition stays web + human-only
33
- * (LUM-426/435/422).
31
+ * half of the acceptance loop: there is no new query and this helper cannot
32
+ * clear anything a ruling goes through `lumo crossing disposition` (exit-4
33
+ * user approval, LUM-769) or the web panel.
34
34
  *
35
35
  * Fails *closed*, not open (LUM-480): any transport / non-ok HTTP / parse
36
36
  * failure returns `{ status: 'error', reason }` so the caller can say "check
@@ -75,9 +75,9 @@ async function fetchOpenCrossings(apiUrl, token, taskIdentifier) {
75
75
  return { status: 'ok', crossings };
76
76
  }
77
77
  /**
78
- * The web deep link where a HUMAN dispositions crossings. Disposition is
79
- * web-only and human-only (LUM-426/435/422); the terminal only ever points
80
- * here, it never clears anything itself. Built from the workspace slug +
78
+ * The web deep link where a human dispositions crossings in the panel — the
79
+ * counterpart of `lumo crossing disposition` (LUM-769); the awareness surfaces
80
+ * (task status, DONE_BLOCKED) point here. Built from the workspace slug +
81
81
  * identifier alone (the `/my-tasks/<id>` route needs no project slug), so no
82
82
  * extra fetch is required.
83
83
  */
@@ -1,8 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.REFERENT_KIND_DECLARABLE = void 0;
3
+ exports.TASK_REFERENT_KIND_DECLARABLE = exports.REFERENT_KIND_DECLARABLE = void 0;
4
4
  exports.classifyCheckpointerGrounding = classifyCheckpointerGrounding;
5
5
  exports.effectiveReferentKind = effectiveReferentKind;
6
+ exports.deriveReferentKind = deriveReferentKind;
6
7
  /**
7
8
  * referentKind (LUM-602): what a criterion anchors — the only dimension that
8
9
  * determines independence. Dependency-light (no @prisma/client) so both the
@@ -14,6 +15,15 @@ exports.REFERENT_KIND_DECLARABLE = [
14
15
  'AGENT_CONSTRUCTED_STATE',
15
16
  'PENDING_OUTCOME',
16
17
  ];
18
+ /**
19
+ * LUM-733: the kinds a TASK criterion may still carry. PENDING_OUTCOME is no
20
+ * longer accepted on task criteria (it had no check and no reader); it remains
21
+ * declarable on milestone exit criteria only.
22
+ */
23
+ exports.TASK_REFERENT_KIND_DECLARABLE = [
24
+ 'EXTERNAL_FACT',
25
+ 'AGENT_CONSTRUCTED_STATE',
26
+ ];
17
27
  /** Tools whose output the agent cannot author — external facts. */
18
28
  const EXTERNAL_TOOLS = [
19
29
  'git',
@@ -319,3 +329,23 @@ function effectiveReferentKind(args) {
319
329
  }
320
330
  return declared;
321
331
  }
332
+ /**
333
+ * LUM-733: the referent kind is DERIVED by the system, not declared by the
334
+ * agent. A MACHINE criterion's kind follows its checkpointer — EXTERNAL_FACT
335
+ * only when the check actually consumes an external tool (git/gh/curl/psql/…),
336
+ * AGENT_CONSTRUCTED_STATE otherwise — so any declaration on a MACHINE criterion
337
+ * is ignored (there is nothing to mis-declare and nothing to catch). A HUMAN
338
+ * criterion has no checkpointer to read, so an optional declaration is kept
339
+ * as-is; absent, it stays null (unclassified).
340
+ */
341
+ function deriveReferentKind(args) {
342
+ const { verifierType, checkpointer, declared } = args;
343
+ if (verifierType === 'MACHINE') {
344
+ return classifyCheckpointerGrounding(checkpointer) === 'EXTERNAL'
345
+ ? 'EXTERNAL_FACT'
346
+ : 'AGENT_CONSTRUCTED_STATE';
347
+ }
348
+ return declared === 'EXTERNAL_FACT' || declared === 'AGENT_CONSTRUCTED_STATE'
349
+ ? declared
350
+ : null;
351
+ }
@@ -0,0 +1,125 @@
1
+ "use strict";
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;
4
+ exports.externalFailureReason = externalFailureReason;
5
+ exports.scanStageLabel = scanStageLabel;
6
+ exports.scanStageStateLabel = scanStageStateLabel;
7
+ exports.secretDowngradeNote = secretDowngradeNote;
8
+ /**
9
+ * PR security scan — the one `PrSecurityScan.error` shape that may be shown
10
+ * as an external-scanner reason (spec P8). Shared by the web panel, the PR
11
+ * summary (both via `lib/security-scan/stages.ts`, which re-exports this)
12
+ * and the CLI (`cli/src/commands/task-status.ts`), so the three renderers
13
+ * cannot drift apart (LUM-756).
14
+ *
15
+ * The column has two writers: the ingest / managed-scan workflow, which
16
+ * scrubs its reason and stamps this prefix on it (the prefix is the proof of
17
+ * provenance), and stage A's own catch-all, which writes a RAW, unscrubbed
18
+ * `error.message` with no prefix. Renderers therefore check the prefix first
19
+ * and print only what follows it; unprefixed text is never published.
20
+ */
21
+ exports.EXTERNAL_ERROR_PREFIX = 'external: ';
22
+ /**
23
+ * The scanner's reason, or null when there is none to show: a missing /
24
+ * empty error, an unprefixed (stage A raw) error, or a prefix with nothing
25
+ * after it.
26
+ */
27
+ function externalFailureReason(error) {
28
+ if (!error || !error.startsWith(exports.EXTERNAL_ERROR_PREFIX))
29
+ return null;
30
+ const reason = error.slice(exports.EXTERNAL_ERROR_PREFIX.length);
31
+ return reason.length > 0 ? reason : null;
32
+ }
33
+ /**
34
+ * LUM-763 — the scan's five layers, as stored.
35
+ *
36
+ * These keys are a persisted contract, not a display choice: they sit in
37
+ * `PrSecurityScan.stages` on every historical row, travel over the
38
+ * task-status API, and are read by CLIs older than this change. They do not
39
+ * get renamed; only what a reader is shown does.
40
+ */
41
+ exports.SCAN_STAGE_KEYS = [
42
+ 'secrets',
43
+ 'external',
44
+ 'supplyChain',
45
+ 'judge',
46
+ 'hunt',
47
+ ];
48
+ /** The states a layer can be in. `ScanStageState` in `lib/security-scan/types.ts` derives from this. */
49
+ exports.SCAN_STAGE_STATES = [
50
+ 'RAN',
51
+ 'FAILED',
52
+ 'SKIPPED',
53
+ 'NOT_CONFIGURED',
54
+ 'PENDING',
55
+ 'PARTIAL',
56
+ 'SUPERSEDED',
57
+ ];
58
+ /**
59
+ * What each layer is called where a person reads it — the PR comment and
60
+ * `lumo task status`, which share this map so the two cannot drift (the web
61
+ * panel renders the same wording through `securityScan.stage.*` in i18n).
62
+ *
63
+ * Named for what the layer checks, never for who runs it or which model it
64
+ * uses: `external` was accurate only while the scanners were the customer's,
65
+ * and stopped being true the moment Lumo started running them itself.
66
+ */
67
+ exports.SCAN_STAGE_LABELS = {
68
+ secrets: 'Secrets',
69
+ external: 'Code scan',
70
+ supplyChain: 'Dependencies',
71
+ judge: 'AI review',
72
+ hunt: 'Exploit paths',
73
+ };
74
+ /**
75
+ * What each state is called. `RAN` deliberately reads as "checked", not as a
76
+ * tick or "clean": it means the layer completed, and a layer that completed
77
+ * may well have found something — the findings are counted on their own
78
+ * lines. Only the states that leave the scan incomplete carry a glyph, so
79
+ * attention is drawn to "do not trust this line" and never to "this is fine"
80
+ * (spec P9 — a security signal never claims safety it did not establish).
81
+ */
82
+ exports.SCAN_STAGE_STATE_LABELS = {
83
+ RAN: 'checked',
84
+ FAILED: '✗ failed',
85
+ SKIPPED: 'skipped',
86
+ NOT_CONFIGURED: 'off',
87
+ PENDING: 'scanning',
88
+ PARTIAL: '⚠ incomplete',
89
+ SUPERSEDED: '⚠ superseded',
90
+ };
91
+ /** The layer's display name; an unrecognised key renders as itself rather than vanishing. */
92
+ function scanStageLabel(key) {
93
+ return exports.SCAN_STAGE_LABELS[key] ?? key;
94
+ }
95
+ /** The state's display name; an unrecognised state renders as itself. */
96
+ function scanStageStateLabel(state) {
97
+ return exports.SCAN_STAGE_STATE_LABELS[state] ?? state;
98
+ }
99
+ /**
100
+ * LUM-758 follow-up — why a deterministic secret hit was lowered to LOW.
101
+ *
102
+ * Downgrading is not suppression: the row is still reported, deliberately, so
103
+ * that a human decides. But a reported row with no stated cause reads as an
104
+ * unexplained credential, which is how a test fixture the scanner *already
105
+ * recognised* as a placeholder ends up looking like an open risk. These say
106
+ * which rule fired, in the same words on every surface.
107
+ *
108
+ * Phrased as a property of the value or the path, never as a verdict: the
109
+ * scanner knows the string looks like a placeholder, not that it is harmless.
110
+ */
111
+ exports.SECRET_DOWNGRADE_LABELS = {
112
+ PLACEHOLDER_VALUE: 'placeholder value',
113
+ DOWNGRADED_PATH: 'test/fixture/doc path',
114
+ };
115
+ /**
116
+ * The parenthesised note appended to a rendered finding line, or `''` when the
117
+ * row carries no downgrade — an unrecognised reason renders as itself rather
118
+ * than vanishing, so a value added server-side is never silently dropped by an
119
+ * older CLI.
120
+ */
121
+ function secretDowngradeNote(reason) {
122
+ if (!reason)
123
+ return '';
124
+ return ` — ${exports.SECRET_DOWNGRADE_LABELS[reason] ?? reason}, downgraded`;
125
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumoai/cli",
3
- "version": "1.58.0",
3
+ "version": "1.60.0",
4
4
  "description": "Lumo CLI — manage tasks and sessions from the terminal",
5
5
  "license": "MIT",
6
6
  "author": "cli@uselumo.ai",
@@ -1,32 +0,0 @@
1
- # Fidelity read-outs (mechanical change-pattern)
2
-
3
- `lumo fidelity` is the fidelity-axis read surface — pure, read-only analytics over what the acceptance loop already records, in the same family as `lumo criteria audit` and `lumo outcome rate`. It adds **no storage and no write path**; it only reads the LUM-609 delivery snapshots and the `CRITERION_CHANGED` audit trail.
4
-
5
- ## `lumo fidelity show <task>`
6
-
7
- ```bash
8
- lumo fidelity show LUM-42
9
- lumo fidelity show LUM-42 --json
10
- ```
11
-
12
- For **each delivery** of the task (one append-only `TaskCriteriaSnapshot` per fresh IN_REVIEW entry — a reopen→re-deliver appends another), it discloses two compositions and deliberately stops there. There is **no single fidelity score and no good/bad direction verdict** — the read-out is purely mechanical (timing + op + checkpointer grounding; it never reads statement or diff semantics).
13
-
14
- ### 1. Grounding composition
15
-
16
- Runs the same `effectiveReferentKind` classifier the LUM-605 verify gate uses over the **frozen** contract (delivery-time truth, not the since-edited `latest`), rolled into three buckets plus the full per-kind tally:
17
-
18
- - **grounded** — `EXTERNAL_FACT` (anchored on a fact the agent cannot author: a git SHA/diff, CI status, a DB row). This count is a **machine-checkable _upper bound_** — the checkpointer heuristic can over-count external grounding, never under-count it.
19
- - **self-confirming** — `AGENT_CONSTRUCTED_STATE` + `UNVERIFIED_ASSERTION` (passes by checking the agent's own output, incl. a declared-EXTERNAL_FACT checkpointer that doesn't truly ground external).
20
- - **inconclusive** — `PENDING_OUTCOME` + `UNCLASSIFIED` (not mechanically resolvable here). **Surfaced, never silently swallowed.**
21
-
22
- ### 2. Independence signal
23
-
24
- A criterion is a **backward-inference suspect** when it is both (a) in this delivery's frozen contract and (b) was `ADDED`/`UPDATED` at **round>0** (after work started) at or before the freeze — i.e. the contract the delivery rode on was bent after the work began rather than fixed up front. round-0 (initial-draft) criteria are clean; `DELETED` ops and edits recorded after the freeze (a later cycle) don't count. Each suspect shows its `op`, `round`, and `causeTag`. The read-out **counts and discloses — it does not judge** whether a given edit was legitimate sharpening or tampering (that direction call is left to a human / a later axis).
25
-
26
- ### Caveat (printed on every report)
27
-
28
- The time anchor is the **work-start boundary** (`Task.workStartedAt`), so the independence signal detects a contract edited _after work started_ — not specifically _after output was produced_. A task with **no delivery snapshot** (never entered IN_REVIEW, or predates the LUM-609 freeze) is reported explicitly — "nothing to read yet", which is **not** a pass.
29
-
30
- `--json` emits the full report (`taskId`, `workStartedAt`, `hasSnapshots`, `deliveries[]` with `grounding`/`independence`, `caveat`) for scripting.
31
-
32
- **When to suggest**: when the user wants to see how externally-grounded a task's delivered acceptance contract actually was, or whether criteria were added/changed after work began (independence). It's a disclosure tool, not a gate — it never blocks DONE and emits no verdict.
@@ -1,108 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.fidelityShow = fidelityShow;
4
- const config_1 = require("../lib/config");
5
- const api_1 = require("../lib/api");
6
- const sanitize_1 = require("../lib/sanitize");
7
- /**
8
- * `lumo fidelity show <task>` — the LUM-610 mechanical change-pattern read-out
9
- * (fidelity axis, second block, on the LUM-609 delivery-snapshot foundation).
10
- *
11
- * Per delivery snapshot it discloses two compositions and NOTHING else — no
12
- * single score, no good/bad verdict (approach (a): purely mechanical):
13
- * • grounding — how much of the FROZEN contract is externally grounded
14
- * (EXTERNAL_FACT) vs self-confirming (agent-authored) vs inconclusive.
15
- * `grounded` is a machine-checkable UPPER BOUND.
16
- * • independence — criteria ADDED/UPDATED after work started (round>0) that
17
- * the delivery rode on: backward-inference suspects. Direction left blank.
18
- */
19
- const EFFECTIVE_KINDS = [
20
- 'EXTERNAL_FACT',
21
- 'AGENT_CONSTRUCTED_STATE',
22
- 'UNVERIFIED_ASSERTION',
23
- 'PENDING_OUTCOME',
24
- 'UNCLASSIFIED',
25
- ];
26
- function authBase() {
27
- const creds = (0, config_1.readCredentials)();
28
- if (!creds)
29
- return { error: 'not logged in. Run `lumo auth login` first.' };
30
- const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
31
- const headers = {
32
- Authorization: `Bearer ${creds.token}`,
33
- };
34
- const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
35
- if (sessionId)
36
- headers['X-Lumo-Session-Id'] = sessionId;
37
- return { base, headers };
38
- }
39
- function formatReport(report, taskId) {
40
- const lines = [];
41
- lines.push(`Fidelity change-pattern — ${(0, sanitize_1.sanitizeField)(taskId)}`);
42
- if (!report.hasSnapshots) {
43
- lines.push(' No delivery snapshot on record (task never entered IN_REVIEW, or ' +
44
- 'predates the LUM-609 freeze). Nothing to read yet — not a pass.');
45
- lines.push('');
46
- lines.push(` ${report.caveat}`);
47
- return lines.join('\n') + '\n';
48
- }
49
- report.deliveries.forEach((d, i) => {
50
- const when = d.frozenAt.slice(0, 19).replace('T', ' ');
51
- lines.push('');
52
- lines.push(` Delivery ${i + 1}/${report.deliveries.length} · frozen ${when}Z · ${(0, sanitize_1.sanitizeField)(d.trigger)} · ${d.total} criteria`);
53
- const g = d.grounding;
54
- lines.push(` grounding: grounded ${g.grounded} · self-confirming ${g.selfConfirming} · inconclusive ${g.inconclusive} (grounded = machine-checkable upper bound)`);
55
- const kinds = EFFECTIVE_KINDS.filter(k => g.byEffectiveKind[k] > 0)
56
- .map(k => `${k} ${g.byEffectiveKind[k]}`)
57
- .join(', ');
58
- lines.push(` effective kinds: ${kinds || '(none)'}`);
59
- const ind = d.independence;
60
- lines.push(` independence: ${ind.postStartChanges}/${ind.total} criteria ADDED/UPDATED after work started (backward-inference suspects; direction not judged)`);
61
- for (const s of ind.suspects) {
62
- const cause = s.causeTag ? ` · ${(0, sanitize_1.sanitizeField)(s.causeTag)}` : '';
63
- lines.push(` ↳ ${(0, sanitize_1.sanitizeField)(s.criterionId)} ${s.op}@round${s.round}${cause} (${s.at.slice(0, 10)})`);
64
- }
65
- });
66
- lines.push('');
67
- lines.push(` ${report.caveat}`);
68
- return lines.join('\n') + '\n';
69
- }
70
- async function fidelityShow(taskId, options = {}) {
71
- if (!taskId || taskId.trim() === '') {
72
- console.error('Error: a task is required: lumo fidelity show <task>');
73
- return 1;
74
- }
75
- const auth = authBase();
76
- if ('error' in auth) {
77
- console.error(`Error: ${auth.error}`);
78
- return 1;
79
- }
80
- let res;
81
- try {
82
- res = await fetch(`${auth.base}/api/tasks/${encodeURIComponent(taskId)}/fidelity`, { headers: auth.headers });
83
- }
84
- catch (err) {
85
- const msg = err instanceof Error ? err.message : String(err);
86
- console.error(`Error: could not reach Lumo API (${msg})`);
87
- return 1;
88
- }
89
- if (res.status === 401) {
90
- console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
91
- return 1;
92
- }
93
- if (res.status === 404) {
94
- console.error(`Error: task ${(0, sanitize_1.sanitizeField)(taskId)} not found.`);
95
- return 1;
96
- }
97
- if (!res.ok) {
98
- console.error(`Error: could not read the fidelity change-pattern (HTTP ${res.status}).`);
99
- return 1;
100
- }
101
- const report = (await res.json());
102
- if (options.json) {
103
- process.stdout.write(JSON.stringify(report, null, 2) + '\n');
104
- return;
105
- }
106
- process.stdout.write(formatReport(report, taskId));
107
- return;
108
- }
@@ -1,189 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.collectCriterion = collectCriterion;
4
- exports.verdict = verdict;
5
- const config_1 = require("../lib/config");
6
- const api_1 = require("../lib/api");
7
- const sanitize_1 = require("../lib/sanitize");
8
- const browser_1 = require("../lib/browser");
9
- /**
10
- * Rejection-reason vocabulary (mirrors the server's VerificationRejectionReason
11
- * enum). Required on the agent --fail path — the agent pays the structured tax
12
- * a human is spared (LUM-422 ①).
13
- */
14
- const FAIL_REASONS = [
15
- 'CRITERION_UNMET',
16
- 'EVIDENCE_INSUFFICIENT',
17
- 'CHECK_EXECUTION_ERROR',
18
- 'SCOPE_MISMATCH',
19
- 'OTHER',
20
- ];
21
- /** Collect repeatable `--criterion <id>` flags into an array (commander idiom). */
22
- function collectCriterion(value, prev = []) {
23
- return [...prev, value];
24
- }
25
- /**
26
- * `lumo verdict [task]` — human + agent acceptance verdicts (LUM-422).
27
- *
28
- * Two modes, exactly one required:
29
- * --pass opens the browser to the task's verdict bar, focused on Pass (a deep
30
- * link — writes NOTHING; a passing data row is only ever produced by a
31
- * human's own click, red line).
32
- * --fail --reason <enum> [--note] [--criterion …] records an AGENT send-back
33
- * (verdict hard-coded FAIL server-side) and bounces the task to
34
- * IN_PROGRESS. Bearer-authed.
35
- *
36
- * Defaults to the session-bound task; an explicit identifier overrides.
37
- */
38
- async function verdict(identifier, options = {}) {
39
- const modes = [options.pass && 'pass', options.fail && 'fail'].filter(Boolean);
40
- if (modes.length === 0) {
41
- console.error('Error: choose a verdict mode — --pass or --fail.');
42
- return 1;
43
- }
44
- if (modes.length > 1) {
45
- console.error(`Error: pick exactly one verdict mode (got ${modes.join(', ')}).`);
46
- return 1;
47
- }
48
- const creds = (0, config_1.readCredentials)();
49
- if (!creds) {
50
- console.error('Error: not logged in. Run `lumo auth login` first.');
51
- return 1;
52
- }
53
- const base = (0, api_1.trimTrailingSlash)((0, api_1.resolveAuthedApiUrl)(creds.apiUrl));
54
- const headers = {
55
- Authorization: `Bearer ${creds.token}`,
56
- };
57
- const sessionId = process.env.CLAUDE_CODE_SESSION_ID;
58
- if (sessionId)
59
- headers['X-Lumo-Session-Id'] = sessionId;
60
- // ── Resolve the task: explicit identifier or the session binding ──────────
61
- let taskId = identifier;
62
- if (!taskId) {
63
- if (!sessionId) {
64
- console.error('Error: no task given and $CLAUDE_CODE_SESSION_ID is not set.\n' +
65
- 'Run `lumo verdict <LUM-N> …` or run inside a session bound via `lumo session attach`.');
66
- return 1;
67
- }
68
- let res;
69
- try {
70
- res = await fetch(`${base}/api/sessions/${encodeURIComponent(sessionId)}`, { headers });
71
- }
72
- catch (err) {
73
- const msg = err instanceof Error ? err.message : String(err);
74
- console.error(`Error: could not reach Lumo API (${msg})`);
75
- return 1;
76
- }
77
- const data = res.ok
78
- ? (await res.json())
79
- : null;
80
- if (!data?.taskIdentifier) {
81
- console.error('Error: this session is not bound to a task. Run `lumo session attach <LUM-N>` first, or pass the task explicitly.');
82
- return 1;
83
- }
84
- taskId = data.taskIdentifier;
85
- }
86
- if (options.fail) {
87
- return failVerdict(base, headers, taskId, options, creds.workspaceSlug);
88
- }
89
- return passDeepLink(base, headers, taskId, creds.workspaceSlug);
90
- }
91
- /**
92
- * --pass: open the human's verdict bar pre-focused on Pass. The CLI never writes
93
- * the verdict — it only carries the human to the one click that does (red line:
94
- * no agent-produced passing row).
95
- */
96
- async function passDeepLink(base, headers, taskId, workspaceSlug) {
97
- let res;
98
- try {
99
- res = await fetch(`${base}/api/tasks/by-identifier/${encodeURIComponent(taskId)}`, { headers });
100
- }
101
- catch (err) {
102
- const msg = err instanceof Error ? err.message : String(err);
103
- console.error(`Error: could not reach Lumo API (${msg})`);
104
- return 1;
105
- }
106
- if (res.status === 401) {
107
- console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
108
- return 1;
109
- }
110
- if (res.status === 404) {
111
- console.error(`Error: task ${taskId} not found in workspace ${workspaceSlug}`);
112
- return 1;
113
- }
114
- if (!res.ok) {
115
- console.error(`Error: could not load task (HTTP ${res.status})`);
116
- return 1;
117
- }
118
- const { task } = (await res.json());
119
- if (!task?.url) {
120
- console.error('Error: server did not return a task URL to open.');
121
- return 1;
122
- }
123
- const sep = task.url.includes('?') ? '&' : '?';
124
- const deepLink = `${task.url}${sep}verdict=pass`;
125
- process.stdout.write(`Opening ${taskId} for a human "Pass" verdict (nothing is recorded until they click):\n` +
126
- ` ${(0, sanitize_1.sanitizeField)(deepLink)}\n`);
127
- (0, browser_1.openBrowser)(deepLink);
128
- return;
129
- }
130
- /**
131
- * --fail: record an AGENT send-back. The server hard-codes verdict=FAIL and
132
- * verifierType=AGENT — there is no passing verdict the CLI can express.
133
- */
134
- async function failVerdict(base, headers, taskId, options, workspaceSlug) {
135
- if (!options.reason) {
136
- console.error(`Error: --fail requires --reason <${FAIL_REASONS.join('|')}>.`);
137
- return 1;
138
- }
139
- // Case-insensitive like every other CLI enum flag (LUM-420): fold to the
140
- // canonical upper-case enum before validating and sending.
141
- const reason = options.reason.toUpperCase();
142
- if (!FAIL_REASONS.includes(reason)) {
143
- console.error(`Error: invalid --reason "${options.reason}". Allowed: ${FAIL_REASONS.join(', ')}.`);
144
- return 1;
145
- }
146
- const body = { rejectionReasonEnum: reason };
147
- if (options.note)
148
- body.note = options.note;
149
- if (options.criterion && options.criterion.length > 0) {
150
- body.criterionIds = options.criterion;
151
- }
152
- let res;
153
- try {
154
- res = await fetch(`${base}/api/tasks/${encodeURIComponent(taskId)}/agent-verdict`, {
155
- method: 'POST',
156
- headers: { ...headers, 'Content-Type': 'application/json' },
157
- body: JSON.stringify(body),
158
- });
159
- }
160
- catch (err) {
161
- const msg = err instanceof Error ? err.message : String(err);
162
- console.error(`Error: could not reach Lumo API (${msg})`);
163
- return 1;
164
- }
165
- if (res.status === 401) {
166
- console.error('Error: API key invalid or revoked. Run `lumo auth login`.');
167
- return 1;
168
- }
169
- if (res.status === 404) {
170
- console.error(`Error: task ${taskId} not found in workspace ${workspaceSlug}`);
171
- return 1;
172
- }
173
- if (!res.ok) {
174
- const errBody = (await res.json().catch(() => null));
175
- const detail = errBody && typeof errBody.error === 'string'
176
- ? (0, sanitize_1.sanitizeField)(errBody.error)
177
- : '';
178
- console.error(`Error: send-back rejected (HTTP ${res.status})${detail ? ` — ${detail}` : ''}`);
179
- return 1;
180
- }
181
- const outcome = (await res.json());
182
- process.stdout.write(`✗ Sent ${taskId} back (AGENT FAIL, reason ${reason}) — ` +
183
- `${outcome.criterionIds.length} ${outcome.criterionIds.length === 1 ? 'criterion' : 'criteria'} at round ${outcome.round}; task is now ${outcome.taskStatus}.\n`);
184
- if (outcome.commentId) {
185
- process.stdout.write(' A send-back note was posted as a task comment.\n');
186
- }
187
- process.stdout.write('Address the unmet criteria (see `lumo task status`), then re-verify.\n');
188
- return;
189
- }