kairn-cli 1.9.1 → 1.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -4,7 +4,7 @@ import chalk14 from "chalk";
4
4
 
5
5
  // src/commands/init.ts
6
6
  import { Command } from "commander";
7
- import { password, select } from "@inquirer/prompts";
7
+ import { input, password, select } from "@inquirer/prompts";
8
8
  import chalk3 from "chalk";
9
9
  import Anthropic from "@anthropic-ai/sdk";
10
10
  import OpenAI from "openai";
@@ -62,6 +62,114 @@ async function saveConfig(config) {
62
62
  await fs.writeFile(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
63
63
  }
64
64
 
65
+ // src/providers.ts
66
+ var PROVIDER_CONFIGS = {
67
+ anthropic: {
68
+ name: "Anthropic",
69
+ verifyModel: "claude-haiku-4-5-20251001",
70
+ cheapModel: "claude-haiku-4-5-20251001"
71
+ },
72
+ openai: {
73
+ name: "OpenAI",
74
+ verifyModel: "gpt-4.1-nano",
75
+ cheapModel: "gpt-4.1-nano"
76
+ },
77
+ google: {
78
+ name: "Google Gemini",
79
+ baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/",
80
+ verifyModel: "gemini-2.5-flash",
81
+ cheapModel: "gemini-2.5-flash"
82
+ },
83
+ xai: {
84
+ name: "xAI (Grok)",
85
+ baseURL: "https://api.x.ai/v1",
86
+ verifyModel: "grok-4-1-fast-non-reasoning",
87
+ cheapModel: "grok-4-1-fast-non-reasoning"
88
+ },
89
+ deepseek: {
90
+ name: "DeepSeek",
91
+ baseURL: "https://api.deepseek.com",
92
+ verifyModel: "deepseek-chat",
93
+ cheapModel: "deepseek-chat"
94
+ },
95
+ mistral: {
96
+ name: "Mistral",
97
+ baseURL: "https://api.mistral.ai/v1",
98
+ verifyModel: "mistral-small-latest",
99
+ cheapModel: "mistral-small-latest"
100
+ },
101
+ groq: {
102
+ name: "Groq (open-source models)",
103
+ baseURL: "https://api.groq.com/openai/v1",
104
+ verifyModel: "meta-llama/llama-4-scout-17b-16e-instruct",
105
+ cheapModel: "meta-llama/llama-4-scout-17b-16e-instruct"
106
+ }
107
+ };
108
+ var PROVIDER_MODELS = {
109
+ anthropic: [
110
+ { name: "Claude Sonnet 4.6 (recommended)", value: "claude-sonnet-4-6" },
111
+ { name: "Claude Opus 4.6 (highest quality)", value: "claude-opus-4-6" },
112
+ { name: "Claude Haiku 4.5 (fastest, cheapest)", value: "claude-haiku-4-5-20251001" }
113
+ ],
114
+ openai: [
115
+ { name: "GPT-4.1 (recommended \u2014 smartest non-reasoning)", value: "gpt-4.1" },
116
+ { name: "GPT-4.1 mini (faster, cheaper)", value: "gpt-4.1-mini" },
117
+ { name: "o4-mini (reasoning, cost-efficient)", value: "o4-mini" },
118
+ { name: "GPT-5 mini (frontier)", value: "gpt-5-mini" }
119
+ ],
120
+ google: [
121
+ { name: "Gemini 2.5 Flash (recommended \u2014 best value)", value: "gemini-2.5-flash" },
122
+ { name: "Gemini 3 Flash (newest frontier)", value: "gemini-3-flash" },
123
+ { name: "Gemini 2.5 Pro (highest quality)", value: "gemini-2.5-pro" },
124
+ { name: "Gemini 3.1 Pro Preview (most advanced)", value: "gemini-3.1-pro-preview" }
125
+ ],
126
+ xai: [
127
+ { name: "Grok 4.1 Fast (recommended \u2014 $0.20/M, very fast)", value: "grok-4-1-fast-non-reasoning" },
128
+ { name: "Grok 4.20 (frontier quality, 2M context)", value: "grok-4.20-0309-non-reasoning" }
129
+ ],
130
+ deepseek: [
131
+ { name: "DeepSeek V3.2 Chat (recommended \u2014 cheapest good model)", value: "deepseek-chat" },
132
+ { name: "DeepSeek V3.2 Reasoner (with chain-of-thought)", value: "deepseek-reasoner" }
133
+ ],
134
+ mistral: [
135
+ { name: "Mistral Large 3 (recommended \u2014 open-weight flagship)", value: "mistral-large-latest" },
136
+ { name: "Codestral (code-optimized, 256K context)", value: "codestral-latest" },
137
+ { name: "Mistral Small 4 (cheapest)", value: "mistral-small-latest" }
138
+ ],
139
+ groq: [
140
+ { name: "Llama 4 Maverick (recommended \u2014 free, fast)", value: "meta-llama/llama-4-maverick-17b-128e-instruct" },
141
+ { name: "Llama 4 Scout (free, fast)", value: "meta-llama/llama-4-scout-17b-16e-instruct" },
142
+ { name: "DeepSeek R1 70B (free reasoning)", value: "deepseek-r1-distill-llama-70b" },
143
+ { name: "Qwen 3 32B (free, multilingual)", value: "qwen/qwen3-32b" }
144
+ ]
145
+ };
146
+ var PROVIDER_CHOICES = [
147
+ { name: "Anthropic (Claude) \u2014 recommended", value: "anthropic" },
148
+ { name: "OpenAI (GPT)", value: "openai" },
149
+ { name: "Google (Gemini)", value: "google" },
150
+ { name: "xAI (Grok)", value: "xai" },
151
+ { name: "DeepSeek \u2014 cheapest", value: "deepseek" },
152
+ { name: "Mistral \u2014 open-weight", value: "mistral" },
153
+ { name: "Groq \u2014 free tier, open-source models", value: "groq" },
154
+ { name: "Other (OpenAI-compatible endpoint)", value: "other" }
155
+ ];
156
+ function getProviderName(provider) {
157
+ if (provider === "other") return "Custom endpoint";
158
+ return PROVIDER_CONFIGS[provider].name;
159
+ }
160
+ function getBaseURL(provider, customBaseURL) {
161
+ if (provider === "other") return customBaseURL;
162
+ return PROVIDER_CONFIGS[provider]?.baseURL;
163
+ }
164
+ function getCheapModel(provider, fallbackModel) {
165
+ if (provider === "other") return fallbackModel;
166
+ return PROVIDER_CONFIGS[provider].cheapModel;
167
+ }
168
+ function getVerifyModel(provider, fallbackModel) {
169
+ if (provider === "other") return fallbackModel;
170
+ return PROVIDER_CONFIGS[provider].verifyModel;
171
+ }
172
+
65
173
  // src/ui.ts
66
174
  import chalk from "chalk";
67
175
  var maroon = chalk.rgb(139, 0, 0);
@@ -234,62 +342,28 @@ async function installSeedTemplates() {
234
342
  console.log(ui.success(`${installed} template${installed === 1 ? "" : "s"} installed`));
235
343
  }
236
344
  }
