@kody-ade/kody-engine 0.3.34 → 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.34",
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";
@@ -501,11 +501,39 @@ function seedInitialMessage(file, message) {
501
501
 
502
502
  // src/chat/loop.ts
503
503
  var CHAT_SYSTEM_PROMPT = [
504
- "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the user's",
505
- "latest message using the full conversation below as context. Keep replies focused,",
506
- "technical when appropriate, and formatted in Markdown. Use the available tools to",
507
- "read repository code or execute small checks when it helps you answer \u2014 otherwise",
508
- "reply directly. Do not invent file paths, commit SHAs, or command output."
504
+ "You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
505
+ "user's latest message using the full conversation below as context. Keep replies",
506
+ "focused, technical when appropriate, and formatted in Markdown.",
507
+ "",
508
+ "# Your environment and capabilities",
509
+ "You run inside a GitHub Actions job with a full clone of the user's repository",
510
+ "checked out at the current working directory. You have real tools \u2014 use them",
511
+ "before claiming you cannot do something. Never tell the user you lack repo,",
512
+ "filesystem, or GitHub access; you have all three.",
513
+ "",
514
+ "Tools you can call:",
515
+ "- Read, Edit, Write \u2014 full read/write access to every file in the repo (permission",
516
+ " mode is acceptEdits, so writes do not require confirmation).",
517
+ "- Glob, Grep \u2014 search the repo by filename pattern or content.",
518
+ "- Bash \u2014 run any shell command in the repo. The runner has:",
519
+ " - `git` (the repo is a real git checkout \u2014 `git log`, `git diff`,",
520
+ " `git show`, `git blame`, `git branch`, etc. all work).",
521
+ " - `gh` authenticated against this repository's GitHub via the Actions",
522
+ " `GITHUB_TOKEN` (read issues, PRs, workflows, runs, comments; query the API",
523
+ " with `gh api`).",
524
+ " - the repo's package manager and test/build/lint tooling (npm/pnpm/yarn,",
525
+ " pytest, go test, cargo, etc., whatever the project uses).",
526
+ " - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
527
+ "",
528
+ "# How to answer",
529
+ "If the user asks about repo content, code, history, issues, PRs, CI status, or",
530
+ "anything else knowable from the checkout or GitHub API, INVESTIGATE FIRST with",
531
+ "the tools above and answer from what you find. Do not say 'I don't have access'",
532
+ "\u2014 if you have not yet tried, try. Only fall back to a limitation statement after",
533
+ "a tool actually fails, and in that case quote the failing command and its error.",
534
+ "",
535
+ "Do not invent file paths, commit SHAs, line numbers, or command output. If you",
536
+ "cite something concrete, you must have just read or run it in this session."
509
537
  ].join("\n");
510
538
  async function runChatTurn(opts) {
511
539
  const turns = readSession(opts.sessionFile);
@@ -578,8 +606,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
578
606
 
579
607
  // src/kody-cli.ts
580
608
  import { execFileSync as execFileSync22 } from "child_process";
581
- import * as fs21 from "fs";
582
- import * as path18 from "path";
609
+ import * as fs23 from "fs";
610
+ import * as path20 from "path";
583
611
 
584
612
  // src/dispatch.ts
585
613
  import * as fs6 from "fs";
@@ -602,30 +630,51 @@ function getExecutablesRoot() {
602
630
  }
603
631
  return candidates[0];
604
632
  }
605
- function listExecutables(root = getExecutablesRoot()) {
606
- if (!fs5.existsSync(root)) return [];
607
- 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();
608
642
  const out = [];
609
- for (const ent of entries) {
610
- if (!ent.isDirectory()) continue;
611
- const profilePath = path5.join(root, ent.name, "profile.json");
612
- if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
613
- 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
+ }
614
654
  }
615
655
  }
616
656
  return out.sort((a, b) => a.name.localeCompare(b.name));
617
657
  }
618
- function hasExecutable(name, root = getExecutablesRoot()) {
619
- if (!isSafeName(name)) return false;
620
- const profilePath = path5.join(root, name, "profile.json");
621
- 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;
622
671
  }
623
672
  function isSafeName(name) {
624
673
  return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
625
674
  }
