@skrr-ai/cli 0.1.45 → 0.1.47

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/browser/cloud-agents.js +4 -0
  5. package/dist/commands/code/handover.d.ts +11 -0
  6. package/dist/commands/code/handover.js +104 -1
  7. package/dist/commands/code/install.js +31 -0
  8. package/dist/commands/code/update.d.ts +46 -0
  9. package/dist/commands/code/update.js +242 -0
  10. package/dist/commands/commitments/handover.d.ts +29 -0
  11. package/dist/commands/commitments/handover.js +99 -0
  12. package/dist/commands/convos/handover.d.ts +1 -0
  13. package/dist/commands/convos/handover.js +4 -0
  14. package/dist/commands/daemon/index.js +2 -0
  15. package/dist/commands/daemon/update.d.ts +19 -0
  16. package/dist/commands/daemon/update.js +48 -0
  17. package/dist/commands/followups/watch.js +40 -5
  18. package/dist/commands/spaces/briefs/get.js +8 -0
  19. package/dist/commands/spaces/briefs/list.js +2 -0
  20. package/dist/commands/spaces/briefs/set.d.ts +3 -0
  21. package/dist/commands/spaces/briefs/set.js +78 -13
  22. package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
  23. package/dist/commands/spaces/expertise-suggestions.js +71 -0
  24. package/dist/commands/spaces/members/add.d.ts +1 -0
  25. package/dist/commands/spaces/members/add.js +11 -3
  26. package/dist/commands/spaces/members/list.js +11 -0
  27. package/dist/commands/tasks/assign.d.ts +9 -0
  28. package/dist/commands/tasks/assign.js +108 -1
  29. package/dist/commands/tasks/fork.d.ts +32 -0
  30. package/dist/commands/tasks/fork.js +171 -0
  31. package/dist/commands/tasks/handover.d.ts +23 -0
  32. package/dist/commands/tasks/handover.js +205 -0
  33. package/dist/commands/tasks/promote.d.ts +23 -0
  34. package/dist/commands/tasks/promote.js +82 -0
  35. package/dist/commands/tasks/runs.d.ts +16 -0
  36. package/dist/commands/tasks/runs.js +46 -1
  37. package/dist/commands/tasks/show.d.ts +23 -0
  38. package/dist/commands/tasks/show.js +59 -0
  39. package/dist/lib/api-fetch.d.ts +7 -1
  40. package/dist/lib/api-fetch.js +9 -2
  41. package/dist/lib/assignee-resolver.d.ts +22 -0
  42. package/dist/lib/assignee-resolver.js +26 -0
  43. package/dist/lib/code-handover.d.ts +50 -0
  44. package/dist/lib/code-handover.js +135 -0
  45. package/dist/lib/commitments.d.ts +22 -0
  46. package/dist/lib/commitments.js +42 -0
  47. package/dist/lib/dedicated-wait.js +22 -1
  48. package/dist/lib/followups.d.ts +18 -0
  49. package/dist/lib/followups.js +44 -0
  50. package/dist/lib/node-adapter.js +6 -0
  51. package/dist/lib/task-extras.d.ts +44 -0
  52. package/dist/lib/task-extras.js +73 -0
  53. package/dist/lib/update-check.d.ts +80 -7
  54. package/dist/lib/update-check.js +365 -33
  55. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +16 -1
  56. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +15 -0
  57. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +16 -1
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +15 -0
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  68. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -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 +2262 -2241
  71. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  72. package/oclif.manifest.json +34562 -33947
  73. package/package.json +2 -2
@@ -45,6 +45,7 @@ class SpacesMembersAdd extends base_command_1.BaseCommand {
45
45
  '<%= config.bin %> spaces members add <space-id> --user <user-id>',
46
46
  '<%= config.bin %> spaces members add <space-id> --agent <agent-id> --role editor',
47
47
  '<%= config.bin %> spaces members add <space-id> --agent <agent-id> --brief "Owns QA tasks"',
48
+ '<%= config.bin %> spaces members add <space-id> --agent <agent-id> --expertise browser --expertise frontend',
48
49
  '<%= config.bin %> spaces members add <space-id> --user <user-id> --agent <agent-id> --json',
49
50
  ];
50
51
  static args = {
@@ -67,6 +68,10 @@ class SpacesMembersAdd extends base_command_1.BaseCommand {
67
68
  }),
