@kody-ade/kody-engine 0.2.5 → 0.2.7

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/kody2.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.2.5",
6
+ version: "0.2.7",
7
7
  description: "kody2 \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -81,7 +81,7 @@ function loadConfig(projectDir = process.cwd()) {
81
81
  throw new Error(`kody.config.json is invalid JSON: ${msg}`);
82
82
  }
83
83
  const quality = raw.quality ?? {};
84
- const git3 = raw.git ?? {};
84
+ const git4 = raw.git ?? {};
85
85
  const github = raw.github ?? {};
86
86
  const agent = raw.agent ?? {};
87
87
  if (!agent.model || typeof agent.model !== "string") {
@@ -97,7 +97,7 @@ function loadConfig(projectDir = process.cwd()) {
97
97
  testUnit: typeof quality.testUnit === "string" ? quality.testUnit : ""
98
98
  },
99
99
  git: {
100
- defaultBranch: typeof git3.defaultBranch === "string" ? git3.defaultBranch : "main"
100
+ defaultBranch: typeof git4.defaultBranch === "string" ? git4.defaultBranch : "main"
101
101
  },
102
102
  github: {
103
103
  owner: String(github.owner),
@@ -107,9 +107,23 @@ function loadConfig(projectDir = process.cwd()) {
107
107
  model: String(agent.model)
108
108
  },
109
109
  issueContext: parseIssueContext(raw.issueContext),
110
- testRequirements: parseTestRequirements(raw.testRequirements)
110
+ testRequirements: parseTestRequirements(raw.testRequirements),
111
+ release: parseReleaseConfig(raw.release)
111
112
  };
112
113
  }
