@kybernesis/create 0.14.3 → 0.14.5

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
@@ -47,13 +47,22 @@ Run inside an agent project. Checks, with pass/warn/fail per line:
47
47
 
48
48
  Exit code 1 on any failure — usable as a CI preflight.
49
49
 
50
- ## `kyb upgrade [--skip-eval]`
50
+ ## `kyb upgrade [--skip-eval] [--yes]`
51
51
 
52
52
  Compares installed `@kybernesis/*` versions against npm, installs what's
53
53
  behind, typechecks, then **runs the eval suite as the gate** — prints
54
54
  "deploy" only on green. This is the maintenance-retainer loop as a command:
55
55
  package improvement → `kyb upgrade` per client → green → `npx eve deploy`.
56
56
 
57
+ Before an eve version change, upgrade reads `.eve/.workflow-data/runs`, prints
58
+ `This will reset N open conversations`, lists matching persisted Buzz channel
59
+ sessions when available, and requires explicit confirmation. Noninteractive
60
+ runs fail closed unless `--yes` (or `-y`) is supplied. The warning is still printed with
61
+ `--yes` and is not bypassed by `--skip-eval`. Existing direct `eve eval` scripts
62
+ are migrated to `kyb-eval`; custom scripts are left untouched with a manual fix.
63
+ The wrapper counts corruption diagnostics beside the eval result and turns a
64
+ nominally green eve exit into a failure when condemned durable state was seen.
65
+
57
66
  On self-hosted agents, upgrade also reconciles package-owned host artifacts. It
58
67
  refreshes a stale `/etc/cron.daily/kyb-docker-prune` and an existing generated
59
68
  systemd unit, warning before replacement and printing an exact manual repair
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@
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
+ * --yes, -y confirm an eve upgrade noninteractively
14
15
  */
15
16
  import { existsSync, readFileSync } from "node:fs";
16
17
  import { basename, dirname, join } from "node:path";
@@ -73,7 +74,7 @@ const COMMANDS = {
73
74
  credential: "Write the agent credential onto a host (--local to stay here).",
74
75
  register: "Register this agent with the control plane (--name, --url).",
75
76
  deploy: "Deploy this agent to its host (--no-env to leave the env file alone).",
76
- upgrade: "Bring @kybernesis packages and eve to the certified versions (--skip-eval).",
77
+ upgrade: "Bring @kybernesis packages and eve to certified versions (--skip-eval, --yes).",
77
78
  tui: "Talk to your agents in the terminal.",
78
79
  version: "Print the version of this tool.",
79
80
  };
