@skrr-ai/cli 0.1.104 → 0.1.105
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/dist/base-command.js +6 -2
- package/dist/commands/agenda/trigger/list.js +2 -0
- package/dist/commands/agents/chat.js +4 -1
- package/dist/commands/agents/operating-plan.js +5 -2
- package/dist/commands/agents/triggers/create.js +3 -1
- package/dist/commands/agents/triggers/fire.js +2 -0
- package/dist/commands/agents/triggers/replay.js +2 -0
- package/dist/commands/agents/triggers/update.js +12 -4
- package/dist/commands/commitments/action-history.js +7 -2
- package/dist/commands/commitments/cadence-decisions.js +6 -1
- package/dist/commands/commitments/cadence-requests/decide.d.ts +1 -1
- package/dist/commands/commitments/cadence-requests/decide.js +1 -1
- package/dist/commands/commitments/doctor.js +7 -1
- package/dist/commands/commitments/observations.js +7 -1
- package/dist/commands/commitments/operating-review.d.ts +15 -0
- package/dist/commands/commitments/operating-review.js +23 -2
- package/dist/commands/commitments/preflight.d.ts +12 -0
- package/dist/commands/commitments/preflight.js +23 -9
- package/dist/commands/commitments/reflection.js +6 -1
- package/dist/commands/commitments/show.js +3 -1
- package/dist/commands/commitments/trigger-status.js +2 -22
- package/dist/commands/commitments/wake-capabilities.d.ts +13 -0
- package/dist/commands/commitments/wake-capabilities.js +39 -3
- package/dist/commands/daemon/logs.d.ts +41 -3
- package/dist/commands/daemon/logs.js +202 -17
- package/dist/commands/followups/act.js +13 -1
- package/dist/commands/followups/checkin.js +11 -2
- package/dist/commands/followups/reschedule.js +13 -1
- package/dist/commands/proposals/discard.d.ts +1 -1
- package/dist/commands/proposals/keep.d.ts +1 -1
- package/dist/commands/proposals/list.d.ts +7 -0
- package/dist/commands/proposals/list.js +31 -5
- package/dist/commands/proposals/show.d.ts +2 -1
- package/dist/commands/proposals/show.js +31 -4
- package/dist/commands/proposals/snooze.d.ts +1 -1
- package/dist/commands/requests/create.js +8 -3
- package/dist/commands/spaces/workflows/create.js +3 -3
- package/dist/commands/tasks/complete.js +4 -0
- package/dist/commands/tasks/create.d.ts +2 -0
- package/dist/commands/tasks/create.js +17 -3
- package/dist/commands/tasks/deliverable/add.d.ts +2 -0
- package/dist/commands/tasks/deliverable/add.js +22 -7
- package/dist/commands/tasks/deliverable/list.d.ts +13 -0
- package/dist/commands/tasks/deliverable/list.js +34 -1
- package/dist/commands/tasks/economics-compare.js +7 -1
- package/dist/commands/tasks/expectations.d.ts +10 -0
- package/dist/commands/tasks/expectations.js +31 -3
- package/dist/commands/tasks/list.d.ts +9 -0
- package/dist/commands/tasks/list.js +40 -5
- package/dist/commands/tasks/report.js +1 -1
- package/dist/commands/tasks/result/show.d.ts +7 -0
- package/dist/commands/tasks/result/show.js +32 -1
- package/dist/commands/tasks/result/submit.js +1 -1
- package/dist/commands/tasks/show.d.ts +6 -0
- package/dist/commands/tasks/show.js +23 -0
- package/dist/commands/tasks/update.d.ts +3 -0
- package/dist/commands/tasks/update.js +23 -3
- package/dist/commands/triggers/audit.js +6 -2
- package/dist/commands/triggers/create.js +3 -1
- package/dist/commands/triggers/fire.d.ts +1 -0
- package/dist/commands/triggers/fire.js +16 -1
- package/dist/commands/triggers/preflight.js +6 -5
- package/dist/commands/triggers/replay.js +2 -0
- package/dist/commands/triggers/update.js +12 -4
- package/dist/commands/workspaces/stewardship.js +5 -1
- package/dist/lib/agentic-stream.d.ts +9 -0
- package/dist/lib/agentic-stream.js +38 -18
- package/dist/lib/commitment-event-wakes.d.ts +8 -0
- package/dist/lib/commitment-event-wakes.js +39 -0
- package/dist/lib/commitment-ledgers.d.ts +9 -0
- package/dist/lib/commitment-ledgers.js +126 -0
- package/dist/lib/commitments.d.ts +102 -2
- package/dist/lib/commitments.js +208 -8
- package/dist/lib/delegated-cli.js +13 -2
- package/dist/lib/followups.d.ts +6 -0
- package/dist/lib/followups.js +12 -0
- package/dist/lib/message-content.d.ts +6 -0
- package/dist/lib/message-content.js +24 -0
- package/dist/lib/operating-plan-loops.d.ts +8 -0
- package/dist/lib/operating-plan-loops.js +32 -0
- package/dist/lib/preflight-copy.d.ts +27 -0
- package/dist/lib/preflight-copy.js +89 -0
- package/dist/lib/proposal-decision-command.d.ts +1 -1
- package/dist/lib/proposal-decision-command.js +27 -3
- package/dist/lib/proposals.d.ts +17 -0
- package/dist/lib/proposals.js +41 -0
- package/dist/lib/stewardship-output.js +57 -12
- package/dist/lib/task-economics-line.d.ts +30 -0
- package/dist/lib/task-economics-line.js +49 -0
- package/dist/lib/task-economics.d.ts +15 -0
- package/dist/lib/task-economics.js +73 -8
- package/dist/lib/task-recurrence-outcome.d.ts +19 -0
- package/dist/lib/task-recurrence-outcome.js +30 -0
- package/dist/lib/triggers.d.ts +42 -0
- package/dist/lib/triggers.js +136 -2
- package/dist/lib/widgetTerminalText.d.ts +8 -0
- package/dist/lib/widgetTerminalText.js +116 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +27 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.d.ts +28 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/telemetryRedaction.js +50 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +33 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +26 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.d.ts +28 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/telemetryRedaction.js +50 -5
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +7261 -7056
- package/docs/v1/operations.md +1 -1
- package/oclif.manifest.json +116 -72
- package/package.json +1 -1
|
@@ -50,6 +50,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
50
50
|
const os = __importStar(require("node:os"));
|
|
51
51
|
const path = __importStar(require("node:path"));
|
|
52
52
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
53
|
+
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
53
54
|
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
54
55
|
function isDelegatedCliSession(env = process.env) {
|
|
55
56
|
return Boolean(env.OVERSKY_AUTONOMOUS_SESSION === '1' &&
|
|
@@ -237,9 +238,17 @@ class DelegatedCliUnavailableError extends Error {
|
|
|
237
238
|
this.upstreamCode = fields.upstreamCode;
|
|
238
239
|
this.upstreamReason = fields.upstreamReason;
|
|
239
240
|
this.retryAt = fields.retryAt;
|
|
241
|
+
// OSK-16216 — an older daemon names no remedy. The fallback is the SAME
|
|
242
|
+
// shared sentence a current daemon sends, with nothing filled in: this
|
|
243
|
+
// process cannot tell whether the desktop app or a `skrrd install` runs
|
|
244
|
+
// the daemon, and `skrr daemon login` alone is wrong for the first.
|
|
240
245
|
this.remedy =
|
|
241
246
|
fields.remedy ??
|
|
242
|
-
(fields.retryable
|
|
247
|
+
(fields.retryable
|
|
248
|
+
? undefined
|
|
249
|
+
: (0, cli_handoff_wire_1.daemonSignInRemedy)({
|
|
250
|
+
repinsDeviceKey: fields.upstreamCode === 'DEVICE_PROOF_PIN_MISMATCH',
|
|
251
|
+
}));
|
|
243
252
|
}
|
|
244
253
|
}
|
|
245
254
|
exports.DelegatedCliUnavailableError = DelegatedCliUnavailableError;
|
|
@@ -321,7 +330,9 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
|
|
|
321
330
|
throw new DelegatedCliUnavailableError(typeof body?.message === 'string' && body.message
|
|
322
331
|
? `the daemon's delegated CLI broker could not mint a credential: ${body.message}`
|
|
323
332
|
: `the skrr server refused the daemon's request for this session's credential (${named}).`, {
|
|
324
|
-
...(typeof body?.upstreamStatus === 'number'
|
|
333
|
+
...(typeof body?.upstreamStatus === 'number'
|
|
334
|
+
? { upstreamStatus: body.upstreamStatus }
|
|
335
|
+
: {}),
|
|
325
336
|
...(typeof body?.upstreamCode === 'string' ? { upstreamCode: body.upstreamCode } : {}),
|
|
326
337
|
...(typeof body?.upstreamReason === 'string'
|
|
327
338
|
? { upstreamReason: body.upstreamReason }
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { withQuery } from './triggers';
|
|
2
2
|
type Query = Parameters<typeof withQuery>[1];
|
|
3
3
|
type Json = Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Check a `--in` / `--at` value against the bound the server enforces, before the round trip, and
|
|
6
|
+
* name the FLAG the user typed. The server's message names its own field (`in`, `once.runAt`) and,
|
|
7
|
+
* for a duration, used to give the bound in raw seconds (OSK-15415). Null when it is acceptable.
|
|
8
|
+
*/
|
|
9
|
+
export declare function whenFlagProblem(flag: 'in' | 'at', value: string | undefined): string | null;
|
|
4
10
|
/**
|
|
5
11
|
* The line a settle verb prints. A follow-up that had already ended is said to
|
|
6
12
|
* have ended, with the terminal it actually has, rather than reported as the
|
package/dist/lib/followups.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.followupsApi = exports.FOLLOWUP_SUBJECT_FLAG_HELP = exports.FOLLOWUP_SUBJECT_EXTERNAL_KEY_RE = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
|
|
4
|
+
exports.whenFlagProblem = whenFlagProblem;
|
|
4
5
|
exports.settledLine = settledLine;
|
|
5
6
|
exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
|
|
6
7
|
exports.resolveFollowUpAgent = resolveFollowUpAgent;
|
|
@@ -28,6 +29,17 @@ const triggers_1 = require("./triggers");
|
|
|
28
29
|
const agent_resolver_1 = require("./agent-resolver");
|
|
29
30
|
const MOUNT = '/api/followups';
|
|
30
31
|
const base = (id) => `${MOUNT}/${encodeURIComponent(id)}`;
|
|
32
|
+
/**
|
|
33
|
+
* Check a `--in` / `--at` value against the bound the server enforces, before the round trip, and
|
|
34
|
+
* name the FLAG the user typed. The server's message names its own field (`in`, `once.runAt`) and,
|
|
35
|
+
* for a duration, used to give the bound in raw seconds (OSK-15415). Null when it is acceptable.
|
|
36
|
+
*/
|
|
37
|
+
function whenFlagProblem(flag, value) {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
return null;
|
|
40
|
+
const refusal = (0, data_provider_1.validateFollowUpWhen)(value, `--${flag}`);
|
|
41
|
+
return refusal ? refusal.message : null;
|
|
42
|
+
}
|
|
31
43
|
/**
|
|
32
44
|
* The line a settle verb prints. A follow-up that had already ended is said to
|
|
33
45
|
* have ended, with the terminal it actually has, rather than reported as the
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
* join them with a paragraph break, and non-text parts (tool calls, images,
|
|
12
12
|
* think blocks) are skipped the same way the `--json` response omits them.
|
|
13
13
|
*
|
|
14
|
+
* A widget part (CSS, HTML and JavaScript an app draws in a frame) is a block of
|
|
15
|
+
* its fallback sentence: a terminal cannot draw it, the markup is never printed,
|
|
16
|
+
* and skipping it like the other non-text parts dropped the sentence from every
|
|
17
|
+
* message that also had text. A cut-off widget contributes nothing, as it does in
|
|
18
|
+
* the stored `text`.
|
|
19
|
+
*
|
|
14
20
|
* Falls back to the flattened `text` string when a message carries no
|
|
15
21
|
* structured content — user messages and historical rows never have it.
|
|
16
22
|
*/
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.messageTextBlocks = messageTextBlocks;
|
|
4
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
4
5
|
/**
|
|
5
6
|
* Flatten a conversation message's assistant `content[]` parts into readable
|
|
6
7
|
* display blocks.
|
|
@@ -14,6 +15,12 @@ exports.messageTextBlocks = messageTextBlocks;
|
|
|
14
15
|
* join them with a paragraph break, and non-text parts (tool calls, images,
|
|
15
16
|
* think blocks) are skipped the same way the `--json` response omits them.
|
|
16
17
|
*
|
|
18
|
+
* A widget part (CSS, HTML and JavaScript an app draws in a frame) is a block of
|
|
19
|
+
* its fallback sentence: a terminal cannot draw it, the markup is never printed,
|
|
20
|
+
* and skipping it like the other non-text parts dropped the sentence from every
|
|
21
|
+
* message that also had text. A cut-off widget contributes nothing, as it does in
|
|
22
|
+
* the stored `text`.
|
|
23
|
+
*
|
|
17
24
|
* Falls back to the flattened `text` string when a message carries no
|
|
18
25
|
* structured content — user messages and historical rows never have it.
|
|
19
26
|
*/
|
|
@@ -43,6 +50,9 @@ function textOfPart(part) {
|
|
|
43
50
|
return '';
|
|
44
51
|
}
|
|
45
52
|
const p = part;
|
|
53
|
+
if (p.type === data_provider_1.UI_WIDGET_PART_TYPE) {
|
|
54
|
+
return widgetFallback(p[data_provider_1.UI_WIDGET_PART_TYPE]);
|
|
55
|
+
}
|
|
46
56
|
if (typeof p.type === 'string' && p.type !== 'text') {
|
|
47
57
|
return '';
|
|
48
58
|
}
|
|
@@ -58,3 +68,17 @@ function textOfPart(part) {
|
|
|
58
68
|
}
|
|
59
69
|
return '';
|
|
60
70
|
}
|
|
71
|
+
/** The sentence that stands for a complete widget part, or `''` (never any of its markup). */
|
|
72
|
+
function widgetFallback(widget) {
|
|
73
|
+
if (!widget || typeof widget !== 'object') {
|
|
74
|
+
return '';
|
|
75
|
+
}
|
|
76
|
+
const w = widget;
|
|
77
|
+
if (w.complete === false) {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
return (0, data_provider_1.uiWidgetFallbackText)({
|
|
81
|
+
title: typeof w.title === 'string' ? w.title : undefined,
|
|
82
|
+
fallbackText: typeof w.fallbackText === 'string' ? w.fallbackText : undefined,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The operating plan's planning loops, named (OSK-15416). The text view printed only
|
|
3
|
+
* "15 existing planning/reflection loops; 0 scheduled work blocks": a count that grew as follow-ups
|
|
4
|
+
* were armed and did not drop when they were cancelled (a cancelled loop stays in the list, disabled),
|
|
5
|
+
* and never said which wake was due when. The server already projects `enabled`, `status` and
|
|
6
|
+
* `nextAt` for each loop; only --json showed them.
|
|
7
|
+
*/
|
|
8
|
+
export declare function planningLoopLines(loops: unknown, todoCount: number): string[];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The operating plan's planning loops, named (OSK-15416). The text view printed only
|
|
4
|
+
* "15 existing planning/reflection loops; 0 scheduled work blocks": a count that grew as follow-ups
|
|
5
|
+
* were armed and did not drop when they were cancelled (a cancelled loop stays in the list, disabled),
|
|
6
|
+
* and never said which wake was due when. The server already projects `enabled`, `status` and
|
|
7
|
+
* `nextAt` for each loop; only --json showed them.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.planningLoopLines = planningLoopLines;
|
|
11
|
+
const SHOWN = 10;
|
|
12
|
+
function planningLoopLines(loops, todoCount) {
|
|
13
|
+
const all = (Array.isArray(loops) ? loops : []);
|
|
14
|
+
const armed = all.filter((loop) => loop.enabled !== false);
|
|
15
|
+
const disabled = all.length - armed.length;
|
|
16
|
+
const lines = [
|
|
17
|
+
'',
|
|
18
|
+
`${armed.length} armed planning/reflection loop${armed.length === 1 ? '' : 's'}` +
|
|
19
|
+
`${disabled > 0 ? ` (${disabled} more disabled or cancelled, not counted)` : ''}; ` +
|
|
20
|
+
`${todoCount} scheduled work block${todoCount === 1 ? '' : 's'}.`,
|
|
21
|
+
];
|
|
22
|
+
// Soonest first; a loop with no due time (an event wait, unknown) after the dated ones.
|
|
23
|
+
const ordered = [...armed].sort((a, b) => (a.nextAt || '9999').localeCompare(b.nextAt || '9999'));
|
|
24
|
+
for (const loop of ordered.slice(0, SHOWN)) {
|
|
25
|
+
lines.push(` ${loop.nextAt ?? '(no due time)'} · ${loop.label ?? 'Scheduled work'} · ` +
|
|
26
|
+
`${loop.kind ?? '-'} · ${loop.status ?? '-'} · ${loop.id ?? '-'}`);
|
|
27
|
+
}
|
|
28
|
+
if (ordered.length > SHOWN) {
|
|
29
|
+
lines.push(` … and ${ordered.length - SHOWN} more armed (all of them in --json).`);
|
|
30
|
+
}
|
|
31
|
+
return lines;
|
|
32
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-words sentences for two preflight gates whose verdicts used to be a bare `warn` (OSK-15425).
|
|
3
|
+
*
|
|
4
|
+
* "When is silence a problem?" printed only that freshness is advisory, with the reason
|
|
5
|
+
* (`evidence_unconfigured`) left in --json, so a reader could not tell whether silence would ever be
|
|
6
|
+
* flagged. "Where does its access come from?" printed no sentence at all. A credential whose expiry
|
|
7
|
+
* had passed read `stale`.
|
|
8
|
+
*/
|
|
9
|
+
type GateLike = {
|
|
10
|
+
id?: string;
|
|
11
|
+
verdict?: string;
|
|
12
|
+
reason?: string | null;
|
|
13
|
+
detail?: Record<string, unknown> | null;
|
|
14
|
+
};
|
|
15
|
+
/** The refusal as `code: words`, or the bare code when it is not one this knows. */
|
|
16
|
+
export declare function sourceRefusalWords(code: string): string;
|
|
17
|
+
/** Extra lines under a gate, or none. The gate's own note is printed by the caller. */
|
|
18
|
+
export declare function gateExplanationLines(gate: GateLike): string[];
|
|
19
|
+
/**
|
|
20
|
+
* The status word for a capability. A credential whose expiry has passed is `expired`, not the
|
|
21
|
+
* storage word `stale`, which gave no sign that time had run out.
|
|
22
|
+
*/
|
|
23
|
+
export declare function capabilityStatusLabel(capability: {
|
|
24
|
+
status?: string;
|
|
25
|
+
expiresAt?: string | null;
|
|
26
|
+
}, now?: Date): string;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plain-words sentences for two preflight gates whose verdicts used to be a bare `warn` (OSK-15425).
|
|
4
|
+
*
|
|
5
|
+
* "When is silence a problem?" printed only that freshness is advisory, with the reason
|
|
6
|
+
* (`evidence_unconfigured`) left in --json, so a reader could not tell whether silence would ever be
|
|
7
|
+
* flagged. "Where does its access come from?" printed no sentence at all. A credential whose expiry
|
|
8
|
+
* had passed read `stale`.
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.sourceRefusalWords = sourceRefusalWords;
|
|
12
|
+
exports.gateExplanationLines = gateExplanationLines;
|
|
13
|
+
exports.capabilityStatusLabel = capabilityStatusLabel;
|
|
14
|
+
/** What a refused event-source code means, in words. An unknown code is printed as it is. */
|
|
15
|
+
const SOURCE_REFUSAL_WORDS = {
|
|
16
|
+
source_authorization_descriptor_invalid: 'authorization record incomplete (it names no id to check access against)',
|
|
17
|
+
source_binding_detached: 'no longer matches a source attached to this Commitment',
|
|
18
|
+
source_access_revoked: 'access refused',
|
|
19
|
+
source_access_unavailable: 'access could not be checked just now',
|
|
20
|
+
};
|
|
21
|
+
/** The refusal as `code: words`, or the bare code when it is not one this knows. */
|
|
22
|
+
function sourceRefusalWords(code) {
|
|
23
|
+
const words = SOURCE_REFUSAL_WORDS[code];
|
|
24
|
+
return words ? `${code}: ${words}` : code;
|
|
25
|
+
}
|
|
26
|
+
function sourceRefusalLine(row) {
|
|
27
|
+
const id = row.refId
|
|
28
|
+
? `${row.selector ?? 'id'} ${row.refId}`
|
|
29
|
+
: `no ${row.selector ?? 'id'} given`;
|
|
30
|
+
const why = row.reason ? (SOURCE_REFUSAL_WORDS[row.reason] ?? row.reason) : 'access was refused';
|
|
31
|
+
return `Source ${row.source ?? 'unknown'} (${id}): ${why}.`;
|
|
32
|
+
}
|
|
33
|
+
/** Extra lines under a gate, or none. The gate's own note is printed by the caller. */
|
|
34
|
+
function gateExplanationLines(gate) {
|
|
35
|
+
if (gate.verdict === 'pass')
|
|
36
|
+
return [];
|
|
37
|
+
if (gate.id === 'freshness') {
|
|
38
|
+
switch (gate.reason) {
|
|
39
|
+
case 'evidence_unconfigured':
|
|
40
|
+
return [
|
|
41
|
+
'No freshness expectation is set, so nothing will flag this Commitment’s evidence as stale or silent.',
|
|
42
|
+
];
|
|
43
|
+
case 'evidence_stale': {
|
|
44
|
+
const criteria = Array.isArray(gate.detail?.criteria)
|
|
45
|
+
? gate.detail.criteria
|
|
46
|
+
: [];
|
|
47
|
+
const observed = criteria
|
|
48
|
+
.filter((item) => item.evidence === 'stale' && item.observedAt)
|
|
49
|
+
.map((item) => String(item.observedAt))
|
|
50
|
+
.sort();
|
|
51
|
+
return [
|
|
52
|
+
observed.length > 0
|
|
53
|
+
? `Its evidence is stale; the oldest reading is from ${observed[0]}.`
|
|
54
|
+
: 'Its evidence is stale.',
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
case 'evidence_partial':
|
|
58
|
+
return ['Only part of its evidence is current.'];
|
|
59
|
+
case 'evidence_unavailable':
|
|
60
|
+
return ['No evidence could be read just now, so freshness cannot be judged.'];
|
|
61
|
+
default:
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// The gate carries the sources it checked; its reason alone is a bare code naming none of them.
|
|
66
|
+
if (gate.id === 'source_access') {
|
|
67
|
+
const rows = Array.isArray(gate.detail?.sources) ? gate.detail.sources : [];
|
|
68
|
+
return rows.filter((row) => row.access && row.access !== 'authorized').map(sourceRefusalLine);
|
|
69
|
+
}
|
|
70
|
+
if (gate.id === 'access' && gate.reason === 'capability_access_unverified') {
|
|
71
|
+
return [
|
|
72
|
+
'No access beyond an identity check is verified as available yet (see “Where access comes from” below).',
|
|
73
|
+
];
|
|
74
|
+
}
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The status word for a capability. A credential whose expiry has passed is `expired`, not the
|
|
79
|
+
* storage word `stale`, which gave no sign that time had run out.
|
|
80
|
+
*/
|
|
81
|
+
function capabilityStatusLabel(capability, now = new Date()) {
|
|
82
|
+
const status = capability.status ?? 'unknown';
|
|
83
|
+
if (status === 'stale' && capability.expiresAt) {
|
|
84
|
+
const expires = Date.parse(capability.expiresAt);
|
|
85
|
+
if (!Number.isNaN(expires) && expires <= now.getTime())
|
|
86
|
+
return 'expired';
|
|
87
|
+
}
|
|
88
|
+
return status;
|
|
89
|
+
}
|
|
@@ -6,7 +6,7 @@ export declare abstract class ProposalDecisionCommand extends BaseCommand {
|
|
|
6
6
|
static decision: ProposalDecision;
|
|
7
7
|
static args: {
|
|
8
8
|
kind: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
-
id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
9
|
+
id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
10
10
|
};
|
|
11
11
|
static flags: {
|
|
12
12
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
@@ -9,9 +9,15 @@ const proposals_1 = require("./proposals");
|
|
|
9
9
|
* before an old decision would approve content the caller never reviewed. */
|
|
10
10
|
class ProposalDecisionCommand extends base_command_1.BaseCommand {
|
|
11
11
|
static decision;
|
|
12
|
+
// The kind is validated in `resolveProposalRef`, not by oclif, so the KIND:ID that `proposals list`
|
|
13
|
+
// prints can be pasted whole (OSK-15371).
|
|
12
14
|
static args = {
|
|
13
|
-
kind: core_1.Args.string({
|
|
14
|
-
|
|
15
|
+
kind: core_1.Args.string({
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'Proposal kind, or the KIND:ID that `proposals list` prints',
|
|
18
|
+
ignoreStdin: true,
|
|
19
|
+
}),
|
|
20
|
+
id: core_1.Args.string({ required: false, description: 'Proposal id', ignoreStdin: true }),
|
|
15
21
|
};
|
|
16
22
|
static flags = {
|
|
17
23
|
json: core_1.Flags.boolean(),
|
|
@@ -45,9 +51,18 @@ class ProposalDecisionCommand extends base_command_1.BaseCommand {
|
|
|
45
51
|
if (until > Date.now() + data_provider_1.PROPOSAL_SNOOZE_MAX_DAYS * 86400000)
|
|
46
52
|
this.error(`--until may be at most ${data_provider_1.PROPOSAL_SNOOZE_MAX_DAYS} days ahead.`);
|
|
47
53
|
}
|
|
54
|
+
const ref = (0, proposals_1.resolveProposalRef)(args.kind, args.id, this.config?.bin);
|
|
55
|
+
if ('error' in ref) {
|
|
56
|
+
this.failWithCliError({
|
|
57
|
+
message: ref.error,
|
|
58
|
+
code: 'PROPOSAL_REF_INVALID',
|
|
59
|
+
exit: 2,
|
|
60
|
+
retryable: false,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
48
63
|
let result;
|
|
49
64
|
try {
|
|
50
|
-
result = await proposals_1.proposalApi.decide(
|
|
65
|
+
result = await proposals_1.proposalApi.decide(ref.kind, ref.id, {
|
|
51
66
|
decision,
|
|
52
67
|
expected,
|
|
53
68
|
reason: flags.reason,
|
|
@@ -56,6 +71,15 @@ class ProposalDecisionCommand extends base_command_1.BaseCommand {
|
|
|
56
71
|
});
|
|
57
72
|
}
|
|
58
73
|
catch (error) {
|
|
74
|
+
if (error?.status === 404) {
|
|
75
|
+
this.failWithCliError({
|
|
76
|
+
message: (0, proposals_1.describeProposalNotFound)(ref.kind, ref.id, this.config?.bin),
|
|
77
|
+
code: 'PROPOSAL_NOT_FOUND',
|
|
78
|
+
status: 404,
|
|
79
|
+
exit: 4,
|
|
80
|
+
retryable: false,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
59
83
|
this.handleApiError(error);
|
|
60
84
|
}
|
|
61
85
|
this.log(flags.json ? JSON.stringify(result, null, 2) : `Proposal ${decision} decision recorded.`);
|
package/dist/lib/proposals.d.ts
CHANGED
|
@@ -6,3 +6,20 @@ export declare const proposalApi: {
|
|
|
6
6
|
show(kind: ProposalKind, id: string): Promise<ProposalView>;
|
|
7
7
|
decide(kind: ProposalKind, id: string, input: ProposalDecisionParams): Promise<ProposalDecisionResponse>;
|
|
8
8
|
};
|
|
9
|
+
/**
|
|
10
|
+
* The proposal a person pointed at, from what they typed or pasted (OSK-15371, OSK-15354).
|
|
11
|
+
*
|
|
12
|
+
* `proposals list` prints one token, `KIND:ID`, and `show`/`keep`/`discard`/`snooze` wanted two
|
|
13
|
+
* arguments, so pasting the list's own output failed with a message listing six kinds that never
|
|
14
|
+
* said the id belonged in a second argument. Both spellings are accepted now. The kind never
|
|
15
|
+
* contains a colon, so the FIRST one splits (a Northstar intent's id is itself
|
|
16
|
+
* `<review-id>:<intent-id>`), and `KIND KIND:ID` is accepted too.
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveProposalRef(kindArg: string | undefined, idArg: string | undefined, bin?: string): {
|
|
19
|
+
kind: ProposalKind;
|
|
20
|
+
id: string;
|
|
21
|
+
} | {
|
|
22
|
+
error: string;
|
|
23
|
+
};
|
|
24
|
+
/** A 404 here means no such proposal under THIS kind: the id is looked up under the kind given. */
|
|
25
|
+
export declare function describeProposalNotFound(kind: string, id: string, bin?: string): string;
|
package/dist/lib/proposals.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.proposalApi = void 0;
|
|
4
|
+
exports.resolveProposalRef = resolveProposalRef;
|
|
5
|
+
exports.describeProposalNotFound = describeProposalNotFound;
|
|
4
6
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
7
|
+
const data_provider_2 = require("@skrr-ai/data-provider");
|
|
5
8
|
const triggers_1 = require("./triggers");
|
|
6
9
|
const MOUNT = '/api/proposals';
|
|
7
10
|
/** The unified, read-time proposal inbox. Decisions are routed by the server to
|
|
@@ -20,3 +23,41 @@ exports.proposalApi = {
|
|
|
20
23
|
});
|
|
21
24
|
},
|
|
22
25
|
};
|
|
26
|
+
/**
|
|
27
|
+
* The proposal a person pointed at, from what they typed or pasted (OSK-15371, OSK-15354).
|
|
28
|
+
*
|
|
29
|
+
* `proposals list` prints one token, `KIND:ID`, and `show`/`keep`/`discard`/`snooze` wanted two
|
|
30
|
+
* arguments, so pasting the list's own output failed with a message listing six kinds that never
|
|
31
|
+
* said the id belonged in a second argument. Both spellings are accepted now. The kind never
|
|
32
|
+
* contains a colon, so the FIRST one splits (a Northstar intent's id is itself
|
|
33
|
+
* `<review-id>:<intent-id>`), and `KIND KIND:ID` is accepted too.
|
|
34
|
+
*/
|
|
35
|
+
function resolveProposalRef(kindArg, idArg, bin = 'skrr') {
|
|
36
|
+
const kinds = [...data_provider_2.PROPOSAL_KINDS].join(', ');
|
|
37
|
+
const usage = `Pass KIND and ID (\`${bin} proposals show request <id>\`) or the KIND:ID that \`${bin} proposals list\` prints.`;
|
|
38
|
+
let kind = (kindArg ?? '').trim();
|
|
39
|
+
let id = (idArg ?? '').trim();
|
|
40
|
+
if (!id) {
|
|
41
|
+
const colon = kind.indexOf(':');
|
|
42
|
+
if (colon < 0) {
|
|
43
|
+
return { error: `Missing the proposal id. ${usage}` };
|
|
44
|
+
}
|
|
45
|
+
id = kind.slice(colon + 1);
|
|
46
|
+
kind = kind.slice(0, colon);
|
|
47
|
+
}
|
|
48
|
+
else if (id.startsWith(`${kind}:`)) {
|
|
49
|
+
id = id.slice(kind.length + 1);
|
|
50
|
+
}
|
|
51
|
+
if (!data_provider_2.PROPOSAL_KINDS.includes(kind)) {
|
|
52
|
+
return { error: `Unknown proposal kind "${kind}". Kinds: ${kinds}. ${usage}` };
|
|
53
|
+
}
|
|
54
|
+
if (!id) {
|
|
55
|
+
return { error: `Missing the proposal id after "${kind}". ${usage}` };
|
|
56
|
+
}
|
|
57
|
+
return { kind: kind, id };
|
|
58
|
+
}
|
|
59
|
+
/** A 404 here means no such proposal under THIS kind: the id is looked up under the kind given. */
|
|
60
|
+
function describeProposalNotFound(kind, id, bin = 'skrr') {
|
|
61
|
+
return (`No ${kind} proposal ${id} is visible to you. A proposal is looked up under its kind, so an id ` +
|
|
62
|
+
`that exists under another kind is not found here; \`${bin} proposals list\` prints each as KIND:ID.`);
|
|
63
|
+
}
|
|
@@ -9,10 +9,26 @@ function printStewardshipCoverage(report, log) {
|
|
|
9
9
|
log('Coverage: complete within the bounded sources.');
|
|
10
10
|
return;
|
|
11
11
|
}
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
const unavailable = report.coverage.sources.some((source) => source.status === 'unavailable');
|
|
13
|
+
// A list cut at the row cap is READABLE and partial; calling it "unreadable" sent the reader looking
|
|
14
|
+
// for a permission problem (OSK-15355). `unattributedGrants` has its own line below.
|
|
15
|
+
const cut = report.coverage.sources.some((source) => source.status !== 'unavailable' && source.truncated && source.name !== 'unattributedGrants');
|
|
16
|
+
const unlistedGrants = report.coverage.sources.some((source) => source.unlisted);
|
|
17
|
+
if (unavailable || cut) {
|
|
18
|
+
log(`Coverage: incomplete. ${[
|
|
19
|
+
unavailable ? 'Unreadable sources do not confirm absence.' : null,
|
|
20
|
+
cut
|
|
21
|
+
? 'Lists cut at the row cap are partial, so a row missing from them is not shown absent; this command has no paging.'
|
|
22
|
+
: null,
|
|
23
|
+
]
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
.join(' ')}`);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
log(unlistedGrants
|
|
29
|
+
? 'Coverage: incomplete. Some Grants are not attributed to this workspace and are not listed.'
|
|
30
|
+
: 'Coverage: incomplete.');
|
|
31
|
+
}
|
|
16
32
|
for (const source of report.coverage.sources) {
|
|
17
33
|
if (source.status === 'unavailable')
|
|
18
34
|
log(`${source.name}: unavailable`);
|
|
@@ -25,6 +41,18 @@ function printStewardshipCoverage(report, log) {
|
|
|
25
41
|
}
|
|
26
42
|
}
|
|
27
43
|
}
|
|
44
|
+
/** Who holds the Grant, in the same `kind id` words `skrr grants list` uses. */
|
|
45
|
+
function principalText(principal) {
|
|
46
|
+
const value = principal;
|
|
47
|
+
if (!value?.kind)
|
|
48
|
+
return '—';
|
|
49
|
+
const id = value.agentId ?? value.commitmentId;
|
|
50
|
+
return id ? `${value.kind} ${id}` : value.kind;
|
|
51
|
+
}
|
|
52
|
+
/** What the Grant allows: its capability names, or an honest dash when it lists none. */
|
|
53
|
+
function accessText(capabilities) {
|
|
54
|
+
return Array.isArray(capabilities) && capabilities.length ? capabilities.join(', ') : '—';
|
|
55
|
+
}
|
|
28
56
|
function printWorkspaceGrants(report, log) {
|
|
29
57
|
printStewardshipCoverage(report, log);
|
|
30
58
|
printGrantRows(report, log);
|
|
@@ -44,16 +72,23 @@ function printGrantRows(report, log) {
|
|
|
44
72
|
(0, format_1.renderTable)(report.grants.map((row) => ({
|
|
45
73
|
id: row.id,
|
|
46
74
|
label: row.label,
|
|
75
|
+
provider: row.provider || '—',
|
|
76
|
+
principal: principalText(row.principal),
|
|
77
|
+
access: accessText(row.capabilities),
|
|
47
78
|
owner: row.ownerUserId,
|
|
48
79
|
active: row.active ? 'yes' : 'no',
|
|
49
|
-
|
|
50
|
-
|
|
80
|
+
// No expiry is a fact about the Grant; a blank cell read as "not readable" (OSK-15357).
|
|
81
|
+
expires: row.expiresAt ?? 'never',
|
|
82
|
+
envKeys: row.credential?.envKeys.length ? row.credential.envKeys.join(', ') : '—',
|
|
51
83
|
referencedBy: row.referencedBy === null
|
|
52
84
|
? 'unavailable'
|
|
53
85
|
: `${row.referencedBy.join(', ')}${row.referencesTruncated ? ' (truncated)' : ''}`,
|
|
54
86
|
})), [
|
|
55
87
|
{ key: 'id', header: 'ID' },
|
|
56
88
|
{ key: 'label', header: 'LABEL' },
|
|
89
|
+
{ key: 'provider', header: 'PROVIDER' },
|
|
90
|
+
{ key: 'principal', header: 'HELD BY' },
|
|
91
|
+
{ key: 'access', header: 'ACCESS' },
|
|
57
92
|
{ key: 'owner', header: 'OWNER' },
|
|
58
93
|
{ key: 'active', header: 'ACTIVE' },
|
|
59
94
|
{ key: 'expires', header: 'EXPIRES' },
|
|
@@ -63,12 +98,22 @@ function printGrantRows(report, log) {
|
|
|
63
98
|
}
|
|
64
99
|
function printWorkspaceStewardship(report, log) {
|
|
65
100
|
printStewardshipCoverage(report, log);
|
|
66
|
-
log(`Membership: ${report.membership.status}${report.membership.role ? ` (${report.membership.role})` : ''}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
101
|
+
log(`Membership: ${report.membership.status}${report.membership.role ? ` (${report.membership.role})` : ''}${
|
|
102
|
+
// "Absent" is all the report knows: a removed member and an id that never existed read alike (OSK-15356).
|
|
103
|
+
report.membership.status === 'absent'
|
|
104
|
+
? ' — not a current member; this report cannot tell a removed member from an id that never existed'
|
|
105
|
+
: ''}`);
|
|
106
|
+
const isCut = (source) => report.coverage.sources.some((entry) => entry.name === source && entry.truncated);
|
|
107
|
+
const count = (rows, source) => rows === null
|
|
108
|
+
? 'unavailable'
|
|
109
|
+
: isCut(source)
|
|
110
|
+
? `${rows.length} shown, cut at the row cap (there are more)`
|
|
111
|
+
: String(rows.length);
|
|
112
|
+
for (const [name, source, rows] of [
|
|
113
|
+
['Commitments', 'commitments', report.commitments],
|
|
114
|
+
['Approver seats', 'approverSeats', report.approverSeats],
|
|
70
115
|
]) {
|
|
71
|
-
log(`${name}: ${rows
|
|
116
|
+
log(`${name}: ${count(rows, source)}`);
|
|
72
117
|
if (rows?.length)
|
|
73
118
|
(0, format_1.renderTable)(rows.map((row) => ({
|
|
74
119
|
id: row.id,
|
|
@@ -82,7 +127,7 @@ function printWorkspaceStewardship(report, log) {
|
|
|
82
127
|
{ key: 'agentId', header: 'EXECUTOR' },
|
|
83
128
|
], log);
|
|
84
129
|
}
|
|
85
|
-
log(`Authored Agents: ${report.agents
|
|
130
|
+
log(`Authored Agents: ${count(report.agents, 'agents')}`);
|
|
86
131
|
if (report.agents?.length)
|
|
87
132
|
(0, format_1.renderTable)(report.agents.map((row) => ({ id: row.id, name: row.name ?? '' })), [
|
|
88
133
|
{ key: 'id', header: 'ID' },
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Task's stored economic claim as one line for `tasks show` (OSK-15363).
|
|
3
|
+
*
|
|
4
|
+
* The detail view printed no savings, effort, risk, basis or confirmer at all: only `--json` carried
|
|
5
|
+
* them, with the confirmer as a raw user id, though `docs/cli/SKRR_TASKS.md` sends a reader to "ordinary
|
|
6
|
+
* task detail" for the claim. A claim is not a measurement, so the basis is part of the line.
|
|
7
|
+
*/
|
|
8
|
+
type Economics = {
|
|
9
|
+
savings?: {
|
|
10
|
+
amount?: number;
|
|
11
|
+
currency?: string;
|
|
12
|
+
per?: string;
|
|
13
|
+
};
|
|
14
|
+
effortHours?: number;
|
|
15
|
+
risk?: string;
|
|
16
|
+
confidence?: string;
|
|
17
|
+
rationale?: string;
|
|
18
|
+
basis?: string;
|
|
19
|
+
confirmedBy?: string;
|
|
20
|
+
confirmedAt?: string;
|
|
21
|
+
revision?: number;
|
|
22
|
+
};
|
|
23
|
+
/** Null when the Task carries no claim at all. `confirmerName` is the resolved name, if there is one. */
|
|
24
|
+
export declare function economicsLine(economics: Economics | null | undefined, confirmerName?: string): string | null;
|
|
25
|
+
/**
|
|
26
|
+
* The claim's rationale on its own line, or null. It is kept when a later edit changes the figures, so
|
|
27
|
+
* it is shown beside the basis it now carries rather than left to `--json`.
|
|
28
|
+
*/
|
|
29
|
+
export declare function economicsRationaleLine(economics: Economics | null | undefined): string | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* The Task's stored economic claim as one line for `tasks show` (OSK-15363).
|
|
4
|
+
*
|
|
5
|
+
* The detail view printed no savings, effort, risk, basis or confirmer at all: only `--json` carried
|
|
6
|
+
* them, with the confirmer as a raw user id, though `docs/cli/SKRR_TASKS.md` sends a reader to "ordinary
|
|
7
|
+
* task detail" for the claim. A claim is not a measurement, so the basis is part of the line.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.economicsLine = economicsLine;
|
|
11
|
+
exports.economicsRationaleLine = economicsRationaleLine;
|
|
12
|
+
const format_1 = require("./format");
|
|
13
|
+
const BASIS = {
|
|
14
|
+
agent_estimate: 'Agent estimate',
|
|
15
|
+
human_confirmed: 'Human confirmed',
|
|
16
|
+
measured: 'Measured',
|
|
17
|
+
};
|
|
18
|
+
/** Null when the Task carries no claim at all. `confirmerName` is the resolved name, if there is one. */
|
|
19
|
+
function economicsLine(economics, confirmerName) {
|
|
20
|
+
if (!economics || typeof economics !== 'object')
|
|
21
|
+
return null;
|
|
22
|
+
const parts = [];
|
|
23
|
+
const savings = economics.savings;
|
|
24
|
+
if (savings && typeof savings.amount === 'number') {
|
|
25
|
+
parts.push(`${savings.currency ?? 'USD'} ${savings.amount}${savings.per ? `/${savings.per}` : ''} saved`);
|
|
26
|
+
}
|
|
27
|
+
if (typeof economics.effortHours === 'number')
|
|
28
|
+
parts.push(`${economics.effortHours}h effort`);
|
|
29
|
+
if (economics.risk)
|
|
30
|
+
parts.push(`risk ${economics.risk}`);
|
|
31
|
+
if (parts.length === 0)
|
|
32
|
+
return null;
|
|
33
|
+
// Part of the claim, and kept across a later re-estimate: a reader must see what the label covers (OSK-15372).
|
|
34
|
+
if (economics.confidence)
|
|
35
|
+
parts.push(`confidence ${economics.confidence}`);
|
|
36
|
+
const basis = economics.basis ? (BASIS[economics.basis] ?? economics.basis) : 'basis not stated';
|
|
37
|
+
const who = economics.confirmedBy ? ` by ${confirmerName || economics.confirmedBy}` : '';
|
|
38
|
+
const when = economics.confirmedAt ? ` on ${String(economics.confirmedAt).slice(0, 10)}` : '';
|
|
39
|
+
const revision = typeof economics.revision === 'number' ? ` (claim revision ${economics.revision})` : '';
|
|
40
|
+
return `${parts.join(' · ')} · ${basis}${who}${when}${revision} — a claim, not a measurement`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The claim's rationale on its own line, or null. It is kept when a later edit changes the figures, so
|
|
44
|
+
* it is shown beside the basis it now carries rather than left to `--json`.
|
|
45
|
+
*/
|
|
46
|
+
function economicsRationaleLine(economics) {
|
|
47
|
+
const rationale = economics?.rationale;
|
|
48
|
+
return typeof rationale === 'string' && rationale.trim() ? (0, format_1.collapseWhitespace)(rationale) : null;
|
|
49
|
+
}
|