@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.
- package/dist/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/browser/cloud-agents.js +4 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +105 -1
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/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/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/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 +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +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 +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2242 -2234
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +35697 -35300
- package/package.json +2 -2
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.taskPointerLines = taskPointerLines;
|
|
4
|
+
exports.blockerDetailLines = blockerDetailLines;
|
|
4
5
|
exports.labelsLine = labelsLine;
|
|
5
6
|
exports.taskExecutorLabel = taskExecutorLabel;
|
|
6
7
|
exports.taskFailureLines = taskFailureLines;
|
|
@@ -52,6 +53,29 @@ function taskPointerLines(t, ref, bin = 'skrr') {
|
|
|
52
53
|
* be read (or a label missing from it) degrades per entry rather than failing
|
|
53
54
|
* the command or hiding the label.
|
|
54
55
|
*/
|
|
56
|
+
/**
|
|
57
|
+
* The lines that say what a blocked Task is waiting on.
|
|
58
|
+
*
|
|
59
|
+
* Kept pure so the rule is testable without a network: the command fetches the
|
|
60
|
+
* newest `blocker` Update and hands its payload here.
|
|
61
|
+
*/
|
|
62
|
+
function blockerDetailLines(detail) {
|
|
63
|
+
if (!detail)
|
|
64
|
+
return [];
|
|
65
|
+
const lines = [];
|
|
66
|
+
const cause = typeof detail.cause === 'string' ? detail.cause.trim() : '';
|
|
67
|
+
const required = typeof detail.requiredAction === 'string' ? detail.requiredAction.trim() : '';
|
|
68
|
+
const owner = detail.nextOwner;
|
|
69
|
+
if (cause)
|
|
70
|
+
lines.push(`Blocked on: ${cause}`);
|
|
71
|
+
if (required)
|
|
72
|
+
lines.push(`Needs: ${required}`);
|
|
73
|
+
if (owner && typeof owner.kind === 'string' && owner.kind) {
|
|
74
|
+
const who = typeof owner.userId === 'string' && owner.userId ? ` ${owner.userId}` : '';
|
|
75
|
+
lines.push(`Next owner: ${owner.kind}${who}`);
|
|
76
|
+
}
|
|
77
|
+
return lines;
|
|
78
|
+
}
|
|
55
79
|
function labelsLine(labelIds, nameById) {
|
|
56
80
|
const ids = (labelIds ?? []).filter((id) => typeof id === 'string' && id.length > 0);
|
|
57
81
|
if (ids.length === 0)
|
|
@@ -370,6 +394,7 @@ class TasksShow extends base_command_1.BaseCommand {
|
|
|
370
394
|
if (actionability) {
|
|
371
395
|
this.log(`Next action: ${actionability.nextActor ?? '-'} · ${actionability.state ?? '-'} · ${actionability.reasonCode ?? '-'}`);
|
|
372
396
|
this.log(`Why: ${actionability.reason ?? '-'}`);
|
|
397
|
+
await this.printBlockerDetail(t, actionability);
|
|
373
398
|
}
|
|
374
399
|
const delivery = t.delivery;
|
|
375
400
|
if (delivery?.prNumber || delivery?.prUrl || delivery?.branch) {
|
|
@@ -493,5 +518,39 @@ class TasksShow extends base_command_1.BaseCommand {
|
|
|
493
518
|
for (const line of taskPointerLines(t, ref, this.config.bin))
|
|
494
519
|
this.log(line);
|
|
495
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* What a blocked Task is actually waiting on.
|
|
523
|
+
*
|
|
524
|
+
* The workflow tells an agent to record a typed `blocker` Update —
|
|
525
|
+
* `{cause, requiredAction, nextOwner}` — and only then move the Task to
|
|
526
|
+
* blocked. The move writes its own exceptional hold, whose summary is the
|
|
527
|
+
* `--reason` flag or the literal fallback `Task was marked blocked`, and that
|
|
528
|
+
* is what `Why:` prints. So the one line a reader looks at restated the
|
|
529
|
+
* status, while the thing the writer was told to record sat one command away
|
|
530
|
+
* in `tasks updates list` (OSK-10391).
|
|
531
|
+
*
|
|
532
|
+
* Read only for a blocked Task, and only the newest blocker Update. A failure
|
|
533
|
+
* here prints nothing: this decorates an answer the caller already has, and
|
|
534
|
+
* must never be the reason `tasks show` cannot answer at all.
|
|
535
|
+
*/
|
|
536
|
+
async printBlockerDetail(task, actionability) {
|
|
537
|
+
const looksBlocked = String(task.status ?? '') === 'blocked' || actionability.reasonCode === 'exceptional_hold';
|
|
538
|
+
if (!looksBlocked) {
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
try {
|
|
542
|
+
const updates = await data_provider_1.dataService.listTaskUpdates(String(task.id), {
|
|
543
|
+
limit: 1,
|
|
544
|
+
type: 'blocker',
|
|
545
|
+
});
|
|
546
|
+
const detail = updates?.data?.[0]?.data;
|
|
547
|
+
for (const line of blockerDetailLines(detail)) {
|
|
548
|
+
this.log(line);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
catch {
|
|
552
|
+
// The status line above already answered the question this decorates.
|
|
553
|
+
}
|
|
554
|
+
}
|
|
496
555
|
}
|
|
497
556
|
exports.default = TasksShow;
|
package/dist/lib/api-fetch.d.ts
CHANGED
|
@@ -30,7 +30,13 @@ export declare class ApiFetchError extends Error {
|
|
|
30
30
|
readonly body?: string;
|
|
31
31
|
readonly code?: string;
|
|
32
32
|
readonly method?: string;
|
|
33
|
-
|
|
33
|
+
/**
|
|
34
|
+
* The server's `X-Request-ID` for the failing call, when it sent one. It is
|
|
35
|
+
* the id the server logs against, so a refusal can be looked up rather than
|
|
36
|
+
* attributed by guesswork — see `node-adapter.ts`.
|
|
37
|
+
*/
|
|
38
|
+
readonly requestId?: string;
|
|
39
|
+
constructor(message: string, status: number, body?: string, code?: string, method?: string, requestId?: string);
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Call a skrr API endpoint with the current credential. Throws
|
package/dist/lib/api-fetch.js
CHANGED
|
@@ -54,13 +54,20 @@ class ApiFetchError extends Error {
|
|
|
54
54
|
// (OSK-6765): `agents show` is a GET and had no body, and the advice was
|
|
55
55
|
// impossible to follow.
|
|
56
56
|
method;
|
|
57
|
-
|
|
57
|
+
/**
|
|
58
|
+
* The server's `X-Request-ID` for the failing call, when it sent one. It is
|
|
59
|
+
* the id the server logs against, so a refusal can be looked up rather than
|
|
60
|
+
* attributed by guesswork — see `node-adapter.ts`.
|
|
61
|
+
*/
|
|
62
|
+
requestId;
|
|
63
|
+
constructor(message, status, body, code, method, requestId) {
|
|
58
64
|
super(message);
|
|
59
65
|
this.name = 'ApiFetchError';
|
|
60
66
|
this.status = status;
|
|
61
67
|
this.body = body;
|
|
62
68
|
this.code = code;
|
|
63
69
|
this.method = method;
|
|
70
|
+
this.requestId = requestId;
|
|
64
71
|
}
|
|
65
72
|
}
|
|
66
73
|
exports.ApiFetchError = ApiFetchError;
|
|
@@ -209,7 +216,7 @@ async function apiFetch(pathOrUrl, opts = {}) {
|
|
|
209
216
|
catch {
|
|
210
217
|
/* ignore body read failures */
|
|
211
218
|
}
|
|
212
|
-
throw new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method);
|
|
219
|
+
throw new ApiFetchError(`HTTP ${res.status} ${res.statusText} — ${method} ${pathOrUrl}`, res.status, text, code, method, res.headers.get('x-request-id') || undefined);
|
|
213
220
|
}
|
|
214
221
|
// 204/205 carry no body by definition; an empty body has no content-type
|
|
215
222
|
// worth judging. Neither is an error.
|
|
@@ -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;
|
|
@@ -12,6 +12,9 @@ exports.parseGitHubRepo = parseGitHubRepo;
|
|
|
12
12
|
exports.evaluateHandoverSafety = evaluateHandoverSafety;
|
|
13
13
|
exports.formatUnsafeHandover = formatUnsafeHandover;
|
|
14
14
|
exports.composeHandoverPrompt = composeHandoverPrompt;
|
|
15
|
+
exports.parseSessionExportEnvelope = parseSessionExportEnvelope;
|
|
16
|
+
exports.classifySessionExportError = classifySessionExportError;
|
|
17
|
+
exports.renderSessionTranscript = renderSessionTranscript;
|
|
15
18
|
exports.matchAgent = matchAgent;
|
|
16
19
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
17
20
|
/** `owner/name` from any GitHub remote URL shape. */
|
|
@@ -72,6 +75,7 @@ function composeHandoverPrompt(input) {
|
|
|
72
75
|
'---',
|
|
73
76
|
`Handed over from a local ${auth_core_1.FIRST_PARTY_HARNESS.displayName} session in ${input.repoFullName} on ${input.branch}${at}.`,
|
|
74
77
|
input.recentCommits ? `Recent commits:\n${input.recentCommits}` : null,
|
|
78
|
+
input.sessionTranscript ?? null,
|
|
75
79
|
input.localWorkNotCarried
|
|
76
80
|
? 'NOTE: local uncommitted or unpushed work was NOT carried over — you are working from the pushed state of this branch.'
|
|
77
81
|
: null,
|
|
@@ -79,6 +83,122 @@ function composeHandoverPrompt(input) {
|
|
|
79
83
|
.filter(Boolean)
|
|
80
84
|
.join('\n');
|
|
81
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Parse the export envelope, or null when the output is not the contract —
|
|
88
|
+
* an engine that predates `session export` cannot produce `{version, messages[]}`.
|
|
89
|
+
*/
|
|
90
|
+
function parseSessionExportEnvelope(stdout) {
|
|
91
|
+
let parsed;
|
|
92
|
+
try {
|
|
93
|
+
parsed = JSON.parse(stdout);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
99
|
+
return null;
|
|
100
|
+
const env = parsed;
|
|
101
|
+
if (env.version === undefined || env.version === null)
|
|
102
|
+
return null;
|
|
103
|
+
if (!Array.isArray(env.messages))
|
|
104
|
+
return null;
|
|
105
|
+
return env;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Classify a failed `session export` spawn (an `execFileSync` throw).
|
|
109
|
+
*
|
|
110
|
+
* C6 makes an unknown session id a distinct outcome from a broken engine, and
|
|
111
|
+
* the distinction matters to the operator: "the engine predates the contract"
|
|
112
|
+
* is an upgrade, "that session is not there" is a different id. The contract
|
|
113
|
+
* does not pin exit codes, so the signal is the engine's own stderr — which is
|
|
114
|
+
* also what the refusal relays verbatim either way.
|
|
115
|
+
*/
|
|
116
|
+
function classifySessionExportError(err) {
|
|
117
|
+
const stderr = (err.stderr ?? '').trim();
|
|
118
|
+
// "session <id> not found" is an unknown session; "unknown command: session"
|
|
119
|
+
// is an engine that predates the contract. Both contain the words 'session'
|
|
120
|
+
// and 'unknown', so the command-not-understood phrasing is excluded FIRST.
|
|
121
|
+
const commandNotUnderstood = /unknown (command|subcommand)|unrecognized|not a valid|did you mean|usage:/i.test(stderr);
|
|
122
|
+
const sessionMissing = /(no such|cannot find|could not find|not found|does not exist|unknown)[\s\S]{0,60}session/i.test(stderr) || /session[\s\S]{0,60}(no such|not found|does not exist|missing|unknown)/i.test(stderr);
|
|
123
|
+
if (!commandNotUnderstood && sessionMissing) {
|
|
124
|
+
return { kind: 'unknown-session', detail: stderr || (err.message ?? '') };
|
|
125
|
+
}
|
|
126
|
+
return { kind: 'unsupported', detail: stderr || err.message || 'session export failed' };
|
|
127
|
+
}
|
|
128
|
+
/** Best-effort text of one exported message, tolerant of the fork's shape. */
|
|
129
|
+
function sessionMessageText(message) {
|
|
130
|
+
const m = (message ?? {});
|
|
131
|
+
const role = typeof m.role === 'string' && m.role ? m.role : 'message';
|
|
132
|
+
const content = m.content ?? m.text;
|
|
133
|
+
if (typeof content === 'string')
|
|
134
|
+
return { role, text: content };
|
|
135
|
+
if (Array.isArray(content)) {
|
|
136
|
+
const parts = content
|
|
137
|
+
.map((p) => p && typeof p === 'object' && typeof p.text === 'string'
|
|
138
|
+
? p.text
|
|
139
|
+
: '')
|
|
140
|
+
.filter(Boolean);
|
|
141
|
+
if (parts.length)
|
|
142
|
+
return { role, text: parts.join('\n') };
|
|
143
|
+
}
|
|
144
|
+
// A message with no readable text is still a fact of the transcript; keep a
|
|
145
|
+
// marker rather than silently dropping it (the C4 honesty rule).
|
|
146
|
+
return { role, text: '[non-text content]' };
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Render the exported session into the handover prompt, bounded.
|
|
150
|
+
*
|
|
151
|
+
* `POST /api/cloud-coding/tasks` has no transcript field — `prompt` is the only
|
|
152
|
+
* channel that reaches the receiving agent (the zod schema strips unknown keys
|
|
153
|
+
* and caps prompt at 50000 chars). So the transcript rides inside the prompt,
|
|
154
|
+
* clipped from the FRONT: the recent tail is what continuation needs, and the
|
|
155
|
+
* clip is declared in the block itself, never inferred (C3's rule applied to
|
|
156
|
+
* our own truncation too).
|
|
157
|
+
*/
|
|
158
|
+
function renderSessionTranscript(exported, budget) {
|
|
159
|
+
const messages = exported.messages ?? [];
|
|
160
|
+
const rendered = messages.map((m) => {
|
|
161
|
+
const { role, text } = sessionMessageText(m);
|
|
162
|
+
return `${role}:\n${text}`;
|
|
163
|
+
});
|
|
164
|
+
const sid = exported.sessionId ? ` ${exported.sessionId}` : '';
|
|
165
|
+
const version = exported.version !== undefined ? `, export v${String(exported.version)}` : '';
|
|
166
|
+
const header = `Carried session transcript (engine session${sid}${version}):`;
|
|
167
|
+
const notes = [];
|
|
168
|
+
if (exported.truncated) {
|
|
169
|
+
notes.push('The engine marked this export TRUNCATED — earlier session content is not in it.');
|
|
170
|
+
}
|
|
171
|
+
if (exported.redactions) {
|
|
172
|
+
notes.push(`The engine declared redactions: ${JSON.stringify(exported.redactions)}`);
|
|
173
|
+
}
|
|
174
|
+
let carried = rendered.length;
|
|
175
|
+
let clipped = false;
|
|
176
|
+
const assemble = () => {
|
|
177
|
+
const clipNote = clipped
|
|
178
|
+
? `(${rendered.length - carried} earlier message(s) clipped to fit the handover prompt budget.)\n`
|
|
179
|
+
: '';
|
|
180
|
+
const body = [
|
|
181
|
+
header,
|
|
182
|
+
...notes,
|
|
183
|
+
clipNote + rendered.slice(rendered.length - carried).join('\n\n'),
|
|
184
|
+
]
|
|
185
|
+
.filter(Boolean)
|
|
186
|
+
.join('\n');
|
|
187
|
+
return body;
|
|
188
|
+
};
|
|
189
|
+
// Drop oldest first — the tail is what a continuation needs.
|
|
190
|
+
while (carried > 0 && assemble().length > budget) {
|
|
191
|
+
carried -= 1;
|
|
192
|
+
clipped = true;
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
text: assemble(),
|
|
196
|
+
carriedCount: carried,
|
|
197
|
+
totalCount: rendered.length,
|
|
198
|
+
engineTruncated: exported.truncated === true,
|
|
199
|
+
clipped,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
82
202
|
/** Resolve `--to` against the agents this user can see: exact id wins, then name. */
|
|
83
203
|
function matchAgent(needle, agents) {
|
|
84
204
|
const query = needle.trim();
|
|
@@ -277,6 +277,18 @@ export declare const commitmentApi: {
|
|
|
277
277
|
executeActionProposal: (id: string, proposalId: string) => Promise<Record<string, unknown>>;
|
|
278
278
|
preflight: (id: string, nextRunCount?: number) => Promise<CommitmentPreflightReport>;
|
|
279
279
|
effectivePolicy: (id: string) => Promise<Record<string, unknown>>;
|
|
280
|
+
/**
|
|
281
|
+
* Executor rebind — `POST /api/commitments/:id/executor` (impl contract B4,
|
|
282
|
+
* docs/architecture/execution-handover-2026-09-17.md P4).
|
|
283
|
+
*
|
|
284
|
+
* `commitment.agentId` is single-valued by design, so moving it is an
|
|
285
|
+
* explicit recorded act: the server appends an `executorHandovers` entry
|
|
286
|
+
* rather than letting a bare PATCH rewrite who owns the loop without a trail.
|
|
287
|
+
*/
|
|
288
|
+
setExecutor: (id: string, body: {
|
|
289
|
+
agentId: string;
|
|
290
|
+
reason?: string;
|
|
291
|
+
}) => Promise<Record<string, unknown>>;
|
|
280
292
|
pause: (id: string) => Promise<Commitment>;
|
|
281
293
|
resume: (id: string) => Promise<Commitment>;
|
|
282
294
|
complete: (id: string) => Promise<Commitment>;
|
|
@@ -557,6 +569,16 @@ export declare function describeAutonomyLine(commitment: CommitmentView, latestC
|
|
|
557
569
|
policyTrace?: Array<Record<string, unknown>>;
|
|
558
570
|
} | null;
|
|
559
571
|
} | null): string;
|
|
572
|
+
/**
|
|
573
|
+
* One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
|
|
574
|
+
* the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
|
|
575
|
+
* provider?}`.
|
|
576
|
+
*
|
|
577
|
+
* Defensive by contract: the field is being added server-side, so its absence
|
|
578
|
+
* is the normal case and means "no information", not "not blocked". Returns
|
|
579
|
+
* null rather than printing a guess.
|
|
580
|
+
*/
|
|
581
|
+
export declare function describeExecutorBlocked(commitment: CommitmentView): string | null;
|
|
560
582
|
export declare function renderCommitmentSummary(commitment: CommitmentView, log: (line: string) => void,
|
|
561
583
|
/**
|
|
562
584
|
* The most recent check, when the caller has one. Optional because pause /
|
package/dist/lib/commitments.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.renderCommitmentList = renderCommitmentList;
|
|
|
19
19
|
exports.effectiveAutonomyMode = effectiveAutonomyMode;
|
|
20
20
|
exports.extractStandstillFromCheck = extractStandstillFromCheck;
|
|
21
21
|
exports.describeAutonomyLine = describeAutonomyLine;
|
|
22
|
+
exports.describeExecutorBlocked = describeExecutorBlocked;
|
|
22
23
|
exports.renderCommitmentSummary = renderCommitmentSummary;
|
|
23
24
|
exports.describeWakeDecision = describeWakeDecision;
|
|
24
25
|
exports.renderCheckList = renderCheckList;
|
|
@@ -176,6 +177,15 @@ exports.commitmentApi = {
|
|
|
176
177
|
...(typeof nextRunCount === 'number' ? { nextRunCount } : {}),
|
|
177
178
|
}),
|
|
178
179
|
effectivePolicy: (id) => data_provider_1.request.get(`${base(id)}/effective-policy`),
|
|
180
|
+
/**
|
|
181
|
+
* Executor rebind — `POST /api/commitments/:id/executor` (impl contract B4,
|
|
182
|
+
* docs/architecture/execution-handover-2026-09-17.md P4).
|
|
183
|
+
*
|
|
184
|
+
* `commitment.agentId` is single-valued by design, so moving it is an
|
|
185
|
+
* explicit recorded act: the server appends an `executorHandovers` entry
|
|
186
|
+
* rather than letting a bare PATCH rewrite who owns the loop without a trail.
|
|
187
|
+
*/
|
|
188
|
+
setExecutor: (id, body) => data_provider_1.request.post(`${base(id)}/executor`, body),
|
|
179
189
|
pause: (id) => data_provider_1.request.post(`${base(id)}/pause`, {}),
|
|
180
190
|
resume: (id) => data_provider_1.request.post(`${base(id)}/resume`, {}),
|
|
181
191
|
complete: (id) => data_provider_1.request.post(`${base(id)}/complete`, {}),
|
|
@@ -738,6 +748,30 @@ function describeAutonomyLine(commitment, latestCheck) {
|
|
|
738
748
|
}
|
|
739
749
|
return (0, commitment_product_1.productModeLabel)(stated);
|
|
740
750
|
}
|
|
751
|
+
/**
|
|
752
|
+
* One-line rendering of `commitment.executorBlocked` — the B4/A7 signal that
|
|
753
|
+
* the executor's targets are all unusable: `{reason:'quota'|'offline', until?,
|
|
754
|
+
* provider?}`.
|
|
755
|
+
*
|
|
756
|
+
* Defensive by contract: the field is being added server-side, so its absence
|
|
757
|
+
* is the normal case and means "no information", not "not blocked". Returns
|
|
758
|
+
* null rather than printing a guess.
|
|
759
|
+
*/
|
|
760
|
+
function describeExecutorBlocked(commitment) {
|
|
761
|
+
const blocked = commitment.executorBlocked;
|
|
762
|
+
if (!blocked || typeof blocked !== 'object')
|
|
763
|
+
return null;
|
|
764
|
+
const b = blocked;
|
|
765
|
+
const reason = typeof b.reason === 'string' ? b.reason : '';
|
|
766
|
+
const provider = typeof b.provider === 'string' ? b.provider : '';
|
|
767
|
+
const until = typeof b.until === 'string' ? b.until : '';
|
|
768
|
+
const label = reason === 'quota'
|
|
769
|
+
? `quota exhausted${provider ? ` on ${provider}` : ''}`
|
|
770
|
+
: reason === 'offline'
|
|
771
|
+
? 'executor runtime offline'
|
|
772
|
+
: reason || 'blocked';
|
|
773
|
+
return `${label}${until ? ` until ${until}` : ''}`;
|
|
774
|
+
}
|
|
741
775
|
function renderCommitmentSummary(commitment, log,
|
|
742
776
|
/**
|
|
743
777
|
* The most recent check, when the caller has one. Optional because pause /
|
|
@@ -750,6 +784,14 @@ latestCheck) {
|
|
|
750
784
|
log(`Kind: ${commitment.kind ?? 'achieve'}`);
|
|
751
785
|
log(`Status: ${commitment.status ?? '-'}`);
|
|
752
786
|
log(`Agent: ${commitment.agentId ?? '-'}`);
|
|
787
|
+
// A commitment whose executor cannot run anywhere should say so on the
|
|
788
|
+
// header, not as a silent skip series — "executor cannot run until <date> —
|
|
789
|
+
// reassign or add a fallback target" (design doc P4).
|
|
790
|
+
const executorBlocked = describeExecutorBlocked(commitment);
|
|
791
|
+
if (executorBlocked) {
|
|
792
|
+
log(`Executor: blocked — ${executorBlocked}`);
|
|
793
|
+
log(` Fix: reassign with \`skrr commitments handover ${commitment.id ?? '<id>'} --to <agent>\` or add a fallback target`);
|
|
794
|
+
}
|
|
753
795
|
if (commitment.goalId)
|
|
754
796
|
log(`Goal: ${commitment.goalId}`);
|
|
755
797
|
if (commitment.northstarId)
|
|
@@ -27,9 +27,30 @@ const PLANNED_REPLACEMENT_PROGRESS = {
|
|
|
27
27
|
dedicated_runtime_image_update: 'replacing the machine',
|
|
28
28
|
dedicated_runtime_image_update_rolling_back: 'replacing the machine again',
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* The same two labels, keyed by the image move's OWN status.
|
|
32
|
+
*
|
|
33
|
+
* `stateReason` describes the state a lease is in, so anything that moves it
|
|
34
|
+
* again during the move can leave it behind: a watched `update-image --wait`
|
|
35
|
+
* still printed the unplanned-recovery hint through the `recovering` phase it
|
|
36
|
+
* had itself asked for (OSK-9933, live 2026-09-17, after a provision attempt
|
|
37
|
+
* lost its fence and was retried). `health.image.lastUpdate.status` says the
|
|
38
|
+
* move is under way for as long as it is, which is the question being asked.
|
|
39
|
+
*/
|
|
40
|
+
const IN_FLIGHT_IMAGE_UPDATE_PROGRESS = {
|
|
41
|
+
applying: 'replacing the machine',
|
|
42
|
+
rolling_back: 'replacing the machine again',
|
|
43
|
+
};
|
|
44
|
+
function plannedReplacementProgress(lease) {
|
|
45
|
+
const byReason = lease.stateReason ? PLANNED_REPLACEMENT_PROGRESS[lease.stateReason] : undefined;
|
|
46
|
+
if (byReason)
|
|
47
|
+
return byReason;
|
|
48
|
+
const imageStatus = lease.health?.image?.lastUpdate?.status;
|
|
49
|
+
return imageStatus ? IN_FLIGHT_IMAGE_UPDATE_PROGRESS[imageStatus] : undefined;
|
|
50
|
+
}
|
|
30
51
|
function progressOf(lease) {
|
|
31
52
|
const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
|
|
32
|
-
const planned = lease
|
|
53
|
+
const planned = plannedReplacementProgress(lease);
|
|
33
54
|
if (state === 'recovering' && planned)
|
|
34
55
|
return planned;
|
|
35
56
|
if (state === 'recovering') {
|
package/dist/lib/followups.d.ts
CHANGED
|
@@ -20,6 +20,24 @@ export declare function settledLine(id: string, result: Record<string, unknown>,
|
|
|
20
20
|
export declare const followUpIdArg: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
|
|
21
21
|
export { FOLLOWUP_KINDS, FOLLOWUP_STATES, FOLLOWUP_RESOLVE_OUTCOMES, type FollowUpKind, } from '@skrr-ai/data-provider';
|
|
22
22
|
export declare const WATCH_WAIT_KINDS: readonly ["clock", "event", "poll_until"];
|
|
23
|
+
export type WatchWaitKind = (typeof WATCH_WAIT_KINDS)[number];
|
|
24
|
+
/**
|
|
25
|
+
* OSK-10407 — `--deadline`, `--check-every` and `--max-checks` are predicate-form
|
|
26
|
+
* flags. `skrr followups watch` accepted them on a recipe watch and dropped
|
|
27
|
+
* them, so a caller who wrote `--deadline 3h` was told the watch was armed and
|
|
28
|
+
* held a stopping condition that was never stored.
|
|
29
|
+
*
|
|
30
|
+
* Dropping them is right; dropping them SILENTLY is the defect. Refuse instead,
|
|
31
|
+
* and name the flag that bounds THIS wait kind — a refusal that only says "not
|
|
32
|
+
* here" leaves the caller with no way to express the thing they wanted.
|
|
33
|
+
*
|
|
34
|
+
* @returns the refusal message, or null when nothing predicate-shaped was passed.
|
|
35
|
+
*/
|
|
36
|
+
export declare function recipeWatchBoundsRefusal(waitKind: string | undefined, passed: {
|
|
37
|
+
deadline?: unknown;
|
|
38
|
+
checkEvery?: unknown;
|
|
39
|
+
maxChecks?: unknown;
|
|
40
|
+
}): string | null;
|
|
23
41
|
export interface FollowUpRow {
|
|
24
42
|
id: string;
|
|
25
43
|
kind?: string;
|
package/dist/lib/followups.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.followupsApi = exports.WATCH_WAIT_KINDS = exports.FOLLOWUP_RESOLVE_OUTCOMES = exports.FOLLOWUP_STATES = exports.FOLLOWUP_KINDS = exports.followUpIdArg = void 0;
|
|
4
4
|
exports.settledLine = settledLine;
|
|
5
|
+
exports.recipeWatchBoundsRefusal = recipeWatchBoundsRefusal;
|
|
5
6
|
exports.resolveFollowUpAgent = resolveFollowUpAgent;
|
|
6
7
|
exports.parseSubjectRef = parseSubjectRef;
|
|
7
8
|
exports.followupSummaryLine = followupSummaryLine;
|
|
@@ -71,6 +72,49 @@ Object.defineProperty(exports, "FOLLOWUP_KINDS", { enumerable: true, get: functi
|
|
|
71
72
|
Object.defineProperty(exports, "FOLLOWUP_STATES", { enumerable: true, get: function () { return data_provider_2.FOLLOWUP_STATES; } });
|
|
72
73
|
Object.defineProperty(exports, "FOLLOWUP_RESOLVE_OUTCOMES", { enumerable: true, get: function () { return data_provider_2.FOLLOWUP_RESOLVE_OUTCOMES; } });
|
|
73
74
|
exports.WATCH_WAIT_KINDS = ['clock', 'event', 'poll_until'];
|
|
75
|
+
/**
|
|
76
|
+
* What actually bounds a recipe watch, per wait kind. A recipe watch ends
|
|
77
|
+
* through its `--wait`, never through the predicate form's `--deadline` /
|
|
78
|
+
* `--check-every` / `--max-checks`: the create route stores none of those for
|
|
79
|
+
* `form: 'recipe'`, so the row comes back `deadlineAt: null`.
|
|
80
|
+
*/
|
|
81
|
+
const RECIPE_WATCH_BOUND_BY_WAIT = Object.freeze({
|
|
82
|
+
clock: '--wait-at, the instant it fires',
|
|
83
|
+
event: '--wait-fallback, the SLA backstop for an event that never arrives',
|
|
84
|
+
poll_until: '--wait-max-attempts and --wait-fallback',
|
|
85
|
+
});
|
|
86
|
+
/**
|
|
87
|
+
* OSK-10407 — `--deadline`, `--check-every` and `--max-checks` are predicate-form
|
|
88
|
+
* flags. `skrr followups watch` accepted them on a recipe watch and dropped
|
|
89
|
+
* them, so a caller who wrote `--deadline 3h` was told the watch was armed and
|
|
90
|
+
* held a stopping condition that was never stored.
|
|
91
|
+
*
|
|
92
|
+
* Dropping them is right; dropping them SILENTLY is the defect. Refuse instead,
|
|
93
|
+
* and name the flag that bounds THIS wait kind — a refusal that only says "not
|
|
94
|
+
* here" leaves the caller with no way to express the thing they wanted.
|
|
95
|
+
*
|
|
96
|
+
* @returns the refusal message, or null when nothing predicate-shaped was passed.
|
|
97
|
+
*/
|
|
98
|
+
function recipeWatchBoundsRefusal(waitKind, passed) {
|
|
99
|
+
const offenders = [
|
|
100
|
+
['--deadline', passed.deadline],
|
|
101
|
+
['--check-every', passed.checkEvery],
|
|
102
|
+
['--max-checks', passed.maxChecks],
|
|
103
|
+
]
|
|
104
|
+
.filter(([, value]) => value !== undefined)
|
|
105
|
+
.map(([flag]) => flag);
|
|
106
|
+
if (offenders.length === 0)
|
|
107
|
+
return null;
|
|
108
|
+
const bound = RECIPE_WATCH_BOUND_BY_WAIT[waitKind];
|
|
109
|
+
const list = offenders.join(offenders.length === 2 ? ' and ' : ', ');
|
|
110
|
+
const verb = offenders.length === 1 ? 'bounds' : 'bound';
|
|
111
|
+
return [
|
|
112
|
+
`${list} ${verb} a predicate watch (--url + --until) and ${offenders.length === 1 ? 'is' : 'are'} not stored on a recipe watch.`,
|
|
113
|
+
bound
|
|
114
|
+
? `A recipe watch ends through its --wait ${waitKind}: ${bound}.`
|
|
115
|
+
: 'A recipe watch ends through its --wait.',
|
|
116
|
+
].join(' ');
|
|
117
|
+
}
|
|
74
118
|
/**
|
|
75
119
|
* Which agent a follow-up belongs to.
|
|
76
120
|
*
|
package/dist/lib/node-adapter.js
CHANGED
|
@@ -332,6 +332,12 @@ function createNodeAdapter(opts = {}) {
|
|
|
332
332
|
err.status = res.status;
|
|
333
333
|
err.body = text;
|
|
334
334
|
err.method = method;
|
|
335
|
+
// The server stamps every response with the id it logs against
|
|
336
|
+
// (`requestLogger`). Carrying it means a refused call can be found in
|
|
337
|
+
// the logs instead of guessed at: with several clients on one account,
|
|
338
|
+
// "which of these requests was mine" is otherwise unanswerable, and a
|
|
339
|
+
// 409 that belonged to another session was nearly filed as a CLI bug.
|
|
340
|
+
err.requestId = res.headers.get('x-request-id') || undefined;
|
|
335
341
|
throw err;
|
|
336
342
|
}
|
|
337
343
|
if (options?.responseType === 'arraybuffer') {
|
|
@@ -57,6 +57,50 @@ export declare const taskSelfScheduleApi: {
|
|
|
57
57
|
* parity check.
|
|
58
58
|
*/
|
|
59
59
|
export declare function taskCompletePath(taskId: string): string;
|
|
60
|
+
/**
|
|
61
|
+
* `POST /api/tasks/:taskId/runs/:runId/handover` — the B1 admission path
|
|
62
|
+
* (docs/architecture/execution-handover-impl-contracts.md): mint a new run on a
|
|
63
|
+
* different agent/target, resumed from the source run's Resume Point.
|
|
64
|
+
*
|
|
65
|
+
* `apiFetch` rather than `request.post`: a handover ADMITS a run, so the write
|
|
66
|
+
* needs the shared `X-Idempotency-Key` stamping apiFetch applies — a retried
|
|
67
|
+
* request must not double-admit. Kept in the lib so `domain-api-parity` sees
|
|
68
|
+
* the endpoint — a command that builds its own `/api/tasks/...` string is
|
|
69
|
+
* invisible to the coverage check.
|
|
70
|
+
*/
|
|
71
|
+
export declare function taskRunHandover(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
72
|
+
/**
|
|
73
|
+
* `POST /api/tasks/:taskId/runs/:runId/fork` — B8. Admits a SIBLING run from
|
|
74
|
+
* the same Resume Point, so both attempts are live and neither is canonical
|
|
75
|
+
* until `promote`. Distinct from handover, which continues the work once.
|
|
76
|
+
*/
|
|
77
|
+
export declare function taskRunFork(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
78
|
+
/**
|
|
79
|
+
* `POST /api/tasks/:taskId/runs/:runId/promote` — B8. Fixes one branch of a
|
|
80
|
+
* parallel group as canonical and CANCELS the live losers, which is why it has
|
|
81
|
+
* a surface of its own rather than riding a flag on fork.
|
|
82
|
+
*/
|
|
83
|
+
export declare function taskRunPromote(taskId: string, runId: string, body: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
84
|
+
/** The wire shape of an execution target: `{kind, ref?}`, never a bare string. */
|
|
85
|
+
export interface ExecutionTargetRef {
|
|
86
|
+
kind: 'harness' | 'hosted-machine' | 'agent-worker';
|
|
87
|
+
ref?: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Turn a `--harness` value into the `executionTargetRef` the API parses.
|
|
91
|
+
*
|
|
92
|
+
* The route validates an OBJECT (`{kind, ref?}`); passing the flag through as a
|
|
93
|
+
* string failed its zod parse, so every `--harness` invocation returned
|
|
94
|
+
* `400 Invalid request data` — the harness axis, which is half of what handover
|
|
95
|
+
* is for, was unreachable from the CLI and untested.
|
|
96
|
+
*
|
|
97
|
+
* The kind is INFERRED rather than asked for, because an operator naming a
|
|
98
|
+
* machine should not also have to name its category: the two managed selectors
|
|
99
|
+
* are reserved words, a `hosted-machine:<sessionId>` form addresses one
|
|
100
|
+
* session, and everything else is a harness id — which is what the flag is
|
|
101
|
+
* called.
|
|
102
|
+
*/
|
|
103
|
+
export declare function parseExecutionTargetRef(value: string): ExecutionTargetRef;
|
|
60
104
|
export declare const taskChatApi: {
|
|
61
105
|
transcriptSources: (id: string, query?: Query) => Promise<Json>;
|
|
62
106
|
reindexTranscript: (id: string) => Promise<Json>;
|