@kody-ade/kody-engine 0.4.27 → 0.4.29

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.27",
6
+ version: "0.4.29",
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;
@@ -4493,17 +4521,19 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4493
4521
  const results = [];
4494
4522
  const now = Date.now();
4495
4523
  for (const slug of slugs) {
4496
- const decision = await decideShouldFire(ctx.cwd, jobsDir, slug, backend, now);
4524
+ const frontmatter = readJobFrontmatter(ctx.cwd, jobsDir, slug);
4525
+ const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
4497
4526
  if (decision.skip) {
4498
4527
  process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
4499
4528
  `);
4500
4529
  results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
4501
4530
  continue;
4502
4531
  }
4503
- process.stdout.write(`[jobs] \u2192 tick ${slug}
4532
+ const slugTarget = frontmatter.tickScript ? "job-tick-scripted" : targetExecutable;
4533
+ process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
4504
4534
  `);
4505
4535
  try {
4506
- const out = await runExecutable(targetExecutable, {
4536
+ const out = await runExecutable(slugTarget, {
4507
4537
  cliArgs: { [slugArg]: slug },
4508
4538
  cwd: ctx.cwd,
4509
4539
  config: ctx.config,
@@ -4536,14 +4566,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4536
4566
  }
4537
4567
  }
4538
4568
  };
4539
- async function decideShouldFire(cwd, jobsDir, slug, backend, now) {
4540
- let every;
4541
- try {
4542
- const raw = fs19.readFileSync(path18.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4543
- every = splitFrontmatter(raw).frontmatter.every;
4544
- } catch {
4545
- return { skip: false, reason: "frontmatter unreadable" };
4546
- }
4569
+ async function decideShouldFire(every, slug, backend, now) {
4547
4570
  if (!every) return { skip: false, reason: "no schedule (every cron tick)" };
4548
4571
  if (every === "manual") {
4549
4572
  return { skip: true, reason: "manual-only (no auto-fire; trigger via dashboard Run now)" };
@@ -4583,6 +4606,14 @@ function formatAgo(ms) {
4583
4606
  const day = Math.round(hr / 24);
4584
4607
  return `${day}d`;
4585
4608
  }
4609
+ function readJobFrontmatter(cwd, jobsDir, slug) {
4610
+ try {
4611
+ const raw = fs19.readFileSync(path18.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4612
+ return splitFrontmatter(raw).frontmatter;
4613
+ } catch {
4614
+ return {};
4615
+ }
4616
+ }
4586
4617
  function listJobSlugs(absDir) {
4587
4618
  if (!fs19.existsSync(absDir)) return [];
4588
4619
  let entries;
@@ -6268,42 +6299,47 @@ function isPartialEnvelope2(x) {
6268
6299
  const o = x;
6269
6300
  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
6301
  }
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
- }
6302
+ function extractNextStateFromText(text, fenceLabel, prevRev) {
6280
6303
  const fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
6281
- const match = fenceRegex.exec(agentResult.finalText);
6304
+ const match = fenceRegex.exec(text);
6282
6305
  if (!match) {
6283
- ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
6284
- return;
6306
+ return { error: `missing \`${fenceLabel}\` fenced block` };
6285
6307
  }
6286
6308
  let parsed;
6287
6309
  try {
6288
6310
  parsed = JSON.parse(match[1].trim());
6289
6311
  } catch (err) {
6290
- ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
6291
- return;
6312
+ return { error: `state JSON parse error: ${err instanceof Error ? err.message : String(err)}` };
6292
6313
  }
6293
6314
  if (!isPartialEnvelope2(parsed)) {
6294
- ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
6295
- return;
6315
+ return { error: "state must be an object with string `cursor`, object `data`, and boolean `done`" };
6296
6316
  }
6297
- const loaded = ctx.data.jobState;
6298
- const prevRev = loaded?.state.rev ?? 0;
6299
- const next = {
6317
+ const envelope = {
6300
6318
  version: 1,
6301
6319
  rev: prevRev + 1,
6302
6320
  cursor: parsed.cursor,
6303
6321
  data: parsed.data,
6304
6322
  done: parsed.done
6305
6323
  };
6306
- ctx.data.nextJobState = next;
6324
+ return { envelope };
6325
+ }
6326
+ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
6327
+ const fenceLabel = String(args?.fenceLabel ?? "");
6328
+ if (!fenceLabel) {
6329
+ throw new Error("parseJobStateFromAgentResult: `with.fenceLabel` is required");
6330
+ }
6331
+ if (!agentResult) {
6332
+ ctx.data.nextStateParseError = "agent did not run";
6333
+ return;
6334
+ }
6335
+ const loaded = ctx.data.jobState;
6336
+ const prevRev = loaded?.state.rev ?? 0;
6337
+ const result = extractNextStateFromText(agentResult.finalText, fenceLabel, prevRev);
6338
+ if (result.error) {
6339
+ ctx.data.nextStateParseError = result.error.startsWith("missing `") ? `agent did not emit a \`${fenceLabel}\` fenced block` : result.error;
6340
+ return;
6341
+ }
6342
+ ctx.data.nextJobState = result.envelope;
6307
6343
  };
