@skrr-ai/cli 0.1.44 → 0.1.46

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 (73) hide show
  1. package/dist/base-command.d.ts +13 -1
  2. package/dist/base-command.js +37 -3
  3. package/dist/commands/agents/actions/create.js +11 -4
  4. package/dist/commands/agents/chat.js +3 -4
  5. package/dist/commands/browser/cloud-agents.d.ts +20 -0
  6. package/dist/commands/browser/cloud-agents.js +43 -0
  7. package/dist/commands/code/handover.d.ts +11 -0
  8. package/dist/commands/code/handover.js +105 -1
  9. package/dist/commands/commitments/handover.d.ts +29 -0
  10. package/dist/commands/commitments/handover.js +99 -0
  11. package/dist/commands/followups/list.js +1 -1
  12. package/dist/commands/followups/watch.js +40 -5
  13. package/dist/commands/initiatives/list.d.ts +1 -0
  14. package/dist/commands/initiatives/list.js +11 -2
  15. package/dist/commands/labels/list.d.ts +28 -0
  16. package/dist/commands/labels/list.js +50 -32
  17. package/dist/commands/tasks/fork.d.ts +32 -0
  18. package/dist/commands/tasks/fork.js +171 -0
  19. package/dist/commands/tasks/handover.d.ts +23 -0
  20. package/dist/commands/tasks/handover.js +205 -0
  21. package/dist/commands/tasks/promote.d.ts +23 -0
  22. package/dist/commands/tasks/promote.js +82 -0
  23. package/dist/commands/tasks/runs.d.ts +16 -0
  24. package/dist/commands/tasks/runs.js +46 -1
  25. package/dist/commands/tasks/show.d.ts +23 -0
  26. package/dist/commands/tasks/show.js +59 -0
  27. package/dist/lib/agentic-stream.js +41 -2
  28. package/dist/lib/api-fetch.d.ts +7 -1
  29. package/dist/lib/api-fetch.js +9 -2
  30. package/dist/lib/auth-core-init.js +11 -4
  31. package/dist/lib/code-handover.d.ts +60 -0
  32. package/dist/lib/code-handover.js +120 -0
  33. package/dist/lib/commitments.d.ts +22 -0
  34. package/dist/lib/commitments.js +42 -0
  35. package/dist/lib/dedicated-wait.js +22 -1
  36. package/dist/lib/followups.d.ts +18 -0
  37. package/dist/lib/followups.js +44 -0
  38. package/dist/lib/initiatives.d.ts +15 -3
  39. package/dist/lib/label-ref.d.ts +2 -0
  40. package/dist/lib/node-adapter.js +6 -0
  41. package/dist/lib/task-extras.d.ts +44 -0
  42. package/dist/lib/task-extras.js +73 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
  46. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
  47. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
  48. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  49. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  50. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  51. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  52. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
  53. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
  54. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
  69. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  70. package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34300 -33843
  73. package/package.json +2 -2
@@ -311,14 +311,26 @@ export declare abstract class BaseCommand extends Command {
311
311
  body: unknown;
312
312
  idempotencyKey: string;
313
313
  }): never;
