@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
@@ -346,6 +346,8 @@ export declare function parseGlobalFlags(argv: readonly string[]): ParsedGlobals
346
346
  export declare function apiRefusal(err: unknown): {
347
347
  code?: string;
348
348
  message?: string;
349
+ /** The parsed JSON body, for a refusal that carries structured context. */
350
+ body?: Record<string, unknown>;
349
351
  };
350
352
  /**
351
353
  * Prefix a failure message with the resource it is about.
@@ -1368,6 +1368,7 @@ function apiRefusal(err) {
1368
1368
  return {
1369
1369
  code: stringProp(body, 'code') || stringProp(body, 'errorType'),
1370
1370
  message: stringProp(body, 'message') || stringProp(body, 'error'),
1371
+ ...(body ? { body } : {}),
1371
1372
  };
1372
1373
  }
1373
1374
  /**
@@ -18,7 +18,7 @@ const show_1 = __importDefault(require("./show"));
18
18
  * lists every subcommand, which is where a menu belongs.
19
19
  */
20
20
  class Balance extends show_1.default {
21
- static description = 'Show your hosted usage balance — the ceiling that refuses a turn (not the x402 wallet). ' +
21
+ static description = 'Show your hosted usage balance and plan limits — the ceilings that refuse a turn (not the x402 wallet). ' +
22
22
  'See `balance usage`, `balance overage`, `balance statement` and `balance history` for the detail behind it.';
23
23
  static examples = [
24
24
  '<%= config.bin %> balance',
@@ -1,3 +1,4 @@
1
+ import type { TSkyCodeUsageSnapshot } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
2
3
  import { type RuntimeMonthlySpend } from '../../lib/harnesses';
3
4
  /**
@@ -7,6 +8,30 @@ import { type RuntimeMonthlySpend } from '../../lib/harnesses';
7
8
  * the wording is part of the observable contract and is pinned directly.
8
9
  */
9
10
  export declare function renderRuntimeSpend(spend: RuntimeMonthlySpend[], log: (line: string) => void): void;
11
+ /**
12
+ * Your plan's ceilings, as lines.
13
+ *
14
+ * This screen showed the credits and the spend cap under a footer that called
15
+ * them "the ceiling that refuses a turn" — while the plan's monthly allowance,
16
+ * its five-hour window and its concurrent-session limit, each of which refuses a
17
+ * turn with its own message ("plan allowance is used up", "five-hour usage
18
+ * window is used up", "concurrent-session limit is reached"), were only on
19
+ * `balance usage` (OSK-4638). A refusal that names a ceiling should find that
20
+ * ceiling here, in the same words.
21
+ *
22
+ * The allowance is spent before the credits above; past it, overage consent
23
+ * decides whether the credits pay or the turn is refused. The five-hour window
24
+ * and the session limit are enforced together or not at all (admission needs
25
+ * both to be set), and what is left of the window is what admission computes:
26
+ * limit less used less reserved.
27
+ *
28
+ * Personal only. The workspace read does not report the workspace's overage
29
+ * terms, so rendering this for a workspace would state a refusal rule it cannot
30
+ * see.
31
+ *
32
+ * Pure so the wording is pinned directly, like `renderRuntimeSpend`.
33
+ */
34
+ export declare function renderPlanCeilings(usage: TSkyCodeUsageSnapshot, log: (line: string) => void, bin?: string): void;
10
35
  /**
11
36
  * YOUR hosted usage balance — the personal account, and only that one.
12
37
  *
@@ -41,6 +66,13 @@ export default class BalanceShow extends BaseCommand {
41
66
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
42
67
  };
43
68
  run(): Promise<void>;
69
+ /**
70
+ * Your plan's ceilings, beside the balance they are spent before.
71
+ *
72
+ * Best-effort, like the runtime cap below it: a failed read prints one line
73
+ * and leaves the balance intact rather than failing the command.
74
+ */
75
+ private renderPlanCeilings;
44
76
  /**
45
77
  * The OTHER cap — the one that actually refuses a Hosted Machine.
46
78
  *
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderRuntimeSpend = renderRuntimeSpend;
4
+ exports.renderPlanCeilings = renderPlanCeilings;
4
5
  const core_1 = require("@oclif/core");
6
+ const auth_core_1 = require("@skrr-ai/auth-core");
5
7
  const base_command_1 = require("../../base-command");
6
8
  const balance_1 = require("../../lib/balance");
7
9
  const workspaces_1 = require("../../lib/workspaces");
@@ -35,6 +37,52 @@ function renderRuntimeSpend(spend, log) {
35
37
  }
36
38
  }
37
39
  }
40
+ /**
41
+ * Your plan's ceilings, as lines.
42
+ *
43
+ * This screen showed the credits and the spend cap under a footer that called
44
+ * them "the ceiling that refuses a turn" — while the plan's monthly allowance,
45
+ * its five-hour window and its concurrent-session limit, each of which refuses a
46
+ * turn with its own message ("plan allowance is used up", "five-hour usage
47
+ * window is used up", "concurrent-session limit is reached"), were only on
48
+ * `balance usage` (OSK-4638). A refusal that names a ceiling should find that
49
+ * ceiling here, in the same words.
50
+ *
51
+ * The allowance is spent before the credits above; past it, overage consent
52
+ * decides whether the credits pay or the turn is refused. The five-hour window
53
+ * and the session limit are enforced together or not at all (admission needs
54
+ * both to be set), and what is left of the window is what admission computes:
55
+ * limit less used less reserved.
56
+ *
57
+ * Personal only. The workspace read does not report the workspace's overage
58
+ * terms, so rendering this for a workspace would state a refusal rule it cannot
59
+ * see.
60
+ *
61
+ * Pure so the wording is pinned directly, like `renderRuntimeSpend`.
62
+ */
63
+ function renderPlanCeilings(usage, log, bin = 'skrr') {
64
+ const resets = (at) => (at ? ` — resets ${(0, balance_1.formatBalanceTimestamp)(at)}` : '');
65
+ const window = usage.fiveHour;
66
+ log('');
67
+ log(`${auth_core_1.FIRST_PARTY_HARNESS.displayName} ${usage.plan ?? 'current'} plan — spent before the credits, and each limit refuses a turn:`);
68
+ log(` Plan monthly: ${(0, balance_1.formatCreditsWithUsd)(usage.monthly.remainingUsageMicros)} left${resets(usage.monthly.resetsAt)}`);
69
+ if (window.limitUsageMicros > 0 && window.maxConcurrentSessions > 0) {
70
+ const windowLeft = Math.max(0, window.limitUsageMicros - window.usedUsageMicros - window.reservedUsageMicros);
71
+ log(` Plan 5-hour: ${(0, balance_1.formatCreditsWithUsd)(windowLeft)} left of ${(0, balance_1.formatCreditsWithUsd)(window.limitUsageMicros)}${resets(window.resetsAt)}`);
72
+ log(` Plan sessions: ${window.activeSessions} of ${window.maxConcurrentSessions} running`);
73
+ }
74
+ else {
75
+ log(' Plan 5-hour: no five-hour or concurrent-session limit');
76
+ }
77
+ log(usage.overageEnabled
78
+ ? ' Past the plan: paid from the credits (overage on)'
79
+ : ` Past the plan: refused — overage is off (\`${bin} balance overage enabled\`)`);
80
+ if (usage.partialReasons?.includes('missing_entitlement_period')) {
81
+ // Catalog defaults, not this account's period: say so rather than present
82
+ // them as measured.
83
+ log(' (plan period unavailable; catalog limits shown)');
84
+ }
85
+ }
38
86
  /**
39
87
  * YOUR hosted usage balance — the personal account, and only that one.
40
88
  *
@@ -171,17 +219,40 @@ class BalanceShow extends base_command_1.BaseCommand {
171
219
  else {
172
220
  this.log(' Auto-refill: off');
173
221
  }
222
+ await this.renderPlanCeilings();
174
223
  await this.renderRuntimeSpend();
175
224
  this.log('');
176
225
  this.log('A credit is a micro-dollar: 1,000,000 credits = $1.00. The spend cap');
177
226
  this.log('is a per-period ceiling, not a second balance. `skrr balance statement`');
178
227
  this.log('itemises this same account and says what each charge was for.');
179
228
  this.log('');
180
- this.log('This is the ceiling that refuses a turn for work you own. A session');
181
- this.log("carrying a workspace is refused against the WORKSPACE's balance —");
182
- this.log('see `skrr balance show --workspace <id>`. `skrr payments wallet` is the');
229
+ // Plural, because they are: a turn you own can be refused by the credits,
230
+ // the spend cap, a plan limit or a hosted runtime's monthly cap, and the
231
+ // refusal names which (OSK-4638).
232
+ this.log('These are the ceilings that refuse a turn for work you own; a refusal');
233
+ this.log('names the one that ran out. A session carrying a workspace is refused');
234
+ this.log("against the WORKSPACE's balance instead — see");
235
+ this.log('`skrr balance show --workspace <id>`. `skrr payments wallet` is the');
183
236
  this.log('separate x402 crypto wallet, in USDC — not this balance.');
184
237
  }
238
+ /**
239
+ * Your plan's ceilings, beside the balance they are spent before.
240
+ *
241
+ * Best-effort, like the runtime cap below it: a failed read prints one line
242
+ * and leaves the balance intact rather than failing the command.
243
+ */
244
+ async renderPlanCeilings() {
245
+ let usage;
246
+ try {
247
+ usage = await (0, balance_1.getSkyCodeUsage)();
248
+ }
249
+ catch {
250
+ this.log('');
251
+ this.log(` Plan limits: unavailable (\`${this.config.bin} balance usage\` to retry)`);
252
+ return;
253
+ }
254
+ renderPlanCeilings(usage, (line) => this.log(line), this.config.bin);
255
+ }
185
256
  /**
186
257
  * The OTHER cap — the one that actually refuses a Hosted Machine.
187
258
  *
@@ -37,6 +37,7 @@ export default class CodeHandover extends BaseCommand {
37
37
  repo: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
38
38
  base: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
39
39
  'allow-dirty': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
40
+ browser: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
40
41
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
41
42
  };
42
43
  /** Run git, returning null rather than throwing — every caller has a fallback. */
@@ -61,6 +61,9 @@ class CodeHandover extends base_command_1.BaseCommand {
61
61
  description: 'Hand over anyway when the working tree is dirty or the branch is unpushed. The cloud agent will NOT see that work.',
62
62
  default: false,
63
63
  }),
64
+ browser: core_1.Flags.boolean({
65
+ description: 'Give the receiving agent a browser. Refused where the deployment has no browser runtime, never run without one.',
66
+ }),
64
67
  json: core_1.Flags.boolean({ description: 'Output the created task as JSON' }),
65
68
  };
