@skrr-ai/cli 0.1.39 → 0.1.41

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 (134) hide show
  1. package/README.md +1 -1
  2. package/dist/base-command.d.ts +9 -0
  3. package/dist/base-command.js +26 -3
  4. package/dist/commands/agent-shares/list-for.js +4 -11
  5. package/dist/commands/agent-shares/received.js +4 -11
  6. package/dist/commands/agent-shares/sent.js +4 -11
  7. package/dist/commands/agent-shares/with.js +3 -10
  8. package/dist/commands/agents/activity.js +1 -1
  9. package/dist/commands/agents/api-actions/add.js +6 -0
  10. package/dist/commands/agents/chat.js +6 -1
  11. package/dist/commands/agents/create.d.ts +7 -0
  12. package/dist/commands/agents/create.js +19 -2
  13. package/dist/commands/agents/manager/ensure.d.ts +18 -0
  14. package/dist/commands/agents/manager/ensure.js +56 -0
  15. package/dist/commands/agents/show.js +4 -0
  16. package/dist/commands/agents/tools/auth.d.ts +6 -0
  17. package/dist/commands/agents/tools/auth.js +27 -1
  18. package/dist/commands/agents/tools/index.js +11 -1
  19. package/dist/commands/agents/triggers/policy.d.ts +1 -0
  20. package/dist/commands/agents/triggers/policy.js +32 -0
  21. package/dist/commands/agents/update.js +10 -4
  22. package/dist/commands/code/install.d.ts +4 -2
  23. package/dist/commands/code/install.js +12 -10
  24. package/dist/commands/convos/list.js +1 -3
  25. package/dist/commands/convos/show.js +4 -11
  26. package/dist/commands/daemon/index.js +3 -3
  27. package/dist/commands/daemon/install.js +4 -4
  28. package/dist/commands/daemon/login.js +1 -1
  29. package/dist/commands/daemon/logs.d.ts +3 -1
  30. package/dist/commands/daemon/logs.js +16 -1
  31. package/dist/commands/daemon/start.js +3 -3
  32. package/dist/commands/daemon/status.d.ts +9 -0
  33. package/dist/commands/daemon/status.js +22 -0
  34. package/dist/commands/dm/messages.js +1 -11
  35. package/dist/commands/followups/cancel.js +1 -1
  36. package/dist/commands/followups/observability.js +6 -2
  37. package/dist/commands/followups/remind.js +5 -2
  38. package/dist/commands/followups/reschedule.js +2 -1
  39. package/dist/commands/followups/resolve.js +1 -1
  40. package/dist/commands/followups/show.js +20 -0
  41. package/dist/commands/followups/watch.d.ts +1 -0
  42. package/dist/commands/followups/watch.js +9 -3
  43. package/dist/commands/harnesses/forget.js +20 -3
  44. package/dist/commands/harnesses/install.js +18 -6
  45. package/dist/commands/harnesses/installers.d.ts +1 -1
  46. package/dist/commands/harnesses/installers.js +36 -12
  47. package/dist/commands/harnesses/list.d.ts +74 -2
  48. package/dist/commands/harnesses/list.js +231 -15
  49. package/dist/commands/harnesses/ping.d.ts +2 -0
  50. package/dist/commands/harnesses/ping.js +21 -0
  51. package/dist/commands/harnesses/show.d.ts +22 -0
  52. package/dist/commands/harnesses/show.js +116 -17
  53. package/dist/commands/harnesses/update.d.ts +8 -0
  54. package/dist/commands/harnesses/update.js +25 -1
  55. package/dist/commands/harnesses/usage.d.ts +2 -0
  56. package/dist/commands/harnesses/usage.js +7 -2
  57. package/dist/commands/instructions/status.js +13 -0
  58. package/dist/commands/instructions/update-target.d.ts +1 -0
  59. package/dist/commands/instructions/update-target.js +11 -3
  60. package/dist/commands/login.js +5 -5
  61. package/dist/commands/machines/list.js +8 -9
  62. package/dist/commands/machines/show.js +46 -17
  63. package/dist/commands/messages/show.js +3 -10
  64. package/dist/commands/shares/get.js +2 -9
  65. package/dist/commands/shares/list.js +1 -9
  66. package/dist/commands/shortcuts/show.js +4 -11
  67. package/dist/commands/skills/local.js +6 -0
  68. package/dist/commands/spaces/actions/run.js +3 -4
  69. package/dist/commands/spaces/briefs/get.js +2 -9
  70. package/dist/commands/spaces/briefs/list.js +1 -9
  71. package/dist/commands/spaces/conversations.js +1 -9
  72. package/dist/commands/spaces/heartbeats/list.js +1 -9
  73. package/dist/commands/spaces/heartbeats/status.js +2 -9
  74. package/dist/commands/tasks/activity.js +1 -10
  75. package/dist/commands/tasks/approvals/list.js +1 -9
  76. package/dist/commands/tasks/attachments/list.js +1 -9
  77. package/dist/commands/tasks/comments/list.js +1 -9
  78. package/dist/commands/tasks/dri-status.js +2 -9
  79. package/dist/commands/tasks/events/append.js +9 -7
  80. package/dist/commands/tasks/execution-timeline.js +1 -9
  81. package/dist/commands/tasks/lifecycle/detach.js +26 -5
  82. package/dist/commands/tasks/monitor.js +1 -9
  83. package/dist/commands/tasks/result/submit.js +9 -0
  84. package/dist/commands/tasks/runs.js +1 -9
  85. package/dist/commands/tasks/show.js +5 -12
  86. package/dist/commands/token/list.d.ts +8 -0
  87. package/dist/commands/token/list.js +51 -6
  88. package/dist/help.js +8 -12
  89. package/dist/lib/agent-home-workspace.d.ts +19 -0
  90. package/dist/lib/agent-home-workspace.js +39 -0
  91. package/dist/lib/agent-resolver.js +2 -2
  92. package/dist/lib/agentic-stream.d.ts +22 -1
  93. package/dist/lib/agentic-stream.js +48 -7
  94. package/dist/lib/cli-installers.d.ts +84 -2
  95. package/dist/lib/cli-installers.js +146 -13
  96. package/dist/lib/command-miss.d.ts +22 -0
  97. package/dist/lib/command-miss.js +78 -0
  98. package/dist/lib/daemon-setup.js +2 -2
  99. package/dist/lib/daemon-target.d.ts +14 -0
  100. package/dist/lib/daemon-target.js +32 -0
  101. package/dist/lib/daemonBroker.js +1 -1
  102. package/dist/lib/daemonHandoff.js +1 -1
  103. package/dist/lib/dedicated-lease-command.js +4 -1
  104. package/dist/lib/dedicated-machines.d.ts +8 -1
  105. package/dist/lib/dedicated-machines.js +22 -6
  106. package/dist/lib/dedicated-wait.js +13 -0
  107. package/dist/lib/exec-runtime-binary.js +2 -2
  108. package/dist/lib/followups.d.ts +18 -0
  109. package/dist/lib/followups.js +14 -1
  110. package/dist/lib/format.d.ts +11 -0
  111. package/dist/lib/format.js +21 -0
  112. package/dist/lib/harness-labels.d.ts +39 -0
  113. package/dist/lib/harness-labels.js +60 -0
  114. package/dist/lib/harnesses.d.ts +6 -0
  115. package/dist/lib/instruction-input.d.ts +13 -0
  116. package/dist/lib/instruction-input.js +20 -1
  117. package/dist/lib/label-ref.d.ts +8 -1
  118. package/dist/lib/label-ref.js +12 -2
  119. package/dist/lib/local-skills.d.ts +17 -1
  120. package/dist/lib/local-skills.js +20 -2
  121. package/dist/lib/login.js +5 -5
  122. package/dist/lib/machines.d.ts +6 -0
  123. package/dist/lib/machines.js +68 -2
  124. package/dist/lib/option-hint.d.ts +15 -0
  125. package/dist/lib/option-hint.js +75 -0
  126. package/dist/lib/runtime-tool-support.d.ts +34 -0
  127. package/dist/lib/runtime-tool-support.js +77 -0
  128. package/dist/lib/share-rows.d.ts +17 -0
  129. package/dist/lib/share-rows.js +35 -0
  130. package/dist/lib/workspaces.d.ts +3 -0
  131. package/dist/lib/workspaces.js +38 -0
  132. package/dist/node_modules/@skrr-ai/data-provider/index.js +5029 -5007
  133. package/oclif.manifest.json +34255 -34142
  134. package/package.json +10 -5
