@kody-ade/kody-engine 0.2.12 → 0.2.14

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/bin/kody2.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.2.12",
6
+ version: "0.2.14",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -17,7 +17,7 @@ var package_default = {
17
17
  ],
18
18
  scripts: {
19
19
  kody2: "tsx bin/kody2.ts",
20
- build: `tsup && node -e "const fs=require('fs');fs.rmSync('dist/executables',{recursive:true,force:true});fs.cpSync('src/executables','dist/executables',{recursive:true})"`,
20
+ build: "tsup && node scripts/copy-assets.cjs",
21
21
  test: "vitest run tests/unit tests/int --no-coverage",
22
22
  "test:e2e": "vitest run tests/e2e --no-coverage",
23
23
  "test:all": "vitest run tests --no-coverage",
@@ -50,8 +50,8 @@ var package_default = {
50
50
  };
51
51
 
52
52
  // src/executor.ts
53
- import * as fs13 from "fs";
54
- import * as path11 from "path";
53
+ import * as fs14 from "fs";
54
+ import * as path12 from "path";
55
55
 
56
56
  // src/agent.ts
57
57
  import * as fs2 from "fs";
@@ -289,6 +289,15 @@ async function runAgent(opts) {
289
289
  if (opts.mcpServers && opts.mcpServers.length > 0) {
290
290
  queryOptions.mcpServers = opts.mcpServers;
291
291
  }
292
+ if (opts.pluginPaths && opts.pluginPaths.length > 0) {
293
+ queryOptions.plugins = opts.pluginPaths.map((p) => ({ type: "local", path: p }));
294
+ }
295
+ if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
296
+ queryOptions.maxTurns = opts.maxTurns;
297
+ }
298
+ if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
299
+ queryOptions.systemPrompt = { type: "preset", preset: "claude_code", append: opts.systemPromptAppend };
300
+ }
292
301
  const result = query({
293
302
  prompt: opts.prompt,
294
303
  // biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
@@ -540,19 +549,13 @@ function parseClaudeCode(p, raw) {
540
549
  throw new ProfileError(p, `claudeCode.permissionMode must be one of default|acceptEdits|plan|bypassPermissions`);
541
550
  }
542
551
  const tools = Array.isArray(r.tools) ? r.tools : [];
543
- const hooksRaw = r.hooks ?? {};
544
- const hooks = {
545
- PreToolUse: Array.isArray(hooksRaw.PreToolUse) ? hooksRaw.PreToolUse : [],
546
- PostToolUse: Array.isArray(hooksRaw.PostToolUse) ? hooksRaw.PostToolUse : [],
547
- Stop: Array.isArray(hooksRaw.Stop) ? hooksRaw.Stop : []
548
- };
549
552
  return {
550
553
  model: typeof r.model === "string" ? r.model : "inherit",
551
554
  permissionMode,
552
555
  maxTurns: typeof r.maxTurns === "number" ? r.maxTurns : null,
553
556
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
554
557
  tools,
555
- hooks,
558
+ hooks: Array.isArray(r.hooks) ? r.hooks : [],
556
559
  skills: Array.isArray(r.skills) ? r.skills : [],
557
560
  commands: Array.isArray(r.commands) ? r.commands : [],
558
561
  subagents: Array.isArray(r.subagents) ? r.subagents : [],
@@ -617,6 +620,99 @@ function parseScriptList(p, key, raw) {
617
620
  return out;
618
621
  }
619
622
 
623
+ // src/scripts/buildSyntheticPlugin.ts
624
+ import * as fs5 from "fs";
625
+ import * as os2 from "os";
626
+ import * as path5 from "path";
627
+ function getPluginsCatalogRoot() {
628
+ const here = path5.dirname(new URL(import.meta.url).pathname);
629
+ const candidates = [
630
+ path5.join(here, "..", "plugins"),
631
+ // dev: src/scripts → src/plugins
632
+ path5.join(here, "..", "..", "plugins"),
633
+ // built: dist/scripts → dist/plugins
634
+ path5.join(here, "..", "..", "src", "plugins")
635
+ // fallback
636
+ ];
637
+ for (const c of candidates) {
638
+ if (fs5.existsSync(c) && fs5.statSync(c).isDirectory()) return c;
639
+ }
640
+ return candidates[0];
641
+ }
642
+ var buildSyntheticPlugin = async (ctx, profile) => {
643
+ const cc = profile.claudeCode;
644
+ const needsSynthetic = cc.skills.length > 0 || cc.commands.length > 0 || cc.hooks.length > 0 || cc.subagents.length > 0;
645
+ if (!needsSynthetic) return;
646
+ const catalog = getPluginsCatalogRoot();
647
+ const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
648
+ const root = path5.join(os2.tmpdir(), `kody2-synth-${runId}`);
649
+ fs5.mkdirSync(path5.join(root, ".claude-plugin"), { recursive: true });
650
+ if (cc.skills.length > 0) {
651
+ const dst = path5.join(root, "skills");
652
+ fs5.mkdirSync(dst, { recursive: true });
653
+ for (const name of cc.skills) {
654
+ const src = path5.join(catalog, "skills", name);
655
+ if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
656
+ copyDir(src, path5.join(dst, name));
657
+ }
658
+ }
659
+ if (cc.commands.length > 0) {
660
+ const dst = path5.join(root, "commands");
661
+ fs5.mkdirSync(dst, { recursive: true });
662
+ for (const name of cc.commands) {
663
+ const src = path5.join(catalog, "commands", `${name}.md`);
664
+ if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
665
+ fs5.copyFileSync(src, path5.join(dst, `${name}.md`));
666
+ }
667
+ }
668
+ if (cc.subagents.length > 0) {
669
+ const dst = path5.join(root, "agents");
670
+ fs5.mkdirSync(dst, { recursive: true });
671
+ for (const name of cc.subagents) {
672
+ const src = path5.join(catalog, "agents", `${name}.md`);
673
+ if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
674
+ fs5.copyFileSync(src, path5.join(dst, `${name}.md`));
675
+ }
676
+ }
677
+ if (cc.hooks.length > 0) {
678
+ const dst = path5.join(root, "hooks");
679
+ fs5.mkdirSync(dst, { recursive: true });
680
+ const merged = { hooks: {} };
681
+ for (const name of cc.hooks) {
682
+ const src = path5.join(catalog, "hooks", `${name}.json`);
683
+ if (!fs5.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
684
+ const parsed = JSON.parse(fs5.readFileSync(src, "utf-8"));
685
+ for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
686
+ if (!Array.isArray(entries)) continue;
687
+ if (!merged.hooks[event]) merged.hooks[event] = [];
688
+ merged.hooks[event].push(...entries);
689
+ }
690
+ }
691
+ fs5.writeFileSync(path5.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
692
+ `);
693
+ }
694
+ const manifest = {
695
+ name: `kody2-synth-${profile.name}`,
696
+ version: "1.0.0",
697
+ description: `Synthetic plugin assembled by Kody2 for profile '${profile.name}' at runtime.`
698
+ };
699
+ if (cc.skills.length > 0) manifest.skills = ["./skills/"];
700
+ if (cc.commands.length > 0) manifest.commands = ["./commands/"];
701
+ if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
702
+ fs5.writeFileSync(path5.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
703
+ `);
704
+ ctx.data.syntheticPluginPath = root;
705
+ };
706
+ function copyDir(src, dst) {
707
+ fs5.mkdirSync(dst, { recursive: true });
708
+ for (const ent of fs5.readdirSync(src, { withFileTypes: true })) {
709
+ const s = path5.join(src, ent.name);
710
+ const d = path5.join(dst, ent.name);
711
+ if (ent.isDirectory()) copyDir(s, d);
712
+ else if (ent.isFile()) fs5.copyFileSync(s, d);
713
+ }
714
+ }
715
+
620
716
  // src/coverage.ts
621
717
  import { execFileSync as execFileSync2 } from "child_process";
622
718
  function patternToRegex(pattern) {
@@ -679,18 +775,18 @@ function formatMissesForFeedback(misses) {
679
775
  }
680
776
 
681
777
  // src/prompt.ts
682
- import * as fs5 from "fs";
683
- import * as path5 from "path";
778
+ import * as fs6 from "fs";
779
+ import * as path6 from "path";
684
780
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
685
781
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
686
782
  function loadProjectConventions(projectDir) {
687
783
  const out = [];
688
784
  for (const rel of CONVENTION_FILES) {
689
- const abs = path5.join(projectDir, rel);
690
- if (!fs5.existsSync(abs)) continue;
785
+ const abs = path6.join(projectDir, rel);
786
+ if (!fs6.existsSync(abs)) continue;
691
787
  let content;
692
788
  try {
693
- content = fs5.readFileSync(abs, "utf-8");
789
+ content = fs6.readFileSync(abs, "utf-8");
694
790
  } catch {
695
791
  continue;
696
792
  }
@@ -767,8 +863,8 @@ import { execFileSync as execFileSync4 } from "child_process";
767
863
 
768
864
  // src/commit.ts
769
865
  import { execFileSync as execFileSync3 } from "child_process";
770
- import * as fs6 from "fs";
771
- import * as path6 from "path";
866
+ import * as fs7 from "fs";
867
+ import * as path7 from "path";
772
868
  var FORBIDDEN_PATH_PREFIXES = [
773
869
  ".kody/",
774
870
  ".kody-engine/",
@@ -823,18 +919,18 @@ function tryGit(args, cwd) {
823
919
  }
824
920
  function abortUnfinishedGitOps(cwd) {
825
921
  const aborted = [];
826
- const gitDir = path6.join(cwd ?? process.cwd(), ".git");
827
- if (!fs6.existsSync(gitDir)) return aborted;
828
- if (fs6.existsSync(path6.join(gitDir, "MERGE_HEAD"))) {
922
+ const gitDir = path7.join(cwd ?? process.cwd(), ".git");
923
+ if (!fs7.existsSync(gitDir)) return aborted;
924
+ if (fs7.existsSync(path7.join(gitDir, "MERGE_HEAD"))) {
829
925
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
830
926
  }
831
- if (fs6.existsSync(path6.join(gitDir, "CHERRY_PICK_HEAD"))) {
927
+ if (fs7.existsSync(path7.join(gitDir, "CHERRY_PICK_HEAD"))) {
832
928
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
833
929
  }
834
- if (fs6.existsSync(path6.join(gitDir, "REVERT_HEAD"))) {
930
+ if (fs7.existsSync(path7.join(gitDir, "REVERT_HEAD"))) {
835
931
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
836
932
  }
837
- if (fs6.existsSync(path6.join(gitDir, "rebase-merge")) || fs6.existsSync(path6.join(gitDir, "rebase-apply"))) {
933
+ if (fs7.existsSync(path7.join(gitDir, "rebase-merge")) || fs7.existsSync(path7.join(gitDir, "rebase-apply"))) {
838
934
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
839
935
  }
840
936
  try {
@@ -876,7 +972,7 @@ function normalizeCommitMessage(raw) {
876
972
  function commitAndPush(branch, agentMessage, cwd) {
877
973
  const allChanged = listChangedFiles(cwd);
878
974
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
879
- const mergeHeadExists = fs6.existsSync(path6.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
975
+ const mergeHeadExists = fs7.existsSync(path7.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
880
976
  if (allowedFiles.length === 0 && !mergeHeadExists) {
881
977
  return { committed: false, pushed: false, sha: "", message: "" };
882
978
  }
@@ -966,20 +1062,20 @@ function defaultCommitMessage(mode, data) {
966
1062
  }
967
1063
 
968
1064
  // src/scripts/composePrompt.ts
969
- import * as fs7 from "fs";
970
- import * as path7 from "path";
1065
+ import * as fs8 from "fs";
1066
+ import * as path8 from "path";
971
1067
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
972
1068
  var composePrompt = async (ctx, profile) => {
973
1069
  const explicit = ctx.data.promptTemplate;
974
1070
  const mode = ctx.args.mode;
975
1071
  const candidates = [
976
- explicit ? path7.join(profile.dir, explicit) : null,
977
- mode ? path7.join(profile.dir, "prompts", `${mode}.md`) : null,
978
- path7.join(profile.dir, "prompt.md")
1072
+ explicit ? path8.join(profile.dir, explicit) : null,
1073
+ mode ? path8.join(profile.dir, "prompts", `${mode}.md`) : null,
1074
+ path8.join(profile.dir, "prompt.md")
979
1075
  ].filter(Boolean);
980
1076
  let templatePath = "";
981
1077
  for (const c of candidates) {
982
- if (fs7.existsSync(c)) {
1078
+ if (fs8.existsSync(c)) {
983
1079
  templatePath = c;
984
1080
  break;
985
1081
  }
@@ -987,7 +1083,7 @@ var composePrompt = async (ctx, profile) => {
987
1083
  if (!templatePath) {
988
1084
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
989
1085
  }
990
- const template = fs7.readFileSync(templatePath, "utf-8");
1086
+ const template = fs8.readFileSync(templatePath, "utf-8");
991
1087
  const tokens = {
992
1088
  ...stringifyAll(ctx.args, "args."),
993
1089
  ...stringifyAll(ctx.data, ""),
@@ -1459,7 +1555,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1459
1555
 
1460
1556
  // src/gha.ts
1461
1557
  import { execFileSync as execFileSync7 } from "child_process";
1462
- import * as fs8 from "fs";
1558
+ import * as fs9 from "fs";
1463
1559
  function getRunUrl() {
1464
1560
  const server = process.env.GITHUB_SERVER_URL;
1465
1561
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1470,10 +1566,10 @@ function getRunUrl() {
1470
1566
  function reactToTriggerComment(cwd) {
1471
1567
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1472
1568
  const eventPath = process.env.GITHUB_EVENT_PATH;
1473
- if (!eventPath || !fs8.existsSync(eventPath)) return;
1569
+ if (!eventPath || !fs9.existsSync(eventPath)) return;
1474
1570
  let event = null;
1475
1571
  try {
1476
- event = JSON.parse(fs8.readFileSync(eventPath, "utf-8"));
1572
+ event = JSON.parse(fs9.readFileSync(eventPath, "utf-8"));
1477
1573
  } catch {
1478
1574
  return;
1479
1575
  }
@@ -1672,35 +1768,35 @@ function tryPostPr2(prNumber, body, cwd) {
1672
1768
 
1673
1769
  // src/scripts/initFlow.ts
1674
1770
  import { execFileSync as execFileSync9 } from "child_process";
1675
- import * as fs10 from "fs";
1676
- import * as path9 from "path";
1771
+ import * as fs11 from "fs";
1772
+ import * as path10 from "path";
1677
1773
 
1678
1774
  // src/registry.ts
1679
- import * as fs9 from "fs";
1680
- import * as path8 from "path";
1775
+ import * as fs10 from "fs";
1776
+ import * as path9 from "path";
1681
1777
  function getExecutablesRoot() {
1682
- const here = path8.dirname(new URL(import.meta.url).pathname);
1778
+ const here = path9.dirname(new URL(import.meta.url).pathname);
1683
1779
  const candidates = [
1684
- path8.join(here, "executables"),
1780
+ path9.join(here, "executables"),
1685
1781
  // dev: src/
1686
- path8.join(here, "..", "executables"),
1782
+ path9.join(here, "..", "executables"),
1687
1783
  // built: dist/bin → dist/executables
1688
- path8.join(here, "..", "src", "executables")
1784
+ path9.join(here, "..", "src", "executables")
1689
1785
  // fallback
1690
1786
  ];
1691
1787
  for (const c of candidates) {
1692
- if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1788
+ if (fs10.existsSync(c) && fs10.statSync(c).isDirectory()) return c;
1693
1789
  }
1694
1790
  return candidates[0];
1695
1791
  }
1696
1792
  function listExecutables(root = getExecutablesRoot()) {
1697
- if (!fs9.existsSync(root)) return [];
1698
- const entries = fs9.readdirSync(root, { withFileTypes: true });
1793
+ if (!fs10.existsSync(root)) return [];
1794
+ const entries = fs10.readdirSync(root, { withFileTypes: true });
1699
1795
  const out = [];
1700
1796
  for (const ent of entries) {
1701
1797
  if (!ent.isDirectory()) continue;
1702
- const profilePath = path8.join(root, ent.name, "profile.json");
1703
- if (fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile()) {
1798
+ const profilePath = path9.join(root, ent.name, "profile.json");
1799
+ if (fs10.existsSync(profilePath) && fs10.statSync(profilePath).isFile()) {
1704
1800
  out.push({ name: ent.name, profilePath });
1705
1801
  }
1706
1802
  }
@@ -1708,8 +1804,8 @@ function listExecutables(root = getExecutablesRoot()) {
1708
1804
  }
1709
1805
  function hasExecutable(name, root = getExecutablesRoot()) {
1710
1806
  if (!isSafeName(name)) return false;
1711
- const profilePath = path8.join(root, name, "profile.json");
1712
- return fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile();
1807
+ const profilePath = path9.join(root, name, "profile.json");
1808
+ return fs10.existsSync(profilePath) && fs10.statSync(profilePath).isFile();
1713
1809
  }
1714
1810
  function isSafeName(name) {
1715
1811
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -1738,9 +1834,9 @@ function parseGenericFlags(argv) {
1738
1834
 
1739
1835
  // src/scripts/initFlow.ts
1740
1836
  function detectPackageManager(cwd) {
1741
- if (fs10.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1742
- if (fs10.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
1743
- if (fs10.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
1837
+ if (fs11.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1838
+ if (fs11.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
1839
+ if (fs11.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
1744
1840
  return "npm";
1745
1841
  }
1746
1842
  function qualityCommandsFor(pm) {
@@ -1861,22 +1957,22 @@ function performInit(cwd, force) {
1861
1957
  const pm = detectPackageManager(cwd);
1862
1958
  const ownerRepo = detectOwnerRepo(cwd);
1863
1959
  const defaultBranch = defaultBranchFromGit(cwd);
1864
- const configPath = path9.join(cwd, "kody.config.json");
1865
- if (fs10.existsSync(configPath) && !force) {
1960
+ const configPath = path10.join(cwd, "kody.config.json");
1961
+ if (fs11.existsSync(configPath) && !force) {
1866
1962
  skipped.push("kody.config.json");
1867
1963
  } else {
1868
1964
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
1869
- fs10.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1965
+ fs11.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1870
1966
  `);
1871
1967
  wrote.push("kody.config.json");
1872
1968
  }
1873
- const workflowDir = path9.join(cwd, ".github", "workflows");
1874
- const workflowPath = path9.join(workflowDir, "kody2.yml");
1875
- if (fs10.existsSync(workflowPath) && !force) {
1969
+ const workflowDir = path10.join(cwd, ".github", "workflows");
1970
+ const workflowPath = path10.join(workflowDir, "kody2.yml");
1971
+ if (fs11.existsSync(workflowPath) && !force) {
1876
1972
  skipped.push(".github/workflows/kody2.yml");
1877
1973
  } else {
1878
- fs10.mkdirSync(workflowDir, { recursive: true });
1879
- fs10.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1974
+ fs11.mkdirSync(workflowDir, { recursive: true });
1975
+ fs11.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1880
1976
  wrote.push(".github/workflows/kody2.yml");
1881
1977
  }
1882
1978
  for (const exe of listExecutables()) {
@@ -1887,12 +1983,12 @@ function performInit(cwd, force) {
1887
1983
  continue;
1888
1984
  }
1889
1985
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
1890
- const target = path9.join(workflowDir, `kody2-${exe.name}.yml`);
1891
- if (fs10.existsSync(target) && !force) {
1986
+ const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
1987
+ if (fs11.existsSync(target) && !force) {
1892
1988
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
1893
1989
  continue;
1894
1990
  }
1895
- fs10.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
1991
+ fs11.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
1896
1992
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
1897
1993
  }
1898
1994
  return { wrote, skipped };
@@ -2308,8 +2404,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2308
2404
 
2309
2405
  // src/scripts/releaseFlow.ts
2310
2406
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2311
- import * as fs11 from "fs";
2312
- import * as path10 from "path";
2407
+ import * as fs12 from "fs";
2408
+ import * as path11 from "path";
2313
2409
  function bumpVersion(current, bump) {
2314
2410
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2315
2411
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2325,12 +2421,12 @@ function bumpVersion(current, bump) {
2325
2421
  return `${major}.${minor}.${patch}`;
2326
2422
  }
2327
2423
  function updateVersionInFile(file, newVersion, cwd) {
2328
- const abs = path10.join(cwd, file);
2329
- if (!fs11.existsSync(abs)) return false;
2330
- const content = fs11.readFileSync(abs, "utf-8");
2424
+ const abs = path11.join(cwd, file);
2425
+ if (!fs12.existsSync(abs)) return false;
2426
+ const content = fs12.readFileSync(abs, "utf-8");
2331
2427
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2332
2428
  if (updated === content) return false;
2333
- fs11.writeFileSync(abs, updated);
2429
+ fs12.writeFileSync(abs, updated);
2334
2430
  return true;
2335
2431
  }
2336
2432
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2378,19 +2474,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2378
2474
  return parts.join("\n");
2379
2475
  }
2380
2476
  function prependChangelog(cwd, entry) {
2381
- const p = path10.join(cwd, "CHANGELOG.md");
2477
+ const p = path11.join(cwd, "CHANGELOG.md");
2382
2478
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2383
- if (fs11.existsSync(p)) {
2384
- const prior = fs11.readFileSync(p, "utf-8");
2479
+ if (fs12.existsSync(p)) {
2480
+ const prior = fs12.readFileSync(p, "utf-8");
2385
2481
  if (/^#\s*Changelog\b/m.test(prior)) {
2386
2482
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2387
- fs11.writeFileSync(p, `${prior.slice(0, idx + 1)}
2483
+ fs12.writeFileSync(p, `${prior.slice(0, idx + 1)}
2388
2484
  ${entry}${prior.slice(idx + 1)}`);
2389
2485
  } else {
2390
- fs11.writeFileSync(p, `${header}${entry}${prior}`);
2486
+ fs12.writeFileSync(p, `${header}${entry}${prior}`);
2391
2487
  }
2392
2488
  } else {
2393
- fs11.writeFileSync(p, `${header}${entry}`);
2489
+ fs12.writeFileSync(p, `${header}${entry}`);
2394
2490
  }
2395
2491
  }
2396
2492
  function git3(args, cwd, timeout = 6e4) {
@@ -2441,13 +2537,13 @@ var releaseFlow = async (ctx) => {
2441
2537
  };
2442
2538
  async function runPrepare(args) {
2443
2539
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
2444
- const pkgPath = path10.join(cwd, "package.json");
2445
- if (!fs11.existsSync(pkgPath)) {
2540
+ const pkgPath = path11.join(cwd, "package.json");
2541
+ if (!fs12.existsSync(pkgPath)) {
2446
2542
  ctx.output.exitCode = 99;
2447
2543
  ctx.output.reason = "release prepare: package.json not found";
2448
2544
  return;
2449
2545
  }
2450
- const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2546
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2451
2547
  if (typeof pkg.version !== "string") {
2452
2548
  ctx.output.exitCode = 99;
2453
2549
  ctx.output.reason = "release prepare: package.json has no version";
@@ -2518,8 +2614,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
2518
2614
  }
2519
2615
  async function runFinalize(args) {
2520
2616
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2521
- const pkgPath = path10.join(cwd, "package.json");
2522
- const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2617
+ const pkgPath = path11.join(cwd, "package.json");
2618
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2523
2619
  if (typeof pkg.version !== "string") {
2524
2620
  ctx.output.exitCode = 99;
2525
2621
  ctx.output.reason = "release finalize: package.json has no version";
@@ -2790,7 +2886,7 @@ import { spawn as spawn2 } from "child_process";
2790
2886
  var TAIL_CHARS = 4e3;
2791
2887
  var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
2792
2888
  function runCommand(command, cwd) {
2793
- return new Promise((resolve2) => {
2889
+ return new Promise((resolve3) => {
2794
2890
  const start = Date.now();
2795
2891
  const child = spawn2(command, {
2796
2892
  cwd,
@@ -2819,11 +2915,11 @@ function runCommand(command, cwd) {
2819
2915
  child.on("exit", (code) => {
2820
2916
  clearTimeout(timer);
2821
2917
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
2822
- resolve2({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
2918
+ resolve3({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
2823
2919
  });
2824
2920
  child.on("error", (err) => {
2825
2921
  clearTimeout(timer);
2826
- resolve2({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
2922
+ resolve3({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
2827
2923
  });
2828
2924
  });
2829
2925
  }
@@ -2949,7 +3045,7 @@ var watchStalePrsFlow = async (ctx) => {
2949
3045
  };
2950
3046
 
2951
3047
  // src/scripts/writeRunSummary.ts
2952
- import * as fs12 from "fs";
3048
+ import * as fs13 from "fs";
2953
3049
  var writeRunSummary = async (ctx, profile) => {
2954
3050
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
2955
3051
  if (!summaryPath) return;
@@ -2971,7 +3067,7 @@ var writeRunSummary = async (ctx, profile) => {
2971
3067
  if (reason) lines.push(`- **Reason:** ${reason}`);
2972
3068
  lines.push("");
2973
3069
  try {
2974
- fs12.appendFileSync(summaryPath, `${lines.join("\n")}
3070
+ fs13.appendFileSync(summaryPath, `${lines.join("\n")}
2975
3071
  `);
2976
3072
  } catch {
2977
3073
  }
@@ -2991,6 +3087,7 @@ var preflightScripts = {
2991
3087
  loadIssueContext,
2992
3088
  loadConventions,
2993
3089
  loadCoverageRules,
3090
+ buildSyntheticPlugin,
2994
3091
  composePrompt
2995
3092
  };
2996
3093
  var postflightScripts = {
@@ -3111,19 +3208,27 @@ async function runExecutable(profileName, input) {
3111
3208
  data: {},
3112
3209
  output: { exitCode: 0 }
3113
3210
  };
3114
- const ndjsonDir = path11.join(input.cwd, ".kody2");
3115
- const invokeAgent = async (prompt) => runAgent({
3116
- prompt,
3117
- model,
3118
- cwd: input.cwd,
3119
- litellmUrl: litellm?.url ?? null,
3120
- verbose: input.verbose,
3121
- quiet: input.quiet,
3122
- ndjsonDir,
3123
- allowedToolsOverride: profile.claudeCode.tools,
3124
- permissionModeOverride: profile.claudeCode.permissionMode,
3125
- mcpServers: profile.claudeCode.mcpServers
3126
- });
3211
+ const ndjsonDir = path12.join(input.cwd, ".kody2");
3212
+ const invokeAgent = async (prompt) => {
3213
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path12.isAbsolute(p) ? p : path12.resolve(profile.dir, p)).filter((p) => p.length > 0);
3214
+ const syntheticPath = ctx.data.syntheticPluginPath;
3215
+ const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
3216
+ return runAgent({
3217
+ prompt,
3218
+ model,
3219
+ cwd: input.cwd,
3220
+ litellmUrl: litellm?.url ?? null,
3221
+ verbose: input.verbose,
3222
+ quiet: input.quiet,
3223
+ ndjsonDir,
3224
+ allowedToolsOverride: profile.claudeCode.tools,
3225
+ permissionModeOverride: profile.claudeCode.permissionMode,
3226
+ mcpServers: profile.claudeCode.mcpServers,
3227
+ pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
3228
+ maxTurns: profile.claudeCode.maxTurns,
3229
+ systemPromptAppend: profile.claudeCode.systemPromptAppend
3230
+ });
3231
+ };
3127
3232
  ctx.data.__invokeAgent = invokeAgent;
3128
3233
  try {
3129
3234
  for (const entry of profile.scripts.preflight) {
@@ -3170,17 +3275,17 @@ async function runExecutable(profileName, input) {
3170
3275
  }
3171
3276
  }
3172
3277
  function resolveProfilePath(profileName) {
3173
- const here = path11.dirname(new URL(import.meta.url).pathname);
3278
+ const here = path12.dirname(new URL(import.meta.url).pathname);
3174
3279
  const candidates = [
3175
- path11.join(here, "executables", profileName, "profile.json"),
3280
+ path12.join(here, "executables", profileName, "profile.json"),
3176
3281
  // same-dir sibling (dev)
3177
- path11.join(here, "..", "executables", profileName, "profile.json"),
3282
+ path12.join(here, "..", "executables", profileName, "profile.json"),
3178
3283
  // up one (prod: dist/bin → dist/executables)
3179
- path11.join(here, "..", "src", "executables", profileName, "profile.json")
3284
+ path12.join(here, "..", "src", "executables", profileName, "profile.json")
3180
3285
  // fallback
3181
3286
  ];
3182
3287
  for (const c of candidates) {
3183
- if (fs13.existsSync(c)) return c;
3288
+ if (fs14.existsSync(c)) return c;
3184
3289
  }
3185
3290
  return candidates[0];
3186
3291
  }
@@ -3273,11 +3378,11 @@ function finish(out) {
3273
3378
 
3274
3379
  // src/kody2-cli.ts
3275
3380
  import { execFileSync as execFileSync14 } from "child_process";
3276
- import * as fs15 from "fs";
3277
- import * as path12 from "path";
3381
+ import * as fs16 from "fs";
3382
+ import * as path13 from "path";
3278
3383
 
3279
3384
  // src/dispatch.ts
3280
- import * as fs14 from "fs";
3385
+ import * as fs15 from "fs";
3281
3386
  function autoDispatch(opts) {
3282
3387
  const explicit = opts?.explicit;
3283
3388
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
@@ -3289,10 +3394,10 @@ function autoDispatch(opts) {
3289
3394
  }
3290
3395
  const eventName = process.env.GITHUB_EVENT_NAME;
3291
3396
  const eventPath = process.env.GITHUB_EVENT_PATH;
3292
- if (!eventName || !eventPath || !fs14.existsSync(eventPath)) return null;
3397
+ if (!eventName || !eventPath || !fs15.existsSync(eventPath)) return null;
3293
3398
  let event = {};
3294
3399
  try {
3295
- event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
3400
+ event = JSON.parse(fs15.readFileSync(eventPath, "utf-8"));
3296
3401
  } catch {
3297
3402
  return null;
3298
3403
  }
@@ -3442,9 +3547,9 @@ function resolveAuthToken(env = process.env) {
3442
3547
  return token;
3443
3548
  }
3444
3549
  function detectPackageManager2(cwd) {
3445
- if (fs15.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3446
- if (fs15.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
3447
- if (fs15.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
3550
+ if (fs16.existsSync(path13.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3551
+ if (fs16.existsSync(path13.join(cwd, "yarn.lock"))) return "yarn";
3552
+ if (fs16.existsSync(path13.join(cwd, "bun.lockb"))) return "bun";
3448
3553
  return "npm";
3449
3554
  }
3450
3555
  function shellOut(cmd, args, cwd, stream = true) {
@@ -3521,11 +3626,11 @@ function configureGitIdentity(cwd) {
3521
3626
  }
3522
3627
  function postFailureTail(issueNumber, cwd, reason) {
3523
3628
  if (!issueNumber) return;
3524
- const logPath = path12.join(cwd, ".kody2", "last-run.jsonl");
3629
+ const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
3525
3630
  let tail = "";
3526
3631
  try {
3527
- if (fs15.existsSync(logPath)) {
3528
- const content = fs15.readFileSync(logPath, "utf-8");
3632
+ if (fs16.existsSync(logPath)) {
3633
+ const content = fs16.readFileSync(logPath, "utf-8");
3529
3634
  tail = content.slice(-3e3);
3530
3635
  }
3531
3636
  } catch {
@@ -3550,7 +3655,7 @@ async function runCi(argv) {
3550
3655
  return 0;
3551
3656
  }
3552
3657
  const args = parseCiArgs(argv);
3553
- const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
3658
+ const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
3554
3659
  let earlyConfig;
3555
3660
  try {
3556
3661
  earlyConfig = loadConfig(cwd);