@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.20",
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"
@@ -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
- # Open the draft goal PR now that the branch exists. The PR is the dashboard's
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.20",
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",