@kody-ade/kody-engine 0.4.12 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.12",
6
+ version: "0.4.14",
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",
@@ -51,9 +51,9 @@ var package_default = {
51
51
  };
52
52
 
53
53
  // src/chat-cli.ts
54
- import { execFileSync as execFileSync29 } from "child_process";
55
- import * as fs28 from "fs";
56
- import * as path25 from "path";
54
+ import { execFileSync as execFileSync31 } from "child_process";
55
+ import * as fs29 from "fs";
56
+ import * as path26 from "path";
57
57
 
58
58
  // src/chat/events.ts
59
59
  import * as fs from "fs";
@@ -188,9 +188,21 @@ function loadConfig(projectDir = process.cwd()) {
188
188
  aliases: mergeAliases(raw.aliases),
189
189
  classify: parseClassifyConfig(raw.classify),
190
190
  release: parseReleaseConfig(raw.release),
191
- jobs: parseJobsConfig(raw.jobs)
191
+ jobs: parseJobsConfig(raw.jobs),
192
+ qa: parseQaConfig(raw.qa)
192
193
  };
193
194
  }
195
+ function parseQaConfig(raw) {
196
+ if (!raw || typeof raw !== "object") return void 0;
197
+ const r = raw;
198
+ const out = {};
199
+ if (typeof r.fallbackUrl === "string" && r.fallbackUrl.trim().length > 0) {
200
+ out.fallbackUrl = r.fallbackUrl.trim();
201
+ } else if (r.fallbackUrl !== void 0) {
202
+ throw new Error(`kody.config.json: qa.fallbackUrl must be a non-empty string`);
203
+ }
204
+ return Object.keys(out).length > 0 ? out : void 0;
205
+ }
194
206
  function parseJobsConfig(raw) {
195
207
  if (!raw || typeof raw !== "object") return void 0;
196
208
  const r = raw;
@@ -388,7 +400,15 @@ async function runAgent(opts) {
388
400
  ...process.env,
389
401
  SKIP_HOOKS: "1",
390
402
  HUSKY: "0",
391
- CI: process.env.CI ?? "1"
403
+ CI: process.env.CI ?? "1",
404
+ // MCP servers are spawned asynchronously by the SDK. With the default
405
+ // non-blocking behavior, the SDK announces its tool list at session
406
+ // init while servers are still in `pending`, so their tools never
407
+ // reach the model. Block until each MCP completes its handshake (or
408
+ // the timeout below elapses) so the tool list is complete on first
409
+ // turn.
410
+ MCP_CONNECTION_NONBLOCKING: process.env.MCP_CONNECTION_NONBLOCKING ?? "false",
411
+ MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "60000"
392
412
  };
393
413
  if (opts.litellmUrl) {
394
414
  env.ANTHROPIC_BASE_URL = opts.litellmUrl;
@@ -892,9 +912,9 @@ async function emit2(sink, type, sessionId, suffix, payload) {
892
912
  }
893
913
 
894
914
  // src/kody-cli.ts
895
- import { execFileSync as execFileSync28 } from "child_process";
896
- import * as fs27 from "fs";
897
- import * as path24 from "path";
915
+ import { execFileSync as execFileSync30 } from "child_process";
916
+ import * as fs28 from "fs";
917
+ import * as path25 from "path";
898
918
 
899
919
  // src/dispatch.ts
900
920
  import * as fs7 from "fs";
@@ -1253,9 +1273,9 @@ function coerceBare(spec, value) {
1253
1273
  }
1254
1274
 
1255
1275
  // src/executor.ts
1256
- import { execFileSync as execFileSync27, spawn as spawn4 } from "child_process";
1257
- import * as fs26 from "fs";
1258
- import * as path23 from "path";
1276
+ import { execFileSync as execFileSync29, spawn as spawn5 } from "child_process";
1277
+ import * as fs27 from "fs";
1278
+ import * as path24 from "path";
1259
1279
 
1260
1280
  // src/litellm.ts
1261
1281
  import { execFileSync as execFileSync3, spawn } from "child_process";
@@ -2581,118 +2601,803 @@ function formatToolsUsage(profile) {
2581
2601
  return lines.join("\n");
2582
2602
  }
2583
2603
 
2584
- // src/scripts/diagMcp.ts
2585
- import { execFileSync as execFileSync8 } from "child_process";
2604
+ // src/scripts/createQaGoal.ts
2605
+ import { execFileSync as execFileSync9 } from "child_process";
2586
2606
  import * as fs14 from "fs";
2587
- import * as os3 from "os";
2588
2607
  import * as path13 from "path";
2589
- var diagMcp = async (_ctx) => {
2590
- const home = os3.homedir();
2591
- const cacheDir = path13.join(home, ".cache", "ms-playwright");
2592
- let entries = [];
2593
- try {
2594
- entries = fs14.readdirSync(cacheDir);
2595
- } catch {
2608
+
2609
+ // src/issue.ts
2610
+ import { execFileSync as execFileSync8 } from "child_process";
2611
+ var API_TIMEOUT_MS3 = 3e4;
2612
+ function ghToken2() {
2613
+ return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2614
+ }
2615
+ function gh2(args, options) {
2616
+ const token = ghToken2();
2617
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2618
+ return execFileSync8("gh", args, {
2619
+ encoding: "utf-8",
2620
+ timeout: API_TIMEOUT_MS3,
2621
+ cwd: options?.cwd,
2622
+ env,
2623
+ input: options?.input,
2624
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
2625
+ }).trim();
2626
+ }
2627
+ function getIssue(issueNumber, cwd) {
2628
+ const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
2629
+ const parsed = JSON.parse(output);
2630
+ if (typeof parsed?.title !== "string") {
2631
+ throw new Error(`Issue #${issueNumber}: unexpected response shape`);
2596
2632
  }
2597
- const hasChromium = entries.some((e) => e.startsWith("chromium"));
2598
- process.stderr.write(
2599
- `[kody diag] ms-playwright cache: ${entries.length === 0 ? "EMPTY (or missing)" : entries.join(", ")}
2633
+ return {
2634
+ number: parsed.number ?? issueNumber,
2635
+ title: parsed.title,
2636
+ body: parsed.body ?? "",
2637
+ comments: (parsed.comments ?? []).map((c) => ({
2638
+ body: c.body ?? "",
2639
+ author: c.author?.login ?? "unknown",
2640
+ createdAt: c.createdAt ?? ""
2641
+ })),
2642
+ labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
2643
+ };
2644
+ }
2645
+ function stripKodyMentions(body) {
2646
+ return body.replace(/(@)(kody)/gi, "$1\u200B$2");
2647
+ }
2648
+ function postIssueComment(issueNumber, body, cwd) {
2649
+ try {
2650
+ gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2651
+ } catch (err) {
2652
+ process.stderr.write(
2653
+ `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
2600
2654
  `
2601
- );
2602
- process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
2603
- `);
2655
+ );
2656
+ }
2657
+ }
2658
+ function truncate2(s, maxBytes) {
2659
+ if (s.length <= maxBytes) return s;
2660
+ return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
2661
+ }
2662
+ function parsePrNumber(url) {
2663
+ const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
2664
+ if (!m) return null;
2665
+ const n = parseInt(m[1], 10);
2666
+ return Number.isFinite(n) ? n : null;
2667
+ }
2668
+ function getPr(prNumber, cwd) {
2669
+ const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
2670
+ cwd
2671
+ });
2672
+ const parsed = JSON.parse(output);
2673
+ if (typeof parsed?.title !== "string") {
2674
+ throw new Error(`PR #${prNumber}: unexpected response shape`);
2675
+ }
2676
+ return {
2677
+ number: parsed.number ?? prNumber,
2678
+ title: parsed.title,
2679
+ body: parsed.body ?? "",
2680
+ headRefName: String(parsed.headRefName ?? ""),
2681
+ baseRefName: String(parsed.baseRefName ?? ""),
2682
+ state: String(parsed.state ?? "")
2683
+ };
2684
+ }
2685
+ function getPrDiff(prNumber, cwd) {
2604
2686
  try {
2605
- const v = execFileSync8("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
2606
- stdio: "pipe",
2607
- timeout: 6e4,
2608
- encoding: "utf8"
2609
- }).trim();
2610
- process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
2611
- `);
2612
- } catch (e) {
2613
- const err = e instanceof Error ? e.message : String(e);
2614
- process.stderr.write(`[kody diag] @playwright/mcp spawn FAILED: ${err}
2615
- `);
2687
+ return gh2(["pr", "diff", String(prNumber)], { cwd });
2688
+ } catch (err) {
2689
+ process.stderr.write(
2690
+ `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2691
+ `
2692
+ );
2693
+ return "";
2616
2694
  }
2617
- };
2618
-
2619
- // src/scripts/discoverQaContext.ts
2620
- import * as fs16 from "fs";
2621
- import * as path15 from "path";
2622
-
2623
- // src/scripts/frameworkDetectors.ts
2624
- import * as fs15 from "fs";
2625
- import * as path14 from "path";
2626
- function detectFrameworks(cwd) {
2627
- const out = [];
2628
- let deps = {};
2695
+ }
2696
+ function getPrReviews(prNumber, cwd) {
2629
2697
  try {
2630
- const pkg = JSON.parse(fs15.readFileSync(path14.join(cwd, "package.json"), "utf-8"));
2631
- deps = { ...pkg.dependencies, ...pkg.devDependencies };
2698
+ const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
2699
+ const parsed = JSON.parse(output);
2700
+ if (!Array.isArray(parsed?.reviews)) return [];
2701
+ return parsed.reviews.map(
2702
+ (r) => ({
2703
+ body: r.body ?? "",
2704
+ state: r.state ?? "",
2705
+ author: r.author?.login ?? "unknown",
2706
+ submittedAt: r.submittedAt ?? ""
2707
+ })
2708
+ );
2632
2709
  } catch {
2633
- return out;
2634
- }
2635
- if (deps.payload || deps["@payloadcms/next"]) {
2636
- out.push({
2637
- name: "payload-cms",
2638
- version: deps.payload ?? deps["@payloadcms/next"] ?? null,
2639
- configFile: findFile(cwd, ["payload.config.ts", "payload-config.ts", "src/payload.config.ts"])
2640
- });
2641
- }
2642
- if (deps["next-auth"]) {
2643
- out.push({
2644
- name: "nextauth",
2645
- version: deps["next-auth"] ?? null,
2646
- configFile: findFile(cwd, ["auth.ts", "auth.config.ts", "src/auth.ts", "src/auth.config.ts"])
2647
- });
2648
- }
2649
- if (deps.prisma || deps["@prisma/client"]) {
2650
- out.push({
2651
- name: "prisma",
2652
- version: deps.prisma ?? deps["@prisma/client"] ?? null,
2653
- configFile: findFile(cwd, ["prisma/schema.prisma"])
2654
- });
2710
+ return [];
2655
2711
  }
2656
- if (deps.next) {
2657
- out.push({
2658
- name: "nextjs",
2659
- version: deps.next ?? null,
2660
- configFile: findFile(cwd, ["next.config.ts", "next.config.mjs", "next.config.js"])
2661
- });
2712
+ }
2713
+ function getPrComments(prNumber, cwd) {
2714
+ try {
2715
+ const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
2716
+ const parsed = JSON.parse(output);
2717
+ if (!Array.isArray(parsed?.comments)) return [];
2718
+ return parsed.comments.map((c) => ({
2719
+ body: c.body ?? "",
2720
+ author: c.author?.login ?? "unknown",
2721
+ createdAt: c.createdAt ?? ""
2722
+ })).filter((c) => c.body.trim().length > 0);
2723
+ } catch {
2724
+ return [];
2662
2725
  }
2663
- return out;
2664
2726
  }
2665
- function findFile(cwd, candidates) {
2666
- for (const c of candidates) {
2667
- if (fs15.existsSync(path14.join(cwd, c))) return c;
2727
+ var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
2728
+ function isReviewShaped(body) {
2729
+ return VERDICT_HEADING.test(body);
2730
+ }
2731
+ function getPrLatestReviewBody(prNumber, cwd) {
2732
+ const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
2733
+ const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
2734
+ const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
2735
+ if (all.length > 0) return all[0].body;
2736
+ const pr = getPr(prNumber, cwd);
2737
+ return pr.body;
2738
+ }
2739
+ function postPrReviewComment(prNumber, body, cwd) {
2740
+ try {
2741
+ gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2742
+ } catch (err) {
2743
+ process.stderr.write(
2744
+ `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
2745
+ `
2746
+ );
2668
2747
  }
2669
- return null;
2670
2748
  }
2671
- var COLLECTION_DIRS = [
2672
- "src/server/payload/collections",
2673
- "src/payload/collections",
2674
- "src/collections",
2675
- "payload/collections"
2676
- ];
2677
- function discoverPayloadCollections(cwd) {
2678
- const out = [];
2679
- for (const dir of COLLECTION_DIRS) {
2680
- const full = path14.join(cwd, dir);
2681
- if (!fs15.existsSync(full)) continue;
2682
- let files;
2749
+
2750
+ // src/scripts/postReviewResult.ts
2751
+ function detectVerdict(body) {
2752
+ const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
2753
+ if (!m) return "UNKNOWN";
2754
+ return m[1].toUpperCase();
2755
+ }
2756
+ function reviewAction(verdict, payload) {
2757
+ const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
2758
+ return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2759
+ }
2760
+ function failedAction(reason) {
2761
+ return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2762
+ }
2763
+ var postReviewResult = async (ctx, _profile, agentResult) => {
2764
+ const prNumber = ctx.data.commentTargetNumber;
2765
+ if (!prNumber) {
2766
+ ctx.output.exitCode = 99;
2767
+ ctx.output.reason = "review postflight: no PR number in context";
2768
+ ctx.data.action = failedAction(ctx.output.reason);
2769
+ return;
2770
+ }
2771
+ if (!agentResult || agentResult.outcome !== "completed") {
2772
+ const reason = agentResult?.error ?? "agent did not complete";
2683
2773
  try {
2684
- files = fs15.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2774
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
2685
2775
  } catch {
2686
- continue;
2687
2776
  }
2688
- for (const file of files) {
2689
- try {
2690
- const filePath = path14.join(full, file);
2691
- const content = fs15.readFileSync(filePath, "utf-8").slice(0, 1e4);
2692
- const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
2693
- if (!slugMatch) continue;
2694
- const slug = slugMatch[1];
2695
- const name = file.replace(/\.(ts|tsx)$/, "");
2777
+ ctx.output.exitCode = 1;
2778
+ ctx.output.reason = reason;
2779
+ ctx.data.action = failedAction(reason);
2780
+ return;
2781
+ }
2782
+ const reviewBody = agentResult.finalText.trim();
2783
+ if (!reviewBody) {
2784
+ try {
2785
+ postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
2786
+ } catch {
2787
+ }
2788
+ ctx.output.exitCode = 1;
2789
+ ctx.output.reason = "empty review body";
2790
+ ctx.data.action = failedAction("empty review body");
2791
+ return;
2792
+ }
2793
+ try {
2794
+ postPrReviewComment(prNumber, reviewBody, ctx.cwd);
2795
+ } catch (err) {
2796
+ const msg = err instanceof Error ? err.message : String(err);
2797
+ ctx.output.exitCode = 4;
2798
+ ctx.output.reason = `failed to post review comment: ${msg}`;
2799
+ ctx.data.action = failedAction(ctx.output.reason);
2800
+ return;
2801
+ }
2802
+ const verdict = detectVerdict(reviewBody);
2803
+ ctx.data.reviewVerdict = verdict;
2804
+ ctx.data.reviewBody = reviewBody;
2805
+ ctx.data.action = reviewAction(verdict, { bodyPreview: truncate2(reviewBody, 500) });
2806
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
2807
+ process.stdout.write(
2808
+ `
2809
+ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
2810
+ `
2811
+ );
2812
+ };
2813
+
2814
+ // src/scripts/createQaGoal.ts
2815
+ var MANIFEST_LABEL = "kody:goals-manifest";
2816
+ var MANIFEST_TITLE = "Kody Goals Manifest";
2817
+ var MANIFEST_START = "<!-- kody-goals-start -->";
2818
+ var MANIFEST_END = "<!-- kody-goals-end -->";
2819
+ var FINDING_LABEL = "kody:qa-finding";
2820
+ var REPORT_JSON_OPEN = "<!-- KODY_QA_REPORT_JSON";
2821
+ var REPORT_JSON_CLOSE = "-->";
2822
+ function qaAction(verdict, payload) {
2823
+ const type = verdict === "PASS" ? "QA_PASS" : verdict === "CONCERNS" ? "QA_CONCERNS" : verdict === "FAIL" ? "QA_FAIL" : "QA_COMPLETED";
2824
+ return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2825
+ }
2826
+ function failedAction2(reason) {
2827
+ return { type: "QA_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
2828
+ }
2829
+ function slugify(input) {
2830
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 60);
2831
+ }
2832
+ function todayIso() {
2833
+ return (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2834
+ }
2835
+ function buildGoalId(scope, existing) {
2836
+ const focus = slugify(scope?.trim() ? scope.trim() : "smoke") || "smoke";
2837
+ const base = `qa-${focus}-${todayIso()}`;
2838
+ if (!existing.has(base)) return base;
2839
+ let i = 2;
2840
+ while (existing.has(`${base}-${i}`)) i++;
2841
+ return `${base}-${i}`;
2842
+ }
2843
+ function buildGoalName(scope, verdict) {
2844
+ const focus = scope?.trim() ? scope.trim() : "smoke";
2845
+ const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
2846
+ return `QA: ${focus} \u2014 ${verdictTag} \u2014 ${todayIso()}`.slice(0, 240);
2847
+ }
2848
+ function splitReport(text) {
2849
+ const open = text.indexOf(REPORT_JSON_OPEN);
2850
+ if (open < 0) {
2851
+ return { markdown: text.trim(), data: null, jsonError: "no JSON block marker" };
2852
+ }
2853
+ const closeRel = text.slice(open + REPORT_JSON_OPEN.length).indexOf(REPORT_JSON_CLOSE);
2854
+ if (closeRel < 0) {
2855
+ return { markdown: text.slice(0, open).trim(), data: null, jsonError: "JSON block not terminated" };
2856
+ }
2857
+ const closeAbs = open + REPORT_JSON_OPEN.length + closeRel;
2858
+ const rawJson = text.slice(open + REPORT_JSON_OPEN.length, closeAbs).trim();
2859
+ const fenced = rawJson.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/);
2860
+ const cleanJson = fenced ? fenced[1].trim() : rawJson;
2861
+ let parsed = null;
2862
+ let parseError;
2863
+ try {
2864
+ const obj = JSON.parse(cleanJson);
2865
+ if (!obj || !Array.isArray(obj.findings)) {
2866
+ parseError = "JSON missing 'findings' array";
2867
+ } else {
2868
+ parsed = obj;
2869
+ }
2870
+ } catch (err) {
2871
+ parseError = err instanceof Error ? err.message : String(err);
2872
+ }
2873
+ const markdown = text.slice(0, open).trim();
2874
+ return { markdown, data: parsed, jsonError: parseError };
2875
+ }
2876
+ function loadManifest(cwd) {
2877
+ let issuesJson;
2878
+ try {
2879
+ issuesJson = gh2(
2880
+ ["issue", "list", "--label", MANIFEST_LABEL, "--state", "all", "--limit", "1", "--json", "number,body"],
2881
+ { cwd }
2882
+ );
2883
+ } catch {
2884
+ return { number: null, manifest: { version: 1, goals: [] } };
2885
+ }
2886
+ let arr = [];
2887
+ try {
2888
+ arr = JSON.parse(issuesJson);
2889
+ } catch {
2890
+ return { number: null, manifest: { version: 1, goals: [] } };
2891
+ }
2892
+ if (arr.length === 0) return { number: null, manifest: { version: 1, goals: [] } };
2893
+ const issue = arr[0];
2894
+ const manifest = parseManifestBody(issue.body);
2895
+ return { number: issue.number, manifest };
2896
+ }
2897
+ function parseManifestBody(body) {
2898
+ if (!body) return { version: 1, goals: [] };
2899
+ const start = body.indexOf(MANIFEST_START);
2900
+ const end = body.indexOf(MANIFEST_END);
2901
+ if (start < 0 || end < 0 || end < start) return { version: 1, goals: [] };
2902
+ const inner = body.slice(start + MANIFEST_START.length, end);
2903
+ const fenceOpen = inner.indexOf("```");
2904
+ const fenceClose = inner.lastIndexOf("```");
2905
+ if (fenceOpen < 0 || fenceClose <= fenceOpen) return { version: 1, goals: [] };
2906
+ const afterOpen = inner.indexOf("\n", fenceOpen);
2907
+ if (afterOpen < 0) return { version: 1, goals: [] };
2908
+ const json = inner.slice(afterOpen + 1, fenceClose).trim();
2909
+ if (!json) return { version: 1, goals: [] };
2910
+ try {
2911
+ const parsed = JSON.parse(json);
2912
+ if (!parsed || !Array.isArray(parsed.goals)) return { version: 1, goals: [] };
2913
+ return { version: 1, goals: parsed.goals };
2914
+ } catch {
2915
+ return { version: 1, goals: [] };
2916
+ }
2917
+ }
2918
+ function serializeManifestBody(manifest) {
2919
+ const preamble = "> Kody goals manifest \u2014 the dashboard reads and writes the JSON block below.\n> Prefer editing via the UI to avoid merge conflicts.\n\n";
2920
+ const json = JSON.stringify(manifest, null, 2);
2921
+ return `${preamble}${MANIFEST_START}
2922
+
2923
+ \`\`\`json
2924
+ ${json}
2925
+ \`\`\`
2926
+
2927
+ ${MANIFEST_END}
2928
+ `;
2929
+ }
2930
+ function ensureLabel(name, color, description, cwd) {
2931
+ try {
2932
+ gh2(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
2933
+ } catch {
2934
+ }
2935
+ }
2936
+ function severityLabel(sev) {
2937
+ return `severity:${sev}`;
2938
+ }
2939
+ var SEVERITY_COLORS = {
2940
+ P0: "b60205",
2941
+ P1: "d93f0b",
2942
+ P2: "fbca04",
2943
+ P3: "0e8a16"
2944
+ };
2945
+ function ensureSeverityLabels(findings, cwd) {
2946
+ const seen = /* @__PURE__ */ new Set();
2947
+ for (const f of findings) {
2948
+ if (seen.has(f.severity)) continue;
2949
+ seen.add(f.severity);
2950
+ ensureLabel(severityLabel(f.severity), SEVERITY_COLORS[f.severity], `kody QA finding severity ${f.severity}`, cwd);
2951
+ }
2952
+ }
2953
+ function buildIssueBody(f, goalId, parentManifestNumber) {
2954
+ const lines = [];
2955
+ if (f.route) lines.push(`**Route:** \`${f.route}\``);
2956
+ lines.push("");
2957
+ lines.push("**Steps**");
2958
+ lines.push("");
2959
+ lines.push(f.steps.trim());
2960
+ lines.push("");
2961
+ lines.push("**Expected**");
2962
+ lines.push("");
2963
+ lines.push(f.expected.trim());
2964
+ lines.push("");
2965
+ lines.push("**Actual**");
2966
+ lines.push("");
2967
+ lines.push(f.actual.trim());
2968
+ lines.push("");
2969
+ if (f.evidence?.trim()) {
2970
+ lines.push("**Evidence**");
2971
+ lines.push("");
2972
+ lines.push(f.evidence.trim());
2973
+ lines.push("");
2974
+ }
2975
+ lines.push("---");
2976
+ if (parentManifestNumber !== null) {
2977
+ lines.push(`Goal: \`${goalId}\` \u2014 manifest issue #${parentManifestNumber}`);
2978
+ } else {
2979
+ lines.push(`Goal: \`${goalId}\``);
2980
+ }
2981
+ return lines.join("\n");
2982
+ }
2983
+ function createOrUpdateManifestIssue(number, manifest, cwd) {
2984
+ ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
2985
+ const body = serializeManifestBody(manifest);
2986
+ if (number !== null) {
2987
+ gh2(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
2988
+ return { number, created: false };
2989
+ }
2990
+ const out = gh2(["issue", "create", "--title", MANIFEST_TITLE, "--label", MANIFEST_LABEL, "--body-file", "-"], {
2991
+ input: body,
2992
+ cwd
2993
+ });
2994
+ const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
2995
+ const m = url.match(/\/issues\/(\d+)\b/);
2996
+ if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
2997
+ return { number: Number(m[1]), created: true };
2998
+ }
2999
+ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3000
+ const dir = path13.join(cwd, ".kody", "goals", goalId);
3001
+ fs14.mkdirSync(dir, { recursive: true });
3002
+ const state = {
3003
+ version: 1,
3004
+ state: "active",
3005
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
3006
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3007
+ ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
3008
+ };
3009
+ const filePath = path13.join(dir, "state.json");
3010
+ fs14.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3011
+ `);
3012
+ return filePath;
3013
+ }
3014
+ function gitTry(args, cwd) {
3015
+ const env = { ...process.env, SKIP_HOOKS: "1", HUSKY: "0" };
3016
+ try {
3017
+ execFileSync9("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3018
+ return { ok: true, stderr: "" };
3019
+ } catch (err) {
3020
+ const e = err;
3021
+ const stderr = typeof e?.stderr === "string" ? e.stderr : Buffer.isBuffer(e?.stderr) ? e.stderr.toString("utf8") : e?.message ?? "";
3022
+ return { ok: false, stderr: stderr.trim() };
3023
+ }
3024
+ }
3025
+ function commitAndPushState(filePath, goalId, cwd) {
3026
+ const add = gitTry(["add", filePath], cwd);
3027
+ if (!add.ok) {
3028
+ process.stderr.write(`[createQaGoal] git add failed: ${add.stderr.slice(-400) || "(no stderr)"}
3029
+ `);
3030
+ return;
3031
+ }
3032
+ const diff = gitTry(["diff", "--cached", "--quiet"], cwd);
3033
+ if (diff.ok) {
3034
+ process.stderr.write(`[createQaGoal] state.json unchanged \u2014 nothing to commit
3035
+ `);
3036
+ return;
3037
+ }
3038
+ const commit = gitTry(["commit", "-m", `chore(goals): activate ${goalId}`, "--quiet"], cwd);
3039
+ if (!commit.ok) {
3040
+ process.stderr.write(`[createQaGoal] git commit failed: ${commit.stderr.slice(-400) || "(no stderr)"}
3041
+ `);
3042
+ return;
3043
+ }
3044
+ const push = gitTry(["push", "--quiet"], cwd);
3045
+ if (push.ok) return;
3046
+ const stderr = push.stderr;
3047
+ const tail = stderr.slice(-400) || "(no stderr captured)";
3048
+ if (/non-fast-forward|rejected|fetch first|behind/i.test(stderr)) {
3049
+ process.stderr.write(`[createQaGoal] push rejected (non-fast-forward) \u2014 pulling --rebase and retrying
3050
+ `);
3051
+ const rebase = gitTry(["pull", "--rebase", "--autostash", "--quiet"], cwd);
3052
+ if (!rebase.ok) {
3053
+ process.stderr.write(
3054
+ `[createQaGoal] rebase failed (manual recovery required): ${rebase.stderr.slice(-400) || "(no stderr)"}
3055
+ `
3056
+ );
3057
+ return;
3058
+ }
3059
+ const retryPush = gitTry(["push", "--quiet"], cwd);
3060
+ if (retryPush.ok) {
3061
+ process.stderr.write(`[createQaGoal] push succeeded after rebase
3062
+ `);
3063
+ return;
3064
+ }
3065
+ process.stderr.write(
3066
+ `[createQaGoal] push still failed after rebase: ${retryPush.stderr.slice(-400) || "(no stderr)"}
3067
+ `
3068
+ );
3069
+ return;
3070
+ }
3071
+ if (/pre-push|hook|husky/i.test(stderr)) {
3072
+ process.stderr.write(`[createQaGoal] push rejected by pre-push hook \u2014 retrying with --no-verify
3073
+ `);
3074
+ process.stderr.write(`[createQaGoal] hook output:
3075
+ ${tail}
3076
+ `);
3077
+ const noVerify = gitTry(["push", "--no-verify", "--quiet"], cwd);
3078
+ if (noVerify.ok) {
3079
+ process.stderr.write(`[createQaGoal] push succeeded with --no-verify (consider adding kody artifacts to ignore configs)
3080
+ `);
3081
+ return;
3082
+ }
3083
+ process.stderr.write(
3084
+ `[createQaGoal] --no-verify push also failed: ${noVerify.stderr.slice(-400) || "(no stderr)"}
3085
+ `
3086
+ );
3087
+ return;
3088
+ }
3089
+ process.stderr.write(
3090
+ `[createQaGoal] state.json commit landed but push failed.
3091
+ [createQaGoal] The goal will not be visible to goal-scheduler in CI until you run 'git push' manually.
3092
+ [createQaGoal] git stderr:
3093
+ ${tail}
3094
+ `
3095
+ );
3096
+ }
3097
+ function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3098
+ const labels = [`goal:${goalId}`, severityLabel(finding.severity), FINDING_LABEL];
3099
+ ensureLabel(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3100
+ ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", cwd);
3101
+ const title = `[${finding.severity}] ${finding.title}`.slice(0, 240);
3102
+ const body = buildIssueBody(finding, goalId, manifestNumber);
3103
+ const args = ["issue", "create", "--title", title, "--body-file", "-"];
3104
+ for (const l of labels) {
3105
+ args.push("--label", l);
3106
+ }
3107
+ const out = gh2(args, { input: body, cwd });
3108
+ const url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
3109
+ const m = url.match(/\/issues\/(\d+)\b/);
3110
+ if (!m) throw new Error(`gh issue create returned unexpected output: ${out}`);
3111
+ return { number: Number(m[1]), url };
3112
+ }
3113
+ var createQaGoal = async (ctx, _profile, agentResult) => {
3114
+ if (!agentResult || agentResult.outcome !== "completed") {
3115
+ const reason = agentResult?.error ?? "agent did not complete";
3116
+ process.stderr.write(`[createQaGoal] ${reason}
3117
+ `);
3118
+ ctx.output.exitCode = 1;
3119
+ ctx.output.reason = reason;
3120
+ ctx.data.action = failedAction2(reason);
3121
+ return;
3122
+ }
3123
+ const finalText = agentResult.finalText.trim();
3124
+ if (!finalText) {
3125
+ process.stderr.write("[createQaGoal] agent produced no report body\n");
3126
+ ctx.output.exitCode = 1;
3127
+ ctx.output.reason = "empty report body";
3128
+ ctx.data.action = failedAction2("empty report body");
3129
+ return;
3130
+ }
3131
+ const { markdown, data, jsonError } = splitReport(finalText);
3132
+ const verdict = detectVerdict(markdown);
3133
+ const findings = data?.findings ?? [];
3134
+ const existingIssue = ctx.args.issue;
3135
+ if (findings.length === 0 || jsonError) {
3136
+ if (jsonError) {
3137
+ process.stderr.write(`[createQaGoal] JSON parse: ${jsonError} \u2014 falling back to single-issue mode
3138
+ `);
3139
+ }
3140
+ if (typeof existingIssue === "number" && existingIssue > 0) {
3141
+ try {
3142
+ postIssueComment(existingIssue, finalText, ctx.cwd);
3143
+ } catch (err) {
3144
+ const msg = err instanceof Error ? err.message : String(err);
3145
+ ctx.output.exitCode = 4;
3146
+ ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
3147
+ ctx.data.action = failedAction2(ctx.output.reason);
3148
+ return;
3149
+ }
3150
+ process.stdout.write(
3151
+ `
3152
+ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
3153
+ `
3154
+ );
3155
+ ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
3156
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3157
+ return;
3158
+ }
3159
+ ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3160
+ const scope2 = ctx.args.scope;
3161
+ const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3162
+ let url = "";
3163
+ try {
3164
+ const out = gh2(
3165
+ ["issue", "create", "--title", title, "--label", FINDING_LABEL, "--body-file", "-"],
3166
+ { input: finalText, cwd: ctx.cwd }
3167
+ );
3168
+ url = out.split("\n").map((l) => l.trim()).filter(Boolean).pop() ?? "";
3169
+ } catch (err) {
3170
+ const msg = err instanceof Error ? err.message : String(err);
3171
+ ctx.output.exitCode = 4;
3172
+ ctx.output.reason = `failed to open record issue: ${truncate2(msg, 1e3)}`;
3173
+ ctx.data.action = failedAction2(ctx.output.reason);
3174
+ return;
3175
+ }
3176
+ process.stdout.write(`
3177
+ QA_REPORT_POSTED=${url} (verdict: ${verdict})
3178
+ `);
3179
+ const m = url.match(/\/issues\/(\d+)\b/);
3180
+ ctx.data.action = qaAction(verdict, {
3181
+ issueNumber: m ? Number(m[1]) : 0,
3182
+ issueUrl: url,
3183
+ mode: "create-record"
3184
+ });
3185
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3186
+ return;
3187
+ }
3188
+ const explicitGoal = ctx.args.goal?.trim();
3189
+ const scope = ctx.args.scope;
3190
+ let goalId;
3191
+ let manifestIssueNumber = null;
3192
+ let manifestCreated = false;
3193
+ let manifestUpdated = false;
3194
+ if (explicitGoal && explicitGoal.length > 0) {
3195
+ goalId = explicitGoal;
3196
+ const manifestRead = loadManifest(ctx.cwd);
3197
+ if (manifestRead.number !== null) {
3198
+ manifestIssueNumber = manifestRead.number;
3199
+ try {
3200
+ postIssueComment(
3201
+ manifestRead.number,
3202
+ `## QA \u2014 ${verdict} \xB7 goal \`${goalId}\`
3203
+
3204
+ ${markdown}`,
3205
+ ctx.cwd
3206
+ );
3207
+ } catch (err) {
3208
+ const reason = err instanceof Error ? err.message : String(err);
3209
+ process.stderr.write(`[createQaGoal] could not comment on manifest issue: ${reason.slice(0, 300)}
3210
+ `);
3211
+ }
3212
+ }
3213
+ } else {
3214
+ const manifestRead = loadManifest(ctx.cwd);
3215
+ const existingIds = new Set(manifestRead.manifest.goals.map((g) => g.id));
3216
+ goalId = buildGoalId(scope, existingIds);
3217
+ const goalName = buildGoalName(scope, verdict);
3218
+ const newGoal = {
3219
+ id: goalId,
3220
+ name: goalName,
3221
+ description: markdown,
3222
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
3223
+ };
3224
+ const nextManifest = {
3225
+ version: 1,
3226
+ goals: [...manifestRead.manifest.goals, newGoal]
3227
+ };
3228
+ let manifestIssue;
3229
+ try {
3230
+ manifestIssue = createOrUpdateManifestIssue(manifestRead.number, nextManifest, ctx.cwd);
3231
+ } catch (err) {
3232
+ const msg = err instanceof Error ? err.message : String(err);
3233
+ ctx.output.exitCode = 4;
3234
+ ctx.output.reason = `failed to update goals manifest: ${truncate2(msg, 1e3)}`;
3235
+ ctx.data.action = failedAction2(ctx.output.reason);
3236
+ return;
3237
+ }
3238
+ manifestIssueNumber = manifestIssue.number;
3239
+ manifestCreated = manifestIssue.created;
3240
+ manifestUpdated = true;
3241
+ }
3242
+ ensureSeverityLabels(findings, ctx.cwd);
3243
+ const opened = [];
3244
+ const failed = [];
3245
+ for (const f of findings) {
3246
+ try {
3247
+ const issue = createTaskIssue(f, goalId, manifestIssueNumber, ctx.cwd);
3248
+ opened.push({ ...issue, severity: f.severity });
3249
+ } catch (err) {
3250
+ const reason = err instanceof Error ? err.message : String(err);
3251
+ failed.push({ title: f.title, reason });
3252
+ process.stderr.write(`[createQaGoal] could not open issue for "${f.title}": ${reason.slice(0, 300)}
3253
+ `);
3254
+ }
3255
+ }
3256
+ const stateFile = writeStateFile(ctx.cwd, goalId);
3257
+ commitAndPushState(stateFile, goalId, ctx.cwd);
3258
+ const repoUrl = `https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}`;
3259
+ if (manifestIssueNumber !== null) {
3260
+ const verb = manifestUpdated ? manifestCreated ? "OPENED" : "UPDATED" : "TARGETED";
3261
+ process.stdout.write(
3262
+ `
3263
+ QA_GOAL_${verb}=${repoUrl}/issues/${manifestIssueNumber} (id: ${goalId}, verdict: ${verdict})
3264
+ `
3265
+ );
3266
+ } else {
3267
+ process.stdout.write(`
3268
+ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
3269
+ `);
3270
+ }
3271
+ for (const o of opened) {
3272
+ process.stdout.write(`QA_FINDING_OPENED=${o.url} (severity: ${o.severity})
3273
+ `);
3274
+ }
3275
+ if (failed.length > 0) {
3276
+ process.stdout.write(`QA_FINDINGS_FAILED=${failed.length} (see stderr above)
3277
+ `);
3278
+ }
3279
+ ctx.data.action = qaAction(verdict, {
3280
+ goalId,
3281
+ manifestIssue: manifestIssueNumber ?? void 0,
3282
+ findingsOpened: opened.length,
3283
+ findingsFailed: failed.length,
3284
+ mode: explicitGoal ? "goal-attach" : manifestCreated ? "goal-create" : "goal-append"
3285
+ });
3286
+ ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3287
+ };
3288
+
3289
+ // src/scripts/diagMcp.ts
3290
+ import { execFileSync as execFileSync10 } from "child_process";
3291
+ import * as fs15 from "fs";
3292
+ import * as os3 from "os";
3293
+ import * as path14 from "path";
3294
+ var diagMcp = async (_ctx) => {
3295
+ const home = os3.homedir();
3296
+ const cacheDir = path14.join(home, ".cache", "ms-playwright");
3297
+ let entries = [];
3298
+ try {
3299
+ entries = fs15.readdirSync(cacheDir);
3300
+ } catch {
3301
+ }
3302
+ const hasChromium = entries.some((e) => e.startsWith("chromium"));
3303
+ process.stderr.write(
3304
+ `[kody diag] ms-playwright cache: ${entries.length === 0 ? "EMPTY (or missing)" : entries.join(", ")}
3305
+ `
3306
+ );
3307
+ process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
3308
+ `);
3309
+ try {
3310
+ const v = execFileSync10("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
3311
+ stdio: "pipe",
3312
+ timeout: 6e4,
3313
+ encoding: "utf8"
3314
+ }).trim();
3315
+ process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
3316
+ `);
3317
+ } catch (e) {
3318
+ const err = e instanceof Error ? e.message : String(e);
3319
+ process.stderr.write(`[kody diag] @playwright/mcp spawn FAILED: ${err}
3320
+ `);
3321
+ }
3322
+ };
3323
+
3324
+ // src/scripts/discoverQaContext.ts
3325
+ import * as fs17 from "fs";
3326
+ import * as path16 from "path";
3327
+
3328
+ // src/scripts/frameworkDetectors.ts
3329
+ import * as fs16 from "fs";
3330
+ import * as path15 from "path";
3331
+ function detectFrameworks(cwd) {
3332
+ const out = [];
3333
+ let deps = {};
3334
+ try {
3335
+ const pkg = JSON.parse(fs16.readFileSync(path15.join(cwd, "package.json"), "utf-8"));
3336
+ deps = { ...pkg.dependencies, ...pkg.devDependencies };
3337
+ } catch {
3338
+ return out;
3339
+ }
3340
+ if (deps.payload || deps["@payloadcms/next"]) {
3341
+ out.push({
3342
+ name: "payload-cms",
3343
+ version: deps.payload ?? deps["@payloadcms/next"] ?? null,
3344
+ configFile: findFile(cwd, ["payload.config.ts", "payload-config.ts", "src/payload.config.ts"])
3345
+ });
3346
+ }
3347
+ if (deps["next-auth"]) {
3348
+ out.push({
3349
+ name: "nextauth",
3350
+ version: deps["next-auth"] ?? null,
3351
+ configFile: findFile(cwd, ["auth.ts", "auth.config.ts", "src/auth.ts", "src/auth.config.ts"])
3352
+ });
3353
+ }
3354
+ if (deps.prisma || deps["@prisma/client"]) {
3355
+ out.push({
3356
+ name: "prisma",
3357
+ version: deps.prisma ?? deps["@prisma/client"] ?? null,
3358
+ configFile: findFile(cwd, ["prisma/schema.prisma"])
3359
+ });
3360
+ }
3361
+ if (deps.next) {
3362
+ out.push({
3363
+ name: "nextjs",
3364
+ version: deps.next ?? null,
3365
+ configFile: findFile(cwd, ["next.config.ts", "next.config.mjs", "next.config.js"])
3366
+ });
3367
+ }
3368
+ return out;
3369
+ }
3370
+ function findFile(cwd, candidates) {
3371
+ for (const c of candidates) {
3372
+ if (fs16.existsSync(path15.join(cwd, c))) return c;
3373
+ }
3374
+ return null;
3375
+ }
3376
+ var COLLECTION_DIRS = [
3377
+ "src/server/payload/collections",
3378
+ "src/payload/collections",
3379
+ "src/collections",
3380
+ "payload/collections"
3381
+ ];
3382
+ function discoverPayloadCollections(cwd) {
3383
+ const out = [];
3384
+ for (const dir of COLLECTION_DIRS) {
3385
+ const full = path15.join(cwd, dir);
3386
+ if (!fs16.existsSync(full)) continue;
3387
+ let files;
3388
+ try {
3389
+ files = fs16.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
3390
+ } catch {
3391
+ continue;
3392
+ }
3393
+ for (const file of files) {
3394
+ try {
3395
+ const filePath = path15.join(full, file);
3396
+ const content = fs16.readFileSync(filePath, "utf-8").slice(0, 1e4);
3397
+ const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
3398
+ if (!slugMatch) continue;
3399
+ const slug = slugMatch[1];
3400
+ const name = file.replace(/\.(ts|tsx)$/, "");
2696
3401
  const fields = [];
2697
3402
  const fieldMatches = content.matchAll(/name:\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g);
2698
3403
  for (const m of fieldMatches) {
@@ -2702,7 +3407,7 @@ function discoverPayloadCollections(cwd) {
2702
3407
  out.push({
2703
3408
  name,
2704
3409
  slug,
2705
- filePath: path14.relative(cwd, filePath),
3410
+ filePath: path15.relative(cwd, filePath),
2706
3411
  fields: fields.slice(0, 20),
2707
3412
  hasAdmin
2708
3413
  });
@@ -2716,28 +3421,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
2716
3421
  function discoverAdminComponents(cwd, collections) {
2717
3422
  const out = [];
2718
3423
  for (const dir of ADMIN_COMPONENT_DIRS) {
2719
- const full = path14.join(cwd, dir);
2720
- if (!fs15.existsSync(full)) continue;
3424
+ const full = path15.join(cwd, dir);
3425
+ if (!fs16.existsSync(full)) continue;
2721
3426
  let entries;
2722
3427
  try {
2723
- entries = fs15.readdirSync(full, { withFileTypes: true });
3428
+ entries = fs16.readdirSync(full, { withFileTypes: true });
2724
3429
  } catch {
2725
3430
  continue;
2726
3431
  }
2727
3432
  for (const entry of entries) {
2728
- const entryPath = path14.join(full, entry.name);
3433
+ const entryPath = path15.join(full, entry.name);
2729
3434
  let name;
2730
3435
  let filePath;
2731
3436
  if (entry.isDirectory()) {
2732
3437
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
2733
- (f) => fs15.existsSync(path14.join(entryPath, f))
3438
+ (f) => fs16.existsSync(path15.join(entryPath, f))
2734
3439
  );
2735
3440
  if (!indexFile) continue;
2736
3441
  name = entry.name;
2737
- filePath = path14.relative(cwd, path14.join(entryPath, indexFile));
3442
+ filePath = path15.relative(cwd, path15.join(entryPath, indexFile));
2738
3443
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
2739
3444
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
2740
- filePath = path14.relative(cwd, entryPath);
3445
+ filePath = path15.relative(cwd, entryPath);
2741
3446
  } else {
2742
3447
  continue;
2743
3448
  }
@@ -2745,7 +3450,7 @@ function discoverAdminComponents(cwd, collections) {
2745
3450
  if (collections) {
2746
3451
  for (const col of collections) {
2747
3452
  try {
2748
- const colContent = fs15.readFileSync(path14.join(cwd, col.filePath), "utf-8");
3453
+ const colContent = fs16.readFileSync(path15.join(cwd, col.filePath), "utf-8");
2749
3454
  if (colContent.includes(name)) {
2750
3455
  usedInCollection = col.slug;
2751
3456
  break;
@@ -2764,8 +3469,8 @@ function scanApiRoutes(cwd) {
2764
3469
  const out = [];
2765
3470
  const appDirs = ["src/app", "app"];
2766
3471
  for (const appDir of appDirs) {
2767
- const apiDir = path14.join(cwd, appDir, "api");
2768
- if (!fs15.existsSync(apiDir)) continue;
3472
+ const apiDir = path15.join(cwd, appDir, "api");
3473
+ if (!fs16.existsSync(apiDir)) continue;
2769
3474
  walkApiRoutes(apiDir, "/api", cwd, out);
2770
3475
  break;
2771
3476
  }
@@ -2774,14 +3479,14 @@ function scanApiRoutes(cwd) {
2774
3479
  function walkApiRoutes(dir, prefix, cwd, out) {
2775
3480
  let entries;
2776
3481
  try {
2777
- entries = fs15.readdirSync(dir, { withFileTypes: true });
3482
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
2778
3483
  } catch {
2779
3484
  return;
2780
3485
  }
2781
3486
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
2782
3487
  if (routeFile) {
2783
3488
  try {
2784
- const content = fs15.readFileSync(path14.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
3489
+ const content = fs16.readFileSync(path15.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
2785
3490
  const methods = HTTP_METHODS.filter(
2786
3491
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
2787
3492
  );
@@ -2789,7 +3494,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2789
3494
  out.push({
2790
3495
  path: prefix,
2791
3496
  methods,
2792
- filePath: path14.relative(cwd, path14.join(dir, routeFile.name))
3497
+ filePath: path15.relative(cwd, path15.join(dir, routeFile.name))
2793
3498
  });
2794
3499
  }
2795
3500
  } catch {
@@ -2800,7 +3505,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2800
3505
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2801
3506
  let segment = entry.name;
2802
3507
  if (segment.startsWith("(") && segment.endsWith(")")) {
2803
- walkApiRoutes(path14.join(dir, entry.name), prefix, cwd, out);
3508
+ walkApiRoutes(path15.join(dir, entry.name), prefix, cwd, out);
2804
3509
  continue;
2805
3510
  }
2806
3511
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2808,7 +3513,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2808
3513
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2809
3514
  segment = `:${segment.slice(1, -1)}`;
2810
3515
  }
2811
- walkApiRoutes(path14.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
3516
+ walkApiRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2812
3517
  }
2813
3518
  }
2814
3519
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -2828,10 +3533,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
2828
3533
  function scanEnvVars(cwd) {
2829
3534
  const candidates = [".env.example", ".env.local.example", ".env.template"];
2830
3535
  for (const envFile of candidates) {
2831
- const envPath = path14.join(cwd, envFile);
2832
- if (!fs15.existsSync(envPath)) continue;
3536
+ const envPath = path15.join(cwd, envFile);
3537
+ if (!fs16.existsSync(envPath)) continue;
2833
3538
  try {
2834
- const content = fs15.readFileSync(envPath, "utf-8");
3539
+ const content = fs16.readFileSync(envPath, "utf-8");
2835
3540
  const vars = [];
2836
3541
  for (const line of content.split("\n")) {
2837
3542
  const trimmed = line.trim();
@@ -2879,9 +3584,9 @@ function runQaDiscovery(cwd) {
2879
3584
  }
2880
3585
  function detectDevServer(cwd, out) {
2881
3586
  try {
2882
- const pkg = JSON.parse(fs16.readFileSync(path15.join(cwd, "package.json"), "utf-8"));
3587
+ const pkg = JSON.parse(fs17.readFileSync(path16.join(cwd, "package.json"), "utf-8"));
2883
3588
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2884
- const pm = fs16.existsSync(path15.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs16.existsSync(path15.join(cwd, "yarn.lock")) ? "yarn" : fs16.existsSync(path15.join(cwd, "bun.lockb")) ? "bun" : "npm";
3589
+ const pm = fs17.existsSync(path16.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs17.existsSync(path16.join(cwd, "yarn.lock")) ? "yarn" : fs17.existsSync(path16.join(cwd, "bun.lockb")) ? "bun" : "npm";
2885
3590
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
2886
3591
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
2887
3592
  else if (allDeps.vite) out.devPort = 5173;
@@ -2891,8 +3596,8 @@ function detectDevServer(cwd, out) {
2891
3596
  function scanFrontendRoutes(cwd, out) {
2892
3597
  const appDirs = ["src/app", "app"];
2893
3598
  for (const appDir of appDirs) {
2894
- const full = path15.join(cwd, appDir);
2895
- if (!fs16.existsSync(full)) continue;
3599
+ const full = path16.join(cwd, appDir);
3600
+ if (!fs17.existsSync(full)) continue;
2896
3601
  walkFrontendRoutes(full, "", out);
2897
3602
  break;
2898
3603
  }
@@ -2900,7 +3605,7 @@ function scanFrontendRoutes(cwd, out) {
2900
3605
  function walkFrontendRoutes(dir, prefix, out) {
2901
3606
  let entries;
2902
3607
  try {
2903
- entries = fs16.readdirSync(dir, { withFileTypes: true });
3608
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
2904
3609
  } catch {
2905
3610
  return;
2906
3611
  }
@@ -2917,7 +3622,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2917
3622
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2918
3623
  let segment = entry.name;
2919
3624
  if (segment.startsWith("(") && segment.endsWith(")")) {
2920
- walkFrontendRoutes(path15.join(dir, entry.name), prefix, out);
3625
+ walkFrontendRoutes(path16.join(dir, entry.name), prefix, out);
2921
3626
  continue;
2922
3627
  }
2923
3628
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2925,7 +3630,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2925
3630
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2926
3631
  segment = `:${segment.slice(1, -1)}`;
2927
3632
  }
2928
- walkFrontendRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, out);
3633
+ walkFrontendRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, out);
2929
3634
  }
2930
3635
  }
2931
3636
  function detectAuthFiles(cwd, out) {
@@ -2942,23 +3647,23 @@ function detectAuthFiles(cwd, out) {
2942
3647
  "src/app/api/oauth"
2943
3648
  ];
2944
3649
  for (const c of candidates) {
2945
- if (fs16.existsSync(path15.join(cwd, c))) out.authFiles.push(c);
3650
+ if (fs17.existsSync(path16.join(cwd, c))) out.authFiles.push(c);
2946
3651
  }
2947
3652
  }
2948
3653
  function detectRoles(cwd, out) {
2949
3654
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
2950
3655
  for (const rp of rolePaths) {
2951
- const dir = path15.join(cwd, rp);
2952
- if (!fs16.existsSync(dir)) continue;
3656
+ const dir = path16.join(cwd, rp);
3657
+ if (!fs17.existsSync(dir)) continue;
2953
3658
  let files;
2954
3659
  try {
2955
- files = fs16.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
3660
+ files = fs17.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
2956
3661
  } catch {
2957
3662
  continue;
2958
3663
  }
2959
3664
  for (const f of files) {
2960
3665
  try {
2961
- const content = fs16.readFileSync(path15.join(dir, f), "utf-8").slice(0, 5e3);
3666
+ const content = fs17.readFileSync(path16.join(dir, f), "utf-8").slice(0, 5e3);
2962
3667
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
2963
3668
  if (roleMatches) {
2964
3669
  for (const m of roleMatches) {
@@ -3104,8 +3809,8 @@ var discoverQaContext = async (ctx) => {
3104
3809
  };
3105
3810
 
3106
3811
  // src/scripts/dispatch.ts
3107
- import { execFileSync as execFileSync9 } from "child_process";
3108
- var API_TIMEOUT_MS3 = 3e4;
3812
+ import { execFileSync as execFileSync11 } from "child_process";
3813
+ var API_TIMEOUT_MS4 = 3e4;
3109
3814
  var dispatch = async (ctx, _profile, _agentResult, args) => {
3110
3815
  const next = args?.next;
3111
3816
  if (!next) {
@@ -3140,199 +3845,58 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
3140
3845
  const sub = usePr ? "pr" : "issue";
3141
3846
  const body = `@kody ${next}`;
3142
3847
  try {
3143
- execFileSync9("gh", [sub, "comment", String(targetNumber), "--body", body], {
3144
- timeout: API_TIMEOUT_MS3,
3145
- cwd: ctx.cwd,
3146
- stdio: ["ignore", "pipe", "pipe"]
3147
- });
3148
- } catch (err) {
3149
- process.stderr.write(
3150
- `[kody dispatch] failed to post @kody ${next} on ${sub} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
3151
- `
3152
- );
3153
- }
3154
- };
3155
- function parsePr(url) {
3156
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3157
- if (!m) return null;
3158
- const n = parseInt(m[1], 10);
3159
- return Number.isFinite(n) ? n : null;
3160
- }
3161
-
3162
- // src/scripts/dispatchClassified.ts
3163
- import { execFileSync as execFileSync10 } from "child_process";
3164
- var API_TIMEOUT_MS4 = 3e4;
3165
- var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3166
- var dispatchClassified = async (ctx) => {
3167
- const issueNumber = ctx.args.issue;
3168
- if (!issueNumber) return;
3169
- const classification = ctx.data.classification;
3170
- if (!classification || !VALID_CLASSES2.has(classification)) return;
3171
- try {
3172
- execFileSync10("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
3173
- cwd: ctx.cwd,
3174
- timeout: API_TIMEOUT_MS4,
3175
- stdio: ["ignore", "pipe", "pipe"]
3176
- });
3177
- } catch (err) {
3178
- process.stderr.write(
3179
- `[kody dispatchClassified] failed to dispatch @kody ${classification}: ${err instanceof Error ? err.message : String(err)}
3180
- `
3181
- );
3182
- ctx.data.action = failedAction("dispatch post failed");
3183
- ctx.output.exitCode = 1;
3184
- ctx.output.reason = "classify: dispatch failed";
3185
- }
3186
- };
3187
- function failedAction(reason) {
3188
- return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3189
- }
3190
-
3191
- // src/scripts/dispatchJobFileTicks.ts
3192
- import * as fs18 from "fs";
3193
- import * as path17 from "path";
3194
-
3195
- // src/issue.ts
3196
- import { execFileSync as execFileSync11 } from "child_process";
3197
- var API_TIMEOUT_MS5 = 3e4;
3198
- function ghToken2() {
3199
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
3200
- }
3201
- function gh2(args, options) {
3202
- const token = ghToken2();
3203
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3204
- return execFileSync11("gh", args, {
3205
- encoding: "utf-8",
3206
- timeout: API_TIMEOUT_MS5,
3207
- cwd: options?.cwd,
3208
- env,
3209
- input: options?.input,
3210
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
3211
- }).trim();
3212
- }
3213
- function getIssue(issueNumber, cwd) {
3214
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
3215
- const parsed = JSON.parse(output);
3216
- if (typeof parsed?.title !== "string") {
3217
- throw new Error(`Issue #${issueNumber}: unexpected response shape`);
3218
- }
3219
- return {
3220
- number: parsed.number ?? issueNumber,
3221
- title: parsed.title,
3222
- body: parsed.body ?? "",
3223
- comments: (parsed.comments ?? []).map((c) => ({
3224
- body: c.body ?? "",
3225
- author: c.author?.login ?? "unknown",
3226
- createdAt: c.createdAt ?? ""
3227
- })),
3228
- labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
3229
- };
3230
- }
3231
- function stripKodyMentions(body) {
3232
- return body.replace(/(@)(kody)/gi, "$1\u200B$2");
3233
- }
3234
- function postIssueComment(issueNumber, body, cwd) {
3235
- try {
3236
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
3237
- } catch (err) {
3238
- process.stderr.write(
3239
- `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
3240
- `
3241
- );
3242
- }
3243
- }
3244
- function truncate2(s, maxBytes) {
3245
- if (s.length <= maxBytes) return s;
3246
- return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
3247
- }
3248
- function parsePrNumber(url) {
3249
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3250
- if (!m) return null;
3251
- const n = parseInt(m[1], 10);
3252
- return Number.isFinite(n) ? n : null;
3253
- }
3254
- function getPr(prNumber, cwd) {
3255
- const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
3256
- cwd
3257
- });
3258
- const parsed = JSON.parse(output);
3259
- if (typeof parsed?.title !== "string") {
3260
- throw new Error(`PR #${prNumber}: unexpected response shape`);
3261
- }
3262
- return {
3263
- number: parsed.number ?? prNumber,
3264
- title: parsed.title,
3265
- body: parsed.body ?? "",
3266
- headRefName: String(parsed.headRefName ?? ""),
3267
- baseRefName: String(parsed.baseRefName ?? ""),
3268
- state: String(parsed.state ?? "")
3269
- };
3270
- }
3271
- function getPrDiff(prNumber, cwd) {
3272
- try {
3273
- return gh2(["pr", "diff", String(prNumber)], { cwd });
3274
- } catch (err) {
3275
- process.stderr.write(
3276
- `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
3277
- `
3278
- );
3279
- return "";
3280
- }
3281
- }
3282
- function getPrReviews(prNumber, cwd) {
3283
- try {
3284
- const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
3285
- const parsed = JSON.parse(output);
3286
- if (!Array.isArray(parsed?.reviews)) return [];
3287
- return parsed.reviews.map(
3288
- (r) => ({
3289
- body: r.body ?? "",
3290
- state: r.state ?? "",
3291
- author: r.author?.login ?? "unknown",
3292
- submittedAt: r.submittedAt ?? ""
3293
- })
3294
- );
3295
- } catch {
3296
- return [];
3297
- }
3298
- }
3299
- function getPrComments(prNumber, cwd) {
3300
- try {
3301
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
3302
- const parsed = JSON.parse(output);
3303
- if (!Array.isArray(parsed?.comments)) return [];
3304
- return parsed.comments.map((c) => ({
3305
- body: c.body ?? "",
3306
- author: c.author?.login ?? "unknown",
3307
- createdAt: c.createdAt ?? ""
3308
- })).filter((c) => c.body.trim().length > 0);
3309
- } catch {
3310
- return [];
3311
- }
3312
- }
3313
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
3314
- function isReviewShaped(body) {
3315
- return VERDICT_HEADING.test(body);
3316
- }
3317
- function getPrLatestReviewBody(prNumber, cwd) {
3318
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
3319
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
3320
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
3321
- if (all.length > 0) return all[0].body;
3322
- const pr = getPr(prNumber, cwd);
3323
- return pr.body;
3848
+ execFileSync11("gh", [sub, "comment", String(targetNumber), "--body", body], {
3849
+ timeout: API_TIMEOUT_MS4,
3850
+ cwd: ctx.cwd,
3851
+ stdio: ["ignore", "pipe", "pipe"]
3852
+ });
3853
+ } catch (err) {
3854
+ process.stderr.write(
3855
+ `[kody dispatch] failed to post @kody ${next} on ${sub} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
3856
+ `
3857
+ );
3858
+ }
3859
+ };
3860
+ function parsePr(url) {
3861
+ const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3862
+ if (!m) return null;
3863
+ const n = parseInt(m[1], 10);
3864
+ return Number.isFinite(n) ? n : null;
3324
3865
  }