314
- protected failWithCliError({ message, code, status, exit, retryable, details, }: {
314
+ protected failWithCliError({ message, code, status, exit, retryable, details, requestId, }: {
315
315
  message: string;
316
316
  code: string;
317
317
  status?: number;
318
318
  exit: number;
319
319
  retryable: boolean;
320
320
  details?: unknown;
321
+ /** The server's `X-Request-ID` for the refused call, when there was one. */
322
+ requestId?: string;
321
323
  }): never;
324
+ /**
325
+ * The server's `X-Request-ID` for the most recent refused API call.
326
+ *
327
+ * The CLI prints its own refusals but not the id the server logged them
328
+ * against, so matching one to a log line meant guessing from timestamps. On
329
+ * an account with several clients that misattributes: a 409 raised by
330
+ * another session's concurrent image update was nearly filed as a CLI retry
331
+ * bug (2026-09-18).
332
+ */
333
+ private lastApiRequestId?;
322
334
  private writeJsonCliError;
323
335
  protected wantsJsonOutput(): boolean;
324
336
  }
@@ -853,9 +853,26 @@ class BaseCommand extends core_1.Command {
853
853
  * envelope carry the same identifying text.
854
854
  */
855
855
  handleApiError(err, { subject } = {}) {
856
+ // An error this command already raised for the user is not an API failure
857
+ // to translate. `this.error()` throws a CLIError carrying `oclif.exit`, and
858
+ // under --json it is the ExitError thrown AFTER `writeJsonCliError` already
859
+ // printed the envelope. When the raise sits inside the try whose catch is
860
+ // this call, re-wrapping it printed a SECOND envelope (`--json` saw the
861
+ // real message followed by `EEXIT: 1`) and rewrote the caller's exit code
862
+ // to 1 — `agents actions create --prompt ""` did exactly that. Pass it
863
+ // through so `catch()` renders it once, with its code and exit intact.
864
+ if (err instanceof Error) {
865
+ const raised = err;
866
+ if (isOclifExitError(raised) || typeof raised.oclif?.exit === 'number') {
867
+ throw err;
868
+ }
869
+ }
856
870
  const say = (message) => withSubject(subject, message);
857
871
  if (err instanceof Error) {
858
872
  const e = err;
873
+ // Remembered once here rather than threaded through every branch below:
874
+ // each one ends in `failWithCliError`, and the id belongs on all of them.
875
+ this.lastApiRequestId = e.requestId;
859
876
  const body = parseErrorBody(e.body);
860
877
  const bodyCode = stringProp(body, 'code') || stringProp(body, 'errorType');
861
878
  const bodyMessage = stringProp(body, 'message') || stringProp(body, 'error');
@@ -1242,9 +1259,9 @@ class BaseCommand extends core_1.Command {
1242
1259
  details: { outboxId: queuedId, idempotencyKey: write.idempotencyKey },
1243
1260
  });
1244
1261
  }
1245
- failWithCliError({ message, code, status, exit, retryable, details, }) {
1262
+ failWithCliError({ message, code, status, exit, retryable, details, requestId, }) {
1246
1263
  if (this.wantsJsonOutput()) {
1247
- this.writeJsonCliError({ message, code, status, exit, retryable, details });
1264
+ this.writeJsonCliError({ message, code, status, exit, retryable, details, requestId });
1248
1265
  }
1249
1266
  // Human path. Deliberately stderr-only: stdout is the SUCCESS channel, so
1250
1267
  // `id=$(skrr tasks start "$x")` and `sky ... | jq` are never fed error
@@ -1277,7 +1294,17 @@ class BaseCommand extends core_1.Command {
1277
1294
  ...(suggestion ? { suggestions: [suggestion] } : {}),
1278
1295
  });
1279
1296
  }
1280
- writeJsonCliError({ message, code, status, exit, retryable, details, }) {
1297
+ /**
1298
+ * The server's `X-Request-ID` for the most recent refused API call.
1299
+ *
1300
+ * The CLI prints its own refusals but not the id the server logged them
1301
+ * against, so matching one to a log line meant guessing from timestamps. On
1302
+ * an account with several clients that misattributes: a 409 raised by
1303
+ * another session's concurrent image update was nearly filed as a CLI retry
1304
+ * bug (2026-09-18).
1305
+ */
1306
+ lastApiRequestId;
1307
+ writeJsonCliError({ message, code, status, exit, retryable, details, requestId, }) {
1281
1308
  this.log(JSON.stringify({
1282
1309
  ok: false,
1283
1310
  error: {
@@ -1287,6 +1314,13 @@ class BaseCommand extends core_1.Command {
1287
1314
  retryable,
1288
1315
  command: this.id ?? null,
1289
1316
  server: this.cliConfig?.baseURL ?? null,
1317
+ // The id the server logged this refusal against. Without it, a
1318
+ // refused call can only be matched to a log line by guessing from
1319
+ // timestamps — which, with several clients on one account,
1320
+ // attributes another session's request to yours.
1321
+ ...(requestId || this.lastApiRequestId
1322
+ ? { requestId: requestId ?? this.lastApiRequestId }
1323
+ : {}),
1290
1324
  ...(details ? { details } : {}),
1291
1325
  },
1292
1326
  }, null, 2));
@@ -81,8 +81,11 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
81
81
  let replayed = false;
82
82
  try {
83
83
  const title = typeof body.title === 'string' ? body.title.trim() : '';
84
- if (!title)
85
- this.error('Action title is required. Pass --title or include title.', { exit: 1 });
84
+ if (!title) {
85
+ this.error(typeof body.title === 'string'
86
+ ? 'The action title was provided but is empty. Pass non-empty text to --title or title in --from-json.'
87
+ : 'Action title is required. Pass --title or include title.', { exit: 1 });
88
+ }
86
89
  const kind = body.kind === 'builtin' ? 'builtin' : 'prompt';
87
90
  // No read of the library: position is assigned by the server at the
88
91
  // moment of the append, so it cannot be computed from a stale copy. An
@@ -97,7 +100,9 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
97
100
  if (kind === 'builtin') {
98
101
  const builtinType = body.builtinType;
99
102
  if (!agent_actions_1.BUILTIN_ACTION_TYPES.includes(builtinType)) {
100
- this.error(`--builtin-type is required for built-in actions.`, { exit: 1 });
103
+ this.error(builtinType === undefined
104
+ ? '--builtin-type is required for built-in actions.'
105
+ : `Invalid --builtin-type "${String(builtinType)}" — expected one of ${agent_actions_1.BUILTIN_ACTION_TYPES.join(', ')}.`, { exit: 1 });
101
106
  }
102
107
  created = (0, agent_actions_1.makeBuiltinAction)({
103
108
  scope,
@@ -118,7 +123,9 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
118
123
  else {
119
124
  const prompt = typeof body.prompt === 'string' ? body.prompt : '';
120
125
  if (!prompt.trim()) {
121
- this.error('Prompt actions require --prompt or prompt in --from-json.', { exit: 1 });
126
+ this.error(typeof body.prompt === 'string'
127
+ ? 'The prompt was provided but is empty. Pass non-empty text to --prompt or prompt in --from-json.'
128
+ : 'Prompt actions require --prompt or prompt in --from-json.', { exit: 1 });
122
129
  }
123
130
  // Embedded built-in actions (chip source of truth). The prompt keeps the
124
131
  // inline `[[action:refId]]` tokens (positional); the server expands each
@@ -235,7 +235,9 @@ class AgentsChat extends base_command_1.BaseCommand {
235
235
  description: 'Emit the final one-shot response as JSON (use with --no-stream)',
236
236
  }),
237
237
  'approve-tools': core_1.Flags.boolean({
238
- description: 'Prompt interactively before approving tool calls',
238
+ description: 'Allow this turn to use permission-gated tools. In a terminal it asks per call; ' +
239
+ 'with no terminal the flag itself is the approval for this turn, and each one is ' +
240
+ 'reported on the stream. Without it every such call is denied.',
239
241
  }),
240
242
  'local-skill': core_1.Flags.string({
241
243
  description: 'Invoke a skill on your machine on this turn, as `skrr skills local` lists it: its ' +
@@ -333,9 +335,6 @@ class AgentsChat extends base_command_1.BaseCommand {
333
335
  this.error('--local-skill has no meaning with --quick: the persona-only summarizer runs no ' +
334
336
  'runtime, so no skill on your machine can load. Drop --quick for a real turn.', { exit: 2 });
335
337
  }
336
- if (flags['approve-tools'] && !process.stdin.isTTY) {
337
- this.error('--approve-tools requires an interactive terminal.', { exit: 2 });
338
- }
339
338
  if (flags.conversation && flags['thread-root']) {
340
339
  this.error('--conversation and --thread-root are mutually exclusive.', { exit: 2 });
341
340
  }
@@ -0,0 +1,20 @@
1
+ import { Command } from '@oclif/core';
2
+ /**
3
+ * Whether a cloud agent may drive the browser on THIS computer (OSK-10285).
4
+ *
5
+ * The setting lives in the daemon's own config on this computer, so it is
6
+ * forwarded to the daemon rather than sent to the API: consent to a cloud agent
7
+ * using a signed-in browser belongs to the person at the machine, and a request
8
+ * from the cloud must not be able to grant it.
9
+ */
10
+ export default class BrowserCloudAgents extends Command {
11
+ static description: string;
12
+ static examples: string[];
13
+ static args: {
14
+ action: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
15
+ };
16
+ static flags: {
17
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
18
+ };
19
+ run(): Promise<void>;
20
+ }
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const browser_1 = require("../browser");
5
+ /**
6
+ * Whether a cloud agent may drive the browser on THIS computer (OSK-10285).
7
+ *
8
+ * The setting lives in the daemon's own config on this computer, so it is
9
+ * forwarded to the daemon rather than sent to the API: consent to a cloud agent
10
+ * using a signed-in browser belongs to the person at the machine, and a request
11
+ * from the cloud must not be able to grant it.
12
+ */
13
+ class BrowserCloudAgents extends core_1.Command {
14
+ static description = 'Allow, deny, or show whether cloud agents may drive the browser on this computer (off by default)';
15
+ static examples = [
16
+ '<%= config.bin %> browser cloud-agents status',
17
+ '<%= config.bin %> browser cloud-agents allow',
18
+ '<%= config.bin %> browser cloud-agents deny',
19
+ ];
20
+ static args = {
21
+ action: core_1.Args.string({
22
+ description: 'allow, deny, or status',
23
+ required: true,
24
+ options: ['allow', 'deny', 'status'],
25
+ // Without this oclif reads stdin for a missing positional, so the command
26
+ // hangs when run from a pipe or a non-tty instead of refusing. Enforced
27
+ // by ARG_IMPLICIT_STDIN_ENABLED in the command-hygiene audit.
28
+ ignoreStdin: true,
29
+ }),
30
+ };
31
+ static flags = {
32
+ json: core_1.Flags.boolean({ description: 'Output JSON' }),
33
+ };
34
+ async run() {
35
+ const { args, flags } = await this.parse(BrowserCloudAgents);
36
+ await (0, browser_1.forwardBrowserCommand)(this, [
37
+ 'cloud-agents',
38
+ args.action,
39
+ ...(flags.json ? ['--json'] : []),
40
+ ]);
41
+ }
42
+ }
43
+ exports.default = BrowserCloudAgents;
@@ -21,6 +21,15 @@ import { BaseCommand } from '../../base-command';
21
21
  * code in the cloud (`skrr machines hosted start --repo` is the second, and stays the
22
22
  * right choice when you want a persistent machine instead of a one-shot job).
23
23
  *
24
+ * ── `--carry-session` ──────────────────────────────────────────────────────
25
+ *
26
+ * The transcript CAN travel once the engine implements the session-export
27
+ * contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
28
+ * `sky-code session export <id> --format json` writes a versioned envelope to
29
+ * stdout. This flag runs exactly that contract and folds the export into the
30
+ * prompt — the endpoint's only carrier. An engine that predates the contract
31
+ * is a clean refusal, never a silent fallback to brief-only.
32
+ *
24
33
  * ── The safety gate is the point ───────────────────────────────────────────
25
34
  *
26
35
  * The runtime CLONES FROM GITHUB at `--base`. Uncommitted or unpushed work is
@@ -38,6 +47,8 @@ export default class CodeHandover extends BaseCommand {
38
47
  base: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
39
48
  'allow-dirty': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
40
49
  browser: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
50
+ 'carry-session': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
51
+ session: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
41
52
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
42
53
  };
43
54
  /** Run git, returning null rather than throwing — every caller has a fallback. */
@@ -6,6 +6,7 @@ const node_crypto_1 = require("node:crypto");
6
6
  const data_provider_1 = require("@skrr-ai/data-provider");
7
7
  const base_command_1 = require("../../base-command");
8
8
  const code_handover_1 = require("../../lib/code-handover");
9
+ const first_party_harness_1 = require("../../lib/first-party-harness");
9
10
  /**
10
11
  * `skrr code handover` — hand the work in this directory to an agent running on
11
12
  * a cloud runtime.
@@ -28,6 +29,15 @@ const code_handover_1 = require("../../lib/code-handover");
28
29
  * code in the cloud (`skrr machines hosted start --repo` is the second, and stays the
29
30
  * right choice when you want a persistent machine instead of a one-shot job).
30
31
  *
32
+ * ── `--carry-session` ──────────────────────────────────────────────────────
33
+ *
34
+ * The transcript CAN travel once the engine implements the session-export
35
+ * contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
36
+ * `sky-code session export <id> --format json` writes a versioned envelope to
37
+ * stdout. This flag runs exactly that contract and folds the export into the
38
+ * prompt — the endpoint's only carrier. An engine that predates the contract
39
+ * is a clean refusal, never a silent fallback to brief-only.
40
+ *
31
41
  * ── The safety gate is the point ───────────────────────────────────────────
32
42
  *
33
43
  * The runtime CLONES FROM GITHUB at `--base`. Uncommitted or unpushed work is
@@ -36,11 +46,13 @@ const code_handover_1 = require("../../lib/code-handover");
36
46
  * the pull request. This refuses by default and says what to do.
37
47
  */
38
48
  class CodeHandover extends base_command_1.BaseCommand {
39
- static description = 'Hand the work in this directory to an agent on a cloud runtime';
49
+ static description = 'Hand the work in this directory to an agent on a cloud runtime ' +
50
+ '(--carry-session also carries the engine session transcript, when the engine supports export)';
40
51
  static examples = [
41
52
  '<%= config.bin %> code handover --to "Frontend Agent" --brief "finish the bundle-size fix"',
42
53
  '<%= config.bin %> code handover --to agent_abc --brief "continue the parser refactor" --base main',
43
54
  '<%= config.bin %> code handover --to "Engineer" --brief "..." --allow-dirty --json',
55
+ '<%= config.bin %> code handover --to "Engineer" --brief "..." --carry-session --session <engine-session-id>',
44
56
  ];
45
57
  static flags = {
46
58
  to: core_1.Flags.string({
@@ -64,6 +76,17 @@ class CodeHandover extends base_command_1.BaseCommand {
64
76
  browser: core_1.Flags.boolean({
65
77
  description: 'Give the receiving agent a browser. Refused where the deployment has no browser runtime, never run without one.',
66
78
  }),
79
+ 'carry-session': core_1.Flags.boolean({
80
+ description: 'Export the engine session transcript (`sky-code session export`) and carry it into the ' +
81
+ 'handover. Opt-in because carrying a whole session is a cost the user should choose. ' +
82
+ 'Refused outright when the engine predates the export contract — there is no silent ' +
83
+ 'fallback to brief-only.',
84
+ default: false,
85
+ }),
86
+ session: core_1.Flags.string({
87
+ description: 'Engine session id to export with --carry-session (default: the session id the engine ' +
88
+ 'exports to its environment — SKRR_CODE_SESSION_ID or SKY_CODE_SESSION_ID)',
89
+ }),
67
90
  json: core_1.Flags.boolean({ description: 'Output the created task as JSON' }),
68
91
  };
69
92
  /** Run git, returning null rather than throwing — every caller has a fallback. */
@@ -105,6 +128,78 @@ class CodeHandover extends base_command_1.BaseCommand {
105
128
  if (!safety.safe && !flags['allow-dirty']) {
106
129
  this.error((0, code_handover_1.formatUnsafeHandover)(gitState, safety));
107
130
  }
131
+ if (flags.session && !flags['carry-session']) {
132
+ this.warn('--session has no effect without --carry-session.');
133
+ }
134
+ // ── Optional: carry the engine session. A refused export is terminal —
135
+ // the flag promised the transcript would travel, so dispatching without it
136
+ // would silently deliver a different product than the one asked for. ────
137
+ let sessionExport = null;
138
+ let carriedSessionId = null;
139
+ if (flags['carry-session']) {
140
+ const { path: engineBin } = (0, first_party_harness_1.resolveEngine)();
141
+ if (!engineBin)
142
+ this.error((0, first_party_harness_1.notInstalledMessage)());
143
+ // `--session` wins; otherwise the variable the engine stamps into
144
+ // processes it spawns (current namespace, then the legacy spelling).
145
+ const sessionId = flags.session ||
146
+ process.env.SKRR_CODE_SESSION_ID?.trim() ||
147
+ process.env.SKY_CODE_SESSION_ID?.trim();
148
+ if (!sessionId) {
149
+ this.error('--carry-session needs the engine session to carry. Pass --session <engine-session-id>, ' +
150
+ 'or run inside a session that exports SKRR_CODE_SESSION_ID.');
151
+ }
152
+ carriedSessionId = sessionId;
153
+ let stdout;
154
+ try {
155
+ stdout = (0, node_child_process_1.execFileSync)(engineBin, ['session', 'export', sessionId, '--format', 'json'], {
156
+ encoding: 'utf8',
157
+ stdio: ['ignore', 'pipe', 'pipe'],
158
+ maxBuffer: 64 * 1024 * 1024,
159
+ });
160
+ }
161
+ catch (err) {
162
+ const e = err;
163
+ const failure = (0, code_handover_1.classifySessionExportError)(e);
164
+ if (failure.kind === 'unknown-session') {
165
+ this.error(`No engine session "${sessionId}" — the engine found nothing to carry.\n` +
166
+ ` ${failure.detail}`);
167
+ }
168
+ this.error('This engine does not support session export — --carry-session requires a sky-code ' +
169
+ 'engine implementing the session-export contract ' +
170
+ '(docs/architecture/sky-code-session-export-contract-2026-08-21.md).\n' +
171
+ ` ${failure.detail}\n` +
172
+ ' Re-run without --carry-session to hand over the situation only, or upgrade the engine.');
173
+ }
174
+ const envelope = (0, code_handover_1.parseSessionExportEnvelope)(stdout);
175
+ if (!envelope) {
176
+ // Exit 0 but no contract envelope is a deviation, not a success: the
177
+ // one shape this side may depend on (C2) did not arrive, so attaching
178
+ // the output anyway would be guessing with someone else's session.
179
+ this.error('The engine ran `session export` but did not produce the contract envelope ' +
180
+ '({version, sessionId, messages[], truncated}) — this build predates or deviates from ' +
181
+ 'the export contract, so nothing was attached.');
182
+ }
183
+ // The endpoint's prompt cap is 50000 chars; the transcript gets what the
184
+ // brief and the git context leave behind.
185
+ const budget = 50000 -
186
+ (0, code_handover_1.composeHandoverPrompt)({
187
+ brief: flags.brief,
188
+ repoFullName,
189
+ branch,
190
+ headSha: this.git(['rev-parse', '--short', 'HEAD']),
191
+ recentCommits: this.git(['log', '--oneline', '-5']),
192
+ localWorkNotCarried: safety.localWorkNotCarried,
193
+ }).length -
194
+ 128;
195
+ sessionExport = (0, code_handover_1.renderSessionTranscript)(envelope, Math.max(0, budget));
196
+ if (sessionExport.totalCount > 0 && sessionExport.carriedCount === 0) {
197
+ // A "carried" block containing zero messages is a lie about what moved.
198
+ // The brief --brief plus git context already consumed the prompt budget.
199
+ this.error('The session transcript does not fit the 50000-character task prompt alongside this ' +
200
+ 'brief — nothing would be carried. Shorten --brief, or re-run without --carry-session.');
201
+ }
202
+ }
108
203
  // ── Target agent: id or name, from the agents this user can see. ────────
109
204
  let agents = [];
110
205
  try {
@@ -145,6 +240,7 @@ class CodeHandover extends base_command_1.BaseCommand {
145
240
  headSha: this.git(['rev-parse', '--short', 'HEAD']),
146
241
  recentCommits: this.git(['log', '--oneline', '-5']),
147
242
  localWorkNotCarried: safety.localWorkNotCarried,
243
+ sessionTranscript: sessionExport?.text,
148
244
  });
149
245
  let result;
150
246
  try {
@@ -173,6 +269,14 @@ class CodeHandover extends base_command_1.BaseCommand {
173
269
  const url = task.previewUrl ?? task.prUrl;
174
270
  if (url)
175
271
  this.log(` ${String(url)}`);
272
+ if (sessionExport) {
273
+ const qualifiers = [
274
+ sessionExport.engineTruncated ? 'engine-truncated' : null,
275
+ sessionExport.clipped ? 'clipped to fit' : null,
276
+ ].filter(Boolean);
277
+ this.log(` session ${carriedSessionId} — ${sessionExport.carriedCount}/${sessionExport.totalCount} ` +
278
+ `messages carried${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`);
279
+ }
176
280
  if (safety.localWorkNotCarried) {
177
281
  this.log(' note local uncommitted/unpushed work was not carried over.');
178
282
  }
@@ -0,0 +1,29 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * `skrr commitments handover` — rebind a commitment's executor to a different
4
+ * agent.
5
+ *
6
+ * `commitment.agentId` is single-valued by design, so this is NOT a patch: the
7
+ * server records the move on the commitment's audit trail (`executorHandovers`)
8
+ * with who initiated it and why (impl contract B4, design doc P4). The
9
+ * canonical case is a commitment whose agent's execution targets are all
10
+ * quota-blocked — this is the "reassign" half of "executor cannot run until
11
+ * <date> — reassign or add a fallback target".
12
+ *
13
+ * Only the WHO moves: the contract, cadence, watched sources and audit trail
14
+ * are unchanged, and the new executor's own execution policy decides WHERE the
15
+ * next check runs.
16
+ */
17
+ export default class CommitmentsHandover extends BaseCommand {
18
+ static description: string;
19
+ static examples: string[];
20
+ static args: {
21
+ id: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
22
+ };
23
+ static flags: {
24
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
25
+ to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
+ };
28
+ run(): Promise<void>;
29
+ }
@@ -0,0 +1,99 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
+ const base_command_1 = require("../../base-command");
6
+ const commitments_1 = require("../../lib/commitments");
7
+ const web_url_1 = require("../../lib/web-url");
8
+ const code_handover_1 = require("../../lib/code-handover");
9
+ /**
10
+ * `skrr commitments handover` — rebind a commitment's executor to a different
11
+ * agent.
12
+ *
13
+ * `commitment.agentId` is single-valued by design, so this is NOT a patch: the
14
+ * server records the move on the commitment's audit trail (`executorHandovers`)
15
+ * with who initiated it and why (impl contract B4, design doc P4). The
16
+ * canonical case is a commitment whose agent's execution targets are all
17
+ * quota-blocked — this is the "reassign" half of "executor cannot run until
18
+ * <date> — reassign or add a fallback target".
19
+ *
20
+ * Only the WHO moves: the contract, cadence, watched sources and audit trail
21
+ * are unchanged, and the new executor's own execution policy decides WHERE the
22
+ * next check runs.
23
+ */
24
+ class CommitmentsHandover extends base_command_1.BaseCommand {
25
+ static description = 'Reassign a commitment to a different executor agent';
26
+ static examples = [
27
+ '<%= config.bin %> commitments handover <commitment-id> --to "Backup Agent"',
28
+ '<%= config.bin %> commitments handover <id> --to agent_abc --reason "claude quota exhausted until Sep 19"',
29
+ '<%= config.bin %> commitments handover <id> --to agent_abc --json',
30
+ ];
31
+ static args = {
32
+ id: core_1.Args.string({ description: 'Commitment ID', required: true, ignoreStdin: true }),
33
+ };
34
+ static flags = {
35
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
36
+ to: core_1.Flags.string({
37
+ description: 'Agent that takes over the commitment — id or name',
38
+ required: true,
39
+ }),
40
+ reason: core_1.Flags.string({
41
+ description: 'Why the executor is changing — recorded on the audit trail',
42
+ }),
43
+ };
44
+ async run() {
45
+ this.requireAuth();
46
+ const { args, flags } = await this.parse(CommitmentsHandover);
47
+ // ── Target agent: id or name, resolved like `code handover --to`. ──────
48
+ let agents = [];
49
+ try {
50
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
51
+ agents = (resp?.data ?? []).filter(Boolean);
52
+ }
53
+ catch (err) {
54
+ this.handleApiError(err);
55
+ }
56
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
57
+ if (matched.kind === 'none') {
58
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
59
+ }
60
+ if (matched.kind === 'ambiguous') {
61
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
62
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
63
+ }
64
+ const target = matched.agent;
65
+ let result;
66
+ try {
67
+ result = await commitments_1.commitmentApi.setExecutor(args.id, {
68
+ agentId: target.id,
69
+ ...(flags.reason ? { reason: flags.reason } : {}),
70
+ });
71
+ }
72
+ catch (err) {
73
+ this.handleApiError(err, { subject: `Commitment ${args.id}` });
74
+ }
75
+ if (flags.json) {
76
+ this.log(JSON.stringify(result, null, 2));
77
+ return;
78
+ }
79
+ const agentLabel = target.name ? `${target.name} (${target.id})` : target.id;
80
+ this.log(`Handed commitment ${args.id} over to ${agentLabel}`);
81
+ // The audit entry the server appended — echo it so "from whom" is answered
82
+ // without a second call. Absent on a server that predates the trail.
83
+ const commitment = result?.commitment ??
84
+ result;
85
+ const handovers = commitment?.executorHandovers;
86
+ if (Array.isArray(handovers) && handovers.length > 0) {
87
+ const last = handovers[handovers.length - 1];
88
+ if (typeof last?.fromAgentId === 'string' && typeof last?.toAgentId === 'string') {
89
+ this.log(` executor ${last.fromAgentId} → ${last.toAgentId}`);
90
+ }
91
+ }
92
+ if (commitment) {
93
+ const withUrl = (0, web_url_1.withCommitmentUrl)(this.cliConfig.baseURL, commitment);
94
+ if (withUrl.url)
95
+ this.log(` ${withUrl.url}`);
96
+ }
97
+ }
98
+ }
99
+ exports.default = CommitmentsHandover;
@@ -14,7 +14,7 @@ const agent_resolver_1 = require("../../lib/agent-resolver");
14
14
  * obligations and start reasoning about them.
15
15
  */
16
16
  class FollowupsList extends base_command_1.BaseCommand {
17
- static description = 'List follow-ups — armed, proposed, paused, or settled';
17
+ static description = 'List follow-ups — armed, proposed, paused, settling, or settled';
18
18
  static examples = [
19
19
  '<%= config.bin %> followups list',
20
20
  '<%= config.bin %> followups list --agent agent_1 --kind watch --state armed',
@@ -30,6 +30,7 @@ class FollowupsWatch extends base_command_1.BaseCommand {
30
30
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy still running" --url https://ci.example.com/api/build/9 --until \'{"path":"status","op":"equals","value":"done"}\' --deadline 6h',
31
31
  '<%= config.bin %> followups watch --subject task:OSK-42 --why "pr review" --check "gh pr view 42 --json state" --success "state == MERGED" --abort "state == CLOSED" --wait event --wait-entity github --wait-event pull_request_merged --wait-fallback 2h',
32
32
  '<%= config.bin %> followups watch --subject conversation:abc --why "vendor reply" --check "check inbox" --success "reply arrived" --abort "3 days" --wait clock --wait-at 2026-09-20T09:00:00Z',
33
+ '<%= config.bin %> followups watch --subject task:OSK-42 --why "deploy tail" --check "release-status says live" --success "live" --abort "6h" --wait clock --wait-at 45m',
33
34
  ];
34
35
  static flags = {
35
36
  subject: core_1.Flags.string({ description: 'Subject ref: type:externalKey', required: true }),
@@ -41,9 +42,15 @@ class FollowupsWatch extends base_command_1.BaseCommand {
41
42
  description: 'Predicate tree as JSON (predicate form — the server evaluates it; no agent turn per check): a leaf {"path","op","value"} or {"all"|"any": [...]}. op is equals, not_equals, contains, starts_with, in, gt, lt, exists, … — an unknown op is refused with the full list',
42
43
  }),
43
44
  grant: core_1.Flags.string({ description: 'Grant id supplying the watch credentials' }),
44
- deadline: core_1.Flags.string({ description: 'When the watch must stop (predicate form, required)' }),
45
- 'check-every': core_1.Flags.string({ description: 'Observation interval (predicate form)' }),
46
- 'max-checks': core_1.Flags.integer({ description: 'Observation cap (predicate form)' }),
45
+ deadline: core_1.Flags.string({
46
+ description: 'When the watch must stop (predicate form, required — refused on a recipe watch, which ends through its --wait)',
47
+ }),
48
+ 'check-every': core_1.Flags.string({
49
+ description: 'Observation interval (predicate form — refused on a recipe watch)',
50
+ }),
51
+ 'max-checks': core_1.Flags.integer({
52
+ description: 'Observation cap (predicate form — refused on a recipe watch)',
53
+ }),
47
54
  'root-path': core_1.Flags.string({ description: 'JSON root path inside the response' }),
48
55
  'resume-prompt': core_1.Flags.string({ description: 'What to tell the settling turn' }),
49
56
  // recipe form
@@ -53,10 +60,15 @@ class FollowupsWatch extends base_command_1.BaseCommand {
53
60
  success: core_1.Flags.string({ description: 'What state means the wait resolved (recipe form)' }),
54
61
  abort: core_1.Flags.string({ description: 'When to stop and escalate (recipe form)' }),
55
62
  wait: core_1.Flags.string({
56
- description: 'Wait kind (recipe form)',
63
+ description: 'Wait kind (recipe form, required)',
57
64
  options: [...followups_1.WATCH_WAIT_KINDS],
58
65
  }),
59
- 'wait-at': core_1.Flags.string({ description: 'Instant/duration for --wait clock' }),
66
+ 'wait-at': core_1.Flags.string({
67
+ // Naming both shapes here and in the server's rejection, because the
68
+ // duration grammar is a shorthand and not ISO-8601: `PT45M` is refused
69
+ // (OSK-10392).
70
+ description: 'When --wait clock fires: an ISO-8601 instant, or a duration from now (45m, 2h, 3d, 1w)',
71
+ }),
60
72
  'wait-entity': core_1.Flags.string({ description: 'Entity type for --wait event' }),
61
73
  'wait-event': core_1.Flags.string({ description: 'Event type for --wait event' }),
62
74
  'wait-filter-field': core_1.Flags.string({ description: 'Payload field for an event filter' }),
@@ -161,6 +173,19 @@ class FollowupsWatch extends base_command_1.BaseCommand {
161
173
  });
162
174
  }
163
175
  }
176
+ const boundsRefusal = (0, followups_1.recipeWatchBoundsRefusal)(flags.wait, {
177
+ deadline: flags.deadline,
178
+ checkEvery: flags['check-every'],
179
+ maxChecks: flags['max-checks'],
180
+ });
181
+ if (boundsRefusal) {
182
+ this.failWithCliError({
183
+ message: boundsRefusal,
184
+ code: 'FOLLOWUP_INVALID_FIELD',
185
+ exit: 2,
186
+ retryable: false,
187
+ });
188
+ }
164
189
  const wait = { kind: flags.wait };
165
190
  if (flags.wait === 'clock') {
166
191
  wait.at = flags['wait-at'];
@@ -188,6 +213,16 @@ class FollowupsWatch extends base_command_1.BaseCommand {
188
213
  success: flags.success,
189
214
  abort: flags.abort,
190
215
  wait,
216
+ // Forwarded so the SERVER refuses them, rather than dropped here so
217
+ // nobody does (OSK-10407). These are predicate-only bounds; this branch
218
+ // used to assemble the five fields above and silently discard the rest,
219
+ // so `--deadline 7d` armed a watch and reported success having stored
220
+ // no deadline. The rule that answers now lives once, in
221
+ // `normalizeFollowUpInput`, so the CLI and the agent-facing paths
222
+ // cannot drift into disagreeing about which form owns a flag.
223
+ ...(flags.deadline !== undefined ? { deadline: flags.deadline } : {}),
224
+ ...(flags['check-every'] !== undefined ? { checkEvery: flags['check-every'] } : {}),
225
+ ...(flags['max-checks'] !== undefined ? { maxChecks: flags['max-checks'] } : {}),
191
226
  });
192
227
  }
193
228
  let response;
@@ -15,6 +15,7 @@ export default class InitiativesList extends BaseCommand {
15
15
  state: import("@oclif/core/lib/interfaces").OptionFlag<string[] | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
16
16
  pending: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
17
17
  limit: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ cursor: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
18
19
  };
19
20
  run(): Promise<void>;
20
21
  }