@skrr-ai/cli 0.1.52 → 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 (32) 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 +22 -0
  6. package/dist/commands/tasks/approvals/list.js +80 -3
  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 +9 -3
  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 +8 -3
  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/oclif.manifest.json +35912 -35900
  32. 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,28 @@ 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
+ /**
34
+ * What to print for such a run: why it is blocked, what needed approval (the
35
+ * run's own recorded sentence, which names the refused tool), that an
36
+ * unattended run cannot prompt, and the two ways forward with exact commands.
37
+ */
38
+ export declare function permissionBlockLines(run: Record<string, unknown>, bin?: string, taskRef?: string): string[];
17
39
  export default class TasksApprovalsList extends BaseCommand {
18
40
  static description: string;
19
41
  static examples: string[];
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.approvalRow = approvalRow;
4
+ exports.permissionBlockedRun = permissionBlockedRun;
5
+ exports.permissionBlockLines = permissionBlockLines;
4
6
  const core_1 = require("@oclif/core");
5
7
  const data_provider_1 = require("@skrr-ai/data-provider");
6
8
  const base_command_1 = require("../../../base-command");
@@ -22,6 +24,52 @@ function approvalRow(a) {
22
24
  created: a.createdAt ? (0, format_1.localTime)(a.createdAt) : '-',
23
25
  };
24
26
  }
27
+ /**
28
+ * OSK-12165 — the task's latest run, when it ended blocked on a permission
29
+ * nobody could grant.
30
+ *
31
+ * An unattended run (`tasks start`) cannot ask anyone: the permission bridge
32
+ * declines every ask at once, so no approval row is ever created, and this
33
+ * command used to answer "No approvals." in exactly the case `tasks start`
34
+ * sends people here for. The evidence lives on the run, not in the approvals
35
+ * collection: the leaf finalizer settles it with `error.code`
36
+ * `permission_blocked`, which `GET /tasks/:id/runs` projects as `failureCode`
37
+ * alongside the sentence (what was refused, and the fix) as `failureReason`.
38
+ *
39
+ * Only the LATEST run counts (the route sorts newest first): a permission block
40
+ * on an older run that a later run got past is history, not the current state.
41
+ */
42
+ function permissionBlockedRun(runs) {
43
+ const latest = runs[0];
44
+ if (!latest)
45
+ return null;
46
+ const code = latest.failureCode ?? latest.failureClass;
47
+ return code === data_provider_1.PERMISSION_BLOCKED_FAILURE_CODE ? latest : null;
48
+ }
49
+ /**
50
+ * What to print for such a run: why it is blocked, what needed approval (the
51
+ * run's own recorded sentence, which names the refused tool), that an
52
+ * unattended run cannot prompt, and the two ways forward with exact commands.
53
+ */
54
+ function permissionBlockLines(run, bin = 'skrr', taskRef = '<task-id>') {
55
+ const runId = String(run.runId ?? run.id ?? '-');
56
+ const reason = typeof run.failureReason === 'string' ? run.failureReason.trim() : '';
57
+ const executor = run.executor;
58
+ const agent = executor && typeof executor.agentId === 'string' && executor.agentId
59
+ ? executor.agentId
60
+ : '<agent>';
61
+ return [
62
+ `The latest run (${runId}) is blocked on a permission, not on an approval request.`,
63
+ ...(reason ? [` ${reason}`] : []),
64
+ 'It ran unattended (`tasks start`), and an unattended run cannot prompt anyone to approve a',
65
+ 'tool call, so the ask was refused on the spot and no approval request was recorded here.',
66
+ '',
67
+ 'To continue, either:',
68
+ ` ${bin} agents chat ${agent} --approve-tools --prompt "..." do the work interactively, approving each call`,
69
+ ` ${bin} agents update ${agent} --default-permission-mode bypassPermissions let unattended runs use the tool`,
70
+ ` then: ${bin} tasks start ${taskRef} (or pick an agent/harness whose mode allows it)`,
71
+ ];
72
+ }
25
73
  class TasksApprovalsList extends base_command_1.BaseCommand {
26
74
  static description = 'List approvals for a task';
27
75
  static examples = [
@@ -44,14 +92,38 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
44
92
  catch (err) {
45
93
  this.handleApiError(err);
46
94
  }
95
+ const r = response;
96
+ const items = r?.data ?? [];
97
+ // OSK-12165 — an unattended run blocked on a permission leaves no approval
98
+ // row, so the empty list alone would say "nothing to act on" about a task
99
+ // that is waiting on exactly that. Read the latest run, best-effort: the
100
+ // approvals list is still the answer if the runs read fails.
101
+ let blockedRun = null;
102
+ try {
103
+ const runs = (await data_provider_1.dataService.getTaskRuns(args.id));
104
+ blockedRun = permissionBlockedRun(runs?.data ?? []);
105
+ }
106
+ catch {
107
+ blockedRun = null;
108
+ }
109
+ const blockLines = blockedRun ? permissionBlockLines(blockedRun, this.config.bin, args.id) : [];
47
110
  if (flags.json) {
111
+ // The server payload stays byte-identical; the pointer goes to stderr so
112
+ // `--json` remains parseable and a script still learns the task is stuck.
113
+ if (blockedRun) {
114
+ this.warn(`The latest run is blocked on a permission an unattended run could not ask for; ` +
115
+ `\`${this.config.bin} tasks approvals list ${args.id}\` (without --json) says what to do.`);
116
+ }
48
117
  this.log(JSON.stringify(response, null, 2));
49
118
  return;
50
119
  }
51
- const r = response;
52
- const items = r?.data ?? [];
53
120
  if (items.length === 0) {
54
- this.log('No approvals.');
121
+ this.log('No approval requests.');
122
+ if (blockLines.length > 0) {
123
+ this.log('');
124
+ for (const line of blockLines)
125
+ this.log(line);
126
+ }
55
127
  return;
56
128
  }
57
129
  const rows = items.map(approvalRow);
@@ -62,6 +134,11 @@ class TasksApprovalsList extends base_command_1.BaseCommand {
62
134
  { key: 'approver', header: 'APPROVER', maxWidth: 30 },
63
135
  { key: 'created', header: 'CREATED' },
64
136
  ], (line) => this.log(line));
137
+ if (blockLines.length > 0) {
138
+ this.log('');
139
+ for (const line of blockLines)
140
+ this.log(line);
141
+ }
65
142
  }
