@kybernesis/create 0.7.0 → 0.7.2

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/cli.js CHANGED
@@ -6,6 +6,8 @@
6
6
  * self-testing eve agent (also: npm create @kybernesis)
7
7
  * kyb doctor preflight an agent project: keys, issuer, envs, discovery
8
8
  * kyb skills [--global] install/refresh the FDE Claude Code skill suite
9
+ * kyb register register this agent with the control plane (device flow)
10
+ * kyb deploy put this repo on its host and restart it, with proof
9
11
  * kyb upgrade bump @kybernesis/* to latest, gated on the eval suite
10
12
  * --skip-eval skip the eval gate (not for production changes)
11
13
  */
@@ -14,6 +16,8 @@ import { init } from "./init.js";
14
16
  import { doctor } from "./doctor.js";
15
17
  import { upgrade } from "./upgrade.js";
16
18
  import { installSkills } from "./skills.js";
19
+ import { deploy } from "./deploy.js";
20
+ import { register } from "./register.js";
17
21
  function flag(rest, key) {
18
22
  const hit = rest.find((a) => a.startsWith(`--${key}=`));
19
23
  return hit ? hit.slice(key.length + 3) : undefined;
@@ -40,6 +44,12 @@ switch (command) {
40
44
  case "skills":
41
45
  installSkills({ global: rest.includes("--global") });
42
46
  break;
47
+ case "register":
48
+ await register({ name: flag(rest, "name"), url: flag(rest, "url") });
49
+ break;
50
+ case "deploy":
51
+ await deploy({ host: flag(rest, "host") });
52
+ break;
43
53
  case "upgrade":
44
54
  await upgrade(rest.includes("--skip-eval"));
45
55
  break;
@@ -65,6 +75,11 @@ ${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
65
75
  ${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
66
76
  ${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
67
77
  --global ${dim("install to ~/.claude/skills instead of this repo")}
78
+ ${bold("kyb register")} register this agent with the control plane
79
+ --name=<name> ${dim("defaults to KYBERNESIS_AGENT in .env.local")}
80
+ --url=<url> ${dim("defaults to https://$EXE_VM_NAME.exe.xyz")}
81
+ ${bold("kyb deploy")} copy to the host, install, restart, prove it took
82
+ --host=<target> ${dim("ssh target; defaults to $EXE_VM_NAME.exe.xyz")}
68
83
  ${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
69
84
  --skip-eval ${dim("skip the eval gate")}
70
85
 
@@ -0,0 +1,4 @@
1
+ export declare function deploy(options: {
2
+ host?: string;
3
+ dir?: string;
4
+ }): Promise<void>;
package/dist/deploy.js ADDED
@@ -0,0 +1,169 @@
1
+ import { execFileSync, spawnSync } from "node:child_process";
2
+ import { existsSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { bold, dim, green, red, yellow } from "./util.js";
5
+ /**
6
+ * `kyb deploy` — put this repo on its host and restart it, with proof.
7
+ *
8
+ * On Vercel this is `eve deploy` and always was. Off Vercel it was a paragraph
9
+ * in a skill telling people to rsync and then run a script, which is the sort
10
+ * of step that gets done differently by each person doing it — and the
11
+ * differences are exactly where the outages came from.
12
+ */
13
+ function env(dir) {
14
+ const out = {};
15
+ for (const file of [".env.local", ".env"]) {
16
+ const p = join(dir, file);
17
+ if (!existsSync(p))
18
+ continue;
19
+ for (const line of readFileSync(p, "utf8").split("\n")) {
20
+ const m = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line);
21
+ if (m?.[1] && out[m[1]] === undefined)
22
+ out[m[1]] = (m[2] ?? "").trim().replace(/^["']|["']$/g, "");
23
+ }
24
+ }
25
+ return out;
26
+ }
27
+ function hostOf(dir) {
28
+ const pkg = join(dir, "package.json");
29
+ const deps = existsSync(pkg)
30
+ ? (JSON.parse(readFileSync(pkg, "utf8")).dependencies ?? {})
31
+ : {};
32
+ return deps["@kybernesis/exe"] ? "exe" : "vercel";
33
+ }
34
+ /** The ssh target: an explicit flag, then EVE_SSH_HOST, then the exe VM name. */
35
+ function sshTarget(dir, explicit) {
36
+ const e = env(dir);
37
+ if (explicit)
38
+ return explicit;
39
+ if (e.EVE_SSH_HOST)
40
+ return e.EVE_SSH_HOST;
41
+ if (e.EXE_VM_NAME)
42
+ return `${e.EXE_VM_NAME}.exe.xyz`;
43
+ return null;
44
+ }
45
+ export async function deploy(options) {
46
+ const dir = options.dir ?? process.cwd();
47
+ if (!existsSync(join(dir, "agent"))) {
48
+ console.log(red("Not an eve agent project (no agent/ directory)."));
49
+ process.exitCode = 1;
50
+ return;
51
+ }
52
+ console.log(bold("kyb deploy"));
53
+ if (hostOf(dir) === "vercel") {
54
+ console.log(dim(" host: vercel — handing over to eve deploy\n"));
55
+ const r = spawnSync("npx", ["eve", "deploy"], { cwd: dir, stdio: "inherit" });
56
+ process.exitCode = r.status ?? 0;
57
+ return;
58
+ }
59
+ const target = sshTarget(dir, options.host);
60
+ if (!target) {
61
+ console.log(red(" No host to deploy to."));
62
+ console.log(dim(" Set EXE_VM_NAME (or EVE_SSH_HOST) in .env.local, or pass --host=<ssh target>."));
63
+ process.exitCode = 1;
64
+ return;
65
+ }
66
+ const name = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")).name ?? "agent";
67
+ const remote = `~/${name}`;
68
+ console.log(dim(` host: ${target} path: ${remote}\n`));
69
+ const run = (cmd, args) => {
70
+ const r = spawnSync(cmd, args, { cwd: dir, stdio: "inherit" });
71
+ return (r.status ?? 1) === 0;
72
+ };
73
+ /**
74
+ * Never copy node_modules, .eve, or a build.
75
+ *
76
+ * node_modules is platform-specific and native modules built on a laptop do
77
+ * not run on the host. `.eve` is the durable store — conversations, turn
78
+ * history, the workflow queue — and overwriting it with a local copy is how
79
+ * a deployment eats its own production state.
80
+ */
81
+ console.log(bold("1/3 Copying source …"));
82
+ const ok = run("rsync", [
83
+ "-az",
84
+ "--delete",
85
+ "--exclude",
86
+ "node_modules",
87
+ "--exclude",
88
+ ".eve",
89
+ "--exclude",
90
+ ".output",
91
+ "--exclude",
92
+ ".git",
93
+ "--exclude",
94
+ ".env.local",
95
+ `${dir}/`,
96
+ `${target}:${remote}/`,
97
+ ]);
98
+ if (!ok) {
99
+ console.log(red(" rsync failed — is the host reachable, and is the path writable?"));
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ console.log(bold("\n2/3 Installing dependencies on the host …"));
104
+ if (!run("ssh", [target, `cd ${remote} && npm install --no-audit --no-fund`])) {
105
+ console.log(red(" npm install failed on the host."));
106
+ process.exitCode = 1;
107
+ return;
108
+ }
109
+ /**
110
+ * Detached, always.
111
+ *
112
+ * `ssh host "script"` sends SIGHUP when the connection ends, which kills the
113
+ * restart halfway — leaving exactly the half-restarted state the script
114
+ * exists to prevent. setsid + nohup + no stdin is the difference between a
115
+ * deploy and an outage.
116
+ */
117
+ console.log(bold("\n3/3 Restarting (detached) …"));
118
+ /**
119
+ * Find the restart script rather than assuming its name.
120
+ *
121
+ * `kyb init` installs scripts/eve-server.sh, but agents predating that have
122
+ * their own — and a deploy that fails on a naming difference is a deploy
123
+ * people stop using. Falls back to installing the packaged script, so a
124
+ * project that has none ends up with the hardened one rather than an error.
125
+ */
126
+ const remoteScript = [
127
+ 'SCRIPT=""',
128
+ // A loop, not a chain of `[ -f x ] && echo x || …` — that keeps evaluating
129
+ // after the first hit and yields every match, so SCRIPT becomes three
130
+ // filenames and `bash "$SCRIPT"` fails on a name nothing has.
131
+ "for f in scripts/eve-server.sh scripts/restart.sh eve-server.sh restart.sh; do",
132
+ ' if [ -f "$f" ]; then SCRIPT="$f"; break; fi',
133
+ "done",
134
+ 'if [ -z "$SCRIPT" ] && [ -f node_modules/@kybernesis/exe/scripts/eve-server.sh ]; then',
135
+ " mkdir -p scripts",
136
+ " cp node_modules/@kybernesis/exe/scripts/eve-server.sh scripts/",
137
+ " chmod +x scripts/eve-server.sh",
138
+ ' SCRIPT="scripts/eve-server.sh"',
139
+ "fi",
140
+ 'if [ -z "$SCRIPT" ]; then echo "FAILED: no restart script on the host"; exit 1; fi',
141
+ 'echo "using $SCRIPT"',
142
+ 'setsid nohup bash "$SCRIPT" > /tmp/kyb-deploy.log 2>&1 < /dev/null &',
143
+ "sleep 2",
144
+ "echo started",
145
+ ].join("\n");
146
+ run("ssh", [target, `cd ${remote}\n${remoteScript}`]);
147
+ console.log(dim("\n Waiting for the restart to report …"));
148
+ let last = "";
149
+ for (let i = 0; i < 40; i++) {
150
+ const out = execFileSync("ssh", [target, `tail -6 /tmp/kyb-deploy.log 2>/dev/null || true`], {
151
+ encoding: "utf8",
152
+ });
153
+ last = out;
154
+ if (/health:|FAILED/.test(out))
155
+ break;
156
+ await new Promise((r) => setTimeout(r, 5000));
157
+ }
158
+ const healthy = /health:\s*200/.test(last);
159
+ console.log(last
160
+ .split("\n")
161
+ .filter((l) => /pid=|build:|OK:|health:|FAILED|SOURCE IS NEWER|^built/.test(l))
162
+ .map((l) => ` ${l}`)
163
+ .join("\n"));
164
+ console.log(healthy
165
+ ? green("\n ✓ deployed and serving the current build")
166
+ : yellow("\n ! the restart did not report health — check /tmp/kyb-deploy.log on the host"));
167
+ if (!healthy)
168
+ process.exitCode = 1;
169
+ }
package/dist/doctor.js CHANGED
@@ -247,7 +247,11 @@ export async function doctor() {
247
247
  // deployment is incomplete. This is NOT a value to go and set by hand: the
248
248
  // switch in Studio installs it, and a missing one means nobody has turned
249
249
  // local access on yet.
250
- if (process.env.KYBERNESIS_AGENT_CREDENTIAL) {
250
+ // `env`, not `process.env`: every other check reads the merged view, and
251
+ // reading the bare environment here reported a missing credential on an
252
+ // agent whose .env.local had one two lines above. A preflight tool that
253
+ // cries wolf is a preflight tool people learn to skip.
254
+ if (env.KYBERNESIS_AGENT_CREDENTIAL) {
251
255
  add("pass", "local execution can identify this agent to the control plane");
252
256
  }
253
257
  else {
@@ -257,7 +261,7 @@ export async function doctor() {
257
261
  if (hasManage) {
258
262
  // manage authorizes with the caller's control-plane grant, so it needs to
259
263
  // know which agent it IS before it can check one.
260
- if (process.env.KYBERNESIS_AGENT) {
264
+ if (env.KYBERNESIS_AGENT) {
261
265
  add("pass", "management routes can resolve this agent's grants");
262
266
  }
263
267
  else {
package/dist/init.js CHANGED
@@ -1,4 +1,4 @@
1
- import { cpSync, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
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, yellow, } from "./util.js";
4
4
  import { CHANNEL_KINDS, channelPlan, engineerPlan, envExample, evalFileTs, evalScript, hostAgentTs, hostSteps, identityMd, rootArcanaTs, subagentAgentTs, subagentArcanaTs, subagentInstructionsMd, } from "./templates.js";
@@ -111,6 +111,31 @@ export async function init(rawName, options = {}) {
111
111
  writeFileSync(join(dir, "agent/agent.ts"), hostAgentTs(host, DEFAULT_MODEL));
112
112
  writeFileSync(join(dir, "agent/extensions/arcana.ts"), rootArcanaTs());
113
113
  writeFileSync(join(dir, "evals/kybernesis.eval.ts"), evalFileTs(displayName, depts));
114
+ /**
115
+ * A self-hosted agent gets its restart script installed, not described.
116
+ *
117
+ * There is no deploy pipeline off Vercel, so restarting IS the release — and
118
+ * the script that does it carries every lesson that path has cost: serialize
119
+ * concurrent restarts, build when the source moved, wait for in-flight turns,
120
+ * and count servers by what they are rather than by who mentions them.
121
+ *
122
+ * It used to ship inside @kybernesis/exe with a line in the docs telling
123
+ * people where to find it, which means a new deployment starts with none of
124
+ * that and rediscovers it one outage at a time.
125
+ */
126
+ if (host === "exe") {
127
+ const source = join(dir, "node_modules/@kybernesis/exe/scripts/eve-server.sh");
128
+ const target = join(dir, "scripts/eve-server.sh");
129
+ try {
130
+ mkdirSync(join(dir, "scripts"), { recursive: true });
131
+ copyFileSync(source, target);
132
+ chmodSync(target, 0o755);
133
+ console.log(dim(" scripts/eve-server.sh — restart with proof (serialized, builds if stale)"));
134
+ }
135
+ catch {
136
+ console.log(yellow(" ! could not install scripts/eve-server.sh — copy it from node_modules/@kybernesis/exe/scripts/"));
137
+ }
138
+ }
114
139
  if (plan.file) {
115
140
  console.log(bold(`\n4/6 Channel: ${channel} …`));
116
141
  mkdirSync(join(dir, "agent/channels"), { recursive: true });
@@ -0,0 +1,5 @@
1
+ export declare function register(options: {
2
+ name?: string;
3
+ url?: string;
4
+ dir?: string;
5
+ }): Promise<void>;
@@ -0,0 +1,110 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { bold, dim, green, red, yellow } from "./util.js";
4
+ function envOf(dir) {
5
+ const out = {};
6
+ for (const file of [".env.local", ".env"]) {
7
+ const p = join(dir, file);
8
+ if (!existsSync(p))
9
+ continue;
10
+ for (const line of readFileSync(p, "utf8").split("\n")) {
11
+ const m = /^\s*([A-Z0-9_]+)\s*=\s*(.*)$/.exec(line);
12
+ if (m?.[1] && out[m[1]] === undefined)
13
+ out[m[1]] = (m[2] ?? "").trim().replace(/^["']|["']$/g, "");
14
+ }
15
+ }
16
+ return out;
17
+ }
18
+ /** Sign in with the device flow and return an identity token. */
19
+ async function signIn(issuer) {
20
+ const started = await fetch(`${issuer}/api/oauth/device`, {
21
+ method: "POST",
22
+ headers: { "content-type": "application/json" },
23
+ body: JSON.stringify({ deviceId: `kyb-cli-${process.pid}`, deviceLabel: "kyb CLI" }),
24
+ }).catch(() => null);
25
+ if (!started?.ok) {
26
+ console.log(red(` Could not start sign-in at ${issuer}.`));
27
+ return null;
28
+ }
29
+ const body = (await started.json());
30
+ const url = String(body.verification_uri_complete ?? body.verification_uri ?? "");
31
+ const code = String(body.user_code ?? "");
32
+ const deviceCode = String(body.device_code ?? "");
33
+ let interval = Number(body.interval ?? 5) * 1000;
34
+ const deadline = Date.now() + Number(body.expires_in ?? 600) * 1000;
35
+ console.log(`\n Approve this in your browser:\n`);
36
+ console.log(` ${bold(url)}`);
37
+ if (code)
38
+ console.log(` code: ${bold(code)}\n`);
39
+ while (Date.now() < deadline) {
40
+ await new Promise((r) => setTimeout(r, interval));
41
+ const res = await fetch(`${issuer}/api/oauth/token`, {
42
+ method: "POST",
43
+ headers: { "content-type": "application/json" },
44
+ body: JSON.stringify({ device_code: deviceCode }),
45
+ }).catch(() => null);
46
+ if (!res)
47
+ continue;
48
+ const out = (await res.json().catch(() => ({})));
49
+ if (res.ok && typeof out.token === "string")
50
+ return out.token;
51
+ if (out.error === "slow_down")
52
+ interval += 5000;
53
+ // authorization_pending is the normal case; anything else is fatal.
54
+ if (out.error && out.error !== "authorization_pending" && out.error !== "slow_down") {
55
+ console.log(red(` Sign-in failed: ${String(out.error)}`));
56
+ return null;
57
+ }
58
+ }
59
+ console.log(red(" Sign-in timed out."));
60
+ return null;
61
+ }
62
+ export async function register(options) {
63
+ const dir = options.dir ?? process.cwd();
64
+ const env = envOf(dir);
65
+ const issuer = (env.KYBERNESIS_ISSUER || "https://agent.kybernesis.ai").replace(/\/$/, "");
66
+ // The registered name must equal KYBERNESIS_AGENT exactly — it is what the
67
+ // agent checks grants against, and a mismatch is a 403 with no clue in it.
68
+ const name = options.name ?? env.KYBERNESIS_AGENT;
69
+ const url = options.url ??
70
+ env.EVE_PUBLIC_URL ??
71
+ (env.EXE_VM_NAME ? `https://${env.EXE_VM_NAME}.exe.xyz` : undefined);
72
+ console.log(bold("kyb register"));
73
+ console.log(dim(` issuer: ${issuer}`));
74
+ if (!name) {
75
+ console.log(red(" No agent name. Set KYBERNESIS_AGENT in .env.local, or pass --name=<name>."));
76
+ process.exitCode = 1;
77
+ return;
78
+ }
79
+ if (!url) {
80
+ console.log(red(" No deployment URL. Set EXE_VM_NAME in .env.local, or pass --url=<https://…>."));
81
+ process.exitCode = 1;
82
+ return;
83
+ }
84
+ console.log(dim(` agent: ${name}\n url: ${url}`));
85
+ const token = await signIn(issuer);
86
+ if (!token) {
87
+ process.exitCode = 1;
88
+ return;
89
+ }
90
+ const res = await fetch(`${issuer}/api/agents/register`, {
91
+ method: "POST",
92
+ headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
93
+ body: JSON.stringify({ name, url }),
94
+ }).catch(() => null);
95
+ if (!res?.ok) {
96
+ const detail = res ? (await res.json().catch(() => ({}))).error : "unreachable";
97
+ console.log(red(`\n Registration failed: ${detail ?? res?.status}`));
98
+ process.exitCode = 1;
99
+ return;
100
+ }
101
+ const out = (await res.json());
102
+ console.log(green(out.created
103
+ ? `\n ✓ registered "${name}" and granted you access`
104
+ : `\n ✓ "${name}" already existed — its URL now points at ${url}`));
105
+ console.log(dim(" Grant others in the admin; they do not inherit yours."));
106
+ if (!env.KYBERNESIS_AGENT_CREDENTIAL) {
107
+ console.log(yellow("\n ! This agent has no credential yet. Turn on 'Work on this computer' in\n" +
108
+ " KYBER Studio to mint and install one — do not paste a credential by hand."));
109
+ }
110
+ }
package/dist/skills.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { cpSync, existsSync, mkdirSync, readdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
- import { dirname, join } from "node:path";
3
+ import { dirname, join, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { bold, dim, green } from "./util.js";
6
6
  /** The skill suite shipped inside this package (skills/ beside dist/). */
@@ -26,6 +26,18 @@ export function installSkills(opts = {}) {
26
26
  const target = opts.global
27
27
  ? join(homedir(), ".claude", "skills")
28
28
  : join(process.cwd(), ".claude", "skills");
29
+ /**
30
+ * Never install the suite into itself.
31
+ *
32
+ * Run from inside the package's own skills/ directory, this copies the suite
33
+ * to skills/.claude/skills — which then ships inside the published tarball,
34
+ * so every consumer installs a duplicate suite nested one level down. That
35
+ * happened, got committed, and was one `npm publish` from being everyone's.
36
+ */
37
+ if (resolve(target).startsWith(resolve(src))) {
38
+ console.error("Refusing to install the suite into itself — run kyb skills from an agent repo, not from the package.");
39
+ process.exit(2);
40
+ }
29
41
  mkdirSync(target, { recursive: true });
30
42
  const names = readdirSync(src).filter((n) => !n.startsWith("."));
31
43
  for (const name of names) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",
@@ -1804,6 +1804,17 @@ keys of your own.
1804
1804
 
1805
1805
  ### 11.1 Scaffold
1806
1806
 
1807
+ The whole path is commands now — there is no step where an FDE is expected to
1808
+ improvise a deploy:
1809
+
1810
+ ```bash
1811
+ kyb init <name> --host=exe --channel=none --engineer --studio
1812
+ kyb register # device flow; grants you; idempotent by name
1813
+ kyb deploy # copy + install + restart + prove
1814
+ kyb doctor && npm run eval
1815
+ ```
1816
+
1817
+
1807
1818
  ```bash
1808
1819
  kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer
1809
1820
  cd <name> && kyb doctor
@@ -2032,6 +2043,14 @@ because the session is stranded rather than stuck. Poll
2032
2043
  wait for it to clear — with a cap, so a wedged turn cannot block the restart
2033
2044
  that would clear it.
2034
2045
 
2046
+ *And build before you restart.* Proving the process started after the BUILD says
2047
+ nothing about whether the build reflects the SOURCE. A production agent ran for a
2048
+ day on a build ten hours older than its files, reporting "OK: serving the current
2049
+ build" every time. Worse, `@kybernesis/manage` calls the restart script after
2050
+ writing files — so **every capability installed from Studio reported success and
2051
+ changed nothing.** Build when the source has moved, and refuse to restart into a
2052
+ build that failed.
2053
+
2035
2054
  *And measure it correctly.* `pgrep -f 'server/index.mjs'` run over ssh matches
2036
2055
  **the shell running the pgrep** — the pattern is in its own command line — so it
2037
2056
  reports two servers when there is one. An entire investigation went into hunting
@@ -2251,6 +2270,46 @@ The things that cost real sessions here:
2251
2270
  reach across a network to a laptop that might be shut. Budgeted at 6s with a
2252
2271
  five-minute cache; without that, one closed lid makes every turn hang.
2253
2272
 
2273
+ ### 12.9 Rooms — several agents in one conversation
2274
+
2275
+ A client with a planner, a designer, and an engineer agent can put all three in
2276
+ one room and work with them the way they would with people. The room lives
2277
+ entirely in the desktop app: each member keeps its own session with its own
2278
+ deployment, and the app is what puts a message in front of all of them. **No
2279
+ agent needs to know the feature exists**, which is what makes it work with an
2280
+ agent the client wrote themselves.
2281
+
2282
+ **Addressing is the routing, and it is worth teaching in one line.** Name a
2283
+ member and only they answer. `@everyone` addresses the room. Name nobody and
2284
+ the room's lead answers — the first member — who brings the others in. Matching
2285
+ is literal and requires the `@`, because an agent called Design must not be
2286
+ summoned by the word "design" in an ordinary sentence.
2287
+
2288
+ **Hand-offs are how work moves.** An agent's reply reaches another agent only
2289
+ when it names them, and whoever is brought in receives what they missed since
2290
+ they last spoke. Without that catch-up a hand-off is incoherent: the engineer is
2291
+ asked to "build this" having never seen what "this" is.
2292
+
2293
+ Say these plainly to a client, because all three will come up:
2294
+
2295
+ - **Every hop is a billed turn** on a deployed agent. A three-agent hand-off
2296
+ chain is three turns, and there is a depth cap so a pair that keeps addressing
2297
+ each other cannot run away.
2298
+ - **The convention is a prompt, not a protocol.** Each turn carries a line
2299
+ telling the agent it is in a room and how to hand off. A well-behaved agent
2300
+ follows it; nothing enforces it. A misbehaving one is ignored rather than able
2301
+ to start a cascade — agent-to-agent relay has NO policy fallback, precisely so
2302
+ one reply cannot become a reply from everyone.
2303
+ - **A relayed message runs under the human's identity.** When the planner hands
2304
+ to the engineer, the engineer acts with that person's authority on another
2305
+ agent's say-so. Among a client's own agents that is usually what they want.
2306
+ It is still a governance decision, and it should be made rather than
2307
+ discovered.
2308
+
2309
+ What NOT to promise: emergent self-organisation. The chain works when someone —
2310
+ a person or an agent — explicitly hands off. It is a room where people and
2311
+ agents talk, not an autonomous workflow engine.
2312
+
2254
2313
  ## 13. Known gaps — state these plainly, do not sell around them
2255
2314
 
2256
2315
  Being straight about these is a feature. Clients have met vendors who were not.
@@ -86,9 +86,13 @@ then `eve add @kybernesis/<item>`). Each covers one axis:
86
86
  engineer? })` = smoke + 5 memory + routing per dept + optional vision-loop
87
87
  eval. Judge model ≠ model under test. Hermetic runs force all workspaces to
88
88
  `<name>-eval` via the npm script.
89
- - **create** — the `kyb` CLI: `init [--engineer]`, `doctor`, `upgrade`
90
- (carries eve to the Kybernesis-CERTIFIED pin, never blind latest),
91
- `skills`. Ships THIS skill suite.
89
+ - **create** — the `kyb` CLI, and the whole lifecycle of an agent:
90
+ `init` (scaffold; `--host=exe` also installs the hardened restart script),
91
+ `register` (control plane, via device flow — no admin session, no pasted
92
+ token, grants the person who ran it, idempotent by name),
93
+ `deploy` (copy + install + restart + PROVE it; `eve deploy` on Vercel),
94
+ `doctor`, `upgrade` (carries eve to the Kybernesis-CERTIFIED pin, never
95
+ blind latest), `skills`. Ships THIS skill suite.
92
96
 
93
97
  ## Gotchas that each cost a real debugging session
94
98
 
@@ -15,11 +15,21 @@ the deployment, not a shortcut. It will fail on the real engagement.
15
15
  ## Scaffold
16
16
 
17
17
  ```bash
18
- kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer
18
+ kyb init <name> --host=exe --channel=<imessage|slack|telegram|none> --engineer --studio
19
+ kyb register # control plane: device flow, grants you, idempotent by name
20
+ kyb deploy # copy + install + restart, and prove it took
21
+ kyb doctor # preflight; it knows the self-hosted failure modes below
19
22
  ```
20
23
 
21
- `--host=exe` swaps the bindings; everything else is the same product. Run
22
- `kyb doctor` after it knows the self-hosted failure modes below.
24
+ `--host=exe` swaps the bindings and installs `scripts/eve-server.sh` the
25
+ restart script, with every lesson below already in it. `--studio` adds local
26
+ execution and the management routes.
27
+
28
+ **Deploying is `kyb deploy`, not a hand-rolled rsync.** It refuses to copy
29
+ `node_modules` (native modules built on a laptop do not run on the host) or
30
+ `.eve` (the durable store — copying over it eats production state), restarts
31
+ detached so a dropped connection cannot SIGHUP the restart halfway, and waits
32
+ for the script to report health rather than reporting success on exit code.
23
33
 
24
34
  ## What Vercel gives you that a client host does not
25
35
 
@@ -193,6 +203,11 @@ Run restarts **detached** from your ssh connection —
193
203
  connection SIGHUPs the script halfway through and leaves exactly the mess it
194
204
  exists to prevent.
195
205
 
206
+ **Build before you restart.** Proving the process started after the build says
207
+ nothing about whether the build reflects the source — an agent served a build ten
208
+ hours older than its files while reporting success. It also breaks installs:
209
+ `@kybernesis/manage` writes files and then calls the restart script.
210
+
196
211
  **And measure it correctly.** `pgrep -f 'server/index.mjs'` typed over ssh
197
212
  matches the shell running it: the pattern is in that shell's own command line,
198
213
  so it reports two servers when there is one. A whole investigation went into a