@kody-ade/kody-engine 0.3.35 → 0.3.38

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.38",
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,168 @@ 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/missionGist.ts
4062
+ function gistDescription(owner, repo, slug) {
4063
+ return `kody-mission:${owner}/${repo}:${slug}`;
4064
+ }
4065
+ function listGists(cwd) {
4066
+ let raw = "";
4067
+ try {
4068
+ raw = gh2(["api", "--paginate", "/gists?per_page=100"], { cwd });
4069
+ } catch {
4070
+ return [];
4071
+ }
4072
+ let parsed;
4073
+ try {
4074
+ parsed = JSON.parse(raw);
4075
+ } catch {
4076
+ return [];
4077
+ }
4078
+ if (!Array.isArray(parsed)) return [];
4079
+ return parsed.filter((g) => typeof g.id === "string").map((g) => ({
4080
+ id: g.id,
4081
+ description: typeof g.description === "string" ? g.description : null,
4082
+ files: g.files ?? {}
4083
+ }));
4084
+ }
4085
+ function getGist(gistId, cwd) {
4086
+ let raw = "";
4087
+ try {
4088
+ raw = gh2(["api", `/gists/${gistId}`], { cwd });
4089
+ } catch {
4090
+ return null;
4091
+ }
4092
+ let parsed;
4093
+ try {
4094
+ parsed = JSON.parse(raw);
4095
+ } catch {
4096
+ return null;
4097
+ }
4098
+ if (!parsed || typeof parsed !== "object") return null;
4099
+ const o = parsed;
4100
+ if (typeof o.id !== "string") return null;
4101
+ return {
4102
+ id: o.id,
4103
+ description: typeof o.description === "string" ? o.description : null,
4104
+ files: o.files ?? {}
4105
+ };
4106
+ }
4107
+ function findGistByDescription(description, cwd) {
4108
+ const all = listGists(cwd);
4109
+ return all.find((g) => g.description === description) ?? null;
4110
+ }
4111
+ function readEnvelope(gist) {
4112
+ const file = gist.files["state.json"];
4113
+ if (!file?.content) return null;
4114
+ let parsed;
4115
+ try {
4116
+ parsed = JSON.parse(file.content);
4117
+ } catch {
4118
+ const fallback = parseStateCommentBody("kody-mission-state", file.content);
4119
+ return fallback ?? null;
4120
+ }
4121
+ return isStateEnvelope(parsed) ? parsed : null;
4122
+ }
4123
+ function findMissionGist(owner, repo, slug, cwd) {
4124
+ const desc = gistDescription(owner, repo, slug);
4125
+ const gist = findGistByDescription(desc, cwd);
4126
+ if (!gist) return null;
4127
+ const full = getGist(gist.id, cwd) ?? gist;
4128
+ const envelope = readEnvelope(full);
4129
+ if (!envelope) return null;
4130
+ return { gistId: full.id, state: envelope };
4131
+ }
4132
+ function createMissionGist(owner, repo, slug, cursor = "seed", cwd) {
4133
+ const description = gistDescription(owner, repo, slug);
4134
+ const initial = initialStateEnvelope(cursor);
4135
+ const payload = {
4136
+ description,
4137
+ public: false,
4138
+ files: {
4139
+ "state.json": { content: JSON.stringify(initial, null, 2) + "\n" }
4140
+ }
4141
+ };
4142
+ const raw = gh2(["api", "--method", "POST", "/gists", "--input", "-"], {
4143
+ cwd,
4144
+ input: JSON.stringify(payload)
4145
+ });
4146
+ let parsed;
4147
+ try {
4148
+ parsed = JSON.parse(raw);
4149
+ } catch {
4150
+ throw new Error(`createMissionGist: gh did not return JSON: ${raw.slice(0, 200)}`);
4151
+ }
4152
+ if (!parsed || typeof parsed !== "object" || typeof parsed.id !== "string") {
4153
+ throw new Error("createMissionGist: gist creation response missing id");
4154
+ }
4155
+ return { gistId: parsed.id, state: initial };
4156
+ }
4157
+ function writeMissionGist(gistId, next, cwd) {
4158
+ const payload = {
4159
+ files: {
4160
+ "state.json": { content: JSON.stringify(next, null, 2) + "\n" }
4161
+ }
4162
+ };
4163
+ gh2(["api", "--method", "PATCH", `/gists/${gistId}`, "--input", "-"], {
4164
+ cwd,
4165
+ input: JSON.stringify(payload)
4166
+ });
4167
+ }
4168
+
4169
+ // src/scripts/loadMissionFromFile.ts
4170
+ var loadMissionFromFile = async (ctx, _profile, args) => {
4171
+ const missionsDir = String(args?.missionsDir ?? ".kody/missions");
4172
+ const slugArg = String(args?.slugArg ?? "mission");
4173
+ const slug = String(ctx.args[slugArg] ?? "").trim();
4174
+ if (!slug) {
4175
+ throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
4176
+ }
4177
+ const owner = ctx.config.github.owner;
4178
+ const repo = ctx.config.github.repo;
4179
+ if (!owner || !repo) {
4180
+ throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
4181
+ }
4182
+ const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
4183
+ if (!fs20.existsSync(absPath)) {
4184
+ throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
4185
+ }
4186
+ const raw = fs20.readFileSync(absPath, "utf-8");
4187
+ const { title, body } = parseMissionFile(raw, slug);
4188
+ let loaded = findMissionGist(owner, repo, slug, ctx.cwd);
4189
+ if (!loaded) {
4190
+ loaded = createMissionGist(owner, repo, slug, "seed", ctx.cwd);
4191
+ }
4192
+ ctx.data.missionSlug = slug;
4193
+ ctx.data.missionTitle = title;
4194
+ ctx.data.missionIntent = body;
4195
+ ctx.data.missionGist = loaded;
4196
+ ctx.data.missionStateJson = JSON.stringify(loaded.state, null, 2);
4197
+ };
4198
+ function parseMissionFile(raw, slug) {
4199
+ let stripped = raw;
4200
+ if (stripped.startsWith("---\n")) {
4201
+ const end = stripped.indexOf("\n---\n", 4);
4202
+ if (end !== -1) {
4203
+ stripped = stripped.slice(end + 5);
4204
+ }
4205
+ }
4206
+ const trimmed = stripped.trim();
4207
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
4208
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
4209
+ if (h1) {
4210
+ const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
4211
+ return { title: h1[1].trim(), body: rest };
4212
+ }
4213
+ return { title: humanizeSlug(slug), body: trimmed };
4214
+ }
4215
+ function humanizeSlug(slug) {
4216
+ return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
4217
+ }
4218
+
3980
4219
  // src/scripts/loadPriorArt.ts
