@skrr-ai/cli 0.1.127 → 0.1.128

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 (113) hide show
  1. package/dist/base-command.d.ts +23 -0
  2. package/dist/base-command.js +72 -0
  3. package/dist/commands/agents/chat.js +7 -0
  4. package/dist/commands/commitments/action-history.js +1 -1
  5. package/dist/commands/commitments/cases.js +3 -3
  6. package/dist/commands/commitments/check-now.d.ts +10 -2
  7. package/dist/commands/commitments/check-now.js +53 -3
  8. package/dist/commands/commitments/create.d.ts +6 -14
  9. package/dist/commands/commitments/create.js +61 -6
  10. package/dist/commands/commitments/doctor.js +3 -1
  11. package/dist/commands/commitments/effective-policy.d.ts +11 -1
  12. package/dist/commands/commitments/effective-policy.js +84 -4
  13. package/dist/commands/commitments/hypotheses.js +12 -12
  14. package/dist/commands/commitments/next-tick.js +5 -0
  15. package/dist/commands/commitments/observations.js +18 -17
  16. package/dist/commands/commitments/pack/list.js +2 -1
  17. package/dist/commands/commitments/pack/plan.d.ts +6 -0
  18. package/dist/commands/commitments/pack/plan.js +19 -0
  19. package/dist/commands/commitments/preflight.d.ts +19 -0
  20. package/dist/commands/commitments/preflight.js +108 -10
  21. package/dist/commands/commitments/trigger-status.js +8 -0
  22. package/dist/commands/commitments/update.d.ts +1 -0
  23. package/dist/commands/commitments/update.js +45 -4
  24. package/dist/commands/commitments/wake-capabilities.js +13 -0
  25. package/dist/commands/compass/archive.js +4 -0
  26. package/dist/commands/compass/commitments.js +15 -1
  27. package/dist/commands/compass/create.js +11 -3
  28. package/dist/commands/compass/effective-policy.d.ts +1 -0
  29. package/dist/commands/compass/effective-policy.js +8 -7
  30. package/dist/commands/compass/revert.d.ts +8 -0
  31. package/dist/commands/compass/revert.js +25 -1
  32. package/dist/commands/compass/revisions.d.ts +2 -0
  33. package/dist/commands/compass/revisions.js +18 -2
  34. package/dist/commands/compass/tactic.d.ts +5 -3
  35. package/dist/commands/compass/tactic.js +8 -7
  36. package/dist/commands/compass/update.js +19 -3
  37. package/dist/commands/followups/act.js +1 -1
  38. package/dist/commands/followups/cancel.js +13 -0
  39. package/dist/commands/followups/observability.js +20 -1
  40. package/dist/commands/followups/plans/create.js +12 -1
  41. package/dist/commands/followups/show.js +13 -0
  42. package/dist/commands/followups/watch.js +1 -1
  43. package/dist/commands/goals/planner-config.d.ts +11 -0
  44. package/dist/commands/goals/planner-config.js +16 -0
  45. package/dist/commands/machines/update.d.ts +5 -3
  46. package/dist/commands/machines/update.js +70 -9
  47. package/dist/commands/northstars/create.js +3 -1
  48. package/dist/commands/northstars/governance/configure.d.ts +4 -0
  49. package/dist/commands/northstars/governance/configure.js +75 -9
  50. package/dist/commands/northstars/intents/decide.d.ts +6 -0
  51. package/dist/commands/northstars/intents/decide.js +69 -15
  52. package/dist/commands/northstars/intents/list.d.ts +2 -1
  53. package/dist/commands/northstars/intents/list.js +15 -5
  54. package/dist/commands/northstars/intents.d.ts +26 -0
  55. package/dist/commands/northstars/intents.js +49 -0
  56. package/dist/commands/northstars/list.d.ts +1 -0
  57. package/dist/commands/northstars/list.js +24 -3
  58. package/dist/commands/northstars/recommendations/list.d.ts +1 -1
  59. package/dist/commands/northstars/recommendations/list.js +3 -3
  60. package/dist/commands/northstars/recommendations.d.ts +19 -0
  61. package/dist/commands/northstars/recommendations.js +42 -0
  62. package/dist/commands/northstars/review-now.d.ts +1 -0
  63. package/dist/commands/northstars/review-now.js +10 -4
  64. package/dist/commands/northstars/reviews.d.ts +1 -0
  65. package/dist/commands/northstars/reviews.js +19 -3
  66. package/dist/commands/northstars/update.js +8 -1
  67. package/dist/commands/proposals/list.js +11 -2
  68. package/dist/commands/proposals/show.js +9 -1
  69. package/dist/commands/spaces/auto-executor/preview.d.ts +14 -1
  70. package/dist/commands/spaces/auto-executor/preview.js +35 -1
  71. package/dist/commands/spaces/auto-executor/run.d.ts +17 -0
  72. package/dist/commands/spaces/auto-executor/run.js +60 -2
  73. package/dist/commands/spaces/files/upload.js +1 -2
  74. package/dist/commands/spaces/index.js +23 -0
  75. package/dist/generated/agent-guides.json +69 -69
  76. package/dist/lib/agentic-stream.d.ts +9 -0
  77. package/dist/lib/agentic-stream.js +14 -0
  78. package/dist/lib/command-miss.js +11 -2
  79. package/dist/lib/commitment-attention-copy.d.ts +96 -0
  80. package/dist/lib/commitment-attention-copy.js +209 -0
  81. package/dist/lib/commitment-loop-status.d.ts +6 -0
  82. package/dist/lib/commitment-loop-status.js +16 -6
  83. package/dist/lib/commitments.d.ts +88 -0
  84. package/dist/lib/commitments.js +188 -0
  85. package/dist/lib/compass-body-help.d.ts +20 -0
  86. package/dist/lib/compass-body-help.js +61 -0
  87. package/dist/lib/compass-refusals.d.ts +35 -0
  88. package/dist/lib/compass-refusals.js +50 -0
  89. package/dist/lib/compasses.d.ts +80 -4
  90. package/dist/lib/compasses.js +232 -12
  91. package/dist/lib/followups.d.ts +22 -1
  92. package/dist/lib/followups.js +21 -1
  93. package/dist/lib/format.d.ts +11 -0
  94. package/dist/lib/format.js +29 -0
  95. package/dist/lib/initiatives.d.ts +9 -2
  96. package/dist/lib/initiatives.js +13 -2
  97. package/dist/lib/machines.d.ts +14 -1
  98. package/dist/lib/machines.js +24 -0
  99. package/dist/lib/northstars.d.ts +158 -24
  100. package/dist/lib/northstars.js +538 -26
  101. package/dist/lib/proposal-decision-command.js +21 -1
  102. package/dist/lib/proposals.d.ts +26 -1
  103. package/dist/lib/proposals.js +163 -0
  104. package/dist/lib/rate-limit-retry.d.ts +5 -0
  105. package/dist/lib/rate-limit-retry.js +19 -0
  106. package/dist/lib/roles.d.ts +1 -1
  107. package/dist/lib/space-charter.js +9 -0
  108. package/dist/lib/task-asset-upload.js +1 -2
  109. package/dist/lib/triggers.d.ts +1 -1
  110. package/dist/lib/triggers.js +11 -3
  111. package/dist/node_modules/@skrr-ai/data-provider/index.js +6504 -6493
  112. package/oclif.manifest.json +350 -34
  113. package/package.json +4 -4
