@kody-ade/kody-engine 0.4.140 → 0.4.142

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
@@ -93,7 +93,7 @@ var init_events = __esm({
93
93
  // src/verify.ts
94
94
  import { spawn } from "child_process";
95
95
  function runCommand(command, cwd) {
96
- return new Promise((resolve5) => {
96
+ return new Promise((resolve6) => {
97
97
  const start = Date.now();
98
98
  const child = spawn(command, {
99
99
  cwd,
@@ -122,11 +122,11 @@ function runCommand(command, cwd) {
122
122
  child.on("exit", (code) => {
123
123
  clearTimeout(timer);
124
124
  const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
125
- resolve5({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
125
+ resolve6({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
126
126
  });
127
127
  child.on("error", (err) => {
128
128
  clearTimeout(timer);
129
- resolve5({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
129
+ resolve6({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
130
130
  });
131
131
  });
132
132
  }
@@ -358,6 +358,141 @@ var init_submitMcp = __esm({
358
358
  }
359
359
  });
360
360
 
361
+ // src/repoWorkspace.ts
362
+ import { spawn as spawn2, spawnSync } from "child_process";
363
+ import * as fs6 from "fs";
364
+ import * as path6 from "path";
365
+ async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
366
+ const name = repo?.trim();
367
+ if (!name || !REPO_RE.test(name)) return null;
368
+ const root = path6.resolve(reposRoot);
369
+ const dir = path6.resolve(root, name);
370
+ if (dir !== root && !dir.startsWith(root + path6.sep)) return null;
371
+ if (fs6.existsSync(path6.join(dir, ".git"))) return dir;
372
+ const inflight = repoClones.get(dir);
373
+ if (inflight) {
374
+ await inflight;
375
+ return dir;
376
+ }
377
+ const p = cloneRepo(name, repoToken, dir).finally(() => {
378
+ if (repoClones.get(dir) === p) repoClones.delete(dir);
379
+ });
380
+ repoClones.set(dir, p);
381
+ await p;
382
+ return dir;
383
+ }
384
+ async function ensureRepoCwd(opts) {
385
+ const dir = await resolveAndClone(
386
+ opts.reposRoot,
387
+ opts.repo,
388
+ opts.repoToken,
389
+ opts.cloneRepo
390
+ );
391
+ return dir ?? opts.baseCwd;
392
+ }
393
+ async function fetchRepo(opts) {
394
+ const dir = await resolveAndClone(
395
+ opts.reposRoot,
396
+ opts.repo,
397
+ opts.repoToken,
398
+ opts.cloneRepo ?? defaultCloneRepo
399
+ );
400
+ if (!dir) {
401
+ throw new Error(
402
+ `invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`
403
+ );
404
+ }
405
+ return dir;
406
+ }
407
+ var REPO_RE, repoClones, defaultCloneRepo;
408
+ var init_repoWorkspace = __esm({
409
+ "src/repoWorkspace.ts"() {
410
+ "use strict";
411
+ REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
412
+ repoClones = /* @__PURE__ */ new Map();
413
+ defaultCloneRepo = (repo, token, dir) => {
414
+ fs6.mkdirSync(path6.dirname(dir), { recursive: true });
415
+ const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
416
+ return new Promise((resolve6, reject) => {
417
+ const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
418
+ stdio: "inherit"
419
+ });
420
+ child.on("exit", (code) => {
421
+ if (code !== 0) {
422
+ reject(new Error(`git clone ${repo} failed (exit ${code})`));
423
+ return;
424
+ }
425
+ try {
426
+ const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
427
+ const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
428
+ spawnSync("git", ["-C", dir, "config", "user.name", name]);
429
+ spawnSync("git", ["-C", dir, "config", "user.email", email]);
430
+ } catch {
431
+ }
432
+ resolve6();
433
+ });
434
+ child.on("error", reject);
435
+ });
436
+ };
437
+ }
438
+ });
439
+
440
+ // src/fetchRepoMcp.ts
441
+ var fetchRepoMcp_exports = {};
442
+ __export(fetchRepoMcp_exports, {
443
+ buildFetchRepoMcpServer: () => buildFetchRepoMcpServer
444
+ });
445
+ import {
446
+ createSdkMcpServer as createSdkMcpServer3,
447
+ tool as tool3
448
+ } from "@anthropic-ai/claude-agent-sdk";
449
+ import { z as z3 } from "zod";
450
+ function buildFetchRepoMcpServer(opts) {
451
+ const fetchTool = tool3(
452
+ "fetch_repo",
453
+ 'Clone another GitHub repository into your workspace so you can read and work on it. Pass `repo` as "owner/name" (e.g. "A-Guy-educ/A-Guy"). Returns the absolute path of the clone \u2014 then use your Read/Grep/Glob/Bash tools at that path to inspect it. Already-fetched repos are reused instantly. Use this whenever the user asks about a repository other than your current one \u2014 you are NOT limited to a single repo.',
454
+ {
455
+ repo: z3.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
456
+ },
457
+ async (args) => {
458
+ const repo = String(args.repo ?? "").trim();
459
+ try {
460
+ const dir = await fetchRepo({
461
+ reposRoot: opts.reposRoot,
462
+ repo,
463
+ repoToken: opts.repoToken
464
+ });
465
+ return {
466
+ content: [
467
+ {
468
+ type: "text",
469
+ text: `Cloned ${repo} \u2192 ${dir}
470
+ Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in your workspace alongside any other repos you've fetched.`
471
+ }
472
+ ]
473
+ };
474
+ } catch (err) {
475
+ const msg = err instanceof Error ? err.message : String(err);
476
+ return {
477
+ content: [{ type: "text", text: `Could not fetch ${repo}: ${msg}` }],
478
+ isError: true
479
+ };
480
+ }
481
+ }
482
+ );
483
+ return createSdkMcpServer3({
484
+ name: "kody-fetch-repo",
485
+ version: "0.1.0",
486
+ tools: [fetchTool]
487
+ });
488
+ }
489
+ var init_fetchRepoMcp = __esm({
490
+ "src/fetchRepoMcp.ts"() {
491
+ "use strict";
492
+ init_repoWorkspace();
493
+ }
494
+ });
495
+
361
496
  // src/issue.ts
362
497
  import { execFileSync } from "child_process";
363
498
  function ghToken() {
@@ -515,16 +650,16 @@ var init_issue = __esm({
515
650
  });
516
651
 
517
652
  // src/prompt.ts
518
- import * as fs19 from "fs";
519
- import * as path18 from "path";
653
+ import * as fs20 from "fs";
654
+ import * as path19 from "path";
520
655
  function loadProjectConventions(projectDir) {
521
656
  const out = [];
522
657
  for (const rel of CONVENTION_FILES) {
523
- const abs = path18.join(projectDir, rel);
524
- if (!fs19.existsSync(abs)) continue;
658
+ const abs = path19.join(projectDir, rel);
659
+ if (!fs20.existsSync(abs)) continue;
525
660
  let content;
526
661
  try {
527
- content = fs19.readFileSync(abs, "utf-8");
662
+ content = fs20.readFileSync(abs, "utf-8");
528
663
  } catch {
529
664
  continue;
530
665
  }
@@ -673,7 +808,7 @@ __export(loadMemoryContext_exports, {
673
808
  loadMemoryContext: () => loadMemoryContext
674
809
  });
675
810
  import * as fs32 from "fs";
676
- import * as path31 from "path";
811
+ import * as path30 from "path";
677
812
  function collectPages(memoryAbs) {
678
813
  const out = [];
679
814
  walkMd(memoryAbs, (file) => {
@@ -690,10 +825,10 @@ function collectPages(memoryAbs) {
690
825
  return;
691
826
  }
692
827
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
693
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path31.basename(file, ".md");
828
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path30.basename(file, ".md");
694
829
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
695
830
  out.push({
696
- relPath: path31.relative(memoryAbs, file),
831
+ relPath: path30.relative(memoryAbs, file),
697
832
  title,
698
833
  updated,
699
834
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -767,7 +902,7 @@ function walkMd(root, visit) {
767
902
  }
768
903
  for (const name of names) {
769
904
  if (name.startsWith(".")) continue;
770
- const full = path31.join(dir, name);
905
+ const full = path30.join(dir, name);
771
906
  let stat;
772
907
  try {
773
908
  stat = fs32.statSync(full);
@@ -793,7 +928,7 @@ var init_loadMemoryContext = __esm({
793
928
  TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
794
929
  loadMemoryContext = async (ctx) => {
795
930
  if (typeof ctx.data.memoryContext === "string") return;
796
- const memoryAbs = path31.join(ctx.cwd, MEMORY_DIR_RELATIVE);
931
+ const memoryAbs = path30.join(ctx.cwd, MEMORY_DIR_RELATIVE);
797
932
  if (!fs32.existsSync(memoryAbs)) {
798
933
  ctx.data.memoryContext = "";
799
934
  return;
@@ -926,7 +1061,7 @@ var init_loadPriorArt = __esm({
926
1061
  // package.json
927
1062
  var package_default = {
928
1063
  name: "@kody-ade/kody-engine",
929
- version: "0.4.140",
1064
+ version: "0.4.142",
930
1065
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
931
1066
  license: "MIT",
932
1067
  type: "module",
@@ -982,9 +1117,9 @@ var package_default = {
982
1117
  };
983
1118
 
984
1119
  // src/chat-cli.ts
985
- import { execFileSync as execFileSync32 } from "child_process";
1120
+ import { execFileSync as execFileSync30 } from "child_process";
986
1121
  import * as fs42 from "fs";
987
- import * as path39 from "path";
1122
+ import * as path38 from "path";
988
1123
 
989
1124
  // src/chat/events.ts
990
1125
  import * as fs from "fs";
@@ -1050,8 +1185,8 @@ function makeRunId(sessionId, suffix) {
1050
1185
  }
1051
1186
 
1052
1187
  // src/chat/loop.ts
1053
- import * as fs9 from "fs";
1054
- import * as path9 from "path";
1188
+ import * as fs10 from "fs";
1189
+ import * as path10 from "path";
1055
1190
 
1056
1191
  // src/task-artifacts.ts
1057
1192
  import fs2 from "fs";
@@ -1142,8 +1277,8 @@ function taskArtifactsPromptAddendum(opts) {
1142
1277
  }
1143
1278
 
1144
1279
  // src/agent.ts
1145
- import * as fs6 from "fs";
1146
- import * as path6 from "path";
1280
+ import * as fs7 from "fs";
1281
+ import * as path7 from "path";
1147
1282
  import { query } from "@anthropic-ai/claude-agent-sdk";
1148
1283
 
1149
1284
  // src/claudeBinary.ts
@@ -1528,10 +1663,10 @@ function resolveTurnTimeoutMs(opts) {
1528
1663
  return DEFAULT_TURN_TIMEOUT_MS;
1529
1664
  }
1530
1665
  async function runAgent(opts) {
1531
- const ndjsonDir = opts.ndjsonDir ?? path6.join(opts.cwd, ".kody");
1532
- fs6.mkdirSync(ndjsonDir, { recursive: true });
1533
- const ndjsonPath = path6.join(ndjsonDir, "last-run.jsonl");
1534
- const fullLog = fs6.createWriteStream(ndjsonPath, { flags: "w" });
1666
+ const ndjsonDir = opts.ndjsonDir ?? path7.join(opts.cwd, ".kody");
1667
+ fs7.mkdirSync(ndjsonDir, { recursive: true });
1668
+ const ndjsonPath = path7.join(ndjsonDir, "last-run.jsonl");
1669
+ const fullLog = fs7.createWriteStream(ndjsonPath, { flags: "w" });
1535
1670
  const env = {
1536
1671
  ...process.env,
1537
1672
  SKIP_HOOKS: "1",
@@ -1565,7 +1700,9 @@ async function runAgent(opts) {
1565
1700
  const queryOptions = {
1566
1701
  model: opts.model.model,
1567
1702
  cwd: opts.cwd,
1568
- allowedTools: opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS,
1703
+ // Fresh array (never mutate the shared DEFAULT_ALLOWED_TOOLS const) so
1704
+ // opt-in tools like fetch_repo can be appended below.
1705
+ allowedTools: [...opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS],
1569
1706
  permissionMode: opts.permissionModeOverride ?? "acceptEdits",
1570
1707
  env
1571
1708
  };
@@ -1594,6 +1731,16 @@ async function runAgent(opts) {
1594
1731
  getSubmitted = submitHandle.getSubmitted;
1595
1732
  mcpEntries.push(["kody-submit", submitHandle.server]);
1596
1733
  }
1734
+ if (opts.enableFetchRepoTool && opts.reposRoot) {
1735
+ const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
1736
+ const fetchServer = buildFetchRepoMcpServer2({
1737
+ reposRoot: opts.reposRoot,
1738
+ repoToken: opts.repoToken
1739
+ });
1740
+ mcpEntries.push(["kody-fetch-repo", fetchServer]);
1741
+ queryOptions.allowedTools.push("mcp__kody-fetch-repo__fetch_repo");
1742
+ queryOptions.additionalDirectories = [opts.reposRoot];
1743
+ }
1597
1744
  if (mcpEntries.length > 0) {
1598
1745
  queryOptions.mcpServers = Object.fromEntries(mcpEntries);
1599
1746
  }
@@ -1641,10 +1788,10 @@ async function runAgent(opts) {
1641
1788
  let timer;
1642
1789
  let next;
1643
1790
  if (turnTimeoutMs > 0) {
1644
- const timeoutPromise = new Promise((resolve5) => {
1791
+ const timeoutPromise = new Promise((resolve6) => {
1645
1792
  timer = setTimeout(() => {
1646
1793
  timedOut = true;
1647
- resolve5({ done: true, value: void 0 });
1794
+ resolve6({ done: true, value: void 0 });
1648
1795
  }, turnTimeoutMs);
1649
1796
  });
1650
1797
  next = await Promise.race([nextPromise, timeoutPromise]);
@@ -1774,48 +1921,48 @@ async function runAgent(opts) {
1774
1921
  }
1775
1922
 
1776
1923
  // src/registry.ts
1777
- import * as fs7 from "fs";
1778
- import * as path7 from "path";
1924
+ import * as fs8 from "fs";
1925
+ import * as path8 from "path";
1779
1926
  function getExecutablesRoot() {
1780
- const here = path7.dirname(new URL(import.meta.url).pathname);
1927
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1781
1928
  const candidates = [
1782
- path7.join(here, "executables"),
1929
+ path8.join(here, "executables"),
1783
1930
  // dev: src/
1784
- path7.join(here, "..", "executables"),
1931
+ path8.join(here, "..", "executables"),
1785
1932
  // built: dist/bin → dist/executables
1786
- path7.join(here, "..", "src", "executables")
1933
+ path8.join(here, "..", "src", "executables")
1787
1934
  // fallback
1788
1935
  ];
1789
1936
  for (const c of candidates) {
1790
- if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
1937
+ if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
1791
1938
  }
1792
1939
  return candidates[0];
1793
1940
  }
1794
1941
  function getProjectExecutablesRoot() {
1795
- return path7.join(process.cwd(), ".kody", "executables");
1942
+ return path8.join(process.cwd(), ".kody", "executables");
1796
1943
  }
1797
1944
  function getBuiltinJobsRoot() {
1798
- const here = path7.dirname(new URL(import.meta.url).pathname);
1945
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1799
1946
  const candidates = [
1800
- path7.join(here, "jobs"),
1947
+ path8.join(here, "jobs"),
1801
1948
  // dev: src/
1802
- path7.join(here, "..", "jobs"),
1949
+ path8.join(here, "..", "jobs"),
1803
1950
  // built: dist/bin → dist/jobs
1804
- path7.join(here, "..", "src", "jobs")
1951
+ path8.join(here, "..", "src", "jobs")
1805
1952
  // fallback
1806
1953
  ];
1807
1954
  for (const c of candidates) {
1808
- if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
1955
+ if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
1809
1956
  }
1810
1957
  return candidates[0];
1811
1958
  }
1812
1959
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
1813
- if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
1960
+ if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
1814
1961
  const out = [];
1815
- for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
1962
+ for (const ent of fs8.readdirSync(root, { withFileTypes: true })) {
1816
1963
  if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
1817
1964
  const slug = ent.name.slice(0, -3);
1818
- out.push({ slug, filePath: path7.join(root, ent.name) });
1965
+ out.push({ slug, filePath: path8.join(root, ent.name) });
1819
1966
  }
1820
1967
  out.sort((a, b) => a.slug.localeCompare(b.slug));
1821
1968
  return out;
@@ -1828,13 +1975,13 @@ function listExecutables(roots = getExecutableRoots()) {
1828
1975
  const seen = /* @__PURE__ */ new Set();
1829
1976
  const out = [];
1830
1977
  for (const root of rootList) {
1831
- if (!fs7.existsSync(root)) continue;
1832
- const entries = fs7.readdirSync(root, { withFileTypes: true });
1978
+ if (!fs8.existsSync(root)) continue;
1979
+ const entries = fs8.readdirSync(root, { withFileTypes: true });
1833
1980
  for (const ent of entries) {
1834
1981
  if (!ent.isDirectory()) continue;
1835
1982
  if (seen.has(ent.name)) continue;
1836
- const profilePath = path7.join(root, ent.name, "profile.json");
1837
- if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
1983
+ const profilePath = path8.join(root, ent.name, "profile.json");
1984
+ if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
1838
1985
  out.push({ name: ent.name, profilePath });
1839
1986
  seen.add(ent.name);
1840
1987
  }
@@ -1846,8 +1993,8 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
1846
1993
  if (!isSafeName(name)) return null;
1847
1994
  const rootList = typeof roots === "string" ? [roots] : roots;
1848
1995
  for (const root of rootList) {
1849
- const profilePath = path7.join(root, name, "profile.json");
1850
- if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
1996
+ const profilePath = path8.join(root, name, "profile.json");
1997
+ if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
1851
1998
  return profilePath;
1852
1999
  }
1853
2000
  }
@@ -1863,7 +2010,7 @@ function getProfileInputs(name, roots = getExecutableRoots()) {
1863
2010
  const profilePath = resolveExecutable(name, roots);
1864
2011
  if (!profilePath) return null;
1865
2012
  try {
1866
- const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
2013
+ const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
1867
2014
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
1868
2015
  return raw.inputs;
1869
2016
  } catch {
@@ -1893,14 +2040,14 @@ function parseGenericFlags(argv) {
1893
2040
  }
1894
2041
 
1895
2042
  // src/chat/session.ts
1896
- import * as fs8 from "fs";
1897
- import * as path8 from "path";
2043
+ import * as fs9 from "fs";
2044
+ import * as path9 from "path";
1898
2045
  function sessionFilePath(cwd, sessionId) {
1899
- return path8.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
2046
+ return path9.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
1900
2047
  }
1901
2048
  function readMeta(file) {
1902
- if (!fs8.existsSync(file)) return null;
1903
- const raw = fs8.readFileSync(file, "utf-8");
2049
+ if (!fs9.existsSync(file)) return null;
2050
+ const raw = fs9.readFileSync(file, "utf-8");
1904
2051
  const firstLine2 = raw.split("\n", 1)[0]?.trim();
1905
2052
  if (!firstLine2) return null;
1906
2053
  try {
@@ -1913,8 +2060,8 @@ function readMeta(file) {
1913
2060
  }
1914
2061
  }
1915
2062
  function readSession(file) {
1916
- if (!fs8.existsSync(file)) return [];
1917
- const raw = fs8.readFileSync(file, "utf-8").trim();
2063
+ if (!fs9.existsSync(file)) return [];
2064
+ const raw = fs9.readFileSync(file, "utf-8").trim();
1918
2065
  if (!raw) return [];
1919
2066
  const turns = [];
1920
2067
  for (const line of raw.split("\n")) {
@@ -1930,14 +2077,14 @@ function readSession(file) {
1930
2077
  return turns;
1931
2078
  }
1932
2079
  function appendTurn(file, turn) {
1933
- fs8.mkdirSync(path8.dirname(file), { recursive: true });
2080
+ fs9.mkdirSync(path9.dirname(file), { recursive: true });
1934
2081
  const line = JSON.stringify({
1935
2082
  role: turn.role,
1936
2083
  content: turn.content,
1937
2084
  timestamp: turn.timestamp,
1938
2085
  toolCalls: turn.toolCalls ?? []
1939
2086
  });
1940
- fs8.appendFileSync(file, `${line}
2087
+ fs9.appendFileSync(file, `${line}
1941
2088
  `);
1942
2089
  }
1943
2090
  function seedInitialMessage(file, message) {
@@ -2037,6 +2184,15 @@ var CHAT_SYSTEM_PROMPT = [
2037
2184
  "Do not invent file paths, commit SHAs, line numbers, or command output. If you",
2038
2185
  "cite something concrete, you must have just read or run it in this session."
2039
2186
  ].join("\n");
2187
+ var CROSS_REPO_PROMPT = [
2188
+ "# Working across repositories",
2189
+ "You are NOT limited to the repository at your current working directory. You",
2190
+ 'have a `fetch_repo` tool: call fetch_repo("owner/name") to clone another repo',
2191
+ "into your workspace; it returns an absolute path. Then use Read/Grep/Glob/Bash",
2192
+ "at that path to inspect or work on it. Already-fetched repos are reused",
2193
+ "instantly. When the user asks about a different repo \u2014 or to compare repos \u2014",
2194
+ "fetch it instead of saying you are scoped to a single repo."
2195
+ ].join("\n");
2040
2196
  function buildExecutableCatalog() {
2041
2197
  let discovered;
2042
2198
  try {
@@ -2047,7 +2203,7 @@ function buildExecutableCatalog() {
2047
2203
  const entries = [];
2048
2204
  for (const { name, profilePath } of discovered) {
2049
2205
  try {
2050
- const raw = JSON.parse(fs9.readFileSync(profilePath, "utf-8"));
2206
+ const raw = JSON.parse(fs10.readFileSync(profilePath, "utf-8"));
2051
2207
  const describe = typeof raw.describe === "string" ? raw.describe : "";
2052
2208
  const firstSentence = describe.split(/(?<=[.!?])\s+/, 1)[0] ?? "";
2053
2209
  entries.push({ name, describe: firstSentence.trim() });
@@ -2093,11 +2249,13 @@ async function runChatTurn(opts) {
2093
2249
  const contextBlock = readContextBlock(opts.cwd);
2094
2250
  const memoryBlock = readMemoryIndexBlock(opts.cwd);
2095
2251
  const instructionsBlock = readInstructionsBlock(opts.cwd);
2252
+ const crossRepoBlock = opts.reposRoot ? CROSS_REPO_PROMPT : null;
2096
2253
  const systemPrompt = [
2097
2254
  basePrompt,
2098
2255
  contextBlock,
2099
2256
  memoryBlock,
2100
2257
  instructionsBlock,
2258
+ crossRepoBlock,
2101
2259
  catalog,
2102
2260
  artifactAddendum
2103
2261
  ].filter((s) => typeof s === "string" && s.length > 0).join("\n\n");
@@ -2111,6 +2269,14 @@ async function runChatTurn(opts) {
2111
2269
  verbose: opts.verbose,
2112
2270
  quiet: opts.quiet,
2113
2271
  systemPromptAppend: systemPrompt,
2272
+ // Let the agent clone + work on OTHER repos mid-conversation (a
2273
+ // repo-less Brain serves many). Enabled whenever we know where repos
2274
+ // live; grants read access to that root via additionalDirectories.
2275
+ ...opts.reposRoot ? {
2276
+ enableFetchRepoTool: true,
2277
+ reposRoot: opts.reposRoot,
2278
+ repoToken: opts.repoToken
2279
+ } : {},
2114
2280
  onProgress: async (ev) => {
2115
2281
  progressSeq += 1;
2116
2282
  if (ev.kind === "thinking") {
@@ -2195,10 +2361,10 @@ async function emit(sink, type, sessionId, suffix, payload) {
2195
2361
  var MEMORY_INDEX_REL = ".kody/memory/INDEX.md";
2196
2362
  var MAX_INDEX_BYTES = 8e3;
2197
2363
  function readMemoryIndexBlock(cwd) {
2198
- const indexPath = path9.join(cwd, MEMORY_INDEX_REL);
2364
+ const indexPath = path10.join(cwd, MEMORY_INDEX_REL);
2199
2365
  let raw;
2200
2366
  try {
2201
- raw = fs9.readFileSync(indexPath, "utf-8");
2367
+ raw = fs10.readFileSync(indexPath, "utf-8");
2202
2368
  } catch {
2203
2369
  return "";
2204
2370
  }
@@ -2216,17 +2382,17 @@ function readMemoryIndexBlock(cwd) {
2216
2382
  var CONTEXT_DIR_REL = ".kody/context";
2217
2383
  var MAX_CONTEXT_BYTES = 12e3;
2218
2384
  function readContextBlock(cwd) {
2219
- const dir = path9.join(cwd, CONTEXT_DIR_REL);
2385
+ const dir = path10.join(cwd, CONTEXT_DIR_REL);
2220
2386
  let files;
2221
2387
  try {
2222
- files = fs9.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
2388
+ files = fs10.readdirSync(dir).filter((f) => f.endsWith(".md")).sort();
2223
2389
  } catch {
2224
2390
  return "";
2225
2391
  }
2226
2392
  const sections = [];
2227
2393
  for (const file of files) {
2228
2394
  try {
2229
- const content = fs9.readFileSync(path9.join(dir, file), "utf-8").trim();
2395
+ const content = fs10.readFileSync(path10.join(dir, file), "utf-8").trim();
2230
2396
  if (content) sections.push(`### ${file.replace(/\.md$/, "")}
2231
2397
 
2232
2398
  ${content}`);
@@ -2247,10 +2413,10 @@ ${content}`);
2247
2413
  var INSTRUCTIONS_REL = ".kody/instructions.md";
2248
2414
  var MAX_INSTRUCTIONS_BYTES = 8e3;
2249
2415
  function readInstructionsBlock(cwd) {
2250
- const instructionsPath = path9.join(cwd, INSTRUCTIONS_REL);
2416
+ const instructionsPath = path10.join(cwd, INSTRUCTIONS_REL);
2251
2417
  let raw;
2252
2418
  try {
2253
- raw = fs9.readFileSync(instructionsPath, "utf-8");
2419
+ raw = fs10.readFileSync(instructionsPath, "utf-8");
2254
2420
  } catch {
2255
2421
  return "";
2256
2422
  }
@@ -2269,8 +2435,8 @@ function readInstructionsBlock(cwd) {
2269
2435
  // src/chat/modes/interactive.ts
2270
2436
  init_issue();
2271
2437
  import { execFileSync as execFileSync3 } from "child_process";
2272
- import * as fs10 from "fs";
2273
- import * as path10 from "path";
2438
+ import * as fs11 from "fs";
2439
+ import * as path11 from "path";
2274
2440
 
2275
2441
  // src/chat/inbox.ts
2276
2442
  import { execFileSync as execFileSync2 } from "child_process";
@@ -2318,7 +2484,7 @@ async function waitForNextUserMessage(opts) {
2318
2484
  }
2319
2485
  }
2320
2486
  function sleep(ms) {
2321
- return new Promise((resolve5) => setTimeout(resolve5, ms));
2487
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
2322
2488
  }
2323
2489
  function currentBranch(cwd) {
2324
2490
  try {
@@ -2429,9 +2595,9 @@ function findNextUserTurn(turns, fromIdx) {
2429
2595
  return -1;
2430
2596
  }
2431
2597
  function commitTurn(cwd, sessionId, _verbose) {
2432
- const sessionRel = path10.relative(cwd, sessionFilePath(cwd, sessionId));
2433
- const eventsRel = path10.relative(cwd, eventsFilePath(cwd, sessionId));
2434
- const rels = [sessionRel, eventsRel].filter((p) => fs10.existsSync(path10.join(cwd, p)));
2598
+ const sessionRel = path11.relative(cwd, sessionFilePath(cwd, sessionId));
2599
+ const eventsRel = path11.relative(cwd, eventsFilePath(cwd, sessionId));
2600
+ const rels = [sessionRel, eventsRel].filter((p) => fs11.existsSync(path11.join(cwd, p)));
2435
2601
  if (rels.length === 0) return;
2436
2602
  const repository = process.env.GITHUB_REPOSITORY;
2437
2603
  if (!repository) {
@@ -2443,8 +2609,8 @@ function commitTurn(cwd, sessionId, _verbose) {
2443
2609
  }
2444
2610
  const branch = defaultBranch(cwd) ?? "main";
2445
2611
  for (const rel of rels) {
2446
- const repoPath = rel.split(path10.sep).join("/");
2447
- const localText = fs10.readFileSync(path10.join(cwd, rel), "utf-8");
2612
+ const repoPath = rel.split(path11.sep).join("/");
2613
+ const localText = fs11.readFileSync(path11.join(cwd, rel), "utf-8");
2448
2614
  putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
2449
2615
  }
2450
2616
  }
@@ -2533,12 +2699,12 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2533
2699
  }
2534
2700
 
2535
2701
  // src/kody-cli.ts
2536
- import { execFileSync as execFileSync31 } from "child_process";
2702
+ import { execFileSync as execFileSync29 } from "child_process";
2537
2703
  import * as fs41 from "fs";
2538
- import * as path38 from "path";
2704
+ import * as path37 from "path";
2539
2705
 
2540
2706
  // src/dispatch.ts
2541
- import * as fs11 from "fs";
2707
+ import * as fs12 from "fs";
2542
2708
 
2543
2709
  // src/cron-match.ts
2544
2710
  var FIELD_BOUNDS = [
@@ -2622,10 +2788,10 @@ function autoDispatch(opts) {
2622
2788
  }
2623
2789
  const eventName = process.env.GITHUB_EVENT_NAME;
2624
2790
  const eventPath = process.env.GITHUB_EVENT_PATH;
2625
- if (!eventName || !eventPath || !fs11.existsSync(eventPath)) return null;
2791
+ if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
2626
2792
  let event = {};
2627
2793
  try {
2628
- event = JSON.parse(fs11.readFileSync(eventPath, "utf-8"));
2794
+ event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
2629
2795
  } catch {
2630
2796
  return null;
2631
2797
  }
@@ -2699,7 +2865,7 @@ function autoDispatchTyped(opts) {
2699
2865
  if (legacy) return { kind: "route", ...legacy };
2700
2866
  const eventName = process.env.GITHUB_EVENT_NAME;
2701
2867
  const eventPath = process.env.GITHUB_EVENT_PATH;
2702
- if (!eventName || !eventPath || !fs11.existsSync(eventPath)) {
2868
+ if (!eventName || !eventPath || !fs12.existsSync(eventPath)) {
2703
2869
  return { kind: "silent", reason: "no GHA event context" };
2704
2870
  }
2705
2871
  if (eventName !== "issue_comment") {
@@ -2707,7 +2873,7 @@ function autoDispatchTyped(opts) {
2707
2873
  }
2708
2874
  let event = {};
2709
2875
  try {
2710
- event = JSON.parse(fs11.readFileSync(eventPath, "utf-8"));
2876
+ event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
2711
2877
  } catch {
2712
2878
  return { kind: "silent", reason: "GHA event payload unreadable" };
2713
2879
  }
@@ -2745,7 +2911,7 @@ function dispatchScheduledWatches(opts) {
2745
2911
  for (const exe of listExecutables()) {
2746
2912
  let raw;
2747
2913
  try {
2748
- raw = fs11.readFileSync(exe.profilePath, "utf-8");
2914
+ raw = fs12.readFileSync(exe.profilePath, "utf-8");
2749
2915
  } catch {
2750
2916
  continue;
2751
2917
  }
@@ -2867,9 +3033,9 @@ function coerceBare(spec, value) {
2867
3033
  init_issue();
2868
3034
 
2869
3035
  // src/executor.ts
2870
- import { execFileSync as execFileSync30, spawn as spawn9 } from "child_process";
3036
+ import { execFileSync as execFileSync28, spawn as spawn9 } from "child_process";
2871
3037
  import * as fs40 from "fs";
2872
- import * as path37 from "path";
3038
+ import * as path36 from "path";
2873
3039
 
2874
3040
  // src/discipline.ts
2875
3041
  var DISCIPLINE = `# Working discipline (applies to this entire task)
@@ -2920,8 +3086,8 @@ init_events();
2920
3086
  init_issue();
2921
3087
 
2922
3088
  // src/profile.ts
2923
- import * as fs12 from "fs";
2924
- import * as path11 from "path";
3089
+ import * as fs13 from "fs";
3090
+ import * as path12 from "path";
2925
3091
 
2926
3092
  // src/profile-error.ts
2927
3093
  var ProfileError = class extends Error {
@@ -3089,12 +3255,12 @@ var KNOWN_PROFILE_KEYS = /* @__PURE__ */ new Set([
3089
3255
  "preloadContext"
3090
3256
  ]);
3091
3257
  function loadProfile(profilePath) {
3092
- if (!fs12.existsSync(profilePath)) {
3258
+ if (!fs13.existsSync(profilePath)) {
3093
3259
  throw new ProfileError(profilePath, "file not found");
3094
3260
  }
3095
3261
  let raw;
3096
3262
  try {
3097
- raw = JSON.parse(fs12.readFileSync(profilePath, "utf-8"));
3263
+ raw = JSON.parse(fs13.readFileSync(profilePath, "utf-8"));
3098
3264
  } catch (err) {
3099
3265
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
3100
3266
  }
@@ -3105,7 +3271,7 @@ function loadProfile(profilePath) {
3105
3271
  const unknownKeys = Object.keys(r).filter((k) => !KNOWN_PROFILE_KEYS.has(k));
3106
3272
  if (unknownKeys.length > 0) {
3107
3273
  process.stderr.write(
3108
- `[kody profile] ${path11.basename(path11.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
3274
+ `[kody profile] ${path12.basename(path12.dirname(profilePath))}: unknown top-level keys ignored: ${unknownKeys.join(", ")}
3109
3275
  `
3110
3276
  );
3111
3277
  }
@@ -3166,7 +3332,7 @@ function loadProfile(profilePath) {
3166
3332
  // Phase 5 in-process handoff opt-in. Default false; containers
3167
3333
  // flip to true after end-to-end verification.
3168
3334
  preloadContext: r.preloadContext === true,
3169
- dir: path11.dirname(profilePath)
3335
+ dir: path12.dirname(profilePath)
3170
3336
  };
3171
3337
  if (lifecycle) {
3172
3338
  applyLifecycle(profile, profilePath);
@@ -3536,10 +3702,10 @@ function errMsg(err) {
3536
3702
  }
3537
3703
 
3538
3704
  // src/litellm.ts
3539
- import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
3540
- import * as fs13 from "fs";
3705
+ import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
3706
+ import * as fs14 from "fs";
3541
3707
  import * as os2 from "os";
3542
- import * as path12 from "path";
3708
+ import * as path13 from "path";
3543
3709
  async function checkLitellmHealth(url) {
3544
3710
  try {
3545
3711
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -3586,20 +3752,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3586
3752
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
3587
3753
  }
3588
3754
  }
3589
- const configPath = path12.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3590
- fs13.writeFileSync(configPath, generateLitellmConfigYaml(model));
3755
+ const configPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3756
+ fs14.writeFileSync(configPath, generateLitellmConfigYaml(model));
3591
3757
  const portMatch = url.match(/:(\d+)/);
3592
3758
  const port = portMatch ? portMatch[1] : "4000";
3593
3759
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
3594
3760
  const dotenvVars = readDotenvApiKeys(projectDir);
3595
- const logPath = path12.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3596
- const outFd = fs13.openSync(logPath, "w");
3597
- const child = spawn2(cmd, args, {
3761
+ const logPath = path13.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3762
+ const outFd = fs14.openSync(logPath, "w");
3763
+ const child = spawn3(cmd, args, {
3598
3764
  stdio: ["ignore", outFd, outFd],
3599
3765
  detached: true,
3600
3766
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
3601
3767
  });
3602
- fs13.closeSync(outFd);
3768
+ fs14.closeSync(outFd);
3603
3769
  const timeoutMs = resolveLitellmTimeoutMs();
3604
3770
  const deadline = Date.now() + timeoutMs;
3605
3771
  while (Date.now() < deadline) {
@@ -3618,7 +3784,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3618
3784
  }
3619
3785
  let logTail = "";
3620
3786
  try {
3621
- logTail = fs13.readFileSync(logPath, "utf-8").slice(-2e3);
3787
+ logTail = fs14.readFileSync(logPath, "utf-8").slice(-2e3);
3622
3788
  } catch {
3623
3789
  }
3624
3790
  try {
@@ -3630,10 +3796,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3630
3796
  ${logTail}`);
3631
3797
  }
3632
3798
  function readDotenvApiKeys(projectDir) {
3633
- const dotenvPath = path12.join(projectDir, ".env");
3634
- if (!fs13.existsSync(dotenvPath)) return {};
3799
+ const dotenvPath = path13.join(projectDir, ".env");
3800
+ if (!fs14.existsSync(dotenvPath)) return {};
3635
3801
  const result = {};
3636
- for (const rawLine of fs13.readFileSync(dotenvPath, "utf-8").split("\n")) {
3802
+ for (const rawLine of fs14.readFileSync(dotenvPath, "utf-8").split("\n")) {
3637
3803
  const line = rawLine.trim();
3638
3804
  if (!line || line.startsWith("#")) continue;
3639
3805
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -3656,25 +3822,25 @@ function stripBlockingEnv(env) {
3656
3822
  }
3657
3823
 
3658
3824
  // src/subagents.ts
3659
- import * as fs15 from "fs";
3660
- import * as path14 from "path";
3825
+ import * as fs16 from "fs";
3826
+ import * as path15 from "path";
3661
3827
 
3662
3828
  // src/scripts/buildSyntheticPlugin.ts
3663
- import * as fs14 from "fs";
3829
+ import * as fs15 from "fs";
3664
3830
  import * as os3 from "os";
3665
- import * as path13 from "path";
3831
+ import * as path14 from "path";
3666
3832
  function getPluginsCatalogRoot() {
3667
- const here = path13.dirname(new URL(import.meta.url).pathname);
3833
+ const here = path14.dirname(new URL(import.meta.url).pathname);
3668
3834
  const candidates = [
3669
- path13.join(here, "..", "plugins"),
3835
+ path14.join(here, "..", "plugins"),
3670
3836
  // dev: src/scripts → src/plugins
3671
- path13.join(here, "..", "..", "plugins"),
3837
+ path14.join(here, "..", "..", "plugins"),
3672
3838
  // built: dist/scripts → dist/plugins
3673
- path13.join(here, "..", "..", "src", "plugins")
3839
+ path14.join(here, "..", "..", "src", "plugins")
3674
3840
  // fallback
3675
3841
  ];
3676
3842
  for (const c of candidates) {
3677
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
3843
+ if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
3678
3844
  }
3679
3845
  return candidates[0];
3680
3846
  }
@@ -3684,45 +3850,45 @@ var buildSyntheticPlugin = async (ctx, profile) => {
3684
3850
  if (!needsSynthetic) return;
3685
3851
  const catalog = getPluginsCatalogRoot();
3686
3852
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
3687
- const root = path13.join(os3.tmpdir(), `kody-synth-${runId}`);
3688
- fs14.mkdirSync(path13.join(root, ".claude-plugin"), { recursive: true });
3853
+ const root = path14.join(os3.tmpdir(), `kody-synth-${runId}`);
3854
+ fs15.mkdirSync(path14.join(root, ".claude-plugin"), { recursive: true });
3689
3855
  const resolvePart = (bucket, entry) => {
3690
- const local = path13.join(profile.dir, bucket, entry);
3691
- if (fs14.existsSync(local)) return local;
3692
- const central = path13.join(catalog, bucket, entry);
3693
- if (fs14.existsSync(central)) return central;
3856
+ const local = path14.join(profile.dir, bucket, entry);
3857
+ if (fs15.existsSync(local)) return local;
3858
+ const central = path14.join(catalog, bucket, entry);
3859
+ if (fs15.existsSync(central)) return central;
3694
3860
  throw new Error(
3695
3861
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
3696
3862
  );
3697
3863
  };
3698
3864
  if (cc.skills.length > 0) {
3699
- const dst = path13.join(root, "skills");
3700
- fs14.mkdirSync(dst, { recursive: true });
3865
+ const dst = path14.join(root, "skills");
3866
+ fs15.mkdirSync(dst, { recursive: true });
3701
3867
  for (const name of cc.skills) {
3702
- copyDir(resolvePart("skills", name), path13.join(dst, name));
3868
+ copyDir(resolvePart("skills", name), path14.join(dst, name));
3703
3869
  }
3704
3870
  }
3705
3871
  if (cc.commands.length > 0) {
3706
- const dst = path13.join(root, "commands");
3707
- fs14.mkdirSync(dst, { recursive: true });
3872
+ const dst = path14.join(root, "commands");
3873
+ fs15.mkdirSync(dst, { recursive: true });
3708
3874
  for (const name of cc.commands) {
3709
- fs14.copyFileSync(resolvePart("commands", `${name}.md`), path13.join(dst, `${name}.md`));
3875
+ fs15.copyFileSync(resolvePart("commands", `${name}.md`), path14.join(dst, `${name}.md`));
3710
3876
  }
3711
3877
  }
3712
3878
  if (cc.hooks.length > 0) {
3713
- const dst = path13.join(root, "hooks");
3714
- fs14.mkdirSync(dst, { recursive: true });
3879
+ const dst = path14.join(root, "hooks");
3880
+ fs15.mkdirSync(dst, { recursive: true });
3715
3881
  const merged = { hooks: {} };
3716
3882
  for (const name of cc.hooks) {
3717
3883
  const src = resolvePart("hooks", `${name}.json`);
3718
- const parsed = JSON.parse(fs14.readFileSync(src, "utf-8"));
3884
+ const parsed = JSON.parse(fs15.readFileSync(src, "utf-8"));
3719
3885
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
3720
3886
  if (!Array.isArray(entries)) continue;
3721
3887
  if (!merged.hooks[event]) merged.hooks[event] = [];
3722
3888
  merged.hooks[event].push(...entries);
3723
3889
  }
3724
3890
  }
3725
- fs14.writeFileSync(path13.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
3891
+ fs15.writeFileSync(path14.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
3726
3892
  `);
3727
3893
  }
3728
3894
  const manifest = {
@@ -3732,17 +3898,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
3732
3898
  };
3733
3899
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
3734
3900
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
3735
- fs14.writeFileSync(path13.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
3901
+ fs15.writeFileSync(path14.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
3736
3902
  `);
3737
3903
  ctx.data.syntheticPluginPath = root;
3738
3904
  };
3739
3905
  function copyDir(src, dst) {
3740
- fs14.mkdirSync(dst, { recursive: true });
3741
- for (const ent of fs14.readdirSync(src, { withFileTypes: true })) {
3742
- const s = path13.join(src, ent.name);
3743
- const d = path13.join(dst, ent.name);
3906
+ fs15.mkdirSync(dst, { recursive: true });
3907
+ for (const ent of fs15.readdirSync(src, { withFileTypes: true })) {
3908
+ const s = path14.join(src, ent.name);
3909
+ const d = path14.join(dst, ent.name);
3744
3910
  if (ent.isDirectory()) copyDir(s, d);
3745
- else if (ent.isFile()) fs14.copyFileSync(s, d);
3911
+ else if (ent.isFile()) fs15.copyFileSync(s, d);
3746
3912
  }
3747
3913
  }
3748
3914
 
@@ -3759,10 +3925,10 @@ function splitFrontmatter(raw) {
3759
3925
  return { fm, body: (match[2] ?? "").trim() };
3760
3926
  }
3761
3927
  function resolveAgentFile(profileDir, name) {
3762
- const local = path14.join(profileDir, "agents", `${name}.md`);
3763
- if (fs15.existsSync(local)) return local;
3764
- const central = path14.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
3765
- if (fs15.existsSync(central)) return central;
3928
+ const local = path15.join(profileDir, "agents", `${name}.md`);
3929
+ if (fs16.existsSync(local)) return local;
3930
+ const central = path15.join(getPluginsCatalogRoot(), "agents", `${name}.md`);
3931
+ if (fs16.existsSync(central)) return central;
3766
3932
  throw new Error(
3767
3933
  `loadSubagents: agent '${name}' not found in ${profileDir}/agents/ or shared catalog`
3768
3934
  );
@@ -3772,7 +3938,7 @@ function loadSubagents(profile) {
3772
3938
  if (!names || names.length === 0) return void 0;
3773
3939
  const agents = {};
3774
3940
  for (const name of names) {
3775
- const { fm, body } = splitFrontmatter(fs15.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
3941
+ const { fm, body } = splitFrontmatter(fs16.readFileSync(resolveAgentFile(profile.dir, name), "utf-8"));
3776
3942
  if (!body) throw new Error(`loadSubagents: agent '${name}' has an empty prompt body`);
3777
3943
  const def = {
3778
3944
  description: fm.description ?? `Subagent ${name}`,
@@ -3868,8 +4034,8 @@ function pushWithRetry(opts = {}) {
3868
4034
  }
3869
4035
 
3870
4036
  // src/commit.ts
3871
- import * as fs16 from "fs";
3872
- import * as path15 from "path";
4037
+ import * as fs17 from "fs";
4038
+ import * as path16 from "path";
3873
4039
  var FORBIDDEN_PATH_PREFIXES = [
3874
4040
  ".kody/",
3875
4041
  ".kody-engine/",
@@ -3925,18 +4091,18 @@ function tryGit(args, cwd) {
3925
4091
  }
3926
4092
  function abortUnfinishedGitOps(cwd) {
3927
4093
  const aborted = [];
3928
- const gitDir = path15.join(cwd ?? process.cwd(), ".git");
3929
- if (!fs16.existsSync(gitDir)) return aborted;
3930
- if (fs16.existsSync(path15.join(gitDir, "MERGE_HEAD"))) {
4094
+ const gitDir = path16.join(cwd ?? process.cwd(), ".git");
4095
+ if (!fs17.existsSync(gitDir)) return aborted;
4096
+ if (fs17.existsSync(path16.join(gitDir, "MERGE_HEAD"))) {
3931
4097
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
3932
4098
  }
3933
- if (fs16.existsSync(path15.join(gitDir, "CHERRY_PICK_HEAD"))) {
4099
+ if (fs17.existsSync(path16.join(gitDir, "CHERRY_PICK_HEAD"))) {
3934
4100
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
3935
4101
  }
3936
- if (fs16.existsSync(path15.join(gitDir, "REVERT_HEAD"))) {
4102
+ if (fs17.existsSync(path16.join(gitDir, "REVERT_HEAD"))) {
3937
4103
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
3938
4104
  }
3939
- if (fs16.existsSync(path15.join(gitDir, "rebase-merge")) || fs16.existsSync(path15.join(gitDir, "rebase-apply"))) {
4105
+ if (fs17.existsSync(path16.join(gitDir, "rebase-merge")) || fs17.existsSync(path16.join(gitDir, "rebase-apply"))) {
3940
4106
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
3941
4107
  }
3942
4108
  try {
@@ -3992,7 +4158,7 @@ function normalizeCommitMessage(raw) {
3992
4158
  function commitAndPush(branch, agentMessage, cwd) {
3993
4159
  const allChanged = listChangedFiles(cwd);
3994
4160
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
3995
- const mergeHeadExists = fs16.existsSync(path15.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
4161
+ const mergeHeadExists = fs17.existsSync(path16.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
3996
4162
  if (allowedFiles.length === 0 && !mergeHeadExists) {
3997
4163
  return { committed: false, pushed: false, sha: "", message: "" };
3998
4164
  }
@@ -4290,22 +4456,22 @@ var advanceFlow = async (ctx, profile) => {
4290
4456
  };
4291
4457
 
4292
4458
  // src/scripts/brainServe.ts
4459
+ init_repoWorkspace();
4293
4460
  import { createServer } from "http";
4294
- import { spawn as spawn3, spawnSync } from "child_process";
4295
- import * as fs18 from "fs";
4296
- import * as path17 from "path";
4461
+ import * as fs19 from "fs";
4462
+ import * as path18 from "path";
4297
4463
 
4298
4464
  // src/scripts/brainTurnLog.ts
4299
- import * as fs17 from "fs";
4300
- import * as path16 from "path";
4465
+ import * as fs18 from "fs";
4466
+ import * as path17 from "path";
4301
4467
  var live = /* @__PURE__ */ new Map();
4302
4468
  function eventsPath(dir, chatId) {
4303
- return path16.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
4469
+ return path17.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
4304
4470
  }
4305
4471
  function lastPersistedSeq(dir, chatId) {
4306
4472
  const p = eventsPath(dir, chatId);
4307
- if (!fs17.existsSync(p)) return 0;
4308
- const lines = fs17.readFileSync(p, "utf-8").split("\n").filter(Boolean);
4473
+ if (!fs18.existsSync(p)) return 0;
4474
+ const lines = fs18.readFileSync(p, "utf-8").split("\n").filter(Boolean);
4309
4475
  if (lines.length === 0) return 0;
4310
4476
  try {
4311
4477
  return JSON.parse(lines[lines.length - 1]).seq || 0;
@@ -4315,9 +4481,9 @@ function lastPersistedSeq(dir, chatId) {
4315
4481
  }
4316
4482
  function readSince(dir, chatId, since) {
4317
4483
  const p = eventsPath(dir, chatId);
4318
- if (!fs17.existsSync(p)) return [];
4484
+ if (!fs18.existsSync(p)) return [];
4319
4485
  const out = [];
4320
- for (const line of fs17.readFileSync(p, "utf-8").split("\n")) {
4486
+ for (const line of fs18.readFileSync(p, "utf-8").split("\n")) {
4321
4487
  if (!line) continue;
4322
4488
  try {
4323
4489
  const rec = JSON.parse(line);
@@ -4343,12 +4509,12 @@ function beginTurn(dir, chatId) {
4343
4509
  };
4344
4510
  live.set(chatId, state);
4345
4511
  const p = eventsPath(dir, chatId);
4346
- fs17.mkdirSync(path16.dirname(p), { recursive: true });
4512
+ fs18.mkdirSync(path17.dirname(p), { recursive: true });
4347
4513
  return (event) => {
4348
4514
  state.seq += 1;
4349
4515
  const rec = { seq: state.seq, turn, ts: Date.now(), event };
4350
4516
  try {
4351
- fs17.appendFileSync(p, JSON.stringify(rec) + "\n");
4517
+ fs18.appendFileSync(p, JSON.stringify(rec) + "\n");
4352
4518
  } catch (err) {
4353
4519
  process.stderr.write(
4354
4520
  `[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
@@ -4386,7 +4552,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
4386
4552
  event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
4387
4553
  };
4388
4554
  try {
4389
- fs17.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
4555
+ fs18.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
4390
4556
  } catch {
4391
4557
  }
4392
4558
  state.status = "ended";
@@ -4474,17 +4640,17 @@ function authOk(req, expected) {
4474
4640
  return false;
4475
4641
  }
4476
4642
  function readJsonBody(req) {
4477
- return new Promise((resolve5, reject) => {
4643
+ return new Promise((resolve6, reject) => {
4478
4644
  const chunks = [];
4479
4645
  req.on("data", (c) => chunks.push(c));
4480
4646
  req.on("end", () => {
4481
4647
  const raw = Buffer.concat(chunks).toString("utf-8");
4482
4648
  if (!raw.trim()) {
4483
- resolve5({});
4649
+ resolve6({});
4484
4650
  return;
4485
4651
  }
4486
4652
  try {
4487
- resolve5(JSON.parse(raw));
4653
+ resolve6(JSON.parse(raw));
4488
4654
  } catch (err) {
4489
4655
  reject(err instanceof Error ? err : new Error(String(err)));
4490
4656
  }
@@ -4596,51 +4762,6 @@ function streamToRes(res, dir, chatId, since) {
4596
4762
  );
4597
4763
  res.on("close", unsubscribe);
4598
4764
  }
4599
- var REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
4600
- var repoClones = /* @__PURE__ */ new Map();
4601
- async function ensureRepoCwd(opts) {
4602
- const repo = opts.repo?.trim();
4603
- if (!repo || !REPO_RE.test(repo)) return opts.baseCwd;
4604
- const root = path17.resolve(opts.reposRoot);
4605
- const dir = path17.resolve(root, repo);
4606
- if (dir !== root && !dir.startsWith(root + path17.sep)) return opts.baseCwd;
4607
- if (fs18.existsSync(path17.join(dir, ".git"))) return dir;
4608
- const inflight = repoClones.get(dir);
4609
- if (inflight) {
4610
- await inflight;
4611
- return dir;
4612
- }
4613
- const p = opts.cloneRepo(repo, opts.repoToken, dir).finally(() => {
4614
- if (repoClones.get(dir) === p) repoClones.delete(dir);
4615
- });
4616
- repoClones.set(dir, p);
4617
- await p;
4618
- return dir;
4619
- }
4620
- var defaultCloneRepo = (repo, token, dir) => {
4621
- fs18.mkdirSync(path17.dirname(dir), { recursive: true });
4622
- const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
4623
- return new Promise((resolve5, reject) => {
4624
- const child = spawn3("git", ["clone", "--depth=1", authUrl, dir], {
4625
- stdio: "inherit"
4626
- });
4627
- child.on("exit", (code) => {
4628
- if (code !== 0) {
4629
- reject(new Error(`git clone ${repo} failed (exit ${code})`));
4630
- return;
4631
- }
4632
- try {
4633
- const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
4634
- const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
4635
- spawnSync("git", ["-C", dir, "config", "user.name", name]);
4636
- spawnSync("git", ["-C", dir, "config", "user.email", email]);
4637
- } catch {
4638
- }
4639
- resolve5();
4640
- });
4641
- child.on("error", reject);
4642
- });
4643
- };
4644
4765
  async function handleChatTurn(req, res, chatId, opts) {
4645
4766
  let body;
4646
4767
  try {
@@ -4657,7 +4778,7 @@ async function handleChatTurn(req, res, chatId, opts) {
4657
4778
  const repo = strField(body, "repo");
4658
4779
  const repoToken = strField(body, "repoToken");
4659
4780
  const sessionFile = sessionFilePath(opts.cwd, chatId);
4660
- fs18.mkdirSync(path17.dirname(sessionFile), { recursive: true });
4781
+ fs19.mkdirSync(path18.dirname(sessionFile), { recursive: true });
4661
4782
  appendTurn(sessionFile, {
4662
4783
  role: "user",
4663
4784
  content: message,
@@ -4681,7 +4802,10 @@ async function handleChatTurn(req, res, chatId, opts) {
4681
4802
  cwd: agentCwd,
4682
4803
  model: opts.model,
4683
4804
  litellmUrl: opts.litellmUrl,
4684
- sink
4805
+ sink,
4806
+ // Let the agent clone + work on OTHER repos via the fetch_repo tool.
4807
+ reposRoot: opts.reposRoot,
4808
+ repoToken
4685
4809
  });
4686
4810
  } catch (err) {
4687
4811
  const errMsg3 = err instanceof Error ? err.message : String(err);
@@ -4701,7 +4825,7 @@ async function handleChatTurn(req, res, chatId, opts) {
4701
4825
  function buildServer(opts) {
4702
4826
  const runTurn = opts.runTurn ?? runChatTurn;
4703
4827
  const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
4704
- const reposRoot = opts.reposRoot ?? path17.join(path17.dirname(path17.resolve(opts.cwd)), "repos");
4828
+ const reposRoot = opts.reposRoot ?? path18.join(path18.dirname(path18.resolve(opts.cwd)), "repos");
4705
4829
  return createServer(async (req, res) => {
4706
4830
  if (!req.method || !req.url) {
4707
4831
  sendJson(res, 400, { error: "bad request" });
@@ -4782,13 +4906,13 @@ var brainServe = async (ctx) => {
4782
4906
  model,
4783
4907
  litellmUrl
4784
4908
  });
4785
- await new Promise((resolve5) => {
4909
+ await new Promise((resolve6) => {
4786
4910
  server.listen(port, "0.0.0.0", () => {
4787
4911
  process.stdout.write(
4788
4912
  `[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
4789
4913
  `
4790
4914
  );
4791
- resolve5();
4915
+ resolve6();
4792
4916
  });
4793
4917
  });
4794
4918
  const shutdown = (signal) => {
@@ -4956,13 +5080,13 @@ function defaultLabelMap() {
4956
5080
  }
4957
5081
 
4958
5082
  // src/scripts/commitAndPush.ts
4959
- import * as fs20 from "fs";
4960
- import * as path19 from "path";
5083
+ import * as fs21 from "fs";
5084
+ import * as path20 from "path";
4961
5085
  init_events();
4962
5086
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
4963
5087
  function sentinelPathForStage(cwd, profileName) {
4964
5088
  const runId = resolveRunId();
4965
- return path19.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
5089
+ return path20.join(cwd, ".kody", "runs", runId, `commit-${profileName}.lock`);
4966
5090
  }
4967
5091
  var commitAndPush2 = async (ctx, profile) => {
4968
5092
  const branch = ctx.data.branch;
@@ -4972,9 +5096,9 @@ var commitAndPush2 = async (ctx, profile) => {
4972
5096
  }
4973
5097
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
4974
5098
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
4975
- if (sentinel && fs20.existsSync(sentinel)) {
5099
+ if (sentinel && fs21.existsSync(sentinel)) {
4976
5100
  try {
4977
- const replay = JSON.parse(fs20.readFileSync(sentinel, "utf-8"));
5101
+ const replay = JSON.parse(fs21.readFileSync(sentinel, "utf-8"));
4978
5102
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
4979
5103
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
4980
5104
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -5027,8 +5151,8 @@ var commitAndPush2 = async (ctx, profile) => {
5027
5151
  const result = ctx.data.commitResult;
5028
5152
  if (sentinel && result?.committed) {
5029
5153
  try {
5030
- fs20.mkdirSync(path19.dirname(sentinel), { recursive: true });
5031
- fs20.writeFileSync(
5154
+ fs21.mkdirSync(path20.dirname(sentinel), { recursive: true });
5155
+ fs21.writeFileSync(
5032
5156
  sentinel,
5033
5157
  JSON.stringify(
5034
5158
  {
@@ -5047,43 +5171,181 @@ var commitAndPush2 = async (ctx, profile) => {
5047
5171
  }
5048
5172
  };
5049
5173
 
5050
- // src/scripts/commitGoalState.ts
5051
- import { execFileSync as execFileSync10 } from "child_process";
5052
- import * as path20 from "path";
5053
- var commitGoalState = async (ctx) => {
5054
- const goal = ctx.data.goal;
5055
- if (!goal) return;
5056
- const stateRel = path20.posix.join(".kody", "goals", goal.id, "state.json");
5174
+ // src/goal/stateStore.ts
5175
+ init_issue();
5176
+
5177
+ // src/stateBranch.ts
5178
+ init_issue();
5179
+ var STATE_BRANCH = "kody-state";
5180
+ function is404(err) {
5181
+ const msg = err instanceof Error ? err.message : String(err);
5182
+ return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5183
+ }
5184
+ function ensureStateBranch(owner, repo, cwd) {
5057
5185
  try {
5058
- execFileSync10("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
5186
+ gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
5187
+ return;
5059
5188
  } catch (err) {
5060
- process.stderr.write(
5061
- `[goal-tick] commitGoalState: git add failed: ${err instanceof Error ? err.message : String(err)}
5062
- `
5189
+ if (!is404(err)) throw err;
5190
+ }
5191
+ const repoInfo = JSON.parse(gh(["api", `/repos/${owner}/${repo}`], { cwd }));
5192
+ const defaultBranch2 = repoInfo.default_branch;
5193
+ if (!defaultBranch2) {
5194
+ throw new Error(`ensureStateBranch: could not resolve default branch for ${owner}/${repo}`);
5195
+ }
5196
+ const headRef = JSON.parse(
5197
+ gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${defaultBranch2}`], { cwd })
5198
+ );
5199
+ const sha = headRef.object?.sha;
5200
+ if (!sha) {
5201
+ throw new Error(`ensureStateBranch: could not resolve head sha for ${owner}/${repo}@${defaultBranch2}`);
5202
+ }
5203
+ try {
5204
+ gh(["api", "--method", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], {
5205
+ cwd,
5206
+ input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
5207
+ });
5208
+ } catch (err) {
5209
+ const msg = err instanceof Error ? err.message : String(err);
5210
+ if (/already exists/i.test(msg) || /HTTP 422/i.test(msg)) return;
5211
+ throw err;
5212
+ }
5213
+ }
5214
+
5215
+ // src/goal/state.ts
5216
+ import * as fs22 from "fs";
5217
+ import * as path21 from "path";
5218
+ var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
5219
+ var GoalStateError = class extends Error {
5220
+ constructor(path39, message) {
5221
+ super(`Invalid goal state at ${path39}:
5222
+ ${message}`);
5223
+ this.path = path39;
5224
+ this.name = "GoalStateError";
5225
+ }
5226
+ path;
5227
+ };
5228
+ function parseGoalState(filePath, raw) {
5229
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5230
+ throw new GoalStateError(filePath, "must be a JSON object");
5231
+ }
5232
+ const r = raw;
5233
+ const stateValue = r.state;
5234
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5235
+ throw new GoalStateError(
5236
+ filePath,
5237
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5063
5238
  );
5064
- return;
5065
5239
  }
5240
+ const parsed = {
5241
+ state: stateValue,
5242
+ extra: {}
5243
+ };
5244
+ if (typeof r.mergeApproved === "boolean") {
5245
+ parsed.mergeApproved = r.mergeApproved;
5246
+ }
5247
+ if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5248
+ parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5249
+ }
5250
+ for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
5251
+ const v = r[ts];
5252
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5253
+ }
5254
+ const known = /* @__PURE__ */ new Set(["state", "mergeApproved", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
5255
+ for (const [k, v] of Object.entries(r)) {
5256
+ if (!known.has(k)) parsed.extra[k] = v;
5257
+ }
5258
+ return parsed;
5259
+ }
5260
+ function serializeGoalState(s) {
5261
+ const obj = { ...s.extra, state: s.state };
5262
+ if (s.mergeApproved !== void 0) obj.mergeApproved = s.mergeApproved;
5263
+ if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5264
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5265
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5266
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5267
+ return `${JSON.stringify(obj, null, 2)}
5268
+ `;
5269
+ }
5270
+ function nowIso() {
5271
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5272
+ }
5273
+
5274
+ // src/goal/stateStore.ts
5275
+ function statePath(goalId) {
5276
+ return `.kody/goals/${goalId}/state.json`;
5277
+ }
5278
+ function is4042(err) {
5279
+ const msg = err instanceof Error ? err.message : String(err);
5280
+ return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
5281
+ }
5282
+ function fetchGoalState(owner, repo, goalId, cwd) {
5283
+ const filePath = statePath(goalId);
5284
+ let raw;
5066
5285
  try {
5067
- execFileSync10("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
5286
+ raw = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5287
+ } catch (err) {
5288
+ if (is4042(err)) return null;
5289
+ throw err;
5290
+ }
5291
+ const o = JSON.parse(raw);
5292
+ if (!o.content) return null;
5293
+ const decoded = Buffer.from(o.content, "base64").toString("utf-8");
5294
+ return parseGoalState(filePath, JSON.parse(decoded));
5295
+ }
5296
+ function putGoalState(owner, repo, goalId, state, message, cwd) {
5297
+ ensureStateBranch(owner, repo, cwd);
5298
+ const filePath = statePath(goalId);
5299
+ const content = Buffer.from(serializeGoalState(state), "utf-8").toString("base64");
5300
+ for (let attempt = 1; attempt <= 3; attempt++) {
5301
+ let sha;
5302
+ try {
5303
+ const cur = gh(["api", `/repos/${owner}/${repo}/contents/${filePath}?ref=${STATE_BRANCH}`], { cwd });
5304
+ const o = JSON.parse(cur);
5305
+ if (o.sha) sha = o.sha;
5306
+ } catch (err) {
5307
+ if (!is4042(err)) throw err;
5308
+ }
5309
+ const payload = { message, content, branch: STATE_BRANCH };
5310
+ if (sha) payload.sha = sha;
5311
+ try {
5312
+ gh(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${filePath}`, "--input", "-"], {
5313
+ cwd,
5314
+ input: JSON.stringify(payload)
5315
+ });
5316
+ return;
5317
+ } catch (err) {
5318
+ const msg = err instanceof Error ? err.message : String(err);
5319
+ const conflict = /HTTP 409/i.test(msg) || /HTTP 422/i.test(msg) || /does not match|but expected/i.test(msg);
5320
+ if (!conflict || attempt === 3) throw err;
5321
+ }
5322
+ }
5323
+ }
5324
+
5325
+ // src/scripts/commitGoalState.ts
5326
+ var commitGoalState = async (ctx) => {
5327
+ const goal = ctx.data.goal;
5328
+ if (!goal) return;
5329
+ if (ctx.data.goalPersistChanged !== true) return;
5330
+ const updated = ctx.data.goalPersistState;
5331
+ if (!updated) return;
5332
+ const owner = ctx.config.github?.owner;
5333
+ const repo = ctx.config.github?.repo;
5334
+ if (!owner || !repo) {
5335
+ process.stderr.write(`[goal-tick] commitGoalState: missing github owner/repo; cannot persist ${goal.id}
5336
+ `);
5068
5337
  return;
5069
- } catch {
5070
5338
  }
5071
- const msg = describeCommitMessage(goal);
5072
5339
  try {
5073
- execFileSync10("git", ["commit", "-m", msg, "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
5340
+ putGoalState(owner, repo, goal.id, updated, describeCommitMessage(goal), ctx.cwd);
5074
5341
  } catch (err) {
5075
5342
  process.stderr.write(
5076
- `[goal-tick] commitGoalState: git commit failed: ${err instanceof Error ? err.message : String(err)}
5077
- `
5078
- );
5079
- return;
5080
- }
5081
- const result = pushWithRetry({ cwd: ctx.cwd });
5082
- if (!result.ok) {
5083
- process.stderr.write(`[goal-tick] commitGoalState: push failed (${result.reason}); will retry next tick
5084
- `);
5343
+ `[goal-tick] commitGoalState: persist to ${STATE_BRANCH_LABEL} failed (${err instanceof Error ? err.message : String(err)}); will retry next tick
5344
+ `
5345
+ );
5085
5346
  }
5086
5347
  };
5348
+ var STATE_BRANCH_LABEL = "kody-state";
5087
5349
  function describeCommitMessage(goal) {
5088
5350
  if (goal.state === "closed") return `chore(goals): abandon ${goal.id} (cleanup complete)`;
5089
5351
  if (goal.state === "awaiting-merge") return `chore(goals): park ${goal.id} awaiting merge`;
@@ -5098,20 +5360,20 @@ function describeCommitMessage(goal) {
5098
5360
  }
5099
5361
 
5100
5362
  // src/scripts/composePrompt.ts
5101
- import * as fs21 from "fs";
5102
- import * as path21 from "path";
5363
+ import * as fs23 from "fs";
5364
+ import * as path22 from "path";
5103
5365
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
5104
5366
  var composePrompt = async (ctx, profile) => {
5105
5367
  const explicit = ctx.data.promptTemplate;
5106
5368
  const mode = ctx.args.mode;
5107
5369
  const candidates = [
5108
- explicit ? path21.join(profile.dir, explicit) : null,
5109
- mode ? path21.join(profile.dir, "prompts", `${mode}.md`) : null,
5110
- path21.join(profile.dir, "prompt.md")
5370
+ explicit ? path22.join(profile.dir, explicit) : null,
5371
+ mode ? path22.join(profile.dir, "prompts", `${mode}.md`) : null,
5372
+ path22.join(profile.dir, "prompt.md")
5111
5373
  ].filter(Boolean);
5112
5374
  let templatePath = "";
5113
5375
  for (const c of candidates) {
5114
- if (fs21.existsSync(c)) {
5376
+ if (fs23.existsSync(c)) {
5115
5377
  templatePath = c;
5116
5378
  break;
5117
5379
  }
@@ -5119,7 +5381,7 @@ var composePrompt = async (ctx, profile) => {
5119
5381
  if (!templatePath) {
5120
5382
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
5121
5383
  }
5122
- const template = fs21.readFileSync(templatePath, "utf-8");
5384
+ const template = fs23.readFileSync(templatePath, "utf-8");
5123
5385
  const tokens = {
5124
5386
  ...stringifyAll(ctx.args, "args."),
5125
5387
  ...stringifyAll(ctx.data, ""),
@@ -5197,9 +5459,6 @@ function formatToolsUsage(profile) {
5197
5459
 
5198
5460
  // src/scripts/createQaGoal.ts
5199
5461
  init_issue();
5200
- import { execFileSync as execFileSync11 } from "child_process";
5201
- import * as fs22 from "fs";
5202
- import * as path22 from "path";
5203
5462
 
5204
5463
  // src/scripts/postReviewResult.ts
5205
5464
  init_issue();
@@ -5451,104 +5710,6 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
5451
5710
  if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
5452
5711
  return { number: Number(m[1]), created: true };
5453
5712
  }
5454
- function writeStateFile(cwd, goalId, lastDispatchedIssue) {
5455
- const dir = path22.join(cwd, ".kody", "goals", goalId);
5456
- fs22.mkdirSync(dir, { recursive: true });
5457
- const state = {
5458
- version: 1,
5459
- state: "active",
5460
- startedAt: (/* @__PURE__ */ new Date()).toISOString(),
5461
- updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5462
- ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
5463
- };
5464
- const filePath = path22.join(dir, "state.json");
5465
- fs22.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
5466
- `);
5467
- return filePath;
5468
- }
5469
- function gitTry(args, cwd) {
5470
- const env = { ...process.env, SKIP_HOOKS: "1", HUSKY: "0" };
5471
- try {
5472
- execFileSync11("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
5473
- return { ok: true, stderr: "" };
5474
- } catch (err) {
5475
- const e = err;
5476
- const stderr = typeof e?.stderr === "string" ? e.stderr : Buffer.isBuffer(e?.stderr) ? e.stderr.toString("utf8") : e?.message ?? "";
5477
- return { ok: false, stderr: stderr.trim() };
5478
- }
5479
- }
5480
- function commitAndPushState(filePath, goalId, cwd) {
5481
- const add = gitTry(["add", filePath], cwd);
5482
- if (!add.ok) {
5483
- process.stderr.write(`[createQaGoal] git add failed: ${add.stderr.slice(-400) || "(no stderr)"}
5484
- `);
5485
- return;
5486
- }
5487
- const diff = gitTry(["diff", "--cached", "--quiet"], cwd);
5488
- if (diff.ok) {
5489
- process.stderr.write(`[createQaGoal] state.json unchanged \u2014 nothing to commit
5490
- `);
5491
- return;
5492
- }
5493
- const commit = gitTry(["commit", "-m", `chore(goals): activate ${goalId}`, "--quiet"], cwd);
5494
- if (!commit.ok) {
5495
- process.stderr.write(`[createQaGoal] git commit failed: ${commit.stderr.slice(-400) || "(no stderr)"}
5496
- `);
5497
- return;
5498
- }
5499
- const push = gitTry(["push", "--quiet"], cwd);
5500
- if (push.ok) return;
5501
- const stderr = push.stderr;
5502
- const tail = stderr.slice(-400) || "(no stderr captured)";
5503
- if (/non-fast-forward|rejected|fetch first|behind/i.test(stderr)) {
5504
- process.stderr.write(`[createQaGoal] push rejected (non-fast-forward) \u2014 pulling --rebase and retrying
5505
- `);
5506
- const rebase = gitTry(["pull", "--rebase", "--autostash", "--quiet"], cwd);
5507
- if (!rebase.ok) {
5508
- process.stderr.write(
5509
- `[createQaGoal] rebase failed (manual recovery required): ${rebase.stderr.slice(-400) || "(no stderr)"}
5510
- `
5511
- );
5512
- return;
5513
- }
5514
- const retryPush = gitTry(["push", "--quiet"], cwd);
5515
- if (retryPush.ok) {
5516
- process.stderr.write(`[createQaGoal] push succeeded after rebase
5517
- `);
5518
- return;
5519
- }
5520
- process.stderr.write(
5521
- `[createQaGoal] push still failed after rebase: ${retryPush.stderr.slice(-400) || "(no stderr)"}
5522
- `
5523
- );
5524
- return;
5525
- }
5526
- if (/pre-push|hook|husky/i.test(stderr)) {
5527
- process.stderr.write(`[createQaGoal] push rejected by pre-push hook \u2014 retrying with --no-verify
5528
- `);
5529
- process.stderr.write(`[createQaGoal] hook output:
5530
- ${tail}
5531
- `);
5532
- const noVerify = gitTry(["push", "--no-verify", "--quiet"], cwd);
5533
- if (noVerify.ok) {
5534
- process.stderr.write(`[createQaGoal] push succeeded with --no-verify (consider adding kody artifacts to ignore configs)
5535
- `);
5536
- return;
5537
- }
5538
- process.stderr.write(
5539
- `[createQaGoal] --no-verify push also failed: ${noVerify.stderr.slice(-400) || "(no stderr)"}
5540
- `
5541
- );
5542
- return;
5543
- }
5544
- process.stderr.write(
5545
- `[createQaGoal] state.json commit landed but push failed.
5546
- [createQaGoal] The goal will not be visible to goal-scheduler in CI until you run 'git push' manually.
5547
- [createQaGoal] git stderr:
5548
- ${tail}
5549
- `
5550
- );
5551
- }
5552
5713
  function createTaskIssue(finding, goalId, manifestNumber, cwd) {
5553
5714
  const labels = [`goal:${goalId}`, severityLabel(finding.severity), FINDING_LABEL];
5554
5715
  ensureLabel(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
@@ -5718,8 +5879,17 @@ ${markdown}`,
5718
5879
  `);
5719
5880
  }
5720
5881
  }
5721
- const stateFile = writeStateFile(ctx.cwd, goalId);
5722
- commitAndPushState(stateFile, goalId, ctx.cwd);
5882
+ const now = nowIso();
5883
+ const goalState = { state: "active", startedAt: now, updatedAt: now, extra: { version: 1 } };
5884
+ try {
5885
+ putGoalState(ctx.config.github.owner, ctx.config.github.repo, goalId, goalState, `chore(goals): activate ${goalId}`, ctx.cwd);
5886
+ } catch (err) {
5887
+ process.stderr.write(
5888
+ `[createQaGoal] failed to persist goal state to kody-state: ${err instanceof Error ? err.message : String(err)}
5889
+ [createQaGoal] goal-scheduler will not see ${goalId} until this succeeds.
5890
+ `
5891
+ );
5892
+ }
5723
5893
  const repoUrl = `https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}`;
5724
5894
  if (manifestIssueNumber !== null) {
5725
5895
  const verb = manifestUpdated ? manifestCreated ? "OPENED" : "UPDATED" : "TARGETED";
@@ -5988,8 +6158,8 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
5988
6158
  }
5989
6159
 
5990
6160
  // src/scripts/diagMcp.ts
5991
- import { execFileSync as execFileSync12 } from "child_process";
5992
- import * as fs23 from "fs";
6161
+ import { execFileSync as execFileSync10 } from "child_process";
6162
+ import * as fs24 from "fs";
5993
6163
  import * as os4 from "os";
5994
6164
  import * as path23 from "path";
5995
6165
  var diagMcp = async (_ctx) => {
@@ -5997,7 +6167,7 @@ var diagMcp = async (_ctx) => {
5997
6167
  const cacheDir = path23.join(home, ".cache", "ms-playwright");
5998
6168
  let entries = [];
5999
6169
  try {
6000
- entries = fs23.readdirSync(cacheDir);
6170
+ entries = fs24.readdirSync(cacheDir);
6001
6171
  } catch {
6002
6172
  }
6003
6173
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -6008,7 +6178,7 @@ var diagMcp = async (_ctx) => {
6008
6178
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
6009
6179
  `);
6010
6180
  try {
6011
- const v = execFileSync12("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
6181
+ const v = execFileSync10("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
6012
6182
  stdio: "pipe",
6013
6183
  timeout: 6e4,
6014
6184
  encoding: "utf8"
@@ -6023,17 +6193,17 @@ var diagMcp = async (_ctx) => {
6023
6193
  };
6024
6194
 
6025
6195
  // src/scripts/discoverQaContext.ts
6026
- import * as fs25 from "fs";
6196
+ import * as fs26 from "fs";
6027
6197
  import * as path25 from "path";
6028
6198
 
6029
6199
  // src/scripts/frameworkDetectors.ts
6030
- import * as fs24 from "fs";
6200
+ import * as fs25 from "fs";
6031
6201
  import * as path24 from "path";
6032
6202
  function detectFrameworks(cwd) {
6033
6203
  const out = [];
6034
6204
  let deps = {};
6035
6205
  try {
6036
- const pkg = JSON.parse(fs24.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6206
+ const pkg = JSON.parse(fs25.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6037
6207
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
6038
6208
  } catch {
6039
6209
  return out;
@@ -6070,7 +6240,7 @@ function detectFrameworks(cwd) {
6070
6240
  }
6071
6241
  function findFile(cwd, candidates) {
6072
6242
  for (const c of candidates) {
6073
- if (fs24.existsSync(path24.join(cwd, c))) return c;
6243
+ if (fs25.existsSync(path24.join(cwd, c))) return c;
6074
6244
  }
6075
6245
  return null;
6076
6246
  }
@@ -6084,17 +6254,17 @@ function discoverPayloadCollections(cwd) {
6084
6254
  const out = [];
6085
6255
  for (const dir of COLLECTION_DIRS) {
6086
6256
  const full = path24.join(cwd, dir);
6087
- if (!fs24.existsSync(full)) continue;
6257
+ if (!fs25.existsSync(full)) continue;
6088
6258
  let files;
6089
6259
  try {
6090
- files = fs24.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6260
+ files = fs25.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6091
6261
  } catch {
6092
6262
  continue;
6093
6263
  }
6094
6264
  for (const file of files) {
6095
6265
  try {
6096
6266
  const filePath = path24.join(full, file);
6097
- const content = fs24.readFileSync(filePath, "utf-8").slice(0, 1e4);
6267
+ const content = fs25.readFileSync(filePath, "utf-8").slice(0, 1e4);
6098
6268
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
6099
6269
  if (!slugMatch) continue;
6100
6270
  const slug = slugMatch[1];
@@ -6123,10 +6293,10 @@ function discoverAdminComponents(cwd, collections) {
6123
6293
  const out = [];
6124
6294
  for (const dir of ADMIN_COMPONENT_DIRS) {
6125
6295
  const full = path24.join(cwd, dir);
6126
- if (!fs24.existsSync(full)) continue;
6296
+ if (!fs25.existsSync(full)) continue;
6127
6297
  let entries;
6128
6298
  try {
6129
- entries = fs24.readdirSync(full, { withFileTypes: true });
6299
+ entries = fs25.readdirSync(full, { withFileTypes: true });
6130
6300
  } catch {
6131
6301
  continue;
6132
6302
  }
@@ -6136,7 +6306,7 @@ function discoverAdminComponents(cwd, collections) {
6136
6306
  let filePath;
6137
6307
  if (entry.isDirectory()) {
6138
6308
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
6139
- (f) => fs24.existsSync(path24.join(entryPath, f))
6309
+ (f) => fs25.existsSync(path24.join(entryPath, f))
6140
6310
  );
6141
6311
  if (!indexFile) continue;
6142
6312
  name = entry.name;
@@ -6151,7 +6321,7 @@ function discoverAdminComponents(cwd, collections) {
6151
6321
  if (collections) {
6152
6322
  for (const col of collections) {
6153
6323
  try {
6154
- const colContent = fs24.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6324
+ const colContent = fs25.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6155
6325
  if (colContent.includes(name)) {
6156
6326
  usedInCollection = col.slug;
6157
6327
  break;
@@ -6171,7 +6341,7 @@ function scanApiRoutes(cwd) {
6171
6341
  const appDirs = ["src/app", "app"];
6172
6342
  for (const appDir of appDirs) {
6173
6343
  const apiDir = path24.join(cwd, appDir, "api");
6174
- if (!fs24.existsSync(apiDir)) continue;
6344
+ if (!fs25.existsSync(apiDir)) continue;
6175
6345
  walkApiRoutes(apiDir, "/api", cwd, out);
6176
6346
  break;
6177
6347
  }
@@ -6180,14 +6350,14 @@ function scanApiRoutes(cwd) {
6180
6350
  function walkApiRoutes(dir, prefix, cwd, out) {
6181
6351
  let entries;
6182
6352
  try {
6183
- entries = fs24.readdirSync(dir, { withFileTypes: true });
6353
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
6184
6354
  } catch {
6185
6355
  return;
6186
6356
  }
6187
6357
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
6188
6358
  if (routeFile) {
6189
6359
  try {
6190
- const content = fs24.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6360
+ const content = fs25.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6191
6361
  const methods = HTTP_METHODS.filter(
6192
6362
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
6193
6363
  );
@@ -6235,9 +6405,9 @@ function scanEnvVars(cwd) {
6235
6405
  const candidates = [".env.example", ".env.local.example", ".env.template"];
6236
6406
  for (const envFile of candidates) {
6237
6407
  const envPath = path24.join(cwd, envFile);
6238
- if (!fs24.existsSync(envPath)) continue;
6408
+ if (!fs25.existsSync(envPath)) continue;
6239
6409
  try {
6240
- const content = fs24.readFileSync(envPath, "utf-8");
6410
+ const content = fs25.readFileSync(envPath, "utf-8");
6241
6411
  const vars = [];
6242
6412
  for (const line of content.split("\n")) {
6243
6413
  const trimmed = line.trim();
@@ -6285,9 +6455,9 @@ function runQaDiscovery(cwd) {
6285
6455
  }
6286
6456
  function detectDevServer(cwd, out) {
6287
6457
  try {
6288
- const pkg = JSON.parse(fs25.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
6458
+ const pkg = JSON.parse(fs26.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
6289
6459
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6290
- const pm = fs25.existsSync(path25.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs25.existsSync(path25.join(cwd, "yarn.lock")) ? "yarn" : fs25.existsSync(path25.join(cwd, "bun.lockb")) ? "bun" : "npm";
6460
+ const pm = fs26.existsSync(path25.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs26.existsSync(path25.join(cwd, "yarn.lock")) ? "yarn" : fs26.existsSync(path25.join(cwd, "bun.lockb")) ? "bun" : "npm";
6291
6461
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
6292
6462
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
6293
6463
  else if (allDeps.vite) out.devPort = 5173;
@@ -6298,7 +6468,7 @@ function scanFrontendRoutes(cwd, out) {
6298
6468
  const appDirs = ["src/app", "app"];
6299
6469
  for (const appDir of appDirs) {
6300
6470
  const full = path25.join(cwd, appDir);
6301
- if (!fs25.existsSync(full)) continue;
6471
+ if (!fs26.existsSync(full)) continue;
6302
6472
  walkFrontendRoutes(full, "", out);
6303
6473
  break;
6304
6474
  }
@@ -6306,7 +6476,7 @@ function scanFrontendRoutes(cwd, out) {
6306
6476
  function walkFrontendRoutes(dir, prefix, out) {
6307
6477
  let entries;
6308
6478
  try {
6309
- entries = fs25.readdirSync(dir, { withFileTypes: true });
6479
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
6310
6480
  } catch {
6311
6481
  return;
6312
6482
  }
@@ -6348,23 +6518,23 @@ function detectAuthFiles(cwd, out) {
6348
6518
  "src/app/api/oauth"
6349
6519
  ];
6350
6520
  for (const c of candidates) {
6351
- if (fs25.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
6521
+ if (fs26.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
6352
6522
  }
6353
6523
  }
6354
6524
  function detectRoles(cwd, out) {
6355
6525
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
6356
6526
  for (const rp of rolePaths) {
6357
6527
  const dir = path25.join(cwd, rp);
6358
- if (!fs25.existsSync(dir)) continue;
6528
+ if (!fs26.existsSync(dir)) continue;
6359
6529
  let files;
6360
6530
  try {
6361
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6531
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6362
6532
  } catch {
6363
6533
  continue;
6364
6534
  }
6365
6535
  for (const f of files) {
6366
6536
  try {
6367
- const content = fs25.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
6537
+ const content = fs26.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
6368
6538
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
6369
6539
  if (roleMatches) {
6370
6540
  for (const m of roleMatches) {
@@ -6448,7 +6618,7 @@ var discoverQaContext = async (ctx) => {
6448
6618
  };
6449
6619
 
6450
6620
  // src/scripts/dispatch.ts
6451
- import { execFileSync as execFileSync13 } from "child_process";
6621
+ import { execFileSync as execFileSync11 } from "child_process";
6452
6622
  var API_TIMEOUT_MS4 = 3e4;
6453
6623
  var dispatch = async (ctx, _profile, _agentResult, args) => {
6454
6624
  const next = args?.next;
@@ -6484,7 +6654,7 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
6484
6654
  const sub = usePr ? "pr" : "issue";
6485
6655
  const body = `@kody ${next}`;
6486
6656
  try {
6487
- execFileSync13("gh", [sub, "comment", String(targetNumber), "--body", body], {
6657
+ execFileSync11("gh", [sub, "comment", String(targetNumber), "--body", body], {
6488
6658
  timeout: API_TIMEOUT_MS4,
6489
6659
  cwd: ctx.cwd,
6490
6660
  stdio: ["ignore", "pipe", "pipe"]
@@ -6504,7 +6674,7 @@ function parsePr(url) {
6504
6674
  }
6505
6675
 
6506
6676
  // src/scripts/dispatchClassified.ts
6507
- import { execFileSync as execFileSync14 } from "child_process";
6677
+ import { execFileSync as execFileSync12 } from "child_process";
6508
6678
  var API_TIMEOUT_MS5 = 3e4;
6509
6679
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6510
6680
  var dispatchClassified = async (ctx) => {
@@ -6528,7 +6698,7 @@ ${auditLine}
6528
6698
 
6529
6699
  ${stateBody}`;
6530
6700
  try {
6531
- execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6701
+ execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6532
6702
  cwd: ctx.cwd,
6533
6703
  timeout: API_TIMEOUT_MS5,
6534
6704
  stdio: ["ignore", "pipe", "pipe"]
@@ -6548,7 +6718,7 @@ function failedAction3(reason) {
6548
6718
  }
6549
6719
 
6550
6720
  // src/scripts/dispatchJobFileTicks.ts
6551
- import * as fs27 from "fs";
6721
+ import * as fs28 from "fs";
6552
6722
  import * as path27 from "path";
6553
6723
 
6554
6724
  // src/scripts/jobFrontmatter.ts
@@ -6642,44 +6812,6 @@ function stripQuotes(value) {
6642
6812
  // src/scripts/jobState/contentsApiBackend.ts
6643
6813
  init_issue();
6644
6814
 
6645
- // src/stateBranch.ts
6646
- init_issue();
6647
- var STATE_BRANCH = "kody-state";
6648
- function is404(err) {
6649
- const msg = err instanceof Error ? err.message : String(err);
6650
- return /HTTP 404/i.test(msg) || /Not Found/i.test(msg);
6651
- }
6652
- function ensureStateBranch(owner, repo, cwd) {
6653
- try {
6654
- gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
6655
- return;
6656
- } catch (err) {
6657
- if (!is404(err)) throw err;
6658
- }
6659
- const repoInfo = JSON.parse(gh(["api", `/repos/${owner}/${repo}`], { cwd }));
6660
- const defaultBranch2 = repoInfo.default_branch;
6661
- if (!defaultBranch2) {
6662
- throw new Error(`ensureStateBranch: could not resolve default branch for ${owner}/${repo}`);
6663
- }
6664
- const headRef = JSON.parse(
6665
- gh(["api", `/repos/${owner}/${repo}/git/ref/heads/${defaultBranch2}`], { cwd })
6666
- );
6667
- const sha = headRef.object?.sha;
6668
- if (!sha) {
6669
- throw new Error(`ensureStateBranch: could not resolve head sha for ${owner}/${repo}@${defaultBranch2}`);
6670
- }
6671
- try {
6672
- gh(["api", "--method", "POST", `/repos/${owner}/${repo}/git/refs`, "--input", "-"], {
6673
- cwd,
6674
- input: JSON.stringify({ ref: `refs/heads/${STATE_BRANCH}`, sha })
6675
- });
6676
- } catch (err) {
6677
- const msg = err instanceof Error ? err.message : String(err);
6678
- if (/already exists/i.test(msg) || /HTTP 422/i.test(msg)) return;
6679
- throw err;
6680
- }
6681
- }
6682
-
6683
6815
  // src/scripts/issueStateComment.ts
6684
6816
  init_issue();
6685
6817
  function isStateEnvelope(x) {
@@ -6856,7 +6988,7 @@ var ContentsApiBackend = class {
6856
6988
  };
6857
6989
 
6858
6990
  // src/scripts/jobState/localFileBackend.ts
6859
- import * as fs26 from "fs";
6991
+ import * as fs27 from "fs";
6860
6992
  import * as path26 from "path";
6861
6993
  var LocalFileBackend = class {
6862
6994
  name = "local-file";
@@ -6887,7 +7019,7 @@ var LocalFileBackend = class {
6887
7019
  `);
6888
7020
  return;
6889
7021
  }
6890
- fs26.mkdirSync(this.absDir, { recursive: true });
7022
+ fs27.mkdirSync(this.absDir, { recursive: true });
6891
7023
  const prefix = this.cacheKeyPrefix();
6892
7024
  const probeKey = `${prefix}probe-${Date.now()}`;
6893
7025
  try {
@@ -6916,7 +7048,7 @@ var LocalFileBackend = class {
6916
7048
  `);
6917
7049
  return;
6918
7050
  }
6919
- if (!fs26.existsSync(this.absDir)) {
7051
+ if (!fs27.existsSync(this.absDir)) {
6920
7052
  return;
6921
7053
  }
6922
7054
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -6933,10 +7065,10 @@ var LocalFileBackend = class {
6933
7065
  load(slug) {
6934
7066
  const relPath = stateFilePath(this.jobsDir, slug);
6935
7067
  const absPath = path26.join(this.cwd, relPath);
6936
- if (!fs26.existsSync(absPath)) {
7068
+ if (!fs27.existsSync(absPath)) {
6937
7069
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
6938
7070
  }
6939
- const raw = fs26.readFileSync(absPath, "utf-8");
7071
+ const raw = fs27.readFileSync(absPath, "utf-8");
6940
7072
  let parsed;
6941
7073
  try {
6942
7074
  parsed = JSON.parse(raw);
@@ -6954,9 +7086,9 @@ var LocalFileBackend = class {
6954
7086
  return false;
6955
7087
  }
6956
7088
  const absPath = path26.join(this.cwd, loaded.path);
6957
- fs26.mkdirSync(path26.dirname(absPath), { recursive: true });
7089
+ fs27.mkdirSync(path26.dirname(absPath), { recursive: true });
6958
7090
  const body = JSON.stringify(next, null, 2) + "\n";
6959
- fs26.writeFileSync(absPath, body, "utf-8");
7091
+ fs27.writeFileSync(absPath, body, "utf-8");
6960
7092
  return true;
6961
7093
  }
6962
7094
  cacheKeyPrefix() {
@@ -7147,17 +7279,17 @@ function formatAgo(ms) {
7147
7279
  }
7148
7280
  function readJobFrontmatter(cwd, jobsDir, slug) {
7149
7281
  try {
7150
- const raw = fs27.readFileSync(path27.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7282
+ const raw = fs28.readFileSync(path27.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7151
7283
  return splitFrontmatter2(raw).frontmatter;
7152
7284
  } catch {
7153
7285
  return {};
7154
7286
  }
7155
7287
  }
7156
7288
  function listJobSlugs(absDir) {
7157
- if (!fs27.existsSync(absDir)) return [];
7289
+ if (!fs28.existsSync(absDir)) return [];
7158
7290
  let entries;
7159
7291
  try {
7160
- entries = fs27.readdirSync(absDir, { withFileTypes: true });
7292
+ entries = fs28.readdirSync(absDir, { withFileTypes: true });
7161
7293
  } catch {
7162
7294
  return [];
7163
7295
  }
@@ -7684,7 +7816,7 @@ var finalizeTerminal = async (ctx) => {
7684
7816
 
7685
7817
  // src/scripts/finishFlow.ts
7686
7818
  init_issue();
7687
- import { execFileSync as execFileSync15 } from "child_process";
7819
+ import { execFileSync as execFileSync13 } from "child_process";
7688
7820
  var TERMINAL_PHASE = {
7689
7821
  "review-passed": { phase: "shipped", status: "succeeded" },
7690
7822
  "fix-applied": { phase: "shipped", status: "succeeded" },
@@ -7724,7 +7856,7 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
7724
7856
  **PR:** ${state.core.prUrl}` : "";
7725
7857
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
7726
7858
  try {
7727
- execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7859
+ execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7728
7860
  timeout: API_TIMEOUT_MS6,
7729
7861
  cwd: ctx.cwd,
7730
7862
  stdio: ["ignore", "pipe", "pipe"]
@@ -7754,9 +7886,9 @@ var finishFlow = async (ctx, profile, _agentResult, args) => {
7754
7886
  };
7755
7887
 
7756
7888
  // src/branch.ts
7757
- import { execFileSync as execFileSync16 } from "child_process";
7889
+ import { execFileSync as execFileSync14 } from "child_process";
7758
7890
  function git2(args, cwd) {
7759
- return execFileSync16("git", args, {
7891
+ return execFileSync14("git", args, {
7760
7892
  encoding: "utf-8",
7761
7893
  timeout: 3e4,
7762
7894
  cwd,
@@ -7773,11 +7905,11 @@ function getCurrentBranch(cwd) {
7773
7905
  }
7774
7906
  function resetWorkingTree(cwd) {
7775
7907
  try {
7776
- execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7908
+ execFileSync14("git", ["reset", "--hard", "HEAD"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7777
7909
  } catch {
7778
7910
  }
7779
7911
  try {
7780
- execFileSync16("git", ["clean", "-fd"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7912
+ execFileSync14("git", ["clean", "-fd"], { cwd, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7781
7913
  } catch {
7782
7914
  }
7783
7915
  }
@@ -7789,14 +7921,14 @@ function checkoutPrBranch(prNumber, cwd) {
7789
7921
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
7790
7922
  };
7791
7923
  try {
7792
- execFileSync16("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7924
+ execFileSync14("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7793
7925
  } catch {
7794
7926
  }
7795
7927
  try {
7796
- execFileSync16("git", ["clean", "-fd"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7928
+ execFileSync14("git", ["clean", "-fd"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
7797
7929
  } catch {
7798
7930
  }
7799
- execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
7931
+ execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
7800
7932
  cwd,
7801
7933
  env,
7802
7934
  stdio: ["ignore", "pipe", "pipe"],
@@ -7922,8 +8054,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
7922
8054
  }
7923
8055
 
7924
8056
  // src/gha.ts
7925
- import { execFileSync as execFileSync17 } from "child_process";
7926
- import * as fs28 from "fs";
8057
+ import { execFileSync as execFileSync15 } from "child_process";
8058
+ import * as fs29 from "fs";
7927
8059
  function getRunUrl() {
7928
8060
  const server = process.env.GITHUB_SERVER_URL;
7929
8061
  const repo = process.env.GITHUB_REPOSITORY;
@@ -7934,10 +8066,10 @@ function getRunUrl() {
7934
8066
  function reactToTriggerComment(cwd) {
7935
8067
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
7936
8068
  const eventPath = process.env.GITHUB_EVENT_PATH;
7937
- if (!eventPath || !fs28.existsSync(eventPath)) return;
8069
+ if (!eventPath || !fs29.existsSync(eventPath)) return;
7938
8070
  let event = null;
7939
8071
  try {
7940
- event = JSON.parse(fs28.readFileSync(eventPath, "utf-8"));
8072
+ event = JSON.parse(fs29.readFileSync(eventPath, "utf-8"));
7941
8073
  } catch {
7942
8074
  return;
7943
8075
  }
@@ -7965,7 +8097,7 @@ function reactToTriggerComment(cwd) {
7965
8097
  for (let attempt = 0; attempt < 3; attempt++) {
7966
8098
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
7967
8099
  try {
7968
- execFileSync17("gh", args, opts);
8100
+ execFileSync15("gh", args, opts);
7969
8101
  return;
7970
8102
  } catch (err) {
7971
8103
  lastErr = err;
@@ -7978,7 +8110,7 @@ function reactToTriggerComment(cwd) {
7978
8110
  }
7979
8111
  function sleepMs(ms) {
7980
8112
  try {
7981
- execFileSync17("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
8113
+ execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
7982
8114
  } catch {
7983
8115
  }
7984
8116
  }
@@ -7987,7 +8119,7 @@ function sleepMs(ms) {
7987
8119
  init_issue();
7988
8120
 
7989
8121
  // src/workflow.ts
7990
- import { execFileSync as execFileSync18 } from "child_process";
8122
+ import { execFileSync as execFileSync16 } from "child_process";
7991
8123
  var GH_TIMEOUT_MS = 3e4;
7992
8124
  function ghToken3() {
7993
8125
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -7995,7 +8127,7 @@ function ghToken3() {
7995
8127
  function gh3(args, cwd) {
7996
8128
  const token = ghToken3();
7997
8129
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
7998
- return execFileSync18("gh", args, {
8130
+ return execFileSync16("gh", args, {
7999
8131
  encoding: "utf-8",
8000
8132
  timeout: GH_TIMEOUT_MS,
8001
8133
  cwd,
@@ -8230,13 +8362,13 @@ var handleAbandonedGoal = async (ctx) => {
8230
8362
  };
8231
8363
 
8232
8364
  // src/scripts/initFlow.ts
8233
- import { execFileSync as execFileSync19 } from "child_process";
8234
- import * as fs29 from "fs";
8365
+ import { execFileSync as execFileSync17 } from "child_process";
8366
+ import * as fs30 from "fs";
8235
8367
  import * as path28 from "path";
8236
8368
  function detectPackageManager(cwd) {
8237
- if (fs29.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8238
- if (fs29.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
8239
- if (fs29.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
8369
+ if (fs30.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8370
+ if (fs30.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
8371
+ if (fs30.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
8240
8372
  return "npm";
8241
8373
  }
8242
8374
  function qualityCommandsFor(pm) {
@@ -8256,7 +8388,7 @@ function schemaUrlFromPkg() {
8256
8388
  function detectOwnerRepo(cwd) {
8257
8389
  let url;
8258
8390
  try {
8259
- url = execFileSync19("git", ["remote", "get-url", "origin"], {
8391
+ url = execFileSync17("git", ["remote", "get-url", "origin"], {
8260
8392
  cwd,
8261
8393
  encoding: "utf-8",
8262
8394
  stdio: ["ignore", "pipe", "pipe"]
@@ -8341,7 +8473,7 @@ jobs:
8341
8473
  `;
8342
8474
  function defaultBranchFromGit(cwd) {
8343
8475
  try {
8344
- const ref = execFileSync19("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
8476
+ const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
8345
8477
  cwd,
8346
8478
  encoding: "utf-8",
8347
8479
  stdio: ["ignore", "pipe", "pipe"]
@@ -8349,7 +8481,7 @@ function defaultBranchFromGit(cwd) {
8349
8481
  return ref.replace("refs/remotes/origin/", "");
8350
8482
  } catch {
8351
8483
  try {
8352
- return execFileSync19("git", ["branch", "--show-current"], {
8484
+ return execFileSync17("git", ["branch", "--show-current"], {
8353
8485
  cwd,
8354
8486
  encoding: "utf-8",
8355
8487
  stdio: ["ignore", "pipe", "pipe"]
@@ -8366,35 +8498,35 @@ function performInit(cwd, force) {
8366
8498
  const ownerRepo = detectOwnerRepo(cwd);
8367
8499
  const defaultBranch2 = defaultBranchFromGit(cwd);
8368
8500
  const configPath = path28.join(cwd, "kody.config.json");
8369
- if (fs29.existsSync(configPath) && !force) {
8501
+ if (fs30.existsSync(configPath) && !force) {
8370
8502
  skipped.push("kody.config.json");
8371
8503
  } else {
8372
8504
  const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
8373
- fs29.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
8505
+ fs30.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
8374
8506
  `);
8375
8507
  wrote.push("kody.config.json");
8376
8508
  }
8377
8509
  const workflowDir = path28.join(cwd, ".github", "workflows");
8378
8510
  const workflowPath = path28.join(workflowDir, "kody.yml");
8379
- if (fs29.existsSync(workflowPath) && !force) {
8511
+ if (fs30.existsSync(workflowPath) && !force) {
8380
8512
  skipped.push(".github/workflows/kody.yml");
8381
8513
  } else {
8382
- fs29.mkdirSync(workflowDir, { recursive: true });
8383
- fs29.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
8514
+ fs30.mkdirSync(workflowDir, { recursive: true });
8515
+ fs30.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
8384
8516
  wrote.push(".github/workflows/kody.yml");
8385
8517
  }
8386
8518
  const builtinJobs = listBuiltinJobs();
8387
8519
  if (builtinJobs.length > 0) {
8388
8520
  const jobsDir = path28.join(cwd, ".kody", "duties");
8389
- fs29.mkdirSync(jobsDir, { recursive: true });
8521
+ fs30.mkdirSync(jobsDir, { recursive: true });
8390
8522
  for (const job of builtinJobs) {
8391
8523
  const rel = path28.join(".kody", "duties", `${job.slug}.md`);
8392
8524
  const target = path28.join(cwd, rel);
8393
- if (fs29.existsSync(target) && !force) {
8525
+ if (fs30.existsSync(target) && !force) {
8394
8526
  skipped.push(rel);
8395
8527
  continue;
8396
8528
  }
8397
- fs29.writeFileSync(target, fs29.readFileSync(job.filePath, "utf-8"));
8529
+ fs30.writeFileSync(target, fs30.readFileSync(job.filePath, "utf-8"));
8398
8530
  wrote.push(rel);
8399
8531
  }
8400
8532
  }
@@ -8407,11 +8539,11 @@ function performInit(cwd, force) {
8407
8539
  }
8408
8540
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
8409
8541
  const target = path28.join(workflowDir, `kody-${exe.name}.yml`);
8410
- if (fs29.existsSync(target) && !force) {
8542
+ if (fs30.existsSync(target) && !force) {
8411
8543
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
8412
8544
  continue;
8413
8545
  }
8414
- fs29.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
8546
+ fs30.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
8415
8547
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
8416
8548
  }
8417
8549
  let labels;
@@ -8488,86 +8620,6 @@ Nothing to do. All files already present. (Use --force to overwrite.)
8488
8620
  init_loadConventions();
8489
8621
  init_loadCoverageRules();
8490
8622
 
8491
- // src/goal/state.ts
8492
- import * as fs30 from "fs";
8493
- import * as path29 from "path";
8494
- var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
8495
- var GoalStateError = class extends Error {
8496
- constructor(path40, message) {
8497
- super(`Invalid goal state at ${path40}:
8498
- ${message}`);
8499
- this.path = path40;
8500
- this.name = "GoalStateError";
8501
- }
8502
- path;
8503
- };
8504
- function parseGoalState(filePath, raw) {
8505
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
8506
- throw new GoalStateError(filePath, "must be a JSON object");
8507
- }
8508
- const r = raw;
8509
- const stateValue = r.state;
8510
- if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
8511
- throw new GoalStateError(
8512
- filePath,
8513
- `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
8514
- );
8515
- }
8516
- const parsed = {
8517
- state: stateValue,
8518
- extra: {}
8519
- };
8520
- if (typeof r.mergeApproved === "boolean") {
8521
- parsed.mergeApproved = r.mergeApproved;
8522
- }
8523
- if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
8524
- parsed.lastDispatchedIssue = r.lastDispatchedIssue;
8525
- }
8526
- for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
8527
- const v = r[ts];
8528
- if (typeof v === "string" && v.length > 0) parsed[ts] = v;
8529
- }
8530
- const known = /* @__PURE__ */ new Set(["state", "mergeApproved", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
8531
- for (const [k, v] of Object.entries(r)) {
8532
- if (!known.has(k)) parsed.extra[k] = v;
8533
- }
8534
- return parsed;
8535
- }
8536
- function serializeGoalState(s) {
8537
- const obj = { ...s.extra, state: s.state };
8538
- if (s.mergeApproved !== void 0) obj.mergeApproved = s.mergeApproved;
8539
- if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
8540
- if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
8541
- if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
8542
- if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
8543
- return `${JSON.stringify(obj, null, 2)}
8544
- `;
8545
- }
8546
- function goalStatePath(cwd, goalId) {
8547
- return path29.join(cwd, ".kody", "goals", goalId, "state.json");
8548
- }
8549
- function readGoalState(cwd, goalId) {
8550
- const file = goalStatePath(cwd, goalId);
8551
- if (!fs30.existsSync(file)) {
8552
- throw new GoalStateError(file, "file not found");
8553
- }
8554
- let raw;
8555
- try {
8556
- raw = JSON.parse(fs30.readFileSync(file, "utf-8"));
8557
- } catch (err) {
8558
- throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
8559
- }
8560
- return parseGoalState(file, raw);
8561
- }
8562
- function writeGoalState(cwd, goalId, state) {
8563
- const file = goalStatePath(cwd, goalId);
8564
- fs30.mkdirSync(path29.dirname(file), { recursive: true });
8565
- fs30.writeFileSync(file, serializeGoalState(state), "utf-8");
8566
- }
8567
- function nowIso() {
8568
- return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
8569
- }
8570
-
8571
8623
  // src/scripts/loadGoalState.ts
8572
8624
  var loadGoalState = async (ctx) => {
8573
8625
  const goalId = ctx.args.goal;
@@ -8583,8 +8635,24 @@ var loadGoalState = async (ctx) => {
8583
8635
  ctx.output.reason = "invalid goal id (no slashes or '..' allowed)";
8584
8636
  return;
8585
8637
  }
8638
+ const owner = ctx.config.github?.owner;
8639
+ const repo = ctx.config.github?.repo;
8640
+ if (!owner || !repo) {
8641
+ ctx.skipAgent = true;
8642
+ ctx.output.exitCode = 1;
8643
+ ctx.output.reason = "missing github owner/repo in config";
8644
+ return;
8645
+ }
8586
8646
  try {
8587
- const state = readGoalState(ctx.cwd, goalId);
8647
+ const state = fetchGoalState(owner, repo, goalId, ctx.cwd);
8648
+ if (!state) {
8649
+ process.stdout.write(`[goal-tick] no goal state for ${goalId} on ${owner}/${repo} \u2014 nothing to tick
8650
+ `);
8651
+ ctx.skipAgent = true;
8652
+ ctx.output.exitCode = 0;
8653
+ ctx.output.reason = "no goal state to tick";
8654
+ return;
8655
+ }
8588
8656
  ctx.data.goal = {
8589
8657
  id: goalId,
8590
8658
  state: state.state,
@@ -8660,7 +8728,7 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
8660
8728
 
8661
8729
  // src/scripts/loadJobFromFile.ts
8662
8730
  import * as fs31 from "fs";
8663
- import * as path30 from "path";
8731
+ import * as path29 from "path";
8664
8732
  var loadJobFromFile = async (ctx, _profile, args) => {
8665
8733
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
8666
8734
  const workersDir = String(args?.workersDir ?? ".kody/staff");
@@ -8669,7 +8737,7 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8669
8737
  if (!slug) {
8670
8738
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
8671
8739
  }
8672
- const absPath = path30.join(ctx.cwd, jobsDir, `${slug}.md`);
8740
+ const absPath = path29.join(ctx.cwd, jobsDir, `${slug}.md`);
8673
8741
  if (!fs31.existsSync(absPath)) {
8674
8742
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
8675
8743
  }
@@ -8681,7 +8749,7 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8681
8749
  let workerTitle = "";
8682
8750
  let workerPersona = "";
8683
8751
  if (workerSlug) {
8684
- const workerPath = path30.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8752
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8685
8753
  if (!fs31.existsSync(workerPath)) {
8686
8754
  throw new Error(
8687
8755
  `loadJobFromFile: duty '${slug}' declares staff '${workerSlug}' but ${workerPath} does not exist`
@@ -8731,14 +8799,14 @@ init_loadPriorArt();
8731
8799
 
8732
8800
  // src/scripts/loadQaContext.ts
8733
8801
  import * as fs34 from "fs";
8734
- import * as path33 from "path";
8802
+ import * as path32 from "path";
8735
8803
 
8736
8804
  // src/scripts/kodyVariables.ts
8737
8805
  import * as fs33 from "fs";
8738
- import * as path32 from "path";
8806
+ import * as path31 from "path";
8739
8807
  var KODY_VARIABLES_REL_PATH = ".kody/variables.json";
8740
8808
  function readKodyVariables(cwd) {
8741
- const full = path32.join(cwd, KODY_VARIABLES_REL_PATH);
8809
+ const full = path31.join(cwd, KODY_VARIABLES_REL_PATH);
8742
8810
  let raw;
8743
8811
  try {
8744
8812
  raw = fs33.readFileSync(full, "utf-8");
@@ -8790,7 +8858,7 @@ function readProfileStaff(raw) {
8790
8858
  return { staff: staff ?? legacy ?? ["kody"], body };
8791
8859
  }
8792
8860
  function readProfile(cwd) {
8793
- const dir = path33.join(cwd, CONTEXT_DIR_REL_PATH);
8861
+ const dir = path32.join(cwd, CONTEXT_DIR_REL_PATH);
8794
8862
  if (!fs34.existsSync(dir)) return "";
8795
8863
  let entries;
8796
8864
  try {
@@ -8801,7 +8869,7 @@ function readProfile(cwd) {
8801
8869
  const blocks = [];
8802
8870
  for (const file of entries) {
8803
8871
  try {
8804
- const raw = fs34.readFileSync(path33.join(dir, file), "utf-8");
8872
+ const raw = fs34.readFileSync(path32.join(dir, file), "utf-8");
8805
8873
  const { staff, body } = readProfileStaff(raw);
8806
8874
  if (!staff.includes(QA_STAFF) && !staff.includes(ALL_STAFF)) continue;
8807
8875
  blocks.push(`## ${file}
@@ -8839,7 +8907,7 @@ init_events();
8839
8907
 
8840
8908
  // src/taskContext.ts
8841
8909
  import * as fs35 from "fs";
8842
- import * as path34 from "path";
8910
+ import * as path33 from "path";
8843
8911
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
8844
8912
  function buildTaskContext(args) {
8845
8913
  return {
@@ -8855,9 +8923,9 @@ function buildTaskContext(args) {
8855
8923
  }
8856
8924
  function persistTaskContext(cwd, ctx) {
8857
8925
  try {
8858
- const dir = path34.join(cwd, ".kody", "runs", ctx.runId);
8926
+ const dir = path33.join(cwd, ".kody", "runs", ctx.runId);
8859
8927
  fs35.mkdirSync(dir, { recursive: true });
8860
- const file = path34.join(dir, "task-context.json");
8928
+ const file = path33.join(dir, "task-context.json");
8861
8929
  fs35.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8862
8930
  `);
8863
8931
  return file;
@@ -8907,14 +8975,14 @@ var loadTaskState = async (ctx) => {
8907
8975
 
8908
8976
  // src/scripts/loadWorkerAdhoc.ts
8909
8977
  import * as fs36 from "fs";
8910
- import * as path35 from "path";
8978
+ import * as path34 from "path";
8911
8979
  var loadWorkerAdhoc = async (ctx, _profile, args) => {
8912
8980
  const workersDir = String(args?.workersDir ?? ".kody/staff");
8913
8981
  const workerSlug = String(ctx.args.worker ?? "").trim();
8914
8982
  if (!workerSlug) {
8915
8983
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
8916
8984
  }
8917
- const workerPath = path35.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8985
+ const workerPath = path34.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8918
8986
  if (!fs36.existsSync(workerPath)) {
8919
8987
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
8920
8988
  }
@@ -9103,7 +9171,7 @@ var mergeFlow = async (ctx) => {
9103
9171
  };
9104
9172
 
9105
9173
  // src/scripts/mergeReleasePr.ts
9106
- import { execFileSync as execFileSync20 } from "child_process";
9174
+ import { execFileSync as execFileSync18 } from "child_process";
9107
9175
  var API_TIMEOUT_MS7 = 6e4;
9108
9176
  var mergeReleasePr = async (ctx) => {
9109
9177
  const state = ctx.data.taskState;
@@ -9122,7 +9190,7 @@ var mergeReleasePr = async (ctx) => {
9122
9190
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
9123
9191
  `);
9124
9192
  try {
9125
- const out = execFileSync20("gh", ["pr", "merge", String(prNumber), "--merge"], {
9193
+ const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
9126
9194
  timeout: API_TIMEOUT_MS7,
9127
9195
  cwd: ctx.cwd,
9128
9196
  stdio: ["ignore", "pipe", "pipe"]
@@ -9610,8 +9678,8 @@ var FlyClient = class {
9610
9678
  get fetch() {
9611
9679
  return this.opts.fetchImpl ?? fetch;
9612
9680
  }
9613
- async call(path40, init = {}) {
9614
- const res = await this.fetch(`${FLY_API_BASE}${path40}`, {
9681
+ async call(path39, init = {}) {
9682
+ const res = await this.fetch(`${FLY_API_BASE}${path39}`, {
9615
9683
  method: init.method ?? "GET",
9616
9684
  headers: {
9617
9685
  Authorization: `Bearer ${this.opts.token}`,
@@ -9622,7 +9690,7 @@ var FlyClient = class {
9622
9690
  if (res.status === 404 && init.allow404) return null;
9623
9691
  if (!res.ok) {
9624
9692
  const text = await res.text().catch(() => "");
9625
- throw new Error(`Fly API ${res.status} on ${path40}: ${text.slice(0, 200) || res.statusText}`);
9693
+ throw new Error(`Fly API ${res.status} on ${path39}: ${text.slice(0, 200) || res.statusText}`);
9626
9694
  }
9627
9695
  if (res.status === 204) return null;
9628
9696
  const raw = await res.text();
@@ -10162,14 +10230,14 @@ function sendJson2(res, status, body) {
10162
10230
  res.end(JSON.stringify(body));
10163
10231
  }
10164
10232
  function readJsonBody2(req) {
10165
- return new Promise((resolve5, reject) => {
10233
+ return new Promise((resolve6, reject) => {
10166
10234
  const chunks = [];
10167
10235
  req.on("data", (c) => chunks.push(c));
10168
10236
  req.on("end", () => {
10169
10237
  const raw = Buffer.concat(chunks).toString("utf-8");
10170
- if (!raw.trim()) return resolve5({});
10238
+ if (!raw.trim()) return resolve6({});
10171
10239
  try {
10172
- resolve5(JSON.parse(raw));
10240
+ resolve6(JSON.parse(raw));
10173
10241
  } catch (err) {
10174
10242
  reject(err instanceof Error ? err : new Error(String(err)));
10175
10243
  }
@@ -10316,10 +10384,10 @@ var poolServe = async (ctx) => {
10316
10384
  }
10317
10385
  });
10318
10386
  const apiHost = process.env.POOL_API_HOST ?? "::";
10319
- await new Promise((resolve5) => {
10387
+ await new Promise((resolve6) => {
10320
10388
  server.listen(apiPort, apiHost, () => {
10321
10389
  log(`listening on ${apiHost}:${apiPort} (min=${min}, app=${app}, region=${region})`);
10322
- resolve5();
10390
+ resolve6();
10323
10391
  });
10324
10392
  });
10325
10393
  const shutdown = (signal) => {
@@ -10521,7 +10589,7 @@ ${body}`;
10521
10589
  }
10522
10590
 
10523
10591
  // src/scripts/recordClassification.ts
10524
- import { execFileSync as execFileSync21 } from "child_process";
10592
+ import { execFileSync as execFileSync19 } from "child_process";
10525
10593
  var API_TIMEOUT_MS8 = 3e4;
10526
10594
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
10527
10595
  var recordClassification = async (ctx) => {
@@ -10569,7 +10637,7 @@ function parseClassification(prSummary) {
10569
10637
  }
10570
10638
  function tryAuditComment(issueNumber, body, cwd) {
10571
10639
  try {
10572
- execFileSync21("gh", ["issue", "comment", String(issueNumber), "--body", body], {
10640
+ execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
10573
10641
  cwd,
10574
10642
  timeout: API_TIMEOUT_MS8,
10575
10643
  stdio: ["ignore", "pipe", "pipe"]
@@ -10683,7 +10751,7 @@ var resolveArtifacts = async (ctx, profile) => {
10683
10751
  };
10684
10752
 
10685
10753
  // src/scripts/resolveFlow.ts
10686
- import { execFileSync as execFileSync22 } from "child_process";
10754
+ import { execFileSync as execFileSync20 } from "child_process";
10687
10755
  init_issue();
10688
10756
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
10689
10757
  var resolveFlow = async (ctx) => {
@@ -10777,7 +10845,7 @@ function buildPreferBlock(prefer, baseBranch) {
10777
10845
  }
10778
10846
  function getConflictedFiles(cwd) {
10779
10847
  try {
10780
- const out = execFileSync22("git", ["diff", "--name-only", "--diff-filter=U"], {
10848
+ const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
10781
10849
  encoding: "utf-8",
10782
10850
  cwd,
10783
10851
  env: { ...process.env, HUSKY: "0" }
@@ -10792,7 +10860,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
10792
10860
  let total = 0;
10793
10861
  for (const f of files) {
10794
10862
  try {
10795
- const content = execFileSync22("cat", [f], { encoding: "utf-8", cwd }).toString();
10863
+ const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
10796
10864
  const snippet = `### ${f}
10797
10865
 
10798
10866
  \`\`\`
@@ -10816,12 +10884,12 @@ function tryPostPr3(prNumber, body, cwd) {
10816
10884
  function pushEmptyCommit(branch, cwd) {
10817
10885
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
10818
10886
  try {
10819
- execFileSync22(
10887
+ execFileSync20(
10820
10888
  "git",
10821
10889
  ["commit", "--allow-empty", "-m", "chore: kody resolve refresh \u2014 empty commit to recompute mergeable status"],
10822
10890
  { cwd, env, stdio: ["ignore", "pipe", "pipe"] }
10823
10891
  );
10824
- execFileSync22("git", ["push", "-u", "origin", branch], {
10892
+ execFileSync20("git", ["push", "-u", "origin", branch], {
10825
10893
  cwd,
10826
10894
  env,
10827
10895
  stdio: ["ignore", "pipe", "pipe"]
@@ -10952,10 +11020,10 @@ var resolvePreviewUrl = async (ctx) => {
10952
11020
  };
10953
11021
 
10954
11022
  // src/scripts/resolveQaUrl.ts
10955
- import { execFileSync as execFileSync23 } from "child_process";
11023
+ import { execFileSync as execFileSync21 } from "child_process";
10956
11024
  function ghQuery(args, cwd) {
10957
11025
  try {
10958
- const out = execFileSync23("gh", args, {
11026
+ const out = execFileSync21("gh", args, {
10959
11027
  cwd,
10960
11028
  stdio: ["ignore", "pipe", "pipe"],
10961
11029
  encoding: "utf-8",
@@ -11025,7 +11093,7 @@ var resolveQaUrl = async (ctx) => {
11025
11093
  };
11026
11094
 
11027
11095
  // src/scripts/revertFlow.ts
11028
- import { execFileSync as execFileSync24 } from "child_process";
11096
+ import { execFileSync as execFileSync22 } from "child_process";
11029
11097
  init_issue();
11030
11098
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
11031
11099
  var revertFlow = async (ctx) => {
@@ -11108,7 +11176,7 @@ function buildPrSummary(resolved) {
11108
11176
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
11109
11177
  }
11110
11178
  function git3(args, cwd) {
11111
- return execFileSync24("git", args, {
11179
+ return execFileSync22("git", args, {
11112
11180
  encoding: "utf-8",
11113
11181
  timeout: 3e4,
11114
11182
  cwd,
@@ -11118,7 +11186,7 @@ function git3(args, cwd) {
11118
11186
  }
11119
11187
  function isAncestorOfHead(sha, cwd) {
11120
11188
  try {
11121
- execFileSync24("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
11189
+ execFileSync22("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
11122
11190
  cwd,
11123
11191
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
11124
11192
  stdio: ["ignore", "ignore", "ignore"]
@@ -11237,17 +11305,17 @@ function authOk2(req, expected) {
11237
11305
  return false;
11238
11306
  }
11239
11307
  function readJsonBody3(req) {
11240
- return new Promise((resolve5, reject) => {
11308
+ return new Promise((resolve6, reject) => {
11241
11309
  const chunks = [];
11242
11310
  req.on("data", (c) => chunks.push(c));
11243
11311
  req.on("end", () => {
11244
11312
  const raw = Buffer.concat(chunks).toString("utf-8");
11245
11313
  if (!raw.trim()) {
11246
- resolve5({});
11314
+ resolve6({});
11247
11315
  return;
11248
11316
  }
11249
11317
  try {
11250
- resolve5(JSON.parse(raw));
11318
+ resolve6(JSON.parse(raw));
11251
11319
  } catch (err) {
11252
11320
  reject(err instanceof Error ? err : new Error(String(err)));
11253
11321
  }
@@ -11322,13 +11390,13 @@ async function defaultRunJob(job) {
11322
11390
  ...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
11323
11391
  ...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
11324
11392
  };
11325
- const run = (cmd, args, cwd) => new Promise((resolve5) => {
11393
+ const run = (cmd, args, cwd) => new Promise((resolve6) => {
11326
11394
  const child = spawn5(cmd, args, { stdio: "inherit", env: childEnv, cwd });
11327
- child.on("exit", (code) => resolve5(code ?? 0));
11395
+ child.on("exit", (code) => resolve6(code ?? 0));
11328
11396
  child.on("error", (err) => {
11329
11397
  process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
11330
11398
  `);
11331
- resolve5(1);
11399
+ resolve6(1);
11332
11400
  });
11333
11401
  });
11334
11402
  process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
@@ -11415,11 +11483,11 @@ var runnerServe = async (ctx) => {
11415
11483
  const port = Number(process.env.PORT ?? DEFAULT_PORT2);
11416
11484
  const server = buildServer2({ apiKey });
11417
11485
  const host = process.env.RUNNER_HOST ?? "::";
11418
- await new Promise((resolve5) => {
11486
+ await new Promise((resolve6) => {
11419
11487
  server.listen(port, host, () => {
11420
11488
  process.stdout.write(`[runner-serve] listening on ${host}:${port} (idle, awaiting job)
11421
11489
  `);
11422
- resolve5();
11490
+ resolve6();
11423
11491
  });
11424
11492
  });
11425
11493
  const shutdown = (signal) => {
@@ -11436,7 +11504,7 @@ var runnerServe = async (ctx) => {
11436
11504
  // src/scripts/runTickScript.ts
11437
11505
  import { spawnSync as spawnSync2 } from "child_process";
11438
11506
  import * as fs38 from "fs";
11439
- import * as path36 from "path";
11507
+ import * as path35 from "path";
11440
11508
  var runTickScript = async (ctx, _profile, args) => {
11441
11509
  ctx.skipAgent = true;
11442
11510
  const jobsDir = String(args?.jobsDir ?? ".kody/duties");
@@ -11448,7 +11516,7 @@ var runTickScript = async (ctx, _profile, args) => {
11448
11516
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
11449
11517
  return;
11450
11518
  }
11451
- const jobPath = path36.join(ctx.cwd, jobsDir, `${slug}.md`);
11519
+ const jobPath = path35.join(ctx.cwd, jobsDir, `${slug}.md`);
11452
11520
  if (!fs38.existsSync(jobPath)) {
11453
11521
  ctx.output.exitCode = 99;
11454
11522
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
@@ -11462,7 +11530,7 @@ var runTickScript = async (ctx, _profile, args) => {
11462
11530
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
11463
11531
  return;
11464
11532
  }
11465
- const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
11533
+ const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
11466
11534
  if (!fs38.existsSync(scriptPath)) {
11467
11535
  ctx.output.exitCode = 99;
11468
11536
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
@@ -11579,7 +11647,8 @@ var saveGoalState = async (ctx) => {
11579
11647
  lastDispatchedIssue: goal.lastDispatchedIssue,
11580
11648
  updatedAt: changed ? nowIso() : prev?.updatedAt
11581
11649
  };
11582
- writeGoalState(ctx.cwd, goal.id, updated);
11650
+ ctx.data.goalPersistState = updated;
11651
+ ctx.data.goalPersistChanged = changed;
11583
11652
  ctx.skipAgent = true;
11584
11653
  };
11585
11654
 
@@ -11663,14 +11732,14 @@ var serveFlow = async (ctx) => {
11663
11732
  `);
11664
11733
  const args = ["--dangerously-skip-permissions", "--model", model.model];
11665
11734
  const child = spawn6("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
11666
- const exitCode = await new Promise((resolve5) => {
11667
- child.on("exit", (code) => resolve5(code ?? 0));
11735
+ const exitCode = await new Promise((resolve6) => {
11736
+ child.on("exit", (code) => resolve6(code ?? 0));
11668
11737
  child.on("error", (err) => {
11669
11738
  process.stderr.write(`[kody serve] failed to launch Claude Code: ${err.message}
11670
11739
  `);
11671
11740
  process.stderr.write(` Install: https://docs.anthropic.com/claude/docs/claude-code
11672
11741
  `);
11673
- resolve5(1);
11742
+ resolve6(1);
11674
11743
  });
11675
11744
  });
11676
11745
  killProxy();
@@ -11760,11 +11829,11 @@ var skipAgent = async (ctx) => {
11760
11829
  };
11761
11830
 
11762
11831
  // src/scripts/stageMergeConflicts.ts
11763
- import { execFileSync as execFileSync25 } from "child_process";
11832
+ import { execFileSync as execFileSync23 } from "child_process";
11764
11833
  var stageMergeConflicts = async (ctx) => {
11765
11834
  if (ctx.data.agentDone === false) return;
11766
11835
  try {
11767
- execFileSync25("git", ["add", "-A"], {
11836
+ execFileSync23("git", ["add", "-A"], {
11768
11837
  cwd: ctx.cwd,
11769
11838
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
11770
11839
  stdio: "pipe"
@@ -11775,7 +11844,7 @@ var stageMergeConflicts = async (ctx) => {
11775
11844
 
11776
11845
  // src/scripts/startFlow.ts
11777
11846
  init_issue();
11778
- import { execFileSync as execFileSync26 } from "child_process";
11847
+ import { execFileSync as execFileSync24 } from "child_process";
11779
11848
  var API_TIMEOUT_MS9 = 3e4;
11780
11849
  var startFlow = async (ctx, profile, _agentResult, args) => {
11781
11850
  const entry = args?.entry;
@@ -11809,7 +11878,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
11809
11878
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
11810
11879
  const body = `@kody ${next}`;
11811
11880
  try {
11812
- execFileSync26("gh", [sub, "comment", String(targetNumber), "--body", body], {
11881
+ execFileSync24("gh", [sub, "comment", String(targetNumber), "--body", body], {
11813
11882
  timeout: API_TIMEOUT_MS9,
11814
11883
  cwd,
11815
11884
  stdio: ["ignore", "pipe", "pipe"]
@@ -11823,7 +11892,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
11823
11892
  }
11824
11893
 
11825
11894
  // src/scripts/syncFlow.ts
11826
- import { execFileSync as execFileSync27 } from "child_process";
11895
+ import { execFileSync as execFileSync25 } from "child_process";
11827
11896
  init_issue();
11828
11897
  var syncFlow = async (ctx, _profile, args) => {
11829
11898
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
@@ -11888,7 +11957,7 @@ function bail2(ctx, prNumber, reason) {
11888
11957
  }
11889
11958
  function revParseHead(cwd) {
11890
11959
  try {
11891
- return execFileSync27("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
11960
+ return execFileSync25("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
11892
11961
  } catch {
11893
11962
  return "";
11894
11963
  }
@@ -12006,7 +12075,7 @@ function stripAnsi2(s) {
12006
12075
  return s.replace(ANSI_RE2, "");
12007
12076
  }
12008
12077
  function runCommand2(command, cwd) {
12009
- return new Promise((resolve5) => {
12078
+ return new Promise((resolve6) => {
12010
12079
  const child = spawn7(command, {
12011
12080
  cwd,
12012
12081
  shell: true,
@@ -12033,11 +12102,11 @@ function runCommand2(command, cwd) {
12033
12102
  }, TEST_TIMEOUT_MS);
12034
12103
  child.on("exit", (code) => {
12035
12104
  clearTimeout(timer);
12036
- resolve5({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
12105
+ resolve6({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
12037
12106
  });
12038
12107
  child.on("error", (err) => {
12039
12108
  clearTimeout(timer);
12040
- resolve5({ exitCode: -1, output: err.message });
12109
+ resolve6({ exitCode: -1, output: err.message });
12041
12110
  });
12042
12111
  });
12043
12112
  }
@@ -12145,7 +12214,7 @@ var verifyWithRetry = async (ctx) => {
12145
12214
 
12146
12215
  // src/scripts/waitForCi.ts
12147
12216
  init_issue();
12148
- import { execFileSync as execFileSync28 } from "child_process";
12217
+ import { execFileSync as execFileSync26 } from "child_process";
12149
12218
  var API_TIMEOUT_MS10 = 3e4;
12150
12219
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
12151
12220
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -12223,7 +12292,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
12223
12292
  };
12224
12293
  function fetchChecks(prNumber, cwd) {
12225
12294
  try {
12226
- const raw = execFileSync28("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
12295
+ const raw = execFileSync26("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
12227
12296
  encoding: "utf-8",
12228
12297
  timeout: API_TIMEOUT_MS10,
12229
12298
  cwd,
@@ -12459,20 +12528,20 @@ function lineStream(stream) {
12459
12528
  tryDeliver();
12460
12529
  });
12461
12530
  return {
12462
- next: (timeoutMs) => new Promise((resolve5) => {
12531
+ next: (timeoutMs) => new Promise((resolve6) => {
12463
12532
  if (queue.length > 0) {
12464
- resolve5(queue.shift());
12533
+ resolve6(queue.shift());
12465
12534
  return;
12466
12535
  }
12467
12536
  if (ended) {
12468
- resolve5(null);
12537
+ resolve6(null);
12469
12538
  return;
12470
12539
  }
12471
- waiter = resolve5;
12540
+ waiter = resolve6;
12472
12541
  const t = setTimeout(() => {
12473
- if (waiter === resolve5) {
12542
+ if (waiter === resolve6) {
12474
12543
  waiter = null;
12475
- resolve5(null);
12544
+ resolve6(null);
12476
12545
  }
12477
12546
  }, Math.max(0, timeoutMs));
12478
12547
  t.unref?.();
@@ -12696,7 +12765,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
12696
12765
  ]);
12697
12766
 
12698
12767
  // src/tools.ts
12699
- import { execFileSync as execFileSync29 } from "child_process";
12768
+ import { execFileSync as execFileSync27 } from "child_process";
12700
12769
  function verifyCliTools(tools, cwd) {
12701
12770
  const out = [];
12702
12771
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -12710,31 +12779,49 @@ function firstRequiredFailure(results, tools) {
12710
12779
  }
12711
12780
  return null;
12712
12781
  }
12713
- function verifyOne(tool3, cwd) {
12714
- const result = { name: tool3.name, present: false, verified: false };
12715
- let present = runShell(tool3.install.checkCommand, cwd);
12716
- if (!present && tool3.install.installCommand) {
12717
- runShell(tool3.install.installCommand, cwd, 12e4);
12718
- present = runShell(tool3.install.checkCommand, cwd);
12782
+ function verifyOne(tool4, cwd) {
12783
+ const result = { name: tool4.name, present: false, verified: false };
12784
+ const checkRes = runShell(tool4.install.checkCommand, cwd);
12785
+ let present = checkRes.ok;
12786
+ if (!present && tool4.install.installCommand) {
12787
+ runShell(tool4.install.installCommand, cwd, 12e4);
12788
+ present = runShell(tool4.install.checkCommand, cwd).ok;
12719
12789
  }
12720
12790
  result.present = present;
12721
12791
  if (!present) {
12722
- result.error = `tool "${tool3.name}" not on PATH (check: ${tool3.install.checkCommand})`;
12792
+ result.error = `tool "${tool4.name}" not on PATH (check: ${tool4.install.checkCommand})`;
12723
12793
  return result;
12724
12794
  }
12725
- const verified = runShell(tool3.verify, cwd);
12726
- result.verified = verified;
12727
- if (!verified) result.error = `tool "${tool3.name}" failed verify: ${tool3.verify}`;
12795
+ const verifyRes = runShell(tool4.verify, cwd);
12796
+ result.verified = verifyRes.ok;
12797
+ if (!verifyRes.ok) {
12798
+ const tail = formatStderrTail(verifyRes.stderr, verifyRes.stdout);
12799
+ result.error = `tool "${tool4.name}" failed verify: ${tool4.verify}${tail ? ` \u2014 ${tail}` : ""}`;
12800
+ }
12728
12801
  return result;
12729
12802
  }
12730
12803
  function runShell(cmd, cwd, timeoutMs = 3e4) {
12731
12804
  try {
12732
- execFileSync29("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
12733
- return true;
12734
- } catch {
12735
- return false;
12805
+ const stdout = execFileSync27("sh", ["-c", cmd], {
12806
+ cwd,
12807
+ stdio: ["ignore", "pipe", "pipe"],
12808
+ timeout: timeoutMs,
12809
+ encoding: "utf-8"
12810
+ });
12811
+ return { ok: true, stdout: stdout ?? "", stderr: "" };
12812
+ } catch (err) {
12813
+ const e = err;
12814
+ const stdout = e.stdout ? e.stdout.toString() : "";
12815
+ const stderr = e.stderr ? e.stderr.toString() : "";
12816
+ return { ok: false, stdout, stderr };
12736
12817
  }
12737
12818
  }
12819
+ function formatStderrTail(stderr, stdout) {
12820
+ const source = stderr.trim() || stdout.trim();
12821
+ if (!source) return "";
12822
+ const flat = source.replace(/\s+/g, " ").trim();
12823
+ return flat.length > 400 ? `\u2026${flat.slice(-400)}` : flat;
12824
+ }
12738
12825
 
12739
12826
  // src/executor.ts
12740
12827
  var CONTAINER_MAX_ITERATIONS = 50;
@@ -12780,11 +12867,12 @@ async function runExecutable(profileName, input) {
12780
12867
  if (input.config) {
12781
12868
  config = input.config;
12782
12869
  } else if (input.skipConfig) {
12870
+ const envModel = process.env.MODEL?.trim();
12783
12871
  config = {
12784
12872
  quality: { typecheck: "", lint: "", testUnit: "", format: "" },
12785
12873
  git: { defaultBranch: "main" },
12786
12874
  github: { owner: "", repo: "" },
12787
- agent: { model: "claude/claude-haiku-4-5-20251001" }
12875
+ agent: { model: envModel || "claude/claude-haiku-4-5-20251001" }
12788
12876
  };
12789
12877
  } else {
12790
12878
  try {
@@ -12837,9 +12925,9 @@ async function runExecutable(profileName, input) {
12837
12925
  })
12838
12926
  };
12839
12927
  })() : null;
12840
- const ndjsonDir = path37.join(input.cwd, ".kody");
12928
+ const ndjsonDir = path36.join(input.cwd, ".kody");
12841
12929
  const invokeAgent = async (prompt) => {
12842
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path37.isAbsolute(p) ? p : path37.resolve(profile.dir, p)).filter((p) => p.length > 0);
12930
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path36.isAbsolute(p) ? p : path36.resolve(profile.dir, p)).filter((p) => p.length > 0);
12843
12931
  const syntheticPath = ctx.data.syntheticPluginPath;
12844
12932
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
12845
12933
  const agents = loadSubagents(profile);
@@ -13060,13 +13148,13 @@ function getProfileInputsForChild(profileName, _cwd) {
13060
13148
  function resolveProfilePath(profileName) {
13061
13149
  const found = resolveExecutable(profileName);
13062
13150
  if (found) return found;
13063
- const here = path37.dirname(new URL(import.meta.url).pathname);
13151
+ const here = path36.dirname(new URL(import.meta.url).pathname);
13064
13152
  const candidates = [
13065
- path37.join(here, "executables", profileName, "profile.json"),
13153
+ path36.join(here, "executables", profileName, "profile.json"),
13066
13154
  // same-dir sibling (dev)
13067
- path37.join(here, "..", "executables", profileName, "profile.json"),
13155
+ path36.join(here, "..", "executables", profileName, "profile.json"),
13068
13156
  // up one (prod: dist/bin → dist/executables)
13069
- path37.join(here, "..", "src", "executables", profileName, "profile.json")
13157
+ path36.join(here, "..", "src", "executables", profileName, "profile.json")
13070
13158
  // fallback
13071
13159
  ];
13072
13160
  for (const c of candidates) {
@@ -13170,7 +13258,7 @@ function resolveShellTimeoutMs(entry) {
13170
13258
  var SIGKILL_GRACE_MS = 5e3;
13171
13259
  async function runShellEntry(entry, ctx, profile) {
13172
13260
  const shellName = entry.shell;
13173
- const shellPath = path37.join(profile.dir, shellName);
13261
+ const shellPath = path36.join(profile.dir, shellName);
13174
13262
  if (!fs40.existsSync(shellPath)) {
13175
13263
  ctx.skipAgent = true;
13176
13264
  ctx.output.exitCode = 99;
@@ -13209,14 +13297,14 @@ async function runShellEntry(entry, ctx, profile) {
13209
13297
  let killTimer;
13210
13298
  let escalateTimer;
13211
13299
  const result = await new Promise(
13212
- (resolve5) => {
13300
+ (resolve6) => {
13213
13301
  let settled = false;
13214
13302
  const settle = (code, signal, spawnErr) => {
13215
13303
  if (settled) return;
13216
13304
  settled = true;
13217
13305
  if (killTimer) clearTimeout(killTimer);
13218
13306
  if (escalateTimer) clearTimeout(escalateTimer);
13219
- resolve5({ code, signal, spawnErr });
13307
+ resolve6({ code, signal, spawnErr });
13220
13308
  };
13221
13309
  child.on("error", (err) => settle(null, null, err));
13222
13310
  child.on("close", (code, signal) => settle(code, signal));
@@ -13498,7 +13586,7 @@ async function runContainerLoop(profile, ctx, input) {
13498
13586
  }
13499
13587
  function resetWorkingTree2(cwd) {
13500
13588
  try {
13501
- execFileSync30("git", ["reset", "--hard", "HEAD"], {
13589
+ execFileSync28("git", ["reset", "--hard", "HEAD"], {
13502
13590
  cwd,
13503
13591
  stdio: ["ignore", "pipe", "pipe"],
13504
13592
  timeout: 3e4
@@ -13650,14 +13738,14 @@ function resolveAuthToken(env = process.env) {
13650
13738
  return token;
13651
13739
  }
13652
13740
  function detectPackageManager2(cwd) {
13653
- if (fs41.existsSync(path38.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
13654
- if (fs41.existsSync(path38.join(cwd, "yarn.lock"))) return "yarn";
13655
- if (fs41.existsSync(path38.join(cwd, "bun.lockb"))) return "bun";
13741
+ if (fs41.existsSync(path37.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
13742
+ if (fs41.existsSync(path37.join(cwd, "yarn.lock"))) return "yarn";
13743
+ if (fs41.existsSync(path37.join(cwd, "bun.lockb"))) return "bun";
13656
13744
  return "npm";
13657
13745
  }
13658
13746
  function shellOut(cmd, args, cwd, stream = true) {
13659
13747
  try {
13660
- execFileSync31(cmd, args, {
13748
+ execFileSync29(cmd, args, {
13661
13749
  cwd,
13662
13750
  stdio: stream ? "inherit" : "pipe",
13663
13751
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -13670,7 +13758,7 @@ function shellOut(cmd, args, cwd, stream = true) {
13670
13758
  }
13671
13759
  function isOnPath(bin) {
13672
13760
  try {
13673
- execFileSync31("which", [bin], { stdio: "pipe" });
13761
+ execFileSync29("which", [bin], { stdio: "pipe" });
13674
13762
  return true;
13675
13763
  } catch {
13676
13764
  return false;
@@ -13711,7 +13799,7 @@ function installLitellmIfNeeded(cwd) {
13711
13799
  } catch {
13712
13800
  }
13713
13801
  try {
13714
- execFileSync31("python3", ["-c", "import litellm"], { stdio: "pipe" });
13802
+ execFileSync29("python3", ["-c", "import litellm"], { stdio: "pipe" });
13715
13803
  process.stdout.write("\u2192 kody: litellm already installed\n");
13716
13804
  return 0;
13717
13805
  } catch {
@@ -13721,16 +13809,16 @@ function installLitellmIfNeeded(cwd) {
13721
13809
  }
13722
13810
  function configureGitIdentity(cwd) {
13723
13811
  try {
13724
- const name = execFileSync31("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
13812
+ const name = execFileSync29("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
13725
13813
  if (name) return;
13726
13814
  } catch {
13727
13815
  }
13728
13816
  try {
13729
- execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
13817
+ execFileSync29("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
13730
13818
  } catch {
13731
13819
  }
13732
13820
  try {
13733
- execFileSync31("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
13821
+ execFileSync29("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
13734
13822
  cwd,
13735
13823
  stdio: "pipe"
13736
13824
  });
@@ -13739,7 +13827,7 @@ function configureGitIdentity(cwd) {
13739
13827
  }
13740
13828
  function postFailureTail(issueNumber, cwd, reason) {
13741
13829
  if (!issueNumber) return;
13742
- const logPath = path38.join(cwd, ".kody", "last-run.jsonl");
13830
+ const logPath = path37.join(cwd, ".kody", "last-run.jsonl");
13743
13831
  let tail = "";
13744
13832
  try {
13745
13833
  if (fs41.existsSync(logPath)) {
@@ -13768,7 +13856,7 @@ async function runCi(argv) {
13768
13856
  return 0;
13769
13857
  }
13770
13858
  const args = parseCiArgs(argv);
13771
- const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
13859
+ const cwd = args.cwd ? path37.resolve(args.cwd) : process.cwd();
13772
13860
  let earlyConfig;
13773
13861
  try {
13774
13862
  earlyConfig = loadConfig(cwd);
@@ -14039,17 +14127,17 @@ function parseChatArgs(argv, env = process.env) {
14039
14127
  return result;
14040
14128
  }
14041
14129
  function commitChatFiles(cwd, sessionId, verbose) {
14042
- const sessionFile = path39.relative(cwd, sessionFilePath(cwd, sessionId));
14043
- const eventsFile = path39.relative(cwd, eventsFilePath(cwd, sessionId));
14130
+ const sessionFile = path38.relative(cwd, sessionFilePath(cwd, sessionId));
14131
+ const eventsFile = path38.relative(cwd, eventsFilePath(cwd, sessionId));
14044
14132
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
14045
- const tasksDir = path39.join(".kody", "tasks", safeSession);
14133
+ const tasksDir = path38.join(".kody", "tasks", safeSession);
14046
14134
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
14047
- const paths = candidatePaths.filter((p) => fs42.existsSync(path39.join(cwd, p)));
14135
+ const paths = candidatePaths.filter((p) => fs42.existsSync(path38.join(cwd, p)));
14048
14136
  if (paths.length === 0) return;
14049
14137
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
14050
14138
  try {
14051
- execFileSync32("git", ["add", "-f", ...paths], opts);
14052
- execFileSync32("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
14139
+ execFileSync30("git", ["add", "-f", ...paths], opts);
14140
+ execFileSync30("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
14053
14141
  } catch (err) {
14054
14142
  const msg = err instanceof Error ? err.message : String(err);
14055
14143
  process.stderr.write(`[kody:chat] commit skipped: ${msg}
@@ -14088,7 +14176,7 @@ async function runChat(argv) {
14088
14176
  ${CHAT_HELP}`);
14089
14177
  return 64;
14090
14178
  }
14091
- const cwd = args.cwd ? path39.resolve(args.cwd) : process.cwd();
14179
+ const cwd = args.cwd ? path38.resolve(args.cwd) : process.cwd();
14092
14180
  const sessionId = args.sessionId;
14093
14181
  const unpackedSecrets = unpackAllSecrets();
14094
14182
  if (unpackedSecrets > 0) {
@@ -14496,7 +14584,7 @@ ${HELP_TEXT}`);
14496
14584
  }
14497
14585
  }
14498
14586
  const cwd = args.cwd ?? process.cwd();
14499
- const configlessCommands = /* @__PURE__ */ new Set(["init", "goal-scheduler"]);
14587
+ const configlessCommands = /* @__PURE__ */ new Set(["init", "goal-scheduler", "brain-serve"]);
14500
14588
  const skipConfig = configlessCommands.has(args.executableName ?? "");
14501
14589
  try {
14502
14590
  const result = await runExecutable(args.executableName, {