@kody-ade/kody-engine 0.3.39 → 0.3.41

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.3.39",
6
+ version: "0.3.41",
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",
@@ -612,6 +612,68 @@ import * as path20 from "path";
612
612
  // src/dispatch.ts
613
613
  import * as fs6 from "fs";
614
614
 
615
+ // src/cron-match.ts
616
+ var FIELD_BOUNDS = [
617
+ [0, 59],
618
+ // minute
619
+ [0, 23],
620
+ // hour
621
+ [1, 31],
622
+ // day-of-month
623
+ [1, 12],
624
+ // month
625
+ [0, 6]
626
+ // day-of-week
627
+ ];
628
+ function parseCron(spec) {
629
+ const fields = spec.trim().split(/\s+/);
630
+ if (fields.length !== 5) {
631
+ throw new Error(`Invalid cron expression: "${spec}" \u2014 expected 5 space-separated fields`);
632
+ }
633
+ const sets = fields.map((f, i) => parseField(f, FIELD_BOUNDS[i][0], FIELD_BOUNDS[i][1]));
634
+ return { minute: sets[0], hour: sets[1], dom: sets[2], month: sets[3], dow: sets[4] };
635
+ }
636
+ function parseField(field, min, max) {
637
+ const out = /* @__PURE__ */ new Set();
638
+ for (const part of field.split(",")) {
639
+ const [base, stepStr] = part.split("/");
640
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
641
+ if (!Number.isFinite(step) || step < 1) {
642
+ throw new Error(`Invalid step in cron field "${field}"`);
643
+ }
644
+ let lo;
645
+ let hi;
646
+ if (base === "*") {
647
+ lo = min;
648
+ hi = max;
649
+ } else if (base.includes("-")) {
650
+ const [aStr, bStr] = base.split("-");
651
+ lo = parseInt(aStr, 10);
652
+ hi = parseInt(bStr, 10);
653
+ } else {
654
+ lo = parseInt(base, 10);
655
+ hi = lo;
656
+ }
657
+ if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo < min || hi > max || lo > hi) {
658
+ throw new Error(`Invalid cron field "${field}" \u2014 out of range [${min},${max}] or reversed`);
659
+ }
660
+ for (let i = lo; i <= hi; i += step) out.add(i);
661
+ }
662
+ return out;
663
+ }
664
+ function cronMatchesAt(expr, date) {
665
+ return expr.minute.has(date.getUTCMinutes()) && expr.hour.has(date.getUTCHours()) && expr.dom.has(date.getUTCDate()) && expr.month.has(date.getUTCMonth() + 1) && expr.dow.has(date.getUTCDay());
666
+ }
667
+ function cronMatchesInWindow(spec, end, windowSec) {
668
+ const expr = parseCron(spec);
669
+ const endMs = Math.floor(end.getTime() / 6e4) * 6e4;
670
+ const minuteSteps = Math.max(1, Math.ceil(windowSec / 60));
671
+ for (let i = 0; i < minuteSteps; i++) {
672
+ if (cronMatchesAt(expr, new Date(endMs - i * 6e4))) return true;
673
+ }
674
+ return false;
675
+ }
676
+
615
677
  // src/registry.ts
616
678
  import * as fs5 from "fs";
617
679
  import * as path5 from "path";
@@ -725,11 +787,9 @@ function autoDispatch(opts) {
725
787
  if (!Number.isNaN(n) && n > 0) {
726
788
  return { executable: "run", cliArgs: { issue: n }, target: n };
727
789
  }
728
- return { executable: "mission-scheduler", cliArgs: {}, target: 0 };
729
- }
730
- if (eventName === "schedule") {
731
- return { executable: "mission-scheduler", cliArgs: {}, target: 0 };
790
+ return null;
732
791
  }
792
+ if (eventName === "schedule") return null;
733
793
  if (eventName === "pull_request") return null;
734
794
  if (eventName !== "issue_comment") return null;
735
795
  const rawBody = String(event.comment?.body ?? "");
@@ -771,6 +831,39 @@ function autoDispatch(opts) {
771
831
  }
772
832
  return { executable, cliArgs: args, target: targetNum };
773
833
  }
