@kody-ade/kody-engine 0.4.40 → 0.4.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/bin/kody.js +668 -276
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.40",
6
+ version: "0.4.42",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -52,8 +52,8 @@ var package_default = {
52
52
 
53
53
  // src/chat-cli.ts
54
54
  import { execFileSync as execFileSync31 } from "child_process";
55
- import * as fs30 from "fs";
56
- import * as path28 from "path";
55
+ import * as fs31 from "fs";
56
+ import * as path29 from "path";
57
57
 
58
58
  // src/chat/events.ts
59
59
  import * as fs from "fs";
@@ -417,6 +417,9 @@ async function runAgent(opts) {
417
417
  const resultTexts = [];
418
418
  let outcome = "failed";
419
419
  let errorMessage;
420
+ const tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
421
+ let messageCount = 0;
422
+ const startedAt = Date.now();
420
423
  try {
421
424
  const queryOptions = {
422
425
  model: opts.model.model,
@@ -454,6 +457,7 @@ async function runAgent(opts) {
454
457
  options: queryOptions
455
458
  });
456
459
  for await (const msg of result) {
460
+ messageCount++;
457
461
  try {
458
462
  fullLog.write(`${JSON.stringify(msg)}
459
463
  `);
@@ -463,6 +467,17 @@ async function runAgent(opts) {
463
467
  if (line) process.stdout.write(`${line}
464
468
  `);
465
469
  const m = msg;
470
+ const usage = m.usage;
471
+ if (usage && typeof usage === "object") {
472
+ const i = Number(usage.input_tokens ?? 0);
473
+ const o = Number(usage.output_tokens ?? 0);
474
+ const cr = Number(usage.cache_read_input_tokens ?? 0);
475
+ const cc = Number(usage.cache_creation_input_tokens ?? 0);
476
+ if (Number.isFinite(i)) tokens.input += i;
477
+ if (Number.isFinite(o)) tokens.output += o;
478
+ if (Number.isFinite(cr)) tokens.cacheRead += cr;
479
+ if (Number.isFinite(cc)) tokens.cacheCreate += cc;
480
+ }
466
481
  if (m.type === "result") {
467
482
  if (m.subtype === "success") {
468
483
  outcome = "completed";
@@ -484,7 +499,15 @@ async function runAgent(opts) {
484
499
  }
485
500
  }
486
501
  const finalText = resultTexts.join("\n\n---\n\n");
487
- return { outcome, finalText, error: errorMessage, ndjsonPath };
502
+ return {
503
+ outcome,
504
+ finalText,
505
+ error: errorMessage,
506
+ ndjsonPath,
507
+ durationMs: Date.now() - startedAt,
508
+ tokens,
509
+ messageCount
510
+ };
488
511
  }
489
512
 
490
513
  // src/chat/session.ts
@@ -913,8 +936,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
913
936
 
914
937
  // src/kody-cli.ts
915
938
  import { execFileSync as execFileSync30 } from "child_process";
916
- import * as fs29 from "fs";
917
- import * as path27 from "path";
939
+ import * as fs30 from "fs";
940
+ import * as path28 from "path";
918
941
 
919
942
  // src/dispatch.ts
920
943
  import * as fs7 from "fs";
@@ -1493,12 +1516,75 @@ function postPrReviewComment(prNumber, body, cwd) {
1493
1516
 
1494
1517
  // src/executor.ts
1495
1518
  import { execFileSync as execFileSync29, spawn as spawn5 } from "child_process";
1496
- import * as fs28 from "fs";
1497
- import * as path26 from "path";
1519
+ import * as fs29 from "fs";
1520
+ import * as path27 from "path";
1498
1521
 
1499
- // src/profile.ts
1522
+ // src/events.ts
1523
+ import * as crypto from "crypto";
1500
1524
  import * as fs8 from "fs";
1501
1525
  import * as path7 from "path";
1526
+ var cachedRunId = null;
1527
+ function resolveRunId() {
1528
+ if (cachedRunId) return cachedRunId;
1529
+ if (process.env.KODY_RUN_ID) {
1530
+ cachedRunId = process.env.KODY_RUN_ID;
1531
+ return cachedRunId;
1532
+ }
1533
+ if (process.env.GITHUB_RUN_ID) {
1534
+ const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
1535
+ cachedRunId = `gh-${process.env.GITHUB_RUN_ID}-${attempt}`;
1536
+ } else {
1537
+ cachedRunId = `${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
1538
+ }
1539
+ process.env.KODY_RUN_ID = cachedRunId;
1540
+ return cachedRunId;
1541
+ }
1542
+ function emitEvent(cwd, ev) {
1543
+ if (process.env.KODY_EVENTS === "0") return;
1544
+ try {
1545
+ const runId = resolveRunId();
1546
+ const fullEvent = {
1547
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1548
+ runId,
1549
+ ...ev
1550
+ };
1551
+ const eventsPath = path7.join(cwd, ".kody", "runs", runId, "events.jsonl");
1552
+ fs8.mkdirSync(path7.dirname(eventsPath), { recursive: true });
1553
+ fs8.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}
1554
+ `);
1555
+ } catch {
1556
+ }
1557
+ }
1558
+ function readEvents(cwd, runId) {
1559
+ const eventsPath = path7.join(cwd, ".kody", "runs", runId, "events.jsonl");
1560
+ if (!fs8.existsSync(eventsPath)) return [];
1561
+ const lines = fs8.readFileSync(eventsPath, "utf-8").split("\n");
1562
+ const out = [];
1563
+ for (const line of lines) {
1564
+ const trimmed = line.trim();
1565
+ if (!trimmed) continue;
1566
+ try {
1567
+ out.push(JSON.parse(trimmed));
1568
+ } catch {
1569
+ }
1570
+ }
1571
+ return out;
1572
+ }
1573
+ function listRuns(cwd) {
1574
+ const runsDir = path7.join(cwd, ".kody", "runs");
1575
+ if (!fs8.existsSync(runsDir)) return [];
1576
+ return fs8.readdirSync(runsDir).filter((name) => {
1577
+ try {
1578
+ return fs8.statSync(path7.join(runsDir, name)).isDirectory();
1579
+ } catch {
1580
+ return false;
1581
+ }
1582
+ }).sort();
1583
+ }
1584
+
1585
+ // src/profile.ts
1586
+ import * as fs9 from "fs";
1587
+ import * as path8 from "path";
1502
1588
  var VALID_INPUT_TYPES = /* @__PURE__ */ new Set(["int", "string", "bool", "enum"]);
1503
1589
  var VALID_PERMISSION_MODES = /* @__PURE__ */ new Set(["default", "acceptEdits", "plan", "bypassPermissions"]);
1504
1590
  var VALID_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
@@ -1514,12 +1600,12 @@ var ProfileError = class extends Error {
1514
1600
  profilePath;
1515
1601
  };
1516
1602
  function loadProfile(profilePath) {
1517
- if (!fs8.existsSync(profilePath)) {
1603
+ if (!fs9.existsSync(profilePath)) {
1518
1604
  throw new ProfileError(profilePath, "file not found");
1519
1605
  }
1520
1606
  let raw;
1521
1607
  try {
1522
- raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
1608
+ raw = JSON.parse(fs9.readFileSync(profilePath, "utf-8"));
1523
1609
  } catch (err) {
1524
1610
  throw new ProfileError(profilePath, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
1525
1611
  }
@@ -1558,7 +1644,7 @@ function loadProfile(profilePath) {
1558
1644
  inputArtifacts: parseInputArtifacts(profilePath, r.input),
1559
1645
  outputArtifacts: parseOutputArtifacts(profilePath, r.output),
1560
1646
  children,
1561
- dir: path7.dirname(profilePath)
1647
+ dir: path8.dirname(profilePath)
1562
1648
  };
1563
1649
  return profile;
1564
1650
  }
@@ -1921,9 +2007,9 @@ function errMsg(err) {
1921
2007
 
1922
2008
  // src/litellm.ts
1923
2009
  import { execFileSync as execFileSync4, spawn } from "child_process";
1924
- import * as fs9 from "fs";
2010
+ import * as fs10 from "fs";
1925
2011
  import * as os from "os";
1926
- import * as path8 from "path";
2012
+ import * as path9 from "path";
1927
2013
  async function checkLitellmHealth(url) {
1928
2014
  try {
1929
2015
  const response = await fetch(`${url}/health`, { signal: AbortSignal.timeout(3e3) });
@@ -1963,20 +2049,20 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
1963
2049
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
1964
2050
  }
1965
2051
  }
1966
- const configPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
1967
- fs9.writeFileSync(configPath, generateLitellmConfigYaml(model));
2052
+ const configPath = path9.join(os.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
2053
+ fs10.writeFileSync(configPath, generateLitellmConfigYaml(model));
1968
2054
  const portMatch = url.match(/:(\d+)/);
1969
2055
  const port = portMatch ? portMatch[1] : "4000";
1970
2056
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
1971
2057
  const dotenvVars = readDotenvApiKeys(projectDir);
1972
- const logPath = path8.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
1973
- const outFd = fs9.openSync(logPath, "w");
2058
+ const logPath = path9.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
2059
+ const outFd = fs10.openSync(logPath, "w");
1974
2060
  const child = spawn(cmd, args, {
1975
2061
  stdio: ["ignore", outFd, outFd],
1976
2062
  detached: true,
1977
2063
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
1978
2064
  });
1979
- fs9.closeSync(outFd);
2065
+ fs10.closeSync(outFd);
1980
2066
  for (let i = 0; i < 30; i++) {
1981
2067
  await new Promise((r) => setTimeout(r, 2e3));
1982
2068
  if (await checkLitellmHealth(url)) {
@@ -1993,7 +2079,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
1993
2079
  }
1994
2080
  let logTail = "";
1995
2081
  try {
1996
- logTail = fs9.readFileSync(logPath, "utf-8").slice(-2e3);
2082
+ logTail = fs10.readFileSync(logPath, "utf-8").slice(-2e3);
1997
2083
  } catch {
1998
2084
  }
1999
2085
  try {
@@ -2004,10 +2090,10 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2004
2090
  ${logTail}`);
2005
2091
  }
2006
2092
  function readDotenvApiKeys(projectDir) {
2007
- const dotenvPath = path8.join(projectDir, ".env");
2008
- if (!fs9.existsSync(dotenvPath)) return {};
2093
+ const dotenvPath = path9.join(projectDir, ".env");
2094
+ if (!fs10.existsSync(dotenvPath)) return {};
2009
2095
  const result = {};
2010
- for (const rawLine of fs9.readFileSync(dotenvPath, "utf-8").split("\n")) {
2096
+ for (const rawLine of fs10.readFileSync(dotenvPath, "utf-8").split("\n")) {
2011
2097
  const line = rawLine.trim();
2012
2098
  if (!line || line.startsWith("#")) continue;
2013
2099
  const match = line.match(/^([A-Z_][A-Z0-9_]*_API_KEY)=(.*)$/);
@@ -2031,8 +2117,8 @@ function stripBlockingEnv(env) {
2031
2117
 
2032
2118
  // src/commit.ts
2033
2119
  import { execFileSync as execFileSync5 } from "child_process";
2034
- import * as fs10 from "fs";
2035
- import * as path9 from "path";
2120
+ import * as fs11 from "fs";
2121
+ import * as path10 from "path";
2036
2122
  var FORBIDDEN_PATH_PREFIXES = [
2037
2123
  ".kody/",
2038
2124
  ".kody-engine/",
@@ -2088,18 +2174,18 @@ function tryGit(args, cwd) {
2088
2174
  }
2089
2175
  function abortUnfinishedGitOps(cwd) {
2090
2176
  const aborted = [];
2091
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
2092
- if (!fs10.existsSync(gitDir)) return aborted;
2093
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
2177
+ const gitDir = path10.join(cwd ?? process.cwd(), ".git");
2178
+ if (!fs11.existsSync(gitDir)) return aborted;
2179
+ if (fs11.existsSync(path10.join(gitDir, "MERGE_HEAD"))) {
2094
2180
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
2095
2181
  }
2096
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
2182
+ if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
2097
2183
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
2098
2184
  }
2099
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
2185
+ if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
2100
2186
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
2101
2187
  }
2102
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
2188
+ if (fs11.existsSync(path10.join(gitDir, "rebase-merge")) || fs11.existsSync(path10.join(gitDir, "rebase-apply"))) {
2103
2189
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
2104
2190
  }
2105
2191
  try {
@@ -2155,7 +2241,7 @@ function normalizeCommitMessage(raw) {
2155
2241
  function commitAndPush(branch, agentMessage, cwd) {
2156
2242
  const allChanged = listChangedFiles(cwd);
2157
2243
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
2158
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2244
+ const mergeHeadExists = fs11.existsSync(path10.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2159
2245
  if (allowedFiles.length === 0 && !mergeHeadExists) {
2160
2246
  return { committed: false, pushed: false, sha: "", message: "" };
2161
2247
  }
@@ -2467,21 +2553,21 @@ var advanceFlow = async (ctx, profile) => {
2467
2553
  };
2468
2554
 
2469
2555
  // src/scripts/buildSyntheticPlugin.ts
2470
- import * as fs11 from "fs";
2556
+ import * as fs12 from "fs";
2471
2557
  import * as os2 from "os";
2472
- import * as path10 from "path";
2558
+ import * as path11 from "path";
2473
2559
  function getPluginsCatalogRoot() {
2474
- const here = path10.dirname(new URL(import.meta.url).pathname);
2560
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2475
2561
  const candidates = [
2476
- path10.join(here, "..", "plugins"),
2562
+ path11.join(here, "..", "plugins"),
2477
2563
  // dev: src/scripts → src/plugins
2478
- path10.join(here, "..", "..", "plugins"),
2564
+ path11.join(here, "..", "..", "plugins"),
2479
2565
  // built: dist/scripts → dist/plugins
2480
- path10.join(here, "..", "..", "src", "plugins")
2566
+ path11.join(here, "..", "..", "src", "plugins")
2481
2567
  // fallback
2482
2568
  ];
2483
2569
  for (const c of candidates) {
2484
- if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2570
+ if (fs12.existsSync(c) && fs12.statSync(c).isDirectory()) return c;
2485
2571
  }
2486
2572
  return candidates[0];
2487
2573
  }
@@ -2491,52 +2577,52 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2491
2577
  if (!needsSynthetic) return;
2492
2578
  const catalog = getPluginsCatalogRoot();
2493
2579
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2494
- const root = path10.join(os2.tmpdir(), `kody-synth-${runId}`);
2495
- fs11.mkdirSync(path10.join(root, ".claude-plugin"), { recursive: true });
2580
+ const root = path11.join(os2.tmpdir(), `kody-synth-${runId}`);
2581
+ fs12.mkdirSync(path11.join(root, ".claude-plugin"), { recursive: true });
2496
2582
  const resolvePart = (bucket, entry) => {
2497
- const local = path10.join(profile.dir, bucket, entry);
2498
- if (fs11.existsSync(local)) return local;
2499
- const central = path10.join(catalog, bucket, entry);
2500
- if (fs11.existsSync(central)) return central;
2583
+ const local = path11.join(profile.dir, bucket, entry);
2584
+ if (fs12.existsSync(local)) return local;
2585
+ const central = path11.join(catalog, bucket, entry);
2586
+ if (fs12.existsSync(central)) return central;
2501
2587
  throw new Error(
2502
2588
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2503
2589
  );
2504
2590
  };
2505
2591
  if (cc.skills.length > 0) {
2506
- const dst = path10.join(root, "skills");
2507
- fs11.mkdirSync(dst, { recursive: true });
2592
+ const dst = path11.join(root, "skills");
2593
+ fs12.mkdirSync(dst, { recursive: true });
2508
2594
  for (const name of cc.skills) {
2509
- copyDir(resolvePart("skills", name), path10.join(dst, name));
2595
+ copyDir(resolvePart("skills", name), path11.join(dst, name));
2510
2596
  }
2511
2597
  }
2512
2598
  if (cc.commands.length > 0) {
2513
- const dst = path10.join(root, "commands");
2514
- fs11.mkdirSync(dst, { recursive: true });
2599
+ const dst = path11.join(root, "commands");
2600
+ fs12.mkdirSync(dst, { recursive: true });
2515
2601
  for (const name of cc.commands) {
2516
- fs11.copyFileSync(resolvePart("commands", `${name}.md`), path10.join(dst, `${name}.md`));
2602
+ fs12.copyFileSync(resolvePart("commands", `${name}.md`), path11.join(dst, `${name}.md`));
2517
2603
  }
2518
2604
  }
2519
2605
  if (cc.subagents.length > 0) {
2520
- const dst = path10.join(root, "agents");
2521
- fs11.mkdirSync(dst, { recursive: true });
2606
+ const dst = path11.join(root, "agents");
2607
+ fs12.mkdirSync(dst, { recursive: true });
2522
2608
  for (const name of cc.subagents) {
2523
- fs11.copyFileSync(resolvePart("agents", `${name}.md`), path10.join(dst, `${name}.md`));
2609
+ fs12.copyFileSync(resolvePart("agents", `${name}.md`), path11.join(dst, `${name}.md`));
2524
2610
  }
2525
2611
  }
2526
2612
  if (cc.hooks.length > 0) {
2527
- const dst = path10.join(root, "hooks");
2528
- fs11.mkdirSync(dst, { recursive: true });
2613
+ const dst = path11.join(root, "hooks");
2614
+ fs12.mkdirSync(dst, { recursive: true });
2529
2615
  const merged = { hooks: {} };
2530
2616
  for (const name of cc.hooks) {
2531
2617
  const src = resolvePart("hooks", `${name}.json`);
2532
- const parsed = JSON.parse(fs11.readFileSync(src, "utf-8"));
2618
+ const parsed = JSON.parse(fs12.readFileSync(src, "utf-8"));
2533
2619
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2534
2620
  if (!Array.isArray(entries)) continue;
2535
2621
  if (!merged.hooks[event]) merged.hooks[event] = [];
2536
2622
  merged.hooks[event].push(...entries);
2537
2623
  }
2538
2624
  }
2539
- fs11.writeFileSync(path10.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2625
+ fs12.writeFileSync(path11.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2540
2626
  `);
2541
2627
  }
2542
2628
  const manifest = {
@@ -2547,17 +2633,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2547
2633
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2548
2634
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2549
2635
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
2550
- fs11.writeFileSync(path10.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2636
+ fs12.writeFileSync(path11.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2551
2637
  `);
2552
2638
  ctx.data.syntheticPluginPath = root;
2553
2639
  };
2554
2640
  function copyDir(src, dst) {
2555
- fs11.mkdirSync(dst, { recursive: true });
2556
- for (const ent of fs11.readdirSync(src, { withFileTypes: true })) {
2557
- const s = path10.join(src, ent.name);
2558
- const d = path10.join(dst, ent.name);
2641
+ fs12.mkdirSync(dst, { recursive: true });
2642
+ for (const ent of fs12.readdirSync(src, { withFileTypes: true })) {
2643
+ const s = path11.join(src, ent.name);
2644
+ const d = path11.join(dst, ent.name);
2559
2645
  if (ent.isDirectory()) copyDir(s, d);
2560
- else if (ent.isFile()) fs11.copyFileSync(s, d);
2646
+ else if (ent.isFile()) fs12.copyFileSync(s, d);
2561
2647
  }
2562
2648
  }
2563
2649
 
@@ -2623,18 +2709,18 @@ function formatMissesForFeedback(misses) {
2623
2709
  }
2624
2710
 
2625
2711
  // src/prompt.ts
2626
- import * as fs12 from "fs";
2627
- import * as path11 from "path";
2712
+ import * as fs13 from "fs";
2713
+ import * as path12 from "path";
2628
2714
  var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
2629
2715
  var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2630
2716
  function loadProjectConventions(projectDir) {
2631
2717
  const out = [];
2632
2718
  for (const rel of CONVENTION_FILES) {
2633
- const abs = path11.join(projectDir, rel);
2634
- if (!fs12.existsSync(abs)) continue;
2719
+ const abs = path12.join(projectDir, rel);
2720
+ if (!fs13.existsSync(abs)) continue;
2635
2721
  let content;
2636
2722
  try {
2637
- content = fs12.readFileSync(abs, "utf-8");
2723
+ content = fs13.readFileSync(abs, "utf-8");
2638
2724
  } catch {
2639
2725
  continue;
2640
2726
  }
@@ -2846,11 +2932,11 @@ var commitAndPush2 = async (ctx) => {
2846
2932
 
2847
2933
  // src/scripts/commitGoalState.ts
2848
2934
  import { execFileSync as execFileSync9 } from "child_process";
2849
- import * as path12 from "path";
2935
+ import * as path13 from "path";
2850
2936
  var commitGoalState = async (ctx) => {
2851
2937
  const goal = ctx.data.goal;
2852
2938
  if (!goal) return;
2853
- const stateRel = path12.posix.join(".kody", "goals", goal.id, "state.json");
2939
+ const stateRel = path13.posix.join(".kody", "goals", goal.id, "state.json");
2854
2940
  try {
2855
2941
  execFileSync9("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
2856
2942
  } catch (err) {
@@ -2894,20 +2980,20 @@ function describeCommitMessage(goal) {
2894
2980
  }
2895
2981
 
2896
2982
  // src/scripts/composePrompt.ts
2897
- import * as fs13 from "fs";
2898
- import * as path13 from "path";
2983
+ import * as fs14 from "fs";
2984
+ import * as path14 from "path";
2899
2985
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
2900
2986
  var composePrompt = async (ctx, profile) => {
2901
2987
  const explicit = ctx.data.promptTemplate;
2902
2988
  const mode = ctx.args.mode;
2903
2989
  const candidates = [
2904
- explicit ? path13.join(profile.dir, explicit) : null,
2905
- mode ? path13.join(profile.dir, "prompts", `${mode}.md`) : null,
2906
- path13.join(profile.dir, "prompt.md")
2990
+ explicit ? path14.join(profile.dir, explicit) : null,
2991
+ mode ? path14.join(profile.dir, "prompts", `${mode}.md`) : null,
2992
+ path14.join(profile.dir, "prompt.md")
2907
2993
  ].filter(Boolean);
2908
2994
  let templatePath = "";
2909
2995
  for (const c of candidates) {
2910
- if (fs13.existsSync(c)) {
2996
+ if (fs14.existsSync(c)) {
2911
2997
  templatePath = c;
2912
2998
  break;
2913
2999
  }
@@ -2915,7 +3001,7 @@ var composePrompt = async (ctx, profile) => {
2915
3001
  if (!templatePath) {
2916
3002
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
2917
3003
  }
2918
- const template = fs13.readFileSync(templatePath, "utf-8");
3004
+ const template = fs14.readFileSync(templatePath, "utf-8");
2919
3005
  const tokens = {
2920
3006
  ...stringifyAll(ctx.args, "args."),
2921
3007
  ...stringifyAll(ctx.data, ""),
@@ -2993,8 +3079,8 @@ function formatToolsUsage(profile) {
2993
3079
 
2994
3080
  // src/scripts/createQaGoal.ts
2995
3081
  import { execFileSync as execFileSync10 } from "child_process";
2996
- import * as fs14 from "fs";
2997
- import * as path14 from "path";
3082
+ import * as fs15 from "fs";
3083
+ import * as path15 from "path";
2998
3084
 
2999
3085
  // src/scripts/postReviewResult.ts
3000
3086
  function detectVerdict(body) {
@@ -3246,8 +3332,8 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3246
3332
  return { number: Number(m[1]), created: true };
3247
3333
  }
3248
3334
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3249
- const dir = path14.join(cwd, ".kody", "goals", goalId);
3250
- fs14.mkdirSync(dir, { recursive: true });
3335
+ const dir = path15.join(cwd, ".kody", "goals", goalId);
3336
+ fs15.mkdirSync(dir, { recursive: true });
3251
3337
  const state = {
3252
3338
  version: 1,
3253
3339
  state: "active",
@@ -3255,8 +3341,8 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3255
3341
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3256
3342
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
3257
3343
  };
3258
- const filePath = path14.join(dir, "state.json");
3259
- fs14.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3344
+ const filePath = path15.join(dir, "state.json");
3345
+ fs15.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3260
3346
  `);
3261
3347
  return filePath;
3262
3348
  }
@@ -3667,6 +3753,14 @@ function markPrReady(prNumber, cwd) {
3667
3753
  return fail(err);
3668
3754
  }
3669
3755
  }
3756
+ function fetchDefaultBranch(cwd) {
3757
+ try {
3758
+ const out = gh(["api", "repos/{owner}/{repo}", "--jq", ".default_branch"], { cwd });
3759
+ return { ok: true, value: out.trim() };
3760
+ } catch (err) {
3761
+ return fail(err);
3762
+ }
3763
+ }
3670
3764
 
3671
3765
  // src/goal/phase.ts
3672
3766
  function derivePhase(snap) {
@@ -3690,6 +3784,15 @@ function pickNextDispatchable(snap) {
3690
3784
  var deriveGoalPhase = async (ctx) => {
3691
3785
  const goal = ctx.data.goal;
3692
3786
  if (!goal) return;
3787
+ const defaultBranchResult = fetchDefaultBranch(ctx.cwd);
3788
+ if (defaultBranchResult.ok && defaultBranchResult.value) {
3789
+ goal.defaultBranch = defaultBranchResult.value;
3790
+ } else if (defaultBranchResult.error) {
3791
+ process.stderr.write(
3792
+ `[goal-tick] deriveGoalPhase: fetchDefaultBranch failed (${defaultBranchResult.error}); falling back to ${goal.defaultBranch}
3793
+ `
3794
+ );
3795
+ }
3693
3796
  const issues = listGoalIssues(goal.id, ctx.cwd);
3694
3797
  if (!issues.ok) {
3695
3798
  process.stderr.write(`[goal-tick] deriveGoalPhase: list issues failed: ${issues.error}
@@ -3738,15 +3841,15 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
3738
3841
 
3739
3842
  // src/scripts/diagMcp.ts
3740
3843
  import { execFileSync as execFileSync11 } from "child_process";
3741
- import * as fs15 from "fs";
3844
+ import * as fs16 from "fs";
3742
3845
  import * as os3 from "os";
3743
- import * as path15 from "path";
3846
+ import * as path16 from "path";
3744
3847
  var diagMcp = async (_ctx) => {
3745
3848
  const home = os3.homedir();
3746
- const cacheDir = path15.join(home, ".cache", "ms-playwright");
3849
+ const cacheDir = path16.join(home, ".cache", "ms-playwright");
3747
3850
  let entries = [];
3748
3851
  try {
3749
- entries = fs15.readdirSync(cacheDir);
3852
+ entries = fs16.readdirSync(cacheDir);
3750
3853
  } catch {
3751
3854
  }
3752
3855
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -3772,17 +3875,17 @@ var diagMcp = async (_ctx) => {
3772
3875
  };
3773
3876
 
3774
3877
  // src/scripts/discoverQaContext.ts
3775
- import * as fs17 from "fs";
3776
- import * as path17 from "path";
3878
+ import * as fs18 from "fs";
3879
+ import * as path18 from "path";
3777
3880
 
3778
3881
  // src/scripts/frameworkDetectors.ts
3779
- import * as fs16 from "fs";
3780
- import * as path16 from "path";
3882
+ import * as fs17 from "fs";
3883
+ import * as path17 from "path";
3781
3884
  function detectFrameworks(cwd) {
3782
3885
  const out = [];
3783
3886
  let deps = {};
3784
3887
  try {
3785
- const pkg = JSON.parse(fs16.readFileSync(path16.join(cwd, "package.json"), "utf-8"));
3888
+ const pkg = JSON.parse(fs17.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
3786
3889
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
3787
3890
  } catch {
3788
3891
  return out;
@@ -3819,7 +3922,7 @@ function detectFrameworks(cwd) {
3819
3922
  }
3820
3923
  function findFile(cwd, candidates) {
3821
3924
  for (const c of candidates) {
3822
- if (fs16.existsSync(path16.join(cwd, c))) return c;
3925
+ if (fs17.existsSync(path17.join(cwd, c))) return c;
3823
3926
  }
3824
3927
  return null;
3825
3928
  }
@@ -3832,18 +3935,18 @@ var COLLECTION_DIRS = [
3832
3935
  function discoverPayloadCollections(cwd) {
3833
3936
  const out = [];
3834
3937
  for (const dir of COLLECTION_DIRS) {
3835
- const full = path16.join(cwd, dir);
3836
- if (!fs16.existsSync(full)) continue;
3938
+ const full = path17.join(cwd, dir);
3939
+ if (!fs17.existsSync(full)) continue;
3837
3940
  let files;
3838
3941
  try {
3839
- files = fs16.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
3942
+ files = fs17.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
3840
3943
  } catch {
3841
3944
  continue;
3842
3945
  }
3843
3946
  for (const file of files) {
3844
3947
  try {
3845
- const filePath = path16.join(full, file);
3846
- const content = fs16.readFileSync(filePath, "utf-8").slice(0, 1e4);
3948
+ const filePath = path17.join(full, file);
3949
+ const content = fs17.readFileSync(filePath, "utf-8").slice(0, 1e4);
3847
3950
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
3848
3951
  if (!slugMatch) continue;
3849
3952
  const slug = slugMatch[1];
@@ -3857,7 +3960,7 @@ function discoverPayloadCollections(cwd) {
3857
3960
  out.push({
3858
3961
  name,
3859
3962
  slug,
3860
- filePath: path16.relative(cwd, filePath),
3963
+ filePath: path17.relative(cwd, filePath),
3861
3964
  fields: fields.slice(0, 20),
3862
3965
  hasAdmin
3863
3966
  });
@@ -3871,28 +3974,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
3871
3974
  function discoverAdminComponents(cwd, collections) {
3872
3975
  const out = [];
3873
3976
  for (const dir of ADMIN_COMPONENT_DIRS) {
3874
- const full = path16.join(cwd, dir);
3875
- if (!fs16.existsSync(full)) continue;
3977
+ const full = path17.join(cwd, dir);
3978
+ if (!fs17.existsSync(full)) continue;
3876
3979
  let entries;
3877
3980
  try {
3878
- entries = fs16.readdirSync(full, { withFileTypes: true });
3981
+ entries = fs17.readdirSync(full, { withFileTypes: true });
3879
3982
  } catch {
3880
3983
  continue;
3881
3984
  }
3882
3985
  for (const entry of entries) {
3883
- const entryPath = path16.join(full, entry.name);
3986
+ const entryPath = path17.join(full, entry.name);
3884
3987
  let name;
3885
3988
  let filePath;
3886
3989
  if (entry.isDirectory()) {
3887
3990
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
3888
- (f) => fs16.existsSync(path16.join(entryPath, f))
3991
+ (f) => fs17.existsSync(path17.join(entryPath, f))
3889
3992
  );
3890
3993
  if (!indexFile) continue;
3891
3994
  name = entry.name;
3892
- filePath = path16.relative(cwd, path16.join(entryPath, indexFile));
3995
+ filePath = path17.relative(cwd, path17.join(entryPath, indexFile));
3893
3996
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
3894
3997
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
3895
- filePath = path16.relative(cwd, entryPath);
3998
+ filePath = path17.relative(cwd, entryPath);
3896
3999
  } else {
3897
4000
  continue;
3898
4001
  }
@@ -3900,7 +4003,7 @@ function discoverAdminComponents(cwd, collections) {
3900
4003
  if (collections) {
3901
4004
  for (const col of collections) {
3902
4005
  try {
3903
- const colContent = fs16.readFileSync(path16.join(cwd, col.filePath), "utf-8");
4006
+ const colContent = fs17.readFileSync(path17.join(cwd, col.filePath), "utf-8");
3904
4007
  if (colContent.includes(name)) {
3905
4008
  usedInCollection = col.slug;
3906
4009
  break;
@@ -3919,8 +4022,8 @@ function scanApiRoutes(cwd) {
3919
4022
  const out = [];
3920
4023
  const appDirs = ["src/app", "app"];
3921
4024
  for (const appDir of appDirs) {
3922
- const apiDir = path16.join(cwd, appDir, "api");
3923
- if (!fs16.existsSync(apiDir)) continue;
4025
+ const apiDir = path17.join(cwd, appDir, "api");
4026
+ if (!fs17.existsSync(apiDir)) continue;
3924
4027
  walkApiRoutes(apiDir, "/api", cwd, out);
3925
4028
  break;
3926
4029
  }
@@ -3929,14 +4032,14 @@ function scanApiRoutes(cwd) {
3929
4032
  function walkApiRoutes(dir, prefix, cwd, out) {
3930
4033
  let entries;
3931
4034
  try {
3932
- entries = fs16.readdirSync(dir, { withFileTypes: true });
4035
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
3933
4036
  } catch {
3934
4037
  return;
3935
4038
  }
3936
4039
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
3937
4040
  if (routeFile) {
3938
4041
  try {
3939
- const content = fs16.readFileSync(path16.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
4042
+ const content = fs17.readFileSync(path17.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
3940
4043
  const methods = HTTP_METHODS.filter(
3941
4044
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
3942
4045
  );
@@ -3944,7 +4047,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3944
4047
  out.push({
3945
4048
  path: prefix,
3946
4049
  methods,
3947
- filePath: path16.relative(cwd, path16.join(dir, routeFile.name))
4050
+ filePath: path17.relative(cwd, path17.join(dir, routeFile.name))
3948
4051
  });
3949
4052
  }
3950
4053
  } catch {
@@ -3955,7 +4058,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3955
4058
  if (entry.name === "node_modules" || entry.name === ".next") continue;
3956
4059
  let segment = entry.name;
3957
4060
  if (segment.startsWith("(") && segment.endsWith(")")) {
3958
- walkApiRoutes(path16.join(dir, entry.name), prefix, cwd, out);
4061
+ walkApiRoutes(path17.join(dir, entry.name), prefix, cwd, out);
3959
4062
  continue;
3960
4063
  }
3961
4064
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -3963,7 +4066,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3963
4066
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
3964
4067
  segment = `:${segment.slice(1, -1)}`;
3965
4068
  }
3966
- walkApiRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
4069
+ walkApiRoutes(path17.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
3967
4070
  }
3968
4071
  }
3969
4072
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -3983,10 +4086,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
3983
4086
  function scanEnvVars(cwd) {
3984
4087
  const candidates = [".env.example", ".env.local.example", ".env.template"];
3985
4088
  for (const envFile of candidates) {
3986
- const envPath = path16.join(cwd, envFile);
3987
- if (!fs16.existsSync(envPath)) continue;
4089
+ const envPath = path17.join(cwd, envFile);
4090
+ if (!fs17.existsSync(envPath)) continue;
3988
4091
  try {
3989
- const content = fs16.readFileSync(envPath, "utf-8");
4092
+ const content = fs17.readFileSync(envPath, "utf-8");
3990
4093
  const vars = [];
3991
4094
  for (const line of content.split("\n")) {
3992
4095
  const trimmed = line.trim();
@@ -4034,9 +4137,9 @@ function runQaDiscovery(cwd) {
4034
4137
  }
4035
4138
  function detectDevServer(cwd, out) {
4036
4139
  try {
4037
- const pkg = JSON.parse(fs17.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
4140
+ const pkg = JSON.parse(fs18.readFileSync(path18.join(cwd, "package.json"), "utf-8"));
4038
4141
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
4039
- const pm = fs17.existsSync(path17.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs17.existsSync(path17.join(cwd, "yarn.lock")) ? "yarn" : fs17.existsSync(path17.join(cwd, "bun.lockb")) ? "bun" : "npm";
4142
+ const pm = fs18.existsSync(path18.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs18.existsSync(path18.join(cwd, "yarn.lock")) ? "yarn" : fs18.existsSync(path18.join(cwd, "bun.lockb")) ? "bun" : "npm";
4040
4143
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
4041
4144
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
4042
4145
  else if (allDeps.vite) out.devPort = 5173;
@@ -4046,8 +4149,8 @@ function detectDevServer(cwd, out) {
4046
4149
  function scanFrontendRoutes(cwd, out) {
4047
4150
  const appDirs = ["src/app", "app"];
4048
4151
  for (const appDir of appDirs) {
4049
- const full = path17.join(cwd, appDir);
4050
- if (!fs17.existsSync(full)) continue;
4152
+ const full = path18.join(cwd, appDir);
4153
+ if (!fs18.existsSync(full)) continue;
4051
4154
  walkFrontendRoutes(full, "", out);
4052
4155
  break;
4053
4156
  }
@@ -4055,7 +4158,7 @@ function scanFrontendRoutes(cwd, out) {
4055
4158
  function walkFrontendRoutes(dir, prefix, out) {
4056
4159
  let entries;
4057
4160
  try {
4058
- entries = fs17.readdirSync(dir, { withFileTypes: true });
4161
+ entries = fs18.readdirSync(dir, { withFileTypes: true });
4059
4162
  } catch {
4060
4163
  return;
4061
4164
  }
@@ -4072,7 +4175,7 @@ function walkFrontendRoutes(dir, prefix, out) {
4072
4175
  if (entry.name === "node_modules" || entry.name === ".next") continue;
4073
4176
  let segment = entry.name;
4074
4177
  if (segment.startsWith("(") && segment.endsWith(")")) {
4075
- walkFrontendRoutes(path17.join(dir, entry.name), prefix, out);
4178
+ walkFrontendRoutes(path18.join(dir, entry.name), prefix, out);
4076
4179
  continue;
4077
4180
  }
4078
4181
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -4080,7 +4183,7 @@ function walkFrontendRoutes(dir, prefix, out) {
4080
4183
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
4081
4184
  segment = `:${segment.slice(1, -1)}`;
4082
4185
  }
4083
- walkFrontendRoutes(path17.join(dir, entry.name), `${prefix}/${segment}`, out);
4186
+ walkFrontendRoutes(path18.join(dir, entry.name), `${prefix}/${segment}`, out);
4084
4187
  }
4085
4188
  }
4086
4189
  function detectAuthFiles(cwd, out) {
@@ -4097,23 +4200,23 @@ function detectAuthFiles(cwd, out) {
4097
4200
  "src/app/api/oauth"
4098
4201
  ];
4099
4202
  for (const c of candidates) {
4100
- if (fs17.existsSync(path17.join(cwd, c))) out.authFiles.push(c);
4203
+ if (fs18.existsSync(path18.join(cwd, c))) out.authFiles.push(c);
4101
4204
  }
4102
4205
  }
4103
4206
  function detectRoles(cwd, out) {
4104
4207
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
4105
4208
  for (const rp of rolePaths) {
4106
- const dir = path17.join(cwd, rp);
4107
- if (!fs17.existsSync(dir)) continue;
4209
+ const dir = path18.join(cwd, rp);
4210
+ if (!fs18.existsSync(dir)) continue;
4108
4211
  let files;
4109
4212
  try {
4110
- files = fs17.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4213
+ files = fs18.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
4111
4214
  } catch {
4112
4215
  continue;
4113
4216
  }
4114
4217
  for (const f of files) {
4115
4218
  try {
4116
- const content = fs17.readFileSync(path17.join(dir, f), "utf-8").slice(0, 5e3);
4219
+ const content = fs18.readFileSync(path18.join(dir, f), "utf-8").slice(0, 5e3);
4117
4220
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
4118
4221
  if (roleMatches) {
4119
4222
  for (const m of roleMatches) {
@@ -4344,8 +4447,8 @@ function failedAction3(reason) {
4344
4447
  }
4345
4448
 
4346
4449
  // src/scripts/dispatchJobFileTicks.ts
4347
- import * as fs19 from "fs";
4348
- import * as path19 from "path";
4450
+ import * as fs20 from "fs";
4451
+ import * as path20 from "path";
4349
4452
 
4350
4453
  // src/scripts/jobFrontmatter.ts
4351
4454
  var SCHEDULE_EVERY_VALUES = [
@@ -4596,8 +4699,8 @@ var ContentsApiBackend = class {
4596
4699
  };
4597
4700
 
4598
4701
  // src/scripts/jobState/localFileBackend.ts
4599
- import * as fs18 from "fs";
4600
- import * as path18 from "path";
4702
+ import * as fs19 from "fs";
4703
+ import * as path19 from "path";
4601
4704
  var LocalFileBackend = class {
4602
4705
  name = "local-file";
4603
4706
  cwd;
@@ -4612,7 +4715,7 @@ var LocalFileBackend = class {
4612
4715
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
4613
4716
  this.cwd = opts.cwd;
4614
4717
  this.jobsDir = opts.jobsDir;
4615
- this.absDir = path18.join(opts.cwd, opts.jobsDir);
4718
+ this.absDir = path19.join(opts.cwd, opts.jobsDir);
4616
4719
  this.owner = opts.owner;
4617
4720
  this.repo = opts.repo;
4618
4721
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -4627,7 +4730,7 @@ var LocalFileBackend = class {
4627
4730
  `);
4628
4731
  return;
4629
4732
  }
4630
- fs18.mkdirSync(this.absDir, { recursive: true });
4733
+ fs19.mkdirSync(this.absDir, { recursive: true });
4631
4734
  const prefix = this.cacheKeyPrefix();
4632
4735
  const probeKey = `${prefix}probe-${Date.now()}`;
4633
4736
  try {
@@ -4656,7 +4759,7 @@ var LocalFileBackend = class {
4656
4759
  `);
4657
4760
  return;
4658
4761
  }
4659
- if (!fs18.existsSync(this.absDir)) {
4762
+ if (!fs19.existsSync(this.absDir)) {
4660
4763
  return;
4661
4764
  }
4662
4765
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -4672,11 +4775,11 @@ var LocalFileBackend = class {
4672
4775
  }
4673
4776
  load(slug) {
4674
4777
  const relPath = stateFilePath(this.jobsDir, slug);
4675
- const absPath = path18.join(this.cwd, relPath);
4676
- if (!fs18.existsSync(absPath)) {
4778
+ const absPath = path19.join(this.cwd, relPath);
4779
+ if (!fs19.existsSync(absPath)) {
4677
4780
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
4678
4781
  }
4679
- const raw = fs18.readFileSync(absPath, "utf-8");
4782
+ const raw = fs19.readFileSync(absPath, "utf-8");
4680
4783
  let parsed;
4681
4784
  try {
4682
4785
  parsed = JSON.parse(raw);
@@ -4693,10 +4796,10 @@ var LocalFileBackend = class {
4693
4796
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
4694
4797
  return false;
4695
4798
  }
4696
- const absPath = path18.join(this.cwd, loaded.path);
4697
- fs18.mkdirSync(path18.dirname(absPath), { recursive: true });
4799
+ const absPath = path19.join(this.cwd, loaded.path);
4800
+ fs19.mkdirSync(path19.dirname(absPath), { recursive: true });
4698
4801
  const body = JSON.stringify(next, null, 2) + "\n";
4699
- fs18.writeFileSync(absPath, body, "utf-8");
4802
+ fs19.writeFileSync(absPath, body, "utf-8");
4700
4803
  return true;
4701
4804
  }
4702
4805
  cacheKeyPrefix() {
@@ -4773,7 +4876,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4773
4876
  await backend.hydrate();
4774
4877
  }
4775
4878
  try {
4776
- const slugs = listJobSlugs(path19.join(ctx.cwd, jobsDir));
4879
+ const slugs = listJobSlugs(path20.join(ctx.cwd, jobsDir));
4777
4880
  ctx.data.jobSlugCount = slugs.length;
4778
4881
  if (slugs.length === 0) {
4779
4882
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -4872,17 +4975,17 @@ function formatAgo(ms) {
4872
4975
  }
4873
4976
  function readJobFrontmatter(cwd, jobsDir, slug) {
4874
4977
  try {
4875
- const raw = fs19.readFileSync(path19.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4978
+ const raw = fs20.readFileSync(path20.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4876
4979
  return splitFrontmatter(raw).frontmatter;
4877
4980
  } catch {
4878
4981
  return {};
4879
4982
  }
4880
4983
  }
4881
4984
  function listJobSlugs(absDir) {
4882
- if (!fs19.existsSync(absDir)) return [];
4985
+ if (!fs20.existsSync(absDir)) return [];
4883
4986
  let entries;
4884
4987
  try {
4885
- entries = fs19.readdirSync(absDir, { withFileTypes: true });
4988
+ entries = fs20.readdirSync(absDir, { withFileTypes: true });
4886
4989
  } catch {
4887
4990
  return [];
4888
4991
  }
@@ -5483,7 +5586,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5483
5586
 
5484
5587
  // src/gha.ts
5485
5588
  import { execFileSync as execFileSync16 } from "child_process";
5486
- import * as fs20 from "fs";
5589
+ import * as fs21 from "fs";
5487
5590
  function getRunUrl() {
5488
5591
  const server = process.env.GITHUB_SERVER_URL;
5489
5592
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5494,10 +5597,10 @@ function getRunUrl() {
5494
5597
  function reactToTriggerComment(cwd) {
5495
5598
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5496
5599
  const eventPath = process.env.GITHUB_EVENT_PATH;
5497
- if (!eventPath || !fs20.existsSync(eventPath)) return;
5600
+ if (!eventPath || !fs21.existsSync(eventPath)) return;
5498
5601
  let event = null;
5499
5602
  try {
5500
- event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5603
+ event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5501
5604
  } catch {
5502
5605
  return;
5503
5606
  }
@@ -5782,22 +5885,22 @@ var handleAbandonedGoal = async (ctx) => {
5782
5885
 
5783
5886
  // src/scripts/initFlow.ts
5784
5887
  import { execFileSync as execFileSync18 } from "child_process";
5785
- import * as fs22 from "fs";
5786
- import * as path21 from "path";
5888
+ import * as fs23 from "fs";
5889
+ import * as path22 from "path";
5787
5890
 
5788
5891
  // src/scripts/loadQaGuide.ts
5789
- import * as fs21 from "fs";
5790
- import * as path20 from "path";
5892
+ import * as fs22 from "fs";
5893
+ import * as path21 from "path";
5791
5894
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
5792
5895
  var loadQaGuide = async (ctx) => {
5793
- const full = path20.join(ctx.cwd, QA_GUIDE_REL_PATH);
5794
- if (!fs21.existsSync(full)) {
5896
+ const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
5897
+ if (!fs22.existsSync(full)) {
5795
5898
  ctx.data.qaGuide = "";
5796
5899
  ctx.data.qaGuidePath = "";
5797
5900
  return;
5798
5901
  }
5799
5902
  try {
5800
- ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
5903
+ ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
5801
5904
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
5802
5905
  } catch {
5803
5906
  ctx.data.qaGuide = "";
@@ -5807,9 +5910,9 @@ var loadQaGuide = async (ctx) => {
5807
5910
 
5808
5911
  // src/scripts/initFlow.ts
5809
5912
  function detectPackageManager(cwd) {
5810
- if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5811
- if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) return "yarn";
5812
- if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) return "bun";
5913
+ if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5914
+ if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
5915
+ if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
5813
5916
  return "npm";
5814
5917
  }
5815
5918
  function qualityCommandsFor(pm) {
@@ -5931,48 +6034,48 @@ function performInit(cwd, force) {
5931
6034
  const pm = detectPackageManager(cwd);
5932
6035
  const ownerRepo = detectOwnerRepo(cwd);
5933
6036
  const defaultBranch = defaultBranchFromGit(cwd);
5934
- const configPath = path21.join(cwd, "kody.config.json");
5935
- if (fs22.existsSync(configPath) && !force) {
6037
+ const configPath = path22.join(cwd, "kody.config.json");
6038
+ if (fs23.existsSync(configPath) && !force) {
5936
6039
  skipped.push("kody.config.json");
5937
6040
  } else {
5938
6041
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
5939
- fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6042
+ fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5940
6043
  `);
5941
6044
  wrote.push("kody.config.json");
5942
6045
  }
5943
- const workflowDir = path21.join(cwd, ".github", "workflows");
5944
- const workflowPath = path21.join(workflowDir, "kody.yml");
5945
- if (fs22.existsSync(workflowPath) && !force) {
6046
+ const workflowDir = path22.join(cwd, ".github", "workflows");
6047
+ const workflowPath = path22.join(workflowDir, "kody.yml");
6048
+ if (fs23.existsSync(workflowPath) && !force) {
5946
6049
  skipped.push(".github/workflows/kody.yml");
5947
6050
  } else {
5948
- fs22.mkdirSync(workflowDir, { recursive: true });
5949
- fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6051
+ fs23.mkdirSync(workflowDir, { recursive: true });
6052
+ fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5950
6053
  wrote.push(".github/workflows/kody.yml");
5951
6054
  }
5952
- const hasUi = fs22.existsSync(path21.join(cwd, "src/app")) || fs22.existsSync(path21.join(cwd, "app")) || fs22.existsSync(path21.join(cwd, "pages"));
6055
+ const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
5953
6056
  if (hasUi) {
5954
- const qaGuidePath = path21.join(cwd, QA_GUIDE_REL_PATH);
5955
- if (fs22.existsSync(qaGuidePath) && !force) {
6057
+ const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6058
+ if (fs23.existsSync(qaGuidePath) && !force) {
5956
6059
  skipped.push(QA_GUIDE_REL_PATH);
5957
6060
  } else {
5958
- fs22.mkdirSync(path21.dirname(qaGuidePath), { recursive: true });
6061
+ fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
5959
6062
  const discovery = runQaDiscovery(cwd);
5960
- fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6063
+ fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5961
6064
  wrote.push(QA_GUIDE_REL_PATH);
5962
6065
  }
5963
6066
  }
5964
6067
  const builtinJobs = listBuiltinJobs();
5965
6068
  if (builtinJobs.length > 0) {
5966
- const jobsDir = path21.join(cwd, ".kody", "jobs");
5967
- fs22.mkdirSync(jobsDir, { recursive: true });
6069
+ const jobsDir = path22.join(cwd, ".kody", "jobs");
6070
+ fs23.mkdirSync(jobsDir, { recursive: true });
5968
6071
  for (const job of builtinJobs) {
5969
- const rel = path21.join(".kody", "jobs", `${job.slug}.md`);
5970
- const target = path21.join(cwd, rel);
5971
- if (fs22.existsSync(target) && !force) {
6072
+ const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6073
+ const target = path22.join(cwd, rel);
6074
+ if (fs23.existsSync(target) && !force) {
5972
6075
  skipped.push(rel);
5973
6076
  continue;
5974
6077
  }
5975
- fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
6078
+ fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
5976
6079
  wrote.push(rel);
5977
6080
  }
5978
6081
  }
@@ -5984,12 +6087,12 @@ function performInit(cwd, force) {
5984
6087
  continue;
5985
6088
  }
5986
6089
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
5987
- const target = path21.join(workflowDir, `kody-${exe.name}.yml`);
5988
- if (fs22.existsSync(target) && !force) {
6090
+ const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6091
+ if (fs23.existsSync(target) && !force) {
5989
6092
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
5990
6093
  continue;
5991
6094
  }
5992
- fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6095
+ fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5993
6096
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
5994
6097
  }
5995
6098
  let labels;
@@ -6078,14 +6181,14 @@ var loadCoverageRules = async (ctx) => {
6078
6181
  };
6079
6182
 
6080
6183
  // src/goal/state.ts
6081
- import * as fs23 from "fs";
6082
- import * as path22 from "path";
6184
+ import * as fs24 from "fs";
6185
+ import * as path23 from "path";
6083
6186
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
6084
6187
  var GoalStateError = class extends Error {
6085
- constructor(path29, message) {
6086
- super(`Invalid goal state at ${path29}:
6188
+ constructor(path30, message) {
6189
+ super(`Invalid goal state at ${path30}:
6087
6190
  ${message}`);
6088
- this.path = path29;
6191
+ this.path = path30;
6089
6192
  this.name = "GoalStateError";
6090
6193
  }
6091
6194
  path;
@@ -6129,16 +6232,16 @@ function serializeGoalState(s) {
6129
6232
  `;
6130
6233
  }
6131
6234
  function goalStatePath(cwd, goalId) {
6132
- return path22.join(cwd, ".kody", "goals", goalId, "state.json");
6235
+ return path23.join(cwd, ".kody", "goals", goalId, "state.json");
6133
6236
  }
6134
6237
  function readGoalState(cwd, goalId) {
6135
6238
  const file = goalStatePath(cwd, goalId);
6136
- if (!fs23.existsSync(file)) {
6239
+ if (!fs24.existsSync(file)) {
6137
6240
  throw new GoalStateError(file, "file not found");
6138
6241
  }
6139
6242
  let raw;
6140
6243
  try {
6141
- raw = JSON.parse(fs23.readFileSync(file, "utf-8"));
6244
+ raw = JSON.parse(fs24.readFileSync(file, "utf-8"));
6142
6245
  } catch (err) {
6143
6246
  throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
6144
6247
  }
@@ -6146,8 +6249,8 @@ function readGoalState(cwd, goalId) {
6146
6249
  }
6147
6250
  function writeGoalState(cwd, goalId, state) {
6148
6251
  const file = goalStatePath(cwd, goalId);
6149
- fs23.mkdirSync(path22.dirname(file), { recursive: true });
6150
- fs23.writeFileSync(file, serializeGoalState(state), "utf-8");
6252
+ fs24.mkdirSync(path23.dirname(file), { recursive: true });
6253
+ fs24.writeFileSync(file, serializeGoalState(state), "utf-8");
6151
6254
  }
6152
6255
  function nowIso() {
6153
6256
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -6241,8 +6344,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
6241
6344
  };
6242
6345
 
6243
6346
  // src/scripts/loadJobFromFile.ts
6244
- import * as fs24 from "fs";
6245
- import * as path23 from "path";
6347
+ import * as fs25 from "fs";
6348
+ import * as path24 from "path";
6246
6349
  var loadJobFromFile = async (ctx, _profile, args) => {
6247
6350
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
6248
6351
  const slugArg = String(args?.slugArg ?? "job");
@@ -6250,11 +6353,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
6250
6353
  if (!slug) {
6251
6354
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
6252
6355
  }
6253
- const absPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
6254
- if (!fs24.existsSync(absPath)) {
6356
+ const absPath = path24.join(ctx.cwd, jobsDir, `${slug}.md`);
6357
+ if (!fs25.existsSync(absPath)) {
6255
6358
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
6256
6359
  }
6257
- const raw = fs24.readFileSync(absPath, "utf-8");
6360
+ const raw = fs25.readFileSync(absPath, "utf-8");
6258
6361
  const { title, body } = parseJobFile(raw, slug);
6259
6362
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
6260
6363
  const loaded = await backend.load(slug);
@@ -6286,16 +6389,16 @@ function humanizeSlug(slug) {
6286
6389
  }
6287
6390
 
6288
6391
  // src/scripts/loadMemoryContext.ts
6289
- import * as fs25 from "fs";
6290
- import * as path24 from "path";
6392
+ import * as fs26 from "fs";
6393
+ import * as path25 from "path";
6291
6394
  var MEMORY_DIR_RELATIVE = ".kody/memory";
6292
6395
  var MAX_PAGES = 8;
6293
6396
  var PER_PAGE_MAX_BYTES = 4e3;
6294
6397
  var TOTAL_MAX_BYTES = 24e3;
6295
6398
  var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
6296
6399
  var loadMemoryContext = async (ctx) => {
6297
- const memoryAbs = path24.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6298
- if (!fs25.existsSync(memoryAbs)) {
6400
+ const memoryAbs = path25.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6401
+ if (!fs26.existsSync(memoryAbs)) {
6299
6402
  ctx.data.memoryContext = "";
6300
6403
  return;
6301
6404
  }
@@ -6320,21 +6423,21 @@ function collectPages(memoryAbs) {
6320
6423
  walkMd(memoryAbs, (file) => {
6321
6424
  let stat;
6322
6425
  try {
6323
- stat = fs25.statSync(file);
6426
+ stat = fs26.statSync(file);
6324
6427
  } catch {
6325
6428
  return;
6326
6429
  }
6327
6430
  let raw;
6328
6431
  try {
6329
- raw = fs25.readFileSync(file, "utf-8");
6432
+ raw = fs26.readFileSync(file, "utf-8");
6330
6433
  } catch {
6331
6434
  return;
6332
6435
  }
6333
6436
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
6334
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path24.basename(file, ".md");
6437
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path25.basename(file, ".md");
6335
6438
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
6336
6439
  out.push({
6337
- relPath: path24.relative(memoryAbs, file),
6440
+ relPath: path25.relative(memoryAbs, file),
6338
6441
  title,
6339
6442
  updated,
6340
6443
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -6402,16 +6505,16 @@ function walkMd(root, visit) {
6402
6505
  const dir = stack.pop();
6403
6506
  let names;
6404
6507
  try {
6405
- names = fs25.readdirSync(dir);
6508
+ names = fs26.readdirSync(dir);
6406
6509
  } catch {
6407
6510
  continue;
6408
6511
  }
6409
6512
  for (const name of names) {
6410
6513
  if (name.startsWith(".")) continue;
6411
- const full = path24.join(dir, name);
6514
+ const full = path25.join(dir, name);
6412
6515
  let stat;
6413
6516
  try {
6414
- stat = fs25.statSync(full);
6517
+ stat = fs26.statSync(full);
6415
6518
  } catch {
6416
6519
  continue;
6417
6520
  }
@@ -7249,30 +7352,21 @@ var recordOutcome = async (ctx, profile) => {
7249
7352
 
7250
7353
  // src/scripts/requireFeedbackActions.ts
7251
7354
  var MIN_ITEMS = 1;
7252
- var requireFeedbackActions = async (ctx, profile) => {
7355
+ var requireFeedbackActions = async (ctx) => {
7253
7356
  if (!ctx.data.agentDone) return;
7254
7357
  const actions = String(ctx.data.feedbackActions ?? "").trim();
7255
7358
  const items = countActionItems(actions);
7256
7359
  ctx.data.feedbackAgentItemCount = items;
7257
7360
  if (items < MIN_ITEMS) {
7258
- fail2(
7259
- ctx,
7260
- profile,
7261
- actions.length === 0 ? "agent omitted required FEEDBACK_ACTIONS block \u2014 cannot verify that review feedback was addressed" : "agent FEEDBACK_ACTIONS block listed no items \u2014 cannot verify that review feedback was addressed"
7361
+ const reason = actions.length === 0 ? "FEEDBACK_ACTIONS block missing" : "FEEDBACK_ACTIONS block listed no items";
7362
+ process.stderr.write(
7363
+ `[kody requireFeedbackActions] warning: ${reason} \u2014 proceeding anyway (verifyFixAlignment + tests are the real gate)
7364
+ `
7262
7365
  );
7366
+ ctx.data.feedbackActionsOmitted = actions.length === 0;
7367
+ ctx.data.feedbackActionsMalformed = actions.length > 0;
7263
7368
  }
7264
7369
  };
7265
- function fail2(ctx, profile, reason) {
7266
- ctx.data.agentDone = false;
7267
- ctx.data.agentFailureReason = reason;
7268
- const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
7269
- const failedAction6 = {
7270
- type: `${modeSeg}_FAILED`,
7271
- payload: { reason },
7272
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
7273
- };
7274
- ctx.data.action = failedAction6;
7275
- }
7276
7370
  function countActionItems(block) {
7277
7371
  if (!block.trim()) return 0;
7278
7372
  let count = 0;
@@ -7829,8 +7923,8 @@ function resolveBaseOverride(value) {
7829
7923
 
7830
7924
  // src/scripts/runTickScript.ts
7831
7925
  import { spawnSync } from "child_process";
7832
- import * as fs26 from "fs";
7833
- import * as path25 from "path";
7926
+ import * as fs27 from "fs";
7927
+ import * as path26 from "path";
7834
7928
  var runTickScript = async (ctx, _profile, args) => {
7835
7929
  ctx.skipAgent = true;
7836
7930
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -7842,13 +7936,13 @@ var runTickScript = async (ctx, _profile, args) => {
7842
7936
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7843
7937
  return;
7844
7938
  }
7845
- const jobPath = path25.join(ctx.cwd, jobsDir, `${slug}.md`);
7846
- if (!fs26.existsSync(jobPath)) {
7939
+ const jobPath = path26.join(ctx.cwd, jobsDir, `${slug}.md`);
7940
+ if (!fs27.existsSync(jobPath)) {
7847
7941
  ctx.output.exitCode = 99;
7848
7942
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7849
7943
  return;
7850
7944
  }
7851
- const raw = fs26.readFileSync(jobPath, "utf-8");
7945
+ const raw = fs27.readFileSync(jobPath, "utf-8");
7852
7946
  const { frontmatter } = splitFrontmatter(raw);
7853
7947
  const tickScript = frontmatter.tickScript;
7854
7948
  if (!tickScript) {
@@ -7856,8 +7950,8 @@ var runTickScript = async (ctx, _profile, args) => {
7856
7950
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7857
7951
  return;
7858
7952
  }
7859
- const scriptPath = path25.isAbsolute(tickScript) ? tickScript : path25.join(ctx.cwd, tickScript);
7860
- if (!fs26.existsSync(scriptPath)) {
7953
+ const scriptPath = path26.isAbsolute(tickScript) ? tickScript : path26.join(ctx.cwd, tickScript);
7954
+ if (!fs27.existsSync(scriptPath)) {
7861
7955
  ctx.output.exitCode = 99;
7862
7956
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7863
7957
  return;
@@ -8794,7 +8888,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
8794
8888
  };
8795
8889
 
8796
8890
  // src/scripts/writeRunSummary.ts
8797
- import * as fs27 from "fs";
8891
+ import * as fs28 from "fs";
8798
8892
  var writeRunSummary = async (ctx, profile) => {
8799
8893
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
8800
8894
  if (!summaryPath) return;
@@ -8816,7 +8910,7 @@ var writeRunSummary = async (ctx, profile) => {
8816
8910
  if (reason) lines.push(`- **Reason:** ${reason}`);
8817
8911
  lines.push("");
8818
8912
  try {
8819
- fs27.appendFileSync(summaryPath, `${lines.join("\n")}
8913
+ fs28.appendFileSync(summaryPath, `${lines.join("\n")}
8820
8914
  `);
8821
8915
  } catch {
8822
8916
  }
@@ -8952,22 +9046,42 @@ function runShell(cmd, cwd, timeoutMs = 3e4) {
8952
9046
  // src/executor.ts
8953
9047
  var CONTAINER_MAX_ITERATIONS = 50;
8954
9048
  async function runExecutable(profileName, input) {
9049
+ const stageStartedAt = Date.now();
9050
+ emitEvent(input.cwd, { executable: profileName, kind: "stage_start" });
9051
+ const finishAndEnd = (out) => {
9052
+ emitEvent(input.cwd, {
9053
+ executable: profileName,
9054
+ kind: "stage_end",
9055
+ durationMs: Date.now() - stageStartedAt,
9056
+ outcome: out.exitCode === 0 ? "ok" : "failed",
9057
+ meta: {
9058
+ exitCode: out.exitCode,
9059
+ ...out.reason ? { reason: out.reason } : {},
9060
+ ...out.prUrl ? { prUrl: out.prUrl } : {}
9061
+ }
9062
+ });
9063
+ if (out.prUrl) process.stdout.write(`PR_URL=${out.prUrl}
9064
+ `);
9065
+ else if (out.reason) process.stdout.write(`PR_URL=FAILED: ${out.reason}
9066
+ `);
9067
+ return out;
9068
+ };
8955
9069
  const profilePath = resolveProfilePath(profileName);
8956
9070
  const profile = loadProfile(profilePath);
8957
9071
  const missing = validateScriptReferences(profile, allScriptNames);
8958
9072
  if (missing.length > 0) {
8959
- return finish({ exitCode: 99, reason: `profile references unknown scripts: ${missing.join(", ")}` });
9073
+ return finishAndEnd({ exitCode: 99, reason: `profile references unknown scripts: ${missing.join(", ")}` });
8960
9074
  }
8961
9075
  let args;
8962
9076
  try {
8963
9077
  args = validateInputs(profile.inputs, input.cliArgs);
8964
9078
  } catch (err) {
8965
- return finish({ exitCode: 64, reason: err instanceof Error ? err.message : String(err) });
9079
+ return finishAndEnd({ exitCode: 64, reason: err instanceof Error ? err.message : String(err) });
8966
9080
  }
8967
9081
  const toolResults = verifyCliTools(profile.cliTools, input.cwd);
8968
9082
  const firstFail = firstRequiredFailure(toolResults, profile.cliTools);
8969
9083
  if (firstFail) {
8970
- return finish({ exitCode: 99, reason: `required CLI tool check failed: ${firstFail.error}` });
9084
+ return finishAndEnd({ exitCode: 99, reason: `required CLI tool check failed: ${firstFail.error}` });
8971
9085
  }
8972
9086
  let config;
8973
9087
  if (input.config) {
@@ -8983,7 +9097,7 @@ async function runExecutable(profileName, input) {
8983
9097
  try {
8984
9098
  config = loadConfig(input.cwd);
8985
9099
  } catch (err) {
8986
- return finish({ exitCode: 99, reason: `config error: ${err instanceof Error ? err.message : String(err)}` });
9100
+ return finishAndEnd({ exitCode: 99, reason: `config error: ${err instanceof Error ? err.message : String(err)}` });
8987
9101
  }
8988
9102
  }
8989
9103
  const modelSpec = profile.claudeCode.model === "inherit" ? config.agent.model : profile.claudeCode.model;
@@ -8991,13 +9105,13 @@ async function runExecutable(profileName, input) {
8991
9105
  try {
8992
9106
  model = parseProviderModel(modelSpec);
8993
9107
  } catch (err) {
8994
- return finish({ exitCode: 99, reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}` });
9108
+ return finishAndEnd({ exitCode: 99, reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}` });
8995
9109
  }
8996
9110
  let litellm = null;
8997
9111
  try {
8998
9112
  litellm = await startLitellmIfNeeded(model, input.cwd);
8999
9113
  } catch (err) {
9000
- return finish({
9114
+ return finishAndEnd({
9001
9115
  exitCode: 99,
9002
9116
  reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}`
9003
9117
  });
@@ -9011,9 +9125,9 @@ async function runExecutable(profileName, input) {
9011
9125
  data: {},
9012
9126
  output: { exitCode: 0 }
9013
9127
  };
9014
- const ndjsonDir = path26.join(input.cwd, ".kody");
9128
+ const ndjsonDir = path27.join(input.cwd, ".kody");
9015
9129
  const invokeAgent = async (prompt) => {
9016
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path26.isAbsolute(p) ? p : path26.resolve(profile.dir, p)).filter((p) => p.length > 0);
9130
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path27.isAbsolute(p) ? p : path27.resolve(profile.dir, p)).filter((p) => p.length > 0);
9017
9131
  const syntheticPath = ctx.data.syntheticPluginPath;
9018
9132
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
9019
9133
  return runAgent({
@@ -9037,15 +9151,39 @@ async function runExecutable(profileName, input) {
9037
9151
  ctx.data.__invokeAgent = invokeAgent;
9038
9152
  try {
9039
9153
  for (const entry of profile.scripts.preflight) {
9040
- if (!shouldRun(entry, ctx)) continue;
9154
+ const preLabel = entry.script ?? entry.shell ?? "<unknown>";
9155
+ if (!shouldRun(entry, ctx)) {
9156
+ emitEvent(input.cwd, {
9157
+ executable: profileName,
9158
+ kind: "preflight",
9159
+ name: preLabel,
9160
+ outcome: "skipped"
9161
+ });
9162
+ continue;
9163
+ }
9164
+ const t0 = Date.now();
9041
9165
  if (entry.shell) {
9042
9166
  await runShellEntry(entry, ctx, profile);
9167
+ emitEvent(input.cwd, {
9168
+ executable: profileName,
9169
+ kind: "preflight",
9170
+ name: preLabel,
9171
+ durationMs: Date.now() - t0,
9172
+ outcome: ctx.output.exitCode && ctx.output.exitCode !== 0 ? "failed" : "ok"
9173
+ });
9043
9174
  } else {
9044
9175
  const fn = preflightScripts[entry.script];
9045
- if (!fn) return finish({ exitCode: 99, reason: `preflight script not registered: ${entry.script}` });
9176
+ if (!fn) return finishAndEnd({ exitCode: 99, reason: `preflight script not registered: ${entry.script}` });
9046
9177
  await fn(ctx, profile, entry.with);
9178
+ emitEvent(input.cwd, {
9179
+ executable: profileName,
9180
+ kind: "preflight",
9181
+ name: preLabel,
9182
+ durationMs: Date.now() - t0,
9183
+ outcome: ctx.skipAgent && ctx.output.exitCode && ctx.output.exitCode !== 0 ? "failed" : "ok"
9184
+ });
9047
9185
  if (ctx.skipAgent && ctx.output.exitCode !== void 0 && ctx.output.exitCode !== 0) {
9048
- return finish(ctx.output);
9186
+ return finishAndEnd(ctx.output);
9049
9187
  }
9050
9188
  }
9051
9189
  }
@@ -9056,9 +9194,21 @@ async function runExecutable(profileName, input) {
9056
9194
  } else if (!ctx.skipAgent) {
9057
9195
  const prompt = ctx.data.prompt;
9058
9196
  if (!prompt) {
9059
- return finish({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
9197
+ return finishAndEnd({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
9060
9198
  }
9199
+ emitEvent(input.cwd, { executable: profileName, kind: "agent_start" });
9061
9200
  agentResult = await invokeAgent(prompt);
9201
+ emitEvent(input.cwd, {
9202
+ executable: profileName,
9203
+ kind: "agent_end",
9204
+ durationMs: agentResult.durationMs,
9205
+ outcome: agentResult.outcome === "completed" ? "ok" : "failed",
9206
+ meta: {
9207
+ ...agentResult.tokens ? { tokens: agentResult.tokens } : {},
9208
+ ...typeof agentResult.messageCount === "number" ? { messageCount: agentResult.messageCount } : {},
9209
+ ...agentResult.error ? { error: agentResult.error } : {}
9210
+ }
9211
+ });
9062
9212
  }
9063
9213
  for (const entry of profile.scripts.postflight) {
9064
9214
  const entryLabel = entry.script ?? entry.shell ?? "<unknown>";
@@ -9073,18 +9223,27 @@ async function runExecutable(profileName, input) {
9073
9223
  process.stderr.write(`[kody postflight] skip ${entryLabel}: ${reasons.join("; ")}
9074
9224
  `);
9075
9225
  }
9226
+ emitEvent(input.cwd, {
9227
+ executable: profileName,
9228
+ kind: "postflight",
9229
+ name: entryLabel,
9230
+ outcome: "skipped"
9231
+ });
9076
9232
  continue;
9077
9233
  }
9078
9234
  const label = entryLabel;
9235
+ const t0 = Date.now();
9236
+ let postOutcome = "ok";
9079
9237
  try {
9080
9238
  if (entry.shell) {
9081
9239
  await runShellEntry(entry, ctx, profile);
9082
9240
  } else {
9083
9241
  const fn = postflightScripts[entry.script];
9084
- if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
9242
+ if (!fn) return finishAndEnd({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
9085
9243
  await fn(ctx, profile, agentResult, entry.with);
9086
9244
  }
9087
9245
  } catch (err) {
9246
+ postOutcome = "failed";
9088
9247
  const msg = err instanceof Error ? err.message : String(err);
9089
9248
  process.stderr.write(`[kody] postflight "${label}" crashed: ${msg}
9090
9249
  `);
@@ -9092,8 +9251,15 @@ async function runExecutable(profileName, input) {
9092
9251
  ctx.output.reason = ctx.output.reason ? `${ctx.output.reason}; ${summary}` : summary;
9093
9252
  if (ctx.output.exitCode === 0) ctx.output.exitCode = 99;
9094
9253
  }
9254
+ emitEvent(input.cwd, {
9255
+ executable: profileName,
9256
+ kind: "postflight",
9257
+ name: label,
9258
+ durationMs: Date.now() - t0,
9259
+ outcome: postOutcome
9260
+ });
9095
9261
  }
9096
- return finish({
9262
+ return finishAndEnd({
9097
9263
  exitCode: ctx.output.exitCode ?? 0,
9098
9264
  prUrl: ctx.output.prUrl,
9099
9265
  reason: ctx.output.reason
@@ -9122,17 +9288,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
9122
9288
  function resolveProfilePath(profileName) {
9123
9289
  const found = resolveExecutable(profileName);
9124
9290
  if (found) return found;
9125
- const here = path26.dirname(new URL(import.meta.url).pathname);
9291
+ const here = path27.dirname(new URL(import.meta.url).pathname);
9126
9292
  const candidates = [
9127
- path26.join(here, "executables", profileName, "profile.json"),
9293
+ path27.join(here, "executables", profileName, "profile.json"),
9128
9294
  // same-dir sibling (dev)
9129
- path26.join(here, "..", "executables", profileName, "profile.json"),
9295
+ path27.join(here, "..", "executables", profileName, "profile.json"),
9130
9296
  // up one (prod: dist/bin → dist/executables)
9131
- path26.join(here, "..", "src", "executables", profileName, "profile.json")
9297
+ path27.join(here, "..", "src", "executables", profileName, "profile.json")
9132
9298
  // fallback
9133
9299
  ];
9134
9300
  for (const c of candidates) {
9135
- if (fs28.existsSync(c)) return c;
9301
+ if (fs29.existsSync(c)) return c;
9136
9302
  }
9137
9303
  return candidates[0];
9138
9304
  }
@@ -9215,13 +9381,6 @@ function resolveDottedPath(root, key) {
9215
9381
  }
9216
9382
  return cur;
9217
9383
  }
9218
- function finish(out) {
9219
- if (out.prUrl) process.stdout.write(`PR_URL=${out.prUrl}
9220
- `);
9221
- else if (out.reason) process.stdout.write(`PR_URL=FAILED: ${out.reason}
9222
- `);
9223
- return out;
9224
- }
9225
9384
  var DEFAULT_SHELL_TIMEOUT_MS = 3e5;
9226
9385
  function resolveShellTimeoutMs(entry) {
9227
9386
  if (typeof entry.timeoutSec === "number" && entry.timeoutSec > 0) {
@@ -9236,8 +9395,8 @@ function resolveShellTimeoutMs(entry) {
9236
9395
  var SIGKILL_GRACE_MS = 5e3;
9237
9396
  async function runShellEntry(entry, ctx, profile) {
9238
9397
  const shellName = entry.shell;
9239
- const shellPath = path26.join(profile.dir, shellName);
9240
- if (!fs28.existsSync(shellPath)) {
9398
+ const shellPath = path27.join(profile.dir, shellName);
9399
+ if (!fs29.existsSync(shellPath)) {
9241
9400
  ctx.skipAgent = true;
9242
9401
  ctx.output.exitCode = 99;
9243
9402
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -9412,6 +9571,7 @@ async function runContainerLoop(profile, ctx, input) {
9412
9571
  cliArgs = { issue: issueNumber };
9413
9572
  }
9414
9573
  let childOut;
9574
+ const childStartedAt = Date.now();
9415
9575
  try {
9416
9576
  childOut = await runChild(child.exec, {
9417
9577
  cliArgs,
@@ -9421,7 +9581,23 @@ async function runContainerLoop(profile, ctx, input) {
9421
9581
  verbose: input.verbose,
9422
9582
  quiet: input.quiet
9423
9583
  });
9584
+ emitEvent(input.cwd, {
9585
+ executable: profile.name,
9586
+ kind: "container_child",
9587
+ name: child.exec,
9588
+ durationMs: Date.now() - childStartedAt,
9589
+ outcome: childOut.exitCode === 0 ? "ok" : "failed",
9590
+ meta: { exitCode: childOut.exitCode, iteration }
9591
+ });
9424
9592
  } catch (err) {
9593
+ emitEvent(input.cwd, {
9594
+ executable: profile.name,
9595
+ kind: "container_child",
9596
+ name: child.exec,
9597
+ durationMs: Date.now() - childStartedAt,
9598
+ outcome: "failed",
9599
+ meta: { iteration, error: err instanceof Error ? err.message : String(err) }
9600
+ });
9425
9601
  const msg = err instanceof Error ? err.message : String(err);
9426
9602
  process.stderr.write(`[kody container] child "${child.exec}" crashed: ${msg}
9427
9603
  `);
@@ -9650,9 +9826,9 @@ function resolveAuthToken(env = process.env) {
9650
9826
  return token;
9651
9827
  }
9652
9828
  function detectPackageManager2(cwd) {
9653
- if (fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9654
- if (fs29.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9655
- if (fs29.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
9829
+ if (fs30.existsSync(path28.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9830
+ if (fs30.existsSync(path28.join(cwd, "yarn.lock"))) return "yarn";
9831
+ if (fs30.existsSync(path28.join(cwd, "bun.lockb"))) return "bun";
9656
9832
  return "npm";
9657
9833
  }
9658
9834
  function shellOut(cmd, args, cwd, stream = true) {
@@ -9739,11 +9915,11 @@ function configureGitIdentity(cwd) {
9739
9915
  }
9740
9916
  function postFailureTail(issueNumber, cwd, reason) {
9741
9917
  if (!issueNumber) return;
9742
- const logPath = path27.join(cwd, ".kody", "last-run.jsonl");
9918
+ const logPath = path28.join(cwd, ".kody", "last-run.jsonl");
9743
9919
  let tail = "";
9744
9920
  try {
9745
- if (fs29.existsSync(logPath)) {
9746
- const content = fs29.readFileSync(logPath, "utf-8");
9921
+ if (fs30.existsSync(logPath)) {
9922
+ const content = fs30.readFileSync(logPath, "utf-8");
9747
9923
  tail = content.slice(-3e3);
9748
9924
  }
9749
9925
  } catch {
@@ -9768,7 +9944,7 @@ async function runCi(argv) {
9768
9944
  return 0;
9769
9945
  }
9770
9946
  const args = parseCiArgs(argv);
9771
- const cwd = args.cwd ? path27.resolve(args.cwd) : process.cwd();
9947
+ const cwd = args.cwd ? path28.resolve(args.cwd) : process.cwd();
9772
9948
  let earlyConfig;
9773
9949
  try {
9774
9950
  earlyConfig = loadConfig(cwd);
@@ -9778,9 +9954,9 @@ async function runCi(argv) {
9778
9954
  const eventName = process.env.GITHUB_EVENT_NAME;
9779
9955
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
9780
9956
  let manualWorkflowDispatch = false;
9781
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs29.existsSync(dispatchEventPath)) {
9957
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs30.existsSync(dispatchEventPath)) {
9782
9958
  try {
9783
- const evt = JSON.parse(fs29.readFileSync(dispatchEventPath, "utf-8"));
9959
+ const evt = JSON.parse(fs30.readFileSync(dispatchEventPath, "utf-8"));
9784
9960
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
9785
9961
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
9786
9962
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -10025,9 +10201,9 @@ function parseChatArgs(argv, env = process.env) {
10025
10201
  return result;
10026
10202
  }
10027
10203
  function commitChatFiles(cwd, sessionId, verbose) {
10028
- const sessionFile = path28.relative(cwd, sessionFilePath(cwd, sessionId));
10029
- const eventsFile = path28.relative(cwd, eventsFilePath(cwd, sessionId));
10030
- const paths = [sessionFile, eventsFile].filter((p) => fs30.existsSync(path28.join(cwd, p)));
10204
+ const sessionFile = path29.relative(cwd, sessionFilePath(cwd, sessionId));
10205
+ const eventsFile = path29.relative(cwd, eventsFilePath(cwd, sessionId));
10206
+ const paths = [sessionFile, eventsFile].filter((p) => fs31.existsSync(path29.join(cwd, p)));
10031
10207
  if (paths.length === 0) return;
10032
10208
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
10033
10209
  try {
@@ -10065,7 +10241,7 @@ async function runChat(argv) {
10065
10241
  ${CHAT_HELP}`);
10066
10242
  return 64;
10067
10243
  }
10068
- const cwd = args.cwd ? path28.resolve(args.cwd) : process.cwd();
10244
+ const cwd = args.cwd ? path29.resolve(args.cwd) : process.cwd();
10069
10245
  const sessionId = args.sessionId;
10070
10246
  const unpackedSecrets = unpackAllSecrets();
10071
10247
  if (unpackedSecrets > 0) {
@@ -10117,7 +10293,7 @@ ${CHAT_HELP}`);
10117
10293
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
10118
10294
  const meta = readMeta(sessionFile);
10119
10295
  process.stdout.write(
10120
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs30.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10296
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs31.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10121
10297
  `
10122
10298
  );
10123
10299
  try {
@@ -10154,6 +10330,206 @@ ${CHAT_HELP}`);
10154
10330
  }
10155
10331
  }
10156
10332
 
10333
+ // src/stats.ts
10334
+ function parseStatsArgs(argv) {
10335
+ const out = { cwd: process.cwd() };
10336
+ for (let i = 0; i < argv.length; i++) {
10337
+ const a = argv[i];
10338
+ if (a === "--json") out.asJson = true;
10339
+ else if (a === "--cwd" && argv[i + 1]) {
10340
+ out.cwd = argv[++i];
10341
+ } else if (a === "--since" && argv[i + 1]) {
10342
+ out.sinceMs = parseDuration(argv[++i]);
10343
+ } else if (a === "--run" && argv[i + 1]) {
10344
+ out.runId = argv[++i];
10345
+ }
10346
+ }
10347
+ return out;
10348
+ }
10349
+ function parseDuration(s) {
10350
+ const m = /^(\d+)\s*([smhd])$/i.exec(s.trim());
10351
+ if (!m) return void 0;
10352
+ const n = Number.parseInt(m[1], 10);
10353
+ const unit = m[2].toLowerCase();
10354
+ const mult = unit === "s" ? 1e3 : unit === "m" ? 6e4 : unit === "h" ? 36e5 : 864e5;
10355
+ return n * mult;
10356
+ }
10357
+ function percentile(sorted, p) {
10358
+ if (sorted.length === 0) return 0;
10359
+ const idx = Math.min(sorted.length - 1, Math.max(0, Math.floor(p / 100 * sorted.length)));
10360
+ return sorted[idx];
10361
+ }
10362
+ function summarizeRun(events) {
10363
+ if (events.length === 0) return null;
10364
+ const sorted = [...events].sort((a, b) => a.ts.localeCompare(b.ts));
10365
+ const start = sorted.find((e) => e.kind === "stage_start");
10366
+ const ends = sorted.filter((e) => e.kind === "stage_end");
10367
+ const lastEnd = ends.length > 0 ? ends[ends.length - 1] : void 0;
10368
+ const startedAt = start?.ts ?? sorted[0].ts;
10369
+ const endedAt = lastEnd?.ts ?? sorted[sorted.length - 1].ts;
10370
+ const durationMs = new Date(endedAt).getTime() - new Date(startedAt).getTime();
10371
+ const exitCodeRaw = lastEnd?.meta?.exitCode;
10372
+ const exitCode = typeof exitCodeRaw === "number" ? exitCodeRaw : null;
10373
+ const executables = Array.from(new Set(sorted.map((e) => e.executable)));
10374
+ let tIn = 0;
10375
+ let tOut = 0;
10376
+ let tCacheR = 0;
10377
+ for (const ev of sorted) {
10378
+ if (ev.kind !== "agent_end") continue;
10379
+ const tokens = ev.meta?.tokens;
10380
+ if (tokens) {
10381
+ tIn += Number(tokens.input ?? 0);
10382
+ tOut += Number(tokens.output ?? 0);
10383
+ tCacheR += Number(tokens.cacheRead ?? 0);
10384
+ }
10385
+ }
10386
+ return {
10387
+ runId: sorted[0].runId,
10388
+ startedAt,
10389
+ endedAt,
10390
+ durationMs,
10391
+ executables,
10392
+ exitCode,
10393
+ ok: exitCode === 0,
10394
+ totalInputTokens: tIn,
10395
+ totalOutputTokens: tOut,
10396
+ totalCacheReadTokens: tCacheR
10397
+ };
10398
+ }
10399
+ function rollupByExecutable(events) {
10400
+ const byExec = /* @__PURE__ */ new Map();
10401
+ for (const ev of events) {
10402
+ if (ev.kind !== "stage_end") continue;
10403
+ if (!byExec.has(ev.executable)) byExec.set(ev.executable, []);
10404
+ byExec.get(ev.executable).push(ev);
10405
+ }
10406
+ const rollups = [];
10407
+ for (const [executable, stageEnds] of byExec) {
10408
+ const durations = stageEnds.map((e) => e.durationMs ?? 0).filter((d) => d > 0).sort((a, b) => a - b);
10409
+ const ok = stageEnds.filter((e) => e.outcome === "ok").length;
10410
+ const failed = stageEnds.filter((e) => e.outcome === "failed").length;
10411
+ let tIn = 0;
10412
+ let tOut = 0;
10413
+ let tCacheR = 0;
10414
+ let tCacheC = 0;
10415
+ for (const ev of events) {
10416
+ if (ev.kind !== "agent_end") continue;
10417
+ if (ev.executable !== executable) continue;
10418
+ const tokens = ev.meta?.tokens;
10419
+ if (tokens) {
10420
+ tIn += Number(tokens.input ?? 0);
10421
+ tOut += Number(tokens.output ?? 0);
10422
+ tCacheR += Number(tokens.cacheRead ?? 0);
10423
+ tCacheC += Number(tokens.cacheCreate ?? 0);
10424
+ }
10425
+ }
10426
+ const mean = durations.length > 0 ? durations.reduce((s, n) => s + n, 0) / durations.length : 0;
10427
+ rollups.push({
10428
+ executable,
10429
+ runs: stageEnds.length,
10430
+ ok,
10431
+ failed,
10432
+ p50Ms: percentile(durations, 50),
10433
+ p95Ms: percentile(durations, 95),
10434
+ meanMs: Math.round(mean),
10435
+ totalInputTokens: tIn,
10436
+ totalOutputTokens: tOut,
10437
+ totalCacheReadTokens: tCacheR,
10438
+ totalCacheCreateTokens: tCacheC
10439
+ });
10440
+ }
10441
+ rollups.sort((a, b) => b.runs - a.runs);
10442
+ return rollups;
10443
+ }
10444
+ async function runStats(argv) {
10445
+ const opts = parseStatsArgs(argv);
10446
+ const runIds = opts.runId ? [opts.runId] : listRuns(opts.cwd);
10447
+ if (runIds.length === 0) {
10448
+ process.stdout.write(`no runs found under ${opts.cwd}/.kody/runs/
10449
+ `);
10450
+ return 0;
10451
+ }
10452
+ const cutoff = opts.sinceMs ? Date.now() - opts.sinceMs : null;
10453
+ const allEvents = [];
10454
+ const runSummaries = [];
10455
+ for (const id of runIds) {
10456
+ const events = readEvents(opts.cwd, id);
10457
+ if (events.length === 0) continue;
10458
+ const summary = summarizeRun(events);
10459
+ if (!summary) continue;
10460
+ if (cutoff && new Date(summary.startedAt).getTime() < cutoff) continue;
10461
+ allEvents.push(...events);
10462
+ runSummaries.push(summary);
10463
+ }
10464
+ if (runSummaries.length === 0) {
10465
+ process.stdout.write("no runs in the requested window\n");
10466
+ return 0;
10467
+ }
10468
+ const byExec = rollupByExecutable(allEvents);
10469
+ if (opts.asJson) {
10470
+ process.stdout.write(`${JSON.stringify({ runs: runSummaries, byExecutable: byExec }, null, 2)}
10471
+ `);
10472
+ return 0;
10473
+ }
10474
+ printReport(runSummaries, byExec);
10475
+ return 0;
10476
+ }
10477
+ function printReport(runs, rollups) {
10478
+ const totalRuns = runs.length;
10479
+ const okRuns = runs.filter((r) => r.ok).length;
10480
+ const okPct = totalRuns > 0 ? (okRuns / totalRuns * 100).toFixed(1) : "\u2014";
10481
+ const durations = runs.map((r) => r.durationMs).sort((a, b) => a - b);
10482
+ const meanMs = durations.length > 0 ? durations.reduce((s, n) => s + n, 0) / durations.length : 0;
10483
+ process.stdout.write(`
10484
+ Kody run statistics \u2014 ${totalRuns} runs
10485
+ `);
10486
+ process.stdout.write(` success rate : ${okRuns}/${totalRuns} (${okPct}%)
10487
+ `);
10488
+ process.stdout.write(` mean wall-clock : ${formatMs(meanMs)}
10489
+ `);
10490
+ process.stdout.write(` p50 wall-clock : ${formatMs(percentile(durations, 50))}
10491
+ `);
10492
+ process.stdout.write(` p95 wall-clock : ${formatMs(percentile(durations, 95))}
10493
+ `);
10494
+ const totalIn = runs.reduce((s, r) => s + r.totalInputTokens, 0);
10495
+ const totalOut = runs.reduce((s, r) => s + r.totalOutputTokens, 0);
10496
+ const totalCacheR = runs.reduce((s, r) => s + r.totalCacheReadTokens, 0);
10497
+ process.stdout.write(` total tokens : ${totalIn.toLocaleString()} in / ${totalOut.toLocaleString()} out / ${totalCacheR.toLocaleString()} cache-read
10498
+ `);
10499
+ process.stdout.write(`
10500
+ Per-executable (stage_end events)
10501
+ `);
10502
+ const headers = ["executable", "runs", "ok", "failed", "p50", "p95", "mean", "tok-in", "tok-out", "cache-r"];
10503
+ const widths = [22, 6, 6, 7, 9, 9, 9, 10, 10, 10];
10504
+ process.stdout.write(headers.map((h, i) => h.padEnd(widths[i])).join("") + "\n");
10505
+ process.stdout.write(widths.map((w) => "-".repeat(w - 1) + " ").join("") + "\n");
10506
+ for (const r of rollups) {
10507
+ const row = [
10508
+ r.executable,
10509
+ String(r.runs),
10510
+ String(r.ok),
10511
+ String(r.failed),
10512
+ formatMs(r.p50Ms),
10513
+ formatMs(r.p95Ms),
10514
+ formatMs(r.meanMs),
10515
+ r.totalInputTokens.toLocaleString(),
10516
+ r.totalOutputTokens.toLocaleString(),
10517
+ r.totalCacheReadTokens.toLocaleString()
10518
+ ];
10519
+ process.stdout.write(row.map((c, i) => c.padEnd(widths[i])).join("") + "\n");
10520
+ }
10521
+ process.stdout.write("\n");
10522
+ }
10523
+ function formatMs(ms) {
10524
+ if (!Number.isFinite(ms) || ms <= 0) return "\u2014";
10525
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
10526
+ const seconds = ms / 1e3;
10527
+ if (seconds < 60) return `${seconds.toFixed(1)}s`;
10528
+ const minutes = seconds / 60;
10529
+ if (minutes < 60) return `${minutes.toFixed(1)}m`;
10530
+ return `${(minutes / 60).toFixed(2)}h`;
10531
+ }
10532
+
10157
10533
  // src/entry.ts
10158
10534
  var HELP_TEXT = `kody \u2014 single-session autonomous engineer
10159
10535
 
@@ -10166,6 +10542,7 @@ Usage:
10166
10542
  kody <other> [--cwd <path>] [--verbose|--quiet]
10167
10543
  kody ci --issue <N> [preflight flags \u2014 see: kody ci --help]
10168
10544
  kody chat [chat flags \u2014 see: kody chat --help]
10545
+ kody stats [--since 7d|--run <id>|--json|--cwd <path>]
10169
10546
  kody help
10170
10547
  kody version
10171
10548
 
@@ -10199,6 +10576,9 @@ function parseArgs(argv) {
10199
10576
  if (cmd === "chat") {
10200
10577
  return { ...result, command: "chat", chatArgv: argv.slice(1) };
10201
10578
  }
10579
+ if (cmd === "stats") {
10580
+ return { ...result, command: "stats", statsArgv: argv.slice(1) };
10581
+ }
10202
10582
  if (hasExecutable(cmd)) {
10203
10583
  result.command = "__executable__";
10204
10584
  result.executableName = cmd;
@@ -10209,7 +10589,7 @@ function parseArgs(argv) {
10209
10589
  return result;
10210
10590
  }
10211
10591
  const discovered = listExecutables().map((e) => e.name);
10212
- const available = ["ci", "help", "version", ...discovered];
10592
+ const available = ["ci", "chat", "stats", "help", "version", ...discovered];
10213
10593
  result.errors.push(`unknown command: ${cmd} (available: ${available.join(", ")})`);
10214
10594
  return result;
10215
10595
  }
@@ -10251,6 +10631,18 @@ ${HELP_TEXT}`);
10251
10631
  process.stderr.write(`[kody] fatal: ${msg}
10252
10632
  `);
10253
10633
  if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
10634
+ `);
10635
+ return 99;
10636
+ }
10637
+ }
10638
+ if (args.command === "stats") {
10639
+ try {
10640
+ return await runStats(args.statsArgv ?? []);
10641
+ } catch (err) {
10642
+ const msg = err instanceof Error ? err.message : String(err);
10643
+ process.stderr.write(`[kody] fatal: ${msg}
10644
+ `);
10645
+ if (err instanceof Error && err.stack) process.stderr.write(`${err.stack}
10254
10646
  `);
10255
10647
  return 99;
10256
10648
  }