@skrr-ai/cli 0.1.50 → 0.1.51

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 (100) hide show
  1. package/dist/base-command.js +59 -2
  2. package/dist/commands/agents/chat.d.ts +14 -0
  3. package/dist/commands/agents/chat.js +37 -1
  4. package/dist/commands/agents/show.d.ts +9 -0
  5. package/dist/commands/agents/show.js +53 -0
  6. package/dist/commands/agents/update.d.ts +9 -0
  7. package/dist/commands/agents/update.js +27 -0
  8. package/dist/commands/balance/show.d.ts +34 -0
  9. package/dist/commands/balance/show.js +92 -2
  10. package/dist/commands/commitments/doctor.d.ts +1 -0
  11. package/dist/commands/commitments/doctor.js +19 -4
  12. package/dist/commands/commitments/effective-policy.js +15 -1
  13. package/dist/commands/commitments/next-tick.js +3 -1
  14. package/dist/commands/commitments/preflight.js +10 -0
  15. package/dist/commands/commitments/show.js +11 -1
  16. package/dist/commands/daemon/config.d.ts +15 -0
  17. package/dist/commands/daemon/config.js +38 -0
  18. package/dist/commands/daemon/index.js +1 -0
  19. package/dist/commands/daemon/status.js +15 -1
  20. package/dist/commands/harnesses/models.d.ts +54 -0
  21. package/dist/commands/harnesses/models.js +100 -7
  22. package/dist/commands/harnesses/show.d.ts +11 -0
  23. package/dist/commands/harnesses/show.js +35 -0
  24. package/dist/commands/harnesses/update.d.ts +28 -1
  25. package/dist/commands/harnesses/update.js +73 -10
  26. package/dist/commands/outbox/list.d.ts +12 -1
  27. package/dist/commands/outbox/list.js +31 -2
  28. package/dist/commands/revoke-daemon.js +9 -0
  29. package/dist/commands/spaces/create.d.ts +1 -0
  30. package/dist/commands/spaces/create.js +17 -0
  31. package/dist/commands/spaces/show.js +5 -0
  32. package/dist/commands/spaces/update.d.ts +1 -0
  33. package/dist/commands/spaces/update.js +16 -0
  34. package/dist/commands/tasks/create.d.ts +1 -0
  35. package/dist/commands/tasks/create.js +5 -0
  36. package/dist/commands/tasks/show.js +45 -0
  37. package/dist/commands/tasks/update.d.ts +1 -0
  38. package/dist/commands/tasks/update.js +21 -0
  39. package/dist/commands/tasks/upsert.d.ts +1 -0
  40. package/dist/commands/tasks/upsert.js +16 -0
  41. package/dist/lib/agentic-stream.d.ts +68 -1
  42. package/dist/lib/agentic-stream.js +294 -7
  43. package/dist/lib/auth-core-init.d.ts +2 -10
  44. package/dist/lib/auth-core-init.js +20 -67
  45. package/dist/lib/auth-failure.d.ts +33 -0
  46. package/dist/lib/auth-failure.js +65 -0
  47. package/dist/lib/commitments.d.ts +21 -1
  48. package/dist/lib/commitments.js +34 -0
  49. package/dist/lib/daemonBroker.d.ts +14 -0
  50. package/dist/lib/daemonBroker.js +64 -12
  51. package/dist/lib/delegated-cli.js +4 -1
  52. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  53. package/dist/lib/exec-runtime-binary.js +4 -2
  54. package/dist/lib/first-party-harness-broker.js +4 -1
  55. package/dist/lib/harnesses.d.ts +10 -0
  56. package/dist/lib/live-runtime-binary.d.ts +40 -0
  57. package/dist/lib/live-runtime-binary.js +193 -0
  58. package/dist/lib/login.d.ts +23 -0
  59. package/dist/lib/login.js +44 -10
  60. package/dist/lib/message-intent.js +3 -1
  61. package/dist/lib/tasks.d.ts +20 -0
  62. package/dist/lib/tasks.js +54 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +28 -4
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +23 -5
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -1
  78. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  79. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  80. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  81. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  82. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +28 -4
  83. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +22 -4
  84. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  85. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  86. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  87. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  88. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  89. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  90. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  91. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  92. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  93. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  94. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -1
  95. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  96. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  97. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  98. package/dist/node_modules/@skrr-ai/data-provider/index.js +1928 -1924
  99. package/oclif.manifest.json +23832 -23688
  100. package/package.json +1 -1
@@ -29,6 +29,7 @@ var keychain_1 = require("./lib/keychain");
29
29
  Object.defineProperty(exports, "resolveProfileFromArgv", { enumerable: true, get: function () { return keychain_1.resolveProfileFromArgv; } });
30
30
  const data_provider_1 = require("@skrr-ai/data-provider");
31
31
  const auth_core_1 = require("@skrr-ai/auth-core");
32
+ const auth_failure_1 = require("./lib/auth-failure");
32
33
  const assignee_resolver_1 = require("./lib/assignee-resolver");
33
34
  const task_ref_resolver_1 = require("./lib/task-ref-resolver");
34
35
  const space_resolver_1 = require("./lib/space-resolver");
