@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
@@ -110,6 +110,16 @@ class CommitmentsPreflight extends base_command_1.BaseCommand {
110
110
  return;
111
111
  }
112
112
  this.log(`${args.id ? 'Preflight' : 'Pre-create preview (nothing persisted)'}: ${report.verdict === 'ready' ? 'Ready to run' : report.verdict === 'blocked' ? 'Run is blocked' : 'A capability or configuration check is still needed'}`);
113
+ // Whose permissions the run will use (OSK-11975), before the gates, so an
114
+ // Execute Commitment on a plan-mode Agent is the first thing read
115
+ // (OSK-12065).
116
+ for (const line of (0, commitments_1.permissionLines)(report.permissions))
117
+ this.log(line);
118
+ if (flags.verbose) {
119
+ const retired = (0, commitments_1.retiredFieldsLine)(report.execution?.retired, report.execution?.retiredReason);
120
+ if (retired)
121
+ this.log(retired);
122
+ }
113
123
  this.log('\nGates:');
114
124
  for (const gate of report.gates || []) {
115
125
  for (const line of preflightGateLines(gate, flags.verbose))
@@ -50,6 +50,11 @@ class CommitmentsShow extends base_command_1.BaseCommand {
50
50
  return;
51
51
  }
52
52
  (0, commitments_1.renderCommitmentSummary)(response.commitment, (line) => this.log(line), response.latestCheck);
53
+ // Whose permissions a run uses (OSK-11975) — read from the operating state,
54
+ // which carries the same projection preflight reports (OSK-12065).
55
+ const autonomy = response.operatingState?.autonomy;
56
+ for (const line of (0, commitments_1.permissionLines)(autonomy?.permissions))
57
+ this.log(line);
53
58
  // `show` is the first surface a person reaches after linking something, and
54
59
  // it used to render only watchedSources — a freshly linked goal or task was
55
60
  // invisible until you knew to run `links list` (OSK-10963).
@@ -101,8 +106,13 @@ class CommitmentsShow extends base_command_1.BaseCommand {
101
106
  this.log('');
102
107
  this.log(`Full history: ${this.config.bin} commitments checks ${args.id}`);
103
108
  }
104
- if (flags.verbose)
109
+ if (flags.verbose) {
105
110
  this.log(`Compatibility policy: ${JSON.stringify(response.commitment.policy)}`);
111
+ const runtime = autonomy?.runtime;
112
+ const retired = (0, commitments_1.retiredFieldsLine)(runtime?.retired, runtime?.retiredReason);
113
+ if (retired)
114
+ this.log(retired);
115
+ }
106
116
  const url = (0, web_url_1.commitmentUrlFrom)(this.cliConfig.baseURL, response.commitment);
107
117
  if (url)
108
118
  this.log(`URL: ${url}`);
@@ -0,0 +1,15 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * `skrr daemon config` — set this machine's daemon configuration, including
4
+ * each tool category's permission mode (OSK-12099). Before this the only way
5
+ * to set a category was to guess the separate `skrrd` binary exists; every
6
+ * other daemon verb was forwarded and this one was not.
7
+ */
8
+ export default class DaemonConfig extends Command {
9
+ static aliases: string[];
10
+ static summary: string;
11
+ static description: string;
12
+ static strict: boolean;
13
+ static examples: string[];
14
+ run(): Promise<void>;
15
+ }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
5
+ /**
6
+ * `skrr daemon config` — set this machine's daemon configuration, including
7
+ * each tool category's permission mode (OSK-12099). Before this the only way
8
+ * to set a category was to guess the separate `skrrd` binary exists; every
9
+ * other daemon verb was forwarded and this one was not.
10
+ */
11
+ class DaemonConfig extends core_1.Command {
12
+ static aliases = ['daemons:config'];
13
+ static summary = "Set the local daemon's configuration, e.g. a tool category's permission mode";
14
+ static description = (0, exec_runtime_binary_1.forwardedFlagsNote)('config');
15
+ static strict = false; // forward all args verbatim — see run()
16
+ static examples = [
17
+ '<%= config.bin %> daemon config --mode bash=manual',
18
+ '<%= config.bin %> daemon config --mode bash=auto --mode browser=auto',
19
+ '<%= config.bin %> daemon config --require-approvals',
20
+ ];
21
+ async run() {
22
+ // `this.argv`, NOT `this.parse()`: oclif would reject every `skrrd config`
23
+ // flag this wrapper has not redeclared (OSK-296).
24
+ const argv = this.argv;
25
+ try {
26
+ const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['config', ...argv]);
27
+ this.exit(code);
28
+ }
29
+ catch (err) {
30
+ if (err.message === 'runtime-binary-not-installed') {
31
+ this.log((0, exec_runtime_binary_1.notInstalledMessage)());
32
+ this.exit(127);
33
+ }
34
+ throw err;
35
+ }
36
+ }
37
+ }
38
+ exports.default = DaemonConfig;
@@ -47,6 +47,7 @@ class Daemon extends core_1.Command {
47
47
  this.log(' skrr daemon stop — stop the daemon');
48
48
  this.log(' skrr daemon update — update the daemon binary to the latest signed release');
49
49
  this.log(' skrr daemon status — show running state and config');
50
+ this.log(' skrr daemon config — set config, e.g. a tool category: --mode bash=manual');
50
51
  this.log(' skrr daemon logs — tail recent daemon logs');
51
52
  this.log(' skrr daemon recover-offline-queue — restore recoverable offline WALs');
52
53
  this.log(' skrr daemon usage — show local Claude Code and Codex usage limits');
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.readinessPointer = readinessPointer;
4
4
  const core_1 = require("@oclif/core");
5
5
  const exec_runtime_binary_1 = require("../../lib/exec-runtime-binary");
6
+ const live_runtime_binary_1 = require("../../lib/live-runtime-binary");
6
7
  /**
7
8
  * The status report is the runtime's, and it describes the daemon: running,
8
9
  * connected, which credential. Whether the coding-agent CLIs on the machine can
@@ -41,7 +42,20 @@ class DaemonStatus extends core_1.Command {
41
42
  // mistake, one directory over.
42
43
  const argv = this.argv;
43
44
  try {
44
- const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['status', ...argv]);
45
+ // Status describes the LIVE daemon, so render it with the live daemon's
46
+ // own binary when that is a different build from the first `skrrd` on
47
+ // PATH, and say which one rendered it either way (OSK-12102). The note
48
+ // goes to stderr so `--json` stays one document on stdout.
49
+ const pathBinary = (0, exec_runtime_binary_1.findRuntimeBinary)();
50
+ const decision = pathBinary
51
+ ? (0, live_runtime_binary_1.decideStatusBinary)(pathBinary, (0, live_runtime_binary_1.profileFromArgs)(argv), (0, live_runtime_binary_1.defaultLiveRuntimeDeps)())
52
+ : null;
53
+ const note = decision ? (0, live_runtime_binary_1.describeStatusBinary)(decision) : null;
54
+ if (note)
55
+ process.stderr.write(`${note}\n`);
56
+ const code = await (0, exec_runtime_binary_1.execRuntimeBinary)(['status', ...argv], {
57
+ binary: decision?.binary,
58
+ });
45
59
  const pointer = readinessPointer(argv, code);
46
60
  if (pointer)
47
61
  this.log(pointer);
@@ -1,4 +1,58 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Why the server fell back to its curated list — `sourceReason` on
4
+ * `GET /api/harnesses/:id/models`, present only when `source` is `curated`.
5
+ * The server owns the vocabulary (`MODEL_SOURCE_REASONS` in
6
+ * `api/server/services/Harness/engineModelCatalog.js`); a value this build
7
+ * does not recognise still prints, verbatim, rather than vanishing.
8
+ */
9
+ export type HarnessModelSourceReason = 'no_live_catalog_for_harness' | 'daemon_offline' | 'live_catalog_unsupported' | 'live_probe_failed';
10
+ export interface HarnessModelsReport {
11
+ provider?: string | null;
12
+ source?: string;
13
+ sourceReason?: HarnessModelSourceReason | string;
14
+ sourceDetail?: string;
15
+ /** The harness row's own stored default model; empty when unset. */
16
+ defaultModel?: string | null;
17
+ /** The harness row's own stored default effort; empty when unset. */
18
+ defaultReasoningEffort?: string | null;
19
+ models?: Array<{
20
+ value?: string;
21
+ label?: string;
22
+ description?: string;
23
+ efforts?: string[];
24
+ /** The effort this model starts at when none is chosen. */
25
+ defaultEffort?: string | null;
26
+ /** The harness's OWN recommendation (Codex `model/list`), not skrr's. */
27
+ isDefault?: boolean;
28
+ }>;
29
+ }
30
+ /**
31
+ * What `default` means on this harness, in lines under the table (OSK-12114).
32
+ *
33
+ * Three candidates can answer "which model runs on `default`": the harness
34
+ * row's stored default, the harness's own configuration on that machine (for
35
+ * Codex, `model` in its config.toml), and the vendor's recommendation that the
36
+ * live list marks. They win in that order, and the list alone cannot see the
37
+ * middle one — so say the order rather than imply the marked model runs.
38
+ */
39
+ export declare function describeModelDefaults(report: HarnessModelsReport | undefined): string[];
40
+ /**
41
+ * Stated under every curated answer: the list is ours, not the harness's
42
+ * admission rule. Without it `curated` reads as "these are the only choices"
43
+ * — the web card has always said this; the CLI never did.
44
+ */
45
+ export declare const CURATED_ACCEPTS_ANY_ID = "Any model id the harness itself accepts works too, even if it is not listed \u2014 set it with `skrr agents update <agent> --model <id>`.";
46
+ /**
47
+ * Why a curated answer is curated, as a clause. `codex · curated` used to
48
+ * print identically for an offline daemon, a daemon too old to probe, and a
49
+ * probe that timed out — three different next actions (OSK-12062).
50
+ */
51
+ export declare function curatedReasonClause(report: HarnessModelsReport | undefined): string;
52
+ /** One line saying where the list came from. */
53
+ export declare function describeModelSource(report: HarnessModelsReport | undefined): string;
54
+ /** What to print when the answer carries no models at all. */
55
+ export declare function describeEmptyModels(report: HarnessModelsReport | undefined): string;
2
56
  /**
3
57
  * Which models this harness can actually run.
4
58
  *
@@ -1,9 +1,95 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CURATED_ACCEPTS_ANY_ID = void 0;
4
+ exports.describeModelDefaults = describeModelDefaults;
5
+ exports.curatedReasonClause = curatedReasonClause;
6
+ exports.describeModelSource = describeModelSource;
7
+ exports.describeEmptyModels = describeEmptyModels;
3
8
  const core_1 = require("@oclif/core");
4
9
  const base_command_1 = require("../../base-command");
5
10
  const format_1 = require("../../lib/format");
6
11
  const harnesses_1 = require("../../lib/harnesses");
12
+ /**
13
+ * What `default` means on this harness, in lines under the table (OSK-12114).
14
+ *
15
+ * Three candidates can answer "which model runs on `default`": the harness
16
+ * row's stored default, the harness's own configuration on that machine (for
17
+ * Codex, `model` in its config.toml), and the vendor's recommendation that the
18
+ * live list marks. They win in that order, and the list alone cannot see the
19
+ * middle one — so say the order rather than imply the marked model runs.
20
+ */
21
+ function describeModelDefaults(report) {
22
+ const provider = report?.provider || 'the harness';
23
+ const lines = [];
24
+ const marked = (report?.models ?? []).find((m) => m.isDefault)?.value;
25
+ if (report?.defaultModel) {
26
+ lines.push(`Harness default: ${report.defaultModel} — what an agent on \`default\` runs on this harness.`);
27
+ }
28
+ else {
29
+ lines.push(`No harness default is set, so an agent on \`default\` runs the model the daemon's config ` +
30
+ `or ${provider}'s own config on that machine names` +
31
+ `${marked ? `, else ${marked} (* — ${provider}'s own recommendation)` : ''}.`);
32
+ }
33
+ if (report?.defaultReasoningEffort) {
34
+ lines.push(`Harness default effort: ${report.defaultReasoningEffort}.`);
35
+ }
36
+ lines.push('The model a turn actually ran is reported when it ends (`agents chat` prints `[run] model:`).');
37
+ return lines;
38
+ }
39
+ /**
40
+ * Stated under every curated answer: the list is ours, not the harness's
41
+ * admission rule. Without it `curated` reads as "these are the only choices"
42
+ * — the web card has always said this; the CLI never did.
43
+ */
44
+ exports.CURATED_ACCEPTS_ANY_ID = 'Any model id the harness itself accepts works too, even if it is not listed — set it with `skrr agents update <agent> --model <id>`.';
45
+ /**
46
+ * Why a curated answer is curated, as a clause. `codex · curated` used to
47
+ * print identically for an offline daemon, a daemon too old to probe, and a
48
+ * probe that timed out — three different next actions (OSK-12062).
49
+ */
50
+ function curatedReasonClause(report) {
51
+ const provider = report?.provider || 'this harness';
52
+ const detail = report?.sourceDetail ? ` (${report.sourceDetail})` : '';
53
+ switch (report?.sourceReason) {
54
+ case 'no_live_catalog_for_harness':
55
+ return `${provider} has no way to list its own models, so this list is the catalog`;
56
+ case 'daemon_offline':
57
+ return `the machine running ${provider} is offline, so its live model list could not be read`;
58
+ case 'live_catalog_unsupported':
59
+ return `the daemon on that machine does not offer a live model list — update it to get one`;
60
+ case 'live_probe_failed':
61
+ return `asking ${provider} for its live model list failed${detail}`;
62
+ case undefined:
63
+ case '':
64
+ return 'the server did not say why the live list was not used';
65
+ default:
66
+ return `live list not used: ${report?.sourceReason}${detail}`;
67
+ }
68
+ }
69
+ /** One line saying where the list came from. */
70
+ function describeModelSource(report) {
71
+ switch (report?.source) {
72
+ case 'managed':
73
+ return 'managed — the models your plan admits for this harness';
74
+ case 'live':
75
+ return `live — reported by the installed ${report.provider || 'harness'} on its machine`;
76
+ case 'curated':
77
+ return `curated — skrr's own list; ${curatedReasonClause(report)}`;
78
+ default:
79
+ return report?.source ?? 'unknown source';
80
+ }
81
+ }
82
+ /** What to print when the answer carries no models at all. */
83
+ function describeEmptyModels(report) {
84
+ const resolved = report?.provider;
85
+ if (!resolved) {
86
+ return 'This harness resolved to no provider, so no model list could be built. That is a defect, not a configuration problem — report it with the harness id.';
87
+ }
88
+ // An empty answer is always curated: a live or managed answer with no models
89
+ // degrades to curated server-side. So the reason is what explains it — for
90
+ // opencode it is usually the live probe, which the old message never named.
91
+ return `No models are listed for ${resolved}: skrr's curated list has no entries for it, and ${curatedReasonClause(report)}. ${exports.CURATED_ACCEPTS_ANY_ID}`;
92
+ }
7
93
  /**
8
94
  * Which models this harness can actually run.
9
95
  *
@@ -44,29 +130,36 @@ class HarnessesModels extends base_command_1.BaseCommand {
44
130
  // where the list comes from picks one — and, in the bug this replaced,
45
131
  // would have shown `provider` as null on a Claude harness and named the
46
132
  // defect on sight.
47
- const resolved = report?.provider;
48
- this.log(resolved
49
- ? `No models are listed for ${resolved}. skrr keeps a curated list per provider and ${resolved} has no entries in it, so pick a model on the agent instead — \`skrr agents update <agent> --model <id>\` accepts an id this list does not name.`
50
- : 'This harness resolved to no provider, so no model list could be built. That is a defect, not a configuration problem — report it with the harness id.');
133
+ this.log(describeEmptyModels(report));
51
134
  return;
52
135
  }
53
136
  if (report?.provider || report?.source) {
54
137
  // The source matters when the list is short: `curated` is our table,
55
- // anything else came from the harness itself.
56
- this.log(`${report.provider ?? 'unknown provider'} · ${report.source ?? 'unknown source'}`);
138
+ // anything else came from the harness itself — and a curated answer
139
+ // says why the harness's own list was not used.
140
+ this.log(`${report.provider ?? 'unknown provider'} · ${describeModelSource(report)}`);
141
+ if (report.source === 'curated')
142
+ this.log(exports.CURATED_ACCEPTS_ANY_ID);
57
143
  this.log('');
58
144
  }
59
145
  (0, format_1.renderTable)(models.map((m) => ({
60
- model: (0, format_1.formatCell)(m.value),
146
+ // `*` is the harness's own recommendation, which the JSON carried as
147
+ // `isDefault` and this table dropped (OSK-12114).
148
+ model: `${(0, format_1.formatCell)(m.value)}${m.isDefault ? ' *' : ''}`,
61
149
  label: (0, format_1.formatCell)(m.label),
62
150
  // An empty efforts array means the model takes no effort setting at
63
151
  // all, which is different from one we failed to read.
64
152
  efforts: m.efforts && m.efforts.length > 0 ? m.efforts.join(', ') : '-',
153
+ defaultEffort: m.defaultEffort ? String(m.defaultEffort) : '-',
65
154
  })), [
66
155
  { key: 'model', header: 'MODEL' },
67
156
  { key: 'label', header: 'LABEL', maxWidth: 26 },
68
157
  { key: 'efforts', header: 'EFFORTS' },
158
+ { key: 'defaultEffort', header: 'DEFAULT EFFORT' },
69
159
  ], (line) => this.log(line));
160
+ this.log('');
161
+ for (const line of describeModelDefaults(report))
162
+ this.log(line);
70
163
  }
71
164
  }
72
165
  exports.default = HarnessesModels;
@@ -10,6 +10,17 @@ export default class HarnessesShow extends BaseCommand {
10
10
  };
11
11
  run(): Promise<void>;
12
12
  }
13
+ /**
14
+ * Which permission modes this harness accepts, and its own default (OSK-12130).
15
+ *
16
+ * The rule used to surface only as an agent write refusing a mode, so the way
17
+ * to learn that Gemini runs only as bypassPermissions was three refusals in a
18
+ * row. Printed only when the server sent it (a local harness row).
19
+ */
20
+ export declare function harnessPermissionLines(harness: {
21
+ permissionModes?: unknown;
22
+ defaultPermissionMode?: unknown;
23
+ }): string[];
13
24
  /**
14
25
  * Say what the caller DID pass, not only what they did not.
15
26
  *
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.harnessPermissionLines = harnessPermissionLines;
3
4
  exports.describeWrongHarnessRef = describeWrongHarnessRef;
4
5
  exports.resolveHarnessRef = resolveHarnessRef;
5
6
  exports.pickHarnessRef = pickHarnessRef;
@@ -113,6 +114,8 @@ class HarnessesShow extends base_command_1.BaseCommand {
113
114
  }
114
115
  if (harness.daemonId)
115
116
  this.log(` daemon ${harness.daemonId}`);
117
+ for (const line of harnessPermissionLines(harness))
118
+ this.log(line);
116
119
  if (verdict) {
117
120
  this.log(` readiness ${verdict.label} — ${verdict.detail}`);
118
121
  }
@@ -124,6 +127,38 @@ class HarnessesShow extends base_command_1.BaseCommand {
124
127
  }
125
128
  }
126
129
  exports.default = HarnessesShow;
130
+ const PERMISSION_MODES_SOURCE_LABELS = {
131
+ daemon: 'reported by the daemon',
132
+ daemon_record: 'as the daemon last reported it',
133
+ static: "from skrr's harness table",
134
+ vocabulary: 'not declared for this harness; the daemon enforces',
135
+ };
136
+ /**
137
+ * Which permission modes this harness accepts, and its own default (OSK-12130).
138
+ *
139
+ * The rule used to surface only as an agent write refusing a mode, so the way
140
+ * to learn that Gemini runs only as bypassPermissions was three refusals in a
141
+ * row. Printed only when the server sent it (a local harness row).
142
+ */
143
+ function harnessPermissionLines(harness) {
144
+ const report = harness.permissionModes;
145
+ const modes = Array.isArray(report?.modes)
146
+ ? report.modes.filter((m) => typeof m === 'string')
147
+ : [];
148
+ if (modes.length === 0)
149
+ return [];
150
+ const source = typeof report?.source === 'string'
151
+ ? (PERMISSION_MODES_SOURCE_LABELS[report.source] ?? report.source)
152
+ : '';
153
+ const stored = typeof harness.defaultPermissionMode === 'string' && harness.defaultPermissionMode
154
+ ? harness.defaultPermissionMode
155
+ : null;
156
+ return [
157
+ ` permission accepts ${modes.join(', ')}${source ? ` (${source})` : ''}`,
158
+ ` default ${stored ??
159
+ "not set — an agent's own mode applies, else the daemon's config (`harnesses update --default-permission-mode`)"}`,
160
+ ];
161
+ }
127
162
  /**
128
163
  * Say what the caller DID pass, not only what they did not.
129
164
  *
@@ -1,7 +1,8 @@
1
1
  import { BaseCommand } from '../../base-command';
2
2
  import { getHarness } from '../../lib/harnesses';
3
3
  /**
4
- * Rename an harness, or change how Claude Code runs on it.
4
+ * Rename a harness, change how Claude Code runs on it, or set the model,
5
+ * reasoning effort and permission mode its sessions default to.
5
6
  *
6
7
  * `--name` renames it FOR EVERYONE it is shared with. `skrr harnesses alias`
7
8
  * changes only what you see; the two are different operations and the server
@@ -21,9 +22,35 @@ export default class HarnessesUpdate extends BaseCommand {
21
22
  name: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
22
23
  description: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
23
24
  'claude-engine': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ 'default-model': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ 'clear-default-model': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
27
+ 'default-effort': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
28
+ 'clear-default-effort': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
29
+ 'default-permission-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
+ 'clear-default-permission-mode': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
31
  };
25
32
  run(): Promise<void>;
26
33
  }
34
+ export interface HarnessUpdateFlags {
35
+ name?: string;
36
+ description?: string;
37
+ 'claude-engine'?: string;
38
+ 'default-model'?: string;
39
+ 'clear-default-model'?: boolean;
40
+ 'default-effort'?: string;
41
+ 'clear-default-effort'?: boolean;
42
+ 'default-permission-mode'?: string;
43
+ 'clear-default-permission-mode'?: boolean;
44
+ }
45
+ /**
46
+ * Flags → the `patch` of `PATCH /api/harnesses/:id`. The clear spellings are
47
+ * the server's own: an empty string clears `defaultModel` and
48
+ * `defaultReasoningEffort`, `null` clears `defaultPermissionMode`. Clearing is
49
+ * a separate boolean flag rather than a sentinel value because `none` is a
50
+ * real reasoning effort (GPT-5.x accepts it), so `--default-effort none` has
51
+ * to mean "set it to none", not "remove it".
52
+ */
53
+ export declare function buildHarnessPatch(flags: HarnessUpdateFlags): Record<string, unknown>;
27
54
  /**
28
55
  * `--name` sets the name everyone sees. When YOUR alias covers it, you will not
29
56
  * see the new name anywhere you look, and "Updated <name>." read as though the
@@ -1,12 +1,15 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buildHarnessPatch = buildHarnessPatch;
3
4
  exports.sharedRenameMessage = sharedRenameMessage;
4
5
  const core_1 = require("@oclif/core");
6
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
7
  const base_command_1 = require("../../base-command");
6
8
  const harnesses_1 = require("../../lib/harnesses");
7
9
  const harness_labels_1 = require("../../lib/harness-labels");
8
10
  /**
9
- * Rename an harness, or change how Claude Code runs on it.
11
+ * Rename a harness, change how Claude Code runs on it, or set the model,
12
+ * reasoning effort and permission mode its sessions default to.
10
13
  *
11
14
  * `--name` renames it FOR EVERYONE it is shared with. `skrr harnesses alias`
12
15
  * changes only what you see; the two are different operations and the server
@@ -16,10 +19,13 @@ const harness_labels_1 = require("../../lib/harness-labels");
16
19
  * version first and a concurrent edit answers 409 instead of overwriting.
17
20
  */
18
21
  class HarnessesUpdate extends base_command_1.BaseCommand {
19
- static description = 'Rename a harness or set how Claude Code is driven on it';
22
+ static description = 'Rename a harness, set how Claude Code is driven on it, or set its default model, effort, and permission mode';
20
23
  static examples = [
21
24
  '<%= config.bin %> harnesses update <harness-id> --name "Studio iMac"',
22
25
  '<%= config.bin %> harnesses update <harness-id> --claude-engine sdk',
26
+ '<%= config.bin %> harnesses update <harness-id> --default-model opus --default-effort high',
27
+ '<%= config.bin %> harnesses update <harness-id> --default-permission-mode acceptEdits',
28
+ '<%= config.bin %> harnesses update <harness-id> --clear-default-model --clear-default-effort',
23
29
  ];
24
30
  static args = {
25
31
  harness: core_1.Args.string({ ignoreStdin: true, description: 'Harness ID', required: true }),
@@ -34,19 +40,45 @@ class HarnessesUpdate extends base_command_1.BaseCommand {
34
40
  description: 'How Claude Code runs on this machine: `sdk` in-process (needed for AskUserQuestion), `cli` as a child process. Local CLAUDE harnesses only — the server refuses it on any other provider, because nothing would ever read it.',
35
41
  options: ['cli', 'sdk'],
36
42
  }),
43
+ 'default-model': core_1.Flags.string({
44
+ description: "Model this harness's sessions use when the agent or task names none, in the harness's own " +
45
+ 'namespace (`opus`, `gpt-5-codex`, …). `skrr harnesses models <harness-id>` lists what it can run. ' +
46
+ 'Not allowlisted: any safe model identifier is stored as given, so check the spelling.',
47
+ exclusive: ['clear-default-model'],
48
+ }),
49
+ 'clear-default-model': core_1.Flags.boolean({
50
+ description: "Remove the default model, so the harness's own default applies",
51
+ exclusive: ['default-model'],
52
+ }),
53
+ 'default-effort': core_1.Flags.string({
54
+ description: "Reasoning effort this harness's sessions default to. Checked against the model the write leaves " +
55
+ 'in place (the new --default-model when you pass both); `skrr harnesses models <harness-id>` lists ' +
56
+ 'the efforts each model accepts. Harnesses with no effort setting refuse it.',
57
+ exclusive: ['clear-default-effort'],
58
+ }),
59
+ 'clear-default-effort': core_1.Flags.boolean({
60
+ description: "Remove the default reasoning effort, so the harness's own default applies",
61
+ exclusive: ['default-effort'],
62
+ }),
63
+ 'default-permission-mode': core_1.Flags.string({
64
+ description: "Permission mode this harness's sessions fall back to when the agent sets none. Local harnesses " +
65
+ 'only. One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
66
+ 'bypassPermissions; any other mode is refused for them.',
67
+ options: [...data_provider_1.LOCAL_HARNESS_PERMISSION_MODES],
68
+ exclusive: ['clear-default-permission-mode'],
69
+ }),
70
+ 'clear-default-permission-mode': core_1.Flags.boolean({
71
+ description: "Remove this harness's permission-mode override, so the daemon's own configured default applies",
72
+ exclusive: ['default-permission-mode'],
73
+ }),
37
74
  };
38
75
  async run() {
39
76
  this.requireAuth();
40
77
  const { args, flags } = await this.parse(HarnessesUpdate);
41
- const patch = {};
42
- if (flags.name !== undefined)
43
- patch.name = flags.name;
44
- if (flags.description !== undefined)
45
- patch.description = flags.description;
46
- if (flags['claude-engine'] !== undefined)
47
- patch.claudeEngine = flags['claude-engine'];
78
+ const patch = buildHarnessPatch(flags);
48
79
  if (Object.keys(patch).length === 0) {
49
- this.error('Nothing to change. Pass --name, --description, or --claude-engine.', { exit: 1 });
80
+ this.error('Nothing to change. Pass --name, --description, --claude-engine, --default-model, ' +
81
+ '--default-effort, --default-permission-mode, or a --clear-default-* flag.', { exit: 1 });
50
82
  return;
51
83
  }
52
84
  try {
@@ -65,6 +97,37 @@ class HarnessesUpdate extends base_command_1.BaseCommand {
65
97
  }
66
98
  }
67
99
  exports.default = HarnessesUpdate;
100
+ /**
101
+ * Flags → the `patch` of `PATCH /api/harnesses/:id`. The clear spellings are
102
+ * the server's own: an empty string clears `defaultModel` and
103
+ * `defaultReasoningEffort`, `null` clears `defaultPermissionMode`. Clearing is
104
+ * a separate boolean flag rather than a sentinel value because `none` is a
105
+ * real reasoning effort (GPT-5.x accepts it), so `--default-effort none` has
106
+ * to mean "set it to none", not "remove it".
107
+ */
108
+ function buildHarnessPatch(flags) {
109
+ const patch = {};
110
+ if (flags.name !== undefined)
111
+ patch.name = flags.name;
112
+ if (flags.description !== undefined)
113
+ patch.description = flags.description;
114
+ if (flags['claude-engine'] !== undefined)
115
+ patch.claudeEngine = flags['claude-engine'];
116
+ if (flags['default-model'] !== undefined)
117
+ patch.defaultModel = flags['default-model'];
118
+ if (flags['clear-default-model'])
119
+ patch.defaultModel = '';
120
+ if (flags['default-effort'] !== undefined)
121
+ patch.defaultReasoningEffort = flags['default-effort'];
122
+ if (flags['clear-default-effort'])
123
+ patch.defaultReasoningEffort = '';
124
+ if (flags['default-permission-mode'] !== undefined) {
125
+ patch.defaultPermissionMode = flags['default-permission-mode'];
126
+ }
127
+ if (flags['clear-default-permission-mode'])
128
+ patch.defaultPermissionMode = null;
129
+ return patch;
130
+ }
68
131
  /**
69
132
  * `--name` sets the name everyone sees. When YOUR alias covers it, you will not
70
133
  * see the new name anywhere you look, and "Updated <name>." read as though the
@@ -1,6 +1,17 @@
1
1
  import { BaseCommand } from '../../base-command';
2
+ import { type OutboxEntry } from '../../lib/outbox';
3
+ /**
4
+ * Why one entry was queued, in words (OSK-12121). The queue holds more than
5
+ * outage casualties — a 401 is queued too, so authored content survives a lapsed
6
+ * sign-in — and a list that described every entry as "the API was unavailable"
7
+ * sent a signed-out reader to wait for an outage that was not happening.
8
+ */
9
+ export declare function queuedBecause(entry: Pick<OutboxEntry, 'reason'>): {
10
+ label: string;
11
+ kind: 'auth' | 'throttled' | 'unavailable' | 'other';
12
+ };
2
13
  export declare function outboxListSnapshot(profile?: string): {
3
- queued: import("../../lib/outbox").OutboxEntry[];
14
+ queued: OutboxEntry[];
4
15
  incident: import("../../lib/outbox").IncidentState | null;
5
16
  incidentActive: boolean;
6
17
  };