@kody-ade/kody-engine 0.4.100 → 0.4.102

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
@@ -470,11 +470,11 @@ var init_issue = __esm({
470
470
 
471
471
  // src/prompt.ts
472
472
  import * as fs18 from "fs";
473
- import * as path16 from "path";
473
+ import * as path17 from "path";
474
474
  function loadProjectConventions(projectDir) {
475
475
  const out = [];
476
476
  for (const rel of CONVENTION_FILES) {
477
- const abs = path16.join(projectDir, rel);
477
+ const abs = path17.join(projectDir, rel);
478
478
  if (!fs18.existsSync(abs)) continue;
479
479
  let content;
480
480
  try {
@@ -627,7 +627,7 @@ __export(loadMemoryContext_exports, {
627
627
  loadMemoryContext: () => loadMemoryContext
628
628
  });
629
629
  import * as fs33 from "fs";
630
- import * as path31 from "path";
630
+ import * as path32 from "path";
631
631
  function collectPages(memoryAbs) {
632
632
  const out = [];
633
633
  walkMd(memoryAbs, (file) => {
@@ -644,10 +644,10 @@ function collectPages(memoryAbs) {
644
644
  return;
645
645
  }
646
646
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
647
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path31.basename(file, ".md");
647
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path32.basename(file, ".md");
648
648
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
649
649
  out.push({
650
- relPath: path31.relative(memoryAbs, file),
650
+ relPath: path32.relative(memoryAbs, file),
651
651
  title,
652
652
  updated,
653
653
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -721,7 +721,7 @@ function walkMd(root, visit) {
721
721
  }
722
722
  for (const name of names) {
723
723
  if (name.startsWith(".")) continue;
724
- const full = path31.join(dir, name);
724
+ const full = path32.join(dir, name);
725
725
  let stat;
726
726
  try {
727
727
  stat = fs33.statSync(full);
@@ -747,7 +747,7 @@ var init_loadMemoryContext = __esm({
747
747
  TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
748
748
  loadMemoryContext = async (ctx) => {
749
749
  if (typeof ctx.data.memoryContext === "string") return;
750
- const memoryAbs = path31.join(ctx.cwd, MEMORY_DIR_RELATIVE);
750
+ const memoryAbs = path32.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
751
  if (!fs33.existsSync(memoryAbs)) {
752
752
  ctx.data.memoryContext = "";
753
753
  return;
@@ -765,7 +765,10 @@ var init_loadMemoryContext = __esm({
765
765
  }
766
766
  const queryTerms = extractQueryTerms(ctx);
767
767
  const ranked = queryTerms.length > 0 ? scorePages(pages, queryTerms) : sortByRecency(pages);
768
- const top = ranked.slice(0, MAX_PAGES);
768
+ const indexPage = pages.find((p) => p.relPath === "INDEX.md");
769
+ const withoutIndex = ranked.filter((p) => p.relPath !== "INDEX.md");
770
+ const ordered = indexPage ? [indexPage, ...withoutIndex] : withoutIndex;
771
+ const top = ordered.slice(0, MAX_PAGES);
769
772
  ctx.data.memoryContext = formatBlock(top);
770
773
  };
771
774
  }
@@ -877,7 +880,7 @@ var init_loadPriorArt = __esm({
877
880
  // package.json
878
881
  var package_default = {
879
882
  name: "@kody-ade/kody-engine",
880
- version: "0.4.100",
883
+ version: "0.4.102",
881
884
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
882
885
  license: "MIT",
883
886
  type: "module",
@@ -933,7 +936,7 @@ var package_default = {
933
936
  // src/chat-cli.ts
934
937
  import { execFileSync as execFileSync32 } from "child_process";
935
938
  import * as fs39 from "fs";
936
- import * as path36 from "path";
939
+ import * as path37 from "path";
937
940
 
938
941
  // src/chat/events.ts
939
942
  import * as fs from "fs";
@@ -1000,6 +1003,7 @@ function makeRunId(sessionId, suffix) {
1000
1003
 
1001
1004
  // src/chat/loop.ts
1002
1005
  import * as fs9 from "fs";
1006
+ import * as path9 from "path";
1003
1007
 
1004
1008
  // src/task-artifacts.ts
1005
1009
  import fs2 from "fs";
@@ -1996,7 +2000,8 @@ async function runChatTurn(opts) {
1996
2000
  taskType: "chat",
1997
2001
  relDir: taskArtifactsPaths.relDir
1998
2002
  });
1999
- const systemPrompt = [basePrompt, catalog, artifactAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
2003
+ const memoryBlock = readMemoryIndexBlock(opts.cwd);
2004
+ const systemPrompt = [basePrompt, memoryBlock, catalog, artifactAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
2000
2005
  const prompt = buildPrompt(turns);
2001
2006
  let progressSeq = 0;
2002
2007
  const invoke = opts.invokeAgent ?? ((p) => runAgent({
@@ -2088,12 +2093,33 @@ async function emit(sink, type, sessionId, suffix, payload) {
2088
2093
  emittedAt: (/* @__PURE__ */ new Date()).toISOString()
2089
2094
  });
2090
2095
  }
2096
+ var MEMORY_INDEX_REL = ".kody/memory/INDEX.md";
2097
+ var MAX_INDEX_BYTES = 8e3;
2098
+ function readMemoryIndexBlock(cwd) {
2099
+ const indexPath = path9.join(cwd, MEMORY_INDEX_REL);
2100
+ let raw;
2101
+ try {
2102
+ raw = fs9.readFileSync(indexPath, "utf-8");
2103
+ } catch {
2104
+ return "";
2105
+ }
2106
+ const trimmed = raw.trim();
2107
+ if (!trimmed) return "";
2108
+ const body = trimmed.length > MAX_INDEX_BYTES ? trimmed.slice(0, MAX_INDEX_BYTES) + "\n\n_\u2026 (memory index truncated; open individual files under `.kody/memory/` to read more)_" : trimmed;
2109
+ return [
2110
+ "# Project memory index (`.kody/memory/INDEX.md`)",
2111
+ "",
2112
+ "These are the lessons, decisions, and preferences already captured for this repo. Skim before acting; read individual files only if a line looks relevant to the current task.",
2113
+ "",
2114
+ body
2115
+ ].join("\n");
2116
+ }
2091
2117
 
2092
2118
  // src/chat/modes/interactive.ts
2093
2119
  init_issue();
2094
2120
  import { execFileSync as execFileSync3 } from "child_process";
2095
2121
  import * as fs10 from "fs";
2096
- import * as path9 from "path";
2122
+ import * as path10 from "path";
2097
2123
 
2098
2124
  // src/chat/inbox.ts
2099
2125
  import { execFileSync as execFileSync2 } from "child_process";
@@ -2252,9 +2278,9 @@ function findNextUserTurn(turns, fromIdx) {
2252
2278
  return -1;
2253
2279
  }
2254
2280
  function commitTurn(cwd, sessionId, _verbose) {
2255
- const sessionRel = path9.relative(cwd, sessionFilePath(cwd, sessionId));
2256
- const eventsRel = path9.relative(cwd, eventsFilePath(cwd, sessionId));
2257
- const rels = [sessionRel, eventsRel].filter((p) => fs10.existsSync(path9.join(cwd, p)));
2281
+ const sessionRel = path10.relative(cwd, sessionFilePath(cwd, sessionId));
2282
+ const eventsRel = path10.relative(cwd, eventsFilePath(cwd, sessionId));
2283
+ const rels = [sessionRel, eventsRel].filter((p) => fs10.existsSync(path10.join(cwd, p)));
2258
2284
  if (rels.length === 0) return;
2259
2285
  const repository = process.env.GITHUB_REPOSITORY;
2260
2286
  if (!repository) {
@@ -2266,8 +2292,8 @@ function commitTurn(cwd, sessionId, _verbose) {
2266
2292
  }
2267
2293
  const branch = defaultBranch(cwd) ?? "main";
2268
2294
  for (const rel of rels) {
2269
- const repoPath = rel.split(path9.sep).join("/");
2270
- const localText = fs10.readFileSync(path9.join(cwd, rel), "utf-8");
2295
+ const repoPath = rel.split(path10.sep).join("/");
2296
+ const localText = fs10.readFileSync(path10.join(cwd, rel), "utf-8");
2271
2297
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
2272
2298
  }
2273
2299
  }
@@ -2358,7 +2384,7 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2358
2384
  // src/kody-cli.ts
2359
2385
  import { execFileSync as execFileSync31 } from "child_process";
2360
2386
  import * as fs38 from "fs";
2361
- import * as path35 from "path";
2387
+ import * as path36 from "path";
2362
2388
 
2363
2389
  // src/dispatch.ts
2364
2390
  import * as fs11 from "fs";
@@ -2681,7 +2707,7 @@ init_issue();
2681
2707
  // src/executor.ts
2682
2708
  import { execFileSync as execFileSync30, spawn as spawn6 } from "child_process";
2683
2709
  import * as fs37 from "fs";
2684
- import * as path34 from "path";
2710
+ import * as path35 from "path";
2685
2711
  init_events();
2686
2712
 
2687
2713
  // src/lifecycleLabels.ts
@@ -2689,7 +2715,7 @@ init_issue();
2689
2715
 
2690
2716
  // src/profile.ts
2691
2717
  import * as fs12 from "fs";
2692
- import * as path10 from "path";
2718
+ import * as path11 from "path";
2693
2719
 
2694
2720
  // src/profile-error.ts
2695
2721
  var ProfileError = class extends Error {
@@ -2873,7 +2899,7 @@ function loadProfile(profilePath) {
2873
2899
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
2874
2900
  if (unknownKeys.length > 0) {
2875
2901
  process.stderr.write(
2876
- `[kody profile] ${path10.basename(path10.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2902
+ `[kody profile] ${path11.basename(path11.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
2877
2903
  `
2878
2904
  );
2879
2905
  }
@@ -2934,7 +2960,7 @@ function loadProfile(profilePath) {
2934
2960
  // Phase 5 in-process handoff opt-in. Default false; containers
2935
2961
  // flip to true after end-to-end verification.
2936
2962
  preloadContext: r.preloadContext === true,
2937
- dir: path10.dirname(profilePath)
2963
+ dir: path11.dirname(profilePath)
2938
2964
  };
2939
2965
  if (lifecycle) {
2940
2966
  applyLifecycle(profile, profilePath);
@@ -3306,7 +3332,7 @@ function errMsg(err) {
3306
3332
  import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
3307
3333
  import * as fs13 from "fs";
3308
3334
  import * as os2 from "os";
3309
- import * as path11 from "path";
3335
+ import * as path12 from "path";
3310
3336
  async function checkLitellmHealth(url) {
3311
3337
  try {
3312
3338
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -3353,13 +3379,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3353
3379
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
3354
3380
  }
3355
3381
  }
3356
- const configPath = path11.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3382
+ const configPath = path12.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3357
3383
  fs13.writeFileSync(configPath, generateLitellmConfigYaml(model));
3358
3384
  const portMatch = url.match(/:(\d+)/);
3359
3385
  const port = portMatch ? portMatch[1] : "4000";
3360
3386
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
3361
3387
  const dotenvVars = readDotenvApiKeys(projectDir);
3362
- const logPath = path11.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3388
+ const logPath = path12.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3363
3389
  const outFd = fs13.openSync(logPath, "w");
3364
3390
  const child = spawn2(cmd, args, {
3365
3391
  stdio: ["ignore", outFd, outFd],
@@ -3397,7 +3423,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3397
3423
  ${logTail}`);
3398
3424
  }
3399
3425
  function readDotenvApiKeys(projectDir) {
3400
- const dotenvPath = path11.join(projectDir, ".env");
3426
+ const dotenvPath = path12.join(projectDir, ".env");
3401
3427
  if (!fs13.existsSync(dotenvPath)) return {};
3402
3428
  const result = {};
3403
3429
  for (const rawLine of fs13.readFileSync(dotenvPath, "utf-8").split("\n")) {
@@ -3503,7 +3529,7 @@ function pushWithRetry(opts = {}) {
3503
3529
 
3504
3530
  // src/commit.ts
3505
3531
  import * as fs14 from "fs";
3506
- import * as path12 from "path";
3532
+ import * as path13 from "path";
3507
3533
  var FORBIDDEN_PATH_PREFIXES = [
3508
3534
  ".kody/",
3509
3535
  ".kody-engine/",
@@ -3514,7 +3540,7 @@ var FORBIDDEN_PATH_PREFIXES = [
3514
3540
  "dist/",
3515
3541
  "build/"
3516
3542
  ];
3517
- var ALLOWED_PATH_PREFIXES = [".kody/memory/"];
3543
+ var ALLOWED_PATH_PREFIXES = [".kody/memory/", ".kody/tasks/"];
3518
3544
  var FORBIDDEN_PATH_EXACT = /* @__PURE__ */ new Set([".env", ".kody-pip-requirements.txt"]);
3519
3545
  var FORBIDDEN_PATH_SUFFIXES = [".log"];
3520
3546
  var CONVENTIONAL_PREFIXES = [
@@ -3559,18 +3585,18 @@ function tryGit(args, cwd) {
3559
3585
  }
3560
3586
  function abortUnfinishedGitOps(cwd) {
3561
3587
  const aborted = [];
3562
- const gitDir = path12.join(cwd ?? process.cwd(), ".git");
3588
+ const gitDir = path13.join(cwd ?? process.cwd(), ".git");
3563
3589
  if (!fs14.existsSync(gitDir)) return aborted;
3564
- if (fs14.existsSync(path12.join(gitDir, "MERGE_HEAD"))) {
3590
+ if (fs14.existsSync(path13.join(gitDir, "MERGE_HEAD"))) {
3565
3591
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
3566
3592
  }
3567
- if (fs14.existsSync(path12.join(gitDir, "CHERRY_PICK_HEAD"))) {
3593
+ if (fs14.existsSync(path13.join(gitDir, "CHERRY_PICK_HEAD"))) {
3568
3594
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
3569
3595
  }
3570
- if (fs14.existsSync(path12.join(gitDir, "REVERT_HEAD"))) {
3596
+ if (fs14.existsSync(path13.join(gitDir, "REVERT_HEAD"))) {
3571
3597
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
3572
3598
  }
3573
- if (fs14.existsSync(path12.join(gitDir, "rebase-merge")) || fs14.existsSync(path12.join(gitDir, "rebase-apply"))) {
3599
+ if (fs14.existsSync(path13.join(gitDir, "rebase-merge")) || fs14.existsSync(path13.join(gitDir, "rebase-apply"))) {
3574
3600
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
3575
3601
  }
3576
3602
  try {
@@ -3626,7 +3652,7 @@ function normalizeCommitMessage(raw) {
3626
3652
  function commitAndPush(branch, agentMessage, cwd) {
3627
3653
  const allChanged = listChangedFiles(cwd);
3628
3654
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
3629
- const mergeHeadExists = fs14.existsSync(path12.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
3655
+ const mergeHeadExists = fs14.existsSync(path13.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
3630
3656
  if (allowedFiles.length === 0 && !mergeHeadExists) {
3631
3657
  return { committed: false, pushed: false, sha: "", message: "" };
3632
3658
  }
@@ -3926,14 +3952,14 @@ var advanceFlow = async (ctx, profile) => {
3926
3952
  // src/scripts/brainServe.ts
3927
3953
  import { createServer } from "http";
3928
3954
  import * as fs16 from "fs";
3929
- import * as path14 from "path";
3955
+ import * as path15 from "path";
3930
3956
 
3931
3957
  // src/scripts/brainTurnLog.ts
3932
3958
  import * as fs15 from "fs";
3933
- import * as path13 from "path";
3959
+ import * as path14 from "path";
3934
3960
  var live = /* @__PURE__ */ new Map();
3935
3961
  function eventsPath(dir, chatId) {
3936
- return path13.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
3962
+ return path14.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
3937
3963
  }
3938
3964
  function lastPersistedSeq(dir, chatId) {
3939
3965
  const p = eventsPath(dir, chatId);
@@ -3976,7 +4002,7 @@ function beginTurn(dir, chatId) {
3976
4002
  };
3977
4003
  live.set(chatId, state);
3978
4004
  const p = eventsPath(dir, chatId);
3979
- fs15.mkdirSync(path13.dirname(p), { recursive: true });
4005
+ fs15.mkdirSync(path14.dirname(p), { recursive: true });
3980
4006
  return (event) => {
3981
4007
  state.seq += 1;
3982
4008
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
@@ -4236,7 +4262,7 @@ async function handleChatTurn(req, res, chatId, opts) {
4236
4262
  return;
4237
4263
  }
4238
4264
  const sessionFile = sessionFilePath(opts.cwd, chatId);
4239
- fs16.mkdirSync(path14.dirname(sessionFile), { recursive: true });
4265
+ fs16.mkdirSync(path15.dirname(sessionFile), { recursive: true });
4240
4266
  appendTurn(sessionFile, {
4241
4267
  role: "user",
4242
4268
  content: message,
@@ -4378,15 +4404,15 @@ var brainServe = async (ctx) => {
4378
4404
  // src/scripts/buildSyntheticPlugin.ts
4379
4405
  import * as fs17 from "fs";
4380
4406
  import * as os3 from "os";
4381
- import * as path15 from "path";
4407
+ import * as path16 from "path";
4382
4408
  function getPluginsCatalogRoot() {
4383
- const here = path15.dirname(new URL(import.meta.url).pathname);
4409
+ const here = path16.dirname(new URL(import.meta.url).pathname);
4384
4410
  const candidates = [
4385
- path15.join(here, "..", "plugins"),
4411
+ path16.join(here, "..", "plugins"),
4386
4412
  // dev: src/scripts → src/plugins
4387
- path15.join(here, "..", "..", "plugins"),
4413
+ path16.join(here, "..", "..", "plugins"),
4388
4414
  // built: dist/scripts → dist/plugins
4389
- path15.join(here, "..", "..", "src", "plugins")
4415
+ path16.join(here, "..", "..", "src", "plugins")
4390
4416
  // fallback
4391
4417
  ];
4392
4418
  for (const c of candidates) {
@@ -4400,40 +4426,40 @@ var buildSyntheticPlugin = async (ctx, profile) => {
4400
4426
  if (!needsSynthetic) return;
4401
4427
  const catalog = getPluginsCatalogRoot();
4402
4428
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4403
- const root = path15.join(os3.tmpdir(), `kody-synth-${runId}`);
4404
- fs17.mkdirSync(path15.join(root, ".claude-plugin"), { recursive: true });
4429
+ const root = path16.join(os3.tmpdir(), `kody-synth-${runId}`);
4430
+ fs17.mkdirSync(path16.join(root, ".claude-plugin"), { recursive: true });
4405
4431
  const resolvePart = (bucket, entry) => {
4406
- const local = path15.join(profile.dir, bucket, entry);
4432
+ const local = path16.join(profile.dir, bucket, entry);
4407
4433
  if (fs17.existsSync(local)) return local;
4408
- const central = path15.join(catalog, bucket, entry);
4434
+ const central = path16.join(catalog, bucket, entry);
4409
4435
  if (fs17.existsSync(central)) return central;
4410
4436
  throw new Error(
4411
4437
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
4412
4438
  );
4413
4439
  };
4414
4440
  if (cc.skills.length > 0) {
4415
- const dst = path15.join(root, "skills");
4441
+ const dst = path16.join(root, "skills");
4416
4442
  fs17.mkdirSync(dst, { recursive: true });
4417
4443
  for (const name of cc.skills) {
4418
- copyDir(resolvePart("skills", name), path15.join(dst, name));
4444
+ copyDir(resolvePart("skills", name), path16.join(dst, name));
4419
4445
  }
4420
4446
  }
4421
4447
  if (cc.commands.length > 0) {
4422
- const dst = path15.join(root, "commands");
4448
+ const dst = path16.join(root, "commands");
4423
4449
  fs17.mkdirSync(dst, { recursive: true });
4424
4450
  for (const name of cc.commands) {
4425
- fs17.copyFileSync(resolvePart("commands", `${name}.md`), path15.join(dst, `${name}.md`));
4451
+ fs17.copyFileSync(resolvePart("commands", `${name}.md`), path16.join(dst, `${name}.md`));
4426
4452
  }
4427
4453
  }
4428
4454
  if (cc.subagents.length > 0) {
4429
- const dst = path15.join(root, "agents");
4455
+ const dst = path16.join(root, "agents");
4430
4456
  fs17.mkdirSync(dst, { recursive: true });
4431
4457
  for (const name of cc.subagents) {
4432
- fs17.copyFileSync(resolvePart("agents", `${name}.md`), path15.join(dst, `${name}.md`));
4458
+ fs17.copyFileSync(resolvePart("agents", `${name}.md`), path16.join(dst, `${name}.md`));
4433
4459
  }
4434
4460
  }
4435
4461
  if (cc.hooks.length > 0) {
4436
- const dst = path15.join(root, "hooks");
4462
+ const dst = path16.join(root, "hooks");
4437
4463
  fs17.mkdirSync(dst, { recursive: true });
4438
4464
  const merged = { hooks: {} };
4439
4465
  for (const name of cc.hooks) {
@@ -4445,7 +4471,7 @@ var buildSyntheticPlugin = async (ctx, profile) => {
4445
4471
  merged.hooks[event].push(...entries);
4446
4472
  }
4447
4473
  }
4448
- fs17.writeFileSync(path15.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
4474
+ fs17.writeFileSync(path16.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
4449
4475
  `);
4450
4476
  }
4451
4477
  const manifest = {
@@ -4456,15 +4482,15 @@ var buildSyntheticPlugin = async (ctx, profile) => {
4456
4482
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
4457
4483
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
4458
4484
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
4459
- fs17.writeFileSync(path15.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
4485
+ fs17.writeFileSync(path16.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
4460
4486
  `);
4461
4487
  ctx.data.syntheticPluginPath = root;
4462
4488
  };
4463
4489
  function copyDir(src, dst) {
4464
4490
  fs17.mkdirSync(dst, { recursive: true });
4465
4491
  for (const ent of fs17.readdirSync(src, { withFileTypes: true })) {
4466
- const s = path15.join(src, ent.name);
4467
- const d = path15.join(dst, ent.name);
4492
+ const s = path16.join(src, ent.name);
4493
+ const d = path16.join(dst, ent.name);
4468
4494
  if (ent.isDirectory()) copyDir(s, d);
4469
4495
  else if (ent.isFile()) fs17.copyFileSync(s, d);
4470
4496
  }
@@ -4608,12 +4634,12 @@ function defaultLabelMap() {
4608
4634
 
4609
4635
  // src/scripts/commitAndPush.ts
4610
4636
  import * as fs19 from "fs";
4611
- import * as path17 from "path";
4637
+ import * as path18 from "path";
4612
4638
  init_events();
4613
4639
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
4614
4640
  function sentinelPathForStage(cwd, profileName) {
4615
4641
  const runId = resolveRunId();
4616
- return path17.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
4642
+ return path18.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
4617
4643
  }
4618
4644
  var commitAndPush2 = async (ctx, profile) => {
4619
4645
  const branch = ctx.data.branch;
@@ -4678,7 +4704,7 @@ var commitAndPush2 = async (ctx, profile) => {
4678
4704
  const result = ctx.data.commitResult;
4679
4705
  if (sentinel && result?.committed) {
4680
4706
  try {
4681
- fs19.mkdirSync(path17.dirname(sentinel), { recursive: true });
4707
+ fs19.mkdirSync(path18.dirname(sentinel), { recursive: true });
4682
4708
  fs19.writeFileSync(
4683
4709
  sentinel,
4684
4710
  JSON.stringify(
@@ -4700,11 +4726,11 @@ var commitAndPush2 = async (ctx, profile) => {
4700
4726
 
4701
4727
  // src/scripts/commitGoalState.ts
4702
4728
  import { execFileSync as execFileSync10 } from "child_process";
4703
- import * as path18 from "path";
4729
+ import * as path19 from "path";
4704
4730
  var commitGoalState = async (ctx) => {
4705
4731
  const goal = ctx.data.goal;
4706
4732
  if (!goal) return;
4707
- const stateRel = path18.posix.join(".kody", "goals", goal.id, "state.json");
4733
+ const stateRel = path19.posix.join(".kody", "goals", goal.id, "state.json");
4708
4734
  try {
4709
4735
  execFileSync10("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
4710
4736
  } catch (err) {
@@ -4750,15 +4776,15 @@ function describeCommitMessage(goal) {
4750
4776
 
4751
4777
  // src/scripts/composePrompt.ts
4752
4778
  import * as fs20 from "fs";
4753
- import * as path19 from "path";
4779
+ import * as path20 from "path";
4754
4780
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
4755
4781
  var composePrompt = async (ctx, profile) => {
4756
4782
  const explicit = ctx.data.promptTemplate;
4757
4783
  const mode = ctx.args.mode;
4758
4784
  const candidates = [
4759
- explicit ? path19.join(profile.dir, explicit) : null,
4760
- mode ? path19.join(profile.dir, "prompts", `${mode}.md`) : null,
4761
- path19.join(profile.dir, "prompt.md")
4785
+ explicit ? path20.join(profile.dir, explicit) : null,
4786
+ mode ? path20.join(profile.dir, "prompts", `${mode}.md`) : null,
4787
+ path20.join(profile.dir, "prompt.md")
4762
4788
  ].filter(Boolean);
4763
4789
  let templatePath = "";
4764
4790
  for (const c of candidates) {
@@ -4850,7 +4876,7 @@ function formatToolsUsage(profile) {
4850
4876
  init_issue();
4851
4877
  import { execFileSync as execFileSync11 } from "child_process";
4852
4878
  import * as fs21 from "fs";
4853
- import * as path20 from "path";
4879
+ import * as path21 from "path";
4854
4880
 
4855
4881
  // src/scripts/postReviewResult.ts
4856
4882
  init_issue();
@@ -5103,7 +5129,7 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
5103
5129
  return { number: Number(m[1]), created: true };
5104
5130
  }
5105
5131
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
5106
- const dir = path20.join(cwd, ".kody", "goals", goalId);
5132
+ const dir = path21.join(cwd, ".kody", "goals", goalId);
5107
5133
  fs21.mkdirSync(dir, { recursive: true });
5108
5134
  const state = {
5109
5135
  version: 1,
@@ -5112,7 +5138,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
5112
5138
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5113
5139
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
5114
5140
  };
5115
- const filePath = path20.join(dir, "state.json");
5141
+ const filePath = path21.join(dir, "state.json");
5116
5142
  fs21.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
5117
5143
  `);
5118
5144
  return filePath;
@@ -5624,10 +5650,10 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
5624
5650
  import { execFileSync as execFileSync12 } from "child_process";
5625
5651
  import * as fs22 from "fs";
5626
5652
  import * as os4 from "os";
5627
- import * as path21 from "path";
5653
+ import * as path22 from "path";
5628
5654
  var diagMcp = async (_ctx) => {
5629
5655
  const home = os4.homedir();
5630
- const cacheDir = path21.join(home, ".cache", "ms-playwright");
5656
+ const cacheDir = path22.join(home, ".cache", "ms-playwright");
5631
5657
  let entries = [];
5632
5658
  try {
5633
5659
  entries = fs22.readdirSync(cacheDir);
@@ -5657,16 +5683,16 @@ var diagMcp = async (_ctx) => {
5657
5683
 
5658
5684
  // src/scripts/discoverQaContext.ts
5659
5685
  import * as fs24 from "fs";
5660
- import * as path23 from "path";
5686
+ import * as path24 from "path";
5661
5687
 
5662
5688
  // src/scripts/frameworkDetectors.ts
5663
5689
  import * as fs23 from "fs";
5664
- import * as path22 from "path";
5690
+ import * as path23 from "path";
5665
5691
  function detectFrameworks(cwd) {
5666
5692
  const out = [];
5667
5693
  let deps = {};
5668
5694
  try {
5669
- const pkg = JSON.parse(fs23.readFileSync(path22.join(cwd, "package.json"), "utf-8"));
5695
+ const pkg = JSON.parse(fs23.readFileSync(path23.join(cwd, "package.json"), "utf-8"));
5670
5696
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
5671
5697
  } catch {
5672
5698
  return out;
@@ -5703,7 +5729,7 @@ function detectFrameworks(cwd) {
5703
5729
  }
5704
5730
  function findFile(cwd, candidates) {
5705
5731
  for (const c of candidates) {
5706
- if (fs23.existsSync(path22.join(cwd, c))) return c;
5732
+ if (fs23.existsSync(path23.join(cwd, c))) return c;
5707
5733
  }
5708
5734
  return null;
5709
5735
  }
@@ -5716,7 +5742,7 @@ var COLLECTION_DIRS = [
5716
5742
  function discoverPayloadCollections(cwd) {
5717
5743
  const out = [];
5718
5744
  for (const dir of COLLECTION_DIRS) {
5719
- const full = path22.join(cwd, dir);
5745
+ const full = path23.join(cwd, dir);
5720
5746
  if (!fs23.existsSync(full)) continue;
5721
5747
  let files;
5722
5748
  try {
@@ -5726,7 +5752,7 @@ function discoverPayloadCollections(cwd) {
5726
5752
  }
5727
5753
  for (const file of files) {
5728
5754
  try {
5729
- const filePath = path22.join(full, file);
5755
+ const filePath = path23.join(full, file);
5730
5756
  const content = fs23.readFileSync(filePath, "utf-8").slice(0, 1e4);
5731
5757
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
5732
5758
  if (!slugMatch) continue;
@@ -5741,7 +5767,7 @@ function discoverPayloadCollections(cwd) {
5741
5767
  out.push({
5742
5768
  name,
5743
5769
  slug,
5744
- filePath: path22.relative(cwd, filePath),
5770
+ filePath: path23.relative(cwd, filePath),
5745
5771
  fields: fields.slice(0, 20),
5746
5772
  hasAdmin
5747
5773
  });
@@ -5755,7 +5781,7 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
5755
5781
  function discoverAdminComponents(cwd, collections) {
5756
5782
  const out = [];
5757
5783
  for (const dir of ADMIN_COMPONENT_DIRS) {
5758
- const full = path22.join(cwd, dir);
5784
+ const full = path23.join(cwd, dir);
5759
5785
  if (!fs23.existsSync(full)) continue;
5760
5786
  let entries;
5761
5787
  try {
@@ -5764,19 +5790,19 @@ function discoverAdminComponents(cwd, collections) {
5764
5790
  continue;
5765
5791
  }
5766
5792
  for (const entry of entries) {
5767
- const entryPath = path22.join(full, entry.name);
5793
+ const entryPath = path23.join(full, entry.name);
5768
5794
  let name;
5769
5795
  let filePath;
5770
5796
  if (entry.isDirectory()) {
5771
5797
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
5772
- (f) => fs23.existsSync(path22.join(entryPath, f))
5798
+ (f) => fs23.existsSync(path23.join(entryPath, f))
5773
5799
  );
5774
5800
  if (!indexFile) continue;
5775
5801
  name = entry.name;
5776
- filePath = path22.relative(cwd, path22.join(entryPath, indexFile));
5802
+ filePath = path23.relative(cwd, path23.join(entryPath, indexFile));
5777
5803
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
5778
5804
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
5779
- filePath = path22.relative(cwd, entryPath);
5805
+ filePath = path23.relative(cwd, entryPath);
5780
5806
  } else {
5781
5807
  continue;
5782
5808
  }
@@ -5784,7 +5810,7 @@ function discoverAdminComponents(cwd, collections) {
5784
5810
  if (collections) {
5785
5811
  for (const col of collections) {
5786
5812
  try {
5787
- const colContent = fs23.readFileSync(path22.join(cwd, col.filePath), "utf-8");
5813
+ const colContent = fs23.readFileSync(path23.join(cwd, col.filePath), "utf-8");
5788
5814
  if (colContent.includes(name)) {
5789
5815
  usedInCollection = col.slug;
5790
5816
  break;
@@ -5803,7 +5829,7 @@ function scanApiRoutes(cwd) {
5803
5829
  const out = [];
5804
5830
  const appDirs = ["src/app", "app"];
5805
5831
  for (const appDir of appDirs) {
5806
- const apiDir = path22.join(cwd, appDir, "api");
5832
+ const apiDir = path23.join(cwd, appDir, "api");
5807
5833
  if (!fs23.existsSync(apiDir)) continue;
5808
5834
  walkApiRoutes(apiDir, "/api", cwd, out);
5809
5835
  break;
@@ -5820,7 +5846,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
5820
5846
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
5821
5847
  if (routeFile) {
5822
5848
  try {
5823
- const content = fs23.readFileSync(path22.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
5849
+ const content = fs23.readFileSync(path23.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
5824
5850
  const methods = HTTP_METHODS.filter(
5825
5851
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
5826
5852
  );
@@ -5828,7 +5854,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
5828
5854
  out.push({
5829
5855
  path: prefix,
5830
5856
  methods,
5831
- filePath: path22.relative(cwd, path22.join(dir, routeFile.name))
5857
+ filePath: path23.relative(cwd, path23.join(dir, routeFile.name))
5832
5858
  });
5833
5859
  }
5834
5860
  } catch {
@@ -5839,7 +5865,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
5839
5865
  if (entry.name === "node_modules" || entry.name === ".next") continue;
5840
5866
  let segment = entry.name;
5841
5867
  if (segment.startsWith("(") && segment.endsWith(")")) {
5842
- walkApiRoutes(path22.join(dir, entry.name), prefix, cwd, out);
5868
+ walkApiRoutes(path23.join(dir, entry.name), prefix, cwd, out);
5843
5869
  continue;
5844
5870
  }
5845
5871
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -5847,7 +5873,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
5847
5873
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
5848
5874
  segment = `:${segment.slice(1, -1)}`;
5849
5875
  }
5850
- walkApiRoutes(path22.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
5876
+ walkApiRoutes(path23.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
5851
5877
  }
5852
5878
  }
5853
5879
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -5867,7 +5893,7 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
5867
5893
  function scanEnvVars(cwd) {
5868
5894
  const candidates = [".env.example", ".env.local.example", ".env.template"];
5869
5895
  for (const envFile of candidates) {
5870
- const envPath = path22.join(cwd, envFile);
5896
+ const envPath = path23.join(cwd, envFile);
5871
5897
  if (!fs23.existsSync(envPath)) continue;
5872
5898
  try {
5873
5899
  const content = fs23.readFileSync(envPath, "utf-8");
@@ -5918,9 +5944,9 @@ function runQaDiscovery(cwd) {
5918
5944
  }
5919
5945
  function detectDevServer(cwd, out) {
5920
5946
  try {
5921
- const pkg = JSON.parse(fs24.readFileSync(path23.join(cwd, "package.json"), "utf-8"));
5947
+ const pkg = JSON.parse(fs24.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
5922
5948
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
5923
- const pm = fs24.existsSync(path23.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs24.existsSync(path23.join(cwd, "yarn.lock")) ? "yarn" : fs24.existsSync(path23.join(cwd, "bun.lockb")) ? "bun" : "npm";
5949
+ const pm = fs24.existsSync(path24.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs24.existsSync(path24.join(cwd, "yarn.lock")) ? "yarn" : fs24.existsSync(path24.join(cwd, "bun.lockb")) ? "bun" : "npm";
5924
5950
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
5925
5951
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
5926
5952
  else if (allDeps.vite) out.devPort = 5173;
@@ -5930,7 +5956,7 @@ function detectDevServer(cwd, out) {
5930
5956
  function scanFrontendRoutes(cwd, out) {
5931
5957
  const appDirs = ["src/app", "app"];
5932
5958
  for (const appDir of appDirs) {
5933
- const full = path23.join(cwd, appDir);
5959
+ const full = path24.join(cwd, appDir);
5934
5960
  if (!fs24.existsSync(full)) continue;
5935
5961
  walkFrontendRoutes(full, "", out);
5936
5962
  break;
@@ -5956,7 +5982,7 @@ function walkFrontendRoutes(dir, prefix, out) {
5956
5982
  if (entry.name === "node_modules" || entry.name === ".next") continue;
5957
5983
  let segment = entry.name;
5958
5984
  if (segment.startsWith("(") && segment.endsWith(")")) {
5959
- walkFrontendRoutes(path23.join(dir, entry.name), prefix, out);
5985
+ walkFrontendRoutes(path24.join(dir, entry.name), prefix, out);
5960
5986
  continue;
5961
5987
  }
5962
5988
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -5964,7 +5990,7 @@ function walkFrontendRoutes(dir, prefix, out) {
5964
5990
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
5965
5991
  segment = `:${segment.slice(1, -1)}`;
5966
5992
  }
5967
- walkFrontendRoutes(path23.join(dir, entry.name), `${prefix}/${segment}`, out);
5993
+ walkFrontendRoutes(path24.join(dir, entry.name), `${prefix}/${segment}`, out);
5968
5994
  }
5969
5995
  }
5970
5996
  function detectAuthFiles(cwd, out) {
@@ -5981,13 +6007,13 @@ function detectAuthFiles(cwd, out) {
5981
6007
  "src/app/api/oauth"
5982
6008
  ];
5983
6009
  for (const c of candidates) {
5984
- if (fs24.existsSync(path23.join(cwd, c))) out.authFiles.push(c);
6010
+ if (fs24.existsSync(path24.join(cwd, c))) out.authFiles.push(c);
5985
6011
  }
5986
6012
  }
5987
6013
  function detectRoles(cwd, out) {
5988
6014
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
5989
6015
  for (const rp of rolePaths) {
5990
- const dir = path23.join(cwd, rp);
6016
+ const dir = path24.join(cwd, rp);
5991
6017
  if (!fs24.existsSync(dir)) continue;
5992
6018
  let files;
5993
6019
  try {
@@ -5997,7 +6023,7 @@ function detectRoles(cwd, out) {
5997
6023
  }
5998
6024
  for (const f of files) {
5999
6025
  try {
6000
- const content = fs24.readFileSync(path23.join(dir, f), "utf-8").slice(0, 5e3);
6026
+ const content = fs24.readFileSync(path24.join(dir, f), "utf-8").slice(0, 5e3);
6001
6027
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
6002
6028
  if (roleMatches) {
6003
6029
  for (const m of roleMatches) {
@@ -6244,7 +6270,7 @@ function failedAction3(reason) {
6244
6270
 
6245
6271
  // src/scripts/dispatchJobFileTicks.ts
6246
6272
  import * as fs26 from "fs";
6247
- import * as path25 from "path";
6273
+ import * as path26 from "path";
6248
6274
 
6249
6275
  // src/scripts/jobFrontmatter.ts
6250
6276
  var SCHEDULE_EVERY_VALUES = [
@@ -6506,7 +6532,7 @@ var ContentsApiBackend = class {
6506
6532
 
6507
6533
  // src/scripts/jobState/localFileBackend.ts
6508
6534
  import * as fs25 from "fs";
6509
- import * as path24 from "path";
6535
+ import * as path25 from "path";
6510
6536
  var LocalFileBackend = class {
6511
6537
  name = "local-file";
6512
6538
  cwd;
@@ -6521,7 +6547,7 @@ var LocalFileBackend = class {
6521
6547
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
6522
6548
  this.cwd = opts.cwd;
6523
6549
  this.jobsDir = opts.jobsDir;
6524
- this.absDir = path24.join(opts.cwd, opts.jobsDir);
6550
+ this.absDir = path25.join(opts.cwd, opts.jobsDir);
6525
6551
  this.owner = opts.owner;
6526
6552
  this.repo = opts.repo;
6527
6553
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -6581,7 +6607,7 @@ var LocalFileBackend = class {
6581
6607
  }
6582
6608
  load(slug) {
6583
6609
  const relPath = stateFilePath(this.jobsDir, slug);
6584
- const absPath = path24.join(this.cwd, relPath);
6610
+ const absPath = path25.join(this.cwd, relPath);
6585
6611
  if (!fs25.existsSync(absPath)) {
6586
6612
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
6587
6613
  }
@@ -6602,8 +6628,8 @@ var LocalFileBackend = class {
6602
6628
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
6603
6629
  return false;
6604
6630
  }
6605
- const absPath = path24.join(this.cwd, loaded.path);
6606
- fs25.mkdirSync(path24.dirname(absPath), { recursive: true });
6631
+ const absPath = path25.join(this.cwd, loaded.path);
6632
+ fs25.mkdirSync(path25.dirname(absPath), { recursive: true });
6607
6633
  const body = JSON.stringify(next, null, 2) + "\n";
6608
6634
  fs25.writeFileSync(absPath, body, "utf-8");
6609
6635
  return true;
@@ -6683,7 +6709,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
6683
6709
  await backend.hydrate();
6684
6710
  }
6685
6711
  try {
6686
- const slugs = listJobSlugs(path25.join(ctx.cwd, jobsDir));
6712
+ const slugs = listJobSlugs(path26.join(ctx.cwd, jobsDir));
6687
6713
  ctx.data.jobSlugCount = slugs.length;
6688
6714
  if (slugs.length === 0) {
6689
6715
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -6796,7 +6822,7 @@ function formatAgo(ms) {
6796
6822
  }
6797
6823
  function readJobFrontmatter(cwd, jobsDir, slug) {
6798
6824
  try {
6799
- const raw = fs26.readFileSync(path25.join(cwd, jobsDir, `${slug}.md`), "utf-8");
6825
+ const raw = fs26.readFileSync(path26.join(cwd, jobsDir, `${slug}.md`), "utf-8");
6800
6826
  return splitFrontmatter(raw).frontmatter;
6801
6827
  } catch {
6802
6828
  return {};
@@ -7867,14 +7893,14 @@ var handleAbandonedGoal = async (ctx) => {
7867
7893
  // src/scripts/initFlow.ts
7868
7894
  import { execFileSync as execFileSync19 } from "child_process";
7869
7895
  import * as fs29 from "fs";
7870
- import * as path27 from "path";
7896
+ import * as path28 from "path";
7871
7897
 
7872
7898
  // src/scripts/loadQaGuide.ts
7873
7899
  import * as fs28 from "fs";
7874
- import * as path26 from "path";
7900
+ import * as path27 from "path";
7875
7901
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
7876
7902
  var loadQaGuide = async (ctx) => {
7877
- const full = path26.join(ctx.cwd, QA_GUIDE_REL_PATH);
7903
+ const full = path27.join(ctx.cwd, QA_GUIDE_REL_PATH);
7878
7904
  if (!fs28.existsSync(full)) {
7879
7905
  ctx.data.qaGuide = "";
7880
7906
  ctx.data.qaGuidePath = "";
@@ -7891,9 +7917,9 @@ var loadQaGuide = async (ctx) => {
7891
7917
 
7892
7918
  // src/scripts/initFlow.ts
7893
7919
  function detectPackageManager(cwd) {
7894
- if (fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
7895
- if (fs29.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
7896
- if (fs29.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
7920
+ if (fs29.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
7921
+ if (fs29.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
7922
+ if (fs29.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
7897
7923
  return "npm";
7898
7924
  }
7899
7925
  function qualityCommandsFor(pm) {
@@ -8015,7 +8041,7 @@ function performInit(cwd, force) {
8015
8041
  const pm = detectPackageManager(cwd);
8016
8042
  const ownerRepo = detectOwnerRepo(cwd);
8017
8043
  const defaultBranch2 = defaultBranchFromGit(cwd);
8018
- const configPath = path27.join(cwd, "kody.config.json");
8044
+ const configPath = path28.join(cwd, "kody.config.json");
8019
8045
  if (fs29.existsSync(configPath) && !force) {
8020
8046
  skipped.push("kody.config.json");
8021
8047
  } else {
@@ -8024,8 +8050,8 @@ function performInit(cwd, force) {
8024
8050
  `);
8025
8051
  wrote.push("kody.config.json");
8026
8052
  }
8027
- const workflowDir = path27.join(cwd, ".github", "workflows");
8028
- const workflowPath = path27.join(workflowDir, "kody.yml");
8053
+ const workflowDir = path28.join(cwd, ".github", "workflows");
8054
+ const workflowPath = path28.join(workflowDir, "kody.yml");
8029
8055
  if (fs29.existsSync(workflowPath) && !force) {
8030
8056
  skipped.push(".github/workflows/kody.yml");
8031
8057
  } else {
@@ -8033,13 +8059,13 @@ function performInit(cwd, force) {
8033
8059
  fs29.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
8034
8060
  wrote.push(".github/workflows/kody.yml");
8035
8061
  }
8036
- const hasUi = fs29.existsSync(path27.join(cwd, "src/app")) || fs29.existsSync(path27.join(cwd, "app")) || fs29.existsSync(path27.join(cwd, "pages"));
8062
+ const hasUi = fs29.existsSync(path28.join(cwd, "src/app")) || fs29.existsSync(path28.join(cwd, "app")) || fs29.existsSync(path28.join(cwd, "pages"));
8037
8063
  if (hasUi) {
8038
- const qaGuidePath = path27.join(cwd, QA_GUIDE_REL_PATH);
8064
+ const qaGuidePath = path28.join(cwd, QA_GUIDE_REL_PATH);
8039
8065
  if (fs29.existsSync(qaGuidePath) && !force) {
8040
8066
  skipped.push(QA_GUIDE_REL_PATH);
8041
8067
  } else {
8042
- fs29.mkdirSync(path27.dirname(qaGuidePath), { recursive: true });
8068
+ fs29.mkdirSync(path28.dirname(qaGuidePath), { recursive: true });
8043
8069
  const discovery = runQaDiscovery(cwd);
8044
8070
  fs29.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
8045
8071
  wrote.push(QA_GUIDE_REL_PATH);
@@ -8047,11 +8073,11 @@ function performInit(cwd, force) {
8047
8073
  }
8048
8074
  const builtinJobs = listBuiltinJobs();
8049
8075
  if (builtinJobs.length > 0) {
8050
- const jobsDir = path27.join(cwd, ".kody", "jobs");
8076
+ const jobsDir = path28.join(cwd, ".kody", "jobs");
8051
8077
  fs29.mkdirSync(jobsDir, { recursive: true });
8052
8078
  for (const job of builtinJobs) {
8053
- const rel = path27.join(".kody", "jobs", `${job.slug}.md`);
8054
- const target = path27.join(cwd, rel);
8079
+ const rel = path28.join(".kody", "jobs", `${job.slug}.md`);
8080
+ const target = path28.join(cwd, rel);
8055
8081
  if (fs29.existsSync(target) && !force) {
8056
8082
  skipped.push(rel);
8057
8083
  continue;
@@ -8068,7 +8094,7 @@ function performInit(cwd, force) {
8068
8094
  continue;
8069
8095
  }
8070
8096
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
8071
- const target = path27.join(workflowDir, `kody-${exe.name}.yml`);
8097
+ const target = path28.join(workflowDir, `kody-${exe.name}.yml`);
8072
8098
  if (fs29.existsSync(target) && !force) {
8073
8099
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
8074
8100
  continue;
@@ -8152,13 +8178,13 @@ init_loadCoverageRules();
8152
8178
 
8153
8179
  // src/goal/state.ts
8154
8180
  import * as fs30 from "fs";
8155
- import * as path28 from "path";
8181
+ import * as path29 from "path";
8156
8182
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
8157
8183
  var GoalStateError = class extends Error {
8158
- constructor(path37, message) {
8159
- super(`Invalid goal state at ${path37}:
8184
+ constructor(path38, message) {
8185
+ super(`Invalid goal state at ${path38}:
8160
8186
  ${message}`);
8161
- this.path = path37;
8187
+ this.path = path38;
8162
8188
  this.name = "GoalStateError";
8163
8189
  }
8164
8190
  path;
@@ -8206,7 +8232,7 @@ function serializeGoalState(s) {
8206
8232
  `;
8207
8233
  }
8208
8234
  function goalStatePath(cwd, goalId) {
8209
- return path28.join(cwd, ".kody", "goals", goalId, "state.json");
8235
+ return path29.join(cwd, ".kody", "goals", goalId, "state.json");
8210
8236
  }
8211
8237
  function readGoalState(cwd, goalId) {
8212
8238
  const file = goalStatePath(cwd, goalId);
@@ -8223,7 +8249,7 @@ function readGoalState(cwd, goalId) {
8223
8249
  }
8224
8250
  function writeGoalState(cwd, goalId, state) {
8225
8251
  const file = goalStatePath(cwd, goalId);
8226
- fs30.mkdirSync(path28.dirname(file), { recursive: true });
8252
+ fs30.mkdirSync(path29.dirname(file), { recursive: true });
8227
8253
  fs30.writeFileSync(file, serializeGoalState(state), "utf-8");
8228
8254
  }
8229
8255
  function nowIso() {
@@ -8322,7 +8348,7 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
8322
8348
 
8323
8349
  // src/scripts/loadJobFromFile.ts
8324
8350
  import * as fs31 from "fs";
8325
- import * as path29 from "path";
8351
+ import * as path30 from "path";
8326
8352
  var loadJobFromFile = async (ctx, _profile, args) => {
8327
8353
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
8328
8354
  const workersDir = String(args?.workersDir ?? ".kody/workers");
@@ -8331,7 +8357,7 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8331
8357
  if (!slug) {
8332
8358
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
8333
8359
  }
8334
- const absPath = path29.join(ctx.cwd, jobsDir, `${slug}.md`);
8360
+ const absPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
8335
8361
  if (!fs31.existsSync(absPath)) {
8336
8362
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
8337
8363
  }
@@ -8341,7 +8367,7 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8341
8367
  let workerTitle = "";
8342
8368
  let workerPersona = "";
8343
8369
  if (workerSlug) {
8344
- const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8370
+ const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8345
8371
  if (!fs31.existsSync(workerPath)) {
8346
8372
  throw new Error(
8347
8373
  `loadJobFromFile: job '${slug}' declares worker '${workerSlug}' but ${workerPath} does not exist`
@@ -8386,14 +8412,14 @@ function humanizeSlug(slug) {
8386
8412
 
8387
8413
  // src/scripts/loadWorkerAdhoc.ts
8388
8414
  import * as fs32 from "fs";
8389
- import * as path30 from "path";
8415
+ import * as path31 from "path";
8390
8416
  var loadWorkerAdhoc = async (ctx, _profile, args) => {
8391
8417
  const workersDir = String(args?.workersDir ?? ".kody/workers");
8392
8418
  const workerSlug = String(ctx.args.worker ?? "").trim();
8393
8419
  if (!workerSlug) {
8394
8420
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
8395
8421
  }
8396
- const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8422
+ const workerPath = path31.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8397
8423
  if (!fs32.existsSync(workerPath)) {
8398
8424
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
8399
8425
  }
@@ -8466,7 +8492,7 @@ init_events();
8466
8492
 
8467
8493
  // src/taskContext.ts
8468
8494
  import * as fs34 from "fs";
8469
- import * as path32 from "path";
8495
+ import * as path33 from "path";
8470
8496
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
8471
8497
  function buildTaskContext(args) {
8472
8498
  return {
@@ -8482,9 +8508,9 @@ function buildTaskContext(args) {
8482
8508
  }
8483
8509
  function persistTaskContext(cwd, ctx) {
8484
8510
  try {
8485
- const dir = path32.join(cwd, ".kody", "runs", ctx.runId);
8511
+ const dir = path33.join(cwd, ".kody", "runs", ctx.runId);
8486
8512
  fs34.mkdirSync(dir, { recursive: true });
8487
- const file = path32.join(dir, "task-context.json");
8513
+ const file = path33.join(dir, "task-context.json");
8488
8514
  fs34.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8489
8515
  `);
8490
8516
  return file;
@@ -9849,7 +9875,7 @@ function resolveBaseOverride(value) {
9849
9875
  // src/scripts/runTickScript.ts
9850
9876
  import { spawnSync } from "child_process";
9851
9877
  import * as fs35 from "fs";
9852
- import * as path33 from "path";
9878
+ import * as path34 from "path";
9853
9879
  var runTickScript = async (ctx, _profile, args) => {
9854
9880
  ctx.skipAgent = true;
9855
9881
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -9861,7 +9887,7 @@ var runTickScript = async (ctx, _profile, args) => {
9861
9887
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
9862
9888
  return;
9863
9889
  }
9864
- const jobPath = path33.join(ctx.cwd, jobsDir, `${slug}.md`);
9890
+ const jobPath = path34.join(ctx.cwd, jobsDir, `${slug}.md`);
9865
9891
  if (!fs35.existsSync(jobPath)) {
9866
9892
  ctx.output.exitCode = 99;
9867
9893
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
@@ -9875,7 +9901,7 @@ var runTickScript = async (ctx, _profile, args) => {
9875
9901
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
9876
9902
  return;
9877
9903
  }
9878
- const scriptPath = path33.isAbsolute(tickScript) ? tickScript : path33.join(ctx.cwd, tickScript);
9904
+ const scriptPath = path34.isAbsolute(tickScript) ? tickScript : path34.join(ctx.cwd, tickScript);
9879
9905
  if (!fs35.existsSync(scriptPath)) {
9880
9906
  ctx.output.exitCode = 99;
9881
9907
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
@@ -11154,9 +11180,9 @@ async function runExecutable(profileName, input) {
11154
11180
  })
11155
11181
  };
11156
11182
  })() : null;
11157
- const ndjsonDir = path34.join(input.cwd, ".kody");
11183
+ const ndjsonDir = path35.join(input.cwd, ".kody");
11158
11184
  const invokeAgent = async (prompt) => {
11159
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path34.isAbsolute(p) ? p : path34.resolve(profile.dir, p)).filter((p) => p.length > 0);
11185
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path35.isAbsolute(p) ? p : path35.resolve(profile.dir, p)).filter((p) => p.length > 0);
11160
11186
  const syntheticPath = ctx.data.syntheticPluginPath;
11161
11187
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
11162
11188
  return runAgent({
@@ -11372,13 +11398,13 @@ function getProfileInputsForChild(profileName, _cwd) {
11372
11398
  function resolveProfilePath(profileName) {
11373
11399
  const found = resolveExecutable(profileName);
11374
11400
  if (found) return found;
11375
- const here = path34.dirname(new URL(import.meta.url).pathname);
11401
+ const here = path35.dirname(new URL(import.meta.url).pathname);
11376
11402
  const candidates = [
11377
- path34.join(here, "executables", profileName, "profile.json"),
11403
+ path35.join(here, "executables", profileName, "profile.json"),
11378
11404
  // same-dir sibling (dev)
11379
- path34.join(here, "..", "executables", profileName, "profile.json"),
11405
+ path35.join(here, "..", "executables", profileName, "profile.json"),
11380
11406
  // up one (prod: dist/bin → dist/executables)
11381
- path34.join(here, "..", "src", "executables", profileName, "profile.json")
11407
+ path35.join(here, "..", "src", "executables", profileName, "profile.json")
11382
11408
  // fallback
11383
11409
  ];
11384
11410
  for (const c of candidates) {
@@ -11482,7 +11508,7 @@ function resolveShellTimeoutMs(entry) {
11482
11508
  var SIGKILL_GRACE_MS = 5e3;
11483
11509
  async function runShellEntry(entry, ctx, profile) {
11484
11510
  const shellName = entry.shell;
11485
- const shellPath = path34.join(profile.dir, shellName);
11511
+ const shellPath = path35.join(profile.dir, shellName);
11486
11512
  if (!fs37.existsSync(shellPath)) {
11487
11513
  ctx.skipAgent = true;
11488
11514
  ctx.output.exitCode = 99;
@@ -11962,9 +11988,9 @@ function resolveAuthToken(env = process.env) {
11962
11988
  return token;
11963
11989
  }
11964
11990
  function detectPackageManager2(cwd) {
11965
- if (fs38.existsSync(path35.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11966
- if (fs38.existsSync(path35.join(cwd, "yarn.lock"))) return "yarn";
11967
- if (fs38.existsSync(path35.join(cwd, "bun.lockb"))) return "bun";
11991
+ if (fs38.existsSync(path36.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11992
+ if (fs38.existsSync(path36.join(cwd, "yarn.lock"))) return "yarn";
11993
+ if (fs38.existsSync(path36.join(cwd, "bun.lockb"))) return "bun";
11968
11994
  return "npm";
11969
11995
  }
11970
11996
  function shellOut(cmd, args, cwd, stream = true) {
@@ -12051,7 +12077,7 @@ function configureGitIdentity(cwd) {
12051
12077
  }
12052
12078
  function postFailureTail(issueNumber, cwd, reason) {
12053
12079
  if (!issueNumber) return;
12054
- const logPath = path35.join(cwd, ".kody", "last-run.jsonl");
12080
+ const logPath = path36.join(cwd, ".kody", "last-run.jsonl");
12055
12081
  let tail = "";
12056
12082
  try {
12057
12083
  if (fs38.existsSync(logPath)) {
@@ -12080,7 +12106,7 @@ async function runCi(argv) {
12080
12106
  return 0;
12081
12107
  }
12082
12108
  const args = parseCiArgs(argv);
12083
- const cwd = args.cwd ? path35.resolve(args.cwd) : process.cwd();
12109
+ const cwd = args.cwd ? path36.resolve(args.cwd) : process.cwd();
12084
12110
  let earlyConfig;
12085
12111
  try {
12086
12112
  earlyConfig = loadConfig(cwd);
@@ -12351,12 +12377,12 @@ function parseChatArgs(argv, env = process.env) {
12351
12377
  return result;
12352
12378
  }
12353
12379
  function commitChatFiles(cwd, sessionId, verbose) {
12354
- const sessionFile = path36.relative(cwd, sessionFilePath(cwd, sessionId));
12355
- const eventsFile = path36.relative(cwd, eventsFilePath(cwd, sessionId));
12380
+ const sessionFile = path37.relative(cwd, sessionFilePath(cwd, sessionId));
12381
+ const eventsFile = path37.relative(cwd, eventsFilePath(cwd, sessionId));
12356
12382
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
12357
- const tasksDir = path36.join(".kody", "tasks", safeSession);
12383
+ const tasksDir = path37.join(".kody", "tasks", safeSession);
12358
12384
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
12359
- const paths = candidatePaths.filter((p) => fs39.existsSync(path36.join(cwd, p)));
12385
+ const paths = candidatePaths.filter((p) => fs39.existsSync(path37.join(cwd, p)));
12360
12386
  if (paths.length === 0) return;
12361
12387
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
12362
12388
  try {
@@ -12400,7 +12426,7 @@ async function runChat(argv) {
12400
12426
  ${CHAT_HELP}`);
12401
12427
  return 64;
12402
12428
  }
12403
- const cwd = args.cwd ? path36.resolve(args.cwd) : process.cwd();
12429
+ const cwd = args.cwd ? path37.resolve(args.cwd) : process.cwd();
12404
12430
  const sessionId = args.sessionId;
12405
12431
  const unpackedSecrets = unpackAllSecrets();
12406
12432
  if (unpackedSecrets > 0) {
@@ -0,0 +1,26 @@
1
+ ---
2
+ name: review-correctness
3
+ description: Correctness-focused PR reviewer. Inspects a diff and surrounding code for logic bugs, regressions, broken callers, missing edge cases, and test gaps. Returns findings only; never edits files.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You are a correctness reviewer examining one pull request. You are read-only: never edit files, never run `git`/`gh` write commands. Use Read / Grep / Glob and read-only `git diff` / `git show` to inspect.
8
+
9
+ Scope yourself to correctness and regression risk. Ignore security (another reviewer owns it) and pure style.
10
+
11
+ Method:
12
+ - Read the FULL changed files. A bug introduced 30 lines above a hunk won't show in the diff.
13
+ - For every modified function, grep the repo for its callers and existing tests. A signature or behavior change is only safe if callers and tests changed too.
14
+ - Check edge cases the diff may have dropped: empty input, null/undefined, boundary values, error paths. If a test was deleted, find what case it covered.
15
+ - Cite real `file:line` from files you actually read. Never invent citations.
16
+
17
+ Return ONLY this block — no preamble:
18
+
19
+ ```
20
+ CORRECTNESS
21
+ - severity: BLOCK | WARN | NONE
22
+ - findings:
23
+ - <file:line — concrete bug/regression and how it manifests at runtime, or "None">
24
+ ```
25
+
26
+ Use `BLOCK` only for a clear correctness or regression risk (wrong output, broken caller, dropped tested case). Test-coverage gaps that aren't outright bugs are `WARN`.
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: review-security
3
+ description: Security-focused PR reviewer. Inspects a diff and surrounding code for vulnerabilities — injection, authz/authn gaps, secret leakage, SSRF, unsafe deserialization, missing input validation. Returns findings only; never edits files.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You are a security reviewer examining one pull request. You are read-only: never edit files, never run `git`/`gh` write commands. Use Read / Grep / Glob and read-only `git diff` / `git show` to inspect.
8
+
9
+ Scope yourself strictly to security. Ignore style, naming, and general correctness unless it creates a security risk.
10
+
11
+ Method:
12
+ - Read the FULL changed files, not just the hunks — a vulnerability often lives outside the diff window.
13
+ - For every request handler, query, or external call in the diff, check: is user input validated? Is it parameterized? Is authorization checked before the sensitive action? Are secrets read from env, not hardcoded?
14
+ - Cite real `file:line` from files you actually read. Never invent citations.
15
+
16
+ Return ONLY this block — no preamble:
17
+
18
+ ```
19
+ SECURITY
20
+ - severity: BLOCK | WARN | NONE
21
+ - findings:
22
+ - <file:line — concrete issue and the exploit it enables, or "None">
23
+ ```
24
+
25
+ Use `BLOCK` only for a real, exploitable vulnerability introduced by this diff. Pre-existing issues the diff didn't touch are out of scope.
@@ -0,0 +1,25 @@
1
+ ---
2
+ name: review-style
3
+ description: Structure and convention reviewer. Inspects a diff for adherence to repo conventions, module organization, duplication, and documentation gaps. Returns findings only; never edits files.
4
+ tools: Read, Grep, Glob, Bash
5
+ ---
6
+
7
+ You are a structure/convention reviewer examining one pull request. You are read-only: never edit files, never run `git`/`gh` write commands. Use Read / Grep / Glob and read-only `git diff` / `git show` to inspect.
8
+
9
+ Scope yourself to structure, conventions, duplication, and docs. Do NOT flag things a linter/formatter would catch — that is not a reviewer's job. Ignore security and runtime correctness (other reviewers own those).
10
+
11
+ Method:
12
+ - When the PR adds a new module, find a sibling implementing the same pattern and check the new code follows it. If it diverges, name the sibling and why the divergence is or isn't justified.
13
+ - Flag genuine duplication (logic that already exists elsewhere) and missing docs the repo conventions clearly require (README/CHANGELOG for a public API).
14
+ - Cite real `file:line` from files you actually read. Never invent citations.
15
+
16
+ Return ONLY this block — no preamble:
17
+
18
+ ```
19
+ STRUCTURE
20
+ - severity: WARN | NONE
21
+ - findings:
22
+ - <file:line — concrete structural/convention/doc gap and the existing pattern it should follow, or "None">
23
+ ```
24
+
25
+ Structure findings never `BLOCK` — they are advisory. Use `WARN` for real gaps, `NONE` otherwise.
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "review-parallel",
3
+ "role": "primitive",
4
+ "phase": "reviewing",
5
+ "describe": "A/B variant of `review`: fans out to parallel read-only reviewer subagents (security, correctness, style) via the Task tool, then synthesizes ONE structured comment. Side-effect-light — posts a comment, never drives the pipeline. Used to benchmark swarm review against single-agent `review`.",
6
+ "inputs": [
7
+ {
8
+ "name": "pr",
9
+ "flag": "--pr",
10
+ "type": "int",
11
+ "required": true,
12
+ "describe": "GitHub PR number to review."
13
+ }
14
+ ],
15
+ "claudeCode": {
16
+ "model": "inherit",
17
+ "permissionMode": "default",
18
+ "maxTurns": null,
19
+ "systemPromptAppend": null,
20
+ "tools": [
21
+ "Read",
22
+ "Grep",
23
+ "Glob",
24
+ "Bash",
25
+ "Task"
26
+ ],
27
+ "hooks": ["block-write"],
28
+ "skills": [],
29
+ "commands": [],
30
+ "subagents": ["review-security", "review-correctness", "review-style"],
31
+ "plugins": [],
32
+ "mcpServers": []
33
+ },
34
+ "cliTools": [],
35
+ "scripts": {
36
+ "preflight": [
37
+ {
38
+ "script": "reviewFlow"
39
+ },
40
+ {
41
+ "script": "loadTaskState"
42
+ },
43
+ {
44
+ "script": "loadConventions"
45
+ },
46
+ {
47
+ "script": "composePrompt"
48
+ }
49
+ ],
50
+ "postflight": [
51
+ {
52
+ "script": "postReviewResult"
53
+ }
54
+ ]
55
+ }
56
+ }
@@ -0,0 +1,63 @@
1
+ You are Kody, a senior code reviewer leading a review of PR #{{pr.number}}. You coordinate three specialist reviewers, then write ONE structured review comment. Do NOT edit any files. Do NOT run `git`/`gh` write commands. Read-only inspection only.
2
+
3
+ # PR #{{pr.number}}: {{pr.title}}
4
+
5
+ Base: {{pr.baseRefName}} ← Head: {{pr.headRefName}}
6
+
7
+ {{pr.body}}
8
+
9
+ {{conventionsBlock}}
10
+
11
+ # Diff
12
+
13
+ ```diff
14
+ {{prDiff}}
15
+ ```
16
+
17
+ # How to run this review
18
+
19
+ 1. **Fan out in parallel.** In a SINGLE message, issue three `Task` calls — one to each subagent — so they run concurrently:
20
+ - `review-security` — security vulnerabilities.
21
+ - `review-correctness` — logic bugs, regressions, test gaps.
22
+ - `review-style` — structure, conventions, duplication, docs.
23
+
24
+ Give each subagent the same context: PR #{{pr.number}}, the base/head refs above, and the diff. Instruct each to read the full changed files (not just hunks) and return only its structured block.
25
+
26
+ 2. **Synthesize.** Once all three return, merge their findings into the single comment below. Resolve the verdict from the worst severity reported:
27
+ - any `BLOCK` (security or correctness) → **FAIL**
28
+ - no BLOCK but any `WARN` → **CONCERNS**
29
+ - all `NONE` → **PASS**
30
+
31
+ 3. Drop duplicate findings, keep every distinct `file:line` citation. Do not invent citations — only pass through what the subagents reported.
32
+
33
+ # Required output
34
+
35
+ Your FINAL message must be exactly this markdown — no preamble, no DONE/COMMIT_MSG markers. The entire final message IS the review comment, posted verbatim:
36
+
37
+ ```
38
+ ## Verdict: PASS | CONCERNS | FAIL
39
+
40
+ > Reviewed in parallel by 3 subagents (security · correctness · structure).
41
+
42
+ ### Summary
43
+ <2-3 sentences: what this PR does, is the approach sound>
44
+
45
+ ### Strengths
46
+ - <bullet>
47
+
48
+ ### Concerns
49
+ - <bullet with file:line, or "None">
50
+
51
+ ### Suggestions
52
+ - <bullet with file:line where possible, or "None">
53
+
54
+ ### Bottom line
55
+ <one sentence>
56
+ ```
57
+
58
+ # Rules
59
+
60
+ - No file edits. No `git`/`gh` writes. Read-only.
61
+ - Every citation must come from a file a subagent actually read — no citations from memory.
62
+ - **FAIL** only for clear correctness/security/regression risk. **CONCERNS** for test-coverage/doc/structural gaps that shouldn't block. **PASS** when the PR meets spec with no blocking issues.
63
+ - Pre-existing issues the diff didn't touch are out of scope.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.100",
3
+ "version": "0.4.102",
4
4
  "description": "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",