@@ -2,9 +2,17 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.compassApi = exports.COMPASS_STATUSES = void 0;
4
4
  exports.renderCompassList = renderCompassList;
5
+ exports.describeSettings = describeSettings;
6
+ exports.describeQuietHours = describeQuietHours;
7
+ exports.describeMode = describeMode;
5
8
  exports.renderCompass = renderCompass;
9
+ exports.describeBoundCommitments = describeBoundCommitments;
10
+ exports.renderTacticResult = renderTacticResult;
11
+ exports.renderEffectivePolicy = renderEffectivePolicy;
6
12
  exports.renderRevisionList = renderRevisionList;
7
13
  exports.renderCompassTrace = renderCompassTrace;
14
+ exports.revisionTypeLabel = revisionTypeLabel;
15
+ exports.renderRevertResult = renderRevertResult;
8
16
  const data_provider_1 = require("@skrr-ai/data-provider");
9
17
  /**
10
18
  * compasses.ts — request-based client for `/api/compasses`.
@@ -79,6 +87,76 @@ function renderCompassList(rows, log) {
79
87
  { key: 'updated', header: 'UPDATED' },
80
88
  ], log);
81
89
  }
90
+ function scalar(value) {
91
+ if (value === null || value === undefined)
92
+ return 'none';
93
+ return typeof value === 'object' ? JSON.stringify(value) : String(value);
94
+ }
95
+ /**
96
+ * One setting per line (`key: value`, nested objects as dotted keys, lists joined with `;`), so a
97
+ * settings object reads as labelled lines rather than one JSON blob (OSK-21150).
98
+ */
99
+ function describeSettings(value, prefix = '') {
100
+ if (!value || typeof value !== 'object' || Array.isArray(value))
101
+ return [];
102
+ const lines = [];
103
+ for (const [key, entry] of Object.entries(value)) {
104
+ if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
105
+ lines.push(...describeSettings(entry, `${prefix}${key}.`));
106
+ }
107
+ else if (Array.isArray(entry)) {
108
+ lines.push(`${prefix}${key}: ${entry.map(scalar).join('; ') || 'none'}`);
109
+ }
110
+ else {
111
+ lines.push(`${prefix}${key}: ${scalar(entry)}`);
112
+ }
113
+ }
114
+ return lines;
115
+ }
116
+ /** A quiet-hours window in words: the hours and the zone, `off`, or `not set`. */
117
+ function describeQuietHours(window) {
118
+ if (!window || typeof window !== 'object' || Array.isArray(window))
119
+ return 'not set';
120
+ const w = window;
121
+ if (w.enabled !== true)
122
+ return 'off';
123
+ const zone = typeof w.timezone === 'string' && w.timezone.trim() ? w.timezone.trim() : 'UTC (no zone set)';
124
+ return `${scalar(w.start ?? '?')} to ${scalar(w.end ?? '?')} ${zone}`;
125
+ }
126
+ /**
127
+ * The mode, and the word the author wrote when it differs from it. The server stores `auto` as
128
+ * `auto_run_limited`; saying so beats showing a value nobody typed as if they had (OSK-21150).
129
+ */
130
+ function describeMode(mode, authoredMode) {
131
+ const normalized = typeof mode === 'string' && mode ? mode : null;
132
+ const authored = typeof authoredMode === 'string' && authoredMode ? authoredMode : null;
133
+ if (!normalized) {
134
+ return authored
135
+ ? `not set (${JSON.stringify(authored)} is not a mode the platform knows)`
136
+ : 'not set (the platform default applies)';
137
+ }
138
+ return authored && authored !== normalized
139
+ ? `${normalized} (normalized; you wrote ${JSON.stringify(authored)})`
140
+ : normalized;
141
+ }
142
+ const SOURCE_WORDS = {
143
+ authored: 'the author',
144
+ derived: 'nobody (a default the platform derived)',
145
+ };
146
+ function labelled(label, lines, log, width = 12) {
147
+ lines.forEach((line, index) => log(`${index === 0 ? label.padEnd(width) : ' '.repeat(width)}${line}`));
148
+ }
149
+ /** The stored autonomy defaults, as the lines `show` prints (no JSON). */
150
+ function autonomyLines(autonomy) {
151
+ const { mode, authoredMode, modeSource, quietHours, ...rest } = autonomy;
152
+ const source = typeof modeSource === 'string' ? (SOURCE_WORDS[modeSource] ?? modeSource) : undefined;
153
+ const out = [];
154
+ const modeLine = `${describeMode(mode, authoredMode)}${source ? `; set by ${source}` : ''}`;
155
+ out.push(['Autonomy', [modeLine, ...describeSettings(rest)]]);
156
+ if (quietHours !== undefined)
157
+ out.push(['Quiet hours', [describeQuietHours(quietHours)]]);
158
+ return out;
159
+ }
82
160
  function renderCompass(compass, log) {
83
161
  log(`${compass.title ?? '(untitled compass)'} (${compass.id})`);
84
162
  log(`Agent ${compass.agentId}`);
@@ -86,12 +164,13 @@ function renderCompass(compass, log) {
86
164
  if (compass.domain)
87
165
  log(`Domain ${compass.domain}`);
88
166
  if (compass.scope)
89
- log(`Scope ${JSON.stringify(compass.scope)}`);
167
+ labelled('Scope', describeSettings(compass.scope), log);
90
168
  if (compass.autonomyDefaults) {
91
- log(`Autonomy ${JSON.stringify(compass.autonomyDefaults)}`);
169
+ for (const [label, lines] of autonomyLines(compass.autonomyDefaults))
170
+ labelled(label, lines, log);
92
171
  }
93
172
  if (compass.communicationDefaults) {
94
- log(`Comms ${JSON.stringify(compass.communicationDefaults)}`);
173
+ labelled('Comms', describeSettings(compass.communicationDefaults), log);
95
174
  }
96
175
  const rules = compass.rules ?? [];
97
176
  if (rules.length) {
@@ -126,6 +205,13 @@ function renderCompass(compass, log) {
126
205
  if (target.successMetrics?.length) {
127
206
  log(` measured by: ${target.successMetrics.join('; ')}`);
128
207
  }
208
+ // Tactics live on a target, not on the compass; they used to be printed nowhere.
209
+ if (target.learnedTactics?.length) {
210
+ log(` learned tactics (${target.learnedTactics.length}):`);
211
+ for (const tactic of target.learnedTactics) {
212
+ log(` • ${tactic.id ?? '—'}: ${tactic.description ?? '—'}`);
213
+ }
214
+ }
129
215
  // The agent's prompt carries a bounded number of each list. Say which
130
216
  // were cut, so the list above is not read as what the agent sees.
131
217
  for (const overflow of (0, data_provider_1.describeCompassOverflow)(target)) {
@@ -133,17 +219,103 @@ function renderCompass(compass, log) {
133
219
  }
134
220
  }
135
221
  }
136
- const tactics = compass.learnedTactics ?? [];
137
- if (tactics.length) {
138
- log('');
139
- log(`Learned tactics (${tactics.length}):`);
140
- for (const tactic of tactics)
141
- log(` • ${tactic.description ?? '—'}`);
142
- }
143
222
  const commitments = compass.commitments ?? [];
144
223
  if (commitments.length) {
145
224
  log('');
146
- log(`Commitments governed by this compass: ${commitments.length}`);
225
+ log(describeBoundCommitments(commitments.length, compass.status));
226
+ }
227
+ }
228
+ /**
229
+ * How many Commitments are bound to a Compass, and whether it governs them. A Commitment stays
230
+ * bound to an archived Compass but runs on defaults: the platform reads only an ACTIVE Compass's
231
+ * direction. "Governed by this compass: 1" printed the same words for both, on the Compass side,
232
+ * while the Commitment's own preflight said it was running on defaults (OSK-21242).
233
+ */
234
+ function describeBoundCommitments(count, status) {
235
+ return status === 'archived'
236
+ ? `Commitments bound to this compass: ${count} (not governed while the compass is archived)`
237
+ : `Commitments governed by this compass: ${count}`;
238
+ }
239
+ /**
240
+ * What `compass tactic` says back. Three outcomes, each in its own words: it used to print
241
+ * "Appended" for all of them, with a count that was always 0, and name nothing that rolled off.
242
+ * A server that does not report `tactic` is not guessed at.
243
+ */
244
+ function renderTacticResult(compass, log) {
245
+ const tactic = compass.tactic;
246
+ if (!tactic) {
247
+ log(`Recorded. ${compass.id} is now v${compass.version}. This server did not report what changed; ` +
248
+ 'run `compass show` to see the learned tactics.');
249
+ return;
250
+ }
251
+ const where = tactic.targetInvariantId ? `invariant ${tactic.targetInvariantId}` : 'its target';
252
+ const carries = `${compass.id} now carries ${tactic.count} learned tactic(s) on ${where}`;
253
+ if (tactic.outcome === 'unchanged') {
254
+ log(`No change — tactic already recorded. ${tactic.id} already reads that way on ${where}; ` +
255
+ `${compass.id} stays at v${compass.version}.`);
256
+ }
257
+ else if (tactic.outcome === 'updated') {
258
+ log(`Updated. ${tactic.id} on ${where} has the new text. ${carries} (v${compass.version}).`);
259
+ }
260
+ else {
261
+ log(`Appended. ${tactic.id} filed under ${where}. ${carries} (v${compass.version}).`);
262
+ }
263
+ for (const dropped of tactic.dropped ?? []) {
264
+ log(`Rolled off to stay within the cap: ${dropped.id} — "${dropped.description}"`);
265
+ }
266
+ }
267
+ const record = (value) => value && typeof value === 'object' && !Array.isArray(value)
268
+ ? value
269
+ : {};
270
+ /**
271
+ * `compass effective-policy` in words (OSK-21150): the mode beside what was written when they
272
+ * differ, who chose it, the quiet hours that apply (or why none do), the communication defaults, and
273
+ * where each field came from. It used to print each of those as one JSON line, with a null mode and
274
+ * "derived" unexplained. `--json` is unchanged.
275
+ */
276
+ function renderEffectivePolicy(policy, log) {
277
+ log(`Compass ${scalar(policy.compassId)}, revision ${scalar(policy.compassVersion)}: ${scalar(policy.status)}`);
278
+ log(`Applies to runtime: ${policy.appliesToRuntime === true ? 'yes' : 'no'}`);
279
+ const autonomy = record(policy.autonomy);
280
+ log('Autonomy');
281
+ const source = typeof autonomy.modeSource === 'string' ? autonomy.modeSource : undefined;
282
+ labelled(' Mode', [describeMode(autonomy.mode, autonomy.authoredMode)], log, 16);
283
+ if (source)
284
+ labelled(' Mode set by', [SOURCE_WORDS[source] ?? source], log, 16);
285
+ const applied = autonomy.quietHours;
286
+ const configured = autonomy.configuredQuietHours;
287
+ const issues = Array.isArray(autonomy.quietHoursIssues) ? autonomy.quietHoursIssues : [];
288
+ labelled(' Quiet hours', [
289
+ applied
290
+ ? describeQuietHours(applied)
291
+ : configured
292
+ ? `not applied (stored: ${describeQuietHours(configured)})`
293
+ : 'not set',
294
+ ], log, 16);
295
+ for (const issue of issues) {
296
+ const message = record(issue).message;
297
+ if (typeof message === 'string')
298
+ log(` ! ${message} Quiet hours do not apply until this is fixed.`);
299
+ }
300
+ const communication = describeSettings(policy.communication);
301
+ log('Communication');
302
+ if (communication.length === 0)
303
+ log(' nothing set');
304
+ for (const line of communication)
305
+ log(` ${line}`);
306
+ log(`Authority: ${scalar(policy.authority ?? 'unknown')}`);
307
+ const bySource = new Map();
308
+ for (const [path, entry] of Object.entries(record(policy.fieldProvenance))) {
309
+ const from = scalar(record(entry).source ?? 'unknown');
310
+ bySource.set(from, [...(bySource.get(from) ?? []), path]);
311
+ }
312
+ if (bySource.size === 0) {
313
+ log('Field provenance: none (no defaults are set)');
314
+ }
315
+ else {
316
+ log('Field provenance:');
317
+ for (const [from, paths] of bySource)
318
+ log(` ${from}: ${paths.join(', ')}`);
147
319
  }
148
320
  }
149
321
  function renderRevisionList(rows, log) {
@@ -153,7 +325,7 @@ function renderRevisionList(rows, log) {
153
325
  }
154
326
  (0, format_1.renderTable)(rows.map((row) => ({
155
327
  revision: `v${row.revision}`,
156
- type: row.revisionType ?? '—',
328
+ type: revisionTypeLabel(row),
157
329
  when: shortDate(row.createdAt),
158
330
  actor: row.actorUserId ?? '—',
159
331
  reason: row.reason ?? '—',
@@ -194,3 +366,51 @@ function renderCompassTrace(trace, log) {
194
366
  if (trace.has_more)
195
367
  log('More events exist; request a larger --limit (up to 500).');
196
368
  }
369
+ /**
370
+ * The ledger's name for a row. A revert is stored as `auto_revert`, which reads as something the
371
+ * platform did on its own, so a person's `compass revert` is shown as `revert`, with the versions
372
+ * it moved between: `from v6 → v2` is the version it was at and the version whose content it took.
373
+ * Display only: the stored enum is unchanged.
374
+ */
375
+ function revisionTypeLabel(row) {
376
+ // Reviving an archived compass has no ledger type of its own (the stored enum has no member for it),
377
+ // so it was recorded and shown as a plain `update`, indistinguishable from an edit. Each revision
378
+ // carries the status before and after (`status` is one of the captured config fields), so the
379
+ // lifecycle act is read from that; display only, the stored type is unchanged (OSK-21244).
380
+ if (row.revisionType === 'update' &&
381
+ row.previousConfig?.status === 'archived' &&
382
+ row.nextConfig?.status === 'active') {
383
+ return 'reactivate';
384
+ }
385
+ if (row.revisionType !== 'auto_revert')
386
+ return row.revisionType ?? '—';
387
+ const { fromVersion, toVersion } = row.metadata?.autoRevert ?? {};
388
+ return fromVersion !== undefined && toVersion !== undefined
389
+ ? `revert from v${fromVersion} → v${toVersion}`
390
+ : 'revert';
391
+ }
392
+ /**
393
+ * What `compass revert` says after it ran. Three things the old one-liner left out: whether
394
+ * anything was written, that the status is whatever it was (a revert never changes lifecycle), and
395
+ * which enforced rules the restored version no longer carries.
396
+ */
397
+ function renderRevertResult(compass, toVersion, agentId, log) {
398
+ if (compass.unchanged) {
399
+ log(`Already at v${toVersion}'s content — nothing written. ${compass.id} stays at v${compass.version}.`);
400
+ log(`Status stays ${compass.status}.`);
401
+ return;
402
+ }
403
+ log(`Restored v${toVersion} as v${compass.version} — the intermediate versions stay in the ledger.`);
404
+ log(compass.status === 'archived'
405
+ ? `Status stays archived: a revert restores the configuration, never the lifecycle. ` +
406
+ `Reactivate it with \`compass update ${compass.id} --agent ${agentId} --status active\`.`
407
+ : `Status stays ${compass.status}.`);
408
+ const changed = compass.rulesChanged;
409
+ if (changed && (changed.to < changed.from || (changed.removed ?? []).length > 0)) {
410
+ const removed = changed.removed ?? [];
411
+ log(`Warning: rules went from ${changed.from} to ${changed.to}` +
412
+ `${removed.length ? ` (removed: ${removed.join(', ')})` : ''}. ` +
413
+ `A removed active rule is no longer enforced; ` +
414
+ `\`compass revert --to-version ${compass.version - 1}\` brings it back.`);
415
+ }
416
+ }
@@ -69,10 +69,18 @@ export interface FollowUpRow {
69
69
  /** Who settled it: `trigger_fire` for the runtime, otherwise the actor. */
70
70
  terminalBy?: string | null;
71
71
  escalatedInitiativeId?: string | null;
72
- /** The gate the fire passes: exempt_silent, exempt_user_directed, exempt_system, user_directed_guarded, autonomy_ladder, approval_ladder. */
72
+ /** The gate the fire passes: exempt_silent, exempt_user_directed, exempt_system, exempt_notification (a passive reminder), user_directed_guarded, autonomy_ladder, approval_ladder. */
73
73
  gate?: string | null;
74
74
  /** Set while its fire waits on an offline machine; it escalates after 60 minutes. */
75
75
  parkedAt?: string | null;
76
+ /** The SLA backstop of a paired event/poll watch: it escalates the watch when the wait outlives its deadline. */
77
+ slaFallback?: boolean;
78
+ /** Set while a fire was moved (quiet hours, working hours, an unavailable machine) instead of run. */
79
+ held?: {
80
+ until: string;
81
+ reason?: string | null;
82
+ note?: string | null;
83
+ } | null;
76
84
  capsule?: unknown;
77
85
  nextRunAt?: string | null;
78
86
  deadlineAt?: string | null;
@@ -100,6 +108,15 @@ export interface FollowUpCreateResponse {
100
108
  }
101
109
  /** One line telling a person the follow-up was recorded with weaker provenance than claimed. */
102
110
  export declare function producerDowngradeNotice(response: FollowUpCreateResponse): string | undefined;
111
+ /**
112
+ * What a reused answer means for the person who asked, for the kinds whose lane a repeat
113
+ * declaration cannot move (an act, a watch): it armed nothing and changed neither the note nor
114
+ * the time they typed, and the row that holds the lane keeps ITS time, which may be weeks away.
115
+ * `Already armed … was not duplicated` alone let `act --in 5m` read as scheduled for five minutes
116
+ * while the existing act waited thirty days. Say what is armed and how to move it, as the
117
+ * check-in answer does (OSK-15411).
118
+ */
119
+ export declare function reusedWithoutChangeMessage(row: FollowUpCreateResponse['followup'], what: string): string;
103
120
  export interface FollowUpObservability {
104
121
  generatedAt?: string;
105
122
  agents?: Array<{
@@ -123,6 +140,10 @@ export interface FollowUpObservability {
123
140
  pool: string | null;
124
141
  used: number;
125
142
  limit: number | null;
143
+ /** Creations the platform dropped on purpose today (an Agent's own check-in); null when unreadable. */
144
+ droppedToday?: number | null;
145
+ /** Creations refused with FOLLOWUP_BUDGET_EXCEEDED today; null when unreadable. */
146
+ refusedToday?: number | null;
126
147
  } | null;
127
148
  silenceRate: number | null;
128
149
  silentFires: number | null;
@@ -5,6 +5,7 @@ exports.whenFlagProblem = whenFlagProblem;
5
5
  exports.settledLine = settledLine;
6
6
  exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
7
7
  exports.producerDowngradeNotice = producerDowngradeNotice;
8
+ exports.reusedWithoutChangeMessage = reusedWithoutChangeMessage;
8
9
  exports.resolveFollowUpAgent = resolveFollowUpAgent;
9
10
  exports.parseSubjectRef = parseSubjectRef;
10
11
  exports.followupSummaryLine = followupSummaryLine;
@@ -135,6 +136,25 @@ function producerDowngradeNotice(response) {
135
136
  return undefined;
136
137
  return `Note: recorded as ${downgraded.recorded}, not ${downgraded.requested}. ${downgraded.note}`;
137
138
  }
139
+ /**
140
+ * What a reused answer means for the person who asked, for the kinds whose lane a repeat
141
+ * declaration cannot move (an act, a watch): it armed nothing and changed neither the note nor
142
+ * the time they typed, and the row that holds the lane keeps ITS time, which may be weeks away.
143
+ * `Already armed … was not duplicated` alone let `act --in 5m` read as scheduled for five minutes
144
+ * while the existing act waited thirty days. Say what is armed and how to move it, as the
145
+ * check-in answer does (OSK-15411).
146
+ */
147
+ function reusedWithoutChangeMessage(row, what) {
148
+ const lines = [
149
+ `Already armed: ${row?.id} — ${what} exists and was not duplicated.`,
150
+ 'Nothing in this request was applied: it did not change the note or the time you gave.',
151
+ ];
152
+ if (row) {
153
+ lines.push(`Armed: ${row.why ? `"${row.why}"` : '(no note)'}, ${row.nextRunAt ? `next ${row.nextRunAt}` : 'waiting on its trigger'}.`);
154
+ }
155
+ lines.push(`To change the time: skrr followups reschedule ${row?.id ?? '<id>'} --at <instant> or --in <offset>.`);
156
+ return lines.join('\n');
157
+ }
138
158
  /**
139
159
  * A trigger subject becomes a durable concurrency key, so it cannot be prose.
140
160
  * Keep this grammar aligned with TriggerEngine's subject validation and the
@@ -213,5 +233,5 @@ function followupSummaryLine(row) {
213
233
  const subject = row.subject ? `${row.subject.type}:${row.subject.externalKey}` : '—';
214
234
  const kindForm = row.form ? `${row.kind}/${row.form}` : (row.kind ?? '—');
215
235
  const when = row.state === 'settled' ? `settled:${row.terminal ?? '?'}` : `next:${fmtWhen(row.nextRunAt)}`;
216
- return `${row.id} [${row.state ?? '?'}] ${kindForm} ${subject} ${when}`;
236
+ return `${row.id} [${row.state ?? '?'}] ${kindForm}${row.slaFallback ? ' (SLA fallback)' : ''} ${subject} ${when}`;
217
237
  }
@@ -22,6 +22,17 @@ export declare function pad(s: string, width: number): string;
22
22
  * surrogate pair is never split down the middle into two replacement glyphs.
23
23
  */
24
24
  export declare function truncate(s: string, max: number): string;
25
+ /**
26
+ * `truncate`, but never in the middle of a word.
27
+ *
28
+ * For a prose cell (a reason, a title), where `truncate` cuts a sentence at the
29
+ * column and leaves a half word that reads as a typo. The cut moves back to the
30
+ * last word boundary that fits, and loose punctuation is dropped before the
31
+ * ellipsis. A single token longer than the budget has no boundary to move to, so
32
+ * it is cut hard: a clipped word is better than an empty cell. A boundary so
33
+ * early it would throw away most of the budget is not taken for the same reason.
34
+ */
35
+ export declare function truncateAtWord(s: string, max: number): string;
25
36
  /**
26
37
  * Render an arbitrary API value as a single table cell.
27
38
  *
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.displayWidth = displayWidth;
4
4
  exports.pad = pad;
5
5
  exports.truncate = truncate;
6
+ exports.truncateAtWord = truncateAtWord;
6
7
  exports.formatCell = formatCell;
7
8
  exports.collapseWhitespace = collapseWhitespace;
8
9
  exports.renderTable = renderTable;
@@ -105,6 +106,34 @@ function truncate(s, max) {
105
106
  }
106
107
  return `${out}…`;
107
108
  }
109
+ /**
110
+ * `truncate`, but never in the middle of a word.
111
+ *
112
+ * For a prose cell (a reason, a title), where `truncate` cuts a sentence at the
113
+ * column and leaves a half word that reads as a typo. The cut moves back to the
114
+ * last word boundary that fits, and loose punctuation is dropped before the
115
+ * ellipsis. A single token longer than the budget has no boundary to move to, so
116
+ * it is cut hard: a clipped word is better than an empty cell. A boundary so
117
+ * early it would throw away most of the budget is not taken for the same reason.
118
+ */
119
+ function truncateAtWord(s, max) {
120
+ if (displayWidth(s) <= max)
121
+ return s;
122
+ if (max <= 1)
123
+ return '…';
124
+ const hard = truncate(s, max);
125
+ const kept = hard.slice(0, -1); // the longest prefix that leaves room for the ellipsis
126
+ const rest = s.slice(kept.length);
127
+ const trim = (value) => value.replace(/[\s,;:(\-—]+$/u, '');
128
+ // The cut already fell on a word boundary.
129
+ if (/^\s/u.test(rest))
130
+ return `${trim(kept)}…`;
131
+ const boundary = kept.search(/\s\S*$/u);
132
+ if (boundary > 0 && displayWidth(kept.slice(0, boundary)) >= max * 0.4) {
133
+ return `${trim(kept.slice(0, boundary))}…`;
134
+ }
135
+ return hard;
136
+ }
108
137
  /**
109
138
  * Render an arbitrary API value as a single table cell.
110
139
  *
@@ -2,8 +2,15 @@ import { withQuery } from './triggers';
2
2
  /** The decisions `PATCH /api/initiatives/:id/action` accepts. */
3
3
  export declare const INITIATIVE_ACTIONS: readonly ["approve", "edit_then_approve", "always_allow_similar", "dismiss", "never_suggest_again", "mute_source", "not_now", "snooze", "reopen", "resolve", "wait", "follow_up", "undo", "grant_autonomy", "only_when_urgent"];
4
4
  export type InitiativeAction = (typeof INITIATIVE_ACTIONS)[number];
5
- /** States an Initiative moves through before and after a decision. */
6
- export declare const INITIATIVE_STATES: readonly ["detected", "proposed", "approved", "executing", "waiting", "completed", "dismissed", "failed"];
5
+ /**
6
+ * States an Initiative moves through before and after a decision: exactly the stored
7
+ * vocabulary (`AGENT_INITIATIVE_STATES`, `@skrr-ai/data-schemas`; a spec holds the two
8
+ * equal). This list used to say `completed`, a state nothing stores and the server refuses
9
+ * (`unsupported initiative state(s): completed`), and omitted `resolved`, so a person could
10
+ * not list the cards that had been approved and finished, nor the snoozed, expired,
11
+ * reopened or followed-up ones.
12
+ */
13
+ export declare const INITIATIVE_STATES: readonly ["detected", "proposed", "approved", "executing", "waiting", "followed_up", "reopened", "resolved", "dismissed", "snoozed", "expired", "failed"];
7
14
  export type InitiativeView = {
8
15
  id?: string;
9
16
  title?: string;
@@ -39,15 +39,26 @@ exports.INITIATIVE_ACTIONS = [
39
39
  'grant_autonomy',
40
40
  'only_when_urgent',
41
41
  ];
42
- /** States an Initiative moves through before and after a decision. */
42
+ /**
43
+ * States an Initiative moves through before and after a decision: exactly the stored
44
+ * vocabulary (`AGENT_INITIATIVE_STATES`, `@skrr-ai/data-schemas`; a spec holds the two
45
+ * equal). This list used to say `completed`, a state nothing stores and the server refuses
46
+ * (`unsupported initiative state(s): completed`), and omitted `resolved`, so a person could
47
+ * not list the cards that had been approved and finished, nor the snoozed, expired,
48
+ * reopened or followed-up ones.
49
+ */
43
50
  exports.INITIATIVE_STATES = [
44
51
  'detected',
45
52
  'proposed',
46
53
  'approved',
47
54
  'executing',
48
55
  'waiting',
49
- 'completed',
56
+ 'followed_up',
57
+ 'reopened',
58
+ 'resolved',
50
59
  'dismissed',
60
+ 'snoozed',
61
+ 'expired',
51
62
  'failed',
52
63
  ];
53
64
  exports.initiativeApi = {
@@ -1,4 +1,4 @@
1
- import type { DaemonContactState, DaemonMaintenanceSummary } from '@skrr-ai/data-provider';
1
+ import type { DaemonContactState, DaemonMaintenanceSummary, MachineIcon, MachineIconPatch } from '@skrr-ai/data-provider';
2
2
  /**
3
3
  * machines.ts — request client for `/api/machines`.
4
4
  *
@@ -134,6 +134,7 @@ export interface Machine {
134
134
  name?: string;
135
135
  description?: string;
136
136
  tags?: string[];
137
+ icon?: MachineIcon;
137
138
  };
138
139
  lease: Record<string, unknown> | null;
139
140
  controls: {
@@ -271,7 +272,19 @@ export interface MachinePreferencesPatch {
271
272
  name?: string;
272
273
  description?: string;
273
274
  tags?: string[];
275
+ /** A preset, or null for the default. An uploaded image is not set by a patch. */
276
+ icon?: MachineIconPatch;
274
277
  }
278
+ /**
279
+ * The value of `--icon`: a preset's id, or `none` (also `default`) to go back to
280
+ * the default. Throws a sentence that names the valid ids, because the only other
281
+ * way to learn them would be to send a wrong one and read the server's refusal.
282
+ * An uploaded image is a different flag (`--icon-file`): a name that is not a
283
+ * preset is never guessed to be a path.
284
+ */
285
+ export declare function parseMachineIconFlag(value: string): MachineIconPatch;
286
+ /** What `machines show`/`update` print for an icon: the preset's id, or what kind of image. */
287
+ export declare function describeMachineIcon(icon: MachineIcon | undefined): string;
275
288
  /**
276
289
  * How many of a machine's harnesses still represent a capability it has.
277
290
  *
@@ -10,6 +10,8 @@ exports.harnessStateLabel = harnessStateLabel;
10
10
  exports.summarizeDaemons = summarizeDaemons;
11
11
  exports.isThisComputer = isThisComputer;
12
12
  exports.machineListView = machineListView;
13
+ exports.parseMachineIconFlag = parseMachineIconFlag;
14
+ exports.describeMachineIcon = describeMachineIcon;
13
15
  exports.liveHarnessCount = liveHarnessCount;
14
16
  exports.listMachines = listMachines;
15
17
  exports.getMachine = getMachine;
@@ -229,6 +231,28 @@ function machineListView(machine, options) {
229
231
  ...(hidden > 0 ? { hiddenHarnessCount: hidden } : {}),
230
232
  };
231
233
  }
234
+ /**
235
+ * The value of `--icon`: a preset's id, or `none` (also `default`) to go back to
236
+ * the default. Throws a sentence that names the valid ids, because the only other
237
+ * way to learn them would be to send a wrong one and read the server's refusal.
238
+ * An uploaded image is a different flag (`--icon-file`): a name that is not a
239
+ * preset is never guessed to be a path.
240
+ */
241
+ function parseMachineIconFlag(value) {
242
+ const id = value.trim().toLowerCase();
243
+ if (id === 'none' || id === 'default')
244
+ return null;
245
+ if ((0, data_provider_1.isMachineIconPresetId)(id))
246
+ return { kind: 'preset', id };
247
+ throw new Error(`--icon: "${value}" is not an icon. Choose one of ${data_provider_1.MACHINE_ICON_PRESET_IDS.join(', ')}, ` +
248
+ 'or "none" to go back to the default. To use your own image, pass --icon-file <path>.');
249
+ }
250
+ /** What `machines show`/`update` print for an icon: the preset's id, or what kind of image. */
251
+ function describeMachineIcon(icon) {
252
+ if (!icon)
253
+ return 'default';
254
+ return icon.kind === 'preset' ? icon.id : 'uploaded image';
255
+ }
232
256
  /**
233
257
  * How many of a machine's harnesses still represent a capability it has.
234
258
  *