@skrr-ai/cli 0.1.51 → 0.1.53

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 (67) hide show
  1. package/dist/base-command.d.ts +7 -0
  2. package/dist/base-command.js +38 -8
  3. package/dist/commands/agents/chat.js +2 -1
  4. package/dist/commands/agents/create.js +21 -8
  5. package/dist/commands/agents/update.d.ts +55 -0
  6. package/dist/commands/agents/update.js +119 -23
  7. package/dist/commands/commitments/next-tick.js +9 -1
  8. package/dist/commands/context.js +2 -1
  9. package/dist/commands/daemon/config.js +16 -1
  10. package/dist/commands/doctor.js +8 -1
  11. package/dist/commands/login.d.ts +2 -1
  12. package/dist/commands/login.js +26 -7
  13. package/dist/commands/logout.js +3 -0
  14. package/dist/commands/machines/dedicated/show.js +25 -2
  15. package/dist/commands/tasks/approvals/list.d.ts +22 -0
  16. package/dist/commands/tasks/approvals/list.js +80 -3
  17. package/dist/commands/tasks/create.d.ts +5 -0
  18. package/dist/commands/tasks/create.js +28 -7
  19. package/dist/commands/tasks/start.d.ts +14 -0
  20. package/dist/commands/tasks/start.js +31 -1
  21. package/dist/commands/tasks/update.js +30 -3
  22. package/dist/commands/whoami.js +5 -3
  23. package/dist/lib/agent-config.d.ts +15 -0
  24. package/dist/lib/agent-config.js +46 -10
  25. package/dist/lib/agentic-stream.d.ts +21 -1
  26. package/dist/lib/agentic-stream.js +40 -6
  27. package/dist/lib/auth-failure.js +4 -1
  28. package/dist/lib/auth-storage.d.ts +24 -0
  29. package/dist/lib/auth-storage.js +348 -13
  30. package/dist/lib/cli-id.d.ts +22 -0
  31. package/dist/lib/cli-id.js +56 -0
  32. package/dist/lib/cli-identity.d.ts +49 -0
  33. package/dist/lib/cli-identity.js +159 -0
  34. package/dist/lib/commitments.d.ts +4 -0
  35. package/dist/lib/commitments.js +12 -1
  36. package/dist/lib/config.d.ts +17 -0
  37. package/dist/lib/config.js +15 -3
  38. package/dist/lib/cred-envelope.d.ts +12 -1
  39. package/dist/lib/cred-envelope.js +20 -3
  40. package/dist/lib/daemonBroker.d.ts +26 -4
  41. package/dist/lib/daemonBroker.js +82 -10
  42. package/dist/lib/daemonBrokerRefusal.d.ts +51 -0
  43. package/dist/lib/daemonBrokerRefusal.js +127 -0
  44. package/dist/lib/dedicated-machines.d.ts +53 -4
  45. package/dist/lib/dedicated-machines.js +95 -5
  46. package/dist/lib/delegated-cli.js +16 -0
  47. package/dist/lib/first-party-harness-broker.js +6 -4
  48. package/dist/lib/keychain.d.ts +39 -0
  49. package/dist/lib/keychain.js +254 -40
  50. package/dist/lib/label-ref.d.ts +34 -0
  51. package/dist/lib/label-ref.js +72 -1
  52. package/dist/lib/login.js +24 -12
  53. package/dist/lib/oauthLogin.js +2 -8
  54. package/dist/lib/refresh.js +6 -2
  55. package/dist/lib/tasks.js +33 -8
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +9 -3
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +8 -3
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  65. package/dist/node_modules/@skrr-ai/data-provider/index.js +3138 -3100
  66. package/oclif.manifest.json +26470 -26449
  67. package/package.json +1 -1
@@ -4,6 +4,7 @@ import { type AssigneeResolution, type MemberKind, type SpaceMember } from './li
4
4
  import { type CliConfig } from './lib/config';
5
5
  import { type OutboxMethod } from './lib/outbox';
6
6
  import { type ResolvedCredential } from './lib/credential-resolver';
7
+ import { type BrokerRefusalDescription } from './lib/daemonBrokerRefusal';
7
8
  import { type DefaultAgentResolution } from './lib/tasks';
8
9
  export declare function assigneePayloadNeedsResolution(payload: Record<string, unknown>): boolean;
9
10
  /**
@@ -68,6 +69,12 @@ export declare abstract class BaseCommand extends Command {
68
69
  */
