@kody-ade/kody-engine 0.4.26 → 0.4.28

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.4.26",
6
+ version: "0.4.28",
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",
@@ -52,8 +52,8 @@ var package_default = {
52
52
 
53
53
  // src/chat-cli.ts
54
54
  import { execFileSync as execFileSync30 } from "child_process";
55
- import * as fs28 from "fs";
56
- import * as path25 from "path";
55
+ import * as fs29 from "fs";
56
+ import * as path26 from "path";
57
57
 
58
58
  // src/chat/events.ts
59
59
  import * as fs from "fs";
@@ -913,8 +913,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
913
913
 
914
914
  // src/kody-cli.ts
915
915
  import { execFileSync as execFileSync29 } from "child_process";
916
- import * as fs27 from "fs";
917
- import * as path24 from "path";
916
+ import * as fs28 from "fs";
917
+ import * as path25 from "path";
918
918
 
919
919
  // src/dispatch.ts
920
920
  import * as fs7 from "fs";
@@ -1300,8 +1300,8 @@ function coerceBare(spec, value) {
1300
1300
 
1301
1301
  // src/executor.ts
1302
1302
  import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1303
- import * as fs26 from "fs";
1304
- import * as path23 from "path";
1303
+ import * as fs27 from "fs";
1304
+ import * as path24 from "path";
1305
1305
 
1306
1306
  // src/issue.ts
1307
1307
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1977,10 +1977,153 @@ function stripBlockingEnv(env) {
1977
1977
  return out;
1978
1978
  }
1979
1979
 
1980
- // src/commit.ts
1981
- import { execFileSync as execFileSync5 } from "child_process";
1980
+ // src/prompt.ts
1982
1981
  import * as fs10 from "fs";
1983
1982
  import * as path9 from "path";
1983
+ var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
1984
+ var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
1985
+ function loadProjectConventions(projectDir) {
1986
+ const out = [];
1987
+ for (const rel of CONVENTION_FILES) {
1988
+ const abs = path9.join(projectDir, rel);
1989
+ if (!fs10.existsSync(abs)) continue;
1990
+ let content;
1991
+ try {
1992
+ content = fs10.readFileSync(abs, "utf-8");
1993
+ } catch {
1994
+ continue;
1995
+ }
1996
+ const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
1997
+ if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
1998
+
1999
+ \u2026 (truncated)`;
2000
+ out.push({ path: rel, content, truncated });
2001
+ }
2002
+ return out;
2003
+ }
2004
+ function parseAgentResult(finalText) {
2005
+ const text = (finalText || "").trim();
2006
+ if (!text)
2007
+ return {
2008
+ done: false,
2009
+ commitMessage: "",
2010
+ prSummary: "",
2011
+ feedbackActions: "",
2012
+ planDeviations: "",
2013
+ priorArt: "",
2014
+ failureReason: "agent produced no final message",
2015
+ markerMissing: false
2016
+ };
2017
+ const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
2018
+ const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
2019
+ const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
2020
+ const failedMatch = text.match(FAILED_RE);
2021
+ if (failedMatch) {
2022
+ return {
2023
+ done: false,
2024
+ commitMessage: "",
2025
+ prSummary: "",
2026
+ feedbackActions: "",
2027
+ planDeviations: "",
2028
+ priorArt: "",
2029
+ failureReason: stripMarkdownEmphasis(failedMatch[1]),
2030
+ markerMissing: false
2031
+ };
2032
+ }
2033
+ const hasDoneMarker = DONE_RE.test(text);
2034
+ const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
2035
+ const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
2036
+ if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
2037
+ const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
2038
+ return {
2039
+ done: false,
2040
+ commitMessage: "",
2041
+ prSummary: "",
2042
+ feedbackActions: "",
2043
+ planDeviations: "",
2044
+ priorArt: "",
2045
+ failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`,
2046
+ markerMissing: true
2047
+ };
2048
+ }
2049
+ const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2050
+ const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2051
+ const feedbackActions = extractBlock(
2052
+ text,
2053
+ /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2054
+ /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2055
+ );
2056
+ let planDeviations = extractBlock(
2057
+ text,
2058
+ /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2059
+ /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2060
+ );
2061
+ if (!planDeviations) {
2062
+ const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2063
+ if (inline) planDeviations = inline[1].trim();
2064
+ }
2065
+ let priorArt = "";
2066
+ const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2067
+ if (priorArtInline) priorArt = priorArtInline[1].trim();
2068
+ const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2069
+ let prSummary = "";
2070
+ if (summaryStart !== -1) {
2071
+ const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2072
+ prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2073
+ }
2074
+ return {
2075
+ done: true,
2076
+ commitMessage,
2077
+ prSummary,
2078
+ feedbackActions,
2079
+ planDeviations,
2080
+ priorArt,
2081
+ failureReason: "",
2082
+ markerMissing: false
2083
+ };
2084
+ }
2085
+ function stripMarkdownEmphasis(s) {
2086
+ return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2087
+ }
2088
+ function extractBlock(text, startMarker, endMarker) {
2089
+ const startIdx = text.search(startMarker);
2090
+ if (startIdx === -1) return "";
2091
+ const afterStart = text.slice(startIdx).replace(startMarker, "");
2092
+ const endIdx = afterStart.search(endMarker);
2093
+ const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2094
+ return body.replace(/\n\s*```\s*$/g, "").trim();
2095
+ }
2096
+
2097
+ // src/rescueMissingMarker.ts
2098
+ var NUDGE_PROMPT = "Your previous message did not contain the required terminator. Reply with EXACTLY one of:\n DONE\n COMMIT_MSG: <one-line commit message>\nor, if the work failed:\n FAILED: <one-line reason>\nDo not repeat any earlier content \u2014 emit only the marker line(s).";
2099
+ async function rescueMissingMarker(result, invoke) {
2100
+ if (result.outcome !== "completed") return result;
2101
+ const parsed = parseAgentResult(result.finalText);
2102
+ if (!parsed.markerMissing) return result;
2103
+ try {
2104
+ const rescue = await invoke(NUDGE_PROMPT);
2105
+ if (!rescue.finalText || !rescue.finalText.trim()) return result;
2106
+ return {
2107
+ ...result,
2108
+ finalText: `${result.finalText}
2109
+
2110
+ ---
2111
+
2112
+ ${rescue.finalText}`,
2113
+ outcome: rescue.outcome === "failed" ? result.outcome : rescue.outcome
2114
+ };
2115
+ } catch (err) {
2116
+ const msg = err instanceof Error ? err.message : String(err);
2117
+ process.stderr.write(`[kody] marker-rescue turn failed: ${msg}
2118
+ `);
2119
+ return result;
2120
+ }
2121
+ }
2122
+
2123
+ // src/commit.ts
2124
+ import { execFileSync as execFileSync5 } from "child_process";
2125
+ import * as fs11 from "fs";
2126
+ import * as path10 from "path";
1984
2127
  var FORBIDDEN_PATH_PREFIXES = [
1985
2128
  ".kody/",
1986
2129
  ".kody-engine/",
@@ -2036,18 +2179,18 @@ function tryGit(args, cwd) {
2036
2179
  }
2037
2180
  function abortUnfinishedGitOps(cwd) {
2038
2181
  const aborted = [];
2039
- const gitDir = path9.join(cwd ?? process.cwd(), ".git");
2040
- if (!fs10.existsSync(gitDir)) return aborted;
2041
- if (fs10.existsSync(path9.join(gitDir, "MERGE_HEAD"))) {
2182
+ const gitDir = path10.join(cwd ?? process.cwd(), ".git");
2183
+ if (!fs11.existsSync(gitDir)) return aborted;
2184
+ if (fs11.existsSync(path10.join(gitDir, "MERGE_HEAD"))) {
2042
2185
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
2043
2186
  }
2044
- if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
2187
+ if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
2045
2188
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
2046
2189
  }
2047
- if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
2190
+ if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
2048
2191
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
2049
2192
  }
2050
- if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
2193
+ if (fs11.existsSync(path10.join(gitDir, "rebase-merge")) || fs11.existsSync(path10.join(gitDir, "rebase-apply"))) {
2051
2194
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
2052
2195
  }
2053
2196
  try {
@@ -2103,7 +2246,7 @@ function normalizeCommitMessage(raw) {
2103
2246
  function commitAndPush(branch, agentMessage, cwd) {
2104
2247
  const allChanged = listChangedFiles(cwd);
2105
2248
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
2106
- const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2249
+ const mergeHeadExists = fs11.existsSync(path10.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2107
2250
  if (allowedFiles.length === 0 && !mergeHeadExists) {
2108
2251
  return { committed: false, pushed: false, sha: "", message: "" };
2109
2252
  }
@@ -2415,21 +2558,21 @@ var advanceFlow = async (ctx, profile) => {
2415
2558
  };
2416
2559
 
2417
2560
  // src/scripts/buildSyntheticPlugin.ts
2418
- import * as fs11 from "fs";
2561
+ import * as fs12 from "fs";
2419
2562
  import * as os2 from "os";
2420
- import * as path10 from "path";
2563
+ import * as path11 from "path";
2421
2564
  function getPluginsCatalogRoot() {
2422
- const here = path10.dirname(new URL(import.meta.url).pathname);
2565
+ const here = path11.dirname(new URL(import.meta.url).pathname);
2423
2566
  const candidates = [
2424
- path10.join(here, "..", "plugins"),
2567
+ path11.join(here, "..", "plugins"),
2425
2568
  // dev: src/scripts → src/plugins
2426
- path10.join(here, "..", "..", "plugins"),
2569
+ path11.join(here, "..", "..", "plugins"),
2427
2570
  // built: dist/scripts → dist/plugins
2428
- path10.join(here, "..", "..", "src", "plugins")
2571
+ path11.join(here, "..", "..", "src", "plugins")
2429
2572
  // fallback
2430
2573
  ];
2431
2574
  for (const c of candidates) {
2432
- if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2575
+ if (fs12.existsSync(c) && fs12.statSync(c).isDirectory()) return c;
2433
2576
  }
2434
2577
  return candidates[0];
2435
2578
  }
@@ -2439,52 +2582,52 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2439
2582
  if (!needsSynthetic) return;
2440
2583
  const catalog = getPluginsCatalogRoot();
2441
2584
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2442
- const root = path10.join(os2.tmpdir(), `kody-synth-${runId}`);
2443
- fs11.mkdirSync(path10.join(root, ".claude-plugin"), { recursive: true });
2585
+ const root = path11.join(os2.tmpdir(), `kody-synth-${runId}`);
2586
+ fs12.mkdirSync(path11.join(root, ".claude-plugin"), { recursive: true });
2444
2587
  const resolvePart = (bucket, entry) => {
2445
- const local = path10.join(profile.dir, bucket, entry);
2446
- if (fs11.existsSync(local)) return local;
2447
- const central = path10.join(catalog, bucket, entry);
2448
- if (fs11.existsSync(central)) return central;
2588
+ const local = path11.join(profile.dir, bucket, entry);
2589
+ if (fs12.existsSync(local)) return local;
2590
+ const central = path11.join(catalog, bucket, entry);
2591
+ if (fs12.existsSync(central)) return central;
2449
2592
  throw new Error(
2450
2593
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2451
2594
  );
2452
2595
  };
2453
2596
  if (cc.skills.length > 0) {
2454
- const dst = path10.join(root, "skills");
2455
- fs11.mkdirSync(dst, { recursive: true });
2597
+ const dst = path11.join(root, "skills");
2598
+ fs12.mkdirSync(dst, { recursive: true });
2456
2599
  for (const name of cc.skills) {
2457
- copyDir(resolvePart("skills", name), path10.join(dst, name));
2600
+ copyDir(resolvePart("skills", name), path11.join(dst, name));
2458
2601
  }
2459
2602
  }
2460
2603
  if (cc.commands.length > 0) {
2461
- const dst = path10.join(root, "commands");
2462
- fs11.mkdirSync(dst, { recursive: true });
2604
+ const dst = path11.join(root, "commands");
2605
+ fs12.mkdirSync(dst, { recursive: true });
2463
2606
  for (const name of cc.commands) {
2464
- fs11.copyFileSync(resolvePart("commands", `${name}.md`), path10.join(dst, `${name}.md`));
2607
+ fs12.copyFileSync(resolvePart("commands", `${name}.md`), path11.join(dst, `${name}.md`));
2465
2608
  }
2466
2609
  }
2467
2610
  if (cc.subagents.length > 0) {
2468
- const dst = path10.join(root, "agents");
2469
- fs11.mkdirSync(dst, { recursive: true });
2611
+ const dst = path11.join(root, "agents");
2612
+ fs12.mkdirSync(dst, { recursive: true });
2470
2613
  for (const name of cc.subagents) {
2471
- fs11.copyFileSync(resolvePart("agents", `${name}.md`), path10.join(dst, `${name}.md`));
2614
+ fs12.copyFileSync(resolvePart("agents", `${name}.md`), path11.join(dst, `${name}.md`));
2472
2615
  }
2473
2616
  }
2474
2617
  if (cc.hooks.length > 0) {
2475
- const dst = path10.join(root, "hooks");
2476
- fs11.mkdirSync(dst, { recursive: true });
2618
+ const dst = path11.join(root, "hooks");
2619
+ fs12.mkdirSync(dst, { recursive: true });
2477
2620
  const merged = { hooks: {} };
2478
2621
  for (const name of cc.hooks) {
2479
2622
  const src = resolvePart("hooks", `${name}.json`);
2480
- const parsed = JSON.parse(fs11.readFileSync(src, "utf-8"));
2623
+ const parsed = JSON.parse(fs12.readFileSync(src, "utf-8"));
2481
2624
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2482
2625
  if (!Array.isArray(entries)) continue;
2483
2626
  if (!merged.hooks[event]) merged.hooks[event] = [];
2484
2627
  merged.hooks[event].push(...entries);
2485
2628
  }
2486
2629
  }
2487
- fs11.writeFileSync(path10.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2630
+ fs12.writeFileSync(path11.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2488
2631
  `);
2489
2632
  }
2490
2633
  const manifest = {
@@ -2495,17 +2638,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2495
2638
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2496
2639
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2497
2640
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
2498
- fs11.writeFileSync(path10.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2641
+ fs12.writeFileSync(path11.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2499
2642
  `);
2500
2643
  ctx.data.syntheticPluginPath = root;
2501
2644
  };
2502
2645
  function copyDir(src, dst) {
2503
- fs11.mkdirSync(dst, { recursive: true });
2504
- for (const ent of fs11.readdirSync(src, { withFileTypes: true })) {
2505
- const s = path10.join(src, ent.name);
2506
- const d = path10.join(dst, ent.name);
2646
+ fs12.mkdirSync(dst, { recursive: true });
2647
+ for (const ent of fs12.readdirSync(src, { withFileTypes: true })) {
2648
+ const s = path11.join(src, ent.name);
2649
+ const d = path11.join(dst, ent.name);
2507
2650
  if (ent.isDirectory()) copyDir(s, d);
2508
- else if (ent.isFile()) fs11.copyFileSync(s, d);
2651
+ else if (ent.isFile()) fs12.copyFileSync(s, d);
2509
2652
  }
2510
2653
  }
2511
2654
 
@@ -2570,123 +2713,6 @@ function formatMissesForFeedback(misses) {
2570
2713
  return lines.join("\n");
2571
2714
  }
2572
2715
 
2573
- // src/prompt.ts
2574
- import * as fs12 from "fs";
2575
- import * as path11 from "path";
2576
- var CONVENTIONS_PER_FILE_MAX_BYTES = 3e4;
2577
- var CONVENTION_FILES = ["CLAUDE.md", "AGENTS.md"];
2578
- function loadProjectConventions(projectDir) {
2579
- const out = [];
2580
- for (const rel of CONVENTION_FILES) {
2581
- const abs = path11.join(projectDir, rel);
2582
- if (!fs12.existsSync(abs)) continue;
2583
- let content;
2584
- try {
2585
- content = fs12.readFileSync(abs, "utf-8");
2586
- } catch {
2587
- continue;
2588
- }
2589
- const truncated = content.length > CONVENTIONS_PER_FILE_MAX_BYTES;
2590
- if (truncated) content = `${content.slice(0, CONVENTIONS_PER_FILE_MAX_BYTES)}
2591
-
2592
- \u2026 (truncated)`;
2593
- out.push({ path: rel, content, truncated });
2594
- }
2595
- return out;
2596
- }
2597
- function parseAgentResult(finalText) {
2598
- const text = (finalText || "").trim();
2599
- if (!text)
2600
- return {
2601
- done: false,
2602
- commitMessage: "",
2603
- prSummary: "",
2604
- feedbackActions: "",
2605
- planDeviations: "",
2606
- priorArt: "",
2607
- failureReason: "agent produced no final message",
2608
- markerMissing: false
2609
- };
2610
- const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
2611
- const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
2612
- const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
2613
- const failedMatch = text.match(FAILED_RE);
2614
- if (failedMatch) {
2615
- return {
2616
- done: false,
2617
- commitMessage: "",
2618
- prSummary: "",
2619
- feedbackActions: "",
2620
- planDeviations: "",
2621
- priorArt: "",
2622
- failureReason: stripMarkdownEmphasis(failedMatch[1]),
2623
- markerMissing: false
2624
- };
2625
- }
2626
- const hasDoneMarker = DONE_RE.test(text);
2627
- const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
2628
- const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
2629
- if (!hasDoneMarker && !hasCommitMsg && !hasPrSummary) {
2630
- const tail = text.length > 400 ? `\u2026${text.slice(-400)}` : text;
2631
- return {
2632
- done: false,
2633
- commitMessage: "",
2634
- prSummary: "",
2635
- feedbackActions: "",
2636
- planDeviations: "",
2637
- priorArt: "",
2638
- failureReason: `no DONE or FAILED marker in agent output \u2014 agent tail: ${tail}`,
2639
- markerMissing: true
2640
- };
2641
- }
2642
- const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2643
- const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2644
- const feedbackActions = extractBlock(
2645
- text,
2646
- /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2647
- /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2648
- );
2649
- let planDeviations = extractBlock(
2650
- text,
2651
- /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2652
- /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2653
- );
2654
- if (!planDeviations) {
2655
- const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2656
- if (inline) planDeviations = inline[1].trim();
2657
- }
2658
- let priorArt = "";
2659
- const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2660
- if (priorArtInline) priorArt = priorArtInline[1].trim();
2661
- const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2662
- let prSummary = "";
2663
- if (summaryStart !== -1) {
2664
- const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2665
- prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2666
- }
2667
- return {
2668
- done: true,
2669
- commitMessage,
2670
- prSummary,
2671
- feedbackActions,
2672
- planDeviations,
2673
- priorArt,
2674
- failureReason: "",
2675
- markerMissing: false
2676
- };
2677
- }
2678
- function stripMarkdownEmphasis(s) {
2679
- return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2680
- }
2681
- function extractBlock(text, startMarker, endMarker) {
2682
- const startIdx = text.search(startMarker);
2683
- if (startIdx === -1) return "";
2684
- const afterStart = text.slice(startIdx).replace(startMarker, "");
2685
- const endIdx = afterStart.search(endMarker);
2686
- const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2687
- return body.replace(/\n\s*```\s*$/g, "").trim();
2688
- }
2689
-
2690
2716
  // src/scripts/checkCoverageWithRetry.ts
2691
2717
  var checkCoverageWithRetry = async (ctx) => {
2692
2718
  const reqs = ctx.data.coverageRules ?? [];
@@ -4119,6 +4145,8 @@ function parseFlatYaml(text) {
4119
4145
  const value = stripQuotes(line.slice(colon + 1).trim());
4120
4146
  if (key === "every" && isScheduleEvery(value)) {
4121
4147
  out.every = value;
4148
+ } else if (key === "tickScript" && value.length > 0) {
4149
+ out.tickScript = value;
4122
4150
  }
4123
4151
  }
4124
4152
  return out;
@@ -4500,10 +4528,11 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4500
4528
  results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
4501
4529
  continue;
4502
4530
  }
4503
- process.stdout.write(`[jobs] \u2192 tick ${slug}
4531
+ const slugTarget = readTickScript(ctx.cwd, jobsDir, slug) ? "job-tick-scripted" : targetExecutable;
4532
+ process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
4504
4533
  `);
4505
4534
  try {
4506
- const out = await runExecutable(targetExecutable, {
4535
+ const out = await runExecutable(slugTarget, {
4507
4536
  cliArgs: { [slugArg]: slug },
4508
4537
  cwd: ctx.cwd,
4509
4538
  config: ctx.config,
@@ -4583,6 +4612,14 @@ function formatAgo(ms) {
4583
4612
  const day = Math.round(hr / 24);
4584
4613
  return `${day}d`;
4585
4614
  }
4615
+ function readTickScript(cwd, jobsDir, slug) {
4616
+ try {
4617
+ const raw = fs19.readFileSync(path18.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4618
+ return splitFrontmatter(raw).frontmatter.tickScript ?? null;
4619
+ } catch {
4620
+ return null;
4621
+ }
4622
+ }
4586
4623
  function listJobSlugs(absDir) {
4587
4624
  if (!fs19.existsSync(absDir)) return [];
4588
4625
  let entries;
@@ -6268,42 +6305,47 @@ function isPartialEnvelope2(x) {
6268
6305
  const o = x;
6269
6306
  return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
6270
6307
  }
6271
- var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
6272
- const fenceLabel = String(args?.fenceLabel ?? "");
6273
- if (!fenceLabel) {
6274
- throw new Error("parseJobStateFromAgentResult: `with.fenceLabel` is required");
6275
- }
6276
- if (!agentResult) {
6277
- ctx.data.nextStateParseError = "agent did not run";
6278
- return;
6279
- }
6308
+ function extractNextStateFromText(text, fenceLabel, prevRev) {
6280
6309
  const fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
6281
- const match = fenceRegex.exec(agentResult.finalText);
6310
+ const match = fenceRegex.exec(text);
6282
6311
  if (!match) {
6283
- ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
6284
- return;
6312
+ return { error: `missing \`${fenceLabel}\` fenced block` };
6285
6313
  }
6286
6314
  let parsed;
6287
6315
  try {
6288
6316
  parsed = JSON.parse(match[1].trim());
6289
6317
  } catch (err) {
6290
- ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
6291
- return;
6318
+ return { error: `state JSON parse error: ${err instanceof Error ? err.message : String(err)}` };
6292
6319
  }
6293
6320
  if (!isPartialEnvelope2(parsed)) {
6294
- ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
6295
- return;
6321
+ return { error: "state must be an object with string `cursor`, object `data`, and boolean `done`" };
6296
6322
  }
6297
- const loaded = ctx.data.jobState;
6298
- const prevRev = loaded?.state.rev ?? 0;
6299
- const next = {
6323
+ const envelope = {
6300
6324
  version: 1,
6301
6325
  rev: prevRev + 1,
6302
6326
  cursor: parsed.cursor,
6303
6327
  data: parsed.data,
6304
6328
  done: parsed.done
6305
6329
  };
6306
- ctx.data.nextJobState = next;
6330
+ return { envelope };
6331
+ }
6332
+ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
6333
+ const fenceLabel = String(args?.fenceLabel ?? "");
6334
+ if (!fenceLabel) {
6335
+ throw new Error("parseJobStateFromAgentResult: `with.fenceLabel` is required");
6336
+ }
6337
+ if (!agentResult) {
6338
+ ctx.data.nextStateParseError = "agent did not run";
6339
+ return;
6340
+ }
6341
+ const loaded = ctx.data.jobState;
6342
+ const prevRev = loaded?.state.rev ?? 0;
6343
+ const result = extractNextStateFromText(agentResult.finalText, fenceLabel, prevRev);
6344
+ if (result.error) {
6345
+ ctx.data.nextStateParseError = result.error.startsWith("missing `") ? `agent did not emit a \`${fenceLabel}\` fenced block` : result.error;
6346
+ return;
6347
+ }
6348
+ ctx.data.nextJobState = result.envelope;
6307
6349
  };
6308
6350
  function escapeRegex2(s) {
6309
6351
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
@@ -7189,6 +7231,75 @@ function resolveBaseFromLabels(labels) {
7189
7231
  return `goal-${goalId}`;
7190
7232
  }
7191
7233
 
7234
+ // src/scripts/runTickScript.ts
7235
+ import { spawnSync } from "child_process";
7236
+ import * as fs25 from "fs";
7237
+ import * as path23 from "path";
7238
+ var runTickScript = async (ctx, _profile, args) => {
7239
+ ctx.skipAgent = true;
7240
+ const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
7241
+ const slugArg = String(args?.slugArg ?? "job");
7242
+ const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
7243
+ const slug = String(ctx.args[slugArg] ?? "").trim();
7244
+ if (!slug) {
7245
+ ctx.output.exitCode = 99;
7246
+ ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7247
+ return;
7248
+ }
7249
+ const jobPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
7250
+ if (!fs25.existsSync(jobPath)) {
7251
+ ctx.output.exitCode = 99;
7252
+ ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7253
+ return;
7254
+ }
7255
+ const raw = fs25.readFileSync(jobPath, "utf-8");
7256
+ const { frontmatter } = splitFrontmatter(raw);
7257
+ const tickScript = frontmatter.tickScript;
7258
+ if (!tickScript) {
7259
+ ctx.output.exitCode = 99;
7260
+ ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7261
+ return;
7262
+ }
7263
+ const scriptPath = path23.isAbsolute(tickScript) ? tickScript : path23.join(ctx.cwd, tickScript);
7264
+ if (!fs25.existsSync(scriptPath)) {
7265
+ ctx.output.exitCode = 99;
7266
+ ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7267
+ return;
7268
+ }
7269
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
7270
+ const loaded = await backend.load(slug);
7271
+ ctx.data.jobSlug = slug;
7272
+ ctx.data.jobState = loaded;
7273
+ const result = spawnSync("bash", [scriptPath], {
7274
+ cwd: ctx.cwd,
7275
+ env: process.env,
7276
+ stdio: ["ignore", "pipe", "pipe"],
7277
+ encoding: "utf-8",
7278
+ timeout: 5 * 60 * 1e3
7279
+ });
7280
+ if (result.stdout) process.stdout.write(result.stdout);
7281
+ if (result.stderr) process.stderr.write(result.stderr);
7282
+ if (result.error) {
7283
+ ctx.output.exitCode = 99;
7284
+ ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
7285
+ return;
7286
+ }
7287
+ if (result.status !== 0) {
7288
+ ctx.output.exitCode = result.status ?? 99;
7289
+ ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
7290
+ return;
7291
+ }
7292
+ const prevRev = loaded.state.rev ?? 0;
7293
+ const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
7294
+ if (parsed.error) {
7295
+ ctx.data.nextStateParseError = parsed.error;
7296
+ ctx.output.exitCode = 1;
7297
+ ctx.output.reason = `runTickScript: ${parsed.error}`;
7298
+ return;
7299
+ }
7300
+ ctx.data.nextJobState = parsed.envelope;
7301
+ };
7302
+
7192
7303
  // src/scripts/saveTaskState.ts
7193
7304
  var saveTaskState = async (ctx, profile) => {
7194
7305
  const target = ctx.data.commentTargetType;
@@ -7971,7 +8082,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
7971
8082
  };
7972
8083
 
7973
8084
  // src/scripts/writeRunSummary.ts
7974
- import * as fs25 from "fs";
8085
+ import * as fs26 from "fs";
7975
8086
  var writeRunSummary = async (ctx, profile) => {
7976
8087
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
7977
8088
  if (!summaryPath) return;
@@ -7993,7 +8104,7 @@ var writeRunSummary = async (ctx, profile) => {
7993
8104
  if (reason) lines.push(`- **Reason:** ${reason}`);
7994
8105
  lines.push("");
7995
8106
  try {
7996
- fs25.appendFileSync(summaryPath, `${lines.join("\n")}
8107
+ fs26.appendFileSync(summaryPath, `${lines.join("\n")}
7997
8108
  `);
7998
8109
  } catch {
7999
8110
  }
@@ -8031,7 +8142,8 @@ var preflightScripts = {
8031
8142
  diagMcp,
8032
8143
  warmupMcp,
8033
8144
  dispatchJobTicks,
8034
- dispatchJobFileTicks
8145
+ dispatchJobFileTicks,
8146
+ runTickScript
8035
8147
  };
8036
8148
  var postflightScripts = {
8037
8149
  parseAgentResult: parseAgentResult2,
@@ -8180,9 +8292,9 @@ async function runExecutable(profileName, input) {
8180
8292
  data: {},
8181
8293
  output: { exitCode: 0 }
8182
8294
  };
8183
- const ndjsonDir = path23.join(input.cwd, ".kody");
8295
+ const ndjsonDir = path24.join(input.cwd, ".kody");
8184
8296
  const invokeAgent = async (prompt) => {
8185
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path23.isAbsolute(p) ? p : path23.resolve(profile.dir, p)).filter((p) => p.length > 0);
8297
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path24.isAbsolute(p) ? p : path24.resolve(profile.dir, p)).filter((p) => p.length > 0);
8186
8298
  const syntheticPath = ctx.data.syntheticPluginPath;
8187
8299
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
8188
8300
  return runAgent({
@@ -8228,6 +8340,7 @@ async function runExecutable(profileName, input) {
8228
8340
  return finish({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
8229
8341
  }
8230
8342
  agentResult = await invokeAgent(prompt);
8343
+ agentResult = await rescueMissingMarker(agentResult, invokeAgent);
8231
8344
  }
8232
8345
  for (const entry of profile.scripts.postflight) {
8233
8346
  const entryLabel = entry.script ?? entry.shell ?? "<unknown>";
@@ -8291,17 +8404,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
8291
8404
  function resolveProfilePath(profileName) {
8292
8405
  const found = resolveExecutable(profileName);
8293
8406
  if (found) return found;
8294
- const here = path23.dirname(new URL(import.meta.url).pathname);
8407
+ const here = path24.dirname(new URL(import.meta.url).pathname);
8295
8408
  const candidates = [
8296
- path23.join(here, "executables", profileName, "profile.json"),
8409
+ path24.join(here, "executables", profileName, "profile.json"),
8297
8410
  // same-dir sibling (dev)
8298
- path23.join(here, "..", "executables", profileName, "profile.json"),
8411
+ path24.join(here, "..", "executables", profileName, "profile.json"),
8299
8412
  // up one (prod: dist/bin → dist/executables)
8300
- path23.join(here, "..", "src", "executables", profileName, "profile.json")
8413
+ path24.join(here, "..", "src", "executables", profileName, "profile.json")
8301
8414
  // fallback
8302
8415
  ];
8303
8416
  for (const c of candidates) {
8304
- if (fs26.existsSync(c)) return c;
8417
+ if (fs27.existsSync(c)) return c;
8305
8418
  }
8306
8419
  return candidates[0];
8307
8420
  }
@@ -8405,8 +8518,8 @@ function resolveShellTimeoutMs(entry) {
8405
8518
  var SIGKILL_GRACE_MS = 5e3;
8406
8519
  async function runShellEntry(entry, ctx, profile) {
8407
8520
  const shellName = entry.shell;
8408
- const shellPath = path23.join(profile.dir, shellName);
8409
- if (!fs26.existsSync(shellPath)) {
8521
+ const shellPath = path24.join(profile.dir, shellName);
8522
+ if (!fs27.existsSync(shellPath)) {
8410
8523
  ctx.skipAgent = true;
8411
8524
  ctx.output.exitCode = 99;
8412
8525
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8819,9 +8932,9 @@ function resolveAuthToken(env = process.env) {
8819
8932
  return token;
8820
8933
  }
8821
8934
  function detectPackageManager2(cwd) {
8822
- if (fs27.existsSync(path24.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8823
- if (fs27.existsSync(path24.join(cwd, "yarn.lock"))) return "yarn";
8824
- if (fs27.existsSync(path24.join(cwd, "bun.lockb"))) return "bun";
8935
+ if (fs28.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8936
+ if (fs28.existsSync(path25.join(cwd, "yarn.lock"))) return "yarn";
8937
+ if (fs28.existsSync(path25.join(cwd, "bun.lockb"))) return "bun";
8825
8938
  return "npm";
8826
8939
  }
8827
8940
  function shellOut(cmd, args, cwd, stream = true) {
@@ -8908,11 +9021,11 @@ function configureGitIdentity(cwd) {
8908
9021
  }
8909
9022
  function postFailureTail(issueNumber, cwd, reason) {
8910
9023
  if (!issueNumber) return;
8911
- const logPath = path24.join(cwd, ".kody", "last-run.jsonl");
9024
+ const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
8912
9025
  let tail = "";
8913
9026
  try {
8914
- if (fs27.existsSync(logPath)) {
8915
- const content = fs27.readFileSync(logPath, "utf-8");
9027
+ if (fs28.existsSync(logPath)) {
9028
+ const content = fs28.readFileSync(logPath, "utf-8");
8916
9029
  tail = content.slice(-3e3);
8917
9030
  }
8918
9031
  } catch {
@@ -8937,7 +9050,7 @@ async function runCi(argv) {
8937
9050
  return 0;
8938
9051
  }
8939
9052
  const args = parseCiArgs(argv);
8940
- const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
9053
+ const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
8941
9054
  let earlyConfig;
8942
9055
  try {
8943
9056
  earlyConfig = loadConfig(cwd);
@@ -8947,9 +9060,9 @@ async function runCi(argv) {
8947
9060
  const eventName = process.env.GITHUB_EVENT_NAME;
8948
9061
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
8949
9062
  let manualWorkflowDispatch = false;
8950
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs27.existsSync(dispatchEventPath)) {
9063
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
8951
9064
  try {
8952
- const evt = JSON.parse(fs27.readFileSync(dispatchEventPath, "utf-8"));
9065
+ const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
8953
9066
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
8954
9067
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
8955
9068
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -9164,9 +9277,9 @@ function parseChatArgs(argv, env = process.env) {
9164
9277
  return result;
9165
9278
  }
9166
9279
  function commitChatFiles(cwd, sessionId, verbose) {
9167
- const sessionFile = path25.relative(cwd, sessionFilePath(cwd, sessionId));
9168
- const eventsFile = path25.relative(cwd, eventsFilePath(cwd, sessionId));
9169
- const paths = [sessionFile, eventsFile].filter((p) => fs28.existsSync(path25.join(cwd, p)));
9280
+ const sessionFile = path26.relative(cwd, sessionFilePath(cwd, sessionId));
9281
+ const eventsFile = path26.relative(cwd, eventsFilePath(cwd, sessionId));
9282
+ const paths = [sessionFile, eventsFile].filter((p) => fs29.existsSync(path26.join(cwd, p)));
9170
9283
  if (paths.length === 0) return;
9171
9284
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
9172
9285
  try {
@@ -9204,7 +9317,7 @@ async function runChat(argv) {
9204
9317
  ${CHAT_HELP}`);
9205
9318
  return 64;
9206
9319
  }
9207
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9320
+ const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
9208
9321
  const sessionId = args.sessionId;
9209
9322
  const unpackedSecrets = unpackAllSecrets();
9210
9323
  if (unpackedSecrets > 0) {
@@ -9256,7 +9369,7 @@ ${CHAT_HELP}`);
9256
9369
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
9257
9370
  const meta = readMeta(sessionFile);
9258
9371
  process.stdout.write(
9259
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs28.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9372
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9260
9373
  `
9261
9374
  );
9262
9375
  try {
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "job-tick-scripted",
3
+ "role": "utility",
4
+ "describe": "Deterministic job tick: runs the slug's `tickScript:` (declared in job frontmatter), parses next-state from its stdout, persists. No agent.",
5
+ "kind": "oneshot",
6
+ "inputs": [
7
+ {
8
+ "name": "job",
9
+ "flag": "--job",
10
+ "type": "string",
11
+ "required": true,
12
+ "describe": "Job slug — basename (without .md) of the file under .kody/jobs/."
13
+ },
14
+ {
15
+ "name": "force",
16
+ "flag": "--force",
17
+ "type": "bool",
18
+ "describe": "Accepted for parity with `job-tick`. Scripted ticks have no agent cadence guard to bypass — the dispatcher already gated on frontmatter `every:`. Forwarded to the script via env if it cares."
19
+ }
20
+ ],
21
+ "claudeCode": {
22
+ "model": "inherit",
23
+ "permissionMode": "default",
24
+ "maxTurns": null,
25
+ "maxThinkingTokens": null,
26
+ "systemPromptAppend": null,
27
+ "tools": [],
28
+ "hooks": [],
29
+ "skills": [],
30
+ "commands": [],
31
+ "subagents": [],
32
+ "plugins": [],
33
+ "mcpServers": []
34
+ },
35
+ "cliTools": [
36
+ {
37
+ "name": "gh",
38
+ "install": {
39
+ "required": true,
40
+ "checkCommand": "command -v gh"
41
+ },
42
+ "verify": "gh auth status",
43
+ "usage": "Available to the tickScript via PATH; this executable shells out to `bash <tickScript>`. Scripts use `gh pr list`, `gh pr comment`, etc.",
44
+ "allowedUses": ["pr", "api", "issue"]
45
+ }
46
+ ],
47
+ "inputArtifacts": [],
48
+ "outputArtifacts": [],
49
+ "scripts": {
50
+ "preflight": [
51
+ {
52
+ "script": "runTickScript",
53
+ "with": {
54
+ "jobsDir": ".kody/jobs",
55
+ "slugArg": "job",
56
+ "fenceLabel": "kody-job-next-state"
57
+ }
58
+ }
59
+ ],
60
+ "postflight": [
61
+ {
62
+ "script": "writeJobStateFile",
63
+ "with": {
64
+ "jobsDir": ".kody/jobs"
65
+ }
66
+ }
67
+ ]
68
+ }
69
+ }
@@ -88,12 +88,13 @@ For EACH file you will change or create, include:
88
88
  - Current state — what's there today (function/class/export names, relevant line ranges). Skip for new files.
89
89
  - Target state — what will be there after the change, at the same level of specificity.
90
90
  - Exact locations of edits (function name, line range if stable, or anchor like "after the `meta` group field, before the closing `fields: []`").
91
- - For new files: rough shape including exports, key functions with signatures, and top-level module comment.
91
+ - For new files: rough shape including exports, key functions with signatures, and top-level module comment. **Do not paste full function bodies** — signatures and 1–2 sentence intent per export are enough for an implementer to write the body. Single-line type/interface declarations and short config snippets are fine.
92
92
  - Dependencies touched (imports added/removed, new packages) — call out if anything needs installing.
93
93
 
94
94
  ## Algorithms & pseudocode
95
95
  REQUIRED for any non-trivial logic (sorting, diffing, state transitions, concurrency, batching, caching, conflict resolution).
96
96
  - Write pseudocode (not production code) showing the actual algorithm — inputs, steps, outputs.
97
+ - Pseudocode ≤ ~20 lines per algorithm. If it grows past that, the algorithm needs decomposing, not more lines.
97
98
  - Call out invariants the algorithm preserves.
98
99
  - Call out complexity (N swaps vs N-squared recalc vs single-batch write).
99
100
  - If there's a choice between two algorithms, explain why you picked this one.
@@ -150,5 +151,6 @@ No filler. No marketing language. Depth over brevity.>
150
151
  - Read-only. Do NOT modify any file.
151
152
  - Do NOT run git or gh commands.
152
153
  - No speculative scope — plan only what the issue asks for, but plan it THOROUGHLY.
154
+ - **Plan length ≤ ~1500 lines / ~15k tokens.** Larger plans get truncated by output token caps before the closing `DONE` marker — and a truncated plan is worse than a smaller one. If a feature legitimately needs more, output `FAILED: scope too large for single plan — split into <list of sub-issues>` instead of overrunning.
153
155
  - If the issue is ambiguous and you cannot make progress without input, output `FAILED: <what's unclear>` instead of a plan.
154
156
  - If the Research floor cannot be met because required files are missing or unreadable, output `FAILED: <what could not be read>` instead of a half-blind plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.26",
3
+ "version": "0.4.28",
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",