@kybernesis/create 0.14.6 → 0.14.8

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/README.md CHANGED
@@ -30,8 +30,45 @@ Scaffolds the full Kybernesis stack:
30
30
  the Slack connector browser flow, control-plane registration + grants,
31
31
  eval → deploy → the revoke demo)
32
32
 
33
- Non-interactive (CI / agents): pipe stdin from `/dev/null` and pass the name —
34
- all prompts take defaults (departments: finance, marketing, engineering).
33
+ Non-interactive (CI / agents): pass the name and `--yes`; omitted options use
34
+ the documented defaults.
35
+
36
+ ### Model reach
37
+
38
+ The default route is unchanged: Vercel scaffolds author an AI Gateway model id,
39
+ and exe scaffolds use `exeModel()` plus `EXE_MODEL` for the VM's attached LLM
40
+ integration.
41
+
42
+ To bill model calls to a Claude subscription instead of the exe gateway:
43
+
44
+ ```bash
45
+ kyb init acme-atlas --host=exe --model-reach=claude-sub --yes
46
+ # Factory/noninteractive equivalent:
47
+ KYB_MODEL_REACH=claude-sub kyb init acme-atlas --host=exe --yes
48
+ ```
49
+
50
+ An explicit `--model-reach` takes precedence over `KYB_MODEL_REACH`. The
51
+ `claude-sub` reach is exe-only because its OAuth proxy must run on the same host.
52
+ It currently supports the bare Anthropic id `claude-opus-5` only (the gateway
53
+ form `anthropic/claude-opus-5` is normalized); other model ids are rejected
54
+ because `@kybernesis/exe` certifies its exported context-window constant for that
55
+ model only.
56
+
57
+ The generated root and all generated model-bearing subagents use
58
+ `createAnthropic` from `@ai-sdk/anthropic` with `claudeSubscription()` and
59
+ `CLAUDE_SUBSCRIPTION_CONTEXT_WINDOW` from `@kybernesis/exe`. This is Eve 0.49's
60
+ documented provider-authored AI SDK `LanguageModel` shape (`node_modules/eve/docs/agent-config.md`),
61
+ not a gateway string. Subscription scaffolds do not write or read `EXE_MODEL`.
62
+ On the host, stand up and authenticate the loopback-only proxy:
63
+
64
+ ```bash
65
+ bash scripts/claude-subscription.sh up
66
+ bash scripts/claude-subscription.sh login
67
+ bash scripts/claude-subscription.sh status
68
+ ```
69
+
70
+ The concrete provider and proxy contract is documented in
71
+ `packages/exe/src/claude.ts` and `packages/exe/README.md`.
35
72
 
36
73
  ## `kyb doctor`
37
74
 
@@ -42,6 +79,7 @@ Run inside an agent project. Checks, with pass/warn/fail per line:
42
79
  (200 / wrong-key 403 / missing-workspace 404, each with the fix)
43
80
  - control-plane issuer JWKS reachable; `KYBERNESIS_AGENT` set
44
81
  - Slack connector UID present
82
+ - model reach (`claude-sub`, `exe`, gateway, direct provider, or unresolved)
45
83
  - `eve info` discovery clean
46
84
  - port 2000 free (a running dev server makes `eve eval` exit early)
47
85
 
@@ -1,3 +1,4 @@
1
+ import type { CompiledModelRouting } from "./model-reach.js";
1
2
  /** Eve 0.49's root default when the compiled config omits the policy. */
2
3
  export declare const CERTIFIED_INHERITED_MAX_INPUT_TOKENS_PER_SESSION = 40000000;
3
4
  export type AgentInputLimit = {
@@ -19,6 +20,7 @@ export interface EveInfoInspection {
19
20
  } | null;
20
21
  status: string | null;
21
22
  limit: AgentInputLimit;
23
+ modelRouting: CompiledModelRouting;
22
24
  }
23
25
  /**
24
26
  * Read the effective root policy from Eve's compiled artifact, never from
@@ -21,7 +21,12 @@ export function parseEveInfoInspection(output, readText = (path) => readFileSync
21
21
  ? info.artifacts.compiledManifest
22
22
  : null;
23
23
  if (!manifestPath) {
24
- return { diagnostics, status, limit: { kind: "unresolved", reason: "compiled manifest path was unavailable" } };
24
+ return {
25
+ diagnostics,
26
+ status,
27
+ limit: { kind: "unresolved", reason: "compiled manifest path was unavailable" },
28
+ modelRouting: { kind: "unresolved", reason: "compiled manifest path was unavailable" },
29
+ };
25
30
  }
26
31
  let manifest;
27
32
  try {
@@ -32,31 +37,39 @@ export function parseEveInfoInspection(output, readText = (path) => readFileSync
32
37
  diagnostics,
33
38
  status,
34
39
  limit: { kind: "unresolved", reason: `compiled manifest could not be read: ${error.message}` },
40
+ modelRouting: { kind: "unresolved", reason: `compiled manifest could not be read: ${error.message}` },
35
41
  };
36
42
  }
37
43
  if (!isRecord(manifest) || !isRecord(manifest.config)) {
38
- return { diagnostics, status, limit: { kind: "unresolved", reason: "compiled root config was unavailable" } };
44
+ return {
45
+ diagnostics,
46
+ status,
47
+ limit: { kind: "unresolved", reason: "compiled root config was unavailable" },
48
+ modelRouting: { kind: "unresolved", reason: "compiled root config was unavailable" },
49
+ };
39
50
  }
51
+ const modelRouting = parseModelRouting(manifest.config);
40
52
  const limits = manifest.config.limits;
41
53
  if (limits === undefined) {
42
- return inherited(diagnostics, status);
54
+ return inherited(diagnostics, status, modelRouting);
43
55
  }
44
56
  if (!isRecord(limits)) {
45
- return { diagnostics, status, limit: { kind: "unresolved", reason: "compiled root limits were malformed" } };
57
+ return { diagnostics, status, limit: { kind: "unresolved", reason: "compiled root limits were malformed" }, modelRouting };
46
58
  }
47
59
  if (!Object.hasOwn(limits, "maxInputTokensPerSession")) {
48
- return inherited(diagnostics, status);
60
+ return inherited(diagnostics, status, modelRouting);
49
61
  }
50
62
  const value = limits.maxInputTokensPerSession;
51
63
  if (value === false)
52
- return { diagnostics, status, limit: { kind: "explicit-uncapped" } };
64
+ return { diagnostics, status, limit: { kind: "explicit-uncapped" }, modelRouting };
53
65
  if (typeof value === "number" && Number.isFinite(value) && value > 0) {
54
- return { diagnostics, status, limit: { kind: "explicit-numeric", value } };
66
+ return { diagnostics, status, limit: { kind: "explicit-numeric", value }, modelRouting };
55
67
  }
56
68
  return {
57
69
  diagnostics,
58
70
  status,
59
71
  limit: { kind: "unresolved", reason: "compiled maxInputTokensPerSession was neither a positive number nor false" },
72
+ modelRouting,
60
73
  };
61
74
  }
62
75
  /** Compile once through Eve's structured info contract and inspect its artifact. */
