@skrr-ai/cli 0.1.52 → 0.1.54

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 (33) hide show
  1. package/dist/commands/agents/chat.js +2 -1
  2. package/dist/commands/agents/create.js +13 -4
  3. package/dist/commands/agents/update.d.ts +32 -0
  4. package/dist/commands/agents/update.js +65 -14
  5. package/dist/commands/tasks/approvals/list.d.ts +58 -0
  6. package/dist/commands/tasks/approvals/list.js +173 -4
  7. package/dist/commands/tasks/create.js +2 -1
  8. package/dist/commands/tasks/start.js +1 -1
  9. package/dist/commands/tasks/update.js +23 -3
  10. package/dist/lib/agent-config.d.ts +13 -0
  11. package/dist/lib/agent-config.js +32 -0
  12. package/dist/lib/agentic-stream.d.ts +1 -1
  13. package/dist/lib/agentic-stream.js +1 -1
  14. package/dist/lib/auth-storage.js +7 -3
  15. package/dist/lib/cred-envelope.d.ts +12 -1
  16. package/dist/lib/cred-envelope.js +20 -3
  17. package/dist/lib/dedicated-machines.d.ts +20 -2
  18. package/dist/lib/dedicated-machines.js +46 -5
  19. package/dist/lib/keychain.js +26 -1
  20. package/dist/lib/label-ref.d.ts +1 -1
  21. package/dist/lib/label-ref.js +7 -3
  22. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +13 -1
  23. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +18 -8
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +4 -3
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +13 -1
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +18 -9
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  30. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  31. package/dist/node_modules/@skrr-ai/data-provider/index.js +182 -175
  32. package/oclif.manifest.json +28759 -28747
  33. package/package.json +1 -1
