@kody-ade/kody-engine 0.4.30 → 0.4.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -3,7 +3,7 @@
3
3
  // package.json
4
4
  var package_default = {
5
5
  name: "@kody-ade/kody-engine",
6
- version: "0.4.30",
6
+ version: "0.4.31",
7
7
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
8
  license: "MIT",
9
9
  type: "module",
@@ -51,9 +51,9 @@ var package_default = {
51
51
  };
52
52
 
53
53
  // src/chat-cli.ts
54
- import { execFileSync as execFileSync30 } from "child_process";
55
- import * as fs29 from "fs";
56
- import * as path26 from "path";
54
+ import { execFileSync as execFileSync32 } from "child_process";
55
+ import * as fs30 from "fs";
56
+ import * as path28 from "path";
57
57
 
58
58
  // src/chat/events.ts
59
59
  import * as fs from "fs";
@@ -912,9 +912,9 @@ async function emit2(sink, type, sessionId, suffix, payload) {
912
912
  }
913
913
 
914
914
  // src/kody-cli.ts
915
- import { execFileSync as execFileSync29 } from "child_process";
916
- import * as fs28 from "fs";
917
- import * as path25 from "path";
915
+ import { execFileSync as execFileSync31 } from "child_process";
916
+ import * as fs29 from "fs";
917
+ import * as path27 from "path";
918
918
 
919
919
  // src/dispatch.ts
920
920
  import * as fs7 from "fs";
@@ -1299,9 +1299,9 @@ function coerceBare(spec, value) {
1299
1299
  }
1300
1300
 
1301
1301
  // src/executor.ts
1302
- import { execFileSync as execFileSync28, spawn as spawn5 } from "child_process";
1303
- import * as fs27 from "fs";
1304
- import * as path24 from "path";
1302
+ import { execFileSync as execFileSync30, spawn as spawn5 } from "child_process";
1303
+ import * as fs28 from "fs";
1304
+ import * as path26 from "path";
1305
1305
 
1306
1306
  // src/issue.ts
1307
1307
  import { execFileSync as execFileSync3 } from "child_process";
@@ -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) {
@@ -4863,8 +5381,203 @@ function collectExpectedTests(raw) {
4863
5381
  return out;
4864
5382
  }
4865
5383
 
5384
+ // src/scripts/ensureUmbrellaIssue.ts
5385
+ var ensureUmbrellaIssue = async (ctx) => {
5386
+ const goal = ctx.data.goal;
5387
+ if (!goal) return;
5388
+ if (goal.goalIssueNumber !== void 0) return;
5389
+ const title = `goal: ${goal.id}`;
5390
+ const body = `Umbrella issue for goal **${goal.id}**.
5391
+
5392
+ Closed automatically when the goal PR (\`${goal.goalBranch}\` \u2192 \`${goal.defaultBranch}\`) merges.
5393
+ `;
5394
+ const existing = findUmbrellaByTitle(goal.id, title, ctx.cwd);
5395
+ if (existing.ok && existing.value !== null && existing.value !== void 0) {
5396
+ process.stdout.write(`[goal-tick] adopted existing umbrella issue #${existing.value} for ${goal.id}
5397
+ `);
5398
+ goal.goalIssueNumber = existing.value;
5399
+ return;
5400
+ }
5401
+ const created = createIssue(
5402
+ {
5403
+ title,
5404
+ body,
5405
+ labels: [goalLabel(goal.id), UMBRELLA_BUILDING_LABEL]
5406
+ },
5407
+ ctx.cwd
5408
+ );
5409
+ if (!created.ok) {
5410
+ process.stderr.write(
5411
+ `[goal-tick] ensureUmbrellaIssue: gh issue create failed: ${created.error} \u2014 continuing without umbrella issue
5412
+ `
5413
+ );
5414
+ return;
5415
+ }
5416
+ process.stdout.write(`[goal-tick] opened umbrella issue #${created.value} for ${goal.id}
5417
+ `);
5418
+ goal.goalIssueNumber = created.value;
5419
+ };
5420
+
5421
+ // src/goal/state.ts
5422
+ import * as fs20 from "fs";
5423
+ import * as path20 from "path";
5424
+ var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5425
+ var GoalStateError = class extends Error {
5426
+ constructor(path29, message) {
5427
+ super(`Invalid goal state at ${path29}:
5428
+ ${message}`);
5429
+ this.path = path29;
5430
+ this.name = "GoalStateError";
5431
+ }
5432
+ path;
5433
+ };
5434
+ function parseGoalState(filePath, raw) {
5435
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5436
+ throw new GoalStateError(filePath, "must be a JSON object");
5437
+ }
5438
+ const r = raw;
5439
+ const stateValue = r.state;
5440
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5441
+ throw new GoalStateError(
5442
+ filePath,
5443
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5444
+ );
5445
+ }
5446
+ const parsed = {
5447
+ state: stateValue,
5448
+ extra: {}
5449
+ };
5450
+ if (typeof r.goalIssueNumber === "number" && Number.isFinite(r.goalIssueNumber)) {
5451
+ parsed.goalIssueNumber = r.goalIssueNumber;
5452
+ }
5453
+ if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5454
+ parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5455
+ }
5456
+ if (typeof r.goalPrUrl === "string" && r.goalPrUrl.length > 0) {
5457
+ parsed.goalPrUrl = r.goalPrUrl;
5458
+ }
5459
+ for (const ts of ["updatedAt", "createdAt", "startedAt", "completedAt"]) {
5460
+ const v = r[ts];
5461
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5462
+ }
5463
+ const known = /* @__PURE__ */ new Set([
5464
+ "state",
5465
+ "goalIssueNumber",
5466
+ "lastDispatchedIssue",
5467
+ "goalPrUrl",
5468
+ "updatedAt",
5469
+ "createdAt",
5470
+ "startedAt",
5471
+ "completedAt"
5472
+ ]);
5473
+ for (const [k, v] of Object.entries(r)) {
5474
+ if (!known.has(k)) parsed.extra[k] = v;
5475
+ }
5476
+ return parsed;
5477
+ }
5478
+ function serializeGoalState(s) {
5479
+ const obj = { ...s.extra, state: s.state };
5480
+ if (s.goalIssueNumber !== void 0) obj.goalIssueNumber = s.goalIssueNumber;
5481
+ if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5482
+ if (s.goalPrUrl !== void 0) obj.goalPrUrl = s.goalPrUrl;
5483
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5484
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5485
+ if (s.completedAt !== void 0) obj.completedAt = s.completedAt;
5486
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5487
+ return `${JSON.stringify(obj, null, 2)}
5488
+ `;
5489
+ }
5490
+ function goalStatePath(cwd, goalId) {
5491
+ return path20.join(cwd, ".kody", "goals", goalId, "state.json");
5492
+ }
5493
+ function readGoalState(cwd, goalId) {
5494
+ const file = goalStatePath(cwd, goalId);
5495
+ if (!fs20.existsSync(file)) {
5496
+ throw new GoalStateError(file, "file not found");
5497
+ }
5498
+ let raw;
5499
+ try {
5500
+ raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
5501
+ } catch (err) {
5502
+ throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
5503
+ }
5504
+ return parseGoalState(file, raw);
5505
+ }
5506
+ function writeGoalState(cwd, goalId, state) {
5507
+ const file = goalStatePath(cwd, goalId);
5508
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
5509
+ fs20.writeFileSync(file, serializeGoalState(state), "utf-8");
5510
+ }
5511
+ function nowIso() {
5512
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5513
+ }
5514
+
5515
+ // src/scripts/finalizeGoal.ts
5516
+ var finalizeGoal = async (ctx) => {
5517
+ const goal = ctx.data.goal;
5518
+ if (!goal) return;
5519
+ process.stdout.write(`[goal-tick] all task(s) closed \u2014 finalising goal ${goal.id}
5520
+ `);
5521
+ if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5522
+ process.stderr.write(`[goal-tick] goal branch ${goal.goalBranch} not found on origin \u2014 skipping final PR
5523
+ `);
5524
+ finishState(goal);
5525
+ return;
5526
+ }
5527
+ const title = `goal: ${goal.id}`;
5528
+ const closesLine = goal.goalIssueNumber ? `
5529
+
5530
+ Closes #${goal.goalIssueNumber}
5531
+ ` : "\n";
5532
+ const body = `Final integration PR for goal **${goal.id}**.
5533
+
5534
+ All task issues are closed and merged into \`${goal.goalBranch}\`. Ready for review.${closesLine}`;
5535
+ const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5536
+ if (existing.ok && existing.value && existing.value.length > 0) {
5537
+ const pr = existing.value[0];
5538
+ goal.goalPrUrl = pr.url;
5539
+ const edit = editPrBody(pr.number, body, ctx.cwd);
5540
+ if (!edit.ok) {
5541
+ process.stderr.write(`[goal-tick] finalizeGoal: editPrBody failed: ${edit.error}
5542
+ `);
5543
+ }
5544
+ if (pr.isDraft) {
5545
+ process.stdout.write(`[goal-tick] promoting draft goal PR #${pr.number} to ready-for-review
5546
+ `);
5547
+ const ready = markPrReady(pr.number, ctx.cwd);
5548
+ if (!ready.ok) {
5549
+ process.stderr.write(`[goal-tick] finalizeGoal: markPrReady failed: ${ready.error}
5550
+ `);
5551
+ }
5552
+ }
5553
+ } else {
5554
+ const created = createPr(
5555
+ {
5556
+ head: goal.goalBranch,
5557
+ base: goal.defaultBranch,
5558
+ title,
5559
+ body,
5560
+ // ready-for-review (not draft) since we're finalizing.
5561
+ draft: false
5562
+ },
5563
+ ctx.cwd
5564
+ );
5565
+ if (!created.ok) {
5566
+ process.stderr.write(`[goal-tick] finalizeGoal: gh pr create failed: ${created.error}
5567
+ `);
5568
+ } else {
5569
+ goal.goalPrUrl = created.value;
5570
+ }
5571
+ }
5572
+ finishState(goal);
5573
+ };
5574
+ function finishState(goal) {
5575
+ goal.state = "done";
5576
+ goal.completedAt = nowIso();
5577
+ }
5578
+
4866
5579
  // src/scripts/finishFlow.ts
