@kody-ade/kody-engine 0.3.0 → 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.0",
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;
@@ -366,7 +382,14 @@ async function runAgent(opts) {
366
382
  env
367
383
  };
368
384
  if (opts.mcpServers && opts.mcpServers.length > 0) {
369
- queryOptions.mcpServers = opts.mcpServers;
385
+ queryOptions.mcpServers = Object.fromEntries(
386
+ opts.mcpServers.map((s) => {
387
+ const cfg = { command: s.command };
388
+ if (s.args) cfg.args = s.args;
389
+ if (s.env) cfg.env = s.env;
390
+ return [s.name, cfg];
391
+ })
392
+ );
370
393
  }
371
394
  if (opts.pluginPaths && opts.pluginPaths.length > 0) {
372
395
  queryOptions.plugins = opts.pluginPaths.map((p) => ({ type: "local", path: p }));
@@ -550,22 +573,92 @@ import * as fs21 from "fs";
550
573
  import * as path18 from "path";
551
574
 
552
575
  // src/dispatch.ts
576
+ import * as fs6 from "fs";
577
+
578
+ // src/registry.ts
553
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
554
651
  function autoDispatch(opts) {
555
652
  const explicit = opts?.explicit;
556
653
  if (explicit?.issueNumber && explicit.issueNumber > 0) {
557
- return {
558
- executable: "run",
559
- cliArgs: { issue: explicit.issueNumber },
560
- target: explicit.issueNumber
561
- };
654
+ return { executable: "run", cliArgs: { issue: explicit.issueNumber }, target: explicit.issueNumber };
562
655
  }
563
656
  const eventName = process.env.GITHUB_EVENT_NAME;
564
657
  const eventPath = process.env.GITHUB_EVENT_PATH;
565
- if (!eventName || !eventPath || !fs5.existsSync(eventPath)) return null;
658
+ if (!eventName || !eventPath || !fs6.existsSync(eventPath)) return null;
566
659
  let event = {};
567
660
  try {
568
- event = JSON.parse(fs5.readFileSync(eventPath, "utf-8"));
661
+ event = JSON.parse(fs6.readFileSync(eventPath, "utf-8"));
569
662
  } catch {
570
663
  return null;
571
664
  }
@@ -582,45 +675,34 @@ function autoDispatch(opts) {
582
675
  const isPr = !!event.issue?.pull_request;
583
676
  if (!targetNum) return null;
584
677
  const afterTag = extractAfterTag(body);
585
- if (isPr) {
586
- if (/\bfix-ci\b/.test(afterTag)) {
587
- return { executable: "fix-ci", cliArgs: { pr: targetNum }, target: targetNum };
588
- }
589
- if (/\bresolve\b/.test(afterTag)) {
590
- return { executable: "resolve", cliArgs: { pr: targetNum }, target: targetNum };
591
- }
592
- if (/\bui-review\b/.test(afterTag)) {
593
- return { executable: "ui-review", cliArgs: { pr: targetNum }, target: targetNum };
594
- }
595
- if (/\breview\b/.test(afterTag)) {
596
- return { executable: "review", cliArgs: { pr: targetNum }, target: targetNum };
597
- }
598
- if (/\bsync\b/.test(afterTag)) {
599
- return { executable: "sync", cliArgs: { pr: targetNum }, target: targetNum };
600
- }
601
- const feedback = extractFeedback(afterTag);
602
- return {
603
- executable: "fix",
604
- cliArgs: { pr: targetNum, ...feedback ? { feedback } : {} },
605
- target: targetNum
606
- };
607
- }
608
- const sub = extractSubcommand(afterTag);
609
- if (!sub) {
610
- const defaultExec = opts?.config?.defaultExecutable;
611
- if (!defaultExec) return null;
612
- return asDispatch(defaultExec, targetNum);
613
- }
614
- if (sub === "build") {
615
- return { executable: "run", cliArgs: { issue: targetNum }, target: targetNum };
616
- }
617
- if (sub === "orchestrate" || sub === "orchestrator") {
618
- return { executable: "bug", cliArgs: { issue: targetNum }, target: targetNum };
619
- }
620
- return asDispatch(sub, targetNum);
621
- }
622
- function asDispatch(executable, target) {
623
- 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 };
624
706
  }
625
707
  function extractAfterTag(body) {
626
708
  const idx = body.indexOf("@kody");
@@ -629,12 +711,79 @@ function extractAfterTag(body) {
629
711
  }
630
712
  function extractSubcommand(afterTag) {
631
713
  const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
632
- if (!match) return null;
633
- return match[1];
714
+ return match ? match[1] : null;
634
715
  }
635
- function extractFeedback(afterTag) {
636
- const cleaned = afterTag.replace(/^(fix|please|kindly)(?:[\s:,.-]+|$)/i, "").trim();
637
- return cleaned.length > 0 ? cleaned : void 0;
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}`);
776
+ }
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;
638
787
  }
639
788
 
640
789
  // src/executor.ts
@@ -643,9 +792,9 @@ import * as path17 from "path";
643
792
 
644
793
  // src/litellm.ts
645
794
  import { execFileSync, spawn } from "child_process";
646
- import * as fs6 from "fs";
795
+ import * as fs7 from "fs";
647
796
  import * as os from "os";
648
- import * as path5 from "path";
797
+ import * as path6 from "path";
649
798
  async function checkLitellmHealth(url) {
650
799
  try {
651
800
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -685,20 +834,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
685
834
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
686
835
  }
687
836
  }
688
- const configPath = path5.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
689
- fs6.writeFileSync(configPath, generateLitellmConfigYaml(model));
837
+ const configPath = path6.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
838
+ fs7.writeFileSync(configPath, generateLitellmConfigYaml(model));
690
839
  const portMatch = url.match(/:(\d+)/);
691
840
  const port = portMatch ? portMatch[1] : "4000";
692
841
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
693
842
  const dotenvVars = readDotenvApiKeys(projectDir);
694
- const logPath = path5.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
695
- 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");
696
845
  const child = spawn(cmd, args, {
697
846
  stdio: ["ignore", outFd, outFd],
698
847
  detached: true,
699
848
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
700
849
  });
701
- fs6.closeSync(outFd);
850
+ fs7.closeSync(outFd);
702
851
  for (let i = 0; i < 30; i++) {
703
852
  await new Promise((r) => setTimeout(r, 2e3));
704
853
  if (await checkLitellmHealth(url)) {
@@ -715,7 +864,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
715
864
  }
716
865
  let logTail = "";
717
866
  try {
718
- logTail = fs6.readFileSync(logPath, "utf-8").slice(-2e3);
867
+ logTail = fs7.readFileSync(logPath, "utf-8").slice(-2e3);
719
868
  } catch {
720
869
  }
721
870
  try {
@@ -726,10 +875,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
726
875
  ${logTail}`);
727
876
  }
