@kody-ade/kody-engine 0.3.58 → 0.3.61

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.58",
6
+ version: "0.3.61",
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",
@@ -28,6 +28,7 @@ var package_default = {
28
28
  prepublishOnly: "pnpm build"
29
29
  },
30
30
  dependencies: {
31
+ "@actions/cache": "^6.0.0",
31
32
  "@anthropic-ai/claude-agent-sdk": "0.2.119"
32
33
  },
33
34
  devDependencies: {
@@ -51,8 +52,8 @@ var package_default = {
51
52
 
52
53
  // src/chat-cli.ts
53
54
  import { execFileSync as execFileSync26 } from "child_process";
54
- import * as fs26 from "fs";
55
- import * as path23 from "path";
55
+ import * as fs27 from "fs";
56
+ import * as path24 from "path";
56
57
 
57
58
  // src/chat/events.ts
58
59
  import * as fs from "fs";
@@ -186,9 +187,23 @@ function loadConfig(projectDir = process.cwd()) {
186
187
  defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
187
188
  aliases: mergeAliases(raw.aliases),
188
189
  classify: parseClassifyConfig(raw.classify),
189
- release: parseReleaseConfig(raw.release)
190
+ release: parseReleaseConfig(raw.release),
191
+ missions: parseMissionsConfig(raw.missions)
190
192
  };
191
193
  }
194
+ function parseMissionsConfig(raw) {
195
+ if (!raw || typeof raw !== "object") return void 0;
196
+ const r = raw;
197
+ const out = {};
198
+ if (r.stateBackend === "contents-api" || r.stateBackend === "local-file") {
199
+ out.stateBackend = r.stateBackend;
200
+ } else if (typeof r.stateBackend === "string") {
201
+ throw new Error(
202
+ `kody.config.json: missions.stateBackend must be "contents-api" or "local-file", got "${r.stateBackend}"`
203
+ );
204
+ }
205
+ return Object.keys(out).length > 0 ? out : void 0;
206
+ }
192
207
  var BUILTIN_ALIASES = {
193
208
  build: "run",
194
209
  orchestrate: "bug",
@@ -525,12 +540,35 @@ var CHAT_SYSTEM_PROMPT = [
525
540
  " pytest, go test, cargo, etc., whatever the project uses).",
526
541
  " - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
527
542
  "",
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.",
543
+ "# Investigate before you answer (HARD RULE)",
544
+ "Do not answer from assumptions, training memory, or what the code 'probably'",
545
+ "does. Before replying to any question about this repo \u2014 its code, behavior,",
546
+ "config, history, issues, PRs, CI, or dependencies \u2014 you MUST first ground the",
547
+ "answer in concrete evidence collected in THIS session.",
548
+ "",
549
+ "Required pre-reply protocol for every non-trivial question:",
550
+ "1. Locate the relevant code with Glob/Grep. Don't guess paths.",
551
+ "2. Read the actual files end-to-end (or the relevant ranges). Read more than",
552
+ " you think you need \u2014 adjacent files, callers, tests, types.",
553
+ "3. If behavior depends on runtime state (CI, PRs, issues, git history), run",
554
+ " the matching `gh` / `git` / shell command and look at the real output.",
555
+ "4. Only after steps 1\u20133 do you compose the reply.",
556
+ "",
557
+ "Every factual claim about this repo in your reply must be backed by something",
558
+ "you actually read or executed in this session. Cite the source inline:",
559
+ "`path/to/file.ts:42`, `git show <sha>`, `gh pr view 123`, etc. If you cannot",
560
+ "produce a citation, you have not investigated enough \u2014 go back to step 1.",
561
+ "",
562
+ "Forbidden phrasings unless preceded by an actual tool failure quoted in your",
563
+ "reply: 'I don't have access', 'I can't see', 'it likely', 'it probably',",
564
+ "'typically this would', 'based on common patterns'. These are tells that you",
565
+ "skipped investigation \u2014 replace them with the result of the investigation.",
566
+ "",
567
+ "Speed is not the goal \u2014 correctness grounded in this specific codebase is.",
568
+ "Spend the tool calls. A short answer with three citations beats a long answer",
569
+ "with zero. If a question is genuinely trivial (greeting, clarification,",
570
+ "definition of a generic term unrelated to this repo), you may answer without",
571
+ "tools \u2014 but err on the side of investigating.",
534
572
  "",
535
573
  "Do not invent file paths, commit SHAs, line numbers, or command output. If you",
536
574
  "cite something concrete, you must have just read or run it in this session."
@@ -606,8 +644,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
606
644
 
607
645
  // src/kody-cli.ts
608
646
  import { execFileSync as execFileSync25 } from "child_process";
609
- import * as fs25 from "fs";
610
- import * as path22 from "path";
647
+ import * as fs26 from "fs";
648
+ import * as path23 from "path";
611
649
 
612
650
  // src/dispatch.ts
613
651
  import * as fs6 from "fs";
@@ -960,8 +998,8 @@ function coerceBare(spec, value) {
960
998
 
961
999
  // src/executor.ts
962
1000
  import { spawn as spawn3 } from "child_process";
963
- import * as fs24 from "fs";
964
- import * as path21 from "path";
1001
+ import * as fs25 from "fs";
1002
+ import * as path22 from "path";
965
1003
 
966
1004
  // src/litellm.ts
967
1005
  import { execFileSync, spawn } from "child_process";
@@ -1981,7 +2019,7 @@ function parseAgentResult(finalText) {
1981
2019
  };
1982
2020
  }
1983
2021
  const hasDoneMarker = DONE_RE.test(text);
1984
- const hasCommitMsg = /^[\s>*_#`~\-]*COMMIT_MSG\s*:/im.test(text);
2022
+ const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
1985
2023
  if (!hasDoneMarker && !hasCommitMsg) {
1986
2024
  return {
1987
2025
  done: false,
@@ -1993,7 +2031,7 @@ function parseAgentResult(finalText) {
1993
2031
  failureReason: "no DONE or FAILED marker in agent output"
1994
2032
  };
1995
2033
  }
1996
- const commitMatch = text.match(/^[\s>*_#`~\-]*COMMIT_MSG[\s>*_#`~\-]*\s*:\s*(.+)$/im);
2034
+ const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
1997
2035
  const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
1998
2036
  const feedbackActions = extractBlock(
1999
2037
  text,
@@ -2851,62 +2889,8 @@ function failedAction(reason) {
2851
2889
  }
2852
2890
 
2853
2891
  // src/scripts/dispatchMissionFileTicks.ts
2854
- import * as fs16 from "fs";
2855
- import * as path15 from "path";
2856
- var dispatchMissionFileTicks = async (ctx, _profile, args) => {
2857
- ctx.skipAgent = true;
2858
- const targetExecutable = String(args?.targetExecutable ?? "");
2859
- if (!targetExecutable) {
2860
- throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
2861
- }
2862
- const missionsDir = String(args?.missionsDir ?? ".kody/missions");
2863
- const slugArg = String(args?.slugArg ?? "mission");
2864
- const slugs = listMissionSlugs(path15.join(ctx.cwd, missionsDir));
2865
- ctx.data.missionSlugCount = slugs.length;
2866
- if (slugs.length === 0) {
2867
- process.stdout.write(`[missions] no mission files in ${missionsDir}
2868
- `);
2869
- return;
2870
- }
2871
- process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
2872
- `);
2873
- const results = [];
2874
- for (const slug of slugs) {
2875
- process.stdout.write(`[missions] \u2192 tick ${slug}
2876
- `);
2877
- try {
2878
- const out = await runExecutable(targetExecutable, {
2879
- cliArgs: { [slugArg]: slug },
2880
- cwd: ctx.cwd,
2881
- config: ctx.config,
2882
- verbose: ctx.verbose,
2883
- quiet: ctx.quiet
2884
- });
2885
- results.push({ slug, exitCode: out.exitCode, reason: out.reason });
2886
- if (out.exitCode !== 0) {
2887
- process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
2888
- `);
2889
- }
2890
- } catch (err) {
2891
- const msg = err instanceof Error ? err.message : String(err);
2892
- process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
2893
- `);
2894
- results.push({ slug, exitCode: 99, reason: msg });
2895
- }
2896
- }
2897
- ctx.data.missionTickResults = results;
2898
- ctx.output.exitCode = 0;
2899
- };
2900
- function listMissionSlugs(absDir) {
2901
- if (!fs16.existsSync(absDir)) return [];
2902
- let entries;
2903
- try {
2904
- entries = fs16.readdirSync(absDir, { withFileTypes: true });
2905
- } catch {
2906
- return [];
2907
- }
2908
- 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();
2909
- }
2892
+ import * as fs17 from "fs";
2893
+ import * as path16 from "path";
2910
2894
 
2911
2895
  // src/issue.ts
2912
2896
  import { execFileSync as execFileSync9 } from "child_process";
@@ -3049,6 +3033,411 @@ function postPrReviewComment(prNumber, body, cwd) {
3049
3033
  }
3050
3034
  }
3051
3035
 
3036
+ // src/scripts/issueStateComment.ts
3037
+ function isStateEnvelope(x) {
3038
+ if (x === null || typeof x !== "object") return false;
3039
+ const o = x;
3040
+ 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);
3041
+ }
3042
+ function initialStateEnvelope(cursor = "seed") {
3043
+ return { version: 1, rev: 0, cursor, data: {}, done: false };
3044
+ }
3045
+ function formatStateCommentBody(marker, state) {
3046
+ return `<!-- ${marker} -->
3047
+
3048
+ \`\`\`json
3049
+ ${JSON.stringify(state, null, 2)}
3050
+ \`\`\`
3051
+ `;
3052
+ }
3053
+ function parseStateCommentBody(marker, body) {
3054
+ const markerLine = `<!-- ${marker} -->`;
3055
+ if (!body.trimStart().startsWith(markerLine)) return null;
3056
+ const fenceOpen = body.indexOf("```json");
3057
+ if (fenceOpen === -1) return null;
3058
+ const after = body.slice(fenceOpen + "```json".length);
3059
+ const fenceClose = after.indexOf("```");
3060
+ if (fenceClose === -1) return null;
3061
+ const jsonText = after.slice(0, fenceClose).trim();
3062
+ let parsed;
3063
+ try {
3064
+ parsed = JSON.parse(jsonText);
3065
+ } catch {
3066
+ return null;
3067
+ }
3068
+ return isStateEnvelope(parsed) ? parsed : null;
3069
+ }
3070
+ function listIssueComments(owner, repo, issueNumber, cwd) {
3071
+ const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
3072
+ let parsed;
3073
+ try {
3074
+ parsed = JSON.parse(raw);
3075
+ } catch {
3076
+ return [];
3077
+ }
3078
+ if (!Array.isArray(parsed)) return [];
3079
+ return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
3080
+ }
3081
+ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
3082
+ const comments = listIssueComments(owner, repo, issueNumber, cwd);
3083
+ for (const c of comments) {
3084
+ const state = parseStateCommentBody(marker, c.body);
3085
+ if (!state) continue;
3086
+ return { commentId: c.id, commentNodeId: c.node_id, state };
3087
+ }
3088
+ return null;
3089
+ }
3090
+ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
3091
+ const body = formatStateCommentBody(marker, state);
3092
+ const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
3093
+ cwd,
3094
+ input: JSON.stringify({ body })
3095
+ });
3096
+ const parsed = JSON.parse(raw);
3097
+ try {
3098
+ minimizeComment(parsed.node_id, cwd);
3099
+ } catch {
3100
+ }
3101
+ return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
3102
+ }
3103
+ function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
3104
+ const body = formatStateCommentBody(marker, state);
3105
+ gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
3106
+ cwd,
3107
+ input: JSON.stringify({ body })
3108
+ });
3109
+ try {
3110
+ minimizeComment(commentNodeId, cwd);
3111
+ } catch {
3112
+ }
3113
+ }
3114
+ function minimizeComment(nodeId, cwd) {
3115
+ const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
3116
+ gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
3117
+ }
3118
+
3119
+ // src/scripts/missionState/backend.ts
3120
+ function isStateUnchanged(prev, next) {
3121
+ if (prev.cursor !== next.cursor) return false;
3122
+ if (prev.done !== next.done) return false;
3123
+ return JSON.stringify(prev.data) === JSON.stringify(next.data);
3124
+ }
3125
+ function stateFilePath(missionsDir, slug) {
3126
+ return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
3127
+ }
3128
+ function slugFromStateFilePath(filePath) {
3129
+ const last = filePath.split("/").pop() ?? filePath;
3130
+ return last.replace(/\.state\.json$/i, "");
3131
+ }
3132
+
3133
+ // src/scripts/missionState/contentsApiBackend.ts
3134
+ var ContentsApiBackend = class {
3135
+ name = "contents-api";
3136
+ owner;
3137
+ repo;
3138
+ missionsDir;
3139
+ cwd;
3140
+ constructor(opts) {
3141
+ if (!opts.owner || !opts.repo) {
3142
+ throw new Error("ContentsApiBackend: owner and repo are required");
3143
+ }
3144
+ this.owner = opts.owner;
3145
+ this.repo = opts.repo;
3146
+ this.missionsDir = opts.missionsDir;
3147
+ this.cwd = opts.cwd;
3148
+ }
3149
+ load(slug) {
3150
+ const filePath = stateFilePath(this.missionsDir, slug);
3151
+ let raw = "";
3152
+ try {
3153
+ raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
3154
+ } catch (err) {
3155
+ const msg = err instanceof Error ? err.message : String(err);
3156
+ if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
3157
+ return { path: filePath, handle: null, state: initialStateEnvelope("seed"), created: true };
3158
+ }
3159
+ throw err;
3160
+ }
3161
+ let parsed;
3162
+ try {
3163
+ parsed = JSON.parse(raw);
3164
+ } catch {
3165
+ throw new Error(`ContentsApiBackend: contents API for ${filePath} did not return JSON`);
3166
+ }
3167
+ if (!parsed || typeof parsed !== "object") {
3168
+ throw new Error(`ContentsApiBackend: contents API for ${filePath} returned non-object`);
3169
+ }
3170
+ const o = parsed;
3171
+ if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
3172
+ throw new Error(`ContentsApiBackend: ${filePath} is not a base64 file`);
3173
+ }
3174
+ const decoded = Buffer.from(o.content, "base64").toString("utf-8");
3175
+ let envelope;
3176
+ try {
3177
+ envelope = JSON.parse(decoded);
3178
+ } catch {
3179
+ throw new Error(`ContentsApiBackend: ${filePath} is not valid JSON`);
3180
+ }
3181
+ if (!isStateEnvelope(envelope)) {
3182
+ throw new Error(`ContentsApiBackend: ${filePath} is not a StateEnvelope`);
3183
+ }
3184
+ return { path: filePath, handle: o.sha, state: envelope, created: false };
3185
+ }
3186
+ save(loaded, next) {
3187
+ if (!loaded.created && isStateUnchanged(loaded.state, next)) {
3188
+ return false;
3189
+ }
3190
+ const slug = slugFromStateFilePath(loaded.path);
3191
+ const body = JSON.stringify(next, null, 2) + "\n";
3192
+ const payload = {
3193
+ message: `chore(missions): update state for ${slug} (rev ${next.rev})`,
3194
+ content: Buffer.from(body, "utf-8").toString("base64")
3195
+ };
3196
+ if (typeof loaded.handle === "string") payload.sha = loaded.handle;
3197
+ gh2(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
3198
+ cwd: this.cwd,
3199
+ input: JSON.stringify(payload)
3200
+ });
3201
+ return true;
3202
+ }
3203
+ };
3204
+
3205
+ // src/scripts/missionState/localFileBackend.ts
3206
+ import * as fs16 from "fs";
3207
+ import * as path15 from "path";
3208
+ var LocalFileBackend = class {
3209
+ name = "local-file";
3210
+ cwd;
3211
+ missionsDir;
3212
+ absDir;
3213
+ owner;
3214
+ repo;
3215
+ cache;
3216
+ constructor(opts) {
3217
+ if (!opts.cwd) throw new Error("LocalFileBackend: cwd is required");
3218
+ if (!opts.missionsDir) throw new Error("LocalFileBackend: missionsDir is required");
3219
+ if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
3220
+ this.cwd = opts.cwd;
3221
+ this.missionsDir = opts.missionsDir;
3222
+ this.absDir = path15.join(opts.cwd, opts.missionsDir);
3223
+ this.owner = opts.owner;
3224
+ this.repo = opts.repo;
3225
+ this.cache = opts.cache ?? defaultCacheAdapter();
3226
+ }
3227
+ /**
3228
+ * Restore the mission directory from the most recent Actions cache entry
3229
+ * for this repo. No-op when not running in Actions or when no cache exists.
3230
+ */
3231
+ async hydrate() {
3232
+ if (!this.cache.isAvailable()) {
3233
+ process.stdout.write(`[missions/state] hydrate skipped: actions cache unavailable
3234
+ `);
3235
+ return;
3236
+ }
3237
+ fs16.mkdirSync(this.absDir, { recursive: true });
3238
+ const prefix = this.cacheKeyPrefix();
3239
+ const probeKey = `${prefix}probe-${Date.now()}`;
3240
+ try {
3241
+ const matched = await this.cache.restore([this.absDir], probeKey, [prefix]);
3242
+ if (matched) {
3243
+ process.stdout.write(`[missions/state] hydrate hit: ${matched}
3244
+ `);
3245
+ } else {
3246
+ process.stdout.write(`[missions/state] hydrate miss (cold start)
3247
+ `);
3248
+ }
3249
+ } catch (err) {
3250
+ const msg = err instanceof Error ? err.message : String(err);
3251
+ process.stderr.write(`[missions/state] hydrate failed (continuing): ${msg}
3252
+ `);
3253
+ }
3254
+ }
3255
+ /**
3256
+ * Save the mission directory to the Actions cache under a unique key.
3257
+ * No-op when not running in Actions. Errors are logged, never thrown —
3258
+ * callers run this in a finally block and must not swallow real errors.
3259
+ */
3260
+ async persist() {
3261
+ if (!this.cache.isAvailable()) {
3262
+ process.stdout.write(`[missions/state] persist skipped: actions cache unavailable
3263
+ `);
3264
+ return;
3265
+ }
3266
+ if (!fs16.existsSync(this.absDir)) {
3267
+ return;
3268
+ }
3269
+ const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
3270
+ try {
3271
+ await this.cache.save([this.absDir], key);
3272
+ process.stdout.write(`[missions/state] persist saved: ${key}
3273
+ `);
3274
+ } catch (err) {
3275
+ const msg = err instanceof Error ? err.message : String(err);
3276
+ process.stderr.write(`[missions/state] persist failed (continuing): ${msg}
3277
+ `);
3278
+ }
3279
+ }
3280
+ load(slug) {
3281
+ const relPath = stateFilePath(this.missionsDir, slug);
3282
+ const absPath = path15.join(this.cwd, relPath);
3283
+ if (!fs16.existsSync(absPath)) {
3284
+ return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
3285
+ }
3286
+ const raw = fs16.readFileSync(absPath, "utf-8");
3287
+ let parsed;
3288
+ try {
3289
+ parsed = JSON.parse(raw);
3290
+ } catch (err) {
3291
+ const msg = err instanceof Error ? err.message : String(err);
3292
+ throw new Error(`LocalFileBackend: ${relPath} is not valid JSON: ${msg}`);
3293
+ }
3294
+ if (!isStateEnvelope(parsed)) {
3295
+ throw new Error(`LocalFileBackend: ${relPath} is not a StateEnvelope`);
3296
+ }
3297
+ return { path: relPath, handle: null, state: parsed, created: false };
3298
+ }
3299
+ save(loaded, next) {
3300
+ if (!loaded.created && isStateUnchanged(loaded.state, next)) {
3301
+ return false;
3302
+ }
3303
+ const absPath = path15.join(this.cwd, loaded.path);
3304
+ fs16.mkdirSync(path15.dirname(absPath), { recursive: true });
3305
+ const body = JSON.stringify(next, null, 2) + "\n";
3306
+ fs16.writeFileSync(absPath, body, "utf-8");
3307
+ return true;
3308
+ }
3309
+ cacheKeyPrefix() {
3310
+ return `kody-mission-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
3311
+ }
3312
+ };
3313
+ function sanitizeKey(s) {
3314
+ return s.replace(/[^A-Za-z0-9._-]/g, "-");
3315
+ }
3316
+ function defaultCacheAdapter() {
3317
+ let mod = null;
3318
+ const load = async () => {
3319
+ if (!mod) {
3320
+ mod = await import("@actions/cache");
3321
+ }
3322
+ return mod;
3323
+ };
3324
+ const available = () => {
3325
+ if (process.env.GITHUB_ACTIONS !== "true") return false;
3326
+ return Boolean(process.env.ACTIONS_CACHE_URL || process.env.ACTIONS_RESULTS_URL);
3327
+ };
3328
+ return {
3329
+ isAvailable: available,
3330
+ async restore(paths, primaryKey, restoreKeys) {
3331
+ if (!available()) return void 0;
3332
+ const m = await load();
3333
+ return m.restoreCache(paths, primaryKey, restoreKeys);
3334
+ },
3335
+ async save(paths, primaryKey) {
3336
+ if (!available()) return;
3337
+ const m = await load();
3338
+ try {
3339
+ await m.saveCache(paths, primaryKey);
3340
+ } catch (err) {
3341
+ const name = err?.name ?? "";
3342
+ if (name === "ReserveCacheError") return;
3343
+ throw err;
3344
+ }
3345
+ }
3346
+ };
3347
+ }
3348
+
3349
+ // src/scripts/missionState/index.ts
3350
+ function resolveBackend(opts) {
3351
+ const owner = opts.config.github?.owner;
3352
+ const repo = opts.config.github?.repo;
3353
+ if (!owner || !repo) {
3354
+ throw new Error("resolveBackend: config.github.owner and config.github.repo must be set");
3355
+ }
3356
+ const requested = opts.config.missions?.stateBackend ?? "contents-api";
3357
+ switch (requested) {
3358
+ case "contents-api":
3359
+ return new ContentsApiBackend({ owner, repo, missionsDir: opts.missionsDir, cwd: opts.cwd });
3360
+ case "local-file":
3361
+ return new LocalFileBackend({ cwd: opts.cwd, missionsDir: opts.missionsDir, owner, repo });
3362
+ default: {
3363
+ const _exhaustive = requested;
3364
+ throw new Error(`resolveBackend: unknown stateBackend "${String(_exhaustive)}"`);
3365
+ }
3366
+ }
3367
+ }
3368
+
3369
+ // src/scripts/dispatchMissionFileTicks.ts
3370
+ var dispatchMissionFileTicks = async (ctx, _profile, args) => {
3371
+ ctx.skipAgent = true;
3372
+ const targetExecutable = String(args?.targetExecutable ?? "");
3373
+ if (!targetExecutable) {
3374
+ throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
3375
+ }
3376
+ const missionsDir = String(args?.missionsDir ?? ".kody/missions");
3377
+ const slugArg = String(args?.slugArg ?? "mission");
3378
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
3379
+ if (backend.hydrate) {
3380
+ await backend.hydrate();
3381
+ }
3382
+ try {
3383
+ const slugs = listMissionSlugs(path16.join(ctx.cwd, missionsDir));
3384
+ ctx.data.missionSlugCount = slugs.length;
3385
+ if (slugs.length === 0) {
3386
+ process.stdout.write(`[missions] no mission files in ${missionsDir}
3387
+ `);
3388
+ return;
3389
+ }
3390
+ process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
3391
+ `);
3392
+ const results = [];
3393
+ for (const slug of slugs) {
3394
+ process.stdout.write(`[missions] \u2192 tick ${slug}
3395
+ `);
3396
+ try {
3397
+ const out = await runExecutable(targetExecutable, {
3398
+ cliArgs: { [slugArg]: slug },
3399
+ cwd: ctx.cwd,
3400
+ config: ctx.config,
3401
+ verbose: ctx.verbose,
3402
+ quiet: ctx.quiet
3403
+ });
3404
+ results.push({ slug, exitCode: out.exitCode, reason: out.reason });
3405
+ if (out.exitCode !== 0) {
3406
+ process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
3407
+ `);
3408
+ }
3409
+ } catch (err) {
3410
+ const msg = err instanceof Error ? err.message : String(err);
3411
+ process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
3412
+ `);
3413
+ results.push({ slug, exitCode: 99, reason: msg });
3414
+ }
3415
+ }
3416
+ ctx.data.missionTickResults = results;
3417
+ ctx.output.exitCode = 0;
3418
+ } finally {
3419
+ if (backend.persist) {
3420
+ try {
3421
+ await backend.persist();
3422
+ } catch (err) {
3423
+ const msg = err instanceof Error ? err.message : String(err);
3424
+ process.stderr.write(`[missions] backend persist failed: ${msg}
3425
+ `);
3426
+ }
3427
+ }
3428
+ }
3429
+ };
3430
+ function listMissionSlugs(absDir) {
3431
+ if (!fs17.existsSync(absDir)) return [];
3432
+ let entries;
3433
+ try {
3434
+ entries = fs17.readdirSync(absDir, { withFileTypes: true });
3435
+ } catch {
3436
+ return [];
3437
+ }
3438
+ 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();
3439
+ }
3440
+
3052
3441
  // src/scripts/dispatchMissionTicks.ts
