@skrr-ai/cli 0.1.43 → 0.1.44

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 +2 -0
  2. package/dist/base-command.js +1 -0
  3. package/dist/commands/balance/index.js +1 -1
  4. package/dist/commands/balance/show.d.ts +32 -0
  5. package/dist/commands/balance/show.js +74 -3
  6. package/dist/commands/code/handover.d.ts +1 -0
  7. package/dist/commands/code/handover.js +4 -0
  8. package/dist/commands/code/jobs/run.d.ts +1 -0
  9. package/dist/commands/code/jobs/run.js +4 -0
  10. package/dist/commands/daemon/install.js +7 -0
  11. package/dist/commands/harnesses/leases/show.js +14 -0
  12. package/dist/commands/instructions/install.d.ts +22 -0
  13. package/dist/commands/instructions/install.js +83 -7
  14. package/dist/commands/instructions/list.js +5 -0
  15. package/dist/commands/instructions/show.d.ts +6 -0
  16. package/dist/commands/instructions/show.js +34 -1
  17. package/dist/commands/instructions/status.js +17 -1
  18. package/dist/commands/payments/wallet.js +2 -2
  19. package/dist/commands/tasks/create.d.ts +6 -0
  20. package/dist/commands/tasks/create.js +26 -3
  21. package/dist/commands/tasks/labels/attach.js +4 -0
  22. package/dist/commands/tasks/list.d.ts +1 -0
  23. package/dist/commands/tasks/list.js +9 -0
  24. package/dist/commands/tasks/show.d.ts +23 -0
  25. package/dist/commands/tasks/show.js +61 -1
  26. package/dist/commands/tasks/update.js +12 -0
  27. package/dist/commands/views/create.js +12 -2
  28. package/dist/commands/views/list.js +3 -2
  29. package/dist/commands/views/show.js +2 -0
  30. package/dist/lib/agentic-stream.d.ts +10 -4
  31. package/dist/lib/agentic-stream.js +25 -11
  32. package/dist/lib/cli-installers.js +9 -1
  33. package/dist/lib/daemon-setup.d.ts +18 -1
  34. package/dist/lib/daemon-setup.js +33 -1
  35. package/dist/lib/first-party-harness-agent.d.ts +16 -1
  36. package/dist/lib/first-party-harness-agent.js +41 -12
  37. package/dist/lib/first-party-harness-doctor.js +34 -16
  38. package/dist/lib/first-party-harness.d.ts +18 -11
  39. package/dist/lib/first-party-harness.js +26 -21
  40. package/dist/lib/harnesses.d.ts +6 -0
  41. package/dist/lib/instruction-input.d.ts +21 -0
  42. package/dist/lib/instruction-input.js +30 -0
  43. package/dist/lib/instruction-provenance.d.ts +54 -0
  44. package/dist/lib/instruction-provenance.js +84 -0
  45. package/dist/lib/task-view-render.d.ts +14 -0
  46. package/dist/lib/task-view-render.js +55 -0
  47. package/dist/lib/tasks.d.ts +18 -0
  48. package/dist/lib/tasks.js +22 -1
  49. package/dist/lib/views/vocabulary.d.ts +1 -1
  50. package/dist/lib/views/vocabulary.js +3 -1
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +75 -24
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +143 -34
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.d.ts +21 -1
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/legacyStatePreflight.js +75 -19
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +75 -24
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +138 -34
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.d.ts +21 -1
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/legacyStatePreflight.js +74 -19
  59. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  60. package/dist/node_modules/@skrr-ai/data-provider/index.js +3516 -3513
  61. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  62. package/oclif.manifest.json +21649 -21626
  63. package/package.json +3 -3
@@ -55,18 +55,26 @@ exports.resolveFirstPartyHarnessAgent = resolveFirstPartyHarnessAgent;
55
55
  exports.readConfiguredAgentId = readConfiguredAgentId;
56
56
  exports.setConfiguredAgentId = setConfiguredAgentId;
57
57
  exports.clearConfiguredAgentId = clearConfiguredAgentId;
58
+ exports.collectRosterPages = collectRosterPages;
58
59
  const auth_core_1 = require("@skrr-ai/auth-core");
59
60
  const data_provider_1 = require("@skrr-ai/data-provider");
60
61
  const api_fetch_1 = require("./api-fetch");
61
62
  const config_1 = require("./config");
62
63
  const first_party_harness_managed_1 = require("./first-party-harness-managed");
64
+ /** Page size for one `/api/agents` roster request. */
65
+ const ROSTER_PAGE_SIZE = 100;
63
66
  /**
64
- * How many agents the refusal is allowed to look at.
67
+ * The most agents the roster will read, across pages.
65
68
  *
66
- * A bound rather than pagination: this list exists to answer "did you mean one of
67
- * these?", and a user with more agents than this is a user who will pass the flag.
69
+ * The roster is not only the refusal's "did you mean" list. Resolution also finds
70
+ * the user's EXISTING default agent in it (`createDefault: false`) and matches a
71
+ * configured agent by name, so one fixed page answered "you have no default agent"
72
+ * for an account whose default was agent 198 of 210 (found by the end-to-end run
73
+ * on 2026-09-17). Pages are followed until the server says there are no more, up
74
+ * to this bound, which keeps a pathological account from turning one session
75
+ * start into an unbounded crawl.
68
76
  */
