@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
@@ -29,6 +29,7 @@ class InitiativesList extends base_command_1.BaseCommand {
29
29
  description: 'Only proposals still awaiting a decision (detected, proposed, waiting)',
30
30
  }),
31
31
  limit: core_1.Flags.integer({ description: 'Max results (1..100)', min: 1, max: 100 }),
32
+ cursor: core_1.Flags.string({ description: 'Pagination cursor from a previous page' }),
32
33
  };
33
34
  async run() {
34
35
  this.requireAuth();
@@ -40,6 +41,7 @@ class InitiativesList extends base_command_1.BaseCommand {
40
41
  ...(flags.agent ? { agentId: flags.agent } : {}),
41
42
  ...(states?.length ? { state: states.join(',') } : {}),
42
43
  ...(flags.limit ? { limit: flags.limit } : {}),
44
+ ...(flags.cursor ? { cursor: flags.cursor } : {}),
43
45
  });
44
46
  }
45
47
  catch (err) {
@@ -49,7 +51,7 @@ class InitiativesList extends base_command_1.BaseCommand {
49
51
  this.log(JSON.stringify(response, null, 2));
50
52
  return;
51
53
  }
52
- const rows = response.data ?? [];
54
+ const rows = response.initiatives ?? [];
53
55
  if (rows.length === 0) {
54
56
  this.log(flags.pending ? 'Nothing is waiting on you.' : 'No initiatives.');
55
57
  return;
@@ -63,7 +65,14 @@ class InitiativesList extends base_command_1.BaseCommand {
63
65
  }
64
66
  }
65
67
  this.log('');
66
- this.log(`${rows.length} shown. Decide one with: skrr initiatives decide <id> --approve`);
68
+ const active = response.counts?.active;
69
+ const shown = typeof active === 'number' && flags.pending
70
+ ? `${rows.length} shown of ${active} awaiting a decision.`
71
+ : `${rows.length} shown.`;
72
+ this.log(`${shown} Decide one with: skrr initiatives decide <id> --approve`);
73
+ if (response.nextCursor) {
74
+ this.log(`Next page: --cursor ${response.nextCursor}`);
75
+ }
67
76
  }
68
77
  }
69
78
  exports.default = InitiativesList;
@@ -1,4 +1,6 @@
1
+ import { type SpaceLabelUsageCounts } from '@skrr-ai/data-provider';
1
2
  import { BaseCommand } from '../../base-command';
3
+ import { type LabelRow } from '../../lib/label-ref';
2
4
  /**
3
5
  * `labels list` — every label you can actually USE, by default.
4
6
  *
@@ -21,7 +23,32 @@ import { BaseCommand } from '../../base-command';
21
23
  * The merge itself is `lib/label-ref.collectLabelVocabulary` — the same
22
24
  * function `--label <name>` resolves through. A separate copy here is how the
23
25
  * name you can type and the name you can see start to disagree.
26
+ *
27
+ * `--space` answers the OTHER question people ask of a vocabulary: not "what
28
+ * may I use" but "what do we actually use HERE". It orders the list by how many
29
+ * of that Space's tasks carry each label and prints the count, which is the
30
+ * same ordering the web picker applies when it is opened inside a Space — one
31
+ * comparator (`orderLabelsBySpaceUsage`), so the two surfaces cannot disagree
32
+ * about which label leads.
33
+ */
34
+ /** One printable row per label, in the order the reader should see them. */
35
+ export interface LabelListRow extends Record<string, string> {
36
+ id: string;
37
+ name: string;
38
+ scope: string;
39
+ color: string;
40
+ appliesTo: string;
41
+ used: string;
42
+ inSpace: string;
43
+ state: string;
44
+ }
45
+ /**
46
+ * Order the vocabulary for display and flatten it into table rows.
47
+ *
48
+ * Exported so the ordering can be tested without a command harness, and so the
49
+ * JSON and table outputs cannot drift into two different orders.
24
50
  */
