@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.21",
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: parseScriptList(p, "preflight", r.preflight),
1590
- postflight: parseScriptList(p, "postflight", r.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
- function getPrComments(prNumber, cwd) {
2740
- try {
2741
- const output = gh2(["pr", "view", String(prNumber), "--json", "comments"], { cwd });
2742
- const parsed = JSON.parse(output);
2743
- if (!Array.isArray(parsed?.comments)) return [];
2744
- return parsed.comments.map((c) => ({
2745
- body: c.body ?? "",
2746
- author: c.author?.login ?? "unknown",
2747
- createdAt: c.createdAt ?? ""
2748
- })).filter((c) => c.body.trim().length > 0);
2749
- } catch {
2750
- return [];
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
- var VERDICT_HEADING = /(^|\n)\s*#{1,6}\s*Verdict\s*:/i;
2754
- function isReviewShaped(body) {
2755
- return VERDICT_HEADING.test(body);
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 getPrLatestReviewBody(prNumber, cwd) {
2758
- const reviews = getPrReviews(prNumber, cwd).filter((r) => r.body.trim().length > 0).map((r) => ({ body: r.body, at: r.submittedAt }));
2759
- const comments = getPrComments(prNumber, cwd).filter((c) => isReviewShaped(c.body)).map((c) => ({ body: c.body, at: c.createdAt }));
2760
- const all = [...reviews, ...comments].sort((a, b) => (b.at || "").localeCompare(a.at || ""));
2761
- if (all.length > 0) return all[0].body;
2762
- const pr = getPr(prNumber, cwd);
2763
- return pr.body;
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 postPrReviewComment(prNumber, body, cwd) {
2766
- try {
2767
- gh2(["pr", "comment", String(prNumber), "--body-file", "-"], { input: stripKodyMentions(body), cwd });
2768
- } catch (err) {
2769
- process.stderr.write(
2770
- `[kody] failed to post review comment on PR #${prNumber}: ${err instanceof Error ? err.message : String(err)}
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
- default_branch="${KODY_CFG_GIT_DEFAULTBRANCH:-main}"
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.21",
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",