@@ -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`.',
@@ -252,19 +252,23 @@ class AgentsCreate extends base_command_1.BaseCommand {
252
252
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
253
253
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
254
254
  multiple: true,
255
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
255
+ deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
256
256
  hidden: true,
257
257
  }),
258
258
  'commitment-responsibility': core_1.Flags.string({
259
259
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
260
260
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
261
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
261
+ deprecated: {
262
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
263
+ },
262
264
  hidden: true,
263
265
  }),
264
266
  'commitment-responsibility-ceiling': core_1.Flags.string({
265
267
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
266
268
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
267
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
269
+ deprecated: {
270
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
271
+ },
268
272
  hidden: true,
269
273
  }),
270
274
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
@@ -393,7 +397,12 @@ class AgentsCreate extends base_command_1.BaseCommand {
393
397
  // relationshipLoops is an input-only provisioning control. The server
394
398
  // applies it by deciding which system commitments to seed, so it is not an
395
399
  // Agent document field and is intentionally absent from the response.
396
- 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);
397
406
  if (unapplied.length > 0) {
398
407
  this.warn(unappliedPayloadKeysWarning(unapplied));
399
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,27 @@ 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[];
58
80
  /**
59
81
  * The per-Agent mutation fence's "another request holds this right now"
60
82
  * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
@@ -78,6 +100,16 @@ export declare const AGENT_FENCE_RETRY_CODES: ReadonlySet<string>;
78
100
  export declare function retryOnceOnAgentFence<T>(operation: () => Promise<T>, { delayMs }?: {
79
101
  delayMs?: number;
80
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;
81
113
  /**
82
114
  * What changing the agent's default permission mode actually changed
83
115
  * (OSK-12124). It is the AGENT's default, so it governs every conversation,
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.AGENT_FENCE_RETRY_CODES = void 0;
4
+ exports.agentUpdateWarnings = agentUpdateWarnings;
4
5
  exports.retryOnceOnAgentFence = retryOnceOnAgentFence;
6
+ exports.defaultPermissionModePatch = defaultPermissionModePatch;
5
7
  exports.defaultPermissionModeNote = defaultPermissionModeNote;
6
8
  const core_1 = require("@oclif/core");
7
9
  const data_provider_1 = require("@skrr-ai/data-provider");
@@ -58,6 +60,12 @@ class AgentsUpdate extends base_command_1.BaseCommand {
58
60
  'One-shot harnesses (gemini, opencode, cursor, copilot, goose, pi, openclaw) can only run as ' +
59
61
  'bypassPermissions; any other mode is refused for them.',
60
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'],
61
69
  }),
62
70
  tools: core_1.Flags.string({
63
71
  description: 'Tools JSON array of plugin keys, e.g. \'["calculator"]\'. `skrr agents tools` lists the keys, ' +
@@ -107,19 +115,23 @@ class AgentsUpdate extends base_command_1.BaseCommand {
107
115
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
108
116
  options: [...agent_config_1.REASONING_TOOL_ESCAPES],
109
117
  multiple: true,
110
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
118
+ deprecated: { message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('reasoning-tool-escape') },
111
119
  hidden: true,
112
120
  }),
113
121
  'commitment-responsibility': core_1.Flags.string({
114
122
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
115
123
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
116
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
124
+ deprecated: {
125
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility'),
126
+ },
117
127
  hidden: true,
118
128
  }),
119
129
  'commitment-responsibility-ceiling': core_1.Flags.string({
120
130
  description: "Retired — has no effect and is not sent. Commitment runs use the agent's own permissions.",
121
131
  options: [...agent_config_1.COMMITMENT_RESPONSIBILITY_LEVELS],
122
- deprecated: { message: agent_config_1.COMMITMENT_PERMISSION_FLAG_RETIRED },
132
+ deprecated: {
133
+ message: (0, agent_config_1.retiredCommitmentPermissionFlagMessage)('commitment-responsibility-ceiling'),
134
+ },
123
135
  hidden: true,
124
136
  }),
125
137
  'max-turns': core_1.Flags.integer({ description: 'Maximum turns in one agent run (1-500)' }),
@@ -177,7 +189,7 @@ class AgentsUpdate extends base_command_1.BaseCommand {
177
189
  category: flags.category,
178
190
  executionPolicy,
179
191
  workingDirectory: flags['working-directory'],
180
- defaultPermissionMode: flags['default-permission-mode'],
192
+ defaultPermissionMode: defaultPermissionModePatch(flags),
181
193
  });
182
194
  // `mergeAgentConfigFlags` parses the typed flags (avatar, scene background,
183
195
  // working hours, every JSON flag). A parse failure is the caller's input, so
@@ -206,16 +218,8 @@ class AgentsUpdate extends base_command_1.BaseCommand {
206
218
  catch (err) {
207
219
  this.handleApiError(err);
208
220
  }
209
- // The same check `agents create` makes. Without it, a key the server does
210
- // not model, a typo of one that it does (`max_turns` for `maxTurns`), or a
211
- // field only the server sets was discarded with the same "Updated agent"
212
- // line as a real change.
213
- // `removeProjectIds` is an instruction, not a stored field, so it never
214
- // comes back and is not reported.
215
- const unapplied = (0, create_1.unappliedPayloadKeys)(payload, updated, ['removeProjectIds']);
216
- if (unapplied.length > 0) {
217
- this.warn((0, create_1.unappliedPayloadKeysWarning)(unapplied));
218
- }
221
+ for (const warning of agentUpdateWarnings(payload, updated))
222
+ this.warn(warning);
219
223
  await (0, runtime_tool_support_1.warnPluginsNotLoaded)(payload.tools, (message) => this.warn(message));
220
224
  if (flags.json) {
221
225
  this.log(JSON.stringify((0, web_url_1.withAgentUrl)(this.cliConfig.baseURL, updated), null, 2));
@@ -227,9 +231,45 @@ class AgentsUpdate extends base_command_1.BaseCommand {
227
231
  const modeNote = defaultPermissionModeNote(flags['default-permission-mode'], a);
228
232
  if (modeNote)
229
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
+ }
230
238
  }
231
239
  }
232
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
+ }
233
273
  /**
234
274
  * The per-Agent mutation fence's "another request holds this right now"
235
275
  * refusals. `updateAgent` takes a short lease on the Agent before it writes, so
@@ -267,6 +307,17 @@ async function retryOnceOnAgentFence(operation, { delayMs = 750 } = {}) {
267
307
  return operation();
268
308
  }
269
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
+ }
270
321
  /**
271
322
  * What changing the agent's default permission mode actually changed
272
323
  * (OSK-12124). It is the AGENT's default, so it governs every conversation,
@@ -14,6 +14,64 @@ export declare function approvalRow(a: Record<string, unknown>): {
14
14
  approver: string;
15
15
  created: string;
16
16
  };
17
+ /**
18
+ * OSK-12165 — the task's latest run, when it ended blocked on a permission
19
+ * nobody could grant.
20
+ *
21
+ * An unattended run (`tasks start`) cannot ask anyone: the permission bridge
22
+ * declines every ask at once, so no approval row is ever created, and this
23
+ * command used to answer "No approvals." in exactly the case `tasks start`
24
+ * sends people here for. The evidence lives on the run, not in the approvals
25
+ * collection: the leaf finalizer settles it with `error.code`
26
+ * `permission_blocked`, which `GET /tasks/:id/runs` projects as `failureCode`
27
+ * alongside the sentence (what was refused, and the fix) as `failureReason`.
28
+ *
29
+ * Only the LATEST run counts (the route sorts newest first): a permission block
30
+ * on an older run that a later run got past is history, not the current state.
31
+ */
32
+ export declare function permissionBlockedRun(runs: Array<Record<string, unknown>>): Record<string, unknown> | null;
33
+ /** One refused call: the tool and its input, as the run recorded them. */
34
+ export type PermissionRefusal = {
35
+ tool: string | null;
36
+ input: string;
37
+ };
38
+ /** One way forward, as a command a person or a script can run. */
39
+ export type PermissionBlockRemedy = {
40
+ action: 'run_interactively' | 'allow_unattended' | 'restart';
41
+ command: string;
42
+ description: string;
43
+ };
44
+ /**
45
+ * The structured answer `--json` carries as `blockedRun` (OSK-12173). Before
46
+ * this the explanation existed only as prose, and with `--json` only on
47
+ * stderr, so a script reading the JSON saw `{"data":[]}` and nothing to act on.
48
+ */
49
+ export type BlockedRunSummary = {
50
+ runId: string;
51
+ failureCode: string;
52
+ /** The cause, from the run's own sentence, without its generic remedy. */
53
+ reason: string | null;
54
+ agentId: string | null;
55
+ /**
56
+ * The refused calls with their inputs. Empty when the run recorded none
57
+ * (runs finalized before the server kept them, or evidence without an input
58
+ * preview). Never inferred.
59
+ */
60
+ refused: PermissionRefusal[];
61
+ remedies: PermissionBlockRemedy[];
62
+ };
63
+ /**
64
+ * The block as data: which run, why, what was refused, and the ways forward
65
+ * with the agent id filled in (`<agent>` only when the run recorded no agent).
66
+ */
67
+ export declare function blockedRunSummary(run: Record<string, unknown>, bin?: string, taskRef?: string): BlockedRunSummary;
68
+ /**
69
+ * What to print for such a run: why it is blocked, what needed approval (the
70
+ * run's own sentence, plus each refused input it recorded), that an unattended
71
+ * run cannot prompt, and the ways forward, each stated once with the real
72
+ * agent id.
73
+ */
74
+ export declare function permissionBlockLines(run: Record<string, unknown>, bin?: string, taskRef?: string): string[];
17
75
  export default class TasksApprovalsList extends BaseCommand {
18
76
  static description: string;
19
77
  static examples: string[];
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.approvalRow = approvalRow;
4
+ exports.permissionBlockedRun = permissionBlockedRun;
5
+ exports.blockedRunSummary = blockedRunSummary;
6
+ exports.permissionBlockLines = permissionBlockLines;
4
7
  const core_1 = require("@oclif/core");
5
8
  const data_provider_1 = require("@skrr-ai/data-provider");
6
9
  const base_command_1 = require("../../../base-command");
@@ -22,6 +25,131 @@ function approvalRow(a) {
22
25
  created: a.createdAt ? (0, format_1.localTime)(a.createdAt) : '-',
23
26
  };
24
27
  }
28
+ /**
29
+ * OSK-12165 — the task's latest run, when it ended blocked on a permission
30
+ * nobody could grant.
31
+ *
32
+ * An unattended run (`tasks start`) cannot ask anyone: the permission bridge
33
+ * declines every ask at once, so no approval row is ever created, and this
34
+ * command used to answer "No approvals." in exactly the case `tasks start`
35
+ * sends people here for. The evidence lives on the run, not in the approvals
36
+ * collection: the leaf finalizer settles it with `error.code`
37
+ * `permission_blocked`, which `GET /tasks/:id/runs` projects as `failureCode`
38
+ * alongside the sentence (what was refused, and the fix) as `failureReason`.
39
+ *
40
+ * Only the LATEST run counts (the route sorts newest first): a permission block
41
+ * on an older run that a later run got past is history, not the current state.
42
+ */
43
+ function permissionBlockedRun(runs) {
44
+ const latest = runs[0];
45
+ if (!latest)
46
+ return null;
47
+ const code = latest.failureCode ?? latest.failureClass;
48
+ return code === data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE ? latest : null;
49
+ }
50
+ /**
51
+ * Where the run's own fix begins in its recorded sentence.
52
+ *
53
+ * The server writes `failureReason` as the cause followed by the fix
54
+ * (`permissionBlockedRun.PERMISSION_BLOCK_GUIDANCE`), and that fix names the
55
+ * agent only as `<agent>` because the sentence is shared by every surface.
56
+ * This command knows the agent, so it states the fix itself, with the real id,
57
+ * and keeps only the cause from the sentence (OSK-12173): printing both showed
58
+ * the remedy twice, once with a placeholder.
59
+ */
60
+ const RECORDED_REMEDY_MARKER = 'Re-running will be refused';
61
+ function refusalsOf(run) {
62
+ const raw = run.permissionRefusals;
63
+ if (!Array.isArray(raw))
64
+ return [];
65
+ const out = [];
66
+ for (const entry of raw) {
67
+ if (!entry || typeof entry !== 'object')
68
+ continue;
69
+ const { tool, input } = entry;
70
+ if (typeof input !== 'string' || !input.trim())
71
+ continue;
72
+ out.push({ tool: typeof tool === 'string' && tool ? tool : null, input: input.trim() });
73
+ }
74
+ return out;
75
+ }
76
+ /** The cause, cut before the recorded remedy; `<agent>` filled in if any survives. */
77
+ function blockReason(run, agentId) {
78
+ const full = typeof run.failureReason === 'string' ? run.failureReason.trim() : '';
79
+ if (!full)
80
+ return null;
81
+ const at = full.indexOf(RECORDED_REMEDY_MARKER);
82
+ const cause = at > 0 ? full.slice(0, at).trim() : full;
83
+ return agentId ? cause.split('<agent>').join(agentId) : cause;
84
+ }
85
+ /**
86
+ * The block as data: which run, why, what was refused, and the ways forward
87
+ * with the agent id filled in (`<agent>` only when the run recorded no agent).
88
+ */
89
+ function blockedRunSummary(run, bin = 'skrr', taskRef = '<task-id>') {
90
+ const executor = run.executor;
91
+ const agentId = executor && typeof executor.agentId === 'string' && executor.agentId ? executor.agentId : null;
92
+ const agent = agentId ?? '<agent>';
93
+ const code = run.failureCode ?? run.failureClass;
94
+ return {
95
+ runId: String(run.runId ?? run.id ?? '-'),
96
+ failureCode: typeof code === 'string' ? code : data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE,
97
+ reason: blockReason(run, agentId),
98
+ agentId,
99
+ refused: refusalsOf(run),
100
+ remedies: [
101
+ {
102
+ action: 'run_interactively',
103
+ command: `${bin} agents chat ${agent} --approve-tools --prompt "..."`,
104
+ description: 'Do the work interactively, approving each call.',
105
+ },
106
+ {
107
+ action: 'allow_unattended',
108
+ command: `${bin} agents update ${agent} --default-permission-mode bypassPermissions`,
109
+ description: 'Let unattended runs use the tool without asking.',
110
+ },
111
+ {
112
+ action: 'restart',
113
+ command: `${bin} tasks start ${taskRef}`,
114
+ description: 'Start the task again once its agent may use the tool (or pick an agent/harness whose mode allows it).',
115
+ },
116
+ ],
117
+ };
118
+ }
119
+ const SHELL_TOOL = /^(bash|shell|local_shell|exec_command|commandexecution|command_execution)$/i;
120
+ function toolLabel(tool) {
121
+ if (!tool)
122
+ return '';
123
+ return SHELL_TOOL.test(tool) ? ' (shell)' : ` (${tool})`;
124
+ }
125
+ /**
126
+ * What to print for such a run: why it is blocked, what needed approval (the
127
+ * run's own sentence, plus each refused input it recorded), that an unattended
128
+ * run cannot prompt, and the ways forward, each stated once with the real
129
+ * agent id.
130
+ */
131
+ function permissionBlockLines(run, bin = 'skrr', taskRef = '<task-id>') {
132
+ const summary = blockedRunSummary(run, bin, taskRef);
133
+ const reason = summary.reason ?? '';
134
+ // The sentence already quotes a lone refused input (shortened); list an
135
+ // input only when the sentence does not already show it.
136
+ const refusedLines = summary.refused
137
+ .filter((refusal) => !reason.includes(refusal.input.slice(0, 60)))
138
+ .map((refusal) => ` Refused: ${refusal.input}${toolLabel(refusal.tool)}`);
139
+ const [chat, update, restart] = summary.remedies;
140
+ return [
141
+ `The latest run (${summary.runId}) is blocked on a permission, not on an approval request.`,
142
+ ...(reason ? [` ${reason}`] : []),
143
+ ...refusedLines,
144
+ 'It ran unattended (`tasks start`), and an unattended run cannot prompt anyone to approve a',
145
+ 'tool call, so the ask was refused on the spot and no approval request was recorded here.',
146
+ '',
147
+ 'To continue, either:',
148
+ ` ${chat.command} do the work interactively, approving each call`,
149
+ ` ${update.command} let unattended runs use the tool`,
150
+ ` then: ${restart.command} (or pick an agent/harness whose mode allows it)`,
151
+ ];
152
+ }
25
153
  class TasksApprovalsList extends base_command_1.BaseCommand {
26
154
  static description = 'List approvals for a task';
27
155
  static examples = [
@@ -44,14 +172,50 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
44
172
  catch (err) {
45
173
  this.handleApiError(err);
46
174
  }
175
+ const r = response;
176
+ const items = r?.data ?? [];
177
+ // OSK-12165 — an unattended run blocked on a permission leaves no approval
178
+ // row, so the empty list alone would say "nothing to act on" about a task
179
+ // that is waiting on exactly that. Read the latest run, best-effort: the
180
+ // approvals list is still the answer if the runs read fails.
181
+ // `runsRead` separates "the latest run is not blocked" from "we could not
182
+ // look": only the first may be reported to a script as `blockedRun: null`.
183
+ let blockedRun = null;
184
+ let runsRead = false;
185
+ try {
186
+ const runs = (await data_provider_1.dataService.getTaskRuns(args.id));
187
+ blockedRun = permissionBlockedRun(runs?.data ?? []);
188
+ runsRead = true;
189
+ }
190
+ catch {
191
+ blockedRun = null;
192
+ }
47
193
  if (flags.json) {
48
- this.log(JSON.stringify(response, null, 2));
194
+ // OSK-12173 — the server payload is kept as it was (`data` unchanged)
195
+ // and the block is added beside it as data a script can act on:
196
+ // `blockedRun` is the summary, or null when the latest run is not
197
+ // blocked. It is omitted when the runs could not be read, because null
198
+ // would claim a state nobody checked.
199
+ const payload = response && typeof response === 'object' && !Array.isArray(response)
200
+ ? response
201
+ : { data: items };
202
+ const out = runsRead
203
+ ? {
204
+ ...payload,
205
+ blockedRun: blockedRun ? blockedRunSummary(blockedRun, this.config.bin, args.id) : null,
206
+ }
207
+ : payload;
208
+ this.log(JSON.stringify(out, null, 2));
49
209
  return;
50
210
  }
51
- const r = response;
52
- const items = r?.data ?? [];
211
+ const blockLines = blockedRun ? permissionBlockLines(blockedRun, this.config.bin, args.id) : [];
53
212
  if (items.length === 0) {
54
- this.log('No approvals.');
213
+ this.log('No approval requests.');
214
+ if (blockLines.length > 0) {
215
+ this.log('');
216
+ for (const line of blockLines)
217
+ this.log(line);
218
+ }
55
219
  return;
56
220
  }
57
221
  const rows = items.map(approvalRow);
@@ -62,6 +226,11 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
62
226
  { key: 'approver', header: 'APPROVER', maxWidth: 30 },
63
227
  { key: 'created', header: 'CREATED' },
64
228
  ], (line) => this.log(line));
229
+ if (blockLines.length > 0) {
230
+ this.log('');
231
+ for (const line of blockLines)
232
+ this.log(line);
233
+ }
65
234
  }
66
235
  }
67
236
  exports.default = TasksApprovalsList;
@@ -238,7 +238,8 @@ class TasksCreate extends base_command_1.BaseCommand {
238
238
  'label-id': core_1.Flags.string({
239
239
  aliases: ['label'],
240
240
  description: 'Attach a task label (repeatable) — id, or a name from `skrr labels list --applicable-to task`. ' +
241
- 'An unknown name is refused, never created. Manage labels with `skrr labels`.',
241
+ 'An unknown name is refused, never created. Also accepted as --label. ' +
242
+ 'Manage labels with `skrr labels`.',
242
243
  multiple: true,
243
244
  }),
244
245
  'working-directory': core_1.Flags.string({
@@ -137,7 +137,7 @@ function startFollowUpLines(bin, taskRef) {
137
137
  'Follow it:',
138
138
  ` ${bin} tasks watch ${taskRef} status until it finishes or fails`,
139
139
  ` ${bin} tasks transcript ${taskRef} what the agent has said so far`,
140
- ` ${bin} tasks approvals list ${taskRef} approval requests waiting on you`,
140
+ ` ${bin} tasks approvals list ${taskRef} approvals waiting on you, or why a run is blocked on one`,
141
141
  'On a local machine, the run may add its instruction file (AGENTS.md / CLAUDE.md) to the',
142
142
  'working directory while it runs; files the run wrote are removed afterwards unless changed.',
143
143
  ];
@@ -163,7 +163,8 @@ class TasksUpdate extends base_command_1.BaseCommand {
163
163
  }),
164
164
  'label-id': core_1.Flags.string({
165
165
  aliases: ['label'],
166
- description: 'Replace the task labels with these ids (repeatable, whole-list). ' +
166
+ description: 'Replace the task labels (repeatable, whole-list) — id, or a name from ' +
167
+ '`skrr labels list --applicable-to task`. Also accepted as --label. ' +
167
168
  'Use `skrr tasks labels attach/detach` for incremental changes.',
168
169
  multiple: true,
169
170
  }),
@@ -322,6 +323,25 @@ class TasksUpdate extends base_command_1.BaseCommand {
322
323
  }
323
324
  }
324
325
  }
326
+ // Same resolution as `tasks create`: the `--tag` warning below suggests
327
+ // `--label-id <name>`, so this flag has to accept the name it suggests
328
+ // (OSK-12166). Ids pass through untouched.
329
+ let resolvedLabelIds;
330
+ if (flags['label-id'] !== undefined) {
331
+ try {
332
+ resolvedLabelIds = await (0, label_ref_1.resolveLabelRefs)(flags['label-id'], {
333
+ applicableTo: 'task',
334
+ ...(process.env.OVERSKY_WORKSPACE_ID
335
+ ? { workspaceId: process.env.OVERSKY_WORKSPACE_ID }
336
+ : {}),
337
+ });
338
+ }
339
+ catch (err) {
340
+ if (err instanceof label_ref_1.LabelRefError)
341
+ this.error(err.message, { exit: 1 });
342
+ this.handleApiError(err);
343
+ }
344
+ }
325
345
  const payload = (0, prompt_1.mergeDefined)((0, task_resolver_1.stripTaskIdentityFields)(body), {
326
346
  title: flags.title,
327
347
  expectations: expectationsPatch === null ? [] : (expectationsPatch ?? undefined),
@@ -353,7 +373,7 @@ class TasksUpdate extends base_command_1.BaseCommand {
353
373
  addBlocksIds: flags['add-blocks'],
354
374
  removeBlocksIds: flags['remove-blocks'],
355
375
  tags: flags.tag,
356
- labelIds: flags['label-id'],
376
+ labelIds: resolvedLabelIds,
357
377
  // Worktree isolation override. --worktree ⇒ 'ephemeral', --no-worktree ⇒
358
378
  // 'shared', omitted ⇒ unchanged. (Clear-to-inherit via --from-json.)
359
379
  executionIsolation: (0, tasks_2.executionIsolationFromWorktreeFlag)(flags.worktree),
@@ -448,7 +468,7 @@ class TasksUpdate extends base_command_1.BaseCommand {
448
468
  const estimateWarning = (0, create_1.droppedEstimateWarning)(flags.estimate, updated, updated?.spaceId);
449
469
  if (estimateWarning)
450
470
  this.warn(estimateWarning);
451
- const tagWarning = await (0, create_1.taskTagLabelWarning)(flags.tag, flags['label-id']);
471
+ const tagWarning = await (0, create_1.taskTagLabelWarning)(flags.tag, resolvedLabelIds);
452
472
  if (tagWarning)
453
473
  this.warn(tagWarning);
454
474
  if (flags.json) {
@@ -4,6 +4,19 @@ export declare const REASONING_TOOL_ESCAPES: readonly ["bash", "browser"];
4
4
  export declare const COMMITMENT_PERMISSION_FLAG_RETIRED = "This setting no longer has any effect: Commitment runs use the agent's own permissions.";
5
5
  /** The three retired flags themselves: accepted at parse, warned on, never sent. */
6
6
  export declare const RETIRED_COMMITMENT_PERMISSION_FLAGS: readonly ["reasoning-tool-escape", "commitment-responsibility", "commitment-responsibility-ceiling"];
7
+ /**
8
+ * The deprecation warning ONE retired flag prints. oclif prints a
9
+ * `deprecated.message` verbatim, so a shared message printed the same line
10
+ * once per flag with nothing saying which flag each one was about (OSK-12068).
11
+ */
12
+ export declare function retiredCommitmentPermissionFlagMessage(flag: (typeof RETIRED_COMMITMENT_PERMISSION_FLAGS)[number]): string;
13
+ /**
14
+ * The fields the server ignored and why, from a write's `deprecatedFields`
15
+ * notice, one line each. Empty when the response carries none.
16
+ */
17
+ export declare function deprecatedFieldsWarnings(response: unknown): string[];
18
+ /** The payload keys the server reported as ignored, so they are not also called "not kept". */
19
+ export declare function deprecatedFieldNames(response: unknown): string[];
7
20
  export declare const RELEASE_INSTALL_CHANNELS: readonly ["latest", "manual"];
8
21
  /**
9
22
  * `09:00-18:00` or `09:00-18:00@America/Los_Angeles`, or a JSON object.