@kybernesis/create 0.14.8 → 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 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) => {
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
@@ -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":
@@ -99,6 +112,10 @@ export async function doctor() {
99
112
  const installedAgentService = findMatchingAgentServiceUnit(cwd);
100
113
  const checks = [];
101
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
+ }
102
119
  // ── project shape ──────────────────────────────────────────────────────
103
120
  if (!existsSync(join(cwd, "agent"))) {
104
121
  console.error(red("Not an eve agent project (no agent/ directory). Run inside the agent repo."));
@@ -138,6 +155,9 @@ export async function doctor() {
138
155
  };
139
156
  if (!existsSync(envPath))
140
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);
141
161
  // ── arcana keys: validate every key↔workspace pair read-only ───────────
142
162
  const arcanaPairs = [];
143
163
  if (env.ARCANA_API_KEY && env.ARCANA_COMPANY_WORKSPACE)
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,6 +1,7 @@
1
- import { chmodSync, copyFileSync, 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
  *
@@ -357,6 +368,141 @@ function repairEvalCommand(cwd) {
357
368
  ` Manual remediation: make this command invoke kyb-eval instead of eve eval, preserving its setup and arguments.\n` +
358
369
  ` Current scripts.eval: ${dim(JSON.stringify(current))}`);
359
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
+ }
360
506
  function upgradeEnvironment(cwd) {
361
507
  const envPath = join(cwd, ".env.local");
362
508
  const env = {
@@ -507,6 +653,16 @@ function startStoppedBridge(cwd, unit) {
507
653
  return false;
508
654
  }
509
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
+ }
510
666
  const cwd = process.cwd();
511
667
  const packagePath = join(cwd, "package.json");
512
668
  const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
@@ -514,8 +670,6 @@ export async function upgrade(options = {}) {
514
670
  ...(pkg.dependencies ?? {}),
515
671
  ...(pkg.devDependencies ?? {}),
516
672
  };
517
- console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
518
- warnIfStale();
519
673
  reportAgentInputLimit(cwd);
520
674
  const entries = dependencyEntries(pkg);
521
675
  const installedVersions = new Map();
@@ -597,6 +751,7 @@ export async function upgrade(options = {}) {
597
751
  repairBuzzSetup(cwd, deps);
598
752
  repairRemovedDefaultTools(cwd);
599
753
  repairSandboxCleanupHooks(cwd, deps);
754
+ reportGithubToolsMountRepair(cwd);
600
755
  repairHostArtifacts(cwd, deps);
601
756
  repairEvalCommand(cwd);
602
757
  repairManageRestart(cwd, deps);
@@ -654,6 +809,7 @@ export async function upgrade(options = {}) {
654
809
  repairBuzzSetup(cwd, deps);
655
810
  repairRemovedDefaultTools(cwd);
656
811
  repairSandboxCleanupHooks(cwd, deps);
812
+ reportGithubToolsMountRepair(cwd);
657
813
  repairHostArtifacts(cwd, deps);
658
814
  repairEvalCommand(cwd);
659
815
  repairManageRestart(cwd, deps);
@@ -666,13 +822,5 @@ export async function upgrade(options = {}) {
666
822
  console.log(yellow("\nEval gate SKIPPED (--skip-eval). Run `npm run eval` before deploying.\n"));
667
823
  return;
668
824
  }
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
- }
825
+ await runUpgradeEvalGate(cwd, options.host);
678
826
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.8",
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",