@skrr-ai/cli 0.1.109 → 0.1.110

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 (63) hide show
  1. package/dist/base-command.d.ts +15 -0
  2. package/dist/base-command.js +64 -17
  3. package/dist/commands/agents/triggers/create.js +1 -1
  4. package/dist/commands/agents/triggers/update.js +1 -1
  5. package/dist/commands/agents/update.d.ts +11 -0
  6. package/dist/commands/agents/update.js +58 -2
  7. package/dist/commands/daemon/install.js +17 -1
  8. package/dist/commands/daemon/login.js +14 -0
  9. package/dist/commands/harnesses/prune.d.ts +26 -0
  10. package/dist/commands/harnesses/prune.js +172 -0
  11. package/dist/commands/login.js +11 -0
  12. package/dist/commands/spaces/workflows/start.js +3 -1
  13. package/dist/commands/spaces/workflows/validate.js +12 -2
  14. package/dist/commands/tasks/activity.js +11 -1
  15. package/dist/commands/tasks/list.js +6 -2
  16. package/dist/commands/tasks/rearm-recurrence.js +22 -3
  17. package/dist/commands/tasks/show.d.ts +8 -0
  18. package/dist/commands/tasks/show.js +64 -32
  19. package/dist/commands/tasks/unschedule.js +5 -1
  20. package/dist/commands/tasks/update.js +11 -1
  21. package/dist/commands/triggers/audit.js +12 -3
  22. package/dist/commands/triggers/create.js +1 -1
  23. package/dist/commands/triggers/fire-detail.d.ts +7 -0
  24. package/dist/commands/triggers/fire-detail.js +30 -12
  25. package/dist/commands/triggers/fire.js +13 -3
  26. package/dist/commands/triggers/parse-intent.js +1 -1
  27. package/dist/commands/triggers/update.js +1 -1
  28. package/dist/commands/workspaces/grants.js +1 -4
  29. package/dist/commands/workspaces/members.js +6 -6
  30. package/dist/lib/commitment-product.js +5 -2
  31. package/dist/lib/commitments.d.ts +2 -1
  32. package/dist/lib/commitments.js +40 -40
  33. package/dist/lib/daemon-machine-identity-copy.d.ts +14 -0
  34. package/dist/lib/daemon-machine-identity-copy.js +27 -0
  35. package/dist/lib/daemon-machine-identity.d.ts +27 -0
  36. package/dist/lib/daemon-machine-identity.js +61 -0
  37. package/dist/lib/daemonHandoff.d.ts +19 -1
  38. package/dist/lib/daemonHandoff.js +59 -0
  39. package/dist/lib/harnesses.d.ts +71 -0
  40. package/dist/lib/harnesses.js +70 -0
  41. package/dist/lib/list-output.d.ts +1 -1
  42. package/dist/lib/list-output.js +2 -0
  43. package/dist/lib/profile-machine-identity-guard.js +11 -1
  44. package/dist/lib/task-economics.d.ts +10 -0
  45. package/dist/lib/task-economics.js +24 -2
  46. package/dist/lib/triggers.d.ts +11 -0
  47. package/dist/lib/triggers.js +43 -1
  48. package/dist/lib/workflow-run-view.d.ts +4 -0
  49. package/dist/lib/workflow-run-view.js +5 -0
  50. package/dist/lib/workspaces.d.ts +24 -7
  51. package/dist/lib/workspaces.js +14 -2
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +5 -3
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +60 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +130 -6
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +60 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +128 -6
  60. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  61. package/dist/node_modules/@skrr-ai/data-provider/index.js +4003 -3968
  62. package/oclif.manifest.json +99 -9
  63. package/package.json +10 -1
@@ -958,40 +958,38 @@ function describeOperation(operation, reason) {
958
958
  };
959
959
  return `not running — ${meaning[status] ?? status}`;
960
960
  }
