@kody-ade/kody-engine 0.4.30 → 0.4.32

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.30",
6
+ version: "0.4.32",
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";
@@ -2749,6 +2749,301 @@ function defaultLabelMap() {
2749
2749
  };
2750
2750
  }
2751
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
+
2752
3047
  // src/scripts/commitAndPush.ts
2753
3048
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
2754
3049
  var commitAndPush2 = async (ctx) => {
@@ -2792,17 +3087,69 @@ var commitAndPush2 = async (ctx) => {
2792
3087
  ctx.data.hasCommitsAhead = hasCommitsAhead(branch, ctx.config.git.defaultBranch, ctx.cwd);
2793
3088
  };
2794
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
+
2795
3142
  // src/scripts/composePrompt.ts
2796
3143
  import * as fs13 from "fs";
2797
- import * as path12 from "path";
3144
+ import * as path13 from "path";
2798
3145
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
2799
3146
  var composePrompt = async (ctx, profile) => {
2800
3147
  const explicit = ctx.data.promptTemplate;
2801
3148
  const mode = ctx.args.mode;
2802
3149
  const candidates = [
2803
- explicit ? path12.join(profile.dir, explicit) : null,
2804
- mode ? path12.join(profile.dir, "prompts", `${mode}.md`) : null,
2805
- 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")
2806
3153
  ].filter(Boolean);
2807
3154
  let templatePath = "";
2808
3155
  for (const c of candidates) {
@@ -2891,9 +3238,9 @@ function formatToolsUsage(profile) {
2891
3238
  }
2892
3239
 
2893
3240
  // src/scripts/createQaGoal.ts
2894
- import { execFileSync as execFileSync9 } from "child_process";
3241
+ import { execFileSync as execFileSync11 } from "child_process";
2895
3242
  import * as fs14 from "fs";
2896
- import * as path13 from "path";
3243
+ import * as path14 from "path";
2897
3244
 
2898
3245
  // src/scripts/postReviewResult.ts
2899
3246
  function detectVerdict(body) {
@@ -3075,7 +3422,7 @@ ${json}
3075
3422
  ${MANIFEST_END}
3076
3423
  `;
3077
3424
  }
3078
- function ensureLabel(name, color, description, cwd) {
3425
+ function ensureLabel2(name, color, description, cwd) {
3079
3426
  try {
3080
3427
  gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3081
3428
  } catch {
@@ -3095,7 +3442,7 @@ function ensureSeverityLabels(findings, cwd) {
3095
3442
  for (const f of findings) {
3096
3443
  if (seen.has(f.severity)) continue;
3097
3444
  seen.add(f.severity);
3098
- 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);
3099
3446
  }
3100
3447
  }
3101
3448
  function buildIssueBody(f, goalId, parentManifestNumber) {
@@ -3129,7 +3476,7 @@ function buildIssueBody(f, goalId, parentManifestNumber) {
3129
3476
  return lines.join("\n");
3130
3477
  }
3131
3478
  function createOrUpdateManifestIssue(number, manifest, cwd) {
3132
- ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3479
+ ensureLabel2(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3133
3480
  const body = serializeManifestBody(manifest);
3134
3481
  if (number !== null) {
3135
3482
  gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
@@ -3145,7 +3492,7 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
3145
3492
  return { number: Number(m[1]), created: true };
3146
3493
  }
3147
3494
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3148
- const dir = path13.join(cwd, ".kody", "goals", goalId);
3495
+ const dir = path14.join(cwd, ".kody", "goals", goalId);
3149
3496
  fs14.mkdirSync(dir, { recursive: true });
3150
3497
  const state = {
3151
3498
  version: 1,
@@ -3154,7 +3501,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3154
3501
  updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3155
3502
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
3156
3503
  };
3157
- const filePath = path13.join(dir, "state.json");
3504
+ const filePath = path14.join(dir, "state.json");
3158
3505
  fs14.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
3159
3506
  `);
3160
3507
  return filePath;
@@ -3162,7 +3509,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3162
3509
  function gitTry(args, cwd) {
3163
3510
  const env = { ...process.env, SKIP_HOOKS: "1", HUSKY: "0" };
3164
3511
  try {
3165
- execFileSync9("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3512
+ execFileSync11("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3166
3513
  return { ok: true, stderr: "" };
3167
3514
  } catch (err) {
3168
3515
  const e = err;
@@ -3244,8 +3591,8 @@ ${tail}
3244
3591
  }
3245
3592
  function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3246
3593
  const labels = [`goal:${goalId}`, severityLabel(finding.severity), FINDING_LABEL];
3247
- ensureLabel(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3248
- 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);
3249
3596
  const title = `[${finding.severity}] ${finding.title}`.slice(0, 240);
3250
3597
  const body = buildIssueBody(finding, goalId, manifestNumber);
3251
3598
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
@@ -3304,7 +3651,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3304
3651
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3305
3652
  return;
3306
3653
  }
3307
- ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3654
+ ensureLabel2(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3308
3655
  const scope2 = ctx.args.scope;
3309
3656
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3310
3657
  let url = "";
@@ -3434,14 +3781,57 @@ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
3434
3781
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3435
3782
  };
3436
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
+
3437
3827
  // src/scripts/diagMcp.ts
3438
- import { execFileSync as execFileSync10 } from "child_process";
3828
+ import { execFileSync as execFileSync12 } from "child_process";
3439
3829
  import * as fs15 from "fs";
3440
3830
  import * as os3 from "os";
3441
- import * as path14 from "path";
3831
+ import * as path15 from "path";
3442
3832
  var diagMcp = async (_ctx) => {
3443
3833
  const home = os3.homedir();
3444
- const cacheDir = path14.join(home, ".cache", "ms-playwright");
3834
+ const cacheDir = path15.join(home, ".cache", "ms-playwright");
3445
3835
  let entries = [];
3446
3836
  try {
3447
3837
  entries = fs15.readdirSync(cacheDir);
@@ -3455,7 +3845,7 @@ var diagMcp = async (_ctx) => {
3455
3845
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
3456
3846
  `);
3457
3847
  try {
3458
- 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"], {
3459
3849
  stdio: "pipe",
3460
3850
  timeout: 6e4,
3461
3851
  encoding: "utf8"
@@ -3471,16 +3861,16 @@ var diagMcp = async (_ctx) => {
3471
3861
 
3472
3862
  // src/scripts/discoverQaContext.ts
3473
3863
  import * as fs17 from "fs";
3474
- import * as path16 from "path";
3864
+ import * as path17 from "path";
3475
3865
 
3476
3866
  // src/scripts/frameworkDetectors.ts
3477
3867
  import * as fs16 from "fs";
3478
- import * as path15 from "path";
3868
+ import * as path16 from "path";
3479
3869
  function detectFrameworks(cwd) {
3480
3870
  const out = [];
3481
3871
  let deps = {};
3482
3872
  try {
3483
- 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"));
3484
3874
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
3485
3875
  } catch {
3486
3876
  return out;
@@ -3517,7 +3907,7 @@ function detectFrameworks(cwd) {
3517
3907
  }
3518
3908
  function findFile(cwd, candidates) {
3519
3909
  for (const c of candidates) {
3520
- if (fs16.existsSync(path15.join(cwd, c))) return c;
3910
+ if (fs16.existsSync(path16.join(cwd, c))) return c;
3521
3911
  }
3522
3912
  return null;
3523
3913
  }
@@ -3530,7 +3920,7 @@ var COLLECTION_DIRS = [
3530
3920
  function discoverPayloadCollections(cwd) {
3531
3921
  const out = [];
3532
3922
  for (const dir of COLLECTION_DIRS) {
3533
- const full = path15.join(cwd, dir);
3923
+ const full = path16.join(cwd, dir);
3534
3924
  if (!fs16.existsSync(full)) continue;
3535
3925
  let files;
3536
3926
  try {
@@ -3540,7 +3930,7 @@ function discoverPayloadCollections(cwd) {
3540
3930
  }
3541
3931
  for (const file of files) {
3542
3932
  try {
3543
- const filePath = path15.join(full, file);
3933
+ const filePath = path16.join(full, file);
3544
3934
  const content = fs16.readFileSync(filePath, "utf-8").slice(0, 1e4);
3545
3935
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
3546
3936
  if (!slugMatch) continue;
@@ -3555,7 +3945,7 @@ function discoverPayloadCollections(cwd) {
3555
3945
  out.push({
3556
3946
  name,
3557
3947
  slug,
3558
- filePath: path15.relative(cwd, filePath),
3948
+ filePath: path16.relative(cwd, filePath),
3559
3949
  fields: fields.slice(0, 20),
3560
3950
  hasAdmin
3561
3951
  });
@@ -3569,7 +3959,7 @@ var ADMIN_COMPONENT_DIRS = ["src/ui/admin", "src/admin/components", "src/compone
3569
3959
  function discoverAdminComponents(cwd, collections) {
3570
3960
  const out = [];
3571
3961
  for (const dir of ADMIN_COMPONENT_DIRS) {
3572
- const full = path15.join(cwd, dir);
3962
+ const full = path16.join(cwd, dir);
3573
3963
  if (!fs16.existsSync(full)) continue;
3574
3964
  let entries;
3575
3965
  try {
@@ -3578,19 +3968,19 @@ function discoverAdminComponents(cwd, collections) {
3578
3968
  continue;
3579
3969
  }
3580
3970
  for (const entry of entries) {
3581
- const entryPath = path15.join(full, entry.name);
3971
+ const entryPath = path16.join(full, entry.name);
3582
3972
  let name;
3583
3973
  let filePath;
3584
3974
  if (entry.isDirectory()) {
3585
3975
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
3586
- (f) => fs16.existsSync(path15.join(entryPath, f))
3976
+ (f) => fs16.existsSync(path16.join(entryPath, f))
3587
3977
  );
3588
3978
  if (!indexFile) continue;
3589
3979
  name = entry.name;
3590
- filePath = path15.relative(cwd, path15.join(entryPath, indexFile));
3980
+ filePath = path16.relative(cwd, path16.join(entryPath, indexFile));
3591
3981
  } else if (/\.(tsx?|jsx?)$/.test(entry.name)) {
3592
3982
  name = entry.name.replace(/\.(tsx?|jsx?)$/, "");
3593
- filePath = path15.relative(cwd, entryPath);
3983
+ filePath = path16.relative(cwd, entryPath);
3594
3984
  } else {
3595
3985
  continue;
3596
3986
  }
@@ -3598,7 +3988,7 @@ function discoverAdminComponents(cwd, collections) {
3598
3988
  if (collections) {
3599
3989
  for (const col of collections) {
3600
3990
  try {
3601
- const colContent = fs16.readFileSync(path15.join(cwd, col.filePath), "utf-8");
3991
+ const colContent = fs16.readFileSync(path16.join(cwd, col.filePath), "utf-8");
3602
3992
  if (colContent.includes(name)) {
3603
3993
  usedInCollection = col.slug;
3604
3994
  break;
@@ -3617,7 +4007,7 @@ function scanApiRoutes(cwd) {
3617
4007
  const out = [];
3618
4008
  const appDirs = ["src/app", "app"];
3619
4009
  for (const appDir of appDirs) {
3620
- const apiDir = path15.join(cwd, appDir, "api");
4010
+ const apiDir = path16.join(cwd, appDir, "api");
3621
4011
  if (!fs16.existsSync(apiDir)) continue;
3622
4012
  walkApiRoutes(apiDir, "/api", cwd, out);
3623
4013
  break;
@@ -3634,7 +4024,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3634
4024
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
3635
4025
  if (routeFile) {
3636
4026
  try {
3637
- 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);
3638
4028
  const methods = HTTP_METHODS.filter(
3639
4029
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
3640
4030
  );
@@ -3642,7 +4032,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3642
4032
  out.push({
3643
4033
  path: prefix,
3644
4034
  methods,
3645
- filePath: path15.relative(cwd, path15.join(dir, routeFile.name))
4035
+ filePath: path16.relative(cwd, path16.join(dir, routeFile.name))
3646
4036
  });
3647
4037
  }
3648
4038
  } catch {
@@ -3653,7 +4043,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3653
4043
  if (entry.name === "node_modules" || entry.name === ".next") continue;
3654
4044
  let segment = entry.name;
3655
4045
  if (segment.startsWith("(") && segment.endsWith(")")) {
3656
- walkApiRoutes(path15.join(dir, entry.name), prefix, cwd, out);
4046
+ walkApiRoutes(path16.join(dir, entry.name), prefix, cwd, out);
3657
4047
  continue;
3658
4048
  }
3659
4049
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -3661,7 +4051,7 @@ function walkApiRoutes(dir, prefix, cwd, out) {
3661
4051
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
3662
4052
  segment = `:${segment.slice(1, -1)}`;
3663
4053
  }
3664
- walkApiRoutes(path15.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
4054
+ walkApiRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, cwd, out);
3665
4055
  }
3666
4056
  }
3667
4057
  var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
@@ -3681,7 +4071,7 @@ var BUILTIN_ENV_VARS = /* @__PURE__ */ new Set([
3681
4071
  function scanEnvVars(cwd) {
3682
4072
  const candidates = [".env.example", ".env.local.example", ".env.template"];
3683
4073
  for (const envFile of candidates) {
3684
- const envPath = path15.join(cwd, envFile);
4074
+ const envPath = path16.join(cwd, envFile);
3685
4075
  if (!fs16.existsSync(envPath)) continue;
3686
4076
  try {
3687
4077
  const content = fs16.readFileSync(envPath, "utf-8");
@@ -3732,9 +4122,9 @@ function runQaDiscovery(cwd) {
3732
4122
  }
3733
4123
  function detectDevServer(cwd, out) {
3734
4124
  try {
3735
- 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"));
3736
4126
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
3737
- 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";
3738
4128
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
3739
4129
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
3740
4130
  else if (allDeps.vite) out.devPort = 5173;
@@ -3744,7 +4134,7 @@ function detectDevServer(cwd, out) {
3744
4134
  function scanFrontendRoutes(cwd, out) {
3745
4135
  const appDirs = ["src/app", "app"];
3746
4136
  for (const appDir of appDirs) {
3747
- const full = path16.join(cwd, appDir);
4137
+ const full = path17.join(cwd, appDir);
3748
4138
  if (!fs17.existsSync(full)) continue;
3749
4139
  walkFrontendRoutes(full, "", out);
3750
4140
  break;
@@ -3770,7 +4160,7 @@ function walkFrontendRoutes(dir, prefix, out) {
3770
4160
  if (entry.name === "node_modules" || entry.name === ".next") continue;
3771
4161
  let segment = entry.name;
3772
4162
  if (segment.startsWith("(") && segment.endsWith(")")) {
3773
- walkFrontendRoutes(path16.join(dir, entry.name), prefix, out);
4163
+ walkFrontendRoutes(path17.join(dir, entry.name), prefix, out);
3774
4164
  continue;
3775
4165
  }
3776
4166
  if (segment.startsWith("[[") && segment.endsWith("]]")) {
@@ -3778,7 +4168,7 @@ function walkFrontendRoutes(dir, prefix, out) {
3778
4168
  } else if (segment.startsWith("[") && segment.endsWith("]")) {
3779
4169
  segment = `:${segment.slice(1, -1)}`;
3780
4170
  }
3781
- walkFrontendRoutes(path16.join(dir, entry.name), `${prefix}/${segment}`, out);
4171
+ walkFrontendRoutes(path17.join(dir, entry.name), `${prefix}/${segment}`, out);
3782
4172
  }
3783
4173
  }
3784
4174
  function detectAuthFiles(cwd, out) {
@@ -3795,13 +4185,13 @@ function detectAuthFiles(cwd, out) {
3795
4185
  "src/app/api/oauth"
3796
4186
  ];
3797
4187
  for (const c of candidates) {
3798
- if (fs17.existsSync(path16.join(cwd, c))) out.authFiles.push(c);
4188
+ if (fs17.existsSync(path17.join(cwd, c))) out.authFiles.push(c);
3799
4189
  }
3800
4190
  }
3801
4191
  function detectRoles(cwd, out) {
3802
4192
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
3803
4193
  for (const rp of rolePaths) {
3804
- const dir = path16.join(cwd, rp);
4194
+ const dir = path17.join(cwd, rp);
3805
4195
  if (!fs17.existsSync(dir)) continue;
3806
4196
  let files;
3807
4197
  try {
@@ -3811,7 +4201,7 @@ function detectRoles(cwd, out) {
3811
4201
  }
3812
4202
  for (const f of files) {
3813
4203
  try {
3814
- 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);
3815
4205
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
3816
4206
  if (roleMatches) {
3817
4207
  for (const m of roleMatches) {
@@ -3957,7 +4347,7 @@ var discoverQaContext = async (ctx) => {
3957
4347
  };
3958
4348
 
3959
4349
  // src/scripts/dispatch.ts
3960
- import { execFileSync as execFileSync11 } from "child_process";
4350
+ import { execFileSync as execFileSync13 } from "child_process";
3961
4351
  var API_TIMEOUT_MS4 = 3e4;
3962
4352
  var dispatch = async (ctx, _profile, _agentResult, args) => {
3963
4353
  const next = args?.next;
@@ -3993,7 +4383,7 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
3993
4383
  const sub = usePr ? "pr" : "issue";
3994
4384
  const body = `@kody ${next}`;
3995
4385
  try {
3996
- execFileSync11("gh", [sub, "comment", String(targetNumber), "--body", body], {
4386
+ execFileSync13("gh", [sub, "comment", String(targetNumber), "--body", body], {
3997
4387
  timeout: API_TIMEOUT_MS4,
3998
4388
  cwd: ctx.cwd,
3999
4389
  stdio: ["ignore", "pipe", "pipe"]
@@ -4013,7 +4403,7 @@ function parsePr(url) {
4013
4403
  }
4014
4404
 
4015
4405
  // src/scripts/dispatchClassified.ts
4016
- import { execFileSync as execFileSync12 } from "child_process";
4406
+ import { execFileSync as execFileSync14 } from "child_process";
4017
4407
  var API_TIMEOUT_MS5 = 3e4;
4018
4408
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
4019
4409
  var dispatchClassified = async (ctx) => {
@@ -4022,7 +4412,7 @@ var dispatchClassified = async (ctx) => {
4022
4412
  const classification = ctx.data.classification;
4023
4413
  if (!classification || !VALID_CLASSES2.has(classification)) return;
4024
4414
  try {
4025
- execFileSync12("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4415
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4026
4416
  cwd: ctx.cwd,
4027
4417
  timeout: API_TIMEOUT_MS5,
4028
4418
  stdio: ["ignore", "pipe", "pipe"]
@@ -4043,7 +4433,7 @@ function failedAction3(reason) {
4043
4433
 
4044
4434
  // src/scripts/dispatchJobFileTicks.ts
4045
4435
  import * as fs19 from "fs";
4046
- import * as path18 from "path";
4436
+ import * as path19 from "path";
4047
4437
 
4048
4438
  // src/scripts/jobFrontmatter.ts
4049
4439
  var SCHEDULE_EVERY_VALUES = [
@@ -4295,7 +4685,7 @@ var ContentsApiBackend = class {
4295
4685
 
4296
4686
  // src/scripts/jobState/localFileBackend.ts
4297
4687
  import * as fs18 from "fs";
4298
- import * as path17 from "path";
4688
+ import * as path18 from "path";
4299
4689
  var LocalFileBackend = class {
4300
4690
  name = "local-file";
4301
4691
  cwd;
@@ -4310,7 +4700,7 @@ var LocalFileBackend = class {
4310
4700
  if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
4311
4701
  this.cwd = opts.cwd;
4312
4702
  this.jobsDir = opts.jobsDir;
4313
- this.absDir = path17.join(opts.cwd, opts.jobsDir);
4703
+ this.absDir = path18.join(opts.cwd, opts.jobsDir);
4314
4704
  this.owner = opts.owner;
4315
4705
  this.repo = opts.repo;
4316
4706
  this.cache = opts.cache ?? defaultCacheAdapter();
@@ -4370,7 +4760,7 @@ var LocalFileBackend = class {
4370
4760
  }
4371
4761
  load(slug) {
4372
4762
  const relPath = stateFilePath(this.jobsDir, slug);
4373
- const absPath = path17.join(this.cwd, relPath);
4763
+ const absPath = path18.join(this.cwd, relPath);
4374
4764
  if (!fs18.existsSync(absPath)) {
4375
4765
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
4376
4766
  }
@@ -4391,8 +4781,8 @@ var LocalFileBackend = class {
4391
4781
  if (!loaded.created && isStateUnchanged(loaded.state, next)) {
4392
4782
  return false;
4393
4783
  }
4394
- const absPath = path17.join(this.cwd, loaded.path);
4395
- fs18.mkdirSync(path17.dirname(absPath), { recursive: true });
4784
+ const absPath = path18.join(this.cwd, loaded.path);
4785
+ fs18.mkdirSync(path18.dirname(absPath), { recursive: true });
4396
4786
  const body = JSON.stringify(next, null, 2) + "\n";
4397
4787
  fs18.writeFileSync(absPath, body, "utf-8");
4398
4788
  return true;
@@ -4471,7 +4861,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
4471
4861
  await backend.hydrate();
4472
4862
  }
4473
4863
  try {
4474
- const slugs = listJobSlugs(path18.join(ctx.cwd, jobsDir));
4864
+ const slugs = listJobSlugs(path19.join(ctx.cwd, jobsDir));
4475
4865
  ctx.data.jobSlugCount = slugs.length;
4476
4866
  if (slugs.length === 0) {
4477
4867
  process.stdout.write(`[jobs] no job files in ${jobsDir}
@@ -4570,7 +4960,7 @@ function formatAgo(ms) {
4570
4960
  }
4571
4961
  function readJobFrontmatter(cwd, jobsDir, slug) {
4572
4962
  try {
4573
- 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");
4574
4964
  return splitFrontmatter(raw).frontmatter;
4575
4965
  } catch {
4576
4966
  return {};
@@ -4650,6 +5040,134 @@ function listIssuesByLabel(label, cwd) {
4650
5040
  return list.filter((x) => typeof x.number === "number" && typeof x.title === "string").map((x) => ({ number: x.number, title: x.title }));
4651
5041
  }
4652
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
+
4653
5171
  // src/pr.ts
4654
5172
  var TITLE_MAX = 72;
4655
5173
  function stripTitlePrefixes(raw) {
@@ -4786,20 +5304,37 @@ function ensurePr(opts) {
4786
5304
  }
4787
5305
 
4788
5306
  // src/scripts/ensurePr.ts
5307
+ function setOutcome(ctx, outcome) {
5308
+ ctx.data.prResult = outcome;
5309
+ if (outcome.kind === "created" || outcome.kind === "updated") {
5310
+ ctx.output.prUrl = outcome.url;
5311
+ }
5312
+ }
4789
5313
  var ensurePr2 = async (ctx) => {
4790
5314
  if (ctx.skipAgent && ctx.output.exitCode !== void 0) {
5315
+ setOutcome(ctx, { kind: "skipped", reason: "preflight short-circuited (skipAgent)" });
4791
5316
  return;
4792
5317
  }
4793
5318
  const commitResult = ctx.data.commitResult;
4794
5319
  const hasCommits = Boolean(ctx.data.hasCommitsAhead);
4795
5320
  if (!commitResult?.committed && !hasCommits) {
5321
+ setOutcome(ctx, { kind: "skipped", reason: "no commits to ship" });
4796
5322
  return;
4797
5323
  }
4798
5324
  if (commitResult?.committed && commitResult.pushed === false) {
5325
+ setOutcome(ctx, { kind: "skipped", reason: "local commit succeeded but push failed" });
5326
+ return;
5327
+ }
5328
+ if (ctx.data.verifyOk === false) {
5329
+ const reason = `verify failed: ${ctx.data.verifyReason ?? "unknown"}`;
5330
+ setOutcome(ctx, { kind: "skipped", reason });
4799
5331
  return;
4800
5332
  }
4801
5333
  const branch = ctx.data.branch;
4802
- if (!branch) return;
5334
+ if (!branch) {
5335
+ setOutcome(ctx, { kind: "skipped", reason: "no branch context (ctx.data.branch missing)" });
5336
+ return;
5337
+ }
4803
5338
  const failureReason = computeFailureReason(ctx);
4804
5339
  const isFailure = failureReason.length > 0;
4805
5340
  const changedFiles = ctx.data.changedFiles ?? [];
@@ -4821,13 +5356,26 @@ var ensurePr2 = async (ctx) => {
4821
5356
  baseBranch,
4822
5357
  cwd: ctx.cwd
4823
5358
  });
4824
- ctx.output.prUrl = result.url;
4825
- ctx.data.prResult = result;
5359
+ if (!result.url || result.url.trim().length === 0) {
5360
+ const reason = `gh pr create returned empty URL (action=${result.action}); refusing to claim success`;
5361
+ ctx.data.prCrashReason = reason;
5362
+ ctx.output.exitCode = 4;
5363
+ ctx.output.reason = reason;
5364
+ setOutcome(ctx, { kind: "crashed", reason });
5365
+ return;
5366
+ }
5367
+ setOutcome(ctx, {
5368
+ kind: result.action === "created" ? "created" : "updated",
5369
+ url: result.url,
5370
+ number: result.number,
5371
+ draft: result.draft
5372
+ });
4826
5373
  } catch (err) {
4827
5374
  const reason = `PR creation failed: ${err instanceof Error ? err.message : String(err)}`;
4828
5375
  ctx.data.prCrashReason = reason;
4829
5376
  ctx.output.exitCode = 4;
4830
5377
  ctx.output.reason = reason;
5378
+ setOutcome(ctx, { kind: "crashed", reason });
4831
5379
  }
4832
5380
  };
4833
5381
  function computeFailureReason(ctx) {
@@ -4863,8 +5411,203 @@ function collectExpectedTests(raw) {
4863
5411
  return out;
4864
5412
  }
4865
5413
 
5414
+ // src/scripts/ensureUmbrellaIssue.ts
5415
+ var ensureUmbrellaIssue = async (ctx) => {
5416
+ const goal = ctx.data.goal;
5417
+ if (!goal) return;
5418
+ if (goal.goalIssueNumber !== void 0) return;
5419
+ const title = `goal: ${goal.id}`;
5420
+ const body = `Umbrella issue for goal **${goal.id}**.
5421
+
5422
+ Closed automatically when the goal PR (\`${goal.goalBranch}\` \u2192 \`${goal.defaultBranch}\`) merges.
5423
+ `;
5424
+ const existing = findUmbrellaByTitle(goal.id, title, ctx.cwd);
5425
+ if (existing.ok && existing.value !== null && existing.value !== void 0) {
5426
+ process.stdout.write(`[goal-tick] adopted existing umbrella issue #${existing.value} for ${goal.id}
5427
+ `);
5428
+ goal.goalIssueNumber = existing.value;
5429
+ return;
5430
+ }
5431
+ const created = createIssue(
5432
+ {
5433
+ title,
5434
+ body,
5435
+ labels: [goalLabel(goal.id), UMBRELLA_BUILDING_LABEL]
5436
+ },
5437
+ ctx.cwd
5438
+ );
5439
+ if (!created.ok) {
5440
+ process.stderr.write(
5441
+ `[goal-tick] ensureUmbrellaIssue: gh issue create failed: ${created.error} \u2014 continuing without umbrella issue
5442
+ `
5443
+ );
5444
+ return;
5445
+ }
5446
+ process.stdout.write(`[goal-tick] opened umbrella issue #${created.value} for ${goal.id}
5447
+ `);
5448
+ goal.goalIssueNumber = created.value;
5449
+ };
5450
+
5451
+ // src/goal/state.ts
5452
+ import * as fs20 from "fs";
5453
+ import * as path20 from "path";
5454
+ var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5455
+ var GoalStateError = class extends Error {
5456
+ constructor(path29, message) {
5457
+ super(`Invalid goal state at ${path29}:
5458
+ ${message}`);
5459
+ this.path = path29;
5460
+ this.name = "GoalStateError";
5461
+ }
5462
+ path;
5463
+ };
5464
+ function parseGoalState(filePath, raw) {
5465
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5466
+ throw new GoalStateError(filePath, "must be a JSON object");
5467
+ }
5468
+ const r = raw;
5469
+ const stateValue = r.state;
5470
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5471
+ throw new GoalStateError(
5472
+ filePath,
5473
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5474
+ );
5475
+ }
5476
+ const parsed = {
5477
+ state: stateValue,
5478
+ extra: {}
5479
+ };
5480
+ if (typeof r.goalIssueNumber === "number" && Number.isFinite(r.goalIssueNumber)) {
5481
+ parsed.goalIssueNumber = r.goalIssueNumber;
5482
+ }
5483
+ if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5484
+ parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5485
+ }
5486
+ if (typeof r.goalPrUrl === "string" && r.goalPrUrl.length > 0) {
5487
+ parsed.goalPrUrl = r.goalPrUrl;
5488
+ }
5489
+ for (const ts of ["updatedAt", "createdAt", "startedAt", "completedAt"]) {
5490
+ const v = r[ts];
5491
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5492
+ }
5493
+ const known = /* @__PURE__ */ new Set([
5494
+ "state",
5495
+ "goalIssueNumber",
5496
+ "lastDispatchedIssue",
5497
+ "goalPrUrl",
5498
+ "updatedAt",
5499
+ "createdAt",
5500
+ "startedAt",
5501
+ "completedAt"
5502
+ ]);
5503
+ for (const [k, v] of Object.entries(r)) {
5504
+ if (!known.has(k)) parsed.extra[k] = v;
5505
+ }
5506
+ return parsed;
5507
+ }
5508
+ function serializeGoalState(s) {
5509
+ const obj = { ...s.extra, state: s.state };
5510
+ if (s.goalIssueNumber !== void 0) obj.goalIssueNumber = s.goalIssueNumber;
5511
+ if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5512
+ if (s.goalPrUrl !== void 0) obj.goalPrUrl = s.goalPrUrl;
5513
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5514
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5515
+ if (s.completedAt !== void 0) obj.completedAt = s.completedAt;
5516
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5517
+ return `${JSON.stringify(obj, null, 2)}
5518
+ `;
5519
+ }
5520
+ function goalStatePath(cwd, goalId) {
5521
+ return path20.join(cwd, ".kody", "goals", goalId, "state.json");
5522
+ }
5523
+ function readGoalState(cwd, goalId) {
5524
+ const file = goalStatePath(cwd, goalId);
5525
+ if (!fs20.existsSync(file)) {
5526
+ throw new GoalStateError(file, "file not found");
5527
+ }
5528
+ let raw;
5529
+ try {
5530
+ raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
5531
+ } catch (err) {
5532
+ throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
5533
+ }
5534
+ return parseGoalState(file, raw);
5535
+ }
5536
+ function writeGoalState(cwd, goalId, state) {
5537
+ const file = goalStatePath(cwd, goalId);
5538
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
5539
+ fs20.writeFileSync(file, serializeGoalState(state), "utf-8");
5540
+ }
5541
+ function nowIso() {
5542
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5543
+ }
5544
+
5545
+ // src/scripts/finalizeGoal.ts
5546
+ var finalizeGoal = async (ctx) => {
5547
+ const goal = ctx.data.goal;
5548
+ if (!goal) return;
5549
+ process.stdout.write(`[goal-tick] all task(s) closed \u2014 finalising goal ${goal.id}
5550
+ `);
5551
+ if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5552
+ process.stderr.write(`[goal-tick] goal branch ${goal.goalBranch} not found on origin \u2014 skipping final PR
5553
+ `);
5554
+ finishState(goal);
5555
+ return;
5556
+ }
5557
+ const title = `goal: ${goal.id}`;
5558
+ const closesLine = goal.goalIssueNumber ? `
5559
+
5560
+ Closes #${goal.goalIssueNumber}
5561
+ ` : "\n";
5562
+ const body = `Final integration PR for goal **${goal.id}**.
5563
+
5564
+ All task issues are closed and merged into \`${goal.goalBranch}\`. Ready for review.${closesLine}`;
5565
+ const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5566
+ if (existing.ok && existing.value && existing.value.length > 0) {
5567
+ const pr = existing.value[0];
5568
+ goal.goalPrUrl = pr.url;
5569
+ const edit = editPrBody(pr.number, body, ctx.cwd);
5570
+ if (!edit.ok) {
5571
+ process.stderr.write(`[goal-tick] finalizeGoal: editPrBody failed: ${edit.error}
5572
+ `);
5573
+ }
5574
+ if (pr.isDraft) {
5575
+ process.stdout.write(`[goal-tick] promoting draft goal PR #${pr.number} to ready-for-review
5576
+ `);
5577
+ const ready = markPrReady(pr.number, ctx.cwd);
5578
+ if (!ready.ok) {
5579
+ process.stderr.write(`[goal-tick] finalizeGoal: markPrReady failed: ${ready.error}
5580
+ `);
5581
+ }
5582
+ }
5583
+ } else {
5584
+ const created = createPr(
5585
+ {
5586
+ head: goal.goalBranch,
5587
+ base: goal.defaultBranch,
5588
+ title,
5589
+ body,
5590
+ // ready-for-review (not draft) since we're finalizing.
5591
+ draft: false
5592
+ },
5593
+ ctx.cwd
5594
+ );
5595
+ if (!created.ok) {
5596
+ process.stderr.write(`[goal-tick] finalizeGoal: gh pr create failed: ${created.error}
5597
+ `);
5598
+ } else {
5599
+ goal.goalPrUrl = created.value;
5600
+ }
5601
+ }
5602
+ finishState(goal);
5603
+ };
5604
+ function finishState(goal) {
5605
+ goal.state = "done";
5606
+ goal.completedAt = nowIso();
5607
+ }
5608
+
4866
5609
  // src/scripts/finishFlow.ts
4867
- import { execFileSync as execFileSync13 } from "child_process";
5610
+ import { execFileSync as execFileSync15 } from "child_process";
4868
5611
  var API_TIMEOUT_MS6 = 3e4;
4869
5612
  var STATUS_ICON = {
4870
5613
  "review-passed": "\u2705",
@@ -4898,7 +5641,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4898
5641
  **PR:** ${state.core.prUrl}` : "";
4899
5642
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
4900
5643
  try {
4901
- execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5644
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4902
5645
  timeout: API_TIMEOUT_MS6,
4903
5646
  cwd: ctx.cwd,
4904
5647
  stdio: ["ignore", "pipe", "pipe"]
@@ -4912,7 +5655,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4912
5655
  };
4913
5656
 
4914
5657
  // src/branch.ts
4915
- import { execFileSync as execFileSync14 } from "child_process";
5658
+ import { execFileSync as execFileSync16 } from "child_process";
4916
5659
  var UncommittedChangesError = class extends Error {
4917
5660
  constructor(branch) {
4918
5661
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -4922,7 +5665,7 @@ var UncommittedChangesError = class extends Error {
4922
5665
  branch;
4923
5666
  };
4924
5667
  function git2(args, cwd) {
4925
- return execFileSync14("git", args, {
5668
+ return execFileSync16("git", args, {
4926
5669
  encoding: "utf-8",
4927
5670
  timeout: 3e4,
4928
5671
  cwd,
@@ -4947,7 +5690,7 @@ function checkoutPrBranch(prNumber, cwd) {
4947
5690
  SKIP_HOOKS: "1",
4948
5691
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
4949
5692
  };
4950
- execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
5693
+ execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
4951
5694
  cwd,
4952
5695
  env,
4953
5696
  stdio: ["ignore", "pipe", "pipe"],
@@ -5061,8 +5804,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5061
5804
  }
5062
5805
 
5063
5806
  // src/gha.ts
5064
- import { execFileSync as execFileSync15 } from "child_process";
5065
- import * as fs20 from "fs";
5807
+ import { execFileSync as execFileSync17 } from "child_process";
5808
+ import * as fs21 from "fs";
5066
5809
  function getRunUrl() {
5067
5810
  const server = process.env.GITHUB_SERVER_URL;
5068
5811
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5073,10 +5816,10 @@ function getRunUrl() {
5073
5816
  function reactToTriggerComment(cwd) {
5074
5817
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5075
5818
  const eventPath = process.env.GITHUB_EVENT_PATH;
5076
- if (!eventPath || !fs20.existsSync(eventPath)) return;
5819
+ if (!eventPath || !fs21.existsSync(eventPath)) return;
5077
5820
  let event = null;
5078
5821
  try {
5079
- event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5822
+ event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5080
5823
  } catch {
5081
5824
  return;
5082
5825
  }
@@ -5104,7 +5847,7 @@ function reactToTriggerComment(cwd) {
5104
5847
  for (let attempt = 0; attempt < 3; attempt++) {
5105
5848
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5106
5849
  try {
5107
- execFileSync15("gh", args, opts);
5850
+ execFileSync17("gh", args, opts);
5108
5851
  return;
5109
5852
  } catch (err) {
5110
5853
  lastErr = err;
@@ -5117,13 +5860,13 @@ function reactToTriggerComment(cwd) {
5117
5860
  }
5118
5861
  function sleepMs(ms) {
5119
5862
  try {
5120
- execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5863
+ execFileSync17("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5121
5864
  } catch {
5122
5865
  }
5123
5866
  }
5124
5867
 
5125
5868
  // src/workflow.ts
5126
- import { execFileSync as execFileSync16 } from "child_process";
5869
+ import { execFileSync as execFileSync18 } from "child_process";
5127
5870
  var GH_TIMEOUT_MS = 3e4;
5128
5871
  function ghToken3() {
5129
5872
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -5131,7 +5874,7 @@ function ghToken3() {
5131
5874
  function gh3(args, cwd) {
5132
5875
  const token = ghToken3();
5133
5876
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
5134
- return execFileSync16("gh", args, {
5877
+ return execFileSync18("gh", args, {
5135
5878
  encoding: "utf-8",
5136
5879
  timeout: GH_TIMEOUT_MS,
5137
5880
  cwd,
@@ -5314,24 +6057,63 @@ function tryPostPr2(prNumber, body, cwd) {
5314
6057
  }
5315
6058
  }
5316
6059
 
6060
+ // src/scripts/handleAbandonedGoal.ts
6061
+ var handleAbandonedGoal = async (ctx) => {
6062
+ const goal = ctx.data.goal;
6063
+ if (!goal || goal.state !== "abandoned") return;
6064
+ process.stdout.write(`[goal-tick] ${goal.id} is abandoned \u2014 running cleanup
6065
+ `);
6066
+ const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
6067
+ if (!issues.ok) {
6068
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: list failed: ${issues.error}
6069
+ `);
6070
+ } else {
6071
+ for (const i of issues.value ?? []) {
6072
+ if (i.state !== "OPEN") continue;
6073
+ const r = closeIssue(
6074
+ i.number,
6075
+ {
6076
+ comment: "_Goal abandoned \u2014 closing this task without dispatch._",
6077
+ reason: "not planned"
6078
+ },
6079
+ ctx.cwd
6080
+ );
6081
+ if (!r.ok) {
6082
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close #${i.number}: ${r.error}
6083
+ `);
6084
+ }
6085
+ }
6086
+ }
6087
+ const goalPrs = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
6088
+ if (goalPrs.ok && goalPrs.value && goalPrs.value.length > 0) {
6089
+ const pr = goalPrs.value[0];
6090
+ const r = closePr(pr.number, "_Goal abandoned by operator \u2014 closing without merge._", ctx.cwd);
6091
+ if (!r.ok) {
6092
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close goal PR #${pr.number}: ${r.error}
6093
+ `);
6094
+ }
6095
+ }
6096
+ goal.state = "closed";
6097
+ };
6098
+
5317
6099
  // src/scripts/initFlow.ts
5318
- import { execFileSync as execFileSync17 } from "child_process";
5319
- import * as fs22 from "fs";
5320
- import * as path20 from "path";
6100
+ import { execFileSync as execFileSync19 } from "child_process";
6101
+ import * as fs23 from "fs";
6102
+ import * as path22 from "path";
5321
6103
 
5322
6104
  // src/scripts/loadQaGuide.ts
5323
- import * as fs21 from "fs";
5324
- import * as path19 from "path";
6105
+ import * as fs22 from "fs";
6106
+ import * as path21 from "path";
5325
6107
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
5326
6108
  var loadQaGuide = async (ctx) => {
5327
- const full = path19.join(ctx.cwd, QA_GUIDE_REL_PATH);
5328
- if (!fs21.existsSync(full)) {
6109
+ const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
6110
+ if (!fs22.existsSync(full)) {
5329
6111
  ctx.data.qaGuide = "";
5330
6112
  ctx.data.qaGuidePath = "";
5331
6113
  return;
5332
6114
  }
5333
6115
  try {
5334
- ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
6116
+ ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
5335
6117
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
5336
6118
  } catch {
5337
6119
  ctx.data.qaGuide = "";
@@ -5341,9 +6123,9 @@ var loadQaGuide = async (ctx) => {
5341
6123
 
5342
6124
  // src/scripts/initFlow.ts
5343
6125
  function detectPackageManager(cwd) {
5344
- if (fs22.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5345
- if (fs22.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
5346
- if (fs22.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
6126
+ if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6127
+ if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
6128
+ if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
5347
6129
  return "npm";
5348
6130
  }
5349
6131
  function qualityCommandsFor(pm) {
@@ -5356,7 +6138,7 @@ function qualityCommandsFor(pm) {
5356
6138
  function detectOwnerRepo(cwd) {
5357
6139
  let url;
5358
6140
  try {
5359
- url = execFileSync17("git", ["remote", "get-url", "origin"], {
6141
+ url = execFileSync19("git", ["remote", "get-url", "origin"], {
5360
6142
  cwd,
5361
6143
  encoding: "utf-8",
5362
6144
  stdio: ["ignore", "pipe", "pipe"]
@@ -5441,7 +6223,7 @@ jobs:
5441
6223
  `;
5442
6224
  function defaultBranchFromGit(cwd) {
5443
6225
  try {
5444
- const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
6226
+ const ref = execFileSync19("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5445
6227
  cwd,
5446
6228
  encoding: "utf-8",
5447
6229
  stdio: ["ignore", "pipe", "pipe"]
@@ -5449,7 +6231,7 @@ function defaultBranchFromGit(cwd) {
5449
6231
  return ref.replace("refs/remotes/origin/", "");
5450
6232
  } catch {
5451
6233
  try {
5452
- return execFileSync17("git", ["branch", "--show-current"], {
6234
+ return execFileSync19("git", ["branch", "--show-current"], {
5453
6235
  cwd,
5454
6236
  encoding: "utf-8",
5455
6237
  stdio: ["ignore", "pipe", "pipe"]
@@ -5465,48 +6247,48 @@ function performInit(cwd, force) {
5465
6247
  const pm = detectPackageManager(cwd);
5466
6248
  const ownerRepo = detectOwnerRepo(cwd);
5467
6249
  const defaultBranch = defaultBranchFromGit(cwd);
5468
- const configPath = path20.join(cwd, "kody.config.json");
5469
- if (fs22.existsSync(configPath) && !force) {
6250
+ const configPath = path22.join(cwd, "kody.config.json");
6251
+ if (fs23.existsSync(configPath) && !force) {
5470
6252
  skipped.push("kody.config.json");
5471
6253
  } else {
5472
6254
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
5473
- fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6255
+ fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5474
6256
  `);
5475
6257
  wrote.push("kody.config.json");
5476
6258
  }
5477
- const workflowDir = path20.join(cwd, ".github", "workflows");
5478
- const workflowPath = path20.join(workflowDir, "kody.yml");
5479
- if (fs22.existsSync(workflowPath) && !force) {
6259
+ const workflowDir = path22.join(cwd, ".github", "workflows");
6260
+ const workflowPath = path22.join(workflowDir, "kody.yml");
6261
+ if (fs23.existsSync(workflowPath) && !force) {
5480
6262
  skipped.push(".github/workflows/kody.yml");
5481
6263
  } else {
5482
- fs22.mkdirSync(workflowDir, { recursive: true });
5483
- fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6264
+ fs23.mkdirSync(workflowDir, { recursive: true });
6265
+ fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5484
6266
  wrote.push(".github/workflows/kody.yml");
5485
6267
  }
5486
- const hasUi = fs22.existsSync(path20.join(cwd, "src/app")) || fs22.existsSync(path20.join(cwd, "app")) || fs22.existsSync(path20.join(cwd, "pages"));
6268
+ const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
5487
6269
  if (hasUi) {
5488
- const qaGuidePath = path20.join(cwd, QA_GUIDE_REL_PATH);
5489
- if (fs22.existsSync(qaGuidePath) && !force) {
6270
+ const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6271
+ if (fs23.existsSync(qaGuidePath) && !force) {
5490
6272
  skipped.push(QA_GUIDE_REL_PATH);
5491
6273
  } else {
5492
- fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
6274
+ fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
5493
6275
  const discovery = runQaDiscovery(cwd);
5494
- fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6276
+ fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5495
6277
  wrote.push(QA_GUIDE_REL_PATH);
5496
6278
  }
5497
6279
  }
5498
6280
  const builtinJobs = listBuiltinJobs();
5499
6281
  if (builtinJobs.length > 0) {
5500
- const jobsDir = path20.join(cwd, ".kody", "jobs");
5501
- fs22.mkdirSync(jobsDir, { recursive: true });
6282
+ const jobsDir = path22.join(cwd, ".kody", "jobs");
6283
+ fs23.mkdirSync(jobsDir, { recursive: true });
5502
6284
  for (const job of builtinJobs) {
5503
- const rel = path20.join(".kody", "jobs", `${job.slug}.md`);
5504
- const target = path20.join(cwd, rel);
5505
- if (fs22.existsSync(target) && !force) {
6285
+ const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6286
+ const target = path22.join(cwd, rel);
6287
+ if (fs23.existsSync(target) && !force) {
5506
6288
  skipped.push(rel);
5507
6289
  continue;
5508
6290
  }
5509
- fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
6291
+ fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
5510
6292
  wrote.push(rel);
5511
6293
  }
5512
6294
  }
@@ -5518,12 +6300,12 @@ function performInit(cwd, force) {
5518
6300
  continue;
5519
6301
  }
5520
6302
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
5521
- const target = path20.join(workflowDir, `kody-${exe.name}.yml`);
5522
- if (fs22.existsSync(target) && !force) {
6303
+ const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6304
+ if (fs23.existsSync(target) && !force) {
5523
6305
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
5524
6306
  continue;
5525
6307
  }
5526
- fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6308
+ fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5527
6309
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
5528
6310
  }
5529
6311
  let labels;
@@ -5611,6 +6393,48 @@ var loadCoverageRules = async (ctx) => {
5611
6393
  ctx.data.coverageRules = ctx.config.testRequirements ?? [];
5612
6394
  };
5613
6395
 
6396
+ // src/scripts/loadGoalState.ts
6397
+ var loadGoalState = async (ctx) => {
6398
+ const goalId = ctx.args.goal;
6399
+ if (typeof goalId !== "string" || goalId.length === 0) {
6400
+ ctx.skipAgent = true;
6401
+ ctx.output.exitCode = 1;
6402
+ ctx.output.reason = "missing --goal";
6403
+ return;
6404
+ }
6405
+ if (goalId.includes("/") || goalId.includes("..")) {
6406
+ ctx.skipAgent = true;
6407
+ ctx.output.exitCode = 1;
6408
+ ctx.output.reason = "invalid goal id (no slashes or '..' allowed)";
6409
+ return;
6410
+ }
6411
+ try {
6412
+ const state = readGoalState(ctx.cwd, goalId);
6413
+ ctx.data.goal = {
6414
+ id: goalId,
6415
+ state: state.state,
6416
+ goalIssueNumber: state.goalIssueNumber,
6417
+ lastDispatchedIssue: state.lastDispatchedIssue,
6418
+ goalPrUrl: state.goalPrUrl,
6419
+ // Cache the full parsed object so saveGoalState can preserve `extra`.
6420
+ raw: state,
6421
+ // `phase` is populated by deriveGoalPhase later in the chain. Initialize
6422
+ // to undefined so runWhen on `data.goal.phase` can match correctly.
6423
+ phase: void 0,
6424
+ // Populated by ensureGoalBranch / configured by config.git.defaultBranch.
6425
+ defaultBranch: ctx.config.git.defaultBranch,
6426
+ // Convenience derivations.
6427
+ goalBranch: `goal-${goalId}`
6428
+ };
6429
+ } catch (err) {
6430
+ process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
6431
+ `);
6432
+ ctx.skipAgent = true;
6433
+ ctx.output.exitCode = 0;
6434
+ ctx.output.reason = "no goal state to tick";
6435
+ }
6436
+ };
6437
+
5614
6438
  // src/scripts/loadIssueContext.ts
5615
6439
  var DEFAULT_COMMENT_LIMIT = 12;
5616
6440
  var DEFAULT_COMMENT_MAX_BYTES = 16e3;
@@ -5661,8 +6485,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
5661
6485
  };
5662
6486
 
5663
6487
  // src/scripts/loadJobFromFile.ts
5664
- import * as fs23 from "fs";
5665
- import * as path21 from "path";
6488
+ import * as fs24 from "fs";
6489
+ import * as path23 from "path";
5666
6490
  var loadJobFromFile = async (ctx, _profile, args) => {
5667
6491
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
5668
6492
  const slugArg = String(args?.slugArg ?? "job");
@@ -5670,11 +6494,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
5670
6494
  if (!slug) {
5671
6495
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
5672
6496
  }
5673
- const absPath = path21.join(ctx.cwd, jobsDir, `${slug}.md`);
5674
- if (!fs23.existsSync(absPath)) {
6497
+ const absPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
6498
+ if (!fs24.existsSync(absPath)) {
5675
6499
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
5676
6500
  }
5677
- const raw = fs23.readFileSync(absPath, "utf-8");
6501
+ const raw = fs24.readFileSync(absPath, "utf-8");
5678
6502
  const { title, body } = parseJobFile(raw, slug);
5679
6503
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
5680
6504
  const loaded = await backend.load(slug);
@@ -5706,16 +6530,16 @@ function humanizeSlug(slug) {
5706
6530
  }
5707
6531
 
5708
6532
  // src/scripts/loadMemoryContext.ts
5709
- import * as fs24 from "fs";
5710
- import * as path22 from "path";
6533
+ import * as fs25 from "fs";
6534
+ import * as path24 from "path";
5711
6535
  var MEMORY_DIR_RELATIVE = ".kody/memory";
5712
6536
  var MAX_PAGES = 8;
5713
6537
  var PER_PAGE_MAX_BYTES = 4e3;
5714
6538
  var TOTAL_MAX_BYTES = 24e3;
5715
6539
  var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
5716
6540
  var loadMemoryContext = async (ctx) => {
5717
- const memoryAbs = path22.join(ctx.cwd, MEMORY_DIR_RELATIVE);
5718
- if (!fs24.existsSync(memoryAbs)) {
6541
+ const memoryAbs = path24.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6542
+ if (!fs25.existsSync(memoryAbs)) {
5719
6543
  ctx.data.memoryContext = "";
5720
6544
  return;
5721
6545
  }
@@ -5740,21 +6564,21 @@ function collectPages(memoryAbs) {
5740
6564
  walkMd(memoryAbs, (file) => {
5741
6565
  let stat;
5742
6566
  try {
5743
- stat = fs24.statSync(file);
6567
+ stat = fs25.statSync(file);
5744
6568
  } catch {
5745
6569
  return;
5746
6570
  }
5747
6571
  let raw;
5748
6572
  try {
5749
- raw = fs24.readFileSync(file, "utf-8");
6573
+ raw = fs25.readFileSync(file, "utf-8");
5750
6574
  } catch {
5751
6575
  return;
5752
6576
  }
5753
6577
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5754
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path22.basename(file, ".md");
6578
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path24.basename(file, ".md");
5755
6579
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
5756
6580
  out.push({
5757
- relPath: path22.relative(memoryAbs, file),
6581
+ relPath: path24.relative(memoryAbs, file),
5758
6582
  title,
5759
6583
  updated,
5760
6584
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -5822,16 +6646,16 @@ function walkMd(root, visit) {
5822
6646
  const dir = stack.pop();
5823
6647
  let names;
5824
6648
  try {
5825
- names = fs24.readdirSync(dir);
6649
+ names = fs25.readdirSync(dir);
5826
6650
  } catch {
5827
6651
  continue;
5828
6652
  }
5829
6653
  for (const name of names) {
5830
6654
  if (name.startsWith(".")) continue;
5831
- const full = path22.join(dir, name);
6655
+ const full = path24.join(dir, name);
5832
6656
  let stat;
5833
6657
  try {
5834
- stat = fs24.statSync(full);
6658
+ stat = fs25.statSync(full);
5835
6659
  } catch {
5836
6660
  continue;
5837
6661
  }
@@ -5954,8 +6778,32 @@ var markFlowSuccess = async (ctx) => {
5954
6778
  }
5955
6779
  };
5956
6780
 
6781
+ // src/scripts/mergeReadyTaskPRs.ts
6782
+ var mergeReadyTaskPRs = async (ctx) => {
6783
+ const goal = ctx.data.goal;
6784
+ if (!goal) return;
6785
+ const open = listPrsByBase(goal.goalBranch, "open", ctx.cwd);
6786
+ if (!open.ok) {
6787
+ process.stderr.write(`[goal-tick] mergeReadyTaskPRs: list failed: ${open.error}
6788
+ `);
6789
+ return;
6790
+ }
6791
+ for (const pr of open.value ?? []) {
6792
+ if (pr.isDraft) continue;
6793
+ if (pr.mergeable !== "MERGEABLE") continue;
6794
+ if (pr.mergeStateStatus !== "CLEAN") continue;
6795
+ process.stdout.write(`[goal-tick] merging PR #${pr.number} into ${goal.goalBranch}
6796
+ `);
6797
+ const r = mergePrSquash(pr.number, ctx.cwd);
6798
+ if (!r.ok) {
6799
+ process.stderr.write(`[goal-tick] failed to merge PR #${pr.number}: ${r.error} (continuing)
6800
+ `);
6801
+ }
6802
+ }
6803
+ };
6804
+
5957
6805
  // src/scripts/mergeReleasePr.ts
5958
- import { execFileSync as execFileSync18 } from "child_process";
6806
+ import { execFileSync as execFileSync20 } from "child_process";
5959
6807
  var API_TIMEOUT_MS7 = 6e4;
5960
6808
  var mergeReleasePr = async (ctx) => {
5961
6809
  const state = ctx.data.taskState;
@@ -5974,7 +6822,7 @@ var mergeReleasePr = async (ctx) => {
5974
6822
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
5975
6823
  `);
5976
6824
  try {
5977
- const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
6825
+ const out = execFileSync20("gh", ["pr", "merge", String(prNumber), "--merge"], {
5978
6826
  timeout: API_TIMEOUT_MS7,
5979
6827
  cwd: ctx.cwd,
5980
6828
  stdio: ["ignore", "pipe", "pipe"]
@@ -6092,7 +6940,7 @@ function buildIssueTitle(scope, verdict) {
6092
6940
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
6093
6941
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
6094
6942
  }
6095
- function ensureLabel2(cwd) {
6943
+ function ensureLabel3(cwd) {
6096
6944
  try {
6097
6945
  gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6098
6946
  return true;
@@ -6152,7 +7000,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
6152
7000
  }
6153
7001
  const scope = ctx.args.scope;
6154
7002
  const title = buildIssueTitle(scope, verdict);
6155
- const hasLabel = ensureLabel2(ctx.cwd);
7003
+ const hasLabel = ensureLabel3(ctx.cwd);
6156
7004
  let created;
6157
7005
  try {
6158
7006
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -6417,6 +7265,22 @@ var persistFlowState = async (ctx) => {
6417
7265
  }
6418
7266
  };
6419
7267
 
7268
+ // src/scripts/prOutcome.ts
7269
+ function readPrOutcome(data) {
7270
+ const raw = data.prResult;
7271
+ if (!raw || typeof raw !== "object") return null;
7272
+ const r = raw;
7273
+ switch (r.kind) {
7274
+ case "created":
7275
+ case "updated":
7276
+ case "skipped":
7277
+ case "crashed":
7278
+ return raw;
7279
+ default:
7280
+ return null;
7281
+ }
7282
+ }
7283
+
6420
7284
  // src/scripts/postIssueComment.ts
6421
7285
  var FAILED_LABEL_SPEC = {
6422
7286
  label: "kody:failed",
@@ -6430,8 +7294,7 @@ var postIssueComment2 = async (ctx) => {
6430
7294
  if (!targetType || !targetNumber) return;
6431
7295
  const commitResult = ctx.data.commitResult;
6432
7296
  const hasCommits = Boolean(ctx.data.hasCommitsAhead);
6433
- const prUrl = ctx.output.prUrl;
6434
- const prAction = ctx.data.prResult?.action;
7297
+ const prResult = readPrOutcome(ctx.data);
6435
7298
  if (!commitResult?.committed && !hasCommits) {
6436
7299
  const specific = computeFailureReason2(ctx);
6437
7300
  const reason = specific.length > 0 ? specific : "no changes to commit";
@@ -6449,18 +7312,17 @@ var postIssueComment2 = async (ctx) => {
6449
7312
  }
6450
7313
  const failureReason = computeFailureReason2(ctx);
6451
7314
  const isFailure = failureReason.length > 0;
6452
- const justPushedToExistingPr = prAction === "updated" && commitResult?.committed === true;
6453
- const successMsg = justPushedToExistingPr ? `\u2705 kody pushed to ${prUrl}` : prAction === "updated" ? `\u2139\uFE0F kody made no changes \u2014 PR: ${prUrl}` : `\u2705 kody PR opened: ${prUrl}`;
6454
7315
  const branch = ctx.data.branch;
6455
- const failurePrSuffix = computeFailureSuffix({
6456
- prUrl,
6457
- prAction,
7316
+ const msg = renderMessage({
7317
+ prResult,
7318
+ isFailure,
7319
+ failureReason,
7320
+ justPushedToExistingPr: prResult?.kind === "updated" && commitResult?.committed === true,
6458
7321
  branch,
6459
7322
  branchPushed: commitResult?.committed === true,
6460
7323
  githubOwner: ctx.config.github?.owner,
6461
7324
  githubRepo: ctx.config.github?.repo
6462
7325
  });
6463
- const msg = isFailure ? `\u26A0\uFE0F kody FAILED: ${truncate2(failureReason, 1500)}${failurePrSuffix}` : successMsg;
6464
7326
  postWith(targetType, targetNumber, msg, ctx.cwd);
6465
7327
  let exitCode = 0;
6466
7328
  const agentDone = Boolean(ctx.data.agentDone);
@@ -6484,12 +7346,29 @@ function markRunFailed(ctx) {
6484
7346
  }
6485
7347
  }
6486
7348
  function computeFailureSuffix(input) {
6487
- if (input.prUrl) {
6488
- return input.prAction === "updated" ? ` \u2014 PR: ${input.prUrl}` : ` \u2014 draft PR: ${input.prUrl}`;
6489
- }
7349
+ if (input.prResult?.kind === "created") return ` \u2014 draft PR: ${input.prResult.url}`;
7350
+ if (input.prResult?.kind === "updated") return ` \u2014 PR: ${input.prResult.url}`;
6490
7351
  if (!input.branchPushed || !input.branch || !input.githubOwner || !input.githubRepo) return "";
6491
7352
  return ` \u2014 branch: https://github.com/${input.githubOwner}/${input.githubRepo}/tree/${input.branch}`;
6492
7353
  }
7354
+ function renderMessage(input) {
7355
+ const suffix = computeFailureSuffix(input);
7356
+ if (input.isFailure) {
7357
+ return `\u26A0\uFE0F kody FAILED: ${truncate2(input.failureReason, 1500)}${suffix}`;
7358
+ }
7359
+ switch (input.prResult?.kind) {
7360
+ case "created":
7361
+ return `\u2705 kody PR opened: ${input.prResult.url}`;
7362
+ case "updated":
7363
+ return input.justPushedToExistingPr ? `\u2705 kody pushed to ${input.prResult.url}` : `\u2139\uFE0F kody made no changes \u2014 PR: ${input.prResult.url}`;
7364
+ case "skipped":
7365
+ return `\u26A0\uFE0F kody finished but did not open a PR \u2014 ${input.prResult.reason}${suffix}`;
7366
+ case "crashed":
7367
+ return `\u26A0\uFE0F kody PR step crashed: ${truncate2(input.prResult.reason, 1500)}${suffix}`;
7368
+ case void 0:
7369
+ return `\u26A0\uFE0F kody finished but PR step did not run${suffix}`;
7370
+ }
7371
+ }
6493
7372
  function computeFailureReason2(ctx) {
6494
7373
  const misses = ctx.data.coverageMisses ?? [];
6495
7374
  if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
@@ -6557,7 +7436,7 @@ ${body}`;
6557
7436
  }
6558
7437
 
6559
7438
  // src/scripts/recordClassification.ts
6560
- import { execFileSync as execFileSync19 } from "child_process";
7439
+ import { execFileSync as execFileSync21 } from "child_process";
6561
7440
  var API_TIMEOUT_MS8 = 3e4;
6562
7441
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6563
7442
  var recordClassification = async (ctx) => {
@@ -6605,7 +7484,7 @@ function parseClassification(prSummary) {
6605
7484
  }
6606
7485
  function tryAuditComment(issueNumber, body, cwd) {
6607
7486
  try {
6608
- execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7487
+ execFileSync21("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6609
7488
  cwd,
6610
7489
  timeout: API_TIMEOUT_MS8,
6611
7490
  stdio: ["ignore", "pipe", "pipe"]
@@ -6644,14 +7523,14 @@ var requireFeedbackActions = async (ctx, profile) => {
6644
7523
  const items = countActionItems(actions);
6645
7524
  ctx.data.feedbackAgentItemCount = items;
6646
7525
  if (items < MIN_ITEMS) {
6647
- fail(
7526
+ fail2(
6648
7527
  ctx,
6649
7528
  profile,
6650
7529
  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"
6651
7530
  );
6652
7531
  }
6653
7532
  };
6654
- function fail(ctx, profile, reason) {
7533
+ function fail2(ctx, profile, reason) {
6655
7534
  ctx.data.agentDone = false;
6656
7535
  ctx.data.agentFailureReason = reason;
6657
7536
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
@@ -6728,7 +7607,7 @@ var resolveArtifacts = async (ctx, profile) => {
6728
7607
  };
6729
7608
 
6730
7609
  // src/scripts/resolveFlow.ts
6731
- import { execFileSync as execFileSync20 } from "child_process";
7610
+ import { execFileSync as execFileSync22 } from "child_process";
6732
7611
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
6733
7612
  var resolveFlow = async (ctx) => {
6734
7613
  const prNumber = ctx.args.pr;
@@ -6798,7 +7677,7 @@ function buildPreferBlock(prefer, baseBranch) {
6798
7677
  }
6799
7678
  function getConflictedFiles(cwd) {
6800
7679
  try {
6801
- const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
7680
+ const out = execFileSync22("git", ["diff", "--name-only", "--diff-filter=U"], {
6802
7681
  encoding: "utf-8",
6803
7682
  cwd,
6804
7683
  env: { ...process.env, HUSKY: "0" }
@@ -6813,7 +7692,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
6813
7692
  let total = 0;
6814
7693
  for (const f of files) {
6815
7694
  try {
6816
- const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
7695
+ const content = execFileSync22("cat", [f], { encoding: "utf-8", cwd }).toString();
6817
7696
  const snippet = `### ${f}
6818
7697
 
6819
7698
  \`\`\`
@@ -6914,10 +7793,10 @@ var resolvePreviewUrl = async (ctx) => {
6914
7793
  };
6915
7794
 
6916
7795
  // src/scripts/resolveQaUrl.ts
6917
- import { execFileSync as execFileSync21 } from "child_process";
7796
+ import { execFileSync as execFileSync23 } from "child_process";
6918
7797
  function ghQuery(args, cwd) {
6919
7798
  try {
6920
- const out = execFileSync21("gh", args, {
7799
+ const out = execFileSync23("gh", args, {
6921
7800
  cwd,
6922
7801
  stdio: ["ignore", "pipe", "pipe"],
6923
7802
  encoding: "utf-8",
@@ -6987,7 +7866,7 @@ var resolveQaUrl = async (ctx) => {
6987
7866
  };
6988
7867
 
6989
7868
  // src/scripts/revertFlow.ts
6990
- import { execFileSync as execFileSync22 } from "child_process";
7869
+ import { execFileSync as execFileSync24 } from "child_process";
6991
7870
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
6992
7871
  var revertFlow = async (ctx) => {
6993
7872
  const prNumber = ctx.args.pr;
@@ -7069,7 +7948,7 @@ function buildPrSummary(resolved) {
7069
7948
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
7070
7949
  }
7071
7950
  function git3(args, cwd) {
7072
- return execFileSync22("git", args, {
7951
+ return execFileSync24("git", args, {
7073
7952
  encoding: "utf-8",
7074
7953
  timeout: 3e4,
7075
7954
  cwd,
@@ -7079,7 +7958,7 @@ function git3(args, cwd) {
7079
7958
  }
7080
7959
  function isAncestorOfHead(sha, cwd) {
7081
7960
  try {
7082
- execFileSync22("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7961
+ execFileSync24("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7083
7962
  cwd,
7084
7963
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7085
7964
  stdio: ["ignore", "ignore", "ignore"]
@@ -7175,17 +8054,17 @@ function resolveBaseOverride(value) {
7175
8054
  }
7176
8055
  function resolveBaseFromLabels(labels) {
7177
8056
  if (!labels.includes("goal-runner:dispatched")) return null;
7178
- const goalLabel = labels.find((l) => l.startsWith("goal:"));
7179
- if (!goalLabel) return null;
7180
- const goalId = goalLabel.slice("goal:".length);
8057
+ const goalLabel2 = labels.find((l) => l.startsWith("goal:"));
8058
+ if (!goalLabel2) return null;
8059
+ const goalId = goalLabel2.slice("goal:".length);
7181
8060
  if (!/^[a-z0-9-]+$/.test(goalId)) return null;
7182
8061
  return `goal-${goalId}`;
7183
8062
  }
7184
8063
 
7185
8064
  // src/scripts/runTickScript.ts
7186
8065
  import { spawnSync } from "child_process";
7187
- import * as fs25 from "fs";
7188
- import * as path23 from "path";
8066
+ import * as fs26 from "fs";
8067
+ import * as path25 from "path";
7189
8068
  var runTickScript = async (ctx, _profile, args) => {
7190
8069
  ctx.skipAgent = true;
7191
8070
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -7197,13 +8076,13 @@ var runTickScript = async (ctx, _profile, args) => {
7197
8076
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7198
8077
  return;
7199
8078
  }
7200
- const jobPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
7201
- if (!fs25.existsSync(jobPath)) {
8079
+ const jobPath = path25.join(ctx.cwd, jobsDir, `${slug}.md`);
8080
+ if (!fs26.existsSync(jobPath)) {
7202
8081
  ctx.output.exitCode = 99;
7203
8082
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7204
8083
  return;
7205
8084
  }
7206
- const raw = fs25.readFileSync(jobPath, "utf-8");
8085
+ const raw = fs26.readFileSync(jobPath, "utf-8");
7207
8086
  const { frontmatter } = splitFrontmatter(raw);
7208
8087
  const tickScript = frontmatter.tickScript;
7209
8088
  if (!tickScript) {
@@ -7211,8 +8090,8 @@ var runTickScript = async (ctx, _profile, args) => {
7211
8090
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7212
8091
  return;
7213
8092
  }
7214
- const scriptPath = path23.isAbsolute(tickScript) ? tickScript : path23.join(ctx.cwd, tickScript);
7215
- if (!fs25.existsSync(scriptPath)) {
8093
+ const scriptPath = path25.isAbsolute(tickScript) ? tickScript : path25.join(ctx.cwd, tickScript);
8094
+ if (!fs26.existsSync(scriptPath)) {
7216
8095
  ctx.output.exitCode = 99;
7217
8096
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7218
8097
  return;
@@ -7313,6 +8192,26 @@ function buildChildEnv(parent, force) {
7313
8192
  return out;
7314
8193
  }
7315
8194
 
8195
+ // src/scripts/saveGoalState.ts
8196
+ var saveGoalState = async (ctx) => {
8197
+ const goal = ctx.data.goal;
8198
+ if (!goal) {
8199
+ ctx.skipAgent = true;
8200
+ return;
8201
+ }
8202
+ const updated = {
8203
+ ...goal.raw ?? { state: goal.state, extra: {} },
8204
+ state: goal.state,
8205
+ goalIssueNumber: goal.goalIssueNumber,
8206
+ lastDispatchedIssue: goal.lastDispatchedIssue,
8207
+ goalPrUrl: goal.goalPrUrl,
8208
+ completedAt: goal.completedAt ?? goal.raw?.completedAt,
8209
+ updatedAt: nowIso()
8210
+ };
8211
+ writeGoalState(ctx.cwd, goal.id, updated);
8212
+ ctx.skipAgent = true;
8213
+ };
8214
+
7316
8215
  // src/scripts/saveTaskState.ts
7317
8216
  var saveTaskState = async (ctx, profile) => {
7318
8217
  const target = ctx.data.commentTargetType;
@@ -7388,11 +8287,11 @@ var skipAgent = async (ctx) => {
7388
8287
  };
7389
8288
 
7390
8289
  // src/scripts/stageMergeConflicts.ts
7391
- import { execFileSync as execFileSync23 } from "child_process";
8290
+ import { execFileSync as execFileSync25 } from "child_process";
7392
8291
  var stageMergeConflicts = async (ctx) => {
7393
8292
  if (ctx.data.agentDone === false) return;
7394
8293
  try {
7395
- execFileSync23("git", ["add", "-A"], {
8294
+ execFileSync25("git", ["add", "-A"], {
7396
8295
  cwd: ctx.cwd,
7397
8296
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7398
8297
  stdio: "pipe"
@@ -7402,7 +8301,7 @@ var stageMergeConflicts = async (ctx) => {
7402
8301
  };
7403
8302
 
7404
8303
  // src/scripts/startFlow.ts
7405
- import { execFileSync as execFileSync24 } from "child_process";
8304
+ import { execFileSync as execFileSync26 } from "child_process";
7406
8305
  var API_TIMEOUT_MS9 = 3e4;
7407
8306
  var startFlow = async (ctx, profile, _agentResult, args) => {
7408
8307
  const entry = args?.entry;
@@ -7436,7 +8335,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7436
8335
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
7437
8336
  const body = `@kody ${next}`;
7438
8337
  try {
7439
- execFileSync24("gh", [sub, "comment", String(targetNumber), "--body", body], {
8338
+ execFileSync26("gh", [sub, "comment", String(targetNumber), "--body", body], {
7440
8339
  timeout: API_TIMEOUT_MS9,
7441
8340
  cwd,
7442
8341
  stdio: ["ignore", "pipe", "pipe"]
@@ -7450,7 +8349,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7450
8349
  }
7451
8350
 
7452
8351
  // src/scripts/syncFlow.ts
7453
- import { execFileSync as execFileSync25 } from "child_process";
8352
+ import { execFileSync as execFileSync27 } from "child_process";
7454
8353
  var syncFlow = async (ctx, _profile, args) => {
7455
8354
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
7456
8355
  const prNumber = ctx.args.pr;
@@ -7522,7 +8421,7 @@ function bail2(ctx, prNumber, reason) {
7522
8421
  }
7523
8422
  function revParseHead(cwd) {
7524
8423
  try {
7525
- return execFileSync25("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
8424
+ return execFileSync27("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
7526
8425
  } catch {
7527
8426
  return "";
7528
8427
  }
@@ -7530,9 +8429,9 @@ function revParseHead(cwd) {
7530
8429
  function pushBranch(branch, cwd) {
7531
8430
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
7532
8431
  try {
7533
- execFileSync25("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
8432
+ execFileSync27("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
7534
8433
  } catch {
7535
- execFileSync25("git", ["push", "--force-with-lease", "-u", "origin", branch], {
8434
+ execFileSync27("git", ["push", "--force-with-lease", "-u", "origin", branch], {
7536
8435
  cwd,
7537
8436
  env,
7538
8437
  stdio: ["ignore", "pipe", "pipe"]
@@ -7759,7 +8658,7 @@ function downgrade2(ctx, reason) {
7759
8658
  }
7760
8659
 
7761
8660
  // src/scripts/waitForCi.ts
7762
- import { execFileSync as execFileSync26 } from "child_process";
8661
+ import { execFileSync as execFileSync28 } from "child_process";
7763
8662
  var API_TIMEOUT_MS10 = 3e4;
7764
8663
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
7765
8664
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -7837,7 +8736,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
7837
8736
  };
7838
8737
  function fetchChecks(prNumber, cwd) {
7839
8738
  try {
7840
- const raw = execFileSync26("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
8739
+ const raw = execFileSync28("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
7841
8740
  encoding: "utf-8",
7842
8741
  timeout: API_TIMEOUT_MS10,
7843
8742
  cwd,
@@ -8095,7 +8994,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
8095
8994
  };
8096
8995
 
8097
8996
  // src/scripts/writeRunSummary.ts
8098
- import * as fs26 from "fs";
8997
+ import * as fs27 from "fs";
8099
8998
  var writeRunSummary = async (ctx, profile) => {
8100
8999
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
8101
9000
  if (!summaryPath) return;
@@ -8117,7 +9016,7 @@ var writeRunSummary = async (ctx, profile) => {
8117
9016
  if (reason) lines.push(`- **Reason:** ${reason}`);
8118
9017
  lines.push("");
8119
9018
  try {
8120
- fs26.appendFileSync(summaryPath, `${lines.join("\n")}
9019
+ fs27.appendFileSync(summaryPath, `${lines.join("\n")}
8121
9020
  `);
8122
9021
  } catch {
8123
9022
  }
@@ -8156,7 +9055,19 @@ var preflightScripts = {
8156
9055
  warmupMcp,
8157
9056
  dispatchJobTicks,
8158
9057
  dispatchJobFileTicks,
8159
- runTickScript
9058
+ runTickScript,
9059
+ loadGoalState,
9060
+ handleAbandonedGoal,
9061
+ ensureLifecycleLabels,
9062
+ ensureUmbrellaIssue,
9063
+ ensureGoalPr,
9064
+ mergeReadyTaskPRs,
9065
+ closeMergedTaskIssues,
9066
+ deriveGoalPhase,
9067
+ ensureGoalBranch,
9068
+ dispatchNextTask,
9069
+ finalizeGoal,
9070
+ saveGoalState
8160
9071
  };
8161
9072
  var postflightScripts = {
8162
9073
  parseAgentResult: parseAgentResult2,
@@ -8195,7 +9106,8 @@ var postflightScripts = {
8195
9106
  recordOutcome,
8196
9107
  mergeReleasePr,
8197
9108
  waitForCi,
8198
- markFlowSuccess
9109
+ markFlowSuccess,
9110
+ commitGoalState
8199
9111
  };
8200
9112
  var allScriptNames = /* @__PURE__ */ new Set([
8201
9113
  ...Object.keys(preflightScripts),
@@ -8203,7 +9115,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
8203
9115
  ]);
8204
9116
 
8205
9117
  // src/tools.ts
8206
- import { execFileSync as execFileSync27 } from "child_process";
9118
+ import { execFileSync as execFileSync29 } from "child_process";
8207
9119
  function verifyCliTools(tools, cwd) {
8208
9120
  const out = [];
8209
9121
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -8236,7 +9148,7 @@ function verifyOne(tool, cwd) {
8236
9148
  }
8237
9149
  function runShell(cmd, cwd, timeoutMs = 3e4) {
8238
9150
  try {
8239
- execFileSync27("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
9151
+ execFileSync29("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8240
9152
  return true;
8241
9153
  } catch {
8242
9154
  return false;
@@ -8305,9 +9217,9 @@ async function runExecutable(profileName, input) {
8305
9217
  data: {},
8306
9218
  output: { exitCode: 0 }
8307
9219
  };
8308
- const ndjsonDir = path24.join(input.cwd, ".kody");
9220
+ const ndjsonDir = path26.join(input.cwd, ".kody");
8309
9221
  const invokeAgent = async (prompt) => {
8310
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path24.isAbsolute(p) ? p : path24.resolve(profile.dir, p)).filter((p) => p.length > 0);
9222
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path26.isAbsolute(p) ? p : path26.resolve(profile.dir, p)).filter((p) => p.length > 0);
8311
9223
  const syntheticPath = ctx.data.syntheticPluginPath;
8312
9224
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
8313
9225
  return runAgent({
@@ -8416,17 +9328,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
8416
9328
  function resolveProfilePath(profileName) {
8417
9329
  const found = resolveExecutable(profileName);
8418
9330
  if (found) return found;
8419
- const here = path24.dirname(new URL(import.meta.url).pathname);
9331
+ const here = path26.dirname(new URL(import.meta.url).pathname);
8420
9332
  const candidates = [
8421
- path24.join(here, "executables", profileName, "profile.json"),
9333
+ path26.join(here, "executables", profileName, "profile.json"),
8422
9334
  // same-dir sibling (dev)
8423
- path24.join(here, "..", "executables", profileName, "profile.json"),
9335
+ path26.join(here, "..", "executables", profileName, "profile.json"),
8424
9336
  // up one (prod: dist/bin → dist/executables)
8425
- path24.join(here, "..", "src", "executables", profileName, "profile.json")
9337
+ path26.join(here, "..", "src", "executables", profileName, "profile.json")
8426
9338
  // fallback
8427
9339
  ];
8428
9340
  for (const c of candidates) {
8429
- if (fs27.existsSync(c)) return c;
9341
+ if (fs28.existsSync(c)) return c;
8430
9342
  }
8431
9343
  return candidates[0];
8432
9344
  }
@@ -8530,8 +9442,8 @@ function resolveShellTimeoutMs(entry) {
8530
9442
  var SIGKILL_GRACE_MS = 5e3;
8531
9443
  async function runShellEntry(entry, ctx, profile) {
8532
9444
  const shellName = entry.shell;
8533
- const shellPath = path24.join(profile.dir, shellName);
8534
- if (!fs27.existsSync(shellPath)) {
9445
+ const shellPath = path26.join(profile.dir, shellName);
9446
+ if (!fs28.existsSync(shellPath)) {
8535
9447
  ctx.skipAgent = true;
8536
9448
  ctx.output.exitCode = 99;
8537
9449
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8792,7 +9704,7 @@ async function runContainerLoop(profile, ctx, input) {
8792
9704
  }
8793
9705
  function resetWorkingTree(cwd) {
8794
9706
  try {
8795
- execFileSync28("git", ["reset", "--hard", "HEAD"], {
9707
+ execFileSync30("git", ["reset", "--hard", "HEAD"], {
8796
9708
  cwd,
8797
9709
  stdio: ["ignore", "pipe", "pipe"],
8798
9710
  timeout: 3e4
@@ -8944,14 +9856,14 @@ function resolveAuthToken(env = process.env) {
8944
9856
  return token;
8945
9857
  }
8946
9858
  function detectPackageManager2(cwd) {
8947
- if (fs28.existsSync(path25.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8948
- if (fs28.existsSync(path25.join(cwd, "yarn.lock"))) return "yarn";
8949
- if (fs28.existsSync(path25.join(cwd, "bun.lockb"))) return "bun";
9859
+ if (fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9860
+ if (fs29.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9861
+ if (fs29.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
8950
9862
  return "npm";
8951
9863
  }
8952
9864
  function shellOut(cmd, args, cwd, stream = true) {
8953
9865
  try {
8954
- execFileSync29(cmd, args, {
9866
+ execFileSync31(cmd, args, {
8955
9867
  cwd,
8956
9868
  stdio: stream ? "inherit" : "pipe",
8957
9869
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -8964,7 +9876,7 @@ function shellOut(cmd, args, cwd, stream = true) {
8964
9876
  }
8965
9877
  function isOnPath(bin) {
8966
9878
  try {
8967
- execFileSync29("which", [bin], { stdio: "pipe" });
9879
+ execFileSync31("which", [bin], { stdio: "pipe" });
8968
9880
  return true;
8969
9881
  } catch {
8970
9882
  return false;
@@ -9005,7 +9917,7 @@ function installLitellmIfNeeded(cwd) {
9005
9917
  } catch {
9006
9918
  }
9007
9919
  try {
9008
- execFileSync29("python3", ["-c", "import litellm"], { stdio: "pipe" });
9920
+ execFileSync31("python3", ["-c", "import litellm"], { stdio: "pipe" });
9009
9921
  process.stdout.write("\u2192 kody: litellm already installed\n");
9010
9922
  return 0;
9011
9923
  } catch {
@@ -9015,16 +9927,16 @@ function installLitellmIfNeeded(cwd) {
9015
9927
  }
9016
9928
  function configureGitIdentity(cwd) {
9017
9929
  try {
9018
- const name = execFileSync29("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9930
+ const name = execFileSync31("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9019
9931
  if (name) return;
9020
9932
  } catch {
9021
9933
  }
9022
9934
  try {
9023
- execFileSync29("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9935
+ execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9024
9936
  } catch {
9025
9937
  }
9026
9938
  try {
9027
- execFileSync29("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9939
+ execFileSync31("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9028
9940
  cwd,
9029
9941
  stdio: "pipe"
9030
9942
  });
@@ -9033,11 +9945,11 @@ function configureGitIdentity(cwd) {
9033
9945
  }
9034
9946
  function postFailureTail(issueNumber, cwd, reason) {
9035
9947
  if (!issueNumber) return;
9036
- const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
9948
+ const logPath = path27.join(cwd, ".kody", "last-run.jsonl");
9037
9949
  let tail = "";
9038
9950
  try {
9039
- if (fs28.existsSync(logPath)) {
9040
- const content = fs28.readFileSync(logPath, "utf-8");
9951
+ if (fs29.existsSync(logPath)) {
9952
+ const content = fs29.readFileSync(logPath, "utf-8");
9041
9953
  tail = content.slice(-3e3);
9042
9954
  }
9043
9955
  } catch {
@@ -9062,7 +9974,7 @@ async function runCi(argv) {
9062
9974
  return 0;
9063
9975
  }
9064
9976
  const args = parseCiArgs(argv);
9065
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9977
+ const cwd = args.cwd ? path27.resolve(args.cwd) : process.cwd();
9066
9978
  let earlyConfig;
9067
9979
  try {
9068
9980
  earlyConfig = loadConfig(cwd);
@@ -9072,9 +9984,9 @@ async function runCi(argv) {
9072
9984
  const eventName = process.env.GITHUB_EVENT_NAME;
9073
9985
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
9074
9986
  let manualWorkflowDispatch = false;
9075
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
9987
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs29.existsSync(dispatchEventPath)) {
9076
9988
  try {
9077
- const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
9989
+ const evt = JSON.parse(fs29.readFileSync(dispatchEventPath, "utf-8"));
9078
9990
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
9079
9991
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
9080
9992
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -9289,15 +10201,15 @@ function parseChatArgs(argv, env = process.env) {
9289
10201
  return result;
9290
10202
  }
9291
10203
  function commitChatFiles(cwd, sessionId, verbose) {
9292
- const sessionFile = path26.relative(cwd, sessionFilePath(cwd, sessionId));
9293
- const eventsFile = path26.relative(cwd, eventsFilePath(cwd, sessionId));
9294
- const paths = [sessionFile, eventsFile].filter((p) => fs29.existsSync(path26.join(cwd, p)));
10204
+ const sessionFile = path28.relative(cwd, sessionFilePath(cwd, sessionId));
10205
+ const eventsFile = path28.relative(cwd, eventsFilePath(cwd, sessionId));
10206
+ const paths = [sessionFile, eventsFile].filter((p) => fs30.existsSync(path28.join(cwd, p)));
9295
10207
  if (paths.length === 0) return;
9296
10208
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
9297
10209
  try {
9298
- execFileSync30("git", ["add", "-f", ...paths], opts);
9299
- execFileSync30("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
9300
- execFileSync30("git", ["push", "--quiet", "origin", "HEAD"], opts);
10210
+ execFileSync32("git", ["add", "-f", ...paths], opts);
10211
+ execFileSync32("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
10212
+ execFileSync32("git", ["push", "--quiet", "origin", "HEAD"], opts);
9301
10213
  } catch (err) {
9302
10214
  const msg = err instanceof Error ? err.message : String(err);
9303
10215
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -9329,7 +10241,7 @@ async function runChat(argv) {
9329
10241
  ${CHAT_HELP}`);
9330
10242
  return 64;
9331
10243
  }
9332
- const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
10244
+ const cwd = args.cwd ? path28.resolve(args.cwd) : process.cwd();
9333
10245
  const sessionId = args.sessionId;
9334
10246
  const unpackedSecrets = unpackAllSecrets();
9335
10247
  if (unpackedSecrets > 0) {
@@ -9381,7 +10293,7 @@ ${CHAT_HELP}`);
9381
10293
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
9382
10294
  const meta = readMeta(sessionFile);
9383
10295
  process.stdout.write(
9384
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10296
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs30.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9385
10297
  `
9386
10298
  );
9387
10299
  try {