@kybernesis/create 0.14.6 → 0.14.7

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/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
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import { bold, capture, dim, green, parseEnv, red, yellow } from "./util.js";
5
5
  import { diagnoseManageRestart, findMatchingAgentServiceUnit } from "./systemd.js";
6
6
  import { inspectEveAgent } from "./agent-limits.js";
7
+ import { classifyModelReach } from "./model-reach.js";
7
8
  const MARK = {
8
9
  pass: green("✓"),
9
10
  warn: yellow("!"),
@@ -31,6 +32,24 @@ export function dockerTemplateDoctorChecks(result) {
31
32
  detail: issue.detail,
32
33
  }));
33
34
  }
35
+ export function modelReachDoctorCheck(authoredSource, compiledRouting) {
36
+ const reach = classifyModelReach(authoredSource, compiledRouting);
37
+ switch (reach.kind) {
38
+ case "claude-sub":
39
+ return { verdict: "pass", label: "Model reach: claude-sub (Claude subscription)" };
40
+ case "exe":
41
+ return { verdict: "pass", label: "Model reach: exe (host LLM integration)" };
42
+ case "gateway":
43
+ return { verdict: "pass", label: "Model reach: gateway" };
44
+ case "direct-provider":
45
+ return {
46
+ verdict: "pass",
47
+ label: `Model reach: direct-provider${reach.provider ? ` (${reach.provider})` : ""}`,
48
+ };
49
+ case "unresolved":
50
+ return { verdict: "warn", label: "Model reach: unresolved", detail: reach.reason };
51
+ }
52
+ }
34
53
  export function agentInputLimitDoctorCheck(limit) {
35
54
  switch (limit.kind) {
36
55
  case "explicit-numeric":
@@ -502,9 +521,16 @@ export async function doctor() {
502
521
  * `env` here is the same merged view every other check reads: .env.local
503
522
  * underneath, the real environment on top.
504
523
  */
524
+ const rootAgentPath = join(cwd, "agent/agent.ts");
525
+ const rootAgentSource = existsSync(rootAgentPath) ? readFileSync(rootAgentPath, "utf8") : null;
505
526
  const info = inspectEveAgent(cwd, env);
506
527
  if (info === null) {
507
528
  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");
529
+ const reachCheck = modelReachDoctorCheck(rootAgentSource, {
530
+ kind: "unresolved",
531
+ reason: "eve info failed before compiled model routing could be inspected",
532
+ });
533
+ add(reachCheck.verdict, reachCheck.label, reachCheck.detail);
508
534
  add("warn", "Eve max input tokens/session: unresolved", "eve info failed before the compiled root policy could be inspected");
509
535
  }
510
536
  else {
@@ -513,6 +539,8 @@ export async function doctor() {
513
539
  add("pass", `eve discovery clean (${diag.warnings} warnings)`);
514
540
  else
515
541
  add("fail", `eve discovery: ${diag ? `${diag.errors} errors` : "unparsed"}`, "npx eve info --json");
542
+ const reachCheck = modelReachDoctorCheck(rootAgentSource, info.modelRouting);
543
+ add(reachCheck.verdict, reachCheck.label, reachCheck.detail);
516
544
  const limitCheck = agentInputLimitDoctorCheck(info.limit);
517
545
  add(limitCheck.verdict, limitCheck.label, limitCheck.detail);
518
546
  }
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
+ }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kybernesis/create",
3
- "version": "0.14.6",
3
+ "version": "0.14.7",
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/