@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.
Files changed (79) hide show
  1. package/dist/base-command.d.ts +12 -0
  2. package/dist/base-command.js +35 -0
  3. package/dist/commands/agenda/trigger/list.d.ts +2 -1
  4. package/dist/commands/agenda/trigger/list.js +55 -6
  5. package/dist/commands/agents/create.js +3 -1
  6. package/dist/commands/browser/persistent-profile.d.ts +22 -0
  7. package/dist/commands/browser/persistent-profile.js +46 -0
  8. package/dist/commands/commitments/create.js +3 -1
  9. package/dist/commands/commitments/effective-policy.js +6 -0
  10. package/dist/commands/commitments/explain.js +10 -1
  11. package/dist/commands/commitments/plan.d.ts +14 -0
  12. package/dist/commands/commitments/plan.js +32 -0
  13. package/dist/commands/commitments/preflight.d.ts +1 -1
  14. package/dist/commands/commitments/preflight.js +12 -4
  15. package/dist/commands/commitments/show.d.ts +9 -0
  16. package/dist/commands/commitments/show.js +14 -1
  17. package/dist/commands/compass/revisions.d.ts +6 -2
  18. package/dist/commands/compass/revisions.js +13 -5
  19. package/dist/commands/compass/tactic.d.ts +5 -0
  20. package/dist/commands/compass/tactic.js +14 -0
  21. package/dist/commands/followups/plans/create.js +1 -1
  22. package/dist/commands/harnesses/list.js +1 -1
  23. package/dist/commands/northstars/governance/configure.js +1 -1
  24. package/dist/commands/northstars/intents/decide.js +2 -3
  25. package/dist/commands/northstars/review-now.js +5 -0
  26. package/dist/commands/northstars/reviews.js +15 -3
  27. package/dist/commands/spaces/auto-executor/runs.d.ts +9 -1
  28. package/dist/commands/spaces/auto-executor/runs.js +18 -1
  29. package/dist/commands/spaces/heartbeats/create.js +7 -5
  30. package/dist/commands/spaces/heartbeats/update.js +5 -3
  31. package/dist/commands/store/artifacts/releases.d.ts +20 -0
  32. package/dist/commands/store/artifacts/releases.js +85 -0
  33. package/dist/commands/store/library.d.ts +14 -0
  34. package/dist/commands/store/library.js +71 -0
  35. package/dist/commands/tasks/list.d.ts +48 -0
  36. package/dist/commands/tasks/list.js +141 -9
  37. package/dist/commands/tasks/remind.js +23 -11
  38. package/dist/commands/tasks/retry-schedule.js +6 -3
  39. package/dist/commands/tasks/schedule.js +29 -7
  40. package/dist/commands/tasks/show.js +4 -0
  41. package/dist/lib/agent-trigger-listing.d.ts +26 -0
  42. package/dist/lib/agent-trigger-listing.js +63 -0
  43. package/dist/lib/agentic-stream.js +138 -41
  44. package/dist/lib/commitment-ledgers.js +3 -1
  45. package/dist/lib/commitments.d.ts +24 -0
  46. package/dist/lib/commitments.js +64 -4
  47. package/dist/lib/compasses.d.ts +2 -0
  48. package/dist/lib/iso-instant.d.ts +52 -0
  49. package/dist/lib/iso-instant.js +162 -0
  50. package/dist/lib/northstars.d.ts +43 -0
  51. package/dist/lib/northstars.js +122 -6
  52. package/dist/lib/proposals.js +6 -2
  53. package/dist/lib/store-artifact-releases.d.ts +39 -0
  54. package/dist/lib/store-artifact-releases.js +12 -0
  55. package/dist/lib/store-library.d.ts +37 -0
  56. package/dist/lib/store-library.js +12 -0
  57. package/dist/lib/task-calendar.d.ts +19 -0
  58. package/dist/lib/task-calendar.js +72 -0
  59. package/dist/lib/task-recurrence-outcome.js +3 -1
  60. package/dist/lib/task-recurrence-summary.d.ts +15 -0
  61. package/dist/lib/task-recurrence-summary.js +228 -0
  62. package/dist/lib/task-reminder.d.ts +28 -0
  63. package/dist/lib/task-reminder.js +50 -0
  64. package/dist/lib/triggers.d.ts +13 -2
  65. package/dist/lib/triggers.js +22 -12
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +11 -1
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.d.ts +123 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.js +160 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +17 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +11 -1
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.d.ts +123 -0
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.js +173 -0
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  77. package/dist/node_modules/@skrr-ai/data-provider/index.js +6755 -6672
  78. package/oclif.manifest.json +263 -29
  79. 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
+ }
@@ -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.
@@ -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
- superseded: 'a newer review proposed the same thing',
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
- { key: 'risk', header: 'RISK' },
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 = ['id', 'kind', 'target', 'verdict', 'source'];
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
- // Five gaps of two spaces separate the six columns.
758
- const whyBudget = Math.max(INTENT_WHY_MIN_WIDTH, terminal - fixedWidth - 10);
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);
@@ -153,11 +153,15 @@ function renderNorthstarIntentView(view) {
153
153
  return null;
154
154
  const context = view.decisionContext;
155
155
  const verdict = view.sourceStatus;
156
- const risk = context.riskLevel ?? context.snapshot.riskLevel ?? 'unknown';
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} · risk ${risk}`);
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;