@skrr-ai/cli 0.1.45 → 0.1.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) 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 +105 -1
  7. package/dist/commands/commitments/handover.d.ts +29 -0
  8. package/dist/commands/commitments/handover.js +99 -0
  9. package/dist/commands/followups/watch.js +40 -5
  10. package/dist/commands/tasks/fork.d.ts +32 -0
  11. package/dist/commands/tasks/fork.js +171 -0
  12. package/dist/commands/tasks/handover.d.ts +23 -0
  13. package/dist/commands/tasks/handover.js +205 -0
  14. package/dist/commands/tasks/promote.d.ts +23 -0
  15. package/dist/commands/tasks/promote.js +82 -0
  16. package/dist/commands/tasks/runs.d.ts +16 -0
  17. package/dist/commands/tasks/runs.js +46 -1
  18. package/dist/commands/tasks/show.d.ts +23 -0
  19. package/dist/commands/tasks/show.js +59 -0
  20. package/dist/lib/api-fetch.d.ts +7 -1
  21. package/dist/lib/api-fetch.js +9 -2
  22. package/dist/lib/code-handover.d.ts +60 -0
  23. package/dist/lib/code-handover.js +120 -0
  24. package/dist/lib/commitments.d.ts +22 -0
  25. package/dist/lib/commitments.js +42 -0
  26. package/dist/lib/dedicated-wait.js +22 -1
  27. package/dist/lib/followups.d.ts +18 -0
  28. package/dist/lib/followups.js +44 -0
  29. package/dist/lib/node-adapter.js +6 -0
  30. package/dist/lib/task-extras.d.ts +44 -0
  31. package/dist/lib/task-extras.js +73 -0
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -0
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -0
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
  44. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
  45. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
  46. package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
  47. package/dist/node_modules/@skrr-ai/data-provider/index.js +2242 -2234
  48. package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
  49. package/oclif.manifest.json +35697 -35300
  50. package/package.json +2 -2