@@ -22,10 +22,44 @@
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.editDistance = editDistance;
24
24
  exports.nearestCommands = nearestCommands;
25
+ exports.helpSubjectId = helpSubjectId;
25
26
  exports.describeCommandMiss = describeCommandMiss;
26
27
  exports.lookupFromConfig = lookupFromConfig;
27
28
  /** A uuid-ish token, which is what a stray space id looks like. */
28
29
  const LOOKS_LIKE_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
30
+ const DAEMON_READINESS = [
31
+ {
32
+ id: 'harnesses:installers',
33
+ usage: 'harnesses installers --daemon <id>',
34
+ answers: 'whether each coding-agent CLI on that machine is installed, and signed in where the daemon can tell',
35
+ },
36
+ {
37
+ id: 'daemon:status',
38
+ usage: 'daemon status',
39
+ answers: 'whether the daemon itself is running and connected',
40
+ },
41
+ ];
42
+ const ANSWERED_ELSEWHERE = {
43
+ daemon: {
44
+ health: DAEMON_READINESS,
45
+ readiness: DAEMON_READINESS,
46
+ ready: DAEMON_READINESS,
47
+ doctor: DAEMON_READINESS,
48
+ check: DAEMON_READINESS,
49
+ },
50
+ };
51
+ // `skrr daemons <sub>` is the same topic.
52
+ const ANSWERED_ELSEWHERE_BY_COMMAND = { ...ANSWERED_ELSEWHERE, daemons: ANSWERED_ELSEWHERE.daemon };
53
+ function describeAnsweredElsewhere(config, resolved, extras) {
54
+ if (extras.length !== 1)
55
+ return null;
56
+ const targets = (ANSWERED_ELSEWHERE_BY_COMMAND[resolved]?.[extras[0].toLowerCase()] ?? []).filter((target) => config.findCommand(target.id) !== undefined);
57
+ if (targets.length === 0)
58
+ return null;
59
+ const spoken = `${config.bin} ${resolved.split(':').join(' ')}`;
60
+ return (`\`${spoken}\` has no \`${extras[0]}\`. Depending on what you want to know:\n` +
61
+ targets.map((t) => ` \`${config.bin} ${t.usage}\` — ${t.answers}`).join('\n'));
62
+ }
29
63
  /** Cheap edit distance, bounded — this runs only on a miss. */