3325
- function postPrReviewComment(prNumber, body, cwd) {
3866
+
3867
+ // src/scripts/dispatchClassified.ts
3868
+ import { execFileSync as execFileSync12 } from "child_process";
3869
+ var API_TIMEOUT_MS5 = 3e4;
3870
+ var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3871
+ var dispatchClassified = async (ctx) => {
3872
+ const issueNumber = ctx.args.issue;
3873
+ if (!issueNumber) return;
3874
+ const classification = ctx.data.classification;
3875
+ if (!classification || !VALID_CLASSES2.has(classification)) return;
3326
3876
  try {
3327
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
3877
+ execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
3878
+ cwd: ctx.cwd,
3879
+ timeout: API_TIMEOUT_MS5,
3880
+ stdio: ["ignore", "pipe", "pipe"]
3881
+ });
3328
3882
  } catch (err) {
3329
3883
  process.stderr.write(
3330
- `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
3884
+ `[kody dispatchClassified] failed to dispatch @kody ${classification}: ${err instanceof Error ? err.message : String(err)}
3331
3885
  `
3332
3886
  );
3887
+ ctx.data.action = failedAction3("dispatch post failed");
3888
+ ctx.output.exitCode = 1;
3889
+ ctx.output.reason = "classify: dispatch failed";
3333
3890
  }
3891
+ };
3892
+ function failedAction3(reason) {
3893
+ return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3334
3894
  }
3335
3895
 
3896
+ // src/scripts/dispatchJobFileTicks.ts
3897
+ import * as fs19 from "fs";
3898
+ import * as path18 from "path";
3899
+
3336
3900
  // src/scripts/issueStateComment.ts
3337
3901
  function isStateEnvelope(x) {
3338
3902
  if (x === null || typeof x !== "object") return false;
@@ -3503,8 +4067,8 @@ var ContentsApiBackend = class {
3503
4067
  };
3504
4068
 
3505
4069
  // src/scripts/jobState/localFileBackend.ts
3506
- import * as fs17 from "fs";
3507
- import * as path16 from "path";
4070
+ import * as fs18 from "fs";
4071
+ import * as path17 from "path";
3508
4072
  var LocalFileBackend = class {
3509
4073
  name = "local-file";
3510
4074
  cwd;
@@ -3519,7 +4083,7 @@ var LocalFileBackend = class {
3519
4083
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
3520
4084
  this.cwd = opts.cwd;
3521
4085
  this.jobsDir = opts.jobsDir;
3522
- this.absDir = path16.join(opts.cwd, opts.jobsDir);
4086
+ this.absDir = path17.join(opts.cwd, opts.jobsDir);
3523
4087
  this.owner = opts.owner;
3524
4088
  this.repo = opts.repo;
3525
4089
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -3534,7 +4098,7 @@ var LocalFileBackend = class {
3534
4098
  `);
3535
4099
  return;
3536
4100
  }