@@ -1002,16 +1003,29 @@ class BaseCommand extends core_1.Command {
1002
1003
  // `login` sends a CI job to do the one thing it cannot, and hides the
1003
1004
  // real mistake (dogfood OSK-4693). Name the credential instead.
1004
1005
  const presentedCiToken = Boolean(this.resolvedCredential?.token?.startsWith('osk_ci_'));
1006
+ // OSK-12116 — the 401 is usually the SECOND failure: the token expired
1007
+ // (routine) and renewing it was refused (the fault). The server's code
1008
+ // for the retry is `TOKEN_EXPIRED`, which names the routine half; the
1009
+ // latched refresh refusal names the half the reader can act on.
1010
+ const refreshRefusal = presentedCiToken
1011
+ ? null
1012
+ : (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1013
+ bin: this.config.bin,
1014
+ cliId: this.cliConfig?.cliId,
1015
+ });
1005
1016
  this.failWithCliError({
1006
1017
  message: say(presentedCiToken
1007
1018
  ? `Authentication failed: this is an \`osk_ci_*\` token, which only bootstraps a ` +
1008
1019
  `daemon (\`OVERSKY_TOKEN=<token> skrrd start\`). ${this.config.bin} commands do ` +
1009
1020
  `not accept it — sign in with \`${this.config.bin} login\` instead.`
1010
- : `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`),
1011
- code: bodyCode || 'AUTHENTICATION_FAILED',
1021
+ : refreshRefusal
1022
+ ? `Authentication failed. ${refreshRefusal.message}`
1023
+ : `Authentication failed. Run \`${this.config.bin} login\` to sign in again.`),
1024
+ code: refreshRefusal?.code || bodyCode || 'AUTHENTICATION_FAILED',
1012
1025
  status: e.status,
1013
1026
  exit: exitCode,
1014
1027
  retryable: true,
1028
+ ...(refreshRefusal && bodyCode ? { details: { accessTokenCode: bodyCode } } : {}),
1015
1029
  });
1016
1030
  }
1017
1031
  else if ((e.status === 403 || e.status === 504) &&
@@ -1287,6 +1301,36 @@ class BaseCommand extends core_1.Command {
1287
1301
  // attempt is assumed absent.
1288
1302
  const outage = (0, outbox_1.isOutageFailure)(err);
1289
1303
  const statusSuffix = e.status ? ` (HTTP ${e.status})` : '';
1304
+ // OSK-12121 — a 401 is queued (the payload is authored, and it is worth
1305
+ // keeping until someone signs in again), but it is an AUTHENTICATION
1306
+ // failure, and saying "did not accept this write" left the reader to guess
1307
+ // that. Lead with the sign-in, name the refused refresh when one is latched,
1308
+ // and give the order the fix has to happen in: sign in, THEN replay.
1309
+ if (e.status === 401) {
1310
+ const refusal = (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1311
+ bin: this.config.bin,
1312
+ cliId: this.cliConfig?.cliId,
1313
+ });
1314
+ const why = refusal
1315
+ ? refusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
1316
+ : 'The server did not accept this sign-in.';
1317
+ this.failWithCliError({
1318
+ message: `Authentication failed${statusSuffix}. ${why} Your ${write.summary} was not applied; it ` +
1319
+ `was saved locally as ${queuedId} so it is not lost. Sign in with ` +
1320
+ `\`${this.config.bin} login\`, then replay it with \`${this.config.bin} outbox retry\` ` +
1321
+ `(or discard it with \`${this.config.bin} outbox drop ${queuedId}\`).`,
1322
+ code: refusal?.code || 'AUTHENTICATION_FAILED',
1323
+ status: e.status,
1324
+ exit: this.bareMode ? 75 : 2,
1325
+ retryable: true,
1326
+ details: {
1327
+ queued: true,
1328
+ outboxId: queuedId,
1329
+ idempotencyKey: write.idempotencyKey,
1330
+ ...(e.code ? { accessTokenCode: e.code } : {}),
1331
+ },
1332
+ });
1333
+ }
1290
1334
  const lead = outage
1291
1335
  ? `${this.cliConfig.baseURL} is not serving requests right now${statusSuffix}. ` +
1292
1336
  `Your ${write.summary} was queued locally as ${queuedId}; whether the server already ` +
@@ -1419,6 +1463,19 @@ function parseGlobalFlags(argv) {
1419
1463
  }
1420
1464
  return { bare, token, workspace };
1421
1465
  }
