@kody-ade/kody-engine 0.2.11 → 0.2.13

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.
Files changed (28) hide show
  1. package/README.md +5 -4
  2. package/dist/bin/kody2.js +309 -243
  3. package/dist/executables/fix/profile.json +93 -0
  4. package/dist/executables/fix-ci/profile.json +93 -0
  5. package/dist/executables/init/profile.json +4 -6
  6. package/dist/executables/orchestrator/profile.json +28 -15
  7. package/dist/executables/plan/profile.json +33 -16
  8. package/dist/executables/plan-verify/profile.json +37 -0
  9. package/dist/executables/plan-verify/prompt.md +53 -0
  10. package/dist/executables/release/profile.json +13 -8
  11. package/dist/executables/resolve/profile.json +80 -0
  12. package/dist/executables/review/profile.json +19 -10
  13. package/dist/executables/run/profile.json +86 -0
  14. package/dist/executables/types.ts +13 -11
  15. package/dist/executables/watch-stale-prs/profile.json +4 -7
  16. package/dist/plugins/commands/kody-live-probe.md +9 -0
  17. package/dist/plugins/hooks/kody-live-trace.json +17 -0
  18. package/dist/plugins/skills/kody-live-marker/SKILL.md +18 -0
  19. package/dist/plugins/test-plugin/.claude-plugin/plugin.json +6 -0
  20. package/dist/plugins/test-plugin/skills/kody-plugin-marker/SKILL.md +16 -0
  21. package/package.json +2 -2
  22. package/dist/executables/build/profile.json +0 -99
  23. package/dist/executables/orchestrator/prompts/orchestrator.md +0 -56
  24. package/dist/executables/plan/prompts/plan.md +0 -42
  25. /package/dist/executables/{build/prompts/fix.md → fix/prompt.md} +0 -0
  26. /package/dist/executables/{build/prompts/fix-ci.md → fix-ci/prompt.md} +0 -0
  27. /package/dist/executables/{build/prompts/resolve.md → resolve/prompt.md} +0 -0
  28. /package/dist/executables/{build/prompts/run.md → run/prompt.md} +0 -0
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.11",
6
+ version: "0.2.13",
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 "require('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",
@@ -49,6 +49,15 @@ var package_default = {
49
49
  bugs: "https://github.com/aharonyaircohen/kody-engine/issues"
50
50
  };
51
51
 
52
+ // src/executor.ts
53
+ import * as fs14 from "fs";
54
+ import * as path12 from "path";
55
+
56
+ // src/agent.ts
57
+ import * as fs2 from "fs";
58
+ import * as path2 from "path";
59
+ import { query } from "@anthropic-ai/claude-agent-sdk";
60
+
52
61
  // src/config.ts
53
62
  import * as fs from "fs";
54
63
  import * as path from "path";
@@ -151,15 +160,6 @@ function getAnthropicApiKeyOrDummy() {
151
160
  return process.env.ANTHROPIC_API_KEY || `sk-ant-api03-${"0".repeat(64)}`;
152
161
  }
153
162
 
154
- // src/executor.ts
155
- import * as fs13 from "fs";
156
- import * as path11 from "path";
157
-
158
- // src/agent.ts
159
- import * as fs2 from "fs";
160
- import * as path2 from "path";
161
- import { query } from "@anthropic-ai/claude-agent-sdk";
162
-
163
163
  // src/format.ts