237
- var PROVIDER_MODELS = {
238
- anthropic: {
239
- name: "Anthropic",
240
- models: [
241
- { name: "Claude Sonnet 4.6 (recommended \u2014 fast, smart)", value: "claude-sonnet-4-6" },
242
- { name: "Claude Opus 4.6 (highest quality)", value: "claude-opus-4-6" },
243
- { name: "Claude Haiku 4.5 (fastest, cheapest)", value: "claude-haiku-4-5-20251001" }
244
- ]
245
- },
246
- openai: {
247
- name: "OpenAI",
248
- models: [
249
- { name: "GPT-4o (recommended)", value: "gpt-4o" },
250
- { name: "GPT-4o mini (faster, cheaper)", value: "gpt-4o-mini" },
251
- { name: "o3 (reasoning)", value: "o3" }
252
- ]
253
- },
254
- google: {
255
- name: "Google Gemini",
256
- models: [
257
- { name: "Gemini 2.5 Flash (recommended)", value: "gemini-2.5-flash-preview-05-20" },
258
- { name: "Gemini 2.5 Pro (highest quality)", value: "gemini-2.5-pro-preview-05-06" }
259
- ]
260
- }
261
- };
262
- async function verifyKey(provider, apiKey, model) {
345
+ async function verifyKey(provider, apiKey, baseURL, model) {
263
346
  try {
264
347
  if (provider === "anthropic") {
265
- const client = new Anthropic({ apiKey });
266
- await client.messages.create({
267
- model: "claude-haiku-4-5-20251001",
268
- max_tokens: 10,
269
- messages: [{ role: "user", content: "ping" }]
270
- });
271
- return true;
272
- } else if (provider === "openai") {
273
- const client = new OpenAI({ apiKey });
274
- await client.chat.completions.create({
275
- model: "gpt-4o-mini",
276
- max_tokens: 10,
277
- messages: [{ role: "user", content: "ping" }]
278
- });
279
- return true;
280
- } else if (provider === "google") {
281
- const client = new OpenAI({
282
- apiKey,
283
- baseURL: "https://generativelanguage.googleapis.com/v1beta/openai/"
284
- });
285
- await client.chat.completions.create({
286
- model: "gemini-2.5-flash-preview-05-20",
348
+ const client2 = new Anthropic({ apiKey });
349
+ await client2.messages.create({
350
+ model: getVerifyModel(provider, model || "claude-haiku-4-5-20251001"),
287
351
  max_tokens: 10,
288
352
  messages: [{ role: "user", content: "ping" }]
289
353
  });
290
354
  return true;
291
355
  }
292
- return false;
356
+ const verifyModel = provider === "other" ? model || "test" : getVerifyModel(provider, model || "");
357
+ const resolvedBaseURL = getBaseURL(provider, baseURL);
358
+ const clientOptions = { apiKey };
359
+ if (resolvedBaseURL) clientOptions.baseURL = resolvedBaseURL;
360
+ const client = new OpenAI(clientOptions);
361
+ await client.chat.completions.create({
362
+ model: verifyModel,
363
+ max_tokens: 10,
364
+ messages: [{ role: "user", content: "ping" }]
365
+ });
366
+ return true;
293
367
  } catch {
294
368
  return false;
295
369
  }
@@ -311,42 +385,52 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
311
385
  }
312
386
  const provider = await select({
313
387
  message: "LLM provider",
314
- choices: [
315
- { name: "Anthropic (Claude) \u2014 recommended", value: "anthropic" },
316
- { name: "OpenAI (GPT)", value: "openai" },
317
- { name: "Google (Gemini)", value: "google" }
318
- ]
319
- });
320
- const providerInfo = PROVIDER_MODELS[provider];
321
- const model = await select({
322
- message: "Compilation model",
323
- choices: providerInfo.models
388
+ choices: PROVIDER_CHOICES
324
389
  });
390
+ let model;
391
+ let baseURL;
392
+ let providerDisplayName;
393
+ if (provider === "other") {
394
+ providerDisplayName = "Custom endpoint";
395
+ baseURL = await input({ message: "Base URL" });
396
+ model = await input({ message: "Model name" });
397
+ } else {
398
+ providerDisplayName = getProviderName(provider);
399
+ model = await select({
400
+ message: "Compilation model",
401
+ choices: PROVIDER_MODELS[provider]
402
+ });
403
+ }
325
404
  const apiKey = await password({
326
- message: `${providerInfo.name} API key`,
405
+ message: `${providerDisplayName} API key${provider === "other" ? " (Enter to skip)" : ""}`,
327
406
  mask: "*"
328
407
  });
329
- if (!apiKey) {
408
+ if (!apiKey && provider !== "other") {
330
409
  console.log(ui.error("No API key provided. Aborting."));
331
410
  process.exit(1);
332
411
  }
333
- console.log(chalk3.dim("\n Verifying API key..."));
334
- const valid = await verifyKey(provider, apiKey, model);
335
- if (!valid) {
336
- console.log(ui.error("Invalid API key. Check your key and try again."));
337
- process.exit(1);
412
+ if (apiKey) {
413
+ console.log(chalk3.dim("\n Verifying API key..."));
414
+ const valid = await verifyKey(provider, apiKey, baseURL, model);
415
+ if (!valid) {
416
+ console.log(ui.error("Invalid API key. Check your key and try again."));
417
+ process.exit(1);
418
+ }
419
+ console.log(ui.success("API key verified"));
420
+ } else {
421
+ console.log(ui.warn("No API key \u2014 skipping verification"));
338
422
  }
339
- console.log(ui.success("API key verified"));
340
423
  const config = {
341
424
  provider,
342
- api_key: apiKey,
425
+ api_key: apiKey || "",
343
426
  model,
427
+ ...baseURL ? { base_url: baseURL } : {},
344
428
  default_runtime: "claude-code",
345
429
  created_at: (/* @__PURE__ */ new Date()).toISOString()
346
430
  };
347
431
  await saveConfig(config);
348
432
  console.log(ui.success(`Config saved to ${chalk3.dim(getConfigPath())}`));
349
- console.log(ui.kv("Provider", providerInfo.name));
433
+ console.log(ui.kv("Provider", providerDisplayName));
350
434
  console.log(ui.kv("Model", model));
351
435
  await installSeedTemplates();
352
436
  const hasClaude = detectClaudeCode();
@@ -364,7 +448,7 @@ var initCommand = new Command("init").description("Set up Kairn with your API ke
364
448
 
365
449
  // src/commands/describe.ts
366
450
  import { Command as Command2 } from "commander";
367
- import { input, confirm, select as select2 } from "@inquirer/prompts";
451
+ import { input as input2, confirm, select as select2 } from "@inquirer/prompts";
368
452
  import chalk5 from "chalk";
369
453
  import ora from "ora";
370
454
 
@@ -376,17 +460,72 @@ import Anthropic2 from "@anthropic-ai/sdk";
376
460
  import OpenAI2 from "openai";
377
461
 
378
462
  // src/compiler/prompt.ts
379
- var SYSTEM_PROMPT = `You are the Kairn environment compiler. Your job is to generate a minimal, optimal Claude Code agent environment from a user's natural language description of what they want their agent to do.
463
+ var SKELETON_PROMPT = `You are the Kairn skeleton compiler. Your job is to select tools and outline the project structure from a user's natural language description.
380
464
 
381
465
  You will receive:
382
466
  1. The user's intent (what they want to build/do)
383
467
  2. A tool registry (available MCP servers, plugins, and hooks)
384
468
 
385
- You must output a JSON object matching the EnvironmentSpec schema.
469
+ You must output a JSON object matching the SkeletonSpec schema.
386
470
 
387
471
  ## Core Principles
388
472
 
389
473
  - **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
474
+ - **Workflow-specific, not generic.** Select tools that directly support the user's actual workflow.
475
+ - **Security by default.** Essential for all projects.
476
+
477
+ ## Tool Selection Rules
478
+
479
+ - Only select tools directly relevant to the described workflow
480
+ - Prefer free tools (auth: "none") when quality is comparable
481
+ - Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
482
+ - For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
483
+ - Maximum 6-8 MCP servers to avoid context bloat
484
+ - Include a \`reason\` for each selected tool explaining why it fits this workflow
485
+
486
+ ## Context Budget (STRICT)
487
+
488
+ - MCP servers: maximum 6. Prefer fewer.
489
+ - Skills: maximum 3. Only include directly relevant ones.
490
+ - Agents: maximum 3. QA pipeline + one specialist.
491
+ - Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
492
+
493
+ If the workflow doesn't clearly need a tool, DO NOT include it.
494
+ Each MCP server costs 500-2000 tokens of context window.
495
+
496
+ ## Output Schema
497
+
498
+ Return ONLY valid JSON matching this structure:
499
+
500
+ \`\`\`json
501
+ {
502
+ "name": "short-kebab-case-name",
503
+ "description": "One-line description",
504
+ "tools": [
505
+ { "tool_id": "id-from-registry", "reason": "why this tool fits" }
506
+ ],
507
+ "outline": {
508
+ "tech_stack": ["Python", "pandas"],
509
+ "workflow_type": "data-analysis",
510
+ "key_commands": ["ingest", "analyze", "report"],
511
+ "custom_rules": ["data-integrity"],
512
+ "custom_agents": ["data-reviewer"],
513
+ "custom_skills": ["ms-data-analysis"]
514
+ }
515
+ }
516
+ \`\`\`
517
+
518
+ Return ONLY valid JSON. No markdown fences. No text outside the JSON.`;
519
+ var HARNESS_PROMPT = `You are the Kairn harness compiler. Your job is to generate the full environment content from a project skeleton.
520
+
521
+ You will receive:
522
+ 1. The skeleton (tool selections + project outline)
523
+ 2. The user's original intent
524
+
525
+ You must generate all harness content: CLAUDE.md, commands, rules, agents, skills, and docs.
526
+
527
+ ## Core Principles
528
+
390
529
  - **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
391
530
  - **Concise CLAUDE.md.** Under 120 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
392
531
  - **Security by default.** Always include deny rules for destructive commands and secret file access.
@@ -572,28 +711,6 @@ All projects should include a PostCompact hook to restore context after compacti
572
711
 
573
712
  Merge this into the settings hooks alongside the PreToolUse and PostToolUse hooks.
574
713
 
575
- ## Tool Selection Rules
576
-
577
- - Only select tools directly relevant to the described workflow
578
- - Prefer free tools (auth: "none") when quality is comparable
579
- - Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
580
- - For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
581
- - Maximum 6-8 MCP servers to avoid context bloat
582
- - Include a \`reason\` for each selected tool explaining why it fits this workflow
583
-
584
- ## Context Budget (STRICT)
585
-
586
- - MCP servers: maximum 6. Prefer fewer.
587
- - CLAUDE.md: maximum 120 lines.
588
- - Rules: maximum 5 files, each under 20 lines.
589
- - Skills: maximum 3. Only include directly relevant ones.
590
- - Agents: maximum 3. QA pipeline + one specialist.
591
- - Commands: no limit (loaded on demand, zero context cost).
592
- - Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
593
-
594
- If the workflow doesn't clearly need a tool, DO NOT include it.
595
- Each MCP server costs 500-2000 tokens of context window.
596
-
597
714
  ## For Code Projects, Additionally Include
598
715
 
599
716
  - \`/project:plan\` command (plan before coding)
@@ -657,6 +774,133 @@ If no autonomy level is specified, assume Level 1 (Guided).
657
774
 
658
775
  Return ONLY valid JSON matching this structure:
659
776
 
777
+ \`\`\`json
778
+ {
779
+ "claude_md": "Full CLAUDE.md content (under 120 lines)",
780
+ "commands": { "help": "...", "tasks": "...", "status": "...", "fix": "...", "sprint": "...", "spec": "...", "prove": "...", "grill": "...", "reset": "..." },
781
+ "rules": { "continuity": "...", "security": "..." },
782
+ "agents": { "qa-orchestrator": "...", "linter": "...", "e2e-tester": "..." },
783
+ "skills": { "skill-name/SKILL": "..." },
784
+ "docs": { "TODO": "...", "DECISIONS": "...", "LEARNINGS": "...", "SPRINT": "..." }
785
+ }
786
+ \`\`\`
787
+
788
+ Return ONLY valid JSON. No markdown fences. No text outside the JSON.`;
789
+ var SYSTEM_PROMPT = `You are the Kairn environment compiler. Your job is to generate a minimal, optimal Claude Code agent environment from a user's natural language description of what they want their agent to do.
790
+
791
+ You will receive:
792
+ 1. The user's intent (what they want to build/do)
793
+ 2. A tool registry (available MCP servers, plugins, and hooks)
794
+
795
+ You must output a JSON object matching the EnvironmentSpec schema.
796
+
797
+ ## Core Principles
798
+
799
+ - **Minimalism over completeness.** Fewer, well-chosen tools beat many generic ones. Each MCP server costs 500-2000 context tokens.
800
+ - **Workflow-specific, not generic.** Every instruction, command, and rule must relate to the user's actual workflow.
801
+ - **Concise CLAUDE.md.** Under 120 lines. No generic text like "be helpful." Include build/test commands, reference docs/ and skills/.
802
+ - **Security by default.** Always include deny rules for destructive commands and secret file access.
803
+
804
+ ## CLAUDE.md Template (mandatory structure)
805
+
806
+ The \`claude_md\` field MUST follow this exact structure (max 120 lines):
807
+
808
+ \`\`\`
809
+ # {Project Name}
810
+
811
+ ## Purpose
812
+ {one-line description}
813
+
814
+ ## Tech Stack
815
+ {bullet list of frameworks/languages}
816
+
817
+ ## Commands
818
+ {concrete build/test/lint/dev commands}
819
+
820
+ ## Architecture
821
+ {brief folder structure, max 10 lines}
822
+
823
+ ## Conventions
824
+ {3-5 specific coding rules}
825
+
826
+ ## Key Commands
827
+ {list /project: commands with descriptions}
828
+
829
+ ## Output
830
+ {where results go, key files}
831
+
832
+ ## Verification
833
+ After implementing any change, verify it works:
834
+ - {build command} \u2014 must pass with no errors
835
+ - {test command} \u2014 all tests must pass
836
+ - {lint command} \u2014 no warnings or errors
837
+ - {type check command} \u2014 no type errors
838
+
839
+ If any verification step fails, fix the issue before moving on.
840
+ Do NOT skip verification steps.
841
+
842
+ ## Known Gotchas
843
+ <!-- After any correction, add it here: "Update CLAUDE.md so you don't make that mistake again." -->
844
+ <!-- Prune this section when it exceeds 10 items \u2014 keep only the recurring ones. -->
845
+ - (none yet \u2014 this section grows as you work)
846
+
847
+ ## Debugging
848
+ When debugging, paste raw error output. Don't summarize \u2014 Claude works better with raw data.
849
+ Use subagents for deep investigation to keep main context clean.
850
+
851
+ ## Git Workflow
852
+ - Prefer small, focused commits (one feature or fix per commit)
853
+ - Use conventional commits: feat:, fix:, docs:, refactor:, test:
854
+ - Target < 200 lines per PR when possible
855
+ \`\`\`
856
+
857
+ Do not add generic filler. Every line must be specific to the user's workflow.
858
+
859
+ ## What You Must Always Include
860
+
861
+ 1. A concise, workflow-specific \`claude_md\` (the CLAUDE.md content)
862
+ 2. A \`/project:help\` command that explains the environment
863
+ 3. A \`/project:tasks\` command for task management via TODO.md
864
+ 4. A \`docs/TODO.md\` file for continuity
865
+ 5. A \`docs/DECISIONS.md\` file for architectural decisions
866
+ 6. A \`docs/LEARNINGS.md\` file for non-obvious discoveries
867
+ 7. A \`rules/continuity.md\` rule encouraging updates to DECISIONS.md and LEARNINGS.md
868
+ 8. A \`rules/security.md\` rule with essential security instructions
869
+ 9. settings.json with deny rules for \`rm -rf\`, \`curl|sh\`, reading \`.env\` and \`secrets/\`
870
+ 10. A \`/project:status\` command for code projects (uses ! for live git/test output)
871
+ 11. A \`/project:fix\` command for code projects (uses $ARGUMENTS for issue number)
872
+ 12. A \`docs/SPRINT.md\` file for sprint contracts (acceptance criteria, verification steps)
873
+ 13. A "Verification" section in CLAUDE.md with concrete verify commands for the project
874
+ 14. A "Known Gotchas" section in CLAUDE.md (starts empty, grows with corrections)
875
+ 15. A "Debugging" section in CLAUDE.md (2 lines: paste raw errors, use subagents)
876
+ 16. A "Git Workflow" section in CLAUDE.md (3 rules: small commits, conventional format, <200 lines PR)
877
+
878
+ ## Tool Selection Rules
879
+
880
+ - Only select tools directly relevant to the described workflow
881
+ - Prefer free tools (auth: "none") when quality is comparable
882
+ - Tier 1 tools (Context7, Sequential Thinking, security-guidance) should be included in most environments
883
+ - For tools requiring API keys (auth: "api_key"), use \${ENV_VAR} syntax \u2014 never hardcode keys
884
+ - Maximum 6-8 MCP servers to avoid context bloat
885
+ - Include a \`reason\` for each selected tool explaining why it fits this workflow
886
+
887
+ ## Context Budget (STRICT)
888
+
889
+ - MCP servers: maximum 6. Prefer fewer.
890
+ - CLAUDE.md: maximum 120 lines.
891
+ - Rules: maximum 5 files, each under 20 lines.
892
+ - Skills: maximum 3. Only include directly relevant ones.
893
+ - Agents: maximum 3. QA pipeline + one specialist.
894
+ - Commands: no limit (loaded on demand, zero context cost).
895
+ - Hooks: maximum 4 (auto-format, block-destructive, PostCompact, plus one contextual).
896
+
897
+ If the workflow doesn't clearly need a tool, DO NOT include it.
898
+ Each MCP server costs 500-2000 tokens of context window.
899
+
900
+ ## Output Schema
901
+
902
+ Return ONLY valid JSON matching this structure:
903
+
660
904
  \`\`\`json
661
905
  {
662
906
  "name": "short-kebab-case-name",
@@ -677,14 +921,7 @@ Return ONLY valid JSON matching this structure:
677
921
  },
678
922
  "commands": {
679
923
  "help": "markdown content for /project:help",
680
- "tasks": "markdown content for /project:tasks",
681
- "status": "Show project status:\\n\\n!git status --short\\n\\n!git log --oneline -5\\n\\nRead TODO.md and summarize progress.",
682
- "fix": "Fix issue #$ARGUMENTS:\\n\\n1. Read the issue and understand the problem\\n2. Plan the fix\\n3. Implement the fix\\n4. Run tests:\\n\\n!npm test 2>&1 | tail -20\\n\\n5. Commit with: fix: resolve #$ARGUMENTS",
683
- "sprint": "Define a sprint contract for the next feature:\\n\\n1. Read docs/TODO.md for context:\\n\\n!cat docs/TODO.md 2>/dev/null\\n\\n2. Write a CONTRACT to docs/SPRINT.md with: feature name, acceptance criteria, verification steps, files to modify, scope estimate.\\n3. Do NOT start coding until contract is confirmed.",
684
- "spec": "Before building this feature, interview me to create a complete spec.\\n\\nAsk me 5-8 questions, one at a time:\\n1. What specifically should this feature do?\\n2. Who uses it and how?\\n3. What are the edge cases or error states?\\n4. How will we know it works? (acceptance criteria)\\n5. What should it explicitly NOT do? (scope boundaries)\\n6. Any dependencies, APIs, or constraints?\\n7. How does it fit with existing code?\\n8. Priority: speed, quality, or flexibility?\\n\\nAfter my answers, write a structured spec to docs/SPRINT.md:\\n- Feature name\\n- Description (from my answers, not invented)\\n- Acceptance criteria (testable)\\n- Out of scope\\n- Technical approach\\n\\nDo NOT start coding until I confirm the spec.",
685
- "prove": "Prove the current implementation works.\\n\\n1. Run the full test suite:\\n\\n!npm test 2>&1\\n\\n2. Compare against main:\\n\\n!git diff main --stat 2>/dev/null\\n\\n3. Show evidence:\\n - Test results (pass/fail counts)\\n - Behavioral diff (main vs this branch)\\n - Edge cases tested\\n - Error handling verified\\n\\n4. Rate confidence:\\n - HIGH: All tests pass, edge cases covered, no regressions\\n - MEDIUM: Core works, some edges untested\\n - LOW: Needs more verification\\n\\nIf LOW or MEDIUM, explain what's missing and fix it.",
686
- "grill": "Review the current changes adversarially.\\n\\n!git diff --staged 2>/dev/null || git diff HEAD 2>/dev/null\\n\\nAct as a senior engineer. For each file changed:\\n\\n1. \\"Why this approach over X?\\"\\n2. \\"What happens if Y input?\\"\\n3. \\"Performance impact of Z?\\"\\n4. \\"This could break if...\\"\\n\\nFor each concern:\\n- Severity: BLOCKER / SHOULD-FIX / NITPICK\\n- The exact scenario that could fail\\n- A suggested alternative\\n\\nDo NOT approve until all BLOCKERs are resolved.",
687
- "reset": "Stop. Read docs/DECISIONS.md and docs/LEARNINGS.md.\\n\\nConsidering everything we've learned:\\n1. What was the original approach?\\n2. What went wrong or feels inelegant?\\n3. What would the clean solution look like?\\n\\nPropose the new approach. Do NOT implement yet.\\nIf I approve, stash current changes:\\n git stash -m \\"pre-reset: $(date +%Y%m%d-%H%M)\\"\\n\\nThen implement the elegant solution."
924
+ "tasks": "markdown content for /project:tasks"
688
925
  },
689
926
  "rules": {
690
927
  "continuity": "markdown content for continuity rule",
@@ -694,15 +931,13 @@ Return ONLY valid JSON matching this structure:
694
931
  "skill-name/SKILL": "markdown content with YAML frontmatter"
695
932
  },
696
933
  "agents": {
697
- "qa-orchestrator": "---\\nname: qa-orchestrator\\ndescription: Orchestrates QA pipeline\\nmodel: sonnet\\n---\\nRun QA: delegate to @linter for static analysis, @e2e-tester for browser tests. Compile consolidated report.",
698
- "linter": "---\\nname: linter\\ndescription: Fast static analysis\\nmodel: haiku\\n---\\nRun available linters (eslint, prettier, biome, ruff, mypy, semgrep). Report issues.",
699
- "e2e-tester": "---\\nname: e2e-tester\\ndescription: Browser-based QA via Playwright\\nmodel: sonnet\\n---\\nTest user flows via Playwright. Verify behavior, not just DOM. Screenshot failures."
934
+ "qa-orchestrator": "agent markdown with YAML frontmatter"
700
935
  },
701
936
  "docs": {
702
- "TODO": "# TODO\\n\\n- [ ] First task based on workflow",
703
- "DECISIONS": "# Decisions\\n\\nArchitectural decisions for this project.",
704
- "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries and gotchas.",
705
- "SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria before starting work."
937
+ "TODO": "# TODO\\n\\n- [ ] First task",
938
+ "DECISIONS": "# Decisions\\n\\nArchitectural decisions.",
939
+ "LEARNINGS": "# Learnings\\n\\nNon-obvious discoveries.",
940
+ "SPRINT": "# Sprint Contract\\n\\nDefine acceptance criteria."
706
941
  }
707
942
  }
708
943
  }
@@ -780,7 +1015,7 @@ async function loadRegistry() {
780
1015
  }
781
1016
 
782
1017
  // src/compiler/compile.ts
783
- function buildUserMessage(intent, registry) {
1018
+ function buildSkeletonMessage(intent, registry) {
784
1019
  const registrySummary = registry.map(
785
1020
  (t) => `- ${t.id} (${t.type}, tier ${t.tier}, auth: ${t.auth}): ${t.description} [best_for: ${t.best_for.join(", ")}]`
786
1021
  ).join("\n");
@@ -792,25 +1027,60 @@ ${intent}
792
1027
 
793
1028
  ${registrySummary}
794
1029
 
795
- Generate the EnvironmentSpec JSON now.`;
1030
+ Generate the skeleton JSON now.`;
1031
+ }
1032
+ function buildHarnessMessage(intent, skeleton, concise) {
1033
+ const skeletonJson = JSON.stringify(skeleton, null, 2);
1034
+ const conciseNote = concise ? "\n\nIMPORTANT: Be concise. Maximum 80 lines for claude_md. Maximum 5 commands. Keep all content brief." : "";
1035
+ return `## User Intent
1036
+
1037
+ ${intent}
1038
+
1039
+ ## Project Skeleton
1040
+
1041
+ ${skeletonJson}
1042
+
1043
+ Generate the harness content JSON now.${conciseNote}`;
796
1044
  }
797
- function parseSpecResponse(text) {
1045
+ function parseSkeletonResponse(text) {
798
1046
  let cleaned = text.trim();
799
1047
  if (cleaned.startsWith("```")) {
800
1048
  cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
801
1049
  }
802
1050
  const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
803
1051
  if (!jsonMatch) {
1052
+ throw new Error("Pass 1 (skeleton) did not return valid JSON.");
1053
+ }
1054
+ try {
1055
+ const parsed = JSON.parse(jsonMatch[0]);
1056
+ if (!parsed.name || !parsed.tools || !Array.isArray(parsed.tools)) {
1057
+ throw new Error("Skeleton missing required fields: name, tools");
1058
+ }
1059
+ return parsed;
1060
+ } catch (err) {
804
1061
  throw new Error(
805
- "LLM response did not contain valid JSON. Try again or use a different model."
1062
+ `Failed to parse skeleton JSON: ${err instanceof Error ? err.message : String(err)}`
806
1063
  );
807
1064
  }
1065
+ }
1066
+ function parseHarnessResponse(text) {
1067
+ let cleaned = text.trim();
1068
+ if (cleaned.startsWith("```")) {
1069
+ cleaned = cleaned.replace(/^```(?:json)?\n?/, "").replace(/\n?```$/, "");
1070
+ }
1071
+ const jsonMatch = cleaned.match(/\{[\s\S]*\}/);
1072
+ if (!jsonMatch) {
1073
+ throw new Error("Pass 2 (harness) did not return valid JSON.");
1074
+ }
808
1075
  try {
809
- return JSON.parse(jsonMatch[0]);
1076
+ const parsed = JSON.parse(jsonMatch[0]);
1077
+ if (!parsed.claude_md || !parsed.commands) {
1078
+ throw new Error("Harness missing required fields: claude_md, commands");
1079
+ }
1080
+ return parsed;
810
1081
  } catch (err) {
811
1082
  throw new Error(
812
- `Failed to parse LLM response as JSON: ${err instanceof Error ? err.message : String(err)}
813
- Response started with: ${cleaned.slice(0, 200)}...`
1083
+ `Failed to parse harness JSON: ${err instanceof Error ? err.message : String(err)}`
814
1084
  );
815
1085
  }
816
1086
  }
@@ -844,14 +1114,17 @@ function classifyError(err, provider) {
844
1114
  }
845
1115
  return `${provider} API error: ${msg}`;
846
1116
  }
847
- async function callLLM(config, userMessage) {
1117
+ async function callLLM(config, userMessage, options) {
1118
+ const maxTokens = options?.maxTokens ?? 8192;
1119
+ const systemPrompt = options?.systemPrompt ?? SYSTEM_PROMPT;
1120
+ const providerName = getProviderName(config.provider);
848
1121
  if (config.provider === "anthropic") {
849
- const client = new Anthropic2({ apiKey: config.api_key });
1122
+ const client2 = new Anthropic2({ apiKey: config.api_key });
850
1123
  try {
851
- const response = await client.messages.create({
1124
+ const response = await client2.messages.create({
852
1125
  model: config.model,
853
- max_tokens: 8192,
854
- system: SYSTEM_PROMPT,
1126
+ max_tokens: maxTokens,
1127
+ system: systemPrompt,
855
1128
  messages: [{ role: "user", content: userMessage }]
856
1129
  });
857
1130
  const textBlock = response.content.find((block) => block.type === "text");
@@ -860,34 +1133,90 @@ async function callLLM(config, userMessage) {
860
1133
  }
861
1134
  return textBlock.text;
862
1135
  } catch (err) {
863
- throw new Error(classifyError(err, "Anthropic"));
1136
+ throw new Error(classifyError(err, providerName));
864
1137
  }
865
- } else if (config.provider === "openai" || config.provider === "google") {
866
- const providerName = config.provider === "google" ? "Google" : "OpenAI";
867
- const clientOptions = { apiKey: config.api_key };
868
- if (config.provider === "google") {
869
- clientOptions.baseURL = "https://generativelanguage.googleapis.com/v1beta/openai/";
1138
+ }
1139
+ const resolvedBaseURL = getBaseURL(config.provider, config.base_url);
1140
+ const clientOptions = { apiKey: config.api_key };
1141
+ if (resolvedBaseURL) clientOptions.baseURL = resolvedBaseURL;
1142
+ const client = new OpenAI2(clientOptions);
1143
+ try {
1144
+ const response = await client.chat.completions.create({
1145
+ model: config.model,
1146
+ max_tokens: maxTokens,
1147
+ messages: [
1148
+ { role: "system", content: systemPrompt },
1149
+ { role: "user", content: userMessage }
1150
+ ]
1151
+ });
1152
+ const text = response.choices[0]?.message?.content;
1153
+ if (!text) {
1154
+ throw new Error("No text response from compiler LLM");
870
1155
  }
871
- const client = new OpenAI2(clientOptions);
872
- try {
873
- const response = await client.chat.completions.create({
874
- model: config.model,
875
- max_tokens: 8192,
876
- messages: [
877
- { role: "system", content: SYSTEM_PROMPT },
878
- { role: "user", content: userMessage }
1156
+ return text;
1157
+ } catch (err) {
1158
+ throw new Error(classifyError(err, providerName));
1159
+ }
1160
+ }
1161
+ function buildSettings(skeleton, registry) {
1162
+ const selectedTools = skeleton.tools.map((t) => registry.find((r) => r.id === t.tool_id)).filter(Boolean);
1163
+ const allow = ["Read", "Write", "Edit", "Bash(npm run *)", "Bash(npx *)"];
1164
+ const deny = [
1165
+ "Bash(rm -rf *)",
1166
+ "Bash(curl * | sh)",
1167
+ "Bash(wget * | sh)",
1168
+ "Read(./.env)",
1169
+ "Read(./secrets/**)"
1170
+ ];
1171
+ const hooks = {
1172
+ PreToolUse: [
1173
+ {
1174
+ matcher: "Bash",
1175
+ hooks: [
1176
+ {
1177
+ type: "command",
1178
+ command: `CMD=$(cat | jq -r '.tool_input.command // empty') && echo "$CMD" | grep -qiE 'rm\\s+-rf\\s+/|DROP\\s+TABLE|curl.*\\|\\s*sh' && echo 'Blocked destructive command' >&2 && exit 2 || true`
1179
+ }
879
1180
  ]
880
- });
881
- const text = response.choices[0]?.message?.content;
882
- if (!text) {
883
- throw new Error("No text response from compiler LLM");
884
1181
  }
885
- return text;
886
- } catch (err) {
887
- throw new Error(classifyError(err, providerName));
1182
+ ],
1183
+ PostCompact: [
1184
+ {
1185
+ matcher: "",
1186
+ hooks: [
1187
+ {
1188
+ type: "prompt",
1189
+ prompt: "Re-read CLAUDE.md and docs/SPRINT.md (if it exists) to restore project context after compaction."
1190
+ }
1191
+ ]
1192
+ }
1193
+ ]
1194
+ };
1195
+ const techStack = skeleton.outline.tech_stack.map((t) => t.toLowerCase());
1196
+ if (techStack.some((t) => t.includes("typescript") || t.includes("javascript") || t.includes("react") || t.includes("next"))) {
1197
+ hooks.PostToolUse = [
1198
+ {
1199
+ matcher: "Edit|Write",
1200
+ hooks: [
1201
+ {
1202
+ type: "command",
1203
+ command: `FILE=$(cat | jq -r '.tool_input.file_path // empty') && [ -n "$FILE" ] && npx prettier --write "$FILE" 2>/dev/null || true`
1204
+ }
1205
+ ]
1206
+ }
1207
+ ];
1208
+ }
1209
+ return { permissions: { allow, deny }, hooks };
1210
+ }
1211
+ function buildMcpConfig(skeleton, registry) {
1212
+ const config = {};
1213
+ for (const tool of skeleton.tools) {
1214
+ const reg = registry.find((r) => r.id === tool.tool_id);
1215
+ if (reg?.install.mcp_config) {
1216
+ config[tool.tool_id] = reg.install.mcp_config;
888
1217
  }
889
1218
  }
890
- throw new Error(`Unsupported provider: ${config.provider}. Run \`kairn init\` to reconfigure.`);
1219
+ return config;
891
1220
  }
892
1221
  function validateSpec(spec, onProgress) {
893
1222
  const warnings = [];
@@ -914,17 +1243,52 @@ async function compile(intent, onProgress) {
914
1243
  }
915
1244
  onProgress?.("Loading tool registry...");
916
1245
  const registry = await loadRegistry();
917
- onProgress?.(`Compiling with ${config.provider} (${config.model})...`);
918
- const userMessage = buildUserMessage(intent, registry);
919
- const responseText = await callLLM(config, userMessage);
920
- onProgress?.("Parsing environment spec...");
921
- const parsed = parseSpecResponse(responseText);
1246
+ onProgress?.("Analyzing workflow...");
1247
+ const skeletonMsg = buildSkeletonMessage(intent, registry);
1248
+ const skeletonText = await callLLM(config, skeletonMsg, {
1249
+ maxTokens: 2048,
1250
+ systemPrompt: SKELETON_PROMPT
1251
+ });
1252
+ const skeleton = parseSkeletonResponse(skeletonText);
1253
+ onProgress?.("Generating environment...");
1254
+ const harnessMsg = buildHarnessMessage(intent, skeleton);
1255
+ let harness;
1256
+ try {
1257
+ const harnessText = await callLLM(config, harnessMsg, {
1258
+ maxTokens: 8192,
1259
+ systemPrompt: HARNESS_PROMPT
1260
+ });
1261
+ harness = parseHarnessResponse(harnessText);
1262
+ } catch {
1263
+ onProgress?.("Retrying with concise mode...");
1264
+ const retryMsg = buildHarnessMessage(intent, skeleton, true);
1265
+ const retryText = await callLLM(config, retryMsg, {
1266
+ maxTokens: 8192,
1267
+ systemPrompt: HARNESS_PROMPT
1268
+ });
1269
+ harness = parseHarnessResponse(retryText);
1270
+ }
1271
+ onProgress?.("Configuring tools...");
1272
+ const settings = buildSettings(skeleton, registry);
1273
+ const mcpConfig = buildMcpConfig(skeleton, registry);
922
1274
  const spec = {
923
1275
  id: `env_${crypto.randomUUID()}`,
924
1276
  intent,
925
1277
  created_at: (/* @__PURE__ */ new Date()).toISOString(),
926
- ...parsed,
927
- autonomy_level: parsed.autonomy_level ?? 1
1278
+ name: skeleton.name,
1279
+ description: skeleton.description,
1280
+ autonomy_level: 1,
1281
+ tools: skeleton.tools,
1282
+ harness: {
1283
+ claude_md: harness.claude_md,
1284
+ settings,
1285
+ mcp_config: mcpConfig,
1286
+ commands: harness.commands,
1287
+ rules: harness.rules,
1288
+ skills: harness.skills ?? {},
1289
+ agents: harness.agents ?? {},
1290
+ docs: harness.docs
1291
+ }
928
1292
  };
929
1293
  validateSpec(spec, onProgress);
930
1294
  await ensureDirs();
@@ -939,9 +1303,7 @@ async function generateClarifications(intent, onProgress) {
939
1303
  }
940
1304
  onProgress?.("Analyzing your request...");
941
1305
  const clarificationConfig = { ...config };
942
- if (config.provider === "anthropic") {
943
- clarificationConfig.model = "claude-haiku-4-5-20251001";
944
- }
1306
+ clarificationConfig.model = getCheapModel(config.provider, config.model);
945
1307
  const response = await callLLM(clarificationConfig, CLARIFICATION_PROMPT + "\n\nUser description: " + intent);
946
1308
  try {
947
1309
  let cleaned = response.trim();
@@ -1653,7 +2015,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1653
2015
  );
1654
2016
  process.exit(1);
1655
2017
  }
1656
- const intentRaw = intentArg || await input({
2018
+ const intentRaw = intentArg || await input2({
1657
2019
  message: "What do you want your agent to do?"
1658
2020
  });
1659
2021
  if (!intentRaw.trim()) {
@@ -1673,7 +2035,7 @@ var describeCommand = new Command2("describe").description("Describe your workfl
1673
2035
  if (clarifications.length > 0) {
1674
2036
  const answers = [];
1675
2037
  for (const c of clarifications) {
1676
- const answer = await input({
2038
+ const answer = await input2({
1677
2039
  message: c.question,
1678
2040
  default: c.suggestion
1679
2041
  });
@@ -2425,7 +2787,7 @@ var optimizeCommand = new Command6("optimize").description("Scan an existing pro
2425
2787
  console.log(ui.file(file));
2426
2788
  }
2427
2789
  if (hasEnvVars) {
2428
- await collectAndWriteKeys(summary.envSetup, targetDir, { quick: options.quick });
2790
+ await collectAndWriteKeys(summary.envSetup, targetDir);
2429
2791
  console.log("");
2430
2792
  }
2431
2793
  if (summary.pluginCommands.length > 0) {
@@ -2619,7 +2981,7 @@ var doctorCommand = new Command7("doctor").description(
2619
2981
  // src/commands/registry.ts
2620
2982
  import { Command as Command8 } from "commander";
2621
2983
  import chalk11 from "chalk";
2622
- import { input as input2, select as select3 } from "@inquirer/prompts";
2984
+ import { input as input3, select as select3 } from "@inquirer/prompts";
2623
2985
  var listCommand2 = new Command8("list").description("List tools in the registry").option("--category <cat>", "Filter by category").option("--user-only", "Show only user-defined tools").action(async (options) => {
2624
2986
  printCompactBanner();
2625
2987
  let all;
@@ -2679,7 +3041,7 @@ var listCommand2 = new Command8("list").description("List tools in the registry"
2679
3041
  var addCommand = new Command8("add").description("Add a tool to the user registry").action(async () => {
2680
3042
  let id;
2681
3043
  try {
2682
- id = await input2({
3044
+ id = await input3({
2683
3045
  message: "Tool ID (kebab-case)",
2684
3046
  validate: (v) => {
2685
3047
  if (!v) return "ID is required";
@@ -2687,8 +3049,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2687
3049
  return true;
2688
3050
  }
2689
3051
  });
2690
- const name = await input2({ message: "Display name" });
2691
- const description = await input2({ message: "Description" });
3052
+ const name = await input3({ message: "Display name" });
3053
+ const description = await input3({ message: "Description" });
2692
3054
  const category = await select3({
2693
3055
  message: "Category",
2694
3056
  choices: [
@@ -2732,8 +3094,8 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2732
3094
  if (auth === "api_key" || auth === "connection_string") {
2733
3095
  let addMore = true;
2734
3096
  while (addMore) {
2735
- const varName = await input2({ message: "Env var name" });
2736
- const varDesc = await input2({ message: "Env var description" });
3097
+ const varName = await input3({ message: "Env var name" });
3098
+ const varDesc = await input3({ message: "Env var description" });
2737
3099
  env_vars.push({ name: varName, description: varDesc });
2738
3100
  const another = await select3({
2739
3101
  message: "Add another env var?",
@@ -2745,14 +3107,14 @@ var addCommand = new Command8("add").description("Add a tool to the user registr
2745
3107
  addMore = another;
2746
3108
  }
2747
3109
  }
2748
- const signup_url_raw = await input2({ message: "Signup URL (optional, press enter to skip)" });
3110
+ const signup_url_raw = await input3({ message: "Signup URL (optional, press enter to skip)" });
2749
3111
  const signup_url = signup_url_raw.trim() || void 0;
2750
- const best_for_raw = await input2({ message: "Best-for tags, comma-separated" });
3112
+ const best_for_raw = await input3({ message: "Best-for tags, comma-separated" });
2751
3113
  const best_for = best_for_raw.split(",").map((s) => s.trim()).filter(Boolean);
2752
3114
  const install = {};
2753
3115
  if (type === "mcp_server") {
2754
- const command = await input2({ message: "MCP command" });
2755
- const args_raw = await input2({ message: "MCP args, comma-separated (leave blank for none)" });
3116
+ const command = await input3({ message: "MCP command" });
3117
+ const args_raw = await input3({ message: "MCP args, comma-separated (leave blank for none)" });
2756
3118
  const args = args_raw.split(",").map((s) => s.trim()).filter(Boolean);
2757
3119
  install.mcp_config = { command, args };
2758
3120
  }