728
877
  function readDotenvApiKeys(projectDir) {
729
- const dotenvPath = path5.join(projectDir, ".env");
730
- if (!fs6.existsSync(dotenvPath)) return {};
878
+ const dotenvPath = path6.join(projectDir, ".env");
879
+ if (!fs7.existsSync(dotenvPath)) return {};
731
880
  const result = {};
732
- for (const rawLine of fs6.readFileSync(dotenvPath, "utf-8").split("\n")) {
881
+ for (const rawLine of fs7.readFileSync(dotenvPath, "utf-8").split("\n")) {
733
882
  const line = rawLine.trim();
734
883
  if (!line || line.startsWith("#")) continue;
735
884
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -752,8 +901,8 @@ function stripBlockingEnv(env) {
752
901
  }
753
902
 
754
903
  // src/profile.ts
755
- import * as fs7 from "fs";
756
- import * as path6 from "path";
904
+ import * as fs8 from "fs";
905
+ import * as path7 from "path";
757
906
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
758
907
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
759
908
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "watch", "utility"]);
@@ -768,12 +917,12 @@ var ProfileError = class extends Error {
768
917
  profilePath;
769
918
  };
770
919
  function loadProfile(profilePath) {
771
- if (!fs7.existsSync(profilePath)) {
920
+ if (!fs8.existsSync(profilePath)) {
772
921
  throw new ProfileError(profilePath, "file not found");
773
922
  }
774
923
  let raw;
775
924
  try {
776
- raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
925
+ raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
777
926
  } catch (err) {
778
927
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
779
928
  }
@@ -813,7 +962,7 @@ function loadProfile(profilePath) {
813
962
  outputContract: r.outputContract,
814
963
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
815
964
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
816
- dir: path6.dirname(profilePath)
965
+ dir: path7.dirname(profilePath)
817
966
  };
818
967
  return profile;
819
968
  }
@@ -861,6 +1010,7 @@ function parseInputs(p, raw) {
861
1010
  if (r.requiredWhen && typeof r.requiredWhen === "object") {
862
1011
  spec.requiredWhen = r.requiredWhen;
863
1012
  }
1013
+ if (r.bindsCommentRest === true) spec.bindsCommentRest = true;
864
1014
  out.push(spec);
865
1015
  }
866
1016
  return out;
@@ -1242,21 +1392,21 @@ var advanceFlow = async (ctx, profile) => {
1242
1392
  };
1243
1393
 
1244
1394
  // src/scripts/buildSyntheticPlugin.ts
1245
- import * as fs8 from "fs";
1395
+ import * as fs9 from "fs";
1246
1396
  import * as os2 from "os";
1247
- import * as path7 from "path";
1397
+ import * as path8 from "path";
1248
1398
  function getPluginsCatalogRoot() {
1249
- const here = path7.dirname(new URL(import.meta.url).pathname);
1399
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1250
1400
  const candidates = [
1251
- path7.join(here, "..", "plugins"),
1401
+ path8.join(here, "..", "plugins"),
1252
1402
  // dev: src/scripts → src/plugins
1253
- path7.join(here, "..", "..", "plugins"),
1403
+ path8.join(here, "..", "..", "plugins"),
1254
1404
  // built: dist/scripts → dist/plugins
1255
- path7.join(here, "..", "..", "src", "plugins")
1405
+ path8.join(here, "..", "..", "src", "plugins")
1256
1406
  // fallback
1257
1407
  ];
1258
1408
  for (const c of candidates) {
1259
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
1409
+ if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1260
1410
  }
1261
1411
  return candidates[0];
1262
1412
  }
@@ -1266,50 +1416,50 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1266
1416
  if (!needsSynthetic) return;
1267
1417
  const catalog = getPluginsCatalogRoot();
1268
1418
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1269
- const root = path7.join(os2.tmpdir(), `kody-synth-${runId}`);
1270
- 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 });
1271
1421
  if (cc.skills.length > 0) {
1272
- const dst = path7.join(root, "skills");
1273
- fs8.mkdirSync(dst, { recursive: true });
1422
+ const dst = path8.join(root, "skills");
1423
+ fs9.mkdirSync(dst, { recursive: true });
1274
1424
  for (const name of cc.skills) {
1275
- const src = path7.join(catalog, "skills", name);
1276
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1277
- 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));
1278
1428
  }
