@kody-ade/kody-engine 0.4.21 → 0.4.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "@kody-ade/kody-engine",
|
|
6
|
-
version: "0.4.
|
|
6
|
+
version: "0.4.23",
|
|
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",
|
|
@@ -1002,32 +1002,6 @@ function getExecutablesRoot() {
|
|
|
1002
1002
|
function getProjectExecutablesRoot() {
|
|
1003
1003
|
return path6.join(process.cwd(), ".kody", "executables");
|
|
1004
1004
|
}
|
|
1005
|
-
function getBuiltinJobsRoot() {
|
|
1006
|
-
const here = path6.dirname(new URL(import.meta.url).pathname);
|
|
1007
|
-
const candidates = [
|
|
1008
|
-
path6.join(here, "jobs"),
|
|
1009
|
-
// dev: src/
|
|
1010
|
-
path6.join(here, "..", "jobs"),
|
|
1011
|
-
// built: dist/bin → dist/jobs
|
|
1012
|
-
path6.join(here, "..", "src", "jobs")
|
|
1013
|
-
// fallback
|
|
1014
|
-
];
|
|
1015
|
-
for (const c of candidates) {
|
|
1016
|
-
if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
|
|
1017
|
-
}
|
|
1018
|
-
return candidates[0];
|
|
1019
|
-
}
|
|
1020
|
-
function listBuiltinJobs(root = getBuiltinJobsRoot()) {
|
|
1021
|
-
if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
|
|
1022
|
-
const out = [];
|
|
1023
|
-
for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
|
|
1024
|
-
if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
|
|
1025
|
-
const slug = ent.name.slice(0, -3);
|
|
1026
|
-
out.push({ slug, filePath: path6.join(root, ent.name) });
|
|
1027
|
-
}
|
|
1028
|
-
out.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
1029
|
-
return out;
|
|
1030
|
-
}
|
|
1031
1005
|
function getExecutableRoots() {
|
|
1032
1006
|
return [getProjectExecutablesRoot(), getExecutablesRoot()];
|
|
1033
1007
|
}
|
|
@@ -1585,11 +1559,17 @@ function parseScripts(p, raw) {
|
|
|
1585
1559
|
throw new ProfileError(p, `"scripts" must be an object with preflight and postflight arrays`);
|
|
1586
1560
|
}
|
|
1587
1561
|
const r = raw;
|
|
1562
|
+
const preflight = parseScriptList(p, "preflight", r.preflight);
|
|
1563
|
+
const postflight = parseScriptList(p, "postflight", r.postflight);
|
|
1588
1564
|
return {
|
|
1589
|
-
preflight
|
|
1590
|
-
postflight:
|
|
1565
|
+
preflight,
|
|
1566
|
+
postflight: pairLifecycleClears(preflight, postflight)
|
|
1591
1567
|
};
|
|
1592
1568
|
}
|
|
1569
|
+
function pairLifecycleClears(preflight, postflight) {
|
|
1570
|
+
const clears = preflight.filter((e) => e.script === "setLifecycleLabel" && typeof e.with?.label === "string").map((e) => ({ script: "clearLifecycleLabel", with: { label: e.with.label } }));
|
|
1571
|
+
return [...postflight, ...clears];
|
|
1572
|
+
}
|
|
1593
1573
|
function parseInputArtifacts(p, raw) {
|
|
1594
1574
|
if (raw === void 0 || raw === null) return [];
|
|
1595
1575
|
if (typeof raw !== "object" || Array.isArray(raw)) {
|
|
@@ -2490,148 +2470,6 @@ function defaultLabelMap() {
|
|
|
2490
2470
|
};
|
|
2491
2471
|
}
|
|
2492
2472
|
|
|
2493
|
-
// src/scripts/commitAndPush.ts
|
|
2494
|
-
var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
|
|
2495
|
-
var commitAndPush2 = async (ctx) => {
|
|
2496
|
-
const branch = ctx.data.branch;
|
|
2497
|
-
if (!branch) {
|
|
2498
|
-
ctx.data.commitResult = { committed: false, pushed: false };
|
|
2499
|
-
return;
|
|
2500
|
-
}
|
|
2501
|
-
if (ctx.data.agentDone === false) {
|
|
2502
|
-
ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
|
|
2503
|
-
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
2504
|
-
return;
|
|
2505
|
-
}
|
|
2506
|
-
const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
|
|
2507
|
-
try {
|
|
2508
|
-
const result = commitAndPush(branch, message, ctx.cwd);
|
|
2509
|
-
ctx.data.commitResult = result;
|
|
2510
|
-
const postCommitFiles = result.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
|
|
2511
|
-
ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f));
|
|
2512
|
-
if (result.committed && !result.pushed) {
|
|
2513
|
-
const reason = result.pushError ?? "push failed (no error detail)";
|
|
2514
|
-
ctx.data.commitCrash = reason;
|
|
2515
|
-
if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
|
|
2516
|
-
ctx.output.exitCode = 4;
|
|
2517
|
-
}
|
|
2518
|
-
if (!ctx.output.reason) ctx.output.reason = reason;
|
|
2519
|
-
process.stderr.write(`[kody commitAndPush] ${reason}
|
|
2520
|
-
`);
|
|
2521
|
-
}
|
|
2522
|
-
} catch (err) {
|
|
2523
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
2524
|
-
ctx.data.commitCrash = reason;
|
|
2525
|
-
ctx.data.commitResult = { committed: false, pushed: false };
|
|
2526
|
-
process.stderr.write(`[kody commitAndPush] failed: ${reason}
|
|
2527
|
-
`);
|
|
2528
|
-
}
|
|
2529
|
-
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
2530
|
-
};
|
|
2531
|
-
|
|
2532
|
-
// src/scripts/composePrompt.ts
|
|
2533
|
-
import * as fs13 from "fs";
|
|
2534
|
-
import * as path12 from "path";
|
|
2535
|
-
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
2536
|
-
var composePrompt = async (ctx, profile) => {
|
|
2537
|
-
const explicit = ctx.data.promptTemplate;
|
|
2538
|
-
const mode = ctx.args.mode;
|
|
2539
|
-
const candidates = [
|
|
2540
|
-
explicit ? path12.join(profile.dir, explicit) : null,
|
|
2541
|
-
mode ? path12.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
2542
|
-
path12.join(profile.dir, "prompt.md")
|
|
2543
|
-
].filter(Boolean);
|
|
2544
|
-
let templatePath = "";
|
|
2545
|
-
for (const c of candidates) {
|
|
2546
|
-
if (fs13.existsSync(c)) {
|
|
2547
|
-
templatePath = c;
|
|
2548
|
-
break;
|
|
2549
|
-
}
|
|
2550
|
-
}
|
|
2551
|
-
if (!templatePath) {
|
|
2552
|
-
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
2553
|
-
}
|
|
2554
|
-
const template = fs13.readFileSync(templatePath, "utf-8");
|
|
2555
|
-
const tokens = {
|
|
2556
|
-
...stringifyAll(ctx.args, "args."),
|
|
2557
|
-
...stringifyAll(ctx.data, ""),
|
|
2558
|
-
conventionsBlock: formatConventions(ctx.data.conventions),
|
|
2559
|
-
coverageBlock: formatCoverageBlock(
|
|
2560
|
-
ctx.data.coverageRules
|
|
2561
|
-
),
|
|
2562
|
-
toolsUsage: formatToolsUsage(profile),
|
|
2563
|
-
systemPromptAppend: profile.claudeCode.systemPromptAppend ?? "",
|
|
2564
|
-
repoOwner: ctx.config.github.owner,
|
|
2565
|
-
repoName: ctx.config.github.repo,
|
|
2566
|
-
defaultBranch: ctx.config.git.defaultBranch,
|
|
2567
|
-
branch: ctx.data.branch ?? ""
|
|
2568
|
-
};
|
|
2569
|
-
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
2570
|
-
};
|
|
2571
|
-
function stringifyAll(source, prefix) {
|
|
2572
|
-
const out = {};
|
|
2573
|
-
for (const [k, v] of Object.entries(source)) {
|
|
2574
|
-
const key = prefix + k;
|
|
2575
|
-
if (v === null || v === void 0) continue;
|
|
2576
|
-
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
2577
|
-
out[key] = String(v);
|
|
2578
|
-
} else if (Array.isArray(v)) {
|
|
2579
|
-
out[key] = v.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join("\n");
|
|
2580
|
-
} else if (typeof v === "object") {
|
|
2581
|
-
for (const [k2, v2] of Object.entries(v)) {
|
|
2582
|
-
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean") {
|
|
2583
|
-
out[`${key}.${k2}`] = String(v2);
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
}
|
|
2588
|
-
return out;
|
|
2589
|
-
}
|
|
2590
|
-
function formatConventions(conventions) {
|
|
2591
|
-
if (!conventions || conventions.length === 0) return "";
|
|
2592
|
-
const lines = ["# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)", ""];
|
|
2593
|
-
for (const c of conventions) {
|
|
2594
|
-
lines.push(`## ${c.path}${c.truncated ? " (truncated)" : ""}`);
|
|
2595
|
-
lines.push("");
|
|
2596
|
-
lines.push("```");
|
|
2597
|
-
lines.push(c.content);
|
|
2598
|
-
lines.push("```");
|
|
2599
|
-
lines.push("");
|
|
2600
|
-
}
|
|
2601
|
-
return lines.join("\n");
|
|
2602
|
-
}
|
|
2603
|
-
function formatCoverageBlock(reqs) {
|
|
2604
|
-
if (!reqs || reqs.length === 0) return "";
|
|
2605
|
-
const lines = [
|
|
2606
|
-
"# Test coverage requirements (ENFORCED)",
|
|
2607
|
-
"",
|
|
2608
|
-
"Every newly added file matching one of these patterns MUST be accompanied by a sibling test file in the same commit. The wrapper checks this after you finish; if any sibling test is missing, the run will fail and the issue will be re-invoked with the gap as feedback.",
|
|
2609
|
-
""
|
|
2610
|
-
];
|
|
2611
|
-
for (const r of reqs) lines.push(`- new \`${r.pattern}\` \u2192 must include sibling \`${r.requireSibling}\``);
|
|
2612
|
-
lines.push("");
|
|
2613
|
-
return lines.join("\n");
|
|
2614
|
-
}
|
|
2615
|
-
function formatToolsUsage(profile) {
|
|
2616
|
-
const entries = (profile.cliTools ?? []).filter((t) => t.usage.trim().length > 0);
|
|
2617
|
-
if (entries.length === 0) return "";
|
|
2618
|
-
const lines = ["# Available CLI tools", ""];
|
|
2619
|
-
for (const t of entries) {
|
|
2620
|
-
lines.push(`## \`${t.name}\``);
|
|
2621
|
-
lines.push(t.usage);
|
|
2622
|
-
if (t.allowedUses.length > 0) {
|
|
2623
|
-
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) => `\`${u}\``).join(", ")}`);
|
|
2624
|
-
}
|
|
2625
|
-
lines.push("");
|
|
2626
|
-
}
|
|
2627
|
-
return lines.join("\n");
|
|
2628
|
-
}
|
|
2629
|
-
|
|
2630
|
-
// src/scripts/createQaGoal.ts
|
|
2631
|
-
import { execFileSync as execFileSync9 } from "child_process";
|
|
2632
|
-
import * as fs14 from "fs";
|
|
2633
|
-
import * as path13 from "path";
|
|
2634
|
-
|
|
2635
2473
|
// src/issue.ts
|
|
2636
2474
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2637
2475
|
var API_TIMEOUT_MS3 = 3e4;
|
|
@@ -2718,61 +2556,330 @@ function getPrDiff(prNumber, cwd) {
|
|
|
2718
2556
|
);
|
|
2719
2557
|
return "";
|
|
2720
2558
|
}
|
|
2721
|
-
}
|
|
2722
|
-
function getPrReviews(prNumber, cwd) {
|
|
2723
|
-
try {
|
|
2724
|
-
const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
|
|
2725
|
-
const parsed = JSON.parse(output);
|
|
2726
|
-
if (!Array.isArray(parsed?.reviews)) return [];
|
|
2727
|
-
return parsed.reviews.map(
|
|
2728
|
-
(r) => ({
|
|
2729
|
-
body: r.body ?? "",
|
|
2730
|
-
state: r.state ?? "",
|
|
2731
|
-
author: r.author?.login ?? "unknown",
|
|
2732
|
-
submittedAt: r.submittedAt ?? ""
|
|
2733
|
-
})
|
|
2734
|
-
);
|
|
2735
|
-
} catch {
|
|
2736
|
-
return [];
|
|
2559
|
+
}
|
|
2560
|
+
function getPrReviews(prNumber, cwd) {
|
|
2561
|
+
try {
|
|
2562
|
+
const output = gh2(["pr", "view", String(prNumber), "--json", "reviews"], { cwd });
|
|
2563
|
+
const parsed = JSON.parse(output);
|
|
2564
|
+
if (!Array.isArray(parsed?.reviews)) return [];
|
|
2565
|
+
return parsed.reviews.map(
|
|
2566
|
+
(r) => ({
|
|
2567
|
+
body: r.body ?? "",
|
|
2568
|
+
state: r.state ?? "",
|
|
2569
|
+
author: r.author?.login ?? "unknown",
|
|
2570
|
+
submittedAt: r.submittedAt ?? ""
|
|
2571
|
+
})
|
|
2572
|
+
);
|
|
2573
|
+
} catch {
|
|
2574
|
+
return [];
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function getPrComments(prNumber, cwd) {
|
|
2578
|
+
try {
|
|
2579
|
+
const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
|
|
2580
|
+
const parsed = JSON.parse(output);
|
|
2581
|
+
if (!Array.isArray(parsed?.comments)) return [];
|
|
2582
|
+
return parsed.comments.map((c) => ({
|
|
2583
|
+
body: c.body ?? "",
|
|
2584
|
+
author: c.author?.login ?? "unknown",
|
|
2585
|
+
createdAt: c.createdAt ?? ""
|
|
2586
|
+
})).filter((c) => c.body.trim().length > 0);
|
|
2587
|
+
} catch {
|
|
2588
|
+
return [];
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
|
|
2592
|
+
function isReviewShaped(body) {
|
|
2593
|
+
return VERDICT_HEADING.test(body);
|
|
2594
|
+
}
|
|
2595
|
+
function getPrLatestReviewBody(prNumber, cwd) {
|
|
2596
|
+
const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
|
|
2597
|
+
const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
|
|
2598
|
+
const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
|
|
2599
|
+
if (all.length > 0) return all[0].body;
|
|
2600
|
+
const pr = getPr(prNumber, cwd);
|
|
2601
|
+
return pr.body;
|
|
2602
|
+
}
|
|
2603
|
+
function postPrReviewComment(prNumber, body, cwd) {
|
|
2604
|
+
try {
|
|
2605
|
+
gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
|
|
2606
|
+
} catch (err) {
|
|
2607
|
+
process.stderr.write(
|
|
2608
|
+
`[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
|
|
2609
|
+
`
|
|
2610
|
+
);
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2614
|
+
// src/lifecycleLabels.ts
|
|
2615
|
+
var KODY_NAMESPACE = "kody";
|
|
2616
|
+
function groupOf(label) {
|
|
2617
|
+
const idx = label.indexOf(":");
|
|
2618
|
+
return idx === -1 ? label : label.slice(0, idx + 1);
|
|
2619
|
+
}
|
|
2620
|
+
function collectProfileLabels() {
|
|
2621
|
+
const byLabel = /* @__PURE__ */ new Map();
|
|
2622
|
+
for (const exe of listExecutables()) {
|
|
2623
|
+
let profile;
|
|
2624
|
+
try {
|
|
2625
|
+
profile = loadProfile(exe.profilePath);
|
|
2626
|
+
} catch {
|
|
2627
|
+
continue;
|
|
2628
|
+
}
|
|
2629
|
+
for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
2630
|
+
const spec = extractLabelSpec(entry);
|
|
2631
|
+
if (spec) byLabel.set(spec.label, spec);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
return [...byLabel.values()];
|
|
2635
|
+
}
|
|
2636
|
+
function extractLabelSpec(entry) {
|
|
2637
|
+
if (entry.script !== "setLifecycleLabel") return null;
|
|
2638
|
+
const w = entry.with;
|
|
2639
|
+
if (!w) return null;
|
|
2640
|
+
const label = typeof w.label === "string" ? w.label : null;
|
|
2641
|
+
if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
|
|
2642
|
+
return {
|
|
2643
|
+
label,
|
|
2644
|
+
color: typeof w.color === "string" ? w.color : void 0,
|
|
2645
|
+
description: typeof w.description === "string" ? w.description : void 0
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2648
|
+
function ensureLabels(cwd) {
|
|
2649
|
+
const result = { created: [], failed: [] };
|
|
2650
|
+
for (const spec of collectProfileLabels()) {
|
|
2651
|
+
try {
|
|
2652
|
+
createLabelInRepo(spec, cwd);
|
|
2653
|
+
result.created.push(spec.label);
|
|
2654
|
+
} catch (err) {
|
|
2655
|
+
result.failed.push({ label: spec.label, reason: errMsg(err) });
|
|
2656
|
+
}
|
|
2657
|
+
}
|
|
2658
|
+
return result;
|
|
2659
|
+
}
|
|
2660
|
+
function getIssueLabels(issueNumber, cwd) {
|
|
2661
|
+
try {
|
|
2662
|
+
const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
2663
|
+
return output.split("\n").filter(Boolean);
|
|
2664
|
+
} catch {
|
|
2665
|
+
return [];
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
2668
|
+
function addLabel(issueNumber, label, cwd) {
|
|
2669
|
+
gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
|
|
2670
|
+
}
|
|
2671
|
+
function removeLabel(issueNumber, label, cwd) {
|
|
2672
|
+
try {
|
|
2673
|
+
gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
|
|
2674
|
+
} catch {
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
function createLabelInRepo(spec, cwd) {
|
|
2678
|
+
const args = ["label", "create", spec.label, "--force"];
|
|
2679
|
+
if (spec.color) args.push("--color", spec.color);
|
|
2680
|
+
if (spec.description) args.push("--description", spec.description);
|
|
2681
|
+
gh2(args, { cwd });
|
|
2682
|
+
}
|
|
2683
|
+
function setKodyLabel(issueNumber, spec, cwd) {
|
|
2684
|
+
const target = spec.label;
|
|
2685
|
+
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
2686
|
+
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
2687
|
+
`);
|
|
2688
|
+
return;
|
|
2689
|
+
}
|
|
2690
|
+
const targetGroup = groupOf(target);
|
|
2691
|
+
const present = getIssueLabels(issueNumber, cwd);
|
|
2692
|
+
for (const label of present) {
|
|
2693
|
+
if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
|
|
2694
|
+
removeLabel(issueNumber, label, cwd);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
try {
|
|
2698
|
+
addLabel(issueNumber, target, cwd);
|
|
2699
|
+
} catch (err) {
|
|
2700
|
+
if (looksLikeMissingLabel(err)) {
|
|
2701
|
+
try {
|
|
2702
|
+
createLabelInRepo(spec, cwd);
|
|
2703
|
+
addLabel(issueNumber, target, cwd);
|
|
2704
|
+
return;
|
|
2705
|
+
} catch (retryErr) {
|
|
2706
|
+
process.stderr.write(
|
|
2707
|
+
`[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
|
|
2708
|
+
`
|
|
2709
|
+
);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
2714
|
+
`);
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
function looksLikeMissingLabel(err) {
|
|
2718
|
+
const msg = errMsg(err).toLowerCase();
|
|
2719
|
+
return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
|
|
2720
|
+
}
|
|
2721
|
+
function errMsg(err) {
|
|
2722
|
+
if (err instanceof Error) return err.message;
|
|
2723
|
+
if (typeof err === "object" && err !== null) {
|
|
2724
|
+
const e = err;
|
|
2725
|
+
const stderr = e.stderr?.toString().trim();
|
|
2726
|
+
if (stderr) return stderr;
|
|
2727
|
+
if (e.message) return e.message;
|
|
2728
|
+
}
|
|
2729
|
+
return String(err);
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
// src/scripts/clearLifecycleLabel.ts
|
|
2733
|
+
var clearLifecycleLabel = async (ctx, _profile, _agentResult, args) => {
|
|
2734
|
+
const label = args?.label;
|
|
2735
|
+
if (typeof label !== "string" || !label.startsWith(KODY_NAMESPACE)) return;
|
|
2736
|
+
const target = ctx.args.issue ?? ctx.args.pr;
|
|
2737
|
+
if (typeof target !== "number" || !Number.isFinite(target)) return;
|
|
2738
|
+
removeLabel(target, label, ctx.cwd);
|
|
2739
|
+
};
|
|
2740
|
+
|
|
2741
|
+
// src/scripts/commitAndPush.ts
|
|
2742
|
+
var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
|
|
2743
|
+
var commitAndPush2 = async (ctx) => {
|
|
2744
|
+
const branch = ctx.data.branch;
|
|
2745
|
+
if (!branch) {
|
|
2746
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (ctx.data.agentDone === false) {
|
|
2750
|
+
ctx.data.commitResult = { committed: false, pushed: false, skippedReason: "agentDone=false" };
|
|
2751
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2754
|
+
const message = ctx.data.commitMessage || DEFAULT_COMMIT_MESSAGE;
|
|
2755
|
+
try {
|
|
2756
|
+
const result = commitAndPush(branch, message, ctx.cwd);
|
|
2757
|
+
ctx.data.commitResult = result;
|
|
2758
|
+
const postCommitFiles = result.committed ? listFilesInCommit("HEAD", ctx.cwd) : listChangedFiles(ctx.cwd);
|
|
2759
|
+
ctx.data.changedFiles = postCommitFiles.filter((f) => !isForbiddenPath(f));
|
|
2760
|
+
if (result.committed && !result.pushed) {
|
|
2761
|
+
const reason = result.pushError ?? "push failed (no error detail)";
|
|
2762
|
+
ctx.data.commitCrash = reason;
|
|
2763
|
+
if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
|
|
2764
|
+
ctx.output.exitCode = 4;
|
|
2765
|
+
}
|
|
2766
|
+
if (!ctx.output.reason) ctx.output.reason = reason;
|
|
2767
|
+
process.stderr.write(`[kody commitAndPush] ${reason}
|
|
2768
|
+
`);
|
|
2769
|
+
}
|
|
2770
|
+
} catch (err) {
|
|
2771
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
2772
|
+
ctx.data.commitCrash = reason;
|
|
2773
|
+
ctx.data.commitResult = { committed: false, pushed: false };
|
|
2774
|
+
process.stderr.write(`[kody commitAndPush] failed: ${reason}
|
|
2775
|
+
`);
|
|
2776
|
+
}
|
|
2777
|
+
ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
|
|
2778
|
+
};
|
|
2779
|
+
|
|
2780
|
+
// src/scripts/composePrompt.ts
|
|
2781
|
+
import * as fs13 from "fs";
|
|
2782
|
+
import * as path12 from "path";
|
|
2783
|
+
var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
|
|
2784
|
+
var composePrompt = async (ctx, profile) => {
|
|
2785
|
+
const explicit = ctx.data.promptTemplate;
|
|
2786
|
+
const mode = ctx.args.mode;
|
|
2787
|
+
const candidates = [
|
|
2788
|
+
explicit ? path12.join(profile.dir, explicit) : null,
|
|
2789
|
+
mode ? path12.join(profile.dir, "prompts", `${mode}.md`) : null,
|
|
2790
|
+
path12.join(profile.dir, "prompt.md")
|
|
2791
|
+
].filter(Boolean);
|
|
2792
|
+
let templatePath = "";
|
|
2793
|
+
for (const c of candidates) {
|
|
2794
|
+
if (fs13.existsSync(c)) {
|
|
2795
|
+
templatePath = c;
|
|
2796
|
+
break;
|
|
2797
|
+
}
|
|
2798
|
+
}
|
|
2799
|
+
if (!templatePath) {
|
|
2800
|
+
throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
|
|
2737
2801
|
}
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2802
|
+
const template = fs13.readFileSync(templatePath, "utf-8");
|
|
2803
|
+
const tokens = {
|
|
2804
|
+
...stringifyAll(ctx.args, "args."),
|
|
2805
|
+
...stringifyAll(ctx.data, ""),
|
|
2806
|
+
conventionsBlock: formatConventions(ctx.data.conventions),
|
|
2807
|
+
coverageBlock: formatCoverageBlock(
|
|
2808
|
+
ctx.data.coverageRules
|
|
2809
|
+
),
|
|
2810
|
+
toolsUsage: formatToolsUsage(profile),
|
|
2811
|
+
systemPromptAppend: profile.claudeCode.systemPromptAppend ?? "",
|
|
2812
|
+
repoOwner: ctx.config.github.owner,
|
|
2813
|
+
repoName: ctx.config.github.repo,
|
|
2814
|
+
defaultBranch: ctx.config.git.defaultBranch,
|
|
2815
|
+
branch: ctx.data.branch ?? ""
|
|
2816
|
+
};
|
|
2817
|
+
ctx.data.prompt = template.replace(MUSTACHE, (_, key) => tokens[key] ?? "");
|
|
2818
|
+
};
|
|
2819
|
+
function stringifyAll(source, prefix) {
|
|
2820
|
+
const out = {};
|
|
2821
|
+
for (const [k, v] of Object.entries(source)) {
|
|
2822
|
+
const key = prefix + k;
|
|
2823
|
+
if (v === null || v === void 0) continue;
|
|
2824
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
|
|
2825
|
+
out[key] = String(v);
|
|
2826
|
+
} else if (Array.isArray(v)) {
|
|
2827
|
+
out[key] = v.map((x) => typeof x === "string" ? x : JSON.stringify(x)).join("\n");
|
|
2828
|
+
} else if (typeof v === "object") {
|
|
2829
|
+
for (const [k2, v2] of Object.entries(v)) {
|
|
2830
|
+
if (typeof v2 === "string" || typeof v2 === "number" || typeof v2 === "boolean") {
|
|
2831
|
+
out[`${key}.${k2}`] = String(v2);
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2751
2835
|
}
|
|
2836
|
+
return out;
|
|
2752
2837
|
}
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2838
|
+
function formatConventions(conventions) {
|
|
2839
|
+
if (!conventions || conventions.length === 0) return "";
|
|
2840
|
+
const lines = ["# Project conventions (AUTHORITATIVE \u2014 follow these over patterns you infer from code)", ""];
|
|
2841
|
+
for (const c of conventions) {
|
|
2842
|
+
lines.push(`## ${c.path}${c.truncated ? " (truncated)" : ""}`);
|
|
2843
|
+
lines.push("");
|
|
2844
|
+
lines.push("```");
|
|
2845
|
+
lines.push(c.content);
|
|
2846
|
+
lines.push("```");
|
|
2847
|
+
lines.push("");
|
|
2848
|
+
}
|
|
2849
|
+
return lines.join("\n");
|
|
2756
2850
|
}
|
|
2757
|
-
function
|
|
2758
|
-
|
|
2759
|
-
const
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2851
|
+
function formatCoverageBlock(reqs) {
|
|
2852
|
+
if (!reqs || reqs.length === 0) return "";
|
|
2853
|
+
const lines = [
|
|
2854
|
+
"# Test coverage requirements (ENFORCED)",
|
|
2855
|
+
"",
|
|
2856
|
+
"Every newly added file matching one of these patterns MUST be accompanied by a sibling test file in the same commit. The wrapper checks this after you finish; if any sibling test is missing, the run will fail and the issue will be re-invoked with the gap as feedback.",
|
|
2857
|
+
""
|
|
2858
|
+
];
|
|
2859
|
+
for (const r of reqs) lines.push(`- new \`${r.pattern}\` \u2192 must include sibling \`${r.requireSibling}\``);
|
|
2860
|
+
lines.push("");
|
|
2861
|
+
return lines.join("\n");
|
|
2764
2862
|
}
|
|
2765
|
-
function
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
)
|
|
2863
|
+
function formatToolsUsage(profile) {
|
|
2864
|
+
const entries = (profile.cliTools ?? []).filter((t) => t.usage.trim().length > 0);
|
|
2865
|
+
if (entries.length === 0) return "";
|
|
2866
|
+
const lines = ["# Available CLI tools", ""];
|
|
2867
|
+
for (const t of entries) {
|
|
2868
|
+
lines.push(`## \`${t.name}\``);
|
|
2869
|
+
lines.push(t.usage);
|
|
2870
|
+
if (t.allowedUses.length > 0) {
|
|
2871
|
+
lines.push(`Allowed sub-commands: ${t.allowedUses.map((u) => `\`${u}\``).join(", ")}`);
|
|
2872
|
+
}
|
|
2873
|
+
lines.push("");
|
|
2773
2874
|
}
|
|
2875
|
+
return lines.join("\n");
|
|
2774
2876
|
}
|
|
2775
2877
|
|
|
2878
|
+
// src/scripts/createQaGoal.ts
|
|
2879
|
+
import { execFileSync as execFileSync9 } from "child_process";
|
|
2880
|
+
import * as fs14 from "fs";
|
|
2881
|
+
import * as path13 from "path";
|
|
2882
|
+
|
|
2776
2883
|
// src/scripts/postReviewResult.ts
|
|
2777
2884
|
function detectVerdict(body) {
|
|
2778
2885
|
const m = body.match(/##\s*Verdict\s*:\s*(PASS|CONCERNS|FAIL)\b/i);
|
|
@@ -4738,125 +4845,6 @@ function collectExpectedTests(raw) {
|
|
|
4738
4845
|
|
|
4739
4846
|
// src/scripts/finishFlow.ts
|
|
4740
4847
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
4741
|
-
|
|
4742
|
-
// src/lifecycleLabels.ts
|
|
4743
|
-
var KODY_NAMESPACE = "kody";
|
|
4744
|
-
function groupOf(label) {
|
|
4745
|
-
const idx = label.indexOf(":");
|
|
4746
|
-
return idx === -1 ? label : label.slice(0, idx + 1);
|
|
4747
|
-
}
|
|
4748
|
-
function collectProfileLabels() {
|
|
4749
|
-
const byLabel = /* @__PURE__ */ new Map();
|
|
4750
|
-
for (const exe of listExecutables()) {
|
|
4751
|
-
let profile;
|
|
4752
|
-
try {
|
|
4753
|
-
profile = loadProfile(exe.profilePath);
|
|
4754
|
-
} catch {
|
|
4755
|
-
continue;
|
|
4756
|
-
}
|
|
4757
|
-
for (const entry of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
4758
|
-
const spec = extractLabelSpec(entry);
|
|
4759
|
-
if (spec) byLabel.set(spec.label, spec);
|
|
4760
|
-
}
|
|
4761
|
-
}
|
|
4762
|
-
return [...byLabel.values()];
|
|
4763
|
-
}
|
|
4764
|
-
function extractLabelSpec(entry) {
|
|
4765
|
-
const w = entry.with;
|
|
4766
|
-
if (!w) return null;
|
|
4767
|
-
const label = typeof w.label === "string" ? w.label : null;
|
|
4768
|
-
if (!label || !label.startsWith(KODY_NAMESPACE)) return null;
|
|
4769
|
-
return {
|
|
4770
|
-
label,
|
|
4771
|
-
color: typeof w.color === "string" ? w.color : void 0,
|
|
4772
|
-
description: typeof w.description === "string" ? w.description : void 0
|
|
4773
|
-
};
|
|
4774
|
-
}
|
|
4775
|
-
function ensureLabels(cwd) {
|
|
4776
|
-
const result = { created: [], failed: [] };
|
|
4777
|
-
for (const spec of collectProfileLabels()) {
|
|
4778
|
-
try {
|
|
4779
|
-
createLabelInRepo(spec, cwd);
|
|
4780
|
-
result.created.push(spec.label);
|
|
4781
|
-
} catch (err) {
|
|
4782
|
-
result.failed.push({ label: spec.label, reason: errMsg(err) });
|
|
4783
|
-
}
|
|
4784
|
-
}
|
|
4785
|
-
return result;
|
|
4786
|
-
}
|
|
4787
|
-
function getIssueLabels(issueNumber, cwd) {
|
|
4788
|
-
try {
|
|
4789
|
-
const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
4790
|
-
return output.split("\n").filter(Boolean);
|
|
4791
|
-
} catch {
|
|
4792
|
-
return [];
|
|
4793
|
-
}
|
|
4794
|
-
}
|
|
4795
|
-
function addLabel(issueNumber, label, cwd) {
|
|
4796
|
-
gh2(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
|
|
4797
|
-
}
|
|
4798
|
-
function removeLabel(issueNumber, label, cwd) {
|
|
4799
|
-
try {
|
|
4800
|
-
gh2(["issue", "edit", String(issueNumber), "--remove-label", label], { cwd });
|
|
4801
|
-
} catch {
|
|
4802
|
-
}
|
|
4803
|
-
}
|
|
4804
|
-
function createLabelInRepo(spec, cwd) {
|
|
4805
|
-
const args = ["label", "create", spec.label, "--force"];
|
|
4806
|
-
if (spec.color) args.push("--color", spec.color);
|
|
4807
|
-
if (spec.description) args.push("--description", spec.description);
|
|
4808
|
-
gh2(args, { cwd });
|
|
4809
|
-
}
|
|
4810
|
-
function setKodyLabel(issueNumber, spec, cwd) {
|
|
4811
|
-
const target = spec.label;
|
|
4812
|
-
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
4813
|
-
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
4814
|
-
`);
|
|
4815
|
-
return;
|
|
4816
|
-
}
|
|
4817
|
-
const targetGroup = groupOf(target);
|
|
4818
|
-
const present = getIssueLabels(issueNumber, cwd);
|
|
4819
|
-
for (const label of present) {
|
|
4820
|
-
if (label !== target && label.startsWith(KODY_NAMESPACE) && groupOf(label) === targetGroup) {
|
|
4821
|
-
removeLabel(issueNumber, label, cwd);
|
|
4822
|
-
}
|
|
4823
|
-
}
|
|
4824
|
-
try {
|
|
4825
|
-
addLabel(issueNumber, target, cwd);
|
|
4826
|
-
} catch (err) {
|
|
4827
|
-
if (looksLikeMissingLabel(err)) {
|
|
4828
|
-
try {
|
|
4829
|
-
createLabelInRepo(spec, cwd);
|
|
4830
|
-
addLabel(issueNumber, target, cwd);
|
|
4831
|
-
return;
|
|
4832
|
-
} catch (retryErr) {
|
|
4833
|
-
process.stderr.write(
|
|
4834
|
-
`[kody] setKodyLabel: create+retry failed for ${target} on #${issueNumber}: ${errMsg(retryErr)}
|
|
4835
|
-
`
|
|
4836
|
-
);
|
|
4837
|
-
return;
|
|
4838
|
-
}
|
|
4839
|
-
}
|
|
4840
|
-
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
4841
|
-
`);
|
|
4842
|
-
}
|
|
4843
|
-
}
|
|
4844
|
-
function looksLikeMissingLabel(err) {
|
|
4845
|
-
const msg = errMsg(err).toLowerCase();
|
|
4846
|
-
return msg.includes("not found") || msg.includes("could not add label") || msg.includes("could not resolve to a label");
|
|
4847
|
-
}
|
|
4848
|
-
function errMsg(err) {
|
|
4849
|
-
if (err instanceof Error) return err.message;
|
|
4850
|
-
if (typeof err === "object" && err !== null) {
|
|
4851
|
-
const e = err;
|
|
4852
|
-
const stderr = e.stderr?.toString().trim();
|
|
4853
|
-
if (stderr) return stderr;
|
|
4854
|
-
if (e.message) return e.message;
|
|
4855
|
-
}
|
|
4856
|
-
return String(err);
|
|
4857
|
-
}
|
|
4858
|
-
|
|
4859
|
-
// src/scripts/finishFlow.ts
|
|
4860
4848
|
var API_TIMEOUT_MS6 = 3e4;
|
|
4861
4849
|
var STATUS_ICON = {
|
|
4862
4850
|
"review-passed": "\u2705",
|
|
@@ -5487,21 +5475,6 @@ function performInit(cwd, force) {
|
|
|
5487
5475
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
5488
5476
|
}
|
|
5489
5477
|
}
|
|
5490
|
-
const builtinJobs = listBuiltinJobs();
|
|
5491
|
-
if (builtinJobs.length > 0) {
|
|
5492
|
-
const jobsDir = path20.join(cwd, ".kody", "jobs");
|
|
5493
|
-
fs22.mkdirSync(jobsDir, { recursive: true });
|
|
5494
|
-
for (const job of builtinJobs) {
|
|
5495
|
-
const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
|
|
5496
|
-
const target = path20.join(cwd, rel);
|
|
5497
|
-
if (fs22.existsSync(target) && !force) {
|
|
5498
|
-
skipped.push(rel);
|
|
5499
|
-
continue;
|
|
5500
|
-
}
|
|
5501
|
-
fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
|
|
5502
|
-
wrote.push(rel);
|
|
5503
|
-
}
|
|
5504
|
-
}
|
|
5505
5478
|
for (const exe of listExecutables()) {
|
|
5506
5479
|
let profile;
|
|
5507
5480
|
try {
|
|
@@ -8053,7 +8026,8 @@ var postflightScripts = {
|
|
|
8053
8026
|
recordOutcome,
|
|
8054
8027
|
mergeReleasePr,
|
|
8055
8028
|
waitForCi,
|
|
8056
|
-
markFlowSuccess
|
|
8029
|
+
markFlowSuccess,
|
|
8030
|
+
clearLifecycleLabel
|
|
8057
8031
|
};
|
|
8058
8032
|
var allScriptNames = /* @__PURE__ */ new Set([
|
|
8059
8033
|
...Object.keys(preflightScripts),
|
|
@@ -31,7 +31,17 @@
|
|
|
31
31
|
set -euo pipefail
|
|
32
32
|
|
|
33
33
|
goal_id="${KODY_ARG_GOAL:-}"
|
|
34
|
-
|
|
34
|
+
# Default branch: prefer KODY_CFG_GIT_DEFAULTBRANCH (config), then ask the
|
|
35
|
+
# repo via the GitHub API, finally fall back to "main". Past regression: a
|
|
36
|
+
# hardcoded "main" fallback opened goal PRs against `main` for repos whose
|
|
37
|
+
# real default is `dev`, which then needed manual retargeting.
|
|
38
|
+
default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-}"
|
|
39
|
+
if [ -z "$default_branch" ]; then
|
|
40
|
+
default_branch=$(gh api "repos/{owner}/{repo}" --jq .default_branch 2>/dev/null || echo "")
|
|
41
|
+
fi
|
|
42
|
+
if [ -z "$default_branch" ]; then
|
|
43
|
+
default_branch="main"
|
|
44
|
+
fi
|
|
35
45
|
|
|
36
46
|
if [ -z "$goal_id" ]; then
|
|
37
47
|
echo "KODY_REASON=missing --goal"
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "watch-stale-prs",
|
|
3
|
+
"role": "watch",
|
|
4
|
+
"describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
|
|
5
|
+
"kind": "scheduled",
|
|
6
|
+
"schedule": "0 8 * * MON",
|
|
7
|
+
"inputs": [],
|
|
8
|
+
"claudeCode": {
|
|
9
|
+
"model": "inherit",
|
|
10
|
+
"permissionMode": "default",
|
|
11
|
+
"maxTurns": null,
|
|
12
|
+
"systemPromptAppend": null,
|
|
13
|
+
"tools": [],
|
|
14
|
+
"hooks": [],
|
|
15
|
+
"skills": [],
|
|
16
|
+
"commands": [],
|
|
17
|
+
"subagents": [],
|
|
18
|
+
"plugins": [],
|
|
19
|
+
"mcpServers": []
|
|
20
|
+
},
|
|
21
|
+
"cliTools": [],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"preflight": [
|
|
24
|
+
{
|
|
25
|
+
"script": "watchStalePrsFlow"
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"postflight": []
|
|
29
|
+
}
|
|
30
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.23",
|
|
4
4
|
"description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|