3981
4220
  var PER_PR_DIFF_MAX_BYTES = 8e3;
3982
4221
  var TOTAL_MAX_BYTES = 3e4;
@@ -4269,6 +4508,53 @@ function escapeRegex(s) {
4269
4508
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4270
4509
  }
4271
4510
 
4511
+ // src/scripts/parseMissionStateFromAgentResult.ts
4512
+ function isPartialEnvelope2(x) {
4513
+ if (x === null || typeof x !== "object") return false;
4514
+ const o = x;
4515
+ return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
4516
+ }
4517
+ var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
4518
+ const fenceLabel = String(args?.fenceLabel ?? "");
4519
+ if (!fenceLabel) {
4520
+ throw new Error("parseMissionStateFromAgentResult: `with.fenceLabel` is required");
4521
+ }
4522
+ if (!agentResult) {
4523
+ ctx.data.nextStateParseError = "agent did not run";
4524
+ return;
4525
+ }
4526
+ const fenceRegex = new RegExp("```" + escapeRegex2(fenceLabel) + "\\s*\\n([\\s\\S]*?)\\n```", "m");
4527
+ const match = fenceRegex.exec(agentResult.finalText);
4528
+ if (!match) {
4529
+ ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
4530
+ return;
4531
+ }
4532
+ let parsed;
4533
+ try {
4534
+ parsed = JSON.parse(match[1].trim());
4535
+ } catch (err) {
4536
+ ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
4537
+ return;
4538
+ }
4539
+ if (!isPartialEnvelope2(parsed)) {
4540
+ ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
4541
+ return;
4542
+ }
4543
+ const loaded = ctx.data.missionGist;
4544
+ const prevRev = loaded?.state.rev ?? 0;
4545
+ const next = {
4546
+ version: 1,
4547
+ rev: prevRev + 1,
4548
+ cursor: parsed.cursor,
4549
+ data: parsed.data,
4550
+ done: parsed.done
4551
+ };
4552
+ ctx.data.nextMissionState = next;
4553
+ };
4554
+ function escapeRegex2(s) {
4555
+ return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4556
+ }
4557
+
4272
4558
  // src/scripts/persistArtifacts.ts