3537
- fs17.mkdirSync(this.absDir, { recursive: true });
4101
+ fs18.mkdirSync(this.absDir, { recursive: true });
3538
4102
  const prefix = this.cacheKeyPrefix();
3539
4103
  const probeKey = `${prefix}probe-${Date.now()}`;
3540
4104
  try {
@@ -3563,7 +4127,7 @@ var LocalFileBackend = class {
3563
4127
  `);
3564
4128
  return;
3565
4129
  }
3566
- if (!fs17.existsSync(this.absDir)) {
4130
+ if (!fs18.existsSync(this.absDir)) {
3567
4131
  return;
3568
4132
  }
3569
4133
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -3579,11 +4143,11 @@ var LocalFileBackend = class {
3579
4143
  }
3580
4144
  load(slug) {
3581
4145
  const relPath = stateFilePath(this.jobsDir, slug);
3582
- const absPath = path16.join(this.cwd, relPath);
3583
- if (!fs17.existsSync(absPath)) {
4146
+ const absPath = path17.join(this.cwd, relPath);
4147
+ if (!fs18.existsSync(absPath)) {
3584
4148
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
3585
4149
  }
3586
- const raw = fs17.readFileSync(absPath, "utf-8");
4150
+ const raw = fs18.readFileSync(absPath, "utf-8");
3587
4151
  let parsed;
3588
4152
  try {
3589
4153
  parsed = JSON.parse(raw);
@@ -3600,10 +4164,10 @@ var LocalFileBackend = class {
3600
4164
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
3601
4165
  return false;
3602
4166
  }
3603
- const absPath = path16.join(this.cwd, loaded.path);
3604
- fs17.mkdirSync(path16.dirname(absPath), { recursive: true });
4167
+ const absPath = path17.join(this.cwd, loaded.path);
4168
+ fs18.mkdirSync(path17.dirname(absPath), { recursive: true });
3605
4169
  const body = JSON.stringify(next, null, 2) + "\n";
3606
- fs17.writeFileSync(absPath, body, "utf-8");
4170
+ fs18.writeFileSync(absPath, body, "utf-8");
3607
4171
  return true;
3608
4172
  }
3609
4173
  cacheKeyPrefix() {
@@ -3680,7 +4244,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
3680
4244
  await backend.hydrate();
3681
4245
  }
3682
4246
  try {
3683
- const slugs = listJobSlugs(path17.join(ctx.cwd, jobsDir));
4247
+ const slugs = listJobSlugs(path18.join(ctx.cwd, jobsDir));
3684
4248
  ctx.data.jobSlugCount = slugs.length;
3685
4249
  if (slugs.length === 0) {
3686
4250
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -3728,10 +4292,10 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
3728
4292
  }
3729
4293
  };
3730
4294
  function listJobSlugs(absDir) {
3731
- if (!fs18.existsSync(absDir)) return [];
4295
+ if (!fs19.existsSync(absDir)) return [];
3732
4296
  let entries;
3733
4297
  try {
3734
- entries = fs18.readdirSync(absDir, { withFileTypes: true });
4298
+ entries = fs19.readdirSync(absDir, { withFileTypes: true });
3735
4299
  } catch {
3736
4300
  return [];
3737
4301
  }
@@ -3904,10 +4468,14 @@ function ensurePr(opts) {
3904
4468
  const title = buildPrTitle(effectiveOpts.issueNumber, effectiveOpts.issueTitle, effectiveOpts.draft);
3905
4469
  const body = buildPrBody(effectiveOpts);
3906
4470
  if (existing) {
4471
+ const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
4472
+ const [owner, repo] = stripped.split("/");
3907
4473
  try {
3908
- gh2(["pr", "edit", String(existing.number), "--body-file", "-"], { input: body, cwd: opts.cwd });
4474
+ gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4475
+ cwd: opts.cwd
4476
+ });
3909
4477
  } catch (err) {
3910
- throw new Error(`gh pr edit #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`);
4478
+ throw new Error(`gh api PATCH #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`);
3911
4479
  }
3912
4480
  return { url: existing.url, number: existing.number, draft: opts.draft, action: "updated" };
3913
4481
  }