6308
6344
  function escapeRegex2(s) {
6309
6345
  return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
@@ -7189,6 +7225,137 @@ function resolveBaseFromLabels(labels) {
7189
7225
  return `goal-${goalId}`;
7190
7226
  }
7191
7227
 
7228
+ // src/scripts/runTickScript.ts
7229
+ import { spawnSync } from "child_process";
7230
+ import * as fs25 from "fs";
7231
+ import * as path23 from "path";
7232
+ var runTickScript = async (ctx, _profile, args) => {
7233
+ ctx.skipAgent = true;
7234
+ const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
7235
+ const slugArg = String(args?.slugArg ?? "job");
7236
+ const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
7237
+ const slug = String(ctx.args[slugArg] ?? "").trim();
7238
+ if (!slug) {
7239
+ ctx.output.exitCode = 99;
7240
+ ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7241
+ return;
7242
+ }
7243
+ const jobPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
7244
+ if (!fs25.existsSync(jobPath)) {
7245
+ ctx.output.exitCode = 99;
7246
+ ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7247
+ return;
7248
+ }
7249
+ const raw = fs25.readFileSync(jobPath, "utf-8");
7250
+ const { frontmatter } = splitFrontmatter(raw);
7251
+ const tickScript = frontmatter.tickScript;
7252
+ if (!tickScript) {
7253
+ ctx.output.exitCode = 99;
7254
+ ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7255
+ return;
7256
+ }
7257
+ const scriptPath = path23.isAbsolute(tickScript) ? tickScript : path23.join(ctx.cwd, tickScript);
7258
+ if (!fs25.existsSync(scriptPath)) {
7259
+ ctx.output.exitCode = 99;
7260
+ ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7261
+ return;
7262
+ }
7263
+ const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
7264
+ let loaded;
7265
+ try {
7266
+ loaded = await backend.load(slug);
7267
+ } catch (err) {
7268
+ ctx.output.exitCode = 99;
7269
+ ctx.output.reason = `runTickScript: state load failed: ${err instanceof Error ? err.message : String(err)}`;
7270
+ return;
7271
+ }
7272
+ ctx.data.jobSlug = slug;
7273
+ ctx.data.jobState = loaded;
7274
+ const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
7275
+ const result = spawnSync("bash", [scriptPath], {
7276
+ cwd: ctx.cwd,
7277
+ env: childEnv,
7278
+ stdio: ["ignore", "pipe", "pipe"],
7279
+ encoding: "utf-8",
7280
+ timeout: 5 * 60 * 1e3,
7281
+ // Default maxBuffer is 1MB — a chatty `gh pr list --json …` over a
7282
+ // busy repo (or an accidental `set -x`) can blow that and silently
7283
+ // truncate stdout, which is the exact "silent state drop" failure
7284
+ // mode this whole executable was written to prevent. 16MB is well
7285
+ // above any realistic tick output.
7286
+ maxBuffer: 16 * 1024 * 1024
7287
+ });
7288
+ if (result.stdout) process.stdout.write(result.stdout);
7289
+ if (result.stderr) process.stderr.write(result.stderr);
7290
+ if (result.error) {
7291
+ ctx.output.exitCode = 99;
7292
+ ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
7293
+ return;
7294
+ }
7295
+ if (result.signal) {
7296
+ ctx.output.exitCode = 124;
7297
+ ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
7298
+ return;
7299
+ }
7300
+ if (result.status !== 0) {
7301
+ ctx.output.exitCode = result.status ?? 99;
7302
+ ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
7303
+ return;
7304
+ }
7305
+ const prevRev = loaded.state.rev ?? 0;
7306
+ const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
7307
+ if (parsed.error) {
7308
+ ctx.data.nextStateParseError = parsed.error;
7309
+ ctx.output.exitCode = 1;
7310
+ ctx.output.reason = `runTickScript: ${parsed.error}`;
7311
+ return;
7312
+ }
7313
+ ctx.data.nextJobState = parsed.envelope;
7314
+ };
7315
+ function buildChildEnv(parent, force) {
7316
+ const allow = /* @__PURE__ */ new Set([
7317
+ "PATH",
7318
+ "HOME",
7319
+ "USER",
7320
+ "LOGNAME",
7321
+ "SHELL",
7322
+ "TMPDIR",
7323
+ "LANG",
7324
+ "LC_ALL",
7325
+ "TERM",
7326
+ // GitHub auth — `gh` reads these.
7327
+ "GH_TOKEN",
7328
+ "GH_PAT",
7329
+ "GITHUB_TOKEN",
7330
+ // CI metadata commonly read by tick scripts (`gh repo view`,
7331
+ // workflow run links, etc.). All public values from GitHub Actions.
7332
+ "GITHUB_ACTIONS",
7333
+ "GITHUB_ACTOR",
7334
+ "GITHUB_REPOSITORY",
7335
+ "GITHUB_REPOSITORY_OWNER",
7336
+ "GITHUB_REF",
7337
+ "GITHUB_SHA",
7338
+ "GITHUB_RUN_ID",
7339
+ "GITHUB_RUN_NUMBER",
7340
+ "GITHUB_WORKFLOW",
7341
+ "GITHUB_JOB",
7342
+ "GITHUB_SERVER_URL",
7343
+ "GITHUB_API_URL",
7344
+ "GITHUB_EVENT_NAME",
7345
+ "RUNNER_OS",
7346
+ "RUNNER_ARCH"
7347
+ ]);
7348
+ const out = {};
7349
+ for (const [key, value] of Object.entries(parent)) {
7350
+ if (value === void 0) continue;
7351
+ if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) {
7352
+ out[key] = value;
7353
+ }
7354
+ }
7355
+ if (force) out.KODY_FORCE = "1";
7356
+ return out;
7357
+ }
7358
+
7192
7359
  // src/scripts/saveTaskState.ts