4273
4559
  var persistArtifacts = async (ctx, profile) => {
4274
4560
  if (profile.outputArtifacts.length === 0) return;
@@ -4334,16 +4620,16 @@ var postClassification = async (ctx) => {
4334
4620
  }
4335
4621
  if (!classification) {
4336
4622
  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);
4623
+ tryAuditComment(
4624
+ issueNumber,
4625
+ "\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
4626
+ ctx.cwd
4627
+ );
4338
4628
  ctx.output.exitCode = 1;
4339
4629
  ctx.output.reason = "classify: no decision";
4340
4630
  return;
4341
4631
  }
4342
- tryAuditComment(
4343
- issueNumber,
4344
- `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
4345
- ctx.cwd
4346
- );
4632
+ tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
4347
4633
  try {
4348
4634
  execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4349
4635
  cwd: ctx.cwd,
@@ -4633,7 +4919,9 @@ var requirePlanDeviations = async (ctx, profile) => {
4633
4919
  ctx.data.planDeviationCount = bullets.length;
4634
4920
  };
4635
4921
  function isNoneSentinel(block) {
4636
- const stripped = block.split("\n").map((l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()).filter((l) => l.length > 0);
4922
+ const stripped = block.split("\n").map(
4923
+ (l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()
4924
+ ).filter((l) => l.length > 0);
4637
4925
  if (stripped.length !== 1) return false;
4638
4926
  return stripped[0] === "none";
4639
4927
  }
@@ -5265,16 +5553,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5265
5553
  };
5266
5554
  function fetchChecks(prNumber, cwd) {
5267
5555
  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
- );
5556
+ const raw = execFileSync20("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
5557
+ encoding: "utf-8",
5558
+ timeout: API_TIMEOUT_MS9,
5559
+ cwd,
5560
+ stdio: ["ignore", "pipe", "pipe"]
5561
+ });
5278
5562
  const parsed = JSON.parse(raw);
5279
5563
  return Array.isArray(parsed) ? parsed : [];
5280
5564
  } catch (err) {
@@ -5353,10 +5637,7 @@ function formatStaleReport(stale, staleDays) {
5353
5637
  if (stale.length === 0) {
5354
5638
  return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
5355
5639
  }
5356
- const lines = [
5357
- `\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
5358
- ""
5359
- ];
5640
+ const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
5360
5641
  for (const pr of stale.slice(0, 50)) {
5361
5642
  lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
5362
5643
  }
@@ -5420,8 +5701,29 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
5420
5701
  }
5421
5702
  };
5422
5703
 
