@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.
- package/dist/arcana.d.ts +8 -0
- package/dist/arcana.js +99 -0
- package/dist/cli.js +21 -0
- package/dist/deploy.d.ts +4 -0
- package/dist/deploy.js +169 -0
- package/dist/init.js +6 -0
- 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 +11 -0
- package/skills/kybernesis-packages/SKILL.md +7 -3
- package/skills/self-hosting/SKILL.md +13 -3
- package/skills/.claude/skills/certification/SKILL.md +0 -75
- package/skills/.claude/skills/connect-agents/SKILL.md +0 -119
- package/skills/.claude/skills/control-plane/SKILL.md +0 -80
- package/skills/.claude/skills/eve-building/SKILL.md +0 -106
- package/skills/.claude/skills/fde-engagement/SKILL.md +0 -35
- package/skills/.claude/skills/fde-engagement/references/playbook.md +0 -2312
- package/skills/.claude/skills/kybernesis-packages/SKILL.md +0 -148
- package/skills/.claude/skills/self-hosting/SKILL.md +0 -206
- package/skills/.claude/skills/source-of-truth/SKILL.md +0 -60
package/dist/arcana.d.ts
ADDED
|
@@ -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
|
|
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/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
|
*
|
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.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
|
|
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
|
|
|
@@ -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).
|