30
64
  function editDistance(a, b) {
31
65
  const rows = a.length + 1;
@@ -60,6 +94,47 @@ function nearestCommands(id, all, limit = 3) {
60
94
  .slice(0, limit)
61
95
  .map(({ candidate }) => candidate));
62
96
  }
97
+ /**
98
+ * The command id a `--help` request is about, resolved the way oclif resolves
99
+ * the same words when they are RUN.
100
+ *
101
+ * The help override used to join every leading word into the id, so
102
+ * `skrr harnesses install devin --help` asked for `harnesses:install:devin`,
103
+ * found no such command, and reported that `harnesses install` "does not take
104
+ * the positional argument `devin`" — the positional its own help lists
105
+ * (OSK-10099). Running the same words works, because oclif stops collecting id
106
+ * words at the first resolved command that takes arguments: from there on a
107
+ * word is an argument, not part of the name. Adding `--help` to a command you
108
+ * are about to run is the commonest way to check it, so the two must agree.
109
+ *
110
+ * Mirrors `collateSpacedCmdIDFromArgs` in `@oclif/core/lib/help/util` (not a
111
+ * public export): a word extends the id while the longer id is a command or a
112
+ * prefix of one; otherwise it is appended only when the id so far is not a
113
+ * command that accepts arguments, so a genuinely wrong word under a command
114
+ * that takes none (`daemon reload`) still reaches the miss message.
115
+ */
116
+ function helpSubjectId(tokens, config) {
117
+ const usable = new Set();
118
+ for (const id of [...config.commandIDs, ...config.topicNames]) {
119
+ const parts = id.split(':');
120
+ for (let i = 1; i <= parts.length; i += 1)
121
+ usable.add(parts.slice(0, i).join(':'));
122
+ }
123
+ const taken = [];
124
+ for (const token of tokens) {
125
+ if (token.startsWith('-') || token.includes('='))
126
+ break;
127
+ if (usable.has([...taken, token].join(':'))) {
128
+ taken.push(token);
129
+ continue;
130
+ }
131
+ const current = taken.length > 0 ? config.findCommand(taken.join(':')) : undefined;
132
+ if (current && (current.strict === false || Object.keys(current.args ?? {}).length > 0))
133
+ break;
134
+ taken.push(token);
135
+ }
136
+ return taken.join(':');
137
+ }
63
138
  /**
64
139
  * The message for a colon-joined id that did not resolve.
65
140
  *
@@ -81,6 +156,9 @@ function describeCommandMiss(config, id) {
81
156
  if (!found)
82
157
  continue;
83
158
  const extras = parts.slice(take);
159
+ const redirect = describeAnsweredElsewhere(config, parts.slice(0, take).join(':'), extras);
160
+ if (redirect)
161
+ return redirect;
84
162
  const spoken = `${config.bin} ${parts.slice(0, take).join(' ')}`;
85
163
  // Suggest the flag only when the command actually has it, so the hint can
86
164
  // never point at something that does not exist.
@@ -62,7 +62,7 @@ async function offerDaemonSetup(opts = {}) {
62
62
  }
63
63
  const confirmImpl = opts.confirmImpl ?? prompt_1.confirm;
64
64
  log('');
65
- log(' This machine has no skrr runtime. It is what lets your agents work here —');
65
+ log(' This computer has no skrr background service. It is what lets your agents work here —');
66
66
  log(' running commands, editing files, and driving a browser on this machine.');
67
67
  // This creates a persistent service with broad local capabilities, so a
68
68
  // bare Enter must visibly and behaviorally decline it. `confirm` names its
@@ -100,7 +100,7 @@ async function offerDaemonSetup(opts = {}) {
100
100
  if (err.message === 'runtime-binary-not-installed') {
101
101
  return {
102
102
  status: 'failed',
103
- detail: 'the runtime vanished between fetching and installing it',
103
+ detail: 'the background service vanished between fetching and installing it',
104
104
  };
105
105
  }
106
106
  return { status: 'failed', detail: err.message };
@@ -99,5 +99,19 @@ export interface ResolveDaemonTargetInput {
99
99
  listActiveDaemons: () => Promise<ActiveDaemonRow[]>;
100
100
  }
101
101
  export declare function resolveDaemonTarget(input: ResolveDaemonTargetInput): Promise<DaemonTarget>;
102
+ /**
103
+ * Daemons that are CONNECTED right now, under the id the daemon-control API
104
+ * takes (`daemon_…`, `dedicated-runtime-…`).
105
+ *
106
+ * `listActiveDaemons` returns Daemon RESOURCE rows: "active" means not revoked,
107
+ * and `id` is the resource's own id. The installer routes relay an RPC to a
108
+ * connected daemon by its connection id, so that list could not be used to pick
109
+ * one: its refusal counted 50 "active daemons" — most of them sandboxes gone for
110
+ * weeks — and suggested values like `d_69eb2d1e...` that `--daemon` then failed
111
+ * to find, and a single "active" daemon would have been auto-picked by an id no
112
+ * route accepts (OSK-9993). Online harness rows carry exactly the ids and the
113
+ * liveness needed, one row per (daemon, provider).
114
+ */
115
+ export declare function listConnectedDaemons(credential: ResolvedCredential): Promise<ActiveDaemonRow[]>;
102
116
  /** The same list `skrr list-daemons` prints, so a refusal can name values that work. */
