kairn-cli 2.2.1 → 2.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1237,22 +1237,29 @@ function classifyError(err, provider) {
1237
1237
  }
1238
1238
  async function callLLM(config, userMessage, options) {
1239
1239
  const maxTokens = options.maxTokens ?? 8192;
1240
- const systemPrompt = options.systemPrompt;
1240
+ const { systemPrompt } = options;
1241
+ const jsonMode = options.jsonMode ?? false;
1241
1242
  const providerName = getProviderName(config.provider);
1242
1243
  if (config.provider === "anthropic") {
1243
1244
  const client2 = new Anthropic2({ apiKey: config.api_key });
1245
+ const messages = [
1246
+ { role: "user", content: userMessage }
1247
+ ];
1248
+ if (jsonMode) {
1249
+ messages.push({ role: "assistant", content: "{" });
1250
+ }
1244
1251
  try {
1245
1252
  const response = await client2.messages.create({
1246
1253
  model: config.model,
1247
1254
  max_tokens: maxTokens,
1248
1255
  system: systemPrompt,
1249
- messages: [{ role: "user", content: userMessage }]
1256
+ messages
1250
1257
  });
1251
1258
  const textBlock = response.content.find((block) => block.type === "text");
1252
1259
  if (!textBlock || textBlock.type !== "text") {
1253
1260
  throw new Error("No text response from compiler LLM");
1254
1261
  }
1255
- return textBlock.text;
1262
+ return jsonMode ? `{${textBlock.text}` : textBlock.text;
1256
1263
  } catch (err) {
1257
1264
  throw new Error(classifyError(err, providerName));
1258
1265
  }
@@ -1268,7 +1275,8 @@ async function callLLM(config, userMessage, options) {
1268
1275
  messages: [
1269
1276
  { role: "system", content: systemPrompt },
1270
1277
  { role: "user", content: userMessage }
1271
- ]
1278
+ ],
1279
+ ...jsonMode ? { response_format: { type: "json_object" } } : {}
1272
1280
  });
1273
1281
  const text = response.choices[0]?.message?.content;
1274
1282
  if (!text) {
@@ -4023,6 +4031,13 @@ async function snapshotBaseline(projectRoot, workspacePath) {
4023
4031
  }
4024
4032
  await copyDir(claudeDir, baselineDir);
4025
4033
  await copyDir(claudeDir, iter0Dir);
4034
+ const mcpJsonPath = path16.join(projectRoot, ".mcp.json");
4035
+ try {
4036
+ await fs16.access(mcpJsonPath);
4037
+ await fs16.copyFile(mcpJsonPath, path16.join(baselineDir, ".mcp.json"));
4038
+ await fs16.copyFile(mcpJsonPath, path16.join(iter0Dir, ".mcp.json"));
4039
+ } catch {
4040
+ }
4026
4041
  }