4867
- import { execFileSync as execFileSync13 } from "child_process";
5580
+ import { execFileSync as execFileSync15 } from "child_process";
4868
5581
  var API_TIMEOUT_MS6 = 3e4;
4869
5582
  var STATUS_ICON = {
4870
5583
  "review-passed": "\u2705",
@@ -4898,7 +5611,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4898
5611
  **PR:** ${state.core.prUrl}` : "";
4899
5612
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
4900
5613
  try {
4901
- execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5614
+ execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
4902
5615
  timeout: API_TIMEOUT_MS6,
4903
5616
  cwd: ctx.cwd,
4904
5617
  stdio: ["ignore", "pipe", "pipe"]
@@ -4912,7 +5625,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
4912
5625
  };
4913
5626
 
4914
5627
  // src/branch.ts
4915
- import { execFileSync as execFileSync14 } from "child_process";
5628
+ import { execFileSync as execFileSync16 } from "child_process";
4916
5629
  var UncommittedChangesError = class extends Error {
4917
5630
  constructor(branch) {
4918
5631
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -4922,7 +5635,7 @@ var UncommittedChangesError = class extends Error {
4922
5635
  branch;
4923
5636
  };
4924
5637
  function git2(args, cwd) {
4925
- return execFileSync14("git", args, {
5638
+ return execFileSync16("git", args, {
4926
5639
  encoding: "utf-8",
4927
5640
  timeout: 3e4,
4928
5641
  cwd,
@@ -4947,7 +5660,7 @@ function checkoutPrBranch(prNumber, cwd) {
4947
5660
  SKIP_HOOKS: "1",
4948
5661
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
4949
5662
  };
4950
- execFileSync14("gh", ["pr", "checkout", String(prNumber)], {
5663
+ execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
4951
5664
  cwd,
4952
5665
  env,
4953
5666
  stdio: ["ignore", "pipe", "pipe"],
@@ -5061,8 +5774,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5061
5774
  }
5062
5775
 
5063
5776
  // src/gha.ts
5064
- import { execFileSync as execFileSync15 } from "child_process";
5065
- import * as fs20 from "fs";
5777
+ import { execFileSync as execFileSync17 } from "child_process";
5778
+ import * as fs21 from "fs";
5066
5779
  function getRunUrl() {
5067
5780
  const server = process.env.GITHUB_SERVER_URL;
5068
5781
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5073,10 +5786,10 @@ function getRunUrl() {
5073
5786
  function reactToTriggerComment(cwd) {
5074
5787
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5075
5788
  const eventPath = process.env.GITHUB_EVENT_PATH;
5076
- if (!eventPath || !fs20.existsSync(eventPath)) return;
5789
+ if (!eventPath || !fs21.existsSync(eventPath)) return;
5077
5790
  let event = null;
5078
5791
  try {
5079
- event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5792
+ event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5080
5793
  } catch {
5081
5794
  return;
5082
5795
  }
@@ -5104,7 +5817,7 @@ function reactToTriggerComment(cwd) {
5104
5817
  for (let attempt = 0; attempt < 3; attempt++) {
5105
5818
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5106
5819
  try {
5107
- execFileSync15("gh", args, opts);
5820
+ execFileSync17("gh", args, opts);
5108
5821
  return;
5109
5822
  } catch (err) {
5110
5823
  lastErr = err;
@@ -5117,13 +5830,13 @@ function reactToTriggerComment(cwd) {
5117
5830
  }
5118
5831
  function sleepMs(ms) {
5119
5832
  try {
5120
- execFileSync15("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5833
+ execFileSync17("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5121
5834
  } catch {
5122
5835
  }
5123
5836
  }
5124
5837
 
5125
5838
  // src/workflow.ts
5126
- import { execFileSync as execFileSync16 } from "child_process";
5839
+ import { execFileSync as execFileSync18 } from "child_process";
5127
5840
  var GH_TIMEOUT_MS = 3e4;
5128
5841
  function ghToken3() {
5129
5842
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -5131,7 +5844,7 @@ function ghToken3() {
5131
5844
  function gh3(args, cwd) {
5132
5845
  const token = ghToken3();
5133
5846
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
5134
- return execFileSync16("gh", args, {
5847
+ return execFileSync18("gh", args, {
5135
5848
  encoding: "utf-8",
5136
5849
  timeout: GH_TIMEOUT_MS,
5137
5850
  cwd,
@@ -5314,24 +6027,63 @@ function tryPostPr2(prNumber, body, cwd) {
5314
6027
  }
5315
6028
  }
5316
6029
 
6030
+ // src/scripts/handleAbandonedGoal.ts
6031
+ var handleAbandonedGoal = async (ctx) => {
6032
+ const goal = ctx.data.goal;
6033
+ if (!goal || goal.state !== "abandoned") return;
6034
+ process.stdout.write(`[goal-tick] ${goal.id} is abandoned \u2014 running cleanup
6035
+ `);
6036
+ const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
6037
+ if (!issues.ok) {
6038
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: list failed: ${issues.error}
6039
+ `);
6040
+ } else {
6041
+ for (const i of issues.value ?? []) {
6042
+ if (i.state !== "OPEN") continue;
6043
+ const r = closeIssue(
6044
+ i.number,
6045
+ {
6046
+ comment: "_Goal abandoned \u2014 closing this task without dispatch._",
6047
+ reason: "not planned"
6048
+ },
6049
+ ctx.cwd
6050
+ );
6051
+ if (!r.ok) {
6052
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close #${i.number}: ${r.error}
6053
+ `);
6054
+ }
6055
+ }
6056
+ }
6057
+ const goalPrs = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
6058
+ if (goalPrs.ok && goalPrs.value && goalPrs.value.length > 0) {
6059
+ const pr = goalPrs.value[0];
6060
+ const r = closePr(pr.number, "_Goal abandoned by operator \u2014 closing without merge._", ctx.cwd);
6061
+ if (!r.ok) {
6062
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close goal PR #${pr.number}: ${r.error}
6063
+ `);
6064
+ }
6065
+ }
6066
+ goal.state = "closed";
6067
+ };
6068
+
5317
6069
  // src/scripts/initFlow.ts
