@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
|
@@ -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;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command';
|
|
2
|
+
/**
|
|
3
|
+
* `skrr tasks promote` — settle a parallel group (B8).
|
|
4
|
+
*
|
|
5
|
+
* This is the destructive half of fork/race: fixing one branch as canonical
|
|
6
|
+
* CANCELS every live loser in the group. That is why it is its own verb with
|
|
7
|
+
* its own confirmation rather than a flag, and why the command prints what it
|
|
8
|
+
* cancelled rather than reporting only the winner.
|
|
9
|
+
*/
|
|
10
|
+
export default class TasksPromote extends BaseCommand {
|
|
11
|
+
static description: string;
|
|
12
|
+
static examples: string[];
|
|
13
|
+
static args: {
|
|
14
|
+
id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
|
|
15
|
+
};
|
|
16
|
+
static flags: {
|
|
17
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
18
|
+
task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
19
|
+
run: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
20
|
+
'group-member': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
21
|
+
};
|
|
22
|
+
run(): Promise<void>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const base_command_1 = require("../../base-command");
|
|
5
|
+
const task_extras_1 = require("../../lib/task-extras");
|
|
6
|
+
const task_resolver_1 = require("../../lib/task-resolver");
|
|
7
|
+
/**
|
|
8
|
+
* `skrr tasks promote` — settle a parallel group (B8).
|
|
9
|
+
*
|
|
10
|
+
* This is the destructive half of fork/race: fixing one branch as canonical
|
|
11
|
+
* CANCELS every live loser in the group. That is why it is its own verb with
|
|
12
|
+
* its own confirmation rather than a flag, and why the command prints what it
|
|
13
|
+
* cancelled rather than reporting only the winner.
|
|
14
|
+
*/
|
|
15
|
+
class TasksPromote extends base_command_1.BaseCommand {
|
|
16
|
+
static description = 'Fix one branch of a parallel run group as canonical and cancel the live losers (B8)';
|
|
17
|
+
static examples = [
|
|
18
|
+
'<%= config.bin %> tasks promote <task-id> --run <run-id>',
|
|
19
|
+
'<%= config.bin %> tasks promote <task-id> --run <run-id> --json',
|
|
20
|
+
];
|
|
21
|
+
static args = {
|
|
22
|
+
id: core_1.Args.string({
|
|
23
|
+
description: 'Task ID; defaults to OVERSKY_SESSION_TASK_ID in task runtimes',
|
|
24
|
+
required: false,
|
|
25
|
+
ignoreStdin: true,
|
|
26
|
+
}),
|
|
27
|
+
};
|
|
28
|
+
static flags = {
|
|
29
|
+
json: core_1.Flags.boolean({ description: 'Output the resolution as JSON' }),
|
|
30
|
+
task: core_1.Flags.string({ description: 'Task ID; overrides daemon runtime context' }),
|
|
31
|
+
run: core_1.Flags.string({
|
|
32
|
+
description: 'The run to make canonical — must be a member of the parallel group',
|
|
33
|
+
required: true,
|
|
34
|
+
}),
|
|
35
|
+
'group-member': core_1.Flags.string({
|
|
36
|
+
description: 'Any run in the group to address it by (default: the canonical run itself, which is ' +
|
|
37
|
+
'always a member)',
|
|
38
|
+
}),
|
|
39
|
+
};
|
|
40
|
+
async run() {
|
|
41
|
+
this.requireAuth();
|
|
42
|
+
const { args, flags } = await this.parse(TasksPromote);
|
|
43
|
+
let taskIdentity;
|
|
44
|
+
try {
|
|
45
|
+
taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({ positionalId: args.id, flagTask: flags.task });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
const failure = (0, task_resolver_1.taskResolutionFailure)(err);
|
|
49
|
+
this.failWithCliError({
|
|
50
|
+
message: failure.message,
|
|
51
|
+
code: failure.code,
|
|
52
|
+
exit: 2,
|
|
53
|
+
retryable: false,
|
|
54
|
+
details: { suggestion: failure.suggestion, context: failure.details },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const taskId = taskIdentity.id;
|
|
58
|
+
// The route resolves the GROUP from the addressed run, then requires the
|
|
59
|
+
// canonical run to be a member of it. Addressing by the canonical run
|
|
60
|
+
// itself is always valid and is the obvious default.
|
|
61
|
+
const addressedRunId = flags['group-member'] ?? flags.run;
|
|
62
|
+
let result;
|
|
63
|
+
try {
|
|
64
|
+
result = await (0, task_extras_1.taskRunPromote)(taskId, addressedRunId, { canonicalRunId: flags.run });
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
this.handleApiError(err, { subject: `Task ${taskId}` });
|
|
68
|
+
}
|
|
69
|
+
if (flags.json) {
|
|
70
|
+
this.log(JSON.stringify(result, null, 2));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const r = result;
|
|
74
|
+
const cancelled = Array.isArray(r.cancelledRunIds) ? r.cancelledRunIds.map(String) : [];
|
|
75
|
+
this.log(`Canonical run: ${r.canonicalRunId ?? flags.run}`);
|
|
76
|
+
this.log(`Parallel group: ${r.groupId ?? '?'}`);
|
|
77
|
+
this.log(cancelled.length
|
|
78
|
+
? `Cancelled ${cancelled.length} losing run(s): ${cancelled.join(', ')}`
|
|
79
|
+
: 'Cancelled no runs — every other branch had already finished.');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
exports.default = TasksPromote;
|
|
@@ -16,6 +16,22 @@ export declare function formatRunExecutor(run: Record<string, unknown>): string;
|
|
|
16
16
|
* list, always once something failed.
|
|
17
17
|
*/
|
|
18
18
|
export declare function runNeedsFailureReason(run: Record<string, unknown>): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The handover fields a run row may carry (execution-handover impl contracts):
|
|
21
|
+
*
|
|
22
|
+
* `handover` — the packet stamped on a run ADMITTED BY a handover; this
|
|
23
|
+
* run is the continuation, `packet.from.runId` is where it
|
|
24
|
+
* resumed from.
|
|
25
|
+
* `handoverOffer` — the structured offer a retargetable terminal failure
|
|
26
|
+
* leaves behind (`{failureReason, resetAt?, actedRunId?}`).
|
|
27
|
+
* `actedRunId` means the offer was taken and names the
|
|
28
|
+
* continuation run.
|
|
29
|
+
*
|
|
30
|
+
* Both are absent on a server that predates them, so this returns null and the
|
|
31
|
+
* column below is rendered only when at least one row carries a marker — a
|
|
32
|
+
* healthy list against an older deployment pays and shows nothing.
|
|
33
|
+
*/
|
|
34
|
+
export declare function runHandoverMarker(run: Record<string, unknown>): string | null;
|
|
19
35
|
/**
|
|
20
36
|
* The run's OWN failure fields, when the server carries them.
|
|
21
37
|
*
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.formatRunExecutor = formatRunExecutor;
|
|
4
4
|
exports.runNeedsFailureReason = runNeedsFailureReason;
|
|
5
|
+
exports.runHandoverMarker = runHandoverMarker;
|
|
5
6
|
exports.failureFromRun = failureFromRun;
|
|
6
7
|
exports.renderRunFailures = renderRunFailures;
|
|
7
8
|
const core_1 = require("@oclif/core");
|
|
@@ -46,6 +47,42 @@ function runNeedsFailureReason(run) {
|
|
|
46
47
|
return true;
|
|
47
48
|
return typeof run.errors === 'number' && run.errors > 0;
|
|
48
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The handover fields a run row may carry (execution-handover impl contracts):
|
|
52
|
+
*
|
|
53
|
+
* `handover` — the packet stamped on a run ADMITTED BY a handover; this
|
|
54
|
+
* run is the continuation, `packet.from.runId` is where it
|
|
55
|
+
* resumed from.
|
|
56
|
+
* `handoverOffer` — the structured offer a retargetable terminal failure
|
|
57
|
+
* leaves behind (`{failureReason, resetAt?, actedRunId?}`).
|
|
58
|
+
* `actedRunId` means the offer was taken and names the
|
|
59
|
+
* continuation run.
|
|
60
|
+
*
|
|
61
|
+
* Both are absent on a server that predates them, so this returns null and the
|
|
62
|
+
* column below is rendered only when at least one row carries a marker — a
|
|
63
|
+
* healthy list against an older deployment pays and shows nothing.
|
|
64
|
+
*/
|
|
65
|
+
function runHandoverMarker(run) {
|
|
66
|
+
const packet = run.handover;
|
|
67
|
+
if (packet && typeof packet === 'object') {
|
|
68
|
+
const from = packet.from;
|
|
69
|
+
const fromRun = from &&
|
|
70
|
+
typeof from === 'object' &&
|
|
71
|
+
typeof from.runId === 'string'
|
|
72
|
+
? ` ← ${String(from.runId)}`
|
|
73
|
+
: '';
|
|
74
|
+
return `handover${fromRun}`;
|
|
75
|
+
}
|
|
76
|
+
const offer = run.handoverOffer;
|
|
77
|
+
if (offer && typeof offer === 'object') {
|
|
78
|
+
const o = offer;
|
|
79
|
+
if (o.actedRunId)
|
|
80
|
+
return `offer → ${String(o.actedRunId)}`;
|
|
81
|
+
const reason = typeof o.failureReason === 'string' ? o.failureReason : '';
|
|
82
|
+
return `offer${reason ? `:${reason}` : ''}`;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
49
86
|
/**
|
|
50
87
|
* The run's OWN failure fields, when the server carries them.
|
|
51
88
|
*
|
|
@@ -151,13 +188,20 @@ class TasksRuns extends base_command_1.BaseCommand {
|
|
|
151
188
|
this.log('No runs.');
|
|
152
189
|
return;
|
|
153
190
|
}
|
|
154
|
-
const
|
|
191
|
+
const markers = items.map(runHandoverMarker);
|
|
192
|
+
// The column exists only when a run carries a handover field — an older
|
|
193
|
+
// deployment sends neither, and an always-on column of '-' is noise.
|
|
194
|
+
const showHandover = markers.some(Boolean);
|
|
195
|
+
const rows = items.map((run, i) => ({
|
|
155
196
|
runId: String(run.runId ?? run.id ?? run._id ?? '-'),
|
|
156
197
|
status: String(run.status ?? '-'),
|
|
157
198
|
executor: formatRunExecutor(run),
|
|
158
199
|
started: run.startedAt ? (0, format_1.localTime)(run.startedAt) : '-',
|
|
159
200
|
duration: run.durationMs != null ? `${run.durationMs}ms` : '-',
|
|
160
201
|
tokens: String(run.totalTokens ?? run.tokens ?? '-'),
|
|
202
|
+
// Always present on the row so the record type stays uniform; the COLUMN
|
|
203
|
+
// is what is conditional.
|
|
204
|
+
handover: markers[i] ?? '-',
|
|
161
205
|
}));
|
|
162
206
|
(0, format_1.renderTable)(rows, [
|
|
163
207
|
{ key: 'runId', header: 'RUN ID' },
|
|
@@ -166,6 +210,7 @@ class TasksRuns extends base_command_1.BaseCommand {
|
|
|
166
210
|
{ key: 'started', header: 'STARTED' },
|
|
167
211
|
{ key: 'duration', header: 'DURATION' },
|
|
168
212
|
{ key: 'tokens', header: 'TOKENS' },
|
|
213
|
+
...(showHandover ? [{ key: 'handover', header: 'HANDOVER' }] : []),
|
|
169
214
|
], (line) => this.log(line));
|
|
170
215
|
// Only once something failed: a healthy list pays nothing for this.
|
|
171
216
|
if (!unexplained)
|
|
@@ -20,6 +20,13 @@ export declare function taskPointerLines(t: Record<string, unknown>, ref: string
|
|
|
20
20
|
* be read (or a label missing from it) degrades per entry rather than failing
|
|
21
21
|
* the command or hiding the label.
|
|
22
22
|
*/
|
|
23
|
+
/**
|
|
24
|
+
* The lines that say what a blocked Task is waiting on.
|
|
25
|
+
*
|
|
26
|
+
* Kept pure so the rule is testable without a network: the command fetches the
|
|
27
|
+
* newest `blocker` Update and hands its payload here.
|
|
28
|
+
*/
|
|
29
|
+
export declare function blockerDetailLines(detail: Record<string, unknown> | undefined): string[];
|
|
23
30
|
export declare function labelsLine(labelIds?: string[], nameById?: Map<string, string>): string;
|
|
24
31
|
/**
|
|
25
32
|
* The `Executor:` value — who does this task, and whether anyone said so.
|
|
@@ -73,4 +80,20 @@ export default class TasksShow extends BaseCommand {
|
|
|
73
80
|
'no-enrich': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
74
81
|
};
|
|
75
82
|
run(): Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* What a blocked Task is actually waiting on.
|
|
85
|
+
*
|
|
86
|
+
* The workflow tells an agent to record a typed `blocker` Update —
|
|
87
|
+
* `{cause, requiredAction, nextOwner}` — and only then move the Task to
|
|
88
|
+
* blocked. The move writes its own exceptional hold, whose summary is the
|
|
89
|
+
* `--reason` flag or the literal fallback `Task was marked blocked`, and that
|
|
90
|
+
* is what `Why:` prints. So the one line a reader looks at restated the
|
|
91
|
+
* status, while the thing the writer was told to record sat one command away
|
|
92
|
+
* in `tasks updates list` (OSK-10391).
|
|
93
|
+
*
|
|
94
|
+
* Read only for a blocked Task, and only the newest blocker Update. A failure
|
|
95
|
+
* here prints nothing: this decorates an answer the caller already has, and
|
|
96
|
+
* must never be the reason `tasks show` cannot answer at all.
|
|
97
|
+
*/
|
|
98
|
+
private printBlockerDetail;
|
|
76
99
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.taskPointerLines = taskPointerLines;
|
|
4
|
+
exports.blockerDetailLines = blockerDetailLines;
|
|
4
5
|
exports.labelsLine = labelsLine;
|
|
5
6
|
exports.taskExecutorLabel = taskExecutorLabel;
|
|
6
7
|
exports.taskFailureLines = taskFailureLines;
|
|
@@ -52,6 +53,29 @@ function taskPointerLines(t, ref, bin = 'skrr') {
|
|
|
52
53
|
* be read (or a label missing from it) degrades per entry rather than failing
|
|
53
54
|
* the command or hiding the label.
|
|
54
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* The lines that say what a blocked Task is waiting on.
|
|
58
|
+
*
|
|
59
|
+
* Kept pure so the rule is testable without a network: the command fetches the
|
|
60
|
+
* newest `blocker` Update and hands its payload here.
|
|
61
|
+
*/
|
|
62
|
+
function blockerDetailLines(detail) {
|
|
63
|
+
if (!detail)
|
|
64
|
+
return [];
|
|
65
|
+
const lines = [];
|
|
66
|
+
const cause = typeof detail.cause === 'string' ? detail.cause.trim() : '';
|
|
67
|
+
const required = typeof detail.requiredAction === 'string' ? detail.requiredAction.trim() : '';
|
|
68
|
+
const owner = detail.nextOwner;
|
|
69
|
+
if (cause)
|
|
70
|
+
lines.push(`Blocked on: ${cause}`);
|
|
71
|
+
if (required)
|
|
72
|
+
lines.push(`Needs: ${required}`);
|
|
73
|
+
if (owner && typeof owner.kind === 'string' && owner.kind) {
|
|
74
|
+
const who = typeof owner.userId === 'string' && owner.userId ? ` ${owner.userId}` : '';
|
|
75
|
+
lines.push(`Next owner: ${owner.kind}${who}`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
55
79
|
function labelsLine(labelIds, nameById) {
|
|
56
80
|
const ids = (labelIds ?? []).filter((id) => typeof id === 'string' && id.length > 0);
|
|
57
81
|
if (ids.length === 0)
|
|
@@ -370,6 +394,7 @@ class TasksShow extends base_command_1.BaseCommand {
|
|
|
370
394
|
if (actionability) {
|
|
371
395
|
this.log(`Next action: ${actionability.nextActor ?? '-'} · ${actionability.state ?? '-'} · ${actionability.reasonCode ?? '-'}`);
|
|
372
396
|
this.log(`Why: ${actionability.reason ?? '-'}`);
|
|
397
|
+
await this.printBlockerDetail(t, actionability);
|
|
373
398
|
}
|
|
374
399
|
const delivery = t.delivery;
|
|
375
400
|
if (delivery?.prNumber || delivery?.prUrl || delivery?.branch) {
|
|
@@ -493,5 +518,39 @@ class TasksShow extends base_command_1.BaseCommand {
|
|
|
493
518
|
for (const line of taskPointerLines(t, ref, this.config.bin))
|
|
494
519
|
this.log(line);
|
|
495
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* What a blocked Task is actually waiting on.
|
|
523
|
+
*
|
|
524
|
+
* The workflow tells an agent to record a typed `blocker` Update —
|
|
525
|
+
* `{cause, requiredAction, nextOwner}` — and only then move the Task to
|
|
526
|
+
* blocked. The move writes its own exceptional hold, whose summary is the
|
|
527
|
+
* `--reason` flag or the literal fallback `Task was marked blocked`, and that
|
|
528
|
+
* is what `Why:` prints. So the one line a reader looks at restated the
|
|
529
|
+
* status, while the thing the writer was told to record sat one command away
|
|
530
|
+
* in `tasks updates list` (OSK-10391).
|
|
531
|
+
*
|
|
532
|
+
* Read only for a blocked Task, and only the newest blocker Update. A failure
|
|
533
|
+
* here prints nothing: this decorates an answer the caller already has, and
|
|
534
|
+
* must never be the reason `tasks show` cannot answer at all.
|
|
535
|
+
*/
|
|
536
|
+
async printBlockerDetail(task, actionability) {
|
|
537
|
+
const looksBlocked = String(task.status ?? '') === 'blocked' || actionability.reasonCode === 'exceptional_hold';
|
|
538
|
+
if (!looksBlocked) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
const updates = await data_provider_1.dataService.listTaskUpdates(String(task.id), {
|
|
543
|
+
limit: 1,
|
|
544
|
+
type: 'blocker',
|
|
545
|
+
});
|
|
546
|
+
const detail = updates?.data?.[0]?.data;
|
|
547
|
+
for (const line of blockerDetailLines(detail)) {
|
|
548
|
+
this.log(line);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// The status line above already answered the question this decorates.
|
|
553
|
+
}
|
|
554
|
+
}
|
|
496
555
|
}
|
|
497
556
|
exports.default = TasksShow;
|
package/dist/lib/api-fetch.d.ts
CHANGED
|
@@ -30,7 +30,13 @@ export declare class ApiFetchError extends Error {
|
|
|
30
30
|
readonly body?: string;
|
|
31
31
|
readonly code?: string;
|
|
32
32
|
readonly method?: string;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The server's `X-Request-ID` for the failing call, when it sent one. It is
|
|
35
|
+
* the id the server logs against, so a refusal can be looked up rather than
|
|
36
|
+
* attributed by guesswork — see `node-adapter.ts`.
|
|
37
|
+
*/
|
|
38
|
+
readonly requestId?: string;
|
|
39
|
+
constructor(message: string, status: number, body?: string, code?: string, method?: string, requestId?: string);
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Call a skrr API endpoint with the current credential. Throws
|
package/dist/lib/api-fetch.js
CHANGED
|
@@ -54,13 +54,20 @@ class ApiFetchError extends Error {
|
|
|
54
54
|
// (OSK-6765): `agents show` is a GET and had no body, and the advice was
|
|
55
55
|
// impossible to follow.
|
|
56
56
|
method;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The server's `X-Request-ID` for the failing call, when it sent one. It is
|
|
59
|
+
* the id the server logs against, so a refusal can be looked up rather than
|
|
60
|
+
* attributed by guesswork — see `node-adapter.ts`.
|
|
61
|
+
*/
|
|
62
|
+
requestId;
|
|
63
|
+
constructor(message, status, body, code, method, requestId) {
|
|
58
64
|
super(message);
|
|
59
65
|
this.name = 'ApiFetchError';
|
|
60
66
|
this.status = status;
|
|
61
67
|
this.body = body;
|
|
62
68
|
this.code = code;
|
|
63
69
|
this.method = method;
|
|
70
|
+
this.requestId = requestId;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
exports.ApiFetchError = ApiFetchError;
|
|
@@ -209,7 +216,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
209
216
|
catch {
|
|
210
217
|
/* ignore body read failures */
|
|
211
218
|
}
|
|
212
|
-
throw new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method);
|
|
219
|
+
throw new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method, res.headers.get('x-request-id') || undefined);
|
|
213
220
|
}
|
|
214
221
|
// 204/205 carry no body by definition; an empty body has no content-type
|
|
215
222
|
// worth judging. Neither is an error.
|
|
@@ -25,6 +25,28 @@ export type SpaceMember = {
|
|
|
25
25
|
name?: string;
|
|
26
26
|
email?: string;
|
|
27
27
|
};
|
|
28
|
+
export type ExpertiseSuggestion = {
|
|
29
|
+
member: SpaceMember;
|
|
30
|
+
/** Expertise entries that exactly match a task tag (strong match). */
|
|
31
|
+
matchedTags: string[];
|
|
32
|
+
/** Expertise entries that appear in the task title/description (weak match). */
|
|
33
|
+
matchedText: string[];
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Deterministic advisory lookup: which space members' declared `expertise`
|
|
37
|
+
* intersects this task's tags (strong) or appears in its title/description
|
|
38
|
+
* (weak). Not a routing decision — the caller still picks the assignee; this
|
|
39
|
+
* just surfaces the declared contract. Ranked: more tag matches first, then
|
|
40
|
+
* more text matches.
|
|
41
|
+
*/
|
|
42
|
+
export declare function suggestMembersByExpertise({ members, expertiseByMemberId, taskTags, taskText, }: {
|
|
43
|
+
members: SpaceMember[];
|
|
44
|
+
/** member.id → declared expertise labels */
|
|
45
|
+
expertiseByMemberId: Map<string, string[]>;
|
|
46
|
+
taskTags: string[];
|
|
47
|
+
/** Task title + description, any case — normalized internally. */
|
|
48
|
+
taskText: string;
|
|
49
|
+
}): ExpertiseSuggestion[];
|
|
28
50
|
export type ResolutionFailure = {
|
|
29
51
|
ok: false;
|
|
30
52
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.suggestMembersByExpertise = suggestMembersByExpertise;
|
|
3
4
|
exports.looksLikeRawId = looksLikeRawId;
|
|
4
5
|
exports.membersFromPrincipals = membersFromPrincipals;
|
|
5
6
|
exports.resolveMember = resolveMember;
|
|
@@ -9,6 +10,31 @@ exports.resolveAssigneeReferences = resolveAssigneeReferences;
|
|
|
9
10
|
exports.formatResolutionFailure = formatResolutionFailure;
|
|
10
11
|
const failed_lookup_1 = require("./failed-lookup");
|
|
11
12
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
13
|
+
/**
|
|
14
|
+
* Deterministic advisory lookup: which space members' declared `expertise`
|
|
15
|
+
* intersects this task's tags (strong) or appears in its title/description
|
|
16
|
+
* (weak). Not a routing decision — the caller still picks the assignee; this
|
|
17
|
+
* just surfaces the declared contract. Ranked: more tag matches first, then
|
|
18
|
+
* more text matches.
|
|
19
|
+
*/
|
|
20
|
+
function suggestMembersByExpertise({ members, expertiseByMemberId, taskTags, taskText, }) {
|
|
21
|
+
const normTag = (v) => v.trim().toLowerCase();
|
|
22
|
+
const tags = new Set(taskTags.map(normTag));
|
|
23
|
+
const text = taskText.toLowerCase();
|
|
24
|
+
const out = [];
|
|
25
|
+
for (const member of members) {
|
|
26
|
+
const expertise = expertiseByMemberId.get(member.id);
|
|
27
|
+
if (!expertise || expertise.length === 0)
|
|
28
|
+
continue;
|
|
29
|
+
const matchedTags = expertise.filter((e) => tags.has(normTag(e)));
|
|
30
|
+
const matchedText = expertise.filter((e) => !matchedTags.includes(e) && normTag(e).length > 0 && text.includes(normTag(e)));
|
|
31
|
+
if (matchedTags.length > 0 || matchedText.length > 0) {
|
|
32
|
+
out.push({ member, matchedTags, matchedText });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
out.sort((a, b) => b.matchedTags.length - a.matchedTags.length || b.matchedText.length - a.matchedText.length);
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
12
38
|
const OBJECT_ID_RE = /^[0-9a-fA-F]{24}$/;
|
|
13
39
|
const UUID_RE = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
|
|
14
40
|
// Agent ids are transitioning from public UUIDs to opaque `agent_…` ids.
|