@kody-ade/kody-engine 0.3.1 → 0.3.2

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/kody.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.3.1",
6
+ version: "0.3.2",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -182,10 +182,26 @@ function loadConfig(projectDir = process.cwd()) {
182
182
  issueContext: parseIssueContext(raw.issueContext),
183
183
  testRequirements: parseTestRequirements(raw.testRequirements),
184
184
  defaultExecutable: typeof raw.defaultExecutable === "string" && raw.defaultExecutable.length > 0 ? raw.defaultExecutable : "classify",
185
+ defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
186
+ aliases: mergeAliases(raw.aliases),
185
187
  classify: parseClassifyConfig(raw.classify),
186
188
  release: parseReleaseConfig(raw.release)
187
189
  };
188
190
  }
191
+ var BUILTIN_ALIASES = {
192
+ build: "run",
193
+ orchestrate: "bug",
194
+ orchestrator: "bug"
195
+ };
196
+ function mergeAliases(raw) {
197
+ const out = { ...BUILTIN_ALIASES };
198
+ if (raw && typeof raw === "object") {
199
+ for (const [k, v] of Object.entries(raw)) {
200
+ if (typeof v === "string" && v.length > 0) out[k.toLowerCase()] = v;
201
+ }
202
+ }
203
+ return out;
204
+ }
189
205
  function parseClassifyConfig(raw) {
190
206
  if (!raw || typeof raw !== "object") return void 0;
191
207
  const r = raw;
@@ -557,22 +573,92 @@ import * as fs21 from "fs";
557
573
  import * as path18 from "path";
558
574
 
559
575
  // src/dispatch.ts
576
+ import * as fs6 from "fs";
577
+
578
+ // src/registry.ts
560
579
  import * as fs5 from "fs";
580
+ import * as path5 from "path";
581
+ function getExecutablesRoot() {
582
+ const here = path5.dirname(new URL(import.meta.url).pathname);
583
+ const candidates = [
584
+ path5.join(here, "executables"),
585
+ // dev: src/
586
+ path5.join(here, "..", "executables"),
587
+ // built: dist/bin → dist/executables
588
+ path5.join(here, "..", "src", "executables")
589
+ // fallback
590
+ ];
591
+ for (const c of candidates) {
592
+ if (fs5.existsSync(c) && fs5.statSync(c).isDirectory()) return c;
593
+ }
594
+ return candidates[0];
595
+ }
596
+ function listExecutables(root = getExecutablesRoot()) {
597
+ if (!fs5.existsSync(root)) return [];
598
+ const entries = fs5.readdirSync(root, { withFileTypes: true });
599
+ const out = [];
600
+ for (const ent of entries) {
601
+ if (!ent.isDirectory()) continue;
602
+ const profilePath = path5.join(root, ent.name, "profile.json");
603
+ if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
604
+ out.push({ name: ent.name, profilePath });
605
+ }
606
+ }
607
+ return out.sort((a, b) => a.name.localeCompare(b.name));
608
+ }
609
+ function hasExecutable(name, root = getExecutablesRoot()) {
610
+ if (!isSafeName(name)) return false;
611
+ const profilePath = path5.join(root, name, "profile.json");
612
+ return fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile();
613
+ }
614
+ function isSafeName(name) {
615
+ return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
616
+ }
617
+ function getProfileInputs(name, root = getExecutablesRoot()) {
618
+ if (!hasExecutable(name, root)) return null;
619
+ const profilePath = path5.join(root, name, "profile.json");
620
+ try {
621
+ const raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
622
+ if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
623
+ return raw.inputs;
624
+ } catch {
625
+ return null;
626
+ }
627
+ }
628
+ function parseGenericFlags(argv) {
629
+ const args = {};
630
+ const positional = [];
631
+ for (let i = 0; i < argv.length; i++) {
632
+ const arg = argv[i];
633
+ if (!arg.startsWith("--")) {
634
+ positional.push(arg);
635
+ continue;
636
+ }
637
+ const key = arg.slice(2);
638
+ const next = argv[i + 1];
639
+ const value = next !== void 0 && !next.startsWith("--") ? (i++, next) : true;
640
+ args[key] = value;
641
+ if (key.includes("-")) {
642
+ const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
643
+ if (camel !== key && args[camel] === void 0) args[camel] = value;
644
+ }
645
+ }
646
+ if (positional.length > 0) args._ = positional;
647
+ return args;
648
+ }
649
+
650
+ // src/dispatch.ts
561
651
  function autoDispatch(opts) {
562
652
  const explicit = opts?.explicit;
563
653
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
564
- return {
565
- executable: "run",
566
- cliArgs: { issue: explicit.issueNumber },
567
- target: explicit.issueNumber
568
- };
654
+ return { executable: "run", cliArgs: { issue: explicit.issueNumber }, target: explicit.issueNumber };
569
655
  }
570
656
  const eventName = process.env.GITHUB_EVENT_NAME;
571
657
  const eventPath = process.env.GITHUB_EVENT_PATH;
572
- if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
658
+ if (!eventName || !eventPath || !fs6.existsSync(eventPath)) return null;
573
659
  let event = {};
574
660
  try {
575
- event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
661
+ event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
576
662
  } catch {
577
663
  return null;
578
664
  }