961
- /**
962
- * What to do about an operation that is not running, from the operating state's
963
- * first actionable blocker (`withLiveAssessment` carries it onto the view).
964
- *
965
- * `show` printed `Operation: blocked — typed_external_blocker` and nothing else,
966
- * so the one line a reader acts on was a code; the remediation the server
967
- * already composed sat only in `--json` and `doctor` (OSK-16212). Nothing is
968
- * printed while the operation is ready or working, where a fix would contradict
969
- * the line above it.
970
- *
971
- * Returns one or two lines. `Fix:` only when the blocker carries an action; a
972
- * typed blocker an agent returned carries only what happened, which printed
973
- * under "Fix:" read as a remedy nobody wrote (OSK-16230). That narrative is
974
- * labelled `Blocker:` instead, and no remedy is made up for it.
975
- */
976
961
  function describeOperationFix(commitment, operation) {
977
962
  if (operation === 'ready' || operation === 'working')
978
963
  return [];
979
964
  const blocker = commitment.firstActionableBlocker;
980
- if (!blocker)
981
- return [];
982
- const remediation = typeof blocker.remediation === 'string' ? blocker.remediation.trim() : '';
983
- const detail = typeof blocker.detail === 'string' ? blocker.detail.trim() : '';
984
- const who = typeof blocker.ownerLabel === 'string' && blocker.ownerLabel
985
- ? blocker.ownerLabel
986
- : typeof blocker.owner === 'string' && blocker.owner
987
- ? blocker.owner
988
- : '';
989
965
  const lines = [];
990
- // The detail is shown only when the fix does not already say it.
991
- if (detail && detail !== remediation)
992
- lines.push({ label: 'Blocker', text: `${detail}${!remediation && who ? ` (${who})` : ''}` });
993
- if (remediation)
994
- lines.push({ label: 'Fix', text: `${remediation}${who ? ` (${who})` : ''}` });
966
+ let remediation = '';
967
+ if (blocker) {
968
+ remediation = typeof blocker.remediation === 'string' ? blocker.remediation.trim() : '';
969
+ const detail = typeof blocker.detail === 'string' ? blocker.detail.trim() : '';
970
+ const who = typeof blocker.ownerLabel === 'string' && blocker.ownerLabel
971
+ ? blocker.ownerLabel
972
+ : typeof blocker.owner === 'string' && blocker.owner
973
+ ? blocker.owner
974
+ : '';
975
+ // The detail is shown only when the fix does not already say it.
976
+ if (detail && detail !== remediation)
977
+ lines.push({ label: 'Blocker', text: `${detail}${!remediation && who ? ` (${who})` : ''}` });
978
+ if (remediation)
979
+ lines.push({ label: 'Fix', text: `${remediation}${who ? ` (${who})` : ''}` });
980
+ }
981
+ // What refused the last Run is a second fact, not an alternative to the
982
+ // blocker: a paused Commitment whose Run was refused needs both a resume and
983
+ // the fix, or resuming only fails again (OSK-15389). The sentence is the
984
+ // server's, derived on read from the failure code; it is printed whenever it
985
+ // exists and says something the blocker's fix does not.
986
+ const liveness = commitment.state?.liveness;
987
+ const lastRunFix = liveness && liveness.current !== false && typeof liveness.diagnosisFix === 'string'
988
+ ? liveness.diagnosisFix.trim()
989
+ : '';
990
+ if (lastRunFix && lastRunFix !== remediation) {
991
+ lines.push({ label: lines.length ? 'Last run' : 'Fix', text: lastRunFix });
992
+ }
995
993
  return lines;
996
994
  }
997
995
  /**
@@ -1293,9 +1291,16 @@ function renderCheckList(checks, log) {
1293
1291
  // meant re-running the command with `--json` (OSK-6175).
1294
1292
  { key: 'id', header: 'ID', verbatim: true },
1295
1293
  ], log);
1296
- if (checks.some((c) => c.historyDetail)) {
1297
- log("'-' = not retained: an older check's detailed history was compacted; its stop and retained reason remain.");
1298
- }
1294
+ // The legend states each distinct reason the server recorded, in the same
1295
+ // words explain and cycles use; it never guesses "compacted" for a Check
1296
+ // whose history is merely missing (OSK-15148).
1297
+ const reasons = [
1298
+ ...new Set(checks
1299
+ .map((c) => (0, data_provider_2.describeCommitmentHistoryDetail)(c.historyDetail))
1300
+ .filter((sentence) => Boolean(sentence))),
1301
+ ];
1302
+ if (reasons.length > 0)
1303
+ log(`'-' = not retained. ${reasons.join(' ')}`);
1299
1304
  }
1300
1305
  function records(value) {
1301
1306
  return Array.isArray(value)
@@ -1663,14 +1668,9 @@ function formatCommitmentCycles(page) {
1663
1668
  */
