@kybernesis/create 0.15.6 → 0.15.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/doctor.js CHANGED
@@ -7,7 +7,9 @@ import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.j
7
7
  import { inspectEveAgent } from "./agent-limits.js";
8
8
  import { classifyModelReach } from "./model-reach.js";
9
9
  import { countAbandonedRuns } from "./run-store.js";
10
+ import { describeStaleSidecar, newestPackageChange, staleSidecars } from "./stale-sidecar.js";
10
11
  import { checkSelfVersion } from "./self-version.js";
12
+ import { envUrlProblems } from "./env-urls.js";
11
13
  const MARK = {
12
14
  pass: green("✓"),
13
15
  warn: yellow("!"),
@@ -117,6 +119,17 @@ async function head(url, headers) {
117
119
  return null;
118
120
  }
119
121
  }
122
+ /** Running units that belong to a Kybernesis agent on this host. */
123
+ function listAgentUnits() {
124
+ const out = capture("systemctl", ["list-units", "--type=service", "--state=running", "--no-legend", "--plain"]);
125
+ if (!out)
126
+ return [];
127
+ return out
128
+ .split("\n")
129
+ .map((line) => line.trim().split(/\s+/)[0] ?? "")
130
+ .filter((unit) => /-(agent|buzz-bridge)\.service$/.test(unit))
131
+ .map((unit) => unit.replace(/\.service$/, ""));
132
+ }
120
133
  export async function doctor() {
121
134
  const cwd = process.cwd();
122
135
  const installedAgentService = findMatchingAgentServiceUnit(cwd);
@@ -132,6 +145,29 @@ export async function doctor() {
132
145
  * them. Reported rather than fixed: the remedy restarts the agent, which is
133
146
  * not something a diagnostic should do behind someone's back.
134
147
  */
148
+ /**
149
+ * Services still running the code they loaded at start.
150
+ *
151
+ * An agent host has more than one: deploy.sh restarts the agent, and the Buzz
152
+ * bridge is a separate unit it never touches. Both bridges on the reference
153
+ * fleet ran four days on a superseded package while the fix sat on disk, so
154
+ * two changes signed off as deployed were live nowhere. Nothing looked
155
+ * broken — the old code simply kept doing what it did.
156
+ */
157
+ const packageChange = newestPackageChange(cwd);
158
+ const started = new Map();
159
+ for (const unit of listAgentUnits()) {
160
+ const stamp = capture("systemctl", ["show", "-p", "ActiveEnterTimestamp", "--value", unit])?.trim();
161
+ const at = stamp ? new Date(stamp) : null;
162
+ if (at && !Number.isNaN(at.getTime()))
163
+ started.set(unit, at);
164
+ }
165
+ for (const stale of staleSidecars({ started, packageChange })) {
166
+ add("fail", `${stale.service} is running superseded code`, describeStaleSidecar(stale));
167
+ }
168
+ if (started.size > 0 && staleSidecars({ started, packageChange }).length === 0) {
169
+ add("pass", `every service is running the code currently installed (${started.size} checked)`);
170
+ }
135
171
  const runs = countAbandonedRuns(cwd);
136
172
  if (runs) {
137
173
  if (runs.abandoned === 0) {
@@ -180,6 +216,22 @@ export async function doctor() {
180
216
  };
181
217
  if (!existsSync(envPath))
182
218
  add("warn", ".env.local missing", "copy .env.example and fill it");
219
+ /**
220
+ * Config that is shaped wrong rather than missing.
221
+ *
222
+ * A URL-valued setting with no scheme does not fail at start; it fails inside
223
+ * whatever background drain uses it, once per turn, forever. The reference
224
+ * fleet logged 121 of these and lost every analytics event of a deployment
225
+ * while reporting healthy throughout. The value alone is enough to know, so
226
+ * this costs nothing and lands before a client's first turn.
227
+ */
228
+ const urlProblems = existsSync(envPath) ? envUrlProblems(readFileSync(envPath, "utf8")) : [];
229
+ for (const problem of urlProblems) {
230
+ add("fail", `${problem.name} cannot build a request (${problem.reason})`, problem.fix);
231
+ }
232
+ if (existsSync(envPath) && urlProblems.length === 0) {
233
+ add("pass", "every URL-valued setting is an absolute http(s) URL");
234
+ }
183
235
  const githubCheck = githubToolsDoctorCheck(existsSync(join(cwd, "agent/extensions/github.ts")), env);
184
236
  if (githubCheck)
185
237
  add(githubCheck.verdict, githubCheck.label, githubCheck.detail);
@@ -0,0 +1,17 @@
1
+ /** Does this name hold a whole URL? */
2
+ export declare function expectsUrl(name: string): boolean;
3
+ export type EnvUrlProblem = {
4
+ readonly name: string;
5
+ /** Why it cannot be used to build a request. Never includes the value. */
6
+ readonly reason: "no scheme" | "not a url" | "not http(s)";
7
+ readonly fix: string;
8
+ };
9
+ /**
10
+ * Check one value. Returns undefined when it is fine.
11
+ *
12
+ * The value never appears in the result: these files sit beside real secrets
13
+ * and a diagnostic that echoes them turns a config warning into a leak.
14
+ */
15
+ export declare function checkEnvUrl(name: string, value: string): EnvUrlProblem | undefined;
16
+ /** Every URL-shaped value in this env text that cannot build a request. */
17
+ export declare function envUrlProblems(text: string): EnvUrlProblem[];
@@ -0,0 +1,79 @@
1
+ import { parseEnv } from "./util.js";
2
+ /**
3
+ * Environment values that are used to build a URL, and whether they can be.
4
+ *
5
+ * An SDK handed `POSTHOG_HOST=eu.i.posthog.com` does not complain at start. It
6
+ * fails once per turn, forever, inside a background drain: `Failed to parse URL
7
+ * from eu.i.posthog.com/batch/`. On the reference fleet that was 121 logged
8
+ * failures and every analytics event of the deployment silently discarded,
9
+ * while the agent itself looked perfectly healthy. Nothing surfaces it because
10
+ * nothing that matters to the turn depends on it.
11
+ *
12
+ * Hence checking the SHAPE of the value at rest, which costs nothing and finds
13
+ * it before a client's first turn.
14
+ */
15
+ /**
16
+ * Names whose value is unambiguously a whole URL.
17
+ *
18
+ * `_HOST` is deliberately NOT here: plenty of them legitimately hold a bare
19
+ * hostname (`SMTP_HOST`, `PGHOST`, `REDIS_HOST`), and flagging those would make
20
+ * doctor cry wolf on a correct config.
21
+ */
22
+ const URL_SUFFIXES = ["_URL", "_ENDPOINT", "_ISSUER"];
23
+ /**
24
+ * `_HOST` names that specific SDKs use as a base URL, so they do need a scheme.
25
+ * An allowlist rather than a rule, because only the SDK knows which it is.
26
+ */
27
+ const BASE_URL_HOSTS = new Set(["POSTHOG_HOST"]);
28
+ /** Does this name hold a whole URL? */
29
+ export function expectsUrl(name) {
30
+ if (BASE_URL_HOSTS.has(name))
31
+ return true;
32
+ return URL_SUFFIXES.some((suffix) => name.endsWith(suffix));
33
+ }
34
+ /**
35
+ * Check one value. Returns undefined when it is fine.
36
+ *
37
+ * The value never appears in the result: these files sit beside real secrets
38
+ * and a diagnostic that echoes them turns a config warning into a leak.
39
+ */
40
+ export function checkEnvUrl(name, value) {
41
+ const trimmed = value.trim().replace(/^["']|["']$/g, "");
42
+ if (trimmed === "")
43
+ return undefined;
44
+ let parsed;
45
+ try {
46
+ parsed = new URL(trimmed);
47
+ }
48
+ catch {
49
+ // A bare host is by far the common case, and the remedy differs from junk.
50
+ const bare = /^[a-z0-9.-]+(:\d+)?(\/.*)?$/i.test(trimmed);
51
+ return {
52
+ name,
53
+ reason: bare ? "no scheme" : "not a url",
54
+ fix: bare
55
+ ? `${name} has no scheme, so anything built from it fails to parse. Prefix it with https://`
56
+ : `${name} is not a URL. Set it to an absolute https:// URL.`,
57
+ };
58
+ }
59
+ if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
60
+ return {
61
+ name,
62
+ reason: "not http(s)",
63
+ fix: `${name} is a ${parsed.protocol.replace(":", "")} URL. Set it to an absolute https:// URL.`,
64
+ };
65
+ }
66
+ return undefined;
67
+ }
68
+ /** Every URL-shaped value in this env text that cannot build a request. */
69
+ export function envUrlProblems(text) {
70
+ const problems = [];
71
+ for (const [name, value] of Object.entries(parseEnv(text))) {
72
+ if (!expectsUrl(name))
73
+ continue;
74
+ const problem = checkEnvUrl(name, value);
75
+ if (problem)
76
+ problems.push(problem);
77
+ }
78
+ return problems;
79
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Picking the right build of a per-eve-line package.
3
+ *
4
+ * `@kybernesis/*` packages ship one build per eve line, and the version numbers
5
+ * are NOT ordered by line: `@kybernesis/voice` 0.1.2 is the eve 0.49 build and
6
+ * was published AFTER 0.1.1, which is the 0.51 build. So `npm view <pkg>
7
+ * version` — npm's `latest` — is routinely the wrong package for a given host,
8
+ * and installing it puts an agent on a build compiled against an eve API it is
9
+ * not running.
10
+ *
11
+ * That failure is silent. The package installs, the agent builds, the service
12
+ * starts, and one code path breaks later at runtime. It is how
13
+ * `@kybernesis/notify` took an agent down and how two agents ran a 0.51 build of
14
+ * `@kybernesis/manage` on eve 0.49 for days without anyone noticing.
15
+ *
16
+ * So an upgrade must never ask for "latest". It must ask for the newest version
17
+ * whose declared eve peer range contains the eve this host actually runs.
18
+ */
19
+ /** A published version and the eve range it declares. */
20
+ export interface Candidate {
21
+ version: string;
22
+ /** `peerDependencies.eve`, or undefined when the package declares none. */
23
+ peer?: string;
24
+ }
25
+ /**
26
+ * Compare semver-ish versions numerically.
27
+ *
28
+ * Numerically, because these are picked by sorting and string order puts 0.8.9
29
+ * above 0.8.10 — which would hand a host the wrong build. A prerelease sorts
30
+ * BELOW its release (0.1.2-rc < 0.1.2), so a release candidate is never chosen
31
+ * over the release it precedes.
32
+ */
33
+ export declare function versionCompare(a: string, b: string): number;
34
+ /**
35
+ * Whether an eve version falls inside a declared peer range.
36
+ *
37
+ * Deliberately narrow: it understands the two forms these packages actually
38
+ * publish — `>=X <Y` and a caret — and returns false for anything it does not
39
+ * recognise. Guessing at a range it cannot parse is how a wrong build gets
40
+ * installed, and "I do not know" must not read as "yes".
41
+ */
42
+ export declare function rangeContains(range: string | undefined, eve: string): boolean;
43
+ /**
44
+ * The newest published build that supports this host's eve.
45
+ *
46
+ * Returns undefined when no published version declares support — which is a
47
+ * real answer and must be surfaced, not smoothed over by falling back to
48
+ * latest. `@kybernesis/voice` had NO eve 0.49 build for weeks while two agents
49
+ * on 0.49 ran its 0.51 one.
50
+ */
51
+ export declare function newestOnLine(candidates: readonly Candidate[], eve: string): string | undefined;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Picking the right build of a per-eve-line package.
3
+ *
4
+ * `@kybernesis/*` packages ship one build per eve line, and the version numbers
5
+ * are NOT ordered by line: `@kybernesis/voice` 0.1.2 is the eve 0.49 build and
6
+ * was published AFTER 0.1.1, which is the 0.51 build. So `npm view <pkg>
7
+ * version` — npm's `latest` — is routinely the wrong package for a given host,
8
+ * and installing it puts an agent on a build compiled against an eve API it is
9
+ * not running.
10
+ *
11
+ * That failure is silent. The package installs, the agent builds, the service
12
+ * starts, and one code path breaks later at runtime. It is how
13
+ * `@kybernesis/notify` took an agent down and how two agents ran a 0.51 build of
14
+ * `@kybernesis/manage` on eve 0.49 for days without anyone noticing.
15
+ *
16
+ * So an upgrade must never ask for "latest". It must ask for the newest version
17
+ * whose declared eve peer range contains the eve this host actually runs.
18
+ */
19
+ /**
20
+ * Compare semver-ish versions numerically.
21
+ *
22
+ * Numerically, because these are picked by sorting and string order puts 0.8.9
23
+ * above 0.8.10 — which would hand a host the wrong build. A prerelease sorts
24
+ * BELOW its release (0.1.2-rc < 0.1.2), so a release candidate is never chosen
25
+ * over the release it precedes.
26
+ */
27
+ export function versionCompare(a, b) {
28
+ const split = (v) => {
29
+ const dash = v.indexOf("-");
30
+ const core = (dash === -1 ? v : v.slice(0, dash)).split(".").map((p) => Number(p) || 0);
31
+ return { core, pre: dash !== -1 };
32
+ };
33
+ const pa = split(a);
34
+ const pb = split(b);
35
+ for (let i = 0; i < Math.max(pa.core.length, pb.core.length); i += 1) {
36
+ const x = pa.core[i] ?? 0;
37
+ const y = pb.core[i] ?? 0;
38
+ if (x !== y)
39
+ return x < y ? -1 : 1;
40
+ }
41
+ if (pa.pre === pb.pre)
42
+ return 0;
43
+ return pa.pre ? -1 : 1;
44
+ }
45
+ /**
46
+ * Whether an eve version falls inside a declared peer range.
47
+ *
48
+ * Deliberately narrow: it understands the two forms these packages actually
49
+ * publish — `>=X <Y` and a caret — and returns false for anything it does not
50
+ * recognise. Guessing at a range it cannot parse is how a wrong build gets
51
+ * installed, and "I do not know" must not read as "yes".
52
+ */
53
+ export function rangeContains(range, eve) {
54
+ if (!range)
55
+ return false;
56
+ const bounded = /^>=\s*([0-9][^\s]*)\s*<\s*([0-9][^\s]*)$/.exec(range.trim());
57
+ if (bounded) {
58
+ return versionCompare(eve, bounded[1]) >= 0 && versionCompare(eve, bounded[2]) < 0;
59
+ }
60
+ const caret = /^\^([0-9][^\s]*)$/.exec(range.trim());
61
+ if (caret) {
62
+ const base = caret[1].split(".");
63
+ const major = Number(base[0] ?? 0);
64
+ const minor = Number(base[1] ?? 0);
65
+ // ^0.x is minor-locked, as npm treats it.
66
+ const upper = major === 0 ? `0.${minor + 1}.0` : `${major + 1}.0.0`;
67
+ return versionCompare(eve, caret[1]) >= 0 && versionCompare(eve, upper) < 0;
68
+ }
69
+ const atLeast = /^>=\s*([0-9][^\s]*)$/.exec(range.trim());
70
+ if (atLeast)
71
+ return versionCompare(eve, atLeast[1]) >= 0;
72
+ return false;
73
+ }
74
+ /**
75
+ * The newest published build that supports this host's eve.
76
+ *
77
+ * Returns undefined when no published version declares support — which is a
78
+ * real answer and must be surfaced, not smoothed over by falling back to
79
+ * latest. `@kybernesis/voice` had NO eve 0.49 build for weeks while two agents
80
+ * on 0.49 ran its 0.51 one.
81
+ */
82
+ export function newestOnLine(candidates, eve) {
83
+ const supported = candidates.filter((c) => rangeContains(c.peer, eve));
84
+ if (supported.length === 0)
85
+ return undefined;
86
+ return supported.sort((a, b) => versionCompare(a.version, b.version)).at(-1)?.version;
87
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Services that are still running the code they loaded at start, after the
3
+ * package on disk has moved underneath them.
4
+ *
5
+ * Node reads a module once, at process start. Upgrading a package therefore
6
+ * changes nothing in a service that was already running — it keeps executing
7
+ * the old code from memory while the new version sits on disk, indefinitely.
8
+ *
9
+ * An agent host has more than one service. `deploy.sh` restarts the agent, and
10
+ * the Buzz bridge is a separate long-lived unit it does not touch. On the
11
+ * reference fleet both bridges ran for four days on `@kybernesis/buzz` 0.9.2
12
+ * while 0.9.9 was installed, so two fixes that had been signed off as deployed
13
+ * were live nowhere. Nothing was broken enough to notice; the bridges simply
14
+ * kept doing what the old code did.
15
+ *
16
+ * Checking the FILES is what hid it. The only honest question is whether the
17
+ * process started after the package changed.
18
+ */
19
+ export interface StaleSidecar {
20
+ service: string;
21
+ /** When the unit last started. */
22
+ startedAt: Date;
23
+ /** The newest package change it has not picked up. */
24
+ packageChangedAt: Date;
25
+ /** The package whose change is newest, for the message. */
26
+ packageName: string;
27
+ }
28
+ /** The newest mtime across installed @kybernesis packages, and which one it was. */
29
+ export declare function newestPackageChange(cwd: string): {
30
+ at: Date;
31
+ name: string;
32
+ } | null;
33
+ /**
34
+ * Compare each running unit's start time against the newest package change.
35
+ *
36
+ * `started` maps a unit name to when it started — supplied by the caller so
37
+ * this stays testable and does not shell out.
38
+ *
39
+ * A grace margin avoids flagging the restart that an install performs itself:
40
+ * a service restarted within a minute of the package landing has the new code.
41
+ */
42
+ export declare function staleSidecars(input: {
43
+ started: ReadonlyMap<string, Date>;
44
+ packageChange: {
45
+ at: Date;
46
+ name: string;
47
+ } | null;
48
+ graceMs?: number;
49
+ }): StaleSidecar[];
50
+ /** One line per stale service, naming the fix. */
51
+ export declare function describeStaleSidecar(stale: StaleSidecar): string;
@@ -0,0 +1,56 @@
1
+ import { readdirSync, statSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ /** The newest mtime across installed @kybernesis packages, and which one it was. */
4
+ export function newestPackageChange(cwd) {
5
+ const scope = join(cwd, "node_modules", "@kybernesis");
6
+ let newest = null;
7
+ let entries;
8
+ try {
9
+ entries = readdirSync(scope);
10
+ }
11
+ catch {
12
+ return null;
13
+ }
14
+ for (const name of entries) {
15
+ try {
16
+ // The manifest is rewritten on every install of that package, so its
17
+ // mtime is when the package last changed on this host.
18
+ const at = statSync(join(scope, name, "package.json")).mtime;
19
+ if (!newest || at > newest.at)
20
+ newest = { at, name: `@kybernesis/${name}` };
21
+ }
22
+ catch {
23
+ // A half-removed package tells us nothing; skip it.
24
+ }
25
+ }
26
+ return newest;
27
+ }
28
+ /**
29
+ * Compare each running unit's start time against the newest package change.
30
+ *
31
+ * `started` maps a unit name to when it started — supplied by the caller so
32
+ * this stays testable and does not shell out.
33
+ *
34
+ * A grace margin avoids flagging the restart that an install performs itself:
35
+ * a service restarted within a minute of the package landing has the new code.
36
+ */
37
+ export function staleSidecars(input) {
38
+ const change = input.packageChange;
39
+ if (!change)
40
+ return [];
41
+ const grace = input.graceMs ?? 60_000;
42
+ const out = [];
43
+ for (const [service, startedAt] of input.started) {
44
+ if (startedAt.getTime() >= change.at.getTime() - grace)
45
+ continue;
46
+ out.push({ service, startedAt, packageChangedAt: change.at, packageName: change.name });
47
+ }
48
+ return out;
49
+ }
50
+ /** One line per stale service, naming the fix. */
51
+ export function describeStaleSidecar(stale) {
52
+ const started = stale.startedAt.toISOString().slice(0, 16).replace("T", " ");
53
+ const changed = stale.packageChangedAt.toISOString().slice(0, 16).replace("T", " ");
54
+ return (`${stale.service} started ${started} but ${stale.packageName} changed ${changed} — ` +
55
+ `it is still running the code it loaded at start. Restart it: sudo systemctl restart ${stale.service}`);
56
+ }
package/dist/upgrade.js CHANGED
@@ -7,6 +7,7 @@ import { upsertEnv } from "./envfile.js";
7
7
  import { reconcileHostArtifact } from "./host-artifacts.js";
8
8
  import { assessPeerTree, staleRangeWarning } from "./peer-tree.js";
9
9
  import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
10
+ import { newestOnLine } from "./on-line-version.js";
10
11
  import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
11
12
  import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
12
13
  import { repairRemovedDefaultTools as repairRemovedDefaultTools_ } from "./removed-default-tools.js";
@@ -753,6 +754,44 @@ function installedPackageValue(cwd, name, expression) {
753
754
  function targetEvePeer(name, version) {
754
755
  return capture("npm", ["view", `${name}@${version}`, "peerDependencies.eve"])?.trim() || undefined;
755
756
  }
757
+ /**
758
+ * The newest published build of a @kybernesis package that supports a given eve.
759
+ *
760
+ * NOT `npm view <name> version`. These packages ship one build per eve line and
761
+ * the versions are not ordered by line — voice 0.1.2 is the eve 0.49 build and
762
+ * was published after 0.1.1, the 0.51 build — so npm's `latest` is routinely the
763
+ * wrong package. Installing it puts the agent on a build compiled against an eve
764
+ * API it is not running, which fails at neither install nor build, only later
765
+ * and only on one path. That is how an 0.51 build of @kybernesis/manage ran on
766
+ * two eve 0.49 agents for days, and how @kybernesis/notify took one down.
767
+ *
768
+ * Returns undefined when nothing published supports this eve — a real answer,
769
+ * and one the caller must report rather than paper over.
770
+ */
771
+ function newestBuildForEve(name, eve) {
772
+ const raw = capture("npm", ["view", name, "versions", "--json"]);
773
+ if (!raw)
774
+ return undefined;
775
+ let versions;
776
+ try {
777
+ const parsed = JSON.parse(raw);
778
+ versions = Array.isArray(parsed) ? parsed : [String(parsed)];
779
+ }
780
+ catch {
781
+ return undefined;
782
+ }
783
+ // Newest first, so the usual case stops after one lookup.
784
+ const ordered = [...versions].reverse();
785
+ const candidates = [];
786
+ for (const version of ordered) {
787
+ const peer = targetEvePeer(name, version);
788
+ candidates.push({ version, ...(peer ? { peer } : {}) });
789
+ const hit = newestOnLine(candidates, eve);
790
+ if (hit)
791
+ return hit;
792
+ }
793
+ return undefined;
794
+ }
756
795
  function dependencyEntries(pkg) {
757
796
  const entries = [];
758
797
  for (const section of ["dependencies", "devDependencies"]) {
@@ -879,9 +918,20 @@ export async function upgrade(options = {}) {
879
918
  continue;
880
919
  }
881
920
  installedVersions.set(name, installed);
882
- const target = name === "eve" ? EVE_VERSION : capture("npm", ["view", name, "version"])?.trim();
921
+ // eve goes to the certified pin. Everything else goes to the newest build
922
+ // that supports the eve this host will be running — NOT npm's `latest`,
923
+ // which for a per-eve-line package is routinely a build for a different
924
+ // line and installs without complaint.
925
+ const target = name === "eve"
926
+ ? EVE_VERSION
927
+ : name.startsWith("@kybernesis/")
928
+ ? newestBuildForEve(name, EVE_VERSION)
929
+ : capture("npm", ["view", name, "version"])?.trim();
883
930
  if (!target) {
884
- console.log(` ${yellow("!")} ${name}: could not resolve target version`);
931
+ console.log(` ${yellow("!")} ${name}: ` +
932
+ (name.startsWith("@kybernesis/")
933
+ ? `no published build declares support for eve ${EVE_VERSION} — leaving it alone`
934
+ : "could not resolve target version"));
885
935
  unresolved.add(name);
886
936
  continue;
887
937
  }
@@ -891,7 +941,7 @@ export async function upgrade(options = {}) {
891
941
  targetPeers.set(name, targetEvePeer(name, target));
892
942
  }
893
943
  if (installed === target) {
894
- console.log(` ${green("✓")} ${name}@${installed} ${dim(name === "eve" ? "(certified)" : "(latest)")}`);
944
+ console.log(` ${green("✓")} ${name}@${installed} ${dim(name === "eve" ? "(certified)" : name.startsWith("@kybernesis/") ? `(newest for eve ${EVE_VERSION})` : "(latest)")}`);
895
945
  }
896
946
  else if (name === "eve" && versionLt(EVE_VERSION, installed)) {
897
947
  console.log(` ${yellow("!")} eve@${installed} is AHEAD of the certified ${EVE_VERSION} ${dim("— unsupported territory")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.15.6",
3
+ "version": "0.15.8",
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",
@@ -46,5 +46,6 @@
46
46
  },
47
47
  "publishConfig": {
48
48
  "access": "public"
49
- }
49
+ },
50
+ "peerDependencies": {}
50
51
  }