@kybernesis/create 0.14.7 → 0.14.9
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/README.md +18 -1
- package/dist/cli.js +21 -1
- package/dist/deploy.d.ts +3 -0
- package/dist/deploy.js +52 -15
- package/dist/doctor.d.ts +2 -0
- package/dist/doctor.js +26 -0
- package/dist/init.d.ts +5 -0
- package/dist/init.js +16 -1
- package/dist/model-reach.d.ts +2 -0
- package/dist/model-reach.js +8 -4
- package/dist/sandbox-orphans.d.ts +56 -0
- package/dist/sandbox-orphans.js +77 -0
- package/dist/self-version.d.ts +24 -0
- package/dist/self-version.js +56 -0
- package/dist/templates.d.ts +12 -0
- package/dist/templates.js +31 -0
- package/dist/upgrade.d.ts +18 -0
- package/dist/upgrade.js +223 -48
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Run inside an agent project. Checks, with pass/warn/fail per line:
|
|
|
85
85
|
|
|
86
86
|
Exit code 1 on any failure — usable as a CI preflight.
|
|
87
87
|
|
|
88
|
-
## `kyb upgrade [--skip-eval] [--yes]`
|
|
88
|
+
## `kyb upgrade [--host[=<ssh-target>]] [--skip-eval] [--yes]`
|
|
89
89
|
|
|
90
90
|
Compares installed `@kybernesis/*` versions against npm, installs what's
|
|
91
91
|
behind, typechecks, then **runs the eval suite as the gate** — prints
|
|
@@ -100,6 +100,23 @@ runs fail closed unless `--yes` (or `-y`) is supplied. The warning is still prin
|
|
|
100
100
|
are migrated to `kyb-eval`; custom scripts are left untouched with a manual fix.
|
|
101
101
|
The wrapper counts corruption diagnostics beside the eval result and turns a
|
|
102
102
|
nominally green eve exit into a failure when condemned durable state was seen.
|
|
103
|
+
It also identifies failures from `judge.autoevals.*` assertions as judge
|
|
104
|
+
reachability or HTTP failures rather than implying that the agent model failed.
|
|
105
|
+
|
|
106
|
+
Immediately before a local gate, upgrade classifies the authored and compiled
|
|
107
|
+
model route the same way as `kyb doctor`, then checks host-only dependencies. A
|
|
108
|
+
Claude subscription agent must have its loopback proxy answering, and an
|
|
109
|
+
exe-backed agent or judge must be able to reach `EXE_LLM_URL` (or the internal
|
|
110
|
+
exe model catalog). If those routes are unavailable, the eval command is not
|
|
111
|
+
run and upgrade exits with status 2, prints that the gate can only run on the
|
|
112
|
+
host, and gives the exact ssh command. It does not print "Do NOT deploy" because
|
|
113
|
+
no eval exercised the code.
|
|
114
|
+
|
|
115
|
+
Use `kyb upgrade --host` to run the gate over ssh after the upgrade. The target
|
|
116
|
+
uses the same defaults as `kyb deploy`: `EVE_SSH_HOST`, then
|
|
117
|
+
`$EXE_VM_NAME.exe.xyz`; `--host=<ssh-target>` overrides them. The remote project
|
|
118
|
+
path is `~/<package-name>`, and the remote eval output streams directly to the
|
|
119
|
+
terminal. `--skip-eval` remains an explicit bypass and performs no preflight.
|
|
103
120
|
|
|
104
121
|
On self-hosted agents, upgrade also reconciles package-owned host artifacts. It
|
|
105
122
|
refreshes a stale `/etc/cron.daily/kyb-docker-prune` and an existing generated
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* kyb deploy put this repo on its host and restart it, with proof
|
|
12
12
|
* kyb upgrade bump @kybernesis/* to latest, gated on the eval suite
|
|
13
13
|
* --skip-eval skip the eval gate (not for production changes)
|
|
14
|
+
* --host[=<target>] run the eval gate over ssh on the agent host
|
|
15
|
+
* --allow-stale proceed even when this kyb is behind npm
|
|
14
16
|
* --yes, -y confirm an eve upgrade noninteractively
|
|
15
17
|
*/
|
|
16
18
|
import { existsSync, readFileSync } from "node:fs";
|
|
@@ -52,6 +54,12 @@ function flag(rest, key) {
|
|
|
52
54
|
const hit = rest.find((a) => a.startsWith(`--${key}=`));
|
|
53
55
|
return hit ? hit.slice(key.length + 3) : undefined;
|
|
54
56
|
}
|
|
57
|
+
function optionalValueFlag(rest, key) {
|
|
58
|
+
if (rest.includes(`--${key}`))
|
|
59
|
+
return true;
|
|
60
|
+
const value = flag(rest, key);
|
|
61
|
+
return value === undefined ? undefined : value;
|
|
62
|
+
}
|
|
55
63
|
function initOptions(rest) {
|
|
56
64
|
const subs = flag(rest, 'subagents');
|
|
57
65
|
return {
|
|
@@ -75,7 +83,7 @@ const COMMANDS = {
|
|
|
75
83
|
credential: "Write the agent credential onto a host (--local to stay here).",
|
|
76
84
|
register: "Register this agent with the control plane (--name, --url).",
|
|
77
85
|
deploy: "Deploy this agent to its host (--no-env to leave the env file alone).",
|
|
78
|
-
upgrade: "Bring @kybernesis packages and eve to certified versions (--skip-eval, --yes).",
|
|
86
|
+
upgrade: "Bring @kybernesis packages and eve to certified versions (--host, --skip-eval, --allow-stale, --yes).",
|
|
79
87
|
tui: "Talk to your agents in the terminal.",
|
|
80
88
|
version: "Print the version of this tool.",
|
|
81
89
|
};
|
|
@@ -101,6 +109,14 @@ function usage(command) {
|
|
|
101
109
|
|
|
102
110
|
${dim("Factory env: KYB_MODEL_REACH=claude-sub (the CLI flag takes precedence).")}
|
|
103
111
|
${dim("Claude subscription reach uses the loopback OAuth proxy; it does not read EXE_MODEL.")}\n`);
|
|
112
|
+
}
|
|
113
|
+
else if (command === "upgrade") {
|
|
114
|
+
console.log(` ${bold("kyb upgrade")} [options]
|
|
115
|
+
--host[=<target>] ${dim("run the eval gate over ssh; defaults like kyb deploy")}
|
|
116
|
+
--skip-eval ${dim("skip the eval gate")}
|
|
117
|
+
--allow-stale ${dim("continue even when this kyb is behind the published version")}
|
|
118
|
+
--yes, -y ${dim("confirm an eve version change noninteractively")}
|
|
119
|
+
`);
|
|
104
120
|
}
|
|
105
121
|
return;
|
|
106
122
|
}
|
|
@@ -161,6 +177,8 @@ switch (command) {
|
|
|
161
177
|
break;
|
|
162
178
|
case "upgrade":
|
|
163
179
|
await upgrade({
|
|
180
|
+
allowStale: rest.includes("--allow-stale"),
|
|
181
|
+
host: optionalValueFlag(rest, "host"),
|
|
164
182
|
skipEval: rest.includes("--skip-eval"),
|
|
165
183
|
yes: rest.includes("--yes") || rest.includes("-y"),
|
|
166
184
|
});
|
|
@@ -228,7 +246,9 @@ ${dim(" npm i -g @kybernesis/create@latest")}
|
|
|
228
246
|
--host=<target> ${dim("ssh target; defaults to $EXE_VM_NAME.exe.xyz")}
|
|
229
247
|
--no-env ${dim("do not send .env.local (host manages its own secrets)")}
|
|
230
248
|
${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
|
|
249
|
+
--host[=<target>] ${dim("run the eval gate over ssh; defaults to EVE_SSH_HOST or EXE_VM_NAME")}
|
|
231
250
|
--skip-eval ${dim("skip the eval gate")}
|
|
251
|
+
--allow-stale ${dim("continue even when this kyb is behind the published version")}
|
|
232
252
|
--yes, -y ${dim("confirm an eve version change noninteractively")}
|
|
233
253
|
|
|
234
254
|
Registry: https://registry.kybernesis.ai · Docs: the FDE playbook
|
package/dist/deploy.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/** The ssh target: an explicit flag, then EVE_SSH_HOST, then the exe VM name. */
|
|
2
|
+
export declare function sshTarget(dir: string, explicit?: string): string | null;
|
|
3
|
+
export declare function remoteProjectPath(dir: string): string;
|
|
1
4
|
export declare function deploy(options: {
|
|
2
5
|
host?: string;
|
|
3
6
|
dir?: string;
|
package/dist/deploy.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
2
|
+
import { TEMPLATE_CONTAINER_PROBE, assessRestart, parseTemplateContainers } from "./sandbox-orphans.js";
|
|
2
3
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { upsertEnv } from "./envfile.js";
|
|
@@ -33,7 +34,7 @@ function hostOf(dir) {
|
|
|
33
34
|
return deps["@kybernesis/exe"] ? "exe" : "vercel";
|
|
34
35
|
}
|
|
35
36
|
/** The ssh target: an explicit flag, then EVE_SSH_HOST, then the exe VM name. */
|
|
36
|
-
function sshTarget(dir, explicit) {
|
|
37
|
+
export function sshTarget(dir, explicit) {
|
|
37
38
|
const e = env(dir);
|
|
38
39
|
if (explicit)
|
|
39
40
|
return explicit;
|
|
@@ -96,6 +97,10 @@ async function ensureModel(dir, target) {
|
|
|
96
97
|
upsertEnv(dir, { EXE_MODEL: chosen });
|
|
97
98
|
console.log(green(` ✓ EXE_MODEL="${chosen}" written to .env.local\n`));
|
|
98
99
|
}
|
|
100
|
+
export function remoteProjectPath(dir) {
|
|
101
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
102
|
+
return `~/${pkg.name ?? "agent"}`;
|
|
103
|
+
}
|
|
99
104
|
export async function deploy(options) {
|
|
100
105
|
const dir = options.dir ?? process.cwd();
|
|
101
106
|
if (!existsSync(join(dir, "agent"))) {
|
|
@@ -117,8 +122,7 @@ export async function deploy(options) {
|
|
|
117
122
|
process.exitCode = 1;
|
|
118
123
|
return;
|
|
119
124
|
}
|
|
120
|
-
const
|
|
121
|
-
const remote = `~/${name}`;
|
|
125
|
+
const remote = remoteProjectPath(dir);
|
|
122
126
|
console.log(dim(` host: ${target} path: ${remote}\n`));
|
|
123
127
|
await ensureModel(dir, target);
|
|
124
128
|
const run = (cmd, args) => {
|
|
@@ -239,6 +243,14 @@ export async function deploy(options) {
|
|
|
239
243
|
' SCRIPT="scripts/eve-server.sh"',
|
|
240
244
|
"fi",
|
|
241
245
|
'if [ -z "$SCRIPT" ]; then echo "FAILED: no restart script on the host"; exit 1; fi',
|
|
246
|
+
// An orphaned sandbox template container (a start killed mid-build; only
|
|
247
|
+
// `sleep` inside) makes every eve start hang forever. The packaged start
|
|
248
|
+
// script clears it too, but a host still running its own older copy
|
|
249
|
+
// would not, so the deploy does it first and says so (KYB-531).
|
|
250
|
+
"for c in $(command -v docker >/dev/null 2>&1 && docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
|
|
251
|
+
" live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
|
|
252
|
+
' if [ "${live:-0}" = "0" ]; then echo "removing orphaned sandbox template container $c (only sleep inside; it would block every start)"; docker rm -f "$c" >/dev/null 2>&1 || true; fi',
|
|
253
|
+
"done",
|
|
242
254
|
'echo "using $SCRIPT"',
|
|
243
255
|
'setsid nohup bash "$SCRIPT" > /tmp/kyb-deploy.log 2>&1 < /dev/null &',
|
|
244
256
|
"sleep 2",
|
|
@@ -266,23 +278,45 @@ export async function deploy(options) {
|
|
|
266
278
|
let last = "";
|
|
267
279
|
let seen = "";
|
|
268
280
|
let lastChange = Date.now();
|
|
281
|
+
let lastBuildingNote = 0;
|
|
282
|
+
let stuck = null;
|
|
269
283
|
while (Date.now() - startedAt < CEILING_MS) {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
284
|
+
// Two witnesses: the log, and the sandbox template containers. A cold
|
|
285
|
+
// template build is silent in the log for longer than the quiet limit,
|
|
286
|
+
// and judging it on the log alone is how a fine build got killed (KYB-531).
|
|
287
|
+
const out = execFileSync("ssh", [target, `tail -40 /tmp/kyb-deploy.log 2>/dev/null || true; echo __TEMPLATES__; ${TEMPLATE_CONTAINER_PROBE}`], { encoding: "utf8" });
|
|
288
|
+
const [logPart, probePart = ""] = out.split("__TEMPLATES__");
|
|
289
|
+
last = logPart;
|
|
290
|
+
if (logPart !== seen) {
|
|
291
|
+
for (const line of logPart.split("\n")) {
|
|
292
|
+
if (line && !seen.includes(line) && /npm|install|build|SOURCE IS NEWER|waiting|pid=|health:|FAILED|template|removing/i.test(line)) {
|
|
277
293
|
console.log(dim(` ${line.trim().slice(0, 120)}`));
|
|
278
294
|
}
|
|
279
295
|
}
|
|
280
|
-
seen =
|
|
296
|
+
seen = logPart;
|
|
281
297
|
lastChange = Date.now();
|
|
282
298
|
}
|
|
283
|
-
|
|
299
|
+
const verdict = assessRestart({
|
|
300
|
+
log: logPart,
|
|
301
|
+
templates: parseTemplateContainers(probePart),
|
|
302
|
+
quietMs: Date.now() - lastChange,
|
|
303
|
+
quietLimitMs: QUIET_LIMIT_MS,
|
|
304
|
+
});
|
|
305
|
+
if (verdict.state === "healthy" || verdict.state === "failed")
|
|
284
306
|
break;
|
|
285
|
-
if (
|
|
307
|
+
if (verdict.state === "building") {
|
|
308
|
+
lastChange = Date.now();
|
|
309
|
+
if (Date.now() - lastBuildingNote > 60_000) {
|
|
310
|
+
console.log(dim(` still building the sandbox template (${Math.round((Date.now() - startedAt) / 60_000)} min): ${verdict.detail}`));
|
|
311
|
+
lastBuildingNote = Date.now();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
else if (verdict.state === "orphaned") {
|
|
315
|
+
stuck = verdict.detail ?? null;
|
|
316
|
+
console.log(yellow(`\n ! an orphaned sandbox template container is blocking the start: ${stuck}`));
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
else if (verdict.state === "quiet") {
|
|
286
320
|
console.log(yellow(`\n ! nothing new on the host for ${Math.round(QUIET_LIMIT_MS / 60_000)} minutes — giving up on the wait`));
|
|
287
321
|
break;
|
|
288
322
|
}
|
|
@@ -296,8 +330,11 @@ export async function deploy(options) {
|
|
|
296
330
|
.join("\n"));
|
|
297
331
|
console.log(healthy
|
|
298
332
|
? green("\n ✓ deployed and serving the current build")
|
|
299
|
-
:
|
|
300
|
-
|
|
333
|
+
: stuck
|
|
334
|
+
? yellow(`\n ! the restart cannot finish while that container exists. Remove it on the host, then run kyb deploy again.\n` +
|
|
335
|
+
` ssh ${target} '${stuck.replace(/^.*remove it: /, "").replace(/\).*$/, "")}'`)
|
|
336
|
+
: yellow("\n ! the restart did not report health. It may still be building — this stops watching, it does not stop the host.\n" +
|
|
337
|
+
` Watch it: ssh ${target} 'tail -f /tmp/kyb-deploy.log'`));
|
|
301
338
|
if (!healthy)
|
|
302
339
|
process.exitCode = 1;
|
|
303
340
|
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ export interface Check {
|
|
|
6
6
|
label: string;
|
|
7
7
|
detail?: string;
|
|
8
8
|
}
|
|
9
|
+
/** Report the opt-in GitHub capability only when its mount is present. */
|
|
10
|
+
export declare function githubToolsDoctorCheck(mountExists: boolean, env: Record<string, string | undefined>): Check | null;
|
|
9
11
|
/** The shape @kybernesis/exe's inspector returns; declared here so create keeps zero runtime deps. */
|
|
10
12
|
export interface DockerTemplateInspection {
|
|
11
13
|
status: "skipped" | "present" | "failed";
|
package/dist/doctor.js
CHANGED
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { TEMPLATE_CONTAINER_PROBE, parseTemplateContainers, templateContainerDoctorChecks } from "./sandbox-orphans.js";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
|
|
5
6
|
import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.js";
|
|
6
7
|
import { inspectEveAgent } from "./agent-limits.js";
|
|
7
8
|
import { classifyModelReach } from "./model-reach.js";
|
|
9
|
+
import { checkSelfVersion } from "./self-version.js";
|
|
8
10
|
const MARK = {
|
|
9
11
|
pass: green("✓"),
|
|
10
12
|
warn: yellow("!"),
|
|
11
13
|
fail: red("✗"),
|
|
12
14
|
};
|
|
15
|
+
/** Report the opt-in GitHub capability only when its mount is present. */
|
|
16
|
+
export function githubToolsDoctorCheck(mountExists, env) {
|
|
17
|
+
if (!mountExists || env.GITHUB_TOKEN)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
verdict: "warn",
|
|
21
|
+
label: "GitHub tools off (no GITHUB_TOKEN)",
|
|
22
|
+
detail: "set GITHUB_TOKEN in .env.local or the deployment environment, then rebuild and restart",
|
|
23
|
+
};
|
|
24
|
+
}
|
|
13
25
|
export function dockerTemplateDoctorChecks(result) {
|
|
14
26
|
if (result.status === "skipped")
|
|
15
27
|
return [];
|
|
@@ -39,6 +51,8 @@ export function modelReachDoctorCheck(authoredSource, compiledRouting) {
|
|
|
39
51
|
return { verdict: "pass", label: "Model reach: claude-sub (Claude subscription)" };
|
|
40
52
|
case "exe":
|
|
41
53
|
return { verdict: "pass", label: "Model reach: exe (host LLM integration)" };
|
|
54
|
+
case "grok-sub":
|
|
55
|
+
return { verdict: "pass", label: "Model reach: grok-sub (direct xAI subscription credential)" };
|
|
42
56
|
case "gateway":
|
|
43
57
|
return { verdict: "pass", label: "Model reach: gateway" };
|
|
44
58
|
case "direct-provider":
|
|
@@ -98,6 +112,10 @@ export async function doctor() {
|
|
|
98
112
|
const installedAgentService = findMatchingAgentServiceUnit(cwd);
|
|
99
113
|
const checks = [];
|
|
100
114
|
const add = (verdict, label, detail) => checks.push({ verdict, label, detail });
|
|
115
|
+
const selfVersion = checkSelfVersion();
|
|
116
|
+
if (selfVersion.kind === "stale") {
|
|
117
|
+
add("warn", selfVersion.message, selfVersion.fix);
|
|
118
|
+
}
|
|
101
119
|
// ── project shape ──────────────────────────────────────────────────────
|
|
102
120
|
if (!existsSync(join(cwd, "agent"))) {
|
|
103
121
|
console.error(red("Not an eve agent project (no agent/ directory). Run inside the agent repo."));
|
|
@@ -137,6 +155,9 @@ export async function doctor() {
|
|
|
137
155
|
};
|
|
138
156
|
if (!existsSync(envPath))
|
|
139
157
|
add("warn", ".env.local missing", "copy .env.example and fill it");
|
|
158
|
+
const githubCheck = githubToolsDoctorCheck(existsSync(join(cwd, "agent/extensions/github.ts")), env);
|
|
159
|
+
if (githubCheck)
|
|
160
|
+
add(githubCheck.verdict, githubCheck.label, githubCheck.detail);
|
|
140
161
|
// ── arcana keys: validate every key↔workspace pair read-only ───────────
|
|
141
162
|
const arcanaPairs = [];
|
|
142
163
|
if (env.ARCANA_API_KEY && env.ARCANA_COMPANY_WORKSPACE)
|
|
@@ -377,6 +398,11 @@ export async function doctor() {
|
|
|
377
398
|
* eventually produces looks like anything except a full disk.
|
|
378
399
|
*/
|
|
379
400
|
if (capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes") {
|
|
401
|
+
// A template container left behind by a killed start blocks every
|
|
402
|
+
// later start forever; a live one means "do not restart yet" (KYB-531).
|
|
403
|
+
for (const check of templateContainerDoctorChecks(parseTemplateContainers(capture("sh", ["-c", TEMPLATE_CONTAINER_PROBE])))) {
|
|
404
|
+
checks.push(check);
|
|
405
|
+
}
|
|
380
406
|
const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
|
|
381
407
|
const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
|
|
382
408
|
if (job !== "yes") {
|
package/dist/init.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type ChannelKind, type HostKind } from "./templates.js";
|
|
2
|
+
/**
|
|
3
|
+
* Replace Eve's freshly installed registry mount with the Kybernesis-managed
|
|
4
|
+
* token-aware form. A failed registry add must not create a capability file.
|
|
5
|
+
*/
|
|
6
|
+
export declare function finalizeGithubToolsRegistryMount(cwd: string, registryAddSucceeded: boolean): boolean;
|
|
2
7
|
export interface InitOptions {
|
|
3
8
|
engineer?: boolean;
|
|
4
9
|
/**
|
package/dist/init.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { chmodSync, copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { DEFAULT_ISSUER, EVE_VERSION, REGISTRY_URL, ask, bold, closePrompts, dim, green, run, slug, red, yellow, } from "./util.js";
|
|
4
|
-
import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
4
|
+
import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, exeEvalConfigTs, evalScript, hostAgentTs, hostSteps, githubToolsMountTs, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
|
|
5
5
|
import { suiteDir } from "./skills.js";
|
|
6
6
|
import { configureArcana } from "./arcana.js";
|
|
7
7
|
import { upsertEnv } from "./envfile.js";
|
|
@@ -19,6 +19,19 @@ const CORE_ITEMS = ["enterprise", "arcana", "evals"];
|
|
|
19
19
|
// self-hosted agent it cannot get an OIDC token and the agent fails to boot.
|
|
20
20
|
const ENGINEER_ITEMS_ALL = ["extension/agent-browser", "extension/github-tools"];
|
|
21
21
|
const ENGINEER_ITEMS_VERCEL = ["connection/vercel"];
|
|
22
|
+
/**
|
|
23
|
+
* Replace Eve's freshly installed registry mount with the Kybernesis-managed
|
|
24
|
+
* token-aware form. A failed registry add must not create a capability file.
|
|
25
|
+
*/
|
|
26
|
+
export function finalizeGithubToolsRegistryMount(cwd, registryAddSucceeded) {
|
|
27
|
+
if (!registryAddSucceeded)
|
|
28
|
+
return false;
|
|
29
|
+
const path = join(cwd, "agent/extensions/github.ts");
|
|
30
|
+
if (!existsSync(path))
|
|
31
|
+
return false;
|
|
32
|
+
writeFileSync(path, githubToolsMountTs());
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
22
35
|
export async function init(rawName, options = {}) {
|
|
23
36
|
const engineer = options.engineer === true;
|
|
24
37
|
const studio = options.studio === true;
|
|
@@ -165,6 +178,8 @@ export async function init(rawName, options = {}) {
|
|
|
165
178
|
const engItems = [...ENGINEER_ITEMS_ALL, ...(host === "vercel" ? ENGINEER_ITEMS_VERCEL : [])];
|
|
166
179
|
for (const item of engItems) {
|
|
167
180
|
const ok = run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
|
|
181
|
+
if (item === "extension/github-tools")
|
|
182
|
+
finalizeGithubToolsRegistryMount(dir, ok);
|
|
168
183
|
if (!ok)
|
|
169
184
|
console.log(yellow(` ! ${item} did not install cleanly — re-run: npx eve add ${item}`));
|
|
170
185
|
}
|
package/dist/model-reach.d.ts
CHANGED
package/dist/model-reach.js
CHANGED
|
@@ -35,12 +35,16 @@ export function classifyModelReach(authoredSource, compiledRouting) {
|
|
|
35
35
|
if (authoredSource !== null) {
|
|
36
36
|
const claudeSubscription = /\bclaudeSubscription\s*\(/.test(authoredSource);
|
|
37
37
|
const exeModel = /\bexeModel\s*\(/.test(authoredSource);
|
|
38
|
-
|
|
38
|
+
const grokSubscription = /\bgrokSubscription\s*\(/.test(authoredSource);
|
|
39
|
+
const authored = [claudeSubscription, exeModel, grokSubscription].filter(Boolean).length;
|
|
40
|
+
if (authored === 1 && claudeSubscription)
|
|
39
41
|
return { kind: "claude-sub" };
|
|
40
|
-
if (
|
|
42
|
+
if (authored === 1 && exeModel)
|
|
41
43
|
return { kind: "exe" };
|
|
42
|
-
if (
|
|
43
|
-
return { kind: "
|
|
44
|
+
if (authored === 1 && grokSubscription)
|
|
45
|
+
return { kind: "grok-sub" };
|
|
46
|
+
if (authored > 1) {
|
|
47
|
+
return { kind: "unresolved", reason: "root agent authors more than one model reach helper" };
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
if (compiledRouting.kind === "gateway")
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Check } from "./doctor.js";
|
|
2
|
+
/**
|
|
3
|
+
* Docker sandbox template containers: the ones still building, and the ones
|
|
4
|
+
* left behind.
|
|
5
|
+
*
|
|
6
|
+
* eve builds each Docker sandbox template inside an `eve-sbx-tpl-*` container
|
|
7
|
+
* on first start. A cold build (apt-get, pnpm, playwright with its browsers)
|
|
8
|
+
* runs for minutes and prints nothing to the deploy log while it does. Two
|
|
9
|
+
* things went wrong with that on one deployment (KYB-531):
|
|
10
|
+
*
|
|
11
|
+
* - the host start script judged the start on a 45-second clock and printed
|
|
12
|
+
* "FAILED to start" while the template was building, so the operator killed
|
|
13
|
+
* the start;
|
|
14
|
+
* - the killed start left the container behind with `sleep 2147483647` as its
|
|
15
|
+
* only process, and while it existed every later `eve start` blocked forever
|
|
16
|
+
* at "initializing N sandbox templates" with nothing in any log to say why.
|
|
17
|
+
*
|
|
18
|
+
* Everything here reads one probe: each template container's name and how
|
|
19
|
+
* many processes inside it are not `sleep`. Building means at least one; an
|
|
20
|
+
* orphan means none.
|
|
21
|
+
*/
|
|
22
|
+
export interface TemplateContainer {
|
|
23
|
+
name: string;
|
|
24
|
+
/** Processes inside the container other than `sleep`. */
|
|
25
|
+
liveProcesses: number;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
|
|
29
|
+
* nothing when Docker is absent. The same probe serves doctor (locally), the
|
|
30
|
+
* deploy (over ssh), and the host start script.
|
|
31
|
+
*/
|
|
32
|
+
export declare const TEMPLATE_CONTAINER_PROBE: string;
|
|
33
|
+
export declare function parseTemplateContainers(output: string | null | undefined): TemplateContainer[];
|
|
34
|
+
export declare function orphanedTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
|
|
35
|
+
export declare function buildingTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
|
|
36
|
+
/** The exact command that clears an orphan; printed as the remedy and run by the start script. */
|
|
37
|
+
export declare function removeOrphanCommand(name: string): string;
|
|
38
|
+
export declare function templateContainerDoctorChecks(containers: readonly TemplateContainer[]): Check[];
|
|
39
|
+
export type RestartState = "healthy" | "failed" | "building" | "orphaned" | "quiet" | "waiting";
|
|
40
|
+
/**
|
|
41
|
+
* What a deploy should conclude from the host right now.
|
|
42
|
+
*
|
|
43
|
+
* The log alone was the whole verdict before, and a template build is silent
|
|
44
|
+
* in it. The template probe is the second witness: while something is building,
|
|
45
|
+
* silence is progress and the wait goes on; an orphan with nothing building
|
|
46
|
+
* and no server is a stuck host with a known remedy, not a slow one.
|
|
47
|
+
*/
|
|
48
|
+
export declare function assessRestart(input: {
|
|
49
|
+
log: string;
|
|
50
|
+
templates: readonly TemplateContainer[];
|
|
51
|
+
quietMs: number;
|
|
52
|
+
quietLimitMs: number;
|
|
53
|
+
}): {
|
|
54
|
+
state: RestartState;
|
|
55
|
+
detail?: string;
|
|
56
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
|
|
3
|
+
* nothing when Docker is absent. The same probe serves doctor (locally), the
|
|
4
|
+
* deploy (over ssh), and the host start script.
|
|
5
|
+
*/
|
|
6
|
+
export const TEMPLATE_CONTAINER_PROBE = [
|
|
7
|
+
"command -v docker >/dev/null 2>&1 || exit 0",
|
|
8
|
+
"for c in $(docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
|
|
9
|
+
" live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
|
|
10
|
+
' echo "$c ${live:-0}"',
|
|
11
|
+
"done",
|
|
12
|
+
].join("\n");
|
|
13
|
+
export function parseTemplateContainers(output) {
|
|
14
|
+
return (output ?? "")
|
|
15
|
+
.split("\n")
|
|
16
|
+
.map((line) => line.trim())
|
|
17
|
+
.filter((line) => line.startsWith("eve-sbx-tpl-"))
|
|
18
|
+
.map((line) => {
|
|
19
|
+
const [name, count] = line.split(/\s+/);
|
|
20
|
+
return { name, liveProcesses: Number(count) || 0 };
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function orphanedTemplates(containers) {
|
|
24
|
+
return containers.filter((c) => c.liveProcesses === 0);
|
|
25
|
+
}
|
|
26
|
+
export function buildingTemplates(containers) {
|
|
27
|
+
return containers.filter((c) => c.liveProcesses > 0);
|
|
28
|
+
}
|
|
29
|
+
/** The exact command that clears an orphan; printed as the remedy and run by the start script. */
|
|
30
|
+
export function removeOrphanCommand(name) {
|
|
31
|
+
return `docker rm -f ${name}`;
|
|
32
|
+
}
|
|
33
|
+
export function templateContainerDoctorChecks(containers) {
|
|
34
|
+
const orphans = orphanedTemplates(containers);
|
|
35
|
+
const building = buildingTemplates(containers);
|
|
36
|
+
const checks = orphans.map((c) => ({
|
|
37
|
+
verdict: "fail",
|
|
38
|
+
label: `orphaned sandbox template container ${c.name}: every eve start will hang until it is removed`,
|
|
39
|
+
detail: `its only process is sleep; a start was killed mid-build. Remove it: ${removeOrphanCommand(c.name)}`,
|
|
40
|
+
}));
|
|
41
|
+
if (building.length) {
|
|
42
|
+
checks.push({
|
|
43
|
+
verdict: "warn",
|
|
44
|
+
label: `sandbox template building: ${building.map((c) => c.name).join(", ")}`,
|
|
45
|
+
detail: "a cold build takes minutes and the server binds its port only after it finishes; do not restart",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return checks;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* What a deploy should conclude from the host right now.
|
|
52
|
+
*
|
|
53
|
+
* The log alone was the whole verdict before, and a template build is silent
|
|
54
|
+
* in it. The template probe is the second witness: while something is building,
|
|
55
|
+
* silence is progress and the wait goes on; an orphan with nothing building
|
|
56
|
+
* and no server is a stuck host with a known remedy, not a slow one.
|
|
57
|
+
*/
|
|
58
|
+
export function assessRestart(input) {
|
|
59
|
+
if (/health:\s*200/.test(input.log))
|
|
60
|
+
return { state: "healthy" };
|
|
61
|
+
if (/FAILED/.test(input.log))
|
|
62
|
+
return { state: "failed" };
|
|
63
|
+
const building = buildingTemplates(input.templates);
|
|
64
|
+
if (building.length) {
|
|
65
|
+
return { state: "building", detail: building.map((c) => c.name).join(", ") };
|
|
66
|
+
}
|
|
67
|
+
const orphans = orphanedTemplates(input.templates);
|
|
68
|
+
if (orphans.length) {
|
|
69
|
+
return {
|
|
70
|
+
state: "orphaned",
|
|
71
|
+
detail: orphans.map((c) => `${c.name} (remove it: ${removeOrphanCommand(c.name)})`).join("; "),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (input.quietMs > input.quietLimitMs)
|
|
75
|
+
return { state: "quiet" };
|
|
76
|
+
return { state: "waiting" };
|
|
77
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const STALE_KYB_FIX = "npm install -g @kybernesis/create@latest";
|
|
2
|
+
export type SelfVersionResult = {
|
|
3
|
+
kind: "stale";
|
|
4
|
+
installed: string;
|
|
5
|
+
latest: string;
|
|
6
|
+
message: string;
|
|
7
|
+
fix: typeof STALE_KYB_FIX;
|
|
8
|
+
} | {
|
|
9
|
+
kind: "current";
|
|
10
|
+
installed: string;
|
|
11
|
+
latest: string;
|
|
12
|
+
} | {
|
|
13
|
+
kind: "ahead";
|
|
14
|
+
installed: string;
|
|
15
|
+
latest: string;
|
|
16
|
+
} | {
|
|
17
|
+
kind: "unknown";
|
|
18
|
+
installed?: string;
|
|
19
|
+
latest?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Compare the three numeric version components exactly as upgrade historically has. */
|
|
22
|
+
export declare function versionLt(a: string, b: string): boolean;
|
|
23
|
+
/** Look up this CLI's published version. Unknown lookup state is deliberately non-blocking. */
|
|
24
|
+
export declare function checkSelfVersion(): SelfVersionResult;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
export const STALE_KYB_FIX = "npm install -g @kybernesis/create@latest";
|
|
4
|
+
function usableVersion(value) {
|
|
5
|
+
return Boolean(value && /^\d+\.\d+\.\d+$/.test(value));
|
|
6
|
+
}
|
|
7
|
+
/** Compare the three numeric version components exactly as upgrade historically has. */
|
|
8
|
+
export function versionLt(a, b) {
|
|
9
|
+
const pa = a.split(".").map(Number);
|
|
10
|
+
const pb = b.split(".").map(Number);
|
|
11
|
+
for (let i = 0; i < 3; i++) {
|
|
12
|
+
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
13
|
+
return true;
|
|
14
|
+
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
/** Look up this CLI's published version. Unknown lookup state is deliberately non-blocking. */
|
|
20
|
+
export function checkSelfVersion() {
|
|
21
|
+
let installed;
|
|
22
|
+
try {
|
|
23
|
+
installed = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return { kind: "unknown" };
|
|
27
|
+
}
|
|
28
|
+
let latest;
|
|
29
|
+
try {
|
|
30
|
+
const result = spawnSync("npm", ["view", "@kybernesis/create", "version"], {
|
|
31
|
+
encoding: "utf8",
|
|
32
|
+
env: process.env,
|
|
33
|
+
});
|
|
34
|
+
if (result.status === 0)
|
|
35
|
+
latest = result.stdout.trim();
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return { kind: "unknown", installed };
|
|
39
|
+
}
|
|
40
|
+
if (!usableVersion(installed) || !usableVersion(latest)) {
|
|
41
|
+
return { kind: "unknown", ...(installed ? { installed } : {}), ...(latest ? { latest } : {}) };
|
|
42
|
+
}
|
|
43
|
+
if (installed === latest)
|
|
44
|
+
return { kind: "current", installed, latest };
|
|
45
|
+
if (versionLt(installed, latest)) {
|
|
46
|
+
return {
|
|
47
|
+
kind: "stale",
|
|
48
|
+
installed,
|
|
49
|
+
latest,
|
|
50
|
+
message: `kyb ${installed} is behind ${latest}. The certified eve version is ` +
|
|
51
|
+
"compiled into this tool, so an old kyb reports an old pin as current.",
|
|
52
|
+
fix: STALE_KYB_FIX,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return { kind: "ahead", installed, latest };
|
|
56
|
+
}
|
package/dist/templates.d.ts
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
/** File templates written by `kyb init`. Kept in sync with the proven Kyber patterns. */
|
|
2
2
|
import type { ModelScaffoldConfig } from "./model-reach.js";
|
|
3
|
+
/** Exact output of Eve's legacy extension/github-tools registry item. */
|
|
4
|
+
export declare const LEGACY_GITHUB_TOOLS_MOUNT = "import githubTools from \"@github-tools/eve-extension\";\n\nexport default githubTools({});\n";
|
|
5
|
+
/**
|
|
6
|
+
* The managed GitHub extension mount written by init and recognized by upgrade.
|
|
7
|
+
*
|
|
8
|
+
* Eve discovers configured extensions from the direct imported-factory default
|
|
9
|
+
* call, so keep that syntax. An empty include list prevents the dynamic resolver
|
|
10
|
+
* from building any descriptors without a credential, while the lazy provider
|
|
11
|
+
* avoids eager missing-token validation. After GITHUB_TOKEN is added, a restart
|
|
12
|
+
* evaluates the module again and preserves the registry's original `{}` behavior.
|
|
13
|
+
*/
|
|
14
|
+
export declare function githubToolsMountTs(): string;
|
|
3
15
|
export declare const DEPT_DESCRIPTIONS: Record<string, string>;
|
|
4
16
|
export declare function deptDescription(dept: string): string;
|
|
5
17
|
export declare function identityMd(displayName: string, depts: string[]): string;
|
package/dist/templates.js
CHANGED
|
@@ -1,4 +1,35 @@
|
|
|
1
1
|
/** File templates written by `kyb init`. Kept in sync with the proven Kyber patterns. */
|
|
2
|
+
/** Exact output of Eve's legacy extension/github-tools registry item. */
|
|
3
|
+
export const LEGACY_GITHUB_TOOLS_MOUNT = `import githubTools from "@github-tools/eve-extension";
|
|
4
|
+
|
|
5
|
+
export default githubTools({});
|
|
6
|
+
`;
|
|
7
|
+
/**
|
|
8
|
+
* The managed GitHub extension mount written by init and recognized by upgrade.
|
|
9
|
+
*
|
|
10
|
+
* Eve discovers configured extensions from the direct imported-factory default
|
|
11
|
+
* call, so keep that syntax. An empty include list prevents the dynamic resolver
|
|
12
|
+
* from building any descriptors without a credential, while the lazy provider
|
|
13
|
+
* avoids eager missing-token validation. After GITHUB_TOKEN is added, a restart
|
|
14
|
+
* evaluates the module again and preserves the registry's original `{}` behavior.
|
|
15
|
+
*/
|
|
16
|
+
export function githubToolsMountTs() {
|
|
17
|
+
return `import githubTools from "@github-tools/eve-extension";
|
|
18
|
+
|
|
19
|
+
function githubToolsOptions(hasToken = Boolean(process.env.GITHUB_TOKEN)) {
|
|
20
|
+
return hasToken
|
|
21
|
+
? {}
|
|
22
|
+
: {
|
|
23
|
+
include: [],
|
|
24
|
+
token: async () => {
|
|
25
|
+
throw new Error("GITHUB_TOKEN is not set.");
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default githubTools(githubToolsOptions());
|
|
31
|
+
`;
|
|
32
|
+
}
|
|
2
33
|
export const DEPT_DESCRIPTIONS = {
|
|
3
34
|
finance: "Finance specialist: financials, budgets, spend, revenue, invoices, runway, and financial reporting. Keeps the finance team's own memory workspace. Delegate any finance-shaped task, question, or reporting request here.",
|
|
4
35
|
marketing: "Marketing specialist: campaigns, content, positioning, brand, launches, social, and ads. Keeps the marketing team's own memory workspace. Delegate any marketing-shaped task or question here.",
|
package/dist/upgrade.d.ts
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { type AgentInputLimit } from "./agent-limits.js";
|
|
2
2
|
export declare function agentInputLimitUpgradeMessage(limit: AgentInputLimit): string;
|
|
3
|
+
export type GithubToolsMountRepair = {
|
|
4
|
+
kind: "missing" | "current" | "updated" | "customized";
|
|
5
|
+
} | {
|
|
6
|
+
kind: "error";
|
|
7
|
+
message: string;
|
|
8
|
+
};
|
|
9
|
+
/** Upgrade only the exact registry scaffold; authored GitHub policy is not ours. */
|
|
10
|
+
export declare function repairGithubToolsMount(cwd: string): GithubToolsMountRepair;
|
|
3
11
|
export interface UpgradeOptions {
|
|
12
|
+
allowStale?: boolean;
|
|
13
|
+
host?: true | string;
|
|
4
14
|
skipEval?: boolean;
|
|
5
15
|
yes?: boolean;
|
|
6
16
|
}
|
|
17
|
+
type EvalGatePreflight = {
|
|
18
|
+
kind: "ready";
|
|
19
|
+
} | {
|
|
20
|
+
kind: "unavailable";
|
|
21
|
+
reasons: string[];
|
|
22
|
+
};
|
|
23
|
+
export declare function preflightEvalGate(cwd: string): Promise<EvalGatePreflight>;
|
|
7
24
|
export declare function upgrade(options?: UpgradeOptions): Promise<void>;
|
|
25
|
+
export {};
|
package/dist/upgrade.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { pathToFileURL } from "node:url";
|
|
4
5
|
import { upsertEnv } from "./envfile.js";
|
|
5
6
|
import { reconcileHostArtifact } from "./host-artifacts.js";
|
|
6
7
|
import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
|
|
@@ -8,43 +9,11 @@ import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
|
|
|
8
9
|
import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
|
|
9
10
|
import { EVE_VERSION, bold, capture, dim, green, parseEnv, red, run, yellow } from "./util.js";
|
|
10
11
|
import { inspectEveAgent } from "./agent-limits.js";
|
|
12
|
+
import { remoteProjectPath, sshTarget } from "./deploy.js";
|
|
13
|
+
import { classifyModelReach } from "./model-reach.js";
|
|
14
|
+
import { checkSelfVersion, versionLt } from "./self-version.js";
|
|
15
|
+
import { LEGACY_GITHUB_TOOLS_MOUNT, githubToolsMountTs } from "./templates.js";
|
|
11
16
|
import { confirmEveUpgrade, inspectBuzzSessions, inspectDurableRuns, reconcileEvalScript, } from "./upgrade-sessions.js";
|
|
12
|
-
function versionLt(a, b) {
|
|
13
|
-
const pa = a.split(".").map(Number);
|
|
14
|
-
const pb = b.split(".").map(Number);
|
|
15
|
-
for (let i = 0; i < 3; i++) {
|
|
16
|
-
if ((pa[i] ?? 0) < (pb[i] ?? 0))
|
|
17
|
-
return true;
|
|
18
|
-
if ((pa[i] ?? 0) > (pb[i] ?? 0))
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Warn when this CLI is itself out of date.
|
|
25
|
-
*
|
|
26
|
-
* @remarks
|
|
27
|
-
* The certified eve version is a constant compiled INTO this tool, so an old
|
|
28
|
-
* kyb reports an old pin as though it were current — and does it with total
|
|
29
|
-
* confidence, in the one command whose entire job is telling you what current
|
|
30
|
-
* means. That failure runs the wrong way round: it tells a healthy agent it is
|
|
31
|
-
* ahead of certified and in "unsupported territory", which invites someone to
|
|
32
|
-
* downgrade a fleet that was fine.
|
|
33
|
-
*
|
|
34
|
-
* Checked here rather than at install because this is the command where being
|
|
35
|
-
* stale changes the answer.
|
|
36
|
-
*/
|
|
37
|
-
function warnIfStale() {
|
|
38
|
-
const installed = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
39
|
-
const latest = capture("npm", ["view", "@kybernesis/create", "version"])?.trim();
|
|
40
|
-
if (!latest || latest === installed)
|
|
41
|
-
return;
|
|
42
|
-
if (!versionLt(installed, latest))
|
|
43
|
-
return;
|
|
44
|
-
console.log(` ${yellow("!")} kyb ${installed} is behind ${latest}. The certified eve version is ` +
|
|
45
|
-
`compiled into this tool, so an old kyb reports an old pin as current.`);
|
|
46
|
-
console.log(` ${dim("npm install -g @kybernesis/create@latest")}\n`);
|
|
47
|
-
}
|
|
48
17
|
export function agentInputLimitUpgradeMessage(limit) {
|
|
49
18
|
switch (limit.kind) {
|
|
50
19
|
case "explicit-numeric":
|
|
@@ -223,6 +192,48 @@ function repairSandboxCleanupHooks(cwd, deps) {
|
|
|
223
192
|
console.log(` ${green("+")} installed terminal sandbox cleanup for ${added.length} agent scope(s) ` +
|
|
224
193
|
`${dim("(completed and failed sessions only)")}\n`);
|
|
225
194
|
}
|
|
195
|
+
/** Upgrade only the exact registry scaffold; authored GitHub policy is not ours. */
|
|
196
|
+
export function repairGithubToolsMount(cwd) {
|
|
197
|
+
const path = join(cwd, "agent/extensions/github.ts");
|
|
198
|
+
if (!existsSync(path))
|
|
199
|
+
return { kind: "missing" };
|
|
200
|
+
let current;
|
|
201
|
+
try {
|
|
202
|
+
current = readFileSync(path, "utf8");
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
return { kind: "error", message: `could not read agent/extensions/github.ts: ${error.message}` };
|
|
206
|
+
}
|
|
207
|
+
const desired = githubToolsMountTs();
|
|
208
|
+
if (current === desired)
|
|
209
|
+
return { kind: "current" };
|
|
210
|
+
if (current !== LEGACY_GITHUB_TOOLS_MOUNT)
|
|
211
|
+
return { kind: "customized" };
|
|
212
|
+
const temp = `${path}.kyb-${process.pid}`;
|
|
213
|
+
try {
|
|
214
|
+
writeFileSync(temp, desired);
|
|
215
|
+
renameSync(temp, path);
|
|
216
|
+
return { kind: "updated" };
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
rmSync(temp, { force: true });
|
|
220
|
+
return { kind: "error", message: `could not update agent/extensions/github.ts: ${error.message}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function reportGithubToolsMountRepair(cwd) {
|
|
224
|
+
const result = repairGithubToolsMount(cwd);
|
|
225
|
+
if (result.kind === "updated") {
|
|
226
|
+
console.log(` ${green("+")} updated agent/extensions/github.ts ` +
|
|
227
|
+
`${dim("(GitHub tools stay off until GITHUB_TOKEN is set)")}`);
|
|
228
|
+
}
|
|
229
|
+
else if (result.kind === "customized") {
|
|
230
|
+
console.log(` ${yellow("!")} agent/extensions/github.ts is customized, so it was left unchanged.\n` +
|
|
231
|
+
` To adopt the quiet no-token behavior, compare it with a new \`kyb init --engineer\` scaffold.`);
|
|
232
|
+
}
|
|
233
|
+
else if (result.kind === "error") {
|
|
234
|
+
console.log(` ${yellow("!")} ${result.message}; leaving the file unchanged.`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
226
237
|
/**
|
|
227
238
|
* Keep package-owned host files synchronized with the package that installed them.
|
|
228
239
|
*
|
|
@@ -237,6 +248,33 @@ function repairHostArtifacts(cwd, deps) {
|
|
|
237
248
|
const pruneSource = join(scripts, "docker-prune.sh");
|
|
238
249
|
const pruneTarget = "/etc/cron.daily/kyb-docker-prune";
|
|
239
250
|
const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
|
|
251
|
+
// The host start script is package-owned too. It carried a 45-second
|
|
252
|
+
// verdict clock that called a cold sandbox template build a failure, and no
|
|
253
|
+
// sweep for the orphaned template container that a killed start leaves
|
|
254
|
+
// behind (KYB-531). An agent still running the copy `kyb init` made keeps
|
|
255
|
+
// both faults until something refreshes it; this does, on every upgrade.
|
|
256
|
+
const serverSource = join(scripts, "eve-server.sh");
|
|
257
|
+
const serverTarget = join(cwd, "scripts/eve-server.sh");
|
|
258
|
+
if (existsSync(serverSource) && existsSync(serverTarget)) {
|
|
259
|
+
reconcileHostArtifact({
|
|
260
|
+
targetPath: serverTarget,
|
|
261
|
+
desiredContent: readFileSync(serverSource),
|
|
262
|
+
expectedMode: 0o755,
|
|
263
|
+
installIfMissing: false,
|
|
264
|
+
owner: "@kybernesis/exe/scripts/eve-server.sh",
|
|
265
|
+
update: () => {
|
|
266
|
+
try {
|
|
267
|
+
copyFileSync(serverSource, serverTarget);
|
|
268
|
+
chmodSync(serverTarget, 0o755);
|
|
269
|
+
return true;
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
manualCommand: `cp ${shellQuote(serverSource)} scripts/eve-server.sh && chmod +x scripts/eve-server.sh`,
|
|
276
|
+
});
|
|
277
|
+
}
|
|
240
278
|
if (existsSync(pruneSource)) {
|
|
241
279
|
const pruneResult = reconcileHostArtifact({
|
|
242
280
|
targetPath: pruneTarget,
|
|
@@ -330,6 +368,141 @@ function repairEvalCommand(cwd) {
|
|
|
330
368
|
` Manual remediation: make this command invoke kyb-eval instead of eve eval, preserving its setup and arguments.\n` +
|
|
331
369
|
` Current scripts.eval: ${dim(JSON.stringify(current))}`);
|
|
332
370
|
}
|
|
371
|
+
const EVAL_GATE_UNAVAILABLE_EXIT = 2;
|
|
372
|
+
const DEFAULT_EXE_MODELS_URL = "https://llm.int.exe.xyz/models.json";
|
|
373
|
+
function evalGateEnvironment(cwd) {
|
|
374
|
+
const envPath = join(cwd, ".env.local");
|
|
375
|
+
return {
|
|
376
|
+
...(existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {}),
|
|
377
|
+
...process.env,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
async function routeAnswers(url) {
|
|
381
|
+
try {
|
|
382
|
+
await fetch(url, { signal: AbortSignal.timeout(6000) });
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
385
|
+
catch {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async function claudeSubscriptionProxyReady(cwd) {
|
|
390
|
+
const module = join(cwd, "node_modules/@kybernesis/exe/dist/claude.js");
|
|
391
|
+
if (!existsSync(module))
|
|
392
|
+
return false;
|
|
393
|
+
try {
|
|
394
|
+
const loaded = (await import(pathToFileURL(module).href));
|
|
395
|
+
if (typeof loaded.claudeProxyReady !== "function")
|
|
396
|
+
return false;
|
|
397
|
+
return (await loaded.claudeProxyReady()).ok;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return false;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
/** Resolve an exe-backed judge from the provider the authored config actually calls. */
|
|
404
|
+
function exeJudgeUrl(cwd, env) {
|
|
405
|
+
const paths = [join(cwd, "evals/evals.config.ts"), join(cwd, "evals.config.ts")];
|
|
406
|
+
const path = paths.find((candidate) => existsSync(candidate));
|
|
407
|
+
if (!path)
|
|
408
|
+
return null;
|
|
409
|
+
const source = readFileSync(path, "utf8");
|
|
410
|
+
const judgeProvider = /judge\s*:\s*\{[\s\S]*?model\s*:\s*([A-Za-z_$][\w$]*)\s*\(/.exec(source)?.[1];
|
|
411
|
+
if (!judgeProvider)
|
|
412
|
+
return null;
|
|
413
|
+
const escaped = judgeProvider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
414
|
+
const providerBody = new RegExp(`(?:const|let)\\s+${escaped}\\s*=\\s*create[A-Za-z]+\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`).exec(source)?.[1];
|
|
415
|
+
if (!providerBody)
|
|
416
|
+
return /EXE_LLM_URL|llm\.int\.exe\.xyz/.test(source) ? undefined : null;
|
|
417
|
+
if (/EXE_LLM_URL/.test(providerBody))
|
|
418
|
+
return env.EXE_LLM_URL || DEFAULT_EXE_MODELS_URL;
|
|
419
|
+
const baseUrl = /baseURL\s*:\s*["']([^"']+)["']/.exec(providerBody)?.[1];
|
|
420
|
+
if (baseUrl?.includes("llm.int.exe.xyz"))
|
|
421
|
+
return baseUrl;
|
|
422
|
+
return null;
|
|
423
|
+
}
|
|
424
|
+
export async function preflightEvalGate(cwd) {
|
|
425
|
+
const env = evalGateEnvironment(cwd);
|
|
426
|
+
const agentPath = join(cwd, "agent/agent.ts");
|
|
427
|
+
const authoredSource = existsSync(agentPath) ? readFileSync(agentPath, "utf8") : null;
|
|
428
|
+
const inspection = inspectEveAgent(cwd, env);
|
|
429
|
+
const reach = classifyModelReach(authoredSource, inspection?.modelRouting ?? {
|
|
430
|
+
kind: "unresolved",
|
|
431
|
+
reason: "eve info failed before compiled model routing could be inspected",
|
|
432
|
+
});
|
|
433
|
+
const reasons = [];
|
|
434
|
+
switch (reach.kind) {
|
|
435
|
+
case "claude-sub":
|
|
436
|
+
if (!(await claudeSubscriptionProxyReady(cwd)))
|
|
437
|
+
reasons.push("the agent model proxy is unreachable");
|
|
438
|
+
break;
|
|
439
|
+
case "exe":
|
|
440
|
+
if (!(await routeAnswers(env.EXE_LLM_URL || DEFAULT_EXE_MODELS_URL)))
|
|
441
|
+
reasons.push("the agent model route is unreachable");
|
|
442
|
+
break;
|
|
443
|
+
case "unresolved":
|
|
444
|
+
reasons.push(`the agent model route could not be resolved (${reach.reason})`);
|
|
445
|
+
break;
|
|
446
|
+
case "grok-sub":
|
|
447
|
+
case "gateway":
|
|
448
|
+
case "direct-provider":
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
const judgeUrl = exeJudgeUrl(cwd, env);
|
|
452
|
+
if (judgeUrl === undefined)
|
|
453
|
+
reasons.push("the eve judge route could not be resolved");
|
|
454
|
+
else if (judgeUrl && !(await routeAnswers(judgeUrl)))
|
|
455
|
+
reasons.push("the eve judge is unreachable");
|
|
456
|
+
return reasons.length === 0 ? { kind: "ready" } : { kind: "unavailable", reasons };
|
|
457
|
+
}
|
|
458
|
+
function hostEvalCommand(cwd, target) {
|
|
459
|
+
const host = target ?? "<host>";
|
|
460
|
+
return `ssh ${host} ${shellQuote(`cd ${remoteProjectPath(cwd)} && npm run eval`)}`;
|
|
461
|
+
}
|
|
462
|
+
function reportEvalGateUnavailable(cwd, target, reasons) {
|
|
463
|
+
console.log(`\n${yellow("The eval gate for this agent can only run on its host (the model proxy and the eve judge are only reachable there).")}`);
|
|
464
|
+
for (const reason of reasons)
|
|
465
|
+
console.log(` ${dim(reason)}`);
|
|
466
|
+
console.log(`Run it there: ${bold(hostEvalCommand(cwd, target ?? undefined))}\n`);
|
|
467
|
+
process.exitCode = EVAL_GATE_UNAVAILABLE_EXIT;
|
|
468
|
+
}
|
|
469
|
+
async function runUpgradeEvalGate(cwd, host) {
|
|
470
|
+
if (host !== undefined) {
|
|
471
|
+
const target = sshTarget(cwd, typeof host === "string" && host.length > 0 ? host : undefined);
|
|
472
|
+
if (!target) {
|
|
473
|
+
reportEvalGateUnavailable(cwd, null, ["no ssh target is configured"]);
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
console.log(bold(`\nRunning the eval gate on ${target} (${remoteProjectPath(cwd)}) — deploy only on green …\n`));
|
|
477
|
+
const result = spawnSync("ssh", [target, `cd ${remoteProjectPath(cwd)} && npm run eval`], {
|
|
478
|
+
cwd,
|
|
479
|
+
env: process.env,
|
|
480
|
+
stdio: "inherit",
|
|
481
|
+
});
|
|
482
|
+
if (result.status === 0) {
|
|
483
|
+
console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts on the host.\n`);
|
|
487
|
+
process.exitCode = 1;
|
|
488
|
+
}
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const preflight = await preflightEvalGate(cwd);
|
|
492
|
+
if (preflight.kind === "unavailable") {
|
|
493
|
+
reportEvalGateUnavailable(cwd, sshTarget(cwd), preflight.reasons);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
console.log(bold("\nRunning the eval gate (npm run eval) — deploy only on green …\n"));
|
|
497
|
+
const ok = run("npm", ["run", "eval"], { cwd, allowFail: true });
|
|
498
|
+
if (ok) {
|
|
499
|
+
console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts.\n`);
|
|
503
|
+
process.exitCode = 1;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
333
506
|
function upgradeEnvironment(cwd) {
|
|
334
507
|
const envPath = join(cwd, ".env.local");
|
|
335
508
|
const env = {
|
|
@@ -480,6 +653,16 @@ function startStoppedBridge(cwd, unit) {
|
|
|
480
653
|
return false;
|
|
481
654
|
}
|
|
482
655
|
export async function upgrade(options = {}) {
|
|
656
|
+
console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
|
|
657
|
+
const selfVersion = checkSelfVersion();
|
|
658
|
+
if (selfVersion.kind === "stale") {
|
|
659
|
+
console.log(` ${yellow("!")} ${selfVersion.message}`);
|
|
660
|
+
console.log(` ${dim(selfVersion.fix)}\n`);
|
|
661
|
+
if (!options.allowStale) {
|
|
662
|
+
process.exitCode = 1;
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
}
|
|
483
666
|
const cwd = process.cwd();
|
|
484
667
|
const packagePath = join(cwd, "package.json");
|
|
485
668
|
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
@@ -487,8 +670,6 @@ export async function upgrade(options = {}) {
|
|
|
487
670
|
...(pkg.dependencies ?? {}),
|
|
488
671
|
...(pkg.devDependencies ?? {}),
|
|
489
672
|
};
|
|
490
|
-
console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
|
|
491
|
-
warnIfStale();
|
|
492
673
|
reportAgentInputLimit(cwd);
|
|
493
674
|
const entries = dependencyEntries(pkg);
|
|
494
675
|
const installedVersions = new Map();
|
|
@@ -570,6 +751,7 @@ export async function upgrade(options = {}) {
|
|
|
570
751
|
repairBuzzSetup(cwd, deps);
|
|
571
752
|
repairRemovedDefaultTools(cwd);
|
|
572
753
|
repairSandboxCleanupHooks(cwd, deps);
|
|
754
|
+
reportGithubToolsMountRepair(cwd);
|
|
573
755
|
repairHostArtifacts(cwd, deps);
|
|
574
756
|
repairEvalCommand(cwd);
|
|
575
757
|
repairManageRestart(cwd, deps);
|
|
@@ -627,6 +809,7 @@ export async function upgrade(options = {}) {
|
|
|
627
809
|
repairBuzzSetup(cwd, deps);
|
|
628
810
|
repairRemovedDefaultTools(cwd);
|
|
629
811
|
repairSandboxCleanupHooks(cwd, deps);
|
|
812
|
+
reportGithubToolsMountRepair(cwd);
|
|
630
813
|
repairHostArtifacts(cwd, deps);
|
|
631
814
|
repairEvalCommand(cwd);
|
|
632
815
|
repairManageRestart(cwd, deps);
|
|
@@ -639,13 +822,5 @@ export async function upgrade(options = {}) {
|
|
|
639
822
|
console.log(yellow("\nEval gate SKIPPED (--skip-eval). Run `npm run eval` before deploying.\n"));
|
|
640
823
|
return;
|
|
641
824
|
}
|
|
642
|
-
|
|
643
|
-
const ok = run("npm", ["run", "eval"], { cwd, allowFail: true });
|
|
644
|
-
if (ok) {
|
|
645
|
-
console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
|
|
646
|
-
}
|
|
647
|
-
else {
|
|
648
|
-
console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts.\n`);
|
|
649
|
-
process.exit(1);
|
|
650
|
-
}
|
|
825
|
+
await runUpgradeEvalGate(cwd, options.host);
|
|
651
826
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kybernesis/create",
|
|
3
|
-
"version": "0.14.
|
|
3
|
+
"version": "0.14.9",
|
|
4
4
|
"description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|