1466
+ /**
1467
+ * The latched permanent refresh refusal, if any. Never throws: a surface that
1468
+ * cannot read the latch falls back to its generic wording (OSK-12116).
1469
+ */
1470
+ function readRefreshRefusal() {
1471
+ try {
1472
+ const state = (0, auth_core_1.needsReauthState)();
1473
+ return state.needed ? { reason: state.reason, message: state.message } : null;
1474
+ }
1475
+ catch {
1476
+ return null;
1477
+ }
1478
+ }
1422
1479
  function parseErrorBody(body) {
1423
1480
  if (!body)
1424
1481
  return null;
@@ -1,3 +1,4 @@
1
+ import { dataService } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
2
3
  import { type AgenticStreamResult, type ReconciledTerminal } from '../../lib/agentic-stream';
3
4
  /**
@@ -141,6 +142,17 @@ export declare function daemonPinFields(localSkills: {
141
142
  pathHash: string;
142
143
  }>;
143
144
  } | Record<string, never>;
145
+ type ChatPermissionMode = NonNullable<Parameters<typeof dataService.startAgenticChat>[0]['permissionMode']>;
146
+ /**
147
+ * The per-turn permission-mode fragment of the chat-start payload (OSK-12124).
148
+ * It rides the same request layer the web composer's picker sends, which the
149
+ * server already honours above the agent's and the harness's stored defaults —
150
+ * so switching one conversation no longer means changing the agent for every
151
+ * conversation it has.
152
+ */
153
+ export declare function permissionModeFields(mode: string | undefined): {
154
+ permissionMode: ChatPermissionMode;
155
+ } | Record<string, never>;
144
156
  export default class AgentsChat extends BaseCommand {
145
157
  static description: string;
146
158
  static examples: string[];
@@ -157,6 +169,7 @@ export default class AgentsChat extends BaseCommand {
157
169
  jsonl: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
158
170
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
159
171
  'approve-tools': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
172
+ 'permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
160
173
  'local-skill': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
161
174
  daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
162
175
  task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -182,3 +195,4 @@ export default class AgentsChat extends BaseCommand {
182
195
  private resolveLocalSkills;
183
196
  run(): Promise<void>;
184
197
  }
198
+ export {};
@@ -9,6 +9,7 @@ exports.noOutputFailureMessage = noOutputFailureMessage;
9
9
  exports.noStreamJsonPayload = noStreamJsonPayload;
10
10
  exports.chatStreamHeaderLines = chatStreamHeaderLines;
11
11
  exports.daemonPinFields = daemonPinFields;
12
+ exports.permissionModeFields = permissionModeFields;
12
13
  const node_fs_1 = require("node:fs");
13
14
  const core_1 = require("@oclif/core");
14
15
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -157,6 +158,11 @@ function noStreamJsonPayload(input) {
157
158
  responseMessageId: result.responseMessageId,
158
159
  status: result.status,
159
160
  response: result.text,
161
+ // The model a local harness reported running (OSK-12111). Absent on a
162
+ // cloud turn and on a harness that reports none.
163
+ ...(typeof result.lastEvent?.harnessModel === 'string' && result.lastEvent.harnessModel
164
+ ? { harnessModel: result.lastEvent.harnessModel }
165
+ : {}),
160
166
  ...(result.status === 'failed'
161
167
  ? {
162
168
  failureReason: result.lastEvent?.reason ??
@@ -211,11 +217,22 @@ function daemonPinFields(localSkills, daemonFlag) {
211
217
  }
212
218
  return {};
213
219
  }
220
+ /**
221
+ * The per-turn permission-mode fragment of the chat-start payload (OSK-12124).
222
+ * It rides the same request layer the web composer's picker sends, which the
223
+ * server already honours above the agent's and the harness's stored defaults —
224
+ * so switching one conversation no longer means changing the agent for every
225
+ * conversation it has.
226
+ */
227
+ function permissionModeFields(mode) {
228
+ return mode ? { permissionMode: mode } : {};
229
+ }
214
230
  class AgentsChat extends base_command_1.BaseCommand {
215
231
  static description = 'Run an agent turn with live streaming, tool events, cancellation, and reconnects';
216
232
  static examples = [
217
233
  '<%= config.bin %> agents chat <agent-id> --prompt "Review the open tasks"',
218
234
  '<%= config.bin %> agents chat <agent-id> --prompt "Deploy the fix" --approve-tools',
235
+ '<%= config.bin %> agents chat <agent-id> --conversation <id> --prompt "Now apply it" --permission-mode acceptEdits',
219
236
  '<%= config.bin %> agents chat <agent-id> --prompt "Summarize" --jsonl',
220
237
  '<%= config.bin %> agents chat <agent-id> --prompt "Summarize" --no-stream --json',
221
238
  '<%= config.bin %> agents chat <agent-id> --prompt "Review this diff" --local-skill code-review',
@@ -250,7 +267,12 @@ class AgentsChat extends base_command_1.BaseCommand {
250
267
  '`stream.gap` event means envelopes in that seq range never arrived and the streamed ' +
251
268
  'text is short; `run.result` and the persisted message remain authoritative. Tool ' +
252
269
  'calls arrive as `tool.started` / `tool.completed`, once per `toolUseId`, on cloud ' +
253
- 'and local-harness runs alike.',
270
+ 'and local-harness runs alike. A `session.notice` with `notice: ' +
271
+ '"harness-session-reset"` means the local harness could not resume its previous ' +
272
+ 'session and started a new one; `message` says why. A `permission.resolved` says ' +
273
+ 'how a local harness permission request was decided: `decidedBy` in words, and ' +
274
+ '`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
275
+ '`session-grant` — beside the wire `by`.',
254
276
  }),
255
277
  json: core_1.Flags.boolean({
256
278
  description: 'Emit the final one-shot response as JSON (use with --no-stream)',
@@ -260,6 +282,15 @@ class AgentsChat extends base_command_1.BaseCommand {
260
282
  'with no terminal the flag itself is the approval for this turn, and each one is ' +
261
283
  'reported on the stream. Without it every such call is denied.',
262
284
  }),
285
+ 'permission-mode': core_1.Flags.string({
286
+ description: 'Permission mode for THIS turn only, on a local-harness agent. It beats the agent’s and ' +
287
+ 'the harness’s stored defaults for this turn and changes nothing else — pass it again ' +
288
+ 'on each turn that needs it (`agents update --default-permission-mode` changes every ' +
289
+ 'conversation of the agent). A mode the harness cannot honour runs as the one it can, ' +
290
+ 'and run.started says which. --approve-tools is separate: it answers the approval ' +
291
+ 'prompts a mode asks for.',
292
+ options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
293
+ }),
263
294
  'local-skill': core_1.Flags.string({
264
295
  description: 'Invoke a skill on your machine on this turn, as `skrr skills local` lists it: its ' +
265
296
  'name, or <root>/<name> when two roots share one. The turn runs on that machine. ' +
@@ -354,6 +385,10 @@ class AgentsChat extends base_command_1.BaseCommand {
354
385
  'response and streams no events. Use --quick --json for a machine-readable ' +
355
386
  'one-shot, or drop --quick for a real streamed turn.', { exit: 2 });
356
387
  }
388
+ if (flags.quick && flags['permission-mode']) {
389
+ this.error('--permission-mode has no meaning with --quick: the persona-only summarizer runs no ' +
390
+ 'tools, so there is nothing for a mode to govern. Drop --quick for a real turn.', { exit: 2 });
391
+ }
357
392
  if (flags.quick && flags['local-skill']?.length) {
358
393
  this.error('--local-skill has no meaning with --quick: the persona-only summarizer runs no ' +
359
394
  'runtime, so no skill on your machine can load. Drop --quick for a real turn.', { exit: 2 });
@@ -485,6 +520,7 @@ class AgentsChat extends base_command_1.BaseCommand {
485
520
  // execution policy places it elsewhere keeps that placement, and the
486
521
  // run fails saying why rather than running without the skill.
487
522
  ...daemonPinFields(localSkills, flags.daemon),
523
+ ...permissionModeFields(flags['permission-mode']),
488
524
  });
489
525
  }
490
526
  catch (err) {
@@ -13,6 +13,15 @@ export declare function agentToneLine(agent: {
13
13
  toneMode?: unknown;
14
14
  toneId?: unknown;
15
15
  }): string;
16
+ /**
17
+ * The permission mode this agent runs with on its harness, and the layer that
18
+ * decided it (OSK-12069). The server resolves it (`effectivePermissionMode`) the
19
+ * way the dispatch does; the stored agent field alone is one of four layers, so
20
+ * printing it read "not set" for an agent whose harness default governed.
21
+ * Falls back to the stored field against a server that does not send the
22
+ * resolution, and says that is all it knows.
23
+ */
24
+ export declare function agentPermissionLines(agent: Record<string, unknown>, harness: string): string[];
16
25
  /** Text rendering is deliberately based on the server projection, with a
17
26
  * storage-shape fallback while all stored policies are migrated. */
18
27
  export declare function agentExecutionOrderLines(agent: Record<string, unknown>): string[];
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.agentToneLine = agentToneLine;
4
+ exports.agentPermissionLines = agentPermissionLines;
4
5
  exports.agentExecutionOrderLines = agentExecutionOrderLines;
5
6
  const core_1 = require("@oclif/core");
6
7
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -31,6 +32,46 @@ function agentToneLine(agent) {
31
32
  return 'inherit — see `skrr agents tone show`';
32
33
  return '-';
33
34
  }
35
+ const PERMISSION_SOURCE_LABELS = {
36
+ agent: "this agent's default",
37
+ harness: "the harness's default",
38
+ };
39
+ /**
40
+ * The permission mode this agent runs with on its harness, and the layer that
41
+ * decided it (OSK-12069). The server resolves it (`effectivePermissionMode`) the
42
+ * way the dispatch does; the stored agent field alone is one of four layers, so
43
+ * printing it read "not set" for an agent whose harness default governed.
44
+ * Falls back to the stored field against a server that does not send the
45
+ * resolution, and says that is all it knows.
46
+ */
47
+ function agentPermissionLines(agent, harness) {
48
+ const effective = agent.effectivePermissionMode;
49
+ const pad = ' ';
50
+ if (!effective || typeof effective.source !== 'string') {
51
+ const stored = typeof agent.defaultPermissionMode === 'string' && agent.defaultPermissionMode
52
+ ? agent.defaultPermissionMode
53
+ : "not set on the agent (the harness's or the daemon's default applies)";
54
+ return [`Permission: ${stored} — enforced by the ${harness} harness`];
55
+ }
56
+ const lines = [];
57
+ if (typeof effective.mode === 'string' && effective.mode) {
58
+ const from = PERMISSION_SOURCE_LABELS[effective.source] ?? effective.source;
59
+ lines.push(`Permission: ${effective.mode} (from ${from}) — enforced by the ${harness} harness`);
60
+ }
61
+ else {
62
+ lines.push(`Permission: set by the daemon's own config on that machine — no agent or harness mode is set`);
63
+ }
64
+ if (typeof effective.coercedFrom === 'string' && effective.coercedFrom) {
65
+ lines.push(`${pad}${effective.coercedFrom} is configured, but ${harness} cannot honour it, so it runs as ${String(effective.mode)}.`);
66
+ }
67
+ const supported = Array.isArray(effective.supportedModes)
68
+ ? effective.supportedModes.filter((m) => typeof m === 'string')
69
+ : [];
70
+ if (supported.length > 0)
71
+ lines.push(`${pad}${harness} accepts: ${supported.join(', ')}`);
72
+ lines.push(`${pad}A turn's own mode (\`agents chat --permission-mode\`) overrides this for that turn.`);
73
+ return lines;
74
+ }
34
75
  /** Text rendering is deliberately based on the server projection, with a
35
76
  * storage-shape fallback while all stored policies are migrated. */
36
77
  function agentExecutionOrderLines(agent) {
@@ -163,6 +204,18 @@ class AgentsShow extends base_command_1.BaseCommand {
163
204
  this.log(line);
164
205
  if (a.harnessName || a.harnessProvider || a.harnessStatus) {
165
206
  this.log(`Harness: ${a.harnessName ?? (typeof a.harnessProvider === 'string' ? (0, harness_provider_input_1.canonicalHarnessInput)(a.harnessProvider) : a.harnessProvider) ?? '-'}${a.harnessStatus ? ` (${a.harnessStatus})` : ''}`);
207
+ // On a harness, the HARNESS decides what runs and which permission modes
208
+ // exist — `Provider` is only the cloud model contract, so an agent read
209
+ // as "anthropic" while running Codex (OSK-12071). And the mode itself was
210
+ // visible only in --json (OSK-12069).
211
+ if (typeof a.harnessProvider === 'string' && a.harnessProvider) {
212
+ const harness = (0, harness_provider_input_1.canonicalHarnessInput)(a.harnessProvider);
213
+ for (const line of agentPermissionLines(a, harness))
214
+ this.log(line);
215
+ if (typeof a.provider === 'string' && a.provider && a.provider !== harness) {
216
+ this.log(` Runs on ${harness}; Provider "${a.provider}" is only the cloud model contract.`);
217
+ }
218
+ }
166
219
  // An offline engine is the difference between "this agent works" and
167
220
  // "this agent silently does nothing", and the display name alone cannot
168
221
  // carry it: two runtimes for the same machine share an identical name,
@@ -55,3 +55,12 @@ export default class AgentsUpdate extends BaseCommand {
55
55
  };
56
56
  run(): Promise<void>;
57
57
  }
58
+ /**
59
+ * What changing the agent's default permission mode actually changed
60
+ * (OSK-12124). It is the AGENT's default, so it governs every conversation,
61
+ * task run and Commitment Run of the agent from its next turn — which the bare
62
+ * "Updated agent" line never said, and the only way to learn that a live
63
+ * conversation picked it up was to spend another turn. Names the per-turn
64
+ * alternative for the case that wanted one conversation changed.
65
+ */
66
+ export declare function defaultPermissionModeNote(requested: string | undefined, updated: Record<string, unknown>): string | null;
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.defaultPermissionModeNote = defaultPermissionModeNote;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const agent_visual_refs_1 = require("../../lib/agent-visual-refs");
@@ -50,6 +51,8 @@ class AgentsUpdate extends base_command_1.BaseCommand {
50
51
  }),
51
52
  'default-permission-mode': core_1.Flags.string({
52
53
  description: "Permission mode for this agent's local-harness sessions (every harness, not only Claude Code). " +
54
+ 'It is the default for EVERY conversation, task run and Commitment Run of the agent; for one ' +
55
+ 'turn only use `agents chat --permission-mode`. ' +
53
56
  'One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
54
57
  'bypassPermissions; any other mode is refused for them.',
55
58
  options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
@@ -213,6 +216,30 @@ class AgentsUpdate extends base_command_1.BaseCommand {
213
216
  const a = updated;
214
217
  this.log(`Updated agent ${a.id ?? a._id ?? args.id} — ${a.name ?? '(no name)'}`);
215
218
  this.log(`URL: ${(0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, { ...a, id: a.id ?? a._id ?? args.id }).url ?? '-'}`);
219
+ const modeNote = defaultPermissionModeNote(flags['default-permission-mode'], a);
220
+ if (modeNote)
221
+ this.log(modeNote);
216
222
  }
217
223
  }
218
224
  exports.default = AgentsUpdate;
225
+ /**
226
+ * What changing the agent's default permission mode actually changed
227
+ * (OSK-12124). It is the AGENT's default, so it governs every conversation,
228
+ * task run and Commitment Run of the agent from its next turn — which the bare
229
+ * "Updated agent" line never said, and the only way to learn that a live
230
+ * conversation picked it up was to spend another turn. Names the per-turn
231
+ * alternative for the case that wanted one conversation changed.
232
+ */
233
+ function defaultPermissionModeNote(requested, updated) {
234
+ if (!requested)
235
+ return null;
236
+ const stored = typeof updated.defaultPermissionMode === 'string' && updated.defaultPermissionMode
237
+ ? updated.defaultPermissionMode
238
+ : requested;
239
+ return [
240
+ `Permission: ${stored} is now this agent's default for EVERY conversation, task run and`,
241
+ ' Commitment Run, starting with each one’s next turn. A turn that names its own',
242
+ ' mode still wins. To change one turn only, leave the default alone and pass',
243
+ ` \`skrr agents chat <agent> --conversation <id> --permission-mode ${requested}\`.`,
244
+ ].join('\n');
245
+ }
@@ -1,5 +1,6 @@
1
1
  import type { TSkyCodeUsageSnapshot } from '@skrr-ai/data-provider';
2
2
  import { BaseCommand } from '../../base-command';
3
+ import type { WorkspaceBalance } from '../../lib/balance';
3
4
  import { type RuntimeMonthlySpend } from '../../lib/harnesses';
4
5
  /**
5
6
  * The hosted-runtime monthly cap rows, as lines.
@@ -8,6 +9,32 @@ import { type RuntimeMonthlySpend } from '../../lib/harnesses';
8
9
  * the wording is part of the observable contract and is pinned directly.
9
10
  */
10
11
  export declare function renderRuntimeSpend(spend: RuntimeMonthlySpend[], log: (line: string) => void): void;
12
+ /**
13
+ * The workspace the next session on this machine is billed to, and why that one.
14
+ *
15
+ * `selected`: `workspaces use` / `--workspace` / a daemon handoff put it in
16
+ * `OVERSKY_WORKSPACE_ID`. `default`: nothing is selected, so the session carries
17
+ * the caller's server-side default workspace — the same resolution `whoami` and
18
+ * `workspaces list` describe ("resolves to your default workspace").
19
+ */
20
+ export interface SessionBillingTarget {
21
+ workspaceId: string;
22
+ name?: string;
23
+ source: 'selected' | 'default';
24
+ }
25
+ /**
26
+ * The account the NEXT session bills against, as lines — printed FIRST (OSK-12122).
27
+ *
28
+ * `skrr balance` opened on the personal account, so a user whose sessions bill
29
+ * the default workspace read `Credits: 999,503,672,326 ($999,503.67)` while every
30
+ * session was refused against that workspace's $0. The screen that exists to
31
+ * answer "why was I refused for credit" answered for the one account the
32
+ * refusal was not about, and nothing on it said which account that was.
33
+ *
34
+ * `balance` absent means the read failed: say so, never print a figure.
35
+ * Pure so the wording is pinned directly, like `renderRuntimeSpend`.
36
+ */
37
+ export declare function renderSessionBillingAccount(target: SessionBillingTarget, balance: WorkspaceBalance | null, log: (line: string) => void, bin?: string): void;
11
38
  /**
12
39
  * Your plan's ceilings, as lines.
13
40
  *
@@ -66,6 +93,13 @@ export default class BalanceShow extends BaseCommand {
66
93
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
67
94
  };
68
95
  run(): Promise<void>;
96
+ /**
97
+ * Which workspace the next session carries: the selected one, else the
98
+ * caller's default. Fail-open — an unreadable listing with nothing selected
99
+ * returns null and the screen falls back to the personal view it always had,
100
+ * rather than guessing an account.
101
+ */
102
+ private resolveSessionBillingTarget;
69
103
  /**
70
104
  * Your plan's ceilings, beside the balance they are spent before.
71
105
  *
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.renderRuntimeSpend = renderRuntimeSpend;
4
+ exports.renderSessionBillingAccount = renderSessionBillingAccount;
4
5
  exports.renderPlanCeilings = renderPlanCeilings;
5
6
  const core_1 = require("@oclif/core");
6
7
  const auth_core_1 = require("@skrr-ai/auth-core");
@@ -37,6 +38,45 @@ function renderRuntimeSpend(spend, log) {
37
38
  }
38
39
  }
39
40
  }
41
+ /**
42
+ * The account the NEXT session bills against, as lines — printed FIRST (OSK-12122).
43
+ *
44
+ * `skrr balance` opened on the personal account, so a user whose sessions bill
45
+ * the default workspace read `Credits: 999,503,672,326 ($999,503.67)` while every
46
+ * session was refused against that workspace's $0. The screen that exists to
47
+ * answer "why was I refused for credit" answered for the one account the
48
+ * refusal was not about, and nothing on it said which account that was.
49
+ *
50
+ * `balance` absent means the read failed: say so, never print a figure.
51
+ * Pure so the wording is pinned directly, like `renderRuntimeSpend`.
52
+ */
53
+ function renderSessionBillingAccount(target, balance, log, bin = 'skrr') {
54
+ const label = target.name ? `"${target.name}" (${target.workspaceId})` : target.workspaceId;
55
+ const why = target.source === 'selected'
56
+ ? 'the workspace selected for this CLI'
57
+ : 'no workspace is selected, so sessions resolve to your default workspace';
58
+ log(`Next session bills: workspace ${label}`);
59
+ log(` (${why})`);
60
+ if (!balance) {
61
+ log(` Balance: unavailable (\`${bin} balance show --workspace ${target.workspaceId}\` to retry)`);
62
+ return;
63
+ }
64
+ if (!balance.exists) {
65
+ log(' No billing account yet — nothing has ever been funded or spent here.');
66
+ log(' WARNING: a managed session carrying this workspace cannot be funded; it will be refused.');
67
+ }
68
+ else {
69
+ for (const line of (0, balance_1.workspaceBalanceLines)(balance))
70
+ log(line);
71
+ if (balance.availableMicros <= 0) {
72
+ log(' WARNING: nothing available — managed sessions billed here are refused for credit, ' +
73
+ 'whatever your personal balance below says.');
74
+ }
75
+ }
76
+ if (balance.availableMicros <= 0) {
77
+ log(` Add credit to this workspace, or bill another one with \`${bin} workspaces use <workspace>\`.`);
78
+ }
79
+ }
40
80
  /**
41
81
  * Your plan's ceilings, as lines.
42
82
  *
@@ -135,17 +175,42 @@ class BalanceShow extends base_command_1.BaseCommand {
135
175
  catch (err) {
136
176
  this.handleApiError(err);
137
177
  }
178
+ // OSK-12122 — the account the next session bills, resolved the way the
179
+ // session resolves it, read before anything is printed so it can go first.
180
+ const sessionTarget = await this.resolveSessionBillingTarget();
181
+ let sessionBalance = null;
182
+ if (sessionTarget) {
183
+ try {
184
+ sessionBalance = await (0, balance_1.getWorkspaceBalance)(sessionTarget.workspaceId);
185
+ }
186
+ catch {
187
+ sessionBalance = null;
188
+ }
189
+ }
138
190
  if (flags.json) {
139
- this.log(JSON.stringify(balance, null, 2));
191
+ // Additive: the personal balance keeps its shape, and the account a
192
+ // session bills rides beside it (`null` when no workspace resolves).
193
+ this.log(JSON.stringify({
194
+ ...(balance ?? {}),
195
+ sessionBilling: sessionTarget
196
+ ? { ...sessionTarget, balance: sessionBalance, balanceReadable: !!sessionBalance }
197
+ : null,
198
+ }, null, 2));
140
199
  return;
141
200
  }
142
201
  if (!balance) {
143
202
  this.error('No balance record for this account.', { exit: 1 });
144
203
  }
204
+ if (sessionTarget) {
205
+ renderSessionBillingAccount(sessionTarget, sessionBalance, (line) => this.log(line), this.config.bin);
206
+ this.log('');
207
+ }
145
208
  // Name the account. "Hosted usage balance" alone reads as "the balance
146
209
  // that matters here", which is exactly the wrong reading for someone sent
147
210
  // by a workspace refusal.
148
- this.log('Hosted usage balance — your personal account');
211
+ this.log(sessionTarget
212
+ ? 'Hosted usage balance — your personal account (not what the session above bills)'
213
+ : 'Hosted usage balance — your personal account');
149
214
  // Credits AND dollars, because a credit is a micro-dollar and nothing on
150
215
  // this surface said so. Four spend screens reported four units with no
151
216
  // stated relationship between them (OSK-4664).
@@ -235,6 +300,31 @@ class BalanceShow extends base_command_1.BaseCommand {
235
300
  this.log('`skrr balance show --workspace <id>`. `skrr payments wallet` is the');
236
301
  this.log('separate x402 crypto wallet, in USDC — not this balance.');
237
302
  }
303
+ /**
304
+ * Which workspace the next session carries: the selected one, else the
305
+ * caller's default. Fail-open — an unreadable listing with nothing selected
306
+ * returns null and the screen falls back to the personal view it always had,
307
+ * rather than guessing an account.
308
+ */
309
+ async resolveSessionBillingTarget() {
310
+ const selected = process.env.OVERSKY_WORKSPACE_ID?.trim();
311
+ let listing = null;
312
+ try {
313
+ listing = await (0, workspaces_1.listWorkspaceListing)();
314
+ }
315
+ catch {
316
+ listing = null;
317
+ }
318
+ const workspaceId = selected || listing?.defaultWorkspaceId || null;
319
+ if (!workspaceId)
320
+ return null;
321
+ const name = listing?.rows.find((row) => row.id === workspaceId)?.name;
322
+ return {
323
+ workspaceId,
324
+ ...(name ? { name } : {}),
325
+ source: selected ? 'selected' : 'default',
326
+ };
327
+ }
238
328
  /**
239
329
  * Your plan's ceilings, beside the balance they are spent before.
240
330
  *
@@ -6,6 +6,7 @@ export declare function doctorVerdict(input: {
6
6
  }): {
7
7
  verdict: string;
8
8
  next: string;
9
+ why?: string;
9
10
  };
10
11
  export default class CommitmentsDoctor extends BaseCommand {
11
12
  static description: string;
@@ -15,8 +15,12 @@ function doctorVerdict(input) {
15
15
  if (operatingState.status === 'waiting_approval') {
16
16
  return { verdict: 'waiting_approval', next: 'Open the pending prepared action for review' };
17
17
  }
18
- if (operatingState.coverage && operatingState.coverage.complete === false) {
19
- return { verdict: 'degraded', next: 'Retry doctor; one or more operating-state projections are unavailable' };
18
+ if (operatingState.coverage &&
19
+ operatingState.coverage.complete === false) {
20
+ return {
21
+ verdict: 'degraded',
22
+ next: 'Retry doctor; one or more operating-state projections are unavailable',
23
+ };
20
24
  }
21
25
  const autonomy = operatingState.autonomy;
22
26
  const budget = autonomy?.budget;
@@ -30,9 +34,18 @@ function doctorVerdict(input) {
30
34
  }
31
35
  if (operatingState.status && commitment.status === 'active') {
32
36
  const status = String(operatingState.status);
37
+ const healthy = ['on_track', 'healthy'].includes(status);
38
+ // Where the latest cycle stopped, as the server words it. "Read the raw
39
+ // trace" was the only next step an unhealthy Commitment was ever given,
40
+ // even when the stop already named the fix (OSK-12120).
41
+ const latestCheck = operatingState.latestCheck;
42
+ const stop = latestCheck?.stop;
43
+ const fix = typeof stop?.fix === 'string' && stop.fix ? stop.fix : undefined;
44
+ const why = typeof stop?.detail === 'string' && stop.detail ? stop.detail : undefined;
33
45
  return {
34
- verdict: ['on_track', 'healthy'].includes(status) ? 'active_healthy' : status,
35
- next: 'skrr commitments trace <id> --limit 50',
46
+ verdict: healthy ? 'active_healthy' : status,
47
+ next: !healthy && fix ? fix : 'skrr commitments trace <id> --limit 50',
48
+ ...(!healthy && why ? { why } : {}),
36
49
  };
37
50
  }
38
51
  if (commitment.status !== 'active')
@@ -81,6 +94,8 @@ class CommitmentsDoctor extends base_command_1.BaseCommand {
81
94
  this.log(JSON.stringify(report, null, 2));
82
95
  else {
83
96
  this.log(`Verdict: ${diagnosis.verdict}`);
97
+ if (diagnosis.why)
98
+ this.log(`Why: ${diagnosis.why}`);
84
99
  this.log(`Next: ${diagnosis.next.replace('<id>', args.id)}`);
85
100
  const autonomy = operatingState.autonomy;
86
101
  if (autonomy) {
@@ -15,6 +15,9 @@ function effectivePolicyLines(policy) {
15
15
  const governance = policy.governance || {};
16
16
  const owner = policy.owner || {};
17
17
  const lines = [`Mode in force: ${(0, commitment_product_1.productModeLabel)(governance.effectiveAutonomyMode)}`];
18
+ // Whose permissions a run uses (OSK-11975), directly under the mode it can
19
+ // contradict — an Execute Commitment on a plan-mode Agent (OSK-12065).
20
+ lines.push(...(0, commitments_1.permissionLines)(policy.permissions));
18
21
  if (governance.blocked) {
19
22
  lines.push(`Workspace status: blocked${governance.blockedReason ? ` — ${governance.blockedReason}` : ''}`);
20
23
  }
@@ -56,7 +59,18 @@ function effectivePolicyLines(policy) {
56
59
  // part that is NOT enforced, because that is the one an operator granting
57
60
  // it has to see.
58
61
  const observation = policy.observation;
59
- if (observation?.sensing) {
62
+ if (observation?.sensing && observation.retired) {
63
+ // OSK-11975 retired the sensing fence: printing "granted — read-only" for
64
+ // a grant that no longer fences anything is the claim this must not make.
65
+ const recorded = [
66
+ observation.shell?.granted ? 'shell' : null,
67
+ observation.browser?.granted ? 'browser' : null,
68
+ ].filter(Boolean);
69
+ lines.push(`Sensing runs: the Agent's own permissions${recorded.length
70
+ ? ` (recorded ${recorded.join(' and ')} grant is retired and has no effect)`
71
+ : ''}`);
72
+ }
73
+ else if (observation?.sensing) {
60
74
  lines.push(`Sensing shell: ${observation.shell?.granted
61
75
  ? 'granted — read-only filesystem on the Hosted Machine; network NOT fenced (a command can still change remote state); withheld on your own machine'
62
76
  : 'not granted'}`);
@@ -75,7 +75,9 @@ function formatNextTick(projection) {
75
75
  `Posture: ${decision.posture ?? '-'}`,
76
76
  `Health: ${decision.health ?? '-'}`,
77
77
  `Risk: ${decision.riskLevel ?? '-'}`,
78
- `Confidence: ${decision.confidence ?? '-'}`,
78
+ // Two decimals: the arbiter's arithmetic yields values like
79
+ // 0.6000000000000001, which reads as a precision the number does not have.
80
+ `Confidence: ${typeof decision.confidence === 'number' ? decision.confidence.toFixed(2) : '-'}`,
79
81
  ];
80
82
  if (decision.whyNow)
81
83
  lines.push(`Why now: ${decision.whyNow}`);