@kody-ade/kody-engine 0.4.20 → 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"
|
|
@@ -335,6 +345,16 @@ ensure_label "$failed_label" "b60205" "kody goal-runner: task failed; needs huma
|
|
|
335
345
|
# counting child tasks, so list_goal_issues can filter it out cleanly.
|
|
336
346
|
ensure_goal_issue
|
|
337
347
|
|
|
348
|
+
# Open the draft goal PR if the goal branch already exists. Must run BEFORE
|
|
349
|
+
# any of the early exits below (in_flight check, no-undispatched-task idle,
|
|
350
|
+
# etc.) — otherwise active goals that always have a task in flight would
|
|
351
|
+
# never get past the in_flight gate to reach the late call site, leaving
|
|
352
|
+
# the umbrella row without its branch + preview anchor in the dashboard.
|
|
353
|
+
# `ensure_goal_pr` is a safe no-op when the branch hasn't been created yet
|
|
354
|
+
# (the lazy-branch-creation block at the dispatch site handles that case;
|
|
355
|
+
# the next tick picks up the PR creation here).
|
|
356
|
+
ensure_goal_pr
|
|
357
|
+
|
|
338
358
|
# Merge ready goal-task PRs into the goal branch. We own the merge here
|
|
339
359
|
# instead of relying on GitHub's `--auto` flag (which requires the repo's
|
|
340
360
|
# "Allow auto-merge" setting and silently no-ops when disabled). Only merge
|
|
@@ -543,11 +563,8 @@ else
|
|
|
543
563
|
fi
|
|
544
564
|
fi
|
|
545
565
|
fi
|
|
546
|
-
|
|
547
|
-
#
|
|
548
|
-
# single anchor for the goal's branch + preview + CI; finalize promotes it
|
|
549
|
-
# from draft to ready-for-review when every task has closed.
|
|
550
|
-
ensure_goal_pr
|
|
566
|
+
# (`ensure_goal_pr` runs at the top of the active path so it's reached even
|
|
567
|
+
# when this tick exits early via the in_flight gate; not duplicated here.)
|
|
551
568
|
|
|
552
569
|
echo "[goal-tick] dispatching @kody on task #$next_issue (--base $goal_branch)"
|
|
553
570
|
gh issue comment "$next_issue" --body "@kody --base ${goal_branch}"
|
|
@@ -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
|
+
}
|
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
every: 7d
|
|
3
|
+
---
|
|
4
|
+
|
|
1
5
|
# watch-stale-prs
|
|
2
6
|
|
|
3
7
|
> Weekly digest of open PRs that haven't been touched in a while. Writes a
|
|
4
8
|
> markdown report at `.kody/reports/watch-stale-prs.md` (surfaced by the
|
|
5
9
|
> dashboard's `/reports` page).
|
|
10
|
+
>
|
|
11
|
+
> Cadence is enforced by the engine via the `every: 7d` frontmatter — this
|
|
12
|
+
> file only fires once per 7 days regardless of how often `job-scheduler`
|
|
13
|
+
> wakes. No prose cadence guard needed.
|
|
6
14
|
|
|
7
15
|
## Job
|
|
8
16
|
|
|
@@ -10,10 +18,6 @@ Find every open PR untouched for **≥ 7 days** and write a report listing
|
|
|
10
18
|
them, sorted by staleness (oldest first). When there are no stale PRs,
|
|
11
19
|
write a short "all clear" report so operators know the check ran.
|
|
12
20
|
|
|
13
|
-
**Cadence guard.** Skip this tick unless `data.lastRunAt` is null or older
|
|
14
|
-
than 7 days from now. When skipping, emit the same state back unchanged
|
|
15
|
-
with no `gh` calls.
|
|
16
|
-
|
|
17
21
|
### What "stale" means
|
|
18
22
|
|
|
19
23
|
A PR is stale if:
|
|
@@ -71,16 +75,15 @@ Truncate to the 50 oldest if the list is longer; append a final line
|
|
|
71
75
|
|
|
72
76
|
## State
|
|
73
77
|
|
|
74
|
-
`cursor`:
|
|
75
|
-
|
|
76
|
-
- `"idle"` — between runs (cadence guard skipped this tick or the run
|
|
77
|
-
finished cleanly).
|
|
78
|
+
`cursor`: always `"idle"` — this job has no phases; each fire is a
|
|
79
|
+
one-shot report write.
|
|
78
80
|
|
|
79
81
|
`data`:
|
|
80
82
|
|
|
81
|
-
- `lastRunAt` (string, ISO) — when the report was last written. Used by
|
|
82
|
-
the cadence guard.
|
|
83
83
|
- `lastStaleCount` (number) — how many stale PRs were in the most recent
|
|
84
|
-
report. Diagnostic only.
|
|
84
|
+
report. Diagnostic only; the engine ignores it.
|
|
85
|
+
|
|
86
|
+
(Engine-managed fields like `lastFiredAt` live under `data` automatically;
|
|
87
|
+
do not write or rely on them from the prompt.)
|
|
85
88
|
|
|
86
89
|
`done`: always `false` — this job is evergreen.
|
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",
|