@@ -3966,15 +4534,19 @@ var ensureMemorizePr = async (ctx) => {
3966
4534
  const body = buildBody(ctx, branch, datestamp);
3967
4535
  const existing = findExistingPr(branch, ctx.cwd);
3968
4536
  if (existing) {
4537
+ const stripped = existing.url.replace(/^https:\/\/github\.com\//, "");
4538
+ const [owner, repo] = stripped.split("/");
3969
4539
  try {
3970
- gh2(["pr", "edit", String(existing.number), "--body-file", "-"], { input: body, cwd: ctx.cwd });
4540
+ gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/pulls/${existing.number}`, "-f", `body=${body}`], {
4541
+ cwd: ctx.cwd
4542
+ });
3971
4543
  ctx.output.prUrl = existing.url;
3972
4544
  ctx.data.prResult = { url: existing.url, number: existing.number, action: "updated" };
3973
4545
  process.stdout.write(`[kody memorize] updated PR ${existing.url}
3974
4546
  `);
3975
4547
  } catch (err) {
3976
4548
  ctx.output.exitCode = 4;
3977
- ctx.output.reason = `gh pr edit #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`;
4549
+ ctx.output.reason = `gh api PATCH #${existing.number} failed: ${err instanceof Error ? err.message : String(err)}`;
3978
4550
  }
3979
4551
  return;
3980
4552
  }
@@ -4103,7 +4675,7 @@ function collectExpectedTests(raw) {
4103
4675
  }
4104
4676
 
4105
4677
  // src/scripts/finishFlow.ts
4106
- import { execFileSync as execFileSync12 } from "child_process";
4678
+ import { execFileSync as execFileSync13 } from "child_process";
4107
4679
 
4108
4680
  // src/lifecycleLabels.ts
4109
4681
  var KODY_NAMESPACE = "kody";
@@ -4256,7 +4828,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4256
4828
  **PR:** ${state.core.prUrl}` : "";
4257
4829
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
4258
4830
  try {
4259
- execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4831
+ execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4260
4832
  timeout: API_TIMEOUT_MS6,
4261
4833
  cwd: ctx.cwd,
4262
4834
  stdio: ["ignore", "pipe", "pipe"]
@@ -4270,7 +4842,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4270
4842
  };
4271
4843
 
4272
4844
  // src/branch.ts
4273
- import { execFileSync as execFileSync13 } from "child_process";
4845
+ import { execFileSync as execFileSync14 } from "child_process";
4274
4846
  var UncommittedChangesError = class extends Error {
4275
4847
  constructor(branch) {
4276
4848
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -4280,7 +4852,7 @@ var UncommittedChangesError = class extends Error {
4280
4852
  branch;
4281
4853
  };
4282
4854
  function git2(args, cwd) {
4283
- return execFileSync13("git", args, {
4855
+ return execFileSync14("git", args, {
4284
4856
  encoding: "utf-8",
4285
4857
  timeout: 3e4,
4286
4858
  cwd,
@@ -4305,7 +4877,7 @@ function checkoutPrBranch(prNumber, cwd) {
4305
4877
  SKIP_HOOKS: "1",
4306
4878
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
4307
4879
  };
4308
- execFileSync13("gh", ["pr", "checkout", String(prNumber)], {
4880
+ execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
4309
4881
  cwd,
4310
4882
  env,
4311
4883
  stdio: ["ignore", "pipe", "pipe"],
@@ -4419,8 +4991,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
4419
4991
  }
4420
4992
 
4421
4993
  // src/gha.ts
4422
- import { execFileSync as execFileSync14 } from "child_process";
4423
- import * as fs19 from "fs";
4994
+ import { execFileSync as execFileSync15 } from "child_process";
4995
+ import * as fs20 from "fs";
4424
4996
  function getRunUrl() {
4425
4997
  const server = process.env.GITHUB_SERVER_URL;
4426
4998
  const repo = process.env.GITHUB_REPOSITORY;
@@ -4431,10 +5003,10 @@ function getRunUrl() {
4431
5003
  function reactToTriggerComment(cwd) {
4432
5004
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
4433
5005
  const eventPath = process.env.GITHUB_EVENT_PATH;
4434
- if (!eventPath || !fs19.existsSync(eventPath)) return;
5006
+ if (!eventPath || !fs20.existsSync(eventPath)) return;
4435
5007
  let event = null;
4436
5008
  try {
4437
- event = JSON.parse(fs19.readFileSync(eventPath, "utf-8"));
5009
+ event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
4438
5010
  } catch {
4439
5011
  return;
4440
5012
  }
@@ -4462,7 +5034,7 @@ function reactToTriggerComment(cwd) {
4462
5034
  for (let attempt = 0; attempt < 3; attempt++) {
4463
5035
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
4464
5036
  try {
4465
- execFileSync14("gh", args, opts);
5037
+ execFileSync15("gh", args, opts);
4466
5038
  return;
4467
5039
  } catch (err) {
4468
5040
  lastErr = err;
@@ -4475,13 +5047,13 @@ function reactToTriggerComment(cwd) {
4475
5047
  }
4476
5048
  function sleepMs(ms) {
4477
5049
  try {
4478
- execFileSync14("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5050
+ execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
4479
5051
  } catch {
4480
5052
  }
4481
5053
  }
4482
5054
 
4483
5055
  // src/workflow.ts
4484
- import { execFileSync as execFileSync15 } from "child_process";
5056
+ import { execFileSync as execFileSync16 } from "child_process";
4485
5057
  var GH_TIMEOUT_MS = 3e4;
4486
5058
  function ghToken3() {
4487
5059
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -4489,7 +5061,7 @@ function ghToken3() {
4489
5061
  function gh3(args, cwd) {
4490
5062
  const token = ghToken3();
4491
5063
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
4492
- return execFileSync15("gh", args, {
5064
+ return execFileSync16("gh", args, {
4493
5065
  encoding: "utf-8",
4494
5066
  timeout: GH_TIMEOUT_MS,
4495
5067
  cwd,
@@ -4673,23 +5245,23 @@ function tryPostPr2(prNumber, body, cwd) {
4673
5245
  }
4674
5246
 
4675
5247
  // src/scripts/initFlow.ts
4676
- import { execFileSync as execFileSync16 } from "child_process";
4677
- import * as fs21 from "fs";
4678
- import * as path19 from "path";
5248
+ import { execFileSync as execFileSync17 } from "child_process";
5249
+ import * as fs22 from "fs";
5250
+ import * as path20 from "path";
4679
5251
 
4680
5252
  // src/scripts/loadQaGuide.ts
4681
- import * as fs20 from "fs";
4682
- import * as path18 from "path";
5253
+ import * as fs21 from "fs";
5254
+ import * as path19 from "path";
4683
5255
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
4684
5256
  var loadQaGuide = async (ctx) => {
4685
- const full = path18.join(ctx.cwd, QA_GUIDE_REL_PATH);
4686
- if (!fs20.existsSync(full)) {
5257
+ const full = path19.join(ctx.cwd, QA_GUIDE_REL_PATH);
5258
+ if (!fs21.existsSync(full)) {
4687
5259
  ctx.data.qaGuide = "";
4688
5260
  ctx.data.qaGuidePath = "";
4689
5261
  return;
4690
5262
  }
4691
5263
  try {
4692
- ctx.data.qaGuide = fs20.readFileSync(full, "utf-8");
5264
+ ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
4693
5265
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
4694
5266
  } catch {
4695
5267
  ctx.data.qaGuide = "";
@@ -4699,9 +5271,9 @@ var loadQaGuide = async (ctx) => {
4699
5271
 
4700
5272
  // src/scripts/initFlow.ts
4701
5273
  function detectPackageManager(cwd) {
4702
- if (fs21.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4703
- if (fs21.existsSync(path19.join(cwd, "yarn.lock"))) return "yarn";
4704
- if (fs21.existsSync(path19.join(cwd, "bun.lockb"))) return "bun";
5274
+ if (fs22.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5275
+ if (fs22.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
5276
+ if (fs22.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
4705
5277
  return "npm";
4706
5278
  }
4707
5279
  function qualityCommandsFor(pm) {
@@ -4714,7 +5286,7 @@ function qualityCommandsFor(pm) {
4714
5286
  function detectOwnerRepo(cwd) {
4715
5287
  let url;
4716
5288
  try {
4717
- url = execFileSync16("git", ["remote", "get-url", "origin"], {
5289
+ url = execFileSync17("git", ["remote", "get-url", "origin"], {
4718
5290
  cwd,
4719
5291
  encoding: "utf-8",
4720
5292
  stdio: ["ignore", "pipe", "pipe"]
@@ -4799,7 +5371,7 @@ jobs:
4799
5371
  `;
4800
5372
  function defaultBranchFromGit(cwd) {
4801
5373
  try {
4802
- const ref = execFileSync16("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5374
+ const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4803
5375
  cwd,
4804
5376
  encoding: "utf-8",
4805
5377
  stdio: ["ignore", "pipe", "pipe"]
@@ -4807,7 +5379,7 @@ function defaultBranchFromGit(cwd) {
4807
5379
  return ref.replace("refs/remotes/origin/", "");
4808
5380
  } catch {
4809
5381
  try {
4810
- return execFileSync16("git", ["branch", "--show-current"], {
5382
+ return execFileSync17("git", ["branch", "--show-current"], {
4811
5383
  cwd,
4812
5384
  encoding: "utf-8",
4813
5385
  stdio: ["ignore", "pipe", "pipe"]
@@ -4823,33 +5395,33 @@ function performInit(cwd, force) {
4823
5395
  const pm = detectPackageManager(cwd);
4824
5396
  const ownerRepo = detectOwnerRepo(cwd);
4825
5397
  const defaultBranch = defaultBranchFromGit(cwd);
4826
- const configPath = path19.join(cwd, "kody.config.json");
4827
- if (fs21.existsSync(configPath) && !force) {
5398
+ const configPath = path20.join(cwd, "kody.config.json");
5399
+ if (fs22.existsSync(configPath) && !force) {
4828
5400
  skipped.push("kody.config.json");
4829
5401
  } else {
4830
5402
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
4831
- fs21.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5403
+ fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
4832
5404
  `);
4833
5405
  wrote.push("kody.config.json");
4834
5406
  }
4835
- const workflowDir = path19.join(cwd, ".github", "workflows");
4836
- const workflowPath = path19.join(workflowDir, "kody.yml");
4837
- if (fs21.existsSync(workflowPath) && !force) {
5407
+ const workflowDir = path20.join(cwd, ".github", "workflows");
5408
+ const workflowPath = path20.join(workflowDir, "kody.yml");
5409
+ if (fs22.existsSync(workflowPath) && !force) {
4838
5410
  skipped.push(".github/workflows/kody.yml");
4839
5411
  } else {
4840
- fs21.mkdirSync(workflowDir, { recursive: true });
4841
- fs21.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5412
+ fs22.mkdirSync(workflowDir, { recursive: true });
5413
+ fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
4842
5414
  wrote.push(".github/workflows/kody.yml");
4843
5415
  }
4844
- const hasUi = fs21.existsSync(path19.join(cwd, "src/app")) || fs21.existsSync(path19.join(cwd, "app")) || fs21.existsSync(path19.join(cwd, "pages"));
5416
+ const hasUi = fs22.existsSync(path20.join(cwd, "src/app")) || fs22.existsSync(path20.join(cwd, "app")) || fs22.existsSync(path20.join(cwd, "pages"));
4845
5417
  if (hasUi) {
4846
- const qaGuidePath = path19.join(cwd, QA_GUIDE_REL_PATH);
4847
- if (fs21.existsSync(qaGuidePath) && !force) {
5418
+ const qaGuidePath = path20.join(cwd, QA_GUIDE_REL_PATH);
5419
+ if (fs22.existsSync(qaGuidePath) && !force) {
4848
5420
  skipped.push(QA_GUIDE_REL_PATH);
4849
5421
  } else {
4850
- fs21.mkdirSync(path19.dirname(qaGuidePath), { recursive: true });
5422
+ fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
4851
5423
  const discovery = runQaDiscovery(cwd);
4852
- fs21.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5424
+ fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
4853
5425
  wrote.push(QA_GUIDE_REL_PATH);
4854
5426
  }
4855
5427
  }
@@ -4861,12 +5433,12 @@ function performInit(cwd, force) {
4861
5433
  continue;
4862
5434
  }
4863
5435
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
4864
- const target = path19.join(workflowDir, `kody-${exe.name}.yml`);
4865
- if (fs21.existsSync(target) && !force) {
5436
+ const target = path20.join(workflowDir, `kody-${exe.name}.yml`);
5437
+ if (fs22.existsSync(target) && !force) {
4866
5438
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
4867
5439
  continue;
4868
5440
  }
4869
- fs21.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5441
+ fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
4870
5442
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
4871
5443
  }
4872
5444
  let labels;
@@ -5004,8 +5576,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
5004
5576
  };
5005
5577
 
5006
5578
  // src/scripts/loadJobFromFile.ts
5007
- import * as fs22 from "fs";
5008
- import * as path20 from "path";
5579
+ import * as fs23 from "fs";
5580
+ import * as path21 from "path";
5009
5581
  var loadJobFromFile = async (ctx, _profile, args) => {
5010
5582
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
5011
5583
  const slugArg = String(args?.slugArg ?? "job");
@@ -5013,11 +5585,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
5013
5585
  if (!slug) {
5014
5586
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
5015
5587
  }
5016
- const absPath = path20.join(ctx.cwd, jobsDir, `${slug}.md`);
5017
- if (!fs22.existsSync(absPath)) {
5588
+ const absPath = path21.join(ctx.cwd, jobsDir, `${slug}.md`);
5589
+ if (!fs23.existsSync(absPath)) {
5018
5590
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
5019
5591
  }
5020
- const raw = fs22.readFileSync(absPath, "utf-8");
5592
+ const raw = fs23.readFileSync(absPath, "utf-8");
5021
5593
  const { title, body } = parseJobFile(raw, slug);
5022
5594
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
5023
5595
  const loaded = await backend.load(slug);
@@ -5151,16 +5723,16 @@ var loadTaskState = async (ctx) => {
5151
5723
  };
5152
5724
 
5153
5725
  // src/scripts/loadVaultContext.ts
5154
- import * as fs23 from "fs";
5155
- import * as path21 from "path";
5726
+ import * as fs24 from "fs";
5727
+ import * as path22 from "path";
5156
5728
  var VAULT_DIR_RELATIVE = ".kody/vault";
5157
5729
  var MAX_PAGES = 8;
5158
5730
  var PER_PAGE_MAX_BYTES = 4e3;
5159
5731
  var TOTAL_MAX_BYTES2 = 24e3;
5160
5732
  var TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
5161
5733
  var loadVaultContext = async (ctx) => {
5162
- const vaultAbs = path21.join(ctx.cwd, VAULT_DIR_RELATIVE);
5163
- if (!fs23.existsSync(vaultAbs)) {
5734
+ const vaultAbs = path22.join(ctx.cwd, VAULT_DIR_RELATIVE);
5735
+ if (!fs24.existsSync(vaultAbs)) {
5164
5736
  ctx.data.vaultContext = "";
5165
5737
  return;
5166
5738
  }
@@ -5185,21 +5757,21 @@ function collectPages(vaultAbs) {
5185
5757
  walkMd(vaultAbs, (file) => {
5186
5758
  let stat;
5187
5759
  try {
5188
- stat = fs23.statSync(file);
5760
+ stat = fs24.statSync(file);
5189
5761
  } catch {
5190
5762
  return;
5191
5763
  }
5192
5764
  let raw;
5193
5765
  try {
5194
- raw = fs23.readFileSync(file, "utf-8");
5766
+ raw = fs24.readFileSync(file, "utf-8");
5195
5767
  } catch {
5196
5768
  return;
5197
5769
  }
5198
5770
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5199
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path21.basename(file, ".md");
5771
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path22.basename(file, ".md");
5200
5772
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
5201
5773
  out.push({
5202
- relPath: path21.relative(vaultAbs, file),
5774
+ relPath: path22.relative(vaultAbs, file),
5203
5775
  title,
5204
5776
  updated,
5205
5777
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -5267,16 +5839,16 @@ function walkMd(root, visit) {
5267
5839
  const dir = stack.pop();
5268
5840
  let names;
5269
5841
  try {
5270
- names = fs23.readdirSync(dir);
5842
+ names = fs24.readdirSync(dir);
5271
5843
  } catch {
5272
5844
  continue;
5273
5845
  }
5274
5846
  for (const name of names) {
5275
5847
  if (name.startsWith(".")) continue;
5276
- const full = path21.join(dir, name);
5848
+ const full = path22.join(dir, name);
5277
5849
  let stat;
5278
5850
  try {
5279
- stat = fs23.statSync(full);
5851
+ stat = fs24.statSync(full);
5280
5852
  } catch {
5281
5853
  continue;
5282
5854
  }
@@ -5298,16 +5870,16 @@ var markFlowSuccess = async (ctx) => {
5298
5870
  };
5299
5871
 
5300
5872
  // src/scripts/memorizeFlow.ts
5301
- import { execFileSync as execFileSync17 } from "child_process";
5302
- import * as fs24 from "fs";
5303
- import * as path22 from "path";
5873
+ import { execFileSync as execFileSync18 } from "child_process";
5874
+ import * as fs25 from "fs";
5875
+ import * as path23 from "path";
5304
5876
  var VAULT_DIR_RELATIVE2 = ".kody/vault";
5305
5877
  var DEFAULT_LOOKBACK_HOURS = 36;
5306
5878
  var MAX_RECENT_PRS = 25;
5307
5879
  var MAX_VAULT_INDEX_ENTRIES = 200;
5308
5880
  var PR_BODY_TRUNC = 2e3;
5309
5881
  var memorizeFlow = async (ctx) => {
5310
- const vaultAbs = path22.join(ctx.cwd, VAULT_DIR_RELATIVE2);
5882
+ const vaultAbs = path23.join(ctx.cwd, VAULT_DIR_RELATIVE2);
5311
5883
  ensureBranch(ctx, vaultAbs);
5312
5884
  if (ctx.skipAgent) return;
5313
5885
  const sinceIso = computeSinceIso(vaultAbs);
@@ -5317,8 +5889,8 @@ var memorizeFlow = async (ctx) => {
5317
5889
  const recent = fetchRecentPrs(ctx.cwd, sinceIso);
5318
5890
  ctx.data.recentPrs = formatRecentPrs(recent);
5319
5891
  ctx.data.recentPrCount = recent.length;
5320
- if (!fs24.existsSync(vaultAbs)) {
5321
- fs24.mkdirSync(vaultAbs, { recursive: true });
5892
+ if (!fs25.existsSync(vaultAbs)) {
5893
+ fs25.mkdirSync(vaultAbs, { recursive: true });
5322
5894
  }
5323
5895
  ctx.data.vaultIndex = formatVaultIndex(vaultAbs);
5324
5896
  if (recent.length === 0) {
@@ -5350,18 +5922,18 @@ function ensureBranch(ctx, vaultAbs) {
5350
5922
  }
5351
5923
  }
5352
5924
  ctx.data.branch = branch;
5353
- if (!fs24.existsSync(vaultAbs)) {
5354
- fs24.mkdirSync(vaultAbs, { recursive: true });
5925
+ if (!fs25.existsSync(vaultAbs)) {
5926
+ fs25.mkdirSync(vaultAbs, { recursive: true });
5355
5927
  }
5356
5928
  }
5357
5929
  function computeSinceIso(vaultAbs) {
5358
5930
  const fallback = new Date(Date.now() - DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1e3).toISOString();
5359
- if (!fs24.existsSync(vaultAbs)) return fallback;
5931
+ if (!fs25.existsSync(vaultAbs)) return fallback;
5360
5932
  let latest = "";
5361
5933
  walkMd2(vaultAbs, (file) => {
5362
5934
  let raw;
5363
5935
  try {
5364
- raw = fs24.readFileSync(file, "utf-8");
5936
+ raw = fs25.readFileSync(file, "utf-8");
5365
5937
  } catch {
5366
5938
  return;
5367
5939
  }
@@ -5438,10 +6010,10 @@ function formatVaultIndex(vaultAbs) {
5438
6010
  const entries = [];
5439
6011
  walkMd2(vaultAbs, (file) => {
5440
6012
  if (entries.length >= MAX_VAULT_INDEX_ENTRIES) return;
5441
- const rel = path22.relative(vaultAbs, file);
6013
+ const rel = path23.relative(vaultAbs, file);
5442
6014
  let title = rel;
5443
6015
  try {
5444
- const raw = fs24.readFileSync(file, "utf-8");
6016
+ const raw = fs25.readFileSync(file, "utf-8");
5445
6017
  const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5446
6018
  const titleMatch = m?.[1]?.match(/^title:\s*(.+)$/m);
5447
6019
  if (titleMatch) title = `${titleMatch[1].trim()} (${rel})`;
@@ -5453,22 +6025,22 @@ function formatVaultIndex(vaultAbs) {
5453
6025
  return entries.join("\n");
5454
6026
  }
5455
6027
  function walkMd2(root, visit) {
5456
- if (!fs24.existsSync(root)) return;
6028
+ if (!fs25.existsSync(root)) return;
5457
6029
  const stack = [root];
5458
6030
  while (stack.length > 0) {
5459
6031
  const dir = stack.pop();
5460
6032
  let names;
5461
6033
  try {
5462
- names = fs24.readdirSync(dir);
6034
+ names = fs25.readdirSync(dir);
5463
6035
  } catch {
5464
6036
  continue;
5465
6037
  }
5466
6038
  for (const name of names) {
5467
6039
  if (name.startsWith(".")) continue;
5468
- const full = path22.join(dir, name);
6040
+ const full = path23.join(dir, name);
5469
6041
  let stat;
5470
6042
  try {
5471
- stat = fs24.statSync(full);
6043
+ stat = fs25.statSync(full);
5472
6044
  } catch {
5473
6045
  continue;
5474
6046
  }
@@ -5481,7 +6053,7 @@ function walkMd2(root, visit) {
5481
6053
  }
5482
6054
  }
5483
6055
  function git3(args, cwd) {
5484
- return execFileSync17("git", args, {
6056
+ return execFileSync18("git", args, {
5485
6057
  encoding: "utf-8",
5486
6058
  timeout: 3e4,
5487
6059
  cwd,
@@ -5491,7 +6063,7 @@ function git3(args, cwd) {
5491
6063
  }
5492
6064
 
5493
6065
  // src/scripts/mergeReleasePr.ts
5494
- import { execFileSync as execFileSync18 } from "child_process";
6066
+ import { execFileSync as execFileSync19 } from "child_process";
5495
6067
  var API_TIMEOUT_MS7 = 6e4;
5496
6068
  var mergeReleasePr = async (ctx) => {
5497
6069
  const state = ctx.data.taskState;
@@ -5510,7 +6082,7 @@ var mergeReleasePr = async (ctx) => {
5510
6082
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
5511
6083
  `);
5512
6084
  try {
5513
- const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
6085
+ const out = execFileSync19("gh", ["pr", "merge", String(prNumber), "--merge"], {
5514
6086
  timeout: API_TIMEOUT_MS7,
5515
6087
  cwd: ctx.cwd,
5516
6088
  stdio: ["ignore", "pipe", "pipe"]
@@ -5610,77 +6182,13 @@ function composeBody({ label, exit, prUrl, reason, dryRun }) {
5610
6182
  return `\u2705 kody ${label} complete`;
5611
6183
  }
5612
6184
 
5613
- // src/scripts/postReviewResult.ts
5614
- function detectVerdict(body) {
5615
- const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
5616
- if (!m) return "UNKNOWN";
5617
- return m[1].toUpperCase();
5618
- }
5619
- function reviewAction(verdict, payload) {
5620
- const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
5621
- return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5622
- }
5623
- function failedAction2(reason) {
5624
- return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5625
- }
5626
- var postReviewResult = async (ctx, _profile, agentResult) => {
5627
- const prNumber = ctx.data.commentTargetNumber;
5628
- if (!prNumber) {
5629
- ctx.output.exitCode = 99;
5630
- ctx.output.reason = "review postflight: no PR number in context";
5631
- ctx.data.action = failedAction2(ctx.output.reason);
5632
- return;
5633
- }
5634
- if (!agentResult || agentResult.outcome !== "completed") {
5635
- const reason = agentResult?.error ?? "agent did not complete";
5636
- try {
5637
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
5638
- } catch {
5639
- }
5640
- ctx.output.exitCode = 1;
5641
- ctx.output.reason = reason;
5642
- ctx.data.action = failedAction2(reason);
5643
- return;
5644
- }
5645
- const reviewBody = agentResult.finalText.trim();
5646
- if (!reviewBody) {
5647
- try {
5648
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
5649
- } catch {
5650
- }
5651
- ctx.output.exitCode = 1;
5652
- ctx.output.reason = "empty review body";
5653
- ctx.data.action = failedAction2("empty review body");
5654
- return;
5655
- }
5656
- try {
5657
- postPrReviewComment(prNumber, reviewBody, ctx.cwd);
5658
- } catch (err) {
5659
- const msg = err instanceof Error ? err.message : String(err);
5660
- ctx.output.exitCode = 4;
5661
- ctx.output.reason = `failed to post review comment: ${msg}`;
5662
- ctx.data.action = failedAction2(ctx.output.reason);
5663
- return;
5664
- }
5665
- const verdict = detectVerdict(reviewBody);
5666
- ctx.data.reviewVerdict = verdict;
5667
- ctx.data.reviewBody = reviewBody;
5668
- ctx.data.action = reviewAction(verdict, { bodyPreview: truncate2(reviewBody, 500) });
5669
- ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5670
- process.stdout.write(
5671
- `
5672
- REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
5673
- `
5674
- );
5675
- };
5676
-
5677
6185
  // src/scripts/openQaIssue.ts
5678
6186
  var QA_LABEL = "kody:qa-report";
5679
- function qaAction(verdict, payload) {
6187
+ function qaAction2(verdict, payload) {
5680
6188
  const type = verdict === "PASS" ? "QA_PASS" : verdict === "CONCERNS" ? "QA_CONCERNS" : verdict === "FAIL" ? "QA_FAIL" : "QA_COMPLETED";
5681
6189
  return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5682
6190
  }
5683
- function failedAction3(reason) {
6191
+ function failedAction4(reason) {
5684
6192
  return { type: "QA_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5685
6193
  }
5686
6194
  function slugifyScope(scope) {
@@ -5692,7 +6200,7 @@ function buildIssueTitle(scope, verdict) {
5692
6200
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
5693
6201
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
5694
6202
  }
5695
- function ensureLabel(cwd) {
6203
+ function ensureLabel2(cwd) {
5696
6204
  try {
5697
6205
  gh2(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
5698
6206
  return true;
@@ -5716,7 +6224,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5716
6224
  `);
5717
6225
  ctx.output.exitCode = 1;
5718
6226
  ctx.output.reason = reason;
5719
- ctx.data.action = failedAction3(reason);
6227
+ ctx.data.action = failedAction4(reason);
5720
6228
  return;
5721
6229
  }
5722
6230
  const reportBody = agentResult.finalText.trim();
@@ -5724,7 +6232,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5724
6232
  process.stderr.write("qa-engineer: agent produced no report body\n");
5725
6233
  ctx.output.exitCode = 1;
5726
6234
  ctx.output.reason = "empty report body";
5727
- ctx.data.action = failedAction3("empty report body");
6235
+ ctx.data.action = failedAction4("empty report body");
5728
6236
  return;
5729
6237
  }
5730
6238
  const verdict = detectVerdict(reportBody);
@@ -5738,7 +6246,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5738
6246
  const msg = err instanceof Error ? err.message : String(err);
5739
6247
  ctx.output.exitCode = 4;
5740
6248
  ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
5741
- ctx.data.action = failedAction3(ctx.output.reason);
6249
+ ctx.data.action = failedAction4(ctx.output.reason);
5742
6250
  return;
5743
6251
  }
5744
6252
  process.stdout.write(
@@ -5746,13 +6254,13 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5746
6254
  QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
5747
6255
  `
5748
6256
  );
5749
- ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
6257
+ ctx.data.action = qaAction2(verdict, { issueNumber: existingIssue, mode: "comment" });
5750
6258
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5751
6259
  return;
5752
6260
  }
5753
6261
  const scope = ctx.args.scope;
5754
6262
  const title = buildIssueTitle(scope, verdict);
5755
- const hasLabel = ensureLabel(ctx.cwd);
6263
+ const hasLabel = ensureLabel2(ctx.cwd);
5756
6264
  let created;
5757
6265
  try {
5758
6266
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -5760,13 +6268,13 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
5760
6268
  const msg = err instanceof Error ? err.message : String(err);
5761
6269
  ctx.output.exitCode = 4;
5762
6270
  ctx.output.reason = `failed to open QA issue: ${truncate2(msg, 1e3)}`;
5763
- ctx.data.action = failedAction3(ctx.output.reason);
6271
+ ctx.data.action = failedAction4(ctx.output.reason);
5764
6272
  return;
5765
6273
  }
5766
6274
  process.stdout.write(`
5767
6275
  QA_REPORT_POSTED=${created.url} (verdict: ${verdict})
5768
6276
  `);
5769
- ctx.data.action = qaAction(verdict, {
6277
+ ctx.data.action = qaAction2(verdict, {
5770
6278
  issueNumber: created.number,
5771
6279
  issueUrl: created.url,
5772
6280
  titleSlug: scope ? slugifyScope(scope) : "smoke",
@@ -6150,7 +6658,7 @@ ${body}`;
6150
6658
  }
6151
6659
 
6152
6660
  // src/scripts/recordClassification.ts
6153
- import { execFileSync as execFileSync19 } from "child_process";
6661
+ import { execFileSync as execFileSync20 } from "child_process";
6154
6662
  var API_TIMEOUT_MS8 = 3e4;
6155
6663
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6156
6664
  var recordClassification = async (ctx) => {
@@ -6168,7 +6676,7 @@ var recordClassification = async (ctx) => {
6168
6676
  reason = parsed?.reason ?? null;
6169
6677
  }
6170
6678
  if (!classification) {
6171
- ctx.data.action = failedAction4("classification missing or invalid");
6679
+ ctx.data.action = failedAction5("classification missing or invalid");
6172
6680
  tryAuditComment(
6173
6681
  issueNumber,
6174
6682
  "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
@@ -6198,7 +6706,7 @@ function parseClassification(prSummary) {
6198
6706
  }
6199
6707
  function tryAuditComment(issueNumber, body, cwd) {
6200
6708
  try {
6201
- execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6709
+ execFileSync20("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6202
6710
  cwd,
6203
6711
  timeout: API_TIMEOUT_MS8,
6204
6712
  stdio: ["ignore", "pipe", "pipe"]
@@ -6209,7 +6717,7 @@ function tryAuditComment(issueNumber, body, cwd) {
6209
6717
  function makeAction3(type, payload) {
6210
6718
  return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6211
6719
  }
6212
- function failedAction4(reason) {
6720
+ function failedAction5(reason) {
6213
6721
  return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6214
6722
  }
6215
6723
 
@@ -6248,12 +6756,12 @@ function fail(ctx, profile, reason) {
6248
6756
  ctx.data.agentDone = false;
6249
6757
  ctx.data.agentFailureReason = reason;
6250
6758
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6251
- const failedAction5 = {
6759
+ const failedAction6 = {
6252
6760
  type: `${modeSeg}_FAILED`,
6253
6761
  payload: { reason },
6254
6762
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6255
6763
  };
6256
- ctx.data.action = failedAction5;
6764
+ ctx.data.action = failedAction6;
6257
6765
  }
6258
6766
  function countActionItems(block) {
6259
6767
  if (!block.trim()) return 0;
@@ -6294,12 +6802,12 @@ function fail2(ctx, profile, reason) {
6294
6802
  ctx.data.agentDone = false;
6295
6803
  ctx.data.agentFailureReason = reason;
6296
6804
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6297
- const failedAction5 = {
6805
+ const failedAction6 = {
6298
6806
  type: `${modeSeg}_FAILED`,
6299
6807
  payload: { reason },
6300
6808
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6301
6809
  };
6302
- ctx.data.action = failedAction5;
6810
+ ctx.data.action = failedAction6;
6303
6811
  }
6304
6812
 
6305
6813
  // src/scripts/resolveArtifacts.ts
@@ -6326,7 +6834,7 @@ var resolveArtifacts = async (ctx, profile) => {
6326
6834
  };
6327
6835
 
6328
6836
  // src/scripts/resolveFlow.ts
6329
- import { execFileSync as execFileSync20 } from "child_process";
6837
+ import { execFileSync as execFileSync21 } from "child_process";
6330
6838
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
6331
6839
  var resolveFlow = async (ctx) => {
6332
6840
  const prNumber = ctx.args.pr;
@@ -6396,7 +6904,7 @@ function buildPreferBlock(prefer, baseBranch) {
6396
6904
  }
6397
6905
  function getConflictedFiles(cwd) {
6398
6906
  try {
6399
- const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
6907
+ const out = execFileSync21("git", ["diff", "--name-only", "--diff-filter=U"], {
6400
6908
  encoding: "utf-8",
6401
6909
  cwd,
6402
6910
  env: { ...process.env, HUSKY: "0" }
@@ -6411,7 +6919,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
6411
6919
  let total = 0;
6412
6920
  for (const f of files) {
6413
6921
  try {
6414
- const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
6922
+ const content = execFileSync21("cat", [f], { encoding: "utf-8", cwd }).toString();
6415
6923
  const snippet = `### ${f}
6416
6924
 
6417
6925
  \`\`\`
@@ -6511,8 +7019,81 @@ var resolvePreviewUrl = async (ctx) => {
6511
7019
  ctx.data.previewUrlSource = "default";
6512
7020
  };
6513
7021
 
7022
+ // src/scripts/resolveQaUrl.ts
7023
+ import { execFileSync as execFileSync22 } from "child_process";
7024
+ function ghQuery(args, cwd) {
7025
+ try {
7026
+ const out = execFileSync22("gh", args, {
7027
+ cwd,
7028
+ stdio: ["ignore", "pipe", "pipe"],
7029
+ encoding: "utf-8",
7030
+ timeout: 15e3
7031
+ }).trim();
7032
+ if (!out) return null;
7033
+ return JSON.parse(out);
7034
+ } catch {
7035
+ return null;
7036
+ }
7037
+ }
7038
+ function lookupGoalDeploymentUrl(goalId, owner, repo, cwd) {
7039
+ const ref = `goal-${goalId}`;
7040
+ const deployments = ghQuery(
7041
+ ["api", `repos/${owner}/${repo}/deployments?ref=${encodeURIComponent(ref)}&per_page=5`],
7042
+ cwd
7043
+ );
7044
+ if (!deployments || deployments.length === 0) return null;
7045
+ for (const d of deployments) {
7046
+ const statuses = ghQuery(
7047
+ ["api", `repos/${owner}/${repo}/deployments/${d.id}/statuses?per_page=10`],
7048
+ cwd
7049
+ );
7050
+ if (!statuses || statuses.length === 0) continue;
7051
+ const success = statuses.find((s) => s.state === "success");
7052
+ if (!success) continue;
7053
+ const url = success.environment_url || success.target_url;
7054
+ if (typeof url === "string" && /^https?:\/\//.test(url)) return url;
7055
+ }
7056
+ return null;
7057
+ }
7058
+ var resolveQaUrl = async (ctx) => {
7059
+ const explicit = ctx.args.url?.trim();
7060
+ if (explicit && explicit.length > 0) {
7061
+ ctx.data.previewUrl = explicit;
7062
+ ctx.data.previewUrlSource = "--url flag";
7063
+ return;
7064
+ }
7065
+ const goal = ctx.args.goal?.trim();
7066
+ if (goal && goal.length > 0) {
7067
+ const url = lookupGoalDeploymentUrl(goal, ctx.config.github.owner, ctx.config.github.repo, ctx.cwd);
7068
+ if (url) {
7069
+ ctx.data.previewUrl = url;
7070
+ ctx.data.previewUrlSource = `goal-${goal} latest Vercel deployment`;
7071
+ return;
7072
+ }
7073
+ process.stderr.write(
7074
+ `[resolveQaUrl] no successful deployment found for ref goal-${goal}; falling back to env/config
7075
+ `
7076
+ );
7077
+ }
7078
+ const envUrl = process.env.PREVIEW_URL?.trim();
7079
+ if (envUrl && envUrl.length > 0) {
7080
+ ctx.data.previewUrl = envUrl;
7081
+ ctx.data.previewUrlSource = "$PREVIEW_URL env var";
7082
+ return;
7083
+ }
7084
+ const fallback = ctx.config.qa?.fallbackUrl?.trim();
7085
+ if (fallback && fallback.length > 0) {
7086
+ ctx.data.previewUrl = fallback;
7087
+ ctx.data.previewUrlSource = "kody.config.json qa.fallbackUrl";
7088
+ return;
7089
+ }
7090
+ throw new Error(
7091
+ "qa-engineer: no URL resolved. Pass --url, set --goal <id> on a goal that has a Vercel preview, set $PREVIEW_URL, or configure qa.fallbackUrl in kody.config.json."
7092
+ );
7093
+ };
7094
+
6514
7095
  // src/scripts/revertFlow.ts
6515
- import { execFileSync as execFileSync21 } from "child_process";
7096
+ import { execFileSync as execFileSync23 } from "child_process";
6516
7097
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
6517
7098
  var revertFlow = async (ctx) => {
6518
7099
  const prNumber = ctx.args.pr;
@@ -6594,7 +7175,7 @@ function buildPrSummary(resolved) {
6594
7175
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
6595
7176
  }
6596
7177
  function git4(args, cwd) {
6597
- return execFileSync21("git", args, {
7178
+ return execFileSync23("git", args, {
6598
7179
  encoding: "utf-8",
6599
7180
  timeout: 3e4,
6600
7181
  cwd,
@@ -6604,7 +7185,7 @@ function git4(args, cwd) {
6604
7185
  }
6605
7186
  function isAncestorOfHead(sha, cwd) {
6606
7187
  try {
6607
- execFileSync21("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7188
+ execFileSync23("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
6608
7189
  cwd,
6609
7190
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
6610
7191
  stdio: ["ignore", "ignore", "ignore"]
@@ -6782,11 +7363,11 @@ var skipAgent = async (ctx) => {
6782
7363
  };
6783
7364
 
6784
7365
  // src/scripts/stageMergeConflicts.ts
6785
- import { execFileSync as execFileSync22 } from "child_process";
7366
+ import { execFileSync as execFileSync24 } from "child_process";
6786
7367
  var stageMergeConflicts = async (ctx) => {
6787
7368
  if (ctx.data.agentDone === false) return;
6788
7369
  try {
6789
- execFileSync22("git", ["add", "-A"], {
7370
+ execFileSync24("git", ["add", "-A"], {
6790
7371
  cwd: ctx.cwd,
6791
7372
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
6792
7373
  stdio: "pipe"
@@ -6796,7 +7377,7 @@ var stageMergeConflicts = async (ctx) => {
6796
7377
  };
6797
7378
 
6798
7379
  // src/scripts/startFlow.ts
6799
- import { execFileSync as execFileSync23 } from "child_process";
7380
+ import { execFileSync as execFileSync25 } from "child_process";
6800
7381
  var API_TIMEOUT_MS9 = 3e4;
6801
7382
  var startFlow = async (ctx, profile, _agentResult, args) => {
6802
7383
  const entry = args?.entry;
@@ -6830,7 +7411,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
6830
7411
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
6831
7412
  const body = `@kody ${next}`;
6832
7413
  try {
6833
- execFileSync23("gh", [sub, "comment", String(targetNumber), "--body", body], {
7414
+ execFileSync25("gh", [sub, "comment", String(targetNumber), "--body", body], {
6834
7415
  timeout: API_TIMEOUT_MS9,
6835
7416
  cwd,
6836
7417
  stdio: ["ignore", "pipe", "pipe"]
@@ -6844,7 +7425,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
6844
7425
  }
6845
7426
 
6846
7427
  // src/scripts/syncFlow.ts
6847
- import { execFileSync as execFileSync24 } from "child_process";
7428
+ import { execFileSync as execFileSync26 } from "child_process";
6848
7429
  var syncFlow = async (ctx, _profile, args) => {
6849
7430
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
6850
7431
  const prNumber = ctx.args.pr;
@@ -6916,7 +7497,7 @@ function bail2(ctx, prNumber, reason) {
6916
7497
  }
6917
7498
  function revParseHead(cwd) {
6918
7499
  try {
6919
- return execFileSync24("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
7500
+ return execFileSync26("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
6920
7501
  } catch {
6921
7502
  return "";
6922
7503
  }
@@ -6924,9 +7505,9 @@ function revParseHead(cwd) {
6924
7505
  function pushBranch(branch, cwd) {
6925
7506
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
6926
7507
  try {
6927
- execFileSync24("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
7508
+ execFileSync26("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
6928
7509
  } catch {
6929
- execFileSync24("git", ["push", "--force-with-lease", "-u", "origin", branch], {
7510
+ execFileSync26("git", ["push", "--force-with-lease", "-u", "origin", branch], {
6930
7511
  cwd,
6931
7512
  env,
6932
7513
  stdio: ["ignore", "pipe", "pipe"]
@@ -7153,7 +7734,7 @@ function downgrade2(ctx, reason) {
7153
7734
  }
7154
7735
 
7155
7736
  // src/scripts/waitForCi.ts
7156
- import { execFileSync as execFileSync25 } from "child_process";
7737
+ import { execFileSync as execFileSync27 } from "child_process";
7157
7738
  var API_TIMEOUT_MS10 = 3e4;
7158
7739
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
7159
7740
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -7231,7 +7812,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
7231
7812
  };
7232
7813
  function fetchChecks(prNumber, cwd) {
7233
7814
  try {
7234
- const raw = execFileSync25("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
7815
+ const raw = execFileSync27("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
7235
7816
  encoding: "utf-8",
7236
7817
  timeout: API_TIMEOUT_MS10,
7237
7818
  cwd,
@@ -7277,6 +7858,151 @@ function sleep2(ms) {
7277
7858
  return new Promise((res) => setTimeout(res, ms));
7278
7859
  }
7279
7860
 
7861
+ // src/scripts/warmupMcp.ts
7862
+ import { spawn as spawn4 } from "child_process";
7863
+ var PER_SERVER_TIMEOUT_MS = 6e4;
7864
+ var PER_REQUEST_TIMEOUT_MS = 2e4;
7865
+ var warmupMcp = async (_ctx, profile) => {
7866
+ const servers = profile.claudeCode.mcpServers ?? [];
7867
+ if (servers.length === 0) return;
7868
+ for (const s of servers) {
7869
+ const start = Date.now();
7870
+ try {
7871
+ const result = await warmupOne(s.command, s.args ?? [], s.env);
7872
+ const ms = Date.now() - start;
7873
+ process.stderr.write(`[kody warmup] ${s.name}: ${result.toolCount} tools (${ms}ms)
7874
+ `);
7875
+ } catch (err) {
7876
+ const ms = Date.now() - start;
7877
+ const reason = err instanceof Error ? err.message : String(err);
7878
+ process.stderr.write(`[kody warmup] ${s.name} FAILED after ${ms}ms: ${reason}
7879
+ `);
7880
+ }
7881
+ }
7882
+ };
7883
+ async function warmupOne(command, args, env) {
7884
+ const child = spawn4(command, args, {
7885
+ stdio: ["pipe", "pipe", "pipe"],
7886
+ env: env ? { ...process.env, ...env } : process.env
7887
+ });
7888
+ let stderrBuf = "";
7889
+ child.stderr.on("data", (b) => {
7890
+ stderrBuf += b.toString("utf8");
7891
+ if (stderrBuf.length > 4096) stderrBuf = stderrBuf.slice(-4096);
7892
+ });
7893
+ const overallDeadline = Date.now() + PER_SERVER_TIMEOUT_MS;
7894
+ const lines = lineStream(child.stdout);
7895
+ let nextId = 1;
7896
+ const send = (method, params) => {
7897
+ const id = nextId++;
7898
+ const payload = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
7899
+ child.stdin.write(payload);
7900
+ return id;
7901
+ };
7902
+ const notify = (method, params) => {
7903
+ const payload = JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n";
7904
+ child.stdin.write(payload);
7905
+ };
7906
+ const awaitResponse = async (id) => {
7907
+ const reqDeadline = Math.min(Date.now() + PER_REQUEST_TIMEOUT_MS, overallDeadline);
7908
+ while (Date.now() < reqDeadline) {
7909
+ const line = await lines.next(reqDeadline - Date.now());
7910
+ if (line === null) break;
7911
+ let msg = null;
7912
+ try {
7913
+ msg = JSON.parse(line);
7914
+ } catch {
7915
+ continue;
7916
+ }
7917
+ if (msg && msg.id === id) return msg;
7918
+ }
7919
+ throw new Error(`request id=${id} timed out (stderr tail: ${stderrBuf.trim().slice(-300) || "(empty)"})`);
7920
+ };
7921
+ try {
7922
+ const initId = send("initialize", {
7923
+ protocolVersion: "2024-11-05",
7924
+ capabilities: {},
7925
+ clientInfo: { name: "kody-warmup", version: "0.1.0" }
7926
+ });
7927
+ const initResp = await awaitResponse(initId);
7928
+ if (initResp.error) throw new Error(`initialize error: ${initResp.error.message}`);
7929
+ notify("notifications/initialized");
7930
+ const listId = send("tools/list");
7931
+ const listResp = await awaitResponse(listId);
7932
+ if (listResp.error) throw new Error(`tools/list error: ${listResp.error.message}`);
7933
+ const tools = listResp.result?.tools;
7934
+ const toolCount = Array.isArray(tools) ? tools.length : 0;
7935
+ if (toolCount === 0) throw new Error("tools/list returned 0 tools");
7936
+ return { toolCount };
7937
+ } finally {
7938
+ try {
7939
+ child.kill("SIGTERM");
7940
+ } catch {
7941
+ }
7942
+ setTimeout(() => {
7943
+ try {
7944
+ child.kill("SIGKILL");
7945
+ } catch {
7946
+ }
7947
+ }, 2e3).unref();
7948
+ }
7949
+ }
7950
+ function lineStream(stream) {
7951
+ let buf = "";
7952
+ const queue = [];
7953
+ let waiter = null;
7954
+ let ended = false;
7955
+ const tryDeliver = () => {
7956
+ if (waiter && queue.length > 0) {
7957
+ const w = waiter;
7958
+ waiter = null;
7959
+ w(queue.shift());
7960
+ } else if (waiter && ended) {
7961
+ const w = waiter;
7962
+ waiter = null;
7963
+ w(null);
7964
+ }
7965
+ };
7966
+ stream.on("data", (chunk) => {
7967
+ buf += typeof chunk === "string" ? chunk : chunk.toString("utf8");
7968
+ let idx;
7969
+ while ((idx = buf.indexOf("\n")) >= 0) {
7970
+ const line = buf.slice(0, idx).replace(/\r$/, "");
7971
+ buf = buf.slice(idx + 1);
7972
+ if (line.length > 0) queue.push(line);
7973
+ }
7974
+ tryDeliver();
7975
+ });
7976
+ stream.on("end", () => {
7977
+ if (buf.length > 0) {
7978
+ queue.push(buf);
7979
+ buf = "";
7980
+ }
7981
+ ended = true;
7982
+ tryDeliver();
7983
+ });
7984
+ return {
7985
+ next: (timeoutMs) => new Promise((resolve4) => {
7986
+ if (queue.length > 0) {
7987
+ resolve4(queue.shift());
7988
+ return;
7989
+ }
7990
+ if (ended) {
7991
+ resolve4(null);
7992
+ return;
7993
+ }
7994
+ waiter = resolve4;
7995
+ const t = setTimeout(() => {
7996
+ if (waiter === resolve4) {
7997
+ waiter = null;
7998
+ resolve4(null);
7999
+ }
8000
+ }, Math.max(0, timeoutMs));
8001
+ t.unref?.();
8002
+ })
8003
+ };
8004
+ }
8005
+
7280
8006
  // src/scripts/watchStalePrsFlow.ts
7281
8007
  function readWatchConfig(ctx) {
7282
8008
  const cfg = ctx.config.watch;
@@ -7403,7 +8129,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
7403
8129
  };
7404
8130
 
7405
8131
  // src/scripts/writeRunSummary.ts
7406
- import * as fs25 from "fs";
8132
+ import * as fs26 from "fs";
7407
8133
  var writeRunSummary = async (ctx, profile) => {
7408
8134
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
7409
8135
  if (!summaryPath) return;
@@ -7425,7 +8151,7 @@ var writeRunSummary = async (ctx, profile) => {
7425
8151
  if (reason) lines.push(`- **Reason:** ${reason}`);
7426
8152
  lines.push("");
7427
8153
  try {
7428
- fs25.appendFileSync(summaryPath, `${lines.join("\n")}
8154
+ fs26.appendFileSync(summaryPath, `${lines.join("\n")}
7429
8155
  `);
7430
8156
  } catch {
7431
8157
  }
@@ -7456,12 +8182,14 @@ var preflightScripts = {
7456
8182
  resolveArtifacts,
7457
8183
  discoverQaContext,
7458
8184
  resolvePreviewUrl,
8185
+ resolveQaUrl,
7459
8186
  composePrompt,
7460
8187
  setCommentTarget,
7461
8188
  setLifecycleLabel,
7462
8189
  skipAgent,
7463
8190
  classifyByLabel,
7464
8191
  diagMcp,
8192
+ warmupMcp,
7465
8193
  dispatchJobTicks,
7466
8194
  dispatchJobFileTicks
7467
8195
  };
@@ -7499,6 +8227,7 @@ var postflightScripts = {
7499
8227
  dispatchClassified,
7500
8228
  notifyTerminal,
7501
8229
  openQaIssue,
8230
+ createQaGoal,
7502
8231
  recordOutcome,
7503
8232
  mergeReleasePr,
7504
8233
  waitForCi,
@@ -7510,7 +8239,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
7510
8239
  ]);
7511
8240
 
7512
8241
  // src/tools.ts
7513
- import { execFileSync as execFileSync26 } from "child_process";
8242
+ import { execFileSync as execFileSync28 } from "child_process";
7514
8243
  function verifyCliTools(tools, cwd) {
7515
8244
  const out = [];
7516
8245
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -7543,7 +8272,7 @@ function verifyOne(tool, cwd) {
7543
8272
  }
7544
8273
  function runShell(cmd, cwd, timeoutMs = 3e4) {
7545
8274
  try {
7546
- execFileSync26("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8275
+ execFileSync28("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
7547
8276
  return true;
7548
8277
  } catch {
7549
8278
  return false;
@@ -7612,9 +8341,9 @@ async function runExecutable(profileName, input) {
7612
8341
  data: {},
7613
8342
  output: { exitCode: 0 }
7614
8343
  };
7615
- const ndjsonDir = path23.join(input.cwd, ".kody");
8344
+ const ndjsonDir = path24.join(input.cwd, ".kody");
7616
8345
  const invokeAgent = async (prompt) => {
7617
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path23.isAbsolute(p) ? p : path23.resolve(profile.dir, p)).filter((p) => p.length > 0);
8346
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path24.isAbsolute(p) ? p : path24.resolve(profile.dir, p)).filter((p) => p.length > 0);
7618
8347
  const syntheticPath = ctx.data.syntheticPluginPath;
7619
8348
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
7620
8349
  return runAgent({
@@ -7709,17 +8438,17 @@ async function runExecutable(profileName, input) {
7709
8438
  function resolveProfilePath(profileName) {
7710
8439
  const found = resolveExecutable(profileName);
7711
8440
  if (found) return found;
7712
- const here = path23.dirname(new URL(import.meta.url).pathname);
8441
+ const here = path24.dirname(new URL(import.meta.url).pathname);
7713
8442
  const candidates = [
7714
- path23.join(here, "executables", profileName, "profile.json"),
8443
+ path24.join(here, "executables", profileName, "profile.json"),
7715
8444
  // same-dir sibling (dev)
7716
- path23.join(here, "..", "executables", profileName, "profile.json"),
8445
+ path24.join(here, "..", "executables", profileName, "profile.json"),
7717
8446
  // up one (prod: dist/bin → dist/executables)
7718
- path23.join(here, "..", "src", "executables", profileName, "profile.json")
8447
+ path24.join(here, "..", "src", "executables", profileName, "profile.json")
7719
8448
  // fallback
7720
8449
  ];
7721
8450
  for (const c of candidates) {
7722
- if (fs26.existsSync(c)) return c;
8451
+ if (fs27.existsSync(c)) return c;
7723
8452
  }
7724
8453
  return candidates[0];
7725
8454
  }
@@ -7823,8 +8552,8 @@ function resolveShellTimeoutMs(entry) {
7823
8552
  var SIGKILL_GRACE_MS = 5e3;
7824
8553
  async function runShellEntry(entry, ctx, profile) {
7825
8554
  const shellName = entry.shell;
7826
- const shellPath = path23.join(profile.dir, shellName);
7827
- if (!fs26.existsSync(shellPath)) {
8555
+ const shellPath = path24.join(profile.dir, shellName);
8556
+ if (!fs27.existsSync(shellPath)) {
7828
8557
  ctx.skipAgent = true;
7829
8558
  ctx.output.exitCode = 99;
7830
8559
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -7840,7 +8569,7 @@ async function runShellEntry(entry, ctx, profile) {
7840
8569
  env[`KODY_CFG_${k}`] = v;
7841
8570
  }
7842
8571
  const timeoutMs = resolveShellTimeoutMs(entry);
7843
- const child = spawn4("bash", [shellPath, ...positional], {
8572
+ const child = spawn5("bash", [shellPath, ...positional], {
7844
8573
  cwd: ctx.cwd,
7845
8574
  env,
7846
8575
  stdio: ["pipe", "pipe", "pipe"],
@@ -8085,7 +8814,7 @@ async function runContainerLoop(profile, ctx, input) {
8085
8814
  }
8086
8815
  function resetWorkingTree(cwd) {
8087
8816
  try {
8088
- execFileSync27("git", ["reset", "--hard", "HEAD"], {
8817
+ execFileSync29("git", ["reset", "--hard", "HEAD"], {
8089
8818
  cwd,
8090
8819
  stdio: ["ignore", "pipe", "pipe"],
8091
8820
  timeout: 3e4
@@ -8237,14 +8966,14 @@ function resolveAuthToken(env = process.env) {
8237
8966
  return token;
8238
8967
  }
8239
8968
  function detectPackageManager2(cwd) {
8240
- if (fs27.existsSync(path24.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8241
- if (fs27.existsSync(path24.join(cwd, "yarn.lock"))) return "yarn";
8242
- if (fs27.existsSync(path24.join(cwd, "bun.lockb"))) return "bun";
8969
+ if (fs28.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8970
+ if (fs28.existsSync(path25.join(cwd, "yarn.lock"))) return "yarn";
8971
+ if (fs28.existsSync(path25.join(cwd, "bun.lockb"))) return "bun";
8243
8972
  return "npm";
8244
8973
  }
8245
8974
  function shellOut(cmd, args, cwd, stream = true) {
8246
8975
  try {
8247
- execFileSync28(cmd, args, {
8976
+ execFileSync30(cmd, args, {
8248
8977
  cwd,
8249
8978
  stdio: stream ? "inherit" : "pipe",
8250
8979
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -8257,7 +8986,7 @@ function shellOut(cmd, args, cwd, stream = true) {
8257
8986
  }
8258
8987
  function isOnPath(bin) {
8259
8988
  try {
8260
- execFileSync28("which", [bin], { stdio: "pipe" });
8989
+ execFileSync30("which", [bin], { stdio: "pipe" });
8261
8990
  return true;
8262
8991
  } catch {
8263
8992
  return false;
@@ -8298,7 +9027,7 @@ function installLitellmIfNeeded(cwd) {
8298
9027
  } catch {
8299
9028
  }
8300
9029
  try {
8301
- execFileSync28("python3", ["-c", "import litellm"], { stdio: "pipe" });
9030
+ execFileSync30("python3", ["-c", "import litellm"], { stdio: "pipe" });
8302
9031
  process.stdout.write("\u2192 kody: litellm already installed\n");
8303
9032
  return 0;
8304
9033
  } catch {
@@ -8308,16 +9037,16 @@ function installLitellmIfNeeded(cwd) {
8308
9037
  }
8309
9038
  function configureGitIdentity(cwd) {
8310
9039
  try {
8311
- const name = execFileSync28("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9040
+ const name = execFileSync30("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
8312
9041
  if (name) return;
8313
9042
  } catch {
8314
9043
  }
8315
9044
  try {
8316
- execFileSync28("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9045
+ execFileSync30("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
8317
9046
  } catch {
8318
9047
  }
8319
9048
  try {
8320
- execFileSync28("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9049
+ execFileSync30("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
8321
9050
  cwd,
8322
9051
  stdio: "pipe"
8323
9052
  });
@@ -8326,11 +9055,11 @@ function configureGitIdentity(cwd) {
8326
9055
  }
8327
9056
  function postFailureTail(issueNumber, cwd, reason) {
8328
9057
  if (!issueNumber) return;
8329
- const logPath = path24.join(cwd, ".kody", "last-run.jsonl");
9058
+ const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
8330
9059
  let tail = "";
8331
9060
  try {
8332
- if (fs27.existsSync(logPath)) {
8333
- const content = fs27.readFileSync(logPath, "utf-8");
9061
+ if (fs28.existsSync(logPath)) {
9062
+ const content = fs28.readFileSync(logPath, "utf-8");
8334
9063
  tail = content.slice(-3e3);
8335
9064
  }
8336
9065
  } catch {
@@ -8355,7 +9084,7 @@ async function runCi(argv) {
8355
9084
  return 0;
8356
9085
  }
8357
9086
  const args = parseCiArgs(argv);
8358
- const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
9087
+ const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
8359
9088
  let earlyConfig;
8360
9089
  try {
8361
9090
  earlyConfig = loadConfig(cwd);
@@ -8365,9 +9094,9 @@ async function runCi(argv) {
8365
9094
  const eventName = process.env.GITHUB_EVENT_NAME;
8366
9095
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
8367
9096
  let manualWorkflowDispatch = false;
8368
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs27.existsSync(dispatchEventPath)) {
9097
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
8369
9098
  try {
8370
- const evt = JSON.parse(fs27.readFileSync(dispatchEventPath, "utf-8"));
9099
+ const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
8371
9100
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
8372
9101
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
8373
9102
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -8582,15 +9311,15 @@ function parseChatArgs(argv, env = process.env) {
8582
9311
  return result;
8583
9312
  }
8584
9313
  function commitChatFiles(cwd, sessionId, verbose) {
8585
- const sessionFile = path25.relative(cwd, sessionFilePath(cwd, sessionId));
8586
- const eventsFile = path25.relative(cwd, eventsFilePath(cwd, sessionId));
8587
- const paths = [sessionFile, eventsFile].filter((p) => fs28.existsSync(path25.join(cwd, p)));
9314
+ const sessionFile = path26.relative(cwd, sessionFilePath(cwd, sessionId));
9315
+ const eventsFile = path26.relative(cwd, eventsFilePath(cwd, sessionId));
9316
+ const paths = [sessionFile, eventsFile].filter((p) => fs29.existsSync(path26.join(cwd, p)));
8588
9317
  if (paths.length === 0) return;
8589
9318
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
8590
9319
  try {
8591
- execFileSync29("git", ["add", "-f", ...paths], opts);
8592
- execFileSync29("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
8593
- execFileSync29("git", ["push", "--quiet", "origin", "HEAD"], opts);
9320
+ execFileSync31("git", ["add", "-f", ...paths], opts);
9321
+ execFileSync31("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
9322
+ execFileSync31("git", ["push", "--quiet", "origin", "HEAD"], opts);
8594
9323
  } catch (err) {
8595
9324
  const msg = err instanceof Error ? err.message : String(err);
8596
9325
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -8622,7 +9351,7 @@ async function runChat(argv) {
8622
9351
  ${CHAT_HELP}`);
8623
9352
  return 64;
8624
9353
  }
8625
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9354
+ const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
8626
9355
  const sessionId = args.sessionId;
8627
9356
  const unpackedSecrets = unpackAllSecrets();
8628
9357
  if (unpackedSecrets > 0) {
@@ -8674,7 +9403,7 @@ ${CHAT_HELP}`);
8674
9403
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
8675
9404
  const meta = readMeta(sessionFile);
8676
9405
  process.stdout.write(
8677
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs28.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9406
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
8678
9407
  `
8679
9408
  );
8680
9409
  try {