@skrr-ai/cli 0.1.44 → 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.
- 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/agents/chat.js +3 -4
- package/dist/commands/browser/cloud-agents.d.ts +20 -0
- package/dist/commands/browser/cloud-agents.js +43 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +105 -1
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/followups/list.js +1 -1
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/initiatives/list.d.ts +1 -0
- package/dist/commands/initiatives/list.js +11 -2
- package/dist/commands/labels/list.d.ts +28 -0
- package/dist/commands/labels/list.js +50 -32
- 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/agentic-stream.js +41 -2
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/auth-core-init.js +11 -4
- package/dist/lib/code-handover.d.ts +60 -0
- package/dist/lib/code-handover.js +120 -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/initiatives.d.ts +15 -3
- package/dist/lib/label-ref.d.ts +2 -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/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +169 -23
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +91 -5
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +7 -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 +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +26 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/profileStateDir.js +346 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +29 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +169 -24
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +93 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +8 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +7 -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 +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loginLocalhost.js +177 -26
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.d.ts +119 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/profileStateDir.js +333 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2746 -2711
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34300 -33843
- package/package.json +2 -2
|
@@ -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;
|
|
@@ -678,18 +678,51 @@ class AgenticStreamClient {
|
|
|
678
678
|
if (this.closed)
|
|
679
679
|
return;
|
|
680
680
|
this.emit({ ...event, type: 'permission.requested' });
|
|
681
|
-
if (!this.options.approveTools || !
|
|
681
|
+
if (!this.options.approveTools || !this.socket) {
|
|
682
682
|
if (event.toolUseId) {
|
|
683
683
|
this.socket?.emit('permission:deny', {
|
|
684
684
|
sessionId: this.options.sessionId,
|
|
685
685
|
toolUseId: event.toolUseId,
|
|
686
686
|
toolName: event.toolName,
|
|
687
|
-
reason: 'CLI
|
|
687
|
+
reason: 'CLI was not started with --approve-tools',
|
|
688
688
|
});
|
|
689
689
|
}
|
|
690
690
|
this.emit({ ...event, type: 'permission.denied', reason: 'noninteractive_or_not_enabled' });
|
|
691
691
|
return;
|
|
692
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* With no terminal, `--approve-tools` IS the answer (OSK-10323).
|
|
695
|
+
*
|
|
696
|
+
* The flag used to be refused outside a TTY, and without it every
|
|
697
|
+
* permission-gated tool call was denied. So a scripted turn — the
|
|
698
|
+
* documented headless shape, `agents chat --prompt … --no-stream --json` —
|
|
699
|
+
* had `schedule_followup`, `followup_create`, `followup_list` and
|
|
700
|
+
* `followup_resolve` all rejected at the approval layer, and the agent
|
|
701
|
+
* reported its own tools as broken. No caller could answer, because the
|
|
702
|
+
* only way to answer needed a terminal.
|
|
703
|
+
*
|
|
704
|
+
* A person typing the flag is the consent, given once for this turn instead
|
|
705
|
+
* of once per call. That is a narrower grant than it sounds: it lasts one
|
|
706
|
+
* turn, it is written in the command the person ran, and every approval is
|
|
707
|
+
* announced on the stream (`auto: true`) rather than being silent. An
|
|
708
|
+
* explicit `input` still means a terminal is answering, and keeps the
|
|
709
|
+
* prompt.
|
|
710
|
+
*/
|
|
711
|
+
const interactive = this.options.input !== undefined || Boolean(process.stdin.isTTY);
|
|
712
|
+
if (!interactive) {
|
|
713
|
+
this.socket.emit('permission:approve', {
|
|
714
|
+
sessionId: this.options.sessionId,
|
|
715
|
+
toolUseId: event.toolUseId,
|
|
716
|
+
toolName: event.toolName,
|
|
717
|
+
});
|
|
718
|
+
this.emit({
|
|
719
|
+
...event,
|
|
720
|
+
type: 'permission.approved',
|
|
721
|
+
auto: true,
|
|
722
|
+
reason: 'approved by --approve-tools, which was given with no terminal to ask at',
|
|
723
|
+
});
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
693
726
|
const answer = await this.ask(`Allow tool ${event.toolName ?? 'unknown'}? [y/N] `);
|
|
694
727
|
const approved = /^y(es)?$/i.test(answer.trim());
|
|
695
728
|
this.socket.emit(approved ? 'permission:approve' : 'permission:deny', {
|
|
@@ -837,6 +870,12 @@ class AgenticStreamClient {
|
|
|
837
870
|
output.write(`\n[${event.type}]\n`);
|
|
838
871
|
this.renderedText = this.accumulated;
|
|
839
872
|
}
|
|
873
|
+
else if (event.type === 'permission.approved' && event.auto === true) {
|
|
874
|
+
// Said out loud, because nobody was asked: the flag approved it, and a
|
|
875
|
+
// reader of this log should see which tools that covered (OSK-10323).
|
|
876
|
+
output.write(`\n[permission] ${String(event.toolName ?? 'unknown')} allowed by --approve-tools\n`);
|
|
877
|
+
this.renderedText = this.accumulated;
|
|
878
|
+
}
|
|
840
879
|
else if (event.type === 'question.answered' && event.nonInteractive) {
|
|
841
880
|
output.write('\n[question] no terminal to answer on, so the question was dismissed unanswered\n');
|
|
842
881
|
this.renderedText = this.accumulated;
|
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.
|
|
@@ -35,7 +35,6 @@ exports.shouldUseAccessTokenForEnrollment = shouldUseAccessTokenForEnrollment;
|
|
|
35
35
|
* present — short-circuits silently on a fresh install before login.
|
|
36
36
|
*/
|
|
37
37
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
38
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
39
38
|
const node_path_1 = __importDefault(require("node:path"));
|
|
40
39
|
const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
|
|
41
40
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
@@ -86,7 +85,7 @@ async function prepareDedicatedRuntimeAuthGeneration(options = {}) {
|
|
|
86
85
|
const machineUuid = String(options.machineUuid ?? process.env.SKRR_MACHINE_UUID ?? '').trim();
|
|
87
86
|
if (!machineUuid.startsWith('dedicated-runtime:'))
|
|
88
87
|
return false;
|
|
89
|
-
const configDir = options.configDir ??
|
|
88
|
+
const configDir = options.configDir ?? (0, auth_core_1.resolveConfigRoot)();
|
|
90
89
|
node_fs_1.default.mkdirSync(configDir, { recursive: true, mode: 0o700 });
|
|
91
90
|
const markerPath = node_path_1.default.join(configDir, DEDICATED_AUTH_GENERATION_MARKER);
|
|
92
91
|
const release = await proper_lockfile_1.default.lock(configDir, {
|
|
@@ -145,7 +144,15 @@ function ensureAuthCoreConfigured() {
|
|
|
145
144
|
(0, auth_core_1.configureAuthCore)({
|
|
146
145
|
// Phase D: share the daemon's config root so auth.lock and the
|
|
147
146
|
// needs-reauth flag are coordinated across binaries.
|
|
148
|
-
|
|
147
|
+
//
|
|
148
|
+
// `resolveConfigRoot()`, not a fourth derivation of `~/.skrr`. This line
|
|
149
|
+
// used to compute the root itself and therefore ignored SKRR_CONFIG_DIR /
|
|
150
|
+
// OVERSKY_CONFIG_DIR — which `cli/src/lib/config.ts` honours — so an
|
|
151
|
+
// operator who relocated the root split the CLI's own credentials from its
|
|
152
|
+
// own config, and split both from the daemon, which honours it too. That is
|
|
153
|
+
// the exact defect `configRoot.ts` was written to end; see its header for
|
|
154
|
+
// the three times it was fixed locally and came back.
|
|
155
|
+
configDir: () => (0, auth_core_1.resolveConfigRoot)(),
|
|
149
156
|
/**
|
|
150
157
|
* The machine root, PINNED — never the active profile's directory.
|
|
151
158
|
*
|
|
@@ -165,7 +172,7 @@ function ensureAuthCoreConfigured() {
|
|
|
165
172
|
* Wired ahead of the profile work rather than with it, so the landmine is
|
|
166
173
|
* gone before anything can step on it.
|
|
167
174
|
*/
|
|
168
|
-
machineConfigDir: () =>
|
|
175
|
+
machineConfigDir: () => (0, auth_core_1.resolveConfigRoot)(),
|
|
169
176
|
logger: cliLogger,
|
|
170
177
|
killSwitches: {
|
|
171
178
|
// CLI shares the same kill-switch surface as the daemon for ops parity.
|
|
@@ -41,6 +41,12 @@ export interface HandoverPromptInput {
|
|
|
41
41
|
headSha?: string | null;
|
|
42
42
|
recentCommits?: string | null;
|
|
43
43
|
localWorkNotCarried: boolean;
|
|
44
|
+
/**
|
|
45
|
+
* A `renderSessionTranscript` block — the exported engine session, carried
|
|
46
|
+
* by `--carry-session`. Pre-rendered so this function stays a composer: it
|
|
47
|
+
* owns ordering, the renderer owns the envelope and the budget.
|
|
48
|
+
*/
|
|
49
|
+
sessionTranscript?: string;
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* Compose what the receiving agent reads.
|
|
@@ -51,6 +57,60 @@ export interface HandoverPromptInput {
|
|
|
51
57
|
* is machine-collected context the agent would otherwise have to ask for.
|
|
52
58
|
*/
|
|
53
59
|
export declare function composeHandoverPrompt(input: HandoverPromptInput): string;
|
|
60
|
+
/** The contract envelope, read defensively — C2 pins the keys, not the innards. */
|
|
61
|
+
export interface EngineSessionExport {
|
|
62
|
+
version?: unknown;
|
|
63
|
+
sessionId?: string;
|
|
64
|
+
messages?: unknown[];
|
|
65
|
+
/** Explicit, never inferred from length (C3). */
|
|
66
|
+
truncated?: boolean;
|
|
67
|
+
/** C4: anything the engine redacted, declared in the envelope. */
|
|
68
|
+
redactions?: unknown;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Parse the export envelope, or null when the output is not the contract —
|
|
72
|
+
* an engine that predates `session export` cannot produce `{version, messages[]}`.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseSessionExportEnvelope(stdout: string): EngineSessionExport | null;
|
|
75
|
+
export type SessionExportFailureKind = 'unsupported' | 'unknown-session';
|
|
76
|
+
/**
|
|
77
|
+
* Classify a failed `session export` spawn (an `execFileSync` throw).
|
|
78
|
+
*
|
|
79
|
+
* C6 makes an unknown session id a distinct outcome from a broken engine, and
|
|
80
|
+
* the distinction matters to the operator: "the engine predates the contract"
|
|
81
|
+
* is an upgrade, "that session is not there" is a different id. The contract
|
|
82
|
+
* does not pin exit codes, so the signal is the engine's own stderr — which is
|
|
83
|
+
* also what the refusal relays verbatim either way.
|
|
84
|
+
*/
|
|
85
|
+
export declare function classifySessionExportError(err: {
|
|
86
|
+
status?: number | null;
|
|
87
|
+
stderr?: string;
|
|
88
|
+
message?: string;
|
|
89
|
+
}): {
|
|
90
|
+
kind: SessionExportFailureKind;
|
|
91
|
+
detail: string;
|
|
92
|
+
};
|
|
93
|
+
export interface SessionTranscriptRender {
|
|
94
|
+
/** The block to fold into the handover prompt. */
|
|
95
|
+
text: string;
|
|
96
|
+
carriedCount: number;
|
|
97
|
+
totalCount: number;
|
|
98
|
+
/** The engine declared the export itself incomplete (C3). */
|
|
99
|
+
engineTruncated: boolean;
|
|
100
|
+
/** This side dropped oldest messages to fit the prompt budget — declared. */
|
|
101
|
+
clipped: boolean;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Render the exported session into the handover prompt, bounded.
|
|
105
|
+
*
|
|
106
|
+
* `POST /api/cloud-coding/tasks` has no transcript field — `prompt` is the only
|
|
107
|
+
* channel that reaches the receiving agent (the zod schema strips unknown keys
|
|
108
|
+
* and caps prompt at 50000 chars). So the transcript rides inside the prompt,
|
|
109
|
+
* clipped from the FRONT: the recent tail is what continuation needs, and the
|
|
110
|
+
* clip is declared in the block itself, never inferred (C3's rule applied to
|
|
111
|
+
* our own truncation too).
|
|
112
|
+
*/
|
|
113
|
+
export declare function renderSessionTranscript(exported: EngineSessionExport, budget: number): SessionTranscriptRender;
|
|
54
114
|
export interface AgentCandidate {
|
|
55
115
|
id: string;
|
|
56
116
|
name?: string;
|