@kybernesis/create 0.7.1 → 0.7.3

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.
@@ -0,0 +1,8 @@
1
+ export interface ArcanaSetup {
2
+ dir: string;
3
+ /** Proposed workspace prefix — the agent's name. Only a suggestion. */
4
+ suggest: string;
5
+ /** Department subagents, each of which gets its own brain. */
6
+ depts?: string[];
7
+ }
8
+ export declare function configureArcana({ dir, suggest, depts }: ArcanaSetup): Promise<void>;
package/dist/arcana.js ADDED
@@ -0,0 +1,99 @@
1
+ import { appendFileSync, existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { ask, bold, dim, green, red, yellow } from "./util.js";
4
+ /**
5
+ * Collect the memory workspaces and their keys, and check them before moving on.
6
+ *
7
+ * This exists because the registry item's `envVars` only APPENDS blank names to
8
+ * .env.local — nothing ever asked for a value. So a scaffold finished looking
9
+ * complete, with an agent whose memory could not work, and the first sign of it
10
+ * was an agent that had lost its mind at runtime.
11
+ *
12
+ * It also stops guessing the workspace names. `<agent>-company` is a
13
+ * convention, not a fact: a workspace with that name may already exist for
14
+ * something else, or the client may have named theirs differently years ago.
15
+ * The scaffolder proposes; the person deploying decides.
16
+ */
17
+ const API = "https://api.arcana.kybernesis.ai";
18
+ /** Read a key/workspace pair the way the agent will: read-only, one call. */
19
+ async function check(workspace, key) {
20
+ try {
21
+ const res = await fetch(`${API}/brain/${encodeURIComponent(workspace)}/timeline?limit=1`, {
22
+ headers: { authorization: `Bearer ${key}`, "X-Kyberagent-Agent": workspace },
23
+ signal: AbortSignal.timeout(15_000),
24
+ });
25
+ if (res.ok)
26
+ return "ok";
27
+ // 403 is the one that matters: keys are workspace-scoped, and a key for the
28
+ // wrong brain is the single most common way this is set up wrong.
29
+ return res.status === 401 || res.status === 403 ? "forbidden" : "unreachable";
30
+ }
31
+ catch {
32
+ return "unreachable";
33
+ }
34
+ }
35
+ function upsertEnv(dir, values) {
36
+ const p = join(dir, ".env.local");
37
+ let text = existsSync(p) ? readFileSync(p, "utf8") : "";
38
+ for (const [k, v] of Object.entries(values)) {
39
+ if (!v)
40
+ continue;
41
+ const line = `${k}="${v}"`;
42
+ const re = new RegExp(`^${k}=.*$`, "m");
43
+ if (re.test(text))
44
+ text = text.replace(re, line);
45
+ else
46
+ text += (text.endsWith("\n") || text === "" ? "" : "\n") + line + "\n";
47
+ }
48
+ if (existsSync(p))
49
+ writeFileSync(p, text);
50
+ else
51
+ appendFileSync(p, text);
52
+ }
53
+ export async function configureArcana({ dir, suggest, depts = [] }) {
54
+ console.log(bold("\n Memory (Arcana) — workspaces and keys"));
55
+ console.log(dim(" Create these at https://arcana.kybernesis.ai and mint a scoped kb_ key\n" +
56
+ " for each. Names are yours — the suggestions are only a convention."));
57
+ const values = {};
58
+ let anyBad = false;
59
+ const pair = async (label, workspaceVar, keyVar, proposed) => {
60
+ const workspace = await ask(` ${label} workspace?`, proposed);
61
+ const key = await ask(` ${label} key (kb_…, empty to skip)?`, "");
62
+ if (!key) {
63
+ console.log(yellow(` skipped — ${keyVar} left unset`));
64
+ values[workspaceVar] = workspace;
65
+ return;
66
+ }
67
+ const state = await check(workspace, key);
68
+ if (state === "ok")
69
+ console.log(green(` ✓ ${workspace} reachable`));
70
+ else if (state === "forbidden") {
71
+ anyBad = true;
72
+ console.log(red(` ✗ that key is not valid for "${workspace}" (keys are workspace-scoped)`));
73
+ }
74
+ else {
75
+ console.log(yellow(` ! could not reach Arcana to check — saved anyway`));
76
+ }
77
+ values[workspaceVar] = workspace;
78
+ values[keyVar] = key;
79
+ };
80
+ await pair("Company", "ARCANA_COMPANY_WORKSPACE", "ARCANA_API_KEY", `${suggest}-company`);
81
+ // The eval script the scaffolder writes REQUIRES this one; leaving it to a
82
+ // template comment is how `npm run eval` fails on a fresh, correct install.
83
+ await pair("Eval", "ARCANA_EVAL_API_KEY_WORKSPACE", "ARCANA_EVAL_API_KEY", `${suggest}-eval`);
84
+ // The eval workspace name itself is derived by the npm script, so only the
85
+ // key is stored; drop the placeholder we used to prompt with.
86
+ delete values.ARCANA_EVAL_API_KEY_WORKSPACE;
87
+ for (const dept of depts) {
88
+ await pair(`Subagent "${dept}"`, `ARCANA_${dept.toUpperCase()}_WORKSPACE`, `ARCANA_${dept.toUpperCase()}_API_KEY`, `${suggest}-${dept}`);
89
+ }
90
+ // DM sessions default to the company brain unless the deployment splits them.
91
+ if (values.ARCANA_COMPANY_WORKSPACE) {
92
+ values.ARCANA_DM_WORKSPACE ??= values.ARCANA_COMPANY_WORKSPACE;
93
+ }
94
+ upsertEnv(dir, values);
95
+ console.log(dim(" written to .env.local"));
96
+ if (anyBad) {
97
+ console.log(yellow(" Fix the mismatched pair and re-run `kyb arcana` — `kyb doctor` checks them too."));
98
+ }
99
+ }
package/dist/cli.js CHANGED
@@ -6,6 +6,9 @@
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 arcana set memory workspaces + keys, and verify them
10
+ * kyb register register this agent with the control plane (device flow)
11
+ * kyb deploy put this repo on its host and restart it, with proof
9
12
  * kyb upgrade bump @kybernesis/* to latest, gated on the eval suite
10
13
  * --skip-eval skip the eval gate (not for production changes)
11
14
  */
@@ -14,6 +17,9 @@ import { init } from "./init.js";
14
17
  import { doctor } from "./doctor.js";
15
18
  import { upgrade } from "./upgrade.js";
16
19
  import { installSkills } from "./skills.js";
20
+ import { deploy } from "./deploy.js";
21
+ import { register } from "./register.js";
22
+ import { configureArcana } from "./arcana.js";
17
23
  function flag(rest, key) {
18
24
  const hit = rest.find((a) => a.startsWith(`--${key}=`));
19
25
  return hit ? hit.slice(key.length + 3) : undefined;
@@ -40,6 +46,15 @@ switch (command) {
40
46
  case "skills":
41
47
  installSkills({ global: rest.includes("--global") });
42
48
  break;
49
+ case "arcana":
50
+ await configureArcana({ dir: process.cwd(), suggest: flag(rest, "name") ?? "agent" });
51
+ break;
52
+ case "register":
53
+ await register({ name: flag(rest, "name"), url: flag(rest, "url") });
54
+ break;
55
+ case "deploy":
56
+ await deploy({ host: flag(rest, "host") });
57
+ break;
43
58
  case "upgrade":
44
59
  await upgrade(rest.includes("--skip-eval"));
45
60
  break;
@@ -65,6 +80,12 @@ ${bold("kyb")} — Kybernesis agent scaffolder & FDE toolkit
65
80
  ${bold("kyb doctor")} preflight checks (keys, issuer, envs, discovery)
66
81
  ${bold("kyb skills")} install/refresh the FDE skill suite for Claude Code
67
82
  --global ${dim("install to ~/.claude/skills instead of this repo")}
83
+ ${bold("kyb arcana")} set memory workspaces + keys, and verify each pair
84
+ ${bold("kyb register")} register this agent with the control plane
85
+ --name=<name> ${dim("defaults to KYBERNESIS_AGENT in .env.local")}
86
+ --url=<url> ${dim("defaults to https://$EXE_VM_NAME.exe.xyz")}
87
+ ${bold("kyb deploy")} copy to the host, install, restart, prove it took
88
+ --host=<target> ${dim("ssh target; defaults to $EXE_VM_NAME.exe.xyz")}
68
89
  ${bold("kyb upgrade")} bump @kybernesis/* packages, gated on evals
69
90
  --skip-eval ${dim("skip the eval gate")}
70
91
 
@@ -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/init.js CHANGED
@@ -3,6 +3,7 @@ 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";
5
5
  import { suiteDir } from "./skills.js";
6
+ import { configureArcana } from "./arcana.js";
6
7
  /**
7
8
  * The always-installed core. Everything else — channels, subagents, engineer,
8
9
  * host bindings — is opt-in, because assuming them means the FDE deletes files
@@ -111,6 +112,11 @@ export async function init(rawName, options = {}) {
111
112
  writeFileSync(join(dir, "agent/agent.ts"), hostAgentTs(host, DEFAULT_MODEL));
112
113
  writeFileSync(join(dir, "agent/extensions/arcana.ts"), rootArcanaTs());
113
114
  writeFileSync(join(dir, "evals/kybernesis.eval.ts"), evalFileTs(displayName, depts));
115
+ // Ask for the memory keys HERE, while the person is still standing in the
116
+ // scaffold — not in a printed next-step they will read after the context has
117
+ // gone. Skipped with --yes, which is for CI and takes no input by design.
118
+ if (!options.yes)
119
+ await configureArcana({ dir, suggest: name, depts });
114
120
  /**
115
121
  * A self-hosted agent gets its restart script installed, not described.
116
122
  *
@@ -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.1",
3
+ "version": "0.7.3",
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
@@ -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
 
@@ -1,75 +0,0 @@
1
- ---
2
- description: Use when running evals, certifying an agent or an eve version bump, debugging eval failures, or preparing a release — the Kybernesis QA discipline and its run hygiene.
3
- ---
4
-
5
- # Certification & eval discipline
6
-
7
- The rule: **evals gate every deploy, and the consuming agent's suite is the
8
- release gate for every package change.** Nothing ships on "it looks right" —
9
- green suite or it doesn't go.
10
-
11
- ## The suite
12
-
13
- `kybernesisBaseline()` from `@kybernesis/evals` in `evals/kybernesis.eval.ts`:
14
- smoke (boots, replies, identifies itself), five memory evals (no memory
15
- thrash on greetings; explicit remember never refused; proactive storage;
16
- brain-note two-step in order; cross-session unprompted recall), one routing
17
- eval per department, and with `engineer: true` the vision-loop eval
18
- (screenshot tool fires and the judge confirms the model SAW the render).
19
- Judge model is configured in `evals/evals.config.ts` and must NEVER be the
20
- model under test.
21
-
22
- ## Run hygiene (each rule ate a real run)
23
-
24
- - `npm run eval` — always through the npm script: it forces every Arcana
25
- workspace to `<name>-eval` so evals never write into a real brain.
26
- - **Kill any running dev server first** (`pkill -f "eve dev"`) — eve eval
27
- attaches to an existing instance and runs stale code.
28
- - **Never edit the repo mid-run** — the dev runtime watches `agent/`; an
29
- edit breaks the rebuild and kills remaining evals.
30
- - Engineer eval: hosted Vercel sandbox (no Docker), needs `vercel link` +
31
- `vercel env pull` (VERCEL_OIDC_TOKEN). Warm template ≈3–4 min; a
32
- pre-first-deploy cold bake is budgeted 20 min.
33
- - Stale sandbox state (migration errors, re-baking templates):
34
- `rm -rf .eve/sandbox-cache .eve/dev-runtime` and rerun.
35
- - Don't pipe the eval command through `tail` in scripts — it masks the exit
36
- code (and `| tail -N` on a backgrounded run destroys the per-eval detail —
37
- `tee` to a file instead).
38
- - **Heavy-model suites: `maxConcurrency: 1` locally.** At 2, long opus turns
39
- overload the local world-queue transport (`Queue delivery failed … fetch
40
- failed`); crashed deliveries REPLAY subagent steps, surfacing as
41
- `lost continuationToken` races and phantom failures that move between runs.
42
- The deployed runtime uses real queue infra — this is a local-harness limit.
43
- - **AI Gateway budget is a silent eval killer**: Vercel applies a default
44
- per-project budget (e.g. $10/daily); a suite of real opus turns can exhaust
45
- it MID-RUN → `MODEL_CALL_FAILED` on whatever ran last. Check/raise:
46
- `vercel ai-gateway budgets list` / `budgets set project <name> --limit 30
47
- --refresh-period monthly`.
48
- - **"run parked on N unanswered input request(s)"** = the agent called a
49
- human-in-the-loop tool (`approval: status=pending tool=ask_question` in the
50
- turn log) — no one answers in an eval. Usually a behavior finding: the
51
- fixture was self-contained and the agent asked instead of acting. Fix the
52
- agent's bias-to-act instructions, not the fixture.
53
-
54
- ## eve version certification
55
-
56
- Clients pin the **Kybernesis-certified** eve version (`kyb upgrade` carries
57
- them there — never blind npm-latest). Certifying a new eve: bump in a branch
58
- → typecheck → `npx eve info` → full suite → live smoke on the deployed
59
- surface → advance the pin in @kybernesis/create → record the certification.
60
-
61
- ## When an eval fails
62
-
63
- Read the eval's transcript before touching fixtures. Order of suspicion:
64
- (1) environment (stale dev server, missing env, cold template), (2) a real
65
- behavior regression — fix the agent, (3) only THEN the fixture — and if a
66
- fixture changes, the reason becomes a comment on it. A failure that reveals
67
- a new failure mode becomes a new fixture: that is how the suite grew every
68
- guard it has.
69
-
70
- ## Release flow (packages)
71
-
72
- Edit in `~/platform` → build → bump → human publishes (browser auth) →
73
- consuming agent bumps → **full suite green** → deploy → registry item update
74
- + deploy if install files changed. Then propagate the lesson (see the
75
- `source-of-truth` skill).