1664
1669
  function formatCycleLatency(cycle) {
1665
1670
  const latency = cycle.latency;
1666
- const history = cycle.historyDetail;
1667
- const disclosure = history?.state === 'compacted'
1668
- ? [
1669
- ` Detailed history was compacted${history.at ? ` at ${history.at}` : ''}; available reason and source timestamps were retained.`,
1670
- ]
1671
- : history?.state === 'unavailable'
1672
- ? [' Detailed history is unavailable; why it is missing was not recorded.']
1673
- : [];
1671
+ // One sentence for every surface, from the server's own record (OSK-15148).
1672
+ const historySentence = (0, data_provider_2.describeCommitmentHistoryDetail)(cycle.historyDetail);
1673
+ const disclosure = historySentence ? [` ${historySentence}`] : [];
1674
1674
  // A hop whose inputs were compacted away without a retained projection is
1675
1675
  // "not retained": it may have been measured and shown before (OSK-15148).
1676
1676
  const unretained = new Set(latency?.unretainedStages || []);
@@ -0,0 +1,14 @@
1
+ import type { DaemonMachineIdentitySkew } from './daemon-machine-identity';
2
+ /**
3
+ * What to tell someone whose installed background service would register their
4
+ * profile on the default profile's machine row (OSK-S-440).
5
+ *
6
+ * The consequence is stated before the remedy because the remedy costs something
7
+ * (updating a service) and the consequence is what justifies it: a second
8
+ * profile on the default's row replaces its Daemon and its device-key pin, and
9
+ * the default profile's next refresh is refused until it signs in again.
10
+ *
11
+ * Lives here, apart from the comparison, because this is copy and reads
12
+ * `BRAND.displayName`; the comparison is pure and imports nothing from it.
13
+ */
14
+ export declare function describeDaemonMachineIdentitySkew(skew: DaemonMachineIdentitySkew): string[];
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.describeDaemonMachineIdentitySkew = describeDaemonMachineIdentitySkew;
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
+ /**
6
+ * What to tell someone whose installed background service would register their
7
+ * profile on the default profile's machine row (OSK-S-440).
8
+ *
9
+ * The consequence is stated before the remedy because the remedy costs something
10
+ * (updating a service) and the consequence is what justifies it: a second
11
+ * profile on the default's row replaces its Daemon and its device-key pin, and
12
+ * the default profile's next refresh is refused until it signs in again.
13
+ *
14
+ * Lives here, apart from the comparison, because this is copy and reads
15
+ * `BRAND.displayName`; the comparison is pure and imports nothing from it.
16
+ */
17
+ function describeDaemonMachineIdentitySkew(skew) {
18
+ const product = data_provider_1.BRAND.displayName;
19
+ const said = skew.reported
20
+ ? `it reports machine identity ${skew.reported}, where profile '${skew.profile}' has ${skew.expected}`
21
+ : `it does not report a machine identity at all, so ${product} cannot tell which one it would use`;
22
+ return [
23
+ `The ${product} background service at ${skew.binary} does not support a separate machine identity per profile: ${said}.`,
24
+ `Using it for '${skew.profile}' would register that profile on the default profile's machine and could replace the default profile's device-key pin. Nothing was changed.`,
25
+ `Update the background service (skrr daemon update), or put a current skrrd first on your PATH, then run: skrr login --profile ${skew.profile}`,
26
+ ];
27
+ }
@@ -0,0 +1,27 @@
1
+ export interface DaemonMachineIdentitySkew {
2
+ /** The profile being signed in. */
3
+ profile: string;
4
+ /** The machine identity this profile has, as decided by the profile's own record. */
5
+ expected: string;
6
+ /** What the daemon binary says it will send; null when it said nothing. */
7
+ reported: string | null;
8
+ /** The `skrrd` that was asked. */
9
+ binary: string;
10
+ }
11
+ /**
12
+ * Null when the daemon will register this profile under the identity it has;
13
+ * otherwise what disagrees.
14
+ *
15
+ * Not applicable to the default profile (its identity is the hardware value in
16
+ * every version), to a hosted runtime (the control plane issues it), or when no
17
+ * profile is named.
18
+ *
19
+ * A daemon that reports NOTHING is a skew, not a pass: this check exists because
20
+ * an older binary cannot say, and "could not confirm" must not read as "fine" on
21
+ * the step that binds a credential to a machine row.
22
+ */
23
+ export declare function daemonMachineIdentitySkew(opts: {
24
+ profile: string | null | undefined;
25
+ binary: string;
26
+ reported: string | null | undefined;
27
+ }): DaemonMachineIdentitySkew | null;
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.daemonMachineIdentitySkew = daemonMachineIdentitySkew;
4
+ /**
5
+ * daemon-machine-identity.ts — does the installed daemon give this profile the
6
+ * machine identity the CLI decided it has?
7
+ *
8
+ * Two programs compute one identity: the CLI (and the profile's persisted
9
+ * `machine-identity.json`) say a profile that started new has its own machine
10
+ * row, and the DAEMON is what registers. A daemon that predates per-profile
11
+ * identities (OSK-13845) does not read that file; it sends the hardware value
12
+ * whatever the profile is. Nothing noticed. The CLI minted the daemon's credential
13
+ * for what the daemon REPORTED (`accept-handoff --print-id`), so the credential
14
+ * was bound to the default machine's row, the service registered there, and the
15
+ * failure OSK-13845 exists to prevent happened anyway: two profiles on one Daemon
16
+ * row and one device-key pin, the second profile's interactive login re-pinning
17
+ * the first one's key (OSK-S-440, profile `aapexp`).
18
+ *
19
+ * Measured, not inferred: the same new profile, in an empty home, answered by
20
+ * two `skrrd` binaries. The current one reports `<hardware>:p-<hash>` and writes
21
+ * `machine-identity.json` and `daemon-identity.json`; the build that was first on
22
+ * the dogfooder's PATH (a checkout `dist` from before OSK-13845, whose version
23
+ * string still reads the checkout's package.json) reports the bare hardware value
24
+ * and writes neither. The profile it left behind had a `machine-identity.json`
25
+ * (the CLI's) and no `daemon-identity.json` after four daemon starts.
26
+ *
27
+ * The fix is to compare, not to guess a version: ask the binary what it will
28
+ * send, compare with what this profile's decided identity is, and refuse BEFORE
29
+ * anything is minted or installed when they differ. Comparing the value is the
30
+ * only test that cannot be wrong about a build that is older than its own
31
+ * version string says.
32
+ *
33
+ * Pure: the I/O (spawning the binary) is in `daemonHandoff.ts`, the copy in
34
+ * `daemon-machine-identity-copy.ts`.
35
+ */
36
+ const machine_uuid_1 = require("@skrr-ai/auth-core/machine-uuid");
37
+ const keychain_1 = require("./keychain");
38
+ /**
39
+ * Null when the daemon will register this profile under the identity it has;
40
+ * otherwise what disagrees.
41
+ *
42
+ * Not applicable to the default profile (its identity is the hardware value in
43
+ * every version), to a hosted runtime (the control plane issues it), or when no
44
+ * profile is named.
45
+ *
46
+ * A daemon that reports NOTHING is a skew, not a pass: this check exists because
47
+ * an older binary cannot say, and "could not confirm" must not read as "fine" on
48
+ * the step that binds a credential to a machine row.
49
+ */
50
+ function daemonMachineIdentitySkew(opts) {
51
+ const profile = opts.profile?.trim();
52
+ if (!profile || profile === keychain_1.DEFAULT_PROFILE)
53
+ return null;
54
+ if ((0, machine_uuid_1.hostedRuntimeIdOverride)('OVERSKY_MACHINE_UUID'))
55
+ return null;
56
+ const expected = (0, machine_uuid_1.getMachineUuidForProfile)(profile);
57
+ const reported = opts.reported?.trim() || null;
58
+ if (reported === expected)
59
+ return null;
60
+ return { profile, expected, reported, binary: opts.binary };
61
+ }
@@ -1,3 +1,4 @@
1
+ import { type DaemonMachineIdentitySkew } from './daemon-machine-identity';
1
2
  /**
2
3
  * Hand the local daemon a credential of its own, right after `skrr login`.
3
4
  *
@@ -40,8 +41,25 @@ export interface DaemonHandoffOutcome {
40
41
  * The two absent cases are separated because they need different work — one
41
42
  * needs a binary fetched, the other only needs the service registered.
42
43
  */
43
- reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused' | 'platform-managed';
44
+ reason?: 'opted-out' | 'brokered' | 'no-binary' | 'legacy-binary' | 'unsupported-daemon' | 'no-service' | 'foreign-root' | 'mint-refused' | 'platform-managed' | 'machine-identity-skew';
45
+ /** Set with `reason: 'machine-identity-skew'`; the caller words it. */
46
+ skew?: DaemonMachineIdentitySkew;
44
47
  }
