@kybernesis/create 0.14.8 → 0.14.10

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 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
@@ -34,7 +34,7 @@ function hostOf(dir) {
34
34
  return deps["@kybernesis/exe"] ? "exe" : "vercel";
35
35
  }
36
36
  /** The ssh target: an explicit flag, then EVE_SSH_HOST, then the exe VM name. */
37
- function sshTarget(dir, explicit) {
37
+ export function sshTarget(dir, explicit) {
38
38
  const e = env(dir);
39
39
  if (explicit)
40
40
  return explicit;
@@ -97,6 +97,10 @@ async function ensureModel(dir, target) {
97
97
  upsertEnv(dir, { EXE_MODEL: chosen });
98
98
  console.log(green(` ✓ EXE_MODEL="${chosen}" written to .env.local\n`));
99
99
  }
100
+ export function remoteProjectPath(dir) {
101
+ const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
102
+ return `~/${pkg.name ?? "agent"}`;
103
+ }
100
104
  export async function deploy(options) {
101
105
  const dir = options.dir ?? process.cwd();
102
106
  if (!existsSync(join(dir, "agent"))) {
@@ -118,8 +122,7 @@ export async function deploy(options) {
118
122
  process.exitCode = 1;
119
123
  return;
120
124
  }
121
- const name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name ?? "agent";
122
- const remote = `~/${name}`;
125
+ const remote = remoteProjectPath(dir);
123
126
  console.log(dim(` host: ${target} path: ${remote}\n`));
124
127
  await ensureModel(dir, target);
125
128
  const run = (cmd, args) => {
@@ -0,0 +1,8 @@
1
+ export declare function registeredDockerPruneAppDirs(installedContent: string | null): string[];
2
+ export interface DockerPruneCronArtifact {
3
+ content: Buffer;
4
+ appDirs: string[];
5
+ manualCommand: string;
6
+ }
7
+ /** Render the package script with a managed, mergeable app-directory header. */
8
+ export declare function dockerPruneCronArtifact(packageSource: string, currentAppDir: string, installedContent: string | null, targetPath: string): DockerPruneCronArtifact;
@@ -0,0 +1,38 @@
1
+ const MANAGED_PREFIX = "# kyb-managed-eve-app-dirs: ";
2
+ function shellSingleQuote(value) {
3
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
4
+ }
5
+ export function registeredDockerPruneAppDirs(installedContent) {
6
+ if (!installedContent)
7
+ return [];
8
+ const line = installedContent.split("\n").find((candidate) => candidate.startsWith(MANAGED_PREFIX));
9
+ if (!line)
10
+ return [];
11
+ try {
12
+ const parsed = JSON.parse(line.slice(MANAGED_PREFIX.length));
13
+ return Array.isArray(parsed) && parsed.every((value) => typeof value === "string") ? parsed : [];
14
+ }
15
+ catch {
16
+ return [];
17
+ }
18
+ }
19
+ /** Render the package script with a managed, mergeable app-directory header. */
20
+ export function dockerPruneCronArtifact(packageSource, currentAppDir, installedContent, targetPath) {
21
+ const appDirs = [...new Set([...registeredDockerPruneAppDirs(installedContent), currentAppDir])];
22
+ const newline = packageSource.indexOf("\n");
23
+ const shebang = newline === -1 ? packageSource : packageSource.slice(0, newline);
24
+ const body = newline === -1 ? "" : packageSource.slice(newline + 1);
25
+ const joined = appDirs.join(":");
26
+ const content = Buffer.from(`${shebang}\n` +
27
+ `${MANAGED_PREFIX}${JSON.stringify(appDirs)}\n` +
28
+ `if [ -z "\${EVE_APP_DIRS:-}" ]; then EVE_APP_DIRS=${shellSingleQuote(joined)}; fi\n` +
29
+ `export EVE_APP_DIRS\n` +
30
+ body);
31
+ const encoded = content.toString("base64");
32
+ return {
33
+ content,
34
+ appDirs,
35
+ manualCommand: `printf %s ${shellSingleQuote(encoded)} | base64 -d | ` +
36
+ `sudo install -m 0755 /dev/stdin ${shellSingleQuote(targetPath)}`,
37
+ };
38
+ }
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";
@@ -20,6 +22,7 @@ export interface DockerTemplateInspection {
20
22
  export declare function dockerTemplateDoctorChecks(result: DockerTemplateInspection): Check[];
21
23
  export declare function modelReachDoctorCheck(authoredSource: string | null, compiledRouting: CompiledModelRouting): Check;
22
24
  export declare function agentInputLimitDoctorCheck(limit: AgentInputLimit): Check;
25
+ export declare function diskUsageDoctorCheck(percent: number, dockerSummary: string | null): Check;
23
26
  /**
24
27
  * The inspector lives in @kybernesis/exe, next to the runtime that owns the
25
28
  * templates; create already resolves that package's scripts out of
package/dist/doctor.js CHANGED
@@ -6,11 +6,22 @@ import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
6
6
  import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.js";
7
7
  import { inspectEveAgent } from "./agent-limits.js";
8
8
  import { classifyModelReach } from "./model-reach.js";
9
+ import { checkSelfVersion } from "./self-version.js";
9
10
  const MARK = {
10
11
  pass: green("✓"),
11
12
  warn: yellow("!"),
12
13
  fail: red("✗"),
13
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
+ }
14
25
  export function dockerTemplateDoctorChecks(result) {
15
26
  if (result.status === "skipped")
16
27
  return [];
@@ -40,6 +51,8 @@ export function modelReachDoctorCheck(authoredSource, compiledRouting) {
40
51
  return { verdict: "pass", label: "Model reach: claude-sub (Claude subscription)" };
41
52
  case "exe":
42
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)" };
43
56
  case "gateway":
44
57
  return { verdict: "pass", label: "Model reach: gateway" };
45
58
  case "direct-provider":
@@ -71,6 +84,15 @@ export function agentInputLimitDoctorCheck(limit) {
71
84
  };
72
85
  }
73
86
  }
87
+ export function diskUsageDoctorCheck(percent, dockerSummary) {
88
+ if (percent < 85)
89
+ return { verdict: "pass", label: `disk ${percent}% used` };
90
+ const detail = `Docker: ${dockerSummary?.trim() || "docker system df unavailable"}. ` +
91
+ `Run: sudo /etc/cron.daily/kyb-docker-prune`;
92
+ if (percent < 95)
93
+ return { verdict: "warn", label: `disk ${percent}% used`, detail };
94
+ return { verdict: "fail", label: `disk ${percent}% used`, detail };
95
+ }
74
96
  /**
75
97
  * The inspector lives in @kybernesis/exe, next to the runtime that owns the
76
98
  * templates; create already resolves that package's scripts out of
@@ -99,6 +121,10 @@ export async function doctor() {
99
121
  const installedAgentService = findMatchingAgentServiceUnit(cwd);
100
122
  const checks = [];
101
123
  const add = (verdict, label, detail) => checks.push({ verdict, label, detail });
124
+ const selfVersion = checkSelfVersion();
125
+ if (selfVersion.kind === "stale") {
126
+ add("warn", selfVersion.message, selfVersion.fix);
127
+ }
102
128
  // ── project shape ──────────────────────────────────────────────────────
103
129
  if (!existsSync(join(cwd, "agent"))) {
104
130
  console.error(red("Not an eve agent project (no agent/ directory). Run inside the agent repo."));
@@ -138,6 +164,9 @@ export async function doctor() {
138
164
  };
139
165
  if (!existsSync(envPath))
140
166
  add("warn", ".env.local missing", "copy .env.example and fill it");
167
+ const githubCheck = githubToolsDoctorCheck(existsSync(join(cwd, "agent/extensions/github.ts")), env);
168
+ if (githubCheck)
169
+ add(githubCheck.verdict, githubCheck.label, githubCheck.detail);
141
170
  // ── arcana keys: validate every key↔workspace pair read-only ───────────
142
171
  const arcanaPairs = [];
143
172
  if (env.ARCANA_API_KEY && env.ARCANA_COMPANY_WORKSPACE)
@@ -386,14 +415,17 @@ export async function doctor() {
386
415
  const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
387
416
  const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
388
417
  if (job !== "yes") {
389
- add("warn", "no daily docker reclaim on this host", "sandbox templates, build cache, and generic stopped containers accumulate; durable sessions stay protected and terminal hooks remove closed sessions; `kyb upgrade` installs /etc/cron.daily/kyb-docker-prune");
390
- }
391
- else if (percent >= 80) {
392
- add("fail", `disk ${percent}% full despite the reclaim job`, "run it now: sudo /etc/cron.daily/kyb-docker-prune, and check what else is on this host");
418
+ add("warn", "no daily docker reclaim on this host", "sandbox templates, build cache, and generic stopped containers accumulate; `kyb upgrade` installs /etc/cron.daily/kyb-docker-prune");
393
419
  }
394
420
  else {
395
- add("pass", `daily docker reclaim installed (disk ${percent}% used)`);
421
+ add("pass", "daily docker reclaim installed");
396
422
  }
423
+ const dockerSummary = capture("sh", [
424
+ "-c",
425
+ "docker system df 2>/dev/null | head -3 | paste -sd ';' -",
426
+ ]);
427
+ const diskCheck = diskUsageDoctorCheck(percent, dockerSummary);
428
+ add(diskCheck.verdict, diskCheck.label, diskCheck.detail);
397
429
  }
398
430
  /**
399
431
  * Instructions that name a tool the agent does not have.
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
  }
@@ -19,6 +19,8 @@ export type DoctorModelReach = {
19
19
  kind: "claude-sub";
20
20
  } | {
21
21
  kind: "exe";
22
+ } | {
23
+ kind: "grok-sub";
22
24
  } | {
23
25
  kind: "gateway";
24
26
  } | {
@@ -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
- if (claudeSubscription && !exeModel)
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 (exeModel && !claudeSubscription)
42
+ if (authored === 1 && exeModel)
41
43
  return { kind: "exe" };
42
- if (claudeSubscription && exeModel) {
43
- return { kind: "unresolved", reason: "root agent authors both claudeSubscription() and exeModel()" };
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,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
+ }
@@ -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,50 +1,21 @@
1
- import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
+ import { tmpdir } from "node:os";
3
4
  import { join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
4
6
  import { upsertEnv } from "./envfile.js";
5
7
  import { reconcileHostArtifact } from "./host-artifacts.js";
8
+ import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
6
9
  import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
7
10
  import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
8
11
  import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
9
12
  import { EVE_VERSION, bold, capture, dim, green, parseEnv, red, run, yellow } from "./util.js";
10
13
  import { inspectEveAgent } from "./agent-limits.js";
14
+ import { remoteProjectPath, sshTarget } from "./deploy.js";
15
+ import { classifyModelReach } from "./model-reach.js";
16
+ import { checkSelfVersion, versionLt } from "./self-version.js";
17
+ import { LEGACY_GITHUB_TOOLS_MOUNT, githubToolsMountTs } from "./templates.js";
11
18
  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
19
  export function agentInputLimitUpgradeMessage(limit) {
49
20
  switch (limit.kind) {
50
21
  case "explicit-numeric":
@@ -223,6 +194,48 @@ function repairSandboxCleanupHooks(cwd, deps) {
223
194
  console.log(` ${green("+")} installed terminal sandbox cleanup for ${added.length} agent scope(s) ` +
224
195
  `${dim("(completed and failed sessions only)")}\n`);
225
196
  }
197
+ /** Upgrade only the exact registry scaffold; authored GitHub policy is not ours. */
198
+ export function repairGithubToolsMount(cwd) {
199
+ const path = join(cwd, "agent/extensions/github.ts");
200
+ if (!existsSync(path))
201
+ return { kind: "missing" };
202
+ let current;
203
+ try {
204
+ current = readFileSync(path, "utf8");
205
+ }
206
+ catch (error) {
207
+ return { kind: "error", message: `could not read agent/extensions/github.ts: ${error.message}` };
208
+ }
209
+ const desired = githubToolsMountTs();
210
+ if (current === desired)
211
+ return { kind: "current" };
212
+ if (current !== LEGACY_GITHUB_TOOLS_MOUNT)
213
+ return { kind: "customized" };
214
+ const temp = `${path}.kyb-${process.pid}`;
215
+ try {
216
+ writeFileSync(temp, desired);
217
+ renameSync(temp, path);
218
+ return { kind: "updated" };
219
+ }
220
+ catch (error) {
221
+ rmSync(temp, { force: true });
222
+ return { kind: "error", message: `could not update agent/extensions/github.ts: ${error.message}` };
223
+ }
224
+ }
225
+ function reportGithubToolsMountRepair(cwd) {
226
+ const result = repairGithubToolsMount(cwd);
227
+ if (result.kind === "updated") {
228
+ console.log(` ${green("+")} updated agent/extensions/github.ts ` +
229
+ `${dim("(GitHub tools stay off until GITHUB_TOKEN is set)")}`);
230
+ }
231
+ else if (result.kind === "customized") {
232
+ console.log(` ${yellow("!")} agent/extensions/github.ts is customized, so it was left unchanged.\n` +
233
+ ` To adopt the quiet no-token behavior, compare it with a new \`kyb init --engineer\` scaffold.`);
234
+ }
235
+ else if (result.kind === "error") {
236
+ console.log(` ${yellow("!")} ${result.message}; leaving the file unchanged.`);
237
+ }
238
+ }
226
239
  /**
227
240
  * Keep package-owned host files synchronized with the package that installed them.
228
241
  *
@@ -235,7 +248,7 @@ function repairHostArtifacts(cwd, deps) {
235
248
  return;
236
249
  const scripts = join(cwd, "node_modules/@kybernesis/exe/scripts");
237
250
  const pruneSource = join(scripts, "docker-prune.sh");
238
- const pruneTarget = "/etc/cron.daily/kyb-docker-prune";
251
+ const pruneTarget = process.env.KYB_DOCKER_PRUNE_TARGET ?? "/etc/cron.daily/kyb-docker-prune";
239
252
  const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
240
253
  // The host start script is package-owned too. It carried a 45-second
241
254
  // verdict clock that called a cold sandbox template build a failure, and no
@@ -265,18 +278,37 @@ function repairHostArtifacts(cwd, deps) {
265
278
  });
266
279
  }
267
280
  if (existsSync(pruneSource)) {
281
+ let installedPrune = null;
282
+ try {
283
+ if (existsSync(pruneTarget))
284
+ installedPrune = readFileSync(pruneTarget, "utf8");
285
+ }
286
+ catch {
287
+ // reconcileHostArtifact reports the unreadable target and refuses to replace it.
288
+ }
289
+ const pruneArtifact = dockerPruneCronArtifact(readFileSync(pruneSource, "utf8"), cwd, installedPrune, pruneTarget);
268
290
  const pruneResult = reconcileHostArtifact({
269
291
  targetPath: pruneTarget,
270
- desiredContent: readFileSync(pruneSource),
292
+ desiredContent: pruneArtifact.content,
271
293
  expectedMode: 0o755,
272
294
  installIfMissing: hasDocker,
273
- owner: "@kybernesis/exe/scripts/docker-prune.sh",
274
- update: () => run("sudo", ["-n", "install", "-m", "0755", pruneSource, pruneTarget], {
275
- cwd,
276
- allowFail: true,
277
- quiet: true,
278
- }),
279
- manualCommand: `sudo install -m 0755 ${shellQuote(pruneSource)} ${pruneTarget}`,
295
+ owner: "@kybernesis/exe/scripts/docker-prune.sh with registered app directories",
296
+ update: () => {
297
+ const stageDir = mkdtempSync(join(tmpdir(), "kyb-docker-prune-"));
298
+ const staged = join(stageDir, "docker-prune.sh");
299
+ try {
300
+ writeFileSync(staged, pruneArtifact.content, { mode: 0o755 });
301
+ return run("sudo", ["-n", "install", "-m", "0755", staged, pruneTarget], {
302
+ cwd,
303
+ allowFail: true,
304
+ quiet: true,
305
+ });
306
+ }
307
+ finally {
308
+ rmSync(stageDir, { recursive: true, force: true });
309
+ }
310
+ },
311
+ manualCommand: pruneArtifact.manualCommand,
280
312
  });
281
313
  if ((pruneResult === "current" || pruneResult === "updated") && existsSync("/etc/cron.weekly/docker-prune")) {
282
314
  const removed = run("sudo", ["-n", "rm", "-f", "/etc/cron.weekly/docker-prune"], {
@@ -357,6 +389,141 @@ function repairEvalCommand(cwd) {
357
389
  ` Manual remediation: make this command invoke kyb-eval instead of eve eval, preserving its setup and arguments.\n` +
358
390
  ` Current scripts.eval: ${dim(JSON.stringify(current))}`);
359
391
  }
392
+ const EVAL_GATE_UNAVAILABLE_EXIT = 2;
393
+ const DEFAULT_EXE_MODELS_URL = "https://llm.int.exe.xyz/models.json";
394
+ function evalGateEnvironment(cwd) {
395
+ const envPath = join(cwd, ".env.local");
396
+ return {
397
+ ...(existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {}),
398
+ ...process.env,
399
+ };
400
+ }
401
+ async function routeAnswers(url) {
402
+ try {
403
+ await fetch(url, { signal: AbortSignal.timeout(6000) });
404
+ return true;
405
+ }
406
+ catch {
407
+ return false;
408
+ }
409
+ }
410
+ async function claudeSubscriptionProxyReady(cwd) {
411
+ const module = join(cwd, "node_modules/@kybernesis/exe/dist/claude.js");
412
+ if (!existsSync(module))
413
+ return false;
414
+ try {
415
+ const loaded = (await import(pathToFileURL(module).href));
416
+ if (typeof loaded.claudeProxyReady !== "function")
417
+ return false;
418
+ return (await loaded.claudeProxyReady()).ok;
419
+ }
420
+ catch {
421
+ return false;
422
+ }
423
+ }
424
+ /** Resolve an exe-backed judge from the provider the authored config actually calls. */
425
+ function exeJudgeUrl(cwd, env) {
426
+ const paths = [join(cwd, "evals/evals.config.ts"), join(cwd, "evals.config.ts")];
427
+ const path = paths.find((candidate) => existsSync(candidate));
428
+ if (!path)
429
+ return null;
430
+ const source = readFileSync(path, "utf8");
431
+ const judgeProvider = /judge\s*:\s*\{[\s\S]*?model\s*:\s*([A-Za-z_$][\w$]*)\s*\(/.exec(source)?.[1];
432
+ if (!judgeProvider)
433
+ return null;
434
+ const escaped = judgeProvider.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
435
+ const providerBody = new RegExp(`(?:const|let)\\s+${escaped}\\s*=\\s*create[A-Za-z]+\\s*\\(\\s*\\{([\\s\\S]*?)\\}\\s*\\)`).exec(source)?.[1];
436
+ if (!providerBody)
437
+ return /EXE_LLM_URL|llm\.int\.exe\.xyz/.test(source) ? undefined : null;
438
+ if (/EXE_LLM_URL/.test(providerBody))
439
+ return env.EXE_LLM_URL || DEFAULT_EXE_MODELS_URL;
440
+ const baseUrl = /baseURL\s*:\s*["']([^"']+)["']/.exec(providerBody)?.[1];
441
+ if (baseUrl?.includes("llm.int.exe.xyz"))
442
+ return baseUrl;
443
+ return null;
444
+ }
445
+ export async function preflightEvalGate(cwd) {
446
+ const env = evalGateEnvironment(cwd);
447
+ const agentPath = join(cwd, "agent/agent.ts");
448
+ const authoredSource = existsSync(agentPath) ? readFileSync(agentPath, "utf8") : null;
449
+ const inspection = inspectEveAgent(cwd, env);
450
+ const reach = classifyModelReach(authoredSource, inspection?.modelRouting ?? {
451
+ kind: "unresolved",
452
+ reason: "eve info failed before compiled model routing could be inspected",
453
+ });
454
+ const reasons = [];
455
+ switch (reach.kind) {
456
+ case "claude-sub":
457
+ if (!(await claudeSubscriptionProxyReady(cwd)))
458
+ reasons.push("the agent model proxy is unreachable");
459
+ break;
460
+ case "exe":
461
+ if (!(await routeAnswers(env.EXE_LLM_URL || DEFAULT_EXE_MODELS_URL)))
462
+ reasons.push("the agent model route is unreachable");
463
+ break;
464
+ case "unresolved":
465
+ reasons.push(`the agent model route could not be resolved (${reach.reason})`);
466
+ break;
467
+ case "grok-sub":
468
+ case "gateway":
469
+ case "direct-provider":
470
+ break;
471
+ }
472
+ const judgeUrl = exeJudgeUrl(cwd, env);
473
+ if (judgeUrl === undefined)
474
+ reasons.push("the eve judge route could not be resolved");
475
+ else if (judgeUrl && !(await routeAnswers(judgeUrl)))
476
+ reasons.push("the eve judge is unreachable");
477
+ return reasons.length === 0 ? { kind: "ready" } : { kind: "unavailable", reasons };
478
+ }
479
+ function hostEvalCommand(cwd, target) {
480
+ const host = target ?? "<host>";
481
+ return `ssh ${host} ${shellQuote(`cd ${remoteProjectPath(cwd)} && npm run eval`)}`;
482
+ }
483
+ function reportEvalGateUnavailable(cwd, target, reasons) {
484
+ 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).")}`);
485
+ for (const reason of reasons)
486
+ console.log(` ${dim(reason)}`);
487
+ console.log(`Run it there: ${bold(hostEvalCommand(cwd, target ?? undefined))}\n`);
488
+ process.exitCode = EVAL_GATE_UNAVAILABLE_EXIT;
489
+ }
490
+ async function runUpgradeEvalGate(cwd, host) {
491
+ if (host !== undefined) {
492
+ const target = sshTarget(cwd, typeof host === "string" && host.length > 0 ? host : undefined);
493
+ if (!target) {
494
+ reportEvalGateUnavailable(cwd, null, ["no ssh target is configured"]);
495
+ return;
496
+ }
497
+ console.log(bold(`\nRunning the eval gate on ${target} (${remoteProjectPath(cwd)}) — deploy only on green …\n`));
498
+ const result = spawnSync("ssh", [target, `cd ${remoteProjectPath(cwd)} && npm run eval`], {
499
+ cwd,
500
+ env: process.env,
501
+ stdio: "inherit",
502
+ });
503
+ if (result.status === 0) {
504
+ console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
505
+ }
506
+ else {
507
+ console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts on the host.\n`);
508
+ process.exitCode = 1;
509
+ }
510
+ return;
511
+ }
512
+ const preflight = await preflightEvalGate(cwd);
513
+ if (preflight.kind === "unavailable") {
514
+ reportEvalGateUnavailable(cwd, sshTarget(cwd), preflight.reasons);
515
+ return;
516
+ }
517
+ console.log(bold("\nRunning the eval gate (npm run eval) — deploy only on green …\n"));
518
+ const ok = run("npm", ["run", "eval"], { cwd, allowFail: true });
519
+ if (ok) {
520
+ console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
521
+ }
522
+ else {
523
+ console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts.\n`);
524
+ process.exitCode = 1;
525
+ }
526
+ }
360
527
  function upgradeEnvironment(cwd) {
361
528
  const envPath = join(cwd, ".env.local");
362
529
  const env = {
@@ -507,6 +674,16 @@ function startStoppedBridge(cwd, unit) {
507
674
  return false;
508
675
  }
509
676
  export async function upgrade(options = {}) {
677
+ console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
678
+ const selfVersion = checkSelfVersion();
679
+ if (selfVersion.kind === "stale") {
680
+ console.log(` ${yellow("!")} ${selfVersion.message}`);
681
+ console.log(` ${dim(selfVersion.fix)}\n`);
682
+ if (!options.allowStale) {
683
+ process.exitCode = 1;
684
+ return;
685
+ }
686
+ }
510
687
  const cwd = process.cwd();
511
688
  const packagePath = join(cwd, "package.json");
512
689
  const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
@@ -514,8 +691,6 @@ export async function upgrade(options = {}) {
514
691
  ...(pkg.dependencies ?? {}),
515
692
  ...(pkg.devDependencies ?? {}),
516
693
  };
517
- console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
518
- warnIfStale();
519
694
  reportAgentInputLimit(cwd);
520
695
  const entries = dependencyEntries(pkg);
521
696
  const installedVersions = new Map();
@@ -597,6 +772,7 @@ export async function upgrade(options = {}) {
597
772
  repairBuzzSetup(cwd, deps);
598
773
  repairRemovedDefaultTools(cwd);
599
774
  repairSandboxCleanupHooks(cwd, deps);
775
+ reportGithubToolsMountRepair(cwd);
600
776
  repairHostArtifacts(cwd, deps);
601
777
  repairEvalCommand(cwd);
602
778
  repairManageRestart(cwd, deps);
@@ -654,6 +830,7 @@ export async function upgrade(options = {}) {
654
830
  repairBuzzSetup(cwd, deps);
655
831
  repairRemovedDefaultTools(cwd);
656
832
  repairSandboxCleanupHooks(cwd, deps);
833
+ reportGithubToolsMountRepair(cwd);
657
834
  repairHostArtifacts(cwd, deps);
658
835
  repairEvalCommand(cwd);
659
836
  repairManageRestart(cwd, deps);
@@ -666,13 +843,5 @@ export async function upgrade(options = {}) {
666
843
  console.log(yellow("\nEval gate SKIPPED (--skip-eval). Run `npm run eval` before deploying.\n"));
667
844
  return;
668
845
  }
669
- console.log(bold("\nRunning the eval gate (npm run eval) — deploy only on green …\n"));
670
- const ok = run("npm", ["run", "eval"], { cwd, allowFail: true });
671
- if (ok) {
672
- console.log(`\n${green("✓ Upgrade green.")} Deploy with: ${bold("npx eve deploy")}\n`);
673
- }
674
- else {
675
- console.log(`\n${red("✗ Evals failed after upgrade.")} Do NOT deploy — inspect .eve/evals/ artifacts.\n`);
676
- process.exit(1);
677
- }
846
+ await runUpgradeEvalGate(cwd, options.host);
678
847
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.8",
3
+ "version": "0.14.10",
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",