@kody-ade/kody-engine 0.4.29 → 0.4.31

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.29",
6
+ version: "0.4.31",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -51,9 +51,9 @@ var package_default = {
51
51
  };
52
52
 
53
53
  // src/chat-cli.ts
54
- import { execFileSync as execFileSync30 } from "child_process";
55
- import * as fs29 from "fs";
56
- import * as path26 from "path";
54
+ import { execFileSync as execFileSync32 } from "child_process";
55
+ import * as fs30 from "fs";
56
+ import * as path28 from "path";
57
57
 
58
58
  // src/chat/events.ts
59
59
  import * as fs from "fs";
@@ -912,9 +912,9 @@ async function emit2(sink, type, sessionId, suffix, payload) {
912
912
  }
913
913
 
914
914
  // src/kody-cli.ts
915
- import { execFileSync as execFileSync29 } from "child_process";
916
- import * as fs28 from "fs";
917
- import * as path25 from "path";
915
+ import { execFileSync as execFileSync31 } from "child_process";
916
+ import * as fs29 from "fs";
917
+ import * as path27 from "path";
918
918
 
919
919
  // src/dispatch.ts
920
920
  import * as fs7 from "fs";
@@ -1299,9 +1299,9 @@ function coerceBare(spec, value) {
1299
1299
  }
1300
1300
 
1301
1301
  // src/executor.ts
1302
- import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1303
- import * as fs27 from "fs";
1304
- import * as path24 from "path";
1302
+ import { execFileSync as execFileSync30, spawn as spawn5 } from "child_process";
1303
+ import * as fs28 from "fs";
1304
+ import * as path26 from "path";
1305
1305
 
1306
1306
  // src/issue.ts
1307
1307
  import { execFileSync as execFileSync3 } from "child_process";
@@ -1977,153 +1977,10 @@ function stripBlockingEnv(env) {
1977
1977
  return out;
1978
1978
  }
1979
1979
 
1980
- // src/prompt.ts
1981
- import * as fs10 from "fs";
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
1980
  // src/commit.ts
2124
1981
  import { execFileSync as execFileSync5 } from "child_process";