45
48
  export declare function handOffToLocalDaemon(env?: NodeJS.ProcessEnv, opts?: {
46
49
  loginFlow?: string;
47
50
  }): Promise<DaemonHandoffOutcome>;
51
+ /**
52
+ * Ask the installed daemon which machine identity it will register `profile`
53
+ * under, and say what disagrees with the profile's own (OSK-S-440).
54
+ *
55
+ * For the commands that install or sign in a daemon WITHOUT going through the
56
+ * credential hand-off (`skrr daemon install`, `skrr daemon login`): the same
57
+ * question, asked of the binary they are about to run. Null when the daemon
58
+ * agrees, when the profile is the default one, or when there is no binary to ask
59
+ * (the caller has its own answer for that).
60
+ *
61
+ * A binary that cannot be asked at all - too old to have `accept-handoff`, or
62
+ * one that fails the probe - is reported as a skew with no identity, because the
63
+ * point of asking is not to bind a profile to a machine row on a guess.
64
+ */
65
+ export declare function checkLocalDaemonMachineIdentity(profile: string | null | undefined, binary?: string | null): Promise<DaemonMachineIdentitySkew | null>;
@@ -1,10 +1,12 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handOffToLocalDaemon = handOffToLocalDaemon;
4
+ exports.checkLocalDaemonMachineIdentity = checkLocalDaemonMachineIdentity;
4
5
  const node_child_process_1 = require("node:child_process");
5
6
  const data_provider_1 = require("@skrr-ai/data-provider");
6
7
  const config_1 = require("./config");
7
8
  const daemon_handoff_opt_out_1 = require("./daemon-handoff-opt-out");
9
+ const daemon_machine_identity_1 = require("./daemon-machine-identity");
8
10
  const dedicated_service_1 = require("./dedicated-service");
9
11
  const exec_runtime_binary_1 = require("./exec-runtime-binary");
10
12
  const keychain_1 = require("./keychain");
@@ -275,6 +277,26 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
275
277
  };
276
278
  }
277
279
  const { idSource, machineUuid, daemonServer } = parseIdProvenance(printed.stderr);
