@kody-ade/kody-engine 0.4.13 → 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.13",
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;
@@ -900,9 +912,9 @@ async function emit2(sink, type, sessionId, suffix, payload) {
900
912
  }
901
913
 
902
914
  // src/kody-cli.ts
903
- import { execFileSync as execFileSync28 } from "child_process";
904
- import * as fs27 from "fs";
905
- 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";
906
918
 
907
919
  // src/dispatch.ts
908
920
  import * as fs7 from "fs";
@@ -1261,9 +1273,9 @@ function coerceBare(spec, value) {
1261
1273
  }
1262
1274
 
1263
1275
  // src/executor.ts
1264
- import { execFileSync as execFileSync27, spawn as spawn5 } from "child_process";
1265
- import * as fs26 from "fs";
1266
- 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";
1267
1279
 
1268
1280
  // src/litellm.ts
1269
1281
  import { execFileSync as execFileSync3, spawn } from "child_process";
@@ -2589,118 +2601,803 @@ function formatToolsUsage(profile) {
2589
2601
  return lines.join("\n");
2590
2602
  }
2591
2603
 
2592
- // src/scripts/diagMcp.ts
2593
- import { execFileSync as execFileSync8 } from "child_process";
2604
+ // src/scripts/createQaGoal.ts
2605
+ import { execFileSync as execFileSync9 } from "child_process";
2594
2606
  import * as fs14 from "fs";
2595
- import * as os3 from "os";
2596
2607
  import * as path13 from "path";
