@kody-ade/kody-engine 0.3.35 → 0.3.39

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.35",
6
+ version: "0.3.39",
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",
@@ -51,8 +51,8 @@ var package_default = {
51
51
 
52
52
  // src/chat-cli.ts
53
53
  import { execFileSync as execFileSync23 } from "child_process";
54
- import * as fs22 from "fs";
55
- import * as path19 from "path";
54
+ import * as fs24 from "fs";
55
+ import * as path21 from "path";
56
56
 
57
57
  // src/chat/events.ts
58
58
  import * as fs from "fs";
@@ -606,8 +606,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
606
606
 
607
607
  // src/kody-cli.ts
608
608
  import { execFileSync as execFileSync22 } from "child_process";
609
- import * as fs21 from "fs";
610
- import * as path18 from "path";
609
+ import * as fs23 from "fs";
610
+ import * as path20 from "path";
611
611
 
612
612
  // src/dispatch.ts
613
613
  import * as fs6 from "fs";
@@ -630,30 +630,51 @@ function getExecutablesRoot() {
630
630
  }
631
631
  return candidates[0];
632
632
  }
633
- function listExecutables(root = getExecutablesRoot()) {
634
- if (!fs5.existsSync(root)) return [];
635
- const entries = fs5.readdirSync(root, { withFileTypes: true });
633
+ function getProjectExecutablesRoot() {
634
+ return path5.join(process.cwd(), ".kody", "executables");
635
+ }
636
+ function getExecutableRoots() {
637
+ return [getProjectExecutablesRoot(), getExecutablesRoot()];
638
+ }
639
+ function listExecutables(roots = getExecutableRoots()) {
640
+ const rootList = typeof roots === "string" ? [roots] : roots;
641
+ const seen = /* @__PURE__ */ new Set();
636
642
  const out = [];
637
- for (const ent of entries) {
638
- if (!ent.isDirectory()) continue;
639
- const profilePath = path5.join(root, ent.name, "profile.json");
640
- if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
641
- out.push({ name: ent.name, profilePath });
643
+ for (const root of rootList) {
644
+ if (!fs5.existsSync(root)) continue;
645
+ const entries = fs5.readdirSync(root, { withFileTypes: true });
646
+ for (const ent of entries) {
647
+ if (!ent.isDirectory()) continue;
648
+ if (seen.has(ent.name)) continue;
649
+ const profilePath = path5.join(root, ent.name, "profile.json");
650
+ if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
651
+ out.push({ name: ent.name, profilePath });
652
+ seen.add(ent.name);
653
+ }
642
654
  }
643
655
  }
644
656
  return out.sort((a, b) => a.name.localeCompare(b.name));
645
657
  }
646
- function hasExecutable(name, root = getExecutablesRoot()) {
647
- if (!isSafeName(name)) return false;
648
- const profilePath = path5.join(root, name, "profile.json");
649
- return fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile();
658
+ function resolveExecutable(name, roots = getExecutableRoots()) {
659
+ if (!isSafeName(name)) return null;
660
+ const rootList = typeof roots === "string" ? [roots] : roots;
661
+ for (const root of rootList) {
662
+ const profilePath = path5.join(root, name, "profile.json");
663
+ if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
664
+ return profilePath;
665
+ }
666
+ }
667
+ return null;
668
+ }
669
+ function hasExecutable(name, roots = getExecutableRoots()) {
670
+ return resolveExecutable(name, roots) !== null;
650
671
  }
651
672
  function isSafeName(name) {
652
673
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
653
674
  }
654
- function getProfileInputs(name, root = getExecutablesRoot()) {
655
- if (!hasExecutable(name, root)) return null;
656
- const profilePath = path5.join(root, name, "profile.json");
675
+ function getProfileInputs(name, roots = getExecutableRoots()) {
676
+ const profilePath = resolveExecutable(name, roots);
677
+ if (!profilePath) return null;
657
678
  try {
658
679
  const raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
659
680
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
@@ -834,8 +855,8 @@ function coerceBare(spec, value) {
834
855
 
835
856
  // src/executor.ts
836
857
  import { spawnSync } from "child_process";
837
- import * as fs20 from "fs";
838
- import * as path17 from "path";
858
+ import * as fs22 from "fs";
859
+ import * as path19 from "path";
839
860
 
840
861
  // src/litellm.ts
841
862
  import { execFileSync, spawn } from "child_process";
@@ -982,10 +1003,7 @@ function loadProfile(profilePath) {
982
1003
  throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
983
1004
  }
984
1005
  if (typeof r.role !== "string" || !VALID_ROLES.has(r.role)) {
985
- throw new ProfileError(
986
- profilePath,
987
- `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`
988
- );
1006
+ throw new ProfileError(profilePath, `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`);
989
1007
  }
990
1008
  const role = r.role;
991
1009
  let phase;
@@ -1176,7 +1194,10 @@ function parseScriptList(p, key, raw) {
1176
1194
  const out = [];
1177
1195
  for (const [i, item] of raw.entries()) {
1178
1196
  if (!item || typeof item !== "object") {
1179
- throw new ProfileError(p, `scripts.${key}[${i}] must be an object like { script, runWhen? } or { shell, runWhen? }`);
1197
+ throw new ProfileError(
1198
+ p,
1199
+ `scripts.${key}[${i}] must be an object like { script, runWhen? } or { shell, runWhen? }`
1200
+ );
1180
1201
  }
1181
1202
  const r = item;
1182
1203
  const hasScript = typeof r.script === "string" && r.script.length > 0;
@@ -1185,7 +1206,10 @@ function parseScriptList(p, key, raw) {
1185
1206
  throw new ProfileError(p, `scripts.${key}[${i}] cannot set both "script" and "shell" \u2014 pick one`);
1186
1207
  }
1187
1208
  if (!hasScript && !hasShell) {
1188
- throw new ProfileError(p, `scripts.${key}[${i}] must set "script" (registered TS function) or "shell" (filename in executable dir)`);
1209
+ throw new ProfileError(
1210
+ p,
1211
+ `scripts.${key}[${i}] must set "script" (registered TS function) or "shell" (filename in executable dir)`
1212
+ );
1189
1213
  }
1190
1214
  const entry = {};
1191
1215
  if (hasScript) entry.script = r.script;
@@ -2093,11 +2117,11 @@ var diagMcp = async (_ctx) => {
2093
2117
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
2094
2118
  `);
2095
2119
  try {
2096
- const v = execFileSync6(
2097
- "npx",
2098
- ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"],
2099
- { stdio: "pipe", timeout: 6e4, encoding: "utf8" }
2100
- ).trim();
2120
+ const v = execFileSync6("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
2121
+ stdio: "pipe",
2122
+ timeout: 6e4,
2123
+ encoding: "utf8"
2124
+ }).trim();
2101
2125
  process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
2102
2126
  `);
2103
2127
  } catch (e) {
@@ -2273,7 +2297,9 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2273
2297
  if (routeFile) {
2274
2298
  try {
2275
2299
  const content = fs14.readFileSync(path13.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
2276
- const methods = HTTP_METHODS.filter((m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content));
2300
+ const methods = HTTP_METHODS.filter(
2301
+ (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
2302
+ );
2277
2303
  if (methods.length > 0) {
2278
2304
  out.push({
2279
2305
  path: prefix,
@@ -2648,6 +2674,64 @@ function parsePr(url) {
2648
2674
  return Number.isFinite(n) ? n : null;
2649
2675
  }
2650
2676
 
2677
+ // src/scripts/dispatchMissionFileTicks.ts
2678
+ import * as fs16 from "fs";
2679
+ import * as path15 from "path";
2680
+ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
2681
+ ctx.skipAgent = true;
2682
+ const targetExecutable = String(args?.targetExecutable ?? "");
2683
+ if (!targetExecutable) {
2684
+ throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
2685
+ }
2686
+ const missionsDir = String(args?.missionsDir ?? ".kody/missions");
2687
+ const slugArg = String(args?.slugArg ?? "mission");
2688
+ const slugs = listMissionSlugs(path15.join(ctx.cwd, missionsDir));
2689
+ ctx.data.missionSlugCount = slugs.length;
2690
+ if (slugs.length === 0) {
2691
+ process.stdout.write(`[missions] no mission files in ${missionsDir}
2692
+ `);
2693
+ return;
2694
+ }
2695
+ process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
2696
+ `);
2697
+ const results = [];
2698
+ for (const slug of slugs) {
2699
+ process.stdout.write(`[missions] \u2192 tick ${slug}
2700
+ `);
2701
+ try {
2702
+ const out = await runExecutable(targetExecutable, {
2703
+ cliArgs: { [slugArg]: slug },
2704
+ cwd: ctx.cwd,
2705
+ config: ctx.config,
2706
+ verbose: ctx.verbose,
2707
+ quiet: ctx.quiet
2708
+ });
2709
+ results.push({ slug, exitCode: out.exitCode, reason: out.reason });
2710
+ if (out.exitCode !== 0) {
2711
+ process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
2712
+ `);
2713
+ }
2714
+ } catch (err) {
2715
+ const msg = err instanceof Error ? err.message : String(err);
2716
+ process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
2717
+ `);
2718
+ results.push({ slug, exitCode: 99, reason: msg });
2719
+ }
2720
+ }
2721
+ ctx.data.missionTickResults = results;
2722
+ ctx.output.exitCode = 0;
2723
+ };
2724
+ function listMissionSlugs(absDir) {
2725
+ if (!fs16.existsSync(absDir)) return [];
2726
+ let entries;
2727
+ try {
2728
+ entries = fs16.readdirSync(absDir, { withFileTypes: true });
2729
+ } catch {
2730
+ return [];
2731
+ }
2732
+ return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).filter((slug) => slug.length > 0 && !slug.startsWith("_") && !slug.startsWith(".")).sort();
2733
+ }
2734
+
2651
2735
  // src/issue.ts
2652
2736
  import { execFileSync as execFileSync8 } from "child_process";
2653
2737
  var API_TIMEOUT_MS4 = 3e4;
@@ -2836,10 +2920,9 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
2836
2920
  function listIssuesByLabel(label, cwd) {
2837
2921
  let raw = "";
2838
2922
  try {
2839
- raw = gh2(
2840
- ["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"],
2841
- { cwd }
2842
- );
2923
+ raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
2924
+ cwd
2925
+ });
2843
2926
  } catch {
2844
2927
  return [];
2845
2928
  }
@@ -3084,10 +3167,7 @@ function ensureLabels(cwd) {
3084
3167
  }
3085
3168
  function getIssueLabels(issueNumber, cwd) {
3086
3169
  try {
3087
- const output = gh2(
3088
- ["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"],
3089
- { cwd }
3090
- );
3170
+ const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
3091
3171
  return output.split("\n").filter(Boolean);
3092
3172
  } catch {
3093
3173
  return [];
@@ -3111,10 +3191,8 @@ function createLabelInRepo(spec, cwd) {
3111
3191
  function setKodyLabel(issueNumber, spec, cwd) {
3112
3192
  const target = spec.label;
3113
3193
  if (!target.startsWith(KODY_NAMESPACE)) {
3114
- process.stderr.write(
3115
- `[kody] setKodyLabel: refusing to set non-kody label "${target}"
3116
- `
3117
- );
3194
+ process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
3195
+ `);
3118
3196
  return;
3119
3197
  }
3120
3198
  const targetGroup = groupOf(target);
@@ -3140,10 +3218,8 @@ function setKodyLabel(issueNumber, spec, cwd) {
3140
3218
  return;
3141
3219
  }
3142
3220
  }
3143
- process.stderr.write(
3144
- `[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
3145
- `
3146
- );
3221
+ process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
3222
+ `);
3147
3223
  }
3148
3224
  }
3149
3225
  function looksLikeMissingLabel(err) {
@@ -3312,7 +3388,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3312
3388
 
3313
3389
  // src/gha.ts
3314
3390
  import { execFileSync as execFileSync11 } from "child_process";
3315
- import * as fs16 from "fs";
3391
+ import * as fs17 from "fs";
3316
3392
  function getRunUrl() {
3317
3393
  const server = process.env.GITHUB_SERVER_URL;
3318
3394
  const repo = process.env.GITHUB_REPOSITORY;
@@ -3323,10 +3399,10 @@ function getRunUrl() {
3323
3399
  function reactToTriggerComment(cwd) {
3324
3400
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
3325
3401
  const eventPath = process.env.GITHUB_EVENT_PATH;
3326
- if (!eventPath || !fs16.existsSync(eventPath)) return;
3402
+ if (!eventPath || !fs17.existsSync(eventPath)) return;
3327
3403
  let event = null;
3328
3404
  try {
3329
- event = JSON.parse(fs16.readFileSync(eventPath, "utf-8"));
3405
+ event = JSON.parse(fs17.readFileSync(eventPath, "utf-8"));
3330
3406
  } catch {
3331
3407
  return;
3332
3408
  }
@@ -3566,22 +3642,22 @@ function tryPostPr2(prNumber, body, cwd) {
3566
3642
 
3567
3643
  // src/scripts/initFlow.ts
3568
3644
  import { execFileSync as execFileSync13 } from "child_process";
3569
- import * as fs18 from "fs";
3570
- import * as path16 from "path";
3645
+ import * as fs19 from "fs";
3646
+ import * as path17 from "path";
3571
3647
 
3572
3648
  // src/scripts/loadQaGuide.ts
3573
- import * as fs17 from "fs";
3574
- import * as path15 from "path";
3649
+ import * as fs18 from "fs";
3650
+ import * as path16 from "path";
3575
3651
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
3576
3652
  var loadQaGuide = async (ctx) => {
3577
- const full = path15.join(ctx.cwd, QA_GUIDE_REL_PATH);
3578
- if (!fs17.existsSync(full)) {
3653
+ const full = path16.join(ctx.cwd, QA_GUIDE_REL_PATH);
3654
+ if (!fs18.existsSync(full)) {
3579
3655
  ctx.data.qaGuide = "";
3580
3656
  ctx.data.qaGuidePath = "";
3581
3657
  return;
3582
3658
  }
3583
3659
  try {
3584
- ctx.data.qaGuide = fs17.readFileSync(full, "utf-8");
3660
+ ctx.data.qaGuide = fs18.readFileSync(full, "utf-8");
3585
3661
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
3586
3662
  } catch {
3587
3663
  ctx.data.qaGuide = "";
@@ -3591,9 +3667,9 @@ var loadQaGuide = async (ctx) => {
3591
3667
 
3592
3668
  // src/scripts/initFlow.ts
3593
3669
  function detectPackageManager(cwd) {
3594
- if (fs18.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3595
- if (fs18.existsSync(path16.join(cwd, "yarn.lock"))) return "yarn";
3596
- if (fs18.existsSync(path16.join(cwd, "bun.lockb"))) return "bun";
3670
+ if (fs19.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3671
+ if (fs19.existsSync(path17.join(cwd, "yarn.lock"))) return "yarn";
3672
+ if (fs19.existsSync(path17.join(cwd, "bun.lockb"))) return "bun";
3597
3673
  return "npm";
3598
3674
  }
3599
3675
  function qualityCommandsFor(pm) {
@@ -3715,33 +3791,33 @@ function performInit(cwd, force) {
3715
3791
  const pm = detectPackageManager(cwd);
3716
3792
  const ownerRepo = detectOwnerRepo(cwd);
3717
3793
  const defaultBranch = defaultBranchFromGit(cwd);
3718
- const configPath = path16.join(cwd, "kody.config.json");
3719
- if (fs18.existsSync(configPath) && !force) {
3794
+ const configPath = path17.join(cwd, "kody.config.json");
3795
+ if (fs19.existsSync(configPath) && !force) {
3720
3796
  skipped.push("kody.config.json");
3721
3797
  } else {
3722
3798
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
3723
- fs18.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
3799
+ fs19.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
3724
3800
  `);
3725
3801
  wrote.push("kody.config.json");
3726
3802
  }
3727
- const workflowDir = path16.join(cwd, ".github", "workflows");
3728
- const workflowPath = path16.join(workflowDir, "kody.yml");
3729
- if (fs18.existsSync(workflowPath) && !force) {
3803
+ const workflowDir = path17.join(cwd, ".github", "workflows");
3804
+ const workflowPath = path17.join(workflowDir, "kody.yml");
3805
+ if (fs19.existsSync(workflowPath) && !force) {
3730
3806
  skipped.push(".github/workflows/kody.yml");
3731
3807
  } else {
3732
- fs18.mkdirSync(workflowDir, { recursive: true });
3733
- fs18.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
3808
+ fs19.mkdirSync(workflowDir, { recursive: true });
3809
+ fs19.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
3734
3810
  wrote.push(".github/workflows/kody.yml");
3735
3811
  }
3736
- const hasUi = fs18.existsSync(path16.join(cwd, "src/app")) || fs18.existsSync(path16.join(cwd, "app")) || fs18.existsSync(path16.join(cwd, "pages"));
3812
+ const hasUi = fs19.existsSync(path17.join(cwd, "src/app")) || fs19.existsSync(path17.join(cwd, "app")) || fs19.existsSync(path17.join(cwd, "pages"));
3737
3813
  if (hasUi) {
3738
- const qaGuidePath = path16.join(cwd, QA_GUIDE_REL_PATH);
3739
- if (fs18.existsSync(qaGuidePath) && !force) {
3814
+ const qaGuidePath = path17.join(cwd, QA_GUIDE_REL_PATH);
3815
+ if (fs19.existsSync(qaGuidePath) && !force) {
3740
3816
  skipped.push(QA_GUIDE_REL_PATH);
3741
3817
  } else {
3742
- fs18.mkdirSync(path16.dirname(qaGuidePath), { recursive: true });
3818
+ fs19.mkdirSync(path17.dirname(qaGuidePath), { recursive: true });
3743
3819
  const discovery = runQaDiscovery(cwd);
3744
- fs18.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
3820
+ fs19.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
3745
3821
  wrote.push(QA_GUIDE_REL_PATH);
3746
3822
  }
3747
3823
  }
@@ -3753,12 +3829,12 @@ function performInit(cwd, force) {
3753
3829
  continue;
3754
3830
  }
3755
3831
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
3756
- const target = path16.join(workflowDir, `kody-${exe.name}.yml`);
3757
- if (fs18.existsSync(target) && !force) {
3832
+ const target = path17.join(workflowDir, `kody-${exe.name}.yml`);
3833
+ if (fs19.existsSync(target) && !force) {
3758
3834
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
3759
3835
  continue;
3760
3836
  }
3761
- fs18.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
3837
+ fs19.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
3762
3838
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
3763
3839
  }
3764
3840
  let labels;
@@ -3816,10 +3892,8 @@ var initFlow = async (ctx) => {
3816
3892
  `);
3817
3893
  }
3818
3894
  if (labels.failed.length > 0) {
3819
- process.stdout.write(
3820
- ` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
3821
- `
3822
- );
3895
+ process.stdout.write(` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
3896
+ `);
3823
3897
  }
3824
3898
  }
3825
3899
  process.stdout.write(
@@ -3877,6 +3951,9 @@ function isStateEnvelope(x) {
3877
3951
  const o = x;
3878
3952
  return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
3879
3953
  }
3954
+ function initialStateEnvelope(cursor = "seed") {
3955
+ return { version: 1, rev: 0, cursor, data: {}, done: false };
3956
+ }
3880
3957
  function formatStateCommentBody(marker, state) {
3881
3958
  return `<!-- ${marker} -->
3882
3959
 
@@ -3924,10 +4001,10 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
3924
4001
  }
3925
4002
  function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3926
4003
  const body = formatStateCommentBody(marker, state);
3927
- const raw = gh2(
3928
- ["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"],
3929
- { cwd, input: JSON.stringify({ body }) }
3930
- );
4004
+ const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4005
+ cwd,
4006
+ input: JSON.stringify({ body })
4007
+ });
3931
4008
  const parsed = JSON.parse(raw);
3932
4009
  try {
3933
4010
  minimizeComment(parsed.node_id, cwd);
@@ -3937,10 +4014,10 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3937
4014
  }
3938
4015
  function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
3939
4016
  const body = formatStateCommentBody(marker, state);
3940
- gh2(
3941
- ["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"],
3942
- { cwd, input: JSON.stringify({ body }) }
3943
- );
4017
+ gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4018
+ cwd,
4019
+ input: JSON.stringify({ body })
4020
+ });
3944
4021
  try {
3945
4022
  minimizeComment(commentNodeId, cwd);
3946
4023
  } catch {
@@ -3977,6 +4054,122 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
3977
4054
  ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
3978
4055
  };
3979
4056
 
4057
+ // src/scripts/loadMissionFromFile.ts
4058
+ import * as fs20 from "fs";
4059
+ import * as path18 from "path";
4060
+
4061
+ // src/scripts/missionStateFile.ts
4062
+ function stateFilePath(missionsDir, slug) {
4063
+ return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
4064
+ }
4065
+ function loadMissionState(owner, repo, filePath, cwd) {
4066
+ let raw = "";
4067
+ try {
4068
+ raw = gh2(["api", `/repos/${owner}/${repo}/contents/${filePath}`], { cwd });
4069
+ } catch (err) {
4070
+ const msg = err instanceof Error ? err.message : String(err);
4071
+ if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
4072
+ return { path: filePath, sha: null, state: initialStateEnvelope("seed"), created: true };
4073
+ }
4074
+ throw err;
4075
+ }
4076
+ let parsed;
4077
+ try {
4078
+ parsed = JSON.parse(raw);
4079
+ } catch {
4080
+ throw new Error(`loadMissionState: contents API for ${filePath} did not return JSON`);
4081
+ }
4082
+ if (!parsed || typeof parsed !== "object") {
4083
+ throw new Error(`loadMissionState: contents API for ${filePath} returned non-object`);
4084
+ }
4085
+ const o = parsed;
4086
+ if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
4087
+ throw new Error(`loadMissionState: ${filePath} is not a base64 file`);
4088
+ }
4089
+ const decoded = Buffer.from(o.content, "base64").toString("utf-8");
4090
+ let envelope;
4091
+ try {
4092
+ envelope = JSON.parse(decoded);
4093
+ } catch {
4094
+ throw new Error(`loadMissionState: ${filePath} is not valid JSON`);
4095
+ }
4096
+ if (!isStateEnvelope(envelope)) {
4097
+ throw new Error(`loadMissionState: ${filePath} is not a StateEnvelope`);
4098
+ }
4099
+ return { path: filePath, sha: o.sha, state: envelope, created: false };
4100
+ }
4101
+ function writeMissionState(owner, repo, loaded, next, cwd) {
4102
+ if (!loaded.created && deepEqualsState(loaded.state, next)) {
4103
+ return false;
4104
+ }
4105
+ const body = JSON.stringify(next, null, 2) + "\n";
4106
+ const payload = {
4107
+ message: `chore(missions): update state for ${stateFileSlug(loaded.path)} (rev ${next.rev})`,
4108
+ content: Buffer.from(body, "utf-8").toString("base64")
4109
+ };
4110
+ if (loaded.sha) payload.sha = loaded.sha;
4111
+ gh2(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${loaded.path}`, "--input", "-"], {
4112
+ cwd,
4113
+ input: JSON.stringify(payload)
4114
+ });
4115
+ return true;
4116
+ }
4117
+ function deepEqualsState(a, b) {
4118
+ if (a.cursor !== b.cursor || a.done !== b.done) return false;
4119
+ return JSON.stringify(a.data) === JSON.stringify(b.data);
4120
+ }
4121
+ function stateFileSlug(filePath) {
4122
+ const last = filePath.split("/").pop() ?? filePath;
4123
+ return last.replace(/\.state\.json$/i, "");
4124
+ }
4125
+
4126
+ // src/scripts/loadMissionFromFile.ts
4127
+ var loadMissionFromFile = async (ctx, _profile, args) => {
4128
+ const missionsDir = String(args?.missionsDir ?? ".kody/missions");
4129
+ const slugArg = String(args?.slugArg ?? "mission");
4130
+ const slug = String(ctx.args[slugArg] ?? "").trim();
4131
+ if (!slug) {
4132
+ throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
4133
+ }
4134
+ const owner = ctx.config.github.owner;
4135
+ const repo = ctx.config.github.repo;
4136
+ if (!owner || !repo) {
4137
+ throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
4138
+ }
4139
+ const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
4140
+ if (!fs20.existsSync(absPath)) {
4141
+ throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
4142
+ }
4143
+ const raw = fs20.readFileSync(absPath, "utf-8");
4144
+ const { title, body } = parseMissionFile(raw, slug);
4145
+ const loaded = loadMissionState(owner, repo, stateFilePath(missionsDir, slug), ctx.cwd);
4146
+ ctx.data.missionSlug = slug;
4147
+ ctx.data.missionTitle = title;
4148
+ ctx.data.missionIntent = body;
4149
+ ctx.data.missionState = loaded;
4150
+ ctx.data.missionStateJson = JSON.stringify(loaded.state, null, 2);
4151
+ };
4152
+ function parseMissionFile(raw, slug) {
4153
+ let stripped = raw;
4154
+ if (stripped.startsWith("---\n")) {
4155
+ const end = stripped.indexOf("\n---\n", 4);
4156
+ if (end !== -1) {
4157
+ stripped = stripped.slice(end + 5);
4158
+ }
4159
+ }
4160
+ const trimmed = stripped.trim();
4161
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
4162
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
4163
+ if (h1) {
4164
+ const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
4165
+ return { title: h1[1].trim(), body: rest };
4166
+ }
4167
+ return { title: humanizeSlug(slug), body: trimmed };
4168
+ }
4169
+ function humanizeSlug(slug) {
4170
+ return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
4171
+ }
4172
+
3980
4173
  // src/scripts/loadPriorArt.ts
3981
4174
  var PER_PR_DIFF_MAX_BYTES = 8e3;
3982
4175
  var TOTAL_MAX_BYTES = 3e4;
@@ -4269,6 +4462,53 @@ function escapeRegex(s) {
4269
4462
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4270
4463
  }
4271
4464
 
4465
+ // src/scripts/parseMissionStateFromAgentResult.ts
4466
+ function isPartialEnvelope2(x) {
4467
+ if (x === null || typeof x !== "object") return false;
4468
+ const o = x;
4469
+ return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
4470
+ }
4471
+ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
4472
+ const fenceLabel = String(args?.fenceLabel ?? "");
4473
+ if (!fenceLabel) {
4474
+ throw new Error("parseMissionStateFromAgentResult: `with.fenceLabel` is required");
4475
+ }
4476
+ if (!agentResult) {
4477
+ ctx.data.nextStateParseError = "agent did not run";
4478
+ return;
4479
+ }
4480
+ const fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
4481
+ const match = fenceRegex.exec(agentResult.finalText);
4482
+ if (!match) {
4483
+ ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
4484
+ return;
4485
+ }
4486
+ let parsed;
4487
+ try {
4488
+ parsed = JSON.parse(match[1].trim());
4489
+ } catch (err) {
4490
+ ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
4491
+ return;
4492
+ }
4493
+ if (!isPartialEnvelope2(parsed)) {
4494
+ ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
4495
+ return;
4496
+ }
4497
+ const loaded = ctx.data.missionState;
4498
+ const prevRev = loaded?.state.rev ?? 0;
4499
+ const next = {
4500
+ version: 1,
4501
+ rev: prevRev + 1,
4502
+ cursor: parsed.cursor,
4503
+ data: parsed.data,
4504
+ done: parsed.done
4505
+ };
4506
+ ctx.data.nextMissionState = next;
4507
+ };
4508
+ function escapeRegex2(s) {
4509
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4510
+ }
4511
+
4272
4512
  // src/scripts/persistArtifacts.ts
4273
4513
  var persistArtifacts = async (ctx, profile) => {
4274
4514
  if (profile.outputArtifacts.length === 0) return;
@@ -4334,16 +4574,16 @@ var postClassification = async (ctx) => {
4334
4574
  }
4335
4575
  if (!classification) {
4336
4576
  ctx.data.action = failedAction("classification missing or invalid");
4337
- tryAuditComment(issueNumber, "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.", ctx.cwd);
4577
+ tryAuditComment(
4578
+ issueNumber,
4579
+ "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
4580
+ ctx.cwd
4581
+ );
4338
4582
  ctx.output.exitCode = 1;
4339
4583
  ctx.output.reason = "classify: no decision";
4340
4584
  return;
4341
4585
  }
4342
- tryAuditComment(
4343
- issueNumber,
4344
- `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
4345
- ctx.cwd
4346
- );
4586
+ tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
4347
4587
  try {
4348
4588
  execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4349
4589
  cwd: ctx.cwd,
@@ -4633,7 +4873,9 @@ var requirePlanDeviations = async (ctx, profile) => {
4633
4873
  ctx.data.planDeviationCount = bullets.length;
4634
4874
  };
4635
4875
  function isNoneSentinel(block) {
4636
- const stripped = block.split("\n").map((l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()).filter((l) => l.length > 0);
4876
+ const stripped = block.split("\n").map(
4877
+ (l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()
4878
+ ).filter((l) => l.length > 0);
4637
4879
  if (stripped.length !== 1) return false;
4638
4880
  return stripped[0] === "none";
4639
4881
  }
@@ -5265,16 +5507,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5265
5507
  };
5266
5508
  function fetchChecks(prNumber, cwd) {
5267
5509
  try {
5268
- const raw = execFileSync20(
5269
- "gh",
5270
- ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"],
5271
- {
5272
- encoding: "utf-8",
5273
- timeout: API_TIMEOUT_MS9,
5274
- cwd,
5275
- stdio: ["ignore", "pipe", "pipe"]
5276
- }
5277
- );
5510
+ const raw = execFileSync20("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
5511
+ encoding: "utf-8",
5512
+ timeout: API_TIMEOUT_MS9,
5513
+ cwd,
5514
+ stdio: ["ignore", "pipe", "pipe"]
5515
+ });
5278
5516
  const parsed = JSON.parse(raw);
5279
5517
  return Array.isArray(parsed) ? parsed : [];
5280
5518
  } catch (err) {
@@ -5353,10 +5591,7 @@ function formatStaleReport(stale, staleDays) {
5353
5591
  if (stale.length === 0) {
5354
5592
  return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
5355
5593
  }
5356
- const lines = [
5357
- `\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
5358
- ""
5359
- ];
5594
+ const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
5360
5595
  for (const pr of stale.slice(0, 50)) {
5361
5596
  lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
5362
5597
  }
@@ -5420,8 +5655,34 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
5420
5655
  }
5421
5656
  };
5422
5657
 
5658
+ // src/scripts/writeMissionStateFile.ts
5659
+ var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
5660
+ const parseError = ctx.data.nextStateParseError;
5661
+ if (parseError) {
5662
+ process.stderr.write(`[kody] mission state write skipped: ${parseError}
5663
+ `);
5664
+ if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
5665
+ if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
5666
+ return;
5667
+ }
5668
+ const next = ctx.data.nextMissionState;
5669
+ if (!next) {
5670
+ return;
5671
+ }
5672
+ const loaded = ctx.data.missionState;
5673
+ if (!loaded) {
5674
+ throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
5675
+ }
5676
+ const owner = ctx.config.github.owner;
5677
+ const repo = ctx.config.github.repo;
5678
+ if (!owner || !repo) {
5679
+ throw new Error("writeMissionStateFile: ctx.config.github.owner/repo must be set");
5680
+ }
5681
+ writeMissionState(owner, repo, loaded, next, ctx.cwd);
5682
+ };
5683
+
5423
5684
  // src/scripts/writeRunSummary.ts
5424
- import * as fs19 from "fs";
5685
+ import * as fs21 from "fs";
5425
5686
  var writeRunSummary = async (ctx, profile) => {
5426
5687
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
5427
5688
  if (!summaryPath) return;
@@ -5443,7 +5704,7 @@ var writeRunSummary = async (ctx, profile) => {
5443
5704
  if (reason) lines.push(`- **Reason:** ${reason}`);
5444
5705
  lines.push("");
5445
5706
  try {
5446
- fs19.appendFileSync(summaryPath, `${lines.join("\n")}
5707
+ fs21.appendFileSync(summaryPath, `${lines.join("\n")}
5447
5708
  `);
5448
5709
  } catch {
5449
5710
  }
@@ -5462,6 +5723,7 @@ var preflightScripts = {
5462
5723
  loadTaskState,
5463
5724
  loadIssueContext,
5464
5725
  loadIssueStateComment,
5726
+ loadMissionFromFile,
5465
5727
  loadConventions,
5466
5728
  loadCoverageRules,
5467
5729
  loadPriorArt,
@@ -5476,12 +5738,15 @@ var preflightScripts = {
5476
5738
  skipAgent,
5477
5739
  classifyByLabel,
5478
5740
  diagMcp,
5479
- dispatchMissionTicks
5741
+ dispatchMissionTicks,
5742
+ dispatchMissionFileTicks
5480
5743
  };
5481
5744
  var postflightScripts = {
5482
5745
  parseAgentResult: parseAgentResult2,
5483
5746
  parseIssueStateFromAgentResult,
5747
+ parseMissionStateFromAgentResult,
5484
5748
  writeIssueStateComment,
5749
+ writeMissionStateFile,
5485
5750
  requireFeedbackActions,
5486
5751
  requirePlanDeviations,
5487
5752
  verify,
@@ -5616,9 +5881,9 @@ async function runExecutable(profileName, input) {
5616
5881
  data: {},
5617
5882
  output: { exitCode: 0 }
5618
5883
  };
5619
- const ndjsonDir = path17.join(input.cwd, ".kody");
5884
+ const ndjsonDir = path19.join(input.cwd, ".kody");
5620
5885
  const invokeAgent = async (prompt) => {
5621
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path17.isAbsolute(p) ? p : path17.resolve(profile.dir, p)).filter((p) => p.length > 0);
5886
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path19.isAbsolute(p) ? p : path19.resolve(profile.dir, p)).filter((p) => p.length > 0);
5622
5887
  const syntheticPath = ctx.data.syntheticPluginPath;
5623
5888
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
5624
5889
  return runAgent({
@@ -5694,17 +5959,17 @@ async function runExecutable(profileName, input) {
5694
5959
  }
5695
5960
  }
5696
5961
  function resolveProfilePath(profileName) {
5697
- const here = path17.dirname(new URL(import.meta.url).pathname);
5962
+ const here = path19.dirname(new URL(import.meta.url).pathname);
5698
5963
  const candidates = [
5699
- path17.join(here, "executables", profileName, "profile.json"),
5964
+ path19.join(here, "executables", profileName, "profile.json"),
5700
5965
  // same-dir sibling (dev)
5701
- path17.join(here, "..", "executables", profileName, "profile.json"),
5966
+ path19.join(here, "..", "executables", profileName, "profile.json"),
5702
5967
  // up one (prod: dist/bin → dist/executables)
5703
- path17.join(here, "..", "src", "executables", profileName, "profile.json")
5968
+ path19.join(here, "..", "src", "executables", profileName, "profile.json")
5704
5969
  // fallback
5705
5970
  ];
5706
5971
  for (const c of candidates) {
5707
- if (fs20.existsSync(c)) return c;
5972
+ if (fs22.existsSync(c)) return c;
5708
5973
  }
5709
5974
  return candidates[0];
5710
5975
  }
@@ -5797,8 +6062,8 @@ function finish(out) {
5797
6062
  var SHELL_TIMEOUT_MS = 3e5;
5798
6063
  function runShellEntry(entry, ctx, profile) {
5799
6064
  const shellName = entry.shell;
5800
- const shellPath = path17.join(profile.dir, shellName);
5801
- if (!fs20.existsSync(shellPath)) {
6065
+ const shellPath = path19.join(profile.dir, shellName);
6066
+ if (!fs22.existsSync(shellPath)) {
5802
6067
  ctx.skipAgent = true;
5803
6068
  ctx.output.exitCode = 99;
5804
6069
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -5948,9 +6213,9 @@ function resolveAuthToken(env = process.env) {
5948
6213
  return token;
5949
6214
  }
5950
6215
  function detectPackageManager2(cwd) {
5951
- if (fs21.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5952
- if (fs21.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
5953
- if (fs21.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
6216
+ if (fs23.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6217
+ if (fs23.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
6218
+ if (fs23.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
5954
6219
  return "npm";
5955
6220
  }
5956
6221
  function shellOut(cmd, args, cwd, stream = true) {
@@ -6030,11 +6295,11 @@ function configureGitIdentity(cwd) {
6030
6295
  }
6031
6296
  function postFailureTail(issueNumber, cwd, reason) {
6032
6297
  if (!issueNumber) return;
6033
- const logPath = path18.join(cwd, ".kody", "last-run.jsonl");
6298
+ const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
6034
6299
  let tail = "";
6035
6300
  try {
6036
- if (fs21.existsSync(logPath)) {
6037
- const content = fs21.readFileSync(logPath, "utf-8");
6301
+ if (fs23.existsSync(logPath)) {
6302
+ const content = fs23.readFileSync(logPath, "utf-8");
6038
6303
  tail = content.slice(-3e3);
6039
6304
  }
6040
6305
  } catch {
@@ -6059,7 +6324,7 @@ async function runCi(argv) {
6059
6324
  return 0;
6060
6325
  }
6061
6326
  const args = parseCiArgs(argv);
6062
- const cwd = args.cwd ? path18.resolve(args.cwd) : process.cwd();
6327
+ const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
6063
6328
  let earlyConfig;
6064
6329
  try {
6065
6330
  earlyConfig = loadConfig(cwd);
@@ -6197,9 +6462,9 @@ function parseChatArgs(argv, env = process.env) {
6197
6462
  return result;
6198
6463
  }
6199
6464
  function commitChatFiles(cwd, sessionId, verbose) {
6200
- const sessionFile = path19.relative(cwd, sessionFilePath(cwd, sessionId));
6201
- const eventsFile = path19.relative(cwd, eventsFilePath(cwd, sessionId));
6202
- const paths = [sessionFile, eventsFile].filter((p) => fs22.existsSync(path19.join(cwd, p)));
6465
+ const sessionFile = path21.relative(cwd, sessionFilePath(cwd, sessionId));
6466
+ const eventsFile = path21.relative(cwd, eventsFilePath(cwd, sessionId));
6467
+ const paths = [sessionFile, eventsFile].filter((p) => fs24.existsSync(path21.join(cwd, p)));
6203
6468
  if (paths.length === 0) return;
6204
6469
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
6205
6470
  try {
@@ -6237,7 +6502,7 @@ async function runChat(argv) {
6237
6502
  ${CHAT_HELP}`);
6238
6503
  return 64;
6239
6504
  }
6240
- const cwd = args.cwd ? path19.resolve(args.cwd) : process.cwd();
6505
+ const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
6241
6506
  const sessionId = args.sessionId;
6242
6507
  const unpackedSecrets = unpackAllSecrets();
6243
6508
  if (unpackedSecrets > 0) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mission-scheduler",
3
3
  "role": "watch",
4
- "describe": "Scheduled: for every open issue labeled kody:mission, invoke mission-tick once. No agent on the scheduler itself.",
4
+ "describe": "Scheduled: for every mission file under .kody/missions/, invoke mission-tick once. No agent on the scheduler itself.",
5
5
  "kind": "scheduled",
6
6
  "schedule": "*/5 * * * *",
7
7
  "inputs": [],
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "verify": "gh auth status",
30
30
  "usage": "",
31
- "allowedUses": ["issue"]
31
+ "allowedUses": ["api", "issue", "pr"]
32
32
  }
33
33
  ],
34
34
  "inputArtifacts": [],
@@ -36,13 +36,11 @@
36
36
  "scripts": {
37
37
  "preflight": [
38
38
  {
39
- "script": "dispatchMissionTicks",
39
+ "script": "dispatchMissionFileTicks",
40
40
  "with": {
41
- "label": "kody:mission",
42
- "color": "1d76db",
43
- "description": "kody: issue is a mission — scheduler ticks it every cron wake",
41
+ "missionsDir": ".kody/missions",
44
42
  "targetExecutable": "mission-tick",
45
- "issueArg": "issue"
43
+ "slugArg": "mission"
46
44
  }
47
45
  }
48
46
  ],
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "mission-tick",
3
3
  "role": "primitive",
4
- "describe": "One classifier tick for one mission issue: read intent + state, decide and execute via gh, emit next state.",
4
+ "describe": "One classifier tick for one mission file: read intent + state, decide and execute via gh, emit next state.",
5
5
  "kind": "oneshot",
6
6
  "inputs": [
7
7
  {
8
- "name": "issue",
9
- "flag": "--issue",
10
- "type": "int",
8
+ "name": "mission",
9
+ "flag": "--mission",
10
+ "type": "string",
11
11
  "required": true,
12
- "describe": "GitHub issue number that owns this mission."
12
+ "describe": "Mission slug basename (without .md) of the file under .kody/missions/."
13
13
  }
14
14
  ],
15
15
  "claudeCode": {
@@ -34,8 +34,8 @@
34
34
  "checkCommand": "command -v gh"
35
35
  },
36
36
  "verify": "gh auth status",
37
- "usage": "Use `gh` for all GitHub actions: `gh workflow run <workflow> -f <key>=<val>` to spawn a child run, `gh run view <id> --json status,conclusion` to check status, `gh issue comment <n> --body \"...\"` to post narration. NEVER edit files in the working tree.",
38
- "allowedUses": ["workflow", "run", "issue", "pr", "api"]
37
+ "usage": "Use `gh` for all GitHub actions: `gh pr list ...` to enumerate candidate PRs, `gh pr comment <n> --body \"...\"` to issue a Kody command, `gh pr view <n> --json mergeable,statusCheckRollup,headRefOid` to inspect state, `gh api ...` for anything else. NEVER edit files in the working tree.",
38
+ "allowedUses": ["pr", "api", "issue"]
39
39
  }
40
40
  ],
41
41
  "inputArtifacts": [],
@@ -43,10 +43,10 @@
43
43
  "scripts": {
44
44
  "preflight": [
45
45
  {
46
- "script": "loadIssueStateComment",
46
+ "script": "loadMissionFromFile",
47
47
  "with": {
48
- "marker": "kody-mission-state",
49
- "issueArg": "issue"
48
+ "missionsDir": ".kody/missions",
49
+ "slugArg": "mission"
50
50
  }
51
51
  },
52
52
  {
@@ -55,17 +55,13 @@
55
55
  ],
56
56
  "postflight": [
57
57
  {
58
- "script": "parseIssueStateFromAgentResult",
58
+ "script": "parseMissionStateFromAgentResult",
59
59
  "with": {
60
60
  "fenceLabel": "kody-mission-next-state"
61
61
  }
62
62
  },
63
63
  {
64
- "script": "writeIssueStateComment",
65
- "with": {
66
- "marker": "kody-mission-state",
67
- "issueArg": "issue"
68
- }
64
+ "script": "writeMissionStateFile"
69
65
  }
70
66
  ]
71
67
  }
@@ -1,33 +1,29 @@
1
- You are **kody mission-tick**, the coordinator for one GitHub-issue-scoped mission. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate other kody executables by dispatching their workflows, observing their runs, and writing back state.
1
+ You are **kody mission-tick**, the coordinator for one file-based mission. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state and issuing Kody commands as PR comments.
2
2
 
3
3
  ## The mission
4
4
 
5
- Issue **#{{issueNumber}}** — *{{issueTitle}}* owns this mission. The issue description below is authoritative: it states what success looks like, constraints, deadlines, budget, or anything else a human operator has written. Re-read it every tick — the human may have edited it to steer you.
5
+ Slug **`{{missionSlug}}`** — *{{missionTitle}}*. The mission body below is authoritative: it states what success looks like, allowed commands, and restrictions. The mission file is human-edited re-read it every tick.
6
6
 
7
- ### Mission (issue description)
7
+ ### Mission body
8
8
 
9
- {{issueIntent}}
9
+ {{missionIntent}}
10
10
 
11
11
  ## Current state
12
12
 
13
13
  This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
14
14
 
15
15
  ```json
16
- {{issueStateJson}}
16
+ {{missionStateJson}}
17
17
  ```
18
18
 
19
- `cursor` is *your* enum — pick whatever labels map cleanly to your mission's phases (e.g. `seed`, `spawn-release`, `waiting-release`, `merge-to-dev`, `finalize`, `done`). `data` is where you stash anything you need on the next tick (run IDs, SHAs, child issue numbers, budget counters). `done: true` is how you signal to future-you that the mission is over — check for it first on every tick and exit early without acting if it's set. Closing the issue also stops ticks (the scheduler only lists open issues).
19
+ `cursor` is *your* enum — pick whatever labels map cleanly to your mission's phases. `data` is where you stash anything you need on the next tick (per-PR attempt counters, last-seen SHAs, etc). `done: true` is how you signal that the mission is permanently over — for evergreen missions this should always remain `false`.
20
20
 
21
21
  ## What to do on this tick
22
22
 
23
- 1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any other action. The mission is over; don't resurrect it.
24
- 2. **Re-read the mission.** If the human has edited the description in a way that changes what "on track" means, adapt.
25
- 3. **Decide the single next step** based on (cursor, data, mission).
26
- - If `cursor` is `null`/first-run, initialize: plan the pipeline, pick an initial cursor, record any baseline info in `data`.
27
- - If you're waiting on a child run, check its status via `gh run view <id> --json status,conclusion`. If still running, just update cursor/data minimally and exit — the next cron wake will check again. If succeeded, advance. If failed, record the failure and either spawn a remediation child or mark `done: true` with an error.
28
- - If it's time to spawn a child executable, use `gh workflow run kody.yml -f issue_number=<N>` (or the appropriate workflow + inputs for the consumer repo). Capture the dispatched run's ID via `gh run list --workflow=kody.yml --limit 1 --json databaseId --jq '.[0].databaseId'` and stash it in `data`.
29
- - If the mission is complete, set `done: true` and a terminal cursor like `done`.
30
- 4. **Optionally post a human-readable narration comment** on the issue summarizing what you just did (spawned run #12345, waiting on CI, etc.). Keep it short. Use `gh issue comment {{issueNumber}} --body "..."`.
23
+ 1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
24
+ 2. **Re-read the mission body.** It may have changed since the last tick.
25
+ 3. **Execute exactly the work the body's `## Mission` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
26
+ 4. **Optionally post a short narration** wherever the mission tells you to (typically a PR comment alongside the action). Keep it terse.
31
27
  5. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
32
28
 
33
29
  ## Output contract (MANDATORY, exactly once, at the end)
@@ -44,12 +40,13 @@ End your response with a single fenced block using the `kody-mission-next-state`
44
40
  ```
45
41
  ````
46
42
 
47
- If you fail to emit this block, or the JSON is invalid, the tick fails and the state comment is NOT updated. On the next wake you'll see the same prior state and can retry.
43
+ If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
48
44
 
49
45
  ## Rules
50
46
 
51
47
  - Never edit, create, or delete files in the working tree.
52
48
  - Never commit or push.
53
- - Only shell calls allowed: `gh` (for workflows, runs, issues, PRs, API). Everything must go through it.
54
- - Keep each tick focused: do one thing per wake. The cron will call you again.
55
- - If the state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
49
+ - Only shell calls allowed: `gh`. Everything must go through it.
50
+ - Keep each tick focused: do one action per candidate per wake. The cron will call you again.
51
+ - If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
52
+ - Honour the mission body's `## Restrictions` over any inferred shortcut.
File without changes
File without changes
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.3.35",
3
+ "version": "0.3.39",
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",
@@ -12,18 +12,6 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
- "scripts": {
16
- "kody": "tsx bin/kody.ts",
17
- "build": "tsup && node scripts/copy-assets.cjs",
18
- "test": "vitest run tests/unit tests/int --no-coverage",
19
- "test:e2e": "vitest run tests/e2e --no-coverage",
20
- "test:all": "vitest run tests --no-coverage",
21
- "typecheck": "tsc --noEmit",
22
- "lint": "biome check",
23
- "lint:fix": "biome check --write",
24
- "format": "biome format --write",
25
- "prepublishOnly": "pnpm build"
26
- },
27
15
  "dependencies": {
28
16
  "@anthropic-ai/claude-agent-sdk": "0.2.119"
29
17
  },
@@ -43,5 +31,16 @@
43
31
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
44
32
  },
45
33
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
46
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
47
- }
34
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
35
+ "scripts": {
36
+ "kody": "tsx bin/kody.ts",
37
+ "build": "tsup && node scripts/copy-assets.cjs",
38
+ "test": "vitest run tests/unit tests/int --no-coverage",
39
+ "test:e2e": "vitest run tests/e2e --no-coverage",
40
+ "test:all": "vitest run tests --no-coverage",
41
+ "typecheck": "tsc --noEmit",
42
+ "lint": "biome check",
43
+ "lint:fix": "biome check --write",
44
+ "format": "biome format --write"
45
+ }
46
+ }