3053
3442
  var dispatchMissionTicks = async (ctx, _profile, args) => {
3054
3443
  ctx.skipAgent = true;
@@ -3681,7 +4070,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
3681
4070
 
3682
4071
  // src/gha.ts
3683
4072
  import { execFileSync as execFileSync12 } from "child_process";
3684
- import * as fs17 from "fs";
4073
+ import * as fs18 from "fs";
3685
4074
  function getRunUrl() {
3686
4075
  const server = process.env.GITHUB_SERVER_URL;
3687
4076
  const repo = process.env.GITHUB_REPOSITORY;
@@ -3692,10 +4081,10 @@ function getRunUrl() {
3692
4081
  function reactToTriggerComment(cwd) {
3693
4082
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
3694
4083
  const eventPath = process.env.GITHUB_EVENT_PATH;
3695
- if (!eventPath || !fs17.existsSync(eventPath)) return;
4084
+ if (!eventPath || !fs18.existsSync(eventPath)) return;
3696
4085
  let event = null;
3697
4086
  try {
3698
- event = JSON.parse(fs17.readFileSync(eventPath, "utf-8"));
4087
+ event = JSON.parse(fs18.readFileSync(eventPath, "utf-8"));
3699
4088
  } catch {
3700
4089
  return;
3701
4090
  }
@@ -3935,22 +4324,22 @@ function tryPostPr2(prNumber, body, cwd) {
3935
4324
 
3936
4325
  // src/scripts/initFlow.ts
3937
4326
  import { execFileSync as execFileSync14 } from "child_process";
3938
- import * as fs19 from "fs";
3939
- import * as path17 from "path";
4327
+ import * as fs20 from "fs";
4328
+ import * as path18 from "path";
3940
4329
 
3941
4330
  // src/scripts/loadQaGuide.ts
3942
- import * as fs18 from "fs";
3943
- import * as path16 from "path";
4331
+ import * as fs19 from "fs";
4332
+ import * as path17 from "path";
3944
4333
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
3945
4334
  var loadQaGuide = async (ctx) => {
3946
- const full = path16.join(ctx.cwd, QA_GUIDE_REL_PATH);
3947
- if (!fs18.existsSync(full)) {
4335
+ const full = path17.join(ctx.cwd, QA_GUIDE_REL_PATH);
4336
+ if (!fs19.existsSync(full)) {
3948
4337
  ctx.data.qaGuide = "";
3949
4338
  ctx.data.qaGuidePath = "";
3950
4339
  return;
3951
4340
  }
3952
4341
  try {
3953
- ctx.data.qaGuide = fs18.readFileSync(full, "utf-8");
4342
+ ctx.data.qaGuide = fs19.readFileSync(full, "utf-8");
3954
4343
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
3955
4344
  } catch {
3956
4345
  ctx.data.qaGuide = "";
@@ -3960,9 +4349,9 @@ var loadQaGuide = async (ctx) => {
3960
4349
 
3961
4350
  // src/scripts/initFlow.ts
3962
4351
  function detectPackageManager(cwd) {
3963
- if (fs19.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
3964
- if (fs19.existsSync(path17.join(cwd, "yarn.lock"))) return "yarn";
3965
- if (fs19.existsSync(path17.join(cwd, "bun.lockb"))) return "bun";
4352
+ if (fs20.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
4353
+ if (fs20.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
4354
+ if (fs20.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
3966
4355
  return "npm";
3967
4356
  }
3968
4357
  function qualityCommandsFor(pm) {
@@ -4084,33 +4473,33 @@ function performInit(cwd, force) {
4084
4473
  const pm = detectPackageManager(cwd);
4085
4474
  const ownerRepo = detectOwnerRepo(cwd);
4086
4475
  const defaultBranch = defaultBranchFromGit(cwd);
4087
- const configPath = path17.join(cwd, "kody.config.json");
4088
- if (fs19.existsSync(configPath) && !force) {
4476
+ const configPath = path18.join(cwd, "kody.config.json");
4477
+ if (fs20.existsSync(configPath) && !force) {
4089
4478
  skipped.push("kody.config.json");
4090
4479
  } else {
4091
4480
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
4092
- fs19.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
4481
+ fs20.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
4093
4482
  `);
4094
4483
  wrote.push("kody.config.json");
4095
4484
  }
4096
- const workflowDir = path17.join(cwd, ".github", "workflows");
4097
- const workflowPath = path17.join(workflowDir, "kody.yml");
4098
- if (fs19.existsSync(workflowPath) && !force) {
4485
+ const workflowDir = path18.join(cwd, ".github", "workflows");
4486
+ const workflowPath = path18.join(workflowDir, "kody.yml");
4487
+ if (fs20.existsSync(workflowPath) && !force) {
4099
4488
  skipped.push(".github/workflows/kody.yml");
4100
4489
  } else {
4101
- fs19.mkdirSync(workflowDir, { recursive: true });
4102
- fs19.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
4490
+ fs20.mkdirSync(workflowDir, { recursive: true });
4491
+ fs20.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
4103
4492
  wrote.push(".github/workflows/kody.yml");
4104
4493
  }
4105
- const hasUi = fs19.existsSync(path17.join(cwd, "src/app")) || fs19.existsSync(path17.join(cwd, "app")) || fs19.existsSync(path17.join(cwd, "pages"));
4494
+ const hasUi = fs20.existsSync(path18.join(cwd, "src/app")) || fs20.existsSync(path18.join(cwd, "app")) || fs20.existsSync(path18.join(cwd, "pages"));
4106
4495
  if (hasUi) {
4107
- const qaGuidePath = path17.join(cwd, QA_GUIDE_REL_PATH);
4108
- if (fs19.existsSync(qaGuidePath) && !force) {
4496
+ const qaGuidePath = path18.join(cwd, QA_GUIDE_REL_PATH);
4497
+ if (fs20.existsSync(qaGuidePath) && !force) {
4109
4498
  skipped.push(QA_GUIDE_REL_PATH);
4110
4499
  } else {
4111
- fs19.mkdirSync(path17.dirname(qaGuidePath), { recursive: true });
4500
+ fs20.mkdirSync(path18.dirname(qaGuidePath), { recursive: true });
4112
4501
  const discovery = runQaDiscovery(cwd);
4113
- fs19.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
4502
+ fs20.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
4114
4503
  wrote.push(QA_GUIDE_REL_PATH);
4115
4504
  }
4116
4505
  }
@@ -4122,12 +4511,12 @@ function performInit(cwd, force) {
4122
4511
  continue;
4123
4512
  }
4124
4513
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
4125
- const target = path17.join(workflowDir, `kody-${exe.name}.yml`);
4126
- if (fs19.existsSync(target) && !force) {
4514
+ const target = path18.join(workflowDir, `kody-${exe.name}.yml`);
4515
+ if (fs20.existsSync(target) && !force) {
4127
4516
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
4128
4517
  continue;
4129
4518
  }
4130
- fs19.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
4519
+ fs20.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
4131
4520
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
4132
4521
  }
4133
4522
  let labels;
@@ -4238,89 +4627,6 @@ var loadIssueContext = async (ctx) => {
4238
4627
  ctx.data.commentTargetNumber = issueNumber;
4239
4628
  };
4240
4629
 
4241
- // src/scripts/issueStateComment.ts
4242
- function isStateEnvelope(x) {
4243
- if (x === null || typeof x !== "object") return false;
4244
- const o = x;
4245
- 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);
4246
- }
4247
- function initialStateEnvelope(cursor = "seed") {
4248
- return { version: 1, rev: 0, cursor, data: {}, done: false };
4249
- }
4250
- function formatStateCommentBody(marker, state) {
4251
- return `<!-- ${marker} -->
4252
-
4253
- \`\`\`json
4254
- ${JSON.stringify(state, null, 2)}
4255
- \`\`\`
4256
- `;
4257
- }
4258
- function parseStateCommentBody(marker, body) {
4259
- const markerLine = `<!-- ${marker} -->`;
4260
- if (!body.trimStart().startsWith(markerLine)) return null;
4261
- const fenceOpen = body.indexOf("```json");
4262
- if (fenceOpen === -1) return null;
4263
- const after = body.slice(fenceOpen + "```json".length);
4264
- const fenceClose = after.indexOf("```");
4265
- if (fenceClose === -1) return null;
4266
- const jsonText = after.slice(0, fenceClose).trim();
4267
- let parsed;
4268
- try {
4269
- parsed = JSON.parse(jsonText);
4270
- } catch {
4271
- return null;
4272
- }
4273
- return isStateEnvelope(parsed) ? parsed : null;
4274
- }
4275
- function listIssueComments(owner, repo, issueNumber, cwd) {
4276
- const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
4277
- let parsed;
4278
- try {
4279
- parsed = JSON.parse(raw);
4280
- } catch {
4281
- return [];
4282
- }
4283
- if (!Array.isArray(parsed)) return [];
4284
- return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
4285
- }
4286
- function findStateComment2(owner, repo, issueNumber, marker, cwd) {
4287
- const comments = listIssueComments(owner, repo, issueNumber, cwd);
4288
- for (const c of comments) {
4289
- const state = parseStateCommentBody(marker, c.body);
4290
- if (!state) continue;
4291
- return { commentId: c.id, commentNodeId: c.node_id, state };
4292
- }
4293
- return null;
4294
- }
4295
- function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
4296
- const body = formatStateCommentBody(marker, state);
4297
- const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
4298
- cwd,
4299
- input: JSON.stringify({ body })
4300
- });
4301
- const parsed = JSON.parse(raw);
4302
- try {
4303
- minimizeComment(parsed.node_id, cwd);
4304
- } catch {
4305
- }
4306
- return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
4307
- }
4308
- function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
4309
- const body = formatStateCommentBody(marker, state);
4310
- gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
4311
- cwd,
4312
- input: JSON.stringify({ body })
4313
- });
4314
- try {
4315
- minimizeComment(commentNodeId, cwd);
4316
- } catch {
4317
- }
4318
- }
4319
- function minimizeComment(nodeId, cwd) {
4320
- const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
4321
- gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
4322
- }
4323
-
4324
4630
  // src/scripts/loadIssueStateComment.ts
4325
4631
  var loadIssueStateComment = async (ctx, _profile, args) => {
4326
4632
  const marker = String(args?.marker ?? "");
@@ -4348,75 +4654,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
4348
4654
  };
4349
4655
 
4350
4656
  // src/scripts/loadMissionFromFile.ts
4351
- import * as fs20 from "fs";
4352
- import * as path18 from "path";
4353
-
4354
- // src/scripts/missionStateFile.ts
4355
- function stateFilePath(missionsDir, slug) {
4356
- return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
4357
- }
4358
- function loadMissionState(owner, repo, filePath, cwd) {
4359
- let raw = "";
4360
- try {
4361
- raw = gh2(["api", `/repos/${owner}/${repo}/contents/${filePath}`], { cwd });
4362
- } catch (err) {
4363
- const msg = err instanceof Error ? err.message : String(err);
4364
- if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
4365
- return { path: filePath, sha: null, state: initialStateEnvelope("seed"), created: true };
4366
- }
4367
- throw err;
4368
- }
4369
- let parsed;
4370
- try {
4371
- parsed = JSON.parse(raw);
4372
- } catch {
4373
- throw new Error(`loadMissionState: contents API for ${filePath} did not return JSON`);
4374
- }
4375
- if (!parsed || typeof parsed !== "object") {
4376
- throw new Error(`loadMissionState: contents API for ${filePath} returned non-object`);
4377
- }
4378
- const o = parsed;
4379
- if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
4380
- throw new Error(`loadMissionState: ${filePath} is not a base64 file`);
4381
- }
4382
- const decoded = Buffer.from(o.content, "base64").toString("utf-8");
4383
- let envelope;
4384
- try {
4385
- envelope = JSON.parse(decoded);
4386
- } catch {
4387
- throw new Error(`loadMissionState: ${filePath} is not valid JSON`);
4388
- }
4389
- if (!isStateEnvelope(envelope)) {
4390
- throw new Error(`loadMissionState: ${filePath} is not a StateEnvelope`);
4391
- }
4392
- return { path: filePath, sha: o.sha, state: envelope, created: false };
4393
- }
4394
- function writeMissionState(owner, repo, loaded, next, cwd) {
4395
- if (!loaded.created && deepEqualsState(loaded.state, next)) {
4396
- return false;
4397
- }
4398
- const body = JSON.stringify(next, null, 2) + "\n";
4399
- const payload = {
4400
- message: `chore(missions): update state for ${stateFileSlug(loaded.path)} (rev ${next.rev})`,
4401
- content: Buffer.from(body, "utf-8").toString("base64")
4402
- };
4403
- if (loaded.sha) payload.sha = loaded.sha;
4404
- gh2(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${loaded.path}`, "--input", "-"], {
4405
- cwd,
4406
- input: JSON.stringify(payload)
4407
- });
4408
- return true;
4409
- }
4410
- function deepEqualsState(a, b) {
4411
- if (a.cursor !== b.cursor || a.done !== b.done) return false;
4412
- return JSON.stringify(a.data) === JSON.stringify(b.data);
4413
- }
4414
- function stateFileSlug(filePath) {
4415
- const last = filePath.split("/").pop() ?? filePath;
4416
- return last.replace(/\.state\.json$/i, "");
4417
- }
4418
-
4419
- // src/scripts/loadMissionFromFile.ts
4657
+ import * as fs21 from "fs";
4658
+ import * as path19 from "path";
4420
4659
  var loadMissionFromFile = async (ctx, _profile, args) => {
4421
4660
  const missionsDir = String(args?.missionsDir ?? ".kody/missions");
4422
4661
  const slugArg = String(args?.slugArg ?? "mission");
@@ -4424,18 +4663,14 @@ var loadMissionFromFile = async (ctx, _profile, args) => {
4424
4663
  if (!slug) {
4425
4664
  throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
4426
4665
  }
4427
- const owner = ctx.config.github.owner;
4428
- const repo = ctx.config.github.repo;
4429
- if (!owner || !repo) {
4430
- throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
4431
- }
4432
- const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
4433
- if (!fs20.existsSync(absPath)) {
4666
+ const absPath = path19.join(ctx.cwd, missionsDir, `${slug}.md`);
4667
+ if (!fs21.existsSync(absPath)) {
4434
4668
  throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
4435
4669
  }
4436
- const raw = fs20.readFileSync(absPath, "utf-8");
4670
+ const raw = fs21.readFileSync(absPath, "utf-8");
4437
4671
  const { title, body } = parseMissionFile(raw, slug);
4438
- const loaded = loadMissionState(owner, repo, stateFilePath(missionsDir, slug), ctx.cwd);
4672
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
4673
+ const loaded = await backend.load(slug);
4439
4674
  ctx.data.missionSlug = slug;
4440
4675
  ctx.data.missionTitle = title;
4441
4676
  ctx.data.missionIntent = body;
@@ -4566,16 +4801,16 @@ var loadTaskState = async (ctx) => {
4566
4801
  };
4567
4802
 
4568
4803
  // src/scripts/loadVaultContext.ts
4569
- import * as fs21 from "fs";
4570
- import * as path19 from "path";
4804
+ import * as fs22 from "fs";
4805
+ import * as path20 from "path";
4571
4806
  var VAULT_DIR_RELATIVE = ".kody/vault";
4572
4807
  var MAX_PAGES = 8;
4573
4808
  var PER_PAGE_MAX_BYTES = 4e3;
4574
4809
  var TOTAL_MAX_BYTES2 = 24e3;
4575
4810
  var TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
4576
4811
  var loadVaultContext = async (ctx) => {
4577
- const vaultAbs = path19.join(ctx.cwd, VAULT_DIR_RELATIVE);
4578
- if (!fs21.existsSync(vaultAbs)) {
4812
+ const vaultAbs = path20.join(ctx.cwd, VAULT_DIR_RELATIVE);
4813
+ if (!fs22.existsSync(vaultAbs)) {
4579
4814
  ctx.data.vaultContext = "";
4580
4815
  return;
4581
4816
  }
@@ -4600,21 +4835,21 @@ function collectPages(vaultAbs) {
4600
4835
  walkMd(vaultAbs, (file) => {
4601
4836
  let stat;
4602
4837
  try {
4603
- stat = fs21.statSync(file);
4838
+ stat = fs22.statSync(file);
4604
4839
  } catch {
4605
4840
  return;
4606
4841
  }
4607
4842
  let raw;
4608
4843
  try {
4609
- raw = fs21.readFileSync(file, "utf-8");
4844
+ raw = fs22.readFileSync(file, "utf-8");
4610
4845
  } catch {
4611
4846
  return;
4612
4847
  }
4613
4848
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
4614
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path19.basename(file, ".md");
4849
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path20.basename(file, ".md");
4615
4850
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
4616
4851
  out.push({
4617
- relPath: path19.relative(vaultAbs, file),
4852
+ relPath: path20.relative(vaultAbs, file),
4618
4853
  title,
4619
4854
  updated,
4620
4855
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
@@ -4682,16 +4917,16 @@ function walkMd(root, visit) {
4682
4917
  const dir = stack.pop();
4683
4918
  let names;
4684
4919
  try {
4685
- names = fs21.readdirSync(dir);
4920
+ names = fs22.readdirSync(dir);
4686
4921
  } catch {
4687
4922
  continue;
4688
4923
  }
4689
4924
  for (const name of names) {
4690
4925
  if (name.startsWith(".")) continue;
4691
- const full = path19.join(dir, name);
4926
+ const full = path20.join(dir, name);
4692
4927
  let stat;
4693
4928
  try {
4694
- stat = fs21.statSync(full);
4929
+ stat = fs22.statSync(full);
4695
4930
  } catch {
4696
4931
  continue;
4697
4932
  }
@@ -4714,15 +4949,15 @@ var markFlowSuccess = async (ctx) => {
4714
4949
 
4715
4950
  // src/scripts/memorizeFlow.ts
4716
4951
  import { execFileSync as execFileSync15 } from "child_process";
4717
- import * as fs22 from "fs";
4718
- import * as path20 from "path";
4952
+ import * as fs23 from "fs";
4953
+ import * as path21 from "path";
4719
4954
  var VAULT_DIR_RELATIVE2 = ".kody/vault";
4720
4955
  var DEFAULT_LOOKBACK_HOURS = 36;
4721
4956
  var MAX_RECENT_PRS = 25;
4722
4957
  var MAX_VAULT_INDEX_ENTRIES = 200;
4723
4958
  var PR_BODY_TRUNC = 2e3;
4724
4959
  var memorizeFlow = async (ctx) => {
4725
- const vaultAbs = path20.join(ctx.cwd, VAULT_DIR_RELATIVE2);
4960
+ const vaultAbs = path21.join(ctx.cwd, VAULT_DIR_RELATIVE2);
4726
4961
  ensureBranch(ctx, vaultAbs);
4727
4962
  if (ctx.skipAgent) return;
4728
4963
  const sinceIso = computeSinceIso(vaultAbs);
@@ -4732,8 +4967,8 @@ var memorizeFlow = async (ctx) => {
4732
4967
  const recent = fetchRecentPrs(ctx.cwd, sinceIso);
4733
4968
  ctx.data.recentPrs = formatRecentPrs(recent);
4734
4969
  ctx.data.recentPrCount = recent.length;
4735
- if (!fs22.existsSync(vaultAbs)) {
4736
- fs22.mkdirSync(vaultAbs, { recursive: true });
4970
+ if (!fs23.existsSync(vaultAbs)) {
4971
+ fs23.mkdirSync(vaultAbs, { recursive: true });
4737
4972
  }
4738
4973
  ctx.data.vaultIndex = formatVaultIndex(vaultAbs);
4739
4974
  if (recent.length === 0) {
@@ -4765,18 +5000,18 @@ function ensureBranch(ctx, vaultAbs) {
4765
5000
  }
4766
5001
  }
4767
5002
  ctx.data.branch = branch;
4768
- if (!fs22.existsSync(vaultAbs)) {
4769
- fs22.mkdirSync(vaultAbs, { recursive: true });
5003
+ if (!fs23.existsSync(vaultAbs)) {
5004
+ fs23.mkdirSync(vaultAbs, { recursive: true });
4770
5005
  }
4771
5006
  }
4772
5007
  function computeSinceIso(vaultAbs) {
4773
5008
  const fallback = new Date(Date.now() - DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1e3).toISOString();
4774
- if (!fs22.existsSync(vaultAbs)) return fallback;
5009
+ if (!fs23.existsSync(vaultAbs)) return fallback;
4775
5010
  let latest = "";
4776
5011
  walkMd2(vaultAbs, (file) => {
4777
5012
  let raw;
4778
5013
  try {
4779
- raw = fs22.readFileSync(file, "utf-8");
5014
+ raw = fs23.readFileSync(file, "utf-8");
4780
5015
  } catch {
4781
5016
  return;
4782
5017
  }
@@ -4853,10 +5088,10 @@ function formatVaultIndex(vaultAbs) {
4853
5088
  const entries = [];
4854
5089
  walkMd2(vaultAbs, (file) => {
4855
5090
  if (entries.length >= MAX_VAULT_INDEX_ENTRIES) return;
4856
- const rel = path20.relative(vaultAbs, file);
5091
+ const rel = path21.relative(vaultAbs, file);
4857
5092
  let title = rel;
4858
5093
  try {
4859
- const raw = fs22.readFileSync(file, "utf-8");
5094
+ const raw = fs23.readFileSync(file, "utf-8");
4860
5095
  const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
4861
5096
  const titleMatch = m?.[1]?.match(/^title:\s*(.+)$/m);
4862
5097
  if (titleMatch) title = `${titleMatch[1].trim()} (${rel})`;
@@ -4868,22 +5103,22 @@ function formatVaultIndex(vaultAbs) {
4868
5103
  return entries.join("\n");
4869
5104
  }
4870
5105
  function walkMd2(root, visit) {
4871
- if (!fs22.existsSync(root)) return;
5106
+ if (!fs23.existsSync(root)) return;
4872
5107
  const stack = [root];
4873
5108
  while (stack.length > 0) {
4874
5109
  const dir = stack.pop();
4875
5110
  let names;
4876
5111
  try {
4877
- names = fs22.readdirSync(dir);
5112
+ names = fs23.readdirSync(dir);
4878
5113
  } catch {
4879
5114
  continue;
4880
5115
  }
4881
5116
  for (const name of names) {
4882
5117
  if (name.startsWith(".")) continue;
4883
- const full = path20.join(dir, name);
5118
+ const full = path21.join(dir, name);
4884
5119
  let stat;
4885
5120
  try {
4886
- stat = fs22.statSync(full);
5121
+ stat = fs23.statSync(full);
4887
5122
  } catch {
4888
5123
  continue;
4889
5124
  }
@@ -5212,7 +5447,15 @@ var postIssueComment2 = async (ctx) => {
5212
5447
  const isFailure = failureReason.length > 0;
5213
5448
  const justPushedToExistingPr = prAction === "updated" && commitResult?.committed === true;
5214
5449
  const successMsg = justPushedToExistingPr ? `\u2705 kody pushed to ${prUrl}` : prAction === "updated" ? `\u2139\uFE0F kody made no changes \u2014 PR: ${prUrl}` : `\u2705 kody PR opened: ${prUrl}`;
5215
- const failurePrSuffix = prUrl ? prAction === "updated" ? ` \u2014 PR: ${prUrl}` : ` \u2014 draft PR: ${prUrl}` : "";
5450
+ const branch = ctx.data.branch;
5451
+ const failurePrSuffix = computeFailureSuffix({
5452
+ prUrl,
5453
+ prAction,
5454
+ branch,
5455
+ branchPushed: commitResult?.committed === true,
5456
+ githubOwner: ctx.config.github?.owner,
5457
+ githubRepo: ctx.config.github?.repo
5458
+ });
5216
5459
  const msg = isFailure ? `\u26A0\uFE0F kody FAILED: ${truncate2(failureReason, 1500)}${failurePrSuffix}` : successMsg;
5217
5460
  postWith(targetType, targetNumber, msg, ctx.cwd);
5218
5461
  let exitCode = 0;
@@ -5224,6 +5467,13 @@ var postIssueComment2 = async (ctx) => {
5224
5467
  ctx.output.exitCode = exitCode;
5225
5468
  ctx.output.reason = failureReason || void 0;
5226
5469
  };
5470
+ function computeFailureSuffix(input) {
5471
+ if (input.prUrl) {
5472
+ return input.prAction === "updated" ? ` \u2014 PR: ${input.prUrl}` : ` \u2014 draft PR: ${input.prUrl}`;
5473
+ }
5474
+ if (!input.branchPushed || !input.branch || !input.githubOwner || !input.githubRepo) return "";
5475
+ return ` \u2014 branch: https://github.com/${input.githubOwner}/${input.githubRepo}/tree/${input.branch}`;
5476
+ }
5227
5477
  function computeFailureReason2(ctx) {
5228
5478
  const misses = ctx.data.coverageMisses ?? [];
5229
5479
  if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
@@ -5638,6 +5888,25 @@ function tryPostPr3(prNumber, body, cwd) {
5638
5888
  }
5639
5889
  }
5640
5890
 
5891
+ // src/scripts/resolvePreviewUrl.ts
5892
+ var DEFAULT_PREVIEW_URL = "http://localhost:3000";
5893
+ var resolvePreviewUrl = async (ctx) => {
5894
+ const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
5895
+ if (fromFlag.length > 0) {
5896
+ ctx.data.previewUrl = fromFlag;
5897
+ ctx.data.previewUrlSource = "flag";
5898
+ return;
5899
+ }
5900
+ const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
5901
+ if (fromEnv.length > 0) {
5902
+ ctx.data.previewUrl = fromEnv;
5903
+ ctx.data.previewUrlSource = "env";
5904
+ return;
5905
+ }
5906
+ ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
5907
+ ctx.data.previewUrlSource = "default";
5908
+ };
5909
+
5641
5910
  // src/scripts/revertFlow.ts
5642
5911
  import { execFileSync as execFileSync19 } from "child_process";
5643
5912
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
@@ -5707,11 +5976,7 @@ var revertFlow = async (ctx) => {
5707
5976
  const runUrl = getRunUrl();
5708
5977
  const runSuffix = runUrl ? `, run ${runUrl}` : "";
5709
5978
  const shaList = resolved.map((r) => `\`${r.full.slice(0, 7)}\``).join(", ");
5710
- tryPostPr4(
5711
- prNumber,
5712
- `\u2699\uFE0F kody revert started on \`${ctx.data.branch}\`${runSuffix} \u2014 reverting ${shaList}`,
5713
- ctx.cwd
5714
- );
5979
+ tryPostPr4(prNumber, `\u2699\uFE0F kody revert started on \`${ctx.data.branch}\`${runSuffix} \u2014 reverting ${shaList}`, ctx.cwd);
5715
5980
  };
5716
5981
  function buildCommitMessage(resolved) {
5717
5982
  if (resolved.length === 1) {
@@ -5755,25 +6020,6 @@ function tryPostPr4(prNumber, body, cwd) {
5755
6020
  }
5756
6021
  }
5757
6022
 
5758
- // src/scripts/resolvePreviewUrl.ts
5759
- var DEFAULT_PREVIEW_URL = "http://localhost:3000";
5760
- var resolvePreviewUrl = async (ctx) => {
5761
- const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
5762
- if (fromFlag.length > 0) {
5763
- ctx.data.previewUrl = fromFlag;
5764
- ctx.data.previewUrlSource = "flag";
5765
- return;
5766
- }
5767
- const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
5768
- if (fromEnv.length > 0) {
5769
- ctx.data.previewUrl = fromEnv;
5770
- ctx.data.previewUrlSource = "env";
5771
- return;
5772
- }
5773
- ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
5774
- ctx.data.previewUrlSource = "default";
5775
- };
5776
-
5777
6023
  // src/scripts/reviewFlow.ts
5778
6024
  var reviewFlow = async (ctx) => {
5779
6025
  const prNumber = ctx.args.pr;
@@ -6389,7 +6635,7 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
6389
6635
  };
6390
6636
 
6391
6637
  // src/scripts/writeMissionStateFile.ts
6392
- var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
6638
+ var writeMissionStateFile = async (ctx, _profile, _agentResult, args) => {
6393
6639
  const parseError = ctx.data.nextStateParseError;
6394
6640
  if (parseError) {
6395
6641
  process.stderr.write(`[kody] mission state write skipped: ${parseError}
@@ -6406,16 +6652,13 @@ var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
6406
6652
  if (!loaded) {
6407
6653
  throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
6408
6654
  }
6409
- const owner = ctx.config.github.owner;
6410
- const repo = ctx.config.github.repo;
6411
- if (!owner || !repo) {
6412
- throw new Error("writeMissionStateFile: ctx.config.github.owner/repo must be set");
6413
- }
6414
- writeMissionState(owner, repo, loaded, next, ctx.cwd);
6655
+ const missionsDir = String(args?.missionsDir ?? ".kody/missions");
6656
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
6657
+ await backend.save(loaded, next);
6415
6658
  };
6416
6659
 
6417
6660
  // src/scripts/writeRunSummary.ts
6418
- import * as fs23 from "fs";
6661
+ import * as fs24 from "fs";
6419
6662
  var writeRunSummary = async (ctx, profile) => {
6420
6663
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
6421
6664
  if (!summaryPath) return;
@@ -6437,7 +6680,7 @@ var writeRunSummary = async (ctx, profile) => {
6437
6680
  if (reason) lines.push(`- **Reason:** ${reason}`);
6438
6681
  lines.push("");
6439
6682
  try {
6440
- fs23.appendFileSync(summaryPath, `${lines.join("\n")}
6683
+ fs24.appendFileSync(summaryPath, `${lines.join("\n")}
6441
6684
  `);
6442
6685
  } catch {
6443
6686
  }
@@ -6620,9 +6863,9 @@ async function runExecutable(profileName, input) {
6620
6863
  data: {},
6621
6864
  output: { exitCode: 0 }
6622
6865
  };
6623
- const ndjsonDir = path21.join(input.cwd, ".kody");
6866
+ const ndjsonDir = path22.join(input.cwd, ".kody");
6624
6867
  const invokeAgent = async (prompt) => {
6625
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path21.isAbsolute(p) ? p : path21.resolve(profile.dir, p)).filter((p) => p.length > 0);
6868
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path22.isAbsolute(p) ? p : path22.resolve(profile.dir, p)).filter((p) => p.length > 0);
6626
6869
  const syntheticPath = ctx.data.syntheticPluginPath;
6627
6870
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
6628
6871
  return runAgent({
@@ -6701,17 +6944,17 @@ async function runExecutable(profileName, input) {
6701
6944
  function resolveProfilePath(profileName) {
6702
6945
  const found = resolveExecutable(profileName);
6703
6946
  if (found) return found;
6704
- const here = path21.dirname(new URL(import.meta.url).pathname);
6947
+ const here = path22.dirname(new URL(import.meta.url).pathname);
6705
6948
  const candidates = [
6706
- path21.join(here, "executables", profileName, "profile.json"),
6949
+ path22.join(here, "executables", profileName, "profile.json"),
6707
6950
  // same-dir sibling (dev)
6708
- path21.join(here, "..", "executables", profileName, "profile.json"),
6951
+ path22.join(here, "..", "executables", profileName, "profile.json"),
6709
6952
  // up one (prod: dist/bin → dist/executables)
6710
- path21.join(here, "..", "src", "executables", profileName, "profile.json")
6953
+ path22.join(here, "..", "src", "executables", profileName, "profile.json")
6711
6954
  // fallback
6712
6955
  ];
6713
6956
  for (const c of candidates) {
6714
- if (fs24.existsSync(c)) return c;
6957
+ if (fs25.existsSync(c)) return c;
6715
6958
  }
6716
6959
  return candidates[0];
6717
6960
  }
@@ -6815,8 +7058,8 @@ function resolveShellTimeoutMs(entry) {
6815
7058
  var SIGKILL_GRACE_MS = 5e3;
6816
7059
  async function runShellEntry(entry, ctx, profile) {
6817
7060
  const shellName = entry.shell;
6818
- const shellPath = path21.join(profile.dir, shellName);
6819
- if (!fs24.existsSync(shellPath)) {
7061
+ const shellPath = path22.join(profile.dir, shellName);
7062
+ if (!fs25.existsSync(shellPath)) {
6820
7063
  ctx.skipAgent = true;
6821
7064
  ctx.output.exitCode = 99;
6822
7065
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -6962,7 +7205,7 @@ Environment:
6962
7205
  Exit codes (inherited from kody run):
6963
7206
  0 success (PR opened, verify passed)
6964
7207
  1 agent reported FAILED (draft PR opened)
6965
- 2 verify failed (draft PR opened)
7208
+ 2 verify failed (no PR opened \u2014 branch pushed for inspection)
6966
7209
  3 no commits to ship
6967
7210
  4 PR creation failed
6968
7211
  5 uncommitted changes on target branch
@@ -7024,9 +7267,9 @@ function resolveAuthToken(env = process.env) {
7024
7267
  return token;
7025
7268
  }
7026
7269
  function detectPackageManager2(cwd) {
7027
- if (fs25.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
7028
- if (fs25.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
7029
- if (fs25.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
7270
+ if (fs26.existsSync(path23.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
7271
+ if (fs26.existsSync(path23.join(cwd, "yarn.lock"))) return "yarn";
7272
+ if (fs26.existsSync(path23.join(cwd, "bun.lockb"))) return "bun";
7030
7273
  return "npm";
7031
7274
  }
7032
7275
  function shellOut(cmd, args, cwd, stream = true) {
@@ -7106,11 +7349,11 @@ function configureGitIdentity(cwd) {
7106
7349
  }
7107
7350
  function postFailureTail(issueNumber, cwd, reason) {
7108
7351
  if (!issueNumber) return;
7109
- const logPath = path22.join(cwd, ".kody", "last-run.jsonl");
7352
+ const logPath = path23.join(cwd, ".kody", "last-run.jsonl");
7110
7353
  let tail = "";
7111
7354
  try {
7112
- if (fs25.existsSync(logPath)) {
7113
- const content = fs25.readFileSync(logPath, "utf-8");
7355
+ if (fs26.existsSync(logPath)) {
7356
+ const content = fs26.readFileSync(logPath, "utf-8");
7114
7357
  tail = content.slice(-3e3);
7115
7358
  }
7116
7359
  } catch {
@@ -7135,7 +7378,7 @@ async function runCi(argv) {
7135
7378
  return 0;
7136
7379
  }
7137
7380
  const args = parseCiArgs(argv);
7138
- const cwd = args.cwd ? path22.resolve(args.cwd) : process.cwd();
7381
+ const cwd = args.cwd ? path23.resolve(args.cwd) : process.cwd();
7139
7382
  let earlyConfig;
7140
7383
  try {
7141
7384
  earlyConfig = loadConfig(cwd);
@@ -7145,9 +7388,9 @@ async function runCi(argv) {
7145
7388
  const eventName = process.env.GITHUB_EVENT_NAME;
7146
7389
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
7147
7390
  let manualWorkflowDispatch = false;
7148
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs25.existsSync(dispatchEventPath)) {
7391
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs26.existsSync(dispatchEventPath)) {
7149
7392
  try {
7150
- const evt = JSON.parse(fs25.readFileSync(dispatchEventPath, "utf-8"));
7393
+ const evt = JSON.parse(fs26.readFileSync(dispatchEventPath, "utf-8"));
7151
7394
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
7152
7395
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
7153
7396
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -7362,9 +7605,9 @@ function parseChatArgs(argv, env = process.env) {
7362
7605
  return result;
7363
7606
  }
7364
7607
  function commitChatFiles(cwd, sessionId, verbose) {
7365
- const sessionFile = path23.relative(cwd, sessionFilePath(cwd, sessionId));
7366
- const eventsFile = path23.relative(cwd, eventsFilePath(cwd, sessionId));
7367
- const paths = [sessionFile, eventsFile].filter((p) => fs26.existsSync(path23.join(cwd, p)));
7608
+ const sessionFile = path24.relative(cwd, sessionFilePath(cwd, sessionId));
7609
+ const eventsFile = path24.relative(cwd, eventsFilePath(cwd, sessionId));
7610
+ const paths = [sessionFile, eventsFile].filter((p) => fs27.existsSync(path24.join(cwd, p)));
7368
7611
  if (paths.length === 0) return;
7369
7612
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
7370
7613
  try {
@@ -7402,7 +7645,7 @@ async function runChat(argv) {
7402
7645
  ${CHAT_HELP}`);
7403
7646
  return 64;
7404
7647
  }
7405
- const cwd = args.cwd ? path23.resolve(args.cwd) : process.cwd();
7648
+ const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
7406
7649
  const sessionId = args.sessionId;
7407
7650
  const unpackedSecrets = unpackAllSecrets();
7408
7651
  if (unpackedSecrets > 0) {
@@ -7489,7 +7732,7 @@ directory to add a new command.
7489
7732
  Exit codes:
7490
7733
  0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
7491
7734
  1 agent reported FAILED (draft PR opened)
7492
- 2 verify failed (draft PR opened) \u2014 skipped in resolve mode
7735
+ 2 verify failed (no PR opened \u2014 branch pushed for inspection) \u2014 skipped in resolve mode
7493
7736
  3 no commits to ship (also the resolve clean-merge short-circuit)
7494
7737
  4 PR creation failed
7495
7738
  5 uncommitted changes on target branch