51
+ export declare function labelListRows(items: LabelRow[], spaceUsage?: SpaceLabelUsageCounts): LabelListRow[];
25
52
  export default class LabelsList extends BaseCommand {
26
53
  static description: string;
27
54
  static examples: string[];
@@ -29,6 +56,7 @@ export default class LabelsList extends BaseCommand {
29
56
  scope: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
30
57
  'applicable-to': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
31
58
  'include-archived': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
59
+ space: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
32
60
  'scope-id': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
33
61
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
34
62
  };
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.labelListRows = labelListRows;
3
4
  const core_1 = require("@oclif/core");
4
5
  const data_provider_1 = require("@skrr-ai/data-provider");
5
6
  const base_command_1 = require("../../base-command");
@@ -7,28 +8,29 @@ const format_1 = require("../../lib/format");
7
8
  const label_scope_1 = require("../../lib/label-scope");
8
9
  const label_ref_1 = require("../../lib/label-ref");
9
10
  /**
10
- * `labels list` — every label you can actually USE, by default.
11
+ * Order the vocabulary for display and flatten it into table rows.
11
12
  *
12
- * This used to default to `--scope user` and show one scope at a time, which
13
- * is a faithful rendering of `GET /api/labels` and a poor answer to the
14
- * question people ask. `scope` is an OWNERSHIP boundary, not a filter anyone
15
- * browses by: a workspace-scoped label is attachable by every member, so
16
- * leaving it out of the default view means the vocabulary you are told about
17
- * is smaller than the one you have.
18
- *
19
- * It had a measurable cost. Three workspace-scoped labels existed and were
20
- * invisible to the bare command, so nobody knew they were there — which is
21
- * part of why the label system shipped complete and went unused, and why a
22
- * second `dogfood` nearly got created beside one that already existed.
23
- *
24
- * `--scope` still narrows to exactly one scope, so every explicit invocation
25
- * behaves as it always did. Only the bare command changed, and it changed to
26
- * a superset.
27
- *
28
- * The merge itself is `lib/label-ref.collectLabelVocabulary` — the same
29
- * function `--label <name>` resolves through. A separate copy here is how the
30
- * name you can type and the name you can see start to disagree.
13
+ * Exported so the ordering can be tested without a command harness, and so the
14
+ * JSON and table outputs cannot drift into two different orders.
31
15
  */
16
+ function labelListRows(items, spaceUsage) {
17
+ return (0, data_provider_1.orderLabelsBySpaceUsage)(items, spaceUsage).map((l) => ({
18
+ id: String(l.id ?? '-'),
19
+ name: String(l.name ?? '-'),
20
+ // Where it lives decides who else sees it, and it is the one column the
21
+ // old single-scope view never had to show.
22
+ scope: String(l.scope ?? '-'),
23
+ color: String(l.color ?? '-'),
24
+ appliesTo: Array.isArray(l.applicableTo) ? l.applicableTo.join(',') : '-',
25
+ used: String(l.usageCount ?? 0),
26
+ // `USED` is the global counter; `IN SPACE` is this Space's own tasks.
27
+ // Printed side by side because the gap between them is the finding: a
28
+ // label used 30 times across the workspace and 0 times here is not this
29
+ // Space's vocabulary, however alive it looks everywhere else.
30
+ inSpace: String(spaceUsage?.get(String(l.id)) ?? 0),
31
+ state: l.archivedAt ? 'archived' : 'active',
32
+ }));
33
+ }
32
34
  class LabelsList extends base_command_1.BaseCommand {
33
35
  static description = 'List labels — every scope you can use, unless --scope narrows it';
34
36
  static examples = [
@@ -36,6 +38,7 @@ class LabelsList extends base_command_1.BaseCommand {
36
38
  '<%= config.bin %> labels list --applicable-to space',
37
39
  '<%= config.bin %> labels list --scope user',
38
40
  '<%= config.bin %> labels list --scope workspace --scope-id <workspace-id>',
41
+ '<%= config.bin %> labels list --space "Dogfood: Space navigation"',
39
42
  '<%= config.bin %> labels list --include-archived --json',
40
43
  ];
41
44
  static flags = {
@@ -52,6 +55,9 @@ class LabelsList extends base_command_1.BaseCommand {
52
55
  options: [...label_scope_1.LABEL_APPLICABLE],
53
56
  }),
54
57
  'include-archived': core_1.Flags.boolean({ description: 'Include archived labels' }),
58
+ space: core_1.Flags.string({
59
+ description: 'Order by how often this Space uses each label, and show the count',
60
+ }),
55
61
  };
56
62
  async run() {
57
63
  this.requireAuth();
@@ -83,8 +89,29 @@ class LabelsList extends base_command_1.BaseCommand {
83
89
  this.error(err.message, { exit: 1 });
84
90
  this.handleApiError(err);
85
91
  }
92
+ /*
93
+ * Per-Space usage, when asked for. A failure here must not take the list
94
+ * with it: the unordered vocabulary is still the answer to the question,
95
+ * and refusing to print it because a secondary count could not be read
96
+ * would turn a degraded answer into no answer.
97
+ */
98
+ let spaceUsage;
99
+ if (flags.space) {
100
+ try {
101
+ spaceUsage = (0, data_provider_1.spaceLabelUsageCounts)(await data_provider_1.dataService.getSpaceLabelUsage(flags.space));
102
+ }
103
+ catch (err) {
104
+ this.warn(`Could not read label usage for this Space; showing the stored order. ${err instanceof Error ? err.message : String(err)}`);
105
+ }
106
+ }
86
107
  if (flags.json) {
87
- this.log(JSON.stringify({ data: items }, null, 2));
108
+ const ordered = (0, data_provider_1.orderLabelsBySpaceUsage)(items, spaceUsage);
109
+ this.log(JSON.stringify({
110
+ data: spaceUsage
111
+ ? ordered.map((l) => ({ ...l, spaceUsageCount: spaceUsage?.get(String(l.id)) ?? 0 }))
112
+ : ordered,
113
+ ...(flags.space ? { spaceId: flags.space } : {}),
114
+ }, null, 2));
88
115
  return;
89
116
  }
90
117
  if (items.length === 0) {
@@ -93,17 +120,7 @@ class LabelsList extends base_command_1.BaseCommand {
93
120
  : 'No labels you can use. Create one with `labels create --name <name> --applicable-to <entity>`.');
94
121
  return;
95
122
  }
96
- const rows = items.map((l) => ({
97
- id: String(l.id ?? '-'),
98
- name: String(l.name ?? '-'),
99
- // Where it lives decides who else sees it, and it is the one column the
100
- // old single-scope view never had to show.
101
- scope: String(l.scope ?? '-'),
102
- color: String(l.color ?? '-'),
103
- appliesTo: Array.isArray(l.applicableTo) ? l.applicableTo.join(',') : '-',
104
- used: String(l.usageCount ?? 0),
105
- state: l.archivedAt ? 'archived' : 'active',
106
- }));
123
+ const rows = labelListRows(items, spaceUsage);
107
124
  (0, format_1.renderTable)(rows, [
108
125
  { key: 'id', header: 'ID' },
109
126
  { key: 'name', header: 'NAME', maxWidth: 32 },
@@ -111,6 +128,7 @@ class LabelsList extends base_command_1.BaseCommand {
111
128
  { key: 'color', header: 'COLOR' },
112
129
  { key: 'appliesTo', header: 'APPLIES TO' },
113
130
  { key: 'used', header: 'USED' },
131
+ ...(spaceUsage ? [{ key: 'inSpace', header: 'IN SPACE' }] : []),
114
132
  { key: 'state', header: 'STATE' },
115
133
  ], (line) => this.log(line));
116
134
  }
@@ -0,0 +1,32 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * `skrr tasks fork` — race a second attempt at the same work (B8).
4
+ *
5
+ * The line against `tasks handover`: a handover CONTINUES the work once, from a
6
+ * finished run. A fork runs a SIBLING attempt from the same Resume Point while
7
+ * the original is still live — both in one `parallel.groupId`, neither
8
+ * canonical until `tasks promote` picks one.
9
+ *
10
+ * The shared-workspace hazard is the reason this is not a flag on handover. Two
11
+ * executors of the same target class can be writing one workspace, and the
12
+ * server refuses (`shared_workspace_ack_required`) until the caller says
13
+ * plainly that it knows. `--acknowledge-shared-workspace` is that sentence;
14
+ * it is deliberately long to type.
15
+ */
16
+ export default class TasksFork extends BaseCommand {
17
+ static description: string;
18
+ static examples: string[];
19
+ static args: {
20
+ id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
21
+ };
22
+ static flags: {
23
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
24
+ task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
25
+ to: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
26
+ harness: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
27
+ 'from-run': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
28
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
29
+ 'acknowledge-shared-workspace': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
30
+ };
31
+ run(): Promise<void>;
32
+ }
@@ -0,0 +1,171 @@
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 code_handover_1 = require("../../lib/code-handover");
7
+ const task_extras_1 = require("../../lib/task-extras");
8
+ const task_resolver_1 = require("../../lib/task-resolver");
9
+ /**
10
+ * `skrr tasks fork` — race a second attempt at the same work (B8).
11
+ *
12
+ * The line against `tasks handover`: a handover CONTINUES the work once, from a
13
+ * finished run. A fork runs a SIBLING attempt from the same Resume Point while
14
+ * the original is still live — both in one `parallel.groupId`, neither
15
+ * canonical until `tasks promote` picks one.
16
+ *
17
+ * The shared-workspace hazard is the reason this is not a flag on handover. Two
18
+ * executors of the same target class can be writing one workspace, and the
19
+ * server refuses (`shared_workspace_ack_required`) until the caller says
20
+ * plainly that it knows. `--acknowledge-shared-workspace` is that sentence;
21
+ * it is deliberately long to type.
22
+ */
23
+ class TasksFork extends base_command_1.BaseCommand {
24
+ static description = 'Race a sibling run of a task from the same Resume Point (parallel attempt, B8)';
25
+ static examples = [
26
+ '<%= config.bin %> tasks fork <task-id> --to "Codex Agent"',
27
+ '<%= config.bin %> tasks fork <task-id> --to agent_abc --harness agent-worker',
28
+ '<%= config.bin %> tasks fork <task-id> --from-run <run-id> --acknowledge-shared-workspace',
29
+ ];
30
+ static args = {
31
+ id: core_1.Args.string({
32
+ description: 'Task ID; defaults to OVERSKY_SESSION_TASK_ID in task runtimes',
33
+ required: false,
34
+ ignoreStdin: true,
35
+ }),
36
+ };
37
+ static flags = {
38
+ json: core_1.Flags.boolean({ description: 'Output the {run, packet, groupId} response as JSON' }),
39
+ task: core_1.Flags.string({ description: 'Task ID; overrides daemon runtime context' }),
40
+ to: core_1.Flags.string({
41
+ description: 'Agent for the branch — id or name. Default: the source run’s own agent.',
42
+ }),
43
+ harness: core_1.Flags.string({
44
+ description: 'Execution target for the branch. Default: the target agent’s own policy chain.',
45
+ }),
46
+ 'from-run': core_1.Flags.string({
47
+ description: 'Run to branch from (default: the task’s most recent run)',
48
+ }),
49
+ reason: core_1.Flags.string({ description: 'Why a second attempt — recorded in the packet' }),
50
+ 'acknowledge-shared-workspace': core_1.Flags.boolean({
51
+ description: 'Proceed when the branch resolves to the same execution-target class as the source, ' +
52
+ 'where two executors can write one workspace. Required by the server in that case.',
53
+ default: false,
54
+ }),
55
+ };
56
+ async run() {
57
+ this.requireAuth();
58
+ const { args, flags } = await this.parse(TasksFork);
59
+ let taskIdentity;
60
+ try {
61
+ taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({ positionalId: args.id, flagTask: flags.task });
62
+ }
63
+ catch (err) {
64
+ const failure = (0, task_resolver_1.taskResolutionFailure)(err);
65
+ this.failWithCliError({
66
+ message: failure.message,
67
+ code: failure.code,
68
+ exit: 2,
69
+ retryable: false,
70
+ details: { suggestion: failure.suggestion, context: failure.details },
71
+ });
72
+ }
73
+ const taskId = taskIdentity.id;
74
+ // The branch agent is OPTIONAL — a fork onto the same agent on a different
75
+ // target is the "try it on Codex too" case, and the server derives the
76
+ // agent from the source run when none is named.
77
+ let targetAgentId;
78
+ if (flags.to) {
79
+ let agents = [];
80
+ try {
81
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
82
+ agents = (resp?.data ?? []).filter(Boolean);
83
+ }
84
+ catch (err) {
85
+ this.handleApiError(err);
86
+ }
87
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
88
+ if (matched.kind === 'none') {
89
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
90
+ }
91
+ if (matched.kind === 'ambiguous') {
92
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
93
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
94
+ }
95
+ targetAgentId = matched.agent.id;
96
+ }
97
+ let sourceRunId = flags['from-run'] ?? '';
98
+ if (!sourceRunId) {
99
+ let runs = [];
100
+ try {
101
+ const resp = await data_provider_1.dataService.getTaskRuns(taskId);
102
+ runs = (resp?.data ?? []);
103
+ }
104
+ catch (err) {
105
+ this.handleApiError(err);
106
+ }
107
+ const latest = runs[0];
108
+ if (!latest) {
109
+ this.failWithCliError({
110
+ message: `Task ${taskId} has no runs to fork from. Run \`${this.config.bin} tasks runs ${taskId}\` to see the ledger.`,
111
+ code: 'FORK_NO_SOURCE_RUN',
112
+ exit: 2,
113
+ retryable: false,
114
+ });
115
+ }
116
+ sourceRunId = String(latest.runId ?? latest.id ?? '');
117
+ }
118
+ let result;
119
+ try {
120
+ result = await (0, task_extras_1.taskRunFork)(taskId, sourceRunId, {
121
+ ...(targetAgentId ? { targetAgentId } : {}),
122
+ ...(flags.harness ? { executionTargetRef: (0, task_extras_1.parseExecutionTargetRef)(flags.harness) } : {}),
123
+ ...(flags.reason ? { reason: flags.reason } : {}),
124
+ ...(flags['acknowledge-shared-workspace'] ? { acknowledgeSharedWorkspace: true } : {}),
125
+ });
126
+ }
127
+ catch (err) {
128
+ const refusal = (0, base_command_1.apiRefusal)(err);
129
+ if (refusal.code?.toLowerCase() === 'shared_workspace_ack_required') {
130
+ this.failWithCliError({
131
+ message: (refusal.message ??
132
+ 'The branch resolves to the same execution-target class as the source.') +
133
+ '\n → two executors could write one workspace. Re-run with ' +
134
+ '`--acknowledge-shared-workspace` if that is acceptable, or pass `--harness` ' +
135
+ 'to put the branch on a different target class.',
136
+ code: refusal.code,
137
+ exit: 3,
138
+ retryable: false,
139
+ details: refusal.body,
140
+ });
141
+ }
142
+ if (refusal.code?.toLowerCase() === 'target_not_assignee') {
143
+ this.failWithCliError({
144
+ message: (refusal.message ?? `Agent ${flags.to} may not work this task.`) +
145
+ `\n → assign it first: \`${this.config.bin} tasks assign ${taskId} --agent "${flags.to}"\`, then re-run.`,
146
+ code: refusal.code,
147
+ exit: 3,
148
+ retryable: false,
149
+ details: refusal.body,
150
+ });
151
+ }
152
+ this.handleApiError(err, { subject: `Task ${taskId}` });
153
+ }
154
+ if (flags.json) {
155
+ this.log(JSON.stringify(result, null, 2));
156
+ return;
157
+ }
158
+ const r = result;
159
+ const branch = r.run ?? {};
160
+ const branchRunId = String(branch.runId ?? branch.id ?? '?');
161
+ this.log(`Forked run ${sourceRunId} → branch ${branchRunId}`);
162
+ this.log(`Parallel group: ${r.groupId ?? '?'}`);
163
+ const notCarried = Array.isArray(r.packet?.notCarried)
164
+ ? r.packet.notCarried.map(String).filter(Boolean)
165
+ : [];
166
+ this.log(`Not carried: ${notCarried.length ? notCarried.join(', ') : '(nothing declared)'}`);
167
+ this.log(`Both runs are live and neither is canonical. Pick one with ` +
168
+ `\`${this.config.bin} tasks promote ${taskId} --run <run-id>\` — the losers are cancelled.`);
169
+ }
170
+ }
171
+ exports.default = TasksFork;
@@ -0,0 +1,23 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Whether a run row is a legal handover source — exported for tests.
4
+ * `endedAt` is a defensive OR: a projection that stamps a terminal timestamp
5
+ * without a recognized status is still terminal.
6
+ */
7
+ export declare function isHandoverSourceRun(run: Record<string, unknown>): boolean;
8
+ export default class TasksHandover extends BaseCommand {
9
+ static description: string;
10
+ static examples: string[];
11
+ static args: {
12
+ id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
13
+ };
14
+ static flags: {
15
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
+ task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ harness: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'from-run': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ };
22
+ run(): Promise<void>;
23
+ }
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isHandoverSourceRun = isHandoverSourceRun;
4
+ const core_1 = require("@oclif/core");
5
+ const data_provider_1 = require("@skrr-ai/data-provider");
6
+ const base_command_1 = require("../../base-command");
7
+ const code_handover_1 = require("../../lib/code-handover");
8
+ const task_extras_1 = require("../../lib/task-extras");
9
+ const task_resolver_1 = require("../../lib/task-resolver");
10
+ const format_1 = require("../../lib/format");
11
+ /**
12
+ * `skrr tasks handover` — continue a task's work under a different agent or a
13
+ * different execution target, from where a finished run left off.
14
+ *
15
+ * Distinct from `tasks retry` (same executor, same authority, starts the task
16
+ * over) and `tasks assign` (a prerequisite consent change, not the act). A
17
+ * handover admits a NEW run resumed from the source run's Resume Point, so the
18
+ * ledger keeps both halves: the old run as immutable evidence, the new one as
19
+ * the visible continuation.
20
+ *
21
+ * The two axes are independent (docs/architecture/execution-handover-2026-09-17.md
22
+ * §1): `--to` changes WHO runs, `--harness` changes WHERE it runs. The server
23
+ * contract is B1 — `POST /api/tasks/:taskId/runs/:runId/handover`.
24
+ *
25
+ * What does not move is declared, not implied: the response's `packet.notCarried`
26
+ * names what stayed behind (provider session, agent-private memory, local
27
+ * machine state) and is printed verbatim rather than paraphrased.
28
+ */
29
+ /** The run statuses from which a handover can source a Resume Point. */
30
+ const HANDOVER_SOURCE_RUN_STATUSES = new Set([
31
+ 'succeeded',
32
+ 'failed',
33
+ 'cancelled',
34
+ // Contract B1 admits "terminal or parked". `parked` is not a
35
+ // TASK_EXECUTION_RUN_STATUSES value today, but the contract names it — a run
36
+ // parked awaiting runtime is exactly the handover case, so accept the word
37
+ // when a newer server starts sending it.
38
+ 'parked',
39
+ ]);
40
+ /**
41
+ * Whether a run row is a legal handover source — exported for tests.
42
+ * `endedAt` is a defensive OR: a projection that stamps a terminal timestamp
43
+ * without a recognized status is still terminal.
44
+ */
45
+ function isHandoverSourceRun(run) {
46
+ const status = typeof run.status === 'string' ? run.status : '';
47
+ if (HANDOVER_SOURCE_RUN_STATUSES.has(status))
48
+ return true;
49
+ return typeof run.endedAt === 'string' && run.endedAt !== '';
50
+ }
51
+ class TasksHandover extends base_command_1.BaseCommand {
52
+ static description = 'Hand a task run over to a different agent or execution target, resumed from its Resume Point';
53
+ static examples = [
54
+ '<%= config.bin %> tasks handover <task-id> --to "Codex Agent"',
55
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --harness <target-ref>',
56
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --from-run <run-id> --reason "claude quota exhausted until Sep 19"',
57
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --json',
58
+ ];
59
+ static args = {
60
+ id: core_1.Args.string({
61
+ description: 'Task ID; defaults to OVERSKY_SESSION_TASK_ID in task runtimes',
62
+ required: false,
63
+ ignoreStdin: true,
64
+ }),
65
+ };
66
+ static flags = {
67
+ json: core_1.Flags.boolean({ description: 'Output the {run, packet} response as JSON' }),
68
+ task: core_1.Flags.string({ description: 'Task ID; overrides daemon runtime context' }),
69
+ to: core_1.Flags.string({
70
+ description: 'Agent that takes over — id or name',
71
+ required: true,
72
+ }),
73
+ harness: core_1.Flags.string({
74
+ description: 'Execution target ref for the new run. Default: the TARGET agent\u2019s own execution ' +
75
+ 'policy chain — a handover re-runs the chain, it does not inherit the failed run\u2019s ' +
76
+ 'resolved target.',
77
+ }),
78
+ 'from-run': core_1.Flags.string({
79
+ description: 'Source run to resume from (default: the task\u2019s latest terminal or parked run)',
80
+ }),
81
+ reason: core_1.Flags.string({
82
+ description: 'Why the work is moving — recorded in the handover packet provenance',
83
+ }),
84
+ };
85
+ async run() {
86
+ this.requireAuth();
87
+ const { args, flags } = await this.parse(TasksHandover);
88
+ let taskIdentity;
89
+ try {
90
+ taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({ positionalId: args.id, flagTask: flags.task });
91
+ }
92
+ catch (err) {
93
+ const failure = (0, task_resolver_1.taskResolutionFailure)(err);
94
+ this.failWithCliError({
95
+ message: failure.message,
96
+ code: failure.code,
97
+ exit: 2,
98
+ retryable: false,
99
+ details: { suggestion: failure.suggestion, context: failure.details },
100
+ });
101
+ }
102
+ const taskId = taskIdentity.id;
103
+ // ── Target agent: id or name, resolved like `code handover --to`. ──────
104
+ let agents = [];
105
+ try {
106
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
107
+ agents = (resp?.data ?? []).filter(Boolean);
108
+ }
109
+ catch (err) {
110
+ this.handleApiError(err);
111
+ }
112
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
113
+ if (matched.kind === 'none') {
114
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
115
+ }
116
+ if (matched.kind === 'ambiguous') {
117
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
118
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
119
+ }
120
+ const target = matched.agent;
121
+ // ── Source run: --from-run, else the latest terminal/parked run. ───────
122
+ let sourceRun = null;
123
+ if (flags['from-run']) {
124
+ sourceRun = { runId: flags['from-run'] };
125
+ }
126
+ else {
127
+ let runs = [];
128
+ try {
129
+ const resp = await data_provider_1.dataService.getTaskRuns(taskId);
130
+ runs = (resp?.data ?? []);
131
+ }
132
+ catch (err) {
133
+ this.handleApiError(err);
134
+ }
135
+ // `listRuns` is newest-first (admittedAt desc), so the first match IS the
136
+ // latest terminal run.
137
+ sourceRun = runs.find(isHandoverSourceRun) ?? null;
138
+ if (!sourceRun) {
139
+ this.failWithCliError({
140
+ message: `Task ${taskId} has no terminal or parked run to hand over from — a handover ` +
141
+ `resumes from a finished run's Resume Point, and there is nothing to resume. ` +
142
+ `Run \`${this.config.bin} tasks runs ${taskId}\` to see the ledger.`,
143
+ code: 'HANDOVER_NO_SOURCE_RUN',
144
+ exit: 2,
145
+ retryable: false,
146
+ });
147
+ }
148
+ }
149
+ const sourceRunId = String(sourceRun.runId ?? sourceRun.id ?? '');
150
+ // ── B1. ────────────────────────────────────────────────────────────────
151
+ let result;
152
+ try {
153
+ result = await (0, task_extras_1.taskRunHandover)(taskId, sourceRunId, {
154
+ targetAgentId: target.id,
155
+ ...(flags.harness ? { executionTargetRef: (0, task_extras_1.parseExecutionTargetRef)(flags.harness) } : {}),
156
+ ...(flags.reason ? { reason: flags.reason } : {}),
157
+ });
158
+ }
159
+ catch (err) {
160
+ const refusal = (0, base_command_1.apiRefusal)(err);
161
+ // The consent gate, named: the target must already be an assignee —
162
+ // the API never bundles a silent assign (impl contract B1). Turn the
163
+ // refusal into the two-step the operator actually has to run.
164
+ if (refusal.code?.toLowerCase() === 'target_not_assignee') {
165
+ this.failWithCliError({
166
+ message: (refusal.message ?? `Agent ${flags.to} may not work this task.`) +
167
+ `\n → assign it first: \`${this.config.bin} tasks assign ${taskId} --agent "${flags.to}"\` ` +
168
+ `(or \`${this.config.bin} tasks update ${taskId} --add-assignee-agent <agent-id>\`), ` +
169
+ 'then re-run this handover.',
170
+ code: refusal.code,
171
+ exit: 3,
172
+ retryable: false,
173
+ details: refusal.body,
174
+ });
175
+ }
176
+ this.handleApiError(err, { subject: `Task ${taskId}` });
177
+ }
178
+ if (flags.json) {
179
+ // {run, packet} verbatim — the packet IS the audit record, so a scripted
180
+ // caller gets exactly what the server stamped.
181
+ this.log(JSON.stringify(result, null, 2));
182
+ return;
183
+ }
184
+ const r = result;
185
+ const newRun = r.run ?? {};
186
+ const newRunId = String(newRun.runId ?? newRun.id ?? '?');
187
+ const resumePointId = typeof newRun.resumedFromResumePointId === 'string'
188
+ ? newRun.resumedFromResumePointId
189
+ : undefined;
190
+ const agentLabel = target.name ? `${target.name} (${target.id})` : target.id;
191
+ this.log(`Handed run ${sourceRunId} over to ${agentLabel}`);
192
+ this.log(`New run: ${newRunId}` +
193
+ `${newRun.startedAt || newRun.admittedAt ? ` at ${(0, format_1.localTime)(newRun.startedAt ?? newRun.admittedAt)}` : ''}`);
194
+ this.log(`Source run: ${sourceRunId}`);
195
+ this.log(`Resume point: ${resumePointId ?? 'none recorded'}`);
196
+ // Required, not optional (design §4.1): a packet that says what it left
197
+ // behind is the difference between a contract and a guess. An absent or
198
+ // empty list is printed as undeclared rather than silently dropped.
199
+ const notCarried = Array.isArray(r.packet?.notCarried)
200
+ ? r.packet.notCarried.map(String).filter(Boolean)
201
+ : [];
202
+ this.log(`Not carried: ${notCarried.length ? notCarried.join(', ') : '(nothing declared)'}`);
203
+ }
204
+ }
205
+ exports.default = TasksHandover;