@kody-ade/kody-engine 0.4.13 → 0.4.15
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/README.md +106 -1
- package/dist/bin/kody.js +1117 -550
- package/dist/executables/goal-scheduler/scheduler.sh +7 -28
- package/dist/executables/goal-tick/tick.sh +98 -1
- package/dist/executables/qa-engineer/profile.json +12 -4
- package/dist/executables/qa-engineer/prompt.md +36 -4
- package/dist/executables/release-deploy/deploy.sh +0 -0
- package/dist/executables/release-prepare/prepare.sh +0 -0
- package/dist/executables/release-publish/publish.sh +0 -0
- package/dist/executables/resolve/apply-prefer.sh +0 -0
- package/dist/executables/revert/revert.sh +0 -0
- package/kody.config.schema.json +11 -0
- package/package.json +15 -14
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.
|
|
6
|
+
version: "0.4.15",
|
|
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
|
|
55
|
-
import * as
|
|
56
|
-
import * as
|
|
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
|
|
904
|
-
import * as
|
|
905
|
-
import * as
|
|
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
|
|
1265
|
-
import * as
|
|
1266
|
-
import * as
|
|
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/
|
|
2593
|
-
import { execFileSync as
|
|
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
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
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
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
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
|
|
2639
|
-
|
|
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
|
|
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
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
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
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
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
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
"
|
|
2684
|
-
];
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
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
|
-
|
|
2774
|
+
postPrReviewComment(prNumber, `\u26A0\uFE0F kody review FAILED: ${truncate2(reason, 1e3)}`, ctx.cwd);
|
|
2693
2775
|
} catch {
|
|
2694
|
-
continue;
|
|
2695
2776
|
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
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:
|
|
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 =
|
|
2728
|
-
if (!
|
|
3424
|
+
const full = path15.join(cwd, dir);
|
|
3425
|
+
if (!fs16.existsSync(full)) continue;
|
|
2729
3426
|
let entries;
|
|
2730
3427
|
try {
|
|
2731
|
-
entries =
|
|
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 =
|
|
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) =>
|
|
3438
|
+
(f) => fs16.existsSync(path15.join(entryPath, f))
|
|
2742
3439
|
);
|
|
2743
3440
|
if (!indexFile) continue;
|
|
2744
3441
|
name = entry.name;
|
|
2745
|
-
filePath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
2776
|
-
if (!
|
|
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 =
|
|
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 =
|
|
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:
|
|
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(
|
|
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(
|
|
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 =
|
|
2840
|
-
if (!
|
|
3536
|
+
const envPath = path15.join(cwd, envFile);
|
|
3537
|
+
if (!fs16.existsSync(envPath)) continue;
|
|
2841
3538
|
try {
|
|
2842
|
-
const content =
|
|
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(
|
|
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 =
|
|
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 =
|
|
2903
|
-
if (!
|
|
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 =
|
|
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(
|
|
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(
|
|
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 (
|
|
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 =
|
|
2960
|
-
if (!
|
|
3656
|
+
const dir = path16.join(cwd, rp);
|
|
3657
|
+
if (!fs17.existsSync(dir)) continue;
|
|
2961
3658
|
let files;
|
|
2962
3659
|
try {
|
|
2963
|
-
files =
|
|
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 =
|
|
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
|
|
3116
|
-
var
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
|
|
3327
|
-
const
|
|
3328
|
-
|
|
3329
|
-
|
|
3330
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3515
|
-
import * as
|
|
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 =
|
|
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
|
-
|
|
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 (!
|
|
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 =
|
|
3591
|
-
if (!
|
|
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 =
|
|
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 =
|
|
3612
|
-
|
|
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
|
-
|
|
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(
|
|
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 (!
|
|
4295
|
+
if (!fs19.existsSync(absDir)) return [];
|
|
3740
4296
|
let entries;
|
|
3741
4297
|
try {
|
|
3742
|
-
entries =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
4439
|
-
import * as
|
|
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 || !
|
|
5006
|
+
if (!eventPath || !fs20.existsSync(eventPath)) return;
|
|
4451
5007
|
let event = null;
|
|
4452
5008
|
try {
|
|
4453
|
-
event = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
4693
|
-
import * as
|
|
4694
|
-
import * as
|
|
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
|
|
4698
|
-
import * as
|
|
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 =
|
|
4702
|
-
if (!
|
|
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 =
|
|
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 (
|
|
4719
|
-
if (
|
|
4720
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
4843
|
-
if (
|
|
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
|
-
|
|
5403
|
+
fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
4848
5404
|
`);
|
|
4849
5405
|
wrote.push("kody.config.json");
|
|
4850
5406
|
}
|
|
4851
|
-
const workflowDir =
|
|
4852
|
-
const workflowPath =
|
|
4853
|
-
if (
|
|
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
|
-
|
|
4857
|
-
|
|
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 =
|
|
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 =
|
|
4863
|
-
if (
|
|
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
|
-
|
|
5422
|
+
fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
|
|
4867
5423
|
const discovery = runQaDiscovery(cwd);
|
|
4868
|
-
|
|
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 =
|
|
4881
|
-
if (
|
|
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
|
-
|
|
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
|
|
5024
|
-
import * as
|
|
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 =
|
|
5033
|
-
if (!
|
|
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 =
|
|
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
|
|
5171
|
-
import * as
|
|
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 =
|
|
5179
|
-
if (!
|
|
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 =
|
|
5760
|
+
stat = fs24.statSync(file);
|
|
5205
5761
|
} catch {
|
|
5206
5762
|
return;
|
|
5207
5763
|
}
|
|
5208
5764
|
let raw;
|
|
5209
5765
|
try {
|
|
5210
|
-
raw =
|
|
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() ??
|
|
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:
|
|
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 =
|
|
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 =
|
|
5848
|
+
const full = path22.join(dir, name);
|
|
5293
5849
|
let stat;
|
|
5294
5850
|
try {
|
|
5295
|
-
stat =
|
|
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
|
|
5318
|
-
import * as
|
|
5319
|
-
import * as
|
|
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 =
|
|
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 (!
|
|
5337
|
-
|
|
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 (!
|
|
5370
|
-
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
6013
|
+
const rel = path23.relative(vaultAbs, file);
|
|
5458
6014
|
let title = rel;
|
|
5459
6015
|
try {
|
|
5460
|
-
const raw =
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
6040
|
+
const full = path23.join(dir, name);
|
|
5485
6041
|
let stat;
|
|
5486
6042
|
try {
|
|
5487
|
-
stat =
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
7508
|
+
execFileSync26("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
6944
7509
|
} catch {
|
|
6945
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
8344
|
+
const ndjsonDir = path24.join(input.cwd, ".kody");
|
|
7778
8345
|
const invokeAgent = async (prompt) => {
|
|
7779
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
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 =
|
|
8441
|
+
const here = path24.dirname(new URL(import.meta.url).pathname);
|
|
7875
8442
|
const candidates = [
|
|
7876
|
-
|
|
8443
|
+
path24.join(here, "executables", profileName, "profile.json"),
|
|
7877
8444
|
// same-dir sibling (dev)
|
|
7878
|
-
|
|
8445
|
+
path24.join(here, "..", "executables", profileName, "profile.json"),
|
|
7879
8446
|
// up one (prod: dist/bin → dist/executables)
|
|
7880
|
-
|
|
8447
|
+
path24.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
7881
8448
|
// fallback
|
|
7882
8449
|
];
|
|
7883
8450
|
for (const c of candidates) {
|
|
7884
|
-
if (
|
|
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 =
|
|
7989
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
8403
|
-
if (
|
|
8404
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
9045
|
+
execFileSync30("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
|
|
8479
9046
|
} catch {
|
|
8480
9047
|
}
|
|
8481
9048
|
try {
|
|
8482
|
-
|
|
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 =
|
|
9058
|
+
const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
|
|
8492
9059
|
let tail = "";
|
|
8493
9060
|
try {
|
|
8494
|
-
if (
|
|
8495
|
-
const content =
|
|
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 ?
|
|
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 &&
|
|
9097
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
|
|
8531
9098
|
try {
|
|
8532
|
-
const evt = JSON.parse(
|
|
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 =
|
|
8748
|
-
const eventsFile =
|
|
8749
|
-
const paths = [sessionFile, eventsFile].filter((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
|
-
|
|
8754
|
-
|
|
8755
|
-
|
|
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 ?
|
|
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=${
|
|
9406
|
+
`\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
|
|
8840
9407
|
`
|
|
8841
9408
|
);
|
|
8842
9409
|
try {
|