280
+ // Before anything is minted or installed: does THIS binary register the profile
281
+ // under the machine identity the profile has? The mint below binds the
282
+ // credential to the machine uuid the daemon just REPORTED, so a binary that
283
+ // reports the hardware value for a profile that has its own would put it on the
284
+ // default profile's row - and the install that follows would run that same
285
+ // binary as a service. Nothing about the id or the provenance would look wrong.
286
+ const skew = (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({
287
+ profile: (0, keychain_1.getActiveProfile)(),
288
+ binary,
289
+ reported: machineUuid,
290
+ });
291
+ if (skew) {
292
+ return {
293
+ status: 'failed',
294
+ reason: 'machine-identity-skew',
295
+ skew,
296
+ detail: `the installed daemon reports machine identity ${skew.reported ?? '(none)'} ` +
297
+ `where profile '${skew.profile}' has ${skew.expected}`,
298
+ };
299
+ }
278
300
  // A well-formed id is not the same as a real daemon. With no service installed
279
301
  // the daemon derives one from whatever directory this process is in, so the id
280
302
  // above names a daemon that does not exist and will not exist under that id if
@@ -354,3 +376,40 @@ async function handOffToLocalDaemon(env = process.env, opts = {}) {
354
376
  }),
355
377
  };
356
378
  }