626
- function getProfileInputs(name, root = getExecutablesRoot()) {
627
- if (!hasExecutable(name, root)) return null;
628
- 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;
629
678
  try {
630
679
  const raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
631
680
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
@@ -806,8 +855,8 @@ function coerceBare(spec, value) {
806
855
 
807
856
  // src/executor.ts
808
857
  import { spawnSync } from "child_process";
809
- import * as fs20 from "fs";
810
- import * as path17 from "path";
858
+ import * as fs22 from "fs";
859
+ import * as path19 from "path";
811
860
 
812
861
  // src/litellm.ts
813
862
  import { execFileSync, spawn } from "child_process";
@@ -954,10 +1003,7 @@ function loadProfile(profilePath) {
954
1003
  throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
955
1004
  }
956
1005
  if (typeof r.role !== "string" || !VALID_ROLES.has(r.role)) {
957
- throw new ProfileError(
958
- profilePath,
959
- `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`
960
- );
1006
+ throw new ProfileError(profilePath, `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`);
961
1007
  }
962
1008
  const role = r.role;
963
1009
  let phase;
@@ -1148,7 +1194,10 @@ function parseScriptList(p, key, raw) {
1148
1194
  const out = [];
1149
1195
  for (const [i, item] of raw.entries()) {
1150
1196
  if (!item || typeof item !== "object") {
1151
- 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
+ );
1152
1201
  }
1153
1202
  const r = item;
1154
1203
  const hasScript = typeof r.script === "string" && r.script.length > 0;
@@ -1157,7 +1206,10 @@ function parseScriptList(p, key, raw) {
1157
1206
  throw new ProfileError(p, `scripts.${key}[${i}] cannot set both "script" and "shell" \u2014 pick one`);
1158
1207
  }
1159
1208
  if (!hasScript && !hasShell) {
1160
- 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
+ );
1161
1213
  }
1162
1214
  const entry = {};
1163
1215
  if (hasScript) entry.script = r.script;
@@ -2065,11 +2117,11 @@ var diagMcp = async (_ctx) => {
2065
2117
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
2066
2118
  `);
2067
2119
  try {
2068
- const v = execFileSync6(
2069
- "npx",
2070
- ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"],
2071
- { stdio: "pipe", timeout: 6e4, encoding: "utf8" }
2072
- ).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();
2073
2125
  process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
2074
2126
  `);
2075
2127
  } catch (e) {
@@ -2245,7 +2297,9 @@ function walkApiRoutes(dir, prefix, cwd, out) {
2245
2297
  if (routeFile) {
2246
2298
  try {
2247
2299
  const content = fs14.readFileSync(path13.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
2248
- 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
+ );
2249
2303
  if (methods.length > 0) {
2250
2304
  out.push({
2251
2305
  path: prefix,
@@ -2620,6 +2674,64 @@ function parsePr(url) {
2620
2674
  return Number.isFinite(n) ? n : null;
2621
2675
  }
2622
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
+
2623
2735
  // src/issue.ts
2624
2736
  import { execFileSync as execFileSync8 } from "child_process";
2625
2737
  var API_TIMEOUT_MS4 = 3e4;
@@ -2808,10 +2920,9 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
2808
2920
  function listIssuesByLabel(label, cwd) {
2809
2921
  let raw = "";
2810
2922
  try {
2811
- raw = gh2(
2812
- ["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"],
2813
- { cwd }
2814
- );
2923
+ raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
2924
+ cwd
2925
+ });
2815
2926
  } catch {
2816
2927
  return [];
2817
2928
  }
@@ -3056,10 +3167,7 @@ function ensureLabels(cwd) {
3056
3167
  }
3057
3168
  function getIssueLabels(issueNumber, cwd) {
3058
3169
  try {
3059
- const output = gh2(
3060
- ["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"],
3061
- { cwd }
3062
- );
3170
+ const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
3063
3171
  return output.split("\n").filter(Boolean);
3064
3172
  } catch {
3065
3173
  return [];
@@ -3083,10 +3191,8 @@ function createLabelInRepo(spec, cwd) {
3083
3191
  function setKodyLabel(issueNumber, spec, cwd) {
3084
3192
  const target = spec.label;
3085
3193
  if (!target.startsWith(KODY_NAMESPACE)) {
3086
- process.stderr.write(
3087
- `[kody] setKodyLabel: refusing to set non-kody label "${target}"
3088
- `
3089
- );
3194
+ process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
3195
+ `);
3090
3196
  return;
3091
3197
  }
3092
3198
  const targetGroup = groupOf(target);
@@ -3112,10 +3218,8 @@ function setKodyLabel(issueNumber, spec, cwd) {
3112
3218
  return;
3113
3219
  }
3114
3220
  }
3115
- process.stderr.write(
3116
- `[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
3117
- `
3118
- );
3221
+ process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
3222
+ `);
3119
3223
  }
3120
3224
  }
3121
3225
  function looksLikeMissingLabel(err) {
@@ -3284,7 +3388,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3284
3388
 
3285
3389
  // src/gha.ts
3286
3390
  import { execFileSync as execFileSync11 } from "child_process";
3287
- import * as fs16 from "fs";
3391
+ import * as fs17 from "fs";
3288
3392
  function getRunUrl() {
3289
3393
  const server = process.env.GITHUB_SERVER_URL;
3290
3394
  const repo = process.env.GITHUB_REPOSITORY;
@@ -3295,10 +3399,10 @@ function getRunUrl() {
3295
3399
  function reactToTriggerComment(cwd) {
3296
3400
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
3297
3401
  const eventPath = process.env.GITHUB_EVENT_PATH;
3298
- if (!eventPath || !fs16.existsSync(eventPath)) return;
3402
+ if (!eventPath || !fs17.existsSync(eventPath)) return;
3299
3403
  let event = null;
3300
3404
  try {
3301
- event = JSON.parse(fs16.readFileSync(eventPath, "utf-8"));
3405
+ event = JSON.parse(fs17.readFileSync(eventPath, "utf-8"));
3302
3406
  } catch {
3303
3407
  return;
3304
3408
  }
@@ -3538,22 +3642,22 @@ function tryPostPr2(prNumber, body, cwd) {
3538
3642
 
3539
3643
  // src/scripts/initFlow.ts
3540
3644
  import { execFileSync as execFileSync13 } from "child_process";
3541
- import * as fs18 from "fs";
3542
- import * as path16 from "path";
3645
+ import * as fs19 from "fs";
3646
+ import * as path17 from "path";
3543
3647
 
3544
3648
  // src/scripts/loadQaGuide.ts
3545
- import * as fs17 from "fs";
3546
- import * as path15 from "path";
3649
+ import * as fs18 from "fs";
3650
+ import * as path16 from "path";
3547
3651
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
3548
3652
  var loadQaGuide = async (ctx) => {
3549
- const full = path15.join(ctx.cwd, QA_GUIDE_REL_PATH);
3550
- if (!fs17.existsSync(full)) {
3653
+ const full = path16.join(ctx.cwd, QA_GUIDE_REL_PATH);
3654
+ if (!fs18.existsSync(full)) {
3551
3655
  ctx.data.qaGuide = "";
3552
3656
  ctx.data.qaGuidePath = "";
3553
3657
  return;
3554
3658
  }
3555
3659
  try {
3556
- ctx.data.qaGuide = fs17.readFileSync(full, "utf-8");
3660
+ ctx.data.qaGuide = fs18.readFileSync(full, "utf-8");
3557
3661
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
3558
3662
  } catch {
3559
3663
  ctx.data.qaGuide = "";
@@ -3563,9 +3667,9 @@ var loadQaGuide = async (ctx) => {
3563
3667
 
3564
3668
  // src/scripts/initFlow.ts
3565
3669
  function detectPackageManager(cwd) {
3566
- if (fs18.existsSync(path16.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3567
- if (fs18.existsSync(path16.join(cwd, "yarn.lock"))) return "yarn";
3568
- 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";
3569
3673
  return "npm";
3570
3674
  }
3571
3675
  function qualityCommandsFor(pm) {
@@ -3687,33 +3791,33 @@ function performInit(cwd, force) {
3687
3791
  const pm = detectPackageManager(cwd);
3688
3792
  const ownerRepo = detectOwnerRepo(cwd);
3689
3793
  const defaultBranch = defaultBranchFromGit(cwd);
3690
- const configPath = path16.join(cwd, "kody.config.json");
3691
- if (fs18.existsSync(configPath) && !force) {
3794
+ const configPath = path17.join(cwd, "kody.config.json");
3795
+ if (fs19.existsSync(configPath) && !force) {
3692
3796
  skipped.push("kody.config.json");
3693
3797
  } else {
3694
3798
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
3695
- fs18.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
3799
+ fs19.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
3696
3800
  `);
3697
3801
  wrote.push("kody.config.json");
3698
3802
  }
3699
- const workflowDir = path16.join(cwd, ".github", "workflows");
3700
- const workflowPath = path16.join(workflowDir, "kody.yml");
3701
- 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) {
3702
3806
  skipped.push(".github/workflows/kody.yml");
3703
3807
  } else {
3704
- fs18.mkdirSync(workflowDir, { recursive: true });
3705
- fs18.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
3808
+ fs19.mkdirSync(workflowDir, { recursive: true });
3809
+ fs19.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
3706
3810
  wrote.push(".github/workflows/kody.yml");
3707
3811
  }
3708
- 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"));
3709
3813
  if (hasUi) {
3710
- const qaGuidePath = path16.join(cwd, QA_GUIDE_REL_PATH);
3711
- if (fs18.existsSync(qaGuidePath) && !force) {
3814
+ const qaGuidePath = path17.join(cwd, QA_GUIDE_REL_PATH);
3815
+ if (fs19.existsSync(qaGuidePath) && !force) {
3712
3816
  skipped.push(QA_GUIDE_REL_PATH);
3713
3817
  } else {
3714
- fs18.mkdirSync(path16.dirname(qaGuidePath), { recursive: true });
3818
+ fs19.mkdirSync(path17.dirname(qaGuidePath), { recursive: true });
3715
3819
  const discovery = runQaDiscovery(cwd);
3716
- fs18.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
3820
+ fs19.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
3717
3821
  wrote.push(QA_GUIDE_REL_PATH);
3718
3822
  }
3719
3823
  }
@@ -3725,12 +3829,12 @@ function performInit(cwd, force) {
3725
3829
  continue;
3726
3830
  }
3727
3831
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
3728
- const target = path16.join(workflowDir, `kody-${exe.name}.yml`);
3729
- if (fs18.existsSync(target) && !force) {
3832
+ const target = path17.join(workflowDir, `kody-${exe.name}.yml`);
3833
+ if (fs19.existsSync(target) && !force) {
3730
3834
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
3731
3835
  continue;
3732
3836
  }
3733
- fs18.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
3837
+ fs19.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
3734
3838
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
3735
3839
  }
3736
3840
  let labels;
@@ -3788,10 +3892,8 @@ var initFlow = async (ctx) => {
3788
3892
  `);
3789
3893
  }
3790
3894
  if (labels.failed.length > 0) {
3791
- process.stdout.write(
3792
- ` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
3793
- `
3794
- );
3895
+ process.stdout.write(` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
3896
+ `);
3795
3897
  }
3796
3898
  }
3797
3899
  process.stdout.write(
@@ -3849,6 +3951,9 @@ function isStateEnvelope(x) {
3849
3951
  const o = x;
3850
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);
3851
3953
  }
3954
+ function initialStateEnvelope(cursor = "seed") {
3955
+ return { version: 1, rev: 0, cursor, data: {}, done: false };
3956
+ }
3852
3957
  function formatStateCommentBody(marker, state) {
3853
3958
  return `<!-- ${marker} -->
3854
3959
 
@@ -3896,10 +4001,10 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
3896
4001
  }
3897
4002
  function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3898
4003
  const body = formatStateCommentBody(marker, state);
3899
- const raw = gh2(
3900
- ["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"],
3901
- { cwd, input: JSON.stringify({ body }) }
3902
- );
4004
+ const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4005
+ cwd,
4006
+ input: JSON.stringify({ body })
4007
+ });
3903
4008
  const parsed = JSON.parse(raw);
3904
4009
  try {
3905
4010
  minimizeComment(parsed.node_id, cwd);
@@ -3909,10 +4014,10 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3909
4014
  }
3910
4015
  function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
3911
4016
  const body = formatStateCommentBody(marker, state);
3912
- gh2(
3913
- ["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"],
3914
- { cwd, input: JSON.stringify({ body }) }
3915
- );
4017
+ gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4018
+ cwd,
4019
+ input: JSON.stringify({ body })
4020
+ });
3916
4021
  try {
3917
4022
  minimizeComment(commentNodeId, cwd);
3918
4023
  } catch {
@@ -3949,6 +4054,168 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
3949
4054
  ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
3950
4055
  };
3951
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
+
3952
4219
  // src/scripts/loadPriorArt.ts
3953
4220
  var PER_PR_DIFF_MAX_BYTES = 8e3;
3954
4221
  var TOTAL_MAX_BYTES = 3e4;
@@ -4241,6 +4508,53 @@ function escapeRegex(s) {
4241
4508
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
4242
4509
  }
4243
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
+
4244
4558
  // src/scripts/persistArtifacts.ts
4245
4559
  var persistArtifacts = async (ctx, profile) => {
4246
4560
  if (profile.outputArtifacts.length === 0) return;
@@ -4306,16 +4620,16 @@ var postClassification = async (ctx) => {
4306
4620
  }
4307
4621
  if (!classification) {
4308
4622
  ctx.data.action = failedAction("classification missing or invalid");
4309
- 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
+ );
4310
4628
  ctx.output.exitCode = 1;
4311
4629
  ctx.output.reason = "classify: no decision";
4312
4630
  return;
4313
4631
  }
4314
- tryAuditComment(
4315
- issueNumber,
4316
- `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
4317
- ctx.cwd
4318
- );
4632
+ tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
4319
4633
  try {
4320
4634
  execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4321
4635
  cwd: ctx.cwd,
@@ -4605,7 +4919,9 @@ var requirePlanDeviations = async (ctx, profile) => {
4605
4919
  ctx.data.planDeviationCount = bullets.length;
4606
4920
  };
4607
4921
  function isNoneSentinel(block) {
4608
- 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);
4609
4925
  if (stripped.length !== 1) return false;
4610
4926
  return stripped[0] === "none";
4611
4927
  }
@@ -5237,16 +5553,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
5237
5553
  };
5238
5554
  function fetchChecks(prNumber, cwd) {
5239
5555
  try {
5240
- const raw = execFileSync20(
5241
- "gh",
5242
- ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"],
5243
- {
5244
- encoding: "utf-8",
5245
- timeout: API_TIMEOUT_MS9,
5246
- cwd,
5247
- stdio: ["ignore", "pipe", "pipe"]
5248
- }
5249
- );
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
+ });
5250
5562
  const parsed = JSON.parse(raw);
5251
5563
  return Array.isArray(parsed) ? parsed : [];
5252
5564
  } catch (err) {
@@ -5325,10 +5637,7 @@ function formatStaleReport(stale, staleDays) {
5325
5637
  if (stale.length === 0) {
5326
5638
  return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
5327
5639
  }
5328
- const lines = [
5329
- `\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
5330
- ""
5331
- ];
5640
+ const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
5332
5641
  for (const pr of stale.slice(0, 50)) {
5333
5642
  lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
5334
5643
  }
@@ -5392,8 +5701,29 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
5392
5701
  }
5393
5702
  };
5394
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
+
5395
5725
  // src/scripts/writeRunSummary.ts
5396
- import * as fs19 from "fs";
5726
+ import * as fs21 from "fs";
5397
5727
  var writeRunSummary = async (ctx, profile) => {
5398
5728
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
5399
5729
  if (!summaryPath) return;
@@ -5415,7 +5745,7 @@ var writeRunSummary = async (ctx, profile) => {
5415
5745
  if (reason) lines.push(`- **Reason:** ${reason}`);
5416
5746
  lines.push("");
5417
5747
  try {
5418
- fs19.appendFileSync(summaryPath, `${lines.join("\n")}
5748
+ fs21.appendFileSync(summaryPath, `${lines.join("\n")}
5419
5749
  `);
5420
5750
  } catch {
5421
5751
  }
@@ -5434,6 +5764,7 @@ var preflightScripts = {
5434
5764
  loadTaskState,
5435
5765
  loadIssueContext,
5436
5766
  loadIssueStateComment,
5767
+ loadMissionFromFile,
5437
5768
  loadConventions,
5438
5769
  loadCoverageRules,
5439
5770
  loadPriorArt,
@@ -5448,12 +5779,15 @@ var preflightScripts = {
5448
5779
  skipAgent,
5449
5780
  classifyByLabel,
5450
5781
  diagMcp,
5451
- dispatchMissionTicks
5782
+ dispatchMissionTicks,
5783
+ dispatchMissionFileTicks
5452
5784
  };
5453
5785
  var postflightScripts = {
5454
5786
  parseAgentResult: parseAgentResult2,
5455
5787
  parseIssueStateFromAgentResult,
5788
+ parseMissionStateFromAgentResult,
5456
5789
  writeIssueStateComment,
5790
+ writeMissionGistState,
5457
5791
  requireFeedbackActions,
5458
5792
  requirePlanDeviations,
5459
5793
  verify,
@@ -5588,9 +5922,9 @@ async function runExecutable(profileName, input) {
5588
5922
  data: {},
5589
5923
  output: { exitCode: 0 }
5590
5924
  };
5591
- const ndjsonDir = path17.join(input.cwd, ".kody");
5925
+ const ndjsonDir = path19.join(input.cwd, ".kody");
5592
5926
  const invokeAgent = async (prompt) => {
5593
- 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);
5594
5928
  const syntheticPath = ctx.data.syntheticPluginPath;
5595
5929
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
5596
5930
  return runAgent({
@@ -5666,17 +6000,17 @@ async function runExecutable(profileName, input) {
5666
6000
  }
5667
6001
  }
5668
6002
  function resolveProfilePath(profileName) {
5669
- const here = path17.dirname(new URL(import.meta.url).pathname);
6003
+ const here = path19.dirname(new URL(import.meta.url).pathname);
5670
6004
  const candidates = [
5671
- path17.join(here, "executables", profileName, "profile.json"),
6005
+ path19.join(here, "executables", profileName, "profile.json"),
5672
6006
  // same-dir sibling (dev)
5673
- path17.join(here, "..", "executables", profileName, "profile.json"),
6007
+ path19.join(here, "..", "executables", profileName, "profile.json"),
5674
6008
  // up one (prod: dist/bin → dist/executables)
5675
- path17.join(here, "..", "src", "executables", profileName, "profile.json")
6009
+ path19.join(here, "..", "src", "executables", profileName, "profile.json")
5676
6010
  // fallback
5677
6011
  ];
5678
6012
  for (const c of candidates) {
5679
- if (fs20.existsSync(c)) return c;
6013
+ if (fs22.existsSync(c)) return c;
5680
6014
  }
5681
6015
  return candidates[0];
5682
6016
  }
@@ -5769,8 +6103,8 @@ function finish(out) {
5769
6103
  var SHELL_TIMEOUT_MS = 3e5;
5770
6104
  function runShellEntry(entry, ctx, profile) {
5771
6105
  const shellName = entry.shell;
5772
- const shellPath = path17.join(profile.dir, shellName);
5773
- if (!fs20.existsSync(shellPath)) {
6106
+ const shellPath = path19.join(profile.dir, shellName);
6107
+ if (!fs22.existsSync(shellPath)) {
5774
6108
  ctx.skipAgent = true;
5775
6109
  ctx.output.exitCode = 99;
5776
6110
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -5920,9 +6254,9 @@ function resolveAuthToken(env = process.env) {
5920
6254
  return token;
5921
6255
  }
5922
6256
  function detectPackageManager2(cwd) {
5923
- if (fs21.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5924
- if (fs21.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
5925
- 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";
5926
6260
  return "npm";
5927
6261
  }
5928
6262
  function shellOut(cmd, args, cwd, stream = true) {
@@ -6002,11 +6336,11 @@ function configureGitIdentity(cwd) {
6002
6336
  }
6003
6337
  function postFailureTail(issueNumber, cwd, reason) {
6004
6338
  if (!issueNumber) return;
6005
- const logPath = path18.join(cwd, ".kody", "last-run.jsonl");
6339
+ const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
6006
6340
  let tail = "";
6007
6341
  try {
6008
- if (fs21.existsSync(logPath)) {
6009
- const content = fs21.readFileSync(logPath, "utf-8");
6342
+ if (fs23.existsSync(logPath)) {
6343
+ const content = fs23.readFileSync(logPath, "utf-8");
6010
6344
  tail = content.slice(-3e3);
6011
6345
  }
6012
6346
  } catch {
@@ -6031,7 +6365,7 @@ async function runCi(argv) {
6031
6365
  return 0;
6032
6366
  }
6033
6367
  const args = parseCiArgs(argv);
6034
- const cwd = args.cwd ? path18.resolve(args.cwd) : process.cwd();
6368
+ const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
6035
6369
  let earlyConfig;
6036
6370
  try {
6037
6371
  earlyConfig = loadConfig(cwd);
@@ -6169,9 +6503,9 @@ function parseChatArgs(argv, env = process.env) {
6169
6503
  return result;
6170
6504
  }
6171
6505
  function commitChatFiles(cwd, sessionId, verbose) {
6172
- const sessionFile = path19.relative(cwd, sessionFilePath(cwd, sessionId));
6173
- const eventsFile = path19.relative(cwd, eventsFilePath(cwd, sessionId));
6174
- 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)));
6175
6509
  if (paths.length === 0) return;
6176
6510
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
6177
6511
  try {
@@ -6209,7 +6543,7 @@ async function runChat(argv) {
6209
6543
  ${CHAT_HELP}`);
6210
6544
  return 64;
6211
6545
  }
6212
- const cwd = args.cwd ? path19.resolve(args.cwd) : process.cwd();
6546
+ const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
6213
6547
  const sessionId = args.sessionId;
6214
6548
  const unpackedSecrets = unpackAllSecrets();
6215
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.34",
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
+ }