@lumoai/cli 1.58.0 → 1.59.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 +13 -16
- package/assets/skill/references/artifacts-figma.md +4 -3
- package/assets/skill/references/confirmation.md +131 -0
- package/assets/skill/references/criteria.md +12 -21
- package/assets/skill/references/doc-editing.md +11 -9
- package/assets/skill/references/docs.md +4 -3
- package/assets/skill/references/memory.md +4 -2
- package/assets/skill/references/milestones.md +3 -2
- package/assets/skill/references/outcome.md +1 -14
- package/assets/skill/references/plan-runs.md +5 -0
- package/assets/skill/references/sessions.md +3 -3
- package/assets/skill/references/sprints.md +18 -17
- package/assets/skill/references/task-deps.md +4 -3
- package/assets/skill/references/tasks.md +34 -2
- package/assets/skill/references/verify.md +46 -68
- package/assets/skill/references/worktree.md +13 -7
- package/dist/cli/src/commands/doc-delete.js +35 -23
- package/dist/cli/src/commands/doc-rebuild-source.js +16 -4
- package/dist/cli/src/commands/memory-rm.js +68 -10
- package/dist/cli/src/commands/milestone-delete.js +13 -10
- package/dist/cli/src/commands/outcome.js +0 -77
- package/dist/cli/src/commands/session-attach.js +8 -2
- package/dist/cli/src/commands/sprint-close.js +29 -9
- package/dist/cli/src/commands/sprint-delete.js +13 -10
- package/dist/cli/src/commands/sprint-show.js +3 -9
- package/dist/cli/src/commands/task-artifact-rm.js +58 -28
- package/dist/cli/src/commands/task-criteria-list.js +1 -4
- package/dist/cli/src/commands/task-criteria-set.js +3 -12
- package/dist/cli/src/commands/task-deps.js +20 -6
- package/dist/cli/src/commands/task-status.js +165 -110
- package/dist/cli/src/commands/task-update.js +129 -0
- package/dist/cli/src/commands/verify.js +22 -13
- package/dist/cli/src/commands/worktree-rm.js +35 -7
- package/dist/cli/src/index.js +47 -47
- package/dist/cli/src/lib/blocked-error.js +178 -0
- package/dist/cli/src/lib/confirmation.js +89 -0
- package/dist/cli/src/lib/hook-runner.js +23 -11
- package/dist/shared/src/referent-kind.js +31 -1
- package/dist/shared/src/security-scan.js +29 -0
- package/package.json +1 -1
- package/assets/skill/references/fidelity.md +0 -32
- package/dist/cli/src/commands/fidelity.js +0 -108
- package/dist/cli/src/commands/verdict.js +0 -189
package/dist/cli/src/index.js
CHANGED
|
@@ -64,9 +64,7 @@ const plan_1 = require("./commands/plan");
|
|
|
64
64
|
const cost_1 = require("./commands/cost");
|
|
65
65
|
const priority_1 = require("./commands/priority");
|
|
66
66
|
const criteria_audit_1 = require("./commands/criteria-audit");
|
|
67
|
-
const fidelity_1 = require("./commands/fidelity");
|
|
68
67
|
const verify_1 = require("./commands/verify");
|
|
69
|
-
const verdict_1 = require("./commands/verdict");
|
|
70
68
|
const crossing_explain_1 = require("./commands/crossing-explain");
|
|
71
69
|
const outcome_1 = require("./commands/outcome");
|
|
72
70
|
const task_context_1 = require("./commands/task-context");
|
|
@@ -194,6 +192,21 @@ if (!isIntrospect) {
|
|
|
194
192
|
catch { }
|
|
195
193
|
}
|
|
196
194
|
const collect = (val, acc) => [...acc, val];
|
|
195
|
+
/**
|
|
196
|
+
* Exit-code convention (LUM-755). A command returns the code to propagate:
|
|
197
|
+
* 0 / undefined success
|
|
198
|
+
* 1 general error (message on stderr)
|
|
199
|
+
* 4 confirmation required — a mutation that needs the user's
|
|
200
|
+
* approval printed a `confirmation_required` envelope on
|
|
201
|
+
* stdout (see cli/src/lib/confirmation.ts); re-run the
|
|
202
|
+
* envelope's `confirmCommand` once the user has approved
|
|
203
|
+
* 5 blocked by a human-only gate — the server refused the
|
|
204
|
+
* change (today: a DONE gate) and the CLI printed a
|
|
205
|
+
* `DONE_BLOCKED` error on stdout listing every blocker
|
|
206
|
+
* (see cli/src/lib/blocked-error.ts); nothing on the CLI
|
|
207
|
+
* can clear it — relay it, don't retry
|
|
208
|
+
* 2 (auth) and 3 (validation) are reserved for a later pass.
|
|
209
|
+
*/
|
|
197
210
|
function wrap(fn) {
|
|
198
211
|
return async (...args) => {
|
|
199
212
|
try {
|
|
@@ -257,17 +270,8 @@ program
|
|
|
257
270
|
.command('verify [task]')
|
|
258
271
|
.description('Machine verification loop (LUM-343): run every MACHINE criterion checkpointer locally, report structured verdicts to the server (round cap 3), and print next actions. All-pass moves the task to IN_REVIEW. Defaults to the session-bound task.')
|
|
259
272
|
.option('--timeout <seconds>', 'Per-checkpointer timeout in seconds (default 600)')
|
|
260
|
-
.option('--note <text>', '
|
|
273
|
+
.option('--note <text>', 'Optional one-line self-report — what you did and why it is ready. When given, it is recorded as your claim (source AGENT) when the round passes into IN_REVIEW.')
|
|
261
274
|
.action(wrap((task, options) => (0, verify_1.verify)(task, options)));
|
|
262
|
-
program
|
|
263
|
-
.command('verdict [task]')
|
|
264
|
-
.description('Acceptance verdict (LUM-422). --pass opens the browser to the human verdict bar focused on Pass (a deep link — records nothing; a passing row is only ever a human click). --fail --reason <enum> records an AGENT send-back and bounces the task to IN_PROGRESS. Defaults to the session-bound task.')
|
|
265
|
-
.option('--pass', 'Open the verdict bar focused on Pass (human one-click; no write)')
|
|
266
|
-
.option('--fail', 'Record an AGENT send-back (verdict FAIL) — requires --reason')
|
|
267
|
-
.option('--reason <enum>', 'Rejection reason for --fail: CRITERION_UNMET | EVIDENCE_INSUFFICIENT | CHECK_EXECUTION_ERROR | SCOPE_MISMATCH | OTHER (case-insensitive)')
|
|
268
|
-
.option('--note <text>', 'Optional send-back narrative, posted as a task comment')
|
|
269
|
-
.option('--criterion <id>', 'Narrow a --fail to specific criteria (repeatable); omitted = the whole contract', verdict_1.collectCriterion)
|
|
270
|
-
.action(wrap((task, options) => (0, verdict_1.verdict)(task, options)));
|
|
271
275
|
const crossing = program
|
|
272
276
|
.command('crossing')
|
|
273
277
|
.description('Inspect and annotate boundary crossings');
|
|
@@ -278,18 +282,11 @@ crossing
|
|
|
278
282
|
.action(wrap((id, options) => (0, crossing_explain_1.crossingExplain)(id, options)));
|
|
279
283
|
const outcome = program
|
|
280
284
|
.command('outcome')
|
|
281
|
-
.description('Read
|
|
285
|
+
.description('Read the post-hoc outcome well (LUM-598)');
|
|
282
286
|
outcome
|
|
283
287
|
.command('show <task>')
|
|
284
288
|
.description('Read the post-hoc outcome well for a task: the falsifier verdict (REJECTED | INCONCLUSIVE — never a "pass") plus its backing rejection signals. INCONCLUSIVE means no rejection on record, NOT satisfied.')
|
|
285
289
|
.action(wrap((task) => (0, outcome_1.outcomeShow)(task)));
|
|
286
|
-
outcome
|
|
287
|
-
.command('record <task>')
|
|
288
|
-
.description('Record a human-observed post-hoc REJECTION of a delivery (revert / rollback / CI regression / downstream redirect / bypass). Append-only; there is no "mark satisfied" counterpart — the well only asserts rejection.')
|
|
289
|
-
.requiredOption('--note <text>', 'What reality did (the observed referent), e.g. "reverted in #812 after a prod incident"')
|
|
290
|
-
.option('--kind <kind>', 'reverted | rolled_back | ci_regression | downstream_redirect | bypassed | manual (case-insensitive; default: manual)')
|
|
291
|
-
.option('--occurred-at <iso>', 'When the event happened (ISO 8601; defaults to now)')
|
|
292
|
-
.action(wrap((task, options) => (0, outcome_1.outcomeRecord)(task, options)));
|
|
293
290
|
outcome
|
|
294
291
|
.command('rate')
|
|
295
292
|
.description('Trust × post-hoc fate: per delivery-time forecast-confidence bracket, the post-hoc REJECTED rate from the outcome well. Honest by construction — thin brackets read "insufficient" and the high-vs-low comparison stays "inconclusive" until the well has enough signal. The rate is a LOWER BOUND (no signal = INCONCLUSIVE, never satisfied).')
|
|
@@ -432,14 +429,6 @@ criteria
|
|
|
432
429
|
.command('audit')
|
|
433
430
|
.description('Show the workspace referent-kind distribution + self-confirming-green ratio (GET /api/criteria/audit)')
|
|
434
431
|
.action(wrap(() => (0, criteria_audit_1.criteriaAudit)()));
|
|
435
|
-
const fidelity = program
|
|
436
|
-
.command('fidelity')
|
|
437
|
-
.description('Mechanical fidelity read-outs (LUM-610)');
|
|
438
|
-
fidelity
|
|
439
|
-
.command('show <task>')
|
|
440
|
-
.description('Per delivery snapshot (LUM-609): the grounding composition of the frozen contract (grounded / self-confirming / inconclusive) and the independence signal (criteria ADDED/UPDATED after work started = backward-inference suspects). A composition table, not a single score — direction is never judged. inconclusive is surfaced, never swallowed.')
|
|
441
|
-
.option('--json', 'Emit the report as JSON')
|
|
442
|
-
.action(wrap((task, options) => (0, fidelity_1.fidelityShow)(task, options)));
|
|
443
432
|
const session = program
|
|
444
433
|
.command('session')
|
|
445
434
|
.description('Manage per-terminal coding-session context');
|
|
@@ -473,8 +462,9 @@ task
|
|
|
473
462
|
.action(wrap(identifier => (0, task_show_1.taskShow)(identifier)));
|
|
474
463
|
task
|
|
475
464
|
.command('status [task]')
|
|
476
|
-
.description('Acceptance self-check (LUM-344): contract with latest verdicts,
|
|
465
|
+
.description('Acceptance self-check (LUM-344): contract with latest verdicts, current round, nextActions (unmet criteria + undispositioned security findings, LUM-737), and open boundary crossings. Read-only, no LLM. Defaults to the session-bound task. Run it first when resuming a task or after a verification round was rejected. --full adds the dashboard sections (verification rollup, history, cost, struggle/rework trail, trend).')
|
|
477
466
|
.option('--json', 'Emit the versioned machine-readable payload (version field; breaking changes bump it)')
|
|
467
|
+
.option('--full', 'Also print the dashboard sections: verification rollup, history, cost, struggle/rework trail, trend')
|
|
478
468
|
.action(wrap((taskArg, options) => (0, task_status_1.taskStatus)(taskArg, options)));
|
|
479
469
|
task
|
|
480
470
|
.command('comment <identifier> <body>')
|
|
@@ -567,8 +557,9 @@ taskDeps
|
|
|
567
557
|
.action(wrap((id, edge) => (0, task_deps_1.taskDepsDismiss)(id, edge)));
|
|
568
558
|
taskDeps
|
|
569
559
|
.command('rm <identifier> <edge>')
|
|
570
|
-
.
|
|
571
|
-
.
|
|
560
|
+
.description('Delete a dependency edge. Without --confirm: exit 4 + a confirmation envelope describing the edge.')
|
|
561
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
562
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
572
563
|
.action(wrap((id, edge, opts) => (0, task_deps_1.taskDepsRm)(id, edge, opts)));
|
|
573
564
|
const taskPr = task.command('pr').description('Inspect linked PRs');
|
|
574
565
|
taskPr
|
|
@@ -652,8 +643,9 @@ taskArtifact
|
|
|
652
643
|
.action(wrap((taskId, artifactId) => (0, task_artifact_show_1.taskArtifactShow)(taskId, artifactId)));
|
|
653
644
|
taskArtifact
|
|
654
645
|
.command('rm <task> <artifact-id>')
|
|
655
|
-
.description('Delete an artifact from a task.
|
|
656
|
-
.option('--
|
|
646
|
+
.description('Delete an artifact from a task (irreversible). Without --confirm: exit 4 + a confirmation envelope naming the artifact.')
|
|
647
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
648
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
657
649
|
.action(wrap((taskId, artifactId, options) => (0, task_artifact_rm_1.taskArtifactRm)(taskId, artifactId, options)));
|
|
658
650
|
const projectCmd = program
|
|
659
651
|
.command('project')
|
|
@@ -701,8 +693,9 @@ memoryCmd
|
|
|
701
693
|
.action(wrap((id) => (0, memory_promote_1.memoryPromote)(id)));
|
|
702
694
|
memoryCmd
|
|
703
695
|
.command('rm <memoryId>')
|
|
704
|
-
.description('Delete a memory (hard delete).
|
|
705
|
-
.option('--
|
|
696
|
+
.description('Delete a memory (hard delete). Without --confirm: exit 4 + a confirmation envelope showing the card.')
|
|
697
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
698
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
706
699
|
.action(wrap((id, opts) => (0, memory_rm_1.memoryRm)(id, opts)));
|
|
707
700
|
memoryCmd
|
|
708
701
|
.command('sync')
|
|
@@ -762,9 +755,10 @@ milestoneCmd
|
|
|
762
755
|
.action(wrap((identifier, options) => (0, milestone_update_1.milestoneUpdate)(identifier, options)));
|
|
763
756
|
milestoneCmd
|
|
764
757
|
.command('delete <identifier>')
|
|
765
|
-
.description('Delete a milestone. Tasks under it keep their data (milestoneId is cleared).
|
|
758
|
+
.description('Delete a milestone. Tasks under it keep their data (milestoneId is cleared). Without --confirm: exit 4 + a confirmation envelope with the task count.')
|
|
766
759
|
.option('--project <ref>', 'Project name or slug (when identifier is a name)')
|
|
767
|
-
.option('--
|
|
760
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
761
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
768
762
|
.action(wrap((identifier, options) => (0, milestone_delete_1.milestoneDelete)(identifier, options)));
|
|
769
763
|
milestoneCmd
|
|
770
764
|
.command('archive <identifier>')
|
|
@@ -874,9 +868,10 @@ sprintCmd
|
|
|
874
868
|
.action(wrap((identifier, options) => (0, sprint_update_1.sprintUpdate)(identifier, options)));
|
|
875
869
|
sprintCmd
|
|
876
870
|
.command('delete <identifier>')
|
|
877
|
-
.description('Delete a DRAFT sprint.
|
|
871
|
+
.description('Delete a DRAFT sprint. Without --confirm: exit 4 + a confirmation envelope with the task count. Server rejects ACTIVE/CLOSED sprints.')
|
|
878
872
|
.option('--team <ref>', 'Team name or UUID (informational)')
|
|
879
|
-
.option('--
|
|
873
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
874
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
880
875
|
.action(wrap((identifier, options) => (0, sprint_delete_1.sprintDelete)(identifier, options)));
|
|
881
876
|
sprintCmd
|
|
882
877
|
.command('start <identifier>')
|
|
@@ -885,11 +880,12 @@ sprintCmd
|
|
|
885
880
|
.action(wrap((identifier, options) => (0, sprint_start_1.sprintStart)(identifier, options)));
|
|
886
881
|
sprintCmd
|
|
887
882
|
.command('close <identifier>')
|
|
888
|
-
.description('Close an ACTIVE sprint. With no unfinished tasks, closes immediately. Pass --move-all
|
|
883
|
+
.description('Close an ACTIVE sprint. With no unfinished tasks, closes immediately. Pass --move-all to move unfinished tasks to the next sprint, or --backlog-all to return them to the backlog; either mode without --confirm exits 4 with a confirmation envelope listing the tasks that would move.')
|
|
889
884
|
.option('--team <ref>', 'Team name or UUID (informational)')
|
|
890
885
|
.option('--move-all', 'Move all unfinished tasks to the next sprint')
|
|
891
886
|
.option('--backlog-all', 'Return all unfinished tasks to the backlog')
|
|
892
|
-
.option('--
|
|
887
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
888
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
893
889
|
.action(wrap((identifier, options) => (0, sprint_close_1.sprintClose)(identifier, options)));
|
|
894
890
|
sprintCmd
|
|
895
891
|
.command('summary <identifier>')
|
|
@@ -979,7 +975,8 @@ doc
|
|
|
979
975
|
.command('rebuild-source <doc>')
|
|
980
976
|
.description("Regenerate the stored markdown source from the doc's HTML body using a lossless serializer (tables/rows/headings round-trip), re-enabling doc show --raw / diff / patch / append for a source-less doc. The rebuilt source is structure-guarded: any table/tr/heading shrink is rejected with 422 (no silent flattening) unless --allow-shrink is passed. A doc that already has a source is refused with 409 unless --force re-derives it (replacing a byte-faithful source with a serializer-derived one).")
|
|
981
977
|
.option('--allow-shrink', 'Commit even if the rebuilt source re-renders with fewer tables/rows/headings than the stored body (default: rejected with 422)')
|
|
982
|
-
.option('--force', 'Re-derive the source even when one already exists
|
|
978
|
+
.option('--force', 'Re-derive the source even when one already exists. Without it an existing source yields exit 4 + a confirmation envelope whose confirmCommand carries --force --confirm.')
|
|
979
|
+
.option('--confirm', 'The user has approved replacing the existing source (LUM-755); pair with --force as the envelope instructs')
|
|
983
980
|
.option('--if-revision <n>', 'Only apply if the doc body is still at this revision (from doc show)')
|
|
984
981
|
.action(wrap((reference, opts) => (0, doc_rebuild_source_1.docRebuildSource)(reference, opts)));
|
|
985
982
|
doc
|
|
@@ -999,8 +996,9 @@ doc
|
|
|
999
996
|
.action(wrap((reference, opts) => (0, doc_move_1.docMove)(reference, opts)));
|
|
1000
997
|
doc
|
|
1001
998
|
.command('delete <doc>')
|
|
1002
|
-
.description('Delete a document
|
|
1003
|
-
.option('--
|
|
999
|
+
.description('Delete a document and everything nested under it. Without --confirm: exit 4 + a confirmation envelope with the title.')
|
|
1000
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
1001
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
1004
1002
|
.action(wrap((reference, opts) => (0, doc_delete_1.docDelete)(reference, opts)));
|
|
1005
1003
|
doc
|
|
1006
1004
|
.command('bind <doc> <task>')
|
|
@@ -1041,6 +1039,7 @@ task
|
|
|
1041
1039
|
.option('--add-tag-id <cuid>', 'Add tag by id (repeatable)', collect, [])
|
|
1042
1040
|
.option('--remove-tag <name>', 'Remove tag by name (repeatable). Unknown names are find-or-create, so prefer --remove-tag-id to avoid orphan rows.', collect, [])
|
|
1043
1041
|
.option('--remove-tag-id <cuid>', 'Remove tag by id (repeatable). Unknown ids are a no-op.', collect, [])
|
|
1042
|
+
.option('--confirm', 'With --status done: the user has approved the DONE transition shown in the confirmation envelope (LUM-755). Without it, --status done exits 4 and prints the envelope (status change, every linked PR, a ⚠ line for unmerged ones) instead of a prompt.')
|
|
1044
1043
|
.action(wrap((identifier, options) => (0, task_update_1.taskUpdate)(identifier, options)));
|
|
1045
1044
|
const worktree = program
|
|
1046
1045
|
.command('worktree')
|
|
@@ -1061,9 +1060,10 @@ worktree
|
|
|
1061
1060
|
}));
|
|
1062
1061
|
worktree
|
|
1063
1062
|
.command('rm <identifier>')
|
|
1064
|
-
.description('Remove the worktree for a task (git worktree remove).
|
|
1065
|
-
.option('--
|
|
1066
|
-
.option('--
|
|
1063
|
+
.description('Remove the worktree for a task (git worktree remove). Without --confirm — or with uncommitted changes and no --force — exits 4 with a confirmation envelope naming the path, branch and what would be discarded. Keeps the branch unless --delete-branch.')
|
|
1064
|
+
.option('--confirm', 'The user has approved the change shown in the confirmation envelope (LUM-755). Without it the command exits 4 and prints what it would do.')
|
|
1065
|
+
.option('--yes', 'Alias of --confirm (kept for compatibility)')
|
|
1066
|
+
.option('--force', 'Remove even with uncommitted changes (discards them)')
|
|
1067
1067
|
.option('--delete-branch', 'Also delete the branch (git branch -d; refuses if unmerged)')
|
|
1068
1068
|
.action(wrap((identifier, options) => (0, worktree_rm_1.worktreeRm)(identifier, options)));
|
|
1069
1069
|
worktree
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXIT_BLOCKED = void 0;
|
|
4
|
+
exports.isDoneGateRefusal = isDoneGateRefusal;
|
|
5
|
+
exports.collectDoneBlockers = collectDoneBlockers;
|
|
6
|
+
exports.buildRemediation = buildRemediation;
|
|
7
|
+
exports.buildBlockedError = buildBlockedError;
|
|
8
|
+
exports.renderBlocked = renderBlocked;
|
|
9
|
+
exports.emitBlocked = emitBlocked;
|
|
10
|
+
/**
|
|
11
|
+
* Structured "blocked by a human-only gate" error (LUM-755).
|
|
12
|
+
*
|
|
13
|
+
* The confirmation protocol (confirmation.ts, exit 4) covers mutations the
|
|
14
|
+
* USER can approve. Some refusals are different in kind: the server's DONE
|
|
15
|
+
* gates — an unresolved send-back, an undispositioned boundary crossing, a
|
|
16
|
+
* blocking security finding — are verdicts on the agent's own work, and by
|
|
17
|
+
* design there is no CLI path to clear them (the interested party may not
|
|
18
|
+
* adjudicate itself). Echoing the 409 text and exiting 1 left the agent
|
|
19
|
+
* grepping prose to learn that. Instead the CLI now:
|
|
20
|
+
*
|
|
21
|
+
* 1. recognises the gate refusal (`isDoneGateRefusal`);
|
|
22
|
+
* 2. re-reads the existing read models — `GET …/status` for send-backs and
|
|
23
|
+
* blocking findings, `GET …/boundary-crossings` for open crossings — and
|
|
24
|
+
* lists every blocker with its id;
|
|
25
|
+
* 3. prints a `DONE_BLOCKED` error on stdout and exits 5
|
|
26
|
+
* (`EXIT_BLOCKED`), with remediation lines that only ever point at
|
|
27
|
+
* human-side or explain-only paths.
|
|
28
|
+
*
|
|
29
|
+
* A failed enrichment read is reported in `unconfirmed[]` rather than
|
|
30
|
+
* silently showing fewer blockers — the same fail-closed stance as
|
|
31
|
+
* `lumo task status`.
|
|
32
|
+
*/
|
|
33
|
+
const open_crossings_1 = require("./open-crossings");
|
|
34
|
+
const sanitize_1 = require("./sanitize");
|
|
35
|
+
exports.EXIT_BLOCKED = 5;
|
|
36
|
+
const DONE_GATE_PREFIX = 'Cannot move this task to DONE';
|
|
37
|
+
/** All three server DONE gates share this message prefix. */
|
|
38
|
+
function isDoneGateRefusal(message) {
|
|
39
|
+
return typeof message === 'string' && message.startsWith(DONE_GATE_PREFIX);
|
|
40
|
+
}
|
|
41
|
+
async function collectDoneBlockers(args) {
|
|
42
|
+
const { base, token, identifier } = args;
|
|
43
|
+
const blockers = [];
|
|
44
|
+
const unconfirmed = [];
|
|
45
|
+
const headers = { Authorization: `Bearer ${token}` };
|
|
46
|
+
// Send-backs + blocking security findings: the acceptance-status read model.
|
|
47
|
+
try {
|
|
48
|
+
const res = await fetch(`${base}/api/tasks/${encodeURIComponent(identifier)}/status`, { headers });
|
|
49
|
+
if (!res.ok) {
|
|
50
|
+
unconfirmed.push(`task status read failed: HTTP ${res.status}`);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
const data = (await res.json());
|
|
54
|
+
for (const c of data.criteria ?? []) {
|
|
55
|
+
if (c.latestVerdict?.verdict === 'FAIL') {
|
|
56
|
+
blockers.push({
|
|
57
|
+
kind: 'SEND_BACK',
|
|
58
|
+
criterionId: c.id,
|
|
59
|
+
statement: c.statement,
|
|
60
|
+
failedAtRound: c.latestVerdict.round,
|
|
61
|
+
rejectionReason: c.latestVerdict.rejectionReason,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
for (const a of data.nextActions ?? []) {
|
|
66
|
+
if (a.kind === 'SECURITY_FINDING' && a.blocking) {
|
|
67
|
+
blockers.push({
|
|
68
|
+
kind: 'SECURITY_FINDING',
|
|
69
|
+
findingId: a.findingId,
|
|
70
|
+
severity: a.severity,
|
|
71
|
+
ruleId: a.ruleId,
|
|
72
|
+
filePath: a.filePath,
|
|
73
|
+
line: a.line,
|
|
74
|
+
prNumber: a.prNumber,
|
|
75
|
+
statement: a.statement,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
unconfirmed.push(`task status read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
83
|
+
}
|
|
84
|
+
// Open boundary crossings: the LUM-435 read model (fails closed, LUM-480).
|
|
85
|
+
const crossings = await (0, open_crossings_1.fetchOpenCrossings)(base, token, identifier);
|
|
86
|
+
if (crossings.status === 'error') {
|
|
87
|
+
unconfirmed.push(`boundary-crossings read failed: ${crossings.reason}`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
for (const c of crossings.crossings) {
|
|
91
|
+
blockers.push({
|
|
92
|
+
kind: 'BOUNDARY_CROSSING',
|
|
93
|
+
id: c.id,
|
|
94
|
+
severity: c.severity,
|
|
95
|
+
category: c.category,
|
|
96
|
+
detail: c.detail,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { blockers, unconfirmed };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One line per blocker kind present. Every line names a human-side path or
|
|
104
|
+
* the append-only `crossing explain` — never a flag or command that would let
|
|
105
|
+
* the agent clear its own blocker.
|
|
106
|
+
*/
|
|
107
|
+
function buildRemediation(blockers, unconfirmed, ctx) {
|
|
108
|
+
const kinds = new Set(blockers.map(b => b.kind));
|
|
109
|
+
const lines = [];
|
|
110
|
+
if (kinds.has('SEND_BACK')) {
|
|
111
|
+
lines.push(`SEND_BACK: fix the failed criterion in place and re-run \`lumo verify ${ctx.identifier}\`; a human can also record a PASS in the web verdict bar. Do not reword or delete the criterion to make it pass.`);
|
|
112
|
+
}
|
|
113
|
+
if (kinds.has('BOUNDARY_CROSSING')) {
|
|
114
|
+
const ids = blockers
|
|
115
|
+
.filter(b => b.kind === 'BOUNDARY_CROSSING')
|
|
116
|
+
.map(b => b.id);
|
|
117
|
+
lines.push(`BOUNDARY_CROSSING: leave a rationale with \`lumo crossing explain ${ids[0]} --note "…"\` (append-only; it does not clear anything). A human dispositions each crossing (false positive / confirmed) at ${ctx.dispositionUrl}`);
|
|
118
|
+
}
|
|
119
|
+
if (kinds.has('SECURITY_FINDING')) {
|
|
120
|
+
lines.push('SECURITY_FINDING: fix and push — a fixed fingerprint disappears from the next scan — or a human marks it false positive / accepted risk in the web delivery panel.');
|
|
121
|
+
}
|
|
122
|
+
if (unconfirmed.length > 0) {
|
|
123
|
+
lines.push(`Could not confirm the full blocker list (${unconfirmed.join('; ')}) — run \`lumo task status ${ctx.identifier}\` and check the task page before assuming the list above is complete.`);
|
|
124
|
+
}
|
|
125
|
+
return lines;
|
|
126
|
+
}
|
|
127
|
+
function buildBlockedError(args) {
|
|
128
|
+
const url = (0, open_crossings_1.dispositionUrl)(args.apiUrl, args.workspaceSlug, args.identifier);
|
|
129
|
+
return {
|
|
130
|
+
status: 'blocked',
|
|
131
|
+
code: 'DONE_BLOCKED',
|
|
132
|
+
command: 'task update',
|
|
133
|
+
task: args.identifier,
|
|
134
|
+
message: args.message,
|
|
135
|
+
blockers: args.blockers,
|
|
136
|
+
unconfirmed: args.unconfirmed,
|
|
137
|
+
remediation: buildRemediation(args.blockers, args.unconfirmed, {
|
|
138
|
+
identifier: args.identifier,
|
|
139
|
+
dispositionUrl: url,
|
|
140
|
+
}),
|
|
141
|
+
dispositionUrl: url,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function blockerLine(b) {
|
|
145
|
+
switch (b.kind) {
|
|
146
|
+
case 'SEND_BACK':
|
|
147
|
+
return `SEND_BACK ${b.criterionId} (round ${b.failedAtRound}): ${b.statement}${b.rejectionReason ? ` — ${b.rejectionReason}` : ''}`;
|
|
148
|
+
case 'BOUNDARY_CROSSING':
|
|
149
|
+
return `[${b.severity}] ${b.category} ${b.id}: ${b.detail}`;
|
|
150
|
+
case 'SECURITY_FINDING':
|
|
151
|
+
return `[${b.severity}] ${b.ruleId} ${b.findingId} — ${b.filePath}${b.line != null ? `:${b.line}` : ''} (PR #${b.prNumber}): ${b.statement}`;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** JSON for pipes; the same fields as readable text for a terminal. */
|
|
155
|
+
function renderBlocked(err, isTTY) {
|
|
156
|
+
if (!isTTY)
|
|
157
|
+
return JSON.stringify(err, null, 2) + '\n';
|
|
158
|
+
const lines = [
|
|
159
|
+
`DONE_BLOCKED — \`lumo task update ${err.task} --status done\` was refused by a human-only gate.`,
|
|
160
|
+
...err.message.split('\n').map(l => ` ${l}`),
|
|
161
|
+
'',
|
|
162
|
+
err.blockers.length > 0 ? 'Blockers:' : 'Blockers: (none listed)',
|
|
163
|
+
...err.blockers.map(b => ` • ${(0, sanitize_1.sanitizeField)(blockerLine(b))}`),
|
|
164
|
+
...(err.unconfirmed.length > 0
|
|
165
|
+
? ['', ...err.unconfirmed.map(u => ` ⚠ ${u}`)]
|
|
166
|
+
: []),
|
|
167
|
+
'',
|
|
168
|
+
'What can move it:',
|
|
169
|
+
...err.remediation.map(r => ` - ${r}`),
|
|
170
|
+
` Disposition (human-only): ${err.dispositionUrl}`,
|
|
171
|
+
];
|
|
172
|
+
return lines.join('\n') + '\n';
|
|
173
|
+
}
|
|
174
|
+
/** Print the structured error to stdout and return exit code 5. */
|
|
175
|
+
function emitBlocked(err) {
|
|
176
|
+
process.stdout.write(renderBlocked(err, Boolean(process.stdout.isTTY)));
|
|
177
|
+
return exports.EXIT_BLOCKED;
|
|
178
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Confirmation protocol (LUM-755).
|
|
4
|
+
*
|
|
5
|
+
* A mutation that needs human approval is neither an interactive
|
|
6
|
+
* "are you sure? [y/N]" (an agent has no stdin to answer it) nor a bare text
|
|
7
|
+
* refusal on stderr (exit 1 is indistinguishable from a real error, and the
|
|
8
|
+
* agent is left grepping prose). Instead the command:
|
|
9
|
+
*
|
|
10
|
+
* 1. computes, from real server/local state, what it is about to do;
|
|
11
|
+
* 2. prints a structured envelope to **stdout** and exits **4**
|
|
12
|
+
* (`EXIT_CONFIRMATION_REQUIRED`) without sending any mutation;
|
|
13
|
+
* 3. hands back a copy-pasteable `confirmCommand` — the same argv with
|
|
14
|
+
* `--confirm` appended (plus any second-gate flag such as `--force`).
|
|
15
|
+
*
|
|
16
|
+
* The agent shows `changes` to the user; only after the user approves does
|
|
17
|
+
* it re-run `confirmCommand` verbatim. `--confirm` therefore means "the user
|
|
18
|
+
* has confirmed" — it is not a flag an agent may add on its own initiative.
|
|
19
|
+
*
|
|
20
|
+
* Output shape: JSON when stdout is not a TTY (the agent path), a readable
|
|
21
|
+
* text rendering of the same fields on a TTY. There is no prompt in either
|
|
22
|
+
* mode — a human and an agent walk the same protocol.
|
|
23
|
+
*
|
|
24
|
+
* Exit codes (see `wrap()` in cli/src/index.ts): 0 ok · 1 error · 4 confirm.
|
|
25
|
+
*/
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.EXIT_CONFIRMATION_REQUIRED = void 0;
|
|
28
|
+
exports.isConfirmed = isConfirmed;
|
|
29
|
+
exports.shellQuote = shellQuote;
|
|
30
|
+
exports.buildConfirmCommand = buildConfirmCommand;
|
|
31
|
+
exports.buildEnvelope = buildEnvelope;
|
|
32
|
+
exports.renderConfirmation = renderConfirmation;
|
|
33
|
+
exports.emitConfirmation = emitConfirmation;
|
|
34
|
+
exports.EXIT_CONFIRMATION_REQUIRED = 4;
|
|
35
|
+
/** `--confirm`, or its legacy `--yes` alias. */
|
|
36
|
+
function isConfirmed(opts) {
|
|
37
|
+
return Boolean(opts.confirm || opts.yes);
|
|
38
|
+
}
|
|
39
|
+
/** POSIX-shell quoting: bare when safe, single-quoted otherwise. */
|
|
40
|
+
function shellQuote(arg) {
|
|
41
|
+
if (arg.length > 0 && /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg))
|
|
42
|
+
return arg;
|
|
43
|
+
return `'${arg.replace(/'/g, `'\\''`)}'`;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Rebuild the invocation the user ran, plus `--confirm` (and any
|
|
47
|
+
* `extraFlags`). Flags already present in argv are never duplicated, and
|
|
48
|
+
* `--confirm` always lands last so the command reads as "…, approved".
|
|
49
|
+
*/
|
|
50
|
+
function buildConfirmCommand(argv = process.argv.slice(2), extraFlags = []) {
|
|
51
|
+
const kept = argv.filter(a => a !== '--confirm');
|
|
52
|
+
const parts = ['lumo', ...kept.map(shellQuote)];
|
|
53
|
+
for (const flag of extraFlags) {
|
|
54
|
+
if (!kept.includes(flag))
|
|
55
|
+
parts.push(flag);
|
|
56
|
+
}
|
|
57
|
+
parts.push('--confirm');
|
|
58
|
+
return parts.join(' ');
|
|
59
|
+
}
|
|
60
|
+
function buildEnvelope(input) {
|
|
61
|
+
return {
|
|
62
|
+
status: 'confirmation_required',
|
|
63
|
+
command: input.command,
|
|
64
|
+
changes: input.changes,
|
|
65
|
+
confirmCommand: buildConfirmCommand(input.argv, input.extraFlags),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** JSON for pipes; the same fields as readable text for a terminal. */
|
|
69
|
+
function renderConfirmation(envelope, isTTY) {
|
|
70
|
+
if (!isTTY)
|
|
71
|
+
return JSON.stringify(envelope, null, 2) + '\n';
|
|
72
|
+
const lines = [
|
|
73
|
+
`Confirmation required — \`lumo ${envelope.command}\` was not run.`,
|
|
74
|
+
...envelope.changes.map(c => ` - ${c}`),
|
|
75
|
+
'Re-run with --confirm once the user has approved:',
|
|
76
|
+
` ${envelope.confirmCommand}`,
|
|
77
|
+
];
|
|
78
|
+
return lines.join('\n') + '\n';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Print the envelope to stdout and return the exit code the command should
|
|
82
|
+
* propagate. Nothing goes to stderr: the envelope is the command's result,
|
|
83
|
+
* not an error.
|
|
84
|
+
*/
|
|
85
|
+
function emitConfirmation(input) {
|
|
86
|
+
const envelope = buildEnvelope(input);
|
|
87
|
+
process.stdout.write(renderConfirmation(envelope, Boolean(process.stdout.isTTY)));
|
|
88
|
+
return exports.EXIT_CONFIRMATION_REQUIRED;
|
|
89
|
+
}
|
|
@@ -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
|
|
72
|
-
*
|
|
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
|
-
//
|
|
87
|
-
// reminder) share one
|
|
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 +
|
|
125
|
-
// PR-review todos share one additionalContext block so
|
|
126
|
-
// single coherent context payload at session start.
|
|
127
|
-
// warning (LUM-172) slots in first so it stays
|
|
128
|
-
// session's work entirely.
|
|
129
|
-
//
|
|
130
|
-
//
|
|
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,
|
|
@@ -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,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EXTERNAL_ERROR_PREFIX = void 0;
|
|
4
|
+
exports.externalFailureReason = externalFailureReason;
|
|
5
|
+
/**
|
|
6
|
+
* PR security scan — the one `PrSecurityScan.error` shape that may be shown
|
|
7
|
+
* as an external-scanner reason (spec P8). Shared by the web panel, the PR
|
|
8
|
+
* summary (both via `lib/security-scan/stages.ts`, which re-exports this)
|
|
9
|
+
* and the CLI (`cli/src/commands/task-status.ts`), so the three renderers
|
|
10
|
+
* cannot drift apart (LUM-756).
|
|
11
|
+
*
|
|
12
|
+
* The column has two writers: the ingest / managed-scan workflow, which
|
|
13
|
+
* scrubs its reason and stamps this prefix on it (the prefix is the proof of
|
|
14
|
+
* provenance), and stage A's own catch-all, which writes a RAW, unscrubbed
|
|
15
|
+
* `error.message` with no prefix. Renderers therefore check the prefix first
|
|
16
|
+
* and print only what follows it; unprefixed text is never published.
|
|
17
|
+
*/
|
|
18
|
+
exports.EXTERNAL_ERROR_PREFIX = 'external: ';
|
|
19
|
+
/**
|
|
20
|
+
* The scanner's reason, or null when there is none to show: a missing /
|
|
21
|
+
* empty error, an unprefixed (stage A raw) error, or a prefix with nothing
|
|
22
|
+
* after it.
|
|
23
|
+
*/
|
|
24
|
+
function externalFailureReason(error) {
|
|
25
|
+
if (!error || !error.startsWith(exports.EXTERNAL_ERROR_PREFIX))
|
|
26
|
+
return null;
|
|
27
|
+
const reason = error.slice(exports.EXTERNAL_ERROR_PREFIX.length);
|
|
28
|
+
return reason.length > 0 ? reason : null;
|
|
29
|
+
}
|