2125
- import * as fs11 from "fs";
2126
- import * as path10 from "path";
1982
+ import * as fs10 from "fs";
1983
+ import * as path9 from "path";
2127
1984
  var FORBIDDEN_PATH_PREFIXES = [
2128
1985
  ".kody/",
2129
1986
  ".kody-engine/",
@@ -2179,18 +2036,18 @@ function tryGit(args, cwd) {
2179
2036
  }
2180
2037
  function abortUnfinishedGitOps(cwd) {
2181
2038
  const aborted = [];
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"))) {
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"))) {
2185
2042
  if (tryGit(["merge", "--abort"], cwd)) aborted.push("merge");
2186
2043
  }
2187
- if (fs11.existsSync(path10.join(gitDir, "CHERRY_PICK_HEAD"))) {
2044
+ if (fs10.existsSync(path9.join(gitDir, "CHERRY_PICK_HEAD"))) {
2188
2045
  if (tryGit(["cherry-pick", "--abort"], cwd)) aborted.push("cherry-pick");
2189
2046
  }
2190
- if (fs11.existsSync(path10.join(gitDir, "REVERT_HEAD"))) {
2047
+ if (fs10.existsSync(path9.join(gitDir, "REVERT_HEAD"))) {
2191
2048
  if (tryGit(["revert", "--abort"], cwd)) aborted.push("revert");
2192
2049
  }
2193
- if (fs11.existsSync(path10.join(gitDir, "rebase-merge")) || fs11.existsSync(path10.join(gitDir, "rebase-apply"))) {
2050
+ if (fs10.existsSync(path9.join(gitDir, "rebase-merge")) || fs10.existsSync(path9.join(gitDir, "rebase-apply"))) {
2194
2051
  if (tryGit(["rebase", "--abort"], cwd)) aborted.push("rebase");
2195
2052
  }
2196
2053
  try {
@@ -2246,7 +2103,7 @@ function normalizeCommitMessage(raw) {
2246
2103
  function commitAndPush(branch, agentMessage, cwd) {
2247
2104
  const allChanged = listChangedFiles(cwd);
2248
2105
  const allowedFiles = allChanged.filter((f) => !isForbiddenPath(f));
2249
- const mergeHeadExists = fs11.existsSync(path10.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2106
+ const mergeHeadExists = fs10.existsSync(path9.join(cwd ?? process.cwd(), ".git", "MERGE_HEAD"));
2250
2107
  if (allowedFiles.length === 0 && !mergeHeadExists) {
2251
2108
  return { committed: false, pushed: false, sha: "", message: "" };
2252
2109
  }
@@ -2558,21 +2415,21 @@ var advanceFlow = async (ctx, profile) => {
2558
2415
  };
2559
2416
 
2560
2417
  // src/scripts/buildSyntheticPlugin.ts
2561
- import * as fs12 from "fs";
2418
+ import * as fs11 from "fs";
2562
2419
  import * as os2 from "os";
2563
- import * as path11 from "path";
2420
+ import * as path10 from "path";
2564
2421
  function getPluginsCatalogRoot() {
2565
- const here = path11.dirname(new URL(import.meta.url).pathname);
2422
+ const here = path10.dirname(new URL(import.meta.url).pathname);
2566
2423
  const candidates = [
2567
- path11.join(here, "..", "plugins"),
2424
+ path10.join(here, "..", "plugins"),
2568
2425
  // dev: src/scripts → src/plugins
2569
- path11.join(here, "..", "..", "plugins"),
2426
+ path10.join(here, "..", "..", "plugins"),
2570
2427
  // built: dist/scripts → dist/plugins
2571
- path11.join(here, "..", "..", "src", "plugins")
2428
+ path10.join(here, "..", "..", "src", "plugins")
2572
2429
  // fallback
2573
2430
  ];
2574
2431
  for (const c of candidates) {
2575
- if (fs12.existsSync(c) && fs12.statSync(c).isDirectory()) return c;
2432
+ if (fs11.existsSync(c) && fs11.statSync(c).isDirectory()) return c;
2576
2433
  }
2577
2434
  return candidates[0];
2578
2435
  }
@@ -2582,52 +2439,52 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2582
2439
  if (!needsSynthetic) return;
2583
2440
  const catalog = getPluginsCatalogRoot();
2584
2441
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
2585
- const root = path11.join(os2.tmpdir(), `kody-synth-${runId}`);
2586
- fs12.mkdirSync(path11.join(root, ".claude-plugin"), { recursive: true });
2442
+ const root = path10.join(os2.tmpdir(), `kody-synth-${runId}`);
2443
+ fs11.mkdirSync(path10.join(root, ".claude-plugin"), { recursive: true });
2587
2444
  const resolvePart = (bucket, entry) => {
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;
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;
2592
2449
  throw new Error(
2593
2450
  `buildSyntheticPlugin: ${bucket} entry '${entry}' not found in executable dir (${profile.dir}/${bucket}/) or catalog (${catalog}/${bucket}/)`
2594
2451
  );
2595
2452
  };
2596
2453
  if (cc.skills.length > 0) {
2597
- const dst = path11.join(root, "skills");
2598
- fs12.mkdirSync(dst, { recursive: true });
2454
+ const dst = path10.join(root, "skills");
2455
+ fs11.mkdirSync(dst, { recursive: true });
2599
2456
  for (const name of cc.skills) {
2600
- copyDir(resolvePart("skills", name), path11.join(dst, name));
2457
+ copyDir(resolvePart("skills", name), path10.join(dst, name));
2601
2458
  }
2602
2459
  }
2603
2460
  if (cc.commands.length > 0) {
2604
- const dst = path11.join(root, "commands");
2605
- fs12.mkdirSync(dst, { recursive: true });
2461
+ const dst = path10.join(root, "commands");
2462
+ fs11.mkdirSync(dst, { recursive: true });
2606
2463
  for (const name of cc.commands) {
2607
- fs12.copyFileSync(resolvePart("commands", `${name}.md`), path11.join(dst, `${name}.md`));
2464
+ fs11.copyFileSync(resolvePart("commands", `${name}.md`), path10.join(dst, `${name}.md`));
2608
2465
  }
2609
2466
  }
2610
2467
  if (cc.subagents.length > 0) {
2611
- const dst = path11.join(root, "agents");
2612
- fs12.mkdirSync(dst, { recursive: true });
2468
+ const dst = path10.join(root, "agents");
2469
+ fs11.mkdirSync(dst, { recursive: true });
2613
2470
  for (const name of cc.subagents) {
2614
- fs12.copyFileSync(resolvePart("agents", `${name}.md`), path11.join(dst, `${name}.md`));
2471
+ fs11.copyFileSync(resolvePart("agents", `${name}.md`), path10.join(dst, `${name}.md`));
2615
2472
  }
2616
2473
  }
2617
2474
  if (cc.hooks.length > 0) {
2618
- const dst = path11.join(root, "hooks");
2619
- fs12.mkdirSync(dst, { recursive: true });
2475
+ const dst = path10.join(root, "hooks");
2476
+ fs11.mkdirSync(dst, { recursive: true });
2620
2477
  const merged = { hooks: {} };
2621
2478
  for (const name of cc.hooks) {
2622
2479
  const src = resolvePart("hooks", `${name}.json`);
2623
- const parsed = JSON.parse(fs12.readFileSync(src, "utf-8"));
2480
+ const parsed = JSON.parse(fs11.readFileSync(src, "utf-8"));
2624
2481
  for (const [event, entries] of Object.entries(parsed.hooks ?? {})) {
2625
2482
  if (!Array.isArray(entries)) continue;
2626
2483
  if (!merged.hooks[event]) merged.hooks[event] = [];
2627
2484
  merged.hooks[event].push(...entries);
2628
2485
  }
2629
2486
  }
2630
- fs12.writeFileSync(path11.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2487
+ fs11.writeFileSync(path10.join(dst, "hooks.json"), `${JSON.stringify(merged, null, 2)}
2631
2488
  `);
2632
2489
  }
2633
2490
  const manifest = {
@@ -2638,17 +2495,17 @@ var buildSyntheticPlugin = async (ctx, profile) => {
2638
2495
  if (cc.skills.length > 0) manifest.skills = ["./skills/"];
2639
2496
  if (cc.commands.length > 0) manifest.commands = ["./commands/"];
2640
2497
  if (cc.subagents.length > 0) manifest.agents = cc.subagents.map((n) => `./agents/${n}.md`);
2641
- fs12.writeFileSync(path11.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2498
+ fs11.writeFileSync(path10.join(root, ".claude-plugin", "plugin.json"), `${JSON.stringify(manifest, null, 2)}
2642
2499
  `);
2643
2500
  ctx.data.syntheticPluginPath = root;
2644
2501
  };
2645
2502
  function copyDir(src, dst) {
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);
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);
2650
2507
  if (ent.isDirectory()) copyDir(s, d);
2651
- else if (ent.isFile()) fs12.copyFileSync(s, d);
2508
+ else if (ent.isFile()) fs11.copyFileSync(s, d);
2652
2509
  }
2653
2510
  }
2654
2511
 
@@ -2713,6 +2570,111 @@ function formatMissesForFeedback(misses) {
2713
2570
  return lines.join("\n");
2714
2571
  }
2715
2572
 
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
+ const markerMissing = !hasDoneMarker && !hasCommitMsg && !hasPrSummary;
2630
+ const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
2631
+ const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
2632
+ const feedbackActions = extractBlock(
2633
+ text,
2634
+ /(?:^|\n)[ \t]*FEEDBACK_ACTIONS\s*:[ \t]*\n/i,
2635
+ /(?:^|\n)[ \t]*(?:PLAN_DEVIATIONS|COMMIT_MSG|PR_SUMMARY|PRIOR_ART)\s*:/i
2636
+ );
2637
+ let planDeviations = extractBlock(
2638
+ text,
2639
+ /(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*\n/i,
2640
+ /(?:^|\n)[ \t]*(?:COMMIT_MSG|PR_SUMMARY|FEEDBACK_ACTIONS|PRIOR_ART)\s*:/i
2641
+ );
2642
+ if (!planDeviations) {
2643
+ const inline = text.match(/(?:^|\n)[ \t]*PLAN_DEVIATIONS\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2644
+ if (inline) planDeviations = inline[1].trim();
2645
+ }
2646
+ let priorArt = "";
2647
+ const priorArtInline = text.match(/(?:^|\n)[ \t]*PRIOR_ART\s*:[ \t]*(.+?)[ \t]*(?:\n|$)/i);
2648
+ if (priorArtInline) priorArt = priorArtInline[1].trim();
2649
+ const summaryStart = text.search(/(^|\n)[ \t]*PR_SUMMARY\s*:[ \t]*\n/i);
2650
+ let prSummary = "";
2651
+ if (summaryStart !== -1) {
2652
+ const afterMarker = text.slice(summaryStart).replace(/^[\s\S]*?PR_SUMMARY\s*:[ \t]*\n/i, "");
2653
+ prSummary = afterMarker.replace(/\n\s*```\s*$/g, "").replace(/```\s*$/g, "").trim();
2654
+ }
2655
+ return {
2656
+ done: true,
2657
+ commitMessage,
2658
+ prSummary,
2659
+ feedbackActions,
2660
+ planDeviations,
2661
+ priorArt,
2662
+ failureReason: "",
2663
+ markerMissing
2664
+ };
2665
+ }
2666
+ function stripMarkdownEmphasis(s) {
2667
+ return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
2668
+ }
2669
+ function extractBlock(text, startMarker, endMarker) {
2670
+ const startIdx = text.search(startMarker);
2671
+ if (startIdx === -1) return "";
2672
+ const afterStart = text.slice(startIdx).replace(startMarker, "");
2673
+ const endIdx = afterStart.search(endMarker);
2674
+ const body = endIdx === -1 ? afterStart : afterStart.slice(0, endIdx);
2675
+ return body.replace(/\n\s*```\s*$/g, "").trim();
2676
+ }
2677
+
2716
2678
  // src/scripts/checkCoverageWithRetry.ts
2717
2679
  var checkCoverageWithRetry = async (ctx) => {
2718
2680
  const reqs = ctx.data.coverageRules ?? [];
@@ -2787,6 +2749,301 @@ function defaultLabelMap() {
2787
2749
  };
2788
2750
  }
2789
2751
 
2752
+ // src/goal/operations.ts
2753
+ import { execFileSync as execFileSync9 } from "child_process";
2754
+
2755
+ // src/goal/labels.ts
2756
+ function goalLabel(goalId) {
2757
+ return `goal:${goalId}`;
2758
+ }
2759
+ var DISPATCHED_LABEL = "goal-runner:dispatched";
2760
+ var FAILED_LABEL = "goal-runner:failed";
2761
+ var UMBRELLA_BUILDING_LABEL = "kody:building";
2762
+ var TICK_LABELS = [
2763
+ {
2764
+ name: DISPATCHED_LABEL,
2765
+ color: "ededed",
2766
+ description: "kody goal-runner: already dispatched this tick"
2767
+ },
2768
+ {
2769
+ name: FAILED_LABEL,
2770
+ color: "b60205",
2771
+ description: "kody goal-runner: task failed; needs human attention"
2772
+ }
2773
+ ];
2774
+
2775
+ // src/goal/operations.ts
2776
+ function fail(err) {
2777
+ if (err instanceof Error) {
2778
+ const lines = err.message.split("\n").filter(Boolean);
2779
+ return { ok: false, error: lines[0] ?? err.message };
2780
+ }
2781
+ return { ok: false, error: String(err) };
2782
+ }
2783
+ function listGoalIssues(goalId, excludeIssueNumber, cwd) {
2784
+ try {
2785
+ const out = gh(
2786
+ [
2787
+ "api",
2788
+ `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
2789
+ "--jq",
2790
+ "[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), labels: [.labels[].name]}]"
2791
+ ],
2792
+ { cwd }
2793
+ );
2794
+ const arr = JSON.parse(out);
2795
+ const filtered = excludeIssueNumber !== void 0 ? arr.filter((i) => i.number !== excludeIssueNumber) : arr;
2796
+ return { ok: true, value: filtered };
2797
+ } catch (err) {
2798
+ return fail(err);
2799
+ }
2800
+ }
2801
+ function ensureLabel(name, color, description, cwd) {
2802
+ try {
2803
+ gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
2804
+ return { ok: true };
2805
+ } catch (err) {
2806
+ return fail(err);
2807
+ }
2808
+ }
2809
+ function addLabel2(issueNumber, label, cwd) {
2810
+ try {
2811
+ gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
2812
+ return { ok: true };
2813
+ } catch (err) {
2814
+ return fail(err);
2815
+ }
2816
+ }
2817
+ function commentOnIssue(issueNumber, body, cwd) {
2818
+ try {
2819
+ gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
2820
+ return { ok: true };
2821
+ } catch (err) {
2822
+ return fail(err);
2823
+ }
2824
+ }
2825
+ function closeIssue(issueNumber, options, cwd) {
2826
+ try {
2827
+ if (options.comment) {
2828
+ gh(["issue", "comment", String(issueNumber), "--body", options.comment], { cwd });
2829
+ }
2830
+ const args = ["issue", "close", String(issueNumber)];
2831
+ if (options.reason) args.push("--reason", options.reason);
2832
+ gh(args, { cwd });
2833
+ return { ok: true };
2834
+ } catch (err) {
2835
+ return fail(err);
2836
+ }
2837
+ }
2838
+ function getIssueState(issueNumber, cwd) {
2839
+ try {
2840
+ const out = gh(["issue", "view", String(issueNumber), "--json", "state", "--jq", ".state"], {
2841
+ cwd
2842
+ });
2843
+ const norm = out.trim().toUpperCase();
2844
+ if (norm !== "OPEN" && norm !== "CLOSED") {
2845
+ return { ok: false, error: `unexpected state: ${out}` };
2846
+ }
2847
+ return { ok: true, value: norm };
2848
+ } catch (err) {
2849
+ return fail(err);
2850
+ }
2851
+ }
2852
+ function findUmbrellaByTitle(goalId, title, cwd) {
2853
+ try {
2854
+ const out = gh(
2855
+ [
2856
+ "api",
2857
+ `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
2858
+ "--jq",
2859
+ `[.[] | select(.pull_request == null) | select(.title == "${title.replace(/"/g, '\\"')}")] | (map(select(.state == "open")) + map(select(.state != "open")))[0].number // empty`
2860
+ ],
2861
+ { cwd }
2862
+ );
2863
+ const trimmed = out.trim();
2864
+ if (!trimmed) return { ok: true, value: null };
2865
+ const n = Number.parseInt(trimmed, 10);
2866
+ if (!Number.isFinite(n)) return { ok: true, value: null };
2867
+ return { ok: true, value: n };
2868
+ } catch (err) {
2869
+ return fail(err);
2870
+ }
2871
+ }
2872
+ function createIssue(args, cwd) {
2873
+ try {
2874
+ const cliArgs = ["issue", "create", "--title", args.title, "--body", args.body];
2875
+ for (const l of args.labels) cliArgs.push("--label", l);
2876
+ const url = gh(cliArgs, { cwd });
2877
+ const match = url.match(/\/issues\/(\d+)/);
2878
+ if (!match?.[1]) return { ok: false, error: `couldn't parse issue number from URL: ${url}` };
2879
+ return { ok: true, value: Number.parseInt(match[1], 10) };
2880
+ } catch (err) {
2881
+ return fail(err);
2882
+ }
2883
+ }
2884
+ function listPrsByBase(base, state, cwd) {
2885
+ try {
2886
+ const out = gh(
2887
+ [
2888
+ "pr",
2889
+ "list",
2890
+ "--base",
2891
+ base,
2892
+ "--state",
2893
+ state,
2894
+ "--limit",
2895
+ "50",
2896
+ "--json",
2897
+ "number,isDraft,mergeable,mergeStateStatus,url,headRefName,body"
2898
+ ],
2899
+ { cwd }
2900
+ );
2901
+ return { ok: true, value: JSON.parse(out) };
2902
+ } catch (err) {
2903
+ return fail(err);
2904
+ }
2905
+ }
2906
+ function listPrsByHead(head, state, cwd) {
2907
+ try {
2908
+ const out = gh(
2909
+ [
2910
+ "pr",
2911
+ "list",
2912
+ "--head",
2913
+ head,
2914
+ "--state",
2915
+ state,
2916
+ "--json",
2917
+ "number,isDraft,mergeable,mergeStateStatus,url,headRefName,body"
2918
+ ],
2919
+ { cwd }
2920
+ );
2921
+ return { ok: true, value: JSON.parse(out) };
2922
+ } catch (err) {
2923
+ return fail(err);
2924
+ }
2925
+ }
2926
+ function mergePrSquash(prNumber, cwd) {
2927
+ try {
2928
+ gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
2929
+ return { ok: true };
2930
+ } catch (err) {
2931
+ return fail(err);
2932
+ }
2933
+ }
2934
+ function closePr(prNumber, comment, cwd) {
2935
+ try {
2936
+ gh(["pr", "close", String(prNumber), "--comment", comment], { cwd });
2937
+ return { ok: true };
2938
+ } catch (err) {
2939
+ return fail(err);
2940
+ }
2941
+ }
2942
+ function createPr(args, cwd) {
2943
+ try {
2944
+ const cli = ["pr", "create", "--head", args.head, "--base", args.base, "--title", args.title, "--body", args.body];
2945
+ if (args.draft) cli.push("--draft");
2946
+ const url = gh(cli, { cwd });
2947
+ if (!url.includes("/pull/")) return { ok: false, error: `gh pr create returned unexpected output: ${url}` };
2948
+ return { ok: true, value: url.trim() };
2949
+ } catch (err) {
2950
+ return fail(err);
2951
+ }
2952
+ }
2953
+ function editPrBody(prNumber, body, cwd) {
2954
+ try {
2955
+ gh(["pr", "edit", String(prNumber), "--body", body], { cwd });
2956
+ return { ok: true };
2957
+ } catch (err) {
2958
+ return fail(err);
2959
+ }
2960
+ }
2961
+ function markPrReady(prNumber, cwd) {
2962
+ try {
2963
+ gh(["pr", "ready", String(prNumber)], { cwd });
2964
+ return { ok: true };
2965
+ } catch (err) {
2966
+ return fail(err);
2967
+ }
2968
+ }
2969
+ function ghTokenEnv() {
2970
+ const token = process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
2971
+ return token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
2972
+ }
2973
+ function remoteBranchExists(ref, cwd) {
2974
+ try {
2975
+ execFileSync9("git", ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${ref}`], {
2976
+ cwd,
2977
+ stdio: "pipe",
2978
+ env: ghTokenEnv()
2979
+ });
2980
+ return true;
2981
+ } catch {
2982
+ return false;
2983
+ }
2984
+ }
2985
+ function fetchOrigin(cwd) {
2986
+ try {
2987
+ execFileSync9("git", ["fetch", "origin", "--quiet"], { cwd, stdio: "pipe", env: ghTokenEnv() });
2988
+ } catch {
2989
+ }
2990
+ }
2991
+ function createBranchFrom(branch, base, cwd) {
2992
+ try {
2993
+ execFileSync9("git", ["push", "origin", `refs/remotes/origin/${base}:refs/heads/${branch}`, "--quiet"], {
2994
+ cwd,
2995
+ stdio: "pipe",
2996
+ env: ghTokenEnv()
2997
+ });
2998
+ return { ok: true };
2999
+ } catch (err) {
3000
+ return fail(err);
3001
+ }
3002
+ }
3003
+ function inferLinkedIssue(pr) {
3004
+ const body = pr.body ?? "";
3005
+ const m = body.match(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/i);
3006
+ if (m?.[1]) return Number.parseInt(m[1], 10);
3007
+ const ref = pr.headRefName ?? "";
3008
+ const bm = ref.match(/^(\d+)-/);
3009
+ if (bm?.[1]) return Number.parseInt(bm[1], 10);
3010
+ return void 0;
3011
+ }
3012
+
3013
+ // src/scripts/closeMergedTaskIssues.ts
3014
+ var closeMergedTaskIssues = async (ctx) => {
3015
+ const goal = ctx.data.goal;
3016
+ if (!goal) return;
3017
+ const merged = listPrsByBase(goal.goalBranch, "merged", ctx.cwd);
3018
+ if (!merged.ok) {
3019
+ process.stderr.write(`[goal-tick] closeMergedTaskIssues: list failed: ${merged.error}
3020
+ `);
3021
+ return;
3022
+ }
3023
+ const seen = /* @__PURE__ */ new Set();
3024
+ for (const pr of merged.value ?? []) {
3025
+ const linked = inferLinkedIssue(pr);
3026
+ if (linked === void 0 || seen.has(linked)) continue;
3027
+ seen.add(linked);
3028
+ const stateRes = getIssueState(linked, ctx.cwd);
3029
+ if (!stateRes.ok || stateRes.value !== "OPEN") continue;
3030
+ process.stdout.write(`[goal-tick] closing #${linked} (PR merged into ${goal.goalBranch})
3031
+ `);
3032
+ const r = closeIssue(
3033
+ linked,
3034
+ {
3035
+ comment: `_Closed by goal-tick: PR for this task merged into \`${goal.goalBranch}\`._`,
3036
+ reason: "completed"
3037
+ },
3038
+ ctx.cwd
3039
+ );
3040
+ if (!r.ok) {
3041
+ process.stderr.write(`[goal-tick] failed to close #${linked}: ${r.error} (continuing)
3042
+ `);
3043
+ }
3044
+ }
3045
+ };
3046
+
2790
3047
  // src/scripts/commitAndPush.ts
2791
3048
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
2792
3049
  var commitAndPush2 = async (ctx) => {
@@ -2830,17 +3087,69 @@ var commitAndPush2 = async (ctx) => {
2830
3087
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
2831
3088
  };
2832
3089
 
3090
+ // src/scripts/commitGoalState.ts
3091
+ import { execFileSync as execFileSync10 } from "child_process";
3092
+ import * as path12 from "path";
3093
+ var commitGoalState = async (ctx) => {
3094
+ const goal = ctx.data.goal;
3095
+ if (!goal) return;
3096
+ const stateRel = path12.posix.join(".kody", "goals", goal.id, "state.json");
3097
+ try {
3098
+ execFileSync10("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
3099
+ } catch (err) {
3100
+ process.stderr.write(
3101
+ `[goal-tick] commitGoalState: git add failed: ${err instanceof Error ? err.message : String(err)}
3102
+ `
3103
+ );
3104
+ return;
3105
+ }
3106
+ try {
3107
+ execFileSync10("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3108
+ return;
3109
+ } catch {
3110
+ }
3111
+ const msg = describeCommitMessage(goal);
3112
+ try {
3113
+ execFileSync10("git", ["commit", "-m", msg, "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3114
+ } catch (err) {
3115
+ process.stderr.write(
3116
+ `[goal-tick] commitGoalState: git commit failed: ${err instanceof Error ? err.message : String(err)}
3117
+ `
3118
+ );
3119
+ return;
3120
+ }
3121
+ try {
3122
+ execFileSync10("git", ["push", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3123
+ } catch {
3124
+ process.stderr.write("[goal-tick] commitGoalState: push failed (will retry next tick)\n");
3125
+ }
3126
+ };
3127
+ function describeCommitMessage(goal) {
3128
+ if (goal.state === "closed") return `chore(goals): abandon ${goal.id} (cleanup complete)`;
3129
+ if (goal.state === "done") return `chore(goals): mark ${goal.id} done`;
3130
+ if (goal.lastDispatchedIssue !== void 0) {
3131
+ return `chore(goals): dispatched #${goal.lastDispatchedIssue} for ${goal.id}`;
3132
+ }
3133
+ if (goal.phase === "in-flight") {
3134
+ return `chore(goals): tick ${goal.id} (waiting for in-flight task)`;
3135
+ }
3136
+ if (goal.phase === "blocked-by-failure") {
3137
+ return `chore(goals): tick ${goal.id} (blocked by failed task)`;
3138
+ }
3139
+ return `chore(goals): tick ${goal.id} (idle)`;
3140
+ }
3141
+
2833
3142
  // src/scripts/composePrompt.ts
2834
3143
  import * as fs13 from "fs";
2835
- import * as path12 from "path";
3144
+ import * as path13 from "path";
2836
3145
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
2837
3146
  var composePrompt = async (ctx, profile) => {
2838
3147
  const explicit = ctx.data.promptTemplate;
2839
3148
  const mode = ctx.args.mode;
2840
3149
  const candidates = [
2841
- explicit ? path12.join(profile.dir, explicit) : null,
2842
- mode ? path12.join(profile.dir, "prompts", `${mode}.md`) : null,
2843
- path12.join(profile.dir, "prompt.md")
3150
+ explicit ? path13.join(profile.dir, explicit) : null,
3151
+ mode ? path13.join(profile.dir, "prompts", `${mode}.md`) : null,
3152
+ path13.join(profile.dir, "prompt.md")
2844
3153
  ].filter(Boolean);
2845
3154
  let templatePath = "";
2846
3155
  for (const c of candidates) {
@@ -2929,9 +3238,9 @@ function formatToolsUsage(profile) {
2929
3238
  }
2930
3239
 
2931
3240
  // src/scripts/createQaGoal.ts
2932
- import { execFileSync as execFileSync9 } from "child_process";
3241
+ import { execFileSync as execFileSync11 } from "child_process";
2933
3242
  import * as fs14 from "fs";
2934
- import * as path13 from "path";
3243
+ import * as path14 from "path";
2935
3244
 
2936
3245
  // src/scripts/postReviewResult.ts
2937
3246
  function detectVerdict(body) {
@@ -3113,7 +3422,7 @@ ${json}
3113
3422
  ${MANIFEST_END}
3114
3423
  `;
3115
3424
  }
3116
- function ensureLabel(name, color, description, cwd) {
3425
+ function ensureLabel2(name, color, description, cwd) {
3117
3426
  try {
3118
3427
  gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3119
3428
  } catch {
@@ -3133,7 +3442,7 @@ function ensureSeverityLabels(findings, cwd) {
3133
3442
  for (const f of findings) {
3134
3443
  if (seen.has(f.severity)) continue;
3135
3444
  seen.add(f.severity);
3136
- ensureLabel(severityLabel(f.severity), SEVERITY_COLORS[f.severity], `kody QA finding severity ${f.severity}`, cwd);
3445
+ ensureLabel2(severityLabel(f.severity), SEVERITY_COLORS[f.severity], `kody QA finding severity ${f.severity}`, cwd);
3137
3446
  }
3138
3447
  }
3139
3448
  function buildIssueBody(f, goalId, parentManifestNumber) {
@@ -3167,7 +3476,7 @@ function buildIssueBody(f, goalId, parentManifestNumber) {
3167
3476
  return lines.join("\n");
3168
3477
  }
3169
3478
  function createOrUpdateManifestIssue(number, manifest, cwd) {
3170
- ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3479
+ ensureLabel2(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3171
3480
  const body = serializeManifestBody(manifest);
3172
3481
  if (number !== null) {
3173
3482
  gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
@@ -3183,7 +3492,7 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3183
3492
  return { number: Number(m[1]), created: true };
3184
3493
  }
3185
3494
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3186
- const dir = path13.join(cwd, ".kody", "goals", goalId);
3495
+ const dir = path14.join(cwd, ".kody", "goals", goalId);
3187
3496
  fs14.mkdirSync(dir, { recursive: true });
3188
3497
  const state = {
3189
3498
  version: 1,
@@ -3192,7 +3501,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3192
3501
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3193
3502
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
3194
3503
  };
3195
- const filePath = path13.join(dir, "state.json");
3504
+ const filePath = path14.join(dir, "state.json");
3196
3505
  fs14.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3197
3506
  `);
3198
3507
  return filePath;
@@ -3200,7 +3509,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3200
3509
  function gitTry(args, cwd) {
3201
3510
  const env = { ...process.env, SKIP_HOOKS: "1", HUSKY: "0" };
3202
3511
  try {
3203
- execFileSync9("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3512
+ execFileSync11("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3204
3513
  return { ok: true, stderr: "" };
3205
3514
  } catch (err) {
3206
3515
  const e = err;
@@ -3282,8 +3591,8 @@ ${tail}
3282
3591
  }
3283
3592
  function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3284
3593
  const labels = [`goal:${goalId}`, severityLabel(finding.severity), FINDING_LABEL];
3285
- ensureLabel(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3286
- ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", cwd);
3594
+ ensureLabel2(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3595
+ ensureLabel2(FINDING_LABEL, "ededed", "kody: QA finding", cwd);
3287
3596
  const title = `[${finding.severity}] ${finding.title}`.slice(0, 240);
3288
3597
  const body = buildIssueBody(finding, goalId, manifestNumber);
3289
3598
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
@@ -3342,7 +3651,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3342
3651
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3343
3652
  return;
3344
3653
  }
3345
- ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3654
+ ensureLabel2(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3346
3655
  const scope2 = ctx.args.scope;
3347
3656
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3348
3657
  let url = "";
@@ -3472,14 +3781,57 @@ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
3472
3781
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3473
3782
  };
3474
3783
 
3784
+ // src/goal/phase.ts
3785
+ function derivePhase(snap) {
3786
+ if (snap.lifecycleState === void 0) return "missing";
3787
+ if (snap.lifecycleState === "abandoned") return "abandoned";
3788
+ if (snap.lifecycleState === "closed" || snap.lifecycleState === "done") return "terminal";
3789
+ if (snap.childTasks.length === 0) return "no-tasks";
3790
+ const allClosed = snap.childTasks.every((t) => t.state === "CLOSED");
3791
+ if (allClosed) return "all-done";
3792
+ const anyFailed = snap.childTasks.some((t) => t.labels.includes(FAILED_LABEL));
3793
+ if (anyFailed) return "blocked-by-failure";
3794
+ const inFlight = snap.childTasks.some((t) => t.state === "OPEN" && t.labels.includes(DISPATCHED_LABEL));
3795
+ if (inFlight) return "in-flight";
3796
+ const dispatchable = snap.childTasks.some((t) => t.state === "OPEN" && !t.labels.includes(DISPATCHED_LABEL));
3797
+ if (dispatchable) return "ready-to-dispatch";
3798
+ return "idle";
3799
+ }
3800
+ function pickNextDispatchable(snap) {
3801
+ const candidates = snap.childTasks.filter((t) => t.state === "OPEN" && !t.labels.includes(DISPATCHED_LABEL)).sort((a, b) => a.number - b.number);
3802
+ return candidates[0];
3803
+ }
3804
+
3805
+ // src/scripts/deriveGoalPhase.ts
3806
+ var deriveGoalPhase = async (ctx) => {
3807
+ const goal = ctx.data.goal;
3808
+ if (!goal) return;
3809
+ const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
3810
+ if (!issues.ok) {
3811
+ process.stderr.write(`[goal-tick] deriveGoalPhase: list failed: ${issues.error}
3812
+ `);
3813
+ goal.childTasks = [];
3814
+ goal.phase = "idle";
3815
+ return;
3816
+ }
3817
+ const childTasks = issues.value ?? [];
3818
+ goal.childTasks = childTasks;
3819
+ goal.phase = derivePhase({
3820
+ lifecycleState: goal.state,
3821
+ childTasks
3822
+ });
3823
+ process.stdout.write(`[goal-tick] phase=${goal.phase} goal=${goal.id} tasks=${childTasks.length}
3824
+ `);
3825
+ };
3826
+
3475
3827
  // src/scripts/diagMcp.ts
3476
- import { execFileSync as execFileSync10 } from "child_process";
3828
+ import { execFileSync as execFileSync12 } from "child_process";
3477
3829
  import * as fs15 from "fs";
3478
3830
  import * as os3 from "os";
3479
- import * as path14 from "path";
3831
+ import * as path15 from "path";
3480
3832
  var diagMcp = async (_ctx) => {
3481
3833
  const home = os3.homedir();
3482
- const cacheDir = path14.join(home, ".cache", "ms-playwright");
3834
+ const cacheDir = path15.join(home, ".cache", "ms-playwright");
3483
3835
  let entries = [];
3484
3836
  try {
3485
3837
  entries = fs15.readdirSync(cacheDir);
@@ -3493,7 +3845,7 @@ var diagMcp = async (_ctx) => {
3493
3845
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
3494
3846
  `);
3495
3847
  try {
3496
- const v = execFileSync10("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
3848
+ const v = execFileSync12("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
3497
3849
  stdio: "pipe",
3498
3850
  timeout: 6e4,
3499
3851
  encoding: "utf8"
@@ -3509,16 +3861,16 @@ var diagMcp = async (_ctx) => {
3509
3861
 
3510
3862
  // src/scripts/discoverQaContext.ts
3511
3863
  import * as fs17 from "fs";
3512
- import * as path16 from "path";
3864
+ import * as path17 from "path";
3513
3865
 
3514
3866
  // src/scripts/frameworkDetectors.ts
3515
3867
  import * as fs16 from "fs";
3516
- import * as path15 from "path";
3868
+ import * as path16 from "path";
3517
3869
  function detectFrameworks(cwd) {
3518
3870
  const out = [];
3519
3871
  let deps = {};
3520
3872
  try {
3521
- const pkg = JSON.parse(fs16.readFileSync(path15.join(cwd, "package.json"), "utf-8"));
3873
+ const pkg = JSON.parse(fs16.readFileSync(path16.join(cwd, "package.json"), "utf-8"));
3522
3874
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
3523
3875
  } catch {
3524
3876
  return out;
@@ -3555,7 +3907,7 @@ function detectFrameworks(cwd) {
3555
3907
  }
3556
3908
  function findFile(cwd, candidates) {
3557
3909
  for (const c of candidates) {
3558
- if (fs16.existsSync(path15.join(cwd, c))) return c;
3910
+ if (fs16.existsSync(path16.join(cwd, c))) return c;
3559
3911
  }
3560
3912
  return null;
3561
3913
  }
@@ -3568,7 +3920,7 @@ var COLLECTION_DIRS = [
3568
3920
  function discoverPayloadCollections(cwd) {
3569
3921
  const out = [];
3570
3922
  for (const dir of COLLECTION_DIRS) {
3571
- const full = path15.join(cwd, dir);
3923
+ const full = path16.join(cwd, dir);
3572
3924
  if (!fs16.existsSync(full)) continue;
3573
3925
  let files;
3574
3926
  try {
@@ -3578,7 +3930,7 @@ function discoverPayloadCollections(cwd) {
3578
3930
  }
3579
3931
  for (const file of files) {
3580
3932
  try {
3581
- const filePath = path15.join(full, file);
3933
+ const filePath = path16.join(full, file);
3582
3934
  const content = fs16.readFileSync(filePath, "utf-8").slice(0, 1e4);
3583
3935
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
3584
3936
  if (!slugMatch) continue;
@@ -3593,7 +3945,7 @@ function discoverPayloadCollections(cwd) {
3593
3945
  out.push({
3594
3946
  name,
3595
3947
  slug,
3596
- filePath: path15.relative(cwd, filePath),
3948
+ filePath: path16.relative(cwd, filePath),
3597
3949
  fields: fields.slice(0, 20),
3598
3950
  hasAdmin
3599
3951
  });
@@ -3607,7 +3959,7 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
3607
3959
  function discoverAdminComponents(cwd, collections) {
3608
3960
  const out = [];
3609
3961
  for (const dir of ADMIN_COMPONENT_DIRS) {
3610
- const full = path15.join(cwd, dir);
3962
+ const full = path16.join(cwd, dir);
3611
3963
  if (!fs16.existsSync(full)) continue;
3612
3964
  let entries;
3613
3965
  try {
@@ -3616,19 +3968,19 @@ function discoverAdminComponents(cwd, collections) {
3616
3968
  continue;
3617
3969
  }
3618
3970
  for (const entry of entries) {
3619
- const entryPath = path15.join(full, entry.name);
3971
+ const entryPath = path16.join(full, entry.name);
3620
3972
  let name;
3621
3973
  let filePath;
3622
3974
  if (entry.isDirectory()) {
3623
3975
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
3624
- (f) => fs16.existsSync(path15.join(entryPath, f))
3976
+ (f) => fs16.existsSync(path16.join(entryPath, f))
3625
3977
  );
3626
3978
  if (!indexFile) continue;
3627
3979
  name = entry.name;
3628
- filePath = path15.relative(cwd, path15.join(entryPath, indexFile));
3980
+ filePath = path16.relative(cwd, path16.join(entryPath, indexFile));
3629
3981
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
3630
3982
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
3631
- filePath = path15.relative(cwd, entryPath);
3983
+ filePath = path16.relative(cwd, entryPath);
3632
3984
  } else {
3633
3985
  continue;
3634
3986
  }
@@ -3636,7 +3988,7 @@ function discoverAdminComponents(cwd, collections) {
3636
3988
  if (collections) {
3637
3989
  for (const col of collections) {
3638
3990
  try {
3639
- const colContent = fs16.readFileSync(path15.join(cwd, col.filePath), "utf-8");
3991
+ const colContent = fs16.readFileSync(path16.join(cwd, col.filePath), "utf-8");
3640
3992
  if (colContent.includes(name)) {
3641
3993
  usedInCollection = col.slug;
3642
3994
  break;
@@ -3655,7 +4007,7 @@ function scanApiRoutes(cwd) {
3655
4007
  const out = [];
3656
4008
  const appDirs = ["src/app", "app"];
3657
4009
  for (const appDir of appDirs) {
3658
- const apiDir = path15.join(cwd, appDir, "api");
4010
+ const apiDir = path16.join(cwd, appDir, "api");
3659
4011
  if (!fs16.existsSync(apiDir)) continue;
3660
4012
  walkApiRoutes(apiDir, "/api", cwd, out);
3661
4013
  break;
@@ -3672,7 +4024,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3672
4024
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
3673
4025
  if (routeFile) {
3674
4026
  try {
3675
- const content = fs16.readFileSync(path15.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
4027
+ const content = fs16.readFileSync(path16.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
3676
4028
  const methods = HTTP_METHODS.filter(
3677
4029
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
3678
4030
  );
@@ -3680,7 +4032,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3680
4032
  out.push({
3681
4033
  path: prefix,
3682
4034
  methods,
3683
- filePath: path15.relative(cwd, path15.join(dir, routeFile.name))
4035
+ filePath: path16.relative(cwd, path16.join(dir, routeFile.name))
3684
4036
  });
3685
4037
  }
3686
4038
  } catch {
@@ -3691,7 +4043,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3691
4043
  if (entry.name === "node_modules" || entry.name === ".next") continue;
3692
4044
  let segment = entry.name;
3693
4045
  if (segment.startsWith("(") && segment.endsWith(")")) {
3694
- walkApiRoutes(path15.join(dir, entry.name), prefix, cwd, out);
4046
+ walkApiRoutes(path16.join(dir, entry.name), prefix, cwd, out);
3695
4047
  continue;
3696
4048
  }
3697
4049
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -3699,7 +4051,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3699
4051
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
3700
4052
  segment = `:${segment.slice(1, -1)}`;
3701
4053
  }
3702
- walkApiRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
4054
+ walkApiRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
3703
4055
  }
3704
4056
  }
3705
4057
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -3719,7 +4071,7 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
3719
4071
  function scanEnvVars(cwd) {
3720
4072
  const candidates = [".env.example", ".env.local.example", ".env.template"];
3721
4073
  for (const envFile of candidates) {
3722
- const envPath = path15.join(cwd, envFile);
4074
+ const envPath = path16.join(cwd, envFile);
3723
4075
  if (!fs16.existsSync(envPath)) continue;
3724
4076
  try {
3725
4077
  const content = fs16.readFileSync(envPath, "utf-8");
@@ -3770,9 +4122,9 @@ function runQaDiscovery(cwd) {
3770
4122
  }
3771
4123
  function detectDevServer(cwd, out) {
3772
4124
  try {
3773
- const pkg = JSON.parse(fs17.readFileSync(path16.join(cwd, "package.json"), "utf-8"));
4125
+ const pkg = JSON.parse(fs17.readFileSync(path17.join(cwd, "package.json"), "utf-8"));
3774
4126
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
3775
- const pm = fs17.existsSync(path16.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs17.existsSync(path16.join(cwd, "yarn.lock")) ? "yarn" : fs17.existsSync(path16.join(cwd, "bun.lockb")) ? "bun" : "npm";
4127
+ const pm = fs17.existsSync(path17.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs17.existsSync(path17.join(cwd, "yarn.lock")) ? "yarn" : fs17.existsSync(path17.join(cwd, "bun.lockb")) ? "bun" : "npm";
3776
4128
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
3777
4129
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
3778
4130
  else if (allDeps.vite) out.devPort = 5173;
@@ -3782,7 +4134,7 @@ function detectDevServer(cwd, out) {
3782
4134
  function scanFrontendRoutes(cwd, out) {
3783
4135
  const appDirs = ["src/app", "app"];
3784
4136
  for (const appDir of appDirs) {
3785
- const full = path16.join(cwd, appDir);
4137
+ const full = path17.join(cwd, appDir);
3786
4138
  if (!fs17.existsSync(full)) continue;
3787
4139
  walkFrontendRoutes(full, "", out);
3788
4140
  break;
@@ -3808,7 +4160,7 @@ function walkFrontendRoutes(dir, prefix, out) {
3808
4160
  if (entry.name === "node_modules" || entry.name === ".next") continue;
3809
4161
  let segment = entry.name;
3810
4162
  if (segment.startsWith("(") && segment.endsWith(")")) {
3811
- walkFrontendRoutes(path16.join(dir, entry.name), prefix, out);
4163
+ walkFrontendRoutes(path17.join(dir, entry.name), prefix, out);
3812
4164
  continue;
3813
4165
  }
3814
4166
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -3816,7 +4168,7 @@ function walkFrontendRoutes(dir, prefix, out) {
3816
4168
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
3817
4169
  segment = `:${segment.slice(1, -1)}`;
3818
4170
  }
3819
- walkFrontendRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, out);
4171
+ walkFrontendRoutes(path17.join(dir, entry.name), `${prefix}/${segment}`, out);
3820
4172
  }
3821
4173
  }
3822
4174
  function detectAuthFiles(cwd, out) {
@@ -3833,13 +4185,13 @@ function detectAuthFiles(cwd, out) {
3833
4185
  "src/app/api/oauth"
3834
4186
  ];
3835
4187
  for (const c of candidates) {
3836
- if (fs17.existsSync(path16.join(cwd, c))) out.authFiles.push(c);
4188
+ if (fs17.existsSync(path17.join(cwd, c))) out.authFiles.push(c);
3837
4189
  }
3838
4190
  }
3839
4191
  function detectRoles(cwd, out) {
3840
4192
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
3841
4193
  for (const rp of rolePaths) {
3842
- const dir = path16.join(cwd, rp);
4194
+ const dir = path17.join(cwd, rp);
3843
4195
  if (!fs17.existsSync(dir)) continue;
3844
4196
  let files;
3845
4197
  try {
@@ -3849,7 +4201,7 @@ function detectRoles(cwd, out) {
3849
4201
  }
3850
4202
  for (const f of files) {
3851
4203
  try {
3852
- const content = fs17.readFileSync(path16.join(dir, f), "utf-8").slice(0, 5e3);
4204
+ const content = fs17.readFileSync(path17.join(dir, f), "utf-8").slice(0, 5e3);
3853
4205
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
3854
4206
  if (roleMatches) {
3855
4207
  for (const m of roleMatches) {
@@ -3995,7 +4347,7 @@ var discoverQaContext = async (ctx) => {
3995
4347
  };
3996
4348
 
3997
4349
  // src/scripts/dispatch.ts
3998
- import { execFileSync as execFileSync11 } from "child_process";
4350
+ import { execFileSync as execFileSync13 } from "child_process";
3999
4351
  var API_TIMEOUT_MS4 = 3e4;
4000
4352
  var dispatch = async (ctx, _profile, _agentResult, args) => {
4001
4353
  const next = args?.next;
@@ -4031,7 +4383,7 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
4031
4383
  const sub = usePr ? "pr" : "issue";
4032
4384
  const body = `@kody ${next}`;
4033
4385
  try {
4034
- execFileSync11("gh", [sub, "comment", String(targetNumber), "--body", body], {
4386
+ execFileSync13("gh", [sub, "comment", String(targetNumber), "--body", body], {
4035
4387
  timeout: API_TIMEOUT_MS4,
4036
4388
  cwd: ctx.cwd,
4037
4389
  stdio: ["ignore", "pipe", "pipe"]
@@ -4051,7 +4403,7 @@ function parsePr(url) {
4051
4403
  }
4052
4404
 
4053
4405
  // src/scripts/dispatchClassified.ts
4054
- import { execFileSync as execFileSync12 } from "child_process";
4406
+ import { execFileSync as execFileSync14 } from "child_process";
4055
4407
  var API_TIMEOUT_MS5 = 3e4;
4056
4408
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
4057
4409
  var dispatchClassified = async (ctx) => {
@@ -4060,7 +4412,7 @@ var dispatchClassified = async (ctx) => {
4060
4412
  const classification = ctx.data.classification;
4061
4413
  if (!classification || !VALID_CLASSES2.has(classification)) return;
4062
4414
  try {
4063
- execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4415
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4064
4416
  cwd: ctx.cwd,
4065
4417
  timeout: API_TIMEOUT_MS5,
4066
4418
  stdio: ["ignore", "pipe", "pipe"]
@@ -4081,7 +4433,7 @@ function failedAction3(reason) {
4081
4433
 
4082
4434
  // src/scripts/dispatchJobFileTicks.ts
4083
4435
  import * as fs19 from "fs";
4084
- import * as path18 from "path";
4436
+ import * as path19 from "path";
4085
4437
 
4086
4438
  // src/scripts/jobFrontmatter.ts
4087
4439
  var SCHEDULE_EVERY_VALUES = [
@@ -4333,7 +4685,7 @@ var ContentsApiBackend = class {
4333
4685
 
4334
4686
  // src/scripts/jobState/localFileBackend.ts
4335
4687
  import * as fs18 from "fs";
4336
- import * as path17 from "path";
4688
+ import * as path18 from "path";
4337
4689
  var LocalFileBackend = class {
4338
4690
  name = "local-file";
4339
4691
  cwd;
@@ -4348,7 +4700,7 @@ var LocalFileBackend = class {
4348
4700
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
4349
4701
  this.cwd = opts.cwd;
4350
4702
  this.jobsDir = opts.jobsDir;
4351
- this.absDir = path17.join(opts.cwd, opts.jobsDir);
4703
+ this.absDir = path18.join(opts.cwd, opts.jobsDir);
4352
4704
  this.owner = opts.owner;
4353
4705
  this.repo = opts.repo;
4354
4706
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -4408,7 +4760,7 @@ var LocalFileBackend = class {
4408
4760
  }
4409
4761
  load(slug) {
4410
4762
  const relPath = stateFilePath(this.jobsDir, slug);
4411
- const absPath = path17.join(this.cwd, relPath);
4763
+ const absPath = path18.join(this.cwd, relPath);
4412
4764
  if (!fs18.existsSync(absPath)) {
4413
4765
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
4414
4766
  }
@@ -4429,8 +4781,8 @@ var LocalFileBackend = class {
4429
4781
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
4430
4782
  return false;
4431
4783
  }
4432
- const absPath = path17.join(this.cwd, loaded.path);
4433
- fs18.mkdirSync(path17.dirname(absPath), { recursive: true });
4784
+ const absPath = path18.join(this.cwd, loaded.path);
4785
+ fs18.mkdirSync(path18.dirname(absPath), { recursive: true });
4434
4786
  const body = JSON.stringify(next, null, 2) + "\n";
4435
4787
  fs18.writeFileSync(absPath, body, "utf-8");
4436
4788
  return true;
@@ -4509,7 +4861,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4509
4861
  await backend.hydrate();
4510
4862
  }
4511
4863
  try {
4512
- const slugs = listJobSlugs(path18.join(ctx.cwd, jobsDir));
4864
+ const slugs = listJobSlugs(path19.join(ctx.cwd, jobsDir));
4513
4865
  ctx.data.jobSlugCount = slugs.length;
4514
4866
  if (slugs.length === 0) {
4515
4867
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -4608,7 +4960,7 @@ function formatAgo(ms) {
4608
4960
  }
4609
4961
  function readJobFrontmatter(cwd, jobsDir, slug) {
4610
4962
  try {
4611
- const raw = fs19.readFileSync(path18.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4963
+ const raw = fs19.readFileSync(path19.join(cwd, jobsDir, `${slug}.md`), "utf-8");
4612
4964
  return splitFrontmatter(raw).frontmatter;
4613
4965
  } catch {
4614
4966
  return {};
@@ -4688,6 +5040,134 @@ function listIssuesByLabel(label, cwd) {
4688
5040
  return list.filter((x) => typeof x.number === "number" && typeof x.title === "string").map((x) => ({ number: x.number, title: x.title }));
4689
5041
  }
4690
5042
 
5043
+ // src/scripts/dispatchNextTask.ts
5044
+ var dispatchNextTask = async (ctx) => {
5045
+ const goal = ctx.data.goal;
5046
+ if (!goal?.childTasks) return;
5047
+ const next = pickNextDispatchable({
5048
+ lifecycleState: goal.state,
5049
+ childTasks: goal.childTasks
5050
+ });
5051
+ if (!next) {
5052
+ process.stdout.write("[goal-tick] no undispatched open task \u2014 idle\n");
5053
+ return;
5054
+ }
5055
+ process.stdout.write(`[goal-tick] dispatching @kody on task #${next.number} (--base ${goal.goalBranch})
5056
+ `);
5057
+ const comment = commentOnIssue(next.number, `@kody --base ${goal.goalBranch}`, ctx.cwd);
5058
+ if (!comment.ok) {
5059
+ process.stderr.write(`[goal-tick] dispatchNextTask: comment failed on #${next.number}: ${comment.error}
5060
+ `);
5061
+ return;
5062
+ }
5063
+ const label = addLabel2(next.number, DISPATCHED_LABEL, ctx.cwd);
5064
+ if (!label.ok) {
5065
+ process.stderr.write(
5066
+ `[goal-tick] dispatchNextTask: add-label failed on #${next.number}: ${label.error} (continuing \u2014 comment already posted)
5067
+ `
5068
+ );
5069
+ }
5070
+ goal.lastDispatchedIssue = next.number;
5071
+ };
5072
+
5073
+ // src/scripts/ensureGoalBranch.ts
5074
+ var ensureGoalBranch = async (ctx) => {
5075
+ const goal = ctx.data.goal;
5076
+ if (!goal) return;
5077
+ fetchOrigin(ctx.cwd);
5078
+ if (remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5079
+ process.stdout.write(`[goal-tick] origin/${goal.goalBranch} already exists \u2014 leaving as-is
5080
+ `);
5081
+ return;
5082
+ }
5083
+ if (!remoteBranchExists(goal.defaultBranch, ctx.cwd)) {
5084
+ process.stderr.write(`[goal-tick] cannot create goal branch: origin/${goal.defaultBranch} missing
5085
+ `);
5086
+ return;
5087
+ }
5088
+ process.stdout.write(`[goal-tick] creating origin/${goal.goalBranch} from origin/${goal.defaultBranch}
5089
+ `);
5090
+ const r = createBranchFrom(goal.goalBranch, goal.defaultBranch, ctx.cwd);
5091
+ if (!r.ok) {
5092
+ process.stderr.write(
5093
+ `[goal-tick] push of ${goal.goalBranch} failed: ${r.error} \u2014 task dispatch will fall back to defaultBranch
5094
+ `
5095
+ );
5096
+ }
5097
+ };
5098
+
5099
+ // src/scripts/ensureGoalPr.ts
5100
+ var ensureGoalPr = async (ctx) => {
5101
+ const goal = ctx.data.goal;
5102
+ if (!goal) return;
5103
+ if (goal.goalPrUrl) return;
5104
+ if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) return;
5105
+ const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5106
+ if (existing.ok && existing.value && existing.value.length > 0) {
5107
+ goal.goalPrUrl = existing.value[0].url;
5108
+ return;
5109
+ }
5110
+ const title = `goal: ${goal.id}`;
5111
+ const body = goal.goalIssueNumber ? `Tracking integration PR for goal **${goal.id}**.
5112
+
5113
+ Child task PRs merge into \`${goal.goalBranch}\`. This PR is held in **draft** until every task is complete, then promoted to ready-for-review by goal-tick.
5114
+
5115
+ Closes #${goal.goalIssueNumber}
5116
+ ` : `Tracking integration PR for goal **${goal.id}**.
5117
+
5118
+ Child task PRs merge into \`${goal.goalBranch}\`. Held in **draft** until every task is complete.
5119
+ `;
5120
+ const created = createPr(
5121
+ {
5122
+ head: goal.goalBranch,
5123
+ base: goal.defaultBranch,
5124
+ title,
5125
+ body,
5126
+ draft: true
5127
+ },
5128
+ ctx.cwd
5129
+ );
5130
+ if (!created.ok) {
5131
+ process.stderr.write(
5132
+ `[goal-tick] ensureGoalPr: gh pr create failed: ${created.error} (continuing without goal PR)
5133
+ `
5134
+ );
5135
+ return;
5136
+ }
5137
+ process.stdout.write(`[goal-tick] opened draft goal PR ${created.value} for ${goal.id}
5138
+ `);
5139
+ goal.goalPrUrl = created.value;
5140
+ };
5141
+
5142
+ // src/scripts/ensureLifecycleLabels.ts
5143
+ var ensureLifecycleLabels = async (ctx) => {
5144
+ const goal = ctx.data.goal;
5145
+ if (!goal) return;
5146
+ for (const spec of TICK_LABELS) {
5147
+ const r2 = ensureLabel(spec.name, spec.color, spec.description, ctx.cwd);
5148
+ if (!r2.ok) {
5149
+ process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${spec.name}: ${r2.error}
5150
+ `);
5151
+ }
5152
+ }
5153
+ const goalLbl = goalLabel(goal.id);
5154
+ const r = ensureLabel(goalLbl, "0e8a16", `kody goal task: belongs to goal ${goal.id}`, ctx.cwd);
5155
+ if (!r.ok) {
5156
+ process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${goalLbl}: ${r.error}
5157
+ `);
5158
+ }
5159
+ const u = ensureLabel(
5160
+ UMBRELLA_BUILDING_LABEL,
5161
+ "1d76db",
5162
+ "kody: in-flight (work being assembled on a branch)",
5163
+ ctx.cwd
5164
+ );
5165
+ if (!u.ok) {
5166
+ process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${UMBRELLA_BUILDING_LABEL}: ${u.error}
5167
+ `);
5168
+ }
5169
+ };
5170
+
4691
5171
  // src/pr.ts
4692
5172
  var TITLE_MAX = 72;
4693
5173
  function stripTitlePrefixes(raw) {
@@ -4901,8 +5381,203 @@ function collectExpectedTests(raw) {
4901
5381
  return out;
4902
5382
  }
4903
5383
 
5384
+ // src/scripts/ensureUmbrellaIssue.ts
5385
+ var ensureUmbrellaIssue = async (ctx) => {
5386
+ const goal = ctx.data.goal;
5387
+ if (!goal) return;
5388
+ if (goal.goalIssueNumber !== void 0) return;
5389
+ const title = `goal: ${goal.id}`;
5390
+ const body = `Umbrella issue for goal **${goal.id}**.
5391
+
5392
+ Closed automatically when the goal PR (\`${goal.goalBranch}\` \u2192 \`${goal.defaultBranch}\`) merges.
5393
+ `;
5394
+ const existing = findUmbrellaByTitle(goal.id, title, ctx.cwd);
5395
+ if (existing.ok && existing.value !== null && existing.value !== void 0) {
5396
+ process.stdout.write(`[goal-tick] adopted existing umbrella issue #${existing.value} for ${goal.id}
5397
+ `);
5398
+ goal.goalIssueNumber = existing.value;
5399
+ return;
5400
+ }
5401
+ const created = createIssue(
5402
+ {
5403
+ title,
5404
+ body,
5405
+ labels: [goalLabel(goal.id), UMBRELLA_BUILDING_LABEL]
5406
+ },
5407
+ ctx.cwd
5408
+ );
5409
+ if (!created.ok) {
5410
+ process.stderr.write(
5411
+ `[goal-tick] ensureUmbrellaIssue: gh issue create failed: ${created.error} \u2014 continuing without umbrella issue
5412
+ `
5413
+ );
5414
+ return;
5415
+ }
5416
+ process.stdout.write(`[goal-tick] opened umbrella issue #${created.value} for ${goal.id}
5417
+ `);
5418
+ goal.goalIssueNumber = created.value;
5419
+ };
5420
+
5421
+ // src/goal/state.ts
5422
+ import * as fs20 from "fs";
5423
+ import * as path20 from "path";
5424
+ var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5425
+ var GoalStateError = class extends Error {
5426
+ constructor(path29, message) {
5427
+ super(`Invalid goal state at ${path29}:
5428
+ ${message}`);
5429
+ this.path = path29;
5430
+ this.name = "GoalStateError";
5431
+ }
5432
+ path;
5433
+ };
5434
+ function parseGoalState(filePath, raw) {
5435
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5436
+ throw new GoalStateError(filePath, "must be a JSON object");
5437
+ }
5438
+ const r = raw;
5439
+ const stateValue = r.state;
5440
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5441
+ throw new GoalStateError(
5442
+ filePath,
5443
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5444
+ );
5445
+ }
5446
+ const parsed = {
5447
+ state: stateValue,
5448
+ extra: {}
5449
+ };
5450
+ if (typeof r.goalIssueNumber === "number" && Number.isFinite(r.goalIssueNumber)) {
5451
+ parsed.goalIssueNumber = r.goalIssueNumber;
5452
+ }
5453
+ if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5454
+ parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5455
+ }
5456
+ if (typeof r.goalPrUrl === "string" && r.goalPrUrl.length > 0) {
5457
+ parsed.goalPrUrl = r.goalPrUrl;
5458
+ }
5459
+ for (const ts of ["updatedAt", "createdAt", "startedAt", "completedAt"]) {
5460
+ const v = r[ts];
5461
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5462
+ }
5463
+ const known = /* @__PURE__ */ new Set([
5464
+ "state",
5465
+ "goalIssueNumber",
5466
+ "lastDispatchedIssue",
5467
+ "goalPrUrl",
5468
+ "updatedAt",
5469
+ "createdAt",
5470
+ "startedAt",
5471
+ "completedAt"
5472
+ ]);
5473
+ for (const [k, v] of Object.entries(r)) {
5474
+ if (!known.has(k)) parsed.extra[k] = v;
5475
+ }
5476
+ return parsed;
5477
+ }
5478
+ function serializeGoalState(s) {
5479
+ const obj = { ...s.extra, state: s.state };
5480
+ if (s.goalIssueNumber !== void 0) obj.goalIssueNumber = s.goalIssueNumber;
5481
+ if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5482
+ if (s.goalPrUrl !== void 0) obj.goalPrUrl = s.goalPrUrl;
5483
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5484
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5485
+ if (s.completedAt !== void 0) obj.completedAt = s.completedAt;
5486
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5487
+ return `${JSON.stringify(obj, null, 2)}
5488
+ `;
5489
+ }
5490
+ function goalStatePath(cwd, goalId) {
5491
+ return path20.join(cwd, ".kody", "goals", goalId, "state.json");
5492
+ }
5493
+ function readGoalState(cwd, goalId) {
5494
+ const file = goalStatePath(cwd, goalId);
5495
+ if (!fs20.existsSync(file)) {
5496
+ throw new GoalStateError(file, "file not found");
5497
+ }
5498
+ let raw;
5499
+ try {
5500
+ raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
5501
+ } catch (err) {
5502
+ throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
5503
+ }
5504
+ return parseGoalState(file, raw);
5505
+ }
5506
+ function writeGoalState(cwd, goalId, state) {
5507
+ const file = goalStatePath(cwd, goalId);
5508
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
5509
+ fs20.writeFileSync(file, serializeGoalState(state), "utf-8");
5510
+ }
5511
+ function nowIso() {
5512
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5513
+ }
5514
+
5515
+ // src/scripts/finalizeGoal.ts
5516
+ var finalizeGoal = async (ctx) => {
5517
+ const goal = ctx.data.goal;
5518
+ if (!goal) return;
5519
+ process.stdout.write(`[goal-tick] all task(s) closed \u2014 finalising goal ${goal.id}
5520
+ `);
5521
+ if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5522
+ process.stderr.write(`[goal-tick] goal branch ${goal.goalBranch} not found on origin \u2014 skipping final PR
5523
+ `);
5524
+ finishState(goal);
5525
+ return;
5526
+ }
5527
+ const title = `goal: ${goal.id}`;
5528
+ const closesLine = goal.goalIssueNumber ? `
5529
+
5530
+ Closes #${goal.goalIssueNumber}
5531
+ ` : "\n";
5532
+ const body = `Final integration PR for goal **${goal.id}**.
5533
+
5534
+ All task issues are closed and merged into \`${goal.goalBranch}\`. Ready for review.${closesLine}`;
5535
+ const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5536
+ if (existing.ok && existing.value && existing.value.length > 0) {
5537
+ const pr = existing.value[0];
5538
+ goal.goalPrUrl = pr.url;
5539
+ const edit = editPrBody(pr.number, body, ctx.cwd);
5540
+ if (!edit.ok) {
5541
+ process.stderr.write(`[goal-tick] finalizeGoal: editPrBody failed: ${edit.error}
5542
+ `);
5543
+ }
5544
+ if (pr.isDraft) {
5545
+ process.stdout.write(`[goal-tick] promoting draft goal PR #${pr.number} to ready-for-review
5546
+ `);
5547
+ const ready = markPrReady(pr.number, ctx.cwd);
5548
+ if (!ready.ok) {
5549
+ process.stderr.write(`[goal-tick] finalizeGoal: markPrReady failed: ${ready.error}
5550
+ `);
5551
+ }
5552
+ }
5553
+ } else {
5554
+ const created = createPr(
5555
+ {
5556
+ head: goal.goalBranch,
5557
+ base: goal.defaultBranch,
5558
+ title,
5559
+ body,
5560
+ // ready-for-review (not draft) since we're finalizing.
5561
+ draft: false
5562
+ },
5563
+ ctx.cwd
5564
+ );
5565
+ if (!created.ok) {
5566
+ process.stderr.write(`[goal-tick] finalizeGoal: gh pr create failed: ${created.error}
5567
+ `);
5568
+ } else {
5569
+ goal.goalPrUrl = created.value;
5570
+ }
5571
+ }
5572
+ finishState(goal);
5573
+ };
5574
+ function finishState(goal) {
5575
+ goal.state = "done";
5576
+ goal.completedAt = nowIso();
5577
+ }
5578
+
4904
5579
  // src/scripts/finishFlow.ts
4905
- import { execFileSync as execFileSync13 } from "child_process";
5580
+ import { execFileSync as execFileSync15 } from "child_process";
4906
5581
  var API_TIMEOUT_MS6 = 3e4;
4907
5582
  var STATUS_ICON = {
4908
5583
  "review-passed": "\u2705",
@@ -4936,7 +5611,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4936
5611
  **PR:** ${state.core.prUrl}` : "";
4937
5612
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
4938
5613
  try {
4939
- execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5614
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4940
5615
  timeout: API_TIMEOUT_MS6,
4941
5616
  cwd: ctx.cwd,
4942
5617
  stdio: ["ignore", "pipe", "pipe"]
@@ -4950,7 +5625,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4950
5625
  };
4951
5626
 
4952
5627
  // src/branch.ts
4953
- import { execFileSync as execFileSync14 } from "child_process";
5628
+ import { execFileSync as execFileSync16 } from "child_process";
4954
5629
  var UncommittedChangesError = class extends Error {
4955
5630
  constructor(branch) {
4956
5631
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -4960,7 +5635,7 @@ var UncommittedChangesError = class extends Error {
4960
5635
  branch;
4961
5636
  };
4962
5637
  function git2(args, cwd) {
4963
- return execFileSync14("git", args, {
5638
+ return execFileSync16("git", args, {
4964
5639
  encoding: "utf-8",
4965
5640
  timeout: 3e4,
4966
5641
  cwd,
@@ -4985,7 +5660,7 @@ function checkoutPrBranch(prNumber, cwd) {
4985
5660
  SKIP_HOOKS: "1",
4986
5661
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
4987
5662
  };
4988
- execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
5663
+ execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
4989
5664
  cwd,
4990
5665
  env,
4991
5666
  stdio: ["ignore", "pipe", "pipe"],
@@ -5099,8 +5774,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5099
5774
  }
5100
5775
 
5101
5776
  // src/gha.ts
5102
- import { execFileSync as execFileSync15 } from "child_process";
5103
- import * as fs20 from "fs";
5777
+ import { execFileSync as execFileSync17 } from "child_process";
5778
+ import * as fs21 from "fs";
5104
5779
  function getRunUrl() {
5105
5780
  const server = process.env.GITHUB_SERVER_URL;
5106
5781
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5111,10 +5786,10 @@ function getRunUrl() {
5111
5786
  function reactToTriggerComment(cwd) {
5112
5787
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5113
5788
  const eventPath = process.env.GITHUB_EVENT_PATH;
5114
- if (!eventPath || !fs20.existsSync(eventPath)) return;
5789
+ if (!eventPath || !fs21.existsSync(eventPath)) return;
5115
5790
  let event = null;
5116
5791
  try {
5117
- event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5792
+ event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5118
5793
  } catch {
5119
5794
  return;
5120
5795
  }
@@ -5142,7 +5817,7 @@ function reactToTriggerComment(cwd) {
5142
5817
  for (let attempt = 0; attempt < 3; attempt++) {
5143
5818
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5144
5819
  try {
5145
- execFileSync15("gh", args, opts);
5820
+ execFileSync17("gh", args, opts);
5146
5821
  return;
5147
5822
  } catch (err) {
5148
5823
  lastErr = err;
@@ -5155,13 +5830,13 @@ function reactToTriggerComment(cwd) {
5155
5830
  }
5156
5831
  function sleepMs(ms) {
5157
5832
  try {
5158
- execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5833
+ execFileSync17("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5159
5834
  } catch {
5160
5835
  }
5161
5836
  }
5162
5837
 
5163
5838
  // src/workflow.ts
5164
- import { execFileSync as execFileSync16 } from "child_process";
5839
+ import { execFileSync as execFileSync18 } from "child_process";
5165
5840
  var GH_TIMEOUT_MS = 3e4;
5166
5841
  function ghToken3() {
5167
5842
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -5169,7 +5844,7 @@ function ghToken3() {
5169
5844
  function gh3(args, cwd) {
5170
5845
  const token = ghToken3();
5171
5846
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
5172
- return execFileSync16("gh", args, {
5847
+ return execFileSync18("gh", args, {
5173
5848
  encoding: "utf-8",
5174
5849
  timeout: GH_TIMEOUT_MS,
5175
5850
  cwd,
@@ -5352,24 +6027,63 @@ function tryPostPr2(prNumber, body, cwd) {
5352
6027
  }
5353
6028
  }
5354
6029
 
6030
+ // src/scripts/handleAbandonedGoal.ts
6031
+ var handleAbandonedGoal = async (ctx) => {
6032
+ const goal = ctx.data.goal;
6033
+ if (!goal || goal.state !== "abandoned") return;
6034
+ process.stdout.write(`[goal-tick] ${goal.id} is abandoned \u2014 running cleanup
6035
+ `);
6036
+ const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
6037
+ if (!issues.ok) {
6038
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: list failed: ${issues.error}
6039
+ `);
6040
+ } else {
6041
+ for (const i of issues.value ?? []) {
6042
+ if (i.state !== "OPEN") continue;
6043
+ const r = closeIssue(
6044
+ i.number,
6045
+ {
6046
+ comment: "_Goal abandoned \u2014 closing this task without dispatch._",
6047
+ reason: "not planned"
6048
+ },
6049
+ ctx.cwd
6050
+ );
6051
+ if (!r.ok) {
6052
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close #${i.number}: ${r.error}
6053
+ `);
6054
+ }
6055
+ }
6056
+ }
6057
+ const goalPrs = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
6058
+ if (goalPrs.ok && goalPrs.value && goalPrs.value.length > 0) {
6059
+ const pr = goalPrs.value[0];
6060
+ const r = closePr(pr.number, "_Goal abandoned by operator \u2014 closing without merge._", ctx.cwd);
6061
+ if (!r.ok) {
6062
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close goal PR #${pr.number}: ${r.error}
6063
+ `);
6064
+ }
6065
+ }
6066
+ goal.state = "closed";
6067
+ };
6068
+
5355
6069
  // src/scripts/initFlow.ts
5356
- import { execFileSync as execFileSync17 } from "child_process";
5357
- import * as fs22 from "fs";
5358
- import * as path20 from "path";
6070
+ import { execFileSync as execFileSync19 } from "child_process";
6071
+ import * as fs23 from "fs";
6072
+ import * as path22 from "path";
5359
6073
 
5360
6074
  // src/scripts/loadQaGuide.ts
5361
- import * as fs21 from "fs";
5362
- import * as path19 from "path";
6075
+ import * as fs22 from "fs";
6076
+ import * as path21 from "path";
5363
6077
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
5364
6078
  var loadQaGuide = async (ctx) => {
5365
- const full = path19.join(ctx.cwd, QA_GUIDE_REL_PATH);
5366
- if (!fs21.existsSync(full)) {
6079
+ const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
6080
+ if (!fs22.existsSync(full)) {
5367
6081
  ctx.data.qaGuide = "";
5368
6082
  ctx.data.qaGuidePath = "";
5369
6083
  return;
5370
6084
  }
5371
6085
  try {
5372
- ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
6086
+ ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
5373
6087
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
5374
6088
  } catch {
5375
6089
  ctx.data.qaGuide = "";
@@ -5379,9 +6093,9 @@ var loadQaGuide = async (ctx) => {
5379
6093
 
5380
6094
  // src/scripts/initFlow.ts
5381
6095
  function detectPackageManager(cwd) {
5382
- if (fs22.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5383
- if (fs22.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
5384
- if (fs22.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
6096
+ if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6097
+ if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
6098
+ if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
5385
6099
  return "npm";
5386
6100
  }
5387
6101
  function qualityCommandsFor(pm) {
@@ -5394,7 +6108,7 @@ function qualityCommandsFor(pm) {
5394
6108
  function detectOwnerRepo(cwd) {
5395
6109
  let url;
5396
6110
  try {
5397
- url = execFileSync17("git", ["remote", "get-url", "origin"], {
6111
+ url = execFileSync19("git", ["remote", "get-url", "origin"], {
5398
6112
  cwd,
5399
6113
  encoding: "utf-8",
5400
6114
  stdio: ["ignore", "pipe", "pipe"]
@@ -5479,7 +6193,7 @@ jobs:
5479
6193
  `;
5480
6194
  function defaultBranchFromGit(cwd) {
5481
6195
  try {
5482
- const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
6196
+ const ref = execFileSync19("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5483
6197
  cwd,
5484
6198
  encoding: "utf-8",
5485
6199
  stdio: ["ignore", "pipe", "pipe"]
@@ -5487,7 +6201,7 @@ function defaultBranchFromGit(cwd) {
5487
6201
  return ref.replace("refs/remotes/origin/", "");
5488
6202
  } catch {
5489
6203
  try {
5490
- return execFileSync17("git", ["branch", "--show-current"], {
6204
+ return execFileSync19("git", ["branch", "--show-current"], {
5491
6205
  cwd,
5492
6206
  encoding: "utf-8",
5493
6207
  stdio: ["ignore", "pipe", "pipe"]
@@ -5503,48 +6217,48 @@ function performInit(cwd, force) {
5503
6217
  const pm = detectPackageManager(cwd);
5504
6218
  const ownerRepo = detectOwnerRepo(cwd);
5505
6219
  const defaultBranch = defaultBranchFromGit(cwd);
5506
- const configPath = path20.join(cwd, "kody.config.json");
5507
- if (fs22.existsSync(configPath) && !force) {
6220
+ const configPath = path22.join(cwd, "kody.config.json");
6221
+ if (fs23.existsSync(configPath) && !force) {
5508
6222
  skipped.push("kody.config.json");
5509
6223
  } else {
5510
6224
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
5511
- fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6225
+ fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5512
6226
  `);
5513
6227
  wrote.push("kody.config.json");
5514
6228
  }
5515
- const workflowDir = path20.join(cwd, ".github", "workflows");
5516
- const workflowPath = path20.join(workflowDir, "kody.yml");
5517
- if (fs22.existsSync(workflowPath) && !force) {
6229
+ const workflowDir = path22.join(cwd, ".github", "workflows");
6230
+ const workflowPath = path22.join(workflowDir, "kody.yml");
6231
+ if (fs23.existsSync(workflowPath) && !force) {
5518
6232
  skipped.push(".github/workflows/kody.yml");
5519
6233
  } else {
5520
- fs22.mkdirSync(workflowDir, { recursive: true });
5521
- fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6234
+ fs23.mkdirSync(workflowDir, { recursive: true });
6235
+ fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5522
6236
  wrote.push(".github/workflows/kody.yml");
5523
6237
  }
5524
- const hasUi = fs22.existsSync(path20.join(cwd, "src/app")) || fs22.existsSync(path20.join(cwd, "app")) || fs22.existsSync(path20.join(cwd, "pages"));
6238
+ const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
5525
6239
  if (hasUi) {
5526
- const qaGuidePath = path20.join(cwd, QA_GUIDE_REL_PATH);
5527
- if (fs22.existsSync(qaGuidePath) && !force) {
6240
+ const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6241
+ if (fs23.existsSync(qaGuidePath) && !force) {
5528
6242
  skipped.push(QA_GUIDE_REL_PATH);
5529
6243
  } else {
5530
- fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
6244
+ fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
5531
6245
  const discovery = runQaDiscovery(cwd);
5532
- fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6246
+ fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5533
6247
  wrote.push(QA_GUIDE_REL_PATH);
5534
6248
  }
5535
6249
  }
5536
6250
  const builtinJobs = listBuiltinJobs();
5537
6251
  if (builtinJobs.length > 0) {
5538
- const jobsDir = path20.join(cwd, ".kody", "jobs");
5539
- fs22.mkdirSync(jobsDir, { recursive: true });
6252
+ const jobsDir = path22.join(cwd, ".kody", "jobs");
6253
+ fs23.mkdirSync(jobsDir, { recursive: true });
5540
6254
  for (const job of builtinJobs) {
5541
- const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
5542
- const target = path20.join(cwd, rel);
5543
- if (fs22.existsSync(target) && !force) {
6255
+ const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6256
+ const target = path22.join(cwd, rel);
6257
+ if (fs23.existsSync(target) && !force) {
5544
6258
  skipped.push(rel);
5545
6259
  continue;
5546
6260
  }
5547
- fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
6261
+ fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
5548
6262
  wrote.push(rel);
5549
6263
  }
5550
6264
  }
@@ -5556,12 +6270,12 @@ function performInit(cwd, force) {
5556
6270
  continue;
5557
6271
  }
5558
6272
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
5559
- const target = path20.join(workflowDir, `kody-${exe.name}.yml`);
5560
- if (fs22.existsSync(target) && !force) {
6273
+ const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6274
+ if (fs23.existsSync(target) && !force) {
5561
6275
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
5562
6276
  continue;
5563
6277
  }
5564
- fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6278
+ fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5565
6279
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
5566
6280
  }
5567
6281
  let labels;
@@ -5649,6 +6363,48 @@ var loadCoverageRules = async (ctx) => {
5649
6363
  ctx.data.coverageRules = ctx.config.testRequirements ?? [];
5650
6364
  };
5651
6365
 
6366
+ // src/scripts/loadGoalState.ts
6367
+ var loadGoalState = async (ctx) => {
6368
+ const goalId = ctx.args.goal;
6369
+ if (typeof goalId !== "string" || goalId.length === 0) {
6370
+ ctx.skipAgent = true;
6371
+ ctx.output.exitCode = 1;
6372
+ ctx.output.reason = "missing --goal";
6373
+ return;
6374
+ }
6375
+ if (goalId.includes("/") || goalId.includes("..")) {
6376
+ ctx.skipAgent = true;
6377
+ ctx.output.exitCode = 1;
6378
+ ctx.output.reason = "invalid goal id (no slashes or '..' allowed)";
6379
+ return;
6380
+ }
6381
+ try {
6382
+ const state = readGoalState(ctx.cwd, goalId);
6383
+ ctx.data.goal = {
6384
+ id: goalId,
6385
+ state: state.state,
6386
+ goalIssueNumber: state.goalIssueNumber,
6387
+ lastDispatchedIssue: state.lastDispatchedIssue,
6388
+ goalPrUrl: state.goalPrUrl,
6389
+ // Cache the full parsed object so saveGoalState can preserve `extra`.
6390
+ raw: state,
6391
+ // `phase` is populated by deriveGoalPhase later in the chain. Initialize
6392
+ // to undefined so runWhen on `data.goal.phase` can match correctly.
6393
+ phase: void 0,
6394
+ // Populated by ensureGoalBranch / configured by config.git.defaultBranch.
6395
+ defaultBranch: ctx.config.git.defaultBranch,
6396
+ // Convenience derivations.
6397
+ goalBranch: `goal-${goalId}`
6398
+ };
6399
+ } catch (err) {
6400
+ process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
6401
+ `);
6402
+ ctx.skipAgent = true;
6403
+ ctx.output.exitCode = 0;
6404
+ ctx.output.reason = "no goal state to tick";
6405
+ }
6406
+ };
6407
+
5652
6408
  // src/scripts/loadIssueContext.ts
5653
6409
  var DEFAULT_COMMENT_LIMIT = 12;
5654
6410
  var DEFAULT_COMMENT_MAX_BYTES = 16e3;
@@ -5699,8 +6455,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
5699
6455
  };
5700
6456
 
5701
6457
  // src/scripts/loadJobFromFile.ts
5702
- import * as fs23 from "fs";
5703
- import * as path21 from "path";
6458
+ import * as fs24 from "fs";
6459
+ import * as path23 from "path";
5704
6460
  var loadJobFromFile = async (ctx, _profile, args) => {
5705
6461
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
5706
6462
  const slugArg = String(args?.slugArg ?? "job");
@@ -5708,11 +6464,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
5708
6464
  if (!slug) {
5709
6465
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
5710
6466
  }
5711
- const absPath = path21.join(ctx.cwd, jobsDir, `${slug}.md`);
5712
- if (!fs23.existsSync(absPath)) {
6467
+ const absPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
6468
+ if (!fs24.existsSync(absPath)) {
5713
6469
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
5714
6470
  }
5715
- const raw = fs23.readFileSync(absPath, "utf-8");
6471
+ const raw = fs24.readFileSync(absPath, "utf-8");
5716
6472
  const { title, body } = parseJobFile(raw, slug);
5717
6473
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
5718
6474
  const loaded = await backend.load(slug);
@@ -5744,16 +6500,16 @@ function humanizeSlug(slug) {
5744
6500
  }
5745
6501
 
5746
6502
  // src/scripts/loadMemoryContext.ts
5747
- import * as fs24 from "fs";
5748
- import * as path22 from "path";
6503
+ import * as fs25 from "fs";
6504
+ import * as path24 from "path";
5749
6505
  var MEMORY_DIR_RELATIVE = ".kody/memory";
5750
6506
  var MAX_PAGES = 8;
5751
6507
  var PER_PAGE_MAX_BYTES = 4e3;
5752
6508
  var TOTAL_MAX_BYTES = 24e3;
5753
6509
  var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
5754
6510
  var loadMemoryContext = async (ctx) => {
5755
- const memoryAbs = path22.join(ctx.cwd, MEMORY_DIR_RELATIVE);
5756
- if (!fs24.existsSync(memoryAbs)) {
6511
+ const memoryAbs = path24.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6512
+ if (!fs25.existsSync(memoryAbs)) {
5757
6513
  ctx.data.memoryContext = "";
5758
6514
  return;
5759
6515
  }
@@ -5778,21 +6534,21 @@ function collectPages(memoryAbs) {
5778
6534
  walkMd(memoryAbs, (file) => {
5779
6535
  let stat;
5780
6536
  try {
5781
- stat = fs24.statSync(file);
6537
+ stat = fs25.statSync(file);
5782
6538
  } catch {
5783
6539
  return;
5784
6540
  }
5785
6541
  let raw;
5786
6542
  try {
5787
- raw = fs24.readFileSync(file, "utf-8");
6543
+ raw = fs25.readFileSync(file, "utf-8");
5788
6544
  } catch {
5789
6545
  return;
5790
6546
  }
5791
6547
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5792
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path22.basename(file, ".md");
6548
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path24.basename(file, ".md");
5793
6549
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
5794
6550
  out.push({
5795
- relPath: path22.relative(memoryAbs, file),
6551
+ relPath: path24.relative(memoryAbs, file),
5796
6552
  title,
5797
6553
  updated,
5798
6554
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -5860,16 +6616,16 @@ function walkMd(root, visit) {
5860
6616
  const dir = stack.pop();
5861
6617
  let names;
5862
6618
  try {
5863
- names = fs24.readdirSync(dir);
6619
+ names = fs25.readdirSync(dir);
5864
6620
  } catch {
5865
6621
  continue;
5866
6622
  }
5867
6623
  for (const name of names) {
5868
6624
  if (name.startsWith(".")) continue;
5869
- const full = path22.join(dir, name);
6625
+ const full = path24.join(dir, name);
5870
6626
  let stat;
5871
6627
  try {
5872
- stat = fs24.statSync(full);
6628
+ stat = fs25.statSync(full);
5873
6629
  } catch {
5874
6630
  continue;
5875
6631
  }
@@ -5992,8 +6748,32 @@ var markFlowSuccess = async (ctx) => {
5992
6748
  }
5993
6749
  };
5994
6750
 
6751
+ // src/scripts/mergeReadyTaskPRs.ts
6752
+ var mergeReadyTaskPRs = async (ctx) => {
6753
+ const goal = ctx.data.goal;
6754
+ if (!goal) return;
6755
+ const open = listPrsByBase(goal.goalBranch, "open", ctx.cwd);
6756
+ if (!open.ok) {
6757
+ process.stderr.write(`[goal-tick] mergeReadyTaskPRs: list failed: ${open.error}
6758
+ `);
6759
+ return;
6760
+ }
6761
+ for (const pr of open.value ?? []) {
6762
+ if (pr.isDraft) continue;
6763
+ if (pr.mergeable !== "MERGEABLE") continue;
6764
+ if (pr.mergeStateStatus !== "CLEAN") continue;
6765
+ process.stdout.write(`[goal-tick] merging PR #${pr.number} into ${goal.goalBranch}
6766
+ `);
6767
+ const r = mergePrSquash(pr.number, ctx.cwd);
6768
+ if (!r.ok) {
6769
+ process.stderr.write(`[goal-tick] failed to merge PR #${pr.number}: ${r.error} (continuing)
6770
+ `);
6771
+ }
6772
+ }
6773
+ };
6774
+
5995
6775
  // src/scripts/mergeReleasePr.ts
5996
- import { execFileSync as execFileSync18 } from "child_process";
6776
+ import { execFileSync as execFileSync20 } from "child_process";
5997
6777
  var API_TIMEOUT_MS7 = 6e4;
5998
6778
  var mergeReleasePr = async (ctx) => {
5999
6779
  const state = ctx.data.taskState;
@@ -6012,7 +6792,7 @@ var mergeReleasePr = async (ctx) => {
6012
6792
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
6013
6793
  `);
6014
6794
  try {
6015
- const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
6795
+ const out = execFileSync20("gh", ["pr", "merge", String(prNumber), "--merge"], {
6016
6796
  timeout: API_TIMEOUT_MS7,
6017
6797
  cwd: ctx.cwd,
6018
6798
  stdio: ["ignore", "pipe", "pipe"]
@@ -6130,7 +6910,7 @@ function buildIssueTitle(scope, verdict) {
6130
6910
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
6131
6911
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
6132
6912
  }
6133
- function ensureLabel2(cwd) {
6913
+ function ensureLabel3(cwd) {
6134
6914
  try {
6135
6915
  gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6136
6916
  return true;
@@ -6190,7 +6970,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
6190
6970
  }
6191
6971
  const scope = ctx.args.scope;
6192
6972
  const title = buildIssueTitle(scope, verdict);
6193
- const hasLabel = ensureLabel2(ctx.cwd);
6973
+ const hasLabel = ensureLabel3(ctx.cwd);
6194
6974
  let created;
6195
6975
  try {
6196
6976
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -6595,7 +7375,7 @@ ${body}`;
6595
7375
  }
6596
7376
 
6597
7377
  // src/scripts/recordClassification.ts
6598
- import { execFileSync as execFileSync19 } from "child_process";
7378
+ import { execFileSync as execFileSync21 } from "child_process";
6599
7379
  var API_TIMEOUT_MS8 = 3e4;
6600
7380
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6601
7381
  var recordClassification = async (ctx) => {
@@ -6643,7 +7423,7 @@ function parseClassification(prSummary) {
6643
7423
  }
6644
7424
  function tryAuditComment(issueNumber, body, cwd) {
6645
7425
  try {
6646
- execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7426
+ execFileSync21("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6647
7427
  cwd,
6648
7428
  timeout: API_TIMEOUT_MS8,
6649
7429
  stdio: ["ignore", "pipe", "pipe"]
@@ -6682,14 +7462,14 @@ var requireFeedbackActions = async (ctx, profile) => {
6682
7462
  const items = countActionItems(actions);
6683
7463
  ctx.data.feedbackAgentItemCount = items;
6684
7464
  if (items < MIN_ITEMS) {
6685
- fail(
7465
+ fail2(
6686
7466
  ctx,
6687
7467
  profile,
6688
7468
  actions.length === 0 ? "agent omitted required FEEDBACK_ACTIONS block \u2014 cannot verify that review feedback was addressed" : "agent FEEDBACK_ACTIONS block listed no items \u2014 cannot verify that review feedback was addressed"
6689
7469
  );
6690
7470
  }
6691
7471
  };
6692
- function fail(ctx, profile, reason) {
7472
+ function fail2(ctx, profile, reason) {
6693
7473
  ctx.data.agentDone = false;
6694
7474
  ctx.data.agentFailureReason = reason;
6695
7475
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
@@ -6717,13 +7497,19 @@ var requirePlanDeviations = async (ctx, profile) => {
6717
7497
  if (!planContent) return;
6718
7498
  const raw = String(ctx.data.planDeviations ?? "").trim();
6719
7499
  if (raw.length === 0) {
6720
- fail2(ctx, profile, "agent omitted required PLAN_DEVIATIONS block \u2014 cannot verify whether the plan was followed");
7500
+ process.stderr.write(
7501
+ "[kody requirePlanDeviations] warning: agent omitted PLAN_DEVIATIONS block \u2014 proceeding anyway (verify/tests are the real gate)\n"
7502
+ );
7503
+ ctx.data.planDeviationsOmitted = true;
6721
7504
  return;
6722
7505
  }
6723
7506
  if (isNoneSentinel(raw)) return;
6724
7507
  const bullets = raw.split("\n").filter((l) => /^\s*[-*]\s+/.test(l));
6725
7508
  if (bullets.length === 0) {
6726
- fail2(ctx, profile, "agent PLAN_DEVIATIONS block is not 'none' and lists no bullet items");
7509
+ process.stderr.write(
7510
+ "[kody requirePlanDeviations] warning: PLAN_DEVIATIONS block is not 'none' and lists no bullet items \u2014 proceeding anyway\n"
7511
+ );
7512
+ ctx.data.planDeviationsMalformed = true;
6727
7513
  return;
6728
7514
  }
6729
7515
  ctx.data.planDeviationCount = bullets.length;
@@ -6735,17 +7521,6 @@ function isNoneSentinel(block) {
6735
7521
  if (stripped.length !== 1) return false;
6736
7522
  return stripped[0] === "none";
6737
7523
  }
6738
- function fail2(ctx, profile, reason) {
6739
- ctx.data.agentDone = false;
6740
- ctx.data.agentFailureReason = reason;
6741
- const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
6742
- const failedAction6 = {
6743
- type: `${modeSeg}_FAILED`,
6744
- payload: { reason },
6745
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
6746
- };
6747
- ctx.data.action = failedAction6;
6748
- }
6749
7524
 
6750
7525
  // src/scripts/resolveArtifacts.ts
6751
7526
  var resolveArtifacts = async (ctx, profile) => {
@@ -6771,7 +7546,7 @@ var resolveArtifacts = async (ctx, profile) => {
6771
7546
  };
6772
7547
 
6773
7548
  // src/scripts/resolveFlow.ts
6774
- import { execFileSync as execFileSync20 } from "child_process";
7549
+ import { execFileSync as execFileSync22 } from "child_process";
6775
7550
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
6776
7551
  var resolveFlow = async (ctx) => {
6777
7552
  const prNumber = ctx.args.pr;
@@ -6841,7 +7616,7 @@ function buildPreferBlock(prefer, baseBranch) {
6841
7616
  }
6842
7617
  function getConflictedFiles(cwd) {
6843
7618
  try {
6844
- const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
7619
+ const out = execFileSync22("git", ["diff", "--name-only", "--diff-filter=U"], {
6845
7620
  encoding: "utf-8",
6846
7621
  cwd,
6847
7622
  env: { ...process.env, HUSKY: "0" }
@@ -6856,7 +7631,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
6856
7631
  let total = 0;
6857
7632
  for (const f of files) {
6858
7633
  try {
6859
- const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
7634
+ const content = execFileSync22("cat", [f], { encoding: "utf-8", cwd }).toString();
6860
7635
  const snippet = `### ${f}
6861
7636
 
6862
7637
  \`\`\`
@@ -6957,10 +7732,10 @@ var resolvePreviewUrl = async (ctx) => {
6957
7732
  };
6958
7733
 
6959
7734
  // src/scripts/resolveQaUrl.ts
6960
- import { execFileSync as execFileSync21 } from "child_process";
7735
+ import { execFileSync as execFileSync23 } from "child_process";
6961
7736
  function ghQuery(args, cwd) {
6962
7737
  try {
6963
- const out = execFileSync21("gh", args, {
7738
+ const out = execFileSync23("gh", args, {
6964
7739
  cwd,
6965
7740
  stdio: ["ignore", "pipe", "pipe"],
6966
7741
  encoding: "utf-8",
@@ -7030,7 +7805,7 @@ var resolveQaUrl = async (ctx) => {
7030
7805
  };
7031
7806
 
7032
7807
  // src/scripts/revertFlow.ts
7033
- import { execFileSync as execFileSync22 } from "child_process";
7808
+ import { execFileSync as execFileSync24 } from "child_process";
7034
7809
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
7035
7810
  var revertFlow = async (ctx) => {
7036
7811
  const prNumber = ctx.args.pr;
@@ -7112,7 +7887,7 @@ function buildPrSummary(resolved) {
7112
7887
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
7113
7888
  }
7114
7889
  function git3(args, cwd) {
7115
- return execFileSync22("git", args, {
7890
+ return execFileSync24("git", args, {
7116
7891
  encoding: "utf-8",
7117
7892
  timeout: 3e4,
7118
7893
  cwd,
@@ -7122,7 +7897,7 @@ function git3(args, cwd) {
7122
7897
  }
7123
7898
  function isAncestorOfHead(sha, cwd) {
7124
7899
  try {
7125
- execFileSync22("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7900
+ execFileSync24("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7126
7901
  cwd,
7127
7902
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7128
7903
  stdio: ["ignore", "ignore", "ignore"]
@@ -7218,17 +7993,17 @@ function resolveBaseOverride(value) {
7218
7993
  }
7219
7994
  function resolveBaseFromLabels(labels) {
7220
7995
  if (!labels.includes("goal-runner:dispatched")) return null;
7221
- const goalLabel = labels.find((l) => l.startsWith("goal:"));
7222
- if (!goalLabel) return null;
7223
- const goalId = goalLabel.slice("goal:".length);
7996
+ const goalLabel2 = labels.find((l) => l.startsWith("goal:"));
7997
+ if (!goalLabel2) return null;
7998
+ const goalId = goalLabel2.slice("goal:".length);
7224
7999
  if (!/^[a-z0-9-]+$/.test(goalId)) return null;
7225
8000
  return `goal-${goalId}`;
7226
8001
  }
7227
8002
 
7228
8003
  // src/scripts/runTickScript.ts
7229
8004
  import { spawnSync } from "child_process";
7230
- import * as fs25 from "fs";
7231
- import * as path23 from "path";
8005
+ import * as fs26 from "fs";
8006
+ import * as path25 from "path";
7232
8007
  var runTickScript = async (ctx, _profile, args) => {
7233
8008
  ctx.skipAgent = true;
7234
8009
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -7240,13 +8015,13 @@ var runTickScript = async (ctx, _profile, args) => {
7240
8015
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7241
8016
  return;
7242
8017
  }
7243
- const jobPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
7244
- if (!fs25.existsSync(jobPath)) {
8018
+ const jobPath = path25.join(ctx.cwd, jobsDir, `${slug}.md`);
8019
+ if (!fs26.existsSync(jobPath)) {
7245
8020
  ctx.output.exitCode = 99;
7246
8021
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7247
8022
  return;
7248
8023
  }
7249
- const raw = fs25.readFileSync(jobPath, "utf-8");
8024
+ const raw = fs26.readFileSync(jobPath, "utf-8");
7250
8025
  const { frontmatter } = splitFrontmatter(raw);
7251
8026
  const tickScript = frontmatter.tickScript;
7252
8027
  if (!tickScript) {
@@ -7254,8 +8029,8 @@ var runTickScript = async (ctx, _profile, args) => {
7254
8029
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7255
8030
  return;
7256
8031
  }
7257
- const scriptPath = path23.isAbsolute(tickScript) ? tickScript : path23.join(ctx.cwd, tickScript);
7258
- if (!fs25.existsSync(scriptPath)) {
8032
+ const scriptPath = path25.isAbsolute(tickScript) ? tickScript : path25.join(ctx.cwd, tickScript);
8033
+ if (!fs26.existsSync(scriptPath)) {
7259
8034
  ctx.output.exitCode = 99;
7260
8035
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7261
8036
  return;
@@ -7356,6 +8131,26 @@ function buildChildEnv(parent, force) {
7356
8131
  return out;
7357
8132
  }
7358
8133
 
8134
+ // src/scripts/saveGoalState.ts
8135
+ var saveGoalState = async (ctx) => {
8136
+ const goal = ctx.data.goal;
8137
+ if (!goal) {
8138
+ ctx.skipAgent = true;
8139
+ return;
8140
+ }
8141
+ const updated = {
8142
+ ...goal.raw ?? { state: goal.state, extra: {} },
8143
+ state: goal.state,
8144
+ goalIssueNumber: goal.goalIssueNumber,
8145
+ lastDispatchedIssue: goal.lastDispatchedIssue,
8146
+ goalPrUrl: goal.goalPrUrl,
8147
+ completedAt: goal.completedAt ?? goal.raw?.completedAt,
8148
+ updatedAt: nowIso()
8149
+ };
8150
+ writeGoalState(ctx.cwd, goal.id, updated);
8151
+ ctx.skipAgent = true;
8152
+ };
8153
+
7359
8154
  // src/scripts/saveTaskState.ts
7360
8155
  var saveTaskState = async (ctx, profile) => {
7361
8156
  const target = ctx.data.commentTargetType;
@@ -7431,11 +8226,11 @@ var skipAgent = async (ctx) => {
7431
8226
  };
7432
8227
 
7433
8228
  // src/scripts/stageMergeConflicts.ts
7434
- import { execFileSync as execFileSync23 } from "child_process";
8229
+ import { execFileSync as execFileSync25 } from "child_process";
7435
8230
  var stageMergeConflicts = async (ctx) => {
7436
8231
  if (ctx.data.agentDone === false) return;
7437
8232
  try {
7438
- execFileSync23("git", ["add", "-A"], {
8233
+ execFileSync25("git", ["add", "-A"], {
7439
8234
  cwd: ctx.cwd,
7440
8235
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7441
8236
  stdio: "pipe"
@@ -7445,7 +8240,7 @@ var stageMergeConflicts = async (ctx) => {
7445
8240
  };
7446
8241
 
7447
8242
  // src/scripts/startFlow.ts
7448
- import { execFileSync as execFileSync24 } from "child_process";
8243
+ import { execFileSync as execFileSync26 } from "child_process";
7449
8244
  var API_TIMEOUT_MS9 = 3e4;
7450
8245
  var startFlow = async (ctx, profile, _agentResult, args) => {
7451
8246
  const entry = args?.entry;
@@ -7479,7 +8274,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7479
8274
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
7480
8275
  const body = `@kody ${next}`;
7481
8276
  try {
7482
- execFileSync24("gh", [sub, "comment", String(targetNumber), "--body", body], {
8277
+ execFileSync26("gh", [sub, "comment", String(targetNumber), "--body", body], {
7483
8278
  timeout: API_TIMEOUT_MS9,
7484
8279
  cwd,
7485
8280
  stdio: ["ignore", "pipe", "pipe"]
@@ -7493,7 +8288,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7493
8288
  }
7494
8289
 
7495
8290
  // src/scripts/syncFlow.ts
7496
- import { execFileSync as execFileSync25 } from "child_process";
8291
+ import { execFileSync as execFileSync27 } from "child_process";
7497
8292
  var syncFlow = async (ctx, _profile, args) => {
7498
8293
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
7499
8294
  const prNumber = ctx.args.pr;
@@ -7565,7 +8360,7 @@ function bail2(ctx, prNumber, reason) {
7565
8360
  }
7566
8361
  function revParseHead(cwd) {
7567
8362
  try {
7568
- return execFileSync25("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
8363
+ return execFileSync27("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
7569
8364
  } catch {
7570
8365
  return "";
7571
8366
  }
@@ -7573,9 +8368,9 @@ function revParseHead(cwd) {
7573
8368
  function pushBranch(branch, cwd) {
7574
8369
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
7575
8370
  try {
7576
- execFileSync25("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
8371
+ execFileSync27("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
7577
8372
  } catch {
7578
- execFileSync25("git", ["push", "--force-with-lease", "-u", "origin", branch], {
8373
+ execFileSync27("git", ["push", "--force-with-lease", "-u", "origin", branch], {
7579
8374
  cwd,
7580
8375
  env,
7581
8376
  stdio: ["ignore", "pipe", "pipe"]
@@ -7802,7 +8597,7 @@ function downgrade2(ctx, reason) {
7802
8597
  }
7803
8598
 
7804
8599
  // src/scripts/waitForCi.ts
7805
- import { execFileSync as execFileSync26 } from "child_process";
8600
+ import { execFileSync as execFileSync28 } from "child_process";
7806
8601
  var API_TIMEOUT_MS10 = 3e4;
7807
8602
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
7808
8603
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -7880,7 +8675,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
7880
8675
  };
7881
8676
  function fetchChecks(prNumber, cwd) {
7882
8677
  try {
7883
- const raw = execFileSync26("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
8678
+ const raw = execFileSync28("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
7884
8679
  encoding: "utf-8",
7885
8680
  timeout: API_TIMEOUT_MS10,
7886
8681
  cwd,
@@ -8138,7 +8933,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
8138
8933
  };
8139
8934
 
8140
8935
  // src/scripts/writeRunSummary.ts
8141
- import * as fs26 from "fs";
8936
+ import * as fs27 from "fs";
8142
8937
  var writeRunSummary = async (ctx, profile) => {
8143
8938
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
8144
8939
  if (!summaryPath) return;
@@ -8160,7 +8955,7 @@ var writeRunSummary = async (ctx, profile) => {
8160
8955
  if (reason) lines.push(`- **Reason:** ${reason}`);
8161
8956
  lines.push("");
8162
8957
  try {
8163
- fs26.appendFileSync(summaryPath, `${lines.join("\n")}
8958
+ fs27.appendFileSync(summaryPath, `${lines.join("\n")}
8164
8959
  `);
8165
8960
  } catch {
8166
8961
  }
@@ -8199,7 +8994,19 @@ var preflightScripts = {
8199
8994
  warmupMcp,
8200
8995
  dispatchJobTicks,
8201
8996
  dispatchJobFileTicks,
8202
- runTickScript
8997
+ runTickScript,
8998
+ loadGoalState,
8999
+ handleAbandonedGoal,
9000
+ ensureLifecycleLabels,
9001
+ ensureUmbrellaIssue,
9002
+ ensureGoalPr,
9003
+ mergeReadyTaskPRs,
9004
+ closeMergedTaskIssues,
9005
+ deriveGoalPhase,
9006
+ ensureGoalBranch,
9007
+ dispatchNextTask,
9008
+ finalizeGoal,
9009
+ saveGoalState
8203
9010
  };
8204
9011
  var postflightScripts = {
8205
9012
  parseAgentResult: parseAgentResult2,
@@ -8238,7 +9045,8 @@ var postflightScripts = {
8238
9045
  recordOutcome,
8239
9046
  mergeReleasePr,
8240
9047
  waitForCi,
8241
- markFlowSuccess
9048
+ markFlowSuccess,
9049
+ commitGoalState
8242
9050
  };
8243
9051
  var allScriptNames = /* @__PURE__ */ new Set([
8244
9052
  ...Object.keys(preflightScripts),
@@ -8246,7 +9054,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
8246
9054
  ]);
8247
9055
 
8248
9056
  // src/tools.ts
8249
- import { execFileSync as execFileSync27 } from "child_process";
9057
+ import { execFileSync as execFileSync29 } from "child_process";
8250
9058
  function verifyCliTools(tools, cwd) {
8251
9059
  const out = [];
8252
9060
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -8279,7 +9087,7 @@ function verifyOne(tool, cwd) {
8279
9087
  }
8280
9088
  function runShell(cmd, cwd, timeoutMs = 3e4) {
8281
9089
  try {
8282
- execFileSync27("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
9090
+ execFileSync29("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8283
9091
  return true;
8284
9092
  } catch {
8285
9093
  return false;
@@ -8348,9 +9156,9 @@ async function runExecutable(profileName, input) {
8348
9156
  data: {},
8349
9157
  output: { exitCode: 0 }
8350
9158
  };
8351
- const ndjsonDir = path24.join(input.cwd, ".kody");
9159
+ const ndjsonDir = path26.join(input.cwd, ".kody");
8352
9160
  const invokeAgent = async (prompt) => {
8353
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path24.isAbsolute(p) ? p : path24.resolve(profile.dir, p)).filter((p) => p.length > 0);
9161
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path26.isAbsolute(p) ? p : path26.resolve(profile.dir, p)).filter((p) => p.length > 0);
8354
9162
  const syntheticPath = ctx.data.syntheticPluginPath;
8355
9163
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
8356
9164
  return runAgent({
@@ -8396,7 +9204,6 @@ async function runExecutable(profileName, input) {
8396
9204
  return finish({ exitCode: 99, reason: "composePrompt did not produce a prompt (ctx.data.prompt missing)" });
8397
9205
  }
8398
9206
  agentResult = await invokeAgent(prompt);
8399
- agentResult = await rescueMissingMarker(agentResult, invokeAgent);
8400
9207
  }
8401
9208
  for (const entry of profile.scripts.postflight) {
8402
9209
  const entryLabel = entry.script ?? entry.shell ?? "<unknown>";
@@ -8460,17 +9267,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
8460
9267
  function resolveProfilePath(profileName) {
8461
9268
  const found = resolveExecutable(profileName);
8462
9269
  if (found) return found;
8463
- const here = path24.dirname(new URL(import.meta.url).pathname);
9270
+ const here = path26.dirname(new URL(import.meta.url).pathname);
8464
9271
  const candidates = [
8465
- path24.join(here, "executables", profileName, "profile.json"),
9272
+ path26.join(here, "executables", profileName, "profile.json"),
8466
9273
  // same-dir sibling (dev)
8467
- path24.join(here, "..", "executables", profileName, "profile.json"),
9274
+ path26.join(here, "..", "executables", profileName, "profile.json"),
8468
9275
  // up one (prod: dist/bin → dist/executables)
8469
- path24.join(here, "..", "src", "executables", profileName, "profile.json")
9276
+ path26.join(here, "..", "src", "executables", profileName, "profile.json")
8470
9277
  // fallback
8471
9278
  ];
8472
9279
  for (const c of candidates) {
8473
- if (fs27.existsSync(c)) return c;
9280
+ if (fs28.existsSync(c)) return c;
8474
9281
  }
8475
9282
  return candidates[0];
8476
9283
  }
@@ -8574,8 +9381,8 @@ function resolveShellTimeoutMs(entry) {
8574
9381
  var SIGKILL_GRACE_MS = 5e3;
8575
9382
  async function runShellEntry(entry, ctx, profile) {
8576
9383
  const shellName = entry.shell;
8577
- const shellPath = path24.join(profile.dir, shellName);
8578
- if (!fs27.existsSync(shellPath)) {
9384
+ const shellPath = path26.join(profile.dir, shellName);
9385
+ if (!fs28.existsSync(shellPath)) {
8579
9386
  ctx.skipAgent = true;
8580
9387
  ctx.output.exitCode = 99;
8581
9388
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8836,7 +9643,7 @@ async function runContainerLoop(profile, ctx, input) {
8836
9643
  }
8837
9644
  function resetWorkingTree(cwd) {
8838
9645
  try {
8839
- execFileSync28("git", ["reset", "--hard", "HEAD"], {
9646
+ execFileSync30("git", ["reset", "--hard", "HEAD"], {
8840
9647
  cwd,
8841
9648
  stdio: ["ignore", "pipe", "pipe"],
8842
9649
  timeout: 3e4
@@ -8988,14 +9795,14 @@ function resolveAuthToken(env = process.env) {
8988
9795
  return token;
8989
9796
  }
8990
9797
  function detectPackageManager2(cwd) {
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";
9798
+ if (fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9799
+ if (fs29.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9800
+ if (fs29.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
8994
9801
  return "npm";
8995
9802
  }
8996
9803
  function shellOut(cmd, args, cwd, stream = true) {
8997
9804
  try {
8998
- execFileSync29(cmd, args, {
9805
+ execFileSync31(cmd, args, {
8999
9806
  cwd,
9000
9807
  stdio: stream ? "inherit" : "pipe",
9001
9808
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -9008,7 +9815,7 @@ function shellOut(cmd, args, cwd, stream = true) {
9008
9815
  }
9009
9816
  function isOnPath(bin) {
9010
9817
  try {
9011
- execFileSync29("which", [bin], { stdio: "pipe" });
9818
+ execFileSync31("which", [bin], { stdio: "pipe" });
9012
9819
  return true;
9013
9820
  } catch {
9014
9821
  return false;
@@ -9049,7 +9856,7 @@ function installLitellmIfNeeded(cwd) {
9049
9856
  } catch {
9050
9857
  }
9051
9858
  try {
9052
- execFileSync29("python3", ["-c", "import litellm"], { stdio: "pipe" });
9859
+ execFileSync31("python3", ["-c", "import litellm"], { stdio: "pipe" });
9053
9860
  process.stdout.write("\u2192 kody: litellm already installed\n");
9054
9861
  return 0;
9055
9862
  } catch {
@@ -9059,16 +9866,16 @@ function installLitellmIfNeeded(cwd) {
9059
9866
  }
9060
9867
  function configureGitIdentity(cwd) {
9061
9868
  try {
9062
- const name = execFileSync29("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9869
+ const name = execFileSync31("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9063
9870
  if (name) return;
9064
9871
  } catch {
9065
9872
  }
9066
9873
  try {
9067
- execFileSync29("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9874
+ execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9068
9875
  } catch {
9069
9876
  }
9070
9877
  try {
9071
- execFileSync29("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9878
+ execFileSync31("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9072
9879
  cwd,
9073
9880
  stdio: "pipe"
9074
9881
  });
@@ -9077,11 +9884,11 @@ function configureGitIdentity(cwd) {
9077
9884
  }
9078
9885
  function postFailureTail(issueNumber, cwd, reason) {
9079
9886
  if (!issueNumber) return;
9080
- const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
9887
+ const logPath = path27.join(cwd, ".kody", "last-run.jsonl");
9081
9888
  let tail = "";
9082
9889
  try {
9083
- if (fs28.existsSync(logPath)) {
9084
- const content = fs28.readFileSync(logPath, "utf-8");
9890
+ if (fs29.existsSync(logPath)) {
9891
+ const content = fs29.readFileSync(logPath, "utf-8");
9085
9892
  tail = content.slice(-3e3);
9086
9893
  }
9087
9894
  } catch {
@@ -9106,7 +9913,7 @@ async function runCi(argv) {
9106
9913
  return 0;
9107
9914
  }
9108
9915
  const args = parseCiArgs(argv);
9109
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9916
+ const cwd = args.cwd ? path27.resolve(args.cwd) : process.cwd();
9110
9917
  let earlyConfig;
9111
9918
  try {
9112
9919
  earlyConfig = loadConfig(cwd);
@@ -9116,9 +9923,9 @@ async function runCi(argv) {
9116
9923
  const eventName = process.env.GITHUB_EVENT_NAME;
9117
9924
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
9118
9925
  let manualWorkflowDispatch = false;
9119
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
9926
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs29.existsSync(dispatchEventPath)) {
9120
9927
  try {
9121
- const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
9928
+ const evt = JSON.parse(fs29.readFileSync(dispatchEventPath, "utf-8"));
9122
9929
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
9123
9930
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
9124
9931
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -9333,15 +10140,15 @@ function parseChatArgs(argv, env = process.env) {
9333
10140
  return result;
9334
10141
  }
9335
10142
  function commitChatFiles(cwd, sessionId, verbose) {
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)));
10143
+ const sessionFile = path28.relative(cwd, sessionFilePath(cwd, sessionId));
10144
+ const eventsFile = path28.relative(cwd, eventsFilePath(cwd, sessionId));
10145
+ const paths = [sessionFile, eventsFile].filter((p) => fs30.existsSync(path28.join(cwd, p)));
9339
10146
  if (paths.length === 0) return;
9340
10147
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
9341
10148
  try {
9342
- execFileSync30("git", ["add", "-f", ...paths], opts);
9343
- execFileSync30("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
9344
- execFileSync30("git", ["push", "--quiet", "origin", "HEAD"], opts);
10149
+ execFileSync32("git", ["add", "-f", ...paths], opts);
10150
+ execFileSync32("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
10151
+ execFileSync32("git", ["push", "--quiet", "origin", "HEAD"], opts);
9345
10152
  } catch (err) {
9346
10153
  const msg = err instanceof Error ? err.message : String(err);
9347
10154
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -9373,7 +10180,7 @@ async function runChat(argv) {
9373
10180
  ${CHAT_HELP}`);
9374
10181
  return 64;
9375
10182
  }
9376
- const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
10183
+ const cwd = args.cwd ? path28.resolve(args.cwd) : process.cwd();
9377
10184
  const sessionId = args.sessionId;
9378
10185
  const unpackedSecrets = unpackAllSecrets();
9379
10186
  if (unpackedSecrets > 0) {
@@ -9425,7 +10232,7 @@ ${CHAT_HELP}`);
9425
10232
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
9426
10233
  const meta = readMeta(sessionFile);
9427
10234
  process.stdout.write(
9428
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10235
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs30.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9429
10236
  `
9430
10237
  );
9431
10238
  try {