5704
+ // src/scripts/writeMissionGistState.ts
5705
+ var writeMissionGistState = async (ctx, _profile, _agentResult) => {
5706
+ const parseError = ctx.data.nextStateParseError;
5707
+ if (parseError) {
5708
+ process.stderr.write(`[kody] mission state write skipped: ${parseError}
5709
+ `);
5710
+ if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
5711
+ if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
5712
+ return;
5713
+ }
5714
+ const next = ctx.data.nextMissionState;
5715
+ if (!next) {
5716
+ return;
5717
+ }
5718
+ const loaded = ctx.data.missionGist;
5719
+ if (!loaded) {
5720
+ throw new Error("writeMissionGistState: ctx.data.missionGist missing \u2014 preflight must run first");
5721
+ }
5722
+ writeMissionGist(loaded.gistId, next, ctx.cwd);
5723
+ };
5724
+
5423
5725
  // src/scripts/writeRunSummary.ts
5424
- import * as fs19 from "fs";
5726
+ import * as fs21 from "fs";
5425
5727
  var writeRunSummary = async (ctx, profile) => {
5426
5728
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
5427
5729
  if (!summaryPath) return;
@@ -5443,7 +5745,7 @@ var writeRunSummary = async (ctx, profile) => {
5443
5745
  if (reason) lines.push(`- **Reason:** ${reason}`);
5444
5746
  lines.push("");
5445
5747
  try {
5446
- fs19.appendFileSync(summaryPath, `${lines.join("\n")}
5748
+ fs21.appendFileSync(summaryPath, `${lines.join("\n")}
5447
5749
  `);
5448
5750
  } catch {
5449
5751
  }
@@ -5462,6 +5764,7 @@ var preflightScripts = {
5462
5764
  loadTaskState,
5463
5765
  loadIssueContext,
5464
5766
  loadIssueStateComment,
5767
+ loadMissionFromFile,
5465
5768
  loadConventions,
5466
5769
  loadCoverageRules,
5467
5770
  loadPriorArt,
@@ -5476,12 +5779,15 @@ var preflightScripts = {
5476
5779
  skipAgent,
5477
5780
  classifyByLabel,
5478
5781
  diagMcp,
5479
- dispatchMissionTicks
5782
+ dispatchMissionTicks,
5783
+ dispatchMissionFileTicks
5480
5784
  };
5481
5785
  var postflightScripts = {
5482
5786
  parseAgentResult: parseAgentResult2,
5483
5787
  parseIssueStateFromAgentResult,
5788
+ parseMissionStateFromAgentResult,
5484
5789
  writeIssueStateComment,
5790
+ writeMissionGistState,
5485
5791
  requireFeedbackActions,
5486
5792
  requirePlanDeviations,
5487
5793
  verify,
@@ -5616,9 +5922,9 @@ async function runExecutable(profileName, input) {
5616
5922
  data: {},
5617
5923
  output: { exitCode: 0 }
5618
5924
  };
5619
- const ndjsonDir = path17.join(input.cwd, ".kody");
5925
+ const ndjsonDir = path19.join(input.cwd, ".kody");
5620
5926
  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);
5927
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path19.isAbsolute(p) ? p : path19.resolve(profile.dir, p)).filter((p) => p.length > 0);
5622
5928
  const syntheticPath = ctx.data.syntheticPluginPath;
5623
5929
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
5624
5930
  return runAgent({
@@ -5694,17 +6000,17 @@ async function runExecutable(profileName, input) {
5694
6000
  }
5695
6001
  }
5696
6002
  function resolveProfilePath(profileName) {
5697
- const here = path17.dirname(new URL(import.meta.url).pathname);
6003
+ const here = path19.dirname(new URL(import.meta.url).pathname);
5698
6004
  const candidates = [
5699
- path17.join(here, "executables", profileName, "profile.json"),
6005
+ path19.join(here, "executables", profileName, "profile.json"),
5700
6006
  // same-dir sibling (dev)
5701
- path17.join(here, "..", "executables", profileName, "profile.json"),
6007
+ path19.join(here, "..", "executables", profileName, "profile.json"),
5702
6008
  // up one (prod: dist/bin → dist/executables)
5703
- path17.join(here, "..", "src", "executables", profileName, "profile.json")
6009
+ path19.join(here, "..", "src", "executables", profileName, "profile.json")
5704
6010
  // fallback
5705
6011
  ];
5706
6012
  for (const c of candidates) {
5707
- if (fs20.existsSync(c)) return c;
6013
+ if (fs22.existsSync(c)) return c;
5708
6014
  }
5709
6015
  return candidates[0];
5710
6016
  }
@@ -5797,8 +6103,8 @@ function finish(out) {
5797
6103
  var SHELL_TIMEOUT_MS = 3e5;
5798
6104
  function runShellEntry(entry, ctx, profile) {
5799
6105
  const shellName = entry.shell;
5800
- const shellPath = path17.join(profile.dir, shellName);
5801
- if (!fs20.existsSync(shellPath)) {
6106
+ const shellPath = path19.join(profile.dir, shellName);
6107
+ if (!fs22.existsSync(shellPath)) {
5802
6108
  ctx.skipAgent = true;
5803
6109
  ctx.output.exitCode = 99;
5804
6110
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -5948,9 +6254,9 @@ function resolveAuthToken(env = process.env) {
5948
6254
  return token;
5949
6255
  }
5950
6256
  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";
6257
+ if (fs23.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6258
+ if (fs23.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
6259
+ if (fs23.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
5954
6260
  return "npm";
5955
6261
  }
5956
6262
  function shellOut(cmd, args, cwd, stream = true) {
@@ -6030,11 +6336,11 @@ function configureGitIdentity(cwd) {
6030
6336
  }
6031
6337
  function postFailureTail(issueNumber, cwd, reason) {
6032
6338
  if (!issueNumber) return;
6033
- const logPath = path18.join(cwd, ".kody", "last-run.jsonl");
6339
+ const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
6034
6340
  let tail = "";
6035
6341
  try {
6036
- if (fs21.existsSync(logPath)) {
6037
- const content = fs21.readFileSync(logPath, "utf-8");
6342
+ if (fs23.existsSync(logPath)) {
6343
+ const content = fs23.readFileSync(logPath, "utf-8");
6038
6344
  tail = content.slice(-3e3);
6039
6345
  }
6040
6346
  } catch {
@@ -6059,7 +6365,7 @@ async function runCi(argv) {
6059
6365
  return 0;
6060
6366
  }
6061
6367
  const args = parseCiArgs(argv);
6062
- const cwd = args.cwd ? path18.resolve(args.cwd) : process.cwd();
6368
+ const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
6063
6369
  let earlyConfig;
6064
6370
  try {
6065
6371
  earlyConfig = loadConfig(cwd);
@@ -6197,9 +6503,9 @@ function parseChatArgs(argv, env = process.env) {
6197
6503
  return result;
6198
6504
  }
6199
6505
  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)));
6506
+ const sessionFile = path21.relative(cwd, sessionFilePath(cwd, sessionId));
6507
+ const eventsFile = path21.relative(cwd, eventsFilePath(cwd, sessionId));
6508
+ const paths = [sessionFile, eventsFile].filter((p) => fs24.existsSync(path21.join(cwd, p)));
6203
6509
  if (paths.length === 0) return;
6204
6510
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
6205
6511
  try {
@@ -6237,7 +6543,7 @@ async function runChat(argv) {
6237
6543
  ${CHAT_HELP}`);
6238
6544
  return 64;
6239
6545
  }
6240
- const cwd = args.cwd ? path19.resolve(args.cwd) : process.cwd();
6546
+ const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
6241
6547
  const sessionId = args.sessionId;
6242
6548
  const unpackedSecrets = unpackAllSecrets();
6243
6549
  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": "writeMissionGistState"
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.38",
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
+ }