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