68
69
  brief: core_1.Flags.string({ description: 'Member brief to save for every added principal' }),
69
70
  'brief-file': core_1.Flags.string({ description: 'Path to a markdown/text brief file' }),
71
+ expertise: core_1.Flags.string({
72
+ description: 'Domain label the added members are best suited for (repeatable, ≤20 items, ≤40 chars each)',
73
+ multiple: true,
74
+ }),
70
75
  };
71
76
  async run() {
72
77
  this.requireAuth();
@@ -108,19 +113,22 @@ class SpacesMembersAdd extends base_command_1.BaseCommand {
108
113
  public: Boolean(current.public),
109
114
  publicAccessRoleId: current.publicAccessRoleId,
110
115
  });
111
- if (brief && brief.trim().length > 0) {
116
+ const expertise = flags.expertise;
117
+ if ((brief && brief.trim().length > 0) || (expertise && expertise.length > 0)) {
112
118
  const saves = [
113
119
  ...userIds.map((principalId) => data_provider_1.dataService.upsertSpaceBrief({
114
120
  spaceId: args.id,
115
121
  principalType: 'user',
116
122
  principalId,
117
- content: brief,
123
+ content: brief && brief.trim().length > 0 ? brief : undefined,
124
+ expertise,
118
125
  })),
119
126
  ...agentIds.map((principalId) => data_provider_1.dataService.upsertSpaceBrief({
120
127
  spaceId: args.id,
121
128
  principalType: 'agent',
122
129
  principalId,
123
- content: brief,
130
+ content: brief && brief.trim().length > 0 ? brief : undefined,
131
+ expertise,
124
132
  })),
125
133
  ];
126
134
  await Promise.all(saves);
@@ -22,8 +22,10 @@ class SpacesMembersList extends base_command_1.BaseCommand {
22
22
  this.requireAuth();
23
23
  const { args, flags } = await this.parse(SpacesMembersList);
24
24
  let response;
25
+ let briefs = [];
25
26
  try {
26
27
  response = await data_provider_1.dataService.getResourcePermissions(data_provider_1.ResourceType.SPACE, args.id);
28
+ briefs = (await data_provider_1.dataService.listSpaceBriefs(args.id).catch(() => [])).filter((b) => Array.isArray(b.expertise) && b.expertise.length > 0);
27
29
  }
28
30
  catch (err) {
29
31
  this.handleApiError(err);
@@ -41,14 +43,23 @@ class SpacesMembersList extends base_command_1.BaseCommand {
41
43
  type: String(p.type ?? '-'),
42
44
  name: String(p.name ?? p.email ?? '-'),
43
45
  role: (0, space_members_1.spaceRoleLabel)(String(p.accessRoleId ?? '')),
46
+ expertise: expertiseForPrincipal(briefs, p),
44
47
  id: (0, space_members_1.principalIdForDisplay)(p),
45
48
  }));
46
49
  (0, format_1.renderTable)(rows, [
47
50
  { key: 'type', header: 'TYPE' },
48
51
  { key: 'name', header: 'NAME', maxWidth: 36 },
49
52
  { key: 'role', header: 'ROLE' },
53
+ { key: 'expertise', header: 'EXPERTISE', maxWidth: 32 },
50
54
  { key: 'id', header: 'ID', verbatim: true },
51
55
  ], (line) => this.log(line));
52
56
  }
53
57
  }
54
58
  exports.default = SpacesMembersList;
59
+ /** Brief principalId can be an agent UUID or a Mongo _id; match against both. */
60
+ function expertiseForPrincipal(briefs, principal) {
61
+ const type = String(principal.type ?? '').toLowerCase();
62
+ const ids = new Set([principal.id, principal.idOnTheSource].filter((v) => v !== undefined).map(String));
63
+ const brief = briefs.find((b) => String(b.principalType ?? '').toLowerCase() === type && ids.has(String(b.principalId ?? '')));
64
+ return brief ? brief.expertise.join(', ') : '-';
65
+ }
@@ -34,8 +34,17 @@ export default class TasksAssign extends BaseCommand {
34
34
  'repair-release-pins': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
35
35
  'execution-mode': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
36
36
  list: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
37
+ suggest: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
37
38
  json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
38
39
  };
39
40
  run(): Promise<void>;
40
41
  private renderMembers;