834
+ function dispatchScheduledWatches(opts) {
835
+ const now = opts?.now ?? /* @__PURE__ */ new Date();
836
+ const envWindow = Number(process.env.KODY_SCHEDULE_WINDOW_SEC);
837
+ const windowSec = opts?.windowSec ?? (Number.isFinite(envWindow) && envWindow > 0 ? envWindow : 300);
838
+ const out = [];
839
+ for (const exe of listExecutables()) {
840
+ let raw;
841
+ try {
842
+ raw = fs6.readFileSync(exe.profilePath, "utf-8");
843
+ } catch {
844
+ continue;
845
+ }
846
+ let profile;
847
+ try {
848
+ profile = JSON.parse(raw);
849
+ } catch {
850
+ continue;
851
+ }
852
+ if (profile.role !== "watch") continue;
853
+ if (profile.kind !== "scheduled") continue;
854
+ const schedule = profile.schedule;
855
+ if (typeof schedule !== "string" || schedule.trim().length === 0) continue;
856
+ if (!opts?.force) {
857
+ try {
858
+ if (!cronMatchesInWindow(schedule, now, windowSec)) continue;
859
+ } catch {
860
+ continue;
861
+ }
862
+ }
863
+ out.push({ executable: exe.name, cliArgs: {}, target: 0 });
864
+ }
865
+ return out;
866
+ }
774
867
  function extractAfterTag(body) {
775
868
  const idx = body.indexOf("@kody");
776
869
  if (idx === -1) return body;
@@ -1674,31 +1767,34 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1674
1767
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
1675
1768
  const root = path9.join(os2.tmpdir(), `kody-synth-${runId}`);
1676
1769
  fs10.mkdirSync(path9.join(root, ".claude-plugin"), { recursive: true });
1770
+ const resolvePart = (bucket, entry) => {
1771
+ const local = path9.join(profile.dir, bucket, entry);
1772
+ if (fs10.existsSync(local)) return local;
1773
+ const central = path9.join(catalog, bucket, entry);
1774
+ if (fs10.existsSync(central)) return central;
1775
+ throw new Error(
1776
+ `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
1777
+ );
1778
+ };
1677
1779
  if (cc.skills.length > 0) {
1678
1780
  const dst = path9.join(root, "skills");
1679
1781
  fs10.mkdirSync(dst, { recursive: true });
1680
1782
  for (const name of cc.skills) {
1681
- const src = path9.join(catalog, "skills", name);
1682
- if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: skill not found in catalog: ${name}`);
1683
- copyDir(src, path9.join(dst, name));
1783
+ copyDir(resolvePart("skills", name), path9.join(dst, name));
1684
1784
  }
1685
1785
  }
1686
1786
  if (cc.commands.length > 0) {
1687
1787
  const dst = path9.join(root, "commands");
1688
1788
  fs10.mkdirSync(dst, { recursive: true });
1689
1789
  for (const name of cc.commands) {
1690
- const src = path9.join(catalog, "commands", `${name}.md`);
1691
- if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: command not found in catalog: ${name}`);
1692
- fs10.copyFileSync(src, path9.join(dst, `${name}.md`));
1790
+ fs10.copyFileSync(resolvePart("commands", `${name}.md`), path9.join(dst, `${name}.md`));
1693
1791
  }
1694
1792
  }
1695
1793
  if (cc.subagents.length > 0) {
1696
1794
  const dst = path9.join(root, "agents");
1697
1795
  fs10.mkdirSync(dst, { recursive: true });
1698
1796
  for (const name of cc.subagents) {
1699
- const src = path9.join(catalog, "agents", `${name}.md`);
1700
- if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: subagent not found in catalog: ${name}`);
1701
- fs10.copyFileSync(src, path9.join(dst, `${name}.md`));
1797
+ fs10.copyFileSync(resolvePart("agents", `${name}.md`), path9.join(dst, `${name}.md`));
1702
1798
  }
1703
1799
  }