379
+ /**
380
+ * Ask the installed daemon which machine identity it will register `profile`
381
+ * under, and say what disagrees with the profile's own (OSK-S-440).
382
+ *
383
+ * For the commands that install or sign in a daemon WITHOUT going through the
384
+ * credential hand-off (`skrr daemon install`, `skrr daemon login`): the same
385
+ * question, asked of the binary they are about to run. Null when the daemon
386
+ * agrees, when the profile is the default one, or when there is no binary to ask
387
+ * (the caller has its own answer for that).
388
+ *
389
+ * A binary that cannot be asked at all - too old to have `accept-handoff`, or
390
+ * one that fails the probe - is reported as a skew with no identity, because the
391
+ * point of asking is not to bind a profile to a machine row on a guess.
392
+ */
393
+ async function checkLocalDaemonMachineIdentity(profile, binary = (0, exec_runtime_binary_1.findRuntimeBinary)()) {
394
+ if (!profile || profile === keychain_1.DEFAULT_PROFILE || !binary)
395
+ return null;
396
+ const printed = await runOversky(binary, ['accept-handoff', '--print-id', '--profile', profile], {
397
+ timeoutMs: PROBE_TIMEOUT_MS,
398
+ });
399
+ if (!printed.ok) {
400
+ // A daemon too old to have `accept-handoff` answers "unknown command": it is
401
+ // older than per-profile identities too, which is the skew itself.
402
+ if (/unknown command/i.test(printed.stderr)) {
403
+ return (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({ profile, binary, reported: null });
404
+ }
405
+ // Any other failed probe says nothing about the daemon's identity, and
406
+ // refusing a sign-in on a flaky probe would be a worse failure than the one
407
+ // this guards: let the command run and meet whatever the daemon itself says.
408
+ return null;
409
+ }
410
+ return (0, daemon_machine_identity_1.daemonMachineIdentitySkew)({
411
+ profile,
412
+ binary,
413
+ reported: parseIdProvenance(printed.stderr).machineUuid,
414
+ });
415
+ }
@@ -238,6 +238,77 @@ export declare function updateHarness(id: string, patch: Record<string, unknown>
238
238
  harness?: Harness;
239
239
  }>;
240
240
  export declare function forgetHarness(id: string): Promise<unknown>;
241
+ /**
242
+ * Why a superseded-looking row is KEPT by `harnesses prune`. Declared by the
243
+ * server (`pruneSupersededHarnesses.js`, `REASONS`) and worded once, in
244
+ * `harnessPruneReasonSentence`, so the table and `--json` never disagree.
245
+ */
246
+ export type HarnessPruneReason = 'recent' | 'latest_registration' | 'no_live_sibling' | 'pinned' | 'granted' | 'default' | 'aliased' | 'changed_since_plan';
247
+ export interface HarnessPruneRow {
248
+ id: string;
249
+ daemonId: string | null;
250
+ provider: string | null;
251
+ name: string | null;
252
+ lastSeenAt: string | null;
253
+ machineUuid: string | null;
254
+ /** The live sibling that replaced a row this call removes (or would). */
255
+ supersededBy?: {
256
+ id: string;
257
+ daemonId: string | null;
258
+ lastSeenAt: string | null;
259
+ };
260
+ /** Set on kept rows. */
261
+ reason?: HarnessPruneReason;
262
+ /** For `pinned`: who holds the pin. Other people's agents are a count, never named. */
263
+ detail?: {
264
+ agents?: Array<{
265
+ id: string;
266
+ name: string | null;
267
+ }>;
268
+ moreAgents?: number;
269
+ otherAuthors?: number;
270
+ };
271
+ }
272
+ export interface HarnessPruneResult {
273
+ dryRun: boolean;
274
+ limit: number;
275
+ minUnseenMs: number;
276
+ scan: {
277
+ rows: number;
278
+ complete: boolean;
279
+ };
280
+ /** Rows this call removes (dry run: would remove). */
281
+ remove: HarnessPruneRow[];
282
+ /** Rows this call actually removed; 0 on a dry run. */
283
+ removedCount: number;
284
+ /** Removable rows beyond `limit`; the next call takes them. */
285
+ remaining: number;
286
+ kept: HarnessPruneRow[];
287
+ keptTruncated: boolean;
288
+ counts: Record<string, number>;
289
+ }
290
+ /**
291
+ * Remove the caller's superseded harness rows - or, by default, say which would go.
292
+ *
293
+ * ONE route for both: the server makes the decision once and this only asks for it
294
+ * to be shown (`dryRun: true`, also its own default) or carried out. The caller
295
+ * never lists rows and picks, so there is no client-side rule to drift from the
296
+ * server's. A write, so `apiFetch` gives it an idempotency key and a retried
297
+ * request replays the recorded answer instead of running again.
298
+ */
299
+ export declare function pruneHarnesses(options?: {
300
+ dryRun: boolean;
301
+ limit?: number;
302
+ minUnseenMs?: number;
303
+ }): Promise<HarnessPruneResult>;
304
+ /**
305
+ * `--older-than` takes a whole number with a unit: `90m`, `36h`, `7d`. A bare
306
+ * number is hours. Returns milliseconds. The server never goes below its offline
307
+ * window whatever this says.
308
+ */
309
+ export declare function parseHarnessPruneAge(value: string): number;
310
+ /** One sentence per reason a row was kept. The only place they are worded. */
311
+ export declare function harnessPruneReasonSentence(row: HarnessPruneRow): string;
241
312
  /**
242
313
  * A per-USER display name. Distinct from `updateHarness({name})`, which renames
243
314
  * the harness for everyone it is shared with — this only changes what YOU see,
@@ -13,6 +13,9 @@ exports.listQuotaAccounts = listQuotaAccounts;
13
13
  exports.getQuotaHistory = getQuotaHistory;
14
14
  exports.updateHarness = updateHarness;
15
15
  exports.forgetHarness = forgetHarness;
16
+ exports.pruneHarnesses = pruneHarnesses;
17
+ exports.parseHarnessPruneAge = parseHarnessPruneAge;
18
+ exports.harnessPruneReasonSentence = harnessPruneReasonSentence;
16
19
  exports.setHarnessAlias = setHarnessAlias;
17
20
  exports.setDefaultHarness = setDefaultHarness;
18
21
  exports.mintInstallToken = mintInstallToken;
@@ -173,6 +176,73 @@ async function updateHarness(id, patch) {
173
176
  async function forgetHarness(id) {
174
177
  return (0, api_fetch_1.apiFetch)(`/api/harnesses/${encodeURIComponent(id)}`, { method: 'DELETE' });
175
178
  }
179
+ /**
180
+ * Remove the caller's superseded harness rows - or, by default, say which would go.
181
+ *
182
+ * ONE route for both: the server makes the decision once and this only asks for it
183
+ * to be shown (`dryRun: true`, also its own default) or carried out. The caller
184
+ * never lists rows and picks, so there is no client-side rule to drift from the
185
+ * server's. A write, so `apiFetch` gives it an idempotency key and a retried
186
+ * request replays the recorded answer instead of running again.
187
+ */
188
+ async function pruneHarnesses(options = { dryRun: true }) {
189
+ return (0, api_fetch_1.apiFetch)('/api/harnesses/prune', {
190
+ method: 'POST',
191
+ body: {
192
+ dryRun: options.dryRun,
193
+ ...(options.limit !== undefined ? { limit: options.limit } : {}),
194
+ ...(options.minUnseenMs !== undefined ? { minUnseenMs: options.minUnseenMs } : {}),
195
+ },
196
+ });
197
+ }
198
+ /**
199
+ * `--older-than` takes a whole number with a unit: `90m`, `36h`, `7d`. A bare
200
+ * number is hours. Returns milliseconds. The server never goes below its offline
201
+ * window whatever this says.
202
+ */
203
+ function parseHarnessPruneAge(value) {
204
+ const match = value.trim().match(/^(\d+)\s*(m|h|d)?$/i);
205
+ if (!match) {
206
+ throw new Error(`Invalid --older-than value "${value}". Use a number with m, h or d, such as 90m, 36h or 7d.`);
207
+ }
208
+ const amount = Number(match[1]);
209
+ const unit = (match[2] || 'h').toLowerCase();
210
+ const perUnit = unit === 'd' ? 86_400_000 : unit === 'h' ? 3_600_000 : 60_000;
211
+ return amount * perUnit;
212
+ }
213
+ /** One sentence per reason a row was kept. The only place they are worded. */
214
+ function harnessPruneReasonSentence(row) {
215
+ switch (row.reason) {
216
+ case 'recent':
217
+ return 'seen too recently to be sure it is gone';
218
+ case 'latest_registration':
219
+ return 'the newest registration of its provider on that machine';
220
+ case 'no_live_sibling':
221
+ return 'nothing has replaced it - no daemon on that machine is online for this provider';
222
+ case 'pinned': {
223
+ const names = (row.detail?.agents ?? []).map((a) => a.name ?? a.id);
224
+ const more = row.detail?.moreAgents ?? 0;
225
+ const others = row.detail?.otherAuthors ?? 0;
226
+ const parts = [];
227
+ if (names.length > 0) {
228
+ parts.push(`${names.join(', ')}${more > 0 ? ` and ${more} more` : ''}`);
229
+ }
230
+ if (others > 0)
231
+ parts.push(`${others} agent${others === 1 ? '' : 's'} of other people`);
232
+ return `agents are pinned to it${parts.length > 0 ? ` (${parts.join('; ')})` : ''}`;
233
+ }
234
+ case 'granted':
235
+ return 'it is lent to a workspace';
236
+ case 'default':
237
+ return 'it is your default harness';
238
+ case 'aliased':
239
+ return 'you gave it a name';
240
+ case 'changed_since_plan':
241
+ return 'it changed while this ran (came back online, or was pinned)';
242
+ default:
243
+ return String(row.reason ?? 'kept');
244
+ }
245
+ }
176
246
  /**
177
247
  * A per-USER display name. Distinct from `updateHarness({name})`, which renames
178
248
  * the harness for everyone it is shared with — this only changes what YOU see,
@@ -18,7 +18,7 @@ export type ListEnvelope = {
18
18
  * Documented in docs/cli/SKRR_TASKS.md (List output for agents).
19
19
  */
20
20
  export declare const COMPACT_FIELDS: {
21
- readonly task: readonly ["id", "identifier", "ref", "title", "status", "statusType", "priority", "estimate", "assigneeAgentIds", "assigneeUserIds", "parentId", "spaceId", "projectId", "goalId", "dueDate", "scheduledFor", "labelIds", "tags", "externalKey", "version", "executionMode", "nextAction", "createdAt", "updatedAt", "url"];
21
+ readonly task: readonly ["id", "identifier", "ref", "title", "status", "statusType", "priority", "estimate", "assigneeAgentIds", "assigneeUserIds", "parentId", "spaceId", "projectId", "goalId", "dueDate", "scheduledFor", "labelIds", "tags", "externalKey", "version", "executionMode", "nextAction", "economicsRank", "createdAt", "updatedAt", "url"];
22
22
  readonly space: readonly ["id", "identifier", "title", "status", "parentSpaceId", "leadAgentId", "labelIds", "archivedAt", "createdAt", "updatedAt", "url"];
23
23
  readonly project: readonly ["id", "identifier", "title", "spaceId", "externalKey", "taskCounts", "archivedAt", "createdAt", "updatedAt", "url"];
24
24
  };
@@ -61,6 +61,8 @@ exports.COMPACT_FIELDS = {
61
61
  'version',
62
62
  'executionMode',
63
63
  'nextAction',
64
+ // Present only on a `--rank economics` page: the score the row was sorted by (OSK-16476).
65
+ 'economicsRank',
64
66
  'createdAt',
65
67
  'updatedAt',
66
68
  'url',
@@ -32,9 +32,19 @@ function legacyProfileSharesDefaultMachine() {
32
32
  */
33
33
  function describeLegacySharedProfile(profile) {
34
34
  const next = `${profile}-2`;
35
+ // WHY this profile is on the shared row, from the record that decided it: which
36
+ // file made it "already hold credentials", and when. Absent rather than guessed
37
+ // when the record cannot be read.
38
+ let why = '';
39
+ try {
40
+ why = `${(0, machine_uuid_1.describeMachineIdentity)().sentence} `;
41
+ }
42
+ catch {
43
+ why = '';
44
+ }
35
45
  return [
36
46
  `Profile '${profile}' was set up by an older ${data_provider_1.BRAND.displayName} version that gave every profile on this ` +
37
- 'computer the same machine identity, and it still has it. Signing in here would replace the default ' +
47
+ `computer the same machine identity, and it still has it. ${why}Signing in here would replace the default ` +
38
48
  "profile's device-key pin and sign the default daemon out. Nothing was changed.",
39
49
  `Use a new profile name instead: skrr login --profile ${next}`,
40
50
  `(For the background service alone: skrrd --profile ${next} login)`,
@@ -29,6 +29,16 @@ export declare function describeEstimate(economics: unknown): string;
29
29
  * scored and sorts after every row that is.
30
30
  */
31
31
  export declare function economicsScoreText(economics: unknown): string;
32
+ /** A rank's score as the table prints it: rounded, or `—` when the row is not scored. */
33
+ export declare function scoreText(score: number | null | undefined): string;
34
+ /**
35
+ * What `tasks update` should say about the estimate, read from the server's `economicsChange`
36
+ * (OSK-16476): `null` means the write changed nothing (an identical re-send, or a retraction of
37
+ * an estimate that is not there). `undefined` (an older server, or no estimate in the request) says
38
+ * nothing, so the CLI never guesses. When the estimate was the whole request the caller prints this
39
+ * line INSTEAD of "Updated task"; otherwise it follows it.
40
+ */
41
+ export declare function estimateUnchangedLine(sent: unknown, updated: Record<string, unknown> | undefined): string | null;
32
42
  /**
33
43
  * One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
34
44
  * change and the claim it replaced, so `tasks activity` shows from-what-to-what without `--json`.
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.economicsFromFlags = economicsFromFlags;
4
4
  exports.describeEstimate = describeEstimate;
5
5
  exports.economicsScoreText = economicsScoreText;
6
+ exports.scoreText = scoreText;
7
+ exports.estimateUnchangedLine = estimateUnchangedLine;
6
8
  exports.describeEconomicsChange = describeEconomicsChange;
7
9
  const data_provider_1 = require("@skrr-ai/data-provider");
8
10
  /**
@@ -83,8 +85,28 @@ function describeEstimate(economics) {
83
85
  * scored and sorts after every row that is.
84
86
  */
85
87
  function economicsScoreText(economics) {
86
- const score = (0, data_provider_1.proposalEconomicsRank)(economics).score;
87
- return score === null || !Number.isFinite(score) ? '—' : String(Math.round(score));
88
+ return scoreText((0, data_provider_1.proposalEconomicsRank)(economics).score);
89
+ }
90
+ /** A rank's score as the table prints it: rounded, or `—` when the row is not scored. */
91
+ function scoreText(score) {
92
+ return score === null || score === undefined || !Number.isFinite(score)
93
+ ? '—'
94
+ : String(Math.round(score));
95
+ }
96
+ /**
97
+ * What `tasks update` should say about the estimate, read from the server's `economicsChange`
98
+ * (OSK-16476): `null` means the write changed nothing (an identical re-send, or a retraction of
99
+ * an estimate that is not there). `undefined` (an older server, or no estimate in the request) says
100
+ * nothing, so the CLI never guesses. When the estimate was the whole request the caller prints this
101
+ * line INSTEAD of "Updated task"; otherwise it follows it.
102
+ */
103
+ function estimateUnchangedLine(sent, updated) {
104
+ if (sent === undefined || !updated || updated.economicsChange !== null)
105
+ return null;
106
+ if (sent === null)
107
+ return 'No estimate to retract; nothing changed.';
108
+ const revision = updated.economics?.revision;
109
+ return `Estimate unchanged${revision ? ` (revision ${revision})` : ''}: it already reads ${describeEstimate(updated.economics)}.`;
88
110
  }
89
111
  /**
90
112
  * One line for a `task.economics.*` Activity event (OSK-15365): the revision, the claim after the
@@ -232,6 +232,10 @@ export interface SpaceAssistantResponse {
232
232
  export declare const SCOPE_IMPLIED_REF_KEY: Partial<Record<TriggerScopeKind, string>>;
233
233
  export declare function parseKeyValueFlags(values: string[] | undefined, flagName: string, scopeKind?: string): Record<string, string>;
234
234
  export declare function parseJsonObject(raw: string | undefined, flagName: string): Record<string, unknown> | undefined;
235
+ /**
236
+ * A JSON-array flag value: inline JSON, `@path` (a file), or a bare path to an existing file. A
237
+ * path used to reach `JSON.parse` and fail with "Unexpected token '/'" (OSK-15402).
238
+ */
235
239
  export declare function parseJsonArray(raw: string | undefined, flagName: string): unknown[] | undefined;
236
240
  export declare function buildBuiltinActionPayload(input: {
237
241
  builtinAction?: string;
@@ -332,6 +336,13 @@ export declare function describeScheduledFire(result: Record<string, unknown>):
332
336
  * about `in_progress` once). Only these are success for `triggers fire --strict`.
333
337
  */
334
338
  export declare function fireWasAccepted(result: Record<string, unknown>): boolean;
339
+ /**
340
+ * Why `triggers fire --strict` failed. A strict failure used to print exactly what the exit-0 run
341
+ * printed and then exit 1, so nothing said what the flag had judged (OSK-16474). It names the
342
+ * outcome it refused, the reason, the server's sentence when there is one, and what --strict
343
+ * accepts.
344
+ */
345
+ export declare function describeStrictFireRefusal(result: Record<string, unknown>): string;
335
346
  /**
336
347
  * The lines a fire or replay owes beyond `outcome=… reason=…` (OSK-15494): the sentence the
337
348
  * server wrote ("Replayed fire … resolved a step that was not waiting when that fire ran: