@skrr-ai/cli 0.1.127 → 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 (165) hide show
  1. package/dist/base-command.d.ts +35 -0
  2. package/dist/base-command.js +107 -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/chat.js +7 -0
  6. package/dist/commands/agents/create.js +3 -1
  7. package/dist/commands/browser/persistent-profile.d.ts +22 -0
  8. package/dist/commands/browser/persistent-profile.js +46 -0
  9. package/dist/commands/commitments/action-history.js +1 -1
  10. package/dist/commands/commitments/cases.js +3 -3
  11. package/dist/commands/commitments/check-now.d.ts +10 -2
  12. package/dist/commands/commitments/check-now.js +53 -3
  13. package/dist/commands/commitments/create.d.ts +6 -14
  14. package/dist/commands/commitments/create.js +64 -7
  15. package/dist/commands/commitments/doctor.js +3 -1
  16. package/dist/commands/commitments/effective-policy.d.ts +11 -1
  17. package/dist/commands/commitments/effective-policy.js +90 -4
  18. package/dist/commands/commitments/explain.js +10 -1
  19. package/dist/commands/commitments/hypotheses.js +12 -12
  20. package/dist/commands/commitments/next-tick.js +5 -0
  21. package/dist/commands/commitments/observations.js +18 -17
  22. package/dist/commands/commitments/pack/list.js +2 -1
  23. package/dist/commands/commitments/pack/plan.d.ts +6 -0
  24. package/dist/commands/commitments/pack/plan.js +19 -0
  25. package/dist/commands/commitments/plan.d.ts +14 -0
  26. package/dist/commands/commitments/plan.js +32 -0
  27. package/dist/commands/commitments/preflight.d.ts +20 -1
  28. package/dist/commands/commitments/preflight.js +118 -12
  29. package/dist/commands/commitments/show.d.ts +9 -0
  30. package/dist/commands/commitments/show.js +14 -1
  31. package/dist/commands/commitments/trigger-status.js +8 -0
  32. package/dist/commands/commitments/update.d.ts +1 -0
  33. package/dist/commands/commitments/update.js +45 -4
  34. package/dist/commands/commitments/wake-capabilities.js +13 -0
  35. package/dist/commands/compass/archive.js +4 -0
  36. package/dist/commands/compass/commitments.js +15 -1
  37. package/dist/commands/compass/create.js +11 -3
  38. package/dist/commands/compass/effective-policy.d.ts +1 -0
  39. package/dist/commands/compass/effective-policy.js +8 -7
  40. package/dist/commands/compass/revert.d.ts +8 -0
  41. package/dist/commands/compass/revert.js +25 -1
  42. package/dist/commands/compass/revisions.d.ts +6 -0
  43. package/dist/commands/compass/revisions.js +26 -2
  44. package/dist/commands/compass/tactic.d.ts +10 -3
  45. package/dist/commands/compass/tactic.js +22 -7
  46. package/dist/commands/compass/update.js +19 -3
  47. package/dist/commands/followups/act.js +1 -1
  48. package/dist/commands/followups/cancel.js +13 -0
  49. package/dist/commands/followups/observability.js +20 -1
  50. package/dist/commands/followups/plans/create.js +12 -1
  51. package/dist/commands/followups/show.js +13 -0
  52. package/dist/commands/followups/watch.js +1 -1
  53. package/dist/commands/goals/planner-config.d.ts +11 -0
  54. package/dist/commands/goals/planner-config.js +16 -0
  55. package/dist/commands/harnesses/list.js +1 -1
  56. package/dist/commands/machines/update.d.ts +5 -3
  57. package/dist/commands/machines/update.js +70 -9
  58. package/dist/commands/northstars/create.js +3 -1
  59. package/dist/commands/northstars/governance/configure.d.ts +4 -0
  60. package/dist/commands/northstars/governance/configure.js +76 -10
  61. package/dist/commands/northstars/intents/decide.d.ts +6 -0
  62. package/dist/commands/northstars/intents/decide.js +68 -15
  63. package/dist/commands/northstars/intents/list.d.ts +2 -1
  64. package/dist/commands/northstars/intents/list.js +15 -5
  65. package/dist/commands/northstars/intents.d.ts +26 -0
  66. package/dist/commands/northstars/intents.js +49 -0
  67. package/dist/commands/northstars/list.d.ts +1 -0
  68. package/dist/commands/northstars/list.js +24 -3
  69. package/dist/commands/northstars/recommendations/list.d.ts +1 -1
  70. package/dist/commands/northstars/recommendations/list.js +3 -3
  71. package/dist/commands/northstars/recommendations.d.ts +19 -0
  72. package/dist/commands/northstars/recommendations.js +42 -0
  73. package/dist/commands/northstars/review-now.d.ts +1 -0
  74. package/dist/commands/northstars/review-now.js +15 -4
  75. package/dist/commands/northstars/reviews.d.ts +1 -0
  76. package/dist/commands/northstars/reviews.js +34 -6
  77. package/dist/commands/northstars/update.js +8 -1
  78. package/dist/commands/proposals/list.js +11 -2
  79. package/dist/commands/proposals/show.js +9 -1
  80. package/dist/commands/spaces/auto-executor/preview.d.ts +14 -1
  81. package/dist/commands/spaces/auto-executor/preview.js +35 -1
  82. package/dist/commands/spaces/auto-executor/run.d.ts +17 -0
  83. package/dist/commands/spaces/auto-executor/run.js +60 -2
  84. package/dist/commands/spaces/auto-executor/runs.d.ts +9 -1
  85. package/dist/commands/spaces/auto-executor/runs.js +18 -1
  86. package/dist/commands/spaces/files/upload.js +1 -2
  87. package/dist/commands/spaces/heartbeats/create.js +7 -5
  88. package/dist/commands/spaces/heartbeats/update.js +5 -3
  89. package/dist/commands/spaces/index.js +23 -0
  90. package/dist/commands/store/artifacts/releases.d.ts +20 -0
  91. package/dist/commands/store/artifacts/releases.js +85 -0
  92. package/dist/commands/store/library.d.ts +14 -0
  93. package/dist/commands/store/library.js +71 -0
  94. package/dist/commands/tasks/list.d.ts +48 -0
  95. package/dist/commands/tasks/list.js +141 -9
  96. package/dist/commands/tasks/remind.js +23 -11
  97. package/dist/commands/tasks/retry-schedule.js +6 -3
  98. package/dist/commands/tasks/schedule.js +29 -7
  99. package/dist/commands/tasks/show.js +4 -0
  100. package/dist/generated/agent-guides.json +69 -69
  101. package/dist/lib/agent-trigger-listing.d.ts +26 -0
  102. package/dist/lib/agent-trigger-listing.js +63 -0
  103. package/dist/lib/agentic-stream.d.ts +9 -0
  104. package/dist/lib/agentic-stream.js +152 -41
  105. package/dist/lib/command-miss.js +11 -2
  106. package/dist/lib/commitment-attention-copy.d.ts +96 -0
  107. package/dist/lib/commitment-attention-copy.js +209 -0
  108. package/dist/lib/commitment-ledgers.js +3 -1
  109. package/dist/lib/commitment-loop-status.d.ts +6 -0
  110. package/dist/lib/commitment-loop-status.js +16 -6
  111. package/dist/lib/commitments.d.ts +112 -0
  112. package/dist/lib/commitments.js +250 -2
  113. package/dist/lib/compass-body-help.d.ts +20 -0
  114. package/dist/lib/compass-body-help.js +61 -0
  115. package/dist/lib/compass-refusals.d.ts +35 -0
  116. package/dist/lib/compass-refusals.js +50 -0
  117. package/dist/lib/compasses.d.ts +82 -4
  118. package/dist/lib/compasses.js +232 -12
  119. package/dist/lib/followups.d.ts +22 -1
  120. package/dist/lib/followups.js +21 -1
  121. package/dist/lib/format.d.ts +11 -0
  122. package/dist/lib/format.js +29 -0
  123. package/dist/lib/initiatives.d.ts +9 -2
  124. package/dist/lib/initiatives.js +13 -2
  125. package/dist/lib/iso-instant.d.ts +52 -0
  126. package/dist/lib/iso-instant.js +162 -0
  127. package/dist/lib/machines.d.ts +14 -1
  128. package/dist/lib/machines.js +24 -0
  129. package/dist/lib/northstars.d.ts +201 -24
  130. package/dist/lib/northstars.js +655 -27
  131. package/dist/lib/proposal-decision-command.js +21 -1
  132. package/dist/lib/proposals.d.ts +26 -1
  133. package/dist/lib/proposals.js +167 -0
  134. package/dist/lib/rate-limit-retry.d.ts +5 -0
  135. package/dist/lib/rate-limit-retry.js +19 -0
  136. package/dist/lib/roles.d.ts +1 -1
  137. package/dist/lib/space-charter.js +9 -0
  138. package/dist/lib/store-artifact-releases.d.ts +39 -0
  139. package/dist/lib/store-artifact-releases.js +12 -0
  140. package/dist/lib/store-library.d.ts +37 -0
  141. package/dist/lib/store-library.js +12 -0
  142. package/dist/lib/task-asset-upload.js +1 -2
  143. package/dist/lib/task-calendar.d.ts +19 -0
  144. package/dist/lib/task-calendar.js +72 -0
  145. package/dist/lib/task-recurrence-outcome.js +3 -1
  146. package/dist/lib/task-recurrence-summary.d.ts +15 -0
  147. package/dist/lib/task-recurrence-summary.js +228 -0
  148. package/dist/lib/task-reminder.d.ts +28 -0
  149. package/dist/lib/task-reminder.js +50 -0
  150. package/dist/lib/triggers.d.ts +13 -2
  151. package/dist/lib/triggers.js +31 -13
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonWire.js +11 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.d.ts +123 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/executorStopReceipt.js +160 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +17 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonWire.js +11 -1
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.d.ts +123 -0
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/executorStopReceipt.js +173 -0
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +2 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  163. package/dist/node_modules/@skrr-ai/data-provider/index.js +6922 -6828
  164. package/oclif.manifest.json +612 -62
  165. package/package.json +4 -4
