@skrr-ai/cli 0.1.45 → 0.1.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/base-command.d.ts +13 -1
- package/dist/base-command.js +37 -3
- package/dist/commands/agents/actions/create.js +11 -4
- package/dist/commands/browser/cloud-agents.js +4 -0
- package/dist/commands/code/handover.d.ts +11 -0
- package/dist/commands/code/handover.js +104 -1
- package/dist/commands/code/install.js +31 -0
- package/dist/commands/code/update.d.ts +46 -0
- package/dist/commands/code/update.js +242 -0
- package/dist/commands/commitments/handover.d.ts +29 -0
- package/dist/commands/commitments/handover.js +99 -0
- package/dist/commands/convos/handover.d.ts +1 -0
- package/dist/commands/convos/handover.js +4 -0
- package/dist/commands/daemon/index.js +2 -0
- package/dist/commands/daemon/update.d.ts +19 -0
- package/dist/commands/daemon/update.js +48 -0
- package/dist/commands/followups/watch.js +40 -5
- package/dist/commands/spaces/briefs/get.js +8 -0
- package/dist/commands/spaces/briefs/list.js +2 -0
- package/dist/commands/spaces/briefs/set.d.ts +3 -0
- package/dist/commands/spaces/briefs/set.js +78 -13
- package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
- package/dist/commands/spaces/expertise-suggestions.js +71 -0
- package/dist/commands/spaces/members/add.d.ts +1 -0
- package/dist/commands/spaces/members/add.js +11 -3
- package/dist/commands/spaces/members/list.js +11 -0
- package/dist/commands/tasks/assign.d.ts +9 -0
- package/dist/commands/tasks/assign.js +108 -1
- package/dist/commands/tasks/fork.d.ts +32 -0
- package/dist/commands/tasks/fork.js +171 -0
- package/dist/commands/tasks/handover.d.ts +23 -0
- package/dist/commands/tasks/handover.js +205 -0
- package/dist/commands/tasks/promote.d.ts +23 -0
- package/dist/commands/tasks/promote.js +82 -0
- package/dist/commands/tasks/runs.d.ts +16 -0
- package/dist/commands/tasks/runs.js +46 -1
- package/dist/commands/tasks/show.d.ts +23 -0
- package/dist/commands/tasks/show.js +59 -0
- package/dist/lib/api-fetch.d.ts +7 -1
- package/dist/lib/api-fetch.js +9 -2
- package/dist/lib/assignee-resolver.d.ts +22 -0
- package/dist/lib/assignee-resolver.js +26 -0
- package/dist/lib/code-handover.d.ts +50 -0
- package/dist/lib/code-handover.js +135 -0
- package/dist/lib/commitments.d.ts +22 -0
- package/dist/lib/commitments.js +42 -0
- package/dist/lib/dedicated-wait.js +22 -1
- package/dist/lib/followups.d.ts +18 -0
- package/dist/lib/followups.js +44 -0
- package/dist/lib/node-adapter.js +6 -0
- package/dist/lib/task-extras.d.ts +44 -0
- package/dist/lib/task-extras.js +73 -0
- package/dist/lib/update-check.d.ts +80 -7
- package/dist/lib/update-check.js +365 -33
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/harnessSkillLayout.js +99 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +10 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +16 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +15 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarnessHome.js +45 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.d.ts +65 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/harnessSkillLayout.js +94 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +5 -0
- package/dist/node_modules/@skrr-ai/auth-core/package.json +11 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +2262 -2241
- package/dist/node_modules/@skrr-ai/inference-broker/package.json +1 -1
- package/oclif.manifest.json +34562 -33947
- package/package.json +2 -2
package/dist/base-command.d.ts
CHANGED
|
@@ -311,14 +311,26 @@ export declare abstract class BaseCommand extends Command {
|
|
|
311
311
|
body: unknown;
|
|
312
312
|
idempotencyKey: string;
|
|
313
313
|
}): never;
|
|
314
|
-
protected failWithCliError({ message, code, status, exit, retryable, details, }: {
|
|
314
|
+
protected failWithCliError({ message, code, status, exit, retryable, details, requestId, }: {
|
|
315
315
|
message: string;
|
|
316
316
|
code: string;
|
|
317
317
|
status?: number;
|
|
318
318
|
exit: number;
|
|
319
319
|
retryable: boolean;
|
|
320
320
|
details?: unknown;
|
|
321
|
+
/** The server's `X-Request-ID` for the refused call, when there was one. */
|
|
322
|
+
requestId?: string;
|
|
321
323
|
}): never;
|
|
324
|
+
/**
|
|
325
|
+
* The server's `X-Request-ID` for the most recent refused API call.
|
|
326
|
+
*
|
|
327
|
+
* The CLI prints its own refusals but not the id the server logged them
|
|
328
|
+
* against, so matching one to a log line meant guessing from timestamps. On
|
|
329
|
+
* an account with several clients that misattributes: a 409 raised by
|
|
330
|
+
* another session's concurrent image update was nearly filed as a CLI retry
|
|
331
|
+
* bug (2026-09-18).
|
|
332
|
+
*/
|
|
333
|
+
private lastApiRequestId?;
|
|
322
334
|
private writeJsonCliError;
|
|
323
335
|
protected wantsJsonOutput(): boolean;
|
|
324
336
|
}
|
package/dist/base-command.js
CHANGED
|
@@ -853,9 +853,26 @@ class BaseCommand extends core_1.Command {
|
|
|
853
853
|
* envelope carry the same identifying text.
|
|
854
854
|
*/
|
|
855
855
|
handleApiError(err, { subject } = {}) {
|
|
856
|
+
// An error this command already raised for the user is not an API failure
|
|
857
|
+
// to translate. `this.error()` throws a CLIError carrying `oclif.exit`, and
|
|
858
|
+
// under --json it is the ExitError thrown AFTER `writeJsonCliError` already
|
|
859
|
+
// printed the envelope. When the raise sits inside the try whose catch is
|
|
860
|
+
// this call, re-wrapping it printed a SECOND envelope (`--json` saw the
|
|
861
|
+
// real message followed by `EEXIT: 1`) and rewrote the caller's exit code
|
|
862
|
+
// to 1 — `agents actions create --prompt ""` did exactly that. Pass it
|
|
863
|
+
// through so `catch()` renders it once, with its code and exit intact.
|
|
864
|
+
if (err instanceof Error) {
|
|
865
|
+
const raised = err;
|
|
866
|
+
if (isOclifExitError(raised) || typeof raised.oclif?.exit === 'number') {
|
|
867
|
+
throw err;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
856
870
|
const say = (message) => withSubject(subject, message);
|
|
857
871
|
if (err instanceof Error) {
|
|
858
872
|
const e = err;
|
|
873
|
+
// Remembered once here rather than threaded through every branch below:
|
|
874
|
+
// each one ends in `failWithCliError`, and the id belongs on all of them.
|
|
875
|
+
this.lastApiRequestId = e.requestId;
|
|
859
876
|
const body = parseErrorBody(e.body);
|
|
860
877
|
const bodyCode = stringProp(body, 'code') || stringProp(body, 'errorType');
|
|
861
878
|
const bodyMessage = stringProp(body, 'message') || stringProp(body, 'error');
|
|
@@ -1242,9 +1259,9 @@ class BaseCommand extends core_1.Command {
|
|
|
1242
1259
|
details: { outboxId: queuedId, idempotencyKey: write.idempotencyKey },
|
|
1243
1260
|
});
|
|
1244
1261
|
}
|
|
1245
|
-
failWithCliError({ message, code, status, exit, retryable, details, }) {
|
|
1262
|
+
failWithCliError({ message, code, status, exit, retryable, details, requestId, }) {
|
|
1246
1263
|
if (this.wantsJsonOutput()) {
|
|
1247
|
-
this.writeJsonCliError({ message, code, status, exit, retryable, details });
|
|
1264
|
+
this.writeJsonCliError({ message, code, status, exit, retryable, details, requestId });
|
|
1248
1265
|
}
|
|
1249
1266
|
// Human path. Deliberately stderr-only: stdout is the SUCCESS channel, so
|
|
1250
1267
|
// `id=$(skrr tasks start "$x")` and `sky ... | jq` are never fed error
|
|
@@ -1277,7 +1294,17 @@ class BaseCommand extends core_1.Command {
|
|
|
1277
1294
|
...(suggestion ? { suggestions: [suggestion] } : {}),
|
|
1278
1295
|
});
|
|
1279
1296
|
}
|
|
1280
|
-
|
|
1297
|
+
/**
|
|
1298
|
+
* The server's `X-Request-ID` for the most recent refused API call.
|
|
1299
|
+
*
|
|
1300
|
+
* The CLI prints its own refusals but not the id the server logged them
|
|
1301
|
+
* against, so matching one to a log line meant guessing from timestamps. On
|
|
1302
|
+
* an account with several clients that misattributes: a 409 raised by
|
|
1303
|
+
* another session's concurrent image update was nearly filed as a CLI retry
|
|
1304
|
+
* bug (2026-09-18).
|
|
1305
|
+
*/
|
|
1306
|
+
lastApiRequestId;
|
|
1307
|
+
writeJsonCliError({ message, code, status, exit, retryable, details, requestId, }) {
|
|
1281
1308
|
this.log(JSON.stringify({
|
|
1282
1309
|
ok: false,
|
|
1283
1310
|
error: {
|
|
@@ -1287,6 +1314,13 @@ class BaseCommand extends core_1.Command {
|
|
|
1287
1314
|
retryable,
|
|
1288
1315
|
command: this.id ?? null,
|
|
1289
1316
|
server: this.cliConfig?.baseURL ?? null,
|
|
1317
|
+
// The id the server logged this refusal against. Without it, a
|
|
1318
|
+
// refused call can only be matched to a log line by guessing from
|
|
1319
|
+
// timestamps — which, with several clients on one account,
|
|
1320
|
+
// attributes another session's request to yours.
|
|
1321
|
+
...(requestId || this.lastApiRequestId
|
|
1322
|
+
? { requestId: requestId ?? this.lastApiRequestId }
|
|
1323
|
+
: {}),
|
|
1290
1324
|
...(details ? { details } : {}),
|
|
1291
1325
|
},
|
|
1292
1326
|
}, null, 2));
|
|
@@ -81,8 +81,11 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
|
|
|
81
81
|
let replayed = false;
|
|
82
82
|
try {
|
|
83
83
|
const title = typeof body.title === 'string' ? body.title.trim() : '';
|
|
84
|
-
if (!title)
|
|
85
|
-
this.error(
|
|
84
|
+
if (!title) {
|
|
85
|
+
this.error(typeof body.title === 'string'
|
|
86
|
+
? 'The action title was provided but is empty. Pass non-empty text to --title or title in --from-json.'
|
|
87
|
+
: 'Action title is required. Pass --title or include title.', { exit: 1 });
|
|
88
|
+
}
|
|
86
89
|
const kind = body.kind === 'builtin' ? 'builtin' : 'prompt';
|
|
87
90
|
// No read of the library: position is assigned by the server at the
|
|
88
91
|
// moment of the append, so it cannot be computed from a stale copy. An
|
|
@@ -97,7 +100,9 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
|
|
|
97
100
|
if (kind === 'builtin') {
|
|
98
101
|
const builtinType = body.builtinType;
|
|
99
102
|
if (!agent_actions_1.BUILTIN_ACTION_TYPES.includes(builtinType)) {
|
|
100
|
-
this.error(
|
|
103
|
+
this.error(builtinType === undefined
|
|
104
|
+
? '--builtin-type is required for built-in actions.'
|
|
105
|
+
: `Invalid --builtin-type "${String(builtinType)}" — expected one of ${agent_actions_1.BUILTIN_ACTION_TYPES.join(', ')}.`, { exit: 1 });
|
|
101
106
|
}
|
|
102
107
|
created = (0, agent_actions_1.makeBuiltinAction)({
|
|
103
108
|
scope,
|
|
@@ -118,7 +123,9 @@ class AgentsActionsCreate extends base_command_1.BaseCommand {
|
|
|
118
123
|
else {
|
|
119
124
|
const prompt = typeof body.prompt === 'string' ? body.prompt : '';
|
|
120
125
|
if (!prompt.trim()) {
|
|
121
|
-
this.error(
|
|
126
|
+
this.error(typeof body.prompt === 'string'
|
|
127
|
+
? 'The prompt was provided but is empty. Pass non-empty text to --prompt or prompt in --from-json.'
|
|
128
|
+
: 'Prompt actions require --prompt or prompt in --from-json.', { exit: 1 });
|
|
122
129
|
}
|
|
123
130
|
// Embedded built-in actions (chip source of truth). The prompt keeps the
|
|
124
131
|
// inline `[[action:refId]]` tokens (positional); the server expands each
|
|
@@ -22,6 +22,10 @@ class BrowserCloudAgents extends core_1.Command {
|
|
|
22
22
|
description: 'allow, deny, or status',
|
|
23
23
|
required: true,
|
|
24
24
|
options: ['allow', 'deny', 'status'],
|
|
25
|
+
// Without this oclif reads stdin for a missing positional, so the command
|
|
26
|
+
// hangs when run from a pipe or a non-tty instead of refusing. Enforced
|
|
27
|
+
// by ARG_IMPLICIT_STDIN_ENABLED in the command-hygiene audit.
|
|
28
|
+
ignoreStdin: true,
|
|
25
29
|
}),
|
|
26
30
|
};
|
|
27
31
|
static flags = {
|
|
@@ -21,6 +21,15 @@ import { BaseCommand } from '../../base-command';
|
|
|
21
21
|
* code in the cloud (`skrr machines hosted start --repo` is the second, and stays the
|
|
22
22
|
* right choice when you want a persistent machine instead of a one-shot job).
|
|
23
23
|
*
|
|
24
|
+
* ── `--carry-session` ──────────────────────────────────────────────────────
|
|
25
|
+
*
|
|
26
|
+
* The transcript CAN travel once the engine implements the session-export
|
|
27
|
+
* contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
|
|
28
|
+
* `session export <id> --format json` on the engine writes a versioned envelope
|
|
29
|
+
* to stdout. This flag runs exactly that contract and folds the export into the
|
|
30
|
+
* prompt — the endpoint's only carrier. An engine that predates the contract
|
|
31
|
+
* is a clean refusal, never a silent fallback to brief-only.
|
|
32
|
+
*
|
|
24
33
|
* ── The safety gate is the point ───────────────────────────────────────────
|
|
25
34
|
*
|
|
26
35
|
* The runtime CLONES FROM GITHUB at `--base`. Uncommitted or unpushed work is
|
|
@@ -38,6 +47,8 @@ export default class CodeHandover extends BaseCommand {
|
|
|
38
47
|
base: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
39
48
|
'allow-dirty': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
40
49
|
browser: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
50
|
+
'carry-session': import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
51
|
+
session: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
41
52
|
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
42
53
|
};
|
|
43
54
|
/** Run git, returning null rather than throwing — every caller has a fallback. */
|
|
@@ -4,8 +4,10 @@ const core_1 = require("@oclif/core");
|
|
|
4
4
|
const node_child_process_1 = require("node:child_process");
|
|
5
5
|
const node_crypto_1 = require("node:crypto");
|
|
6
6
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
7
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
7
8
|
const base_command_1 = require("../../base-command");
|
|
8
9
|
const code_handover_1 = require("../../lib/code-handover");
|
|
10
|
+
const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
9
11
|
/**
|
|
10
12
|
* `skrr code handover` — hand the work in this directory to an agent running on
|
|
11
13
|
* a cloud runtime.
|
|
@@ -28,6 +30,15 @@ const code_handover_1 = require("../../lib/code-handover");
|
|
|
28
30
|
* code in the cloud (`skrr machines hosted start --repo` is the second, and stays the
|
|
29
31
|
* right choice when you want a persistent machine instead of a one-shot job).
|
|
30
32
|
*
|
|
33
|
+
* ── `--carry-session` ──────────────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* The transcript CAN travel once the engine implements the session-export
|
|
36
|
+
* contract (docs/architecture/sky-code-session-export-contract-2026-08-21.md):
|
|
37
|
+
* `session export <id> --format json` on the engine writes a versioned envelope
|
|
38
|
+
* to stdout. This flag runs exactly that contract and folds the export into the
|
|
39
|
+
* prompt — the endpoint's only carrier. An engine that predates the contract
|
|
40
|
+
* is a clean refusal, never a silent fallback to brief-only.
|
|
41
|
+
*
|
|
31
42
|
* ── The safety gate is the point ───────────────────────────────────────────
|
|
32
43
|
*
|
|
33
44
|
* The runtime CLONES FROM GITHUB at `--base`. Uncommitted or unpushed work is
|
|
@@ -36,11 +47,13 @@ const code_handover_1 = require("../../lib/code-handover");
|
|
|
36
47
|
* the pull request. This refuses by default and says what to do.
|
|
37
48
|
*/
|
|
38
49
|
class CodeHandover extends base_command_1.BaseCommand {
|
|
39
|
-
static description = 'Hand the work in this directory to an agent on a cloud runtime'
|
|
50
|
+
static description = 'Hand the work in this directory to an agent on a cloud runtime ' +
|
|
51
|
+
'(--carry-session also carries the engine session transcript, when the engine supports export)';
|
|
40
52
|
static examples = [
|
|
41
53
|
'<%= config.bin %> code handover --to "Frontend Agent" --brief "finish the bundle-size fix"',
|
|
42
54
|
'<%= config.bin %> code handover --to agent_abc --brief "continue the parser refactor" --base main',
|
|
43
55
|
'<%= config.bin %> code handover --to "Engineer" --brief "..." --allow-dirty --json',
|
|
56
|
+
'<%= config.bin %> code handover --to "Engineer" --brief "..." --carry-session --session <engine-session-id>',
|
|
44
57
|
];
|
|
45
58
|
static flags = {
|
|
46
59
|
to: core_1.Flags.string({
|
|
@@ -64,6 +77,17 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
64
77
|
browser: core_1.Flags.boolean({
|
|
65
78
|
description: 'Give the receiving agent a browser. Refused where the deployment has no browser runtime, never run without one.',
|
|
66
79
|
}),
|
|
80
|
+
'carry-session': core_1.Flags.boolean({
|
|
81
|
+
description: `Export the engine session transcript (\`${auth_core_1.FIRST_PARTY_HARNESS.provider} session export\`) ` +
|
|
82
|
+
'and carry it into the handover. Opt-in because carrying a whole session is a cost the ' +
|
|
83
|
+
'user should choose. Refused outright when the engine predates the export contract — ' +
|
|
84
|
+
'there is no silent fallback to brief-only.',
|
|
85
|
+
default: false,
|
|
86
|
+
}),
|
|
87
|
+
session: core_1.Flags.string({
|
|
88
|
+
description: 'Engine session id to export with --carry-session (default: the session id the engine ' +
|
|
89
|
+
`exports to its environment — ${(0, auth_core_1.firstPartyHarnessEngineEnvNames)('sessionId').join(' or ')})`,
|
|
90
|
+
}),
|
|
67
91
|
json: core_1.Flags.boolean({ description: 'Output the created task as JSON' }),
|
|
68
92
|
};
|
|
69
93
|
/** Run git, returning null rather than throwing — every caller has a fallback. */
|
|
@@ -105,6 +129,76 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
105
129
|
if (!safety.safe && !flags['allow-dirty']) {
|
|
106
130
|
this.error((0, code_handover_1.formatUnsafeHandover)(gitState, safety));
|
|
107
131
|
}
|
|
132
|
+
if (flags.session && !flags['carry-session']) {
|
|
133
|
+
this.warn('--session has no effect without --carry-session.');
|
|
134
|
+
}
|
|
135
|
+
// ── Optional: carry the engine session. A refused export is terminal —
|
|
136
|
+
// the flag promised the transcript would travel, so dispatching without it
|
|
137
|
+
// would silently deliver a different product than the one asked for. ────
|
|
138
|
+
let sessionExport = null;
|
|
139
|
+
let carriedSessionId = null;
|
|
140
|
+
if (flags['carry-session']) {
|
|
141
|
+
const { path: engineBin } = (0, first_party_harness_1.resolveEngine)();
|
|
142
|
+
if (!engineBin)
|
|
143
|
+
this.error((0, first_party_harness_1.notInstalledMessage)());
|
|
144
|
+
// `--session` wins; otherwise the variable the engine stamps into
|
|
145
|
+
// processes it spawns (current namespace, then the legacy spelling).
|
|
146
|
+
const sessionId = flags.session || (0, auth_core_1.readFirstPartyHarnessEngineEnv)(process.env, 'sessionId').value;
|
|
147
|
+
if (!sessionId) {
|
|
148
|
+
this.error('--carry-session needs the engine session to carry. Pass --session <engine-session-id>, ' +
|
|
149
|
+
`or run inside a session that exports ${(0, auth_core_1.firstPartyHarnessEngineEnvNames)('sessionId')[0]}.`);
|
|
150
|
+
}
|
|
151
|
+
carriedSessionId = sessionId;
|
|
152
|
+
let stdout;
|
|
153
|
+
try {
|
|
154
|
+
stdout = (0, node_child_process_1.execFileSync)(engineBin, ['session', 'export', sessionId, '--format', 'json'], {
|
|
155
|
+
encoding: 'utf8',
|
|
156
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
157
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
const e = err;
|
|
162
|
+
const failure = (0, code_handover_1.classifySessionExportError)(e);
|
|
163
|
+
if (failure.kind === 'unknown-session') {
|
|
164
|
+
this.error(`No engine session "${sessionId}" — the engine found nothing to carry.\n` +
|
|
165
|
+
` ${failure.detail}`);
|
|
166
|
+
}
|
|
167
|
+
this.error(`This engine does not support session export — --carry-session requires a ` +
|
|
168
|
+
`${auth_core_1.FIRST_PARTY_HARNESS_ENGINE.appName} engine implementing the session-export contract ` +
|
|
169
|
+
'(docs/architecture/sky-code-session-export-contract-2026-08-21.md).\n' +
|
|
170
|
+
` ${failure.detail}\n` +
|
|
171
|
+
' Re-run without --carry-session to hand over the situation only, or upgrade the engine.');
|
|
172
|
+
}
|
|
173
|
+
const envelope = (0, code_handover_1.parseSessionExportEnvelope)(stdout);
|
|
174
|
+
if (!envelope) {
|
|
175
|
+
// Exit 0 but no contract envelope is a deviation, not a success: the
|
|
176
|
+
// one shape this side may depend on (C2) did not arrive, so attaching
|
|
177
|
+
// the output anyway would be guessing with someone else's session.
|
|
178
|
+
this.error('The engine ran `session export` but did not produce the contract envelope ' +
|
|
179
|
+
'({version, sessionId, messages[], truncated}) — this build predates or deviates from ' +
|
|
180
|
+
'the export contract, so nothing was attached.');
|
|
181
|
+
}
|
|
182
|
+
// The endpoint's prompt cap is 50000 chars; the transcript gets what the
|
|
183
|
+
// brief and the git context leave behind.
|
|
184
|
+
const budget = 50000 -
|
|
185
|
+
(0, code_handover_1.composeHandoverPrompt)({
|
|
186
|
+
brief: flags.brief,
|
|
187
|
+
repoFullName,
|
|
188
|
+
branch,
|
|
189
|
+
headSha: this.git(['rev-parse', '--short', 'HEAD']),
|
|
190
|
+
recentCommits: this.git(['log', '--oneline', '-5']),
|
|
191
|
+
localWorkNotCarried: safety.localWorkNotCarried,
|
|
192
|
+
}).length -
|
|
193
|
+
128;
|
|
194
|
+
sessionExport = (0, code_handover_1.renderSessionTranscript)(envelope, Math.max(0, budget));
|
|
195
|
+
if (sessionExport.totalCount > 0 && sessionExport.carriedCount === 0) {
|
|
196
|
+
// A "carried" block containing zero messages is a lie about what moved.
|
|
197
|
+
// The brief --brief plus git context already consumed the prompt budget.
|
|
198
|
+
this.error('The session transcript does not fit the 50000-character task prompt alongside this ' +
|
|
199
|
+
'brief — nothing would be carried. Shorten --brief, or re-run without --carry-session.');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
108
202
|
// ── Target agent: id or name, from the agents this user can see. ────────
|
|
109
203
|
let agents = [];
|
|
110
204
|
try {
|
|
@@ -145,6 +239,7 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
145
239
|
headSha: this.git(['rev-parse', '--short', 'HEAD']),
|
|
146
240
|
recentCommits: this.git(['log', '--oneline', '-5']),
|
|
147
241
|
localWorkNotCarried: safety.localWorkNotCarried,
|
|
242
|
+
sessionTranscript: sessionExport?.text,
|
|
148
243
|
});
|
|
149
244
|
let result;
|
|
150
245
|
try {
|
|
@@ -173,6 +268,14 @@ class CodeHandover extends base_command_1.BaseCommand {
|
|
|
173
268
|
const url = task.previewUrl ?? task.prUrl;
|
|
174
269
|
if (url)
|
|
175
270
|
this.log(` ${String(url)}`);
|
|
271
|
+
if (sessionExport) {
|
|
272
|
+
const qualifiers = [
|
|
273
|
+
sessionExport.engineTruncated ? 'engine-truncated' : null,
|
|
274
|
+
sessionExport.clipped ? 'clipped to fit' : null,
|
|
275
|
+
].filter(Boolean);
|
|
276
|
+
this.log(` session ${carriedSessionId} — ${sessionExport.carriedCount}/${sessionExport.totalCount} ` +
|
|
277
|
+
`messages carried${qualifiers.length ? ` (${qualifiers.join(', ')})` : ''}`);
|
|
278
|
+
}
|
|
176
279
|
if (safety.localWorkNotCarried) {
|
|
177
280
|
this.log(' note local uncommitted/unpushed work was not carried over.');
|
|
178
281
|
}
|
|
@@ -57,6 +57,37 @@ class CodeInstall extends base_command_1.BaseCommand {
|
|
|
57
57
|
catch (err) {
|
|
58
58
|
this.handleApiError(err);
|
|
59
59
|
}
|
|
60
|
+
// The daemon distinguishes three refusals, and only one of them attaches a
|
|
61
|
+
// job worth following:
|
|
62
|
+
//
|
|
63
|
+
// - `alreadyInstalled` carries the LAST job the provider ran — context, not
|
|
64
|
+
// this request's work. Following it replays a stale outcome as though it
|
|
65
|
+
// had just happened: `code install` on an engine one release behind
|
|
66
|
+
// reprinted the previous update's log tail verbatim and reported that job
|
|
67
|
+
// as its result (OSK-10650). The right answer is the daemon's own
|
|
68
|
+
// installDisabledReason plus where the update actually comes from.
|
|
69
|
+
// - `alreadyRunning` carries the LIVE job — following it attaches the user
|
|
70
|
+
// to work that is really in flight, which is what they asked for.
|
|
71
|
+
// - `started` carries the job this request began.
|
|
72
|
+
if (started.alreadyInstalled) {
|
|
73
|
+
if (flags.json) {
|
|
74
|
+
this.log(JSON.stringify({ daemonId, ...started }, null, 2));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const state = started.provider;
|
|
78
|
+
const reason = state?.installDisabledReason?.trim();
|
|
79
|
+
const version = state?.version?.trim();
|
|
80
|
+
this.log(`Nothing to do on ${daemonId} — ${reason || 'Already installed.'}${version ? ` (${version})` : ''}`);
|
|
81
|
+
if (state?.updatable === true) {
|
|
82
|
+
this.log(`To update it: ${this.config.bin} code update`);
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
const why = state?.updateDisabledReason?.trim();
|
|
86
|
+
if (why)
|
|
87
|
+
this.log(`Updating: ${why}`);
|
|
88
|
+
}
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
60
91
|
const jobId = started.job?.id;
|
|
61
92
|
// A 202 carrying NO job is not a queued install. It is the daemon
|
|
62
93
|
// acknowledging the call and returning nothing, and the route builds its
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { BaseCommand } from '../../base-command';
|
|
2
|
+
/**
|
|
3
|
+
* `skrr code update` — pull the engine's signed release down through the daemon.
|
|
4
|
+
*
|
|
5
|
+
* ── Why this command exists ────────────────────────────────────────────────
|
|
6
|
+
*
|
|
7
|
+
* The update verb has been API-exposed and daemon-implemented since OSK-10096 —
|
|
8
|
+
* `POST /api/daemons/:id/cli-installers/:provider/update` re-fetches the signed
|
|
9
|
+
* manifest and installs whatever the channel points at. But the only CLI that
|
|
10
|
+
* reached it was `skrr harnesses install <provider> --update`: a flag on a
|
|
11
|
+
* command a `code` user has no reason to read. On stable that left no
|
|
12
|
+
* discoverable path at all — `code install` answered "Already installed." with
|
|
13
|
+
* no next step, and the channel moving past the installed build was invisible
|
|
14
|
+
* from the surface that owns the engine (OSK-10650).
|
|
15
|
+
*
|
|
16
|
+
* So this adds a SURFACE and no mechanism. Same route `harnesses install
|
|
17
|
+
* --update` posts, same inventory pre-check, same 504 recovery — one update
|
|
18
|
+
* path, spelled where the engine's users look.
|
|
19
|
+
*/
|
|
20
|
+
export default class CodeUpdate extends BaseCommand {
|
|
21
|
+
static description: string;
|
|
22
|
+
static examples: string[];
|
|
23
|
+
static flags: {
|
|
24
|
+
daemon: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
25
|
+
wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
26
|
+
json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
27
|
+
};
|
|
28
|
+
run(): Promise<void>;
|
|
29
|
+
/** Which daemon the update targets, by the rule every daemon-picking command shares. */
|
|
30
|
+
private resolveTarget;
|
|
31
|
+
/**
|
|
32
|
+
* The daemon id to update through, or the refusal — identical rules to
|
|
33
|
+
* `code install`: a Dedicated Runtime's own machine is image-managed, several
|
|
34
|
+
* connected daemons get named rather than guessed.
|
|
35
|
+
*/
|
|
36
|
+
private updateTarget;
|
|
37
|
+
/**
|
|
38
|
+
* Start the update, and survive a gateway that cut the request off — the same
|
|
39
|
+
* 504 recovery `harnesses install --update` uses: the relay can drop the
|
|
40
|
+
* answer while the daemon runs the job it started, so on a server-side
|
|
41
|
+
* failure we ask the daemon and follow the job it reports.
|
|
42
|
+
*/
|
|
43
|
+
private start;
|
|
44
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
45
|
+
private follow;
|
|
46
|
+
}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const core_1 = require("@oclif/core");
|
|
4
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
5
|
+
const base_command_1 = require("../../base-command");
|
|
6
|
+
const cli_installers_1 = require("../../lib/cli-installers");
|
|
7
|
+
const daemon_target_1 = require("../../lib/daemon-target");
|
|
8
|
+
const first_party_harness_1 = require("../../lib/first-party-harness");
|
|
9
|
+
/**
|
|
10
|
+
* `skrr code update` — pull the engine's signed release down through the daemon.
|
|
11
|
+
*
|
|
12
|
+
* ── Why this command exists ────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* The update verb has been API-exposed and daemon-implemented since OSK-10096 —
|
|
15
|
+
* `POST /api/daemons/:id/cli-installers/:provider/update` re-fetches the signed
|
|
16
|
+
* manifest and installs whatever the channel points at. But the only CLI that
|
|
17
|
+
* reached it was `skrr harnesses install <provider> --update`: a flag on a
|
|
18
|
+
* command a `code` user has no reason to read. On stable that left no
|
|
19
|
+
* discoverable path at all — `code install` answered "Already installed." with
|
|
20
|
+
* no next step, and the channel moving past the installed build was invisible
|
|
21
|
+
* from the surface that owns the engine (OSK-10650).
|
|
22
|
+
*
|
|
23
|
+
* So this adds a SURFACE and no mechanism. Same route `harnesses install
|
|
24
|
+
* --update` posts, same inventory pre-check, same 504 recovery — one update
|
|
25
|
+
* path, spelled where the engine's users look.
|
|
26
|
+
*/
|
|
27
|
+
class CodeUpdate extends base_command_1.BaseCommand {
|
|
28
|
+
static description = `Update the ${auth_core_1.FIRST_PARTY_HARNESS.displayName} engine on this machine, through the daemon`;
|
|
29
|
+
static examples = [
|
|
30
|
+
'<%= config.bin %> code update',
|
|
31
|
+
'<%= config.bin %> code update --daemon <daemon-id>',
|
|
32
|
+
'<%= config.bin %> code update --json',
|
|
33
|
+
];
|
|
34
|
+
static flags = {
|
|
35
|
+
daemon: core_1.Flags.string({
|
|
36
|
+
description: 'Which daemon runs the update. Defaults to this machine on a Dedicated Runtime, otherwise to the only active one; required when you have several.',
|
|
37
|
+
}),
|
|
38
|
+
wait: core_1.Flags.boolean({
|
|
39
|
+
default: true,
|
|
40
|
+
allowNo: true,
|
|
41
|
+
description: 'Follow the update job until it finishes. --no-wait returns the job id.',
|
|
42
|
+
}),
|
|
43
|
+
json: core_1.Flags.boolean({ description: 'Emit the result as JSON' }),
|
|
44
|
+
};
|
|
45
|
+
async run() {
|
|
46
|
+
const { flags } = await this.parse(CodeUpdate);
|
|
47
|
+
const daemonId = this.updateTarget(await this.resolveTarget(flags.daemon));
|
|
48
|
+
// Ask the inventory before posting: a daemon older than managed updates has
|
|
49
|
+
// no handler for the request and would leave it to time out, and an engine
|
|
50
|
+
// that updates some other way has a reason the daemon already words.
|
|
51
|
+
let inventory;
|
|
52
|
+
try {
|
|
53
|
+
inventory = await (0, cli_installers_1.listCliInstallers)(daemonId, this.resolvedCredential);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
this.handleApiError(err);
|
|
57
|
+
}
|
|
58
|
+
const row = (inventory.providers ?? []).find((p) => (0, auth_core_1.isFirstPartyHarnessProvider)(p.provider));
|
|
59
|
+
if (!row) {
|
|
60
|
+
this.failWithCliError({
|
|
61
|
+
message: `${auth_core_1.FIRST_PARTY_HARNESS.displayName} is not installed on ${daemonId}. ` +
|
|
62
|
+
`Install it with \`${this.config.bin} code install\`.`,
|
|
63
|
+
code: 'NOT_INSTALLED',
|
|
64
|
+
exit: 1,
|
|
65
|
+
retryable: false,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (row.updatable === undefined) {
|
|
69
|
+
this.failWithCliError({
|
|
70
|
+
message: `The daemon ${daemonId} (${inventory.daemonVersion ?? 'version not reported'}) cannot update ${auth_core_1.FIRST_PARTY_HARNESS.displayName}: it predates managed updates. ` +
|
|
71
|
+
`Update the daemon, then run this again.`,
|
|
72
|
+
code: 'DAEMON_UPDATE_UNSUPPORTED',
|
|
73
|
+
exit: 1,
|
|
74
|
+
retryable: false,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
if (!row.updatable) {
|
|
78
|
+
this.failWithCliError({
|
|
79
|
+
message: row.updateDisabledReason?.trim() ||
|
|
80
|
+
`${auth_core_1.FIRST_PARTY_HARNESS.displayName}: the daemon cannot update it here.`,
|
|
81
|
+
code: 'CLI_NOT_UPDATABLE',
|
|
82
|
+
exit: 1,
|
|
83
|
+
retryable: false,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
const start = await this.start(daemonId, flags.json === true);
|
|
87
|
+
if (!start)
|
|
88
|
+
return;
|
|
89
|
+
const jobId = start.job?.id;
|
|
90
|
+
if (!start.started || !jobId) {
|
|
91
|
+
if (flags.json) {
|
|
92
|
+
this.log(JSON.stringify({ daemonId, ...start }, null, 2));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.log(start.alreadyRunning
|
|
96
|
+
? `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: an install or update is already running (job ${start.job?.id ?? 'unknown'}).`
|
|
97
|
+
: `${auth_core_1.FIRST_PARTY_HARNESS.displayName}: ${start.provider?.updateDisabledReason?.trim() || 'the daemon started no update job.'}`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (!flags.wait) {
|
|
101
|
+
if (flags.json) {
|
|
102
|
+
this.log(JSON.stringify({ daemonId, ...start }, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
this.log(`Update started on ${daemonId} — job ${jobId}.`);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (!flags.json) {
|
|
109
|
+
this.logToStderr(`Updating ${auth_core_1.FIRST_PARTY_HARNESS.displayName} ${row.version ? `${row.version} ` : ''}on ${daemonId}…`);
|
|
110
|
+
}
|
|
111
|
+
const job = await this.follow(daemonId, jobId, flags.json === true);
|
|
112
|
+
if (!job)
|
|
113
|
+
return;
|
|
114
|
+
if (flags.json) {
|
|
115
|
+
this.log(JSON.stringify({ daemonId, job }, null, 2));
|
|
116
|
+
if (job.status !== 'succeeded')
|
|
117
|
+
process.exitCode = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (job.status === 'failed') {
|
|
121
|
+
this.failWithCliError({
|
|
122
|
+
message: job.error || 'The update failed and the daemon gave no reason.',
|
|
123
|
+
code: 'UPDATE_FAILED',
|
|
124
|
+
exit: 1,
|
|
125
|
+
retryable: false,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
this.log((0, cli_installers_1.updateOutcomeLine)(job) ??
|
|
129
|
+
`${auth_core_1.FIRST_PARTY_HARNESS.displayName} updated${job.installedVersion ? ` to ${job.installedVersion}` : ''}.`);
|
|
130
|
+
}
|
|
131
|
+
/** Which daemon the update targets, by the rule every daemon-picking command shares. */
|
|
132
|
+
async resolveTarget(explicit) {
|
|
133
|
+
try {
|
|
134
|
+
return await (0, daemon_target_1.resolveDaemonTarget)({
|
|
135
|
+
explicit,
|
|
136
|
+
local: (0, daemon_target_1.localMachine)({ baseURL: this.cliConfig?.baseURL }),
|
|
137
|
+
listActiveDaemons: () => (0, daemon_target_1.listConnectedDaemons)(this.resolvedCredential),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
this.handleApiError(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The daemon id to update through, or the refusal — identical rules to
|
|
146
|
+
* `code install`: a Dedicated Runtime's own machine is image-managed, several
|
|
147
|
+
* connected daemons get named rather than guessed.
|
|
148
|
+
*/
|
|
149
|
+
updateTarget(target) {
|
|
150
|
+
if (target.kind === 'daemon')
|
|
151
|
+
return target.daemonId;
|
|
152
|
+
if (target.kind === 'this-machine') {
|
|
153
|
+
this.failWithCliError({
|
|
154
|
+
message: target.explicit
|
|
155
|
+
? first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE
|
|
156
|
+
: `${first_party_harness_1.DEDICATED_RUNTIME_ENGINE_MESSAGE} This command updates the machine it runs on; ` +
|
|
157
|
+
'to update another of your machines, name it with --daemon.',
|
|
158
|
+
code: 'DEDICATED_RUNTIME_ENGINE_SHIPS_WITH_IMAGE',
|
|
159
|
+
exit: 1,
|
|
160
|
+
retryable: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
if (target.kind === 'none') {
|
|
164
|
+
this.failWithCliError({
|
|
165
|
+
message: 'No daemon is connected for this account, so nothing can perform the update. ' +
|
|
166
|
+
`Start the background service on this computer with \`${this.config.bin} daemon start\`, then run this again.`,
|
|
167
|
+
code: 'NO_DAEMON',
|
|
168
|
+
exit: 1,
|
|
169
|
+
retryable: false,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const { daemons } = target;
|
|
173
|
+
const choices = daemons
|
|
174
|
+
.slice(0, 10)
|
|
175
|
+
.map((d) => ` ${d.id} ${d.name || '—'}`)
|
|
176
|
+
.join('\n');
|
|
177
|
+
this.failWithCliError({
|
|
178
|
+
message: `${daemons.length} connected daemons — say which machine should update the engine:\n${choices}\n` +
|
|
179
|
+
`Pass one with --daemon, or see each daemon id in the DAEMON ID column of \`${this.config.bin} harnesses list\`.`,
|
|
180
|
+
code: 'AMBIGUOUS_DAEMON',
|
|
181
|
+
exit: 1,
|
|
182
|
+
retryable: false,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Start the update, and survive a gateway that cut the request off — the same
|
|
187
|
+
* 504 recovery `harnesses install --update` uses: the relay can drop the
|
|
188
|
+
* answer while the daemon runs the job it started, so on a server-side
|
|
189
|
+
* failure we ask the daemon and follow the job it reports.
|
|
190
|
+
*/
|
|
191
|
+
async start(daemonId, json) {
|
|
192
|
+
try {
|
|
193
|
+
return await (0, cli_installers_1.startManagedCliUpdate)(daemonId, PROVIDER, this.resolvedCredential);
|
|
194
|
+
}
|
|
195
|
+
catch (err) {
|
|
196
|
+
const status = err.status ?? 0;
|
|
197
|
+
if (status >= 500 || status === 408) {
|
|
198
|
+
const job = await (0, cli_installers_1.jobStartedDespiteTimeout)(daemonId, PROVIDER, this.resolvedCredential);
|
|
199
|
+
if (job?.id) {
|
|
200
|
+
if (!json) {
|
|
201
|
+
this.logToStderr(`The request to ${daemonId} did not come back (HTTP ${status}), but the daemon started job ${job.id}. Following it.`);
|
|
202
|
+
}
|
|
203
|
+
return { started: true, job };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
this.handleApiError(err);
|
|
207
|
+
return undefined;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** Follow a job to its end, or report why it could not be followed (and return nothing). */
|
|
211
|
+
async follow(daemonId, jobId, json) {
|
|
212
|
+
const outcome = await (0, cli_installers_1.followManagedCliInstallJob)({
|
|
213
|
+
daemonId,
|
|
214
|
+
jobId,
|
|
215
|
+
credential: this.resolvedCredential,
|
|
216
|
+
onLogLine: json ? undefined : (line) => this.logToStderr(` ${line}`),
|
|
217
|
+
});
|
|
218
|
+
if (outcome.kind === 'missing') {
|
|
219
|
+
this.failWithCliError({
|
|
220
|
+
message: `The daemon no longer has update job ${jobId}.`,
|
|
221
|
+
code: 'JOB_MISSING',
|
|
222
|
+
exit: 1,
|
|
223
|
+
retryable: false,
|
|
224
|
+
});
|
|
225
|
+
return undefined;
|
|
226
|
+
}
|
|
227
|
+
if (outcome.kind === 'timeout') {
|
|
228
|
+
this.failWithCliError({
|
|
229
|
+
message: `Still updating after ${Math.round(cli_installers_1.INSTALL_MAX_WAIT_MS / 60000)} minutes — the daemon is ` +
|
|
230
|
+
`still working on job ${jobId}. Check \`${this.config.bin} code doctor\` in a while.`,
|
|
231
|
+
code: 'UPDATE_PENDING',
|
|
232
|
+
exit: 1,
|
|
233
|
+
retryable: true,
|
|
234
|
+
});
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
return outcome.job;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
exports.default = CodeUpdate;
|
|
241
|
+
/** Same canonical provider id `code install` posts — read from the identity module. */
|
|
242
|
+
const PROVIDER = auth_core_1.FIRST_PARTY_HARNESS.provider;
|