2597
- var diagMcp = async (_ctx) => {
2598
- const home = os3.homedir();
2599
- const cacheDir = path13.join(home, ".cache", "ms-playwright");
2600
- let entries = [];
2601
- try {
2602
- entries = fs14.readdirSync(cacheDir);
2603
- } 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`);
2604
2632
  }
2605
- const hasChromium = entries.some((e) => e.startsWith("chromium"));
2606
- process.stderr.write(
2607
- `[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)}
2608
2654
  `
2609
- );
2610
- process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
2611
- `);
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) {
2612
2686
  try {
2613
- const v = execFileSync8("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
2614
- stdio: "pipe",
2615
- timeout: 6e4,
2616
- encoding: "utf8"
2617
- }).trim();
2618
- process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
2619
- `);
2620
- } catch (e) {
2621
- const err = e instanceof Error ? e.message : String(e);
2622
- process.stderr.write(`[kody diag] @playwright/mcp spawn FAILED: ${err}
2623
- `);
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 "";
2624
2694
  }
2625
- };
2626
-
2627
- // src/scripts/discoverQaContext.ts
2628
- import * as fs16 from "fs";
2629
- import * as path15 from "path";
2630
-
2631
- // src/scripts/frameworkDetectors.ts
2632
- import * as fs15 from "fs";
2633
- import * as path14 from "path";
2634
- function detectFrameworks(cwd) {
2635
- const out = [];
2636
- let deps = {};
2695
+ }
2696
+ function getPrReviews(prNumber, cwd) {
2637
2697
  try {
2638
- const pkg = JSON.parse(fs15.readFileSync(path14.join(cwd, "package.json"), "utf-8"));
2639
- 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
+ );
2640
2709
  } catch {
2641
- return out;
2642
- }
2643
- if (deps.payload || deps["@payloadcms/next"]) {
2644
- out.push({
2645
- name: "payload-cms",
2646
- version: deps.payload ?? deps["@payloadcms/next"] ?? null,
2647
- configFile: findFile(cwd, ["payload.config.ts", "payload-config.ts", "src/payload.config.ts"])
2648
- });
2649
- }
2650
- if (deps["next-auth"]) {
2651
- out.push({
2652
- name: "nextauth",
2653
- version: deps["next-auth"] ?? null,
2654
- configFile: findFile(cwd, ["auth.ts", "auth.config.ts", "src/auth.ts", "src/auth.config.ts"])
2655
- });
2656
- }
2657
- if (deps.prisma || deps["@prisma/client"]) {
2658
- out.push({
2659
- name: "prisma",
2660
- version: deps.prisma ?? deps["@prisma/client"] ?? null,
2661
- configFile: findFile(cwd, ["prisma/schema.prisma"])
2662
- });
2710
+ return [];
2663
2711
  }
2664
- if (deps.next) {
2665
- out.push({
2666
- name: "nextjs",
2667
- version: deps.next ?? null,
2668
- configFile: findFile(cwd, ["next.config.ts", "next.config.mjs", "next.config.js"])
2669
- });
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 [];
2670
2725
  }
2671
- return out;
2672
2726
  }
2673
- function findFile(cwd, candidates) {
2674
- for (const c of candidates) {
2675
- 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
+ );
2676
2747
  }
2677
- return null;
2678
2748
  }
2679
- var COLLECTION_DIRS = [
2680
- "src/server/payload/collections",
2681
- "src/payload/collections",
2682
- "src/collections",
2683
- "payload/collections"
2684
- ];
2685
- function discoverPayloadCollections(cwd) {
2686
- const out = [];
2687
- for (const dir of COLLECTION_DIRS) {
2688
- const full = path14.join(cwd, dir);
2689
- if (!fs15.existsSync(full)) continue;
2690
- 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";
2691
2773
  try {
2692
- 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);
2693
2775
  } catch {
2694
- continue;
2695
2776
  }
2696
- for (const file of files) {
2697
- try {
2698
- const filePath = path14.join(full, file);
2699
- const content = fs15.readFileSync(filePath, "utf-8").slice(0, 1e4);
2700
- const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
2701
- if (!slugMatch) continue;
2702
- const slug = slugMatch[1];
2703
- 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)$/, "");
2704
3401
  const fields = [];
2705
3402
  const fieldMatches = content.matchAll(/name:\s*['"]([a-zA-Z_][a-zA-Z0-9_]*)['"]/g);
2706
3403
  for (const m of fieldMatches) {
@@ -2710,7 +3407,7 @@ function discoverPayloadCollections(cwd) {
2710
3407
  out.push({
2711
3408
  name,
2712
3409
  slug,
2713
- filePath: path14.relative(cwd, filePath),
3410
+ filePath: path15.relative(cwd, filePath),
2714
3411
  fields: fields.slice(0, 20),
2715
3412
  hasAdmin
2716
3413
  });
@@ -2724,28 +3421,28 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
2724
3421
  function discoverAdminComponents(cwd, collections) {
2725
3422
  const out = [];
2726
3423
  for (const dir of ADMIN_COMPONENT_DIRS) {
2727
- const full = path14.join(cwd, dir);
2728
- if (!fs15.existsSync(full)) continue;
3424
+ const full = path15.join(cwd, dir);
3425
+ if (!fs16.existsSync(full)) continue;
2729
3426
  let entries;
2730
3427
  try {
2731
- entries = fs15.readdirSync(full, { withFileTypes: true });
3428
+ entries = fs16.readdirSync(full, { withFileTypes: true });
2732
3429
  } catch {
2733
3430
  continue;
2734
3431
  }
2735
3432
  for (const entry of entries) {
2736
- const entryPath = path14.join(full, entry.name);
3433
+ const entryPath = path15.join(full, entry.name);
2737
3434
  let name;
2738
3435
  let filePath;
2739
3436
  if (entry.isDirectory()) {
2740
3437
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
2741
- (f) => fs15.existsSync(path14.join(entryPath, f))
3438
+ (f) => fs16.existsSync(path15.join(entryPath, f))
2742
3439
  );
2743
3440
  if (!indexFile) continue;
2744
3441
  name = entry.name;
2745
- filePath = path14.relative(cwd, path14.join(entryPath, indexFile));
3442
+ filePath = path15.relative(cwd, path15.join(entryPath, indexFile));
2746
3443
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
2747
3444
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
2748
- filePath = path14.relative(cwd, entryPath);
3445
+ filePath = path15.relative(cwd, entryPath);
2749
3446
  } else {
2750
3447
  continue;
2751
3448
  }
@@ -2753,7 +3450,7 @@ function discoverAdminComponents(cwd, collections) {
2753
3450
  if (collections) {
2754
3451
  for (const col of collections) {
2755
3452
  try {
2756
- const colContent = fs15.readFileSync(path14.join(cwd, col.filePath), "utf-8");
3453
+ const colContent = fs16.readFileSync(path15.join(cwd, col.filePath), "utf-8");
2757
3454
  if (colContent.includes(name)) {
2758
3455
  usedInCollection = col.slug;
2759
3456
  break;
@@ -2772,8 +3469,8 @@ function scanApiRoutes(cwd) {
2772
3469
  const out = [];
2773
3470
  const appDirs = ["src/app", "app"];
2774
3471
  for (const appDir of appDirs) {
2775
- const apiDir = path14.join(cwd, appDir, "api");
2776
- if (!fs15.existsSync(apiDir)) continue;
3472
+ const apiDir = path15.join(cwd, appDir, "api");
3473
+ if (!fs16.existsSync(apiDir)) continue;
2777
3474
  walkApiRoutes(apiDir, "/api", cwd, out);
2778
3475
  break;
2779
3476
  }
@@ -2782,14 +3479,14 @@ function scanApiRoutes(cwd) {
2782
3479
  function walkApiRoutes(dir, prefix, cwd, out) {
2783
3480
  let entries;
2784
3481
  try {
2785
- entries = fs15.readdirSync(dir, { withFileTypes: true });
3482
+ entries = fs16.readdirSync(dir, { withFileTypes: true });
2786
3483
  } catch {
2787
3484
  return;
2788
3485
  }
2789
3486
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
2790
3487
  if (routeFile) {
2791
3488
  try {
2792
- 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);
2793
3490
  const methods = HTTP_METHODS.filter(
2794
3491
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
2795
3492
  );
@@ -2797,7 +3494,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2797
3494
  out.push({
2798
3495
  path: prefix,
2799
3496
  methods,
2800
- filePath: path14.relative(cwd, path14.join(dir, routeFile.name))
3497
+ filePath: path15.relative(cwd, path15.join(dir, routeFile.name))
2801
3498
  });
2802
3499
  }
2803
3500
  } catch {
@@ -2808,7 +3505,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2808
3505
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2809
3506
  let segment = entry.name;
2810
3507
  if (segment.startsWith("(") && segment.endsWith(")")) {
2811
- walkApiRoutes(path14.join(dir, entry.name), prefix, cwd, out);
3508
+ walkApiRoutes(path15.join(dir, entry.name), prefix, cwd, out);
2812
3509
  continue;
2813
3510
  }
2814
3511
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2816,7 +3513,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2816
3513
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2817
3514
  segment = `:${segment.slice(1, -1)}`;
2818
3515
  }
2819
- walkApiRoutes(path14.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
3516
+ walkApiRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
2820
3517
  }
2821
3518
  }
2822
3519
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -2836,10 +3533,10 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
2836
3533
  function scanEnvVars(cwd) {
2837
3534
  const candidates = [".env.example", ".env.local.example", ".env.template"];
2838
3535
  for (const envFile of candidates) {
2839
- const envPath = path14.join(cwd, envFile);
2840
- if (!fs15.existsSync(envPath)) continue;
3536
+ const envPath = path15.join(cwd, envFile);
3537
+ if (!fs16.existsSync(envPath)) continue;
2841
3538
  try {
2842
- const content = fs15.readFileSync(envPath, "utf-8");
3539
+ const content = fs16.readFileSync(envPath, "utf-8");
2843
3540
  const vars = [];
2844
3541
  for (const line of content.split("\n")) {
2845
3542
  const trimmed = line.trim();
@@ -2887,9 +3584,9 @@ function runQaDiscovery(cwd) {
2887
3584
  }
2888
3585
  function detectDevServer(cwd, out) {
2889
3586
  try {
2890
- 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"));
2891
3588
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
2892
- 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";
2893
3590
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
2894
3591
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
2895
3592
  else if (allDeps.vite) out.devPort = 5173;
@@ -2899,8 +3596,8 @@ function detectDevServer(cwd, out) {
2899
3596
  function scanFrontendRoutes(cwd, out) {
2900
3597
  const appDirs = ["src/app", "app"];
2901
3598
  for (const appDir of appDirs) {
2902
- const full = path15.join(cwd, appDir);
2903
- if (!fs16.existsSync(full)) continue;
3599
+ const full = path16.join(cwd, appDir);
3600
+ if (!fs17.existsSync(full)) continue;
2904
3601
  walkFrontendRoutes(full, "", out);
2905
3602
  break;
2906
3603
  }
@@ -2908,7 +3605,7 @@ function scanFrontendRoutes(cwd, out) {
2908
3605
  function walkFrontendRoutes(dir, prefix, out) {
2909
3606
  let entries;
2910
3607
  try {
2911
- entries = fs16.readdirSync(dir, { withFileTypes: true });
3608
+ entries = fs17.readdirSync(dir, { withFileTypes: true });
2912
3609
  } catch {
2913
3610
  return;
2914
3611
  }
@@ -2925,7 +3622,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2925
3622
  if (entry.name === "node_modules" || entry.name === ".next") continue;
2926
3623
  let segment = entry.name;
2927
3624
  if (segment.startsWith("(") && segment.endsWith(")")) {
2928
- walkFrontendRoutes(path15.join(dir, entry.name), prefix, out);
3625
+ walkFrontendRoutes(path16.join(dir, entry.name), prefix, out);
2929
3626
  continue;
2930
3627
  }
2931
3628
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -2933,7 +3630,7 @@ function walkFrontendRoutes(dir, prefix, out) {
2933
3630
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
2934
3631
  segment = `:${segment.slice(1, -1)}`;
2935
3632
  }
2936
- walkFrontendRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, out);
3633
+ walkFrontendRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, out);
2937
3634
  }
2938
3635
  }
2939
3636
  function detectAuthFiles(cwd, out) {
@@ -2950,23 +3647,23 @@ function detectAuthFiles(cwd, out) {
2950
3647
  "src/app/api/oauth"
2951
3648
  ];
2952
3649
  for (const c of candidates) {
2953
- if (fs16.existsSync(path15.join(cwd, c))) out.authFiles.push(c);
3650
+ if (fs17.existsSync(path16.join(cwd, c))) out.authFiles.push(c);
2954
3651
  }
2955
3652
  }
2956
3653
  function detectRoles(cwd, out) {
2957
3654
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
2958
3655
  for (const rp of rolePaths) {
2959
- const dir = path15.join(cwd, rp);
2960
- if (!fs16.existsSync(dir)) continue;
3656
+ const dir = path16.join(cwd, rp);
3657
+ if (!fs17.existsSync(dir)) continue;
2961
3658
  let files;
2962
3659
  try {
2963
- 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"));
2964
3661
  } catch {
2965
3662
  continue;
2966
3663
  }
2967
3664
  for (const f of files) {
2968
3665
  try {
2969
- 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);
2970
3667
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
2971
3668
  if (roleMatches) {
2972
3669
  for (const m of roleMatches) {
@@ -3112,8 +3809,8 @@ var discoverQaContext = async (ctx) => {
3112
3809
  };
3113
3810
 
3114
3811
  // src/scripts/dispatch.ts
3115
- import { execFileSync as execFileSync9 } from "child_process";
3116
- var API_TIMEOUT_MS3 = 3e4;
3812
+ import { execFileSync as execFileSync11 } from "child_process";
3813
+ var API_TIMEOUT_MS4 = 3e4;
3117
3814
  var dispatch = async (ctx, _profile, _agentResult, args) => {
3118
3815
  const next = args?.next;
3119
3816
  if (!next) {
@@ -3143,204 +3840,63 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
3143
3840
  if (state?.flow) {
3144
3841
  state.flow.step = next;
3145
3842
  }
3146
- const usePr = target === "pr" && state?.core.prUrl;
3147
- const targetNumber = usePr ? parsePr(state.core.prUrl) ?? issueNumber : issueNumber;
3148
- const sub = usePr ? "pr" : "issue";
3149
- const body = `@kody ${next}`;
3150
- try {
3151
- execFileSync9("gh", [sub, "comment", String(targetNumber), "--body", body], {
3152
- timeout: API_TIMEOUT_MS3,
3153
- cwd: ctx.cwd,
3154
- stdio: ["ignore", "pipe", "pipe"]
3155
- });
3156
- } catch (err) {
3157
- process.stderr.write(
3158
- `[kody dispatch] failed to post @kody ${next} on ${sub} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
3159
- `
3160
- );
3161
- }
3162
- };
3163
- function parsePr(url) {
3164
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3165
- if (!m) return null;
3166
- const n = parseInt(m[1], 10);
3167
- return Number.isFinite(n) ? n : null;
3168
- }
3169
-
3170
- // src/scripts/dispatchClassified.ts
3171
- import { execFileSync as execFileSync10 } from "child_process";
3172
- var API_TIMEOUT_MS4 = 3e4;
3173
- var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
3174
- var dispatchClassified = async (ctx) => {
3175
- const issueNumber = ctx.args.issue;
3176
- if (!issueNumber) return;
3177
- const classification = ctx.data.classification;
3178
- if (!classification || !VALID_CLASSES2.has(classification)) return;
3179
- try {
3180
- execFileSync10("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
3181
- cwd: ctx.cwd,
3182
- timeout: API_TIMEOUT_MS4,
3183
- stdio: ["ignore", "pipe", "pipe"]
3184
- });
3185
- } catch (err) {
3186
- process.stderr.write(
3187
- `[kody dispatchClassified] failed to dispatch @kody ${classification}: ${err instanceof Error ? err.message : String(err)}
3188
- `
3189
- );
3190
- ctx.data.action = failedAction("dispatch post failed");
3191
- ctx.output.exitCode = 1;
3192
- ctx.output.reason = "classify: dispatch failed";
3193
- }
3194
- };
3195
- function failedAction(reason) {
3196
- return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3197
- }
3198
-
3199
- // src/scripts/dispatchJobFileTicks.ts
3200
- import * as fs18 from "fs";
3201
- import * as path17 from "path";
3202
-
3203
- // src/issue.ts
3204
- import { execFileSync as execFileSync11 } from "child_process";
3205
- var API_TIMEOUT_MS5 = 3e4;
3206
- function ghToken2() {
3207
- return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
3208
- }
3209
- function gh2(args, options) {
3210
- const token = ghToken2();
3211
- const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3212
- return execFileSync11("gh", args, {
3213
- encoding: "utf-8",
3214
- timeout: API_TIMEOUT_MS5,
3215
- cwd: options?.cwd,
3216
- env,
3217
- input: options?.input,
3218
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
3219
- }).trim();
3220
- }
3221
- function getIssue(issueNumber, cwd) {
3222
- const output = gh2(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
3223
- const parsed = JSON.parse(output);
3224
- if (typeof parsed?.title !== "string") {
3225
- throw new Error(`Issue #${issueNumber}: unexpected response shape`);
3226
- }
3227
- return {
3228
- number: parsed.number ?? issueNumber,
3229
- title: parsed.title,
3230
- body: parsed.body ?? "",
3231
- comments: (parsed.comments ?? []).map((c) => ({
3232
- body: c.body ?? "",
3233
- author: c.author?.login ?? "unknown",
3234
- createdAt: c.createdAt ?? ""
3235
- })),
3236
- labels: Array.isArray(parsed.labels) ? parsed.labels.map((l) => l.name ?? "").filter((n) => n.length > 0) : []
3237
- };
3238
- }
3239
- function stripKodyMentions(body) {
3240
- return body.replace(/(@)(kody)/gi, "$1\u200B$2");
3241
- }
3242
- function postIssueComment(issueNumber, body, cwd) {
3243
- try {
3244
- gh2(["issue", "comment", String(issueNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
3245
- } catch (err) {
3246
- process.stderr.write(
3247
- `[kody] failed to post comment on #${issueNumber}: ${err instanceof Error ? err.message : String(err)}
3248
- `
3249
- );
3250
- }
3251
- }
3252
- function truncate2(s, maxBytes) {
3253
- if (s.length <= maxBytes) return s;
3254
- return `${s.slice(0, maxBytes)}\u2026 (+${s.length - maxBytes} chars)`;
3255
- }
3256
- function parsePrNumber(url) {
3257
- const m = url.match(/\/pull\/(\d+)(?:[/?#]|$)/);
3258
- if (!m) return null;
3259
- const n = parseInt(m[1], 10);
3260
- return Number.isFinite(n) ? n : null;
3261
- }
3262
- function getPr(prNumber, cwd) {
3263
- const output = gh2(["pr", "view", String(prNumber), "--json", "number,title,body,headRefName,baseRefName,state"], {
3264
- cwd
3265
- });
3266
- const parsed = JSON.parse(output);
3267
- if (typeof parsed?.title !== "string") {
3268
- throw new Error(`PR #${prNumber}: unexpected response shape`);
3269
- }
3270
- return {
3271
- number: parsed.number ?? prNumber,
3272
- title: parsed.title,
3273
- body: parsed.body ?? "",
3274
- headRefName: String(parsed.headRefName ?? ""),
3275
- baseRefName: String(parsed.baseRefName ?? ""),
3276
- state: String(parsed.state ?? "")
3277
- };
3278
- }
3279
- function getPrDiff(prNumber, cwd) {
3843
+ const usePr = target === "pr" && state?.core.prUrl;
3844
+ const targetNumber = usePr ? parsePr(state.core.prUrl) ?? issueNumber : issueNumber;
3845
+ const sub = usePr ? "pr" : "issue";
3846
+ const body = `@kody ${next}`;
3280
3847
  try {
3281
- return gh2(["pr", "diff", String(prNumber)], { cwd });
3848
+ execFileSync11("gh", [sub, "comment", String(targetNumber), "--body", body], {
3849
+ timeout: API_TIMEOUT_MS4,
3850
+ cwd: ctx.cwd,
3851
+ stdio: ["ignore", "pipe", "pipe"]
3852
+ });
3282
3853
  } catch (err) {
3283
3854
  process.stderr.write(
3284
- `[kody] failed to fetch diff for PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
3855
+ `[kody dispatch] failed to post @kody ${next} on ${sub} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
3285
3856
  `
3286
3857
  );
3287
- return "";
3288
- }
3289
- }
3290
- function getPrReviews(prNumber, cwd) {
3291
- try {
3292
- const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
3293
- const parsed = JSON.parse(output);
3294
- if (!Array.isArray(parsed?.reviews)) return [];
3295
- return parsed.reviews.map(
3296
- (r) => ({
3297
- body: r.body ?? "",
3298
- state: r.state ?? "",
3299
- author: r.author?.login ?? "unknown",
3300
- submittedAt: r.submittedAt ?? ""
3301
- })
3302
- );
3303
- } catch {
3304
- return [];
3305
- }
3306
- }
3307
- function getPrComments(prNumber, cwd) {
3308
- try {
3309
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
3310
- const parsed = JSON.parse(output);
3311
- if (!Array.isArray(parsed?.comments)) return [];
3312
- return parsed.comments.map((c) => ({
3313
- body: c.body ?? "",
3314
- author: c.author?.login ?? "unknown",
3315
- createdAt: c.createdAt ?? ""
3316
- })).filter((c) => c.body.trim().length > 0);
3317
- } catch {
3318
- return [];
3319
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;
3320
3865
  }
3321
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
3322
- function isReviewShaped(body) {
3323
- return VERDICT_HEADING.test(body);
3324
- }
3325
- function getPrLatestReviewBody(prNumber, cwd) {
3326
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
3327
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
3328
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
3329
- if (all.length > 0) return all[0].body;
3330
- const pr = getPr(prNumber, cwd);
3331
- return pr.body;
3332
- }
3333
- 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;
3334
3876
  try {
3335
- 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
+ });
3336
3882
  } catch (err) {
3337
3883
  process.stderr.write(
3338
- `[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)}
3339
3885
  `
3340
3886
  );
3887
+ ctx.data.action = failedAction3("dispatch post failed");
3888
+ ctx.output.exitCode = 1;
3889
+ ctx.output.reason = "classify: dispatch failed";
3341
3890
  }
3891
+ };
3892
+ function failedAction3(reason) {
3893
+ return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
3342
3894
  }
3343
3895
 
3896
+ // src/scripts/dispatchJobFileTicks.ts
3897
+ import * as fs19 from "fs";
3898
+ import * as path18 from "path";
3899
+
3344
3900
  // src/scripts/issueStateComment.ts
3345
3901
  function isStateEnvelope(x) {
3346
3902
  if (x === null || typeof x !== "object") return false;
@@ -3511,8 +4067,8 @@ var ContentsApiBackend = class {
3511
4067
  };
3512
4068
 
3513
4069
  // src/scripts/jobState/localFileBackend.ts
3514
- import * as fs17 from "fs";
3515
- import * as path16 from "path";
4070
+ import * as fs18 from "fs";
4071
+ import * as path17 from "path";
3516
4072
  var LocalFileBackend = class {
3517
4073
  name = "local-file";
3518
4074
  cwd;
@@ -3527,7 +4083,7 @@ var LocalFileBackend = class {
3527
4083
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
3528
4084
  this.cwd = opts.cwd;
3529
4085
  this.jobsDir = opts.jobsDir;
3530
- this.absDir = path16.join(opts.cwd, opts.jobsDir);
4086
+ this.absDir = path17.join(opts.cwd, opts.jobsDir);
3531
4087
  this.owner = opts.owner;
3532
4088
  this.repo = opts.repo;
3533
4089
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -3542,7 +4098,7 @@ var LocalFileBackend = class {
3542
4098
  `);
3543
4099
  return;
3544
4100
  }
3545
- fs17.mkdirSync(this.absDir, { recursive: true });
4101
+ fs18.mkdirSync(this.absDir, { recursive: true });
3546
4102
  const prefix = this.cacheKeyPrefix();
3547
4103
  const probeKey = `${prefix}probe-${Date.now()}`;
3548
4104
  try {
@@ -3571,7 +4127,7 @@ var LocalFileBackend = class {
3571
4127
  `);
3572
4128
  return;
3573
4129
  }
3574
- if (!fs17.existsSync(this.absDir)) {
4130
+ if (!fs18.existsSync(this.absDir)) {
3575
4131
  return;
3576
4132
  }
3577
4133
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -3587,11 +4143,11 @@ var LocalFileBackend = class {
3587
4143
  }
3588
4144
  load(slug) {
3589
4145
  const relPath = stateFilePath(this.jobsDir, slug);
3590
- const absPath = path16.join(this.cwd, relPath);
3591
- if (!fs17.existsSync(absPath)) {
4146
+ const absPath = path17.join(this.cwd, relPath);
4147
+ if (!fs18.existsSync(absPath)) {
3592
4148
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
3593
4149
  }
3594
- const raw = fs17.readFileSync(absPath, "utf-8");
4150
+ const raw = fs18.readFileSync(absPath, "utf-8");
3595
4151
  let parsed;
3596
4152
  try {
3597
4153
  parsed = JSON.parse(raw);
@@ -3608,10 +4164,10 @@ var LocalFileBackend = class {
3608
4164
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
3609
4165
  return false;
3610
4166
  }
3611
- const absPath = path16.join(this.cwd, loaded.path);
3612
- fs17.mkdirSync(path16.dirname(absPath), { recursive: true });
4167
+ const absPath = path17.join(this.cwd, loaded.path);
4168
+ fs18.mkdirSync(path17.dirname(absPath), { recursive: true });
3613
4169
  const body = JSON.stringify(next, null, 2) + "\n";
3614
- fs17.writeFileSync(absPath, body, "utf-8");
4170
+ fs18.writeFileSync(absPath, body, "utf-8");
3615
4171
  return true;
3616
4172
  }
3617
4173
  cacheKeyPrefix() {
@@ -3688,7 +4244,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
3688
4244
  await backend.hydrate();
3689
4245
  }
3690
4246
  try {
3691
- const slugs = listJobSlugs(path17.join(ctx.cwd, jobsDir));
4247
+ const slugs = listJobSlugs(path18.join(ctx.cwd, jobsDir));
3692
4248
  ctx.data.jobSlugCount = slugs.length;
3693
4249
  if (slugs.length === 0) {
3694
4250
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -3736,10 +4292,10 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
3736
4292
  }
3737
4293
  };
3738
4294
  function listJobSlugs(absDir) {
3739
- if (!fs18.existsSync(absDir)) return [];
4295
+ if (!fs19.existsSync(absDir)) return [];
3740
4296
  let entries;
3741
4297
  try {
3742
- entries = fs18.readdirSync(absDir, { withFileTypes: true });
4298
+ entries = fs19.readdirSync(absDir, { withFileTypes: true });
3743
4299
  } catch {
3744
4300
  return [];
3745
4301
  }
@@ -4119,7 +4675,7 @@ function collectExpectedTests(raw) {
4119
4675
  }
4120
4676
 
4121
4677
  // src/scripts/finishFlow.ts
4122
- import { execFileSync as execFileSync12 } from "child_process";
4678
+ import { execFileSync as execFileSync13 } from "child_process";
4123
4679
 
4124
4680
  // src/lifecycleLabels.ts
4125
4681
  var KODY_NAMESPACE = "kody";
@@ -4272,7 +4828,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4272
4828
  **PR:** ${state.core.prUrl}` : "";
4273
4829
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
4274
4830
  try {
4275
- execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4831
+ execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4276
4832
  timeout: API_TIMEOUT_MS6,
4277
4833
  cwd: ctx.cwd,
4278
4834
  stdio: ["ignore", "pipe", "pipe"]
@@ -4286,7 +4842,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4286
4842
  };
4287
4843
 
4288
4844
  // src/branch.ts
4289
- import { execFileSync as execFileSync13 } from "child_process";
4845
+ import { execFileSync as execFileSync14 } from "child_process";
4290
4846
  var UncommittedChangesError = class extends Error {
4291
4847
  constructor(branch) {
4292
4848
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -4296,7 +4852,7 @@ var UncommittedChangesError = class extends Error {
4296
4852
  branch;
4297
4853
  };
4298
4854
  function git2(args, cwd) {
4299
- return execFileSync13("git", args, {
4855
+ return execFileSync14("git", args, {
4300
4856
  encoding: "utf-8",
4301
4857
  timeout: 3e4,
4302
4858
  cwd,
@@ -4321,7 +4877,7 @@ function checkoutPrBranch(prNumber, cwd) {
4321
4877
  SKIP_HOOKS: "1",
4322
4878
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
4323
4879
  };
4324
- execFileSync13("gh", ["pr", "checkout", String(prNumber)], {
4880
+ execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
4325
4881
  cwd,
4326
4882
  env,
4327
4883
  stdio: ["ignore", "pipe", "pipe"],
@@ -4435,8 +4991,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
4435
4991
  }
4436
4992
 
4437
4993
  // src/gha.ts
4438
- import { execFileSync as execFileSync14 } from "child_process";
4439
- import * as fs19 from "fs";
4994
+ import { execFileSync as execFileSync15 } from "child_process";
4995
+ import * as fs20 from "fs";
4440
4996
  function getRunUrl() {
4441
4997
  const server = process.env.GITHUB_SERVER_URL;
4442
4998
  const repo = process.env.GITHUB_REPOSITORY;
@@ -4447,10 +5003,10 @@ function getRunUrl() {
4447
5003
  function reactToTriggerComment(cwd) {
4448
5004
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
4449
5005
  const eventPath = process.env.GITHUB_EVENT_PATH;
4450
- if (!eventPath || !fs19.existsSync(eventPath)) return;
5006
+ if (!eventPath || !fs20.existsSync(eventPath)) return;
4451
5007
  let event = null;
4452
5008
  try {
4453
- event = JSON.parse(fs19.readFileSync(eventPath, "utf-8"));
5009
+ event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
4454
5010
  } catch {
4455
5011
  return;
4456
5012
  }
@@ -4478,7 +5034,7 @@ function reactToTriggerComment(cwd) {
4478
5034
  for (let attempt = 0; attempt < 3; attempt++) {
4479
5035
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
4480
5036
  try {
4481
- execFileSync14("gh", args, opts);
5037
+ execFileSync15("gh", args, opts);
4482
5038
  return;
4483
5039
  } catch (err) {
4484
5040
  lastErr = err;
@@ -4491,13 +5047,13 @@ function reactToTriggerComment(cwd) {
4491
5047
  }
4492
5048
  function sleepMs(ms) {
4493
5049
  try {
4494
- execFileSync14("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5050
+ execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
4495
5051
  } catch {
4496
5052
  }
4497
5053
  }
4498
5054
 
4499
5055
  // src/workflow.ts
4500
- import { execFileSync as execFileSync15 } from "child_process";
5056
+ import { execFileSync as execFileSync16 } from "child_process";
4501
5057
  var GH_TIMEOUT_MS = 3e4;
4502
5058
  function ghToken3() {
4503
5059
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -4505,7 +5061,7 @@ function ghToken3() {
4505
5061
  function gh3(args, cwd) {
4506
5062
  const token = ghToken3();
4507
5063
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
4508
- return execFileSync15("gh", args, {
5064
+ return execFileSync16("gh", args, {
4509
5065
  encoding: "utf-8",
4510
5066
  timeout: GH_TIMEOUT_MS,
4511
5067
  cwd,
@@ -4689,23 +5245,23 @@ function tryPostPr2(prNumber, body, cwd) {
4689
5245
  }
4690
5246
 
4691
5247
  // src/scripts/initFlow.ts
4692
- import { execFileSync as execFileSync16 } from "child_process";
4693
- import * as fs21 from "fs";
4694
- 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";
4695
5251
 
4696
5252
  // src/scripts/loadQaGuide.ts
4697
- import * as fs20 from "fs";
4698
- import * as path18 from "path";
5253
+ import * as fs21 from "fs";
5254
+ import * as path19 from "path";
4699
5255
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
4700
5256
  var loadQaGuide = async (ctx) => {
4701
- const full = path18.join(ctx.cwd, QA_GUIDE_REL_PATH);
4702
- if (!fs20.existsSync(full)) {
5257
+ const full = path19.join(ctx.cwd, QA_GUIDE_REL_PATH);
5258
+ if (!fs21.existsSync(full)) {
4703
5259
  ctx.data.qaGuide = "";
4704
5260
  ctx.data.qaGuidePath = "";
4705
5261
  return;
4706
5262
  }
4707
5263
  try {
4708
- ctx.data.qaGuide = fs20.readFileSync(full, "utf-8");
5264
+ ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
4709
5265
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
4710
5266
  } catch {
4711
5267
  ctx.data.qaGuide = "";
@@ -4715,9 +5271,9 @@ var loadQaGuide = async (ctx) => {
4715
5271
 
4716
5272
  // src/scripts/initFlow.ts
4717
5273
  function detectPackageManager(cwd) {
4718
- if (fs21.existsSync(path19.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4719
- if (fs21.existsSync(path19.join(cwd, "yarn.lock"))) return "yarn";
4720
- 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";
4721
5277
  return "npm";
4722
5278
  }
4723
5279
  function qualityCommandsFor(pm) {
@@ -4730,7 +5286,7 @@ function qualityCommandsFor(pm) {
4730
5286
  function detectOwnerRepo(cwd) {
4731
5287
  let url;
4732
5288
  try {
4733
- url = execFileSync16("git", ["remote", "get-url", "origin"], {
5289
+ url = execFileSync17("git", ["remote", "get-url", "origin"], {
4734
5290
  cwd,
4735
5291
  encoding: "utf-8",
4736
5292
  stdio: ["ignore", "pipe", "pipe"]
@@ -4815,7 +5371,7 @@ jobs:
4815
5371
  `;
4816
5372
  function defaultBranchFromGit(cwd) {
4817
5373
  try {
4818
- const ref = execFileSync16("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5374
+ const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
4819
5375
  cwd,
4820
5376
  encoding: "utf-8",
4821
5377
  stdio: ["ignore", "pipe", "pipe"]
@@ -4823,7 +5379,7 @@ function defaultBranchFromGit(cwd) {
4823
5379
  return ref.replace("refs/remotes/origin/", "");
4824
5380
  } catch {
4825
5381
  try {
4826
- return execFileSync16("git", ["branch", "--show-current"], {
5382
+ return execFileSync17("git", ["branch", "--show-current"], {
4827
5383
  cwd,
4828
5384
  encoding: "utf-8",
4829
5385
  stdio: ["ignore", "pipe", "pipe"]
@@ -4839,33 +5395,33 @@ function performInit(cwd, force) {
4839
5395
  const pm = detectPackageManager(cwd);
4840
5396
  const ownerRepo = detectOwnerRepo(cwd);
4841
5397
  const defaultBranch = defaultBranchFromGit(cwd);
4842
- const configPath = path19.join(cwd, "kody.config.json");
4843
- if (fs21.existsSync(configPath) && !force) {
5398
+ const configPath = path20.join(cwd, "kody.config.json");
5399
+ if (fs22.existsSync(configPath) && !force) {
4844
5400
  skipped.push("kody.config.json");
4845
5401
  } else {
4846
5402
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
4847
- fs21.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5403
+ fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
4848
5404
  `);
4849
5405
  wrote.push("kody.config.json");
4850
5406
  }
4851
- const workflowDir = path19.join(cwd, ".github", "workflows");
4852
- const workflowPath = path19.join(workflowDir, "kody.yml");
4853
- 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) {
4854
5410
  skipped.push(".github/workflows/kody.yml");
4855
5411
  } else {
4856
- fs21.mkdirSync(workflowDir, { recursive: true });
4857
- fs21.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5412
+ fs22.mkdirSync(workflowDir, { recursive: true });
5413
+ fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
4858
5414
  wrote.push(".github/workflows/kody.yml");
4859
5415
  }
4860
- 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"));
4861
5417
  if (hasUi) {
4862
- const qaGuidePath = path19.join(cwd, QA_GUIDE_REL_PATH);
4863
- if (fs21.existsSync(qaGuidePath) && !force) {
5418
+ const qaGuidePath = path20.join(cwd, QA_GUIDE_REL_PATH);
5419
+ if (fs22.existsSync(qaGuidePath) && !force) {
4864
5420
  skipped.push(QA_GUIDE_REL_PATH);
4865
5421
  } else {
4866
- fs21.mkdirSync(path19.dirname(qaGuidePath), { recursive: true });
5422
+ fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
4867
5423
  const discovery = runQaDiscovery(cwd);
4868
- fs21.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5424
+ fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
4869
5425
  wrote.push(QA_GUIDE_REL_PATH);
4870
5426
  }
4871
5427
  }
@@ -4877,12 +5433,12 @@ function performInit(cwd, force) {
4877
5433
  continue;
4878
5434
  }
4879
5435
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
4880
- const target = path19.join(workflowDir, `kody-${exe.name}.yml`);
4881
- if (fs21.existsSync(target) && !force) {
5436
+ const target = path20.join(workflowDir, `kody-${exe.name}.yml`);
5437
+ if (fs22.existsSync(target) && !force) {
4882
5438
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
4883
5439
  continue;
4884
5440
  }
4885
- fs21.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5441
+ fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
4886
5442
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
4887
5443
  }
4888
5444
  let labels;
@@ -5020,8 +5576,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
5020
5576
  };
5021
5577
 
5022
5578
  // src/scripts/loadJobFromFile.ts
5023
- import * as fs22 from "fs";
5024
- import * as path20 from "path";
5579
+ import * as fs23 from "fs";
5580
+ import * as path21 from "path";
5025
5581
  var loadJobFromFile = async (ctx, _profile, args) => {
5026
5582
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
5027
5583
  const slugArg = String(args?.slugArg ?? "job");
@@ -5029,11 +5585,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
5029
5585
  if (!slug) {
5030
5586
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
5031
5587
  }
5032
- const absPath = path20.join(ctx.cwd, jobsDir, `${slug}.md`);
5033
- if (!fs22.existsSync(absPath)) {
5588
+ const absPath = path21.join(ctx.cwd, jobsDir, `${slug}.md`);
5589
+ if (!fs23.existsSync(absPath)) {
5034
5590
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
5035
5591
  }
5036
- const raw = fs22.readFileSync(absPath, "utf-8");
5592
+ const raw = fs23.readFileSync(absPath, "utf-8");
5037
5593
  const { title, body } = parseJobFile(raw, slug);
5038
5594
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
5039
5595
  const loaded = await backend.load(slug);
@@ -5167,16 +5723,16 @@ var loadTaskState = async (ctx) => {
5167
5723
  };
5168
5724
 
5169
5725
  // src/scripts/loadVaultContext.ts
5170
- import * as fs23 from "fs";
5171
- import * as path21 from "path";
5726
+ import * as fs24 from "fs";
5727
+ import * as path22 from "path";
5172
5728
  var VAULT_DIR_RELATIVE = ".kody/vault";
5173
5729
  var MAX_PAGES = 8;
5174
5730
  var PER_PAGE_MAX_BYTES = 4e3;
5175
5731
  var TOTAL_MAX_BYTES2 = 24e3;
5176
5732
  var TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
5177
5733
  var loadVaultContext = async (ctx) => {
5178
- const vaultAbs = path21.join(ctx.cwd, VAULT_DIR_RELATIVE);
5179
- if (!fs23.existsSync(vaultAbs)) {
5734
+ const vaultAbs = path22.join(ctx.cwd, VAULT_DIR_RELATIVE);
5735
+ if (!fs24.existsSync(vaultAbs)) {
5180
5736
  ctx.data.vaultContext = "";
5181
5737
  return;
5182
5738
  }
@@ -5201,21 +5757,21 @@ function collectPages(vaultAbs) {
5201
5757
  walkMd(vaultAbs, (file) => {
5202
5758
  let stat;
5203
5759
  try {
5204
- stat = fs23.statSync(file);
5760
+ stat = fs24.statSync(file);
5205
5761
  } catch {
5206
5762
  return;
5207
5763
  }
5208
5764
  let raw;
5209
5765
  try {
5210
- raw = fs23.readFileSync(file, "utf-8");
5766
+ raw = fs24.readFileSync(file, "utf-8");
5211
5767
  } catch {
5212
5768
  return;
5213
5769
  }
5214
5770
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5215
- 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");
5216
5772
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
5217
5773
  out.push({
5218
- relPath: path21.relative(vaultAbs, file),
5774
+ relPath: path22.relative(vaultAbs, file),
5219
5775
  title,
5220
5776
  updated,
5221
5777
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -5283,16 +5839,16 @@ function walkMd(root, visit) {
5283
5839
  const dir = stack.pop();
5284
5840
  let names;
5285
5841
  try {
5286
- names = fs23.readdirSync(dir);
5842
+ names = fs24.readdirSync(dir);
5287
5843
  } catch {
5288
5844
  continue;
5289
5845
  }
5290
5846
  for (const name of names) {
5291
5847
  if (name.startsWith(".")) continue;
5292
- const full = path21.join(dir, name);
5848
+ const full = path22.join(dir, name);
5293
5849
  let stat;
5294
5850
  try {
5295
- stat = fs23.statSync(full);
5851
+ stat = fs24.statSync(full);
5296
5852
  } catch {
5297
5853
  continue;
5298
5854
  }
@@ -5314,16 +5870,16 @@ var markFlowSuccess = async (ctx) => {
5314
5870
  };
5315
5871
 
5316
5872
  // src/scripts/memorizeFlow.ts
5317
- import { execFileSync as execFileSync17 } from "child_process";
5318
- import * as fs24 from "fs";
5319
- 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";
5320
5876
  var VAULT_DIR_RELATIVE2 = ".kody/vault";
5321
5877
  var DEFAULT_LOOKBACK_HOURS = 36;
5322
5878
  var MAX_RECENT_PRS = 25;
5323
5879
  var MAX_VAULT_INDEX_ENTRIES = 200;
5324
5880
  var PR_BODY_TRUNC = 2e3;
5325
5881
  var memorizeFlow = async (ctx) => {
5326
- const vaultAbs = path22.join(ctx.cwd, VAULT_DIR_RELATIVE2);
5882
+ const vaultAbs = path23.join(ctx.cwd, VAULT_DIR_RELATIVE2);
5327
5883
  ensureBranch(ctx, vaultAbs);
5328
5884
  if (ctx.skipAgent) return;
5329
5885
  const sinceIso = computeSinceIso(vaultAbs);
@@ -5333,8 +5889,8 @@ var memorizeFlow = async (ctx) => {
5333
5889
  const recent = fetchRecentPrs(ctx.cwd, sinceIso);
5334
5890
  ctx.data.recentPrs = formatRecentPrs(recent);
5335
5891
  ctx.data.recentPrCount = recent.length;
5336
- if (!fs24.existsSync(vaultAbs)) {
5337
- fs24.mkdirSync(vaultAbs, { recursive: true });
5892
+ if (!fs25.existsSync(vaultAbs)) {
5893
+ fs25.mkdirSync(vaultAbs, { recursive: true });
5338
5894
  }
5339
5895
  ctx.data.vaultIndex = formatVaultIndex(vaultAbs);
5340
5896
  if (recent.length === 0) {
@@ -5366,18 +5922,18 @@ function ensureBranch(ctx, vaultAbs) {
5366
5922
  }
5367
5923
  }
5368
5924
  ctx.data.branch = branch;
5369
- if (!fs24.existsSync(vaultAbs)) {
5370
- fs24.mkdirSync(vaultAbs, { recursive: true });
5925
+ if (!fs25.existsSync(vaultAbs)) {
5926
+ fs25.mkdirSync(vaultAbs, { recursive: true });
5371
5927
  }
5372
5928
  }
5373
5929
  function computeSinceIso(vaultAbs) {
5374
5930
  const fallback = new Date(Date.now() - DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1e3).toISOString();
5375
- if (!fs24.existsSync(vaultAbs)) return fallback;
5931
+ if (!fs25.existsSync(vaultAbs)) return fallback;
5376
5932
  let latest = "";
5377
5933
  walkMd2(vaultAbs, (file) => {
5378
5934
  let raw;
5379
5935
  try {
5380
- raw = fs24.readFileSync(file, "utf-8");
5936
+ raw = fs25.readFileSync(file, "utf-8");
5381
5937
  } catch {
5382
5938
  return;
5383
5939
  }
@@ -5454,10 +6010,10 @@ function formatVaultIndex(vaultAbs) {
5454
6010
  const entries = [];
5455
6011
  walkMd2(vaultAbs, (file) => {
5456
6012
  if (entries.length >= MAX_VAULT_INDEX_ENTRIES) return;
5457
- const rel = path22.relative(vaultAbs, file);
6013
+ const rel = path23.relative(vaultAbs, file);
5458
6014
  let title = rel;
5459
6015
  try {
5460
- const raw = fs24.readFileSync(file, "utf-8");
6016
+ const raw = fs25.readFileSync(file, "utf-8");
5461
6017
  const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5462
6018
  const titleMatch = m?.[1]?.match(/^title:\s*(.+)$/m);
5463
6019
  if (titleMatch) title = `${titleMatch[1].trim()} (${rel})`;
@@ -5469,22 +6025,22 @@ function formatVaultIndex(vaultAbs) {
5469
6025
  return entries.join("\n");
5470
6026
  }
5471
6027
  function walkMd2(root, visit) {
5472
- if (!fs24.existsSync(root)) return;
6028
+ if (!fs25.existsSync(root)) return;
5473
6029
  const stack = [root];
5474
6030
  while (stack.length > 0) {
5475
6031
  const dir = stack.pop();
5476
6032
  let names;
5477
6033
  try {
5478
- names = fs24.readdirSync(dir);
6034
+ names = fs25.readdirSync(dir);
5479
6035
  } catch {
5480
6036
  continue;
5481
6037
  }
5482
6038
  for (const name of names) {
5483
6039
  if (name.startsWith(".")) continue;
5484
- const full = path22.join(dir, name);
6040
+ const full = path23.join(dir, name);
5485
6041
  let stat;
5486
6042
  try {
5487
- stat = fs24.statSync(full);
6043
+ stat = fs25.statSync(full);
5488
6044
  } catch {
5489
6045
  continue;
5490
6046
  }
@@ -5497,7 +6053,7 @@ function walkMd2(root, visit) {
5497
6053
  }
5498
6054
  }
5499
6055
  function git3(args, cwd) {
5500
- return execFileSync17("git", args, {
6056
+ return execFileSync18("git", args, {
5501
6057
  encoding: "utf-8",
5502
6058
  timeout: 3e4,
5503
6059
  cwd,
@@ -5507,7 +6063,7 @@ function git3(args, cwd) {
5507
6063
  }
5508
6064
 
5509
6065
  // src/scripts/mergeReleasePr.ts
5510
- import { execFileSync as execFileSync18 } from "child_process";
6066
+ import { execFileSync as execFileSync19 } from "child_process";
5511
6067
  var API_TIMEOUT_MS7 = 6e4;
5512
6068
  var mergeReleasePr = async (ctx) => {
5513
6069
  const state = ctx.data.taskState;
@@ -5526,7 +6082,7 @@ var mergeReleasePr = async (ctx) => {
5526
6082
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
5527
6083
  `);
5528
6084
  try {
5529
- const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
6085
+ const out = execFileSync19("gh", ["pr", "merge", String(prNumber), "--merge"], {
5530
6086
  timeout: API_TIMEOUT_MS7,
5531
6087
  cwd: ctx.cwd,
5532
6088
  stdio: ["ignore", "pipe", "pipe"]
@@ -5626,77 +6182,13 @@ function composeBody({ label, exit, prUrl, reason, dryRun }) {
5626
6182
  return `\u2705 kody ${label} complete`;
5627
6183
  }
5628
6184
 
5629
- // src/scripts/postReviewResult.ts
5630
- function detectVerdict(body) {
5631
- const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
5632
- if (!m) return "UNKNOWN";
5633
- return m[1].toUpperCase();
5634
- }
5635
- function reviewAction(verdict, payload) {
5636
- const type = verdict === "PASS" ? "REVIEW_PASS" : verdict === "CONCERNS" ? "REVIEW_CONCERNS" : verdict === "FAIL" ? "REVIEW_FAIL" : "REVIEW_COMPLETED";
5637
- return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5638
- }
5639
- function failedAction2(reason) {
5640
- return { type: "REVIEW_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5641
- }
5642
- var postReviewResult = async (ctx, _profile, agentResult) => {
5643
- const prNumber = ctx.data.commentTargetNumber;
5644
- if (!prNumber) {
5645
- ctx.output.exitCode = 99;
5646
- ctx.output.reason = "review postflight: no PR number in context";
5647
- ctx.data.action = failedAction2(ctx.output.reason);
5648
- return;
5649
- }
5650
- if (!agentResult || agentResult.outcome !== "completed") {
5651
- const reason = agentResult?.error ?? "agent did not complete";
5652
- try {
5653
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
5654
- } catch {
5655
- }
5656
- ctx.output.exitCode = 1;
5657
- ctx.output.reason = reason;
5658
- ctx.data.action = failedAction2(reason);
5659
- return;
5660
- }
5661
- const reviewBody = agentResult.finalText.trim();
5662
- if (!reviewBody) {
5663
- try {
5664
- postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: agent produced no review body`, ctx.cwd);
5665
- } catch {
5666
- }
5667
- ctx.output.exitCode = 1;
5668
- ctx.output.reason = "empty review body";
5669
- ctx.data.action = failedAction2("empty review body");
5670
- return;
5671
- }
5672
- try {
5673
- postPrReviewComment(prNumber, reviewBody, ctx.cwd);
5674
- } catch (err) {
5675
- const msg = err instanceof Error ? err.message : String(err);
5676
- ctx.output.exitCode = 4;
5677
- ctx.output.reason = `failed to post review comment: ${msg}`;
5678
- ctx.data.action = failedAction2(ctx.output.reason);
5679
- return;
5680
- }
5681
- const verdict = detectVerdict(reviewBody);
5682
- ctx.data.reviewVerdict = verdict;
5683
- ctx.data.reviewBody = reviewBody;
5684
- ctx.data.action = reviewAction(verdict, { bodyPreview: truncate2(reviewBody, 500) });
5685
- ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5686
- process.stdout.write(
5687
- `
5688
- REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/pull/${prNumber} (verdict: ${verdict})
5689
- `
5690
- );
5691
- };
5692
-
5693
6185
  // src/scripts/openQaIssue.ts
5694
6186
  var QA_LABEL = "kody:qa-report";
5695
- function qaAction(verdict, payload) {
6187
+ function qaAction2(verdict, payload) {
5696
6188
  const type = verdict === "PASS" ? "QA_PASS" : verdict === "CONCERNS" ? "QA_CONCERNS" : verdict === "FAIL" ? "QA_FAIL" : "QA_COMPLETED";
5697
6189
  return { type, payload: { verdict, ...payload }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5698
6190
  }
5699
- function failedAction3(reason) {
6191
+ function failedAction4(reason) {
5700
6192
  return { type: "QA_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
5701
6193
  }
5702
6194
  function slugifyScope(scope) {
@@ -5708,7 +6200,7 @@ function buildIssueTitle(scope, verdict) {
5708
6200
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
5709
6201
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
5710
6202
  }
5711
- function ensureLabel(cwd) {
6203
+ function ensureLabel2(cwd) {
5712
6204
  try {
5713
6205
  gh2(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
5714
6206
  return true;
@@ -5732,7 +6224,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5732
6224
  `);
5733
6225
  ctx.output.exitCode = 1;
5734
6226
  ctx.output.reason = reason;
5735
- ctx.data.action = failedAction3(reason);
6227
+ ctx.data.action = failedAction4(reason);
5736
6228
  return;
5737
6229
  }
5738
6230
  const reportBody = agentResult.finalText.trim();
@@ -5740,7 +6232,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5740
6232
  process.stderr.write("qa-engineer: agent produced no report body\n");
5741
6233
  ctx.output.exitCode = 1;
5742
6234
  ctx.output.reason = "empty report body";
5743
- ctx.data.action = failedAction3("empty report body");
6235
+ ctx.data.action = failedAction4("empty report body");
5744
6236
  return;
5745
6237
  }
5746
6238
  const verdict = detectVerdict(reportBody);
@@ -5754,7 +6246,7 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5754
6246
  const msg = err instanceof Error ? err.message : String(err);
5755
6247
  ctx.output.exitCode = 4;
5756
6248
  ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
5757
- ctx.data.action = failedAction3(ctx.output.reason);
6249
+ ctx.data.action = failedAction4(ctx.output.reason);
5758
6250
  return;
5759
6251
  }
5760
6252
  process.stdout.write(
@@ -5762,13 +6254,13 @@ var openQaIssue = async (ctx, _profile, agentResult) => {
5762
6254
  QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
5763
6255
  `
5764
6256
  );
5765
- ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
6257
+ ctx.data.action = qaAction2(verdict, { issueNumber: existingIssue, mode: "comment" });
5766
6258
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
5767
6259
  return;
5768
6260
  }
5769
6261
  const scope = ctx.args.scope;
5770
6262
  const title = buildIssueTitle(scope, verdict);
5771
- const hasLabel = ensureLabel(ctx.cwd);
6263
+ const hasLabel = ensureLabel2(ctx.cwd);
5772
6264
  let created;
5773
6265
  try {
5774
6266
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -5776,13 +6268,13 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
5776
6268
  const msg = err instanceof Error ? err.message : String(err);
5777
6269
  ctx.output.exitCode = 4;
5778
6270
  ctx.output.reason = `failed to open QA issue: ${truncate2(msg, 1e3)}`;
5779
- ctx.data.action = failedAction3(ctx.output.reason);
6271
+ ctx.data.action = failedAction4(ctx.output.reason);
5780
6272
  return;
5781
6273
  }
5782
6274
  process.stdout.write(`
5783
6275
  QA_REPORT_POSTED=${created.url} (verdict: ${verdict})
5784
6276
  `);
5785
- ctx.data.action = qaAction(verdict, {
6277
+ ctx.data.action = qaAction2(verdict, {
5786
6278
  issueNumber: created.number,
5787
6279
  issueUrl: created.url,
5788
6280
  titleSlug: scope ? slugifyScope(scope) : "smoke",
@@ -6166,7 +6658,7 @@ ${body}`;
6166
6658
  }
6167
6659
 
6168
6660
  // src/scripts/recordClassification.ts
6169
- import { execFileSync as execFileSync19 } from "child_process";
6661
+ import { execFileSync as execFileSync20 } from "child_process";
6170
6662
  var API_TIMEOUT_MS8 = 3e4;
6171
6663
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6172
6664
  var recordClassification = async (ctx) => {
@@ -6184,7 +6676,7 @@ var recordClassification = async (ctx) => {
6184
6676
  reason = parsed?.reason ?? null;
6185
6677
  }
6186
6678
  if (!classification) {
6187
- ctx.data.action = failedAction4("classification missing or invalid");
6679
+ ctx.data.action = failedAction5("classification missing or invalid");
6188
6680
  tryAuditComment(
6189
6681
  issueNumber,
6190
6682
  "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
@@ -6214,7 +6706,7 @@ function parseClassification(prSummary) {
6214
6706
  }
6215
6707
  function tryAuditComment(issueNumber, body, cwd) {
6216
6708
  try {
6217
- execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6709
+ execFileSync20("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6218
6710
  cwd,
6219
6711
  timeout: API_TIMEOUT_MS8,
6220
6712
  stdio: ["ignore", "pipe", "pipe"]
@@ -6225,7 +6717,7 @@ function tryAuditComment(issueNumber, body, cwd) {
6225
6717
  function makeAction3(type, payload) {
6226
6718
  return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6227
6719
  }
6228
- function failedAction4(reason) {
6720
+ function failedAction5(reason) {
6229
6721
  return { type: "CLASSIFY_FAILED", payload: { reason }, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
6230
6722
  }
6231
6723
 
@@ -6264,12 +6756,12 @@ function fail(ctx, profile, reason) {
6264
6756
  ctx.data.agentDone = false;
6265
6757
  ctx.data.agentFailureReason = reason;
6266
6758
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6267
- const failedAction5 = {
6759
+ const failedAction6 = {
6268
6760
  type: `${modeSeg}_FAILED`,
6269
6761
  payload: { reason },
6270
6762
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6271
6763
  };
6272
- ctx.data.action = failedAction5;
6764
+ ctx.data.action = failedAction6;
6273
6765
  }
6274
6766
  function countActionItems(block) {
6275
6767
  if (!block.trim()) return 0;
@@ -6310,12 +6802,12 @@ function fail2(ctx, profile, reason) {
6310
6802
  ctx.data.agentDone = false;
6311
6803
  ctx.data.agentFailureReason = reason;
6312
6804
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6313
- const failedAction5 = {
6805
+ const failedAction6 = {
6314
6806
  type: `${modeSeg}_FAILED`,
6315
6807
  payload: { reason },
6316
6808
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
6317
6809
  };
6318
- ctx.data.action = failedAction5;
6810
+ ctx.data.action = failedAction6;
6319
6811
  }
6320
6812
 
6321
6813
  // src/scripts/resolveArtifacts.ts
@@ -6342,7 +6834,7 @@ var resolveArtifacts = async (ctx, profile) => {
6342
6834
  };
6343
6835
 
6344
6836
  // src/scripts/resolveFlow.ts
6345
- import { execFileSync as execFileSync20 } from "child_process";
6837
+ import { execFileSync as execFileSync21 } from "child_process";
6346
6838
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
6347
6839
  var resolveFlow = async (ctx) => {
6348
6840
  const prNumber = ctx.args.pr;
@@ -6412,7 +6904,7 @@ function buildPreferBlock(prefer, baseBranch) {
6412
6904
  }
6413
6905
  function getConflictedFiles(cwd) {
6414
6906
  try {
6415
- const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
6907
+ const out = execFileSync21("git", ["diff", "--name-only", "--diff-filter=U"], {
6416
6908
  encoding: "utf-8",
6417
6909
  cwd,
6418
6910
  env: { ...process.env, HUSKY: "0" }
@@ -6427,7 +6919,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
6427
6919
  let total = 0;
6428
6920
  for (const f of files) {
6429
6921
  try {
6430
- const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
6922
+ const content = execFileSync21("cat", [f], { encoding: "utf-8", cwd }).toString();
6431
6923
  const snippet = `### ${f}
6432
6924
 
6433
6925
  \`\`\`
@@ -6527,8 +7019,81 @@ var resolvePreviewUrl = async (ctx) => {
6527
7019
  ctx.data.previewUrlSource = "default";
6528
7020
  };
6529
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
+
6530
7095
  // src/scripts/revertFlow.ts
6531
- import { execFileSync as execFileSync21 } from "child_process";
7096
+ import { execFileSync as execFileSync23 } from "child_process";
6532
7097
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
6533
7098
  var revertFlow = async (ctx) => {
6534
7099
  const prNumber = ctx.args.pr;
@@ -6610,7 +7175,7 @@ function buildPrSummary(resolved) {
6610
7175
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
6611
7176
  }
6612
7177
  function git4(args, cwd) {
6613
- return execFileSync21("git", args, {
7178
+ return execFileSync23("git", args, {
6614
7179
  encoding: "utf-8",
6615
7180
  timeout: 3e4,
6616
7181
  cwd,
@@ -6620,7 +7185,7 @@ function git4(args, cwd) {
6620
7185
  }
6621
7186
  function isAncestorOfHead(sha, cwd) {
6622
7187
  try {
6623
- execFileSync21("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7188
+ execFileSync23("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
6624
7189
  cwd,
6625
7190
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
6626
7191
  stdio: ["ignore", "ignore", "ignore"]
@@ -6798,11 +7363,11 @@ var skipAgent = async (ctx) => {
6798
7363
  };
6799
7364
 
6800
7365
  // src/scripts/stageMergeConflicts.ts
6801
- import { execFileSync as execFileSync22 } from "child_process";
7366
+ import { execFileSync as execFileSync24 } from "child_process";
6802
7367
  var stageMergeConflicts = async (ctx) => {
6803
7368
  if (ctx.data.agentDone === false) return;
6804
7369
  try {
6805
- execFileSync22("git", ["add", "-A"], {
7370
+ execFileSync24("git", ["add", "-A"], {
6806
7371
  cwd: ctx.cwd,
6807
7372
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
6808
7373
  stdio: "pipe"
@@ -6812,7 +7377,7 @@ var stageMergeConflicts = async (ctx) => {
6812
7377
  };
6813
7378
 
6814
7379
  // src/scripts/startFlow.ts
6815
- import { execFileSync as execFileSync23 } from "child_process";
7380
+ import { execFileSync as execFileSync25 } from "child_process";
6816
7381
  var API_TIMEOUT_MS9 = 3e4;
6817
7382
  var startFlow = async (ctx, profile, _agentResult, args) => {
6818
7383
  const entry = args?.entry;
@@ -6846,7 +7411,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
6846
7411
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
6847
7412
  const body = `@kody ${next}`;
6848
7413
  try {
6849
- execFileSync23("gh", [sub, "comment", String(targetNumber), "--body", body], {
7414
+ execFileSync25("gh", [sub, "comment", String(targetNumber), "--body", body], {
6850
7415
  timeout: API_TIMEOUT_MS9,
6851
7416
  cwd,
6852
7417
  stdio: ["ignore", "pipe", "pipe"]
@@ -6860,7 +7425,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
6860
7425
  }
6861
7426
 
6862
7427
  // src/scripts/syncFlow.ts
6863
- import { execFileSync as execFileSync24 } from "child_process";
7428
+ import { execFileSync as execFileSync26 } from "child_process";
6864
7429
  var syncFlow = async (ctx, _profile, args) => {
6865
7430
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
6866
7431
  const prNumber = ctx.args.pr;
@@ -6932,7 +7497,7 @@ function bail2(ctx, prNumber, reason) {
6932
7497
  }
6933
7498
  function revParseHead(cwd) {
6934
7499
  try {
6935
- 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();
6936
7501
  } catch {
6937
7502
  return "";
6938
7503
  }
@@ -6940,9 +7505,9 @@ function revParseHead(cwd) {
6940
7505
  function pushBranch(branch, cwd) {
6941
7506
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
6942
7507
  try {
6943
- 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"] });
6944
7509
  } catch {
6945
- execFileSync24("git", ["push", "--force-with-lease", "-u", "origin", branch], {
7510
+ execFileSync26("git", ["push", "--force-with-lease", "-u", "origin", branch], {
6946
7511
  cwd,
6947
7512
  env,
6948
7513
  stdio: ["ignore", "pipe", "pipe"]
@@ -7169,7 +7734,7 @@ function downgrade2(ctx, reason) {
7169
7734
  }
7170
7735
 
7171
7736
  // src/scripts/waitForCi.ts
7172
- import { execFileSync as execFileSync25 } from "child_process";
7737
+ import { execFileSync as execFileSync27 } from "child_process";
7173
7738
  var API_TIMEOUT_MS10 = 3e4;
7174
7739
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
7175
7740
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -7247,7 +7812,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
7247
7812
  };
7248
7813
  function fetchChecks(prNumber, cwd) {
7249
7814
  try {
7250
- 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"], {
7251
7816
  encoding: "utf-8",
7252
7817
  timeout: API_TIMEOUT_MS10,
7253
7818
  cwd,
@@ -7564,7 +8129,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
7564
8129
  };
7565
8130
 
7566
8131
  // src/scripts/writeRunSummary.ts
7567
- import * as fs25 from "fs";
8132
+ import * as fs26 from "fs";
7568
8133
  var writeRunSummary = async (ctx, profile) => {
7569
8134
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
7570
8135
  if (!summaryPath) return;
@@ -7586,7 +8151,7 @@ var writeRunSummary = async (ctx, profile) => {
7586
8151
  if (reason) lines.push(`- **Reason:** ${reason}`);
7587
8152
  lines.push("");
7588
8153
  try {
7589
- fs25.appendFileSync(summaryPath, `${lines.join("\n")}
8154
+ fs26.appendFileSync(summaryPath, `${lines.join("\n")}
7590
8155
  `);
7591
8156
  } catch {
7592
8157
  }
@@ -7617,6 +8182,7 @@ var preflightScripts = {
7617
8182
  resolveArtifacts,
7618
8183
  discoverQaContext,
7619
8184
  resolvePreviewUrl,
8185
+ resolveQaUrl,
7620
8186
  composePrompt,
7621
8187
  setCommentTarget,
7622
8188
  setLifecycleLabel,
@@ -7661,6 +8227,7 @@ var postflightScripts = {
7661
8227
  dispatchClassified,
7662
8228
  notifyTerminal,
7663
8229
  openQaIssue,
8230
+ createQaGoal,
7664
8231
  recordOutcome,
7665
8232
  mergeReleasePr,
7666
8233
  waitForCi,
@@ -7672,7 +8239,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
7672
8239
  ]);
7673
8240
 
7674
8241
  // src/tools.ts
7675
- import { execFileSync as execFileSync26 } from "child_process";
8242
+ import { execFileSync as execFileSync28 } from "child_process";
7676
8243
  function verifyCliTools(tools, cwd) {
7677
8244
  const out = [];
7678
8245
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -7705,7 +8272,7 @@ function verifyOne(tool, cwd) {
7705
8272
  }
7706
8273
  function runShell(cmd, cwd, timeoutMs = 3e4) {
7707
8274
  try {
7708
- execFileSync26("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8275
+ execFileSync28("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
7709
8276
  return true;
7710
8277
  } catch {
7711
8278
  return false;
@@ -7774,9 +8341,9 @@ async function runExecutable(profileName, input) {
7774
8341
  data: {},
7775
8342
  output: { exitCode: 0 }
7776
8343
  };
7777
- const ndjsonDir = path23.join(input.cwd, ".kody");
8344
+ const ndjsonDir = path24.join(input.cwd, ".kody");
7778
8345
  const invokeAgent = async (prompt) => {
7779
- 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);
7780
8347
  const syntheticPath = ctx.data.syntheticPluginPath;
7781
8348
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
7782
8349
  return runAgent({
@@ -7871,17 +8438,17 @@ async function runExecutable(profileName, input) {
7871
8438
  function resolveProfilePath(profileName) {
7872
8439
  const found = resolveExecutable(profileName);
7873
8440
  if (found) return found;
7874
- const here = path23.dirname(new URL(import.meta.url).pathname);
8441
+ const here = path24.dirname(new URL(import.meta.url).pathname);
7875
8442
  const candidates = [
7876
- path23.join(here, "executables", profileName, "profile.json"),
8443
+ path24.join(here, "executables", profileName, "profile.json"),
7877
8444
  // same-dir sibling (dev)
7878
- path23.join(here, "..", "executables", profileName, "profile.json"),
8445
+ path24.join(here, "..", "executables", profileName, "profile.json"),
7879
8446
  // up one (prod: dist/bin → dist/executables)
7880
- path23.join(here, "..", "src", "executables", profileName, "profile.json")
8447
+ path24.join(here, "..", "src", "executables", profileName, "profile.json")
7881
8448
  // fallback
7882
8449
  ];
7883
8450
  for (const c of candidates) {
7884
- if (fs26.existsSync(c)) return c;
8451
+ if (fs27.existsSync(c)) return c;
7885
8452
  }
7886
8453
  return candidates[0];
7887
8454
  }
@@ -7985,8 +8552,8 @@ function resolveShellTimeoutMs(entry) {
7985
8552
  var SIGKILL_GRACE_MS = 5e3;
7986
8553
  async function runShellEntry(entry, ctx, profile) {
7987
8554
  const shellName = entry.shell;
7988
- const shellPath = path23.join(profile.dir, shellName);
7989
- if (!fs26.existsSync(shellPath)) {
8555
+ const shellPath = path24.join(profile.dir, shellName);
8556
+ if (!fs27.existsSync(shellPath)) {
7990
8557
  ctx.skipAgent = true;
7991
8558
  ctx.output.exitCode = 99;
7992
8559
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8247,7 +8814,7 @@ async function runContainerLoop(profile, ctx, input) {
8247
8814
  }
8248
8815
  function resetWorkingTree(cwd) {
8249
8816
  try {
8250
- execFileSync27("git", ["reset", "--hard", "HEAD"], {
8817
+ execFileSync29("git", ["reset", "--hard", "HEAD"], {
8251
8818
  cwd,
8252
8819
  stdio: ["ignore", "pipe", "pipe"],
8253
8820
  timeout: 3e4
@@ -8399,14 +8966,14 @@ function resolveAuthToken(env = process.env) {
8399
8966
  return token;
8400
8967
  }
8401
8968
  function detectPackageManager2(cwd) {
8402
- if (fs27.existsSync(path24.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8403
- if (fs27.existsSync(path24.join(cwd, "yarn.lock"))) return "yarn";
8404
- 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";
8405
8972
  return "npm";
8406
8973
  }
8407
8974
  function shellOut(cmd, args, cwd, stream = true) {
8408
8975
  try {
8409
- execFileSync28(cmd, args, {
8976
+ execFileSync30(cmd, args, {
8410
8977
  cwd,
8411
8978
  stdio: stream ? "inherit" : "pipe",
8412
8979
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -8419,7 +8986,7 @@ function shellOut(cmd, args, cwd, stream = true) {
8419
8986
  }
8420
8987
  function isOnPath(bin) {
8421
8988
  try {
8422
- execFileSync28("which", [bin], { stdio: "pipe" });
8989
+ execFileSync30("which", [bin], { stdio: "pipe" });
8423
8990
  return true;
8424
8991
  } catch {
8425
8992
  return false;
@@ -8460,7 +9027,7 @@ function installLitellmIfNeeded(cwd) {
8460
9027
  } catch {
8461
9028
  }
8462
9029
  try {
8463
- execFileSync28("python3", ["-c", "import litellm"], { stdio: "pipe" });
9030
+ execFileSync30("python3", ["-c", "import litellm"], { stdio: "pipe" });
8464
9031
  process.stdout.write("\u2192 kody: litellm already installed\n");
8465
9032
  return 0;
8466
9033
  } catch {
@@ -8470,16 +9037,16 @@ function installLitellmIfNeeded(cwd) {
8470
9037
  }
8471
9038
  function configureGitIdentity(cwd) {
8472
9039
  try {
8473
- 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();
8474
9041
  if (name) return;
8475
9042
  } catch {
8476
9043
  }
8477
9044
  try {
8478
- execFileSync28("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9045
+ execFileSync30("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
8479
9046
  } catch {
8480
9047
  }
8481
9048
  try {
8482
- 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"], {
8483
9050
  cwd,
8484
9051
  stdio: "pipe"
8485
9052
  });
@@ -8488,11 +9055,11 @@ function configureGitIdentity(cwd) {
8488
9055
  }
8489
9056
  function postFailureTail(issueNumber, cwd, reason) {
8490
9057
  if (!issueNumber) return;
8491
- const logPath = path24.join(cwd, ".kody", "last-run.jsonl");
9058
+ const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
8492
9059
  let tail = "";
8493
9060
  try {
8494
- if (fs27.existsSync(logPath)) {
8495
- const content = fs27.readFileSync(logPath, "utf-8");
9061
+ if (fs28.existsSync(logPath)) {
9062
+ const content = fs28.readFileSync(logPath, "utf-8");
8496
9063
  tail = content.slice(-3e3);
8497
9064
  }
8498
9065
  } catch {
@@ -8517,7 +9084,7 @@ async function runCi(argv) {
8517
9084
  return 0;
8518
9085
  }
8519
9086
  const args = parseCiArgs(argv);
8520
- const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
9087
+ const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
8521
9088
  let earlyConfig;
8522
9089
  try {
8523
9090
  earlyConfig = loadConfig(cwd);
@@ -8527,9 +9094,9 @@ async function runCi(argv) {
8527
9094
  const eventName = process.env.GITHUB_EVENT_NAME;
8528
9095
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
8529
9096
  let manualWorkflowDispatch = false;
8530
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs27.existsSync(dispatchEventPath)) {
9097
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
8531
9098
  try {
8532
- const evt = JSON.parse(fs27.readFileSync(dispatchEventPath, "utf-8"));
9099
+ const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
8533
9100
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
8534
9101
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
8535
9102
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -8744,15 +9311,15 @@ function parseChatArgs(argv, env = process.env) {
8744
9311
  return result;
8745
9312
  }
8746
9313
  function commitChatFiles(cwd, sessionId, verbose) {
8747
- const sessionFile = path25.relative(cwd, sessionFilePath(cwd, sessionId));
8748
- const eventsFile = path25.relative(cwd, eventsFilePath(cwd, sessionId));
8749
- 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)));
8750
9317
  if (paths.length === 0) return;
8751
9318
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
8752
9319
  try {
8753
- execFileSync29("git", ["add", "-f", ...paths], opts);
8754
- execFileSync29("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
8755
- 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);
8756
9323
  } catch (err) {
8757
9324
  const msg = err instanceof Error ? err.message : String(err);
8758
9325
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -8784,7 +9351,7 @@ async function runChat(argv) {
8784
9351
  ${CHAT_HELP}`);
8785
9352
  return 64;
8786
9353
  }
8787
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9354
+ const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
8788
9355
  const sessionId = args.sessionId;
8789
9356
  const unpackedSecrets = unpackAllSecrets();
8790
9357
  if (unpackedSecrets > 0) {
@@ -8836,7 +9403,7 @@ ${CHAT_HELP}`);
8836
9403
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
8837
9404
  const meta = readMeta(sessionFile);
8838
9405
  process.stdout.write(
8839
- `\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"}
8840
9407
  `
8841
9408
  );
8842
9409
  try {