@@ -1,14 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- 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;
9
+ exports.describeNorthstarReason = describeNorthstarReason;
10
+ exports.describeDecisionRefusal = describeDecisionRefusal;
11
+ exports.describeDecisionOutcome = describeDecisionOutcome;
12
+ exports.listFooter = listFooter;
6
13
  exports.renderNorthstarList = renderNorthstarList;
14
+ exports.describeMaxActive = describeMaxActive;
7
15
  exports.renderNorthstar = renderNorthstar;
8
16
  exports.renderRecommendationList = renderRecommendationList;
17
+ exports.reviewerReadiness = reviewerReadiness;
18
+ exports.readAllocationRefresh = readAllocationRefresh;
19
+ exports.describeAllocationRefresh = describeAllocationRefresh;
20
+ exports.describeReviewAllocations = describeReviewAllocations;
21
+ exports.runBudgetFloorNote = runBudgetFloorNote;
9
22
  exports.renderGovernance = renderGovernance;
23
+ exports.describeAutoBudget = describeAutoBudget;
24
+ exports.reviewNote = reviewNote;
25
+ exports.describeReviewReason = describeReviewReason;
10
26
  exports.renderReviewList = renderReviewList;
11
27
  exports.renderIntentList = renderIntentList;
28
+ exports.intentBudgetNote = intentBudgetNote;
12
29
  /**
13
30
  * northstars.ts — request-based client for the Northstar portfolio layer
14
31
  * (`/api/northstars/*`) and its Portfolio Governor.
@@ -31,6 +48,7 @@ exports.renderIntentList = renderIntentList;
31
48
  * re-declared. The API validates against those exact strings; a second copy here
32
49
  * is how the CLI silently drifts from the server that rejects it.
33
50
  */
51
+ const core_1 = require("@oclif/core");
34
52
  const data_provider_1 = require("@skrr-ai/data-provider");
35
53
  Object.defineProperty(exports, "NORTHSTAR_ANALYSIS_MODES", { enumerable: true, get: function () { return data_provider_1.NORTHSTAR_ANALYSIS_MODES; } });
36
54
  Object.defineProperty(exports, "NORTHSTAR_APPROVAL_TRIGGERS", { enumerable: true, get: function () { return data_provider_1.NORTHSTAR_APPROVAL_TRIGGERS; } });
@@ -53,6 +71,37 @@ exports.NORTHSTAR_STATUSES = [
53
71
  ];
54
72
  exports.NORTHSTAR_PRIORITY_TIERS = ['critical', 'high', 'medium', 'low'];
