@kybernesis/create 0.14.10 → 0.15.0

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/init.js CHANGED
@@ -142,13 +142,14 @@ export async function init(rawName, options = {}) {
142
142
  run("npx", ["eve", "add", item, "--overwrite"], { cwd: dir, allowFail: true });
143
143
  }
144
144
  if (studio) {
145
- // Two separate items on purpose: `local` lets the agent act on the USER'S
145
+ // Three separate items on purpose: `local` lets the agent act on the USER'S
146
146
  // machine (consent per effect, granted on the desktop); `manage` lets a
147
- // client change THIS AGENT — its dependencies and its source. Different
148
- // blast radius, so an agent can have one without the other.
149
- console.log(bold("\n2b2 KYBER Studio: local execution + management routes …"));
147
+ // client change THIS AGENT — its dependencies and its source; `notify`
148
+ // lets it reach the person's phone. Different blast radius, so an agent
149
+ // can have one without the others.
150
+ console.log(bold("\n2b2 KYBER Studio: local execution + management routes + phone notifications …"));
150
151
  const studioFailures = [];
151
- for (const item of ["local", "manage"]) {
152
+ for (const item of ["local", "manage", "notify"]) {
152
153
  // The @kybernesis/ prefix is load-bearing: a bare name resolves against
153
154
  // eve OWN registry, which has no such item, so both installs failed with
154
155
  // "not found" — and allowFail swallowed it. --studio therefore did
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Who, exactly, is unhappy about the installed eve.
3
+ *
4
+ * @remarks
5
+ * `kyb upgrade` refuses to touch a project whose peer tree is broken (KYB-538),
6
+ * because changing a project on a bad tree is how a half-upgraded host happens.
7
+ * That gate read `npm ls eve`'s exit code and nothing else, so any package
8
+ * anywhere in the tree could veto the whole upgrade.
9
+ *
10
+ * On 2026-09-08 one did. `@github-tools/eve-extension` still declares
11
+ * `>=0.44.0 <0.48.0` while every Kybernesis package declares the 0.49 line the
12
+ * host actually runs. The upgrade aborted before `repairHostArtifacts`, so
13
+ * kyber did not receive the KYB-542 reclaim job it was being upgraded for, and
14
+ * the job had to be installed by hand.
15
+ *
16
+ * A third-party range that trails the framework is not a broken install. A
17
+ * Kybernesis package disagreeing with the framework it is pinned against is.
18
+ * So the verdict distinguishes them, and anything it cannot attribute is
19
+ * treated as fatal rather than waved through.
20
+ */
21
+ export interface PeerTreeVerdict {
22
+ /** Nothing in the tree objects to the installed version. */
23
+ readonly ok: boolean;
24
+ /** Packages whose declared range excludes the installed version, deduplicated and sorted. */
25
+ readonly offenders: readonly string[];
26
+ /** An objection from a Kybernesis package, or one that could not be attributed. */
27
+ readonly fatal: boolean;
28
+ }
29
+ /**
30
+ * Read `npm ls <pkg> --json` and say whether the upgrade may proceed.
31
+ *
32
+ * Unparseable output is fatal: not knowing the shape of the tree is exactly the
33
+ * case the gate exists for.
34
+ */
35
+ export declare function assessPeerTree(stdout: string): PeerTreeVerdict;
36
+ /** What to print when a third-party range trails the framework but nothing else does. */
37
+ export declare function staleRangeWarning(offenders: readonly string[], dependency: string): string;
@@ -0,0 +1,62 @@
1
+ const OWNED_SCOPE = "@kybernesis/";
2
+ /** Every `from node_modules/<pkg>` in one npm `invalid` string. */
3
+ function requiredBy(invalid) {
4
+ const found = [];
5
+ const pattern = /from node_modules\/((?:@[^/\s,]+\/)?[^/\s,]+)/g;
6
+ let match = pattern.exec(invalid);
7
+ while (match !== null) {
8
+ if (match[1])
9
+ found.push(match[1]);
10
+ match = pattern.exec(invalid);
11
+ }
12
+ return found;
13
+ }
14
+ function collect(node, offenders, unattributed) {
15
+ if (!node || typeof node !== "object")
16
+ return;
17
+ const record = node;
18
+ if (typeof record.invalid === "string" && record.invalid.length > 0) {
19
+ const owners = requiredBy(record.invalid);
20
+ // An objection nobody signed is not evidence that the tree is fine.
21
+ if (owners.length === 0)
22
+ unattributed.seen = true;
23
+ for (const owner of owners)
24
+ offenders.add(owner);
25
+ }
26
+ const dependencies = record.dependencies;
27
+ if (dependencies && typeof dependencies === "object") {
28
+ for (const child of Object.values(dependencies)) {
29
+ collect(child, offenders, unattributed);
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * Read `npm ls <pkg> --json` and say whether the upgrade may proceed.
35
+ *
36
+ * Unparseable output is fatal: not knowing the shape of the tree is exactly the
37
+ * case the gate exists for.
38
+ */
39
+ export function assessPeerTree(stdout) {
40
+ let parsed;
41
+ try {
42
+ parsed = JSON.parse(stdout);
43
+ }
44
+ catch {
45
+ return { ok: false, offenders: [], fatal: true };
46
+ }
47
+ const offenders = new Set();
48
+ const unattributed = { seen: false };
49
+ collect(parsed, offenders, unattributed);
50
+ if (offenders.size === 0 && !unattributed.seen)
51
+ return { ok: true, offenders: [], fatal: false };
52
+ const sorted = [...offenders].sort();
53
+ const ours = sorted.some((name) => name.startsWith(OWNED_SCOPE));
54
+ return { ok: false, offenders: sorted, fatal: ours || unattributed.seen };
55
+ }
56
+ /** What to print when a third-party range trails the framework but nothing else does. */
57
+ export function staleRangeWarning(offenders, dependency) {
58
+ const names = offenders.join(", ");
59
+ return (`${names} declare${offenders.length === 1 ? "s" : ""} a ${dependency} range that excludes the installed version. ` +
60
+ `Every Kybernesis package agrees with it, and the runtime already runs on it, so the upgrade continues. ` +
61
+ `Upgrade or replace ${offenders.length === 1 ? "that package" : "those packages"} when a compatible release exists.`);
62
+ }
@@ -75,3 +75,8 @@ export declare function exeEvalConfigTs(): string;
75
75
  export declare function arcanaMemorySlotTs(options: {
76
76
  extensionMounted: boolean;
77
77
  }): string;
78
+ /**
79
+ * The notify mount. One source for `kyb init --studio`, `kyb upgrade` and the
80
+ * registry item, so the three can never disagree about what the file says.
81
+ */
82
+ export declare function notifyMountTs(): string;
package/dist/templates.js CHANGED
@@ -722,3 +722,14 @@ export default defineMemory({
722
722
  });
723
723
  `;
724
724
  }
725
+ /**
726
+ * The notify mount. One source for `kyb init --studio`, `kyb upgrade` and the
727
+ * registry item, so the three can never disagree about what the file says.
728
+ */
729
+ export function notifyMountTs() {
730
+ return `// Tell the person's phone when this agent parks on a question or finishes a
731
+ // reply while they are away. The person is the verified session principal;
732
+ // the control plane does the ringing. See @kybernesis/notify.
733
+ export { default } from "@kybernesis/notify/extension";
734
+ `;
735
+ }
package/dist/upgrade.d.ts CHANGED
@@ -1,5 +1,20 @@
1
1
  import { type AgentInputLimit } from "./agent-limits.js";
2
2
  export declare function agentInputLimitUpgradeMessage(limit: AgentInputLimit): string;
3
+ /**
4
+ * A Studio agent that cannot ring the person's phone.
5
+ *
6
+ * `@kybernesis/notify` arrived after most agents were made, and an upgrade
7
+ * only ever moved packages an agent already had — so every existing agent
8
+ * would have stayed silent on the phone, and the first sign would have been
9
+ * a person waiting for a notification that never came. The agents that need
10
+ * it are the ones a desktop can manage: anything with `@kybernesis/manage`.
11
+ *
12
+ * The pure part — is it needed, is the mount there — is separate from the
13
+ * npm call so it can be tested without a network.
14
+ */
15
+ export declare function needsNotify(deps: Record<string, string>): boolean;
16
+ /** Write the notify mount if it is missing. Never touches a file that exists. */
17
+ export declare function repairNotifyMount(cwd: string): "mounted" | "present";
3
18
  export type GithubToolsMountRepair = {
4
19
  kind: "missing" | "current" | "updated" | "customized";
5
20
  } | {
package/dist/upgrade.js CHANGED
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { pathToFileURL } from "node:url";
6
6
  import { upsertEnv } from "./envfile.js";
7
7
  import { reconcileHostArtifact } from "./host-artifacts.js";
8
+ import { assessPeerTree, staleRangeWarning } from "./peer-tree.js";
8
9
  import { dockerPruneCronArtifact } from "./docker-prune-cron.js";
9
10
  import { findMatchingAgentServiceUnit, repairManageRestart } from "./systemd.js";
10
11
  import { repairTerminalSandboxCleanupHooks } from "./sandbox-cleanup.js";
@@ -14,7 +15,7 @@ import { inspectEveAgent } from "./agent-limits.js";
14
15
  import { remoteProjectPath, sshTarget } from "./deploy.js";
15
16
  import { classifyModelReach } from "./model-reach.js";
16
17
  import { checkSelfVersion, versionLt } from "./self-version.js";
17
- import { LEGACY_GITHUB_TOOLS_MOUNT, githubToolsMountTs } from "./templates.js";
18
+ import { LEGACY_GITHUB_TOOLS_MOUNT, githubToolsMountTs, notifyMountTs } from "./templates.js";
18
19
  import { confirmEveUpgrade, inspectBuzzSessions, inspectDurableRuns, reconcileEvalScript, } from "./upgrade-sessions.js";
19
20
  export function agentInputLimitUpgradeMessage(limit) {
20
21
  switch (limit.kind) {
@@ -164,6 +165,51 @@ function repairBuzzSetup(cwd, deps) {
164
165
  console.log(` ${dim("Takes effect after the next build and restart.")}\n`);
165
166
  }
166
167
  }
168
+ /**
169
+ * A Studio agent that cannot ring the person's phone.
170
+ *
171
+ * `@kybernesis/notify` arrived after most agents were made, and an upgrade
172
+ * only ever moved packages an agent already had — so every existing agent
173
+ * would have stayed silent on the phone, and the first sign would have been
174
+ * a person waiting for a notification that never came. The agents that need
175
+ * it are the ones a desktop can manage: anything with `@kybernesis/manage`.
176
+ *
177
+ * The pure part — is it needed, is the mount there — is separate from the
178
+ * npm call so it can be tested without a network.
179
+ */
180
+ export function needsNotify(deps) {
181
+ return Boolean(deps["@kybernesis/manage"]) && !deps["@kybernesis/notify"];
182
+ }
183
+ /** Write the notify mount if it is missing. Never touches a file that exists. */
184
+ export function repairNotifyMount(cwd) {
185
+ const mount = join(cwd, "agent/extensions/notify.ts");
186
+ if (existsSync(mount))
187
+ return "present";
188
+ mkdirSync(join(cwd, "agent/extensions"), { recursive: true });
189
+ writeFileSync(mount, notifyMountTs());
190
+ return "mounted";
191
+ }
192
+ function repairNotifySetup(cwd, deps) {
193
+ if (!deps["@kybernesis/manage"])
194
+ return;
195
+ const done = [];
196
+ if (needsNotify(deps)) {
197
+ const ok = run("npm", ["install", "@kybernesis/notify@latest", "--no-audit", "--no-fund"], { cwd, allowFail: true, quiet: true });
198
+ if (!ok) {
199
+ console.log(` ${yellow("!")} could not install @kybernesis/notify, so this agent cannot reach the person's phone.\n` +
200
+ ` Run: npm install @kybernesis/notify@latest && npx kyb upgrade`);
201
+ return;
202
+ }
203
+ deps["@kybernesis/notify"] = "latest";
204
+ done.push("installed @kybernesis/notify");
205
+ }
206
+ if (repairNotifyMount(cwd) === "mounted")
207
+ done.push("mounted it (agent/extensions/notify.ts)");
208
+ if (done.length > 0) {
209
+ console.log(` ${green("+")} Notify: ${done.join("; ")} ${dim("— the person's phone hears when this agent needs them")}`);
210
+ console.log(` ${dim("Takes effect after the next build and restart.")}\n`);
211
+ }
212
+ }
167
213
  /** Quote one argument for the manual shell commands printed after a failed repair. */
168
214
  function shellQuote(value) {
169
215
  return `'${value.replaceAll("'", `'"'"'`)}'`;
@@ -775,6 +821,7 @@ export async function upgrade(options = {}) {
775
821
  reportGithubToolsMountRepair(cwd);
776
822
  repairHostArtifacts(cwd, deps);
777
823
  repairEvalCommand(cwd);
824
+ repairNotifySetup(cwd, deps);
778
825
  repairManageRestart(cwd, deps);
779
826
  return;
780
827
  }
@@ -821,9 +868,22 @@ export async function upgrade(options = {}) {
821
868
  }
822
869
  const validation = commandResult("npm", ["ls", "eve"], cwd);
823
870
  if (validation.status !== 0) {
824
- fail("npm ls eve failed. The installed peer tree is not valid.");
825
- recoveryGuidance(targets, stoppedBridge ?? undefined);
826
- return;
871
+ /**
872
+ * Judge WHO objects, not merely that something did.
873
+ *
874
+ * A third-party extension whose declared range trails the framework is not
875
+ * a broken install, and letting it veto the upgrade left kyber without the
876
+ * host artifacts it was being upgraded for (KYB-548). A Kybernesis package
877
+ * disagreeing with the framework it is pinned against still stops
878
+ * everything, and so does an objection we cannot attribute.
879
+ */
880
+ const verdict = assessPeerTree(commandResult("npm", ["ls", "eve", "--json"], cwd, false).stdout);
881
+ if (verdict.fatal || verdict.ok) {
882
+ fail("npm ls eve failed. The installed peer tree is not valid.");
883
+ recoveryGuidance(targets, stoppedBridge ?? undefined);
884
+ return;
885
+ }
886
+ console.log(yellow(` ! ${staleRangeWarning(verdict.offenders, "eve")}`));
827
887
  }
828
888
  if (stoppedBridge && !startStoppedBridge(cwd, stoppedBridge))
829
889
  return;
@@ -833,6 +893,7 @@ export async function upgrade(options = {}) {
833
893
  reportGithubToolsMountRepair(cwd);
834
894
  repairHostArtifacts(cwd, deps);
835
895
  repairEvalCommand(cwd);
896
+ repairNotifySetup(cwd, deps);
836
897
  repairManageRestart(cwd, deps);
837
898
  run("npm", ["run", "typecheck"], { cwd });
838
899
  if (eveChanged) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.10",
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.15.0",
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",