69
- const ROSTER_LIMIT = 100;
77
+ const ROSTER_MAX = 1000;
70
78
  /** How many candidates a refusal prints before it summarizes the rest. */
71
79
  const CANDIDATES_SHOWN = 12;
72
80
  class FirstPartyHarnessAgentError extends Error {
@@ -369,14 +377,35 @@ function writeConfigKey(id) {
369
377
  * `fields=summary` drops the prompt-sized fields; this needs an id and a name.
370
378
  */
371
379
  async function listAgents(signal) {
372
- const response = await (0, api_fetch_1.apiFetch)(`/api/agents?limit=${ROSTER_LIMIT}&fields=summary&requiredPermission=${data_provider_1.PermissionBits.EDIT}`, signal ? { signal } : {});
373
- if (Array.isArray(response))
374
- return response;
375
- if (Array.isArray(response?.data))
376
- return response.data;
377
- if (Array.isArray(response?.agents))
378
- return response.agents;
379
- return [];
380
+ return collectRosterPages((cursor) => (0, api_fetch_1.apiFetch)(`/api/agents?limit=${ROSTER_PAGE_SIZE}&fields=summary&requiredPermission=${data_provider_1.PermissionBits.EDIT}` +
381
+ (cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''), signal ? { signal } : {}));
382
+ }
383
+ /**
384
+ * Read roster pages until the server reports no more, up to `max` rows.
385
+ *
386
+ * A page that does not say `has_more` with a usable `after` cursor ends the walk,
387
+ * so an older server that ignores `cursor` (or returns a bare array) costs exactly
388
+ * the one request it always did.
389
+ */
390
+ async function collectRosterPages(fetchPage, max = ROSTER_MAX) {
391
+ const rows = [];
392
+ let cursor;
393
+ do {
394
+ const response = await fetchPage(cursor);
395
+ const page = Array.isArray(response)
396
+ ? response
397
+ : Array.isArray(response?.data)
398
+ ? response.data
399
+ : Array.isArray(response?.agents)
400
+ ? response.agents
401
+ : [];
402
+ rows.push(...page);
403
+ const next = !Array.isArray(response) && response?.has_more === true && typeof response.after === 'string'
404
+ ? response.after
405
+ : undefined;
406
+ cursor = next && next !== cursor && page.length > 0 ? next : undefined;
407
+ } while (cursor && rows.length < max);
408
+ return rows.slice(0, max);
380
409
  }
381
410
  /**
382
411
  * The caller's own user id, memoized FOR THIS PROCESS ONLY.
@@ -263,8 +263,16 @@ function stateDirCheck(cwd, env) {
263
263
  remedy: `This is a build bug — report it. Do not run ${auth_core_1.FIRST_PARTY_HARNESS.displayName} from this directory.`,
264
264
  };
265
265
  }
266
+ // The engine still reads a project directory under a legacy namespace, after the
267
+ // current one (OSK-8674); one that exists is worth naming, or "not created yet"
268
+ // reads as "no project config" on a repository that has some.
269
+ const legacyProjects = (0, auth_core_1.firstPartyHarnessEngineProjectDirectories)()
270
+ .slice(1)
271
+ .map((directory) => node_path_1.default.join(node_path_1.default.resolve(cwd), directory))
272
+ .filter((directory) => (0, node_fs_1.existsSync)(directory));
266
273
  const parts = [
267
274
  `project: ${project}${(0, node_fs_1.existsSync)(project) ? '' : ' (not created yet)'}`,
275
+ ...legacyProjects.map((directory) => `also read (legacy): ${directory}`),
268
276
  `user: ${user}${(0, node_fs_1.existsSync)(user) ? '' : ' (not created yet)'}`,
269
277
  ];
270
278
  return { name: 'state-paths', status: 'ok', detail: parts.join('; ') };
@@ -306,21 +314,29 @@ function compatCheck(cwd, env) {
306
314
  // session only when the daemon links them in, which `skrr skills local` shows.
307
315
  const root = node_path_1.default.resolve(cwd);
308
316
  const home = env.HOME || (0, node_os_1.homedir)();
309
- const projectDir = auth_core_1.FIRST_PARTY_HARNESS_ENGINE.projectDirectory;
310
- const noCompatPrompts = Boolean(env[auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.noCompatPrompts]);
317
+ // The engine reads its variables under every namespace it has had, current first.
318
+ const noCompatPrompts = (0, auth_core_1.readFirstPartyHarnessEngineEnv)(env, 'noCompatPrompts').name;
311
319
  const engineHomeDir = (0, first_party_harness_1.engineInstructionHome)(env);
312
320
  const shown = (abs) => abs.startsWith(root + node_path_1.default.sep)
313
321
  ? node_path_1.default.relative(root, abs)
314
322
  : abs.startsWith(home + node_path_1.default.sep)
315
323
  ? `~/${node_path_1.default.relative(home, abs)}`
316
324
  : abs;
325
+ // The engine's project instruction file under every project directory it reads,
326
+ // current first. It reads the first that exists, so a legacy one beside a current
327
+ // one is present and NOT read — said, rather than listed as if it applied.
328
+ const projectInstructions = (0, auth_core_1.firstPartyHarnessEngineProjectDirectories)().map((directory) => node_path_1.default.join(root, directory, 'AGENTS.md'));
317
329
  const declared = [
318
- {
319
- abs: node_path_1.default.join(root, projectDir, 'AGENTS.md'),
320
- kind: 'instructions',
321
- owner: 'engine',
322
- scope: 'project',
323
- },
330
+ ...projectInstructions.map((abs, i) => {
331
+ const newer = projectInstructions.slice(0, i).find((candidate) => (0, node_fs_1.existsSync)(candidate));
332
+ return {
333
+ abs,
334
+ kind: 'instructions',
335
+ owner: 'engine',
336
+ scope: 'project',
337
+ ...(newer ? { disabled: `superseded by ${node_path_1.default.relative(root, newer)}` } : {}),
338
+ };
339
+ }),
324
340
  {
325
341
  abs: node_path_1.default.join(engineHomeDir, 'AGENTS.md'),
326
342
  kind: 'instructions',
@@ -338,14 +354,14 @@ function compatCheck(cwd, env) {
338
354
  kind: 'instructions',
339
355
  owner: 'compatibility',
340
356
  scope: 'project',
341
- ...(noCompatPrompts ? { disabled: auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.noCompatPrompts } : {}),
357
+ ...(noCompatPrompts ? { disabled: noCompatPrompts } : {}),
342
358
  },
343
359
  {
344
360
  abs: node_path_1.default.join(home, '.claude', 'CLAUDE.md'),
345
361
  kind: 'instructions',
346
362
  owner: 'compatibility',
347
363
  scope: 'user',
348
- ...(noCompatPrompts ? { disabled: auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.noCompatPrompts } : {}),
364
+ ...(noCompatPrompts ? { disabled: noCompatPrompts } : {}),
349
365
  },
350
366
  {
351
367
  abs: node_path_1.default.join(root, '.agents', 'skills'),
@@ -494,15 +510,17 @@ const EGRESS_DENIED_HOSTS = [
494
510
  'social-cards.sst.dev',
495
511
  'opencode.internal',
496
512
  ];
497
- /** The ENGINE reads this one under its own name; the platform only reports it. */
498
- const EGRESS_OVERRIDE_ENV = auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.allowUpstreamEgress;
513
+ /**
514
+ * The ENGINE reads this one under its own names, current namespace first; the
515
+ * platform only reports it, naming the variable that actually supplied it.
516
+ */
499
517
  function egressCheck(env) {
500
- const override = env[EGRESS_OVERRIDE_ENV];
501
- if (override === '1' || override === 'true') {
518
+ const override = (0, auth_core_1.readFirstPartyHarnessEngineEnv)(env, 'allowUpstreamEgress');
519
+ if (override.value === '1' || override.value === 'true') {
502
520
  return {
503
521
  name: 'upstream-egress',
504
522
  status: 'warn',
505
- detail: `${EGRESS_OVERRIDE_ENV} is set — ${auth_core_1.FIRST_PARTY_HARNESS.displayName} may reach upstream hosted services`,
523
+ detail: `${override.name} is set — ${auth_core_1.FIRST_PARTY_HARNESS.displayName} may reach upstream hosted services`,
506
524
  remedy: 'Unset it unless you are deliberately debugging upstream behaviour. Session content, ' +
507
525
  'repository names, and account tokens can leave for a third party while it is set.',
508
526
  };
@@ -746,7 +764,7 @@ function sessionAuthorityCheck(env) {
746
764
  remedy: 'Intended for local use, and nothing to fix. Worth knowing because it is NOT the same ' +
747
765
  'trust plane as a daemon-dispatched run — and neither plane is an OS-level sandbox ' +
748
766
  '(no seccomp, container, or namespace). An untrusted autonomous workload needs a hosted ' +
749
- `or container profile; see docs/${auth_core_1.FIRST_PARTY_HARNESS_ENGINE.appName}/threat-model.md in the engine's repository.`,
767
+ "or container profile; see the threat model in the engine's repository.",
750
768
  };
751
769
  }
752
770
  }
@@ -47,11 +47,13 @@ export declare function engineHome(env?: NodeJS.ProcessEnv): string;
47
47
  * The user-level instruction root the engine owns.
48
48
  *
49
49
  * Read under the platform's neutral variable first, then under the variable the
50
- * ENGINE itself reads (`FIRST_PARTY_HARNESS_ENGINE.env.home`). The second is not a
51
- * legacy fallback: it is the engine's own name, the engine honours it whatever the
52
- * platform does, and a CLI that ignored it would report one instruction root while
53
- * the engine it launches used another. {@link engineOwnedEnv} closes the other
54
- * direction, handing a neutral-variable value to the engine under its own name.
50
+ * ENGINE itself reads, under every name it reads it by, current namespace first
51
+ * (`readFirstPartyHarnessEngineEnv(env, 'home')`; the namespace moved in OSK-8674
52
+ * and the engine reads both). That is not a legacy fallback: it is the engine's own
53
+ * variable, the engine honours it whatever the platform does, and a CLI that
54
+ * ignored it would report one instruction root while the engine it launches used
55
+ * another. {@link engineOwnedEnv} closes the other direction, handing a
56
+ * neutral-variable value to the engine under its own names.
55
57
  */
56
58
  export declare function engineInstructionHome(env?: NodeJS.ProcessEnv): string;
57
59
  /**
@@ -378,12 +380,17 @@ export declare function engineSpawnEnv(env: NodeJS.ProcessEnv, extraEnv: Record<
378
380
  * `skrr code doctor` honour it while the engine it launches ignored it — two
379
381
  * answers to "where do my instructions live" from one command.
380
382
  *
381
- * Only when the neutral variable supplied the value: a value set only under the
382
- * engine's own name is inherited already, and rewriting it would only replace the
383
- * user's spelling of a path with ours. When both are set the neutral one wins, as
384
- * it does in {@link engineInstructionHome}, so the engine is handed that one. The
385
- * resolved path is passed rather than the raw value, so the engine and the doctor
386
- * agree on `~` expansion too.
383
+ * Under EVERY name the engine has read (`firstPartyHarnessEngineEnvEntries`). The
384
+ * engine's namespace moved (OSK-8674), and the binary this CLI launches may be a
385
+ * build from either side of that move; a value handed over under one name only
386
+ * reaches half of them.
387
+ *
388
+ * When the neutral variable supplied the value, every engine name gets it: the
389
+ * neutral one wins, as it does in {@link engineInstructionHome}. When only an
390
+ * engine name did, only the engine names that are UNSET are filled in — the user's
391
+ * own values are inherited already, and two different values the user set under
392
+ * two names are theirs to keep. The resolved path is passed rather than the raw
393
+ * value, so the engine and the doctor agree on `~` expansion too.
387
394
  *
388
395
  * Not a credential and never one: it cannot reintroduce anything the sanitizer
389
396
  * strips, and `extraEnv` still layers over it.
@@ -84,14 +84,17 @@ function engineHome(env = process.env) {
84
84
  * The user-level instruction root the engine owns.
85
85
  *
86
86
  * Read under the platform's neutral variable first, then under the variable the
87
- * ENGINE itself reads (`FIRST_PARTY_HARNESS_ENGINE.env.home`). The second is not a
88
- * legacy fallback: it is the engine's own name, the engine honours it whatever the
89
- * platform does, and a CLI that ignored it would report one instruction root while
90
- * the engine it launches used another. {@link engineOwnedEnv} closes the other
91
- * direction, handing a neutral-variable value to the engine under its own name.
87
+ * ENGINE itself reads, under every name it reads it by, current namespace first
88
+ * (`readFirstPartyHarnessEngineEnv(env, 'home')`; the namespace moved in OSK-8674
89
+ * and the engine reads both). That is not a legacy fallback: it is the engine's own
90
+ * variable, the engine honours it whatever the platform does, and a CLI that
91
+ * ignored it would report one instruction root while the engine it launches used
92
+ * another. {@link engineOwnedEnv} closes the other direction, handing a
93
+ * neutral-variable value to the engine under its own names.
92
94
  */
93
95
  function engineInstructionHome(env = process.env) {
94
- const configured = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value ?? engineHomeVariable(env);
96
+ const configured = (0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value ??
97
+ (0, auth_core_1.readFirstPartyHarnessEngineEnv)(env, 'home').value;
95
98
  // Falls through to `engineHome()` rather than re-deriving `~/.skrr` — the two
96
99
  // describe the same root, and re-deriving it is what made them disagree under
97
100
  // `OVERSKY_CONFIG_DIR` (OSK-300).
@@ -103,11 +106,6 @@ function engineInstructionHome(env = process.env) {
103
106
  return node_path_1.default.join((0, node_os_1.homedir)(), configured.slice(2));
104
107
  return node_path_1.default.resolve(configured);
105
108
  }
106
- /** The engine's own home variable, trimmed; undefined when unset or blank. */
107
- function engineHomeVariable(env) {
108
- const raw = env[auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.home];
109
- return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : undefined;
110
- }
111
109
  /**
112
110
  * The managed install location for one spelling of the binary (canonical by
113
111
  * default) — where the daemon, the Desktop app, and
@@ -142,7 +140,7 @@ function legacyManagedEnginePaths(env = process.env) {
142
140
  function legacyManagedEngineBinDirs(env = process.env) {
143
141
  if ((0, auth_core_1.isConfigRootOverridden)(env))
144
142
  return [];
145
- return [(0, auth_core_1.firstPartyHarnessHomeDirname)(), ...auth_core_1.FIRST_PARTY_HARNESS_ENGINE.legacyAppNames].map((home) => node_path_1.default.join((0, config_1.priorConfigDir)(), home, 'bin'));
143
+ return (0, auth_core_1.firstPartyHarnessEngineHomeDirnames)().map((home) => node_path_1.default.join((0, config_1.priorConfigDir)(), home, 'bin'));
146
144
  }
147
145
  /**
148
146
  * Unmanaged well-known locations, checked after PATH.
@@ -606,22 +604,29 @@ mode) {
606
604
  * `skrr code doctor` honour it while the engine it launches ignored it — two
607
605
  * answers to "where do my instructions live" from one command.
608
606
  *
609
- * Only when the neutral variable supplied the value: a value set only under the
610
- * engine's own name is inherited already, and rewriting it would only replace the
611
- * user's spelling of a path with ours. When both are set the neutral one wins, as
612
- * it does in {@link engineInstructionHome}, so the engine is handed that one. The
613
- * resolved path is passed rather than the raw value, so the engine and the doctor
614
- * agree on `~` expansion too.
607
+ * Under EVERY name the engine has read (`firstPartyHarnessEngineEnvEntries`). The
608
+ * engine's namespace moved (OSK-8674), and the binary this CLI launches may be a
609
+ * build from either side of that move; a value handed over under one name only
610
+ * reaches half of them.
611
+ *
612
+ * When the neutral variable supplied the value, every engine name gets it: the
613
+ * neutral one wins, as it does in {@link engineInstructionHome}. When only an
614
+ * engine name did, only the engine names that are UNSET are filled in — the user's
615
+ * own values are inherited already, and two different values the user set under
616
+ * two names are theirs to keep. The resolved path is passed rather than the raw
617
+ * value, so the engine and the doctor agree on `~` expansion too.
615
618
  *
616
619
  * Not a credential and never one: it cannot reintroduce anything the sanitizer
617
620
  * strips, and `extraEnv` still layers over it.
618
621
  */
619
622
  function engineOwnedEnv(env) {
620
- const out = {};
623
+ const everyName = (value) => (0, auth_core_1.firstPartyHarnessEngineEnvEntries)('home', value);
621
624
  if ((0, auth_core_1.readFirstPartyHarnessEnv)(env, 'home').value) {
622
- out[auth_core_1.FIRST_PARTY_HARNESS_ENGINE.env.home] = engineInstructionHome(env);
625
+ return everyName(engineInstructionHome(env));
623
626
  }
624
- return out;
627
+ if (!(0, auth_core_1.readFirstPartyHarnessEngineEnv)(env, 'home').value)
628
+ return {};
629
+ return Object.fromEntries(Object.entries(everyName(engineInstructionHome(env))).filter(([name]) => !(typeof env[name] === 'string' && env[name].trim() !== '')));
625
630
  }
626
631
  async function execEngine(args, env = process.env, options = {}) {
627
632
  // Move an engine installed under an older binary name onto the canonical one,
@@ -101,6 +101,12 @@ export interface HarnessLease {
101
101
  machineProduct?: string;
102
102
  machineProductLabel?: string;
103
103
  displayName?: string;
104
+ /**
105
+ * Who pays: the workspace whose account the lease bills, or `null` for the
106
+ * owner's personal account (`MachineLease.toSafeJSON`). Absent from a server
107
+ * that predates it, which is not the same as personal.
108
+ */
109
+ workspaceId?: string | null;
104
110
  state?: string;
105
111
  stateReason?: string;
106
112
  lifecycleSemantics?: string;
@@ -76,6 +76,27 @@ export declare function describeTargetSync(target: {
76
76
  updatedAt?: string;
77
77
  driftCount?: number;
78
78
  }, now?: number, liveDaemonIds?: string[]): string;
79
+ /**
80
+ * The whole error of every failed target, and what to do about it.
81
+ *
82
+ * The STATUS column caps at 78 characters, so a daemon error that names a path
83
+ * was cut mid-path — "must be inside a Git repository: /private/…" — and the
84
+ * table offered nothing past it: no remedy, and no way to learn that
85
+ * `instructions remove` was the way out (OSK-4626). The column keeps its cap;
86
+ * the full sentence and the next step go below the table, the way the re-bind
87
+ * commands do.
88
+ *
89
+ * The daemon retries a failed target on its next sync (every five minutes, or
90
+ * sooner on any change), so fixing the cause is itself a remedy and needs no
91
+ * command. A failed REMOVAL is the other case: it can only be abandoned when
92
+ * its daemon is gone, which is the one place `purge-target --force` belongs.
93
+ */
94
+ export declare function failedTargetLines(targets: Array<{
95
+ id: string;
96
+ status?: string;
97
+ desiredState?: string;
98
+ lastError?: string;
99
+ }>, bin?: string): string[];
79
100
  /**
80
101
  * The command that re-binds a stalled target to the daemon now running on its
81
102
  * machine, or null when no such daemon is live.
@@ -7,6 +7,7 @@ exports.resolveBundleLabel = resolveBundleLabel;
7
7
  exports.parseDaemonId = parseDaemonId;
8
8
  exports.sameMachineDaemon = sameMachineDaemon;
9
9
  exports.describeTargetSync = describeTargetSync;
10
+ exports.failedTargetLines = failedTargetLines;
10
11
  exports.rebindCommandFor = rebindCommandFor;
11
12
  const promises_1 = require("node:fs/promises");
12
13
  async function readStdin() {
@@ -188,6 +189,35 @@ function describeTargetSync(target, now = Date.now(), liveDaemonIds = []) {
188
189
  }
189
190
  return `${base} — stalled ${days}d; daemon may be gone: remove, then purge-target --force`;
190
191
  }
192
+ /**
193
+ * The whole error of every failed target, and what to do about it.
194
+ *
195
+ * The STATUS column caps at 78 characters, so a daemon error that names a path
196
+ * was cut mid-path — "must be inside a Git repository: /private/…" — and the
197
+ * table offered nothing past it: no remedy, and no way to learn that
198
+ * `instructions remove` was the way out (OSK-4626). The column keeps its cap;
199
+ * the full sentence and the next step go below the table, the way the re-bind
200
+ * commands do.
201
+ *
202
+ * The daemon retries a failed target on its next sync (every five minutes, or
203
+ * sooner on any change), so fixing the cause is itself a remedy and needs no
204
+ * command. A failed REMOVAL is the other case: it can only be abandoned when
205
+ * its daemon is gone, which is the one place `purge-target --force` belongs.
206
+ */
207
+ function failedTargetLines(targets, bin = 'skrr') {
208
+ const failed = targets.filter((target) => target.status === 'error');
209
+ if (failed.length === 0)
210
+ return [];
211
+ const lines = ['', 'Failed targets:'];
212
+ for (const target of failed) {
213
+ const removing = target.desiredState === 'removed';
214
+ lines.push(` ${target.id} — ${removing ? 'removal failed' : 'install failed'}: ${target.lastError || 'no reason reported'}`);
215
+ lines.push(removing
216
+ ? ` Fix the cause and its daemon retries on the next sync. If that daemon is gone for good: \`${bin} instructions purge-target ${target.id} --force\``
217
+ : ` Fix the cause and its daemon retries on the next sync, or drop the target: \`${bin} instructions remove ${target.id}\``);
218
+ }
219
+ return lines;
220
+ }
191
221
  /**
192
222
  * The command that re-binds a stalled target to the daemon now running on its
193
223
  * machine, or null when no such daemon is live.
@@ -0,0 +1,54 @@
1
+ /**
2
+ * instruction-provenance.ts — tell a prompt's two version counters apart.
3
+ *
4
+ * A managed instruction prompt has its OWN version counter (1, 2, 3 — every
5
+ * saved body), and a prompt installed from a built-in template also carries
6
+ * the TEMPLATE release each body came from (1, 2, 5, 17 — skipping whatever an
7
+ * install never received). Every table labelled both `v`, and they agree only
8
+ * by coincidence: bundle v3 was built-in v5 on the installation that found this
9
+ * (dogfood OSK-4647), so "the preset went to v5" read as two versions behind.
10
+ * The provenance that settles it (`sourceTemplateKey`/`sourceTemplateVersion`)
11
+ * was reachable through `--json` alone.
12
+ *
13
+ * So the prompt's counter stays `v<n>`, and a template release is always
14
+ * written `built-in v<n>` — never a bare `v` that could be either.
15
+ */
16
+ export interface TemplateProvenance {
17
+ sourceTemplateKey?: string;
18
+ sourceTemplateVersion?: number;
19
+ }
20
+ /**
21
+ * `built-in v5` for a body taken from a built-in template, `built-in` when the
22
+ * row is recognised as one but its release was never derivable (the provenance
23
+ * backfill stamps the key without inventing a number), and `` for a body that
24
+ * was not taken from a template at all.
25
+ */
26
+ export declare function builtInLabel(version: TemplateProvenance | null | undefined): string;
27
+ interface VersionLike extends TemplateProvenance {
28
+ versionNumber?: number;
29
+ }
30
+ interface BundleLike extends TemplateProvenance {
31
+ currentVersionNumber?: number;
32
+ currentVersion?: VersionLike | null;
33
+ }
34
+ /**
35
+ * The APPLIED cell of `instructions status`: `v3 (built-in v5)` when the
36
+ * applied body is the prompt's current one and that one came from a template.
37
+ * Only then is the release known without fetching the version history, so any
38
+ * other case says `v3` and nothing it cannot back.
39
+ */
40
+ export declare function appliedVersionLabel(appliedVersionNumber: number | undefined, bundle: BundleLike | undefined): string;
41
+ /**
42
+ * One sentence for `instructions show`: which built-in template a prompt comes
43
+ * from, whether its current body IS a built-in release, and whether a newer
44
+ * release exists — the question no command answered before. `STATUS: available`
45
+ * on a version row means "not archived", not "newer than yours".
46
+ *
47
+ * `latestTemplateVersion` is the release this server would install today;
48
+ * undefined when it could not be read, in which case nothing is claimed about
49
+ * it. Returns null for a prompt that was not installed from a template.
50
+ */
51
+ export declare function describeTemplateProvenance(bundle: BundleLike & {
52
+ currentVersionNumber: number;
53
+ }, latestTemplateVersion?: number): string | null;
54
+ export {};
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * instruction-provenance.ts — tell a prompt's two version counters apart.
4
+ *
5
+ * A managed instruction prompt has its OWN version counter (1, 2, 3 — every
6
+ * saved body), and a prompt installed from a built-in template also carries
7
+ * the TEMPLATE release each body came from (1, 2, 5, 17 — skipping whatever an
8
+ * install never received). Every table labelled both `v`, and they agree only
9
+ * by coincidence: bundle v3 was built-in v5 on the installation that found this
10
+ * (dogfood OSK-4647), so "the preset went to v5" read as two versions behind.
11
+ * The provenance that settles it (`sourceTemplateKey`/`sourceTemplateVersion`)
12
+ * was reachable through `--json` alone.
13
+ *
14
+ * So the prompt's counter stays `v<n>`, and a template release is always
15
+ * written `built-in v<n>` — never a bare `v` that could be either.
16
+ */
17
+ Object.defineProperty(exports, "__esModule", { value: true });
18
+ exports.builtInLabel = builtInLabel;
19
+ exports.appliedVersionLabel = appliedVersionLabel;
20
+ exports.describeTemplateProvenance = describeTemplateProvenance;
21
+ /**
22
+ * `built-in v5` for a body taken from a built-in template, `built-in` when the
23
+ * row is recognised as one but its release was never derivable (the provenance
24
+ * backfill stamps the key without inventing a number), and `` for a body that
25
+ * was not taken from a template at all.
26
+ */
27
+ function builtInLabel(version) {
28
+ if (!version?.sourceTemplateKey)
29
+ return '';
30
+ const release = version.sourceTemplateVersion;
31
+ return Number.isInteger(release) && Number(release) > 0 ? `built-in v${release}` : 'built-in';
32
+ }
33
+ /**
34
+ * The APPLIED cell of `instructions status`: `v3 (built-in v5)` when the
35
+ * applied body is the prompt's current one and that one came from a template.
36
+ * Only then is the release known without fetching the version history, so any
37
+ * other case says `v3` and nothing it cannot back.
38
+ */
39
+ function appliedVersionLabel(appliedVersionNumber, bundle) {
40
+ if (!appliedVersionNumber)
41
+ return '-';
42
+ const base = `v${appliedVersionNumber}`;
43
+ const current = bundle?.currentVersion;
44
+ if (!current || current.versionNumber !== appliedVersionNumber)
45
+ return base;
46
+ const builtIn = builtInLabel(current);
47
+ return builtIn ? `${base} (${builtIn})` : base;
48
+ }
49
+ /**
50
+ * One sentence for `instructions show`: which built-in template a prompt comes
51
+ * from, whether its current body IS a built-in release, and whether a newer
52
+ * release exists — the question no command answered before. `STATUS: available`
53
+ * on a version row means "not archived", not "newer than yours".
54
+ *
55
+ * `latestTemplateVersion` is the release this server would install today;
56
+ * undefined when it could not be read, in which case nothing is claimed about
57
+ * it. Returns null for a prompt that was not installed from a template.
58
+ */
59
+ function describeTemplateProvenance(bundle, latestTemplateVersion) {
60
+ if (!bundle.sourceTemplateKey)
61
+ return null;
62
+ const current = bundle.currentVersion;
63
+ const head = `From built-in template ${bundle.sourceTemplateKey}.`;
64
+ if (!current)
65
+ return head;
66
+ const release = current.sourceTemplateKey ? current.sourceTemplateVersion : undefined;
67
+ const version = `v${bundle.currentVersionNumber}`;
68
+ const latestKnown = Number.isInteger(latestTemplateVersion);
69
+ if (current.sourceTemplateKey) {
70
+ const builtIn = builtInLabel(current);
71
+ if (!latestKnown || !Number.isInteger(release))
72
+ return `${head} ${version} is ${builtIn}.`;
73
+ if (Number(latestTemplateVersion) > Number(release)) {
74
+ return `${head} ${version} is ${builtIn}; built-in v${latestTemplateVersion} is newer.`;
75
+ }
76
+ return `${head} ${version} is ${builtIn}, the latest.`;
77
+ }
78
+ // Newer releases are promoted only onto a prompt whose body is still a
79
+ // built-in one, so an edit is exactly when "is there a newer one" matters.
80
+ const edited = `${head} ${version} is your own edit, so newer built-in releases are not applied to it`;
81
+ return latestKnown
82
+ ? `${edited}; the latest is built-in v${latestTemplateVersion}.`
83
+ : `${edited}.`;
84
+ }
@@ -13,12 +13,15 @@
13
13
  * go.
14
14
  */
15
15
  export interface TaskViewFilters {
16
+ quickFilter?: string;
16
17
  statuses?: string[];
18
+ statusTypes?: string[];
17
19
  priorities?: string[];
18
20
  assignees?: string[];
19
21
  labels?: string[];
20
22
  goalIds?: string[];
21
23
  projectIds?: string[];
24
+ cycleIds?: string[];
22
25
  search?: string;
23
26
  }
24
27
  export interface TaskViewLike {
@@ -69,3 +72,14 @@ export declare function immutableViewPatchKeys(body: Record<string, unknown>): s
69
72
  * than listing six empty ones.
70
73
  */
71
74
  export declare function describeTaskView(view: TaskViewLike): string[];
75
+ /**
76
+ * One line saying what a view SELECTS, for a table cell.
77
+ *
78
+ * `views list` had a SELECTION column whose value was the word `criteria` for
79
+ * every view that was not a pinned task list — a kind label sitting where the
80
+ * reader looks for an answer (dogfood OSK-4646). Values that fit are named
81
+ * (statuses, priorities, the quick filter, the search); ids are counted, since
82
+ * a row of UUIDs answers nothing at a glance and `views show` prints them in
83
+ * full.
84
+ */
85
+ export declare function summarizeTaskViewSelection(view: TaskViewLike): string;
@@ -18,6 +18,12 @@ exports.EDITABLE_VIEW_PATCH_FIELDS = void 0;
18
18
  exports.unshareableViewWarning = unshareableViewWarning;
19
19
  exports.immutableViewPatchKeys = immutableViewPatchKeys;
20
20
  exports.describeTaskView = describeTaskView;
21
+ exports.summarizeTaskViewSelection = summarizeTaskViewSelection;
22
+ /** `all` is the quick filter that filters nothing; any other one narrows. */
23
+ function narrowingQuickFilter(filters) {
24
+ const quick = filters.quickFilter;
25
+ return quick && quick !== 'all' ? quick : undefined;
26
+ }
21
27
  /**
22
28
  * OSK-4859 — a view created with no active workspace comes back with no
23
29
  * `workspaceId`, and the server then refuses to share it
@@ -97,13 +103,20 @@ function describeTaskView(view) {
97
103
  add('pinned tasks', `${pinned.length}: ${pinned.join(', ')}`);
98
104
  }
99
105
  const f = view.filters ?? {};
106
+ // Every selection field the create schema accepts. `--quick-filter` was a
107
+ // create flag this renderer never printed, so a saved quick filter was
108
+ // exactly the kind of selection that "did not take" invisibly.
109
+ const quick = narrowingQuickFilter(f);
100
110
  const filterRows = [
111
+ ['quick filter', quick ? [quick] : undefined],
101
112
  ['statuses', f.statuses],
113
+ ['status types', f.statusTypes],
102
114
  ['priorities', f.priorities],
103
115
  ['assignees', f.assignees],
104
116
  ['labels', f.labels],
105
117
  ['goals', f.goalIds],
106
118
  ['projects', f.projectIds],
119
+ ['cycles', f.cycleIds],
107
120
  ];
108
121
  let anyFilter = false;
109
122
  for (const [label, values] of filterRows) {
@@ -123,3 +136,45 @@ function describeTaskView(view) {
123
136
  add('sort', describeShape(view.sort));
124
137
  return lines;
125
138
  }
139
+ const counted = (count, singular, plural = `${singular}s`) => `${count} ${count === 1 ? singular : plural}`;
140
+ /**
141
+ * One line saying what a view SELECTS, for a table cell.
142
+ *
143
+ * `views list` had a SELECTION column whose value was the word `criteria` for
144
+ * every view that was not a pinned task list — a kind label sitting where the
145
+ * reader looks for an answer (dogfood OSK-4646). Values that fit are named
146
+ * (statuses, priorities, the quick filter, the search); ids are counted, since
147
+ * a row of UUIDs answers nothing at a glance and `views show` prints them in
148
+ * full.
149
+ */
150
+ function summarizeTaskViewSelection(view) {
151
+ const parts = [];
152
+ const pinned = view.taskIds ?? [];
153
+ if (pinned.length)
154
+ parts.push(counted(pinned.length, 'pinned task'));
155
+ const spaces = view.spaceIds ?? [];
156
+ parts.push(spaces.length ? counted(spaces.length, 'space') : 'all spaces');
157
+ const f = view.filters ?? {};
158
+ const quick = narrowingQuickFilter(f);
159
+ if (quick)
160
+ parts.push(quick);
161
+ if (f.statuses?.length)
162
+ parts.push(`status ${f.statuses.join('/')}`);
163
+ if (f.statusTypes?.length)
164
+ parts.push(`status type ${f.statusTypes.join('/')}`);
165
+ if (f.priorities?.length)
166
+ parts.push(`priority ${f.priorities.join('/')}`);
167
+ if (f.assignees?.length)
168
+ parts.push(counted(f.assignees.length, 'assignee'));
169
+ if (f.labels?.length)
170
+ parts.push(counted(f.labels.length, 'label'));
171
+ if (f.goalIds?.length)
172
+ parts.push(counted(f.goalIds.length, 'goal'));
173
+ if (f.projectIds?.length)
174
+ parts.push(counted(f.projectIds.length, 'project'));
175
+ if (f.cycleIds?.length)
176
+ parts.push(counted(f.cycleIds.length, 'cycle'));
177
+ if (f.search)
178
+ parts.push(`"${f.search}"`);
179
+ return parts.join(', ');
180
+ }