66
143
  }
67
144
  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.
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RELEASE_INSTALL_CHANNELS = exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = exports.COMMITMENT_PERMISSION_FLAG_RETIRED = exports.REASONING_TOOL_ESCAPES = exports.COMMITMENT_RESPONSIBILITY_LEVELS = void 0;
4
+ exports.retiredCommitmentPermissionFlagMessage = retiredCommitmentPermissionFlagMessage;
5
+ exports.deprecatedFieldsWarnings = deprecatedFieldsWarnings;
6
+ exports.deprecatedFieldNames = deprecatedFieldNames;
4
7
  exports.parseWorkingHoursFlag = parseWorkingHoursFlag;
5
8
  exports.mergeAgentConfigFlags = mergeAgentConfigFlags;
6
9
  const prompt_1 = require("./prompt");
@@ -15,6 +18,35 @@ exports.RETIRED_COMMITMENT_PERMISSION_FLAGS = [
15
18
  'commitment-responsibility',
16
19
  'commitment-responsibility-ceiling',
17
20
  ];
21
+ /**
22
+ * The deprecation warning ONE retired flag prints. oclif prints a
23
+ * `deprecated.message` verbatim, so a shared message printed the same line
24
+ * once per flag with nothing saying which flag each one was about (OSK-12068).
25
+ */
26
+ function retiredCommitmentPermissionFlagMessage(flag) {
27
+ return `--${flag} is retired and was not sent. ${exports.COMMITMENT_PERMISSION_FLAG_RETIRED}`;
28
+ }
29
+ /**
30
+ * The fields the server ignored and why, from a write's `deprecatedFields`
31
+ * notice, one line each. Empty when the response carries none.
32
+ */
33
+ function deprecatedFieldsWarnings(response) {
34
+ const entries = response?.deprecatedFields;
35
+ if (!Array.isArray(entries))
36
+ return [];
37
+ return entries
38
+ .filter((entry) => Boolean(entry) && typeof entry.field === 'string')
39
+ .map((entry) => `${entry.field}: ${entry.reason || 'ignored by the server'}`);
40
+ }
41
+ /** The payload keys the server reported as ignored, so they are not also called "not kept". */
42
+ function deprecatedFieldNames(response) {
43
+ const entries = response?.deprecatedFields;
44
+ return Array.isArray(entries)
45
+ ? entries
46
+ .map((entry) => entry?.field)
47
+ .filter((field) => typeof field === 'string')
48
+ : [];
49
+ }
18
50
  exports.RELEASE_INSTALL_CHANNELS = ['latest', 'manual'];