1279
1429
  }
1280
1430
  if (cc.commands.length > 0) {
1281
- const dst = path7.join(root, "commands");
1282
- fs8.mkdirSync(dst, { recursive: true });
1431
+ const dst = path8.join(root, "commands");
1432
+ fs9.mkdirSync(dst, { recursive: true });
1283
1433
  for (const name of cc.commands) {
1284
- const src = path7.join(catalog, "commands", `${name}.md`);
1285
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1286
- 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`));
1287
1437
  }
1288
1438
  }
1289
1439
  if (cc.subagents.length > 0) {
1290
- const dst = path7.join(root, "agents");
1291
- fs8.mkdirSync(dst, { recursive: true });
1440
+ const dst = path8.join(root, "agents");
1441
+ fs9.mkdirSync(dst, { recursive: true });
1292
1442
  for (const name of cc.subagents) {
1293
- const src = path7.join(catalog, "agents", `${name}.md`);
1294
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1295
- 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`));
1296
1446
  }
1297
1447
  }
1298
1448
  if (cc.hooks.length > 0) {
1299
- const dst = path7.join(root, "hooks");
1300
- fs8.mkdirSync(dst, { recursive: true });
1449
+ const dst = path8.join(root, "hooks");
1450
+ fs9.mkdirSync(dst, { recursive: true });
1301
1451
  const merged = { hooks: {} };
1302
1452
  for (const name of cc.hooks) {
1303
- const src = path7.join(catalog, "hooks", `${name}.json`);
1304
- if (!fs8.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1305
- 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"));
1306
1456
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1307
1457
  if (!Array.isArray(entries)) continue;
1308
1458
  if (!merged.hooks[event]) merged.hooks[event] = [];
1309
1459
  merged.hooks[event].push(...entries);
1310
1460
  }
1311
1461
  }
1312
- 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)}
1313
1463
  `);
1314
1464
  }
1315
1465
  const manifest = {
@@ -1320,17 +1470,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1320
1470
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
1321
1471
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
1322
1472
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
1323
- 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)}
1324
1474
  `);
1325
1475
  ctx.data.syntheticPluginPath = root;
1326
1476
  };
1327
1477
  function copyDir(src, dst) {
1328
- fs8.mkdirSync(dst, { recursive: true });
1329
- for (const ent of fs8.readdirSync(src, { withFileTypes: true })) {
1330
- const s = path7.join(src, ent.name);
1331
- 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);
1332
1482
  if (ent.isDirectory()) copyDir(s, d);
1333
- else if (ent.isFile()) fs8.copyFileSync(s, d);
1483
+ else if (ent.isFile()) fs9.copyFileSync(s, d);
1334
1484
  }
1335
1485
  }
1336
1486
 
@@ -1396,18 +1546,18 @@ function formatMissesForFeedback(misses) {
1396
1546
  }
1397
1547
 
1398
1548
  // src/prompt.ts
1399
- import * as fs9 from "fs";
1400
- import * as path8 from "path";
1549
+ import * as fs10 from "fs";
1550
+ import * as path9 from "path";
1401
1551
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1402
1552
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1403
1553
  function loadProjectConventions(projectDir) {
1404
1554
  const out = [];
1405
1555
  for (const rel of CONVENTION_FILES) {
1406
- const abs = path8.join(projectDir, rel);
1407
- if (!fs9.existsSync(abs)) continue;
1556
+ const abs = path9.join(projectDir, rel);
1557
+ if (!fs10.existsSync(abs)) continue;
1408
1558
  let content;
1409
1559
  try {
1410
- content = fs9.readFileSync(abs, "utf-8");
1560
+ content = fs10.readFileSync(abs, "utf-8");
1411
1561
  } catch {
1412
1562
  continue;
1413
1563
  }
@@ -1428,6 +1578,7 @@ function parseAgentResult(finalText) {
1428
1578
  prSummary: "",
1429
1579
  feedbackActions: "",
1430
1580
  planDeviations: "",
1581
+ priorArt: "",
1431
1582
  failureReason: "agent produced no final message"
1432
1583
  };
1433
1584
  const failedMatch = text.match(/(?:^|\n)\s*FAILED\s*:\s*(.+?)\s*$/s);
@@ -1438,6 +1589,7 @@ function parseAgentResult(finalText) {
1438
1589
  prSummary: "",
1439
1590
  feedbackActions: "",
1440
1591
  planDeviations: "",
1592
+ priorArt: "",
1441
1593
  failureReason: failedMatch[1].trim()
1442
1594
  };
1443
1595
  }
@@ -1450,6 +1602,7 @@ function parseAgentResult(finalText) {
1450
1602
  prSummary: "",
1451
1603
  feedbackActions: "",
1452
1604
  planDeviations: "",
1605
+ priorArt: "",
1453
1606
  failureReason: "no DONE or FAILED marker in agent output"
1454
1607
  };
1455
1608
  }
