@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
@@ -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 rows = items.map((run) => ({
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;
@@ -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
- constructor(message: string, status: number, body?: string, code?: string, method?: string);
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
@@ -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
- constructor(message, status, body, code, method) {
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.