19
51
  /**
20
52
  * `09:00-18:00` or `09:00-18:00@America/Los_Angeles`, or a JSON object.
@@ -299,7 +299,7 @@ export type SeqGap = {
299
299
  * `service` envelopes that carry a `notice` — words for the person, not
300
300
  * diagnostics — projected as `session.notice` events. Today the only notice is
301
301
  * `harness-session-reset`: the local harness could not resume its previous
302
- * session and began a new one, so its local working context is gone.
302
+ * session and began a new one, which does not see earlier turns (OSK-12155).
303
303
  */
304
304
  export declare function projectSessionNotices(envelopes: unknown[]): AgenticStreamEvent[];
305
305
  export declare function detectSeqGap(highestApplied: number, incoming: number): SeqGap | null;
@@ -426,7 +426,7 @@ function jitteredRetryDelay(retryAfterMs, random = Math.random) {
426
426
  * `service` envelopes that carry a `notice` — words for the person, not
427
427
  * diagnostics — projected as `session.notice` events. Today the only notice is
428
428
  * `harness-session-reset`: the local harness could not resume its previous
429
- * session and began a new one, so its local working context is gone.
429
+ * session and began a new one, which does not see earlier turns (OSK-12155).
430
430
  */
431
431
  function projectSessionNotices(envelopes) {
432
432
  const out = [];
@@ -288,7 +288,12 @@ function readFileAt(p, upgradeInPlace) {
288
288
  // L12 — route the secret string fields through the envelope. Mixed
289
289
  // bundles (e.g., legacy plaintext token + freshly-rotated wrapped
290
290
  // refresh) decrypt independently so partial migrations stay safe.
291
- const tokenResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.token);
291
+ // Read the (plaintext) owner first, so a value another config root sealed
292
+ // under its own key fails with a sentence that says so (OSK-12135).
293
+ const owner = parseOwner(parsed.owner);
294
+ const ownerRoot = owner?.configRoot ? path.resolve(owner.configRoot) : undefined;
295
+ const decryptOptions = ownerRoot && ownerRoot !== path.resolve((0, config_1.configRoot)()) ? { foreignOwnerRoot: ownerRoot } : {};
296
+ const tokenResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.token, decryptOptions);
292
297
  if (tokenResult.plaintext === null) {
293
298
  // Envelope on disk but no DEK to unwrap → treat as missing so the
294
299
  // caller falls through to a clean `skrr login`.
@@ -300,7 +305,7 @@ function readFileAt(p, upgradeInPlace) {
300
305
  bundle.expiresAt = parsed.expiresAt;
301
306
  }
302
307
  if (typeof parsed.refreshToken === 'string' && parsed.refreshToken.length > 0) {
303
- const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken);
308
+ const refreshResult = (0, cred_envelope_1.maybeDecryptOnRead)(parsed.refreshToken, decryptOptions);
304
309
  if (refreshResult.plaintext !== null) {
305
310
  bundle.refreshToken = refreshResult.plaintext;
306
311
  if (refreshResult.needsMigration)
@@ -316,7 +321,6 @@ function readFileAt(p, upgradeInPlace) {
316
321
  if (typeof parsed.serverOrigin === 'string' && parsed.serverOrigin.length > 0) {
317
322
  bundle.serverOrigin = parsed.serverOrigin;
318
323
  }
319
- const owner = parseOwner(parsed.owner);
320
324
  if (owner)
321
325
  bundle.owner = owner;
322
326
  // Best-effort upgrade: re-write the bundle so legacy plaintext fields
@@ -8,7 +8,18 @@
8
8
  * and file backends and avoids a detect-then-read TOCTOU race.
9
9
  */
10
10
  import { type ReadTransformResult } from '@skrr-ai/auth-core';
11
- export declare function maybeDecryptOnRead(stored: string): ReadTransformResult;
11
+ /**
12
+ * The `decrypt_failed` hint for a value whose owner record names ANOTHER
13
+ * config root. Each root seals under its own key, so that value is unreadable
14
+ * here by construction: "signing in again rebuilds it" would send someone to
15
+ * re-login over a credential that is not broken and not theirs (OSK-12135).
16
+ */
17
+ export declare function foreignCredentialHint(ownerConfigRoot: string): string;
18
+ export interface CliDecryptOnReadOptions {
19
+ /** The config root the stored value's owner record names, when not ours. */
20
+ foreignOwnerRoot?: string;
21
+ }
22
+ export declare function maybeDecryptOnRead(stored: string, options?: CliDecryptOnReadOptions): ReadTransformResult;
12
23
  /** Consume and clear the per-process blocked-read observation. */
13
24
  export declare function consumeEncryptedCredentialReadWhileInactive(): boolean;
14
25
  /** Consume any unreadable envelope observation, including tamper/decrypt failure. */
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.__wrappedDekFilePathForTest = exports.maybeEncryptForWrite = exports.describeCredEnvelopeState = exports.isCredEnvelopeActive = exports.shutdownCredEnvelope = exports.resetCredEnvelope = exports.initCredEnvelope = void 0;
4
+ exports.foreignCredentialHint = foreignCredentialHint;
4
5
  exports.maybeDecryptOnRead = maybeDecryptOnRead;
5
6
  exports.consumeEncryptedCredentialReadWhileInactive = consumeEncryptedCredentialReadWhileInactive;
6
7
  exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCredentialRead;
@@ -16,10 +17,26 @@ exports.consumeUnreadableEncryptedCredentialRead = consumeUnreadableEncryptedCre
16
17
  const auth_core_1 = require("@skrr-ai/auth-core");
17
18
  let encryptedReadWhileInactive = false;
18
19
  let unreadableEncryptedRead = false;
19
- function maybeDecryptOnRead(stored) {
20
- const result = (0, auth_core_1.maybeDecryptOnRead)(stored);
20
+ /**
21
+ * The `decrypt_failed` hint for a value whose owner record names ANOTHER
22
+ * config root. Each root seals under its own key, so that value is unreadable
23
+ * here by construction: "signing in again rebuilds it" would send someone to
24
+ * re-login over a credential that is not broken and not theirs (OSK-12135).
25
+ */
26
+ function foreignCredentialHint(ownerConfigRoot) {
27
+ return (`stored credential belongs to another skrr config (config root ${ownerConfigRoot}) and is ` +
28
+ "sealed under that config's key, so this config cannot read it. Nothing is wrong with " +
29
+ 'either credential and it was left untouched; signing in here does not change it');
30
+ }
31
+ function maybeDecryptOnRead(stored, options = {}) {
32
+ const result = (0, auth_core_1.maybeDecryptOnRead)(stored, options.foreignOwnerRoot
33
+ ? { unreadableHint: foreignCredentialHint(options.foreignOwnerRoot) }
34
+ : {});
21
35
  if (result.plaintext === null && (0, auth_core_1.isCredEnvelopeString)(stored)) {
22
- unreadableEncryptedRead = true;
36
+ // Another config's value is not an unreadable credential of OURS, so it
37
+ // must not tell `logout` there is local encrypted state to erase.
38
+ if (!options.foreignOwnerRoot)
39
+ unreadableEncryptedRead = true;
23
40
  if ((0, auth_core_1.describeCredEnvelopeState)().kind !== 'active') {
24
41
  encryptedReadWhileInactive = true;
25
42
  }