1704
1800
  if (cc.hooks.length > 0) {
@@ -1706,8 +1802,7 @@ var buildSyntheticPlugin = async (ctx, profile) => {
1706
1802
  fs10.mkdirSync(dst, { recursive: true });
1707
1803
  const merged = { hooks: {} };
1708
1804
  for (const name of cc.hooks) {
1709
- const src = path9.join(catalog, "hooks", `${name}.json`);
1710
- if (!fs10.existsSync(src)) throw new Error(`buildSyntheticPlugin: hook not found in catalog: ${name}`);
1805
+ const src = resolvePart("hooks", `${name}.json`);
1711
1806
  const parsed = JSON.parse(fs10.readFileSync(src, "utf-8"));
1712
1807
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
1713
1808
  if (!Array.isArray(entries)) continue;
@@ -5959,6 +6054,8 @@ async function runExecutable(profileName, input) {
5959
6054
  }
5960
6055
  }
5961
6056
  function resolveProfilePath(profileName) {
6057
+ const found = resolveExecutable(profileName);
6058
+ if (found) return found;
5962
6059
  const here = path19.dirname(new URL(import.meta.url).pathname);
5963
6060
  const candidates = [
5964
6061
  path19.join(here, "executables", profileName, "profile.json"),
@@ -6331,6 +6428,22 @@ async function runCi(argv) {
6331
6428
  } catch {
6332
6429
  }
6333
6430
  const autoFallback = !args.issueNumber ? autoDispatch({ config: earlyConfig }) : null;
6431
+ const eventName = process.env.GITHUB_EVENT_NAME;
6432
+ const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
6433
+ let manualWorkflowDispatch = false;
6434
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs23.existsSync(dispatchEventPath)) {
6435
+ try {
6436
+ const evt = JSON.parse(fs23.readFileSync(dispatchEventPath, "utf-8"));
6437
+ const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
6438
+ const sessionInput = String(evt?.inputs?.sessionId ?? "");
6439
+ manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
6440
+ } catch {
6441
+ manualWorkflowDispatch = false;
6442
+ }
6443
+ }
6444
+ if (!args.issueNumber && !autoFallback && (eventName === "schedule" || manualWorkflowDispatch)) {
6445
+ return runScheduledFanOut(cwd, args, { force: manualWorkflowDispatch });
6446
+ }
6334
6447
  if (!args.issueNumber && !autoFallback && process.env.GITHUB_EVENT_NAME) {
6335
6448
  process.stdout.write(`\u2192 kody: no action for event ${process.env.GITHUB_EVENT_NAME} \u2014 exiting cleanly
6336
6449
  `);
@@ -6416,6 +6529,79 @@ ${CI_HELP}`);
6416
6529
  return 99;
6417
6530
  }
6418
6531
  }
6532
+ async function runScheduledFanOut(cwd, args, opts) {
6533
+ const matches = dispatchScheduledWatches({ force: opts.force });
6534
+ if (matches.length === 0) {
6535
+ process.stdout.write(
6536
+ `\u2192 kody: scheduled wake \u2014 no watches matched ${opts.force ? "(force mode, no watches discovered)" : "(window)"}, exiting cleanly
6537
+ `
6538
+ );
6539
+ return 0;
6540
+ }
6541
+ const names = matches.map((m) => m.executable).join(", ");
6542
+ process.stdout.write(`\u2192 kody: scheduled wake \u2014 firing ${matches.length} watch(es): ${names}
6543
+ `);
6544
+ try {
6545
+ const n = unpackAllSecrets();
6546
+ if (n > 0) process.stdout.write(`\u2192 kody: unpacked ${n} secret(s) from ALL_SECRETS
6547
+ `);
6548
+ resolveAuthToken();
6549
+ const pm = args.packageManager ?? detectPackageManager2(cwd);
6550
+ process.stdout.write(`\u2192 kody: package manager = ${pm}
6551
+ `);
6552
+ if (!args.skipInstall) {
6553
+ const code = installDeps(pm, cwd);
6554
+ if (code !== 0) {
6555
+ process.stderr.write(`[kody] dep install failed (${pm}, exit ${code})
6556
+ `);
6557
+ return 99;
6558
+ }
6559
+ }
6560
+ if (!args.skipLitellm) {
6561
+ const code = installLitellmIfNeeded(cwd);
6562
+ if (code !== 0) {
6563
+ process.stderr.write(`[kody] litellm install failed (exit ${code})
6564
+ `);
6565
+ return 99;
6566
+ }
6567
+ }
6568
+ configureGitIdentity(cwd);
6569
+ } catch (err) {
6570
+ const msg = err instanceof Error ? err.message : String(err);
6571
+ process.stderr.write(`[kody] preflight crashed: ${msg}
6572
+ `);
6573
+ return 99;
6574
+ }
6575
+ const config = loadConfig(cwd);
6576
+ let worstExit = 0;
6577
+ for (const match of matches) {
6578
+ process.stdout.write(`
6579
+ \u2192 kody: running watch \`${match.executable}\`
6580
+ `);
6581
+ try {
6582
+ const result = await runExecutable(match.executable, {
6583
+ cliArgs: match.cliArgs,
6584
+ cwd,
6585
+ config,
6586
+ verbose: args.verbose,
6587
+ quiet: args.quiet
6588
+ });
6589
+ if (result.exitCode !== 0) {
6590
+ process.stderr.write(
6591
+ `[kody] watch \`${match.executable}\` exited ${result.exitCode}: ${result.reason ?? "(no reason)"}
6592
+ `
6593
+ );
6594
+ if (result.exitCode > worstExit) worstExit = result.exitCode;
6595
+ }
6596
+ } catch (err) {
6597
+ const msg = err instanceof Error ? err.message : String(err);
6598
+ process.stderr.write(`[kody] watch \`${match.executable}\` crashed: ${msg}
6599
+ `);
6600
+ worstExit = Math.max(worstExit, 99);
6601
+ }
6602
+ }
6603
+ return worstExit;
6604
+ }
6419
6605
 
6420
6606
  // src/chat-cli.ts
6421
6607
  var DEFAULT_MODEL = "claude/claude-haiku-4-5-20251001";
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "probe-skill",
3
+ "role": "utility",
4
+ "describe": "Live-test executable. Loads the executable-local 'probe-skill-marker' skill (resolved from src/executables/probe-skill/skills/, NOT the central catalog) and asks the agent to emit the skill's token back as an issue comment. Validates the new local-skill resolution path in buildSyntheticPlugin end-to-end.",
5
+ "inputs": [
6
+ { "name": "issue", "flag": "--issue", "type": "int", "required": true, "describe": "GitHub issue number to verify against." }
7
+ ],
8
+ "claudeCode": {
9
+ "model": "inherit",
10
+ "permissionMode": "default",
11
+ "maxTurns": 12,
12
+ "systemPromptAppend": "You are running Kody's executable-local skill live verification. Emit the token exactly as the skill instructs.",
13
+ "tools": ["Read", "Grep", "Glob", "Bash"],
14
+ "hooks": [],
15
+ "skills": ["probe-skill-marker"],
16
+ "commands": [],
17
+ "subagents": [],
18
+ "plugins": [],
19
+ "mcpServers": []
20
+ },
21
+ "cliTools": [],
22
+ "scripts": {
23
+ "preflight": [
24
+ { "script": "loadIssueContext" },
25
+ { "script": "loadTaskState" },
26
+ { "script": "buildSyntheticPlugin" },
27
+ { "script": "composePrompt" }
28
+ ],
29
+ "postflight": [
30
+ { "script": "parseAgentResult" },
31
+ { "script": "writeRunSummary" },
32
+ { "script": "saveTaskState" }
33
+ ]
34
+ }
35
+ }
@@ -0,0 +1,31 @@
1
+ You are Kody's executable-local skill live verification agent. Your only job: prove that the skill named `probe-skill-marker` was loaded into your session from this executable's own directory (NOT the shared catalog), and report its token back to the issue.
2
+
3
+ Issue #{{issue.number}}: {{issue.title}}
4
+
5
+ ---
6
+
7
+ # What to do
8
+
9
+ 1. List your available skills. Confirm a skill named `probe-skill-marker` is loaded.
10
+ 2. Activate it (its activation phrase is "probe-skill-marker"). The skill instructs you to emit a single token of the form `PROBE_SKILL_OK_<version>`.
11
+ 3. Post a comment on issue #{{issue.number}} via `gh issue comment {{issue.number}} --body "..."`. The body must be a single line:
12
+ ```
13
+ probe-skill verification: <TOKEN>
14
+ ```
15
+ Replace `<TOKEN>` with whatever exact token the skill told you to emit. If the skill is NOT loaded, post `probe-skill verification: SKILL_NOT_LOADED` instead.
16
+
17
+ # Output contract
18
+
19
+ After posting the comment, your final message must be exactly:
20
+
21
+ ```
22
+ DONE
23
+ COMMIT_MSG: probe-skill: live verification for #{{issue.number}}
24
+ PR_SUMMARY: probe-skill ran; see issue comment for the token.
25
+ ```
26
+
27
+ # Rules
28
+
29
+ - Read-only on the repo. Do NOT edit any file. Do NOT run git.
30
+ - The only state-changing command you may run is `gh issue comment`.
31
+ - Do not perform the issue's actual work.
@@ -0,0 +1,18 @@
1
+ ---
2
+ name: probe-skill-marker
3
+ description: Internal Kody live-test skill, shipped from the probe-skill executable's own directory (not the shared src/plugins catalog). Activates when a prompt mentions "probe-skill-marker" and provides a versioned confirmation token.
4
+ ---
5
+
6
+ # probe-skill-marker
7
+
8
+ This skill exists only to verify that Kody's executable-local plugin-part resolution actually loads skills from `src/executables/<name>/skills/` into the agent's session.
9
+
10
+ ## When to activate
11
+
12
+ When the user's prompt contains the phrase "probe-skill-marker" or explicitly asks whether the skill is loaded.
13
+
14
+ ## What to do
15
+
16
+ Emit the literal token `PROBE_SKILL_OK_v1` exactly as written. Do not modify, paraphrase, or interpret it. The token version (`v1`) lets us prove that edits to this file are picked up on the next run — bumping it to `v2` here should result in the agent reporting `PROBE_SKILL_OK_v2` after a fresh publish + trigger.
17
+
18
+ Do not use this skill for anything else. It is a no-op confirmation signal for infrastructure validation.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.39",
3
+ "version": "0.3.41",
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",