@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 +15 -0
- package/dist/deploy.d.ts +4 -0
- package/dist/deploy.js +169 -0
- package/dist/doctor.js +6 -2
- package/dist/init.js +26 -1
- package/dist/register.d.ts +5 -0
- package/dist/register.js +110 -0
- package/dist/skills.js +13 -1
- package/package.json +1 -1
- package/skills/fde-engagement/references/playbook.md +59 -0
- package/skills/kybernesis-packages/SKILL.md +7 -3
- package/skills/self-hosting/SKILL.md +18 -3
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
|
|
package/dist/deploy.d.ts
ADDED
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
|
-
|
|
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 (
|
|
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 });
|
package/dist/register.js
ADDED
|
@@ -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.
|
|
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
|
|
90
|
-
(
|
|
91
|
-
`
|
|
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
|
|
22
|
-
|
|
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
|