103
117
  export declare function listActiveDaemons(credential: ResolvedCredential): Promise<ActiveDaemonRow[]>;
@@ -45,6 +45,7 @@
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.localMachine = localMachine;
47
47
  exports.resolveDaemonTarget = resolveDaemonTarget;
48
+ exports.listConnectedDaemons = listConnectedDaemons;
48
49
  exports.listActiveDaemons = listActiveDaemons;
49
50
  const api_fetch_1 = require("./api-fetch");
50
51
  const daemonBroker_1 = require("./daemonBroker");
@@ -103,6 +104,37 @@ async function resolveDaemonTarget(input) {
103
104
  return { kind: 'none' };
104
105
  return { kind: 'ambiguous', daemons };
105
106
  }
107
+ /**
108
+ * Daemons that are CONNECTED right now, under the id the daemon-control API
109
+ * takes (`daemon_…`, `dedicated-runtime-…`).
110
+ *
111
+ * `listActiveDaemons` returns Daemon RESOURCE rows: "active" means not revoked,
112
+ * and `id` is the resource's own id. The installer routes relay an RPC to a
113
+ * connected daemon by its connection id, so that list could not be used to pick
114
+ * one: its refusal counted 50 "active daemons" — most of them sandboxes gone for
115
+ * weeks — and suggested values like `d_69eb2d1e...` that `--daemon` then failed
116
+ * to find, and a single "active" daemon would have been auto-picked by an id no
117
+ * route accepts (OSK-9993). Online harness rows carry exactly the ids and the
118
+ * liveness needed, one row per (daemon, provider).
119
+ */
120
+ async function listConnectedDaemons(credential) {
121
+ const res = await (0, api_fetch_1.apiFetch)('/api/harnesses?status=online', { credential });
122
+ const byDaemon = new Map();
123
+ for (const row of res.harnesses ?? []) {
124
+ if (row.status !== 'online' || !row.daemonId || byDaemon.has(row.daemonId))
125
+ continue;
126
+ // A harness is named "<provider> — <machine>"; the machine half names the daemon.
127
+ const separator = typeof row.name === 'string' ? row.name.indexOf(' — ') : -1;
128
+ byDaemon.set(row.daemonId, {
129
+ id: row.daemonId,
130
+ name: separator === -1 ? undefined : row.name.slice(separator + 3),
131
+ isActive: true,
132
+ });
133
+ }
134
+ // Stable order: the server returns rows most-recently-seen first, so two
135
+ // commands refusing one ambiguity listed the same daemons in different orders.
136
+ return [...byDaemon.values()].sort((a, b) => a.id.localeCompare(b.id));
137
+ }
106
138
  /** The same list `skrr list-daemons` prints, so a refusal can name values that work. */
107
139
  async function listActiveDaemons(credential) {
108
140
  const res = await (0, api_fetch_1.apiFetch)('/api/daemon-resources?status=active&limit=50', { credential });
@@ -311,7 +311,7 @@ async function attemptDaemonBrokerLogin(opts) {
311
311
  return {
312
312
  ok: false,
313
313
  reason: 'base_url_mismatch',
314
- detail: `No local skrr runtime is logged in to ${opts.baseURL}; available daemon is logged in to ${mismatchedBootstrap.serverUrl}.`,
314
+ detail: `The skrr background service on this computer is not logged in to ${opts.baseURL}; it is logged in to ${mismatchedBootstrap.serverUrl}.`,
315
315
  daemonServerUrl: mismatchedBootstrap.serverUrl,
316
316
  };
317
317
  }
@@ -208,7 +208,7 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
208
208
  return {
209
209
  status: 'skipped',
210
210
  reason: 'legacy-binary',
211
- detail: 'a legacy oversky runtime is installed; bootstrap a current skrr runtime',
211
+ detail: 'a legacy oversky background service is installed; bootstrap a current skrr one',
212
212
  };
213
213
  }
214
214
  // An older daemon has no `accept-handoff`, and spawning it would fail with
@@ -127,7 +127,10 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
127
127
  return await fn();
128
128
  }