@@ -1458,24 +1611,27 @@ function parseAgentResult(finalText) {
1458
1611
  const feedbackActions = extractBlock(
1459
1612
  text,
1460
1613
  /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
1461
- /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY)\s*:/i
1614
+ /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
1462
1615
  );
1463
1616
  let planDeviations = extractBlock(
1464
1617
  text,
1465
1618
  /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
1466
- /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS)\s*:/i
1619
+ /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
1467
1620
  );
1468
1621
  if (!planDeviations) {
1469
1622
  const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
1470
1623
  if (inline) planDeviations = inline[1].trim();
1471
1624
  }
1625
+ let priorArt = "";
1626
+ const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
1627
+ if (priorArtInline) priorArt = priorArtInline[1].trim();
1472
1628
  const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
1473
1629
  let prSummary = "";
1474
1630
  if (summaryStart !== -1) {
1475
1631
  const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
1476
1632
  prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
1477
1633
  }
1478
- return { done: true, commitMessage, prSummary, feedbackActions, planDeviations, failureReason: "" };
1634
+ return { done: true, commitMessage, prSummary, feedbackActions, planDeviations, priorArt, failureReason: "" };
1479
1635
  }
1480
1636
  function extractBlock(text, startMarker, endMarker) {
1481
1637
  const startIdx = text.search(startMarker);
@@ -1565,8 +1721,8 @@ import { execFileSync as execFileSync6 } from "child_process";
1565
1721
 
1566
1722
  // src/commit.ts
1567
1723
  import { execFileSync as execFileSync5 } from "child_process";
1568
- import * as fs10 from "fs";
1569
- import * as path9 from "path";
1724
+ import * as fs11 from "fs";
1725
+ import * as path10 from "path";
1570
1726
  var FORBIDDEN_PATH_PREFIXES = [
1571
1727
  ".kody/",
1572
1728
  ".kody-engine/",
@@ -1621,18 +1777,18 @@ function tryGit(args, cwd) {
1621
1777
  }
1622
1778
  function abortUnfinishedGitOps(cwd) {
1623
1779
  const aborted = [];
1624
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
1625
- if (!fs10.existsSync(gitDir)) return aborted;
1626
- 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"))) {
1627
1783
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
1628
1784
  }
1629
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
1785
+ if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
1630
1786
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
1631
1787
  }
1632
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
1788
+ if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
1633
1789
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
1634
1790
  }
1635
- 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"))) {
1636
1792
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
1637
1793
  }