@@ -144,7 +145,10 @@ switch (command) {
144
145
  tui(rest);
145
146
  break;
146
147
  case "upgrade":
147
- await upgrade(rest.includes("--skip-eval"));
148
+ await upgrade({
149
+ skipEval: rest.includes("--skip-eval"),
150
+ yes: rest.includes("--yes") || rest.includes("-y"),
151
+ });
148
152
  break;
149
153
  case "--version":
150
154
  case "-v":
@@ -209,6 +213,7 @@ ${dim(" npm i -g @kybernesis/create@latest")}
209
213
  --no-env ${dim("do not send .env.local (host manages its own secrets)")}
210
214
  ${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
211
215
  --skip-eval ${dim("skip the eval gate")}
216
+ --yes, -y ${dim("confirm an eve version change noninteractively")}
212
217
 
213
218
  Registry: https://registry.kybernesis.ai · Docs: the FDE playbook
214
219
  `);
package/dist/templates.js CHANGED
@@ -207,7 +207,7 @@ export function evalScript(name, depts) {
207
207
  `ARCANA_DM_WORKSPACE=${name}-eval`,
208
208
  ...depts.map((d) => `ARCANA_${d.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_WORKSPACE=${name}-eval`),
209
209
  ];
210
- return `${overrides.join(" ")} eve eval`;
210
+ return `${overrides.join(" ")} kyb-eval`;
211
211
  }
212
212
  export const CHANNEL_KINDS = [
213
213
  "none",
@@ -0,0 +1,43 @@
1
+ import type { Readable, Writable } from "node:stream";
2
+ export interface DurableRunInspection {
3
+ runningRunIds: string[];
4
+ inspectedRecords: number;
5
+ issues: string[];
6
+ }
7
+ export interface BuzzSessionMatch {
8
+ runId: string;
9
+ community: string;
10
+ channel: string;
11
+ }
12
+ export interface BuzzSessionInspection {
13
+ path?: string;
14
+ matches: BuzzSessionMatch[];
15
+ issue?: string;
16
+ }
17
+ export type EvalScriptReconciliation = {
18
+ kind: "updated";
19
+ script: string;
20
+ } | {
21
+ kind: "current";
22
+ script: string;
23
+ } | {
24
+ kind: "custom";
25
+ script: string;
26
+ } | {
27
+ kind: "missing";
28
+ };
29
+ /** Read the local durable records without loading eve or mutating its store. */
30
+ export declare function inspectDurableRuns(cwd: string): DurableRunInspection;
31
+ /** Follow the same precedence as buzzBridge when enough environment is available. */
32
+ export declare function buzzSessionsPath(cwd: string, env: Record<string, string | undefined>): string | undefined;
33
+ /** Best-effort join from durable run ids to Buzz's persisted community|channel map. */
34
+ export declare function inspectBuzzSessions(cwd: string, env: Record<string, string | undefined>, runningRunIds: readonly string[]): BuzzSessionInspection;
35
+ /** Replace only a simple direct eve eval invocation, preserving assignments and arguments. */
36
+ export declare function reconcileEvalScript(script: unknown): EvalScriptReconciliation;
37
+ export declare function confirmEveUpgrade(options: {
38
+ yes: boolean;
39
+ input?: Readable & {
40
+ isTTY?: boolean;
41
+ };
42
+ output?: Writable;
43
+ }): Promise<boolean>;
@@ -0,0 +1,123 @@
1
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { dirname, isAbsolute, join, resolve } from "node:path";
3
+ import { createInterface } from "node:readline/promises";
4
+ /** Read the local durable records without loading eve or mutating its store. */
5
+ export function inspectDurableRuns(cwd) {
6
+ const runsDir = join(cwd, ".eve", ".workflow-data", "runs");
7
+ if (!existsSync(runsDir))
8
+ return { runningRunIds: [], inspectedRecords: 0, issues: [] };
9
+ let entries;
10
+ try {
11
+ entries = readdirSync(runsDir, { withFileTypes: true });
12
+ }
13
+ catch (error) {
14
+ return {
15
+ runningRunIds: [],
16
+ inspectedRecords: 0,
17
+ issues: [`could not read ${runsDir}: ${error.message}`],
18
+ };
19
+ }
20
+ const runningRunIds = [];
21
+ const issues = [];
22
+ let inspectedRecords = 0;
23
+ for (const entry of entries) {
24
+ if (!entry.isFile() || !entry.name.endsWith(".json"))
25
+ continue;
26
+ const path = join(runsDir, entry.name);
27
+ try {
28
+ const record = JSON.parse(readFileSync(path, "utf8"));
29
+ inspectedRecords += 1;
30
+ if (record.status !== "running")
31
+ continue;
32
+ if (typeof record.runId !== "string" || record.runId.length === 0) {
33
+ issues.push(`${path}: running record has no string runId`);
34
+ continue;
35
+ }
36
+ runningRunIds.push(record.runId);
37
+ }
38
+ catch (error) {
39
+ issues.push(`${path}: ${error.message}`);
40
+ }
41
+ }
42
+ return { runningRunIds, inspectedRecords, issues };
43
+ }
44
+ function resolveEnvPath(cwd, value) {
45
+ return isAbsolute(value) ? value : resolve(cwd, value);
46
+ }
47
+ /** Follow the same precedence as buzzBridge when enough environment is available. */
48
+ export function buzzSessionsPath(cwd, env) {
49
+ if (env.BUZZ_SESSIONS_FILE)
50
+ return resolveEnvPath(cwd, env.BUZZ_SESSIONS_FILE);
51
+ const legacy = join(cwd, ".buzz-sessions.json");
52
+ if (existsSync(legacy))
53
+ return legacy;
54
+ if (env.BUZZ_KEYFILE)
55
+ return join(dirname(resolveEnvPath(cwd, env.BUZZ_KEYFILE)), "buzz-sessions.json");
56
+ return undefined;
57
+ }
58
+ /** Best-effort join from durable run ids to Buzz's persisted community|channel map. */
59
+ export function inspectBuzzSessions(cwd, env, runningRunIds) {
60
+ const path = buzzSessionsPath(cwd, env);
61
+ if (!path) {
62
+ return { matches: [], issue: "no BUZZ_SESSIONS_FILE, legacy .buzz-sessions.json, or BUZZ_KEYFILE was found" };
63
+ }
64
+ if (!existsSync(path))
65
+ return { path, matches: [], issue: `${path} does not exist` };
66
+ try {
67
+ const raw = JSON.parse(readFileSync(path, "utf8"));
68
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
69
+ throw new Error("expected an object map");
70
+ const running = new Set(runningRunIds);
71
+ const matches = [];
72
+ for (const [key, value] of Object.entries(raw)) {
73
+ if (!value || typeof value !== "object" || Array.isArray(value))
74
+ continue;
75
+ const runId = value.id;
76
+ if (typeof runId !== "string" || !running.has(runId))
77
+ continue;
78
+ const separator = key.lastIndexOf("|");
79
+ matches.push({
80
+ runId,
81
+ community: separator < 0 ? "unknown community" : key.slice(0, separator),
82
+ channel: separator < 0 ? key : key.slice(separator + 1),
83
+ });
84
+ }
85
+ return { path, matches };
86
+ }
87
+ catch (error) {
88
+ return { path, matches: [], issue: `could not read Buzz session metadata at ${path}: ${error.message}` };
89
+ }
90
+ }
91
+ /** Replace only a simple direct eve eval invocation, preserving assignments and arguments. */
92
+ export function reconcileEvalScript(script) {
93
+ if (typeof script !== "string" || script.trim().length === 0)
94
+ return { kind: "missing" };
95
+ if (/^\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+))\s+)*kyb-eval(?:\s|$)/.test(script)) {
96
+ return { kind: "current", script };
97
+ }
98
+ if (/[;&|<>`]/.test(script))
99
+ return { kind: "custom", script };
100
+ const direct = /^(\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s]+))\s+)*)eve\s+eval(?=\s|$)(.*)$/.exec(script);
101
+ if (!direct)
102
+ return { kind: "custom", script };
103
+ return { kind: "updated", script: `${direct[1]}kyb-eval${direct[2]}` };
104
+ }
105
+ export async function confirmEveUpgrade(options) {
106
+ if (options.yes)
107
+ return true;
108
+ const input = options.input ?? process.stdin;
109
+ const output = options.output ?? process.stdout;
110
+ if (input.isTTY !== true)
111
+ return false;
112
+ const rl = createInterface({ input, output });
113
+ try {
114
+ const answer = (await rl.question("Continue with the eve upgrade? Type yes to continue: ")).trim().toLowerCase();
115
+ return answer === "yes" || answer === "y";
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ finally {
121
+ rl.close();
122
+ }
123
+ }
package/dist/upgrade.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  import { type AgentInputLimit } from "./agent-limits.js";
2
2
  export declare function agentInputLimitUpgradeMessage(limit: AgentInputLimit): string;
3
- export declare function upgrade(skipEval: boolean): Promise<void>;
3
+ export interface UpgradeOptions {
4
+ skipEval?: boolean;
5
+ yes?: boolean;
6
+ }
7
+ export declare function upgrade(options?: UpgradeOptions): Promise<void>;
package/dist/upgrade.js CHANGED
@@ -1,31 +1,14 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { spawnSync } from "node:child_process";
2
3
  import { join } from "node:path";
3
4
  import { upsertEnv } from "./envfile.js";
4
5
  import { reconcileHostArtifact } from "./host-artifacts.js";
5
- import { repairManageRestart } from "./systemd.js";
6
+ import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
6
7
  import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
7
8
  import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
8
9
  import { EVE_VERSION, bold, capture, dim, green, parseEnv, red, run, yellow } from "./util.js";
9
10
  import { inspectEveAgent } from "./agent-limits.js";
10
- /**
11
- * Which packages to upgrade: every `@kybernesis/*` this agent depends on.
12
- *
13
- * @remarks
14
- * This was a fixed list of six, written when there were six. Four more shipped
15
- * afterwards — connectors, local, manage and exe — and an agent using them was
16
- * told "everything is at latest certified versions" while holding versions from
17
- * months earlier. A hardcoded list does not fail loudly when it falls behind;
18
- * it just quietly stops covering things, and the command that reports it is the
19
- * same one that is wrong.
20
- *
21
- * Reading the manifest cannot fall behind. A package added tomorrow is covered
22
- * by an upgrade run today.
23
- */
24
- function kybernesisPackages(deps) {
25
- return Object.keys(deps)
26
- .filter((name) => name.startsWith("@kybernesis/"))
27
- .sort();
28
- }
11
+ import { confirmEveUpgrade, inspectBuzzSessions, inspectDurableRuns, reconcileEvalScript, } from "./upgrade-sessions.js";
29
12
  function versionLt(a, b) {
30
13
  const pa = a.split(".").map(Number);
31
14
  const pb = b.split(".").map(Number);
@@ -330,113 +313,319 @@ function repairHostArtifacts(cwd, deps) {
330
313
  manualCommand: `bash ${shellQuote(serviceScript)} --refresh-unit`,
331
314
  });
332
315
  }