@@ -0,0 +1,171 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const data_provider_1 = require("@skrr-ai/data-provider");
5
+ const base_command_1 = require("../../base-command");
6
+ const code_handover_1 = require("../../lib/code-handover");
7
+ const task_extras_1 = require("../../lib/task-extras");
8
+ const task_resolver_1 = require("../../lib/task-resolver");
9
+ /**
10
+ * `skrr tasks fork` — race a second attempt at the same work (B8).
11
+ *
12
+ * The line against `tasks handover`: a handover CONTINUES the work once, from a
13
+ * finished run. A fork runs a SIBLING attempt from the same Resume Point while
14
+ * the original is still live — both in one `parallel.groupId`, neither
15
+ * canonical until `tasks promote` picks one.
16
+ *
17
+ * The shared-workspace hazard is the reason this is not a flag on handover. Two
18
+ * executors of the same target class can be writing one workspace, and the
19
+ * server refuses (`shared_workspace_ack_required`) until the caller says
20
+ * plainly that it knows. `--acknowledge-shared-workspace` is that sentence;
21
+ * it is deliberately long to type.
22
+ */
23
+ class TasksFork extends base_command_1.BaseCommand {
24
+ static description = 'Race a sibling run of a task from the same Resume Point (parallel attempt, B8)';
25
+ static examples = [
26
+ '<%= config.bin %> tasks fork <task-id> --to "Codex Agent"',
27
+ '<%= config.bin %> tasks fork <task-id> --to agent_abc --harness agent-worker',
28
+ '<%= config.bin %> tasks fork <task-id> --from-run <run-id> --acknowledge-shared-workspace',
29
+ ];
30
+ static args = {
31
+ id: core_1.Args.string({
32
+ description: 'Task ID; defaults to OVERSKY_SESSION_TASK_ID in task runtimes',
33
+ required: false,
34
+ ignoreStdin: true,
35
+ }),
36
+ };
37
+ static flags = {
38
+ json: core_1.Flags.boolean({ description: 'Output the {run, packet, groupId} response as JSON' }),
39
+ task: core_1.Flags.string({ description: 'Task ID; overrides daemon runtime context' }),
40
+ to: core_1.Flags.string({
41
+ description: 'Agent for the branch — id or name. Default: the source run’s own agent.',
42
+ }),
43
+ harness: core_1.Flags.string({
44
+ description: 'Execution target for the branch. Default: the target agent’s own policy chain.',
45
+ }),
46
+ 'from-run': core_1.Flags.string({
47
+ description: 'Run to branch from (default: the task’s most recent run)',
48
+ }),
49
+ reason: core_1.Flags.string({ description: 'Why a second attempt — recorded in the packet' }),
50
+ 'acknowledge-shared-workspace': core_1.Flags.boolean({
51
+ description: 'Proceed when the branch resolves to the same execution-target class as the source, ' +
52
+ 'where two executors can write one workspace. Required by the server in that case.',
53
+ default: false,
54
+ }),
55
+ };
56
+ async run() {
57
+ this.requireAuth();
58
+ const { args, flags } = await this.parse(TasksFork);
59
+ let taskIdentity;
60
+ try {
61
+ taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({ positionalId: args.id, flagTask: flags.task });
62
+ }
63
+ catch (err) {
64
+ const failure = (0, task_resolver_1.taskResolutionFailure)(err);
65
+ this.failWithCliError({
66
+ message: failure.message,
67
+ code: failure.code,
68
+ exit: 2,
69
+ retryable: false,
70
+ details: { suggestion: failure.suggestion, context: failure.details },
71
+ });
72
+ }
73
+ const taskId = taskIdentity.id;
74
+ // The branch agent is OPTIONAL — a fork onto the same agent on a different
75
+ // target is the "try it on Codex too" case, and the server derives the
76
+ // agent from the source run when none is named.
77
+ let targetAgentId;
78
+ if (flags.to) {
79
+ let agents = [];
80
+ try {
81
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
82
+ agents = (resp?.data ?? []).filter(Boolean);
83
+ }
84
+ catch (err) {
85
+ this.handleApiError(err);
86
+ }
87
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
88
+ if (matched.kind === 'none') {
89
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
90
+ }
91
+ if (matched.kind === 'ambiguous') {
92
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
93
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
94
+ }
95
+ targetAgentId = matched.agent.id;
96
+ }
97
+ let sourceRunId = flags['from-run'] ?? '';
98
+ if (!sourceRunId) {
99
+ let runs = [];
100
+ try {
101
+ const resp = await data_provider_1.dataService.getTaskRuns(taskId);
102
+ runs = (resp?.data ?? []);
103
+ }
104
+ catch (err) {
105
+ this.handleApiError(err);
106
+ }
107
+ const latest = runs[0];
108
+ if (!latest) {
109
+ this.failWithCliError({
110
+ message: `Task ${taskId} has no runs to fork from. Run \`${this.config.bin} tasks runs ${taskId}\` to see the ledger.`,
111
+ code: 'FORK_NO_SOURCE_RUN',
112
+ exit: 2,
113
+ retryable: false,
114
+ });
115
+ }
116
+ sourceRunId = String(latest.runId ?? latest.id ?? '');
117
+ }
118
+ let result;
119
+ try {
120
+ result = await (0, task_extras_1.taskRunFork)(taskId, sourceRunId, {
121
+ ...(targetAgentId ? { targetAgentId } : {}),
122
+ ...(flags.harness ? { executionTargetRef: (0, task_extras_1.parseExecutionTargetRef)(flags.harness) } : {}),
123
+ ...(flags.reason ? { reason: flags.reason } : {}),
124
+ ...(flags['acknowledge-shared-workspace'] ? { acknowledgeSharedWorkspace: true } : {}),
125
+ });
126
+ }
127
+ catch (err) {
128
+ const refusal = (0, base_command_1.apiRefusal)(err);
129
+ if (refusal.code?.toLowerCase() === 'shared_workspace_ack_required') {
130
+ this.failWithCliError({
131
+ message: (refusal.message ??
132
+ 'The branch resolves to the same execution-target class as the source.') +
133
+ '\n → two executors could write one workspace. Re-run with ' +
134
+ '`--acknowledge-shared-workspace` if that is acceptable, or pass `--harness` ' +
135
+ 'to put the branch on a different target class.',
136
+ code: refusal.code,
137
+ exit: 3,
138
+ retryable: false,
139
+ details: refusal.body,
140
+ });
141
+ }
142
+ if (refusal.code?.toLowerCase() === 'target_not_assignee') {
143
+ this.failWithCliError({
144
+ message: (refusal.message ?? `Agent ${flags.to} may not work this task.`) +
145
+ `\n → assign it first: \`${this.config.bin} tasks assign ${taskId} --agent "${flags.to}"\`, then re-run.`,
146
+ code: refusal.code,
147
+ exit: 3,
148
+ retryable: false,
149
+ details: refusal.body,
150
+ });
151
+ }
152
+ this.handleApiError(err, { subject: `Task ${taskId}` });
153
+ }
154
+ if (flags.json) {
155
+ this.log(JSON.stringify(result, null, 2));
156
+ return;
157
+ }
158
+ const r = result;
159
+ const branch = r.run ?? {};
160
+ const branchRunId = String(branch.runId ?? branch.id ?? '?');
161
+ this.log(`Forked run ${sourceRunId} → branch ${branchRunId}`);
162
+ this.log(`Parallel group: ${r.groupId ?? '?'}`);
163
+ const notCarried = Array.isArray(r.packet?.notCarried)
164
+ ? r.packet.notCarried.map(String).filter(Boolean)
165
+ : [];
166
+ this.log(`Not carried: ${notCarried.length ? notCarried.join(', ') : '(nothing declared)'}`);
167
+ this.log(`Both runs are live and neither is canonical. Pick one with ` +
168
+ `\`${this.config.bin} tasks promote ${taskId} --run <run-id>\` — the losers are cancelled.`);
169
+ }
170
+ }
171
+ exports.default = TasksFork;
@@ -0,0 +1,23 @@
1
+ import { BaseCommand } from '../../base-command';
2
+ /**
3
+ * Whether a run row is a legal handover source — exported for tests.
4
+ * `endedAt` is a defensive OR: a projection that stamps a terminal timestamp
5
+ * without a recognized status is still terminal.
6
+ */
7
+ export declare function isHandoverSourceRun(run: Record<string, unknown>): boolean;
8
+ export default class TasksHandover extends BaseCommand {
9
+ static description: string;
10
+ static examples: string[];
11
+ static args: {
12
+ id: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
13
+ };
14
+ static flags: {
15
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
16
+ task: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
17
+ to: import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
18
+ harness: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
19
+ 'from-run': import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
20
+ reason: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
21
+ };
22
+ run(): Promise<void>;
23
+ }
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isHandoverSourceRun = isHandoverSourceRun;
4
+ const core_1 = require("@oclif/core");
5
+ const data_provider_1 = require("@skrr-ai/data-provider");
6
+ const base_command_1 = require("../../base-command");
7
+ const code_handover_1 = require("../../lib/code-handover");
8
+ const task_extras_1 = require("../../lib/task-extras");
9
+ const task_resolver_1 = require("../../lib/task-resolver");
10
+ const format_1 = require("../../lib/format");
11
+ /**
12
+ * `skrr tasks handover` — continue a task's work under a different agent or a
13
+ * different execution target, from where a finished run left off.
14
+ *
15
+ * Distinct from `tasks retry` (same executor, same authority, starts the task
16
+ * over) and `tasks assign` (a prerequisite consent change, not the act). A
17
+ * handover admits a NEW run resumed from the source run's Resume Point, so the
18
+ * ledger keeps both halves: the old run as immutable evidence, the new one as
19
+ * the visible continuation.
20
+ *
21
+ * The two axes are independent (docs/architecture/execution-handover-2026-09-17.md
22
+ * §1): `--to` changes WHO runs, `--harness` changes WHERE it runs. The server
23
+ * contract is B1 — `POST /api/tasks/:taskId/runs/:runId/handover`.
24
+ *
25
+ * What does not move is declared, not implied: the response's `packet.notCarried`
26
+ * names what stayed behind (provider session, agent-private memory, local
27
+ * machine state) and is printed verbatim rather than paraphrased.
28
+ */
29
+ /** The run statuses from which a handover can source a Resume Point. */
30
+ const HANDOVER_SOURCE_RUN_STATUSES = new Set([
31
+ 'succeeded',
32
+ 'failed',
33
+ 'cancelled',
34
+ // Contract B1 admits "terminal or parked". `parked` is not a
35
+ // TASK_EXECUTION_RUN_STATUSES value today, but the contract names it — a run
36
+ // parked awaiting runtime is exactly the handover case, so accept the word
37
+ // when a newer server starts sending it.
38
+ 'parked',
39
+ ]);
40
+ /**
41
+ * Whether a run row is a legal handover source — exported for tests.
42
+ * `endedAt` is a defensive OR: a projection that stamps a terminal timestamp
43
+ * without a recognized status is still terminal.
44
+ */
45
+ function isHandoverSourceRun(run) {
46
+ const status = typeof run.status === 'string' ? run.status : '';
47
+ if (HANDOVER_SOURCE_RUN_STATUSES.has(status))
48
+ return true;
49
+ return typeof run.endedAt === 'string' && run.endedAt !== '';
50
+ }
51
+ class TasksHandover extends base_command_1.BaseCommand {
52
+ static description = 'Hand a task run over to a different agent or execution target, resumed from its Resume Point';
53
+ static examples = [
54
+ '<%= config.bin %> tasks handover <task-id> --to "Codex Agent"',
55
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --harness <target-ref>',
56
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --from-run <run-id> --reason "claude quota exhausted until Sep 19"',
57
+ '<%= config.bin %> tasks handover <task-id> --to agent_abc --json',
58
+ ];
59
+ static args = {
60
+ id: core_1.Args.string({
61
+ description: 'Task ID; defaults to OVERSKY_SESSION_TASK_ID in task runtimes',
62
+ required: false,
63
+ ignoreStdin: true,
64
+ }),
65
+ };
66
+ static flags = {
67
+ json: core_1.Flags.boolean({ description: 'Output the {run, packet} response as JSON' }),
68
+ task: core_1.Flags.string({ description: 'Task ID; overrides daemon runtime context' }),
69
+ to: core_1.Flags.string({
70
+ description: 'Agent that takes over — id or name',
71
+ required: true,
72
+ }),
73
+ harness: core_1.Flags.string({
74
+ description: 'Execution target ref for the new run. Default: the TARGET agent\u2019s own execution ' +
75
+ 'policy chain — a handover re-runs the chain, it does not inherit the failed run\u2019s ' +
76
+ 'resolved target.',
77
+ }),
78
+ 'from-run': core_1.Flags.string({
79
+ description: 'Source run to resume from (default: the task\u2019s latest terminal or parked run)',
80
+ }),
81
+ reason: core_1.Flags.string({
82
+ description: 'Why the work is moving — recorded in the handover packet provenance',
83
+ }),
84
+ };
85
+ async run() {
86
+ this.requireAuth();
87
+ const { args, flags } = await this.parse(TasksHandover);
88
+ let taskIdentity;
89
+ try {
90
+ taskIdentity = (0, task_resolver_1.resolveTaskIdentity)({ positionalId: args.id, flagTask: flags.task });
91
+ }
92
+ catch (err) {
93
+ const failure = (0, task_resolver_1.taskResolutionFailure)(err);
94
+ this.failWithCliError({
95
+ message: failure.message,
96
+ code: failure.code,
97
+ exit: 2,
98
+ retryable: false,
99
+ details: { suggestion: failure.suggestion, context: failure.details },
100
+ });
101
+ }
102
+ const taskId = taskIdentity.id;
103
+ // ── Target agent: id or name, resolved like `code handover --to`. ──────
104
+ let agents = [];
105
+ try {
106
+ const resp = await data_provider_1.dataService.listAgents({ requiredPermission: data_provider_1.PermissionBits.VIEW });
107
+ agents = (resp?.data ?? []).filter(Boolean);
108
+ }
109
+ catch (err) {
110
+ this.handleApiError(err);
111
+ }
112
+ const matched = (0, code_handover_1.matchAgent)(flags.to, agents);
113
+ if (matched.kind === 'none') {
114
+ this.error(`No agent matches "${flags.to}". Run \`${this.config.bin} agents list\` to see the agents you can use.`);
115
+ }
116
+ if (matched.kind === 'ambiguous') {
117
+ this.error(`"${flags.to}" matches ${matched.candidates.length} agents. Pass the agent id instead:\n` +
118
+ matched.candidates.map((a) => ` ${a.id} ${a.name ?? ''}`).join('\n'));
119
+ }
120
+ const target = matched.agent;
121
+ // ── Source run: --from-run, else the latest terminal/parked run. ───────
122
+ let sourceRun = null;
123
+ if (flags['from-run']) {
124
+ sourceRun = { runId: flags['from-run'] };
125
+ }
126
+ else {
127
+ let runs = [];
128
+ try {
129
+ const resp = await data_provider_1.dataService.getTaskRuns(taskId);
130
+ runs = (resp?.data ?? []);
131
+ }
132
+ catch (err) {
133
+ this.handleApiError(err);
134
+ }
135
+ // `listRuns` is newest-first (admittedAt desc), so the first match IS the
136
+ // latest terminal run.
137
+ sourceRun = runs.find(isHandoverSourceRun) ?? null;
138
+ if (!sourceRun) {
139
+ this.failWithCliError({
140
+ message: `Task ${taskId} has no terminal or parked run to hand over from — a handover ` +
141
+ `resumes from a finished run's Resume Point, and there is nothing to resume. ` +
142
+ `Run \`${this.config.bin} tasks runs ${taskId}\` to see the ledger.`,
143
+ code: 'HANDOVER_NO_SOURCE_RUN',
144
+ exit: 2,
145
+ retryable: false,
146
+ });
147
+ }
148
+ }
149
+ const sourceRunId = String(sourceRun.runId ?? sourceRun.id ?? '');
150
+ // ── B1. ────────────────────────────────────────────────────────────────
151
+ let result;
152
+ try {
153
+ result = await (0, task_extras_1.taskRunHandover)(taskId, sourceRunId, {
154
+ targetAgentId: target.id,
155
+ ...(flags.harness ? { executionTargetRef: (0, task_extras_1.parseExecutionTargetRef)(flags.harness) } : {}),
156
+ ...(flags.reason ? { reason: flags.reason } : {}),
157
+ });
158
+ }
159
+ catch (err) {
160
+ const refusal = (0, base_command_1.apiRefusal)(err);
161
+ // The consent gate, named: the target must already be an assignee —
162
+ // the API never bundles a silent assign (impl contract B1). Turn the
163
+ // refusal into the two-step the operator actually has to run.
164
+ if (refusal.code?.toLowerCase() === 'target_not_assignee') {
165
+ this.failWithCliError({
166
+ message: (refusal.message ?? `Agent ${flags.to} may not work this task.`) +
167
+ `\n → assign it first: \`${this.config.bin} tasks assign ${taskId} --agent "${flags.to}"\` ` +
168
+ `(or \`${this.config.bin} tasks update ${taskId} --add-assignee-agent <agent-id>\`), ` +
169
+ 'then re-run this handover.',
170
+ code: refusal.code,
171
+ exit: 3,
172
+ retryable: false,
173
+ details: refusal.body,
174
+ });
175
+ }
176
+ this.handleApiError(err, { subject: `Task ${taskId}` });
177
+ }
178
+ if (flags.json) {
179
+ // {run, packet} verbatim — the packet IS the audit record, so a scripted
180
+ // caller gets exactly what the server stamped.
181
+ this.log(JSON.stringify(result, null, 2));
182
+ return;
183
+ }
184
+ const r = result;
185
+ const newRun = r.run ?? {};
186
+ const newRunId = String(newRun.runId ?? newRun.id ?? '?');
187
+ const resumePointId = typeof newRun.resumedFromResumePointId === 'string'
188
+ ? newRun.resumedFromResumePointId
189
+ : undefined;
190
+ const agentLabel = target.name ? `${target.name} (${target.id})` : target.id;
191
+ this.log(`Handed run ${sourceRunId} over to ${agentLabel}`);
192
+ this.log(`New run: ${newRunId}` +
193
+ `${newRun.startedAt || newRun.admittedAt ? ` at ${(0, format_1.localTime)(newRun.startedAt ?? newRun.admittedAt)}` : ''}`);
194
+ this.log(`Source run: ${sourceRunId}`);
195
+ this.log(`Resume point: ${resumePointId ?? 'none recorded'}`);
196
+ // Required, not optional (design §4.1): a packet that says what it left
197
+ // behind is the difference between a contract and a guess. An absent or
198
+ // empty list is printed as undeclared rather than silently dropped.
199
+ const notCarried = Array.isArray(r.packet?.notCarried)
200
+ ? r.packet.notCarried.map(String).filter(Boolean)
201
+ : [];
202
+ this.log(`Not carried: ${notCarried.length ? notCarried.join(', ') : '(nothing declared)'}`);
203
+ }
204
+ }
205
+ exports.default = TasksHandover;
@@ -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
  }