66
69
  /** Run git, returning null rather than throwing — every caller has a fallback. */
@@ -152,6 +155,7 @@ class CodeHandover extends base_command_1.BaseCommand {
152
155
  baseBranch: branch,
153
156
  prompt,
154
157
  machineProduct: 'hosted-job-machine',
158
+ ...(flags.browser ? { browser: true } : {}),
155
159
  clientRequestId: (0, node_crypto_1.randomUUID)(),
156
160
  });
157
161
  }
@@ -22,6 +22,7 @@ export default class CodeJobsRun extends BaseCommand {
22
22
  prompt: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
23
  'prompt-file': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
24
24
  egress: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ browser: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
25
26
  wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
26
27
  interval: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
27
28
  timeout: import("@oclif/core/lib/interfaces").OptionFlag<number, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -40,6 +40,9 @@ class CodeJobsRun extends base_command_1.BaseCommand {
40
40
  description: 'Network reach. Default `isolated` — the control plane only, nothing else.',
41
41
  options: ['isolated', 'allowlist', 'open'],
42
42
  }),
43
+ browser: core_1.Flags.boolean({
44
+ description: 'Run the job with a browser. Refused where the deployment has no browser runtime, never run without one.',
45
+ }),
43
46
  wait: core_1.Flags.boolean({ description: 'Wait for the job to finish and print its result' }),