@@ -71,15 +84,40 @@ function parseDiagnostics(value) {
71
84
  ? { errors: value.errors, warnings: value.warnings }
72
85
  : null;
73
86
  }
74
- function inherited(diagnostics, status) {
87
+ function inherited(diagnostics, status, modelRouting) {
75
88
  return {
76
89
  diagnostics,
77
90
  status,
78
91
  limit: { kind: "inherited", value: CERTIFIED_INHERITED_MAX_INPUT_TOKENS_PER_SESSION },
92
+ modelRouting,
79
93
  };
80
94
  }
95
+ function parseModelRouting(config) {
96
+ if (config.dynamicModel !== undefined) {
97
+ return { kind: "unresolved", reason: "the root uses dynamic model selection" };
98
+ }
99
+ const model = config.model;
100
+ if (!isRecord(model) || !isRecord(model.routing)) {
101
+ return { kind: "unresolved", reason: "compiled model routing was unavailable" };
102
+ }
103
+ const routing = model.routing;
104
+ if (routing.kind === "gateway")
105
+ return { kind: "gateway" };
106
+ if (routing.kind === "external") {
107
+ return {
108
+ kind: "external",
109
+ ...(typeof routing.provider === "string" ? { provider: routing.provider } : {}),
110
+ };
111
+ }
112
+ return { kind: "unresolved", reason: "compiled model routing was unrecognized" };
113
+ }
81
114
  function unresolved(reason) {
82
- return { diagnostics: null, status: null, limit: { kind: "unresolved", reason } };
115
+ return {
116
+ diagnostics: null,
117
+ status: null,
118
+ limit: { kind: "unresolved", reason },
119
+ modelRouting: { kind: "unresolved", reason },
120
+ };
83
121
  }
84
122
  function isRecord(value) {
85
123
  return typeof value === "object" && value !== null && !Array.isArray(value);
package/dist/cli.js CHANGED
@@ -60,6 +60,7 @@ function initOptions(rest) {
60
60
  channel: flag(rest, "channel"),
61
61
  host: flag(rest, "host"),
62
62
  model: flag(rest, "model"),
63
+ modelReach: flag(rest, "model-reach"),
63
64
  subagents: subs === undefined ? undefined : subs.split(',').map((s) => s.trim()).filter(Boolean),
64
65
  yes: rest.includes('--yes') || rest.includes('-y'),
65
66
  };
@@ -87,6 +88,20 @@ const COMMANDS = {
87
88
  function usage(command) {
88
89
  if (command && COMMANDS[command]) {
89
90
  console.log(`\n ${bold(`kyb ${command}`)} — ${COMMANDS[command]}\n`);
91
+ if (command === "init") {
92
+ console.log(` ${bold("kyb init [name]")} [options]
93
+ --channel=<kind> ${dim("none|slack|imessage|telegram|discord|web")}
94
+ --host=<kind> ${dim("vercel|exe (default: vercel)")}
95
+ --model=<id> ${dim("gateway id; claude-sub requires bare claude-opus-5")}
96
+ --model-reach=<reach> ${dim("claude-sub (exe only; default: host route)")}
97
+ --subagents=a,b ${dim("department subagents")}
98
+ --engineer ${dim("add the engineer layer")}
99
+ --studio ${dim("wire KYBER Studio local/manage routes")}
100
+ --yes, -y ${dim("no prompts")}
101
+
102
+ ${dim("Factory env: KYB_MODEL_REACH=claude-sub (the CLI flag takes precedence).")}
103
+ ${dim("Claude subscription reach uses the loopback OAuth proxy; it does not read EXE_MODEL.")}\n`);
104
+ }
90
105
  return;
91
106
  }
92
107
  console.log(`\n ${bold("kyb")} ${dim(VERSION)} — the Kybernesis agent CLI\n`);
@@ -192,7 +207,8 @@ ${dim(" npm i -g @kybernesis/create@latest")}
192
207
  --channel=<kind> ${dim("none|slack|imessage|telegram|discord|web (default: none)")}
193
208
  --host=<kind> ${dim("vercel|exe (default: vercel)")}
194
209
  --subagents=a,b ${dim("department subagents (default: none)")}
195
- --model=<id> ${dim("provider/model-id (default: sonnet 5)")}
210
+ --model=<id> ${dim("provider/model-id; bare claude-opus-5 for claude-sub")}
211
+ --model-reach=<r> ${dim("claude-sub (exe only); env: KYB_MODEL_REACH")}
196
212
  --engineer ${dim("add the engineer layer: workshop sandbox + vision dev loop")}
197
213
  --studio ${dim("wire for KYBER Studio: local execution + management routes")}
198
214
  --yes ${dim("no prompts; take flags and defaults")}
package/dist/deploy.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
+ import { TEMPLATE_CONTAINER_PROBE, assessRestart, parseTemplateContainers } from "./sandbox-orphans.js";
2
3
  import { existsSync, readFileSync } from "node:fs";
3
4
  import { join } from "node:path";
4
5
  import { upsertEnv } from "./envfile.js";
@@ -239,6 +240,14 @@ export async function deploy(options) {
239
240
  ' SCRIPT="scripts/eve-server.sh"',
240
241
  "fi",
241
242
  'if [ -z "$SCRIPT" ]; then echo "FAILED: no restart script on the host"; exit 1; fi',
243
+ // An orphaned sandbox template container (a start killed mid-build; only
244
+ // `sleep` inside) makes every eve start hang forever. The packaged start
245
+ // script clears it too, but a host still running its own older copy
246
+ // would not, so the deploy does it first and says so (KYB-531).
247
+ "for c in $(command -v docker >/dev/null 2>&1 && docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
248
+ " live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
249
+ ' if [ "${live:-0}" = "0" ]; then echo "removing orphaned sandbox template container $c (only sleep inside; it would block every start)"; docker rm -f "$c" >/dev/null 2>&1 || true; fi',
250
+ "done",
242
251
  'echo "using $SCRIPT"',
243
252
  'setsid nohup bash "$SCRIPT" > /tmp/kyb-deploy.log 2>&1 < /dev/null &',
244
253
  "sleep 2",
@@ -266,23 +275,45 @@ export async function deploy(options) {
266
275
  let last = "";
267
276
  let seen = "";
268
277
  let lastChange = Date.now();
278
+ let lastBuildingNote = 0;
279
+ let stuck = null;
269
280
  while (Date.now() - startedAt < CEILING_MS) {
270
- const out = execFileSync("ssh", [target, `tail -40 /tmp/kyb-deploy.log 2>/dev/null || true`], {
271
- encoding: "utf8",
272
- });
273
- last = out;
274
- if (out !== seen) {
275
- for (const line of out.split("\n")) {
276
- if (line && !seen.includes(line) && /npm|install|build|SOURCE IS NEWER|waiting|pid=|health:|FAILED/i.test(line)) {
281
+ // Two witnesses: the log, and the sandbox template containers. A cold
282
+ // template build is silent in the log for longer than the quiet limit,
283
+ // and judging it on the log alone is how a fine build got killed (KYB-531).
284
+ const out = execFileSync("ssh", [target, `tail -40 /tmp/kyb-deploy.log 2>/dev/null || true; echo __TEMPLATES__; ${TEMPLATE_CONTAINER_PROBE}`], { encoding: "utf8" });
285
+ const [logPart, probePart = ""] = out.split("__TEMPLATES__");
286
+ last = logPart;
287
+ if (logPart !== seen) {
288
+ for (const line of logPart.split("\n")) {
289
+ if (line && !seen.includes(line) && /npm|install|build|SOURCE IS NEWER|waiting|pid=|health:|FAILED|template|removing/i.test(line)) {
277
290
  console.log(dim(` ${line.trim().slice(0, 120)}`));
278
291
  }
279
292
  }
280
- seen = out;
293
+ seen = logPart;
294
+ lastChange = Date.now();
295
+ }
296
+ const verdict = assessRestart({
297
+ log: logPart,
298
+ templates: parseTemplateContainers(probePart),
299
+ quietMs: Date.now() - lastChange,
300
+ quietLimitMs: QUIET_LIMIT_MS,
301
+ });
302
+ if (verdict.state === "healthy" || verdict.state === "failed")
303
+ break;
304
+ if (verdict.state === "building") {
281
305
  lastChange = Date.now();
306
+ if (Date.now() - lastBuildingNote > 60_000) {
307
+ console.log(dim(` still building the sandbox template (${Math.round((Date.now() - startedAt) / 60_000)} min): ${verdict.detail}`));
308
+ lastBuildingNote = Date.now();
309
+ }
282
310
  }
283
- if (/health:|FAILED/.test(out))
311
+ else if (verdict.state === "orphaned") {
312
+ stuck = verdict.detail ?? null;
313
+ console.log(yellow(`\n ! an orphaned sandbox template container is blocking the start: ${stuck}`));
284
314
  break;
285
- if (Date.now() - lastChange > QUIET_LIMIT_MS) {
315
+ }
316
+ else if (verdict.state === "quiet") {
286
317
  console.log(yellow(`\n ! nothing new on the host for ${Math.round(QUIET_LIMIT_MS / 60_000)} minutes — giving up on the wait`));
287
318
  break;
288
319
  }
@@ -296,8 +327,11 @@ export async function deploy(options) {
296
327
  .join("\n"));
297
328
  console.log(healthy
298
329
  ? green("\n ✓ deployed and serving the current build")
299
- : yellow("\n ! the restart did not report health. It may still be building — this stops watching, it does not stop the host.\n" +
300
- ` Watch it: ssh ${target} 'tail -f /tmp/kyb-deploy.log'`));
330
+ : stuck
331
+ ? yellow(`\n ! the restart cannot finish while that container exists. Remove it on the host, then run kyb deploy again.\n` +
332
+ ` ssh ${target} '${stuck.replace(/^.*remove it: /, "").replace(/\).*$/, "")}'`)
333
+ : yellow("\n ! the restart did not report health. It may still be building — this stops watching, it does not stop the host.\n" +
334
+ ` Watch it: ssh ${target} 'tail -f /tmp/kyb-deploy.log'`));
301
335
  if (!healthy)
302
336
  process.exitCode = 1;
303
337
  }
package/dist/doctor.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { type AgentInputLimit } from "./agent-limits.js";
2
+ import { type CompiledModelRouting } from "./model-reach.js";
2
3
  type Verdict = "pass" | "warn" | "fail";
3
4
  export interface Check {
4
5
  verdict: Verdict;
@@ -17,6 +18,7 @@ export interface DockerTemplateInspection {
17
18
  }[];
18
19
  }
19
20
  export declare function dockerTemplateDoctorChecks(result: DockerTemplateInspection): Check[];
21
+ export declare function modelReachDoctorCheck(authoredSource: string | null, compiledRouting: CompiledModelRouting): Check;
20
22
  export declare function agentInputLimitDoctorCheck(limit: AgentInputLimit): Check;
21
23
  /**
22
24
  * The inspector lives in @kybernesis/exe, next to the runtime that owns the
package/dist/doctor.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import { existsSync, readFileSync, readdirSync } from "node:fs";
2
+ import { TEMPLATE_CONTAINER_PROBE, parseTemplateContainers, templateContainerDoctorChecks } from "./sandbox-orphans.js";
2
3
  import { join } from "node:path";
3
4
  import { pathToFileURL } from "node:url";
4
5
  import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
5
6
  import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.js";
6
7
  import { inspectEveAgent } from "./agent-limits.js";
8
+ import { classifyModelReach } from "./model-reach.js";
7
9
  const MARK = {
8
10
  pass: green("✓"),
9
11
  warn: yellow("!"),
@@ -31,6 +33,24 @@ export function dockerTemplateDoctorChecks(result) {
31
33
  detail: issue.detail,
32
34
  }));
33
35
  }
36
+ export function modelReachDoctorCheck(authoredSource, compiledRouting) {
37
+ const reach = classifyModelReach(authoredSource, compiledRouting);
38
+ switch (reach.kind) {
39
+ case "claude-sub":
40
+ return { verdict: "pass", label: "Model reach: claude-sub (Claude subscription)" };
41
+ case "exe":
42
+ return { verdict: "pass", label: "Model reach: exe (host LLM integration)" };
43
+ case "gateway":
44
+ return { verdict: "pass", label: "Model reach: gateway" };
45
+ case "direct-provider":
46
+ return {
47
+ verdict: "pass",
48
+ label: `Model reach: direct-provider${reach.provider ? ` (${reach.provider})` : ""}`,
49
+ };
50
+ case "unresolved":
51
+ return { verdict: "warn", label: "Model reach: unresolved", detail: reach.reason };
52
+ }
53
+ }
34
54
  export function agentInputLimitDoctorCheck(limit) {
35
55
  switch (limit.kind) {
36
56
  case "explicit-numeric":
@@ -358,6 +378,11 @@ export async function doctor() {
358
378
  * eventually produces looks like anything except a full disk.
359
379
  */
360
380
  if (capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes") {
381
+ // A template container left behind by a killed start blocks every
382
+ // later start forever; a live one means "do not restart yet" (KYB-531).
383
+ for (const check of templateContainerDoctorChecks(parseTemplateContainers(capture("sh", ["-c", TEMPLATE_CONTAINER_PROBE])))) {
384
+ checks.push(check);
385
+ }
361
386
  const job = capture("sh", ["-c", "test -x /etc/cron.daily/kyb-docker-prune && echo yes"])?.trim();
362
387
  const percent = Number(capture("sh", ["-c", "df / | awk 'NR==2{print $5}' | tr -d '%'"])?.trim() ?? 0);
363
388
  if (job !== "yes") {
@@ -502,9 +527,16 @@ export async function doctor() {
502
527
  * `env` here is the same merged view every other check reads: .env.local
503
528
  * underneath, the real environment on top.
504
529
  */
530
+ const rootAgentPath = join(cwd, "agent/agent.ts");
531
+ const rootAgentSource = existsSync(rootAgentPath) ? readFileSync(rootAgentPath, "utf8") : null;
505
532
  const info = inspectEveAgent(cwd, env);
506
533
  if (info === null) {
507
534
  add("fail", "eve info failed", "run: set -a && . ./.env.local && set +a && npx eve info --json — the agent's own error is in that output");
535
+ const reachCheck = modelReachDoctorCheck(rootAgentSource, {
536
+ kind: "unresolved",
537
+ reason: "eve info failed before compiled model routing could be inspected",
538
+ });
539
+ add(reachCheck.verdict, reachCheck.label, reachCheck.detail);
508
540
  add("warn", "Eve max input tokens/session: unresolved", "eve info failed before the compiled root policy could be inspected");
509
541
  }
510
542
  else {
@@ -513,6 +545,8 @@ export async function doctor() {
513
545
  add("pass", `eve discovery clean (${diag.warnings} warnings)`);
514
546
  else
515
547
  add("fail", `eve discovery: ${diag ? `${diag.errors} errors` : "unparsed"}`, "npx eve info --json");
548
+ const reachCheck = modelReachDoctorCheck(rootAgentSource, info.modelRouting);
549
+ add(reachCheck.verdict, reachCheck.label, reachCheck.detail);
516
550
  const limitCheck = agentInputLimitDoctorCheck(info.limit);
517
551
  add(limitCheck.verdict, limitCheck.label, limitCheck.detail);
518
552
  }
package/dist/init.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface InitOptions {
26
26
  * actually non-interactive.
27
27
  */
28
28
  model?: string;
29
+ /** One-shot route selector. `claude-sub` uses the host-local Claude OAuth proxy. */
30
+ modelReach?: string;
29
31
  /** Department subagents. Default NONE. */
30
32
  subagents?: string[];
31
33
  /** Skip prompts and take the flags/defaults as given. */
package/dist/init.js CHANGED
@@ -7,6 +7,7 @@ import { configureArcana } from "./arcana.js";
7
7
  import { upsertEnv } from "./envfile.js";
8
8
  import { systemdRestartCommand } from "./systemd.js";
9
9
  import { sandboxCleanupScaffoldFiles } from "./sandbox-cleanup.js";
10
+ import { MODEL_REACH_ENV, resolveModelScaffold } from "./model-reach.js";
10
11
  /**
11
12
  * The always-installed core. Everything else — channels, subagents, engineer,
12
13
  * host bindings — is opt-in, because assuming them means the FDE deletes files
@@ -18,7 +19,6 @@ const CORE_ITEMS = ["enterprise", "arcana", "evals"];
18
19
  // self-hosted agent it cannot get an OIDC token and the agent fails to boot.
19
20
  const ENGINEER_ITEMS_ALL = ["extension/agent-browser", "extension/github-tools"];
20
21
  const ENGINEER_ITEMS_VERCEL = ["connection/vercel"];
21
- const DEFAULT_MODEL = "anthropic/claude-sonnet-5";
22
22
  export async function init(rawName, options = {}) {
23
23
  const engineer = options.engineer === true;
24
24
  const studio = options.studio === true;
@@ -59,8 +59,20 @@ export async function init(rawName, options = {}) {
59
59
  console.error(`Directory ${name}/ already exists.`);
60
60
  process.exit(1);
61
61
  }
62
+ let model;
63
+ try {
64
+ model = resolveModelScaffold({
65
+ host,
66
+ cliReach: options.modelReach,
67
+ envReach: process.env[MODEL_REACH_ENV],
68
+ model: options.model,
69
+ });
70
+ }
71
+ catch (error) {
72
+ console.error(red(error.message));
73
+ process.exit(1);
74
+ }
62
75
  const plan = channelPlan(channel, name, host);
63
- const model = options.model ?? DEFAULT_MODEL;
64
76
  console.log(bold(`\n1/6 Scaffolding eve agent (eve@${EVE_VERSION}) …`));
65
77
  // eve's scaffold ends by opening its interactive model picker
66
78
  // (`eve dev --input /model`), which exits non-zero wherever there is no
@@ -97,11 +109,17 @@ export async function init(rawName, options = {}) {
97
109
  for (const item of CORE_ITEMS) {
98
110
  run("npx", ["eve", "add", `@kybernesis/${item}`, "--overwrite"], { cwd: dir });
99
111
  }
100
- // @ai-sdk/anthropic is for the EVAL JUDGE, not the agent: an agent judged
101
- // by the model it runs on is a weak test.
112
+ // Anthropic is always the exe eval judge and is also the claude-sub agent
113
+ // provider. OpenAI is needed only by the ordinary exeModel gateway route.
102
114
  const extraDeps = [
103
115
  ...plan.deps,
104
- ...(host === "exe" ? ["@kybernesis/exe", "@ai-sdk/openai", "@ai-sdk/anthropic"] : []),
116
+ ...(host === "exe"
117
+ ? [
118
+ "@kybernesis/exe",
119
+ ...(model.reach === "default" ? ["@ai-sdk/openai"] : []),
120
+ "@ai-sdk/anthropic",
121
+ ]
122
+ : []),
105
123
  ];
106
124
  if (extraDeps.length) {
107
125
  console.log(bold(`\n2b Installing for ${channel}/${host}: ${extraDeps.join(", ")} …`));
@@ -197,7 +215,8 @@ export async function init(rawName, options = {}) {
197
215
  * with the model. Empty fails honestly; the host lists its own with
198
216
  * curl https://llm.int.exe.xyz/models.json
199
217
  */
200
- known.EXE_MODEL = "";
218
+ if (model.reach === "default")
219
+ known.EXE_MODEL = "";
201
220
  /**
202
221
  * Longer than any turn, because the alternative is answering twice.
203
222
  *
@@ -299,7 +318,7 @@ export async function init(rawName, options = {}) {
299
318
  for (const dept of depts) {
300
319
  const base = join(dir, "agent/subagents", dept);
301
320
  mkdirSync(join(base, "connections"), { recursive: true });
302
- writeFileSync(join(base, "agent.ts"), subagentAgentTs(dept));
321
+ writeFileSync(join(base, "agent.ts"), subagentAgentTs(dept, host, model));
303
322
  writeFileSync(join(base, "instructions.md"), subagentInstructionsMd(dept));
304
323
  writeFileSync(join(base, "connections/arcana.ts"), subagentArcanaTs(dept));
305
324
  if (existsSync(skillsSource)) {
@@ -340,13 +359,14 @@ export async function init(rawName, options = {}) {
340
359
  "self-testing (evals)",
341
360
  channel === "none" ? null : `${channel} channel`,
342
361
  host === "exe" ? "exe.dev host" : null,
362
+ model.reach === "claude-sub" ? "Claude subscription reach" : null,
343
363
  studio ? "KYBER Studio (local execution + management routes)" : null,
344
364
  engineer ? "engineer subagent (workshop + vision loop)" : null,
345
365
  depts.length ? `${depts.length} dept subagent(s)` : null,
346
366
  ].filter(Boolean);
347
367
  const steps = [
348
368
  `Arcana: create workspaces (${name}-company, ${name}-eval${depts.map((d) => `, ${name}-${d}`).join("")}) + scoped kb_ keys; fill .env.local from .env.example`,
349
- ...hostSteps(host, name),
369
+ ...hostSteps(host, name, model),
350
370
  ...plan.steps,
351
371
  ...(engPlan?.steps ?? []),
352
372
  `Control plane: register agent "${name}" at ${issuer}/agents + grant the pilot cohort`,
@@ -0,0 +1,44 @@
1
+ import type { HostKind } from "./templates.js";
2
+ export declare const MODEL_REACH_ENV = "KYB_MODEL_REACH";
3
+ export declare const CLAUDE_SUBSCRIPTION_MODEL = "claude-opus-5";
4
+ export type ScaffoldModelReach = "default" | "claude-sub";
5
+ export interface ModelScaffoldConfig {
6
+ reach: ScaffoldModelReach;
7
+ model: string;
8
+ }
9
+ export type CompiledModelRouting = {
10
+ kind: "gateway";
11
+ } | {
12
+ kind: "external";
13
+ provider?: string;
14
+ } | {
15
+ kind: "unresolved";
16
+ reason: string;
17
+ };
18
+ export type DoctorModelReach = {
19
+ kind: "claude-sub";
20
+ } | {
21
+ kind: "exe";
22
+ } | {
23
+ kind: "gateway";
24
+ } | {
25
+ kind: "direct-provider";
26
+ provider?: string;
27
+ } | {
28
+ kind: "unresolved";
29
+ reason: string;
30
+ };
31
+ /** Resolve the one-shot scaffold selector. An explicit CLI value wins over Factory env. */
32
+ export declare function resolveModelReach(cliValue?: string, envValue?: string): ScaffoldModelReach;
33
+ /** Validate host/model compatibility before eve creates any files. */
34
+ export declare function resolveModelScaffold(options: {
35
+ host: HostKind;
36
+ cliReach?: string;
37
+ envReach?: string;
38
+ model?: string;
39
+ }): ModelScaffoldConfig;
40
+ /**
41
+ * Classify the effective route from stable authored helpers first, then Eve's
42
+ * compiled routing. Provider=anthropic alone is not evidence of a subscription.
43
+ */
44
+ export declare function classifyModelReach(authoredSource: string | null, compiledRouting: CompiledModelRouting): DoctorModelReach;
@@ -0,0 +1,52 @@
1
+ export const MODEL_REACH_ENV = "KYB_MODEL_REACH";
2
+ export const CLAUDE_SUBSCRIPTION_MODEL = "claude-opus-5";
3
+ /** Resolve the one-shot scaffold selector. An explicit CLI value wins over Factory env. */
4
+ export function resolveModelReach(cliValue, envValue) {
5
+ const selected = cliValue !== undefined ? cliValue : envValue;
6
+ if (selected === undefined)
7
+ return "default";
8
+ if (selected.trim() === "" && cliValue === undefined)
9
+ return "default";
10
+ if (selected.trim() === "claude-sub")
11
+ return "claude-sub";
12
+ throw new Error(`Unsupported model reach ${JSON.stringify(selected)}. Supported values: claude-sub (or omit it for the host default).`);
13
+ }
14
+ /** Validate host/model compatibility before eve creates any files. */
15
+ export function resolveModelScaffold(options) {
16
+ const reach = resolveModelReach(options.cliReach, options.envReach);
17
+ if (reach === "default") {
18
+ return { reach, model: options.model ?? "anthropic/claude-sonnet-5" };
19
+ }
20
+ if (options.host !== "exe") {
21
+ throw new Error("Model reach claude-sub requires --host=exe because its OAuth proxy runs on the same machine as the agent.");
22
+ }
23
+ const model = (options.model ?? CLAUDE_SUBSCRIPTION_MODEL).replace(/^anthropic\//, "");
24
+ if (model !== CLAUDE_SUBSCRIPTION_MODEL) {
25
+ throw new Error(`Model reach claude-sub currently supports only ${CLAUDE_SUBSCRIPTION_MODEL}. ` +
26
+ "That is the model covered by @kybernesis/exe's certified context-window constant; use a bare Anthropic id.");
27
+ }
28
+ return { reach, model };
29
+ }
30
+ /**
31
+ * Classify the effective route from stable authored helpers first, then Eve's
32
+ * compiled routing. Provider=anthropic alone is not evidence of a subscription.
33
+ */
34
+ export function classifyModelReach(authoredSource, compiledRouting) {
35
+ if (authoredSource !== null) {
36
+ const claudeSubscription = /\bclaudeSubscription\s*\(/.test(authoredSource);
37
+ const exeModel = /\bexeModel\s*\(/.test(authoredSource);
38
+ if (claudeSubscription && !exeModel)
39
+ return { kind: "claude-sub" };
40
+ if (exeModel && !claudeSubscription)
41
+ return { kind: "exe" };
42
+ if (claudeSubscription && exeModel) {
43
+ return { kind: "unresolved", reason: "root agent authors both claudeSubscription() and exeModel()" };
44
+ }
45
+ }
46
+ if (compiledRouting.kind === "gateway")
47
+ return { kind: "gateway" };
48
+ if (compiledRouting.kind === "external") {
49
+ return { kind: "direct-provider", ...(compiledRouting.provider ? { provider: compiledRouting.provider } : {}) };
50
+ }
51
+ return { kind: "unresolved", reason: compiledRouting.reason };
52
+ }
@@ -0,0 +1,56 @@
1
+ import type { Check } from "./doctor.js";
2
+ /**
3
+ * Docker sandbox template containers: the ones still building, and the ones
4
+ * left behind.
5
+ *
6
+ * eve builds each Docker sandbox template inside an `eve-sbx-tpl-*` container
7
+ * on first start. A cold build (apt-get, pnpm, playwright with its browsers)
8
+ * runs for minutes and prints nothing to the deploy log while it does. Two
9
+ * things went wrong with that on one deployment (KYB-531):
10
+ *
11
+ * - the host start script judged the start on a 45-second clock and printed
12
+ * "FAILED to start" while the template was building, so the operator killed
13
+ * the start;
14
+ * - the killed start left the container behind with `sleep 2147483647` as its
15
+ * only process, and while it existed every later `eve start` blocked forever
16
+ * at "initializing N sandbox templates" with nothing in any log to say why.
17
+ *
18
+ * Everything here reads one probe: each template container's name and how
19
+ * many processes inside it are not `sleep`. Building means at least one; an
20
+ * orphan means none.
21
+ */
22
+ export interface TemplateContainer {
23
+ name: string;
24
+ /** Processes inside the container other than `sleep`. */
25
+ liveProcesses: number;
26
+ }
27
+ /**
28
+ * Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
29
+ * nothing when Docker is absent. The same probe serves doctor (locally), the
30
+ * deploy (over ssh), and the host start script.
31
+ */
32
+ export declare const TEMPLATE_CONTAINER_PROBE: string;
33
+ export declare function parseTemplateContainers(output: string | null | undefined): TemplateContainer[];
34
+ export declare function orphanedTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
35
+ export declare function buildingTemplates(containers: readonly TemplateContainer[]): TemplateContainer[];
36
+ /** The exact command that clears an orphan; printed as the remedy and run by the start script. */
37
+ export declare function removeOrphanCommand(name: string): string;
38
+ export declare function templateContainerDoctorChecks(containers: readonly TemplateContainer[]): Check[];
39
+ export type RestartState = "healthy" | "failed" | "building" | "orphaned" | "quiet" | "waiting";
40
+ /**
41
+ * What a deploy should conclude from the host right now.
42
+ *
43
+ * The log alone was the whole verdict before, and a template build is silent
44
+ * in it. The template probe is the second witness: while something is building,
45
+ * silence is progress and the wait goes on; an orphan with nothing building
46
+ * and no server is a stuck host with a known remedy, not a slow one.
47
+ */
48
+ export declare function assessRestart(input: {
49
+ log: string;
50
+ templates: readonly TemplateContainer[];
51
+ quietMs: number;
52
+ quietLimitMs: number;
53
+ }): {
54
+ state: RestartState;
55
+ detail?: string;
56
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Shell that prints one `name count` line per `eve-sbx-tpl-*` container, or
3
+ * nothing when Docker is absent. The same probe serves doctor (locally), the
4
+ * deploy (over ssh), and the host start script.
5
+ */
6
+ export const TEMPLATE_CONTAINER_PROBE = [
7
+ "command -v docker >/dev/null 2>&1 || exit 0",
8
+ "for c in $(docker ps --filter name=eve-sbx-tpl- --format '{{.Names}}' 2>/dev/null); do",
9
+ " live=$(docker top \"$c\" 2>/dev/null | tail -n +2 | awk '{print $8}' | grep -vc '^sleep$')",
10
+ ' echo "$c ${live:-0}"',
11
+ "done",
12
+ ].join("\n");
13
+ export function parseTemplateContainers(output) {
14
+ return (output ?? "")
15
+ .split("\n")
16
+ .map((line) => line.trim())
17
+ .filter((line) => line.startsWith("eve-sbx-tpl-"))
18
+ .map((line) => {
19
+ const [name, count] = line.split(/\s+/);
20
+ return { name, liveProcesses: Number(count) || 0 };
21
+ });
22
+ }
23
+ export function orphanedTemplates(containers) {
24
+ return containers.filter((c) => c.liveProcesses === 0);
25
+ }
26
+ export function buildingTemplates(containers) {
27
+ return containers.filter((c) => c.liveProcesses > 0);
28
+ }
29
+ /** The exact command that clears an orphan; printed as the remedy and run by the start script. */
30
+ export function removeOrphanCommand(name) {
31
+ return `docker rm -f ${name}`;
32
+ }
33
+ export function templateContainerDoctorChecks(containers) {
34
+ const orphans = orphanedTemplates(containers);
35
+ const building = buildingTemplates(containers);
36
+ const checks = orphans.map((c) => ({
37
+ verdict: "fail",
38
+ label: `orphaned sandbox template container ${c.name}: every eve start will hang until it is removed`,
39
+ detail: `its only process is sleep; a start was killed mid-build. Remove it: ${removeOrphanCommand(c.name)}`,
40
+ }));
41
+ if (building.length) {
42
+ checks.push({
43
+ verdict: "warn",
44
+ label: `sandbox template building: ${building.map((c) => c.name).join(", ")}`,
45
+ detail: "a cold build takes minutes and the server binds its port only after it finishes; do not restart",
46
+ });
47
+ }
48
+ return checks;
49
+ }
50
+ /**
51
+ * What a deploy should conclude from the host right now.
52
+ *
53
+ * The log alone was the whole verdict before, and a template build is silent
54
+ * in it. The template probe is the second witness: while something is building,
55
+ * silence is progress and the wait goes on; an orphan with nothing building
56
+ * and no server is a stuck host with a known remedy, not a slow one.
57
+ */
58
+ export function assessRestart(input) {
59
+ if (/health:\s*200/.test(input.log))
60
+ return { state: "healthy" };
61
+ if (/FAILED/.test(input.log))
62
+ return { state: "failed" };
63
+ const building = buildingTemplates(input.templates);
64
+ if (building.length) {
65
+ return { state: "building", detail: building.map((c) => c.name).join(", ") };
66
+ }
67
+ const orphans = orphanedTemplates(input.templates);
68
+ if (orphans.length) {
69
+ return {
70
+ state: "orphaned",
71
+ detail: orphans.map((c) => `${c.name} (remove it: ${removeOrphanCommand(c.name)})`).join("; "),
72
+ };
73
+ }
74
+ if (input.quietMs > input.quietLimitMs)
75
+ return { state: "quiet" };
76
+ return { state: "waiting" };
77
+ }
@@ -1,13 +1,14 @@
1
1
  /** File templates written by `kyb init`. Kept in sync with the proven Kyber patterns. */
2
+ import type { ModelScaffoldConfig } from "./model-reach.js";
2
3
  export declare const DEPT_DESCRIPTIONS: Record<string, string>;
3
4
  export declare function deptDescription(dept: string): string;
4
5
  export declare function identityMd(displayName: string, depts: string[]): string;
5
- export declare function subagentAgentTs(dept: string): string;
6
+ export declare function subagentAgentTs(dept: string, host: HostKind, model: ModelScaffoldConfig): string;
6
7
  export declare function subagentInstructionsMd(dept: string): string;
7
8
  export declare function subagentArcanaTs(dept: string): string;
8
9
  export declare function rootArcanaTs(): string;
9
10
  export declare function evalFileTs(displayName: string, depts: string[]): string;
10
- export declare function envExample(name: string, depts: string[], issuer: string, channelEnv?: string[], host?: "vercel" | "exe", model?: string): string;
11
+ export declare function envExample(name: string, depts: string[], issuer: string, channelEnv?: string[], host?: "vercel" | "exe", model?: ModelScaffoldConfig): string;
11
12
  export declare function evalScript(name: string, depts: string[]): string;
12
13
  export type ChannelKind = "none" | "slack" | "imessage" | "telegram" | "discord" | "web";
13
14
  export declare const CHANNEL_KINDS: ChannelKind[];
@@ -26,8 +27,8 @@ export interface ChannelPlan {
26
27
  }
27
28
  export declare function channelPlan(kind: ChannelKind, name: string, host: HostKind): ChannelPlan;
28
29
  export type HostKind = "vercel" | "exe";
29
- export declare function hostAgentTs(host: HostKind, model: string): string;
30
- export declare function hostSteps(host: HostKind, name: string): string[];
30
+ export declare function hostAgentTs(host: HostKind, model: ModelScaffoldConfig): string;
31
+ export declare function hostSteps(host: HostKind, name: string, model: ModelScaffoldConfig): string[];
31
32
  export interface EngineerPlan {
32
33
  files: Array<{
33
34
  path: string;
@@ -36,7 +37,7 @@ export interface EngineerPlan {
36
37
  deps: string[];
37
38
  steps: string[];
38
39
  }
39
- export declare function engineerPlan(host: HostKind, model: string): EngineerPlan;
40
+ export declare function engineerPlan(host: HostKind, model: ModelScaffoldConfig): EngineerPlan;
40
41
  /**
41
42
  * The eval judge, for an exe.dev host.
42
43
  *
package/dist/templates.js CHANGED
@@ -33,14 +33,14 @@ concrete — chat is not a document editor. Prefer short paragraphs and bullet
33
33
  lists.
34
34
  ${delegation}`;
35
35
  }
36
- export function subagentAgentTs(dept) {
36
+ export function subagentAgentTs(dept, host, model) {
37
+ const rendered = agentModelTemplate(host, model);
37
38
  return `import { defineAgent } from "eve";
38
-
39
+ ${rendered.imports}
39
40
  export default defineAgent({
40
41
  description:
41
42
  ${JSON.stringify(deptDescription(dept))},
42
- model: "anthropic/claude-sonnet-5",
43
- });
43
+ ${rendered.fields}});
44
44
  `;
45
45
  }
46
46
  export function subagentInstructionsMd(dept) {
@@ -164,17 +164,22 @@ ${depts.length ? ` routing: [\n${routing}\n ],\n` : ""}});
164
164
  * agent's machine.
165
165
  */
166
166
  function exeEnvBlock(name, model) {
167
- return `# Self-hosted host + model (@kybernesis/exe)
168
- # EXE_MODEL must match the LLM integration you attached to the VM. A ChatGPT
167
+ const gatewayModel = model.reach === "default"
168
+ ? `# EXE_MODEL must match the LLM integration you attached to the VM. A ChatGPT
169
169
  # subscription serves OpenAI models; the code default will fail against one.
170
- # Set it explicitly rather than relying on ${model}.
171
- EXE_MODEL="${model}"
172
- # Preview URLs are built from this. There is no default on purpose.
170
+ # Set it explicitly rather than relying on ${model.model}.
171
+ EXE_MODEL="${model.model}"
172
+ `
173
+ : `# Model reach: Claude subscription through the loopback OAuth proxy.
174
+ # EXE_MODEL is intentionally absent: this agent never calls the exe LLM gateway.
175
+ `;
176
+ return `# Self-hosted host + model (@kybernesis/exe)
177
+ ${gatewayModel}# Preview URLs are built from this. There is no default on purpose.
173
178
  EXE_VM_NAME="${name}"
174
179
 
175
180
  `;
176
181
  }
177
- export function envExample(name, depts, issuer, channelEnv = [], host = "vercel", model = "") {
182
+ export function envExample(name, depts, issuer, channelEnv = [], host = "vercel", model = { reach: "default", model: "" }) {
178
183
  const deptVars = depts
179
184
  .map((d) => {
180
185
  const upper = d.toUpperCase().replace(/[^A-Z0-9]+/g, "_");
@@ -354,31 +359,52 @@ export default discordChannel({ botToken: process.env.DISCORD_BOT_TOKEN! });
354
359
  };
355
360
  }
356
361
  }
357
- export function hostAgentTs(host, model) {
362
+ /** One renderer for every model-bearing generated agent, root or subagent. */
363
+ function agentModelTemplate(host, config) {
364
+ if (config.reach === "claude-sub") {
365
+ if (host !== "exe")
366
+ throw new Error("claude-sub model templates require an exe host");
367
+ return {
368
+ imports: `import { createAnthropic } from "@ai-sdk/anthropic";\n` +
369
+ `import { CLAUDE_SUBSCRIPTION_CONTEXT_WINDOW, claudeSubscription } from "@kybernesis/exe";\n\n`,
370
+ fields: ` model: claudeSubscription({ model: ${JSON.stringify(config.model)}, createAnthropic }),\n` +
371
+ ` modelContextWindowTokens: CLAUDE_SUBSCRIPTION_CONTEXT_WINDOW,\n`,
372
+ };
373
+ }
358
374
  if (host === "exe") {
359
- return `import { defineAgent } from "eve";
360
- import { createOpenAI } from "@ai-sdk/openai";
361
- import { exeModel } from "@kybernesis/exe";
362
-
363
- // Model served by the exe.dev LLM integration — no provider key on the host.
364
- // exe injects the credential (managed gateway, your API key, or a connected
365
- // ChatGPT subscription) server-side.
366
- export default defineAgent({
367
- model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(model)}, createOpenAI }),
368
- modelContextWindowTokens: 200_000,
369
- // This long-lived self-hosted process has no metered platform session ceiling.
370
- limits: { maxInputTokensPerSession: false },
371
- });
372
- `;
375
+ return {
376
+ imports: `import { createOpenAI } from "@ai-sdk/openai";\n` +
377
+ `import { exeModel } from "@kybernesis/exe";\n\n`,
378
+ fields: ` model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(config.model)}, createOpenAI }),\n` +
379
+ ` modelContextWindowTokens: 200_000,\n`,
380
+ };
373
381
  }
382
+ return { imports: "", fields: ` model: ${JSON.stringify(config.model)},\n` };
383
+ }
384
+ export function hostAgentTs(host, model) {
385
+ const rendered = agentModelTemplate(host, model);
386
+ const comment = model.reach === "claude-sub"
387
+ ? `// Claude subscription model through the loopback OAuth proxy — no API key or exe gateway.\n`
388
+ : host === "exe"
389
+ ? `// Model served by the exe.dev LLM integration — no provider key on the host.\n// exe injects the credential (managed gateway, your API key, or a connected\n// ChatGPT subscription) server-side.\n`
390
+ : "";
374
391
  return `import { defineAgent } from "eve";
375
-
376
- export default defineAgent({
377
- model: ${JSON.stringify(model)},
378
- });
392
+ ${rendered.imports}${comment}export default defineAgent({
393
+ ${rendered.fields}${host === "exe" ? ` // This long-lived self-hosted process has no metered platform session ceiling.\n limits: { maxInputTokensPerSession: false },\n` : ""}});
379
394
  `;
380
395
  }
381
- export function hostSteps(host, name) {
396
+ export function hostSteps(host, name, model) {
397
+ if (host === "exe" && model.reach === "claude-sub") {
398
+ return [
399
+ `Create the VM: ssh exe.dev new --name ${name}`,
400
+ `Start the loopback Claude OAuth proxy: bash scripts/claude-subscription.sh up`,
401
+ `Sign in with the client's Claude subscription: bash scripts/claude-subscription.sh login`,
402
+ `Prove the proxy is signed in and not exposed: bash scripts/claude-subscription.sh status`,
403
+ `Install Node 24 + deps on the VM, fill .env.local, then install the production service:`,
404
+ ` bash node_modules/@kybernesis/exe/scripts/install-service.sh`,
405
+ `The systemd unit exports .env.local, builds before every start, and is the single supervisor`,
406
+ ];
407
+ }
382
408
  if (host === "exe") {
383
409
  return [
384
410
  `Create the VM: ssh exe.dev new --name ${name}`,
@@ -507,32 +533,17 @@ export default defineSandbox({
507
533
  }
508
534
  export function engineerPlan(host, model) {
509
535
  const onExe = host === "exe";
536
+ const renderedModel = agentModelTemplate(host, model);
510
537
  const files = [
511
538
  {
512
539
  path: "agent/subagents/builder/agent.ts",
513
- content: onExe
514
- ? `import { defineAgent } from "eve";
515
- import { createOpenAI } from "@ai-sdk/openai";
516
- import { exeModel } from "@kybernesis/exe";
517
-
518
- // The specialist the root agent delegates BUILDING to. \`description\` is what
540
+ content: `import { defineAgent } from "eve";
541
+ ${renderedModel.imports}// The specialist the root agent delegates BUILDING to. \`description\` is what
519
542
  // the root routes on — keep it about building, not answering.
520
543
  export default defineAgent({
521
544
  description:
522
545
  "Builds and runs software: scaffolds projects, writes code, installs dependencies, runs builds and dev servers, and visually verifies rendered pages. Use when the user asks for something to be BUILT, prototyped, deployed, or fixed in code — not for questions, planning, or scheduling.",
523
- model: exeModel({ model: process.env.EXE_MODEL ?? ${JSON.stringify(model)}, createOpenAI }),
524
- modelContextWindowTokens: 200_000,
525
- });
526
- `
527
- : `import { defineAgent } from "eve";
528
-
529
- // The specialist the root agent delegates BUILDING to. \`description\` is what
530
- // the root routes on — keep it about building, not answering.
531
- export default defineAgent({
532
- description:
533
- "Builds and runs software: scaffolds projects, writes code, installs dependencies, runs builds and dev servers, and visually verifies rendered pages. Use when the user asks for something to be BUILT, prototyped, deployed, or fixed in code — not for questions, planning, or scheduling.",
534
- model: ${JSON.stringify(model)},
535
- });
546
+ ${renderedModel.fields}});
536
547
  `,
537
548
  },
538
549
  {
package/dist/upgrade.js CHANGED
@@ -1,4 +1,4 @@
1
- import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
1
+ import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { spawnSync } from "node:child_process";
3
3
  import { join } from "node:path";
4
4
  import { upsertEnv } from "./envfile.js";
@@ -237,6 +237,33 @@ function repairHostArtifacts(cwd, deps) {
237
237
  const pruneSource = join(scripts, "docker-prune.sh");
238
238
  const pruneTarget = "/etc/cron.daily/kyb-docker-prune";
239
239
  const hasDocker = capture("sh", ["-c", "command -v docker >/dev/null && echo yes"])?.trim() === "yes";
240
+ // The host start script is package-owned too. It carried a 45-second
241
+ // verdict clock that called a cold sandbox template build a failure, and no
242
+ // sweep for the orphaned template container that a killed start leaves
243
+ // behind (KYB-531). An agent still running the copy `kyb init` made keeps
244
+ // both faults until something refreshes it; this does, on every upgrade.
245
+ const serverSource = join(scripts, "eve-server.sh");
246
+ const serverTarget = join(cwd, "scripts/eve-server.sh");
247
+ if (existsSync(serverSource) && existsSync(serverTarget)) {
248
+ reconcileHostArtifact({
249
+ targetPath: serverTarget,
250
+ desiredContent: readFileSync(serverSource),
251
+ expectedMode: 0o755,
252
+ installIfMissing: false,
253
+ owner: "@kybernesis/exe/scripts/eve-server.sh",
254
+ update: () => {
255
+ try {
256
+ copyFileSync(serverSource, serverTarget);
257
+ chmodSync(serverTarget, 0o755);
258
+ return true;
259
+ }
260
+ catch {
261
+ return false;
262
+ }
263
+ },
264
+ manualCommand: `cp ${shellQuote(serverSource)} scripts/eve-server.sh && chmod +x scripts/eve-server.sh`,
265
+ });
266
+ }
240
267
  if (existsSync(pruneSource)) {
241
268
  const pruneResult = reconcileHostArtifact({
242
269
  targetPath: pruneTarget,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.6",
3
+ "version": "0.14.8",
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",
@@ -25,6 +25,18 @@ Direct provider: install `@ai-sdk/<provider>`, pass `anthropic("claude-opus-4-8"
25
25
  `defineDynamic({ fallback, events })` — prefer `session.started` scope (prompt
26
26
  caches are per model). Docs: `agent-config.md`.
27
27
 
28
+ For an exe-hosted agent that must use a client's Claude subscription instead of
29
+ the exe LLM gateway, scaffold with `--host=exe --model-reach=claude-sub` (or set
30
+ `KYB_MODEL_REACH=claude-sub` for Factory invocation; the CLI flag wins). The
31
+ certified shape is `createAnthropic` from `@ai-sdk/anthropic` passed to
32
+ `claudeSubscription({ model: "claude-opus-5", createAnthropic })`, with
33
+ `CLAUDE_SUBSCRIPTION_CONTEXT_WINDOW`, both from `@kybernesis/exe`. Use the bare
34
+ Anthropic id, never `anthropic/...`; do not reuse that context-window constant
35
+ for another model. The generated agent does not use `exeModel` or `EXE_MODEL`.
36
+ Run `scripts/claude-subscription.sh up`, `login`, then `status` on the host; the
37
+ OAuth proxy must stay loopback-only. Provider details:
38
+ `packages/exe/src/claude.ts` and `packages/exe/README.md`.
39
+
28
40
  ## Channels (agent/channels/, one file per surface)
29
41
 
30
42
  Filename = channel id. eve normalizes every surface into one runtime — tools/