@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.
- package/dist/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/browser/cloud-agents.js +4 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +104 -1
- package/dist/commands/code/install.js +31 -0
- package/dist/commands/code/update.d.ts +46 -0
- package/dist/commands/code/update.js +242 -0
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/convos/handover.d.ts +1 -0
- package/dist/commands/convos/handover.js +4 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/update.d.ts +19 -0
- package/dist/commands/daemon/update.js +48 -0
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/spaces/briefs/get.js +8 -0
- package/dist/commands/spaces/briefs/list.js +2 -0
- package/dist/commands/spaces/briefs/set.d.ts +3 -0
- package/dist/commands/spaces/briefs/set.js +78 -13
- package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
- package/dist/commands/spaces/expertise-suggestions.js +71 -0
- package/dist/commands/spaces/members/add.d.ts +1 -0
- package/dist/commands/spaces/members/add.js +11 -3
- package/dist/commands/spaces/members/list.js +11 -0
- package/dist/commands/tasks/assign.d.ts +9 -0
- package/dist/commands/tasks/assign.js +108 -1
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/assignee-resolver.d.ts +22 -0
- package/dist/lib/assignee-resolver.js +26 -0
- package/dist/lib/code-handover.d.ts +50 -0
- package/dist/lib/code-handover.js +135 -0
- package/dist/lib/commitments.d.ts +22 -0
- package/dist/lib/commitments.js +42 -0
- package/dist/lib/dedicated-wait.js +22 -1
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +44 -0
- package/dist/lib/node-adapter.js +6 -0
- package/dist/lib/task-extras.d.ts +44 -0
- package/dist/lib/task-extras.js +73 -0
- package/dist/lib/update-check.d.ts +80 -7
- package/dist/lib/update-check.js +365 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2262 -2241
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34562 -33947
- 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
|
-
|
|
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
|
+
}
|