114
+ function parseReleaseConfig(raw) {
115
+ if (!raw || typeof raw !== "object") return void 0;
116
+ const r = raw;
117
+ const out = {};
118
+ if (Array.isArray(r.versionFiles)) out.versionFiles = r.versionFiles.filter((f) => typeof f === "string");
119
+ if (typeof r.publishCommand === "string") out.publishCommand = r.publishCommand;
120
+ if (typeof r.notifyCommand === "string") out.notifyCommand = r.notifyCommand;
121
+ if (typeof r.e2eCommand === "string") out.e2eCommand = r.e2eCommand;
122
+ if (typeof r.draftRelease === "boolean") out.draftRelease = r.draftRelease;
123
+ if (typeof r.releaseBranch === "string") out.releaseBranch = r.releaseBranch;
124
+ if (typeof r.timeoutMs === "number" && r.timeoutMs > 0) out.timeoutMs = Math.floor(r.timeoutMs);
125
+ return Object.keys(out).length > 0 ? out : void 0;
126
+ }
113
127
  function parseIssueContext(raw) {
114
128
  if (!raw || typeof raw !== "object") return void 0;
115
129
  const r = raw;
@@ -137,8 +151,8 @@ function getAnthropicApiKeyOrDummy() {
137
151
  }
138
152
 
139
153
  // src/executor.ts
140
- import * as fs11 from "fs";
141
- import * as path9 from "path";
154
+ import * as fs13 from "fs";
155
+ import * as path11 from "path";
142
156
 
143
157
  // src/agent.ts
144
158
  import * as fs2 from "fs";
@@ -449,9 +463,15 @@ function loadProfile(profilePath) {
449
463
  throw new ProfileError(profilePath, "profile must be a JSON object");
450
464
  }
451
465
  const r = raw;
466
+ const kind = r.kind === "scheduled" ? "scheduled" : "oneshot";
467
+ if (kind === "scheduled" && typeof r.schedule !== "string") {
468
+ throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
469
+ }
452
470
  const profile = {
453
471
  name: requireString(profilePath, r, "name"),
454
472
  describe: typeof r.describe === "string" ? r.describe : "",
473
+ kind,
474
+ schedule: typeof r.schedule === "string" ? r.schedule : void 0,
455
475
  inputs: parseInputs(profilePath, r.inputs),
456
476
  claudeCode: parseClaudeCode(profilePath, r.claudeCode),
457
477
  cliTools: parseCliTools(profilePath, r.cliTools),
@@ -1650,12 +1670,75 @@ function tryPostPr2(prNumber, body, cwd) {
1650
1670
 
1651
1671
  // src/scripts/initFlow.ts
1652
1672
  import { execFileSync as execFileSync9 } from "child_process";
1673
+ import * as fs10 from "fs";
1674
+ import * as path9 from "path";
1675
+
1676
+ // src/registry.ts
1653
1677
  import * as fs9 from "fs";
1654
1678
  import * as path8 from "path";
1679
+ function getExecutablesRoot() {
1680
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1681
+ const candidates = [
1682
+ path8.join(here, "executables"),
1683
+ // dev: src/
1684
+ path8.join(here, "..", "executables"),
1685
+ // built: dist/bin → dist/executables
1686
+ path8.join(here, "..", "src", "executables")
1687
+ // fallback
1688
+ ];
1689
+ for (const c of candidates) {
1690
+ if (fs9.existsSync(c) && fs9.statSync(c).isDirectory()) return c;
1691
+ }
1692
+ return candidates[0];
1693
+ }
1694
+ function listExecutables(root = getExecutablesRoot()) {
1695
+ if (!fs9.existsSync(root)) return [];
1696
+ const entries = fs9.readdirSync(root, { withFileTypes: true });
1697
+ const out = [];
1698
+ for (const ent of entries) {
1699
+ if (!ent.isDirectory()) continue;
1700
+ const profilePath = path8.join(root, ent.name, "profile.json");
1701
+ if (fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile()) {
1702
+ out.push({ name: ent.name, profilePath });
1703
+ }
1704
+ }
1705
+ return out.sort((a, b) => a.name.localeCompare(b.name));
1706
+ }
1707
+ function hasExecutable(name, root = getExecutablesRoot()) {
1708
+ if (!isSafeName(name)) return false;
1709
+ const profilePath = path8.join(root, name, "profile.json");
1710
+ return fs9.existsSync(profilePath) && fs9.statSync(profilePath).isFile();
1711
+ }
1712
+ function isSafeName(name) {
1713
+ return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
1714
+ }
1715
+ function parseGenericFlags(argv) {
1716
+ const args = {};
1717
+ const positional = [];
1718
+ for (let i = 0; i < argv.length; i++) {
1719
+ const arg = argv[i];
1720
+ if (!arg.startsWith("--")) {
1721
+ positional.push(arg);
1722
+ continue;
1723
+ }
1724
+ const key = arg.slice(2);
1725
+ const next = argv[i + 1];
1726
+ if (next !== void 0 && !next.startsWith("--")) {
1727
+ args[key] = next;
1728
+ i++;
1729
+ } else {
1730
+ args[key] = true;
1731
+ }
1732
+ }
1733
+ if (positional.length > 0) args._ = positional;
1734
+ return args;
1735
+ }
1736
+
1737
+ // src/scripts/initFlow.ts
1655
1738
  function detectPackageManager(cwd) {
1656
- if (fs9.existsSync(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1657
- if (fs9.existsSync(path8.join(cwd, "yarn.lock"))) return "yarn";
1658
- if (fs9.existsSync(path8.join(cwd, "bun.lockb"))) return "bun";
1739
+ if (fs10.existsSync(path9.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1740
+ if (fs10.existsSync(path9.join(cwd, "yarn.lock"))) return "yarn";
1741
+ if (fs10.existsSync(path9.join(cwd, "bun.lockb"))) return "bun";
1659
1742
  return "npm";
1660
1743
  }
1661
1744
  function qualityCommandsFor(pm) {
@@ -1776,26 +1859,74 @@ function performInit(cwd, force) {
1776
1859
  const pm = detectPackageManager(cwd);
1777
1860
  const ownerRepo = detectOwnerRepo(cwd);
1778
1861
  const defaultBranch = defaultBranchFromGit(cwd);
1779
- const configPath = path8.join(cwd, "kody.config.json");
1780
- if (fs9.existsSync(configPath) && !force) {
1862
+ const configPath = path9.join(cwd, "kody.config.json");
1863
+ if (fs10.existsSync(configPath) && !force) {
1781
1864
  skipped.push("kody.config.json");
1782
1865
  } else {
1783
1866
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
1784
- fs9.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1867
+ fs10.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1785
1868
  `);
1786
1869
  wrote.push("kody.config.json");
1787
1870
  }
1788
- const workflowDir = path8.join(cwd, ".github", "workflows");
1789
- const workflowPath = path8.join(workflowDir, "kody2.yml");
1790
- if (fs9.existsSync(workflowPath) && !force) {
1871
+ const workflowDir = path9.join(cwd, ".github", "workflows");
1872
+ const workflowPath = path9.join(workflowDir, "kody2.yml");
1873
+ if (fs10.existsSync(workflowPath) && !force) {
1791
1874
  skipped.push(".github/workflows/kody2.yml");
1792
1875
  } else {
1793
- fs9.mkdirSync(workflowDir, { recursive: true });
1794
- fs9.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1876
+ fs10.mkdirSync(workflowDir, { recursive: true });
1877
+ fs10.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1795
1878
  wrote.push(".github/workflows/kody2.yml");
1796
1879
  }
1880
+ for (const exe of listExecutables()) {
1881
+ let profile;
1882
+ try {
1883
+ profile = loadProfile(exe.profilePath);
1884
+ } catch {
1885
+ continue;
1886
+ }
1887
+ if (profile.kind !== "scheduled" || !profile.schedule) continue;
1888
+ const target = path9.join(workflowDir, `kody2-${exe.name}.yml`);
1889
+ if (fs10.existsSync(target) && !force) {
1890
+ skipped.push(`.github/workflows/kody2-${exe.name}.yml`);
1891
+ continue;
1892
+ }
1893
+ fs10.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
1894
+ wrote.push(`.github/workflows/kody2-${exe.name}.yml`);
1895
+ }
1797
1896
  return { wrote, skipped };
1798
1897
  }
1898
+ function renderScheduledWorkflow(name, cron) {
1899
+ return `# Scheduled kody2 executable: ${name}
1900
+ # Generated by \`kody2 init\`. Regenerate with \`kody2 init --force\`.
1901
+ # Edit the cron below or the executable's profile.json#schedule.
1902
+
1903
+ name: kody2 ${name}
1904
+
1905
+ on:
1906
+ schedule:
1907
+ - cron: "${cron}"
1908
+ workflow_dispatch:
1909
+
1910
+ jobs:
1911
+ run:
1912
+ runs-on: ubuntu-latest
1913
+ timeout-minutes: 30
1914
+ permissions:
1915
+ issues: write
1916
+ pull-requests: read
1917
+ contents: read
1918
+ steps:
1919
+ - uses: actions/checkout@v4
1920
+ with:
1921
+ token: \${{ secrets.KODY_TOKEN || github.token }}
1922
+ - uses: actions/setup-node@v4
1923
+ with:
1924
+ node-version: 22
1925
+ - env:
1926
+ GH_TOKEN: \${{ secrets.KODY_TOKEN || github.token }}
1927
+ run: npx -y -p @kody-ade/kody-engine@latest kody2 ${name}
1928
+ `;
1929
+ }
1799
1930
  var initFlow = async (ctx) => {
1800
1931
  const force = ctx.args.force === true;
1801
1932
  const cwd = ctx.cwd;
@@ -1947,8 +2078,310 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
1947
2078
  `);
1948
2079
  };
1949
2080
 
2081
+ // src/scripts/releaseFlow.ts
2082
+ import { execFileSync as execFileSync10, spawnSync } from "child_process";
2083
+ import * as fs11 from "fs";
2084
+ import * as path10 from "path";
2085
+ function bumpVersion(current, bump) {
2086
+ const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
2087
+ if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
2088
+ let [major, minor, patch] = [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)];
2089
+ if (bump === "major") {
2090
+ major++;
2091
+ minor = 0;
2092
+ patch = 0;
2093
+ } else if (bump === "minor") {
2094
+ minor++;
2095
+ patch = 0;
2096
+ } else patch++;
2097
+ return `${major}.${minor}.${patch}`;
2098
+ }
2099
+ function updateVersionInFile(file, newVersion, cwd) {
2100
+ const abs = path10.join(cwd, file);
2101
+ if (!fs11.existsSync(abs)) return false;
2102
+ const content = fs11.readFileSync(abs, "utf-8");
2103
+ const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
2104
+ if (updated === content) return false;
2105
+ fs11.writeFileSync(abs, updated);
2106
+ return true;
2107
+ }
2108
+ function generateChangelog(cwd, newVersion, lastTag) {
2109
+ const range = lastTag ? `${lastTag}..HEAD` : "HEAD";
2110
+ let log = "";
2111
+ try {
2112
+ log = execFileSync10("git", ["log", range, "--pretty=format:%s||%h", "--no-merges"], {
2113
+ cwd,
2114
+ encoding: "utf-8",
2115
+ stdio: ["ignore", "pipe", "pipe"]
2116
+ }).trim();
2117
+ } catch {
2118
+ }
2119
+ const commits = log.split("\n").filter((l) => l.length > 0).map((line) => {
2120
+ const [subject, sha] = line.split("||");
2121
+ return { subject: subject ?? "", sha: sha ?? "" };
2122
+ }).filter((c) => !/^chore:\s*release\s+v\d/i.test(c.subject));
2123
+ const groups = { feat: [], fix: [], perf: [], refactor: [], docs: [], chore: [], other: [] };
2124
+ for (const c of commits) {
2125
+ const m = c.subject.match(/^(\w+)(?:\(.*?\))?\s*:\s*(.+)$/);
2126
+ const type = m?.[1]?.toLowerCase() ?? "other";
2127
+ const msg = m?.[2] ?? c.subject;
2128
+ const bucket = groups[type] ?? groups.other;
2129
+ bucket.push(`- ${msg} (${c.sha})`);
2130
+ }
2131
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
2132
+ const parts = [`## v${newVersion} \u2014 ${date}`, ""];
2133
+ const labels = [
2134
+ ["feat", "Features"],
2135
+ ["fix", "Fixes"],
2136
+ ["perf", "Performance"],
2137
+ ["refactor", "Refactoring"],
2138
+ ["docs", "Docs"],
2139
+ ["chore", "Chores"],
2140
+ ["other", "Other"]
2141
+ ];
2142
+ for (const [key, label] of labels) {
2143
+ const items = groups[key];
2144
+ if (!items || items.length === 0) continue;
2145
+ parts.push(`### ${label}`);
2146
+ parts.push(...items);
2147
+ parts.push("");
2148
+ }
2149
+ if (parts.length === 2) parts.push("_No notable commits since the last release._", "");
2150
+ return parts.join("\n");
2151
+ }
2152
+ function prependChangelog(cwd, entry) {
2153
+ const p = path10.join(cwd, "CHANGELOG.md");
2154
+ const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2155
+ if (fs11.existsSync(p)) {
2156
+ const prior = fs11.readFileSync(p, "utf-8");
2157
+ if (/^#\s*Changelog\b/m.test(prior)) {
2158
+ const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2159
+ fs11.writeFileSync(p, `${prior.slice(0, idx + 1)}
2160
+ ${entry}${prior.slice(idx + 1)}`);
2161
+ } else {
2162
+ fs11.writeFileSync(p, `${header}${entry}${prior}`);
2163
+ }
2164
+ } else {
2165
+ fs11.writeFileSync(p, `${header}${entry}`);
2166
+ }
2167
+ }
2168
+ function git3(args, cwd, timeout = 6e4) {
2169
+ return execFileSync10("git", args, {
2170
+ encoding: "utf-8",
2171
+ timeout,
2172
+ cwd,
2173
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
2174
+ stdio: ["pipe", "pipe", "pipe"]
2175
+ }).trim();
2176
+ }
2177
+ function lastReleaseTag(cwd) {
2178
+ try {
2179
+ return git3(["describe", "--tags", "--abbrev=0", "--match", "v*"], cwd);
2180
+ } catch {
2181
+ return null;
2182
+ }
2183
+ }
2184
+ function runShell(cmd, cwd, timeoutMs) {
2185
+ const r = spawnSync(cmd, {
2186
+ cwd,
2187
+ shell: true,
2188
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
2189
+ encoding: "utf-8",
2190
+ timeout: timeoutMs
2191
+ });
2192
+ return { exitCode: r.status ?? -1, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
2193
+ }
2194
+ var releaseFlow = async (ctx) => {
2195
+ const mode = ctx.args.mode ?? "prepare";
2196
+ const bump = ctx.args.bump ?? "patch";
2197
+ const dryRun = ctx.args["dry-run"] === true || ctx.args.dryRun === true;
2198
+ const cwd = ctx.cwd;
2199
+ const releaseCfg = ctx.config.release ?? {};
2200
+ const versionFiles = releaseCfg.versionFiles && releaseCfg.versionFiles.length > 0 ? releaseCfg.versionFiles : ["package.json"];
2201
+ const timeoutMs = releaseCfg.timeoutMs ?? 6e5;
2202
+ ctx.skipAgent = true;
2203
+ if (mode === "prepare") {
2204
+ await runPrepare({ cwd, bump, dryRun, versionFiles, ctx });
2205
+ return;
2206
+ }
2207
+ if (mode === "finalize") {
2208
+ await runFinalize({ cwd, dryRun, timeoutMs, releaseCfg, ctx });
2209
+ return;
2210
+ }
2211
+ ctx.output.exitCode = 64;
2212
+ ctx.output.reason = `release: unknown mode '${mode}'`;
2213
+ };
2214
+ async function runPrepare(args) {
2215
+ const { cwd, bump, dryRun, versionFiles, ctx } = args;
2216
+ const pkgPath = path10.join(cwd, "package.json");
2217
+ if (!fs11.existsSync(pkgPath)) {
2218
+ ctx.output.exitCode = 99;
2219
+ ctx.output.reason = "release prepare: package.json not found";
2220
+ return;
2221
+ }
2222
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2223
+ if (typeof pkg.version !== "string") {
2224
+ ctx.output.exitCode = 99;
2225
+ ctx.output.reason = "release prepare: package.json has no version";
2226
+ return;
2227
+ }
2228
+ const oldVersion = pkg.version;
2229
+ const newVersion = bumpVersion(oldVersion, bump);
2230
+ const tag = `v${newVersion}`;
2231
+ process.stdout.write(`\u2192 release prepare: ${oldVersion} \u2192 ${newVersion} (${bump})
2232
+ `);
2233
+ if (dryRun) {
2234
+ ctx.output.exitCode = 0;
2235
+ ctx.output.reason = `dry-run \u2014 would bump to ${newVersion}`;
2236
+ process.stdout.write(`RELEASE_PLAN=bump=${newVersion} tag=${tag}
2237
+ `);
2238
+ return;
2239
+ }
2240
+ const touched = [];
2241
+ for (const f of versionFiles) {
2242
+ if (updateVersionInFile(f, newVersion, cwd)) touched.push(f);
2243
+ }
2244
+ if (touched.length === 0) {
2245
+ ctx.output.exitCode = 1;
2246
+ ctx.output.reason = `release prepare: no version strings updated (files: ${versionFiles.join(", ")})`;
2247
+ return;
2248
+ }
2249
+ process.stdout.write(` wrote ${touched.join(", ")}
2250
+ `);
2251
+ const entry = generateChangelog(cwd, newVersion, lastReleaseTag(cwd));
2252
+ prependChangelog(cwd, entry);
2253
+ process.stdout.write(` wrote CHANGELOG.md
2254
+ `);
2255
+ const releaseBranch = `release/${tag}`;
2256
+ try {
2257
+ git3(["checkout", "-b", releaseBranch], cwd);
2258
+ for (const f of [...touched, "CHANGELOG.md"]) git3(["add", "--", f], cwd);
2259
+ git3(["commit", "--no-gpg-sign", "-m", `chore: release ${tag}`], cwd);
2260
+ git3(["push", "-u", "origin", releaseBranch], cwd, 12e4);
2261
+ } catch (err) {
2262
+ const msg = err instanceof Error ? err.message : String(err);
2263
+ ctx.output.exitCode = 4;
2264
+ ctx.output.reason = `release prepare: git commit/push failed: ${msg}`;
2265
+ return;
2266
+ }
2267
+ const base = ctx.config.git.defaultBranch;
2268
+ const title = `chore: release ${tag}`;
2269
+ const body = `Automated release PR opened by kody2.
2270
+
2271
+ ${entry}
2272
+
2273
+ Merge this and then run \`kody2 release --mode finalize\`.`;
2274
+ let prUrl = "";
2275
+ try {
2276
+ prUrl = gh(
2277
+ ["pr", "create", "--head", releaseBranch, "--base", base, "--title", title, "--body-file", "-"],
2278
+ { input: body, cwd }
2279
+ ).trim();
2280
+ } catch (err) {
2281
+ const msg = err instanceof Error ? err.message : String(err);
2282
+ ctx.output.exitCode = 4;
2283
+ ctx.output.reason = `release prepare: gh pr create failed: ${msg}`;
2284
+ return;
2285
+ }
2286
+ ctx.output.prUrl = prUrl;
2287
+ ctx.output.exitCode = 0;
2288
+ process.stdout.write(`RELEASE_PR=${prUrl}
2289
+ `);
2290
+ }
2291
+ async function runFinalize(args) {
2292
+ const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2293
+ const pkgPath = path10.join(cwd, "package.json");
2294
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2295
+ if (typeof pkg.version !== "string") {
2296
+ ctx.output.exitCode = 99;
2297
+ ctx.output.reason = "release finalize: package.json has no version";
2298
+ return;
2299
+ }
2300
+ const version = pkg.version;
2301
+ const tag = `v${version}`;
2302
+ process.stdout.write(`\u2192 release finalize: ${tag}
2303
+ `);
2304
+ try {
2305
+ git3(["rev-parse", "--verify", tag], cwd);
2306
+ ctx.output.exitCode = 1;
2307
+ ctx.output.reason = `release finalize: tag ${tag} already exists`;
2308
+ return;
2309
+ } catch {
2310
+ }
2311
+ if (dryRun) {
2312
+ ctx.output.exitCode = 0;
2313
+ ctx.output.reason = `dry-run \u2014 would tag + publish ${tag}`;
2314
+ return;
2315
+ }
2316
+ if (releaseCfg.e2eCommand && releaseCfg.e2eCommand.trim().length > 0) {
2317
+ const cmd = releaseCfg.e2eCommand.replace(/\$VERSION/g, version);
2318
+ process.stdout.write(` E2E gate: ${cmd}
2319
+ `);
2320
+ const r = runShell(cmd, cwd, timeoutMs);
2321
+ if (r.exitCode !== 0) {
2322
+ ctx.output.exitCode = 2;
2323
+ ctx.output.reason = `release finalize: E2E gate failed (exit ${r.exitCode}): ${truncate2(r.stderr, 600)}`;
2324
+ return;
2325
+ }
2326
+ }
2327
+ try {
2328
+ git3(["tag", "-a", tag, "-m", `Release ${tag}`], cwd);
2329
+ git3(["push", "origin", tag], cwd, 12e4);
2330
+ } catch (err) {
2331
+ const msg = err instanceof Error ? err.message : String(err);
2332
+ ctx.output.exitCode = 4;
2333
+ ctx.output.reason = `release finalize: tag/push failed: ${msg}`;
2334
+ return;
2335
+ }
2336
+ let publishStatus = "skipped";
2337
+ if (releaseCfg.publishCommand && releaseCfg.publishCommand.trim().length > 0) {
2338
+ const cmd = releaseCfg.publishCommand.replace(/\$VERSION/g, version);
2339
+ process.stdout.write(` publish: ${cmd}
2340
+ `);
2341
+ const r = runShell(cmd, cwd, timeoutMs);
2342
+ publishStatus = r.exitCode === 0 ? "ok" : "failed";
2343
+ if (r.exitCode !== 0) {
2344
+ process.stderr.write(`[kody2 release] publishCommand exit ${r.exitCode}
2345
+ ${truncate2(r.stderr, 2e3)}
2346
+ `);
2347
+ }
2348
+ }
2349
+ let releaseUrl = "";
2350
+ try {
2351
+ const releaseArgs = [
2352
+ "release",
2353
+ "create",
2354
+ tag,
2355
+ "--title",
2356
+ tag,
2357
+ "--notes",
2358
+ `Release ${tag} \u2014 automated by kody2.`
2359
+ ];
2360
+ if (releaseCfg.draftRelease) releaseArgs.push("--draft");
2361
+ releaseUrl = gh(releaseArgs, { cwd }).trim();
2362
+ } catch (err) {
2363
+ process.stderr.write(`[kody2 release] gh release create failed: ${err instanceof Error ? err.message : String(err)}
2364
+ `);
2365
+ }
2366
+ if (releaseCfg.notifyCommand && releaseCfg.notifyCommand.trim().length > 0) {
2367
+ const cmd = releaseCfg.notifyCommand.replace(/\$VERSION/g, version);
2368
+ runShell(cmd, cwd, timeoutMs);
2369
+ }
2370
+ if (releaseUrl) ctx.output.prUrl = releaseUrl;
2371
+ if (publishStatus === "failed") {
2372
+ ctx.output.exitCode = 1;
2373
+ ctx.output.reason = `release finalize: tag + gh release created, but publishCommand failed`;
2374
+ return;
2375
+ }
2376
+ ctx.output.exitCode = 0;
2377
+ process.stdout.write(`RELEASE_TAG=${tag}
2378
+ `);
2379
+ if (releaseUrl) process.stdout.write(`RELEASE_URL=${releaseUrl}
2380
+ `);
2381
+ }
2382
+
1950
2383
  // src/scripts/resolveFlow.ts
1951
- import { execFileSync as execFileSync10 } from "child_process";
2384
+ import { execFileSync as execFileSync11 } from "child_process";
1952
2385
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
1953
2386
  var resolveFlow = async (ctx) => {
1954
2387
  const prNumber = ctx.args.pr;
@@ -2000,7 +2433,7 @@ var resolveFlow = async (ctx) => {
2000
2433
  };
2001
2434
  function getConflictedFiles(cwd) {
2002
2435
  try {
2003
- const out = execFileSync10("git", ["diff", "--name-only", "--diff-filter=U"], {
2436
+ const out = execFileSync11("git", ["diff", "--name-only", "--diff-filter=U"], {
2004
2437
  encoding: "utf-8",
2005
2438
  cwd,
2006
2439
  env: { ...process.env, HUSKY: "0" }
@@ -2015,7 +2448,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
2015
2448
  let total = 0;
2016
2449
  for (const f of files) {
2017
2450
  try {
2018
- const content = execFileSync10("cat", [f], { encoding: "utf-8", cwd }).toString();
2451
+ const content = execFileSync11("cat", [f], { encoding: "utf-8", cwd }).toString();
2019
2452
  const snippet = `### ${f}
2020
2453
 
2021
2454
  \`\`\`
@@ -2179,8 +2612,87 @@ var verify = async (ctx) => {
2179
2612
  }
2180
2613
  };
2181
2614
 
2615
+ // src/scripts/watchStalePrsFlow.ts
2616
+ function readWatchConfig(ctx) {
2617
+ const cfg = ctx.config.watch;
2618
+ if (!cfg || typeof cfg !== "object") return {};
2619
+ const r = cfg;
2620
+ return {
2621
+ staleDays: typeof r.staleDays === "number" && r.staleDays > 0 ? Math.floor(r.staleDays) : void 0,
2622
+ reportIssueNumber: typeof r.reportIssueNumber === "number" && r.reportIssueNumber > 0 ? Math.floor(r.reportIssueNumber) : void 0
2623
+ };
2624
+ }
2625
+ function findStalePrs(cwd, staleDays, now = /* @__PURE__ */ new Date()) {
2626
+ let raw = "";
2627
+ try {
2628
+ raw = gh(
2629
+ [
2630
+ "pr",
2631
+ "list",
2632
+ "--state",
2633
+ "open",
2634
+ "--limit",
2635
+ "100",
2636
+ "--json",
2637
+ "number,title,url,updatedAt"
2638
+ ],
2639
+ { cwd }
2640
+ );
2641
+ } catch {
2642
+ return [];
2643
+ }
2644
+ let list;
2645
+ try {
2646
+ list = JSON.parse(raw);
2647
+ } catch {
2648
+ return [];
2649
+ }
2650
+ if (!Array.isArray(list)) return [];
2651
+ const cutoffMs = now.getTime() - staleDays * 24 * 60 * 60 * 1e3;
2652
+ const stale = [];
2653
+ for (const pr of list) {
2654
+ const ts = Date.parse(pr.updatedAt);
2655
+ if (!Number.isFinite(ts) || ts > cutoffMs) continue;
2656
+ const daysStale = Math.floor((now.getTime() - ts) / (24 * 60 * 60 * 1e3));
2657
+ stale.push({ number: pr.number, title: pr.title, url: pr.url, updatedAt: pr.updatedAt, daysStale });
2658
+ }
2659
+ return stale.sort((a, b) => b.daysStale - a.daysStale);
2660
+ }
2661
+ function formatStaleReport(stale, staleDays) {
2662
+ if (stale.length === 0) {
2663
+ return `\u{1F7E2} **kody2 watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
2664
+ }
2665
+ const lines = [
2666
+ `\u{1F7E1} **kody2 watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
2667
+ ""
2668
+ ];
2669
+ for (const pr of stale.slice(0, 50)) {
2670
+ lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
2671
+ }
2672
+ if (stale.length > 50) lines.push(`- \u2026 and ${stale.length - 50} more`);
2673
+ return lines.join("\n");
2674
+ }
2675
+ var watchStalePrsFlow = async (ctx) => {
2676
+ ctx.skipAgent = true;
2677
+ const { staleDays = 7, reportIssueNumber } = readWatchConfig(ctx);
2678
+ const stale = findStalePrs(ctx.cwd, staleDays);
2679
+ const report = formatStaleReport(stale, staleDays);
2680
+ process.stdout.write(`${report}
2681
+ `);
2682
+ if (reportIssueNumber) {
2683
+ try {
2684
+ postIssueComment(reportIssueNumber, report, ctx.cwd);
2685
+ } catch (err) {
2686
+ process.stderr.write(`[kody2 watch] failed to post to issue #${reportIssueNumber}: ${err instanceof Error ? err.message : String(err)}
2687
+ `);
2688
+ }
2689
+ }
2690
+ ctx.output.exitCode = 0;
2691
+ ctx.data.staleCount = stale.length;
2692
+ };
2693
+
2182
2694
  // src/scripts/writeRunSummary.ts
2183
- import * as fs10 from "fs";
2695
+ import * as fs12 from "fs";
2184
2696
  var writeRunSummary = async (ctx) => {
2185
2697
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
2186
2698
  if (!summaryPath) return;
@@ -2202,7 +2714,7 @@ var writeRunSummary = async (ctx) => {
2202
2714
  if (reason) lines.push(`- **Reason:** ${reason}`);
2203
2715
  lines.push("");
2204
2716
  try {
2205
- fs10.appendFileSync(summaryPath, `${lines.join("\n")}
2717
+ fs12.appendFileSync(summaryPath, `${lines.join("\n")}
2206
2718
  `);
2207
2719
  } catch {
2208
2720
  }
@@ -2216,6 +2728,8 @@ var preflightScripts = {
2216
2728
  resolveFlow,
2217
2729
  reviewFlow,
2218
2730
  initFlow,
2731
+ releaseFlow,
2732
+ watchStalePrsFlow,
2219
2733
  loadConventions,
2220
2734
  loadCoverageRules,
2221
2735
  composePrompt
@@ -2236,7 +2750,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
2236
2750
  ]);
2237
2751
 
2238
2752
  // src/tools.ts
2239
- import { execFileSync as execFileSync11 } from "child_process";
2753
+ import { execFileSync as execFileSync12 } from "child_process";
2240
2754
  function verifyCliTools(tools, cwd) {
2241
2755
  const out = [];
2242
2756
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -2252,24 +2766,24 @@ function firstRequiredFailure(results, tools) {
2252
2766
  }
2253
2767
  function verifyOne(tool, cwd) {
2254
2768
  const result = { name: tool.name, present: false, verified: false };
2255
- let present = runShell(tool.install.checkCommand, cwd);
2769
+ let present = runShell2(tool.install.checkCommand, cwd);
2256
2770
  if (!present && tool.install.installCommand) {
2257
- runShell(tool.install.installCommand, cwd, 12e4);
2258
- present = runShell(tool.install.checkCommand, cwd);
2771
+ runShell2(tool.install.installCommand, cwd, 12e4);
2772
+ present = runShell2(tool.install.checkCommand, cwd);
2259
2773
  }
2260
2774
  result.present = present;
2261
2775
  if (!present) {
2262
2776
  result.error = `tool "${tool.name}" not on PATH (check: ${tool.install.checkCommand})`;
2263
2777
  return result;
2264
2778
  }
2265
- const verified = runShell(tool.verify, cwd);
2779
+ const verified = runShell2(tool.verify, cwd);
2266
2780
  result.verified = verified;
2267
2781
  if (!verified) result.error = `tool "${tool.name}" failed verify: ${tool.verify}`;
2268
2782
  return result;
2269
2783
  }
2270
- function runShell(cmd, cwd, timeoutMs = 3e4) {
2784
+ function runShell2(cmd, cwd, timeoutMs = 3e4) {
2271
2785
  try {
2272
- execFileSync11("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2786
+ execFileSync12("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
2273
2787
  return true;
2274
2788
  } catch {
2275
2789
  return false;
@@ -2320,7 +2834,7 @@ async function runExecutable(profileName, input) {
2320
2834
  data: {},
2321
2835
  output: { exitCode: 0 }
2322
2836
  };
2323
- const ndjsonDir = path9.join(input.cwd, ".kody2");
2837
+ const ndjsonDir = path11.join(input.cwd, ".kody2");
2324
2838
  const invokeAgent = async (prompt) => runAgent({
2325
2839
  prompt,
2326
2840
  model,
@@ -2379,17 +2893,17 @@ async function runExecutable(profileName, input) {
2379
2893
  }
2380
2894
  }
2381
2895
  function resolveProfilePath(profileName) {
2382
- const here = path9.dirname(new URL(import.meta.url).pathname);
2896
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2383
2897
  const candidates = [
2384
- path9.join(here, "executables", profileName, "profile.json"),
2898
+ path11.join(here, "executables", profileName, "profile.json"),
2385
2899
  // same-dir sibling (dev)
2386
- path9.join(here, "..", "executables", profileName, "profile.json"),
2900
+ path11.join(here, "..", "executables", profileName, "profile.json"),
2387
2901
  // up one (prod: dist/bin → dist/executables)
2388
- path9.join(here, "..", "src", "executables", profileName, "profile.json")
2902
+ path11.join(here, "..", "src", "executables", profileName, "profile.json")
2389
2903
  // fallback
2390
2904
  ];
2391
2905
  for (const c of candidates) {
2392
- if (fs11.existsSync(c)) return c;
2906
+ if (fs13.existsSync(c)) return c;
2393
2907
  }
2394
2908
  return candidates[0];
2395
2909
  }
@@ -2467,12 +2981,12 @@ function finish(out) {
2467
2981
  }
2468
2982
 
2469
2983
  // src/kody2-cli.ts
2470
- import { execFileSync as execFileSync12 } from "child_process";
2471
- import * as fs13 from "fs";
2472
- import * as path10 from "path";
2984
+ import { execFileSync as execFileSync13 } from "child_process";
2985
+ import * as fs15 from "fs";
2986
+ import * as path12 from "path";
2473
2987
 
2474
2988
  // src/dispatch.ts
2475
- import * as fs12 from "fs";
2989
+ import * as fs14 from "fs";
2476
2990
  function autoDispatch(explicit) {
2477
2991
  if (explicit?.mode && explicit.target) {
2478
2992
  return {
@@ -2482,10 +2996,10 @@ function autoDispatch(explicit) {
2482
2996
  }
2483
2997
  const eventName = process.env.GITHUB_EVENT_NAME;
2484
2998
  const eventPath = process.env.GITHUB_EVENT_PATH;
2485
- if (!eventName || !eventPath || !fs12.existsSync(eventPath)) return null;
2999
+ if (!eventName || !eventPath || !fs14.existsSync(eventPath)) return null;
2486
3000
  let event = {};
2487
3001
  try {
2488
- event = JSON.parse(fs12.readFileSync(eventPath, "utf-8"));
3002
+ event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
2489
3003
  } catch {
2490
3004
  return null;
2491
3005
  }
@@ -2605,14 +3119,14 @@ function resolveAuthToken(env = process.env) {
2605
3119
  return token;
2606
3120
  }
2607
3121
  function detectPackageManager2(cwd) {
2608
- if (fs13.existsSync(path10.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2609
- if (fs13.existsSync(path10.join(cwd, "yarn.lock"))) return "yarn";
2610
- if (fs13.existsSync(path10.join(cwd, "bun.lockb"))) return "bun";
3122
+ if (fs15.existsSync(path12.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3123
+ if (fs15.existsSync(path12.join(cwd, "yarn.lock"))) return "yarn";
3124
+ if (fs15.existsSync(path12.join(cwd, "bun.lockb"))) return "bun";
2611
3125
  return "npm";
2612
3126
  }
2613
3127
  function shellOut(cmd, args, cwd, stream = true) {
2614
3128
  try {
2615
- execFileSync12(cmd, args, {
3129
+ execFileSync13(cmd, args, {
2616
3130
  cwd,
2617
3131
  stdio: stream ? "inherit" : "pipe",
2618
3132
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -2625,7 +3139,7 @@ function shellOut(cmd, args, cwd, stream = true) {
2625
3139
  }
2626
3140
  function isOnPath(bin) {
2627
3141
  try {
2628
- execFileSync12("which", [bin], { stdio: "pipe" });
3142
+ execFileSync13("which", [bin], { stdio: "pipe" });
2629
3143
  return true;
2630
3144
  } catch {
2631
3145
  return false;
@@ -2659,7 +3173,7 @@ function installLitellmIfNeeded(cwd) {
2659
3173
  } catch {
2660
3174
  }
2661
3175
  try {
2662
- execFileSync12("python3", ["-c", "import litellm"], { stdio: "pipe" });
3176
+ execFileSync13("python3", ["-c", "import litellm"], { stdio: "pipe" });
2663
3177
  process.stdout.write("\u2192 kody2: litellm already installed\n");
2664
3178
  return 0;
2665
3179
  } catch {
@@ -2669,26 +3183,26 @@ function installLitellmIfNeeded(cwd) {
2669
3183
  }
2670
3184
  function configureGitIdentity(cwd) {
2671
3185
  try {
2672
- const name = execFileSync12("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
3186
+ const name = execFileSync13("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
2673
3187
  if (name) return;
2674
3188
  } catch {
2675
3189
  }
2676
3190
  try {
2677
- execFileSync12("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
3191
+ execFileSync13("git", ["config", "user.name", "kody2-bot"], { cwd, stdio: "pipe" });
2678
3192
  } catch {
2679
3193
  }
2680
3194
  try {
2681
- execFileSync12("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
3195
+ execFileSync13("git", ["config", "user.email", "kody2-bot@users.noreply.github.com"], { cwd, stdio: "pipe" });
2682
3196
  } catch {
2683
3197
  }
2684
3198
  }
2685
3199
  function postFailureTail(issueNumber, cwd, reason) {
2686
3200
  if (!issueNumber) return;
2687
- const logPath = path10.join(cwd, ".kody2", "last-run.jsonl");
3201
+ const logPath = path12.join(cwd, ".kody2", "last-run.jsonl");
2688
3202
  let tail = "";
2689
3203
  try {
2690
- if (fs13.existsSync(logPath)) {
2691
- const content = fs13.readFileSync(logPath, "utf-8");
3204
+ if (fs15.existsSync(logPath)) {
3205
+ const content = fs15.readFileSync(logPath, "utf-8");
2692
3206
  tail = content.slice(-3e3);
2693
3207
  }
2694
3208
  } catch {
@@ -2725,7 +3239,7 @@ async function runCi(argv) {
2725
3239
  ${CI_HELP}`);
2726
3240
  return 64;
2727
3241
  }
2728
- const cwd = args.cwd ? path10.resolve(args.cwd) : process.cwd();
3242
+ const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
2729
3243
  const dispatch = autoFallback ?? {
2730
3244
  mode: "run",
2731
3245
  target: args.issueNumber,
@@ -2800,67 +3314,6 @@ ${CI_HELP}`);
2800
3314
  }
2801
3315
  }
2802
3316
 
2803
- // src/registry.ts
2804
- import * as fs14 from "fs";
2805
- import * as path11 from "path";
2806
- function getExecutablesRoot() {
2807
- const here = path11.dirname(new URL(import.meta.url).pathname);
2808
- const candidates = [
2809
- path11.join(here, "executables"),
2810
- // dev: src/
2811
- path11.join(here, "..", "executables"),
2812
- // built: dist/bin → dist/executables
2813
- path11.join(here, "..", "src", "executables")
2814
- // fallback
2815
- ];
2816
- for (const c of candidates) {
2817
- if (fs14.existsSync(c) && fs14.statSync(c).isDirectory()) return c;
2818
- }
2819
- return candidates[0];
2820
- }
2821
- function listExecutables(root = getExecutablesRoot()) {
2822
- if (!fs14.existsSync(root)) return [];
2823
- const entries = fs14.readdirSync(root, { withFileTypes: true });
2824
- const out = [];
2825
- for (const ent of entries) {
2826
- if (!ent.isDirectory()) continue;
2827
- const profilePath = path11.join(root, ent.name, "profile.json");
2828
- if (fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile()) {
2829
- out.push({ name: ent.name, profilePath });
2830
- }
2831
- }
2832
- return out.sort((a, b) => a.name.localeCompare(b.name));
2833
- }
2834
- function hasExecutable(name, root = getExecutablesRoot()) {
2835
- if (!isSafeName(name)) return false;
2836
- const profilePath = path11.join(root, name, "profile.json");
2837
- return fs14.existsSync(profilePath) && fs14.statSync(profilePath).isFile();
2838
- }
2839
- function isSafeName(name) {
2840
- return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2841
- }
2842
- function parseGenericFlags(argv) {
2843
- const args = {};
2844
- const positional = [];
2845
- for (let i = 0; i < argv.length; i++) {
2846
- const arg = argv[i];
2847
- if (!arg.startsWith("--")) {
2848
- positional.push(arg);
2849
- continue;
2850
- }
2851
- const key = arg.slice(2);
2852
- const next = argv[i + 1];
2853
- if (next !== void 0 && !next.startsWith("--")) {
2854
- args[key] = next;
2855
- i++;
2856
- } else {
2857
- args[key] = true;
2858
- }
2859
- }
2860
- if (positional.length > 0) args._ = positional;
2861
- return args;
2862
- }
2863
-
2864
3317
  // src/entry.ts
2865
3318
  var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
2866
3319
 
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "release",
3
+ "describe": "Version bump + changelog + release PR (prepare), or tag + publish + GH release (finalize). No agent.",
4
+
5
+ "inputs": [
6
+ {
7
+ "name": "mode",
8
+ "flag": "--mode",
9
+ "type": "enum",
10
+ "values": ["prepare", "finalize"],
11
+ "required": false,
12
+ "describe": "`prepare` (default): bump + changelog + release PR. `finalize`: E2E gate + tag + publish + GH release."
13
+ },
14
+ {
15
+ "name": "bump",
16
+ "flag": "--bump",
17
+ "type": "enum",
18
+ "values": ["patch", "minor", "major"],
19
+ "required": false,
20
+ "describe": "Version bump when mode=prepare (ignored in finalize). Default patch."
21
+ },
22
+ {
23
+ "name": "dry-run",
24
+ "flag": "--dry-run",
25
+ "type": "bool",
26
+ "required": false,
27
+ "describe": "Print plan without writing files, creating PRs, tagging, or publishing."
28
+ }
29
+ ],
30
+
31
+ "claudeCode": {
32
+ "model": "inherit",
33
+ "permissionMode": "acceptEdits",
34
+ "maxTurns": null,
35
+ "systemPromptAppend": null,
36
+ "tools": [],
37
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
38
+ "skills": [],
39
+ "commands": [],
40
+ "subagents": [],
41
+ "plugins": [],
42
+ "mcpServers": []
43
+ },
44
+
45
+ "cliTools": [],
46
+
47
+ "scripts": {
48
+ "preflight": [
49
+ { "script": "releaseFlow" }
50
+ ],
51
+ "postflight": []
52
+ }
53
+ }
@@ -17,6 +17,14 @@ import type { Kody2Config } from "../config.js"
17
17
  export interface Profile {
18
18
  name: string
19
19
  describe: string
20
+ /**
21
+ * Execution model. `oneshot` (default): single invocation on demand.
22
+ * `scheduled`: fires periodically via an external cron (typically GHA
23
+ * `schedule:`). Scheduled profiles must declare a `schedule` cron string.
24
+ */
25
+ kind: "oneshot" | "scheduled"
26
+ /** Cron expression for scheduled profiles (e.g. "0 8 * * MON"). */
27
+ schedule?: string
20
28
  inputs: InputSpec[]
21
29
  claudeCode: ClaudeCodeSpec
22
30
  cliTools: CliToolSpec[]
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "watch-stale-prs",
3
+ "describe": "Scheduled: list open PRs untouched for N days and report. No agent invocation.",
4
+
5
+ "kind": "scheduled",
6
+ "schedule": "0 8 * * MON",
7
+
8
+ "inputs": [],
9
+
10
+ "claudeCode": {
11
+ "model": "inherit",
12
+ "permissionMode": "default",
13
+ "maxTurns": null,
14
+ "systemPromptAppend": null,
15
+ "tools": [],
16
+ "hooks": { "PreToolUse": [], "PostToolUse": [], "Stop": [] },
17
+ "skills": [],
18
+ "commands": [],
19
+ "subagents": [],
20
+ "plugins": [],
21
+ "mcpServers": []
22
+ },
23
+
24
+ "cliTools": [],
25
+
26
+ "scripts": {
27
+ "preflight": [
28
+ { "script": "watchStalePrsFlow" }
29
+ ],
30
+ "postflight": []
31
+ }
32
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "kody2 — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",