4027
4042
  async function copyDir(src, dest) {
4028
4043
  await fs16.mkdir(dest, { recursive: true });
@@ -4345,6 +4360,11 @@ async function scoreTask(task, workspacePath, stdout, stderr, config) {
4345
4360
  // src/evolve/runner.ts
4346
4361
  var execAsync2 = promisify2(exec2);
4347
4362
  var COPY_SKIP_DIRS = /* @__PURE__ */ new Set([".git", "node_modules", ".kairn-evolve", ".claude"]);
4363
+ async function deployMcpJson(harnessPath, workDir) {
4364
+ const src = path18.join(harnessPath, ".mcp.json");
4365
+ await fs18.copyFile(src, path18.join(workDir, ".mcp.json")).catch(() => {
4366
+ });
4367
+ }
4348
4368
  async function createIsolatedWorkspace(projectRoot, harnessPath) {
4349
4369
  const suffix = `${Date.now()}-${Math.random().toString(36).slice(2)}`;
4350
4370
  try {
@@ -4359,6 +4379,7 @@ async function createIsolatedWorkspace(projectRoot, harnessPath) {
4359
4379
  });
4360
4380
  await fs18.rm(path18.join(tmpDir2, ".claude"), { recursive: true, force: true });
4361
4381
  await copyDir(harnessPath, path18.join(tmpDir2, ".claude"));
4382
+ await deployMcpJson(harnessPath, tmpDir2);
4362
4383
  return { workDir: tmpDir2, isWorktree: true };
4363
4384
  } catch {
4364
4385
  }
@@ -4366,6 +4387,7 @@ async function createIsolatedWorkspace(projectRoot, harnessPath) {
4366
4387
  await copyProjectDir(projectRoot, tmpDir);
4367
4388
  await fs18.rm(path18.join(tmpDir, ".claude"), { recursive: true, force: true });
4368
4389
  await copyDir(harnessPath, path18.join(tmpDir, ".claude"));
4390
+ await deployMcpJson(harnessPath, tmpDir);
4369
4391
  return { workDir: tmpDir, isWorktree: false };
4370
4392
  }
4371
4393
  async function copyProjectDir(src, dest) {
@@ -4615,23 +4637,37 @@ minimal changes to the harness files that will fix those failures.
4615
4637
 
4616
4638
  3. Check history for counterfactual evidence
4617
4639
 
4640
+ ## Available Mutation Actions
4641
+ 1. **replace** \u2014 Replace old_text with new_text in a file: { "file": "...", "action": "replace", "old_text": "...", "new_text": "...", "rationale": "..." }
4642
+ 2. **add_section** \u2014 Append new content to a file (or create it): { "file": "...", "action": "add_section", "new_text": "...", "rationale": "..." }
4643
+ 3. **create_file** \u2014 Create a new file: { "file": "...", "action": "create_file", "new_text": "...", "rationale": "..." }
4644
+ 4. **delete_section** \u2014 Remove specific text from a file: { "file": "...", "action": "delete_section", "old_text": "...", "rationale": "..." }
4645
+ 5. **delete_file** \u2014 Delete an entire file: { "file": "...", "action": "delete_file", "rationale": "..." }
4646
+
4618
4647
  ## Output Format
4619
4648
  Return a JSON object:
4620
4649
  {
4621
4650
  "reasoning": "Your full causal analysis...",
4622
4651
  "mutations": [
4623
4652
  { "file": "CLAUDE.md", "action": "replace", "old_text": "...", "new_text": "...", "rationale": "..." },
4624
- { "file": "commands/develop.md", "action": "add_section", "new_text": "...", "rationale": "..." }
4653
+ { "file": "commands/develop.md", "action": "add_section", "new_text": "...", "rationale": "..." },
4654
+ { "file": "rules/obsolete.md", "action": "delete_file", "rationale": "..." }
4625
4655
  ],
4626
4656
  "expected_impact": { "task-id": "+15% \u2014 explanation" }
4627
4657
  }
4628
4658
 
4659
+ ## MCP Configuration
4660
+ You can also mutate .mcp.json to add, remove, or reconfigure MCP servers.
4661
+ Treat .mcp.json like any other harness file \u2014 propose changes when traces show
4662
+ the agent lacks a tool it needs, or has tools that add noise without benefit.
4663
+
4629
4664
  ## Rules
4630
4665
  - MINIMAL changes only. Don't rewrite the entire CLAUDE.md.
4631
4666
  - Each mutation must have a clear rationale tied to a specific trace observation.
4632
4667
  - Never remove something that's working for another task.
4633
4668
  - If a previous iteration's change caused a regression, REVERT it.
4634
- - Prefer ADDITIVE changes over replacements when possible.
4669
+ - Consider both additions AND removals. Remove sections that add noise without improving task performance.
4670
+ - Bloated harnesses hurt performance \u2014 trim what isn't earning its keep.
4635
4671
 
4636
4672
  Return ONLY valid JSON.`;
4637
4673
  var STDOUT_TRUNCATION_LIMIT = 1e3;
@@ -4785,7 +4821,18 @@ function parseProposerResponse(raw) {
4785
4821
  try {
4786
4822
  parsed = JSON.parse(cleaned);
4787
4823
  } catch {
4788
- throw new Error(`Proposer returned invalid JSON: ${cleaned.slice(0, 200)}`);
4824
+ const firstBrace = cleaned.indexOf("{");
4825
+ const lastBrace = cleaned.lastIndexOf("}");
4826
+ if (firstBrace !== -1 && lastBrace > firstBrace) {
4827
+ const extracted = cleaned.slice(firstBrace, lastBrace + 1);
4828
+ try {
4829
+ parsed = JSON.parse(extracted);
4830
+ } catch {
4831
+ throw new Error(`Proposer returned invalid JSON: ${cleaned.slice(0, 200)}`);
4832
+ }
4833
+ } else {
4834
+ throw new Error(`Proposer returned invalid JSON: ${cleaned.slice(0, 200)}`);
4835
+ }
4789
4836
  }
4790
4837
  if (typeof parsed !== "object" || parsed === null) {
4791
4838
  throw new Error("Proposer response is not a JSON object");
@@ -4811,10 +4858,11 @@ function parseProposerResponse(raw) {
4811
4858
  if (file.includes("..")) {
4812
4859
  continue;
4813
4860
  }
4814
- if (action !== "replace" && action !== "add_section" && action !== "create_file") {
4861
+ const validActions = /* @__PURE__ */ new Set(["replace", "add_section", "create_file", "delete_section", "delete_file"]);
4862
+ if (!validActions.has(action)) {
4815
4863
  continue;
4816
4864
  }
4817
- if (action === "replace" && !oldText) {
4865
+ if ((action === "replace" || action === "delete_section") && !oldText) {
4818
4866
  continue;
4819
4867
  }
4820
4868
  const mutation = {
@@ -4848,7 +4896,8 @@ async function propose(iteration, workspacePath, harnessPath, history, tasks, co
4848
4896
  const proposerConfig = { ...config, model: proposerModel };
4849
4897
  const response = await callLLM(proposerConfig, userMessage, {
4850
4898
  systemPrompt: PROPOSER_SYSTEM_PROMPT,
4851
- maxTokens: 8192
4899
+ maxTokens: 8192,
4900
+ jsonMode: true
4852
4901
  });
4853
4902
  return parseProposerResponse(response);
4854
4903
  }
@@ -4892,6 +4941,23 @@ async function applyMutations(currentHarnessPath, nextIterationDir, mutations) {
4892
4941
  } else if (mutation.action === "create_file") {
4893
4942
  await fs20.mkdir(path20.dirname(filePath), { recursive: true });
4894
4943
  await fs20.writeFile(filePath, mutation.newText, "utf-8");
4944
+ } else if (mutation.action === "delete_section") {
4945
+ if (!mutation.oldText) {
4946
+ continue;
4947
+ }
4948
+ let sectionContent;
4949
+ try {
4950
+ sectionContent = await fs20.readFile(filePath, "utf-8");
4951
+ } catch {
4952
+ continue;
4953
+ }
4954
+ if (!sectionContent.includes(mutation.oldText)) {
4955
+ continue;
4956
+ }
4957
+ await fs20.writeFile(filePath, sectionContent.replace(mutation.oldText, ""), "utf-8");
4958
+ } else if (mutation.action === "delete_file") {
4959
+ await fs20.unlink(filePath).catch(() => {
4960
+ });
4895
4961
  }
4896
4962
  }
4897
4963
  const diffPatch = await generateDiff2(currentHarnessPath, newHarnessPath);