@kody-ade/kody-engine 0.2.6 → 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.6",
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",
@@ -151,8 +151,8 @@ function getAnthropicApiKeyOrDummy() {
151
151
  }
152
152
 
153
153
  // src/executor.ts
154
- import * as fs12 from "fs";
155
- import * as path10 from "path";
154
+ import * as fs13 from "fs";
155
+ import * as path11 from "path";
156
156
 
157
157
  // src/agent.ts
158
158
  import * as fs2 from "fs";
@@ -463,9 +463,15 @@ function loadProfile(profilePath) {
463
463
  throw new ProfileError(profilePath, "profile must be a JSON object");
464
464
  }
465
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
+ }
466
470
  const profile = {
467
471
  name: requireString(profilePath, r, "name"),
468
472
  describe: typeof r.describe === "string" ? r.describe : "",
473
+ kind,
474
+ schedule: typeof r.schedule === "string" ? r.schedule : void 0,
469
475
  inputs: parseInputs(profilePath, r.inputs),
470
476
  claudeCode: parseClaudeCode(profilePath, r.claudeCode),
471
477
  cliTools: parseCliTools(profilePath, r.cliTools),
@@ -1664,12 +1670,75 @@ function tryPostPr2(prNumber, body, cwd) {
1664
1670
 
1665
1671
  // src/scripts/initFlow.ts
1666
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
1667
1677
  import * as fs9 from "fs";
1668
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
1669
1738
  function detectPackageManager(cwd) {
1670
- if (fs9.existsSync(path8.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
1671
- if (fs9.existsSync(path8.join(cwd, "yarn.lock"))) return "yarn";
1672
- 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";
1673
1742
  return "npm";
1674
1743
  }
1675
1744
  function qualityCommandsFor(pm) {
@@ -1790,26 +1859,74 @@ function performInit(cwd, force) {
1790
1859
  const pm = detectPackageManager(cwd);
1791
1860
  const ownerRepo = detectOwnerRepo(cwd);
1792
1861
  const defaultBranch = defaultBranchFromGit(cwd);
1793
- const configPath = path8.join(cwd, "kody.config.json");
1794
- if (fs9.existsSync(configPath) && !force) {
1862
+ const configPath = path9.join(cwd, "kody.config.json");
1863
+ if (fs10.existsSync(configPath) && !force) {
1795
1864
  skipped.push("kody.config.json");
1796
1865
  } else {
1797
1866
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
1798
- fs9.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1867
+ fs10.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
1799
1868
  `);
1800
1869
  wrote.push("kody.config.json");
1801
1870
  }
1802
- const workflowDir = path8.join(cwd, ".github", "workflows");
1803
- const workflowPath = path8.join(workflowDir, "kody2.yml");
1804
- 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) {
1805
1874
  skipped.push(".github/workflows/kody2.yml");
1806
1875
  } else {
1807
- fs9.mkdirSync(workflowDir, { recursive: true });
1808
- fs9.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1876
+ fs10.mkdirSync(workflowDir, { recursive: true });
1877
+ fs10.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
1809
1878
  wrote.push(".github/workflows/kody2.yml");
1810
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
+ }
1811
1896
  return { wrote, skipped };
1812
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
+ }
1813
1930
  var initFlow = async (ctx) => {
1814
1931
  const force = ctx.args.force === true;
1815
1932
  const cwd = ctx.cwd;
@@ -1963,8 +2080,8 @@ REVIEW_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.
1963
2080
 
1964
2081
  // src/scripts/releaseFlow.ts
1965
2082
  import { execFileSync as execFileSync10, spawnSync } from "child_process";
1966
- import * as fs10 from "fs";
1967
- import * as path9 from "path";
2083
+ import * as fs11 from "fs";
2084
+ import * as path10 from "path";
1968
2085
  function bumpVersion(current, bump) {
1969
2086
  const m = current.match(/^(\d+)\.(\d+)\.(\d+)(.*)$/);
1970
2087
  if (!m) throw new Error(`cannot parse version '${current}' (expected x.y.z[-suffix])`);
@@ -1980,12 +2097,12 @@ function bumpVersion(current, bump) {
1980
2097
  return `${major}.${minor}.${patch}`;
1981
2098
  }
1982
2099
  function updateVersionInFile(file, newVersion, cwd) {
1983
- const abs = path9.join(cwd, file);
1984
- if (!fs10.existsSync(abs)) return false;
1985
- const content = fs10.readFileSync(abs, "utf-8");
2100
+ const abs = path10.join(cwd, file);
2101
+ if (!fs11.existsSync(abs)) return false;
2102
+ const content = fs11.readFileSync(abs, "utf-8");
1986
2103
  const updated = content.replace(/"version"\s*:\s*"[^"]+"/, `"version": "${newVersion}"`);
1987
2104
  if (updated === content) return false;
1988
- fs10.writeFileSync(abs, updated);
2105
+ fs11.writeFileSync(abs, updated);
1989
2106
  return true;
1990
2107
  }
1991
2108
  function generateChangelog(cwd, newVersion, lastTag) {
@@ -2033,19 +2150,19 @@ function generateChangelog(cwd, newVersion, lastTag) {
2033
2150
  return parts.join("\n");
2034
2151
  }
2035
2152
  function prependChangelog(cwd, entry) {
2036
- const p = path9.join(cwd, "CHANGELOG.md");
2153
+ const p = path10.join(cwd, "CHANGELOG.md");
2037
2154
  const header = "# Changelog\n\nAll notable changes to this project will be documented in this file.\n\n";
2038
- if (fs10.existsSync(p)) {
2039
- const prior = fs10.readFileSync(p, "utf-8");
2155
+ if (fs11.existsSync(p)) {
2156
+ const prior = fs11.readFileSync(p, "utf-8");
2040
2157
  if (/^#\s*Changelog\b/m.test(prior)) {
2041
2158
  const idx = prior.indexOf("\n", prior.indexOf("# Changelog"));
2042
- fs10.writeFileSync(p, `${prior.slice(0, idx + 1)}
2159
+ fs11.writeFileSync(p, `${prior.slice(0, idx + 1)}
2043
2160
  ${entry}${prior.slice(idx + 1)}`);
2044
2161
  } else {
2045
- fs10.writeFileSync(p, `${header}${entry}${prior}`);
2162
+ fs11.writeFileSync(p, `${header}${entry}${prior}`);
2046
2163
  }
2047
2164
  } else {
2048
- fs10.writeFileSync(p, `${header}${entry}`);
2165
+ fs11.writeFileSync(p, `${header}${entry}`);
2049
2166
  }
2050
2167
  }
2051
2168
  function git3(args, cwd, timeout = 6e4) {
@@ -2096,13 +2213,13 @@ var releaseFlow = async (ctx) => {
2096
2213
  };
2097
2214
  async function runPrepare(args) {
2098
2215
  const { cwd, bump, dryRun, versionFiles, ctx } = args;
2099
- const pkgPath = path9.join(cwd, "package.json");
2100
- if (!fs10.existsSync(pkgPath)) {
2216
+ const pkgPath = path10.join(cwd, "package.json");
2217
+ if (!fs11.existsSync(pkgPath)) {
2101
2218
  ctx.output.exitCode = 99;
2102
2219
  ctx.output.reason = "release prepare: package.json not found";
2103
2220
  return;
2104
2221
  }
2105
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
2222
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2106
2223
  if (typeof pkg.version !== "string") {
2107
2224
  ctx.output.exitCode = 99;
2108
2225
  ctx.output.reason = "release prepare: package.json has no version";
@@ -2173,8 +2290,8 @@ Merge this and then run \`kody2 release --mode finalize\`.`;
2173
2290
  }
2174
2291
  async function runFinalize(args) {
2175
2292
  const { cwd, dryRun, timeoutMs, releaseCfg, ctx } = args;
2176
- const pkgPath = path9.join(cwd, "package.json");
2177
- const pkg = JSON.parse(fs10.readFileSync(pkgPath, "utf-8"));
2293
+ const pkgPath = path10.join(cwd, "package.json");
2294
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
2178
2295
  if (typeof pkg.version !== "string") {
2179
2296
  ctx.output.exitCode = 99;
2180
2297
  ctx.output.reason = "release finalize: package.json has no version";
@@ -2495,8 +2612,87 @@ var verify = async (ctx) => {
2495
2612
  }
2496
2613
  };
2497
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
+
2498
2694
  // src/scripts/writeRunSummary.ts
2499
- import * as fs11 from "fs";
2695
+ import * as fs12 from "fs";
2500
2696
  var writeRunSummary = async (ctx) => {
2501
2697
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
2502
2698
  if (!summaryPath) return;
@@ -2518,7 +2714,7 @@ var writeRunSummary = async (ctx) => {
2518
2714
  if (reason) lines.push(`- **Reason:** ${reason}`);
2519
2715
  lines.push("");
2520
2716
  try {
2521
- fs11.appendFileSync(summaryPath, `${lines.join("\n")}
2717
+ fs12.appendFileSync(summaryPath, `${lines.join("\n")}
2522
2718
  `);
2523
2719
  } catch {
2524
2720
  }
@@ -2533,6 +2729,7 @@ var preflightScripts = {
2533
2729
  reviewFlow,
2534
2730
  initFlow,
2535
2731
  releaseFlow,
2732
+ watchStalePrsFlow,
2536
2733
  loadConventions,
2537
2734
  loadCoverageRules,
2538
2735
  composePrompt
@@ -2637,7 +2834,7 @@ async function runExecutable(profileName, input) {
2637
2834
  data: {},
2638
2835
  output: { exitCode: 0 }
2639
2836
  };
2640
- const ndjsonDir = path10.join(input.cwd, ".kody2");
2837
+ const ndjsonDir = path11.join(input.cwd, ".kody2");
2641
2838
  const invokeAgent = async (prompt) => runAgent({
2642
2839
  prompt,
2643
2840
  model,
@@ -2696,17 +2893,17 @@ async function runExecutable(profileName, input) {
2696
2893
  }
2697
2894
  }
2698
2895
  function resolveProfilePath(profileName) {
2699
- const here = path10.dirname(new URL(import.meta.url).pathname);
2896
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2700
2897
  const candidates = [
2701
- path10.join(here, "executables", profileName, "profile.json"),
2898
+ path11.join(here, "executables", profileName, "profile.json"),
2702
2899
  // same-dir sibling (dev)
2703
- path10.join(here, "..", "executables", profileName, "profile.json"),
2900
+ path11.join(here, "..", "executables", profileName, "profile.json"),
2704
2901
  // up one (prod: dist/bin → dist/executables)
2705
- path10.join(here, "..", "src", "executables", profileName, "profile.json")
2902
+ path11.join(here, "..", "src", "executables", profileName, "profile.json")
2706
2903
  // fallback
2707
2904
  ];
2708
2905
  for (const c of candidates) {
2709
- if (fs12.existsSync(c)) return c;
2906
+ if (fs13.existsSync(c)) return c;
2710
2907
  }
2711
2908
  return candidates[0];
2712
2909
  }
@@ -2785,11 +2982,11 @@ function finish(out) {
2785
2982
 
2786
2983
  // src/kody2-cli.ts
2787
2984
  import { execFileSync as execFileSync13 } from "child_process";
2788
- import * as fs14 from "fs";
2789
- import * as path11 from "path";
2985
+ import * as fs15 from "fs";
2986
+ import * as path12 from "path";
2790
2987
 
2791
2988
  // src/dispatch.ts
2792
- import * as fs13 from "fs";
2989
+ import * as fs14 from "fs";
2793
2990
  function autoDispatch(explicit) {
2794
2991
  if (explicit?.mode && explicit.target) {
2795
2992
  return {
@@ -2799,10 +2996,10 @@ function autoDispatch(explicit) {
2799
2996
  }
2800
2997
  const eventName = process.env.GITHUB_EVENT_NAME;
2801
2998
  const eventPath = process.env.GITHUB_EVENT_PATH;
2802
- if (!eventName || !eventPath || !fs13.existsSync(eventPath)) return null;
2999
+ if (!eventName || !eventPath || !fs14.existsSync(eventPath)) return null;
2803
3000
  let event = {};
2804
3001
  try {
2805
- event = JSON.parse(fs13.readFileSync(eventPath, "utf-8"));
3002
+ event = JSON.parse(fs14.readFileSync(eventPath, "utf-8"));
2806
3003
  } catch {
2807
3004
  return null;
2808
3005
  }
@@ -2922,9 +3119,9 @@ function resolveAuthToken(env = process.env) {
2922
3119
  return token;
2923
3120
  }
2924
3121
  function detectPackageManager2(cwd) {
2925
- if (fs14.existsSync(path11.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
2926
- if (fs14.existsSync(path11.join(cwd, "yarn.lock"))) return "yarn";
2927
- if (fs14.existsSync(path11.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";
2928
3125
  return "npm";
2929
3126
  }
2930
3127
  function shellOut(cmd, args, cwd, stream = true) {
@@ -3001,11 +3198,11 @@ function configureGitIdentity(cwd) {
3001
3198
  }
3002
3199
  function postFailureTail(issueNumber, cwd, reason) {
3003
3200
  if (!issueNumber) return;
3004
- const logPath = path11.join(cwd, ".kody2", "last-run.jsonl");
3201
+ const logPath = path12.join(cwd, ".kody2", "last-run.jsonl");
3005
3202
  let tail = "";
3006
3203
  try {
3007
- if (fs14.existsSync(logPath)) {
3008
- const content = fs14.readFileSync(logPath, "utf-8");
3204
+ if (fs15.existsSync(logPath)) {
3205
+ const content = fs15.readFileSync(logPath, "utf-8");
3009
3206
  tail = content.slice(-3e3);
3010
3207
  }
3011
3208
  } catch {
@@ -3042,7 +3239,7 @@ async function runCi(argv) {
3042
3239
  ${CI_HELP}`);
3043
3240
  return 64;
3044
3241
  }
3045
- const cwd = args.cwd ? path11.resolve(args.cwd) : process.cwd();
3242
+ const cwd = args.cwd ? path12.resolve(args.cwd) : process.cwd();
3046
3243
  const dispatch = autoFallback ?? {
3047
3244
  mode: "run",
3048
3245
  target: args.issueNumber,
@@ -3117,67 +3314,6 @@ ${CI_HELP}`);
3117
3314
  }
3118
3315
  }
3119
3316
 
3120
- // src/registry.ts
3121
- import * as fs15 from "fs";
3122
- import * as path12 from "path";
3123
- function getExecutablesRoot() {
3124
- const here = path12.dirname(new URL(import.meta.url).pathname);
3125
- const candidates = [
3126
- path12.join(here, "executables"),
3127
- // dev: src/
3128
- path12.join(here, "..", "executables"),
3129
- // built: dist/bin → dist/executables
3130
- path12.join(here, "..", "src", "executables")
3131
- // fallback
3132
- ];
3133
- for (const c of candidates) {
3134
- if (fs15.existsSync(c) && fs15.statSync(c).isDirectory()) return c;
3135
- }
3136
- return candidates[0];
3137
- }
3138
- function listExecutables(root = getExecutablesRoot()) {
3139
- if (!fs15.existsSync(root)) return [];
3140
- const entries = fs15.readdirSync(root, { withFileTypes: true });
3141
- const out = [];
3142
- for (const ent of entries) {
3143
- if (!ent.isDirectory()) continue;
3144
- const profilePath = path12.join(root, ent.name, "profile.json");
3145
- if (fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile()) {
3146
- out.push({ name: ent.name, profilePath });
3147
- }
3148
- }
3149
- return out.sort((a, b) => a.name.localeCompare(b.name));
3150
- }
3151
- function hasExecutable(name, root = getExecutablesRoot()) {
3152
- if (!isSafeName(name)) return false;
3153
- const profilePath = path12.join(root, name, "profile.json");
3154
- return fs15.existsSync(profilePath) && fs15.statSync(profilePath).isFile();
3155
- }
3156
- function isSafeName(name) {
3157
- return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
3158
- }
3159
- function parseGenericFlags(argv) {
3160
- const args = {};
3161
- const positional = [];
3162
- for (let i = 0; i < argv.length; i++) {
3163
- const arg = argv[i];
3164
- if (!arg.startsWith("--")) {
3165
- positional.push(arg);
3166
- continue;
3167
- }
3168
- const key = arg.slice(2);
3169
- const next = argv[i + 1];
3170
- if (next !== void 0 && !next.startsWith("--")) {
3171
- args[key] = next;
3172
- i++;
3173
- } else {
3174
- args[key] = true;
3175
- }
3176
- }
3177
- if (positional.length > 0) args._ = positional;
3178
- return args;
3179
- }
3180
-
3181
3317
  // src/entry.ts
3182
3318
  var HELP_TEXT = `kody2 \u2014 single-session autonomous engineer
3183
3319
 
@@ -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.6",
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",