@@ -589,45 +675,34 @@ function autoDispatch(opts) {
589
675
  const isPr = !!event.issue?.pull_request;
590
676
  if (!targetNum) return null;
591
677
  const afterTag = extractAfterTag(body);
592
- if (isPr) {
593
- if (/\bfix-ci\b/.test(afterTag)) {
594
- return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
595
- }
596
- if (/\bresolve\b/.test(afterTag)) {
597
- return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
598
- }
599
- if (/\bui-review\b/.test(afterTag)) {
600
- return { executable: "ui-review", cliArgs: { pr: targetNum }, target: targetNum };
601
- }
602
- if (/\breview\b/.test(afterTag)) {
603
- return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
604
- }
605
- if (/\bsync\b/.test(afterTag)) {
606
- return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
607
- }
608
- const feedback = extractFeedback(afterTag);
609
- return {
610
- executable: "fix",
611
- cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
612
- target: targetNum
613
- };
614
- }
615
- const sub = extractSubcommand(afterTag);
616
- if (!sub) {
617
- const defaultExec = opts?.config?.defaultExecutable;
618
- if (!defaultExec) return null;
619
- return asDispatch(defaultExec, targetNum);
620
- }
621
- if (sub === "build") {
622
- return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
623
- }
624
- if (sub === "orchestrate" || sub === "orchestrator") {
625
- return { executable: "bug", cliArgs: { issue: targetNum }, target: targetNum };
626
- }
627
- return asDispatch(sub, targetNum);
628
- }
629
- function asDispatch(executable, target) {
630
- return { executable, cliArgs: { issue: target }, target };
678
+ const firstToken = extractSubcommand(afterTag);
679
+ const aliases = opts?.config?.aliases ?? BUILTIN_ALIASES;
680
+ const aliased = firstToken ? aliases[firstToken] ?? firstToken : null;
681
+ let executable = null;
682
+ let consumedFirstToken = false;
683
+ if (aliased && getProfileInputs(aliased) !== null) {
684
+ executable = aliased;
685
+ consumedFirstToken = true;
686
+ }
687
+ if (!executable) {
688
+ executable = isPr ? opts?.config?.defaultPrExecutable ?? "fix" : opts?.config?.defaultExecutable ?? null;
689
+ }
690
+ if (!executable) return null;
691
+ const inputs = getProfileInputs(executable);
692
+ const effectiveInputs = inputs ?? [];
693
+ const unknownProfile = inputs === null;
694
+ const rest = extractCommentRest(afterTag, consumedFirstToken ? firstToken : null);
695
+ const { args, leftover } = parseCommentArgs(rest, effectiveInputs);
696
+ if (isPr && (unknownProfile || effectiveInputs.some((s) => s.name === "pr"))) {
697
+ args.pr = targetNum;
698
+ } else if (!isPr && (unknownProfile || effectiveInputs.some((s) => s.name === "issue"))) {
699
+ args.issue = targetNum;
700
+ }
701
+ const restInput = effectiveInputs.find((s) => s.bindsCommentRest === true);
702
+ if (restInput && leftover.length > 0 && args[restInput.name] === void 0) {
703
+ args[restInput.name] = leftover;
704
+ }
705
+ return { executable, cliArgs: args, target: targetNum };
631
706
  }