5318
- import { execFileSync as execFileSync17 } from "child_process";
5319
- import * as fs22 from "fs";
5320
- import * as path20 from "path";
6070
+ import { execFileSync as execFileSync19 } from "child_process";
6071
+ import * as fs23 from "fs";
6072
+ import * as path22 from "path";
5321
6073
 
5322
6074
  // src/scripts/loadQaGuide.ts
5323
- import * as fs21 from "fs";
5324
- import * as path19 from "path";
6075
+ import * as fs22 from "fs";
6076
+ import * as path21 from "path";
5325
6077
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
5326
6078
  var loadQaGuide = async (ctx) => {
5327
- const full = path19.join(ctx.cwd, QA_GUIDE_REL_PATH);
5328
- if (!fs21.existsSync(full)) {
6079
+ const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
6080
+ if (!fs22.existsSync(full)) {
5329
6081
  ctx.data.qaGuide = "";
5330
6082
  ctx.data.qaGuidePath = "";
5331
6083
  return;
5332
6084
  }
5333
6085
  try {
5334
- ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
6086
+ ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
5335
6087
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
5336
6088
  } catch {
5337
6089
  ctx.data.qaGuide = "";
@@ -5341,9 +6093,9 @@ var loadQaGuide = async (ctx) => {
5341
6093
 
5342
6094
  // src/scripts/initFlow.ts
5343
6095
  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";
6096
+ if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6097
+ if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
6098
+ if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
5347
6099
  return "npm";
5348
6100
  }
5349
6101
  function qualityCommandsFor(pm) {
@@ -5356,7 +6108,7 @@ function qualityCommandsFor(pm) {
5356
6108
  function detectOwnerRepo(cwd) {
5357
6109
  let url;
5358
6110
  try {
5359
- url = execFileSync17("git", ["remote", "get-url", "origin"], {
6111
+ url = execFileSync19("git", ["remote", "get-url", "origin"], {
5360
6112
  cwd,
5361
6113
  encoding: "utf-8",
5362
6114
  stdio: ["ignore", "pipe", "pipe"]
@@ -5441,7 +6193,7 @@ jobs:
5441
6193
  `;
5442
6194
  function defaultBranchFromGit(cwd) {
5443
6195
  try {
5444
- const ref = execFileSync17("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
6196
+ const ref = execFileSync19("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5445
6197
  cwd,
5446
6198
  encoding: "utf-8",
5447
6199
  stdio: ["ignore", "pipe", "pipe"]
@@ -5449,7 +6201,7 @@ function defaultBranchFromGit(cwd) {
5449
6201
  return ref.replace("refs/remotes/origin/", "");
5450
6202
  } catch {
5451
6203
  try {
5452
- return execFileSync17("git", ["branch", "--show-current"], {
6204
+ return execFileSync19("git", ["branch", "--show-current"], {
5453
6205
  cwd,
5454
6206
  encoding: "utf-8",
5455
6207
  stdio: ["ignore", "pipe", "pipe"]
@@ -5465,48 +6217,48 @@ function performInit(cwd, force) {
5465
6217
  const pm = detectPackageManager(cwd);
5466
6218
  const ownerRepo = detectOwnerRepo(cwd);
5467
6219
  const defaultBranch = defaultBranchFromGit(cwd);
5468
- const configPath = path20.join(cwd, "kody.config.json");
5469
- if (fs22.existsSync(configPath) && !force) {
6220
+ const configPath = path22.join(cwd, "kody.config.json");
6221
+ if (fs23.existsSync(configPath) && !force) {
5470
6222
  skipped.push("kody.config.json");
5471
6223
  } else {
5472
6224
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
5473
- fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6225
+ fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5474
6226
  `);
5475
6227
  wrote.push("kody.config.json");
5476
6228
  }
5477
- const workflowDir = path20.join(cwd, ".github", "workflows");
5478
- const workflowPath = path20.join(workflowDir, "kody.yml");
5479
- if (fs22.existsSync(workflowPath) && !force) {
6229
+ const workflowDir = path22.join(cwd, ".github", "workflows");
6230
+ const workflowPath = path22.join(workflowDir, "kody.yml");
6231
+ if (fs23.existsSync(workflowPath) && !force) {
5480
6232
  skipped.push(".github/workflows/kody.yml");
5481
6233
  } else {
5482
- fs22.mkdirSync(workflowDir, { recursive: true });
5483
- fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6234
+ fs23.mkdirSync(workflowDir, { recursive: true });
6235
+ fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5484
6236
  wrote.push(".github/workflows/kody.yml");
5485
6237
  }
5486
- const hasUi = fs22.existsSync(path20.join(cwd, "src/app")) || fs22.existsSync(path20.join(cwd, "app")) || fs22.existsSync(path20.join(cwd, "pages"));
6238
+ const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
5487
6239
  if (hasUi) {
5488
- const qaGuidePath = path20.join(cwd, QA_GUIDE_REL_PATH);
5489
- if (fs22.existsSync(qaGuidePath) && !force) {
6240
+ const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6241
+ if (fs23.existsSync(qaGuidePath) && !force) {
5490
6242
  skipped.push(QA_GUIDE_REL_PATH);
5491
6243
  } else {
5492
- fs22.mkdirSync(path20.dirname(qaGuidePath), { recursive: true });
6244
+ fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
5493
6245
  const discovery = runQaDiscovery(cwd);
5494
- fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6246
+ fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5495
6247
  wrote.push(QA_GUIDE_REL_PATH);
5496
6248
  }
5497
6249
  }
5498
6250
  const builtinJobs = listBuiltinJobs();
5499
6251
  if (builtinJobs.length > 0) {
5500
- const jobsDir = path20.join(cwd, ".kody", "jobs");
5501
- fs22.mkdirSync(jobsDir, { recursive: true });
6252
+ const jobsDir = path22.join(cwd, ".kody", "jobs");
6253
+ fs23.mkdirSync(jobsDir, { recursive: true });
5502
6254
  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) {
6255
+ const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6256
+ const target = path22.join(cwd, rel);
6257
+ if (fs23.existsSync(target) && !force) {
5506
6258
  skipped.push(rel);
5507
6259
  continue;
5508
6260
  }
5509
- fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
6261
+ fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
5510
6262
  wrote.push(rel);
5511
6263
  }
5512
6264
  }
@@ -5518,12 +6270,12 @@ function performInit(cwd, force) {
5518
6270
  continue;
5519
6271
  }
5520
6272
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
5521
- const target = path20.join(workflowDir, `kody-${exe.name}.yml`);
5522
- if (fs22.existsSync(target) && !force) {
6273
+ const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6274
+ if (fs23.existsSync(target) && !force) {
5523
6275
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
5524
6276
  continue;
5525
6277
  }
5526
- fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6278
+ fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5527
6279
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
5528
6280
  }
5529
6281
  let labels;
@@ -5611,6 +6363,48 @@ var loadCoverageRules = async (ctx) => {
5611
6363
  ctx.data.coverageRules = ctx.config.testRequirements ?? [];
5612
6364
  };
5613
6365
 
6366
+ // src/scripts/loadGoalState.ts
6367
+ var loadGoalState = async (ctx) => {
6368
+ const goalId = ctx.args.goal;
6369
+ if (typeof goalId !== "string" || goalId.length === 0) {
6370
+ ctx.skipAgent = true;
6371
+ ctx.output.exitCode = 1;
6372
+ ctx.output.reason = "missing --goal";
6373
+ return;
6374
+ }
6375
+ if (goalId.includes("/") || goalId.includes("..")) {
6376
+ ctx.skipAgent = true;
6377
+ ctx.output.exitCode = 1;
6378
+ ctx.output.reason = "invalid goal id (no slashes or '..' allowed)";
6379
+ return;
6380
+ }
6381
+ try {
6382
+ const state = readGoalState(ctx.cwd, goalId);
6383
+ ctx.data.goal = {
6384
+ id: goalId,
6385
+ state: state.state,
6386
+ goalIssueNumber: state.goalIssueNumber,
6387
+ lastDispatchedIssue: state.lastDispatchedIssue,
6388
+ goalPrUrl: state.goalPrUrl,
6389
+ // Cache the full parsed object so saveGoalState can preserve `extra`.
6390
+ raw: state,
6391
+ // `phase` is populated by deriveGoalPhase later in the chain. Initialize
6392
+ // to undefined so runWhen on `data.goal.phase` can match correctly.
6393
+ phase: void 0,
6394
+ // Populated by ensureGoalBranch / configured by config.git.defaultBranch.
6395
+ defaultBranch: ctx.config.git.defaultBranch,
6396
+ // Convenience derivations.
6397
+ goalBranch: `goal-${goalId}`
6398
+ };
6399
+ } catch (err) {
6400
+ process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
6401
+ `);
6402
+ ctx.skipAgent = true;
6403
+ ctx.output.exitCode = 0;
6404
+ ctx.output.reason = "no goal state to tick";
6405
+ }
6406
+ };
6407
+
5614
6408
  // src/scripts/loadIssueContext.ts
5615
6409
  var DEFAULT_COMMENT_LIMIT = 12;
5616
6410
  var DEFAULT_COMMENT_MAX_BYTES = 16e3;
@@ -5661,8 +6455,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
5661
6455
  };
5662
6456
 
5663
6457
  // src/scripts/loadJobFromFile.ts
5664
- import * as fs23 from "fs";
5665
- import * as path21 from "path";
6458
+ import * as fs24 from "fs";
6459
+ import * as path23 from "path";
5666
6460
  var loadJobFromFile = async (ctx, _profile, args) => {
5667
6461
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
5668
6462
  const slugArg = String(args?.slugArg ?? "job");
@@ -5670,11 +6464,11 @@ var loadJobFromFile = async (ctx, _profile, args) => {
5670
6464
  if (!slug) {
5671
6465
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
5672
6466
  }
5673
- const absPath = path21.join(ctx.cwd, jobsDir, `${slug}.md`);
5674
- if (!fs23.existsSync(absPath)) {
6467
+ const absPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
6468
+ if (!fs24.existsSync(absPath)) {
5675
6469
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
5676
6470
  }
5677
- const raw = fs23.readFileSync(absPath, "utf-8");
6471
+ const raw = fs24.readFileSync(absPath, "utf-8");
5678
6472
  const { title, body } = parseJobFile(raw, slug);
5679
6473
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
5680
6474
  const loaded = await backend.load(slug);
@@ -5706,16 +6500,16 @@ function humanizeSlug(slug) {
5706
6500
  }
5707
6501
 
5708
6502
  // src/scripts/loadMemoryContext.ts
5709
- import * as fs24 from "fs";
5710
- import * as path22 from "path";
6503
+ import * as fs25 from "fs";
6504
+ import * as path24 from "path";
5711
6505
  var MEMORY_DIR_RELATIVE = ".kody/memory";
5712
6506
  var MAX_PAGES = 8;
5713
6507
  var PER_PAGE_MAX_BYTES = 4e3;
5714
6508
  var TOTAL_MAX_BYTES = 24e3;
5715
6509
  var TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
5716
6510
  var loadMemoryContext = async (ctx) => {
5717
- const memoryAbs = path22.join(ctx.cwd, MEMORY_DIR_RELATIVE);
5718
- if (!fs24.existsSync(memoryAbs)) {
6511
+ const memoryAbs = path24.join(ctx.cwd, MEMORY_DIR_RELATIVE);
6512
+ if (!fs25.existsSync(memoryAbs)) {
5719
6513
  ctx.data.memoryContext = "";
5720
6514
  return;
5721
6515
  }
@@ -5740,21 +6534,21 @@ function collectPages(memoryAbs) {
5740
6534
  walkMd(memoryAbs, (file) => {
5741
6535
  let stat;
5742
6536
  try {
5743
- stat = fs24.statSync(file);
6537
+ stat = fs25.statSync(file);
5744
6538
  } catch {
5745
6539
  return;
5746
6540
  }
5747
6541
  let raw;
5748
6542
  try {
5749
- raw = fs24.readFileSync(file, "utf-8");
6543
+ raw = fs25.readFileSync(file, "utf-8");
5750
6544
  } catch {
5751
6545
  return;
5752
6546
  }
5753
6547
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
5754
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path22.basename(file, ".md");
6548
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path24.basename(file, ".md");
5755
6549
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
5756
6550
  out.push({
5757
- relPath: path22.relative(memoryAbs, file),
6551
+ relPath: path24.relative(memoryAbs, file),
5758
6552
  title,
5759
6553
  updated,
5760
6554
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -5822,16 +6616,16 @@ function walkMd(root, visit) {
5822
6616
  const dir = stack.pop();
5823
6617
  let names;
5824
6618
  try {
5825
- names = fs24.readdirSync(dir);
6619
+ names = fs25.readdirSync(dir);
5826
6620
  } catch {
5827
6621
  continue;
5828
6622
  }
5829
6623
  for (const name of names) {
5830
6624
  if (name.startsWith(".")) continue;
5831
- const full = path22.join(dir, name);
6625
+ const full = path24.join(dir, name);
5832
6626
  let stat;
5833
6627
  try {
5834
- stat = fs24.statSync(full);
6628
+ stat = fs25.statSync(full);
5835
6629
  } catch {
5836
6630
  continue;
5837
6631
  }
@@ -5954,8 +6748,32 @@ var markFlowSuccess = async (ctx) => {
5954
6748
  }
5955
6749
  };
5956
6750
 
6751
+ // src/scripts/mergeReadyTaskPRs.ts
6752
+ var mergeReadyTaskPRs = async (ctx) => {
6753
+ const goal = ctx.data.goal;
6754
+ if (!goal) return;
6755
+ const open = listPrsByBase(goal.goalBranch, "open", ctx.cwd);
6756
+ if (!open.ok) {
6757
+ process.stderr.write(`[goal-tick] mergeReadyTaskPRs: list failed: ${open.error}
6758
+ `);
6759
+ return;
6760
+ }
6761
+ for (const pr of open.value ?? []) {
6762
+ if (pr.isDraft) continue;
6763
+ if (pr.mergeable !== "MERGEABLE") continue;
6764
+ if (pr.mergeStateStatus !== "CLEAN") continue;
6765
+ process.stdout.write(`[goal-tick] merging PR #${pr.number} into ${goal.goalBranch}
6766
+ `);
6767
+ const r = mergePrSquash(pr.number, ctx.cwd);
6768
+ if (!r.ok) {
6769
+ process.stderr.write(`[goal-tick] failed to merge PR #${pr.number}: ${r.error} (continuing)
6770
+ `);
6771
+ }
6772
+ }
6773
+ };
6774
+
5957
6775
  // src/scripts/mergeReleasePr.ts
5958
- import { execFileSync as execFileSync18 } from "child_process";
6776
+ import { execFileSync as execFileSync20 } from "child_process";
5959
6777
  var API_TIMEOUT_MS7 = 6e4;
5960
6778
  var mergeReleasePr = async (ctx) => {
5961
6779
  const state = ctx.data.taskState;
@@ -5974,7 +6792,7 @@ var mergeReleasePr = async (ctx) => {
5974
6792
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
5975
6793
  `);
5976
6794
  try {
5977
- const out = execFileSync18("gh", ["pr", "merge", String(prNumber), "--merge"], {
6795
+ const out = execFileSync20("gh", ["pr", "merge", String(prNumber), "--merge"], {
5978
6796
  timeout: API_TIMEOUT_MS7,
5979
6797
  cwd: ctx.cwd,
5980
6798
  stdio: ["ignore", "pipe", "pipe"]
@@ -6092,7 +6910,7 @@ function buildIssueTitle(scope, verdict) {
6092
6910
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
6093
6911
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
6094
6912
  }
6095
- function ensureLabel2(cwd) {
6913
+ function ensureLabel3(cwd) {
6096
6914
  try {
6097
6915
  gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
6098
6916
  return true;
@@ -6152,7 +6970,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
6152
6970
  }
6153
6971
  const scope = ctx.args.scope;
6154
6972
  const title = buildIssueTitle(scope, verdict);
6155
- const hasLabel = ensureLabel2(ctx.cwd);
6973
+ const hasLabel = ensureLabel3(ctx.cwd);
6156
6974
  let created;
6157
6975
  try {
6158
6976
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -6557,7 +7375,7 @@ ${body}`;
6557
7375
  }
6558
7376
 
6559
7377
  // src/scripts/recordClassification.ts
6560
- import { execFileSync as execFileSync19 } from "child_process";
7378
+ import { execFileSync as execFileSync21 } from "child_process";
6561
7379
  var API_TIMEOUT_MS8 = 3e4;
6562
7380
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
6563
7381
  var recordClassification = async (ctx) => {
@@ -6605,7 +7423,7 @@ function parseClassification(prSummary) {
6605
7423
  }
6606
7424
  function tryAuditComment(issueNumber, body, cwd) {
6607
7425
  try {
6608
- execFileSync19("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7426
+ execFileSync21("gh", ["issue", "comment", String(issueNumber), "--body", body], {
6609
7427
  cwd,
6610
7428
  timeout: API_TIMEOUT_MS8,
6611
7429
  stdio: ["ignore", "pipe", "pipe"]
@@ -6644,14 +7462,14 @@ var requireFeedbackActions = async (ctx, profile) => {
6644
7462
  const items = countActionItems(actions);
6645
7463
  ctx.data.feedbackAgentItemCount = items;
6646
7464
  if (items < MIN_ITEMS) {
6647
- fail(
7465
+ fail2(
6648
7466
  ctx,
6649
7467
  profile,
6650
7468
  actions.length === 0 ? "agent omitted required FEEDBACK_ACTIONS block \u2014 cannot verify that review feedback was addressed" : "agent FEEDBACK_ACTIONS block listed no items \u2014 cannot verify that review feedback was addressed"
6651
7469
  );
6652
7470
  }
6653
7471
  };
6654
- function fail(ctx, profile, reason) {
7472
+ function fail2(ctx, profile, reason) {
6655
7473
  ctx.data.agentDone = false;
6656
7474
  ctx.data.agentFailureReason = reason;
6657
7475
  const modeSeg = profile.name.replace(/-/g, "_").toUpperCase();
@@ -6728,7 +7546,7 @@ var resolveArtifacts = async (ctx, profile) => {
6728
7546
  };
6729
7547
 
6730
7548
  // src/scripts/resolveFlow.ts
6731
- import { execFileSync as execFileSync20 } from "child_process";
7549
+ import { execFileSync as execFileSync22 } from "child_process";
6732
7550
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
6733
7551
  var resolveFlow = async (ctx) => {
6734
7552
  const prNumber = ctx.args.pr;
@@ -6798,7 +7616,7 @@ function buildPreferBlock(prefer, baseBranch) {
6798
7616
  }
6799
7617
  function getConflictedFiles(cwd) {
6800
7618
  try {
6801
- const out = execFileSync20("git", ["diff", "--name-only", "--diff-filter=U"], {
7619
+ const out = execFileSync22("git", ["diff", "--name-only", "--diff-filter=U"], {
6802
7620
  encoding: "utf-8",
6803
7621
  cwd,
6804
7622
  env: { ...process.env, HUSKY: "0" }
@@ -6813,7 +7631,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
6813
7631
  let total = 0;
6814
7632
  for (const f of files) {
6815
7633
  try {
6816
- const content = execFileSync20("cat", [f], { encoding: "utf-8", cwd }).toString();
7634
+ const content = execFileSync22("cat", [f], { encoding: "utf-8", cwd }).toString();
6817
7635
  const snippet = `### ${f}
6818
7636
 
6819
7637
  \`\`\`
@@ -6914,10 +7732,10 @@ var resolvePreviewUrl = async (ctx) => {
6914
7732
  };
6915
7733
 
6916
7734
  // src/scripts/resolveQaUrl.ts
6917
- import { execFileSync as execFileSync21 } from "child_process";
7735
+ import { execFileSync as execFileSync23 } from "child_process";
6918
7736
  function ghQuery(args, cwd) {
6919
7737
  try {
6920
- const out = execFileSync21("gh", args, {
7738
+ const out = execFileSync23("gh", args, {
6921
7739
  cwd,
6922
7740
  stdio: ["ignore", "pipe", "pipe"],
6923
7741
  encoding: "utf-8",
@@ -6987,7 +7805,7 @@ var resolveQaUrl = async (ctx) => {
6987
7805
  };
6988
7806
 
6989
7807
  // src/scripts/revertFlow.ts
6990
- import { execFileSync as execFileSync22 } from "child_process";
7808
+ import { execFileSync as execFileSync24 } from "child_process";
6991
7809
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
6992
7810
  var revertFlow = async (ctx) => {
6993
7811
  const prNumber = ctx.args.pr;
@@ -7069,7 +7887,7 @@ function buildPrSummary(resolved) {
7069
7887
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
7070
7888
  }
7071
7889
  function git3(args, cwd) {
7072
- return execFileSync22("git", args, {
7890
+ return execFileSync24("git", args, {
7073
7891
  encoding: "utf-8",
7074
7892
  timeout: 3e4,
7075
7893
  cwd,
@@ -7079,7 +7897,7 @@ function git3(args, cwd) {
7079
7897
  }
7080
7898
  function isAncestorOfHead(sha, cwd) {
7081
7899
  try {
7082
- execFileSync22("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7900
+ execFileSync24("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7083
7901
  cwd,
7084
7902
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7085
7903
  stdio: ["ignore", "ignore", "ignore"]
@@ -7175,17 +7993,17 @@ function resolveBaseOverride(value) {
7175
7993
  }
7176
7994
  function resolveBaseFromLabels(labels) {
7177
7995
  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);
7996
+ const goalLabel2 = labels.find((l) => l.startsWith("goal:"));
7997
+ if (!goalLabel2) return null;
7998
+ const goalId = goalLabel2.slice("goal:".length);
7181
7999
  if (!/^[a-z0-9-]+$/.test(goalId)) return null;
7182
8000
  return `goal-${goalId}`;
7183
8001
  }
7184
8002
 
7185
8003
  // src/scripts/runTickScript.ts
7186
8004
  import { spawnSync } from "child_process";
7187
- import * as fs25 from "fs";
7188
- import * as path23 from "path";
8005
+ import * as fs26 from "fs";
8006
+ import * as path25 from "path";
7189
8007
  var runTickScript = async (ctx, _profile, args) => {
7190
8008
  ctx.skipAgent = true;
7191
8009
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -7197,13 +8015,13 @@ var runTickScript = async (ctx, _profile, args) => {
7197
8015
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
7198
8016
  return;
7199
8017
  }
7200
- const jobPath = path23.join(ctx.cwd, jobsDir, `${slug}.md`);
7201
- if (!fs25.existsSync(jobPath)) {
8018
+ const jobPath = path25.join(ctx.cwd, jobsDir, `${slug}.md`);
8019
+ if (!fs26.existsSync(jobPath)) {
7202
8020
  ctx.output.exitCode = 99;
7203
8021
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
7204
8022
  return;
7205
8023
  }
7206
- const raw = fs25.readFileSync(jobPath, "utf-8");
8024
+ const raw = fs26.readFileSync(jobPath, "utf-8");
7207
8025
  const { frontmatter } = splitFrontmatter(raw);
7208
8026
  const tickScript = frontmatter.tickScript;
7209
8027
  if (!tickScript) {
@@ -7211,8 +8029,8 @@ var runTickScript = async (ctx, _profile, args) => {
7211
8029
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
7212
8030
  return;
7213
8031
  }
7214
- const scriptPath = path23.isAbsolute(tickScript) ? tickScript : path23.join(ctx.cwd, tickScript);
7215
- if (!fs25.existsSync(scriptPath)) {
8032
+ const scriptPath = path25.isAbsolute(tickScript) ? tickScript : path25.join(ctx.cwd, tickScript);
8033
+ if (!fs26.existsSync(scriptPath)) {
7216
8034
  ctx.output.exitCode = 99;
7217
8035
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
7218
8036
  return;
@@ -7313,6 +8131,26 @@ function buildChildEnv(parent, force) {
7313
8131
  return out;
7314
8132
  }
7315
8133
 
8134
+ // src/scripts/saveGoalState.ts
8135
+ var saveGoalState = async (ctx) => {
8136
+ const goal = ctx.data.goal;
8137
+ if (!goal) {
8138
+ ctx.skipAgent = true;
8139
+ return;
8140
+ }
8141
+ const updated = {
8142
+ ...goal.raw ?? { state: goal.state, extra: {} },
8143
+ state: goal.state,
8144
+ goalIssueNumber: goal.goalIssueNumber,
8145
+ lastDispatchedIssue: goal.lastDispatchedIssue,
8146
+ goalPrUrl: goal.goalPrUrl,
8147
+ completedAt: goal.completedAt ?? goal.raw?.completedAt,
8148
+ updatedAt: nowIso()
8149
+ };
8150
+ writeGoalState(ctx.cwd, goal.id, updated);
8151
+ ctx.skipAgent = true;
8152
+ };
8153
+
7316
8154
  // src/scripts/saveTaskState.ts
7317
8155
  var saveTaskState = async (ctx, profile) => {
7318
8156
  const target = ctx.data.commentTargetType;
@@ -7388,11 +8226,11 @@ var skipAgent = async (ctx) => {
7388
8226
  };
7389
8227
 
7390
8228
  // src/scripts/stageMergeConflicts.ts
7391
- import { execFileSync as execFileSync23 } from "child_process";
8229
+ import { execFileSync as execFileSync25 } from "child_process";
7392
8230
  var stageMergeConflicts = async (ctx) => {
7393
8231
  if (ctx.data.agentDone === false) return;
7394
8232
  try {
7395
- execFileSync23("git", ["add", "-A"], {
8233
+ execFileSync25("git", ["add", "-A"], {
7396
8234
  cwd: ctx.cwd,
7397
8235
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
7398
8236
  stdio: "pipe"
@@ -7402,7 +8240,7 @@ var stageMergeConflicts = async (ctx) => {
7402
8240
  };
7403
8241
 
7404
8242
  // src/scripts/startFlow.ts
7405
- import { execFileSync as execFileSync24 } from "child_process";
8243
+ import { execFileSync as execFileSync26 } from "child_process";
7406
8244
  var API_TIMEOUT_MS9 = 3e4;
7407
8245
  var startFlow = async (ctx, profile, _agentResult, args) => {
7408
8246
  const entry = args?.entry;
@@ -7436,7 +8274,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7436
8274
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
7437
8275
  const body = `@kody ${next}`;
7438
8276
  try {
7439
- execFileSync24("gh", [sub, "comment", String(targetNumber), "--body", body], {
8277
+ execFileSync26("gh", [sub, "comment", String(targetNumber), "--body", body], {
7440
8278
  timeout: API_TIMEOUT_MS9,
7441
8279
  cwd,
7442
8280
  stdio: ["ignore", "pipe", "pipe"]
@@ -7450,7 +8288,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
7450
8288
  }
7451
8289
 
7452
8290
  // src/scripts/syncFlow.ts
7453
- import { execFileSync as execFileSync25 } from "child_process";
8291
+ import { execFileSync as execFileSync27 } from "child_process";
7454
8292
  var syncFlow = async (ctx, _profile, args) => {
7455
8293
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
7456
8294
  const prNumber = ctx.args.pr;
@@ -7522,7 +8360,7 @@ function bail2(ctx, prNumber, reason) {
7522
8360
  }
7523
8361
  function revParseHead(cwd) {
7524
8362
  try {
7525
- return execFileSync25("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
8363
+ return execFileSync27("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
7526
8364
  } catch {
7527
8365
  return "";
7528
8366
  }
@@ -7530,9 +8368,9 @@ function revParseHead(cwd) {
7530
8368
  function pushBranch(branch, cwd) {
7531
8369
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
7532
8370
  try {
7533
- execFileSync25("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
8371
+ execFileSync27("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
7534
8372
  } catch {
7535
- execFileSync25("git", ["push", "--force-with-lease", "-u", "origin", branch], {
8373
+ execFileSync27("git", ["push", "--force-with-lease", "-u", "origin", branch], {
7536
8374
  cwd,
7537
8375
  env,
7538
8376
  stdio: ["ignore", "pipe", "pipe"]
@@ -7759,7 +8597,7 @@ function downgrade2(ctx, reason) {
7759
8597
  }
7760
8598
 
7761
8599
  // src/scripts/waitForCi.ts
7762
- import { execFileSync as execFileSync26 } from "child_process";
8600
+ import { execFileSync as execFileSync28 } from "child_process";
7763
8601
  var API_TIMEOUT_MS10 = 3e4;
7764
8602
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
7765
8603
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -7837,7 +8675,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
7837
8675
  };
7838
8676
  function fetchChecks(prNumber, cwd) {
7839
8677
  try {
7840
- const raw = execFileSync26("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
8678
+ const raw = execFileSync28("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
7841
8679
  encoding: "utf-8",
7842
8680
  timeout: API_TIMEOUT_MS10,
7843
8681
  cwd,
@@ -8095,7 +8933,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
8095
8933
  };
8096
8934
 
8097
8935
  // src/scripts/writeRunSummary.ts
8098
- import * as fs26 from "fs";
8936
+ import * as fs27 from "fs";
8099
8937
  var writeRunSummary = async (ctx, profile) => {
8100
8938
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
8101
8939
  if (!summaryPath) return;
@@ -8117,7 +8955,7 @@ var writeRunSummary = async (ctx, profile) => {
8117
8955
  if (reason) lines.push(`- **Reason:** ${reason}`);
8118
8956
  lines.push("");
8119
8957
  try {
8120
- fs26.appendFileSync(summaryPath, `${lines.join("\n")}
8958
+ fs27.appendFileSync(summaryPath, `${lines.join("\n")}
8121
8959
  `);
8122
8960
  } catch {
8123
8961
  }
@@ -8156,7 +8994,19 @@ var preflightScripts = {
8156
8994
  warmupMcp,
8157
8995
  dispatchJobTicks,
8158
8996
  dispatchJobFileTicks,
8159
- runTickScript
8997
+ runTickScript,
8998
+ loadGoalState,
8999
+ handleAbandonedGoal,
9000
+ ensureLifecycleLabels,
9001
+ ensureUmbrellaIssue,
9002
+ ensureGoalPr,
9003
+ mergeReadyTaskPRs,
9004
+ closeMergedTaskIssues,
9005
+ deriveGoalPhase,
9006
+ ensureGoalBranch,
9007
+ dispatchNextTask,
9008
+ finalizeGoal,
9009
+ saveGoalState
8160
9010
  };
8161
9011
  var postflightScripts = {
8162
9012
  parseAgentResult: parseAgentResult2,
@@ -8195,7 +9045,8 @@ var postflightScripts = {
8195
9045
  recordOutcome,
8196
9046
  mergeReleasePr,
8197
9047
  waitForCi,
8198
- markFlowSuccess
9048
+ markFlowSuccess,
9049
+ commitGoalState
8199
9050
  };
8200
9051
  var allScriptNames = /* @__PURE__ */ new Set([
8201
9052
  ...Object.keys(preflightScripts),
@@ -8203,7 +9054,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
8203
9054
  ]);
8204
9055
 
8205
9056
  // src/tools.ts
8206
- import { execFileSync as execFileSync27 } from "child_process";
9057
+ import { execFileSync as execFileSync29 } from "child_process";
8207
9058
  function verifyCliTools(tools, cwd) {
8208
9059
  const out = [];
8209
9060
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -8236,7 +9087,7 @@ function verifyOne(tool, cwd) {
8236
9087
  }
8237
9088
  function runShell(cmd, cwd, timeoutMs = 3e4) {
8238
9089
  try {
8239
- execFileSync27("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
9090
+ execFileSync29("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8240
9091
  return true;
8241
9092
  } catch {
8242
9093
  return false;
@@ -8305,9 +9156,9 @@ async function runExecutable(profileName, input) {
8305
9156
  data: {},
8306
9157
  output: { exitCode: 0 }
8307
9158
  };
8308
- const ndjsonDir = path24.join(input.cwd, ".kody");
9159
+ const ndjsonDir = path26.join(input.cwd, ".kody");
8309
9160
  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);
9161
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path26.isAbsolute(p) ? p : path26.resolve(profile.dir, p)).filter((p) => p.length > 0);
8311
9162
  const syntheticPath = ctx.data.syntheticPluginPath;
8312
9163
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
8313
9164
  return runAgent({
@@ -8416,17 +9267,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
8416
9267
  function resolveProfilePath(profileName) {
8417
9268
  const found = resolveExecutable(profileName);
8418
9269
  if (found) return found;
8419
- const here = path24.dirname(new URL(import.meta.url).pathname);
9270
+ const here = path26.dirname(new URL(import.meta.url).pathname);
8420
9271
  const candidates = [
8421
- path24.join(here, "executables", profileName, "profile.json"),
9272
+ path26.join(here, "executables", profileName, "profile.json"),
8422
9273
  // same-dir sibling (dev)
8423
- path24.join(here, "..", "executables", profileName, "profile.json"),
9274
+ path26.join(here, "..", "executables", profileName, "profile.json"),
8424
9275
  // up one (prod: dist/bin → dist/executables)
8425
- path24.join(here, "..", "src", "executables", profileName, "profile.json")
9276
+ path26.join(here, "..", "src", "executables", profileName, "profile.json")
8426
9277
  // fallback
8427
9278
  ];
8428
9279
  for (const c of candidates) {
8429
- if (fs27.existsSync(c)) return c;
9280
+ if (fs28.existsSync(c)) return c;
8430
9281
  }
8431
9282
  return candidates[0];
8432
9283
  }
@@ -8530,8 +9381,8 @@ function resolveShellTimeoutMs(entry) {
8530
9381
  var SIGKILL_GRACE_MS = 5e3;
8531
9382
  async function runShellEntry(entry, ctx, profile) {
8532
9383
  const shellName = entry.shell;
8533
- const shellPath = path24.join(profile.dir, shellName);
8534
- if (!fs27.existsSync(shellPath)) {
9384
+ const shellPath = path26.join(profile.dir, shellName);
9385
+ if (!fs28.existsSync(shellPath)) {
8535
9386
  ctx.skipAgent = true;
8536
9387
  ctx.output.exitCode = 99;
8537
9388
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -8792,7 +9643,7 @@ async function runContainerLoop(profile, ctx, input) {
8792
9643
  }
8793
9644
  function resetWorkingTree(cwd) {
8794
9645
  try {
8795
- execFileSync28("git", ["reset", "--hard", "HEAD"], {
9646
+ execFileSync30("git", ["reset", "--hard", "HEAD"], {
8796
9647
  cwd,
8797
9648
  stdio: ["ignore", "pipe", "pipe"],
8798
9649
  timeout: 3e4
@@ -8944,14 +9795,14 @@ function resolveAuthToken(env = process.env) {
8944
9795
  return token;
8945
9796
  }
8946
9797
  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";
9798
+ if (fs29.existsSync(path27.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
9799
+ if (fs29.existsSync(path27.join(cwd, "yarn.lock"))) return "yarn";
9800
+ if (fs29.existsSync(path27.join(cwd, "bun.lockb"))) return "bun";
8950
9801
  return "npm";
8951
9802
  }
8952
9803
  function shellOut(cmd, args, cwd, stream = true) {
8953
9804
  try {
8954
- execFileSync29(cmd, args, {
9805
+ execFileSync31(cmd, args, {
8955
9806
  cwd,
8956
9807
  stdio: stream ? "inherit" : "pipe",
8957
9808
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -8964,7 +9815,7 @@ function shellOut(cmd, args, cwd, stream = true) {
8964
9815
  }
8965
9816
  function isOnPath(bin) {
8966
9817
  try {
8967
- execFileSync29("which", [bin], { stdio: "pipe" });
9818
+ execFileSync31("which", [bin], { stdio: "pipe" });
8968
9819
  return true;
8969
9820
  } catch {
8970
9821
  return false;
@@ -9005,7 +9856,7 @@ function installLitellmIfNeeded(cwd) {
9005
9856
  } catch {
9006
9857
  }
9007
9858
  try {
9008
- execFileSync29("python3", ["-c", "import litellm"], { stdio: "pipe" });
9859
+ execFileSync31("python3", ["-c", "import litellm"], { stdio: "pipe" });
9009
9860
  process.stdout.write("\u2192 kody: litellm already installed\n");
9010
9861
  return 0;
9011
9862
  } catch {
@@ -9015,16 +9866,16 @@ function installLitellmIfNeeded(cwd) {
9015
9866
  }
9016
9867
  function configureGitIdentity(cwd) {
9017
9868
  try {
9018
- const name = execFileSync29("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9869
+ const name = execFileSync31("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9019
9870
  if (name) return;
9020
9871
  } catch {
9021
9872
  }
9022
9873
  try {
9023
- execFileSync29("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9874
+ execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9024
9875
  } catch {
9025
9876
  }
9026
9877
  try {
9027
- execFileSync29("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9878
+ execFileSync31("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9028
9879
  cwd,
9029
9880
  stdio: "pipe"
9030
9881
  });
@@ -9033,11 +9884,11 @@ function configureGitIdentity(cwd) {
9033
9884
  }
9034
9885
  function postFailureTail(issueNumber, cwd, reason) {
9035
9886
  if (!issueNumber) return;
9036
- const logPath = path25.join(cwd, ".kody", "last-run.jsonl");
9887
+ const logPath = path27.join(cwd, ".kody", "last-run.jsonl");
9037
9888
  let tail = "";
9038
9889
  try {
9039
- if (fs28.existsSync(logPath)) {
9040
- const content = fs28.readFileSync(logPath, "utf-8");
9890
+ if (fs29.existsSync(logPath)) {
9891
+ const content = fs29.readFileSync(logPath, "utf-8");
9041
9892
  tail = content.slice(-3e3);
9042
9893
  }
9043
9894
  } catch {
@@ -9062,7 +9913,7 @@ async function runCi(argv) {
9062
9913
  return 0;
9063
9914
  }
9064
9915
  const args = parseCiArgs(argv);
9065
- const cwd = args.cwd ? path25.resolve(args.cwd) : process.cwd();
9916
+ const cwd = args.cwd ? path27.resolve(args.cwd) : process.cwd();
9066
9917
  let earlyConfig;
9067
9918
  try {
9068
9919
  earlyConfig = loadConfig(cwd);
@@ -9072,9 +9923,9 @@ async function runCi(argv) {
9072
9923
  const eventName = process.env.GITHUB_EVENT_NAME;
9073
9924
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
9074
9925
  let manualWorkflowDispatch = false;
9075
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs28.existsSync(dispatchEventPath)) {
9926
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs29.existsSync(dispatchEventPath)) {
9076
9927
  try {
9077
- const evt = JSON.parse(fs28.readFileSync(dispatchEventPath, "utf-8"));
9928
+ const evt = JSON.parse(fs29.readFileSync(dispatchEventPath, "utf-8"));
9078
9929
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
9079
9930
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
9080
9931
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -9289,15 +10140,15 @@ function parseChatArgs(argv, env = process.env) {
9289
10140
  return result;
9290
10141
  }
9291
10142
  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)));
10143
+ const sessionFile = path28.relative(cwd, sessionFilePath(cwd, sessionId));
10144
+ const eventsFile = path28.relative(cwd, eventsFilePath(cwd, sessionId));
10145
+ const paths = [sessionFile, eventsFile].filter((p) => fs30.existsSync(path28.join(cwd, p)));
9295
10146
  if (paths.length === 0) return;
9296
10147
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
9297
10148
  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);
10149
+ execFileSync32("git", ["add", "-f", ...paths], opts);
10150
+ execFileSync32("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
10151
+ execFileSync32("git", ["push", "--quiet", "origin", "HEAD"], opts);
9301
10152
  } catch (err) {
9302
10153
  const msg = err instanceof Error ? err.message : String(err);
9303
10154
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}
@@ -9329,7 +10180,7 @@ async function runChat(argv) {
9329
10180
  ${CHAT_HELP}`);
9330
10181
  return 64;
9331
10182
  }
9332
- const cwd = args.cwd ? path26.resolve(args.cwd) : process.cwd();
10183
+ const cwd = args.cwd ? path28.resolve(args.cwd) : process.cwd();
9333
10184
  const sessionId = args.sessionId;
9334
10185
  const unpackedSecrets = unpackAllSecrets();
9335
10186
  if (unpackedSecrets > 0) {
@@ -9381,7 +10232,7 @@ ${CHAT_HELP}`);
9381
10232
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
9382
10233
  const meta = readMeta(sessionFile);
9383
10234
  process.stdout.write(
9384
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs29.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
10235
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs30.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
9385
10236
  `
9386
10237
  );
9387
10238
  try {