55
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
+ }
56
105
  const base = (id) => `/api/northstars/${encodeURIComponent(id)}`;
57
106
  exports.northstarApi = {
58
107
  /* --- portfolio CRUD ---------------------------------------------------- */
@@ -78,6 +127,30 @@ exports.northstarApi = {
78
127
  intents: (id, query = {}) => data_provider_1.request.get((0, triggers_1.withQuery)(`${base(id)}/intents`, query)),
79
128
  decideIntent: (id, intentId, body) => data_provider_1.request.post(`${base(id)}/intents/${encodeURIComponent(intentId)}/decide`, body),
80
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
+ }
81
154
  /** ms → a short human duration, for a table cell that must stay narrow. */
82
155
  function humanMs(ms) {
83
156
  if (!Number.isFinite(Number(ms)))
@@ -96,6 +169,260 @@ function shortDate(value) {
96
169
  const date = new Date(value);
97
170
  return Number.isNaN(date.getTime()) ? '—' : date.toISOString().replace('T', ' ').slice(0, 16);
98
171
  }
172
+ /* --------------------------------------------------------------------------
173
+ * Reason codes in words
174
+ *
175
+ * The server refuses, skips and decides with machine codes
176
+ * (`reviewer_agent_not_always_on`, `northstar_version_drift`, ...). A code is
177
+ * for a script; a person reading a table or an error needs the sentence. This
178
+ * is the ONE place those sentences live, so a command never invents its own
179
+ * wording (the old governance line guessed "offline" for a reviewer that was
180
+ * simply not Always-On) and `--json` can carry the same text as `reasonText`.
181
+ *
182
+ * Entries are CLAUSES, lower-case with no trailing full stop, so a caller can
183
+ * put them after "agent turn not started:" or inside a longer sentence. A code
184
+ * with no entry is returned AS IT IS: an unknown code printed raw is honest,
185
+ * a guessed explanation is not.
186
+ * ------------------------------------------------------------------------ */
187
+ exports.NORTHSTAR_REASON_WORDS = {
188
+ // An agent turn that was not reserved for an agent_assisted review.
189
+ no_reviewer_agent_configured: 'no reviewer agent is set on this northstar',
190
+ northstar_paused: 'the northstar is paused',
191
+ northstar_achieved: 'the northstar is marked achieved',
192
+ northstar_archived: 'the northstar is archived',
193
+ analysis_mode_not_agent_assisted: 'analysis mode is deterministic, so no agent is asked',
194
+ managed_agent_missing: 'the reviewer agent does not exist or is not visible to you',
195
+ always_on_control_unavailable: "the reviewer agent's Always-On state could not be read just now",
196
+ always_on_disabled: 'Always-On is turned off for the reviewer agent',
197
+ always_on_suspended: 'Always-On is suspended for the reviewer agent',
198
+ always_on_stale_generation: "the reviewer agent's Always-On state changed while this ran",
199
+ reviewer_agent_foreign_owner: "the reviewer agent belongs to someone other than this northstar's owner",
200
+ reviewer_agent_not_always_on: 'the reviewer agent is not Always-On (it is not offline; turn Always-On on for it)',
201
+ run_already_reserved: 'an agent run was already reserved for this review',
202
+ envelope_build_failed: 'the review could not be packaged for the agent',
203
+ governance_run_reservation_failed: 'reserving the agent turn failed unexpectedly',
204
+ run_not_reserved: 'the run queue declined to reserve the agent turn',
205
+ enqueue_failed: 'the agent turn could not be queued',
206
+ agent_run_reserved: 'an agent turn is reserved; its results land asynchronously',
207
+ // Why an intent was recorded, held or refused by policy.
208
+ execution_mode_advisory: 'execution mode is advisory: recorded for you to read, nothing is changed',
209
+ execution_mode_approval: 'execution mode is approval: it waits for a person',
210
+ proposal_only: 'a proposal: the governor never applies it, a person does',
211
+ commitment_protected: "the commitment is on this northstar's protected list",
212
+ intent_kind_not_allowed: "this kind of intent is not in the northstar's allowed list",
213
+ critical_commitment_lifecycle: 'it changes a critical commitment, which always needs a person',
214
+ resume_requires_approval: 'resuming always needs a person',
215
+ budget_telemetry_unavailable: 'the automatic-change budget could not be read, so it fell back to approval',
216
+ critical_demotion: 'it would demote a critical commitment',
217
+ tier_change_too_large: 'the tier change is larger than one step',
218
+ weight_change_too_large: 'the weight change exceeds the allowed relative change',
219
+ bounds_increase_compute: 'it would buy more compute',
220
+ bounds_change_exceeds_max_relative_change: 'the bounds change exceeds the allowed relative change',
221
+ auto_pause_evidence_incomplete: 'the evidence for an automatic pause is incomplete',
222
+ no_auto_rule_for_kind: 'no automatic rule covers this kind of change',
223
+ auto_mutation_review_cap_reached: 'this review already made its maximum automatic changes',
224
+ auto_mutation_daily_cap_reached: "today's automatic-change cap is used up",
225
+ mutation_cooldown_active: 'the commitment was changed automatically too recently (cooldown)',
226
+ commitment_not_linked_to_northstar: 'the commitment is no longer linked to this northstar',
227
+ commitment_not_found: 'the commitment no longer exists',
228
+ approved_by_user: 'approved by a person',
229
+ rejected_by_user: 'rejected by a person',
230
+ // Why a decision (approve / reject) was refused or did not take effect.
231
+ commitment_version_drift: 'the commitment changed after this was proposed',
232
+ northstar_version_drift: 'the northstar changed after this was proposed',
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',
236
+ recommendation_dismissed: 'its recommendation was dismissed',
237
+ intent_not_found: "no such intent in this northstar's 50 most recent reviews",
238
+ review_not_found: 'no such review on this northstar',
239
+ northstar_not_found: 'no such northstar',
240
+ owner_approval_required: "only the northstar's owner can approve a proposal",
241
+ transition_lost: 'another decision landed first',
242
+ claim_lost: 'another process is already applying it',
243
+ mutation_error: 'the change failed part-way and was left for the repair pass',
244
+ proposal_kind_requires_manual_action: 'this kind of proposal is completed by hand',
245
+ };
246
+ /** A code in words; an unknown code comes back unchanged, an absent one says so. */
247
+ function describeNorthstarReason(code) {
248
+ if (!code)
249
+ return 'no reason was recorded';
250
+ return exports.NORTHSTAR_REASON_WORDS[code] ?? code;
251
+ }
252
+ function utcStamp(value) {
253
+ const stamp = shortDate(typeof value === 'string' ? value : undefined);
254
+ return stamp === '—' ? undefined : `${stamp} UTC`;
255
+ }
256
+ function stringField(source, key) {
257
+ const value = source?.[key];
258
+ return typeof value === 'string' && value.trim() ? value : undefined;
259
+ }
260
+ /**
261
+ * A refused decision, as one sentence with its next step.
262
+ *
263
+ * `body` is the server's refusal (`reason`, plus the context it adds: the two
264
+ * northstar versions, or when and by whom the intent was already decided).
265
+ */
266
+ function describeDecisionRefusal(body, ctx) {
267
+ const code = stringField(body, 'reason') ?? stringField(body, 'error') ?? 'unknown';
268
+ const bin = ctx.bin ?? 'skrr';
269
+ const reviewNow = `Run \`${bin} northstars review-now ${ctx.northstarId}\` for a fresh proposal.`;
270
+ const intents = `\`${bin} northstars intents list ${ctx.northstarId}\``;
271
+ const notApplied = ctx.decision === 'approve' ? 'the approval was not applied' : 'the rejection was not recorded';
272
+ if (code === 'northstar_version_drift') {
273
+ const proposed = body.proposedAtNorthstarVersion;
274
+ const current = body.currentNorthstarVersion;
275
+ const versions = typeof proposed === 'number' && typeof current === 'number'
276
+ ? ` (it was at v${proposed}, now v${current})`
277
+ : '';
278
+ return {
279
+ code,
280
+ text: `The northstar changed after this was proposed${versions}, so ${notApplied} and the intent was invalidated. ${reviewNow}`,
281
+ };
282
+ }
283
+ if (code === 'intent_not_pending') {
284
+ const verdict = stringField(body, 'verdict') ?? 'decided';
285
+ if (verdict === 'advisory') {
286
+ return {
287
+ code,
288
+ text: "This intent is advisory: this northstar's execution mode records it for you to read and does not ask for a decision. " +
289
+ `To get proposals you can approve, run \`${bin} northstars governance configure ${ctx.northstarId} --execution approval\` and review again.`,
290
+ };
291
+ }
292
+ if (verdict === 'applying') {
293
+ return {
294
+ code,
295
+ text: `This intent is being applied right now — nothing to do. Check its verdict with ${intents}.`,
296
+ };
297
+ }
298
+ // `invalidated` is the platform ending an intent (the northstar moved, a newer review replaced
299
+ // it, its recommendation was dismissed), never a person's decision: attributing it to "you"
300
+ // because your click was the one that found it out is false (OSK-21247). Say why instead.
301
+ if (verdict === 'invalidated') {
302
+ const why = stringField(body, 'verdictReason');
303
+ const when = utcStamp(body.decidedAt);
304
+ return {
305
+ code,
306
+ text: `This intent was already invalidated${why ? ` (${describeNorthstarReason(why)})` : ''}${when ? ` at ${when}` : ''} — nothing changed.`,
307
+ };
308
+ }
309
+ let by = '';
310
+ if (body.decidedByYou === true)
311
+ by = ' by you';
312
+ else if (body.decidedByYou === false)
313
+ by = ' by someone else';
314
+ const at = utcStamp(body.decidedAt);
315
+ return {
316
+ code,
317
+ text: `This intent was already ${verdict}${by}${at ? ` at ${at}` : ''} — nothing changed.`,
318
+ };
319
+ }
320
+ if (code === 'transition_lost') {
321
+ return {
322
+ code,
323
+ text: `Another decision on this intent landed at the same moment, so yours was not recorded. Run ${intents} to see how it ended.`,
324
+ };
325
+ }
326
+ if (code === 'intent_not_found') {
327
+ return {
328
+ code,
329
+ text: `No intent ${ctx.intentId} in this northstar's 50 most recent reviews. If it is older, pass --review <review-id>.`,
330
+ };
331
+ }
332
+ const words = describeNorthstarReason(code);
333
+ return { code, text: `${words.charAt(0).toUpperCase()}${words.slice(1)}.` };
334
+ }
335
+ /**
336
+ * What a decision that the server ACCEPTED (HTTP 200) actually did.
337
+ *
338
+ * One exit rule: an approval exits 0 only when the intent is `applied`, and a
339
+ * rejection only when it is `rejected`. An approval the version fence
340
+ * invalidated, or one whose change has not landed, used to print a line shaped
341
+ * like success and exit 0, so a script could not tell it from one that worked.
342
+ */
343
+ function describeDecisionOutcome(result, ctx) {
344
+ const bin = ctx.bin ?? 'skrr';
345
+ const verdict = stringField(result, 'verdict') ?? 'unknown';
346
+ const headline = `${ctx.intentId} → ${verdict}`;
347
+ const replayed = result.replayed === true;
348
+ const reviewNow = `Run \`${bin} northstars review-now ${ctx.northstarId}\` for a fresh proposal.`;
349
+ if (ctx.decision === 'reject') {
350
+ if (verdict === 'rejected') {
351
+ return {
352
+ headline,
353
+ exitCode: 0,
354
+ ...(replayed ? { text: 'Already rejected — nothing changed.' } : {}),
355
+ };
356
+ }
357
+ return {
358
+ headline,
359
+ exitCode: 1,
360
+ code: verdict,
361
+ text: `The intent was not rejected; its verdict is ${verdict}.`,
362
+ };
363
+ }
364
+ const continuation = result.continuation;
365
+ const applyResult = result.applyResult;
366
+ if (verdict === 'applied') {
367
+ const commitmentId = stringField(continuation, 'commitmentId');
368
+ const status = stringField(continuation, 'status');
369
+ let text;
370
+ if (status === 'draft_created') {
371
+ text = `Created draft commitment ${commitmentId ?? ''}`.trim();
372
+ text += '. Configure and activate it when you are ready.';
373
+ }
374
+ else if (status === 'executor_reassigned') {
375
+ text = `Reassigned the executor of ${commitmentId ?? 'the commitment'}.`;
376
+ }
377
+ if (replayed)
378
+ text = `Already applied — nothing changed.${text ? ` ${text}` : ''}`;
379
+ return { headline, exitCode: 0, ...(text ? { text } : {}) };
380
+ }
381
+ // Not applied. The reason is the apply step's, or a continuation's.
382
+ const code = stringField(applyResult, 'reason') ??
383
+ stringField(continuation, 'reason') ??
384
+ stringField(continuation, 'status');
385
+ let text;
386
+ if (code === 'commitment_version_drift' || code === 'commitment_not_found') {
387
+ const what = code === 'commitment_not_found'
388
+ ? 'The commitment no longer exists'
389
+ : 'The commitment changed after this was proposed';
390
+ text = `${what}, so the approval was not applied and the intent was invalidated. ${reviewNow}`;
391
+ }
392
+ else if (code) {
393
+ text = `Your approval is recorded, but nothing was applied: ${describeNorthstarReason(code)}.`;
394
+ }
395
+ else {
396
+ text = `Your approval is recorded, but the change has not landed yet (verdict: ${verdict}). It is retried automatically; check \`${bin} northstars intents list ${ctx.northstarId}\`.`;
397
+ }
398
+ return { headline, exitCode: 1, code: code ?? verdict, text };
399
+ }
400
+ /** What the server returned before `--limit` existed; sent explicitly so a page is never silently smaller. */
401
+ exports.NORTHSTAR_LIST_DEFAULT_LIMIT = 50;
402
+ /**
403
+ * The footer under a list that was cut short. `has_more: false` used to be
404
+ * unconditional on these lists, so a page of `--limit` rows read as the whole
405
+ * answer; the server now says when there is more and this says what to do.
406
+ * Empty when the page is complete.
407
+ *
408
+ * `cursor` is true only for a list that can be paged (`northstars list`); the
409
+ * reviews and intents lists have no cursor, so their way on is a larger
410
+ * `--limit`, or a narrower `--status` once `--limit` is at its ceiling.
411
+ */
412
+ function listFooter(input) {
413
+ if (!input.hasMore)
414
+ return '';
415
+ const head = `Showing ${input.shown} — more available;`;
416
+ const atMax = input.limit >= input.max;
417
+ if (input.cursor && input.nextCursor) {
418
+ return atMax
419
+ ? `${head} --limit is already at its maximum (${input.max}), so continue with --cursor ${input.nextCursor}`
420
+ : `${head} raise --limit (max ${input.max}) or page with --cursor ${input.nextCursor}`;
421
+ }
422
+ return atMax
423
+ ? `${head} --limit is already at its maximum (${input.max}), so narrow the list with --status`
424
+ : `${head} raise --limit (max ${input.max})`;
425
+ }
99
426
  function renderNorthstarList(rows, log) {
100
427
  if (rows.length === 0) {
101
428
  log('No northstars.');
@@ -121,6 +448,18 @@ function renderNorthstarList(rows, log) {
121
448
  { key: 'reviewed', header: 'REVIEWED' },
122
449
  ], log);
123
450
  }
451
+ /** A budget that was never set says so; `—` is kept for a timestamp that does not exist yet. */
452
+ const notSet = (value) => value === undefined || value === null ? 'not set' : String(value);
453
+ /**
454
+ * The active-commitment cap, in words. `0` is accepted and stored, and a review
455
+ * treats it as "no cap" (it does not flag every commitment over a cap of zero),
456
+ * so the screen says that rather than letting `0` read as a limit.
457
+ */
458
+ function describeMaxActive(value) {
459
+ if (value === undefined || value === null)
460
+ return 'not set';
461
+ return value === 0 ? '0 (no cap)' : String(value);
462
+ }
124
463
  function renderNorthstar(status, log) {
125
464
  const ns = status.northstar;
126
465
  log(`${ns.title} (${ns.id})`);
@@ -128,7 +467,7 @@ function renderNorthstar(status, log) {
128
467
  if (ns.description)
129
468
  log(`About ${ns.description}`);
130
469
  log(`Priority ${ns.priority?.tier ?? '—'} / weight ${ns.priority?.weight ?? '—'}`);
131
- log(`Attention total ${ns.attentionBudget?.totalWeight ?? '—'} · max active ${ns.attentionBudget?.maxActiveCommitments ?? '—'}`);
470
+ log(`Attention total ${notSet(ns.attentionBudget?.totalWeight)} · max active ${describeMaxActive(ns.attentionBudget?.maxActiveCommitments)}`);
132
471
  const cadence = ns.reviewCadence ?? {};
133
472
  log(`Review ${cadence.mode ?? 'manual'}${cadence.intervalMs ? ` every ${humanMs(cadence.intervalMs)}` : ''} next ${shortDate(cadence.nextReviewAt)} last ${shortDate(ns.state?.lastReviewedAt)}`);
134
473
  if (ns.goalIds?.length)
@@ -182,7 +521,9 @@ function renderRecommendationList(rows, log) {
182
521
  { key: 'kind', header: 'KIND' },
183
522
  { key: 'target', header: 'TARGET' },
184
523
  { key: 'status', header: 'STATUS' },
185
- { 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' },
186
527
  { key: 'reason', header: 'WHY', maxWidth: 46 },
187
528
  ], log);
188
529
  }
@@ -195,45 +536,248 @@ function renderRecommendationList(rows, log) {
195
536
  function reviewerReadiness(reviewer) {
196
537
  if (!reviewer)
197
538
  return '';
539
+ // The server's own admission gate is the authority on whether the NEXT review
540
+ // gets an agent turn: it also knows a paused northstar or a deterministic
541
+ // analysis mode, which `ready` (is the agent Always-On) does not.
542
+ const admission = reviewer.admission;
543
+ if (admission) {
544
+ if (admission.admitted)
545
+ return ' [ready]';
546
+ if (admission.reason === 'analysis_mode_not_agent_assisted') {
547
+ return ` [not in use: ${describeNorthstarReason(admission.reason)}]`;
548
+ }
549
+ return ` [not ready: ${describeNorthstarReason(admission.reason)}]`;
550
+ }
198
551
  if (reviewer.ready)
199
552
  return ' [ready]';
200
- return ` [not ready: ${reviewer.reason ?? 'offline'}]`;
553
+ // No reason from an older server is "unknown", never a guess: the old fallback
554
+ // said "offline" about an agent that was reachable and merely not Always-On.
555
+ return ` [not ready: ${reviewer.reason ? describeNorthstarReason(reviewer.reason) : 'reason unknown'}]`;
556
+ }
557
+ /** The `governance configure` flags that move what a commitment's allocation is computed from. */
558
+ exports.ALLOCATION_INPUT_FLAGS = [
559
+ 'max-runs-per-day',
560
+ 'clear-max-runs-per-day',
561
+ 'critical-reserve',
562
+ 'min-interval-hours',
563
+ 'max-interval-hours',
564
+ 'max-relative-change',
565
+ ];
566
+ /** The refresh summary a configure response carries, or undefined from a server that sends none. */
567
+ function readAllocationRefresh(response) {
568
+ if (typeof response?.allocationsRefreshed !== 'number')
569
+ return undefined;
570
+ return {
571
+ refreshed: response.allocationsRefreshed,
572
+ pending: response.allocationsPending ?? 0,
573
+ failed: response.allocationsRefreshFailed === true,
574
+ };
575
+ }
576
+ /**
577
+ * What `governance configure` says about the linked commitments. The server re-applies a changed
578
+ * budget or cadence band to every linked active commitment at once and answers how many it wrote,
579
+ * how many it could not, and whether the refresh itself failed; the command discarded all of it and
580
+ * re-read the status, so "nothing happened" and "applied to three" looked the same (OSK-21245).
581
+ *
582
+ * `inputsSent` is whether this call touched an allocation input: only then is "already up to date" a
583
+ * true statement, since a zero is also what an unrelated change (the analysis mode) answers.
584
+ */
585
+ function describeAllocationRefresh(refresh, options) {
586
+ const lines = [];
587
+ const warnings = [];
588
+ if (!refresh)
589
+ return { lines, warnings };
590
+ const bin = options.bin ?? 'skrr';
591
+ if (refresh.failed) {
592
+ warnings.push(`The policy is saved, but re-applying it to the linked commitments failed. The next review applies it, or run \`${bin} northstars review-now ${options.northstarId}\`.`);
593
+ }
594
+ if (refresh.pending > 0) {
595
+ warnings.push(`${refresh.pending} linked commitment(s) were out of date but could not be updated yet; the next review applies them.`);
596
+ }
597
+ if (refresh.refreshed > 0) {
598
+ lines.push(`Applied to ${refresh.refreshed} linked commitment(s) now.`);
599
+ }
600
+ else if (options.inputsSent && !refresh.failed && refresh.pending === 0) {
601
+ lines.push('Allocations already up to date: no linked commitment needed a write.');
602
+ }
603
+ return { lines, warnings };
604
+ }
605
+ /**
606
+ * The `Allocations` line of `review-now`. The count is of commitments WRITTEN, so `0` does not say
607
+ * "nothing happened": it is what a review prints when a configure already applied the budget, or
608
+ * when no active commitment is linked, or when no review row could be opened to write under.
609
+ */
610
+ function describeReviewAllocations(result) {
611
+ const written = Array.isArray(result.allocations) ? result.allocations.length : 0;
612
+ if (written > 0)
613
+ return `Allocations ${written} commitment(s) re-budgeted`;
614
+ if (!result.review)
615
+ return 'Allocations not written: the review could not be recorded';
616
+ if (Array.isArray(result.attention) && result.attention.length === 0) {
617
+ return 'Allocations nothing to allocate: no active commitment is linked';
618
+ }
619
+ return 'Allocations already up to date: no commitment needed a write';
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;
201
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';
202
652
  function renderGovernance(status, log) {
203
653
  const g = status.governance;
204
654
  log(`Northstar ${status.northstarId} (v${status.northstarVersion}, ${status.status})`);
205
655
  log(`Analysis ${g.analysisMode}`);
206
656
  log(`Execution ${g.executionMode}`);
207
657
  const readiness = reviewerReadiness(status.reviewer);
208
- log(`Reviewer ${g.reviewerAgentId || '—'}${readiness}`);
658
+ log(`Reviewer ${g.reviewerAgentId || 'none'}${readiness}`);
209
659
  log('');
210
660
  log(`Cadence band ${humanMs(g.cadencePolicy.minIntervalMs)} … ${humanMs(g.cadencePolicy.maxIntervalMs)} (max step ${Math.round((g.cadencePolicy.maxRelativeChange ?? 0) * 100)}%)`);
211
- log(`Intent budget ${g.maxIntentsPerReview}/review · auto ${g.maxAutoMutationsPerReview}/review, ${g.maxAutoMutationsPerDay}/day · cooldown ${humanMs(g.mutationCooldownMs)}`);
212
- log(`Allowed ${g.allowedIntentKinds.join(', ') || '—'}`);
213
- log(`Approval for ${g.requireApprovalFor.join(', ') || '—'}`);
214
- if (g.protectedCommitmentIds.length) {
215
- log(`Protected ${g.protectedCommitmentIds.join(', ')}`);
661
+ // 0 is a setting, not an absence: a review keeps NO intents at all.
662
+ const intentBudget = g.maxIntentsPerReview === 0
663
+ ? '0/review (no intents are ever recorded)'
664
+ : `${g.maxIntentsPerReview}/review`;
665
+ log(`Intent budget ${intentBudget} · auto ${g.maxAutoMutationsPerReview}/review, ${g.maxAutoMutationsPerDay}/day · cooldown ${humanMs(g.mutationCooldownMs)}`);
666
+ // An empty list is a decision with consequences, not an absence of one: say what it does.
667
+ log(`Allowed ${g.allowedIntentKinds.join(', ') || 'none — every intent is refused'}`);
668
+ if (g.allowedIntentKinds.length === 0) {
669
+ log(` ! Every intent a review proposes is refused (intent_kind_not_allowed). Allow a kind with \`governance configure ${status.northstarId} --allow <kind>\`.`);
670
+ }
671
+ log(`Approval for ${g.requireApprovalFor.join(', ') ||
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}.`);
676
+ if (g.requireApprovalFor.length === 0) {
677
+ log(' ! No change class is on the always-ask list, so the execution mode alone decides what waits for you.');
216
678
  }
679
+ log(`Protected ${g.protectedCommitmentIds.join(', ') || 'none'}`);
217
680
  const eb = status.executionBudget ?? {};
218
- log(`Exec budget action runs/day ${eb.maxRunsPerDay ?? '—'} · concurrent ${eb.maxConcurrentRuns ?? '—'} · critical reserve ${eb.reserveWeightForCritical ?? '—'}`);
219
- log(`Observation runs/day ${eb.maxObservationRunsPerDay ?? '—'} · daily budgets reset at 00:00 UTC`);
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)}`);
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}`);
220
687
  log('');
221
- log(`Review trigger ${status.reviewTrigger.scheduled ? 'scheduled' : 'manual'} every ${humanMs(status.reviewTrigger.intervalMs)} next ${shortDate(status.reviewTrigger.nextFireAt)}`);
688
+ const trigger = status.reviewTrigger;
689
+ log(trigger.scheduled
690
+ ? `Review trigger scheduled every ${humanMs(trigger.intervalMs)} next ${shortDate(trigger.nextFireAt)}`
691
+ : 'Review trigger manual (no schedule: a review runs only when you run `review-now`)');
222
692
  if (status.latestReview) {
223
693
  log(`Last review ${status.latestReview.id} ${status.latestReview.status} ${shortDate(status.latestReview.startedAt)} (${status.latestReview.intentCount} intent(s))`);
224
694
  }
225
695
  else {
226
- log('Last review —');
696
+ log('Last review none yet');
227
697
  }
228
698
  if (status.autonomousRun) {
229
699
  log(`Agent run ${status.autonomousRun.id} ${status.autonomousRun.status}`);
230
700
  }
231
701
  const counts = status.intentCounts ?? {};
232
702
  log(`Intents open ${counts.open ?? 0} · awaiting approval ${counts.awaiting_approval ?? 0} · applied ${counts.applied ?? 0} · invalidated ${counts.invalidated ?? 0}`);
233
- const budget = status.mutationBudget;
234
- log('unavailable' in budget
235
- ? 'Auto budget unavailable (governor fails closed to approval)'
236
- : `Auto budget ${budget.appliedToday}/${budget.maxAutoMutationsPerDay} used today · ${budget.remainingToday} left`);
703
+ log(describeAutoBudget(status.mutationBudget));
704
+ }
705
+ /**
706
+ * The automatic-change budget line.
707
+ *
708
+ * `0/day` is a MODE (automatic changes off, everything waits for approval), and `used` can
709
+ * legitimately exceed any cap, because the changes were made before the owner lowered it. A bare
710
+ * `2/0 used today · 0 left` collapses both into a ratio over 100% that says neither (OSK-21233).
711
+ */
712
+ function describeAutoBudget(budget) {
713
+ if ('unavailable' in budget) {
714
+ return 'Auto budget unavailable (governor fails closed to approval)';
715
+ }
716
+ const cap = budget.maxAutoMutationsPerDay;
717
+ const used = budget.appliedToday;
718
+ const earlier = `${used} made earlier today, before the cap was lowered`;
719
+ if (cap === 0) {
720
+ const note = used > 0 ? ` (${earlier})` : '';
721
+ return `Auto budget automatic changes off (0/day) — everything waits for approval${note}`;
722
+ }
723
+ if (used > cap)
724
+ return `Auto budget ${cap}/day · 0 left (${earlier})`;
725
+ return `Auto budget ${used}/${cap} used today · ${budget.remainingToday} left`;
726
+ }
727
+ /**
728
+ * Reasons that mean an `agent_assisted` review did NOT get an agent turn. A
729
+ * `succeeded` status says the review finished, not that an agent reviewed
730
+ * anything, so the table must say it in words (the reason also lives on the
731
+ * record, and used to be visible only through `review-now` and `--review`).
732
+ */
733
+ const AGENT_TURN_NOT_STARTED = new Set([
734
+ 'no_reviewer_agent_configured',
735
+ 'northstar_paused',
736
+ 'northstar_achieved',
737
+ 'northstar_archived',
738
+ 'analysis_mode_not_agent_assisted',
739
+ 'managed_agent_missing',
740
+ 'always_on_control_unavailable',
741
+ 'always_on_disabled',
742
+ 'always_on_suspended',
743
+ 'always_on_stale_generation',
744
+ 'reviewer_agent_foreign_owner',
745
+ 'reviewer_agent_not_always_on',
746
+ 'run_already_reserved',
747
+ 'envelope_build_failed',
748
+ 'governance_run_reservation_failed',
749
+ 'run_not_reserved',
750
+ 'enqueue_failed',
751
+ ]);
752
+ /** One cell of words for what a review did beyond "it ran"; empty when there is nothing to add. */
753
+ function reviewNote(review) {
754
+ // A review the intent budget cut short is not a review with nothing to say. The count lived in the
755
+ // snapshot, and the list printed `—` for it, so scanning the list hid it (OSK-21249).
756
+ const omitted = Number(review.deterministicSnapshot?.truncatedIntents);
757
+ const budget = Number.isFinite(omitted) && omitted > 0
758
+ ? `${omitted} intent(s) omitted (--max-intents-per-review)`
759
+ : '';
760
+ return [agentTurnNote(review), budget].filter(Boolean).join('; ');
761
+ }
762
+ function agentTurnNote(review) {
763
+ if (review.status === 'failed') {
764
+ return `failed: ${review.errorMessage ?? describeNorthstarReason(review.reason)}`;
765
+ }
766
+ if (review.status === 'awaiting_agent')
767
+ return 'waiting for the agent turn';
768
+ if (review.analysisMode === 'agent_assisted' && review.status === 'succeeded') {
769
+ if (review.reason && AGENT_TURN_NOT_STARTED.has(review.reason)) {
770
+ return `agent turn not started: ${describeNorthstarReason(review.reason)}`;
771
+ }
772
+ if (!review.autonomousRunId)
773
+ return 'agent turn not started: no reason was recorded';
774
+ }
775
+ return '';
776
+ }
777
+ /** `Reason` line of one review: the words, with the raw code kept beside them for grep. */
778
+ function describeReviewReason(reason) {
779
+ const words = describeNorthstarReason(reason);
780
+ return words === reason ? reason : `${words} [${reason}]`;
237
781
  }
238
782
  function renderReviewList(reviews, log) {
239
783
  if (reviews.length === 0) {
@@ -247,6 +791,7 @@ function renderReviewList(reviews, log) {
247
791
  mode: `${review.analysisMode ?? '—'}/${review.executionMode ?? '—'}`,
248
792
  intents: String((review.intents ?? []).length),
249
793
  run: review.autonomousRunId ?? '—',
794
+ note: reviewNote(review) || '—',
250
795
  })), [
251
796
  { key: 'id', header: 'REVIEW' },
252
797
  { key: 'started', header: 'STARTED' },
@@ -254,26 +799,109 @@ function renderReviewList(reviews, log) {
254
799
  { key: 'mode', header: 'MODE' },
255
800
  { key: 'intents', header: 'INTENTS' },
256
801
  { key: 'run', header: 'AGENT RUN' },
802
+ { key: 'note', header: 'NOTE' },
257
803
  ], log);
258
804
  }
259
- function renderIntentList(intents, log) {
805
+ /** `--wide` on every command that prints the intents table. */
806
+ exports.northstarWideFlag = core_1.Flags.boolean({
807
+ description: 'Do not clip the WHY column, and keep the machine code beside the reason as [code]',
808
+ });
809
+ /**
810
+ * The WHY column never shrinks below this, however narrow the terminal. The reviewer's sentences run
811
+ * to about 105 characters ("This northstar's attention budget allows 12 active commitments; this
812
+ * is the lowest priority over the cap."), and the part that matters is often at the END of one
813
+ * ("Commitment tier (high) drifted from northstar tier (low)."): at the old floor of 40 the default
814
+ * clip cut it before the fact, and the owner needed `--wide` to read the reason (OSK-21248).
815
+ */
816
+ const INTENT_WHY_MIN_WIDTH = 110;
817
+ /** No terminal to overflow (a pipe, a log): the table is as wide as it needs to be. */
818
+ const INTENT_TABLE_DEFAULT_COLUMNS = 250;
819
+ function whyCell(row, wide, budget) {
820
+ if (!wide)
821
+ return (0, format_1.truncateAtWord)(row.why, budget);
822
+ return row.code && row.code !== row.why ? `${row.why} [${row.code}]` : row.why;
823
+ }
824
+ /**
825
+ * The intents table.
826
+ *
827
+ * WHY is the HUMAN reason the governor wrote (`reason`), not the machine code in
828
+ * `verdictReason`, which says why an intent needed approval (`execution_mode_approval`)
829
+ * and is no reason for the change itself. The code stays in `--json`, and beside
830
+ * the sentence as `[code]` under `--wide`. By default the sentence is cut at a
831
+ * word boundary to what the terminal leaves after the other columns.
832
+ */
833
+ function renderIntentList(intents, log, options = {}) {
260
834
  if (intents.length === 0) {
261
835
  log('No management intents.');
262
836
  return;
263
837
  }
264
- (0, format_1.renderTable)(intents.map((intent) => ({
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));
842
+ const rows = intents.map((intent) => ({
265
843
  id: intent.id,
844
+ review: intent.reviewId ? shortReviewId(intent.reviewId) : '—',
266
845
  kind: intent.kind,
267
846
  target: intent.targetCommitmentId ?? '—',
268
847
  verdict: intent.verdict,
269
- why: intent.verdictReason ?? intent.reason ?? '—',
848
+ // The intent's OWN rating (see `intentRisk`), never its recommendation's.
849
+ risk: intentRisk(intent),
850
+ // No sentence from the governor: the policy code in words (an unknown code stays as it is).
851
+ why: intent.reason ?? (intent.verdictReason ? describeNorthstarReason(intent.verdictReason) : '—'),
852
+ code: intent.verdictReason ?? '',
270
853
  source: intent.source ?? 'deterministic',
854
+ }));
855
+ const headers = {
856
+ id: 'INTENT',
857
+ review: 'REVIEW',
858
+ kind: 'KIND',
859
+ target: 'COMMITMENT',
860
+ verdict: 'VERDICT',
861
+ risk: 'RISK',
862
+ source: 'SOURCE',
863
+ };
864
+ const fixedKeys = (withReview
865
+ ? ['id', 'review', 'kind', 'target', 'verdict', 'risk', 'source']
866
+ : ['id', 'kind', 'target', 'verdict', 'risk', 'source']);
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);
868
+ const terminal = options.columns ?? process.stdout.columns ?? INTENT_TABLE_DEFAULT_COLUMNS;
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);
871
+ (0, format_1.renderTable)(rows.map((row) => ({
872
+ id: row.id,
873
+ review: row.review,
874
+ kind: row.kind,
875
+ target: row.target,
876
+ verdict: row.verdict,
877
+ risk: row.risk,
878
+ source: row.source,
879
+ why: whyCell(row, options.wide === true, whyBudget),
271
880
  })), [
272
- { key: 'id', header: 'INTENT' },
273
- { key: 'kind', header: 'KIND' },
274
- { key: 'target', header: 'COMMITMENT' },
275
- { key: 'verdict', header: 'VERDICT' },
276
- { key: 'source', header: 'SOURCE' },
277
- { key: 'why', header: 'WHY', maxWidth: 48 },
881
+ { key: 'id', header: headers.id },
882
+ ...(withReview ? [{ key: 'review', header: headers.review }] : []),
883
+ { key: 'kind', header: headers.kind },
884
+ { key: 'target', header: headers.target },
885
+ { key: 'verdict', header: headers.verdict },
886
+ { key: 'risk', header: headers.risk },
887
+ { key: 'source', header: headers.source },
888
+ { key: 'why', header: 'WHY' },
278
889
  ], log);
279
890
  }
891
+ /**
892
+ * The line that says a review's intent budget cut something off. A cap of 0
893
+ * records nothing at all, which a screen reading "No management intents." made
894
+ * indistinguishable from a review with nothing to say; the count was only in
895
+ * `--json` (`deterministicSnapshot.truncatedIntents`).
896
+ */
897
+ function intentBudgetNote(truncated, cap) {
898
+ const count = Number(truncated);
899
+ if (!Number.isFinite(count) || count <= 0)
900
+ return '';
901
+ const limit = Number.isFinite(Number(cap)) && cap !== undefined && cap !== null ? Number(cap) : undefined;
902
+ const budget = limit === undefined ? '' : ` (${limit})`;
903
+ const hint = limit === 0
904
+ ? 'a cap of 0 records no intents at all'
905
+ : 'raise it with `governance configure --max-intents-per-review`';
906
+ return `${count} intent(s) omitted by --max-intents-per-review${budget}: ${hint}.`;
907
+ }