632
707
  function extractAfterTag(body) {
633
708
  const idx = body.indexOf("@kody");
@@ -636,12 +711,79 @@ function extractAfterTag(body) {
636
711
  }
637
712
  function extractSubcommand(afterTag) {
638
713
  const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
639
- if (!match) return null;
640
- return match[1];
714
+ return match ? match[1] : null;
715
+ }
716
+ function extractCommentRest(afterTag, consumedToken) {
717
+ let rest = afterTag;
718
+ if (consumedToken) {
719
+ const re = new RegExp(`^${consumedToken.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")}\\b`, "i");
720
+ rest = rest.replace(re, "");
721
+ }
722
+ rest = rest.replace(/^(please|kindly)(?:[\s:,.-]+|$)/i, "");
723
+ return rest.replace(/^[\s:,.-]+/, "").trim();
724
+ }
725
+ function parseCommentArgs(rest, inputs) {
726
+ const tokens = rest.length === 0 ? [] : rest.split(/\s+/).filter((t) => t.length > 0);
727
+ const args = {};
728
+ const unmatched = [];
729
+ for (let i = 0; i < tokens.length; i++) {
730
+ const t = tokens[i];
731
+ if (t.startsWith("--")) {
732
+ const eq = t.indexOf("=");
733
+ const key = eq >= 0 ? t.slice(2, eq) : t.slice(2);
734
+ const inlineValue = eq >= 0 ? t.slice(eq + 1) : void 0;
735
+ const spec = findInputByFlag(inputs, key);
736
+ if (!spec) {
737
+ unmatched.push(t);
738
+ continue;
739
+ }
740
+ if (spec.type === "bool") {
741
+ args[spec.name] = true;
742
+ continue;
743
+ }
744
+ const value = inlineValue ?? tokens[i + 1];
745
+ if (value === void 0 || value.startsWith("--")) {
746
+ unmatched.push(t);
747
+ continue;
748
+ }
749
+ args[spec.name] = coerceBare(spec, value);
750
+ if (inlineValue === void 0) i++;
751
+ continue;
752
+ }
753
+ const enumHit = inputs.find((s) => s.type === "enum" && s.values?.includes(t) && args[s.name] === void 0);
754
+ if (enumHit) {
755
+ args[enumHit.name] = t;
756
+ continue;
757
+ }
758
+ if (/^-?\d+$/.test(t)) {
759
+ const intHit = inputs.find((s) => s.type === "int" && args[s.name] === void 0);
760
+ if (intHit) {
761
+ args[intHit.name] = parseInt(t, 10);
762
+ continue;
763
+ }
764
+ }
765
+ const boolHit = inputs.find((s) => s.type === "bool" && s.flag === `--${t}` && args[s.name] === void 0);
766
+ if (boolHit) {
767
+ args[boolHit.name] = true;
768
+ continue;
769
+ }
770
+ unmatched.push(t);
771
+ }
772
+ return { args, leftover: unmatched.join(" ") };
773
+ }
774
+ function findInputByFlag(inputs, key) {
775
+ return inputs.find((s) => s.name === key || s.flag === `--${key}`);
641
776
  }
642
- function extractFeedback(afterTag) {
643
- const cleaned = afterTag.replace(/^(fix|please|kindly)(?:[\s:,.-]+|$)/i, "").trim();
644
- return cleaned.length > 0 ? cleaned : void 0;
777
+ function coerceBare(spec, value) {
778
+ if (spec.type === "int") {
779
+ const n = parseInt(value, 10);
780
+ return Number.isNaN(n) ? value : n;
781
+ }
782
+ if (spec.type === "bool") {
783
+ const v = value.toLowerCase();
784
+ return v === "true" || v === "1" || v === "yes";
785
+ }
786
+ return value;
645
787
  }
646
788
 
647
789
  // src/executor.ts
@@ -650,9 +792,9 @@ import * as path17 from "path";
650
792
 
651
793
  // src/litellm.ts
652
794
  import { execFileSync, spawn } from "child_process";
653
- import * as fs6 from "fs";
795
+ import * as fs7 from "fs";
654
796
  import * as os from "os";
655
- import * as path5 from "path";
797
+ import * as path6 from "path";
656
798
  async function checkLitellmHealth(url) {
657
799
  try {
658
800
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -692,20 +834,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
692
834
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
693
835
  }
694
836
  }
695
- const configPath = path5.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
696
- fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
837
+ const configPath = path6.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
838
+ fs7.writeFileSync(configPath, generateLitellmConfigYaml(model));
697
839
  const portMatch = url.match(/:(\d+)/);
698
840
  const port = portMatch ? portMatch[1] : "4000";
699
841
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
700
842
  const dotenvVars = readDotenvApiKeys(projectDir);
701
- const logPath = path5.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
702
- const outFd = fs6.openSync(logPath, "w");
843
+ const logPath = path6.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
844
+ const outFd = fs7.openSync(logPath, "w");
703
845
  const child = spawn(cmd, args, {
704
846
  stdio: ["ignore", outFd, outFd],
705
847
  detached: true,
706
848
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
707
849
  });
708
- fs6.closeSync(outFd);
850
+ fs7.closeSync(outFd);
709
851
  for (let i = 0; i < 30; i++) {
710
852
  await new Promise((r) => setTimeout(r, 2e3));
711
853
  if (await checkLitellmHealth(url)) {
@@ -722,7 +864,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
722
864
  }
723
865
  let logTail = "";
724
866
  try {
725
- logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
867
+ logTail = fs7.readFileSync(logPath, "utf-8").slice(-2e3);
726
868
  } catch {
727
869
  }
728
870
  try {
@@ -733,10 +875,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
733
875
  ${logTail}`);
734
876
  }
735
877
  function readDotenvApiKeys(projectDir) {
736
- const dotenvPath = path5.join(projectDir, ".env");
737
- if (!fs6.existsSync(dotenvPath)) return {};
878
+ const dotenvPath = path6.join(projectDir, ".env");
879
+ if (!fs7.existsSync(dotenvPath)) return {};
738
880
  const result = {};
739
- for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
881
+ for (const rawLine of fs7.readFileSync(dotenvPath, "utf-8").split("\n")) {
740
882
  const line = rawLine.trim();
741
883
  if (!line || line.startsWith("#")) continue;
742
884
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -759,8 +901,8 @@ function stripBlockingEnv(env) {
759
901
  }
760
902
 
761
903
  // src/profile.ts
762
- import * as fs7 from "fs";
763
- import * as path6 from "path";
904
+ import * as fs8 from "fs";
905
+ import * as path7 from "path";
764
906
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
765
907
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
766
908
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "watch", "utility"]);
@@ -775,12 +917,12 @@ var ProfileError = class extends Error {
775
917
  profilePath;
776
918
  };
777
919
  function loadProfile(profilePath) {
778
- if (!fs7.existsSync(profilePath)) {
920
+ if (!fs8.existsSync(profilePath)) {
779
921
  throw new ProfileError(profilePath, "file not found");
780
922
  }
781
923
  let raw;
782
924
  try {
783
- raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
925
+ raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
784
926
  } catch (err) {
785
927
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
786
928
  }
@@ -820,7 +962,7 @@ function loadProfile(profilePath) {
820
962
  outputContract: r.outputContract,
821
963
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
822
964
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
823
- dir: path6.dirname(profilePath)
965
+ dir: path7.dirname(profilePath)
824
966
  };
825
967
  return profile;
826
968
  }
@@ -868,6 +1010,7 @@ function parseInputs(p, raw) {
868
1010
  if (r.requiredWhen && typeof r.requiredWhen === "object") {
869
1011
  spec.requiredWhen = r.requiredWhen;
870
1012
  }
1013
+ if (r.bindsCommentRest === true) spec.bindsCommentRest = true;
871
1014
  out.push(spec);
872
1015
  }
873
1016
  return out;
@@ -1249,21 +1392,21 @@ var advanceFlow = async (ctx, profile) => {
1249
1392
  };
1250
1393
 
1251
1394
  // src/scripts/buildSyntheticPlugin.ts
1252
- import * as fs8 from "fs";
1395
+ import * as fs9 from "fs";
1253
1396
  import * as os2 from "os";
1254
- import * as path7 from "path";
1397
+ import * as path8 from "path";
1255
1398
  function getPluginsCatalogRoot() {
1256
- const here = path7.dirname(new URL(import.meta.url).pathname);
1399
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1257
1400
  const candidates = [
1258
- path7.join(here, "..", "plugins"),
1401
+ path8.join(here, "..", "plugins"),
1259
1402
  // dev: src/scripts → src/plugins
1260
- path7.join(here, "..", "..", "plugins"),
1403
+ path8.join(here, "..", "..", "plugins"),
1261
1404
  // built: dist/scripts → dist/plugins
1262
- path7.join(here, "..", "..", "src", "plugins")
1405
+ path8.join(here, "..", "..", "src", "plugins")
1263
1406
  // fallback
1264
1407
  ];
1265
1408
  for (const c of candidates) {
1266
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
1409
+ if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1267
1410
  }
1268
1411
  return candidates[0];
1269
1412
  }
@@ -1273,50 +1416,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1273
1416
  if (!needsSynthetic) return;
1274
1417
  const catalog = getPluginsCatalogRoot();
1275
1418
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1276
- const root = path7.join(os2.tmpdir(), `kody-synth-${runId}`);
1277
- fs8.mkdirSync(path7.join(root, ".claude-plugin"), { recursive: true });
1419
+ const root = path8.join(os2.tmpdir(), `kody-synth-${runId}`);
1420
+ fs9.mkdirSync(path8.join(root, ".claude-plugin"), { recursive: true });
1278
1421
  if (cc.skills.length > 0) {
1279
- const dst = path7.join(root, "skills");
1280
- fs8.mkdirSync(dst, { recursive: true });
1422
+ const dst = path8.join(root, "skills");
1423
+ fs9.mkdirSync(dst, { recursive: true });
1281
1424
  for (const name of cc.skills) {
1282
- const src = path7.join(catalog, "skills", name);
1283
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1284
- copyDir(src, path7.join(dst, name));
1425
+ const src = path8.join(catalog, "skills", name);
1426
+ if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1427
+ copyDir(src, path8.join(dst, name));
1285
1428
  }
1286
1429
  }
1287
1430
  if (cc.commands.length > 0) {
1288
- const dst = path7.join(root, "commands");
1289
- fs8.mkdirSync(dst, { recursive: true });
1431
+ const dst = path8.join(root, "commands");
1432
+ fs9.mkdirSync(dst, { recursive: true });
1290
1433
  for (const name of cc.commands) {
1291
- const src = path7.join(catalog, "commands", `${name}.md`);
1292
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1293
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1434
+ const src = path8.join(catalog, "commands", `${name}.md`);
1435
+ if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1436
+ fs9.copyFileSync(src, path8.join(dst, `${name}.md`));
1294
1437
  }
1295
1438
  }
1296
1439
  if (cc.subagents.length > 0) {
1297
- const dst = path7.join(root, "agents");
1298
- fs8.mkdirSync(dst, { recursive: true });
1440
+ const dst = path8.join(root, "agents");
1441
+ fs9.mkdirSync(dst, { recursive: true });
1299
1442
  for (const name of cc.subagents) {
1300
- const src = path7.join(catalog, "agents", `${name}.md`);
1301
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1302
- fs8.copyFileSync(src, path7.join(dst, `${name}.md`));
1443
+ const src = path8.join(catalog, "agents", `${name}.md`);
1444
+ if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1445
+ fs9.copyFileSync(src, path8.join(dst, `${name}.md`));
1303
1446
  }
1304
1447
  }
1305
1448
  if (cc.hooks.length > 0) {
1306
- const dst = path7.join(root, "hooks");
1307
- fs8.mkdirSync(dst, { recursive: true });
1449
+ const dst = path8.join(root, "hooks");
1450
+ fs9.mkdirSync(dst, { recursive: true });
1308
1451
  const merged = { hooks: {} };
1309
1452
  for (const name of cc.hooks) {
1310
- const src = path7.join(catalog, "hooks", `${name}.json`);
1311
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1312
- const parsed = JSON.parse(fs8.readFileSync(src, "utf-8"));
1453
+ const src = path8.join(catalog, "hooks", `${name}.json`);
1454
+ if (!fs9.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1455
+ const parsed = JSON.parse(fs9.readFileSync(src, "utf-8"));
1313
1456
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1314
1457
  if (!Array.isArray(entries)) continue;
1315
1458
  if (!merged.hooks[event]) merged.hooks[event] = [];
1316
1459
  merged.hooks[event].push(...entries);
1317
1460
  }
1318
1461
  }
1319
- fs8.writeFileSync(path7.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1462
+ fs9.writeFileSync(path8.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
1320
1463
  `);
1321
1464
  }
1322
1465
  const manifest = {
@@ -1327,17 +1470,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1327
1470
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1328
1471
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1329
1472
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1330
- fs8.writeFileSync(path7.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1473
+ fs9.writeFileSync(path8.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
1331
1474
  `);
1332
1475
  ctx.data.syntheticPluginPath = root;
1333
1476
  };
1334
1477
  function copyDir(src, dst) {
1335
- fs8.mkdirSync(dst, { recursive: true });
1336
- for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
1337
- const s = path7.join(src, ent.name);
1338
- const d = path7.join(dst, ent.name);
1478
+ fs9.mkdirSync(dst, { recursive: true });
1479
+ for (const ent of fs9.readdirSync(src, { withFileTypes: true })) {
1480
+ const s = path8.join(src, ent.name);
1481
+ const d = path8.join(dst, ent.name);
1339
1482
  if (ent.isDirectory()) copyDir(s, d);
1340
- else if (ent.isFile()) fs8.copyFileSync(s, d);
1483
+ else if (ent.isFile()) fs9.copyFileSync(s, d);
1341
1484
  }
1342
1485
  }
1343
1486
 
@@ -1403,18 +1546,18 @@ function formatMissesForFeedback(misses) {
1403
1546
  }
1404
1547
 
1405
1548
  // src/prompt.ts
1406
- import * as fs9 from "fs";
1407
- import * as path8 from "path";
1549
+ import * as fs10 from "fs";
1550
+ import * as path9 from "path";
1408
1551
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1409
1552
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1410
1553
  function loadProjectConventions(projectDir) {
1411
1554
  const out = [];
1412
1555
  for (const rel of CONVENTION_FILES) {
1413
- const abs = path8.join(projectDir, rel);
1414
- if (!fs9.existsSync(abs)) continue;
1556
+ const abs = path9.join(projectDir, rel);
1557
+ if (!fs10.existsSync(abs)) continue;
1415
1558
  let content;
1416
1559
  try {
1417
- content = fs9.readFileSync(abs, "utf-8");
1560
+ content = fs10.readFileSync(abs, "utf-8");
1418
1561
  } catch {
1419
1562
  continue;
1420
1563
  }
@@ -1578,8 +1721,8 @@ import { execFileSync as execFileSync6 } from "child_process";
1578
1721
 
1579
1722
  // src/commit.ts
1580
1723
  import { execFileSync as execFileSync5 } from "child_process";
1581
- import * as fs10 from "fs";
1582
- import * as path9 from "path";
1724
+ import * as fs11 from "fs";
1725
+ import * as path10 from "path";
1583
1726
  var FORBIDDEN_PATH_PREFIXES = [
1584
1727
  ".kody/",
1585
1728
  ".kody-engine/",
@@ -1634,18 +1777,18 @@ function tryGit(args, cwd) {
1634
1777
  }
1635
1778
  function abortUnfinishedGitOps(cwd) {
1636
1779
  const aborted = [];
1637
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1638
- if (!fs10.existsSync(gitDir)) return aborted;
1639
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
1780
+ const gitDir = path10.join(cwd ?? process.cwd(), ".git");
1781
+ if (!fs11.existsSync(gitDir)) return aborted;
1782
+ if (fs11.existsSync(path10.join(gitDir, "MERGE_HEAD"))) {
1640
1783
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1641
1784
  }
1642
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1785
+ if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
1643
1786
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1644
1787
  }
1645
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1788
+ if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
1646
1789
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1647
1790
  }
1648
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
1791
+ if (fs11.existsSync(path10.join(gitDir, "rebase-merge")) || fs11.existsSync(path10.join(gitDir, "rebase-apply"))) {
1649
1792
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1650
1793
  }
1651
1794
  try {
@@ -1700,7 +1843,7 @@ function normalizeCommitMessage(raw) {
1700
1843
  function commitAndPush(branch, agentMessage, cwd) {
1701
1844
  const allChanged = listChangedFiles(cwd);
1702
1845
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1703
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1846
+ const mergeHeadExists = fs11.existsSync(path10.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
1704
1847
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1705
1848
  return { committed: false, pushed: false, sha: "", message: "" };
1706
1849
  }
@@ -1796,20 +1939,20 @@ function defaultCommitMessage(mode, data) {
1796
1939
  }
1797
1940
 
1798
1941
  // src/scripts/composePrompt.ts
1799
- import * as fs11 from "fs";
1800
- import * as path10 from "path";
1942
+ import * as fs12 from "fs";
1943
+ import * as path11 from "path";
1801
1944
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1802
1945
  var composePrompt = async (ctx, profile) => {
1803
1946
  const explicit = ctx.data.promptTemplate;
1804
1947
  const mode = ctx.args.mode;
1805
1948
  const candidates = [
1806
- explicit ? path10.join(profile.dir, explicit) : null,
1807
- mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1808
- path10.join(profile.dir, "prompt.md")
1949
+ explicit ? path11.join(profile.dir, explicit) : null,
1950
+ mode ? path11.join(profile.dir, "prompts", `${mode}.md`) : null,
1951
+ path11.join(profile.dir, "prompt.md")
1809
1952
  ].filter(Boolean);
1810
1953
  let templatePath = "";
1811
1954
  for (const c of candidates) {
1812
- if (fs11.existsSync(c)) {
1955
+ if (fs12.existsSync(c)) {
1813
1956
  templatePath = c;
1814
1957
  break;
1815
1958
  }
@@ -1817,7 +1960,7 @@ var composePrompt = async (ctx, profile) => {
1817
1960
  if (!templatePath) {
1818
1961
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1819
1962
  }
1820
- const template = fs11.readFileSync(templatePath, "utf-8");
1963
+ const template = fs12.readFileSync(templatePath, "utf-8");
1821
1964
  const tokens = {
1822
1965
  ...stringifyAll(ctx.args, "args."),
1823
1966
  ...stringifyAll(ctx.data, ""),
@@ -1894,17 +2037,17 @@ function formatToolsUsage(profile) {
1894
2037
  }
1895
2038
 
1896
2039
  // src/scripts/discoverQaContext.ts
1897
- import * as fs13 from "fs";
1898
- import * as path12 from "path";
2040
+ import * as fs14 from "fs";
2041
+ import * as path13 from "path";
1899
2042
 
1900
2043
  // src/scripts/frameworkDetectors.ts
1901
- import * as fs12 from "fs";
1902
- import * as path11 from "path";
2044
+ import * as fs13 from "fs";
2045
+ import * as path12 from "path";
1903
2046
  function detectFrameworks(cwd) {
1904
2047
  const out = [];
1905
2048
  let deps = {};
1906
2049
  try {
1907
- const pkg = JSON.parse(fs12.readFileSync(path11.join(cwd, "package.json"), "utf-8"));
2050
+ const pkg = JSON.parse(fs13.readFileSync(path12.join(cwd, "package.json"), "utf-8"));
1908
2051
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
1909
2052
  } catch {
1910
2053
  return out;
@@ -1941,7 +2084,7 @@ function detectFrameworks(cwd) {
1941
2084
  }
1942
2085
  function findFile(cwd, candidates) {
1943
2086
  for (const c of candidates) {
1944
- if (fs12.existsSync(path11.join(cwd, c))) return c;
2087
+ if (fs13.existsSync(path12.join(cwd, c))) return c;
1945
2088
  }
1946
2089
  return null;
1947
2090
  }
@@ -1954,18 +2097,18 @@ var COLLECTION_DIRS = [
1954
2097
  function discoverPayloadCollections(cwd) {
1955
2098
  const out = [];
1956
2099
  for (const dir of COLLECTION_DIRS) {
1957
- const full = path11.join(cwd, dir);
1958
- if (!fs12.existsSync(full)) continue;
2100
+ const full = path12.join(cwd, dir);
2101
+ if (!fs13.existsSync(full)) continue;
1959
2102
  let files;
1960
2103
  try {
1961
- files = fs12.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2104
+ files = fs13.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
1962
2105
  } catch {
1963
2106
  continue;
1964
2107
  }
1965
2108
  for (const file of files) {
1966
2109
  try {
1967
- const filePath = path11.join(full, file);
1968
- const content = fs12.readFileSync(filePath, "utf-8").slice(0, 1e4);
2110
+ const filePath = path12.join(full, file);
2111
+ const content = fs13.readFileSync(filePath, "utf-8").slice(0, 1e4);
1969
2112
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
1970
2113
  if (!slugMatch) continue;
1971
2114
  const slug = slugMatch[1];
@@ -1979,7 +2122,7 @@ function discoverPayloadCollections(cwd) {
1979
2122
  out.push({
1980
2123
  name,
1981
2124
  slug,
1982
- filePath: path11.relative(cwd, filePath),
2125
+ filePath: path12.relative(cwd, filePath),
1983
2126
  fields: fields.slice(0, 20),
1984
2127
  hasAdmin
1985
2128
  });
@@ -1993,28 +2136,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
1993
2136
  function discoverAdminComponents(cwd, collections) {
1994
2137
  const out = [];
1995
2138
  for (const dir of ADMIN_COMPONENT_DIRS) {
1996
- const full = path11.join(cwd, dir);
1997
- if (!fs12.existsSync(full)) continue;
2139
+ const full = path12.join(cwd, dir);
2140
+ if (!fs13.existsSync(full)) continue;
1998
2141
  let entries;
1999
2142
  try {
2000
- entries = fs12.readdirSync(full, { withFileTypes: true });
2143
+ entries = fs13.readdirSync(full, { withFileTypes: true });
2001
2144
  } catch {
2002
2145
  continue;
2003
2146
  }
2004
2147
  for (const entry of entries) {
2005
- const entryPath = path11.join(full, entry.name);
2148
+ const entryPath = path12.join(full, entry.name);
2006
2149
  let name;
2007
2150
  let filePath;
2008
2151
  if (entry.isDirectory()) {
2009
2152
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
2010
- (f) => fs12.existsSync(path11.join(entryPath, f))
2153
+ (f) => fs13.existsSync(path12.join(entryPath, f))
2011
2154
  );
2012
2155
  if (!indexFile) continue;
2013
2156
  name = entry.name;
2014
- filePath = path11.relative(cwd, path11.join(entryPath, indexFile));
2157
+ filePath = path12.relative(cwd, path12.join(entryPath, indexFile));
2015
2158
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
2016
2159
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
2017
- filePath = path11.relative(cwd, entryPath);
2160
+ filePath = path12.relative(cwd, entryPath);
2018
2161
  } else {
2019
2162
  continue;
2020
2163
  }
@@ -2022,7 +2165,7 @@ function discoverAdminComponents(cwd, collections) {
2022
2165
  if (collections) {
2023
2166
  for (const col of collections) {
2024
2167
  try {
2025
- const colContent = fs12.readFileSync(path11.join(cwd, col.filePath), "utf-8");
2168
+ const colContent = fs13.readFileSync(path12.join(cwd, col.filePath), "utf-8");
2026
2169
  if (colContent.includes(name)) {
2027
2170
  usedInCollection = col.slug;
2028
2171
  break;
@@ -2041,8 +2184,8 @@ function scanApiRoutes(cwd) {
2041
2184
  const out = [];
2042
2185
  const appDirs = ["src/app", "app"];
2043
2186
  for (const appDir of appDirs) {
2044
- const apiDir = path11.join(cwd, appDir, "api");
2045
- if (!fs12.existsSync(apiDir)) continue;
2187
+ const apiDir = path12.join(cwd, appDir, "api");
2188
+ if (!fs13.existsSync(apiDir)) continue;
2046
2189
  walkApiRoutes(apiDir, "/api", cwd, out);
2047
2190
  break;
2048
2191
  }
@@ -2051,20 +2194,20 @@ function scanApiRoutes(cwd) {
2051
2194
  function walkApiRoutes(dir, prefix, cwd, out) {
2052
2195
  let entries;
2053
2196
  try {
2054
- entries = fs12.readdirSync(dir, { withFileTypes: true });
2197
+ entries = fs13.readdirSync(dir, { withFileTypes: true });
2055
2198
  } catch {
2056
2199
  return;
2057
2200
  }
2058
2201
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
2059
2202
  if (routeFile) {
2060
2203
  try {
2061
- const content = fs12.readFileSync(path11.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
2204
+ const content = fs13.readFileSync(path12.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
2062
2205
  const methods = HTTP_METHODS.filter((m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content));
2063
2206
  if (methods.length > 0) {
2064
2207
  out.push({
2065
2208
  path: prefix,
2066
2209
  methods,
2067
- filePath: path11.relative(cwd, path11.join(dir, routeFile.name))
2210
+ filePath: path12.relative(cwd, path12.join(dir, routeFile.name))
2068
2211
  });
2069
2212
  }
2070
2213
  } catch {
@@ -2075,7 +2218,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2075
2218
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2076
2219
  let segment = entry.name;
2077
2220
  if (segment.startsWith("(") && segment.endsWith(")")) {
2078
- walkApiRoutes(path11.join(dir, entry.name), prefix, cwd, out);
2221
+ walkApiRoutes(path12.join(dir, entry.name), prefix, cwd, out);
2079
2222
  continue;
2080
2223
  }
2081
2224
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2083,7 +2226,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2083
2226
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2084
2227
  segment = `:${segment.slice(1, -1)}`;
2085
2228
  }
2086
- walkApiRoutes(path11.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2229
+ walkApiRoutes(path12.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2087
2230
  }
2088
2231
  }
2089
2232
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -2103,10 +2246,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
2103
2246
  function scanEnvVars(cwd) {
2104
2247
  const candidates = [".env.example", ".env.local.example", ".env.template"];
2105
2248
  for (const envFile of candidates) {
2106
- const envPath = path11.join(cwd, envFile);
2107
- if (!fs12.existsSync(envPath)) continue;
2249
+ const envPath = path12.join(cwd, envFile);
2250
+ if (!fs13.existsSync(envPath)) continue;
2108
2251
  try {
2109
- const content = fs12.readFileSync(envPath, "utf-8");
2252
+ const content = fs13.readFileSync(envPath, "utf-8");
2110
2253
  const vars = [];
2111
2254
  for (const line of content.split("\n")) {
2112
2255
  const trimmed = line.trim();
@@ -2154,9 +2297,9 @@ function runQaDiscovery(cwd) {
2154
2297
  }
2155
2298
  function detectDevServer(cwd, out) {
2156
2299
  try {
2157
- const pkg = JSON.parse(fs13.readFileSync(path12.join(cwd, "package.json"), "utf-8"));
2300
+ const pkg = JSON.parse(fs14.readFileSync(path13.join(cwd, "package.json"), "utf-8"));
2158
2301
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2159
- const pm = fs13.existsSync(path12.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs13.existsSync(path12.join(cwd, "yarn.lock")) ? "yarn" : fs13.existsSync(path12.join(cwd, "bun.lockb")) ? "bun" : "npm";
2302
+ const pm = fs14.existsSync(path13.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs14.existsSync(path13.join(cwd, "yarn.lock")) ? "yarn" : fs14.existsSync(path13.join(cwd, "bun.lockb")) ? "bun" : "npm";
2160
2303
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
2161
2304
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
2162
2305
  else if (allDeps.vite) out.devPort = 5173;
@@ -2166,8 +2309,8 @@ function detectDevServer(cwd, out) {
2166
2309
  function scanFrontendRoutes(cwd, out) {
2167
2310
  const appDirs = ["src/app", "app"];
2168
2311
  for (const appDir of appDirs) {
2169
- const full = path12.join(cwd, appDir);
2170
- if (!fs13.existsSync(full)) continue;
2312
+ const full = path13.join(cwd, appDir);
2313
+ if (!fs14.existsSync(full)) continue;
2171
2314
  walkFrontendRoutes(full, "", out);
2172
2315
  break;
2173
2316
  }
@@ -2175,7 +2318,7 @@ function scanFrontendRoutes(cwd, out) {
2175
2318
  function walkFrontendRoutes(dir, prefix, out) {
2176
2319
  let entries;
2177
2320
  try {
2178
- entries = fs13.readdirSync(dir, { withFileTypes: true });
2321
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
2179
2322
  } catch {
2180
2323
  return;
2181
2324
  }
@@ -2192,7 +2335,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2192
2335
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2193
2336
  let segment = entry.name;
2194
2337
  if (segment.startsWith("(") && segment.endsWith(")")) {
2195
- walkFrontendRoutes(path12.join(dir, entry.name), prefix, out);
2338
+ walkFrontendRoutes(path13.join(dir, entry.name), prefix, out);
2196
2339
  continue;
2197
2340
  }
2198
2341
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2200,7 +2343,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2200
2343
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2201
2344
  segment = `:${segment.slice(1, -1)}`;
2202
2345
  }
2203
- walkFrontendRoutes(path12.join(dir, entry.name), `${prefix}/${segment}`, out);
2346
+ walkFrontendRoutes(path13.join(dir, entry.name), `${prefix}/${segment}`, out);
2204
2347
  }
2205
2348
  }
2206
2349
  function detectAuthFiles(cwd, out) {
@@ -2217,23 +2360,23 @@ function detectAuthFiles(cwd, out) {
2217
2360
  "src/app/api/oauth"
2218
2361
  ];
2219
2362
  for (const c of candidates) {
2220
- if (fs13.existsSync(path12.join(cwd, c))) out.authFiles.push(c);
2363
+ if (fs14.existsSync(path13.join(cwd, c))) out.authFiles.push(c);
2221
2364
  }
2222
2365
  }
2223
2366
  function detectRoles(cwd, out) {
2224
2367
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
2225
2368
  for (const rp of rolePaths) {
2226
- const dir = path12.join(cwd, rp);
2227
- if (!fs13.existsSync(dir)) continue;
2369
+ const dir = path13.join(cwd, rp);
2370
+ if (!fs14.existsSync(dir)) continue;
2228
2371
  let files;
2229
2372
  try {
2230
- files = fs13.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2373
+ files = fs14.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2231
2374
  } catch {
2232
2375
  continue;
2233
2376
  }
2234
2377
  for (const f of files) {
2235
2378
  try {
2236
- const content = fs13.readFileSync(path12.join(dir, f), "utf-8").slice(0, 5e3);
2379
+ const content = fs14.readFileSync(path13.join(dir, f), "utf-8").slice(0, 5e3);
2237
2380
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
2238
2381
  if (roleMatches) {
2239
2382
  for (const m of roleMatches) {
@@ -2720,67 +2863,6 @@ function computeFailureReason(ctx) {
2720
2863
  // src/scripts/finishFlow.ts
2721
2864
  import { execFileSync as execFileSync9 } from "child_process";
2722
2865
 
2723
- // src/registry.ts
2724
- import * as fs14 from "fs";
2725
- import * as path13 from "path";
2726
- function getExecutablesRoot() {
2727
- const here = path13.dirname(new URL(import.meta.url).pathname);
2728
- const candidates = [
2729
- path13.join(here, "executables"),
2730
- // dev: src/
2731
- path13.join(here, "..", "executables"),
2732
- // built: dist/bin → dist/executables
2733
- path13.join(here, "..", "src", "executables")
2734
- // fallback
2735
- ];
2736
- for (const c of candidates) {
2737
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2738
- }
2739
- return candidates[0];
2740
- }
2741
- function listExecutables(root = getExecutablesRoot()) {
2742
- if (!fs14.existsSync(root)) return [];
2743
- const entries = fs14.readdirSync(root, { withFileTypes: true });
2744
- const out = [];
2745
- for (const ent of entries) {
2746
- if (!ent.isDirectory()) continue;
2747
- const profilePath = path13.join(root, ent.name, "profile.json");
2748
- if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
2749
- out.push({ name: ent.name, profilePath });
2750
- }
2751
- }
2752
- return out.sort((a, b) => a.name.localeCompare(b.name));
2753
- }
2754
- function hasExecutable(name, root = getExecutablesRoot()) {
2755
- if (!isSafeName(name)) return false;
2756
- const profilePath = path13.join(root, name, "profile.json");
2757
- return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
2758
- }
2759
- function isSafeName(name) {
2760
- return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2761
- }
2762
- function parseGenericFlags(argv) {
2763
- const args = {};
2764
- const positional = [];
2765
- for (let i = 0; i < argv.length; i++) {
2766
- const arg = argv[i];
2767
- if (!arg.startsWith("--")) {
2768
- positional.push(arg);
2769
- continue;
2770
- }
2771
- const key = arg.slice(2);
2772
- const next = argv[i + 1];
2773
- const value = next !== void 0 && !next.startsWith("--") ? (i++, next) : true;
2774
- args[key] = value;
2775
- if (key.includes("-")) {
2776
- const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
2777
- if (camel !== key && args[camel] === void 0) args[camel] = value;
2778
- }
2779
- }
2780
- if (positional.length > 0) args._ = positional;
2781
- return args;
2782
- }
2783
-
2784
2866
  // src/lifecycleLabels.ts
2785
2867
  var KODY_NAMESPACE = "kody";
2786
2868
  function groupOf(label) {
@@ -15,6 +15,7 @@
15
15
  "flag": "--feedback",
16
16
  "type": "string",
17
17
  "required": false,
18
+ "bindsCommentRest": true,
18
19
  "describe": "Inline override. If absent, the flow reads the latest PR review comment."
19
20
  }
20
21
  ],
@@ -49,10 +50,10 @@
49
50
  "name": "playwright",
50
51
  "install": {
51
52
  "required": false,
52
- "checkCommand": "npx --no-install playwright --version",
53
+ "checkCommand": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
53
54
  "installCommand": "npx --yes playwright install --with-deps chromium"
54
55
  },
55
- "verify": "npx --no-install playwright --version",
56
+ "verify": "ls \"$HOME/.cache/ms-playwright\" 2>/dev/null | grep -q '^chromium'",
56
57
  "usage": ""
57
58
  }
58
59
  ],
@@ -97,6 +97,13 @@ export interface InputSpec {
97
97
  * e.g. `{ mode: "run" }` or `{ mode: ["fix", "fix-ci", "resolve"] }`.
98
98
  */
99
99
  requiredWhen?: Record<string, string | string[]>
100
+ /**
101
+ * When true, this input collects any free-text left over from comment
102
+ * dispatch after flag/enum/bool parsing. Only one input per profile may
103
+ * set this. Used by e.g. `fix.feedback` so `@kody please change X` lands
104
+ * "please change X" in `feedback` without hardcoding that in the router.
105
+ */
106
+ bindsCommentRest?: boolean
100
107
  describe: string
101
108
  }
102
109
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",