69
70
  protected resolvedCredential: ResolvedCredential;
70
71
  protected bareMode: boolean;
72
+ /**
73
+ * Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
74
+ * `requireAuth()` reports it instead of a bare "Not signed in", because the
75
+ * daemon's reason decides the repair (OSK-12149, OSK-12151).
76
+ */
77
+ protected brokerRefusal: BrokerRefusalDescription | null;
71
78
  init(): Promise<void>;
72
79
  /**
73
80
  * Read only the two global base flags. A narrow parser — oclif's
@@ -35,6 +35,7 @@ const task_ref_resolver_1 = require("./lib/task-ref-resolver");
35
35
  const space_resolver_1 = require("./lib/space-resolver");
36
36
  const projects_1 = require("./lib/projects");
37
37
  const config_1 = require("./lib/config");
38
+ const cli_id_1 = require("./lib/cli-id");
38
39
  const keychain_2 = require("./lib/keychain");
39
40
  const outbox_1 = require("./lib/outbox");
40
41
  const node_adapter_1 = require("./lib/node-adapter");
@@ -42,6 +43,7 @@ const migrate_1 = require("./lib/migrate");
42
43
  const auth_core_init_1 = require("./lib/auth-core-init");
43
44
  const credential_resolver_1 = require("./lib/credential-resolver");
44
45
  const daemonBroker_1 = require("./lib/daemonBroker");
46
+ const daemonBrokerRefusal_1 = require("./lib/daemonBrokerRefusal");
45
47
  const refresh_1 = require("./lib/refresh");
46
48
  const auth_storage_1 = require("./lib/auth-storage");
47
49
  const update_check_1 = require("./lib/update-check");
@@ -131,6 +133,12 @@ class BaseCommand extends core_1.Command {
131
133
  */
132
134
  resolvedCredential;
133
135
  bareMode = false;