164
164
  function renderEvent(msg, opts = {}) {
165
165
  if (opts.quiet) {
@@ -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
  }
@@ -919,13 +1015,14 @@ function hasCommitsAhead(branch, defaultBranch, cwd) {
919
1015
  }
920
1016
 
921
1017
  // src/scripts/commitAndPush.ts
922
- var commitAndPush2 = async (ctx) => {
1018
+ var commitAndPush2 = async (ctx, profile) => {
923
1019
  const branch = ctx.data.branch;
924
1020
  if (!branch) {
925
1021
  ctx.data.commitResult = { committed: false, pushed: false };
926
1022
  return;
927
1023
  }
928
- if (ctx.args.mode === "resolve") {
1024
+ const kind = profile.name;
1025
+ if (kind === "resolve") {
929
1026
  try {
930
1027
  execFileSync4("git", ["add", "-A"], { cwd: ctx.cwd, env: { ...process.env, HUSKY: "0" }, stdio: "pipe" });
931
1028
  } catch {
@@ -937,7 +1034,7 @@ var commitAndPush2 = async (ctx) => {
937
1034
  `);
938
1035
  }
939
1036
  }
940
- const fallbackMsg = defaultCommitMessage(ctx.args.mode, ctx.data);
1037
+ const fallbackMsg = defaultCommitMessage(kind, ctx.data);
941
1038
  const message = ctx.data.commitMessage || fallbackMsg;
942
1039
  try {
943
1040
  const result = commitAndPush(branch, message, ctx.cwd);
@@ -965,20 +1062,20 @@ function defaultCommitMessage(mode, data) {
965
1062
  }
966
1063
 
967
1064
  // src/scripts/composePrompt.ts
968
- import * as fs7 from "fs";
969
- import * as path7 from "path";
1065
+ import * as fs8 from "fs";
1066
+ import * as path8 from "path";
970
1067
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
971
1068
  var composePrompt = async (ctx, profile) => {
972
1069
  const explicit = ctx.data.promptTemplate;
973
1070
  const mode = ctx.args.mode;
974
1071
  const candidates = [
975
- explicit ? path7.join(profile.dir, explicit) : null,
976
- mode ? path7.join(profile.dir, "prompts", `${mode}.md`) : null,
977
- 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")
978
1075
  ].filter(Boolean);
979
1076
  let templatePath = "";
980
1077
  for (const c of candidates) {
981
- if (fs7.existsSync(c)) {
1078
+ if (fs8.existsSync(c)) {
982
1079
  templatePath = c;
983
1080
  break;
984
1081
  }
@@ -986,7 +1083,7 @@ var composePrompt = async (ctx, profile) => {
986
1083
  if (!templatePath) {
987
1084
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
988
1085
  }
989
- const template = fs7.readFileSync(templatePath, "utf-8");
1086
+ const template = fs8.readFileSync(templatePath, "utf-8");
990
1087
  const tokens = {
991
1088
  ...stringifyAll(ctx.args, "args."),
992
1089
  ...stringifyAll(ctx.data, ""),
@@ -1458,7 +1555,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
1458
1555
 
1459
1556
  // src/gha.ts
1460
1557
  import { execFileSync as execFileSync7 } from "child_process";
1461
- import * as fs8 from "fs";
1558
+ import * as fs9 from "fs";
1462
1559
  function getRunUrl() {
1463
1560
  const server = process.env.GITHUB_SERVER_URL;
1464
1561
  const repo = process.env.GITHUB_REPOSITORY;
@@ -1469,10 +1566,10 @@ function getRunUrl() {
1469
1566
  function reactToTriggerComment(cwd) {
1470
1567
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
1471
1568
  const eventPath = process.env.GITHUB_EVENT_PATH;
1472
- if (!eventPath || !fs8.existsSync(eventPath)) return;
1569
+ if (!eventPath || !fs9.existsSync(eventPath)) return;
1473
1570
  let event = null;
1474
1571
  try {
1475
- event = JSON.parse(fs8.readFileSync(eventPath, "utf-8"));
1572
+ event = JSON.parse(fs9.readFileSync(eventPath, "utf-8"));
1476
1573
  } catch {
1477
1574
  return;
1478
1575
  }
@@ -1671,35 +1768,35 @@ function tryPostPr2(prNumber, body, cwd) {
1671
1768
 
1672
1769
  // src/scripts/initFlow.ts
1673
1770
  import { execFileSync as execFileSync9 } from "child_process";
1674
- import * as fs10 from "fs";
1675
- import * as path9 from "path";
1771
+ import * as fs11 from "fs";
1772
+ import * as path10 from "path";
1676
1773
 
1677
1774
  // src/registry.ts
1678
- import * as fs9 from "fs";
1679
- import * as path8 from "path";
1775
+ import * as fs10 from "fs";
1776
+ import * as path9 from "path";
1680
1777
  function getExecutablesRoot() {
1681
- const here = path8.dirname(new URL(import.meta.url).pathname);
1778
+ const here = path9.dirname(new URL(import.meta.url).pathname);
1682
1779
  const candidates = [
1683
- path8.join(here, "executables"),
1780
+ path9.join(here, "executables"),
1684
1781
  // dev: src/
1685
- path8.join(here, "..", "executables"),
1782
+ path9.join(here, "..", "executables"),
1686
1783
  // built: dist/bin → dist/executables
1687
- path8.join(here, "..", "src", "executables")
1784
+ path9.join(here, "..", "src", "executables")
1688
1785
  // fallback
1689
1786
  ];
1690
1787
  for (const c of candidates) {
1691
- if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1788
+ if (fs10.existsSync(c) && fs10.statSync(c).isDirectory()) return c;
1692
1789
  }
1693
1790
  return candidates[0];
1694
1791
  }
1695
1792
  function listExecutables(root = getExecutablesRoot()) {
1696
- if (!fs9.existsSync(root)) return [];
1697
- const entries = fs9.readdirSync(root, { withFileTypes: true });
1793
+ if (!fs10.existsSync(root)) return [];
1794
+ const entries = fs10.readdirSync(root, { withFileTypes: true });
1698
1795
  const out = [];
1699
1796
  for (const ent of entries) {
1700
1797
  if (!ent.isDirectory()) continue;
1701
- const profilePath = path8.join(root, ent.name, "profile.json");
1702
- 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()) {
1703
1800
  out.push({ name: ent.name, profilePath });
1704
1801
  }
1705
1802
  }
@@ -1707,8 +1804,8 @@ function listExecutables(root = getExecutablesRoot()) {
1707
1804
  }
1708
1805
  function hasExecutable(name, root = getExecutablesRoot()) {
1709
1806
  if (!isSafeName(name)) return false;
1710
- const profilePath = path8.join(root, name, "profile.json");
1711
- 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();
1712
1809
  }
1713
1810
  function isSafeName(name) {
1714
1811
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
@@ -1724,11 +1821,11 @@ function parseGenericFlags(argv) {
1724
1821
  }
1725
1822
  const key = arg.slice(2);
1726
1823
  const next = argv[i + 1];
1727
- if (next !== void 0 && !next.startsWith("--")) {
1728
- args[key] = next;
1729
- i++;
1730
- } else {
1731
- args[key] = true;
1824
+ const value = next !== void 0 && !next.startsWith("--") ? (i++, next) : true;
1825
+ args[key] = value;
1826
+ if (key.includes("-")) {
1827
+ const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1828
+ if (camel !== key && args[camel] === void 0) args[camel] = value;
1732
1829
  }
1733
1830
  }
1734
1831
  if (positional.length > 0) args._ = positional;
@@ -1737,9 +1834,9 @@ function parseGenericFlags(argv) {
1737
1834
 
1738
1835
  // src/scripts/initFlow.ts
1739
1836
  function detectPackageManager(cwd) {
1740
- if (fs10.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1741
- if (fs10.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
1742
- 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";
1743
1840
  return "npm";
1744
1841
  }
1745
1842
  function qualityCommandsFor(pm) {
@@ -1860,22 +1957,22 @@ function performInit(cwd, force) {
1860
1957
  const pm = detectPackageManager(cwd);
1861
1958
  const ownerRepo = detectOwnerRepo(cwd);
1862
1959
  const defaultBranch = defaultBranchFromGit(cwd);
1863
- const configPath = path9.join(cwd, "kody.config.json");
1864
- if (fs10.existsSync(configPath) && !force) {
1960
+ const configPath = path10.join(cwd, "kody.config.json");
1961
+ if (fs11.existsSync(configPath) && !force) {
1865
1962
  skipped.push("kody.config.json");
1866
1963
  } else {
1867
1964
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
1868
- fs10.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1965
+ fs11.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1869
1966
  `);
1870
1967
  wrote.push("kody.config.json");
1871
1968
  }
1872
- const workflowDir = path9.join(cwd, ".github", "workflows");
1873
- const workflowPath = path9.join(workflowDir, "kody2.yml");
1874
- 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) {
1875
1972
  skipped.push(".github/workflows/kody2.yml");
1876
1973
  } else {
1877
- fs10.mkdirSync(workflowDir, { recursive: true });
1878
- fs10.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1974
+ fs11.mkdirSync(workflowDir, { recursive: true });
1975
+ fs11.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1879
1976
  wrote.push(".github/workflows/kody2.yml");
1880
1977
  }
1881
1978
  for (const exe of listExecutables()) {
@@ -1886,12 +1983,12 @@ function performInit(cwd, force) {
1886
1983
  continue;
1887
1984
  }
1888
1985
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
1889
- const target = path9.join(workflowDir, `kody2-${exe.name}.yml`);
1890
- if (fs10.existsSync(target) && !force) {
1986
+ const target = path10.join(workflowDir, `kody2-${exe.name}.yml`);
1987
+ if (fs11.existsSync(target) && !force) {
1891
1988
  skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
1892
1989
  continue;
1893
1990
  }
1894
- fs10.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
1991
+ fs11.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
1895
1992
  wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
1896
1993
  }
1897
1994
  return { wrote, skipped };
@@ -2307,8 +2404,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
2307
2404
 
2308
2405
  // src/scripts/releaseFlow.ts
2309
2406
  import { execFileSync as execFileSync11, spawnSync } from "child_process";
2310
- import * as fs11 from "fs";
2311
- import * as path10 from "path";
2407
+ import * as fs12 from "fs";
2408
+ import * as path11 from "path";
2312
2409
  function bumpVersion(current, bump) {
2313
2410
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2314
2411
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -2324,12 +2421,12 @@ function bumpVersion(current, bump) {
2324
2421
  return `${major}.${minor}.${patch}`;
2325
2422
  }
2326
2423
  function updateVersionInFile(file, newVersion, cwd) {
2327
- const abs = path10.join(cwd, file);
2328
- if (!fs11.existsSync(abs)) return false;
2329
- 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");
2330
2427
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2331
2428
  if (updated === content) return false;
2332
- fs11.writeFileSync(abs, updated);
2429
+ fs12.writeFileSync(abs, updated);
2333
2430
  return true;
2334
2431
  }
2335
2432
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2377,19 +2474,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2377
2474
  return parts.join("\n");
2378
2475
  }
2379
2476
  function prependChangelog(cwd, entry) {
2380
- const p = path10.join(cwd, "CHANGELOG.md");
2477
+ const p = path11.join(cwd, "CHANGELOG.md");
2381
2478
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2382
- if (fs11.existsSync(p)) {
2383
- const prior = fs11.readFileSync(p, "utf-8");
2479
+ if (fs12.existsSync(p)) {
2480
+ const prior = fs12.readFileSync(p, "utf-8");
2384
2481
  if (/^#\s*Changelog\b/m.test(prior)) {
2385
2482
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2386
- fs11.writeFileSync(p, `${prior.slice(0, idx + 1)}
2483
+ fs12.writeFileSync(p, `${prior.slice(0, idx + 1)}
2387
2484
  ${entry}${prior.slice(idx + 1)}`);
2388
2485
  } else {
2389
- fs11.writeFileSync(p, `${header}${entry}${prior}`);
2486
+ fs12.writeFileSync(p, `${header}${entry}${prior}`);
2390
2487
  }
2391
2488
  } else {
2392
- fs11.writeFileSync(p, `${header}${entry}`);
2489
+ fs12.writeFileSync(p, `${header}${entry}`);
2393
2490
  }
2394
2491
  }
2395
2492
  function git3(args, cwd, timeout = 6e4) {
@@ -2440,13 +2537,13 @@ var releaseFlow = async (ctx) => {
2440
2537
  };
2441
2538
  async function runPrepare(args) {
2442
2539
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
2443
- const pkgPath = path10.join(cwd, "package.json");
2444
- if (!fs11.existsSync(pkgPath)) {
2540
+ const pkgPath = path11.join(cwd, "package.json");
2541
+ if (!fs12.existsSync(pkgPath)) {
2445
2542
  ctx.output.exitCode = 99;
2446
2543
  ctx.output.reason = "release prepare: package.json not found";
2447
2544
  return;
2448
2545
  }
2449
- const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2546
+ const pkg = JSON.parse(fs12.readFileSync(pkgPath, "utf-8"));
2450
2547
  if (typeof pkg.version !== "string") {
2451
2548
  ctx.output.exitCode = 99;
2452
2549
  ctx.output.reason = "release prepare: package.json has no version";
@@ -2517,8 +2614,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
2517
2614
  }
2518
2615
  async function runFinalize(args) {
2519
2616
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2520
- const pkgPath = path10.join(cwd, "package.json");
2521
- 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"));
2522
2619
  if (typeof pkg.version !== "string") {
2523
2620
  ctx.output.exitCode = 99;
2524
2621
  ctx.output.reason = "release finalize: package.json has no version";
@@ -2789,7 +2886,7 @@ import { spawn as spawn2 } from "child_process";
2789
2886
  var TAIL_CHARS = 4e3;
2790
2887
  var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
2791
2888
  function runCommand(command, cwd) {
2792
- return new Promise((resolve2) => {
2889
+ return new Promise((resolve3) => {
2793
2890
  const start = Date.now();
2794
2891
  const child = spawn2(command, {
2795
2892
  cwd,
@@ -2818,11 +2915,11 @@ function runCommand(command, cwd) {
2818
2915
  child.on("exit", (code) => {
2819
2916
  clearTimeout(timer);
2820
2917
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
2821
- resolve2({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
2918
+ resolve3({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
2822
2919
  });
2823
2920
  child.on("error", (err) => {
2824
2921
  clearTimeout(timer);
2825
- resolve2({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
2922
+ resolve3({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
2826
2923
  });
2827
2924
  });
2828
2925
  }
@@ -2948,11 +3045,11 @@ var watchStalePrsFlow = async (ctx) => {
2948
3045
  };
2949
3046
 
2950
3047
  // src/scripts/writeRunSummary.ts
2951
- import * as fs12 from "fs";
2952
- var writeRunSummary = async (ctx) => {
3048
+ import * as fs13 from "fs";
3049
+ var writeRunSummary = async (ctx, profile) => {
2953
3050
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
2954
3051
  if (!summaryPath) return;
2955
- const mode = ctx.args.mode;
3052
+ const executable = profile.name;
2956
3053
  const issue = ctx.args.issue;
2957
3054
  const pr = ctx.args.pr;
2958
3055
  const target = issue ? `issue #${issue}` : pr ? `PR #${pr}` : "(unknown)";
@@ -2961,16 +3058,16 @@ var writeRunSummary = async (ctx) => {
2961
3058
  const reason = ctx.output.reason;
2962
3059
  const status = exitCode === 0 ? "\u2705 success" : exitCode === 3 ? "\u23ED\uFE0F no-op" : "\u26A0\uFE0F failed";
2963
3060
  const lines = [];
2964
- lines.push(`## kody2 run \u2014 ${status}`);
3061
+ lines.push(`## kody2 ${executable} \u2014 ${status}`);
2965
3062
  lines.push("");
2966
- lines.push(`- **Mode:** \`${mode ?? "?"}\``);
3063
+ lines.push(`- **Executable:** \`${executable}\``);
2967
3064
  lines.push(`- **Target:** ${target}`);
2968
3065
  if (prUrl) lines.push(`- **PR:** ${prUrl}`);
2969
3066
  lines.push(`- **Exit code:** ${exitCode}`);
2970
3067
  if (reason) lines.push(`- **Reason:** ${reason}`);
2971
3068
  lines.push("");
2972
3069
  try {
2973
- fs12.appendFileSync(summaryPath, `${lines.join("\n")}
3070
+ fs13.appendFileSync(summaryPath, `${lines.join("\n")}
2974
3071
  `);
2975
3072
  } catch {
2976
3073
  }
@@ -2990,6 +3087,7 @@ var preflightScripts = {
2990
3087
  loadIssueContext,
2991
3088
  loadConventions,
2992
3089
  loadCoverageRules,
3090
+ buildSyntheticPlugin,
2993
3091
  composePrompt
2994
3092
  };
2995
3093
  var postflightScripts = {
@@ -3068,7 +3166,24 @@ async function runExecutable(profileName, input) {
3068
3166
  if (firstFail) {
3069
3167
  return finish({ exitCode: 99, reason: `required CLI tool check failed: ${firstFail.error}` });
3070
3168
  }
3071
- const modelSpec = profile.claudeCode.model === "inherit" ? input.config.agent.model : profile.claudeCode.model;
3169
+ let config;
3170
+ if (input.config) {
3171
+ config = input.config;
3172
+ } else if (input.skipConfig) {
3173
+ config = {
3174
+ quality: { typecheck: "", lint: "", testUnit: "" },
3175
+ git: { defaultBranch: "main" },
3176
+ github: { owner: "", repo: "" },
3177
+ agent: { model: "claude/claude-haiku-4-5-20251001" }
3178
+ };
3179
+ } else {
3180
+ try {
3181
+ config = loadConfig(input.cwd);
3182
+ } catch (err) {
3183
+ return finish({ exitCode: 99, reason: `config error: ${err instanceof Error ? err.message : String(err)}` });
3184
+ }
3185
+ }
3186
+ const modelSpec = profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
3072
3187
  let model;
3073
3188
  try {
3074
3189
  model = parseProviderModel(modelSpec);
@@ -3087,25 +3202,33 @@ async function runExecutable(profileName, input) {
3087
3202
  const ctx = {
3088
3203
  args,
3089
3204
  cwd: input.cwd,
3090
- config: input.config,
3205
+ config,
3091
3206
  verbose: input.verbose,
3092
3207
  quiet: input.quiet,
3093
3208
  data: {},
3094
3209
  output: { exitCode: 0 }
3095
3210
  };
3096
- const ndjsonDir = path11.join(input.cwd, ".kody2");
3097
- const invokeAgent = async (prompt) => runAgent({
3098
- prompt,
3099
- model,
3100
- cwd: input.cwd,
3101
- litellmUrl: litellm?.url ?? null,
3102
- verbose: input.verbose,
3103
- quiet: input.quiet,
3104
- ndjsonDir,
3105
- allowedToolsOverride: profile.claudeCode.tools,
3106
- permissionModeOverride: profile.claudeCode.permissionMode,
3107
- mcpServers: profile.claudeCode.mcpServers
3108
- });
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
+ };
3109
3232
  ctx.data.__invokeAgent = invokeAgent;
3110
3233
  try {
3111
3234
  for (const entry of profile.scripts.preflight) {
@@ -3152,22 +3275,36 @@ async function runExecutable(profileName, input) {
3152
3275
  }
3153
3276
  }
3154
3277
  function resolveProfilePath(profileName) {
3155
- const here = path11.dirname(new URL(import.meta.url).pathname);
3278
+ const here = path12.dirname(new URL(import.meta.url).pathname);
3156
3279
  const candidates = [
3157
- path11.join(here, "executables", profileName, "profile.json"),
3280
+ path12.join(here, "executables", profileName, "profile.json"),
3158
3281
  // same-dir sibling (dev)
3159
- path11.join(here, "..", "executables", profileName, "profile.json"),
3282
+ path12.join(here, "..", "executables", profileName, "profile.json"),
3160
3283
  // up one (prod: dist/bin → dist/executables)
3161
- path11.join(here, "..", "src", "executables", profileName, "profile.json")
3284
+ path12.join(here, "..", "src", "executables", profileName, "profile.json")
3162
3285
  // fallback
3163
3286
  ];
3164
3287
  for (const c of candidates) {
3165
- if (fs13.existsSync(c)) return c;
3288
+ if (fs14.existsSync(c)) return c;
3166
3289
  }
3167
3290
  return candidates[0];
3168
3291
  }
3169
3292
  function validateInputs(specs, raw) {
3170
3293
  const out = {};
3294
+ const allowedKeys = /* @__PURE__ */ new Set(["_", "cwd", "verbose", "quiet"]);
3295
+ for (const spec of specs) {
3296
+ const flagKey = spec.flag.replace(/^--/, "");
3297
+ allowedKeys.add(spec.name);
3298
+ allowedKeys.add(flagKey);
3299
+ if (flagKey.includes("-")) {
3300
+ allowedKeys.add(flagKey.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase()));
3301
+ }
3302
+ }
3303
+ for (const key of Object.keys(raw)) {
3304
+ if (!allowedKeys.has(key)) {
3305
+ throw new Error(`unknown arg: --${key}`);
3306
+ }
3307
+ }
3171
3308
  for (const spec of specs) {
3172
3309
  const v = raw[spec.name];
3173
3310
  if (v === void 0 || v === null) continue;
@@ -3241,33 +3378,33 @@ function finish(out) {
3241
3378
 
3242
3379
  // src/kody2-cli.ts
3243
3380
  import { execFileSync as execFileSync14 } from "child_process";
3244
- import * as fs15 from "fs";
3245
- import * as path12 from "path";
3381
+ import * as fs16 from "fs";
3382
+ import * as path13 from "path";
3246
3383
 
3247
3384
  // src/dispatch.ts
3248
- import * as fs14 from "fs";
3385
+ import * as fs15 from "fs";
3249
3386
  function autoDispatch(opts) {
3250
3387
  const explicit = opts?.explicit;
3251
3388
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
3252
3389
  return {
3253
- executable: "build",
3254
- cliArgs: { mode: "run", issue: explicit.issueNumber },
3390
+ executable: "run",
3391
+ cliArgs: { issue: explicit.issueNumber },
3255
3392
  target: explicit.issueNumber
3256
3393
  };
3257
3394
  }
3258
3395
  const eventName = process.env.GITHUB_EVENT_NAME;
3259
3396
  const eventPath = process.env.GITHUB_EVENT_PATH;
3260
- if (!eventName || !eventPath || !fs14.existsSync(eventPath)) return null;
3397
+ if (!eventName || !eventPath || !fs15.existsSync(eventPath)) return null;
3261
3398
  let event = {};
3262
3399
  try {
3263
- event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
3400
+ event = JSON.parse(fs15.readFileSync(eventPath, "utf-8"));
3264
3401
  } catch {
3265
3402
  return null;
3266
3403
  }
3267
3404
  if (eventName === "workflow_dispatch") {
3268
3405
  const n = parseInt(String(event.inputs?.issue_number ?? ""), 10);
3269
3406
  if (!Number.isNaN(n) && n > 0) {
3270
- return { executable: "build", cliArgs: { mode: "run", issue: n }, target: n };
3407
+ return { executable: "run", cliArgs: { issue: n }, target: n };
3271
3408
  }
3272
3409
  return null;
3273
3410
  }
@@ -3279,35 +3416,35 @@ function autoDispatch(opts) {
3279
3416
  const afterTag = extractAfterTag(body);
3280
3417
  if (isPr) {
3281
3418
  if (/\bfix-ci\b/.test(afterTag)) {
3282
- return { executable: "build", cliArgs: { mode: "fix-ci", pr: targetNum }, target: targetNum };
3419
+ return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
3283
3420
  }
3284
3421
  if (/\bresolve\b/.test(afterTag)) {
3285
- return { executable: "build", cliArgs: { mode: "resolve", pr: targetNum }, target: targetNum };
3422
+ return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
3423
+ }
3424
+ if (/\breview\b/.test(afterTag)) {
3425
+ return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
3286
3426
  }
3287
3427
  const feedback = extractFeedback(afterTag);
3288
3428
  return {
3289
- executable: "build",
3290
- cliArgs: { mode: "fix", pr: targetNum, ...feedback ? { feedback } : {} },
3429
+ executable: "fix",
3430
+ cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
3291
3431
  target: targetNum
3292
3432
  };
3293
3433
  }
3294
3434
  const sub = extractSubcommand(afterTag);
3295
- const defaultExec = opts?.config?.defaultExecutable ?? "build";
3435
+ const defaultExec = opts?.config?.defaultExecutable ?? "run";
3296
3436
  if (!sub) {
3297
3437
  return asDispatch(defaultExec, targetNum);
3298
3438
  }
3299
- if (sub === "build") {
3300
- return { executable: "build", cliArgs: { mode: "run", issue: targetNum }, target: targetNum };
3301
- }
3302
3439
  if (sub === "orchestrate" || sub === "orchestrator") {
3303
3440
  return { executable: "orchestrator", cliArgs: { issue: targetNum }, target: targetNum };
3304
3441
  }
3442
+ if (sub === "build") {
3443
+ return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
3444
+ }
3305
3445
  return asDispatch(sub, targetNum);
3306
3446
  }
3307
3447
  function asDispatch(executable, target) {
3308
- if (executable === "build") {
3309
- return { executable, cliArgs: { mode: "run", issue: target }, target };
3310
- }
3311
3448
  return { executable, cliArgs: { issue: target }, target };
3312
3449
  }
3313
3450
  function extractAfterTag(body) {
@@ -3410,9 +3547,9 @@ function resolveAuthToken(env = process.env) {
3410
3547
  return token;
3411
3548
  }
3412
3549
  function detectPackageManager2(cwd) {
3413
- if (fs15.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3414
- if (fs15.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
3415
- 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";
3416
3553
  return "npm";
3417
3554
  }
3418
3555
  function shellOut(cmd, args, cwd, stream = true) {
@@ -3489,11 +3626,11 @@ function configureGitIdentity(cwd) {
3489
3626
  }
3490
3627
  function postFailureTail(issueNumber, cwd, reason) {
3491
3628
  if (!issueNumber) return;
3492
- const logPath = path12.join(cwd, ".kody2", "last-run.jsonl");
3629
+ const logPath = path13.join(cwd, ".kody2", "last-run.jsonl");
3493
3630
  let tail = "";
3494
3631
  try {
3495
- if (fs15.existsSync(logPath)) {
3496
- const content = fs15.readFileSync(logPath, "utf-8");
3632
+ if (fs16.existsSync(logPath)) {
3633
+ const content = fs16.readFileSync(logPath, "utf-8");
3497
3634
  tail = content.slice(-3e3);
3498
3635
  }
3499
3636
  } catch {
@@ -3518,7 +3655,7 @@ async function runCi(argv) {
3518
3655
  return 0;
3519
3656
  }
3520
3657
  const args = parseCiArgs(argv);
3521
- const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
3658
+ const cwd = args.cwd ? path13.resolve(args.cwd) : process.cwd();
3522
3659
  let earlyConfig;
3523
3660
  try {
3524
3661
  earlyConfig = loadConfig(cwd);
@@ -3537,8 +3674,8 @@ ${CI_HELP}`);
3537
3674
  return 64;
3538
3675
  }
3539
3676
  const dispatch = autoFallback ?? {
3540
- executable: "build",
3541
- cliArgs: { mode: "run", issue: args.issueNumber },
3677
+ executable: "run",
3678
+ cliArgs: { issue: args.issueNumber },
3542
3679
  target: args.issueNumber
3543
3680
  };
3544
3681
  const issueNumber = dispatch.target;
@@ -3610,16 +3747,19 @@ ${CI_HELP}`);
3610
3747
  var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
3611
3748
 
3612
3749
  Usage:
3613
- kody2 run --issue <N> [--cwd <path>] [--verbose|--quiet] [--dry-run]
3614
- kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
3750
+ kody2 run --issue <N> [--cwd <path>] [--verbose|--quiet]
3615
3751
  kody2 fix --pr <N> [--feedback "..."] [--cwd <path>] [--verbose|--quiet]
3616
3752
  kody2 fix-ci --pr <N> [--run-id <ID>] [--cwd <path>] [--verbose|--quiet]
3617
3753
  kody2 resolve --pr <N> [--cwd <path>] [--verbose|--quiet]
3754
+ kody2 review --pr <N> [--cwd <path>] [--verbose|--quiet]
3755
+ kody2 <other> [--cwd <path>] [--verbose|--quiet]
3756
+ kody2 ci --issue <N> [preflight flags \u2014 see: kody2 ci --help]
3618
3757
  kody2 help
3619
3758
  kody2 version
3620
3759
 
3621
- All commands dispatch to the Build executable with a specific mode. The
3622
- executable is defined by \`src/executables/build/profile.json\`.
3760
+ Each top-level command (run, fix, fix-ci, resolve, review, \u2026) is a discovered
3761
+ executable under \`src/executables/<name>/profile.json\`. Drop in a new
3762
+ directory to add a new command.
3623
3763
 
3624
3764
  Exit codes:
3625
3765
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
@@ -3640,11 +3780,6 @@ function parseArgs(argv) {
3640
3780
  if (cmd === "ci") {
3641
3781
  return { ...result, command: "ci", ciArgv: argv.slice(1) };
3642
3782
  }
3643
- if (cmd === "run" || cmd === "fix" || cmd === "fix-ci" || cmd === "resolve") {
3644
- result.command = cmd;
3645
- parseCommandArgs(cmd, argv.slice(1), result);
3646
- return result;
3647
- }
3648
3783
  if (hasExecutable(cmd)) {
3649
3784
  result.command = "__executable__";
3650
3785
  result.executableName = cmd;
@@ -3654,38 +3789,11 @@ function parseArgs(argv) {
3654
3789
  if (result.cliArgs.quiet === true) result.quiet = true;
3655
3790
  return result;
3656
3791
  }
3657
- const discovered = listExecutables().map((e) => e.name).filter((n) => n !== "build");
3658
- const available = ["run", "fix", "fix-ci", "resolve", "ci", "help", "version", ...discovered];
3792
+ const discovered = listExecutables().map((e) => e.name);
3793
+ const available = ["ci", "help", "version", ...discovered];
3659
3794
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
3660
3795
  return result;
3661
3796
  }
3662
- function parseCommandArgs(cmd, rest, result) {
3663
- for (let i = 0; i < rest.length; i++) {
3664
- const arg = rest[i];
3665
- if (arg === "--issue") {
3666
- const n = parseInt(rest[++i] ?? "", 10);
3667
- if (Number.isNaN(n) || n <= 0) result.errors.push("--issue requires a positive integer");
3668
- else result.issueNumber = n;
3669
- } else if (arg === "--pr") {
3670
- const n = parseInt(rest[++i] ?? "", 10);
3671
- if (Number.isNaN(n) || n <= 0) result.errors.push("--pr requires a positive integer");
3672
- else result.prNumber = n;
3673
- } else if (arg === "--feedback") {
3674
- result.feedback = rest[++i];
3675
- } else if (arg === "--run-id") {
3676
- result.runId = rest[++i];
3677
- } else if (arg === "--cwd") {
3678
- result.cwd = rest[++i];
3679
- } else if (arg === "--verbose") result.verbose = true;
3680
- else if (arg === "--quiet") result.quiet = true;
3681
- else if (arg === "--dry-run") result.dryRun = true;
3682
- else result.errors.push(`unknown arg: ${arg}`);
3683
- }
3684
- if (cmd === "run" && !result.issueNumber) result.errors.push("--issue <N> is required for run");
3685
- if (cmd === "fix" && !result.prNumber) result.errors.push("--pr <N> is required for fix");
3686
- if (cmd === "fix-ci" && !result.prNumber) result.errors.push("--pr <N> is required for fix-ci");
3687
- if (cmd === "resolve" && !result.prNumber) result.errors.push("--pr <N> is required for resolve");
3688
- }
3689
3797
  async function main(argv = process.argv.slice(2)) {
3690
3798
  const args = parseArgs(argv);
3691
3799
  if (args.errors.length > 0) {
@@ -3718,69 +3826,27 @@ ${HELP_TEXT}`);
3718
3826
  }
3719
3827
  const cwd = args.cwd ?? process.cwd();
3720
3828
  const configlessCommands = /* @__PURE__ */ new Set(["init"]);
3721
- const needsConfig = !(args.command === "__executable__" && configlessCommands.has(args.executableName ?? ""));
3722
- let config;
3723
- if (needsConfig) {
3724
- try {
3725
- config = loadConfig(cwd);
3726
- } catch (err) {
3727
- const msg = err instanceof Error ? err.message : String(err);
3728
- process.stderr.write(`[kody2] config error: ${msg}
3729
- `);
3730
- process.stdout.write(`PR_URL=FAILED: config error: ${msg}
3731
- `);
3732
- return 99;
3733
- }
3734
- } else {
3735
- config = {
3736
- quality: { typecheck: "", lint: "", testUnit: "" },
3737
- git: { defaultBranch: "main" },
3738
- github: { owner: "", repo: "" },
3739
- agent: { model: "claude/claude-haiku-4-5-20251001" }
3740
- };
3741
- }
3742
- if (args.command === "__executable__") {
3743
- try {
3744
- const result = await runExecutable(args.executableName, {
3745
- cliArgs: args.cliArgs ?? {},
3746
- cwd,
3747
- config,
3748
- verbose: args.verbose,
3749
- quiet: args.quiet
3750
- });
3751
- return result.exitCode;
3752
- } catch (err) {
3753
- const msg = err instanceof Error ? err.message : String(err);
3754
- process.stderr.write(`[kody2] ${args.executableName} crashed: ${msg}
3755
- `);
3756
- if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
3757
- `);
3758
- process.stdout.write(`PR_URL=FAILED: ${args.executableName} crashed: ${msg}
3759
- `);
3760
- return 99;
3761
- }
3762
- }
3763
- const cliArgs = { mode: args.command };
3764
- if (args.issueNumber !== void 0) cliArgs.issue = args.issueNumber;
3765
- if (args.prNumber !== void 0) cliArgs.pr = args.prNumber;
3766
- if (args.feedback !== void 0) cliArgs.feedback = args.feedback;
3767
- if (args.runId !== void 0) cliArgs.runId = args.runId;
3829
+ const skipConfig = configlessCommands.has(args.executableName ?? "");
3768
3830
  try {
3769
- const result = await runExecutable("build", {
3770
- cliArgs,
3831
+ const result = await runExecutable(args.executableName, {
3832
+ cliArgs: args.cliArgs ?? {},
3771
3833
  cwd,
3772
- config,
3834
+ skipConfig,
3773
3835
  verbose: args.verbose,
3774
3836
  quiet: args.quiet
3775
3837
  });
3838
+ if (result.exitCode !== 0 && result.reason) {
3839
+ process.stderr.write(`error: ${result.reason}
3840
+ `);
3841
+ }
3776
3842
  return result.exitCode;
3777
3843
  } catch (err) {
3778
3844
  const msg = err instanceof Error ? err.message : String(err);
3779
- process.stderr.write(`[kody2] wrapper crashed: ${msg}
3845
+ process.stderr.write(`[kody2] ${args.executableName} crashed: ${msg}
3780
3846
  `);
3781
3847
  if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
3782
3848
  `);
3783
- process.stdout.write(`PR_URL=FAILED: wrapper crashed: ${msg}
3849
+ process.stdout.write(`PR_URL=FAILED: ${args.executableName} crashed: ${msg}
3784
3850
  `);
3785
3851
  return 99;
3786
3852
  }