7193
7360
  var saveTaskState = async (ctx, profile) => {
7194
7361
  const target = ctx.data.commentTargetType;
@@ -7971,7 +8138,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
7971
8138
  };
7972
8139
 
7973
8140
  // src/scripts/writeRunSummary.ts
7974
- import * as fs25 from "fs";
8141
+ import * as fs26 from "fs";
7975
8142
  var writeRunSummary = async (ctx, profile) => {
7976
8143
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
7977
8144
  if (!summaryPath) return;
@@ -7993,7 +8160,7 @@ var writeRunSummary = async (ctx, profile) => {
7993
8160
  if (reason) lines.push(`- **Reason:** ${reason}`);
7994
8161
  lines.push("");
7995
8162
  try {
7996
- fs25.appendFileSync(summaryPath, `${lines.join("\n")}
8163
+ fs26.appendFileSync(summaryPath, `${lines.join("\n")}
7997
8164
  `);
7998
8165
  } catch {
7999
8166
  }
@@ -8031,7 +8198,8 @@ var preflightScripts = {
8031
8198
  diagMcp,
8032
8199
  warmupMcp,
8033
8200
  dispatchJobTicks,
8034
- dispatchJobFileTicks
8201
+ dispatchJobFileTicks,
8202
+ runTickScript
8035
8203
  };
8036
8204
  var postflightScripts = {
8037
8205
  parseAgentResult: parseAgentResult2,
@@ -8180,9 +8348,9 @@ async function runExecutable(profileName, input) {
8180
8348
  data: {},
8181
8349
  output: { exitCode: 0 }
8182
8350
  };
8183
- const ndjsonDir = path23.join(input.cwd, ".kody");
8351
+ const ndjsonDir = path24.join(input.cwd, ".kody");
8184
8352
  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);
8353
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path24.isAbsolute(p) ? p : path24.resolve(profile.dir, p)).filter((p) => p.length > 0);
8186
8354
  const syntheticPath = ctx.data.syntheticPluginPath;
8187
8355
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
8188
8356
  return runAgent({
@@ -8228,6 +8396,7 @@ async function runExecutable(profileName, input) {
8228
8396
  return finish({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
8229
8397
  }
8230
8398
  agentResult = await invokeAgent(prompt);
8399
+ agentResult = await rescueMissingMarker(agentResult, invokeAgent);
8231
8400
  }
8232
8401
  for (const entry of profile.scripts.postflight) {
8233
8402
  const entryLabel = entry.script ?? entry.shell ?? "<unknown>";
@@ -8291,17 +8460,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
8291
8460
  function resolveProfilePath(profileName) {
8292
8461
  const found = resolveExecutable(profileName);
8293
8462
  if (found) return found;
8294
- const here = path23.dirname(new URL(import.meta.url).pathname);
8463
+ const here = path24.dirname(new URL(import.meta.url).pathname);
8295
8464
  const candidates = [
8296
- path23.join(here, "executables", profileName, "profile.json"),
8465
+ path24.join(here, "executables", profileName, "profile.json"),
8297
8466
  // same-dir sibling (dev)
8298
- path23.join(here, "..", "executables", profileName, "profile.json"),
8467
+ path24.join(here, "..", "executables", profileName, "profile.json"),
8299
8468
  // up one (prod: dist/bin → dist/executables)
8300
- path23.join(here, "..", "src", "executables", profileName, "profile.json")
8469
+ path24.join(here, "..", "src", "executables", profileName, "profile.json")
8301
8470
  // fallback
8302
8471
  ];
8303
8472
  for (const c of candidates) {
8304
- if (fs26.existsSync(c)) return c;
8473
+ if (fs27.existsSync(c)) return c;
8305
8474
  }
8306
8475
  return candidates[0];
8307
8476
  }
@@ -8405,8 +8574,8 @@ function resolveShellTimeoutMs(entry) {
8405
8574
  var SIGKILL_GRACE_MS = 5e3;
8406
8575
  async function runShellEntry(entry, ctx, profile) {
8407
8576
  const shellName = entry.shell;
8408
- const shellPath = path23.join(profile.dir, shellName);
8409
- if (!fs26.existsSync(shellPath)) {
8577
+ const shellPath = path24.join(profile.dir, shellName);
8578
+ if (!fs27.existsSync(shellPath)) {
8410
8579
  ctx.skipAgent = true;
8411
8580
  ctx.output.exitCode = 99;
8412
8581
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8819,9 +8988,9 @@ function resolveAuthToken(env = process.env) {
8819
8988
  return token;
8820
8989
  }
8821
8990
  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";
8991
+ if (fs28.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8992
+ if (fs28.existsSync(path25.join(cwd, "yarn.lock"))) return "yarn";
8993
+ if (fs28.existsSync(path25.join(cwd, "bun.lockb"))) return "bun";
8825
8994
  return "npm";
8826
8995
  }
8827
8996
  function shellOut(cmd, args, cwd, stream = true) {
@@ -8908,11 +9077,11 @@ function configureGitIdentity(cwd) {
8908
9077
  }
8909
9078
  function postFailureTail(issueNumber, cwd, reason) {
8910
9079
  if (!issueNumber) return;
8911
- const logPath = path24.join(cwd, ".kody", "last-run.jsonl");
9080
+ const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
8912
9081
  let tail = "";
8913
9082
  try {
8914
- if (fs27.existsSync(logPath)) {
8915
- const content = fs27.readFileSync(logPath, "utf-8");
9083
+ if (fs28.existsSync(logPath)) {
9084
+ const content = fs28.readFileSync(logPath, "utf-8");
8916
9085
  tail = content.slice(-3e3);
8917
9086
  }
8918
9087
  } catch {
@@ -8937,7 +9106,7 @@ async function runCi(argv) {
8937
9106
  return 0;
8938
9107
  }
8939
9108
  const args = parseCiArgs(argv);
8940
- const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
9109
+ const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
8941
9110
  let earlyConfig;
8942
9111
  try {
8943
9112
  earlyConfig = loadConfig(cwd);
@@ -8947,9 +9116,9 @@ async function runCi(argv) {
8947
9116
  const eventName = process.env.GITHUB_EVENT_NAME;
8948
9117
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
8949
9118
  let manualWorkflowDispatch = false;
8950
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs27.existsSync(dispatchEventPath)) {
9119
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
8951
9120
  try {
8952
- const evt = JSON.parse(fs27.readFileSync(dispatchEventPath, "utf-8"));
9121
+ const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
8953
9122
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
8954
9123
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
8955
9124
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -9164,9 +9333,9 @@ function parseChatArgs(argv, env = process.env) {
9164
9333
  return result;
9165
9334
  }
9166
9335
  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)));
9336
+ const sessionFile = path26.relative(cwd, sessionFilePath(cwd, sessionId));
9337
+ const eventsFile = path26.relative(cwd, eventsFilePath(cwd, sessionId));
9338
+ const paths = [sessionFile, eventsFile].filter((p) => fs29.existsSync(path26.join(cwd, p)));
9170
9339
  if (paths.length === 0) return;
9171
9340
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
9172
9341
  try {
@@ -9204,7 +9373,7 @@ async function runChat(argv) {
9204
9373
  ${CHAT_HELP}`);
9205
9374
  return 64;
9206
9375
  }
9207
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9376
+ const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
9208
9377
  const sessionId = args.sessionId;
9209
9378
  const unpackedSecrets = unpackAllSecrets();
9210
9379
  if (unpackedSecrets > 0) {
@@ -9256,7 +9425,7 @@ ${CHAT_HELP}`);
9256
9425
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
9257
9426
  const meta = readMeta(sessionFile);
9258
9427
  process.stdout.write(
9259
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs28.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9428
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9260
9429
  `
9261
9430
  );
9262
9431
  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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.27",
3
+ "version": "0.4.29",
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",