1638
1794
  try {
@@ -1687,7 +1843,7 @@ function normalizeCommitMessage(raw) {
1687
1843
  function commitAndPush(branch, agentMessage, cwd) {
1688
1844
  const allChanged = listChangedFiles(cwd);
1689
1845
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
1690
- 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"));
1691
1847
  if (allowedFiles.length === 0 && !mergeHeadExists) {
1692
1848
  return { committed: false, pushed: false, sha: "", message: "" };
1693
1849
  }
@@ -1783,20 +1939,20 @@ function defaultCommitMessage(mode, data) {
1783
1939
  }
1784
1940
 
1785
1941
  // src/scripts/composePrompt.ts
1786
- import * as fs11 from "fs";
1787
- import * as path10 from "path";
1942
+ import * as fs12 from "fs";
1943
+ import * as path11 from "path";
1788
1944
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
1789
1945
  var composePrompt = async (ctx, profile) => {
1790
1946
  const explicit = ctx.data.promptTemplate;
1791
1947
  const mode = ctx.args.mode;
1792
1948
  const candidates = [
1793
- explicit ? path10.join(profile.dir, explicit) : null,
1794
- mode ? path10.join(profile.dir, "prompts", `${mode}.md`) : null,
1795
- 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")
1796
1952
  ].filter(Boolean);
1797
1953
  let templatePath = "";
1798
1954
  for (const c of candidates) {
1799
- if (fs11.existsSync(c)) {
1955
+ if (fs12.existsSync(c)) {
1800
1956
  templatePath = c;
1801
1957
  break;
1802
1958
  }
@@ -1804,7 +1960,7 @@ var composePrompt = async (ctx, profile) => {
1804
1960
  if (!templatePath) {
1805
1961
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
1806
1962
  }
1807
- const template = fs11.readFileSync(templatePath, "utf-8");
1963
+ const template = fs12.readFileSync(templatePath, "utf-8");
1808
1964
  const tokens = {
1809
1965
  ...stringifyAll(ctx.args, "args."),
1810
1966
  ...stringifyAll(ctx.data, ""),
@@ -1881,17 +2037,17 @@ function formatToolsUsage(profile) {
1881
2037
  }
1882
2038
 
1883
2039
  // src/scripts/discoverQaContext.ts
1884
- import * as fs13 from "fs";
1885
- import * as path12 from "path";
2040
+ import * as fs14 from "fs";
2041
+ import * as path13 from "path";
1886
2042
 
1887
2043
  // src/scripts/frameworkDetectors.ts
1888
- import * as fs12 from "fs";
1889
- import * as path11 from "path";
2044
+ import * as fs13 from "fs";
2045
+ import * as path12 from "path";
1890
2046
  function detectFrameworks(cwd) {
1891
2047
  const out = [];
1892
2048
  let deps = {};
1893
2049
  try {
1894
- 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"));
1895
2051
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
1896
2052
  } catch {
1897
2053
  return out;
@@ -1928,7 +2084,7 @@ function detectFrameworks(cwd) {
1928
2084
  }
1929
2085
  function findFile(cwd, candidates) {
1930
2086
  for (const c of candidates) {
1931
- if (fs12.existsSync(path11.join(cwd, c))) return c;
2087
+ if (fs13.existsSync(path12.join(cwd, c))) return c;
1932
2088
  }
1933
2089
  return null;
1934
2090
  }
@@ -1941,18 +2097,18 @@ var COLLECTION_DIRS = [
1941
2097
  function discoverPayloadCollections(cwd) {
1942
2098
  const out = [];
1943
2099
  for (const dir of COLLECTION_DIRS) {
1944
- const full = path11.join(cwd, dir);
1945
- if (!fs12.existsSync(full)) continue;
2100
+ const full = path12.join(cwd, dir);
2101
+ if (!fs13.existsSync(full)) continue;
1946
2102
  let files;
1947
2103
  try {
1948
- 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"));
1949
2105
  } catch {
1950
2106
  continue;
1951
2107
  }
1952
2108
  for (const file of files) {
1953
2109
  try {
1954
- const filePath = path11.join(full, file);
1955
- 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);
1956
2112
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
1957
2113
  if (!slugMatch) continue;
1958
2114
  const slug = slugMatch[1];
@@ -1966,7 +2122,7 @@ function discoverPayloadCollections(cwd) {
1966
2122
  out.push({
1967
2123
  name,
1968
2124
  slug,
1969
- filePath: path11.relative(cwd, filePath),
2125
+ filePath: path12.relative(cwd, filePath),
1970
2126
  fields: fields.slice(0, 20),
1971
2127
  hasAdmin
1972
2128
  });
@@ -1980,28 +2136,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
1980
2136
  function discoverAdminComponents(cwd, collections) {
1981
2137
  const out = [];
1982
2138
  for (const dir of ADMIN_COMPONENT_DIRS) {
1983
- const full = path11.join(cwd, dir);
1984
- if (!fs12.existsSync(full)) continue;
2139
+ const full = path12.join(cwd, dir);
2140
+ if (!fs13.existsSync(full)) continue;
1985
2141
  let entries;
1986
2142
  try {
1987
- entries = fs12.readdirSync(full, { withFileTypes: true });
2143
+ entries = fs13.readdirSync(full, { withFileTypes: true });
1988
2144
  } catch {
1989
2145
  continue;
1990
2146
  }
1991
2147
  for (const entry of entries) {
1992
- const entryPath = path11.join(full, entry.name);
2148
+ const entryPath = path12.join(full, entry.name);
1993
2149
  let name;
1994
2150
  let filePath;
1995
2151
  if (entry.isDirectory()) {
1996
2152
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
1997
- (f) => fs12.existsSync(path11.join(entryPath, f))
2153
+ (f) => fs13.existsSync(path12.join(entryPath, f))
1998
2154
  );
1999
2155
  if (!indexFile) continue;
2000
2156
  name = entry.name;
2001
- filePath = path11.relative(cwd, path11.join(entryPath, indexFile));
2157
+ filePath = path12.relative(cwd, path12.join(entryPath, indexFile));
2002
2158
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
2003
2159
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
2004
- filePath = path11.relative(cwd, entryPath);
2160
+ filePath = path12.relative(cwd, entryPath);
2005
2161
  } else {
2006
2162
  continue;
2007
2163
  }
@@ -2009,7 +2165,7 @@ function discoverAdminComponents(cwd, collections) {
2009
2165
  if (collections) {
2010
2166
  for (const col of collections) {
2011
2167
  try {
2012
- const colContent = fs12.readFileSync(path11.join(cwd, col.filePath), "utf-8");
2168
+ const colContent = fs13.readFileSync(path12.join(cwd, col.filePath), "utf-8");
2013
2169
  if (colContent.includes(name)) {
2014
2170
  usedInCollection = col.slug;
2015
2171
  break;
@@ -2028,8 +2184,8 @@ function scanApiRoutes(cwd) {
2028
2184
  const out = [];
2029
2185
  const appDirs = ["src/app", "app"];
2030
2186
  for (const appDir of appDirs) {
2031
- const apiDir = path11.join(cwd, appDir, "api");
2032
- if (!fs12.existsSync(apiDir)) continue;
2187
+ const apiDir = path12.join(cwd, appDir, "api");
2188
+ if (!fs13.existsSync(apiDir)) continue;
2033
2189
  walkApiRoutes(apiDir, "/api", cwd, out);
2034
2190
  break;
2035
2191
  }
@@ -2038,20 +2194,20 @@ function scanApiRoutes(cwd) {
2038
2194
  function walkApiRoutes(dir, prefix, cwd, out) {
2039
2195
  let entries;
2040
2196
  try {
2041
- entries = fs12.readdirSync(dir, { withFileTypes: true });
2197
+ entries = fs13.readdirSync(dir, { withFileTypes: true });
2042
2198
  } catch {
2043
2199
  return;
2044
2200
  }
2045
2201
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
2046
2202
  if (routeFile) {
2047
2203
  try {
2048
- 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);
2049
2205
  const methods = HTTP_METHODS.filter((m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content));
2050
2206
  if (methods.length > 0) {
2051
2207
  out.push({
2052
2208
  path: prefix,
2053
2209
  methods,
2054
- filePath: path11.relative(cwd, path11.join(dir, routeFile.name))
2210
+ filePath: path12.relative(cwd, path12.join(dir, routeFile.name))
2055
2211
  });
2056
2212
  }
2057
2213
  } catch {
@@ -2062,7 +2218,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2062
2218
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2063
2219
  let segment = entry.name;
2064
2220
  if (segment.startsWith("(") && segment.endsWith(")")) {
2065
- walkApiRoutes(path11.join(dir, entry.name), prefix, cwd, out);
2221
+ walkApiRoutes(path12.join(dir, entry.name), prefix, cwd, out);
2066
2222
  continue;
2067
2223
  }
2068
2224
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2070,7 +2226,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2070
2226
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2071
2227
  segment = `:${segment.slice(1, -1)}`;
2072
2228
  }
2073
- walkApiRoutes(path11.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2229
+ walkApiRoutes(path12.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2074
2230
  }
2075
2231
  }
2076
2232
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -2090,10 +2246,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
2090
2246
  function scanEnvVars(cwd) {
2091
2247
  const candidates = [".env.example", ".env.local.example", ".env.template"];
2092
2248
  for (const envFile of candidates) {
2093
- const envPath = path11.join(cwd, envFile);
2094
- if (!fs12.existsSync(envPath)) continue;
2249
+ const envPath = path12.join(cwd, envFile);
2250
+ if (!fs13.existsSync(envPath)) continue;
2095
2251
  try {
2096
- const content = fs12.readFileSync(envPath, "utf-8");
2252
+ const content = fs13.readFileSync(envPath, "utf-8");
2097
2253
  const vars = [];
2098
2254
  for (const line of content.split("\n")) {
2099
2255
  const trimmed = line.trim();
@@ -2141,9 +2297,9 @@ function runQaDiscovery(cwd) {
2141
2297
  }
2142
2298
  function detectDevServer(cwd, out) {
2143
2299
  try {
2144
- 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"));
2145
2301
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2146
- 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";
2147
2303
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
2148
2304
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
2149
2305
  else if (allDeps.vite) out.devPort = 5173;
@@ -2153,8 +2309,8 @@ function detectDevServer(cwd, out) {
2153
2309
  function scanFrontendRoutes(cwd, out) {
2154
2310
  const appDirs = ["src/app", "app"];
2155
2311
  for (const appDir of appDirs) {
2156
- const full = path12.join(cwd, appDir);
2157
- if (!fs13.existsSync(full)) continue;
2312
+ const full = path13.join(cwd, appDir);
2313
+ if (!fs14.existsSync(full)) continue;
2158
2314
  walkFrontendRoutes(full, "", out);
2159
2315
  break;
2160
2316
  }
@@ -2162,7 +2318,7 @@ function scanFrontendRoutes(cwd, out) {
2162
2318
  function walkFrontendRoutes(dir, prefix, out) {
2163
2319
  let entries;
2164
2320
  try {
2165
- entries = fs13.readdirSync(dir, { withFileTypes: true });
2321
+ entries = fs14.readdirSync(dir, { withFileTypes: true });
2166
2322
  } catch {
2167
2323
  return;
2168
2324
  }
@@ -2179,7 +2335,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2179
2335
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2180
2336
  let segment = entry.name;
2181
2337
  if (segment.startsWith("(") && segment.endsWith(")")) {
2182
- walkFrontendRoutes(path12.join(dir, entry.name), prefix, out);
2338
+ walkFrontendRoutes(path13.join(dir, entry.name), prefix, out);
2183
2339
  continue;
2184
2340
  }
2185
2341
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2187,7 +2343,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2187
2343
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2188
2344
  segment = `:${segment.slice(1, -1)}`;
2189
2345
  }
2190
- walkFrontendRoutes(path12.join(dir, entry.name), `${prefix}/${segment}`, out);
2346
+ walkFrontendRoutes(path13.join(dir, entry.name), `${prefix}/${segment}`, out);
2191
2347
  }
2192
2348
  }
2193
2349
  function detectAuthFiles(cwd, out) {
@@ -2204,23 +2360,23 @@ function detectAuthFiles(cwd, out) {
2204
2360
  "src/app/api/oauth"
2205
2361
  ];
2206
2362
  for (const c of candidates) {
2207
- if (fs13.existsSync(path12.join(cwd, c))) out.authFiles.push(c);
2363
+ if (fs14.existsSync(path13.join(cwd, c))) out.authFiles.push(c);
2208
2364
  }
2209
2365
  }
2210
2366
  function detectRoles(cwd, out) {
2211
2367
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
2212
2368
  for (const rp of rolePaths) {
2213
- const dir = path12.join(cwd, rp);
2214
- if (!fs13.existsSync(dir)) continue;
2369
+ const dir = path13.join(cwd, rp);
2370
+ if (!fs14.existsSync(dir)) continue;
2215
2371
  let files;
2216
2372
  try {
2217
- 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"));
2218
2374
  } catch {
2219
2375
  continue;
2220
2376
  }
2221
2377
  for (const f of files) {
2222
2378
  try {
2223
- 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);
2224
2380
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
2225
2381
  if (roleMatches) {
2226
2382
  for (const m of roleMatches) {
@@ -2707,67 +2863,6 @@ function computeFailureReason(ctx) {
2707
2863
  // src/scripts/finishFlow.ts
2708
2864
  import { execFileSync as execFileSync9 } from "child_process";
2709
2865
 
2710
- // src/registry.ts
2711
- import * as fs14 from "fs";
2712
- import * as path13 from "path";
2713
- function getExecutablesRoot() {
2714
- const here = path13.dirname(new URL(import.meta.url).pathname);
2715
- const candidates = [
2716
- path13.join(here, "executables"),
2717
- // dev: src/
2718
- path13.join(here, "..", "executables"),
2719
- // built: dist/bin → dist/executables
2720
- path13.join(here, "..", "src", "executables")
2721
- // fallback
2722
- ];
2723
- for (const c of candidates) {
2724
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2725
- }
2726
- return candidates[0];
2727
- }
2728
- function listExecutables(root = getExecutablesRoot()) {
2729
- if (!fs14.existsSync(root)) return [];
2730
- const entries = fs14.readdirSync(root, { withFileTypes: true });
2731
- const out = [];
2732
- for (const ent of entries) {
2733
- if (!ent.isDirectory()) continue;
2734
- const profilePath = path13.join(root, ent.name, "profile.json");
2735
- if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
2736
- out.push({ name: ent.name, profilePath });
2737
- }
2738
- }
2739
- return out.sort((a, b) => a.name.localeCompare(b.name));
2740
- }
2741
- function hasExecutable(name, root = getExecutablesRoot()) {
2742
- if (!isSafeName(name)) return false;
2743
- const profilePath = path13.join(root, name, "profile.json");
2744
- return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
2745
- }
2746
- function isSafeName(name) {
2747
- return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2748
- }
2749
- function parseGenericFlags(argv) {
2750
- const args = {};
2751
- const positional = [];
2752
- for (let i = 0; i < argv.length; i++) {
2753
- const arg = argv[i];
2754
- if (!arg.startsWith("--")) {
2755
- positional.push(arg);
2756
- continue;
2757
- }
2758
- const key = arg.slice(2);
2759
- const next = argv[i + 1];
2760
- const value = next !== void 0 && !next.startsWith("--") ? (i++, next) : true;
2761
- args[key] = value;
2762
- if (key.includes("-")) {
2763
- const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
2764
- if (camel !== key && args[camel] === void 0) args[camel] = value;
2765
- }
2766
- }
2767
- if (positional.length > 0) args._ = positional;
2768
- return args;
2769
- }
2770
-
2771
2866
  // src/lifecycleLabels.ts
2772
2867
  var KODY_NAMESPACE = "kody";
2773
2868
  function groupOf(label) {
@@ -3600,6 +3695,97 @@ var loadIssueContext = async (ctx) => {
3600
3695
  ctx.data.commentTargetNumber = issueNumber;
3601
3696
  };
3602
3697
 
3698
+ // src/scripts/loadPriorArt.ts
3699
+ var PER_PR_DIFF_MAX_BYTES = 8e3;
3700
+ var TOTAL_MAX_BYTES = 3e4;
3701
+ var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
3702
+ var loadPriorArt = async (ctx, _profile, args) => {
3703
+ const artifactName = typeof args?.artifactName === "string" ? args.artifactName : "priorArt";
3704
+ const state = ctx.data.taskState;
3705
+ const artifact = state?.artifacts?.[artifactName];
3706
+ const prNumbers = parsePrNumbers(artifact?.content);
3707
+ if (prNumbers.length === 0) {
3708
+ ctx.data.priorArt = "";
3709
+ return;
3710
+ }
3711
+ const blocks = [];
3712
+ for (const n of prNumbers) {
3713
+ const block = fetchPrBlock(n, ctx.cwd);
3714
+ if (block) blocks.push(block);
3715
+ }
3716
+ const joined = blocks.join("\n\n---\n\n");
3717
+ ctx.data.priorArt = joined.length > TOTAL_MAX_BYTES ? joined.slice(0, TOTAL_MAX_BYTES) + TRUNCATED_SUFFIX : joined;
3718
+ };
3719
+ function parsePrNumbers(raw) {
3720
+ if (!raw) return [];
3721
+ const trimmed = raw.trim();
3722
+ if (!trimmed) return [];
3723
+ try {
3724
+ const parsed = JSON.parse(trimmed);
3725
+ if (!Array.isArray(parsed)) return [];
3726
+ return parsed.filter((n) => typeof n === "number" && Number.isInteger(n) && n > 0);
3727
+ } catch {
3728
+ return [];
3729
+ }
3730
+ }
3731
+ function fetchPrBlock(prNumber, cwd) {
3732
+ try {
3733
+ const metaRaw = gh2(["pr", "view", String(prNumber), "--json", "title,state,url,mergedAt,closedAt"], { cwd });
3734
+ const meta = JSON.parse(metaRaw);
3735
+ const diff = truncate3(safeGh(["pr", "diff", String(prNumber)], cwd), PER_PR_DIFF_MAX_BYTES);
3736
+ const commentsRaw = safeGh(["pr", "view", String(prNumber), "--json", "comments,reviews"], cwd);
3737
+ const commentsBlock = formatReviewComments(commentsRaw);
3738
+ const lines = [
3739
+ `## Prior art: PR #${prNumber} \u2014 ${meta.title ?? "(no title)"} [${meta.state ?? "unknown"}]`,
3740
+ meta.url ? meta.url : "",
3741
+ "",
3742
+ "### Diff",
3743
+ "```diff",
3744
+ diff || "(empty)",
3745
+ "```"
3746
+ ];
3747
+ if (commentsBlock) {
3748
+ lines.push("");
3749
+ lines.push("### Review comments");
3750
+ lines.push(commentsBlock);
3751
+ }
3752
+ return lines.filter((l) => l !== "").join("\n");
3753
+ } catch (err) {
3754
+ return `## Prior art: PR #${prNumber}
3755
+ _Could not fetch \u2014 ${err instanceof Error ? err.message : String(err)}_`;
3756
+ }
3757
+ }
3758
+ function safeGh(args, cwd) {
3759
+ try {
3760
+ return gh2(args, { cwd });
3761
+ } catch {
3762
+ return "";
3763
+ }
3764
+ }
3765
+ function truncate3(s, max) {
3766
+ return s.length <= max ? s : s.slice(0, max) + TRUNCATED_SUFFIX;
3767
+ }
3768
+ function formatReviewComments(raw) {
3769
+ if (!raw) return "";
3770
+ try {
3771
+ const parsed = JSON.parse(raw);
3772
+ const out = [];
3773
+ for (const c of parsed.comments ?? []) {
3774
+ if (!c.body) continue;
3775
+ out.push(`- **${c.author?.login ?? "unknown"}**: ${c.body.replace(/\n/g, " ").slice(0, 500)}`);
3776
+ }
3777
+ for (const r of parsed.reviews ?? []) {
3778
+ if (!r.body && !r.state) continue;
3779
+ const state = r.state ? ` (${r.state})` : "";
3780
+ const body = r.body ? `: ${r.body.replace(/\n/g, " ").slice(0, 500)}` : "";
3781
+ out.push(`- **${r.author?.login ?? "unknown"}**${state}${body}`);
3782
+ }
3783
+ return out.join("\n");
3784
+ } catch {
3785
+ return "";
3786
+ }
3787
+ }
3788
+
3603
3789
  // src/scripts/loadTaskState.ts
3604
3790
  var loadTaskState = async (ctx) => {
3605
3791
  const target = ctx.data.commentTargetType;
@@ -3656,6 +3842,7 @@ var parseAgentResult2 = async (ctx, profile, agentResult) => {
3656
3842
  ctx.data.prSummary = parsed.prSummary;
3657
3843
  ctx.data.feedbackActions = parsed.feedbackActions;
3658
3844
  ctx.data.planDeviations = parsed.planDeviations;
3845
+ ctx.data.priorArt = parsed.priorArt;
3659
3846
  ctx.data.agentFailureReason = parsed.failureReason;
3660
3847
  ctx.data.agentOutcome = agentResult.outcome;
3661
3848
  ctx.data.agentError = agentResult.error;
@@ -4924,6 +5111,7 @@ var preflightScripts = {
4924
5111
  loadIssueContext,
4925
5112
  loadConventions,
4926
5113
  loadCoverageRules,
5114
+ loadPriorArt,
4927
5115
  loadQaGuide,
4928
5116
  buildSyntheticPlugin,
4929
5117
  resolveArtifacts,
@@ -5079,7 +5267,7 @@ async function runExecutable(profileName, input) {
5079
5267
  ndjsonDir,
5080
5268
  allowedToolsOverride: profile.claudeCode.tools,
5081
5269
  permissionModeOverride: profile.claudeCode.permissionMode,
5082
- mcpServers: profile.claudeCode.mcpServers,
5270
+ mcpServers: profile.claudeCode.mcpServers.length > 0 ? profile.claudeCode.mcpServers : void 0,
5083
5271
  pluginPaths: pluginPaths.length > 0 ? pluginPaths : void 0,
5084
5272
  maxTurns: profile.claudeCode.maxTurns,
5085
5273
  maxThinkingTokens: profile.claudeCode.maxThinkingTokens,