44
47
  interval: core_1.Flags.integer({ description: 'Poll interval in seconds while waiting', default: 5 }),
45
48
  timeout: core_1.Flags.integer({ description: 'Give up waiting after this many minutes', default: 30 }),
@@ -88,6 +91,7 @@ class CodeJobsRun extends base_command_1.BaseCommand {
88
91
  mode: 'bare',
89
92
  machineProduct: 'hosted-job-machine',
90
93
  ...(flags.egress ? { egressPolicy: flags.egress } : {}),
94
+ ...(flags.browser ? { browser: true } : {}),
91
95
  clientRequestId: (0, node_crypto_1.randomUUID)(),
92
96
  });
93
97
  task = res?.task;
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
5
5
  const daemon_binding_1 = require("../../lib/daemon-binding");
6
6
  const daemon_installer_1 = require("../../lib/daemon-installer");
7
+ const daemon_setup_1 = require("../../lib/daemon-setup");
7
8
  /**
8
9
  * `skrr daemon install` — install the local runtime's OS service.
9
10
  *
@@ -52,6 +53,12 @@ class DaemonInstall extends core_1.Command {
52
53
  // own comment records `skrr code run -m <model>` dying the same way. Same
53
54
  // mistake, one directory over.
54
55
  const argv = this.argv;
56
+ // Before fetching anything: a machine still carrying the previous product
57
+ // identity's state is refused by the runtime's own install, so downloading a
58
+ // release first only delays the same refusal (OSK-6895).
59
+ if ((0, daemon_setup_1.refuseOnLegacyLocalState)((line) => this.logToStderr(line))) {
60
+ this.exit(1);
61
+ }
55
62
  // Bootstrap only when there is nothing to forward to. An existing runtime —
56
63
  // brew, the script installer, a dev checkout — is left exactly alone; this
57
64
  // is a floor under the missing case, not a second update channel competing
@@ -82,6 +82,20 @@ class HarnessesLeasesShow extends base_command_1.BaseCommand {
82
82
  line('Updated:', lease.updatedAt);
83
83
  // Money last, and only when the server sent it — an absent billing block
84
84
  // and a zero are different states.
85
+ //
86
+ // Who pays comes first. `harnesses leases billing` used to be "change who
87
+ // pays", so there was a way to move the payer (which never worked) and no
88
+ // way to read it (OSK-4628). The payer is fixed at create, and the lease
89
+ // carries it as `workspaceId`: the workspace account it bills, or `null`
90
+ // for the owner's personal account — the same scope the monthly cap below
91
+ // is counted in. A server that does not send the key says nothing here,
92
+ // because a missing field is not evidence of a personal lease.
93
+ if (Object.prototype.hasOwnProperty.call(lease, 'workspaceId')) {
94
+ const workspaceId = typeof lease.workspaceId === 'string' ? lease.workspaceId.trim() : '';
95
+ line('Billed to:', workspaceId
96
+ ? `workspace ${workspaceId} (\`${this.config.bin} balance show --workspace ${workspaceId}\`)`
97
+ : 'your personal account');
98
+ }
85
99
  line('Billing:', nested(lease.billing, 'billableState'));
86
100
  // The monthly cap, which is what actually refuses a Hosted Machine.
87
101
  //
@@ -1,4 +1,26 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /** The target a `TARGET_CONFLICT` refusal names, as the API sends it. */
3
+ export interface ConflictingTarget {
4
+ id: string;
5
+ desiredState?: string;
6
+ status?: string;
7
+ }
8
+ export declare function conflictingTargetOf(body: Record<string, unknown> | undefined): ConflictingTarget | null;
9
+ /**
10
+ * What to do about a placement that already has a target, as commands.
11
+ *
12
+ * Re-running an install whose first attempt had failed printed "An unexpected
13
+ * error occurred. Code: TARGET_CONFLICT" (OSK-4631): no target, no path, no next
14
+ * step, and no word on whether the re-run had partly applied. The API now names
15
+ * the target in the way; this turns that into the commands for its state. A live
16
+ * target is changed or removed, never installed twice; one being removed can be
17
+ * restored, or purged once its daemon confirms cleanup.
18
+ */
19
+ export declare function describeTargetConflict(target: ConflictingTarget, { bin, placement, created }: {
20
+ bin: string;
21
+ placement: string;
22
+ created: string[];
23
+ }): string;
2
24
  export default class InstructionsInstall extends BaseCommand {
3
25
  static description: string;
4
26
  static flags: {
@@ -1,5 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.conflictingTargetOf = conflictingTargetOf;
4
+ exports.describeTargetConflict = describeTargetConflict;
3
5
  const core_1 = require("@oclif/core");
4
6
  const auth_core_1 = require("@skrr-ai/auth-core");
5
7
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -9,6 +11,41 @@ const harness_provider_input_1 = require("../../lib/harness-provider-input");
9
11
  const prompt_1 = require("../../lib/prompt");
10
12
  /** The providers a managed instruction can target, canonical spellings. */
11
13
  const MANAGED_INSTRUCTION_PROVIDERS = ['claude', 'codex', auth_core_1.FIRST_PARTY_HARNESS.provider];
14
+ function conflictingTargetOf(body) {
15
+ const value = body?.conflictingTarget;
16
+ return value && typeof value.id === 'string' && value.id ? value : null;
17
+ }
18
+ /**
19
+ * What to do about a placement that already has a target, as commands.
20
+ *
21
+ * Re-running an install whose first attempt had failed printed "An unexpected
22
+ * error occurred. Code: TARGET_CONFLICT" (OSK-4631): no target, no path, no next
23
+ * step, and no word on whether the re-run had partly applied. The API now names
24
+ * the target in the way; this turns that into the commands for its state. A live
25
+ * target is changed or removed, never installed twice; one being removed can be
26
+ * restored, or purged once its daemon confirms cleanup.
27
+ */
28
+ function describeTargetConflict(target, { bin, placement, created }) {
29
+ const state = [target.desiredState, target.status].filter(Boolean).join(', ');
30
+ const lines = target.desiredState === 'removed'
31
+ ? [
32
+ `Target ${target.id} already holds ${placement} on that machine and is being removed${state ? ` (${state})` : ''}.`,
33
+ `Bring it back with \`${bin} instructions restore-target ${target.id}\`, or install again ` +
34
+ `after its daemon confirms cleanup and \`${bin} instructions purge-target ${target.id}\`.`,
35
+ ]
36
+ : [
37
+ `Target ${target.id} already manages ${placement} on that machine${state ? ` (${state})` : ''}.`,
38
+ ...(target.status === 'error'
39
+ ? [`Its last sync failed; \`${bin} instructions status\` shows why.`]
40
+ : []),
41
+ `Change what it applies with \`${bin} instructions update-target ${target.id} --bundle <id>\`, ` +
42
+ `or drop it with \`${bin} instructions remove ${target.id}\`.`,
43
+ ];
44
+ lines.push(created.length
45
+ ? `Created before this refusal: ${created.join(', ')}.`
46
+ : 'No target was created by this command.');
47
+ return lines.join(' ');
48
+ }
12
49
  class InstructionsInstall extends base_command_1.BaseCommand {
13
50
  static description = 'Opt in a daemon target to ongoing managed instruction reconciliation';
14
51
  static flags = {
@@ -32,7 +69,13 @@ class InstructionsInstall extends base_command_1.BaseCommand {
32
69
  default: MANAGED_INSTRUCTION_PROVIDERS,
33
70
  }),
34
71
  scope: core_1.Flags.string({ description: 'global or project', options: ['global', 'project'] }),
35
- project: core_1.Flags.string({ description: 'Absolute project path on the daemon machine' }),
72
+ project: core_1.Flags.string({
73
+ // The daemon writes a project target at the repository root, so a path
74
+ // outside any Git repository is accepted here and then fails on the
75
+ // machine. Saying so is the only warning this command can give: the path
76
+ // is checked where it lives, not here (OSK-4626).
77
+ description: "Absolute project path on the daemon's machine, inside a Git repository (the block is written at its root)",
78
+ }),
36
79
  mode: core_1.Flags.string({
37
80
  description: 'Project footprint',
38
81
  options: ['local', 'team'],
@@ -87,12 +130,20 @@ class InstructionsInstall extends base_command_1.BaseCommand {
87
130
  if (providers.includes('codex') && Buffer.byteLength(previewBody, 'utf8') > 30 * 1024) {
88
131
  this.error('Codex managed instruction bodies are limited to 30 KiB; no targets were created', { exit: 2 });
89
132
  }
90
- if (!flags.json) {
133
+ // The body is shown so the confirmation below is informed. With `--yes`
134
+ // there is no confirmation to inform, and printing a ~9 KB body anyway put
135
+ // the command's actual outcome — often a refusal — a screen of scrollback
136
+ // away (OSK-4631). Say what was authorized, and where to read it.
137
+ if (!flags.json && !flags.yes) {
91
138
  this.log('Exact managed instruction body:');
92
139
  this.log('---');
93
140
  this.log(previewBody);
94
141
  this.log('---');
95
142
  }
143
+ else if (!flags.json) {
144
+ this.log(`Authorizing ${Buffer.byteLength(previewBody, 'utf8').toLocaleString()} bytes of instruction ` +
145
+ `${flags.bundle}; \`${this.config.bin} instructions show ${flags.bundle}\` prints them.`);
146
+ }
96
147
  const teamWarning = scope === 'project' && flags.mode === 'team'
97
148
  ? ' Team mode writes to repository files and may affect every contributor.'
98
149
  : '';
@@ -101,8 +152,8 @@ class InstructionsInstall extends base_command_1.BaseCommand {
101
152
  return this.log('Aborted.');
102
153
  const daemonId = await this.resolveDaemon(flags.daemon);
103
154
  const results = [];
104
- try {
105
- for (const provider of providers) {
155
+ for (const provider of providers) {
156
+ try {
106
157
  results.push(await data_provider_1.dataService.createManagedInstructionTarget({
107
158
  daemonId,
108
159
  bundleId: flags.bundle,
@@ -115,15 +166,40 @@ class InstructionsInstall extends base_command_1.BaseCommand {
115
166
  label: flags.label,
116
167
  }));
117
168
  }
118
- }
119
- catch (error) {
120
- this.handleApiError(error);
169
+ catch (error) {
170
+ // One target per provider, created in turn, so a refusal part-way
171
+ // through leaves the earlier ones in place. Say which.
172
+ const created = results.map((result) => `${(0, harness_provider_input_1.canonicalHarnessInput)(result.target.provider)} (${result.target.id})`);
173
+ const refusal = (0, base_command_1.apiRefusal)(error);
174
+ const conflict = refusal.code === 'TARGET_CONFLICT' ? conflictingTargetOf(refusal.body) : null;
175
+ if (conflict) {
176
+ this.failWithCliError({
177
+ message: describeTargetConflict(conflict, {
178
+ bin: this.config.bin,
179
+ placement: `${(0, harness_provider_input_1.canonicalHarnessInput)(provider)} ${scope}${flags.project ? ` ${flags.project}` : ''}`,
180
+ created,
181
+ }),
182
+ code: 'TARGET_CONFLICT',
183
+ status: 409,
184
+ exit: 1,
185
+ retryable: false,
186
+ details: { ...refusal.body, created: results.map((result) => result.target) },
187
+ });
188
+ }
189
+ if (created.length) {
190
+ this.warn(`Created before this failure: ${created.join(', ')}.`);
191
+ }
192
+ this.handleApiError(error);
193
+ }
121
194
  }
122
195
  if (flags.json)
123
196
  return this.log(JSON.stringify(results, null, 2));
124
197
  for (const result of results) {
125
198
  this.log(`Install queued: ${(0, harness_provider_input_1.canonicalHarnessInput)(result.target.provider)} ${result.target.scope}${result.target.projectPath ? ` ${result.target.projectPath}` : ''} (${result.target.id})`);
126
199
  }
200
+ // Queued is not applied: the daemon checks the placement when it writes, and
201
+ // a path it cannot use fails there, after this command has exited 0.
202
+ this.log(`The daemon applies these on its next sync; \`${this.config.bin} instructions status\` shows when, or why not.`);
127
203
  }
128
204
  /**
129
205
  * The daemon the targets are created on, by the rule every daemon-picking
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
+ const instruction_provenance_1 = require("../../lib/instruction-provenance");
7
8
  class InstructionsList extends base_command_1.BaseCommand {
8
9
  static description = 'List version-controlled instruction prompts';
9
10
  static flags = {
@@ -32,12 +33,16 @@ class InstructionsList extends base_command_1.BaseCommand {
32
33
  id: bundle.id,
33
34
  name: bundle.name,
34
35
  version: String(bundle.currentVersionNumber),
36
+ // The built-in release the CURRENT body is, if any — a separate counter
37
+ // from VERSION, which is this prompt's own (OSK-4647).
38
+ template: (0, instruction_provenance_1.builtInLabel)(bundle.currentVersion),
35
39
  status: bundle.deletedAt ? 'deleted' : bundle.status,
36
40
  updated: bundle.updatedAt,
37
41
  })), [
38
42
  { key: 'id', header: 'ID' },
39
43
  { key: 'name', header: 'NAME', maxWidth: 32 },
40
44
  { key: 'version', header: 'VERSION' },
45
+ { key: 'template', header: 'TEMPLATE' },
41
46
  { key: 'status', header: 'STATUS' },
42
47
  { key: 'updated', header: 'UPDATED' },
43
48
  ], (line) => this.log(line));
@@ -9,4 +9,10 @@ export default class InstructionsShow extends BaseCommand {
9
9
  versions: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
10
10
  };
11
11
  run(): Promise<void>;
12
+ /**
13
+ * The built-in release this server would install today, or undefined. Read
14
+ * from the offer, which already reports it as `templateVersion`; a failure
15
+ * costs only the "is there a newer one" clause, never the command.
16
+ */
17
+ private latestTemplateVersion;
12
18
  }
@@ -4,6 +4,7 @@ const core_1 = require("@oclif/core");
4
4
  const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
+ const instruction_provenance_1 = require("../../lib/instruction-provenance");
7
8
  class InstructionsShow extends base_command_1.BaseCommand {
8
9
  static description = 'Show exact prompt content and version history';
9
10
  static args = {
@@ -38,6 +39,9 @@ class InstructionsShow extends base_command_1.BaseCommand {
38
39
  (0, format_1.renderTable)(response.versions.map((version) => ({
39
40
  id: version.id,
40
41
  version: String(version.versionNumber),
42
+ // Its own column, never folded into VERSION: the prompt's counter
43
+ // and the built-in release are different numbers (OSK-4647).
44
+ template: (0, instruction_provenance_1.builtInLabel)(version),
41
45
  hash: version.hash.slice(0, 12),
42
46
  status: version.archivedAt
43
47
  ? 'archived'
@@ -48,10 +52,20 @@ class InstructionsShow extends base_command_1.BaseCommand {
48
52
  })), [
49
53
  { key: 'id', header: 'ID' },
50
54
  { key: 'version', header: 'VERSION' },
51
- { key: 'hash', header: 'HASH' },
55
+ { key: 'hash', header: 'BODY HASH' },
52
56
  { key: 'status', header: 'STATUS' },
57
+ { key: 'template', header: 'TEMPLATE' },
53
58
  { key: 'message', header: 'MESSAGE', maxWidth: 48 },
54
59
  ], (line) => this.log(line));
60
+ // Three hashes answer "is my file current?", and this table shows one.
61
+ // A file's `v=` is the body as rendered for its target, so beside an
62
+ // agent-identity section it matches none of these (OSK-4644).
63
+ this.log('');
64
+ this.log("BODY HASH is the version as written. A managed file's `v=` is the body as");
65
+ this.log('rendered for its target, which differs when it fills in an agent identity;');
66
+ this.log(`compare it with FILE HASH in \`${this.config.bin} instructions status\`.`);
67
+ this.log("VERSION counts this prompt's saved bodies; TEMPLATE is the built-in release a");
68
+ this.log('body was taken from, which is a separate number.');
55
69
  return;
56
70
  }
57
71
  const response = await data_provider_1.dataService.getManagedInstructionBundle(args.id);
@@ -61,6 +75,9 @@ class InstructionsShow extends base_command_1.BaseCommand {
61
75
  this.log(`${bundle.name} (${bundle.id}) — v${bundle.currentVersionNumber} — ${bundle.status}`);
62
76
  if (bundle.description)
63
77
  this.log(bundle.description);
78
+ const provenance = (0, instruction_provenance_1.describeTemplateProvenance)(bundle, await this.latestTemplateVersion(bundle.sourceTemplateKey));
79
+ if (provenance)
80
+ this.log(provenance);
64
81
  this.log('');
65
82
  this.log(bundle.currentVersion?.body || '(no active content)');
66
83
  }
@@ -84,5 +101,21 @@ class InstructionsShow extends base_command_1.BaseCommand {
84
101
  this.handleApiError(error);
85
102
  }
86
103
  }
104
+ /**
105
+ * The built-in release this server would install today, or undefined. Read
106
+ * from the offer, which already reports it as `templateVersion`; a failure
107
+ * costs only the "is there a newer one" clause, never the command.
108
+ */
109
+ async latestTemplateVersion(templateKey) {
110
+ if (!templateKey)
111
+ return undefined;
112
+ try {
113
+ const offer = await data_provider_1.dataService.getManagedInstructionOffer(templateKey);
114
+ return offer?.preset?.templateVersion;
115
+ }
116
+ catch {
117
+ return undefined;
118
+ }
119
+ }
87
120
  }
88
121
  exports.default = InstructionsShow;
@@ -5,6 +5,7 @@ const data_provider_1 = require("@skrr-ai/data-provider");
5
5
  const base_command_1 = require("../../base-command");
6
6
  const format_1 = require("../../lib/format");
7
7
  const instruction_input_1 = require("../../lib/instruction-input");
8
+ const instruction_provenance_1 = require("../../lib/instruction-provenance");
8
9
  class InstructionsStatus extends base_command_1.BaseCommand {
9
10
  static description = 'Show managed targets, applied versions, paths, and reconciliation errors';
10
11
  static flags = {
@@ -42,12 +43,14 @@ class InstructionsStatus extends base_command_1.BaseCommand {
42
43
  // Leave it empty.
43
44
  }
44
45
  let bundleNames = new Map();
46
+ let bundlesById = new Map();
45
47
  try {
46
48
  const bundleResponse = await data_provider_1.dataService.listManagedInstructionBundles({
47
49
  includeArchived: true,
48
50
  includeDeleted: true,
49
51
  });
50
52
  bundleNames = (0, instruction_input_1.buildBundleNameIndex)(bundleResponse.bundles);
53
+ bundlesById = new Map(bundleResponse.bundles.map((bundle) => [bundle.id, bundle]));
51
54
  }
52
55
  catch {
53
56
  // Leave the index empty — rows show the full bundle id instead.
@@ -73,7 +76,17 @@ class InstructionsStatus extends base_command_1.BaseCommand {
73
76
  target: target.appliedPath ||
74
77
  (target.scope === 'global' ? 'global' : target.projectPath || 'project'),
75
78
  desired: target.desiredState,
76
- applied: target.appliedVersionNumber ? `v${target.appliedVersionNumber}` : '-',
79
+ // `v3 (built-in v5)`: the prompt's version, and the built-in release
80
+ // it is when that is known. `APPLIED v3` alone read as two releases
81
+ // behind a preset announced as v5 (OSK-4647).
82
+ applied: (0, instruction_provenance_1.appliedVersionLabel)(target.appliedVersionNumber, bundlesById.get(target.bundleId)),
83
+ // The hash the managed block in the file starts with (`v=`), cut to the
84
+ // same 12 characters `show --versions` prints. It is the body as
85
+ // rendered for THIS target, so it matches a version's hash only when
86
+ // nothing was filled in — and was `--json` only, which made "is the
87
+ // block in my CLAUDE.md current?" read as "my machine is running a body
88
+ // the server never heard of" (OSK-4644).
89
+ fileHash: target.appliedHash ? target.appliedHash.slice(0, 12) : '-',
77
90
  status: (0, instruction_input_1.describeTargetSync)(target, Date.now(), liveDaemonIds),
78
91
  })), [
79
92
  { key: 'id', header: 'TARGET ID' },
@@ -88,6 +101,7 @@ class InstructionsStatus extends base_command_1.BaseCommand {
88
101
  { key: 'target', header: 'PATH', maxWidth: 36 },
89
102
  { key: 'desired', header: 'DESIRED' },
90
103
  { key: 'applied', header: 'APPLIED' },
104
+ { key: 'fileHash', header: 'FILE HASH' },
91
105
  // Wide enough for the stall note to keep its remedy. Truncating at 48
92
106
  // cut `purge-target` off the end, which hid the one actionable half of
93
107
  // the sentence.
@@ -106,6 +120,8 @@ class InstructionsStatus extends base_command_1.BaseCommand {
106
120
  for (const command of rebinds)
107
121
  this.log(` ${command}`);
108
122
  }
123
+ for (const line of (0, instruction_input_1.failedTargetLines)(response.targets, this.config.bin))
124
+ this.log(line);
109
125
  // Where the text went.
110
126
  //
111
127
  // The count in the STATUS column says skrr overwrote something you
@@ -52,8 +52,8 @@ class PaymentsWallet extends base_command_1.BaseCommand {
52
52
  // CLI — so anyone looking for "my balance" found USDC and drew the wrong
53
53
  // conclusion about their usage ceiling. Name the other one.
54
54
  this.log('');
55
- this.log('This is the x402 crypto wallet, in USDC. Your hosted USAGE balance —');
56
- this.log('the ceiling that refuses a turn — is `skrr balance show`.');
55
+ this.log('This is the x402 crypto wallet, in USDC. Your hosted USAGE balance and');
56
+ this.log('plan limits — the ceilings that refuse a turn — are `skrr balance show`.');
57
57
  const payments = history.payments ?? [];
58
58
  if (payments.length === 0) {
59
59
  this.log('');