129
129
  catch (err) {
130
- const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
130
+ const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err, {
131
+ command: this.id,
132
+ bin: this.config.bin,
133
+ });
131
134
  if (message) {
132
135
  this.failWithCliError({
133
136
  message,
@@ -643,5 +643,12 @@ export declare function dedicatedRuntimeApiErrorCode(err: unknown): string | und
643
643
  * second list would only drift from the first.
644
644
  */
645
645
  export declare function dedicatedRuntimeApiErrorDetails(err: unknown): Record<string, unknown> | undefined;
646
- export declare function formatDedicatedRuntimeApiError(err: unknown): string | null;
646
+ /** What a refused command knows about itself, so advice can be one it can follow. */
647
+ export interface DedicatedApiErrorContext {
648
+ /** oclif command id, e.g. `machines:dedicated:create`. */
649
+ command?: string;
650
+ bin?: string;
651
+ leaseId?: string;
652
+ }
653
+ export declare function formatDedicatedRuntimeApiError(err: unknown, context?: DedicatedApiErrorContext): string | null;
647
654
  export {};
@@ -1134,9 +1134,9 @@ function dedicatedRuntimeApiErrorDetails(err) {
1134
1134
  * say nothing to the person running it (OSK-8768); `--json` keeps them in
1135
1135
  * `error.details.reasons`.
1136
1136
  */
1137
- function describeRefusalReasons(err) {
1137
+ function describeRefusalReasons(err, context = {}) {
1138
1138
  const details = dedicatedRuntimeApiErrorDetails(err);
1139
- const spend = describeSpendingCapRefusal(err.body, details);
1139
+ const spend = describeSpendingCapRefusal(err.body, details, context);
1140
1140
  if (spend)
1141
1141
  return spend;
1142
1142
  const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
@@ -1148,7 +1148,7 @@ function describeRefusalReasons(err) {
1148
1148
  * the cap. Without them "would exceed the monthly cap" left a user guessing by
1149
1149
  * how much, and which cap (OSK-9552).
1150
1150
  */
1151
- function describeSpendingCapRefusal(body, details) {
1151
+ function describeSpendingCapRefusal(body, details, context = {}) {
1152
1152
  let code;
1153
1153
  try {
1154
1154
  code = JSON.parse(body || '{}').code;
@@ -1172,9 +1172,25 @@ function describeSpendingCapRefusal(body, details) {
1172
1172
  ].filter(Boolean);
1173
1173
  if (parts.length === 0)
1174
1174
  return '';
1175
- return ` ${parts.join('; ')}. Raise the cap with \`machines dedicated spending-limit <lease-id> --cents <n>\`.`;
1175
+ return ` ${parts.join('; ')}. ${raiseCapHint(context)}`;
1176
1176
  }
1177
- function formatDedicatedRuntimeApiError(err) {
1177
+ /**
1178
+ * How to get past a cap refusal, for the command that was refused. A create or
1179
+ * a restore makes a NEW machine and takes its cap as a flag; only an existing
1180
+ * machine has a lease id to raise. Every refusal used to say
1181
+ * `machines dedicated spending-limit <lease-id>`, which a user refused on create
1182
+ * had no lease id to run (OSK-9552).
1183
+ */
1184
+ function raiseCapHint(context) {
1185
+ const bin = context.bin || 'skrr';
1186
+ const creates = context.command === 'machines:dedicated:create' ||
1187
+ context.command === 'machines:dedicated:restore';
1188
+ if (creates) {
1189
+ return 'Pass a higher --spending-limit-cents: the cap is compared with the whole month, not with this machine alone.';
1190
+ }
1191
+ return `Raise the cap with \`${bin} machines dedicated spending-limit ${context.leaseId || '<lease-id>'} --cents <n>\`.`;
1192
+ }
1193
+ function formatDedicatedRuntimeApiError(err, context = {}) {
1178
1194
  if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
1179
1195
  return null;
1180
1196
  }
@@ -1202,7 +1218,7 @@ function formatDedicatedRuntimeApiError(err) {
1202
1218
  // Machine noun even though the caller named a Dedicated Runtime. Do not
1203
1219
  // repeat that contradictory product name while a mixed deployment rolls.
1204
1220
  const sentence = (parsed.message || parsed.error || parsed.code)?.replace(/(?:skrr )?Hosted Machine/g, 'Dedicated Runtime');
1205
- return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
1221
+ return sentence ? `${sentence}${describeRefusalReasons(err, context)}` : null;
1206
1222
  }
1207
1223
  catch {
1208
1224
  return err.body.trim() || null;
@@ -17,8 +17,21 @@ function backupIsNewer(current, previous) {
17
17
  const b = Date.parse(previous);
18
18
  return Number.isFinite(a) && Number.isFinite(b) ? a > b : current !== previous;
19
19
  }
20
+ /**
21
+ * `recovering` is also the state an image move replaces the machine through, on
22
+ * purpose. Reading every `recovering` as a failed action being retried told an
23
+ * owner watching an ordinary `update-image --wait` that destroy was available
24
+ * "if it never settles".
25
+ */
26
+ const PLANNED_REPLACEMENT_PROGRESS = {
27
+ dedicated_runtime_image_update: 'replacing the machine',
28
+ dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
29
+ };
20
30
  function progressOf(lease) {
21
31
  const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
32
+ const planned = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
33
+ if (state === 'recovering' && planned)
34
+ return planned;
22
35
  if (state === 'recovering') {
23
36
  // `recovering` is where a failed provision or action lands, retried with
24
37
  // backoff. It can settle — or never, which is why destroy is allowed from it.
@@ -64,7 +64,7 @@ exports.RUNTIME_BINARY_NAMES = ['skrrd', 'oversky'];
64
64
  */
65
65
  function forwardedFlagsNote(verb, opts = {}) {
66
66
  const base = opts.bootstrapsRuntime
67
- ? `If no local runtime exists, this command first fetches the signed release. After it is ` +
67
+ ? `If the background service is not on this computer yet, this command first fetches the signed release. After it is ` +
68
68
  `installed, flags are forwarded to \`skrrd ${verb}\` unchanged; run \`skrrd ${verb} --help\` ` +
69
69
  `for this verb's own flags, or \`skrrd --help\` for global ones such as \`--profile\`.`
70
70
  : `Flags are forwarded to \`skrrd ${verb}\` unchanged, so they are not listed here —` +
@@ -196,7 +196,7 @@ function isExecutable(p) {
196
196
  */
197
197
  function notInstalledMessage() {
198
198
  return [
199
- 'The skrr local runtime is not installed on this machine.',
199
+ 'The skrr background service is not installed on this computer.',
200
200
  '',
201
201
  'Install:',
202
202
  ' macOS: brew install dush1023/oversky/oversky',
@@ -1,6 +1,12 @@
1
1
  import { withQuery } from './triggers';
2
2
  type Query = Parameters<typeof withQuery>[1];
3
3
  type Json = Record<string, unknown>;
4
+ /**
5
+ * The line a settle verb prints. A follow-up that had already ended is said to
6
+ * have ended, with the terminal it actually has, rather than reported as the
7
+ * thing this command would have done (OSK-10038).
8
+ */
9
+ export declare function settledLine(id: string, result: Record<string, unknown>, done: string): string;
4
10
  /**
5
11
  * The `id` argument every follow-up command that addresses one takes.
6
12
  *
@@ -34,7 +40,13 @@ export interface FollowUpRow {
34
40
  terminal?: string | null;
35
41
  terminalAt?: string | null;
36
42
  terminalReason?: string | null;
43
+ /** Who settled it: `trigger_fire` for the runtime, otherwise the actor. */
44
+ terminalBy?: string | null;
37
45
  escalatedInitiativeId?: string | null;
46
+ /** The gate the fire passes: exempt_silent, exempt_user_directed, exempt_system, user_directed_guarded, autonomy_ladder, approval_ladder. */
47
+ gate?: string | null;
48
+ /** Set while its fire waits on an offline machine; it escalates after 60 minutes. */
49
+ parkedAt?: string | null;
38
50
  capsule?: unknown;
39
51
  nextRunAt?: string | null;
40
52
  deadlineAt?: string | null;
@@ -62,6 +74,12 @@ export interface FollowUpObservability {
62
74
  terminal: Record<string, number>;
63
75
  escalationRate: number | null;
64
76
  fires: number;
77
+ /** USD from the fires' UsageLog rows; null when the ledger could not be read. */
78
+ spend?: {
79
+ today: number;
80
+ total: number;
81
+ currency: string;
82
+ } | null;
65
83
  budget: {
66
84
  pool: string | null;
67
85
  used: number;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
4
+ exports.settledLine = settledLine;
4
5
  exports.resolveFollowUpAgent = resolveFollowUpAgent;
5
6
  exports.parseSubjectRef = parseSubjectRef;
6
7
  exports.followupSummaryLine = followupSummaryLine;
@@ -26,6 +27,18 @@ const triggers_1 = require("./triggers");
26
27
  const agent_resolver_1 = require("./agent-resolver");
27
28
  const MOUNT = '/api/followups';
28
29
  const base = (id) => `${MOUNT}/${encodeURIComponent(id)}`;
30
+ /**
31
+ * The line a settle verb prints. A follow-up that had already ended is said to
32
+ * have ended, with the terminal it actually has, rather than reported as the
33
+ * thing this command would have done (OSK-10038).
34
+ */
35
+ function settledLine(id, result, done) {
36
+ const followup = result.followup;
37
+ if (result.alreadySettled === true) {
38
+ return `Follow-up ${id} was already settled${followup?.terminal ? ` (${followup.terminal})` : ''}; nothing changed.`;
39
+ }
40
+ return `Follow-up ${id} ${done}.`;
41
+ }
29
42
  /**
30
43
  * The `id` argument every follow-up command that addresses one takes.
31
44
  *
@@ -79,7 +92,7 @@ function resolveFollowUpAgent(explicitAgentId, env = process.env) {
79
92
  if (explicitAgentId && explicitAgentId !== sessionAgent.id) {
80
93
  throw new agent_resolver_1.AgentResolutionError({
81
94
  code: 'AGENT_CONTEXT_CONFLICT',
82
- message: 'This local runtime is bound to a session agent. Do not override it with another agent ID.',
95
+ message: 'This session is bound to its session agent. Do not override it with another agent ID.',
83
96
  suggestion: 'Use the bound session agent, or start a new daemon session for a different agent.',
84
97
  details: { explicitAgentId, sessionAgentId: sessionAgent.id, source: sessionAgent.source },
85
98
  });
@@ -78,6 +78,17 @@ export declare function renderTable<T extends Record<string, string>>(rows: T[],
78
78
  * should print it in the reader's zone and name that zone once.
79
79
  */
80
80
  export declare function localStamp(value: unknown): string;
81
+ /**
82
+ * One timestamp that carries its own zone: `2026-08-21 17:54:03 PDT`.
83
+ *
84
+ * For key/value output and any row that cannot share a header naming the zone.
85
+ * About 25 commands formatted with `toISOString().slice(0, 19)` — UTC with no
86
+ * marker, which reads as local time — while `harnesses list` and `machines`
87
+ * printed local time with the zone named, so one CLI kept two clocks
88
+ * (OSK-10041). Seconds, because the commands that print these (task runs,
89
+ * activity, monitor) order events seconds apart. Never throws.
90
+ */
91
+ export declare function localTime(value: unknown): string;
81
92
  /**
82
93
  * `PDT` — the zone `localStamp` renders in, to be named once per command
83
94
  * rather than repeated on every row.
@@ -7,6 +7,7 @@ exports.formatCell = formatCell;
7
7
  exports.collapseWhitespace = collapseWhitespace;
8
8
  exports.renderTable = renderTable;
9
9
  exports.localStamp = localStamp;
10
+ exports.localTime = localTime;
10
11
  exports.localZone = localZone;
11
12
  /**
12
13
  * Code points that occupy TWO terminal columns (Unicode East Asian Wide and
@@ -212,6 +213,26 @@ function localStamp(value) {
212
213
  return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
213
214
  `${pad(date.getHours())}:${pad(date.getMinutes())}`);
214
215
  }
216
+ /**
217
+ * One timestamp that carries its own zone: `2026-08-21 17:54:03 PDT`.
218
+ *
219
+ * For key/value output and any row that cannot share a header naming the zone.
220
+ * About 25 commands formatted with `toISOString().slice(0, 19)` — UTC with no
221
+ * marker, which reads as local time — while `harnesses list` and `machines`
222
+ * printed local time with the zone named, so one CLI kept two clocks
223
+ * (OSK-10041). Seconds, because the commands that print these (task runs,
224
+ * activity, monitor) order events seconds apart. Never throws.
225
+ */
226
+ function localTime(value) {
227
+ if (value === null || value === undefined || value === '')
228
+ return '-';
229
+ const date = value instanceof Date ? value : new Date(typeof value === 'number' ? value : String(value));
230
+ if (Number.isNaN(date.getTime()))
231
+ return String(value);
232
+ const zone = localZone(date);
233
+ const seconds = `${date.getSeconds()}`.padStart(2, '0');
234
+ return `${localStamp(date)}:${seconds}${zone ? ` ${zone}` : ''}`;
235
+ }
215
236
  /**
216
237
  * `PDT` — the zone `localStamp` renders in, to be named once per command
217
238
  * rather than repeated on every row.
@@ -0,0 +1,39 @@
1
+ /**
2
+ * How a harness row's provider and status read in human output — ONE rule for
3
+ * `harnesses list` and `harnesses show`.
4
+ *
5
+ * Both findings this closes were the same defect on two columns: the rule lived
6
+ * in one command and not the other.
7
+ *
8
+ * - Status (OSK-9610): `list` printed `superseded` for a row a later
9
+ * registration replaced; `show` printed `offline` for the same id. The server
10
+ * sends `superseded` on both routes — `show` never read it.
11
+ * - Provider (OSK-9649 / OSK-9290): the web renders the placeholder row of a
12
+ * machine with no coding engine as "No coding agent installed" and folds the
13
+ * first-party harness's older spelling into its current one. The CLI printed
14
+ * the stored value — `oversky` on 111 rows, and the first-party harness's
15
+ * older spelling beside its current one — so one product had three names here.
16
+ *
17
+ * `--json` keeps the stored values: it is the API contract, not a rendering.
18
+ */
19
+ /** What the PROVIDER column says for a machine that has no coding engine. */
20
+ export declare const NO_CODING_AGENT_PROVIDER_LABEL = "none";
21
+ export declare function harnessStatusLabel(row: {
22
+ status?: string | null;
23
+ superseded?: boolean | null;
24
+ }): string;
25
+ export declare function isNoCodingAgentProvider(provider: string | null | undefined): boolean;
26
+ export declare function harnessProviderLabel(provider: string | null | undefined): string;
27
+ /**
28
+ * YOUR alias is what `name` shows and it differs from the name everyone else
29
+ * sees. The server sends both (`aliased`, `sharedName`); every human surface
30
+ * asks this one question of them, so `list`, `show` and `update --name` cannot
31
+ * disagree about whether a row is aliased (OSK-10004).
32
+ */
33
+ export declare function isAliasCoveringSharedName(row: {
34
+ name?: string | null;
35
+ sharedName?: string | null;
36
+ aliased?: boolean | null;
37
+ }): boolean;
38
+ /** The longer phrase, for a single-row view with room for it. */
39
+ export declare function harnessProviderDescription(provider: string | null | undefined): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ /**
3
+ * How a harness row's provider and status read in human output — ONE rule for
4
+ * `harnesses list` and `harnesses show`.
5
+ *
6
+ * Both findings this closes were the same defect on two columns: the rule lived
7
+ * in one command and not the other.
8
+ *
9
+ * - Status (OSK-9610): `list` printed `superseded` for a row a later
10
+ * registration replaced; `show` printed `offline` for the same id. The server
11
+ * sends `superseded` on both routes — `show` never read it.
12
+ * - Provider (OSK-9649 / OSK-9290): the web renders the placeholder row of a
13
+ * machine with no coding engine as "No coding agent installed" and folds the
14
+ * first-party harness's older spelling into its current one. The CLI printed
15
+ * the stored value — `oversky` on 111 rows, and the first-party harness's
16
+ * older spelling beside its current one — so one product had three names here.
17
+ *
18
+ * `--json` keeps the stored values: it is the API contract, not a rendering.
19
+ */
20
+ Object.defineProperty(exports, "__esModule", { value: true });
21
+ exports.NO_CODING_AGENT_PROVIDER_LABEL = void 0;
22
+ exports.harnessStatusLabel = harnessStatusLabel;
23
+ exports.isNoCodingAgentProvider = isNoCodingAgentProvider;
24
+ exports.harnessProviderLabel = harnessProviderLabel;
25
+ exports.isAliasCoveringSharedName = isAliasCoveringSharedName;
26
+ exports.harnessProviderDescription = harnessProviderDescription;
27
+ const data_provider_1 = require("@skrr-ai/data-provider");
28
+ /** What the PROVIDER column says for a machine that has no coding engine. */
29
+ exports.NO_CODING_AGENT_PROVIDER_LABEL = 'none';
30
+ function harnessStatusLabel(row) {
31
+ if (row.superseded)
32
+ return 'superseded';
33
+ return typeof row.status === 'string' && row.status.length > 0 ? row.status : 'unknown';
34
+ }
35
+ function isNoCodingAgentProvider(provider) {
36
+ return typeof provider === 'string' && (0, data_provider_1.isManagedProductFallbackProvider)(provider);
37
+ }
38
+ function harnessProviderLabel(provider) {
39
+ if (typeof provider !== 'string' || provider.length === 0)
40
+ return 'unknown';
41
+ if (isNoCodingAgentProvider(provider))
42
+ return exports.NO_CODING_AGENT_PROVIDER_LABEL;
43
+ return String((0, data_provider_1.canonicalHarnessProvider)(provider) ?? provider);
44
+ }
45
+ /**
46
+ * YOUR alias is what `name` shows and it differs from the name everyone else
47
+ * sees. The server sends both (`aliased`, `sharedName`); every human surface
48
+ * asks this one question of them, so `list`, `show` and `update --name` cannot
49
+ * disagree about whether a row is aliased (OSK-10004).
50
+ */
51
+ function isAliasCoveringSharedName(row) {
52
+ return Boolean(row.aliased && row.sharedName && row.sharedName !== row.name);
53
+ }
54
+ /** The longer phrase, for a single-row view with room for it. */
55
+ function harnessProviderDescription(provider) {
56
+ if (isNoCodingAgentProvider(provider)) {
57
+ return `${exports.NO_CODING_AGENT_PROVIDER_LABEL} — ${(0, data_provider_1.managedProductFallbackLabel)().toLowerCase()} on this machine`;
58
+ }
59
+ return harnessProviderLabel(provider);
60
+ }
@@ -51,6 +51,12 @@ export interface Harness {
51
51
  sharedName?: string;
52
52
  aliased?: boolean;
53
53
  status?: string;
54
+ /**
55
+ * A later registration on the same machine replaced this row. Sent by both
56
+ * the list and the single-row routes; human output shows it instead of the
57
+ * stored status (`harnessStatusLabel`, OSK-9610).
58
+ */
59
+ superseded?: boolean;
54
60
  harnessLocation?: 'local' | 'cloud';
55
61
  daemonId?: string;
56
62
  machineUuid?: string;
@@ -76,3 +76,16 @@ export declare function describeTargetSync(target: {
76
76
  updatedAt?: string;
77
77
  driftCount?: number;
78
78
  }, now?: number, liveDaemonIds?: string[]): string;
79
+ /**
80
+ * The command that re-binds a stalled target to the daemon now running on its
81
+ * machine, or null when no such daemon is live.
82
+ *
83
+ * A daemon started a different way on the same machine (launchd → cli) gets a
84
+ * new id, and a target stays bound to the old one. Placement is the machine, not
85
+ * the daemon, so the recovery is `update-target --daemon`, never a re-install,
86
+ * which the placement index refuses (OSK-9961).
87
+ */
88
+ export declare function rebindCommandFor(target: {
89
+ id: string;
90
+ daemonId?: string | null;
91
+ }, liveDaemonIds: readonly string[]): string | null;
@@ -7,6 +7,7 @@ exports.resolveBundleLabel = resolveBundleLabel;
7
7
  exports.parseDaemonId = parseDaemonId;
8
8
  exports.sameMachineDaemon = sameMachineDaemon;
9
9
  exports.describeTargetSync = describeTargetSync;
10
+ exports.rebindCommandFor = rebindCommandFor;
10
11
  const promises_1 = require("node:fs/promises");
11
12
  async function readStdin() {
12
13
  const chunks = [];
@@ -180,7 +181,25 @@ function describeTargetSync(target, now = Date.now(), liveDaemonIds = []) {
180
181
  const live = parseDaemonId(elsewhere);
181
182
  const pinned = parseDaemonId(target.daemonId || '');
182
183
  // Kept under the STATUS column's `maxWidth: 78`, same constraint as below.
183
- return `${base} — stalled ${days}d; daemon here now runs as ${live?.source}, not ${pinned?.source}: re-install`;
184
+ // Re-bind, not re-install: install refuses a second target on the same
185
+ // placement, so "re-install" was a remedy that could not succeed (OSK-9961).
186
+ // The full command does not fit the column; status prints it below.
187
+ return `${base} — stalled ${days}d; daemon here now runs as ${live?.source}, not ${pinned?.source}: re-bind below`;
184
188
  }
185
189
  return `${base} — stalled ${days}d; daemon may be gone: remove, then purge-target --force`;
186
190
  }
191
+ /**
192
+ * The command that re-binds a stalled target to the daemon now running on its
193
+ * machine, or null when no such daemon is live.
194
+ *
195
+ * A daemon started a different way on the same machine (launchd → cli) gets a
196
+ * new id, and a target stays bound to the old one. Placement is the machine, not
197
+ * the daemon, so the recovery is `update-target --daemon`, never a re-install,
198
+ * which the placement index refuses (OSK-9961).
199
+ */
200
+ function rebindCommandFor(target, liveDaemonIds) {
201
+ if (!target.daemonId || liveDaemonIds.includes(target.daemonId))
202
+ return null;
203
+ const live = sameMachineDaemon(target.daemonId, [...liveDaemonIds]);
204
+ return live ? `skrr instructions update-target ${target.id} --daemon ${live}` : null;
205
+ }