136
+ /**
137
+ * Set when the auto-broker reached a live daemon and it REFUSED the hand-off.
138
+ * `requireAuth()` reports it instead of a bare "Not signed in", because the
139
+ * daemon's reason decides the repair (OSK-12149, OSK-12151).
140
+ */
141
+ brokerRefusal = null;
134
142
  async init() {
135
143
  await super.init();
136
144
  // The profile is resolved from raw argv, BEFORE oclif parses anything.
@@ -269,11 +277,13 @@ class BaseCommand extends core_1.Command {
269
277
  // skrr command reads it through the normal resolver chain — no
270
278
  // per-command broker round-trip.
271
279
  //
272
- // Fail-quiet: any broker failure leaves resolvedCredential null and
273
- // the command's own `requireAuth()` will surface the "Not signed
274
- // in" error a moment later. Mismatched baseURL (a real foot-gun the
275
- // user might not notice) is the one exception — we print a one-
276
- // line hint so they know how to fix it.
280
+ // A broker failure leaves resolvedCredential null and the command's own
281
+ // `requireAuth()` surfaces the error a moment later. It is quiet only when
282
+ // no daemon was there to ask. A daemon that ANSWERED and refused is kept
283
+ // in `brokerRefusal`, and `requireAuth()` reports its reason and the
284
+ // repair that reason implies — "Not signed in. Run `skrr login`" was true
285
+ // and pointed at the wrong fix (OSK-12149). Rate limiting and a mismatched
286
+ // baseURL keep their own one-line hints below.
277
287
  if (!this.resolvedCredential.token) {
278
288
  const autoResult = await (0, daemonBroker_1.maybeAutoBroker)({
279
289
  cliConfig: this.cliConfig,
@@ -318,6 +328,9 @@ class BaseCommand extends core_1.Command {
318
328
  kind: (0, auth_core_1.classifyTokenKind)(accessToken),
319
329
  };
320
330
  }
331
+ else if ((this.brokerRefusal = (0, daemonBrokerRefusal_1.describeBrokerRefusal)(autoResult.outcome, this.config.bin))) {
332
+ // Reported by requireAuth(); nothing to print here.
333
+ }
321
334
  else if (autoResult.outcome.reason === 'rate_limited') {
322
335
  // Otherwise the only thing the user sees is "Not signed in", which is
323
336
  // true and points at the wrong repair: they are signed in, the local
@@ -340,7 +353,12 @@ class BaseCommand extends core_1.Command {
340
353
  }
341
354
  }
342
355
  if (!this.resolvedCredential.token && ignoredStoredCredential) {
343
- if (ignoredStoredCredential.originStatus === 'mismatch') {
356
+ const foreign = (0, auth_storage_1.describeForeignCredential)(ignoredStoredCredential);
357
+ if (foreign) {
358
+ process.stderr.write(`[skrr] ${foreign} Run \`skrr login\` to sign this configuration in; the other ` +
359
+ `configuration keeps its own credential.\n`);
360
+ }
361
+ else if (ignoredStoredCredential.originStatus === 'mismatch') {
344
362
  process.stderr.write(`[skrr] Ignoring stored credentials issued by ${ignoredStoredCredential.storedServerOrigin}; ` +
345
363
  `skrr is configured for ${this.cliConfig.baseURL}. Run \`skrr login --base-url ${this.cliConfig.baseURL}\` ` +
346
364
  `or switch back to the issuing server.\n`);
@@ -616,6 +634,18 @@ class BaseCommand extends core_1.Command {
616
634
  /** Abort the command with a helpful message if the user isn't signed in. */
617
635
  requireAuth() {
618
636
  if (!this.resolvedCredential?.token) {
637
+ const refusal = this.brokerRefusal;
638
+ if (refusal) {
639
+ // Same code as the plain case — scripts branch on NOT_SIGNED_IN — with
640
+ // the daemon's answer in the message and in `details.broker`.
641
+ this.failWithCliError({
642
+ message: `Not signed in. ${refusal.headline}\n→ ${refusal.remedy}`,
643
+ code: 'NOT_SIGNED_IN',
644
+ exit: 2,
645
+ retryable: refusal.transient,
646
+ details: { broker: refusal.broker, suggestion: refusal.remedy },
647
+ });
648
+ }
619
649
  this.failWithCliError({
620
650
  message: `Not signed in. Run \`${this.config.bin} login\` first.`,
621
651
  code: 'NOT_SIGNED_IN',
@@ -1011,7 +1041,7 @@ class BaseCommand extends core_1.Command {
1011
1041
  ? null
1012
1042
  : (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1013
1043
  bin: this.config.bin,
1014
- cliId: this.cliConfig?.cliId,
1044
+ cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1015
1045
  });
1016
1046
  this.failWithCliError({
1017
1047
  message: say(presentedCiToken
@@ -1309,7 +1339,7 @@ class BaseCommand extends core_1.Command {
1309
1339
  if (e.status === 401) {
1310
1340
  const refusal = (0, auth_failure_1.describeRefreshRefusal)(readRefreshRefusal(), {
1311
1341
  bin: this.config.bin,
1312
- cliId: this.cliConfig?.cliId,
1342
+ cliId: this.cliConfig ? (0, cli_id_1.resolveCliId)(this.cliConfig) : undefined,
1313
1343
  });
1314
1344
  const why = refusal
1315
1345
  ? refusal.message.replace(/\s*Run `[^`]+` to sign in again\.$/, '')
@@ -269,7 +269,8 @@ class AgentsChat extends base_command_1.BaseCommand {
269
269
  'calls arrive as `tool.started` / `tool.completed`, once per `toolUseId`, on cloud ' +
270
270
  'and local-harness runs alike. A `session.notice` with `notice: ' +
271
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 ' +
272
+ 'session and started a new one, which does not see earlier turns of the ' +
273
+ 'conversation; `message` says why. A `permission.resolved` says ' +
273
274
  'how a local harness permission request was decided: `decidedBy` in words, and ' +
274
275
  '`basis` — `person` (with `surface`), `mode` (with `mode`), `policy` or ' +
275
276
  '`session-grant` — beside the wire `by`.',
@@ -246,22 +246,30 @@ class AgentsCreate extends base_command_1.BaseCommand {
246
246
  '09:00-18:00@America/Los_Angeles',
247
247
  }),
248
248
  // Retired (OSK-11975): a Commitment turn works under the Agent's own
249
- // permissions, so these three are stored but no longer read at runtime.
249
+ // permissions. Hidden, warned on use, and never sent (OSK-12068) — see
250
+ // `mergeAgentConfigFlags`. Kept parseable so an old script still runs.
250
251
  'reasoning-tool-escape': core_1.Flags.string({
251
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
252
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
252
253
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
253
254
  multiple: true,
254
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
255
+ deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
256
+ hidden: true,
255
257
  }),
256
258
  'commitment-responsibility': core_1.Flags.string({
257
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
259
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
258
260
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
259
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
261
+ deprecated: {
262
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
263
+ },
264
+ hidden: true,
260
265
  }),
261
266
  'commitment-responsibility-ceiling': core_1.Flags.string({
262
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
267
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
263
268
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
264
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
269
+ deprecated: {
270
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
271
+ },
272
+ hidden: true,
265
273
  }),
266
274
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
267
275
  'tool-resources': core_1.Flags.string({
@@ -389,7 +397,12 @@ class AgentsCreate extends base_command_1.BaseCommand {
389
397
  // relationshipLoops is an input-only provisioning control. The server
390
398
  // applies it by deciding which system commitments to seed, so it is not an
391
399
  // Agent document field and is intentionally absent from the response.
392
- const unapplied = unappliedPayloadKeys(payload, created, ['relationshipLoops']);
400
+ const unapplied = unappliedPayloadKeys(payload, created, [
401
+ 'relationshipLoops',
402
+ ...(0, agent_config_1.deprecatedFieldNames)(created),
403
+ ]);
404
+ for (const warning of (0, agent_config_1.deprecatedFieldsWarnings)(created))
405
+ this.warn(warning);
393
406
  if (unapplied.length > 0) {
394
407
  this.warn(unappliedPayloadKeysWarning(unapplied));
395
408
  }
@@ -18,6 +18,7 @@ export default class AgentsUpdate extends BaseCommand {
18
18
  'next-target': import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
19
  'working-directory': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
20
  'default-permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ 'clear-default-permission-mode': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
21
22
  tools: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
23
  'model-parameters': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
24
  'agent-ids': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
@@ -55,6 +56,60 @@ export default class AgentsUpdate extends BaseCommand {
55
56
  };
56
57
  run(): Promise<void>;
57
58
  }
59
+ /**
60
+ * The warnings an `agents update` prints for what the server did with the
61
+ * PATCH, in order: one line per field the server reported as ignored, with the
62
+ * SERVER's reason (`deprecatedFields`, OSK-12068), then one line for keys that
63
+ * came back missing for no stated reason.
64
+ *
65
+ * That last check is the one `agents create` makes. Without it, a key the
66
+ * server does not model, a typo of one that it does (`max_turns` for
67
+ * `maxTurns`), or a field only the server sets was discarded with the same
68
+ * "Updated agent" line as a real change. It skips:
69
+ * - `removeProjectIds`, an instruction rather than a stored field;
70
+ * - every field the server already explained in `deprecatedFields`, so it is
71
+ * not ALSO called "not an agent field" with the generic guess;
72
+ * - every key sent as `null`. `null` is a clear, and a cleared field coming
73
+ * back absent (or `null`) is the clear working. `defaultPermissionMode: null`
74
+ * through `--from-json` was reported as "not an agent field" after the server
75
+ * had cleared it on purpose (OSK-12167).
76
+ *
77
+ * Exported for tests.
78
+ */
79
+ export declare function agentUpdateWarnings(payload: Record<string, unknown>, updated: unknown): string[];
80
+ /**
81
+ * The per-Agent mutation fence's "another request holds this right now"
82
+ * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
83
+ * a concurrent writer — or, until OSK-12138, a lagging replica read of a lease
84
+ * row the same request had just created — refuses the update with one of these.
85
+ * Nothing was written, so repeating the identical PATCH is safe.
86
+ */
87
+ export declare const AGENT_FENCE_RETRY_CODES: ReadonlySet<string>;
88
+ /**
89
+ * Run an Agent write, and repeat it ONCE if the fence refused it as transient.
90
+ *
91
+ * Retried only when the server said both things: a fence code AND
92
+ * `retryable: true`. `MARKETPLACE_SOURCE_LIFECYCLE_BUSY` also covers a source
93
+ * that is being deleted or published, where the server answers
94
+ * `retryable: false`, and those must surface rather than be repeated. One retry,
95
+ * after a short pause, because the dogfooded case (OSK-12138) cleared on the
96
+ * first identical retry; anything that survives it is worth showing.
97
+ *
98
+ * Exported for tests.
99
+ */
100
+ export declare function retryOnceOnAgentFence<T>(operation: () => Promise<T>, { delayMs }?: {
101
+ delayMs?: number;
102
+ }): Promise<T>;
103
+ /**
104
+ * The `defaultPermissionMode` a PATCH carries for these flags. `null` is the
105
+ * server's clear, the same spelling `harnesses update --clear-default-permission-mode`
106
+ * sends; before it, a mode once set could not be removed from the CLI at all —
107
+ * `""` fails the flag's options and `null` failed the server (OSK-12070).
108
+ */
109
+ export declare function defaultPermissionModePatch(flags: {
110
+ 'default-permission-mode'?: string;
111
+ 'clear-default-permission-mode'?: boolean;
112
+ }): string | null | undefined;
58
113
  /**
59
114
  * What changing the agent's default permission mode actually changed
60
115
  * (OSK-12124). It is the AGENT's default, so it governs every conversation,
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AGENT_FENCE_RETRY_CODES = void 0;
4
+ exports.agentUpdateWarnings = agentUpdateWarnings;
5
+ exports.retryOnceOnAgentFence = retryOnceOnAgentFence;
6
+ exports.defaultPermissionModePatch = defaultPermissionModePatch;
3
7
  exports.defaultPermissionModeNote = defaultPermissionModeNote;
4
8
  const core_1 = require("@oclif/core");
5
9
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -56,6 +60,12 @@ class AgentsUpdate extends base_command_1.BaseCommand {
56
60
  'One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
57
61
  'bypassPermissions; any other mode is refused for them.',
58
62
  options: ['default', 'acceptEdits', 'bypassPermissions', 'plan'],
63
+ exclusive: ['clear-default-permission-mode'],
64
+ }),
65
+ 'clear-default-permission-mode': core_1.Flags.boolean({
66
+ description: "Remove this agent's permission-mode override, so its harness's default (then the daemon's) " +
67
+ 'applies. Allowed on every harness, including one-shot ones.',
68
+ exclusive: ['default-permission-mode'],
59
69
  }),
60
70
  tools: core_1.Flags.string({
61
71
  description: 'Tools JSON array of plugin keys, e.g. \'["calculator"]\'. `skrr agents tools` lists the keys, ' +
@@ -99,22 +109,30 @@ class AgentsUpdate extends base_command_1.BaseCommand {
99
109
  '09:00-18:00@America/Los_Angeles',
100
110
  }),
101
111
  // Retired (OSK-11975): a Commitment turn works under the Agent's own
102
- // permissions, so these three are stored but no longer read at runtime.
112
+ // permissions. Hidden, warned on use, and never sent (OSK-12068) — see
113
+ // `mergeAgentConfigFlags`. Kept parseable so an old script still runs.
103
114
  'reasoning-tool-escape': core_1.Flags.string({
104
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
115
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
105
116
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
106
117
  multiple: true,
107
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
118
+ deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
119
+ hidden: true,
108
120
  }),
109
121
  'commitment-responsibility': core_1.Flags.string({
110
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
122
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
111
123
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
112
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
124
+ deprecated: {
125
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
126
+ },
127
+ hidden: true,
113
128
  }),
114
129
  'commitment-responsibility-ceiling': core_1.Flags.string({
115
- description: "Deprecated — no longer has any effect. Commitment runs use the agent's own permissions.",
130
+ description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
116
131
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
117
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
132
+ deprecated: {
133
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
134
+ },
135
+ hidden: true,
118
136
  }),
119
137
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
120
138
  'tool-resources': core_1.Flags.string({
@@ -171,7 +189,7 @@ class AgentsUpdate extends base_command_1.BaseCommand {
171
189
  category: flags.category,
172
190
  executionPolicy,
173
191
  workingDirectory: flags['working-directory'],
174
- defaultPermissionMode: flags['default-permission-mode'],
192
+ defaultPermissionMode: defaultPermissionModePatch(flags),
175
193
  });
176
194
  // `mergeAgentConfigFlags` parses the typed flags (avatar, scene background,
177
195
  // working hours, every JSON flag). A parse failure is the caller's input, so
@@ -184,30 +202,24 @@ class AgentsUpdate extends base_command_1.BaseCommand {
184
202
  this.error(err.message, { exit: 1 });
185
203
  }
186
204
  if (Object.keys(payload).length === 0) {
187
- this.error('Nothing to update. Pass at least one flag (e.g. --name) or --from-json.', {
188
- exit: 1,
189
- });
205
+ const retired = agent_config_1.RETIRED_COMMITMENT_PERMISSION_FLAGS.filter((flag) => flags[flag] !== undefined);
206
+ this.error(retired.length > 0
207
+ ? `Nothing to update: ${retired.map((flag) => `--${flag}`).join(', ')} ` +
208
+ `${retired.length === 1 ? 'is' : 'are'} retired and not sent. ${agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED}`
209
+ : 'Nothing to update. Pass at least one flag (e.g. --name) or --from-json.', { exit: 1 });
190
210
  }
191
211
  let updated;
192
212
  try {
193
- updated = await data_provider_1.dataService.updateAgent({
213
+ updated = await retryOnceOnAgentFence(() => data_provider_1.dataService.updateAgent({
194
214
  agent_id: args.id,
195
215
  data: payload,
196
- });
216
+ }));
197
217
  }
198
218
  catch (err) {
199
219
  this.handleApiError(err);
200
220
  }
201
- // The same check `agents create` makes. Without it, a key the server does
202
- // not model, a typo of one that it does (`max_turns` for `maxTurns`), or a
203
- // field only the server sets was discarded with the same "Updated agent"
204
- // line as a real change.
205
- // `removeProjectIds` is an instruction, not a stored field, so it never
206
- // comes back and is not reported.
207
- const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, ['removeProjectIds']);
208
- if (unapplied.length > 0) {
209
- this.warn((0, create_1.unappliedPayloadKeysWarning)(unapplied));
210
- }
221
+ for (const warning of agentUpdateWarnings(payload, updated))
222
+ this.warn(warning);
211
223
  await (0, runtime_tool_support_1.warnPluginsNotLoaded)(payload.tools, (message) => this.warn(message));
212
224
  if (flags.json) {
213
225
  this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
@@ -219,9 +231,93 @@ class AgentsUpdate extends base_command_1.BaseCommand {
219
231
  const modeNote = defaultPermissionModeNote(flags['default-permission-mode'], a);
220
232
  if (modeNote)
221
233
  this.log(modeNote);
234
+ if (flags['clear-default-permission-mode']) {
235
+ this.log("Permission: cleared — this agent now follows its harness's default permission mode " +
236
+ "(then the daemon's), starting with each conversation's next turn.");
237
+ }
222
238
  }
223
239
  }
224
240
  exports.default = AgentsUpdate;
241
+ /**
242
+ * The warnings an `agents update` prints for what the server did with the
243
+ * PATCH, in order: one line per field the server reported as ignored, with the
244
+ * SERVER's reason (`deprecatedFields`, OSK-12068), then one line for keys that
245
+ * came back missing for no stated reason.
246
+ *
247
+ * That last check is the one `agents create` makes. Without it, a key the
248
+ * server does not model, a typo of one that it does (`max_turns` for
249
+ * `maxTurns`), or a field only the server sets was discarded with the same
250
+ * "Updated agent" line as a real change. It skips:
251
+ * - `removeProjectIds`, an instruction rather than a stored field;
252
+ * - every field the server already explained in `deprecatedFields`, so it is
253
+ * not ALSO called "not an agent field" with the generic guess;
254
+ * - every key sent as `null`. `null` is a clear, and a cleared field coming
255
+ * back absent (or `null`) is the clear working. `defaultPermissionMode: null`
256
+ * through `--from-json` was reported as "not an agent field" after the server
257
+ * had cleared it on purpose (OSK-12167).
258
+ *
259
+ * Exported for tests.
260
+ */
261
+ function agentUpdateWarnings(payload, updated) {
262
+ const cleared = Object.keys(payload).filter((key) => payload[key] === null);
263
+ const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, [
264
+ 'removeProjectIds',
265
+ ...cleared,
266
+ ...(0, agent_config_1.deprecatedFieldNames)(updated),
267
+ ]);
268
+ return [
269
+ ...(0, agent_config_1.deprecatedFieldsWarnings)(updated),
270
+ ...(unapplied.length > 0 ? [(0, create_1.unappliedPayloadKeysWarning)(unapplied)] : []),
271
+ ];
272
+ }
273
+ /**
274
+ * The per-Agent mutation fence's "another request holds this right now"
275
+ * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
276
+ * a concurrent writer — or, until OSK-12138, a lagging replica read of a lease
277
+ * row the same request had just created — refuses the update with one of these.
278
+ * Nothing was written, so repeating the identical PATCH is safe.
279
+ */
280
+ exports.AGENT_FENCE_RETRY_CODES = new Set([
281
+ 'MARKETPLACE_SOURCE_LIFECYCLE_RACE',
282
+ 'MARKETPLACE_SOURCE_LIFECYCLE_BUSY',
283
+ 'MARKETPLACE_SOURCE_LIFECYCLE_CAS_LOST',
284
+ ]);
285
+ /**
286
+ * Run an Agent write, and repeat it ONCE if the fence refused it as transient.
287
+ *
288
+ * Retried only when the server said both things: a fence code AND
289
+ * `retryable: true`. `MARKETPLACE_SOURCE_LIFECYCLE_BUSY` also covers a source
290
+ * that is being deleted or published, where the server answers
291
+ * `retryable: false`, and those must surface rather than be repeated. One retry,
292
+ * after a short pause, because the dogfooded case (OSK-12138) cleared on the
293
+ * first identical retry; anything that survives it is worth showing.
294
+ *
295
+ * Exported for tests.
296
+ */
297
+ async function retryOnceOnAgentFence(operation, { delayMs = 750 } = {}) {
298
+ try {
299
+ return await operation();
300
+ }
301
+ catch (err) {
302
+ const { code, body } = (0, base_command_1.apiRefusal)(err);
303
+ if (!code || !exports.AGENT_FENCE_RETRY_CODES.has(code) || body?.retryable !== true)
304
+ throw err;
305
+ if (delayMs > 0)
306
+ await new Promise((resolve) => setTimeout(resolve, delayMs));
307
+ return operation();
308
+ }
309
+ }
310
+ /**
311
+ * The `defaultPermissionMode` a PATCH carries for these flags. `null` is the
312
+ * server's clear, the same spelling `harnesses update --clear-default-permission-mode`
313
+ * sends; before it, a mode once set could not be removed from the CLI at all —
314
+ * `""` fails the flag's options and `null` failed the server (OSK-12070).
315
+ */
316
+ function defaultPermissionModePatch(flags) {
317
+ if (flags['clear-default-permission-mode'])
318
+ return null;
319
+ return flags['default-permission-mode'];
320
+ }
225
321
  /**
226
322
  * What changing the agent's default permission mode actually changed
227
323
  * (OSK-12124). It is the AGENT's default, so it governs every conversation,
@@ -86,10 +86,18 @@ function formatNextTick(projection) {
86
86
  // dispatch-refusal loop can't hide behind an on_track projection.
87
87
  if (projection.health || projection.liveness) {
88
88
  const census = projection.liveness?.terminalDispositionCensus;
89
+ const plural = (count) => (count === 1 ? '' : 's');
89
90
  const detail = [
90
91
  projection.liveness?.diagnosisKind,
91
92
  census?.workFailures
92
- ? `${census.workFailures} recent run failure${census.workFailures === 1 ? '' : 's'}`
93
+ ? `${census.workFailures} recent run failure${plural(census.workFailures)}`
94
+ : null,
95
+ // A run that never started is not a failure of the work (OSK-12137).
96
+ census?.machineFailures
97
+ ? `${census.machineFailures} run${plural(census.machineFailures)} never started`
98
+ : null,
99
+ census?.contractFailures
100
+ ? `${census.contractFailures} result${plural(census.contractFailures)} rejected`
93
101
  : null,
94
102
  ]
95
103
  .filter(Boolean)
@@ -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 config_1 = require("../lib/config");
7
+ const cli_id_1 = require("../lib/cli-id");
7
8
  const keychain_1 = require("../lib/keychain");
8
9
  const cli_runtime_1 = require("../lib/cli-runtime");
9
10
  const runtime_context_1 = require("../lib/runtime-context");
@@ -48,7 +49,7 @@ class Context extends base_command_1.BaseCommand {
48
49
  profile: (0, keychain_1.getActiveProfile)(),
49
50
  config: {
50
51
  path: (0, config_1.getConfigPath)(),
51
- cliId: this.cliConfig.cliId ?? null,
52
+ cliId: (0, cli_id_1.resolveCliId)(this.cliConfig) ?? null,
52
53
  },
53
54
  auth: {
54
55
  signedIn: Boolean(this.resolvedCredential?.token),
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
4
  const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
5
+ const live_runtime_binary_1 = require("../../lib/live-runtime-binary");
5
6
  /**
6
7
  * `skrr daemon config` — set this machine's daemon configuration, including
7
8
  * each tool category's permission mode (OSK-12099). Before this the only way
@@ -23,7 +24,21 @@ class DaemonConfig extends core_1.Command {
23
24
  // flag this wrapper has not redeclared (OSK-296).
24
25
  const argv = this.argv;
25
26
  try {
26
- const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['config', ...argv]);
27
+ // Config writes the file the LIVE daemon reloads, so it must be parsed by
28
+ // the live daemon's own binary — the same routing status uses (OSK-12102).
29
+ // Forwarding to the first `skrrd` on PATH let an older build accept
30
+ // `--auto-approve-bash banana` as false and reject `--mode`, then write
31
+ // the live daemon's config anyway (OSK-12099 verifier).
32
+ const pathBinary = (0, exec_runtime_binary_1.findRuntimeBinary)();
33
+ const decision = pathBinary
34
+ ? (0, live_runtime_binary_1.decideStatusBinary)(pathBinary, (0, live_runtime_binary_1.profileFromArgs)(argv), (0, live_runtime_binary_1.defaultLiveRuntimeDeps)())
35
+ : null;
36
+ const note = decision ? (0, live_runtime_binary_1.describeStatusBinary)(decision) : null;
37
+ if (note)
38
+ process.stderr.write(`${note}\n`);
39
+ const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['config', ...argv], {
40
+ binary: decision?.binary,
41
+ });
27
42
  this.exit(code);
28
43
  }
29
44
  catch (err) {
@@ -43,6 +43,7 @@ const auth_core_1 = require("@skrr-ai/auth-core");
43
43
  const base_command_1 = require("../base-command");
44
44
  const auth_storage_1 = require("../lib/auth-storage");
45
45
  const config_1 = require("../lib/config");
46
+ const cli_id_1 = require("../lib/cli-id");
46
47
  const keychain_1 = require("../lib/keychain");
47
48
  const cli_runtime_1 = require("../lib/cli-runtime");
48
49
  const command_manifest_1 = require("../lib/command-manifest");
@@ -272,6 +273,9 @@ class Doctor extends base_command_1.BaseCommand {
272
273
  ? ['auth_token_present_but_user_lookup_failed']
273
274
  : []),
274
275
  ...(!checks.authToken ? ['not_signed_in_or_no_token_available'] : []),
276
+ ...(credential.ownerStatus === 'foreign'
277
+ ? ['stored_credential_belongs_to_another_skrr_config_run_skrr_login']
278
+ : []),
275
279
  ];
276
280
  const payload = {
277
281
  ok: issues.length === 0,
@@ -286,13 +290,16 @@ class Doctor extends base_command_1.BaseCommand {
286
290
  profile: (0, keychain_1.getActiveProfile)(),
287
291
  config: {
288
292
  path: (0, config_1.getConfigPath)(),
289
- cliId: this.cliConfig.cliId ?? null,
293
+ cliId: (0, cli_id_1.resolveCliId)(this.cliConfig) ?? null,
290
294
  },
291
295
  auth: {
292
296
  signedIn: Boolean(account),
293
297
  tokenSource: this.credentialSourceLabel(),
294
298
  tokenKind: this.resolvedCredential?.kind ?? null,
295
299
  refreshAvailable: Boolean(credential.bundle?.refreshToken),
300
+ // OSK-12135 — why a stored credential was not used, when it belongs to
301
+ // another skrr config on this machine. Null when there is nothing to say.
302
+ storedCredentialOwner: (0, auth_storage_1.describeForeignCredential)(credential),
296
303
  accessExpiresAt: accessExpiresAt ? new Date(accessExpiresAt).toISOString() : null,
297
304
  accessExpired: accessExpiresAt ? accessExpiresAt <= now : null,
298
305
  refreshExpiresAt: refreshExpiresAt ? new Date(refreshExpiresAt).toISOString() : null,
@@ -44,7 +44,8 @@ export default class Login extends BaseCommand {
44
44
  *
45
45
  * `cliId` resolution priority:
46
46
  * 1. `OVERSKY_CLI_ID` env var — explicit, wins over everything.
47
- * 2. `cliConfig.cliId` — set by a prior `skrr login` on this machine.
47
+ * 2. `resolveCliId(cliConfig)` — the id a prior `skrr login` of this
48
+ * profile presents (its own, or the machine's).
48
49
  * Honored so a same-machine restore "just works" without forcing
49
50
  * the operator to re-export the cliId.
50
51
  * 3. error — without a cliId the refresh row binding can't be matched