@skrr-ai/cli 0.1.128 → 0.1.129
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.d.ts +12 -0
- package/dist/base-command.js +35 -0
- package/dist/commands/agenda/trigger/list.d.ts +2 -1
- package/dist/commands/agenda/trigger/list.js +55 -6
- package/dist/commands/agents/create.js +3 -1
- package/dist/commands/browser/persistent-profile.d.ts +22 -0
- package/dist/commands/browser/persistent-profile.js +46 -0
- package/dist/commands/commitments/create.js +3 -1
- package/dist/commands/commitments/effective-policy.js +6 -0
- package/dist/commands/commitments/explain.js +10 -1
- package/dist/commands/commitments/plan.d.ts +14 -0
- package/dist/commands/commitments/plan.js +32 -0
- package/dist/commands/commitments/preflight.d.ts +1 -1
- package/dist/commands/commitments/preflight.js +12 -4
- package/dist/commands/commitments/show.d.ts +9 -0
- package/dist/commands/commitments/show.js +14 -1
- package/dist/commands/compass/revisions.d.ts +6 -2
- package/dist/commands/compass/revisions.js +13 -5
- package/dist/commands/compass/tactic.d.ts +5 -0
- package/dist/commands/compass/tactic.js +14 -0
- package/dist/commands/followups/plans/create.js +1 -1
- package/dist/commands/harnesses/list.js +1 -1
- package/dist/commands/northstars/governance/configure.js +1 -1
- package/dist/commands/northstars/intents/decide.js +2 -3
- package/dist/commands/northstars/review-now.js +5 -0
- package/dist/commands/northstars/reviews.js +15 -3
- package/dist/commands/spaces/auto-executor/runs.d.ts +9 -1
- package/dist/commands/spaces/auto-executor/runs.js +18 -1
- package/dist/commands/spaces/heartbeats/create.js +7 -5
- package/dist/commands/spaces/heartbeats/update.js +5 -3
- package/dist/commands/store/artifacts/releases.d.ts +20 -0
- package/dist/commands/store/artifacts/releases.js +85 -0
- package/dist/commands/store/library.d.ts +14 -0
- package/dist/commands/store/library.js +71 -0
- package/dist/commands/tasks/list.d.ts +48 -0
- package/dist/commands/tasks/list.js +141 -9
- package/dist/commands/tasks/remind.js +23 -11
- package/dist/commands/tasks/retry-schedule.js +6 -3
- package/dist/commands/tasks/schedule.js +29 -7
- package/dist/commands/tasks/show.js +4 -0
- package/dist/lib/agent-trigger-listing.d.ts +26 -0
- package/dist/lib/agent-trigger-listing.js +63 -0
- package/dist/lib/agentic-stream.js +138 -41
- package/dist/lib/commitment-ledgers.js +3 -1
- package/dist/lib/commitments.d.ts +24 -0
- package/dist/lib/commitments.js +64 -4
- package/dist/lib/compasses.d.ts +2 -0
- package/dist/lib/iso-instant.d.ts +52 -0
- package/dist/lib/iso-instant.js +162 -0
- package/dist/lib/northstars.d.ts +43 -0
- package/dist/lib/northstars.js +122 -6
- package/dist/lib/proposals.js +6 -2
- package/dist/lib/store-artifact-releases.d.ts +39 -0
- package/dist/lib/store-artifact-releases.js +12 -0
- package/dist/lib/store-library.d.ts +37 -0
- package/dist/lib/store-library.js +12 -0
- package/dist/lib/task-calendar.d.ts +19 -0
- package/dist/lib/task-calendar.js +72 -0
- package/dist/lib/task-recurrence-outcome.js +3 -1
- package/dist/lib/task-recurrence-summary.d.ts +15 -0
- package/dist/lib/task-recurrence-summary.js +228 -0
- package/dist/lib/task-reminder.d.ts +28 -0
- package/dist/lib/task-reminder.js +50 -0
- package/dist/lib/triggers.d.ts +13 -2
- package/dist/lib/triggers.js +22 -12
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +11 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.d.ts +123 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.js +160 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +17 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +11 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.d.ts +123 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.js +173 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +6755 -6672
- package/oclif.manifest.json +263 -29
- package/package.json +1 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* One reading of "an ISO-8601 UTC instant" for every CLI flag that takes one.
|
|
4
|
+
*
|
|
5
|
+
* `tasks remind --at` and `tasks schedule --at` each had their own: remind ran
|
|
6
|
+
* `new Date(value)` behind a `/^\d{4}-\d{2}-\d{2}T/` check, and schedule forwarded
|
|
7
|
+
* the string and printed the server's zod issue. `new Date()` is the wrong tool
|
|
8
|
+
* for an instant, in two ways that both stored a different time than the one
|
|
9
|
+
* typed and said nothing (OSK-21504, OSK-21506):
|
|
10
|
+
*
|
|
11
|
+
* - a time with no zone is read in the zone of the MACHINE running the CLI, so
|
|
12
|
+
* the same command stored different instants on different laptops;
|
|
13
|
+
* - a day that does not exist overflows into the next month
|
|
14
|
+
* (`2027-02-30` is `2027-03-02`).
|
|
15
|
+
*
|
|
16
|
+
* The rule here is the one `tasks schedule --help` already states and the server's
|
|
17
|
+
* `z.string().datetime()` enforces: a calendar-real date, a time with seconds, and
|
|
18
|
+
* a literal `Z`. A numeric offset is refused too, even though it names an instant:
|
|
19
|
+
* the two commands must agree, and the server's schedule door does not take one.
|
|
20
|
+
*
|
|
21
|
+
* Never reads the machine's zone. Pure.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.parseUtcInstant = parseUtcInstant;
|
|
25
|
+
exports.formatUtcInstant = formatUtcInstant;
|
|
26
|
+
exports.formatLocalInstant = formatLocalInstant;
|
|
27
|
+
exports.describeInstant = describeInstant;
|
|
28
|
+
const EXAMPLE = '2026-12-01T09:00:00Z';
|
|
29
|
+
const UTC_INSTANT = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?Z$/;
|
|
30
|
+
const ZONELESS = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?$/;
|
|
31
|
+
const OFFSET = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(?::(\d{2})(\.\d+)?)?([+-])(\d{2}):?(\d{2})$/;
|
|
32
|
+
const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
33
|
+
const MONTH_NAMES = [
|
|
34
|
+
'January',
|
|
35
|
+
'February',
|
|
36
|
+
'March',
|
|
37
|
+
'April',
|
|
38
|
+
'May',
|
|
39
|
+
'June',
|
|
40
|
+
'July',
|
|
41
|
+
'August',
|
|
42
|
+
'September',
|
|
43
|
+
'October',
|
|
44
|
+
'November',
|
|
45
|
+
'December',
|
|
46
|
+
];
|
|
47
|
+
function daysInMonth(year, month) {
|
|
48
|
+
return new Date(Date.UTC(year, month, 0)).getUTCDate();
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What is wrong with the calendar part, or `null` when the date exists.
|
|
52
|
+
* Checked by construction rather than by `new Date()`, which accepts the overflow.
|
|
53
|
+
*/
|
|
54
|
+
function calendarProblem(year, month, day) {
|
|
55
|
+
if (month < 1 || month > 12)
|
|
56
|
+
return `month ${String(month).padStart(2, '0')} does not exist`;
|
|
57
|
+
const last = daysInMonth(year, month);
|
|
58
|
+
if (day < 1 || day > last) {
|
|
59
|
+
return `${MONTH_NAMES[month - 1]} ${year} has ${last} days, so day ${String(day).padStart(2, '0')} does not exist`;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function clockProblem(hour, minute, second) {
|
|
64
|
+
if (hour > 23 || minute > 59 || second > 59) {
|
|
65
|
+
return `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}:${String(second).padStart(2, '0')} is not a time of day`;
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
function rule(label) {
|
|
70
|
+
return `${label} must be an ISO-8601 UTC time ending in Z, for example ${EXAMPLE}`;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Parse a flag value as a UTC instant, or say precisely what is wrong with it.
|
|
74
|
+
*
|
|
75
|
+
* `label` is what the user typed (`--at`, `scheduledFor`), so the message names
|
|
76
|
+
* their input rather than a REST field they never saw.
|
|
77
|
+
*/
|
|
78
|
+
function parseUtcInstant(raw, label = '--at') {
|
|
79
|
+
const value = String(raw ?? '').trim();
|
|
80
|
+
const fail = (problem, reason) => ({
|
|
81
|
+
ok: false,
|
|
82
|
+
problem,
|
|
83
|
+
message: `${rule(label)}. ${reason}`,
|
|
84
|
+
});
|
|
85
|
+
const utc = UTC_INSTANT.exec(value);
|
|
86
|
+
if (utc) {
|
|
87
|
+
const [year, month, day, hour, minute, second] = utc.slice(1, 7).map(Number);
|
|
88
|
+
const calendar = calendarProblem(year, month, day);
|
|
89
|
+
if (calendar)
|
|
90
|
+
return fail('not_a_real_date', `"${value}" is not a real date: ${calendar}.`);
|
|
91
|
+
const clock = clockProblem(hour, minute, second);
|
|
92
|
+
if (clock)
|
|
93
|
+
return fail('not_a_real_time', `"${value}" is not a real time: ${clock}.`);
|
|
94
|
+
const date = new Date(value);
|
|
95
|
+
if (Number.isNaN(date.getTime())) {
|
|
96
|
+
return fail('not_a_date', `"${value}" is not a date.`);
|
|
97
|
+
}
|
|
98
|
+
return { ok: true, date, iso: date.toISOString() };
|
|
99
|
+
}
|
|
100
|
+
const zoneless = ZONELESS.exec(value);
|
|
101
|
+
if (zoneless) {
|
|
102
|
+
return fail('no_zone', `"${value}" has no time zone, so it could mean a different moment on each machine. ` +
|
|
103
|
+
`Add Z if it is UTC, or convert it to UTC first.`);
|
|
104
|
+
}
|
|
105
|
+
const offset = OFFSET.exec(value);
|
|
106
|
+
if (offset) {
|
|
107
|
+
const [year, month, day, hour, minute] = offset.slice(1, 6).map(Number);
|
|
108
|
+
const second = Number(offset[6] ?? 0);
|
|
109
|
+
const sign = offset[8] === '-' ? -1 : 1;
|
|
110
|
+
const shiftMinutes = sign * (Number(offset[9]) * 60 + Number(offset[10]));
|
|
111
|
+
let converted = '';
|
|
112
|
+
if (!calendarProblem(year, month, day) && !clockProblem(hour, minute, second)) {
|
|
113
|
+
const at = new Date(Date.UTC(year, month - 1, day, hour, minute, second) - shiftMinutes * 60_000);
|
|
114
|
+
converted = ` (${value} is ${at.toISOString().replace(/\.000Z$/, 'Z')})`;
|
|
115
|
+
}
|
|
116
|
+
return fail('numeric_offset', `Numeric offsets such as ${offset[8]}${offset[9]}:${offset[10]} are not accepted; convert the time to UTC and end it with Z${converted}.`);
|
|
117
|
+
}
|
|
118
|
+
const dateOnly = DATE_ONLY.exec(value);
|
|
119
|
+
if (dateOnly) {
|
|
120
|
+
const calendar = calendarProblem(Number(dateOnly[1]), Number(dateOnly[2]), Number(dateOnly[3]));
|
|
121
|
+
return fail(calendar ? 'not_a_real_date' : 'date_only', calendar
|
|
122
|
+
? `"${value}" is not a real date: ${calendar}.`
|
|
123
|
+
: `"${value}" is a date without a time; add a time of day, e.g. ${value}T09:00:00Z.`);
|
|
124
|
+
}
|
|
125
|
+
return fail('not_a_date', `"${value}" is not an ISO-8601 date and time.`);
|
|
126
|
+
}
|
|
127
|
+
/** `2027-02-02T09:00:00Z` - the instant in UTC, whole seconds when there are no milliseconds. */
|
|
128
|
+
function formatUtcInstant(date) {
|
|
129
|
+
return date.toISOString().replace(/\.000Z$/, 'Z');
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* `2027-02-02 01:00:00 PST` - the same instant on this machine's wall clock, with the
|
|
133
|
+
* zone NAMED. `toLocaleString()` printed neither the zone nor anything that mapped back
|
|
134
|
+
* to the instant that was typed (OSK-21505). `timeZone` is for tests and defaults to
|
|
135
|
+
* the machine's own.
|
|
136
|
+
*/
|
|
137
|
+
function formatLocalInstant(date, timeZone) {
|
|
138
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
139
|
+
...(timeZone ? { timeZone } : {}),
|
|
140
|
+
year: 'numeric',
|
|
141
|
+
month: '2-digit',
|
|
142
|
+
day: '2-digit',
|
|
143
|
+
hour: '2-digit',
|
|
144
|
+
minute: '2-digit',
|
|
145
|
+
second: '2-digit',
|
|
146
|
+
hourCycle: 'h23',
|
|
147
|
+
timeZoneName: 'short',
|
|
148
|
+
}).formatToParts(date);
|
|
149
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? '';
|
|
150
|
+
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}:${get('second')} ${get('timeZoneName')}`;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The instant for a person: UTC first (it is what was typed and what is stored), then the
|
|
154
|
+
* local reading with its zone name. A machine already on UTC gets the UTC form alone.
|
|
155
|
+
*/
|
|
156
|
+
function describeInstant(date, timeZone) {
|
|
157
|
+
const utc = formatUtcInstant(date);
|
|
158
|
+
const local = formatLocalInstant(date, timeZone);
|
|
159
|
+
if (/ (UTC|GMT)$/.test(local))
|
|
160
|
+
return utc;
|
|
161
|
+
return `${utc} (${local})`;
|
|
162
|
+
}
|
package/dist/lib/northstars.d.ts
CHANGED
|
@@ -11,6 +11,10 @@ export type { Northstar, NorthstarAnalysisMode, NorthstarExecutionMode, Northsta
|
|
|
11
11
|
export declare const NORTHSTAR_STATUSES: readonly ["active", "paused", "achieved", "archived"];
|
|
12
12
|
export declare const NORTHSTAR_PRIORITY_TIERS: readonly ["critical", "high", "medium", "low"];
|
|
13
13
|
export declare const NORTHSTAR_RECOMMENDATION_STATUSES: readonly ["open", "applied", "dismissed"];
|
|
14
|
+
export declare function intentRisk(intent: {
|
|
15
|
+
riskLevel?: string | null;
|
|
16
|
+
} | null | undefined): string;
|
|
17
|
+
export declare function shortReviewId(id: string): string;
|
|
14
18
|
/**
|
|
15
19
|
* What a configure that changed the allocation inputs (the run budget, the critical reserve, the
|
|
16
20
|
* cadence band) says it did to the linked commitments. Additive on the server: absent from an
|
|
@@ -52,6 +56,19 @@ export declare const northstarApi: {
|
|
|
52
56
|
}>;
|
|
53
57
|
decideIntent: (id: string, intentId: string, body: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
54
58
|
};
|
|
59
|
+
/**
|
|
60
|
+
* One review, from the id a person typed. The full id is asked for first, exactly as before. Only when
|
|
61
|
+
* the server has no review of that exact id is the typed text read as the start of one, among the
|
|
62
|
+
* northstar's newest 100 reviews: that is what the REVIEW column of `intents list` prints
|
|
63
|
+
* (`shortReviewId`), so the table's own output can be pasted back. A prefix of several reviews is
|
|
64
|
+
* reported, not guessed; one of none, or of a review older than the newest 100, raises the server's
|
|
65
|
+
* original "not found". Both ids are in `--json`.
|
|
66
|
+
*/
|
|
67
|
+
export declare function lookupReview(northstarId: string, typed: string): Promise<{
|
|
68
|
+
review: NorthstarReviewRecord;
|
|
69
|
+
} | {
|
|
70
|
+
ambiguous: string[];
|
|
71
|
+
}>;
|
|
55
72
|
/** ms → a short human duration, for a table cell that must stay narrow. */
|
|
56
73
|
export declare function humanMs(ms?: number): string;
|
|
57
74
|
export declare function shortDate(value?: string): string;
|
|
@@ -166,6 +183,32 @@ export declare function describeReviewAllocations(result: {
|
|
|
166
183
|
allocations?: unknown;
|
|
167
184
|
attention?: unknown;
|
|
168
185
|
}): string;
|
|
186
|
+
/**
|
|
187
|
+
* The sentence for a portfolio run budget smaller than the number of linked active commitments
|
|
188
|
+
* (OSK-21334): each still gets the floor of 1 run/day, so the ceilings add up to more than the
|
|
189
|
+
* budget. The server derives it once (`portfolioRunBudgetFloor.js`) and carries it, when it
|
|
190
|
+
* applies, on the `governance show` read, the `review-now` result and the effective policy's
|
|
191
|
+
* `portfolio`; every one of those three prints THIS sentence, so none composes its own and they
|
|
192
|
+
* cannot disagree. Absent from an older server, and from a budget that covers every commitment:
|
|
193
|
+
* no line, never a guess.
|
|
194
|
+
*
|
|
195
|
+
* Takes the carried field as it arrives (`unknown`), because two of the three callers read it off
|
|
196
|
+
* an untyped body.
|
|
197
|
+
*/
|
|
198
|
+
export declare function runBudgetFloorNote(value: unknown): string | undefined;
|
|
199
|
+
/**
|
|
200
|
+
* What `critical_commitment` on the always-ask list does and does not do (OSK-21333).
|
|
201
|
+
*
|
|
202
|
+
* The list governs PROMOTION to critical (`critical_promotion`): remove the class and a bounded_auto
|
|
203
|
+
* review applies a small promotion itself. It does not govern the way back: a demotion away from
|
|
204
|
+
* critical is held for a person in `bounded_auto` whatever the list says (`critical_demotion`,
|
|
205
|
+
* `NorthstarGovernancePolicy.classifyIntent`: the demotion rule never reads the list). The behaviour
|
|
206
|
+
* is pinned by `NorthstarGovernancePolicy.spec.js` ("critical_commitment off the approval list"),
|
|
207
|
+
* the words by `cli/src/commands/northstars/__tests__/critical-approval-help.spec.ts`. The flag help
|
|
208
|
+
* and the `Approval for` line of `governance show` both read this one sentence, so neither can
|
|
209
|
+
* drift from the other.
|
|
210
|
+
*/
|
|
211
|
+
export declare const CRITICAL_COMMITMENT_APPROVAL_NOTE = "critical_commitment governs promotion to critical only; demotion away from critical (critical_demotion) is always held for a person, whatever this list says";
|
|
169
212
|
export declare function renderGovernance(status: NorthstarGovernanceStatus, log: (line: string) => void): void;
|
|
170
213
|
/**
|
|
171
214
|
* The automatic-change budget line.
|
package/dist/lib/northstars.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.northstarWideFlag = exports.ALLOCATION_INPUT_FLAGS = exports.NORTHSTAR_LIST_DEFAULT_LIMIT = exports.NORTHSTAR_REASON_WORDS = exports.northstarApi = exports.NORTHSTAR_RECOMMENDATION_STATUSES = exports.NORTHSTAR_PRIORITY_TIERS = exports.NORTHSTAR_STATUSES = exports.NORTHSTAR_REVIEW_STATUSES = exports.NORTHSTAR_MANAGEMENT_INTENT_KINDS = exports.NORTHSTAR_INTENT_VERDICTS = exports.NORTHSTAR_EXECUTION_MODES = exports.NORTHSTAR_APPROVAL_TRIGGERS = exports.NORTHSTAR_ANALYSIS_MODES = void 0;
|
|
3
|
+
exports.northstarWideFlag = exports.CRITICAL_COMMITMENT_APPROVAL_NOTE = exports.ALLOCATION_INPUT_FLAGS = exports.NORTHSTAR_LIST_DEFAULT_LIMIT = exports.NORTHSTAR_REASON_WORDS = exports.northstarApi = exports.NORTHSTAR_RECOMMENDATION_STATUSES = exports.NORTHSTAR_PRIORITY_TIERS = exports.NORTHSTAR_STATUSES = exports.NORTHSTAR_REVIEW_STATUSES = exports.NORTHSTAR_MANAGEMENT_INTENT_KINDS = exports.NORTHSTAR_INTENT_VERDICTS = exports.NORTHSTAR_EXECUTION_MODES = exports.NORTHSTAR_APPROVAL_TRIGGERS = exports.NORTHSTAR_ANALYSIS_MODES = void 0;
|
|
4
|
+
exports.intentRisk = intentRisk;
|
|
5
|
+
exports.shortReviewId = shortReviewId;
|
|
6
|
+
exports.lookupReview = lookupReview;
|
|
4
7
|
exports.humanMs = humanMs;
|
|
5
8
|
exports.shortDate = shortDate;
|
|
6
9
|
exports.describeNorthstarReason = describeNorthstarReason;
|
|
@@ -15,6 +18,7 @@ exports.reviewerReadiness = reviewerReadiness;
|
|
|
15
18
|
exports.readAllocationRefresh = readAllocationRefresh;
|
|
16
19
|
exports.describeAllocationRefresh = describeAllocationRefresh;
|
|
17
20
|
exports.describeReviewAllocations = describeReviewAllocations;
|
|
21
|
+
exports.runBudgetFloorNote = runBudgetFloorNote;
|
|
18
22
|
exports.renderGovernance = renderGovernance;
|
|
19
23
|
exports.describeAutoBudget = describeAutoBudget;
|
|
20
24
|
exports.reviewNote = reviewNote;
|
|
@@ -67,6 +71,37 @@ exports.NORTHSTAR_STATUSES = [
|
|
|
67
71
|
];
|
|
68
72
|
exports.NORTHSTAR_PRIORITY_TIERS = ['critical', 'high', 'medium', 'low'];
|
|
69
73
|
exports.NORTHSTAR_RECOMMENDATION_STATUSES = ['open', 'applied', 'dismissed'];
|
|
74
|
+
/* --------------------------------------------------------------------------
|
|
75
|
+
* Which risk a person sees
|
|
76
|
+
*
|
|
77
|
+
* Two different things carry a `riskLevel` and they are NOT one number. A recommendation
|
|
78
|
+
* (`northstars recommendations`) is rated by the reviewer when it recommends; an INTENT
|
|
79
|
+
* (`northstars intents`, `proposals show`) is rated by the governance policy for the change it
|
|
80
|
+
* would make, and that is the rating the policy gates on, records and sends in the
|
|
81
|
+
* awaiting-approval notice. They differ on purpose: a pause is recommended as `low` (it is the
|
|
82
|
+
* lowest-priority commitment over the cap) and is a `high`-risk change to apply. Printed side by
|
|
83
|
+
* side under one word they read as a contradiction (OSK-21192), so:
|
|
84
|
+
*
|
|
85
|
+
* - every surface that shows an INTENT shows the intent's own rating, labelled `risk`;
|
|
86
|
+
* - a recommendation's rating is labelled `recommendation risk`, wherever it is shown.
|
|
87
|
+
*
|
|
88
|
+
* A rating the server did not send is `unknown`, never `low`: an absent rating is not a safe one.
|
|
89
|
+
* -------------------------------------------------------------------------- */
|
|
90
|
+
function intentRisk(intent) {
|
|
91
|
+
return intent?.riskLevel || 'unknown';
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* A review id short enough for a table: `nsrev_3727b5959f436d29b69c1832` is `nsrev_3727b595`. The
|
|
95
|
+
* full id stays in `--json`. `reviews <northstar> --review` accepts the short form too
|
|
96
|
+
* (`resolveReviewRef`); an id that does not look like a prefix and a long hex run is left as it is.
|
|
97
|
+
*/
|
|
98
|
+
const SHORT_ID_HEX = 8;
|
|
99
|
+
function shortReviewId(id) {
|
|
100
|
+
const match = /^([a-z]+_)([0-9a-f]+)$/i.exec(id);
|
|
101
|
+
return match && match[2].length > SHORT_ID_HEX
|
|
102
|
+
? `${match[1]}${match[2].slice(0, SHORT_ID_HEX)}`
|
|
103
|
+
: id;
|
|
104
|
+
}
|
|
70
105
|
const base = (id) => `/api/northstars/${encodeURIComponent(id)}`;
|
|
71
106
|
exports.northstarApi = {
|
|
72
107
|
/* --- portfolio CRUD ---------------------------------------------------- */
|
|
@@ -92,6 +127,30 @@ exports.northstarApi = {
|
|
|
92
127
|
intents: (id, query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/intents`, query)),
|
|
93
128
|
decideIntent: (id, intentId, body) => data_provider_1.request.post(`${base(id)}/intents/${encodeURIComponent(intentId)}/decide`, body),
|
|
94
129
|
};
|
|
130
|
+
/**
|
|
131
|
+
* One review, from the id a person typed. The full id is asked for first, exactly as before. Only when
|
|
132
|
+
* the server has no review of that exact id is the typed text read as the start of one, among the
|
|
133
|
+
* northstar's newest 100 reviews: that is what the REVIEW column of `intents list` prints
|
|
134
|
+
* (`shortReviewId`), so the table's own output can be pasted back. A prefix of several reviews is
|
|
135
|
+
* reported, not guessed; one of none, or of a review older than the newest 100, raises the server's
|
|
136
|
+
* original "not found". Both ids are in `--json`.
|
|
137
|
+
*/
|
|
138
|
+
async function lookupReview(northstarId, typed) {
|
|
139
|
+
try {
|
|
140
|
+
return { review: await exports.northstarApi.review(northstarId, typed) };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error?.status !== 404 || !typed.trim())
|
|
144
|
+
throw error;
|
|
145
|
+
const listed = await exports.northstarApi.reviews(northstarId, { limit: 100 });
|
|
146
|
+
const matches = (listed.data ?? []).filter((review) => review.id.startsWith(typed.trim()));
|
|
147
|
+
if (matches.length === 0)
|
|
148
|
+
throw error;
|
|
149
|
+
if (matches.length > 1)
|
|
150
|
+
return { ambiguous: matches.map((review) => review.id) };
|
|
151
|
+
return { review: await exports.northstarApi.review(northstarId, matches[0].id) };
|
|
152
|
+
}
|
|
153
|
+
}
|
|
95
154
|
/** ms → a short human duration, for a table cell that must stay narrow. */
|
|
96
155
|
function humanMs(ms) {
|
|
97
156
|
if (!Number.isFinite(Number(ms)))
|
|
@@ -171,7 +230,9 @@ exports.NORTHSTAR_REASON_WORDS = {
|
|
|
171
230
|
// Why a decision (approve / reject) was refused or did not take effect.
|
|
172
231
|
commitment_version_drift: 'the commitment changed after this was proposed',
|
|
173
232
|
northstar_version_drift: 'the northstar changed after this was proposed',
|
|
174
|
-
|
|
233
|
+
// Two ways to the same ending: a newer review asks the same question again, or a concurrent review
|
|
234
|
+
// is already making (or has made) this very change. Either way another review has it.
|
|
235
|
+
superseded: 'another review already proposed or made the same change',
|
|
175
236
|
recommendation_dismissed: 'its recommendation was dismissed',
|
|
176
237
|
intent_not_found: "no such intent in this northstar's 50 most recent reviews",
|
|
177
238
|
review_not_found: 'no such review on this northstar',
|
|
@@ -460,7 +521,9 @@ function renderRecommendationList(rows, log) {
|
|
|
460
521
|
{ key: 'kind', header: 'KIND' },
|
|
461
522
|
{ key: 'target', header: 'TARGET' },
|
|
462
523
|
{ key: 'status', header: 'STATUS' },
|
|
463
|
-
|
|
524
|
+
// The reviewer's rating of the recommendation. The intent made from it is rated separately
|
|
525
|
+
// (`intents list` RISK) and may be higher; see `intentRisk`.
|
|
526
|
+
{ key: 'risk', header: 'RECOMMENDATION RISK' },
|
|
464
527
|
{ key: 'reason', header: 'WHY', maxWidth: 46 },
|
|
465
528
|
], log);
|
|
466
529
|
}
|
|
@@ -555,6 +618,37 @@ function describeReviewAllocations(result) {
|
|
|
555
618
|
}
|
|
556
619
|
return 'Allocations already up to date: no commitment needed a write';
|
|
557
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* The sentence for a portfolio run budget smaller than the number of linked active commitments
|
|
623
|
+
* (OSK-21334): each still gets the floor of 1 run/day, so the ceilings add up to more than the
|
|
624
|
+
* budget. The server derives it once (`portfolioRunBudgetFloor.js`) and carries it, when it
|
|
625
|
+
* applies, on the `governance show` read, the `review-now` result and the effective policy's
|
|
626
|
+
* `portfolio`; every one of those three prints THIS sentence, so none composes its own and they
|
|
627
|
+
* cannot disagree. Absent from an older server, and from a budget that covers every commitment:
|
|
628
|
+
* no line, never a guess.
|
|
629
|
+
*
|
|
630
|
+
* Takes the carried field as it arrives (`unknown`), because two of the three callers read it off
|
|
631
|
+
* an untyped body.
|
|
632
|
+
*/
|
|
633
|
+
function runBudgetFloorNote(value) {
|
|
634
|
+
const floor = value;
|
|
635
|
+
if (!floor || floor.code !== 'run_budget_below_active_commitments')
|
|
636
|
+
return undefined;
|
|
637
|
+
return typeof floor.message === 'string' && floor.message ? floor.message : undefined;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* What `critical_commitment` on the always-ask list does and does not do (OSK-21333).
|
|
641
|
+
*
|
|
642
|
+
* The list governs PROMOTION to critical (`critical_promotion`): remove the class and a bounded_auto
|
|
643
|
+
* review applies a small promotion itself. It does not govern the way back: a demotion away from
|
|
644
|
+
* critical is held for a person in `bounded_auto` whatever the list says (`critical_demotion`,
|
|
645
|
+
* `NorthstarGovernancePolicy.classifyIntent`: the demotion rule never reads the list). The behaviour
|
|
646
|
+
* is pinned by `NorthstarGovernancePolicy.spec.js` ("critical_commitment off the approval list"),
|
|
647
|
+
* the words by `cli/src/commands/northstars/__tests__/critical-approval-help.spec.ts`. The flag help
|
|
648
|
+
* and the `Approval for` line of `governance show` both read this one sentence, so neither can
|
|
649
|
+
* drift from the other.
|
|
650
|
+
*/
|
|
651
|
+
exports.CRITICAL_COMMITMENT_APPROVAL_NOTE = 'critical_commitment governs promotion to critical only; demotion away from critical (critical_demotion) is always held for a person, whatever this list says';
|
|
558
652
|
function renderGovernance(status, log) {
|
|
559
653
|
const g = status.governance;
|
|
560
654
|
log(`Northstar ${status.northstarId} (v${status.northstarVersion}, ${status.status})`);
|
|
@@ -576,6 +670,9 @@ function renderGovernance(status, log) {
|
|
|
576
670
|
}
|
|
577
671
|
log(`Approval for ${g.requireApprovalFor.join(', ') ||
|
|
578
672
|
'none — no change class is always held for a person (the execution mode still decides)'}`);
|
|
673
|
+
// True whatever the list holds, so it is printed for an empty one too: removing
|
|
674
|
+
// `critical_commitment` frees promotion, and never the way back.
|
|
675
|
+
log(` ${exports.CRITICAL_COMMITMENT_APPROVAL_NOTE}.`);
|
|
579
676
|
if (g.requireApprovalFor.length === 0) {
|
|
580
677
|
log(' ! No change class is on the always-ask list, so the execution mode alone decides what waits for you.');
|
|
581
678
|
}
|
|
@@ -583,6 +680,10 @@ function renderGovernance(status, log) {
|
|
|
583
680
|
const eb = status.executionBudget ?? {};
|
|
584
681
|
log(`Exec budget action runs/day ${notSet(eb.maxRunsPerDay)} · concurrent ${eb.maxConcurrentRuns == null ? 'not set' : `${eb.maxConcurrentRuns} (recorded, not enforced)`} · critical reserve ${notSet(eb.reserveWeightForCritical)}`);
|
|
585
682
|
log(`Observation runs/day ${notSet(eb.maxObservationRunsPerDay)} · daily budgets reset at 00:00 UTC`);
|
|
683
|
+
// A budget below the number of active commitments is still given to each at the floor of 1/day.
|
|
684
|
+
const floorNote = runBudgetFloorNote(status.runBudgetFloor);
|
|
685
|
+
if (floorNote)
|
|
686
|
+
log(` ! ${floorNote}`);
|
|
586
687
|
log('');
|
|
587
688
|
const trigger = status.reviewTrigger;
|
|
588
689
|
log(trigger.scheduled
|
|
@@ -734,11 +835,18 @@ function renderIntentList(intents, log, options = {}) {
|
|
|
734
835
|
log('No management intents.');
|
|
735
836
|
return;
|
|
736
837
|
}
|
|
838
|
+
// The flattened list (`intents list`) carries the review each intent came from; one review's own
|
|
839
|
+
// intents (`reviews --review`, `review-now`) all share the review the command already names, so a
|
|
840
|
+
// column of one repeated value would only push WHY off the screen.
|
|
841
|
+
const withReview = intents.some((intent) => Boolean(intent.reviewId));
|
|
737
842
|
const rows = intents.map((intent) => ({
|
|
738
843
|
id: intent.id,
|
|
844
|
+
review: intent.reviewId ? shortReviewId(intent.reviewId) : '—',
|
|
739
845
|
kind: intent.kind,
|
|
740
846
|
target: intent.targetCommitmentId ?? '—',
|
|
741
847
|
verdict: intent.verdict,
|
|
848
|
+
// The intent's OWN rating (see `intentRisk`), never its recommendation's.
|
|
849
|
+
risk: intentRisk(intent),
|
|
742
850
|
// No sentence from the governor: the policy code in words (an unknown code stays as it is).
|
|
743
851
|
why: intent.reason ?? (intent.verdictReason ? describeNorthstarReason(intent.verdictReason) : '—'),
|
|
744
852
|
code: intent.verdictReason ?? '',
|
|
@@ -746,28 +854,36 @@ function renderIntentList(intents, log, options = {}) {
|
|
|
746
854
|
}));
|
|
747
855
|
const headers = {
|
|
748
856
|
id: 'INTENT',
|
|
857
|
+
review: 'REVIEW',
|
|
749
858
|
kind: 'KIND',
|
|
750
859
|
target: 'COMMITMENT',
|
|
751
860
|
verdict: 'VERDICT',
|
|
861
|
+
risk: 'RISK',
|
|
752
862
|
source: 'SOURCE',
|
|
753
863
|
};
|
|
754
|
-
const fixedKeys =
|
|
864
|
+
const fixedKeys = (withReview
|
|
865
|
+
? ['id', 'review', 'kind', 'target', 'verdict', 'risk', 'source']
|
|
866
|
+
: ['id', 'kind', 'target', 'verdict', 'risk', 'source']);
|
|
755
867
|
const fixedWidth = fixedKeys.reduce((sum, key) => sum + Math.max((0, format_1.displayWidth)(headers[key]), ...rows.map((row) => (0, format_1.displayWidth)(row[key]))), 0);
|
|
756
868
|
const terminal = options.columns ?? process.stdout.columns ?? INTENT_TABLE_DEFAULT_COLUMNS;
|
|
757
|
-
//
|
|
758
|
-
const whyBudget = Math.max(INTENT_WHY_MIN_WIDTH, terminal - fixedWidth -
|
|
869
|
+
// Two spaces separate every fixed column from the next, and the last from WHY.
|
|
870
|
+
const whyBudget = Math.max(INTENT_WHY_MIN_WIDTH, terminal - fixedWidth - fixedKeys.length * 2);
|
|
759
871
|
(0, format_1.renderTable)(rows.map((row) => ({
|
|
760
872
|
id: row.id,
|
|
873
|
+
review: row.review,
|
|
761
874
|
kind: row.kind,
|
|
762
875
|
target: row.target,
|
|
763
876
|
verdict: row.verdict,
|
|
877
|
+
risk: row.risk,
|
|
764
878
|
source: row.source,
|
|
765
879
|
why: whyCell(row, options.wide === true, whyBudget),
|
|
766
880
|
})), [
|
|
767
881
|
{ key: 'id', header: headers.id },
|
|
882
|
+
...(withReview ? [{ key: 'review', header: headers.review }] : []),
|
|
768
883
|
{ key: 'kind', header: headers.kind },
|
|
769
884
|
{ key: 'target', header: headers.target },
|
|
770
885
|
{ key: 'verdict', header: headers.verdict },
|
|
886
|
+
{ key: 'risk', header: headers.risk },
|
|
771
887
|
{ key: 'source', header: headers.source },
|
|
772
888
|
{ key: 'why', header: 'WHY' },
|
|
773
889
|
], log);
|
package/dist/lib/proposals.js
CHANGED
|
@@ -153,11 +153,15 @@ function renderNorthstarIntentView(view) {
|
|
|
153
153
|
return null;
|
|
154
154
|
const context = view.decisionContext;
|
|
155
155
|
const verdict = view.sourceStatus;
|
|
156
|
-
|
|
156
|
+
// The intent's own rating (`intentRisk`), the one `northstars intents list` prints under RISK.
|
|
157
|
+
const risk = (0, northstars_1.intentRisk)({ riskLevel: context.riskLevel ?? context.snapshot.riskLevel });
|
|
157
158
|
const hasTarget = Boolean(context.snapshot.targetCommitmentId || view.commitmentId);
|
|
158
159
|
const lines = [proposalTitleFor(view), ''];
|
|
159
160
|
lines.push(`Proposal ${view.id}`);
|
|
160
|
-
lines.push(`Kind ${context.snapshot.kind}
|
|
161
|
+
lines.push(`Kind ${context.snapshot.kind}`);
|
|
162
|
+
// Said once, here, because this is where a person who saw a lower "recommendation risk" on the
|
|
163
|
+
// recommendation this came from looks for the reason the two differ.
|
|
164
|
+
lines.push(`Risk ${risk} (rated for this change; the recommendation behind it has its own "recommendation risk")`);
|
|
161
165
|
lines.push(`Target ${hasTarget ? intentTarget(view) : 'none (a new commitment)'}`);
|
|
162
166
|
if (context.reason)
|
|
163
167
|
lines.push(`Why ${context.reason}`);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/store/artifacts/skill/:entityId/releases` — the seller's own
|
|
3
|
+
* publication history for one skill, newest release first.
|
|
4
|
+
*
|
|
5
|
+
* It answers the question a headless seller has after `store artifacts release`:
|
|
6
|
+
* where did my releases land, and did a curator admit or refuse one (with the
|
|
7
|
+
* note they left)? The source snapshot and its private storage references are
|
|
8
|
+
* never in this projection. One release in full is `store artifacts receipt`.
|
|
9
|
+
*
|
|
10
|
+
* The route is written out at its own call site rather than composed from a
|
|
11
|
+
* builder, like the rest of the Store CLI: the parity checker reads the LITERAL
|
|
12
|
+
* at the call site.
|
|
13
|
+
*/
|
|
14
|
+
export type StoreArtifactReleaseSummary = {
|
|
15
|
+
releaseId: string;
|
|
16
|
+
releaseNumber: number;
|
|
17
|
+
versionLabel?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
admission?: {
|
|
20
|
+
state?: string;
|
|
21
|
+
review?: {
|
|
22
|
+
note?: string;
|
|
23
|
+
decidedAt?: string;
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
createdAt?: string;
|
|
27
|
+
updatedAt?: string;
|
|
28
|
+
publishedAt?: string;
|
|
29
|
+
};
|
|
30
|
+
export type StoreArtifactReleaseHistory = {
|
|
31
|
+
data: StoreArtifactReleaseSummary[];
|
|
32
|
+
/** Pass as `before` for the next (older) page; null when this is the last. */
|
|
33
|
+
nextBefore: number | null;
|
|
34
|
+
};
|
|
35
|
+
/** `GET /artifacts/skill/:entityId/releases` — `limit` 1-50 (default 10), `before` a release number. */
|
|
36
|
+
export declare function listSkillArtifactReleases(entityId: string, params?: {
|
|
37
|
+
limit?: number;
|
|
38
|
+
before?: number;
|
|
39
|
+
}): Promise<StoreArtifactReleaseHistory>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.listSkillArtifactReleases = listSkillArtifactReleases;
|
|
4
|
+
const api_fetch_1 = require("./api-fetch");
|
|
5
|
+
const triggers_1 = require("./triggers");
|
|
6
|
+
/** `GET /artifacts/skill/:entityId/releases` — `limit` 1-50 (default 10), `before` a release number. */
|
|
7
|
+
async function listSkillArtifactReleases(entityId, params = {}) {
|
|
8
|
+
return (0, api_fetch_1.apiFetch)((0, triggers_1.withQuery)(`/api/store/artifacts/skill/${encodeURIComponent(entityId)}/releases`, {
|
|
9
|
+
limit: params.limit,
|
|
10
|
+
before: params.before,
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `/api/store/library/items` — the Skill, Action and Service copies the signed-in
|
|
3
|
+
* buyer holds in the active workspace, with whether each copy can still be used.
|
|
4
|
+
*
|
|
5
|
+
* Agent purchases are NOT here: `skrr store installs` reads those. This is the
|
|
6
|
+
* other three artifact kinds, which the web Library page lists and which had no
|
|
7
|
+
* headless read at all.
|
|
8
|
+
*
|
|
9
|
+
* The route is written out at its own call site rather than composed from a
|
|
10
|
+
* builder, like the rest of the Store CLI: the parity checker reads the LITERAL
|
|
11
|
+
* at the call site.
|
|
12
|
+
*/
|
|
13
|
+
/** One bought copy. `copyAvailable` is false once the copy is suspended, revoked or uninstalled. */
|
|
14
|
+
export type StoreLibraryItem = {
|
|
15
|
+
/** The listing or release the buyer bought. */
|
|
16
|
+
sourceId: string;
|
|
17
|
+
/** The buyer's own installed copy; null while fulfillment has not produced one. */
|
|
18
|
+
copyId: string | null;
|
|
19
|
+
name: string | null;
|
|
20
|
+
copyAvailable: boolean;
|
|
21
|
+
/** The Agent the copy was installed onto (Skills and Actions only). */
|
|
22
|
+
targetAgentId?: string | null;
|
|
23
|
+
entitlementState?: string;
|
|
24
|
+
fulfillmentState?: string;
|
|
25
|
+
purchasedAt?: string;
|
|
26
|
+
};
|
|
27
|
+
export type StoreLibraryItems = {
|
|
28
|
+
skills: StoreLibraryItem[];
|
|
29
|
+
actions: StoreLibraryItem[];
|
|
30
|
+
services: StoreLibraryItem[];
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* `GET /library/items`. The workspace is required by the server (400
|
|
34
|
+
* `A workspace is required` without one), so it travels as the explicit
|
|
35
|
+
* header, the way `store installs` sends it.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getStoreLibraryItems(workspaceId?: string): Promise<StoreLibraryItems>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getStoreLibraryItems = getStoreLibraryItems;
|
|
4
|
+
const api_fetch_1 = require("./api-fetch");
|
|
5
|
+
/**
|
|
6
|
+
* `GET /library/items`. The workspace is required by the server (400
|
|
7
|
+
* `A workspace is required` without one), so it travels as the explicit
|
|
8
|
+
* header, the way `store installs` sends it.
|
|
9
|
+
*/
|
|
10
|
+
async function getStoreLibraryItems(workspaceId) {
|
|
11
|
+
return (0, api_fetch_1.apiFetch)('/api/store/library/items', workspaceId ? { headers: { 'X-Workspace-Id': workspaceId } } : undefined);
|
|
12
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `skrr tasks list --calendar` tells a person about the answer it printed.
|
|
3
|
+
*
|
|
4
|
+
* The calendar read used to end with "Note: calendar occurrence expansion reached its safety cap."
|
|
5
|
+
* whatever had been cut, and "No calendar occurrences." whatever had been missed (OSK-21382,
|
|
6
|
+
* OSK-21510): no limit, no cutoff, no Task, no remedy, and the server's warning codes were never
|
|
7
|
+
* shown. The server now says which bounds it reached (`truncations`) and which access warnings
|
|
8
|
+
* apply; this words them.
|
|
9
|
+
*/
|
|
10
|
+
import type { TaskCalendarResponse, TaskCalendarTruncation } from '@skrr-ai/data-provider';
|
|
11
|
+
export declare function describeCalendarTruncation(truncation: TaskCalendarTruncation): string;
|
|
12
|
+
export declare function describeCalendarWarning(code: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* The lines to print after a calendar answer: empty when the read was complete. A calendar that
|
|
15
|
+
* was cut short never reads as complete, and every cut says what it was.
|
|
16
|
+
*/
|
|
17
|
+
export declare function calendarNoticeLines(calendar: TaskCalendarResponse): string[];
|
|
18
|
+
/** The window that was actually read, in UTC, and what a date-only range stood for. */
|
|
19
|
+
export declare function calendarWindowLine(calendar: Pick<TaskCalendarResponse, 'from' | 'to'>): string;
|