333
- export async function upgrade(skipEval) {
316
+ function repairEvalCommand(cwd) {
317
+ const packagePath = join(cwd, "package.json");
318
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
319
+ const result = reconcileEvalScript(pkg.scripts?.eval);
320
+ if (result.kind === "updated") {
321
+ pkg.scripts = { ...pkg.scripts, eval: result.script };
322
+ writeFileSync(packagePath, `${JSON.stringify(pkg, null, 2)}\n`);
323
+ console.log(` ${green("+")} updated scripts.eval to run through kyb-eval`);
324
+ return;
325
+ }
326
+ if (result.kind === "current")
327
+ return;
328
+ const current = result.kind === "custom" ? result.script : "(missing)";
329
+ console.log(` ${yellow("!")} scripts.eval is not a recognized direct eve eval command, so it was left unchanged.\n` +
330
+ ` Manual remediation: make this command invoke kyb-eval instead of eve eval, preserving its setup and arguments.\n` +
331
+ ` Current scripts.eval: ${dim(JSON.stringify(current))}`);
332
+ }
333
+ function upgradeEnvironment(cwd) {
334
+ const envPath = join(cwd, ".env.local");
335
+ const env = {
336
+ ...(existsSync(envPath) ? parseEnv(readFileSync(envPath, "utf8")) : {}),
337
+ ...process.env,
338
+ };
339
+ if (!env.BUZZ_SESSIONS_FILE && !env.BUZZ_KEYFILE) {
340
+ const unit = capture("sh", [
341
+ "-c",
342
+ "grep -ohE 'BUZZ_(SESSIONS_FILE|KEYFILE)=[^ \" ]+' /etc/systemd/system/*buzz-bridge.service 2>/dev/null | head -1",
343
+ ]);
344
+ const [name, ...value] = (unit ?? "").trim().split("=");
345
+ if (name && value.length > 0)
346
+ env[name] = value.join("=").replace(/^\"|\"$/g, "");
347
+ }
348
+ return env;
349
+ }
350
+ async function approveEveChange(cwd, yes, hasBuzz) {
351
+ const durable = inspectDurableRuns(cwd);
352
+ console.log(bold(`\nThis will reset ${durable.runningRunIds.length} open conversations`));
353
+ if (durable.issues.length > 0) {
354
+ console.log(` ${yellow("!")} Durable-store inspection was incomplete; the count includes only readable records ` +
355
+ `whose status is exactly "running".`);
356
+ for (const issue of durable.issues)
357
+ console.log(` ${dim(issue)}`);
358
+ }
359
+ if (hasBuzz) {
360
+ const buzz = inspectBuzzSessions(cwd, upgradeEnvironment(cwd), durable.runningRunIds);
361
+ if (buzz.matches.length > 0) {
362
+ console.log(" Buzz-bound open conversations:");
363
+ for (const match of buzz.matches) {
364
+ console.log(` ${match.runId} ${dim(`(${match.community} | ${match.channel})`)}`);
365
+ }
366
+ }
367
+ else if (buzz.issue) {
368
+ console.log(` ${yellow("!")} Buzz session metadata unavailable: ${buzz.issue}`);
369
+ }
370
+ else {
371
+ console.log(` ${dim("No open durable conversations were matched to Buzz session metadata.")}`);
372
+ }
373
+ }
374
+ const approved = await confirmEveUpgrade({ yes });
375
+ if (!approved) {
376
+ console.log(`\n${red("Eve upgrade cancelled before installation.")} ` +
377
+ `${dim("Pass --yes for deliberate noninteractive use.")}\n`);
378
+ }
379
+ return approved;
380
+ }
381
+ function commandResult(command, args, cwd, print = true) {
382
+ console.log(dim(` $ ${command} ${args.join(" ")}`));
383
+ const result = spawnSync(command, args, { cwd, encoding: "utf8", env: process.env });
384
+ const stdout = result.stdout ?? "";
385
+ const stderr = result.stderr ?? "";
386
+ if (print) {
387
+ if (stdout)
388
+ process.stdout.write(stdout);
389
+ if (stderr)
390
+ process.stderr.write(stderr);
391
+ }
392
+ return { status: result.status ?? 1, stdout, stderr };
393
+ }
394
+ function fail(message) {
395
+ console.error(red(message));
396
+ process.exitCode = 1;
397
+ }
398
+ function majorMinor(version) {
399
+ const match = /^(\d+)\.(\d+)(?:\.|$)/.exec(version);
400
+ if (!match)
401
+ return null;
402
+ const major = Number(match[1]);
403
+ const minor = Number(match[2]);
404
+ return Number.isSafeInteger(major) && Number.isSafeInteger(minor) ? [major, minor] : null;
405
+ }
406
+ function installedPackageValue(cwd, name, expression) {
407
+ const packageName = JSON.stringify(`${name}/package.json`);
408
+ return capture("node", ["-p", `require(${packageName})${expression}`], cwd)?.trim() || null;
409
+ }
410
+ function targetEvePeer(name, version) {
411
+ return capture("npm", ["view", `${name}@${version}`, "peerDependencies.eve"])?.trim() || undefined;
412
+ }
413
+ function dependencyEntries(pkg) {
414
+ const entries = [];
415
+ for (const section of ["dependencies", "devDependencies"]) {
416
+ const values = pkg[section];
417
+ for (const [name, range] of Object.entries(values ?? {})) {
418
+ if (name === "eve" || name.startsWith("@kybernesis/"))
419
+ entries.push({ name, section, range });
420
+ }
421
+ }
422
+ return entries.sort((a, b) => a.name.localeCompare(b.name) || a.section.localeCompare(b.section));
423
+ }
424
+ function rewriteManifest(cwd, pkg, targets) {
425
+ for (const target of targets) {
426
+ const section = pkg[target.section];
427
+ section[target.name] = `^${target.targetVersion}`;
428
+ }
429
+ writeFileSync(join(cwd, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`);
430
+ }
431
+ function recoveryGuidance(targets, bridgeUnit) {
432
+ console.log(`\n${yellow("The dependency tree was not verified. Complete the clean install before using it:")}\n` +
433
+ ` 1. Ensure every root @kybernesis/* and eve range in package.json is the resolved ^version.\n` +
434
+ ` 2. ${dim("rm -rf node_modules")}\n` +
435
+ ` 3. ${dim("rm -f package-lock.json")}\n` +
436
+ ` 4. ${dim("npm install")}\n` +
437
+ ` 5. ${dim("npm ls eve")}`);
438
+ const peerChanges = targets.filter((target) => target.name.startsWith("@kybernesis/") &&
439
+ target.installedEvePeer !== target.targetEvePeer &&
440
+ (target.installedEvePeer !== undefined || target.targetEvePeer !== undefined));
441
+ if (peerChanges.length > 0) {
442
+ console.log("\n Published Eve peer metadata changed for these root packages:");
443
+ for (const target of peerChanges) {
444
+ console.log(` ${target.name}: ${target.installedVersion} peers on ${target.installedEvePeer ?? "(none)"}; ` +
445
+ `${target.targetVersion} peers on ${target.targetEvePeer ?? "(none)"}`);
446
+ }
447
+ console.log(dim(" These are metadata facts, not an attribution of npm's reported conflict."));
448
+ }
449
+ if (bridgeUnit) {
450
+ console.log(`\n The Buzz bridge remains stopped until the tree is verified. Then run:\n ${dim(`sudo -n systemctl start ${bridgeUnit}`)}`);
451
+ }
452
+ }
453
+ function buzzBridgeUnit(cwd) {
454
+ const systemdDir = process.env.KYB_SYSTEMD_DIR;
455
+ const match = findMatchingAgentServiceUnit(cwd, systemdDir || undefined);
456
+ return match ? `${match.values.name}-buzz-bridge.service` : null;
457
+ }
458
+ function stopActiveBridge(cwd, unit) {
459
+ if (!unit)
460
+ return null;
461
+ const active = commandResult("systemctl", ["is-active", "--quiet", unit], cwd, false);
462
+ if (active.status !== 0)
463
+ return null;
464
+ const stopped = commandResult("sudo", ["-n", "systemctl", "stop", unit], cwd);
465
+ if (stopped.status === 0)
466
+ return unit;
467
+ fail(`Could not stop the active Buzz bridge. Run exactly:\n sudo -n systemctl stop ${unit}`);
468
+ return false;
469
+ }
470
+ function startStoppedBridge(cwd, unit) {
471
+ const started = commandResult("sudo", ["-n", "systemctl", "start", unit], cwd);
472
+ if (started.status === 0)
473
+ return true;
474
+ fail(`Could not restart the Buzz bridge. Run exactly:\n` +
475
+ ` sudo -n systemctl start ${unit}\n` +
476
+ ` systemctl status ${unit}`);
477
+ return false;
478
+ }
479
+ export async function upgrade(options = {}) {
334
480
  const cwd = process.cwd();
335
- const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
336
- const deps = { ...pkg.dependencies, ...pkg.devDependencies };
481
+ const packagePath = join(cwd, "package.json");
482
+ const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
483
+ const deps = {
484
+ ...(pkg.dependencies ?? {}),
485
+ ...(pkg.devDependencies ?? {}),
486
+ };
337
487
  console.log(bold("\nkyb upgrade — checking @kybernesis/* and eve against npm\n"));
338
488
  warnIfStale();
339
- // Read-only: report the compiled effective policy, never rewrite agent/agent.ts.
340
489
  reportAgentInputLimit(cwd);
341
- // Env-only, so it is safe before anything is installed.
342
- repairLocalQueueTimeouts(cwd, deps);
343
- const toUpgrade = [];
344
- const unresolved = [];
345
- for (const name of kybernesisPackages(deps)) {
346
- if (!deps[name])
490
+ const entries = dependencyEntries(pkg);
491
+ const installedVersions = new Map();
492
+ const targetVersions = new Map();
493
+ const installedPeers = new Map();
494
+ const targetPeers = new Map();
495
+ const unresolved = new Set();
496
+ for (const name of [...new Set(entries.map((entry) => entry.name))]) {
497
+ const installed = installedPackageValue(cwd, name, ".version");
498
+ if (!installed) {
499
+ console.log(` ${yellow("!")} ${name}: could not resolve installed version`);
500
+ unresolved.add(name);
347
501
  continue;
348
- const installed = capture("node", ["-p", `require('${name}/package.json').version`], cwd)?.trim();
349
- const latest = capture("npm", ["view", name, "version"])?.trim();
350
- if (!installed || !latest) {
351
- console.log(` ${yellow("!")} ${name}: could not resolve versions`);
352
- unresolved.push(name);
502
+ }
503
+ installedVersions.set(name, installed);
504
+ const target = name === "eve" ? EVE_VERSION : capture("npm", ["view", name, "version"])?.trim();
505
+ if (!target) {
506
+ console.log(` ${yellow("!")} ${name}: could not resolve target version`);
507
+ unresolved.add(name);
353
508
  continue;
354
509
  }
355
- if (installed === latest)
356
- console.log(` ${green("✓")} ${name}@${installed} ${dim("(latest)")}`);
357
- else {
358
- console.log(` ${yellow("↑")} ${name}: ${installed} → ${latest}`);
359
- toUpgrade.push(`${name}@${latest}`);
510
+ targetVersions.set(name, target);
511
+ if (name.startsWith("@kybernesis/")) {
512
+ installedPeers.set(name, installedPackageValue(cwd, name, ".peerDependencies?.eve || ''") ?? undefined);
513
+ targetPeers.set(name, targetEvePeer(name, target));
360
514
  }
361
- }
362
- // eve itself: upgrade to the KYBERNESIS-CERTIFIED version, never blindly to
363
- // npm latest. Certification happens in the platform repo — packages are
364
- // tested against a new eve, then the pin in @kybernesis/create advances,
365
- // then this command carries client agents there behind their eval gate.
366
- let eveChanged = false;
367
- const eveInstalled = capture("node", ["-p", "require('eve/package.json').version"], cwd)?.trim();
368
- const eveLatest = capture("npm", ["view", "eve", "version"])?.trim();
369
- if (eveInstalled) {
370
- if (versionLt(eveInstalled, EVE_VERSION)) {
371
- console.log(` ${yellow("↑")} eve: ${eveInstalled} → ${EVE_VERSION} ${dim("(Kybernesis-certified)")}`);
372
- toUpgrade.push(`eve@${EVE_VERSION}`);
373
- eveChanged = true;
515
+ if (installed === target) {
516
+ console.log(` ${green("✓")} ${name}@${installed} ${dim(name === "eve" ? "(certified)" : "(latest)")}`);
374
517
  }
375
- else if (versionLt(EVE_VERSION, eveInstalled)) {
376
- console.log(` ${yellow("!")} eve@${eveInstalled} is AHEAD of the certified ${EVE_VERSION} ${dim("— unsupported territory")}`);
518
+ else if (name === "eve" && versionLt(EVE_VERSION, installed)) {
519
+ console.log(` ${yellow("!")} eve@${installed} is AHEAD of the certified ${EVE_VERSION} ${dim("— unsupported territory")}`);
377
520
  }
378
521
  else {
379
- console.log(` ${green("✓")} eve@${eveInstalled} ${dim("(certified)")}`);
380
- }
381
- if (eveLatest && versionLt(EVE_VERSION, eveLatest)) {
382
- console.log(dim(` note: eve@${eveLatest} exists upstream; ${EVE_VERSION} is the newest Kybernesis-certified version.`));
522
+ console.log(` ${yellow("↑")} ${name}: ${installed} → ${target}` +
523
+ (name === "eve" ? ` ${dim("(Kybernesis-certified)")}` : ""));
383
524
  }
384
525
  }
385
- if (toUpgrade.length === 0 && unresolved.length > 0) {
386
- // Saying everything is current, having just failed to check several
387
- // packages, is the worst available answer: it is the sentence someone
388
- // repeats to a client. Usually the dependencies are simply not installed
389
- // here, which is worth naming rather than hiding behind a green tick.
390
- console.log(`\n${yellow(`Checked what could be read. ${unresolved.length} package(s) could not be ` +
391
- `resolved, so this is not a clean bill of health.`)}\n` +
392
- ` ${dim("Usually: dependencies are not installed here. Run npm install, then kyb upgrade.")}\n`);
393
- repairManageRestart(cwd, deps);
526
+ const eveInstalled = installedVersions.get("eve");
527
+ const eveLatest = capture("npm", ["view", "eve", "version"])?.trim();
528
+ if (eveLatest && versionLt(EVE_VERSION, eveLatest)) {
529
+ console.log(dim(` note: eve@${eveLatest} exists upstream; ${EVE_VERSION} is the newest Kybernesis-certified version.`));
530
+ }
531
+ if (unresolved.size > 0) {
532
+ fail(`\nUpgrade planning failed for ${[...unresolved].sort().join(", ")}. ` +
533
+ `No manifest, dependency tree, or service state was changed.`);
394
534
  return;
395
535
  }
396
- if (toUpgrade.length === 0) {
536
+ const targets = entries.map((entry) => ({
537
+ name: entry.name,
538
+ section: entry.section,
539
+ currentRange: entry.range,
540
+ installedVersion: installedVersions.get(entry.name),
541
+ targetVersion: targetVersions.get(entry.name),
542
+ installedEvePeer: installedPeers.get(entry.name),
543
+ targetEvePeer: targetPeers.get(entry.name),
544
+ }));
545
+ const eveTarget = targetVersions.get("eve");
546
+ const installedMajorMinor = eveInstalled ? majorMinor(eveInstalled) : null;
547
+ const targetMajorMinor = eveTarget ? majorMinor(eveTarget) : null;
548
+ if ((eveInstalled && !installedMajorMinor) || (eveTarget && !targetMajorMinor)) {
549
+ fail(`Cannot safely compare Eve versions ${JSON.stringify(eveInstalled)} and ${JSON.stringify(eveTarget)}.`);
550
+ return;
551
+ }
552
+ const eveChanged = Boolean(eveInstalled && eveTarget && versionLt(eveInstalled, eveTarget));
553
+ const cleanInstall = Boolean(eveChanged &&
554
+ installedMajorMinor &&
555
+ targetMajorMinor &&
556
+ (installedMajorMinor[0] !== targetMajorMinor[0] || installedMajorMinor[1] !== targetMajorMinor[1]));
557
+ const changed = [...new Set(targets
558
+ .filter((target) => target.installedVersion !== target.targetVersion && !(target.name === "eve" && versionLt(target.targetVersion, target.installedVersion)))
559
+ .map((target) => `${target.name}@${target.targetVersion}`))];
560
+ if (eveChanged && !(await approveEveChange(cwd, options.yes === true, Boolean(deps["@kybernesis/buzz"])))) {
561
+ process.exitCode = 1;
562
+ return;
563
+ }
564
+ repairLocalQueueTimeouts(cwd, deps);
565
+ if (changed.length === 0) {
397
566
  console.log(`\n${green("Everything is at latest certified versions.")}\n`);
398
- // Still repair: being on the right versions is not the same as being set
399
- // up. An agent can sit at latest for weeks with a capability switched off.
400
567
  repairBuzzSetup(cwd, deps);
401
568
  repairRemovedDefaultTools(cwd);
402
569
  repairSandboxCleanupHooks(cwd, deps);
403
570
  repairHostArtifacts(cwd, deps);
571
+ repairEvalCommand(cwd);
404
572
  repairManageRestart(cwd, deps);
405
573
  return;
406
574
  }
407
- console.log(bold(`\nInstalling: ${toUpgrade.join(", ")}\n`));
408
- /**
409
- * One install, with peers relaxed. Every @kybernesis package peers on a
410
- * narrow eve range, and eve moves with them: the installed set demands the
411
- * old eve, the new set demands the new one, and npm's resolver refuses
412
- * either order — eve first, packages first, or all at once — under strict
413
- * peers (seen on Kyber moving 0.38 → 0.49: ERESOLVE all three ways). The
414
- * set being installed is exactly the certified combination, so relaxing
415
- * peer resolution for this one command changes nothing about what ends up
416
- * on disk; the version check above is what guards correctness.
417
- */
418
- run("npm", ["install", ...toUpgrade, "--legacy-peer-deps"], { cwd });
419
- /**
420
- * Repairs run AFTER the install, not before.
421
- *
422
- * Both of these copy files out of packages that the install has just put
423
- * there — a proxy script, a cron job, a CLI. Running them first meant looking
424
- * for a file the older installed version did not ship: the repair found
425
- * nothing, said nothing, and only worked on the NEXT upgrade. Which is a
426
- * bug that hides itself, because by then it looks like it always worked.
427
- */
575
+ console.log(bold(`\nInstalling: ${changed.join(", ")}\n`));
576
+ let stoppedBridge = null;
577
+ if (cleanInstall && deps["@kybernesis/buzz"]) {
578
+ const stopped = stopActiveBridge(cwd, buzzBridgeUnit(cwd));
579
+ if (stopped === false)
580
+ return;
581
+ stoppedBridge = stopped;
582
+ }
583
+ if (cleanInstall) {
584
+ rewriteManifest(cwd, pkg, targets);
585
+ try {
586
+ rmSync(join(cwd, "node_modules"), { recursive: true, force: true });
587
+ rmSync(join(cwd, "package-lock.json"), { force: true });
588
+ }
589
+ catch (error) {
590
+ fail(`Could not remove npm resolver state: ${error.message}`);
591
+ recoveryGuidance(targets, stoppedBridge ?? undefined);
592
+ return;
593
+ }
594
+ }
595
+ const installArgs = cleanInstall ? ["install"] : ["install", ...changed];
596
+ const installed = commandResult("npm", installArgs, cwd);
597
+ if (installed.status !== 0) {
598
+ const output = `${installed.stdout}\n${installed.stderr}`;
599
+ if (/ERESOLVE/i.test(output)) {
600
+ console.log(red("\nnpm reported ERESOLVE while installing the planned dependency set."));
601
+ }
602
+ else {
603
+ console.log(red("\nnpm install failed before the dependency tree could be verified."));
604
+ }
605
+ recoveryGuidance(targets, stoppedBridge ?? undefined);
606
+ process.exitCode = 1;
607
+ return;
608
+ }
609
+ const validation = commandResult("npm", ["ls", "eve"], cwd);
610
+ if (validation.status !== 0) {
611
+ fail("npm ls eve failed. The installed peer tree is not valid.");
612
+ recoveryGuidance(targets, stoppedBridge ?? undefined);
613
+ return;
614
+ }
615
+ if (stoppedBridge && !startStoppedBridge(cwd, stoppedBridge))
616
+ return;
428
617
  repairBuzzSetup(cwd, deps);
429
618
  repairRemovedDefaultTools(cwd);
430
619
  repairSandboxCleanupHooks(cwd, deps);
431
620
  repairHostArtifacts(cwd, deps);
621
+ repairEvalCommand(cwd);
432
622
  repairManageRestart(cwd, deps);
433
623
  run("npm", ["run", "typecheck"], { cwd });
434
624
  if (eveChanged) {
435
- // A framework bump must also pass discovery/compile, not just types.
436
625
  const infoOk = run("npx", ["eve", "info"], { cwd, allowFail: true, quiet: true });
437
626
  console.log(infoOk ? green(" ✓ eve discovery clean after framework upgrade") : red(" ✗ eve info failed after framework upgrade — inspect before going further"));
438
627
  }
439
- if (skipEval) {
628
+ if (options.skipEval) {
440
629
  console.log(yellow("\nEval gate SKIPPED (--skip-eval). Run `npm run eval` before deploying.\n"));
441
630
  return;
442
631
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.3",
4
- "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent — plus doctor and upgrade.",
3
+ "version": "0.14.5",
4
+ "description": "The Kybernesis agent scaffolder and FDE toolkit: one command to a governed, remembering, multiplayer, self-testing eve agent \u2014 plus doctor and upgrade.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "homepage": "https://kybernesis.ai",
@@ -1229,7 +1229,7 @@ to `acme-eval` and evals never write into a real brain:
1229
1229
 
1230
1230
  ```jsonc
1231
1231
  "scripts": {
1232
- "eval": "ARCANA_COMPANY_WORKSPACE=acme-eval ARCANA_DM_WORKSPACE=acme-eval ARCANA_FINANCE_WORKSPACE=acme-eval ARCANA_ENGINEERING_WORKSPACE=acme-eval eve eval",
1232
+ "eval": "ARCANA_COMPANY_WORKSPACE=acme-eval ARCANA_DM_WORKSPACE=acme-eval ARCANA_FINANCE_WORKSPACE=acme-eval ARCANA_ENGINEERING_WORKSPACE=acme-eval kyb-eval",
1233
1233
  "typecheck": "tsc"
1234
1234
  }
1235
1235
  ```