42
+ /**
43
+ * `--suggest`: a deterministic LOOKUP, not a routing decision. Declared
44
+ * member expertise is intersected with the task's tags (strong match) and
45
+ * its title/description text (weak match); the caller still chooses the
46
+ * assignee. This mirrors how the contract stays advisory everywhere else —
47
+ * the data is shown, the judgment stays with whoever is assigning.
48
+ */
49
+ private renderSuggestions;
41
50
  }
@@ -6,6 +6,7 @@ const base_command_1 = require("../../base-command");
6
6
  const task_resolver_1 = require("../../lib/task-resolver");
7
7
  const web_url_1 = require("../../lib/web-url");
8
8
  const assignee_resolver_1 = require("../../lib/assignee-resolver");
9
+ const format_1 = require("../../lib/format");
9
10
  const tasks_1 = require("../../lib/tasks");
10
11
  /**
11
12
  * A first-class `assign` verb.
@@ -37,6 +38,7 @@ class TasksAssign extends base_command_1.BaseCommand {
37
38
  '<%= config.bin %> tasks assign <task-id> frank --replace',
38
39
  '<%= config.bin %> tasks assign <task-id> frank --remove',
39
40
  '<%= config.bin %> tasks assign <task-id> --list',
41
+ '<%= config.bin %> tasks assign <task-id> --suggest',
40
42
  ];
41
43
  static args = {
42
44
  id: core_1.Args.string({
@@ -76,6 +78,10 @@ class TasksAssign extends base_command_1.BaseCommand {
76
78
  list: core_1.Flags.boolean({
77
79
  description: "List the task's space members and exit (your own unshared agents can also be assigned, by id)",
78
80
  }),
81
+ suggest: core_1.Flags.boolean({
82
+ description: "Advisory lookup: list space members whose declared expertise intersects this task's tags " +
83
+ '(or whose expertise appears in the task text). Suggests only — never assigns.',
84
+ }),
79
85
  json: core_1.Flags.boolean({ description: 'Output as JSON' }),
80
86
  };
81
87
  async run() {
@@ -105,7 +111,7 @@ class TasksAssign extends base_command_1.BaseCommand {
105
111
  }
106
112
  const personQueries = [...(args.person ? [args.person] : []), ...(flags.person ?? [])];
107
113
  const agentQueries = flags.agent ?? [];
108
- if (!flags.list && personQueries.length === 0 && agentQueries.length === 0) {
114
+ if (!flags.list && !flags.suggest && personQueries.length === 0 && agentQueries.length === 0) {
109
115
  // Positional order is `<task-id> <person>`. Inside a task runtime the
110
116
  // task id is implicit, so a single positional (`skrr tasks assign frank`)
111
117
  // is silently eaten as the TASK id and the user sees a confusing
@@ -134,6 +140,10 @@ class TasksAssign extends base_command_1.BaseCommand {
134
140
  this.renderMembers(spaceId ? await this.loadSpaceMembers(spaceId) : [], spaceId, flags.json);
135
141
  return;
136
142
  }
143
+ if (flags.suggest) {
144
+ await this.renderSuggestions(taskRecord, spaceId, flags.json);
145
+ return;
146
+ }
137
147
  const people = await this.resolveAssignees(personQueries, 'user', spaceId);
138
148
  const agents = await this.resolveAssignees(agentQueries, 'agent', spaceId);
139
149
  const userIds = people.ids;
@@ -228,5 +238,102 @@ class TasksAssign extends base_command_1.BaseCommand {
228
238
  this.log(`\nAgents (${agents.length}):`);
229
239
  this.log(agents.length ? (0, assignee_resolver_1.describeCandidates)(agents, 100) : ' (none)');
230
240
  }
241
+ /**
242
+ * `--suggest`: a deterministic LOOKUP, not a routing decision. Declared
243
+ * member expertise is intersected with the task's tags (strong match) and
244
+ * its title/description text (weak match); the caller still chooses the
245
+ * assignee. This mirrors how the contract stays advisory everywhere else —
246
+ * the data is shown, the judgment stays with whoever is assigning.
247
+ */
248
+ async renderSuggestions(task, spaceId, json) {
249
+ if (!spaceId) {
250
+ this.failWithCliError({
251
+ message: 'Task is not in a space — no member expertise to suggest from. ' +
252
+ 'Assign by raw id, or move the task into a space first.',
253
+ code: 'ASSIGN_NO_SPACE_CONTEXT',
254
+ exit: 2,
255
+ retryable: false,
256
+ });
257
+ }
258
+ let principals = [];
259
+ let briefs = [];
260
+ try {
261
+ const [perms, briefList] = await Promise.all([
262
+ data_provider_1.dataService.getResourcePermissions(data_provider_1.ResourceType.SPACE, spaceId),
263
+ data_provider_1.dataService.listSpaceBriefs(spaceId).catch(() => []),
264
+ ]);
265
+ principals = (perms.principals ?? []);
266
+ briefs = briefList;
267
+ }
268
+ catch (err) {
269
+ this.handleApiError(err);
270
+ }
271
+ const members = (0, assignee_resolver_1.membersFromPrincipals)(principals);
272
+ // brief.principalId may be an agent UUID or a Mongo _id — resolve through
273
+ // the principal's raw id pair so either convention lands on the member.
274
+ const expertiseByMemberId = new Map();
275
+ for (const p of principals) {
276
+ const kind = String(p.type ?? '').toLowerCase();
277
+ const brief = briefs.find((b) => String(b.principalType ?? '').toLowerCase() === kind &&
278
+ [p.id, p.idOnTheSource]
279
+ .filter(Boolean)
280
+ .map(String)
281
+ .includes(String(b.principalId ?? '')));
282
+ if (!brief || !Array.isArray(brief.expertise) || brief.expertise.length === 0)
283
+ continue;
284
+ const resolvedId = kind === 'agent' ? String(p.idOnTheSource ?? p.id ?? '') : String(p.id ?? '');
285
+ if (resolvedId)
286
+ expertiseByMemberId.set(resolvedId, brief.expertise);
287
+ }
288
+ const norm = (v) => String(v ?? '')
289
+ .trim()
290
+ .toLowerCase();
291
+ const taskTags = (Array.isArray(task.tags) ? task.tags : []).map(norm);
292
+ const taskText = `${task.title ?? ''}\n${task.description ?? ''}`;
293
+ const suggestions = (0, assignee_resolver_1.suggestMembersByExpertise)({
294
+ members,
295
+ expertiseByMemberId,
296
+ taskTags,
297
+ taskText,
298
+ });
299
+ if (json) {
300
+ this.log(JSON.stringify({
301
+ spaceId,
302
+ taskTags,
303
+ suggestions: suggestions.map((s) => ({
304
+ kind: s.member.kind,
305
+ id: s.member.id,
306
+ name: s.member.name ?? s.member.email ?? null,
307
+ matchedTags: s.matchedTags,
308
+ matchedText: s.matchedText,
309
+ })),
310
+ }, null, 2));
311
+ return;
312
+ }
313
+ const ref = task.identifier ?? task.id;
314
+ if (suggestions.length === 0) {
315
+ this.log(`No member expertise matches ${ref}` +
316
+ (taskTags.length === 0
317
+ ? ' (the task has no tags — tag tasks or declare expertise to make routing smarter)'
318
+ : ` (tags: ${taskTags.join(', ')})`) +
319
+ '.');
320
+ return;
321
+ }
322
+ this.log(`Suggested assignees for ${ref}${taskTags.length ? ` (tags: ${taskTags.join(', ')})` : ''}:`);
323
+ (0, format_1.renderTable)(suggestions.map((s) => ({
324
+ kind: s.member.kind,
325
+ member: s.member.name || s.member.email || s.member.id,
326
+ tag: s.matchedTags.join(', ') || '-',
327
+ text: s.matchedText.join(', ') || '-',
328
+ id: s.member.id,
329
+ })), [
330
+ { key: 'kind', header: 'KIND' },
331
+ { key: 'member', header: 'MEMBER', maxWidth: 28 },
332
+ { key: 'tag', header: 'TAG MATCH', maxWidth: 32 },
333
+ { key: 'text', header: 'TEXT MATCH', maxWidth: 32 },
334
+ { key: 'id', header: 'ID', verbatim: true },
335
+ ], (line) => this.log(line));
336
+ this.log(`\nAdvisory only — assign with: ${this.config.bin} tasks assign ${ref} --agent <name>`);
337
+ }
231
338
  }
232
339
  exports.default = TasksAssign;
@@ -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
+ }