@kody-ade/kody-engine 0.4.37 → 0.4.38

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.37",
6
+ version: "0.4.38",
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,7 +51,7 @@ var package_default = {
51
51
  };
52
52
 
53
53
  // src/chat-cli.ts
54
- import { execFileSync as execFileSync32 } from "child_process";
54
+ import { execFileSync as execFileSync31 } from "child_process";
55
55
  import * as fs30 from "fs";
56
56
  import * as path28 from "path";
57
57
 
@@ -912,7 +912,7 @@ async function emit2(sink, type, sessionId, suffix, payload) {
912
912
  }
913
913
 
914
914
  // src/kody-cli.ts
915
- import { execFileSync as execFileSync31 } from "child_process";
915
+ import { execFileSync as execFileSync30 } from "child_process";
916
916
  import * as fs29 from "fs";
917
917
  import * as path27 from "path";
918
918
 
@@ -1492,7 +1492,7 @@ function postPrReviewComment(prNumber, body, cwd) {
1492
1492
  }
1493
1493
 
1494
1494
  // src/executor.ts
1495
- import { execFileSync as execFileSync30, spawn as spawn5 } from "child_process";
1495
+ import { execFileSync as execFileSync29, spawn as spawn5 } from "child_process";
1496
1496
  import * as fs28 from "fs";
1497
1497
  import * as path26 from "path";
1498
1498
 
@@ -2801,301 +2801,6 @@ function defaultLabelMap() {
2801
2801
  };
2802
2802
  }
2803
2803
 
2804
- // src/goal/operations.ts
2805
- import { execFileSync as execFileSync9 } from "child_process";
2806
-
2807
- // src/goal/labels.ts
2808
- function goalLabel(goalId) {
2809
- return `goal:${goalId}`;
2810
- }
2811
- var DISPATCHED_LABEL = "goal-runner:dispatched";
2812
- var FAILED_LABEL = "goal-runner:failed";
2813
- var UMBRELLA_BUILDING_LABEL = "kody:building";
2814
- var TICK_LABELS = [
2815
- {
2816
- name: DISPATCHED_LABEL,
2817
- color: "ededed",
2818
- description: "kody goal-runner: already dispatched this tick"
2819
- },
2820
- {
2821
- name: FAILED_LABEL,
2822
- color: "b60205",
2823
- description: "kody goal-runner: task failed; needs human attention"
2824
- }
2825
- ];
2826
-
2827
- // src/goal/operations.ts
2828
- function fail(err) {
2829
- if (err instanceof Error) {
2830
- const lines = err.message.split("\n").filter(Boolean);
2831
- return { ok: false, error: lines[0] ?? err.message };
2832
- }
2833
- return { ok: false, error: String(err) };
2834
- }
2835
- function listGoalIssues(goalId, excludeIssueNumber, cwd) {
2836
- try {
2837
- const out = gh(
2838
- [
2839
- "api",
2840
- `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
2841
- "--jq",
2842
- "[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase), labels: [.labels[].name]}]"
2843
- ],
2844
- { cwd }
2845
- );
2846
- const arr = JSON.parse(out);
2847
- const filtered = excludeIssueNumber !== void 0 ? arr.filter((i) => i.number !== excludeIssueNumber) : arr;
2848
- return { ok: true, value: filtered };
2849
- } catch (err) {
2850
- return fail(err);
2851
- }
2852
- }
2853
- function ensureLabel(name, color, description, cwd) {
2854
- try {
2855
- gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
2856
- return { ok: true };
2857
- } catch (err) {
2858
- return fail(err);
2859
- }
2860
- }
2861
- function addLabel2(issueNumber, label, cwd) {
2862
- try {
2863
- gh(["issue", "edit", String(issueNumber), "--add-label", label], { cwd });
2864
- return { ok: true };
2865
- } catch (err) {
2866
- return fail(err);
2867
- }
2868
- }
2869
- function commentOnIssue(issueNumber, body, cwd) {
2870
- try {
2871
- gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
2872
- return { ok: true };
2873
- } catch (err) {
2874
- return fail(err);
2875
- }
2876
- }
2877
- function closeIssue(issueNumber, options, cwd) {
2878
- try {
2879
- if (options.comment) {
2880
- gh(["issue", "comment", String(issueNumber), "--body", options.comment], { cwd });
2881
- }
2882
- const args = ["issue", "close", String(issueNumber)];
2883
- if (options.reason) args.push("--reason", options.reason);
2884
- gh(args, { cwd });
2885
- return { ok: true };
2886
- } catch (err) {
2887
- return fail(err);
2888
- }
2889
- }
2890
- function getIssueState(issueNumber, cwd) {
2891
- try {
2892
- const out = gh(["issue", "view", String(issueNumber), "--json", "state", "--jq", ".state"], {
2893
- cwd
2894
- });
2895
- const norm = out.trim().toUpperCase();
2896
- if (norm !== "OPEN" && norm !== "CLOSED") {
2897
- return { ok: false, error: `unexpected state: ${out}` };
2898
- }
2899
- return { ok: true, value: norm };
2900
- } catch (err) {
2901
- return fail(err);
2902
- }
2903
- }
2904
- function findUmbrellaByTitle(goalId, title, cwd) {
2905
- try {
2906
- const out = gh(
2907
- [
2908
- "api",
2909
- `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
2910
- "--jq",
2911
- `[.[] | select(.pull_request == null) | select(.title == "${title.replace(/"/g, '\\"')}")] | (map(select(.state == "open")) + map(select(.state != "open")))[0].number // empty`
2912
- ],
2913
- { cwd }
2914
- );
2915
- const trimmed = out.trim();
2916
- if (!trimmed) return { ok: true, value: null };
2917
- const n = Number.parseInt(trimmed, 10);
2918
- if (!Number.isFinite(n)) return { ok: true, value: null };
2919
- return { ok: true, value: n };
2920
- } catch (err) {
2921
- return fail(err);
2922
- }
2923
- }
2924
- function createIssue(args, cwd) {
2925
- try {
2926
- const cliArgs = ["issue", "create", "--title", args.title, "--body", args.body];
2927
- for (const l of args.labels) cliArgs.push("--label", l);
2928
- const url = gh(cliArgs, { cwd });
2929
- const match = url.match(/\/issues\/(\d+)/);
2930
- if (!match?.[1]) return { ok: false, error: `couldn't parse issue number from URL: ${url}` };
2931
- return { ok: true, value: Number.parseInt(match[1], 10) };
2932
- } catch (err) {
2933
- return fail(err);
2934
- }
2935
- }
2936
- function listPrsByBase(base, state, cwd) {
2937
- try {
2938
- const out = gh(
2939
- [
2940
- "pr",
2941
- "list",
2942
- "--base",
2943
- base,
2944
- "--state",
2945
- state,
2946
- "--limit",
2947
- "50",
2948
- "--json",
2949
- "number,isDraft,mergeable,mergeStateStatus,url,headRefName,body"
2950
- ],
2951
- { cwd }
2952
- );
2953
- return { ok: true, value: JSON.parse(out) };
2954
- } catch (err) {
2955
- return fail(err);
2956
- }
2957
- }
2958
- function listPrsByHead(head, state, cwd) {
2959
- try {
2960
- const out = gh(
2961
- [
2962
- "pr",
2963
- "list",
2964
- "--head",
2965
- head,
2966
- "--state",
2967
- state,
2968
- "--json",
2969
- "number,isDraft,mergeable,mergeStateStatus,url,headRefName,body"
2970
- ],
2971
- { cwd }
2972
- );
2973
- return { ok: true, value: JSON.parse(out) };
2974
- } catch (err) {
2975
- return fail(err);
2976
- }
2977
- }
2978
- function mergePrSquash(prNumber, cwd) {
2979
- try {
2980
- gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
2981
- return { ok: true };
2982
- } catch (err) {
2983
- return fail(err);
2984
- }
2985
- }
2986
- function closePr(prNumber, comment, cwd) {
2987
- try {
2988
- gh(["pr", "close", String(prNumber), "--comment", comment], { cwd });
2989
- return { ok: true };
2990
- } catch (err) {
2991
- return fail(err);
2992
- }
2993
- }
2994
- function createPr(args, cwd) {
2995
- try {
2996
- const cli = ["pr", "create", "--head", args.head, "--base", args.base, "--title", args.title, "--body", args.body];
2997
- if (args.draft) cli.push("--draft");
2998
- const url = gh(cli, { cwd });
2999
- if (!url.includes("/pull/")) return { ok: false, error: `gh pr create returned unexpected output: ${url}` };
3000
- return { ok: true, value: url.trim() };
3001
- } catch (err) {
3002
- return fail(err);
3003
- }
3004
- }
3005
- function editPrBody(prNumber, body, cwd) {
3006
- try {
3007
- gh(["pr", "edit", String(prNumber), "--body", body], { cwd });
3008
- return { ok: true };
3009
- } catch (err) {
3010
- return fail(err);
3011
- }
3012
- }
3013
- function markPrReady(prNumber, cwd) {
3014
- try {
3015
- gh(["pr", "ready", String(prNumber)], { cwd });
3016
- return { ok: true };
3017
- } catch (err) {
3018
- return fail(err);
3019
- }
3020
- }
3021
- function ghTokenEnv() {
3022
- const token = process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
3023
- return token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
3024
- }
3025
- function remoteBranchExists(ref, cwd) {
3026
- try {
3027
- execFileSync9("git", ["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${ref}`], {
3028
- cwd,
3029
- stdio: "pipe",
3030
- env: ghTokenEnv()
3031
- });
3032
- return true;
3033
- } catch {
3034
- return false;
3035
- }
3036
- }
3037
- function fetchOrigin(cwd) {
3038
- try {
3039
- execFileSync9("git", ["fetch", "origin", "--quiet"], { cwd, stdio: "pipe", env: ghTokenEnv() });
3040
- } catch {
3041
- }
3042
- }
3043
- function createBranchFrom(branch, base, cwd) {
3044
- try {
3045
- execFileSync9("git", ["push", "origin", `refs/remotes/origin/${base}:refs/heads/${branch}`, "--quiet"], {
3046
- cwd,
3047
- stdio: "pipe",
3048
- env: ghTokenEnv()
3049
- });
3050
- return { ok: true };
3051
- } catch (err) {
3052
- return fail(err);
3053
- }
3054
- }
3055
- function inferLinkedIssue(pr) {
3056
- const body = pr.body ?? "";
3057
- const m = body.match(/\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/i);
3058
- if (m?.[1]) return Number.parseInt(m[1], 10);
3059
- const ref = pr.headRefName ?? "";
3060
- const bm = ref.match(/^(\d+)-/);
3061
- if (bm?.[1]) return Number.parseInt(bm[1], 10);
3062
- return void 0;
3063
- }
3064
-
3065
- // src/scripts/closeMergedTaskIssues.ts
3066
- var closeMergedTaskIssues = async (ctx) => {
3067
- const goal = ctx.data.goal;
3068
- if (!goal) return;
3069
- const merged = listPrsByBase(goal.goalBranch, "merged", ctx.cwd);
3070
- if (!merged.ok) {
3071
- process.stderr.write(`[goal-tick] closeMergedTaskIssues: list failed: ${merged.error}
3072
- `);
3073
- return;
3074
- }
3075
- const seen = /* @__PURE__ */ new Set();
3076
- for (const pr of merged.value ?? []) {
3077
- const linked = inferLinkedIssue(pr);
3078
- if (linked === void 0 || seen.has(linked)) continue;
3079
- seen.add(linked);
3080
- const stateRes = getIssueState(linked, ctx.cwd);
3081
- if (!stateRes.ok || stateRes.value !== "OPEN") continue;
3082
- process.stdout.write(`[goal-tick] closing #${linked} (PR merged into ${goal.goalBranch})
3083
- `);
3084
- const r = closeIssue(
3085
- linked,
3086
- {
3087
- comment: `_Closed by goal-tick: PR for this task merged into \`${goal.goalBranch}\`._`,
3088
- reason: "completed"
3089
- },
3090
- ctx.cwd
3091
- );
3092
- if (!r.ok) {
3093
- process.stderr.write(`[goal-tick] failed to close #${linked}: ${r.error} (continuing)
3094
- `);
3095
- }
3096
- }
3097
- };
3098
-
3099
2804
  // src/scripts/commitAndPush.ts
3100
2805
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
3101
2806
  var commitAndPush2 = async (ctx) => {
@@ -3140,14 +2845,14 @@ var commitAndPush2 = async (ctx) => {
3140
2845
  };
3141
2846
 
3142
2847
  // src/scripts/commitGoalState.ts
3143
- import { execFileSync as execFileSync10 } from "child_process";
2848
+ import { execFileSync as execFileSync9 } from "child_process";
3144
2849
  import * as path12 from "path";
3145
2850
  var commitGoalState = async (ctx) => {
3146
2851
  const goal = ctx.data.goal;
3147
2852
  if (!goal) return;
3148
2853
  const stateRel = path12.posix.join(".kody", "goals", goal.id, "state.json");
3149
2854
  try {
3150
- execFileSync10("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
2855
+ execFileSync9("git", ["add", stateRel], { cwd: ctx.cwd, stdio: "pipe" });
3151
2856
  } catch (err) {
3152
2857
  process.stderr.write(
3153
2858
  `[goal-tick] commitGoalState: git add failed: ${err instanceof Error ? err.message : String(err)}
@@ -3156,13 +2861,13 @@ var commitGoalState = async (ctx) => {
3156
2861
  return;
3157
2862
  }
3158
2863
  try {
3159
- execFileSync10("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
2864
+ execFileSync9("git", ["diff", "--cached", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3160
2865
  return;
3161
2866
  } catch {
3162
2867
  }
3163
2868
  const msg = describeCommitMessage(goal);
3164
2869
  try {
3165
- execFileSync10("git", ["commit", "-m", msg, "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
2870
+ execFileSync9("git", ["commit", "-m", msg, "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3166
2871
  } catch (err) {
3167
2872
  process.stderr.write(
3168
2873
  `[goal-tick] commitGoalState: git commit failed: ${err instanceof Error ? err.message : String(err)}
@@ -3171,7 +2876,7 @@ var commitGoalState = async (ctx) => {
3171
2876
  return;
3172
2877
  }
3173
2878
  try {
3174
- execFileSync10("git", ["push", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
2879
+ execFileSync9("git", ["push", "--quiet"], { cwd: ctx.cwd, stdio: "pipe" });
3175
2880
  } catch {
3176
2881
  process.stderr.write("[goal-tick] commitGoalState: push failed (will retry next tick)\n");
3177
2882
  }
@@ -3185,9 +2890,6 @@ function describeCommitMessage(goal) {
3185
2890
  if (goal.phase === "in-flight") {
3186
2891
  return `chore(goals): tick ${goal.id} (waiting for in-flight task)`;
3187
2892
  }
3188
- if (goal.phase === "blocked-by-failure") {
3189
- return `chore(goals): tick ${goal.id} (blocked by failed task)`;
3190
- }
3191
2893
  return `chore(goals): tick ${goal.id} (idle)`;
3192
2894
  }
3193
2895
 
@@ -3290,7 +2992,7 @@ function formatToolsUsage(profile) {
3290
2992
  }
3291
2993
 
3292
2994
  // src/scripts/createQaGoal.ts
3293
- import { execFileSync as execFileSync11 } from "child_process";
2995
+ import { execFileSync as execFileSync10 } from "child_process";
3294
2996
  import * as fs14 from "fs";
3295
2997
  import * as path14 from "path";
3296
2998
 
@@ -3474,7 +3176,7 @@ ${json}
3474
3176
  ${MANIFEST_END}
3475
3177
  `;
3476
3178
  }
3477
- function ensureLabel2(name, color, description, cwd) {
3179
+ function ensureLabel(name, color, description, cwd) {
3478
3180
  try {
3479
3181
  gh(["label", "create", name, "--color", color, "--description", description, "--force"], { cwd });
3480
3182
  } catch {
@@ -3494,7 +3196,7 @@ function ensureSeverityLabels(findings, cwd) {
3494
3196
  for (const f of findings) {
3495
3197
  if (seen.has(f.severity)) continue;
3496
3198
  seen.add(f.severity);
3497
- ensureLabel2(severityLabel(f.severity), SEVERITY_COLORS[f.severity], `kody QA finding severity ${f.severity}`, cwd);
3199
+ ensureLabel(severityLabel(f.severity), SEVERITY_COLORS[f.severity], `kody QA finding severity ${f.severity}`, cwd);
3498
3200
  }
3499
3201
  }
3500
3202
  function buildIssueBody(f, goalId, parentManifestNumber) {
@@ -3528,7 +3230,7 @@ function buildIssueBody(f, goalId, parentManifestNumber) {
3528
3230
  return lines.join("\n");
3529
3231
  }
3530
3232
  function createOrUpdateManifestIssue(number, manifest, cwd) {
3531
- ensureLabel2(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3233
+ ensureLabel(MANIFEST_LABEL, "8b5cf6", "kody: goals manifest", cwd);
3532
3234
  const body = serializeManifestBody(manifest);
3533
3235
  if (number !== null) {
3534
3236
  gh(["issue", "edit", String(number), "--body-file", "-"], { input: body, cwd });
@@ -3561,7 +3263,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
3561
3263
  function gitTry(args, cwd) {
3562
3264
  const env = { ...process.env, SKIP_HOOKS: "1", HUSKY: "0" };
3563
3265
  try {
3564
- execFileSync11("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3266
+ execFileSync10("git", args, { cwd, stdio: ["ignore", "pipe", "pipe"], env });
3565
3267
  return { ok: true, stderr: "" };
3566
3268
  } catch (err) {
3567
3269
  const e = err;
@@ -3643,8 +3345,8 @@ ${tail}
3643
3345
  }
3644
3346
  function createTaskIssue(finding, goalId, manifestNumber, cwd) {
3645
3347
  const labels = [`goal:${goalId}`, severityLabel(finding.severity), FINDING_LABEL];
3646
- ensureLabel2(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3647
- ensureLabel2(FINDING_LABEL, "ededed", "kody: QA finding", cwd);
3348
+ ensureLabel(`goal:${goalId}`, "1d76db", `goal: ${goalId}`, cwd);
3349
+ ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", cwd);
3648
3350
  const title = `[${finding.severity}] ${finding.title}`.slice(0, 240);
3649
3351
  const body = buildIssueBody(finding, goalId, manifestNumber);
3650
3352
  const args = ["issue", "create", "--title", title, "--body-file", "-"];
@@ -3703,7 +3405,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
3703
3405
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3704
3406
  return;
3705
3407
  }
3706
- ensureLabel2(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3408
+ ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
3707
3409
  const scope2 = ctx.args.scope;
3708
3410
  const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
3709
3411
  let url = "";
@@ -3833,51 +3535,209 @@ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
3833
3535
  ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
3834
3536
  };
3835
3537
 
3538
+ // src/goal/labels.ts
3539
+ function goalLabel(goalId) {
3540
+ return `goal:${goalId}`;
3541
+ }
3542
+
3543
+ // src/goal/operations.ts
3544
+ function fail(err) {
3545
+ if (err instanceof Error) {
3546
+ const lines = err.message.split("\n").filter(Boolean);
3547
+ return { ok: false, error: lines[0] ?? err.message };
3548
+ }
3549
+ return { ok: false, error: String(err) };
3550
+ }
3551
+ function listGoalIssues(goalId, cwd) {
3552
+ try {
3553
+ const out = gh(
3554
+ [
3555
+ "api",
3556
+ `repos/{owner}/{repo}/issues?labels=${goalLabel(goalId)}&state=all&per_page=100`,
3557
+ "--jq",
3558
+ "[.[] | select(.pull_request == null) | {number, state: (.state | ascii_upcase)}]"
3559
+ ],
3560
+ { cwd }
3561
+ );
3562
+ return { ok: true, value: JSON.parse(out) };
3563
+ } catch (err) {
3564
+ return fail(err);
3565
+ }
3566
+ }
3567
+ function listOpenPrs(cwd) {
3568
+ try {
3569
+ const out = gh(
3570
+ [
3571
+ "pr",
3572
+ "list",
3573
+ "--state",
3574
+ "open",
3575
+ "--limit",
3576
+ "200",
3577
+ "--json",
3578
+ "number,url,isDraft,headRefName,baseRefName,body"
3579
+ ],
3580
+ { cwd }
3581
+ );
3582
+ return { ok: true, value: JSON.parse(out) };
3583
+ } catch (err) {
3584
+ return fail(err);
3585
+ }
3586
+ }
3587
+ function pairIssuesWithPrs(issues, openPrs) {
3588
+ const prByIssue = /* @__PURE__ */ new Map();
3589
+ for (const pr of openPrs) claimPrForIssue(pr, prByIssue);
3590
+ return issues.map((i) => {
3591
+ const pr = prByIssue.get(i.number);
3592
+ let prState = "absent";
3593
+ if (pr) prState = pr.isDraft ? "draft" : "ready";
3594
+ return { number: i.number, state: i.state, prState };
3595
+ });
3596
+ }
3597
+ function claimPrForIssue(pr, prByIssue) {
3598
+ for (const issueNum of extractClosesIssues(pr.body)) {
3599
+ if (!prByIssue.has(issueNum)) {
3600
+ prByIssue.set(issueNum, pr);
3601
+ return;
3602
+ }
3603
+ }
3604
+ const headMatch = pr.headRefName.match(/^(\d+)-/);
3605
+ if (headMatch) {
3606
+ const n = Number.parseInt(headMatch[1], 10);
3607
+ if (Number.isFinite(n) && !prByIssue.has(n)) prByIssue.set(n, pr);
3608
+ }
3609
+ }
3610
+ function extractClosesIssues(body) {
3611
+ const out = [];
3612
+ const re = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)\b/gi;
3613
+ for (let m = re.exec(body); m !== null; m = re.exec(body)) {
3614
+ const n = Number.parseInt(m[1], 10);
3615
+ if (Number.isFinite(n)) out.push(n);
3616
+ }
3617
+ return out;
3618
+ }
3619
+ function pickLeafPr(prs) {
3620
+ if (prs.length === 0) return void 0;
3621
+ const bases = new Set(prs.map((p) => p.baseRefName));
3622
+ const leaves = prs.filter((p) => !bases.has(p.headRefName));
3623
+ return leaves.sort((a, b) => b.number - a.number)[0];
3624
+ }
3625
+ function commentOnIssue(issueNumber, body, cwd) {
3626
+ try {
3627
+ gh(["issue", "comment", String(issueNumber), "--body", body], { cwd });
3628
+ return { ok: true };
3629
+ } catch (err) {
3630
+ return fail(err);
3631
+ }
3632
+ }
3633
+ function closeIssue(issueNumber, options, cwd) {
3634
+ try {
3635
+ if (options.comment) {
3636
+ gh(["issue", "comment", String(issueNumber), "--body", options.comment], { cwd });
3637
+ }
3638
+ const args = ["issue", "close", String(issueNumber)];
3639
+ if (options.reason) args.push("--reason", options.reason);
3640
+ gh(args, { cwd });
3641
+ return { ok: true };
3642
+ } catch (err) {
3643
+ return fail(err);
3644
+ }
3645
+ }
3646
+ function closePr(prNumber, comment, cwd) {
3647
+ try {
3648
+ gh(["pr", "close", String(prNumber), "--comment", comment], { cwd });
3649
+ return { ok: true };
3650
+ } catch (err) {
3651
+ return fail(err);
3652
+ }
3653
+ }
3654
+ function mergePrSquash(prNumber, cwd) {
3655
+ try {
3656
+ gh(["pr", "merge", String(prNumber), "--squash", "--delete-branch"], { cwd });
3657
+ return { ok: true };
3658
+ } catch (err) {
3659
+ return fail(err);
3660
+ }
3661
+ }
3662
+ function markPrReady(prNumber, cwd) {
3663
+ try {
3664
+ gh(["pr", "ready", String(prNumber)], { cwd });
3665
+ return { ok: true };
3666
+ } catch (err) {
3667
+ return fail(err);
3668
+ }
3669
+ }
3670
+
3836
3671
  // src/goal/phase.ts
3837
3672
  function derivePhase(snap) {
3838
- if (snap.lifecycleState === void 0) return "missing";
3673
+ if (!snap.lifecycleState) return "missing";
3839
3674
  if (snap.lifecycleState === "abandoned") return "abandoned";
3840
3675
  if (snap.lifecycleState === "closed" || snap.lifecycleState === "done") return "terminal";
3841
- if (snap.childTasks.length === 0) return "no-tasks";
3842
- const allClosed = snap.childTasks.every((t) => t.state === "CLOSED");
3843
- if (allClosed) return "all-done";
3844
- const anyFailed = snap.childTasks.some((t) => t.labels.includes(FAILED_LABEL));
3845
- if (anyFailed) return "blocked-by-failure";
3846
- const inFlight = snap.childTasks.some((t) => t.state === "OPEN" && t.labels.includes(DISPATCHED_LABEL));
3847
- if (inFlight) return "in-flight";
3848
- const dispatchable = snap.childTasks.some((t) => t.state === "OPEN" && !t.labels.includes(DISPATCHED_LABEL));
3676
+ const hasInFlight = snap.childTasks.some((t) => t.state === "OPEN" && t.prState === "draft");
3677
+ if (hasInFlight) return "in-flight";
3678
+ if (snap.childTasks.length === 0) return "idle";
3679
+ const allDone = snap.childTasks.every((t) => t.state === "CLOSED" || t.prState === "ready");
3680
+ if (allDone) return "all-done";
3681
+ const dispatchable = snap.childTasks.some((t) => t.state === "OPEN" && t.prState === "absent");
3849
3682
  if (dispatchable) return "ready-to-dispatch";
3850
3683
  return "idle";
3851
3684
  }
3852
3685
  function pickNextDispatchable(snap) {
3853
- const candidates = snap.childTasks.filter((t) => t.state === "OPEN" && !t.labels.includes(DISPATCHED_LABEL)).sort((a, b) => a.number - b.number);
3854
- return candidates[0];
3686
+ return snap.childTasks.filter((t) => t.state === "OPEN" && t.prState === "absent").sort((a, b) => a.number - b.number)[0];
3855
3687
  }
3856
3688
 
3857
3689
  // src/scripts/deriveGoalPhase.ts
3858
3690
  var deriveGoalPhase = async (ctx) => {
3859
3691
  const goal = ctx.data.goal;
3860
3692
  if (!goal) return;
3861
- const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
3693
+ const issues = listGoalIssues(goal.id, ctx.cwd);
3862
3694
  if (!issues.ok) {
3863
- process.stderr.write(`[goal-tick] deriveGoalPhase: list failed: ${issues.error}
3695
+ process.stderr.write(`[goal-tick] deriveGoalPhase: list issues failed: ${issues.error}
3864
3696
  `);
3865
3697
  goal.childTasks = [];
3698
+ goal.openTaskPrs = [];
3699
+ goal.phase = "idle";
3700
+ return;
3701
+ }
3702
+ const rawIssues = issues.value ?? [];
3703
+ const allPrs = listOpenPrs(ctx.cwd);
3704
+ if (!allPrs.ok) {
3705
+ process.stderr.write(`[goal-tick] deriveGoalPhase: list PRs failed: ${allPrs.error}
3706
+ `);
3707
+ goal.childTasks = rawIssues.map((i) => ({ ...i, prState: "absent" }));
3708
+ goal.openTaskPrs = [];
3866
3709
  goal.phase = "idle";
3867
3710
  return;
3868
3711
  }
3869
- const childTasks = issues.value ?? [];
3870
- goal.childTasks = childTasks;
3712
+ const taskPrs = filterGoalTaskPrs(allPrs.value ?? [], rawIssues.map((i) => i.number));
3713
+ goal.openTaskPrs = taskPrs;
3714
+ goal.leafPr = pickLeafPr(taskPrs);
3715
+ goal.childTasks = pairIssuesWithPrs(rawIssues, taskPrs);
3871
3716
  goal.phase = derivePhase({
3872
3717
  lifecycleState: goal.state,
3873
- childTasks
3718
+ childTasks: goal.childTasks
3874
3719
  });
3875
- process.stdout.write(`[goal-tick] phase=${goal.phase} goal=${goal.id} tasks=${childTasks.length}
3876
- `);
3720
+ process.stdout.write(
3721
+ `[goal-tick] phase=${goal.phase} goal=${goal.id} tasks=${rawIssues.length} stack=${taskPrs.length}` + (goal.leafPr ? ` leaf=#${goal.leafPr.number}` : "") + "\n"
3722
+ );
3877
3723
  };
3724
+ function filterGoalTaskPrs(prs, taskIssueNumbers) {
3725
+ const taskSet = new Set(taskIssueNumbers);
3726
+ return prs.filter((pr) => {
3727
+ for (const n of extractClosesIssues(pr.body)) {
3728
+ if (taskSet.has(n)) return true;
3729
+ }
3730
+ const headMatch = pr.headRefName.match(/^(\d+)-/);
3731
+ if (headMatch) {
3732
+ const n = Number.parseInt(headMatch[1], 10);
3733
+ if (Number.isFinite(n) && taskSet.has(n)) return true;
3734
+ }
3735
+ return false;
3736
+ });
3737
+ }
3878
3738
 
3879
3739
  // src/scripts/diagMcp.ts
3880
- import { execFileSync as execFileSync12 } from "child_process";
3740
+ import { execFileSync as execFileSync11 } from "child_process";
3881
3741
  import * as fs15 from "fs";
3882
3742
  import * as os3 from "os";
3883
3743
  import * as path15 from "path";
@@ -3897,7 +3757,7 @@ var diagMcp = async (_ctx) => {
3897
3757
  process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
3898
3758
  `);
3899
3759
  try {
3900
- const v = execFileSync12("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
3760
+ const v = execFileSync11("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
3901
3761
  stdio: "pipe",
3902
3762
  timeout: 6e4,
3903
3763
  encoding: "utf8"
@@ -4399,7 +4259,7 @@ var discoverQaContext = async (ctx) => {
4399
4259
  };
4400
4260
 
4401
4261
  // src/scripts/dispatch.ts
4402
- import { execFileSync as execFileSync13 } from "child_process";
4262
+ import { execFileSync as execFileSync12 } from "child_process";
4403
4263
  var API_TIMEOUT_MS4 = 3e4;
4404
4264
  var dispatch = async (ctx, _profile, _agentResult, args) => {
4405
4265
  const next = args?.next;
@@ -4435,7 +4295,7 @@ var dispatch = async (ctx, _profile, _agentResult, args) => {
4435
4295
  const sub = usePr ? "pr" : "issue";
4436
4296
  const body = `@kody ${next}`;
4437
4297
  try {
4438
- execFileSync13("gh", [sub, "comment", String(targetNumber), "--body", body], {
4298
+ execFileSync12("gh", [sub, "comment", String(targetNumber), "--body", body], {
4439
4299
  timeout: API_TIMEOUT_MS4,
4440
4300
  cwd: ctx.cwd,
4441
4301
  stdio: ["ignore", "pipe", "pipe"]
@@ -4455,7 +4315,7 @@ function parsePr(url) {
4455
4315
  }
4456
4316
 
4457
4317
  // src/scripts/dispatchClassified.ts
4458
- import { execFileSync as execFileSync14 } from "child_process";
4318
+ import { execFileSync as execFileSync13 } from "child_process";
4459
4319
  var API_TIMEOUT_MS5 = 3e4;
4460
4320
  var VALID_CLASSES2 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
4461
4321
  var dispatchClassified = async (ctx) => {
@@ -4464,7 +4324,7 @@ var dispatchClassified = async (ctx) => {
4464
4324
  const classification = ctx.data.classification;
4465
4325
  if (!classification || !VALID_CLASSES2.has(classification)) return;
4466
4326
  try {
4467
- execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4327
+ execFileSync13("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
4468
4328
  cwd: ctx.cwd,
4469
4329
  timeout: API_TIMEOUT_MS5,
4470
4330
  stdio: ["ignore", "pipe", "pipe"]
@@ -5095,129 +4955,25 @@ function listIssuesByLabel(label, cwd) {
5095
4955
  // src/scripts/dispatchNextTask.ts
5096
4956
  var dispatchNextTask = async (ctx) => {
5097
4957
  const goal = ctx.data.goal;
5098
- if (!goal?.childTasks) return;
5099
- const next = pickNextDispatchable({
5100
- lifecycleState: goal.state,
5101
- childTasks: goal.childTasks
5102
- });
5103
- if (!next) {
5104
- process.stdout.write("[goal-tick] no undispatched open task \u2014 idle\n");
5105
- return;
5106
- }
5107
- process.stdout.write(`[goal-tick] dispatching @kody on task #${next.number} (--base ${goal.goalBranch})
5108
- `);
5109
- const comment = commentOnIssue(next.number, `@kody --base ${goal.goalBranch}`, ctx.cwd);
5110
- if (!comment.ok) {
5111
- process.stderr.write(`[goal-tick] dispatchNextTask: comment failed on #${next.number}: ${comment.error}
5112
- `);
5113
- return;
5114
- }
5115
- const label = addLabel2(next.number, DISPATCHED_LABEL, ctx.cwd);
5116
- if (!label.ok) {
5117
- process.stderr.write(
5118
- `[goal-tick] dispatchNextTask: add-label failed on #${next.number}: ${label.error} (continuing \u2014 comment already posted)
5119
- `
5120
- );
5121
- }
5122
- goal.lastDispatchedIssue = next.number;
5123
- };
5124
-
5125
- // src/scripts/ensureGoalBranch.ts
5126
- var ensureGoalBranch = async (ctx) => {
5127
- const goal = ctx.data.goal;
5128
- if (!goal) return;
5129
- fetchOrigin(ctx.cwd);
5130
- if (remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5131
- process.stdout.write(`[goal-tick] origin/${goal.goalBranch} already exists \u2014 leaving as-is
5132
- `);
5133
- return;
5134
- }
5135
- if (!remoteBranchExists(goal.defaultBranch, ctx.cwd)) {
5136
- process.stderr.write(`[goal-tick] cannot create goal branch: origin/${goal.defaultBranch} missing
5137
- `);
5138
- return;
5139
- }
5140
- process.stdout.write(`[goal-tick] creating origin/${goal.goalBranch} from origin/${goal.defaultBranch}
5141
- `);
5142
- const r = createBranchFrom(goal.goalBranch, goal.defaultBranch, ctx.cwd);
5143
- if (!r.ok) {
5144
- process.stderr.write(
5145
- `[goal-tick] push of ${goal.goalBranch} failed: ${r.error} \u2014 task dispatch will fall back to defaultBranch
5146
- `
5147
- );
5148
- }
5149
- };
5150
-
5151
- // src/scripts/ensureGoalPr.ts
5152
- var ensureGoalPr = async (ctx) => {
5153
- const goal = ctx.data.goal;
5154
- if (!goal) return;
5155
- if (goal.goalPrUrl) return;
5156
- if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) return;
5157
- const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5158
- if (existing.ok && existing.value && existing.value.length > 0) {
5159
- goal.goalPrUrl = existing.value[0].url;
5160
- return;
5161
- }
5162
- const title = `goal: ${goal.id}`;
5163
- const body = goal.goalIssueNumber ? `Tracking integration PR for goal **${goal.id}**.
5164
-
5165
- 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.
5166
-
5167
- Closes #${goal.goalIssueNumber}
5168
- ` : `Tracking integration PR for goal **${goal.id}**.
5169
-
5170
- Child task PRs merge into \`${goal.goalBranch}\`. Held in **draft** until every task is complete.
5171
- `;
5172
- const created = createPr(
5173
- {
5174
- head: goal.goalBranch,
5175
- base: goal.defaultBranch,
5176
- title,
5177
- body,
5178
- draft: true
5179
- },
5180
- ctx.cwd
5181
- );
5182
- if (!created.ok) {
5183
- process.stderr.write(
5184
- `[goal-tick] ensureGoalPr: gh pr create failed: ${created.error} (continuing without goal PR)
5185
- `
5186
- );
5187
- return;
5188
- }
5189
- process.stdout.write(`[goal-tick] opened draft goal PR ${created.value} for ${goal.id}
5190
- `);
5191
- goal.goalPrUrl = created.value;
5192
- };
5193
-
5194
- // src/scripts/ensureLifecycleLabels.ts
5195
- var ensureLifecycleLabels = async (ctx) => {
5196
- const goal = ctx.data.goal;
5197
- if (!goal) return;
5198
- for (const spec of TICK_LABELS) {
5199
- const r2 = ensureLabel(spec.name, spec.color, spec.description, ctx.cwd);
5200
- if (!r2.ok) {
5201
- process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${spec.name}: ${r2.error}
5202
- `);
5203
- }
4958
+ if (!goal?.childTasks) return;
4959
+ const next = pickNextDispatchable({
4960
+ lifecycleState: goal.state,
4961
+ childTasks: goal.childTasks
4962
+ });
4963
+ if (!next) {
4964
+ process.stdout.write("[goal-tick] no dispatchable task \u2014 idle\n");
4965
+ return;
5204
4966
  }
5205
- const goalLbl = goalLabel(goal.id);
5206
- const r = ensureLabel(goalLbl, "0e8a16", `kody goal task: belongs to goal ${goal.id}`, ctx.cwd);
5207
- if (!r.ok) {
5208
- process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${goalLbl}: ${r.error}
4967
+ const base = goal.leafPr?.headRefName ?? goal.defaultBranch;
4968
+ process.stdout.write(`[goal-tick] dispatching @kody on #${next.number} (--base ${base})
5209
4969
  `);
5210
- }
5211
- const u = ensureLabel(
5212
- UMBRELLA_BUILDING_LABEL,
5213
- "1d76db",
5214
- "kody: in-flight (work being assembled on a branch)",
5215
- ctx.cwd
5216
- );
5217
- if (!u.ok) {
5218
- process.stderr.write(`[goal-tick] ensureLifecycleLabels: ${UMBRELLA_BUILDING_LABEL}: ${u.error}
4970
+ const comment = commentOnIssue(next.number, `@kody --base ${base}`, ctx.cwd);
4971
+ if (!comment.ok) {
4972
+ process.stderr.write(`[goal-tick] dispatchNextTask: comment failed on #${next.number}: ${comment.error}
5219
4973
  `);
4974
+ return;
5220
4975
  }
4976
+ goal.lastDispatchedIssue = next.number;
5221
4977
  };
5222
4978
 
5223
4979
  // src/pr.ts
@@ -5487,203 +5243,41 @@ function collectExpectedTests(raw) {
5487
5243
  return out;
5488
5244
  }
5489
5245
 
5490
- // src/scripts/ensureUmbrellaIssue.ts
5491
- var ensureUmbrellaIssue = async (ctx) => {
5492
- const goal = ctx.data.goal;
5493
- if (!goal) return;
5494
- if (goal.goalIssueNumber !== void 0) return;
5495
- const title = `goal: ${goal.id}`;
5496
- const body = `Umbrella issue for goal **${goal.id}**.
5497
-
5498
- Closed automatically when the goal PR (\`${goal.goalBranch}\` \u2192 \`${goal.defaultBranch}\`) merges.
5499
- `;
5500
- const existing = findUmbrellaByTitle(goal.id, title, ctx.cwd);
5501
- if (existing.ok && existing.value !== null && existing.value !== void 0) {
5502
- process.stdout.write(`[goal-tick] adopted existing umbrella issue #${existing.value} for ${goal.id}
5503
- `);
5504
- goal.goalIssueNumber = existing.value;
5505
- return;
5506
- }
5507
- const created = createIssue(
5508
- {
5509
- title,
5510
- body,
5511
- labels: [goalLabel(goal.id), UMBRELLA_BUILDING_LABEL]
5512
- },
5513
- ctx.cwd
5514
- );
5515
- if (!created.ok) {
5516
- process.stderr.write(
5517
- `[goal-tick] ensureUmbrellaIssue: gh issue create failed: ${created.error} \u2014 continuing without umbrella issue
5518
- `
5519
- );
5520
- return;
5521
- }
5522
- process.stdout.write(`[goal-tick] opened umbrella issue #${created.value} for ${goal.id}
5523
- `);
5524
- goal.goalIssueNumber = created.value;
5525
- };
5526
-
5527
- // src/goal/state.ts
5528
- import * as fs20 from "fs";
5529
- import * as path20 from "path";
5530
- var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
5531
- var GoalStateError = class extends Error {
5532
- constructor(path29, message) {
5533
- super(`Invalid goal state at ${path29}:
5534
- ${message}`);
5535
- this.path = path29;
5536
- this.name = "GoalStateError";
5537
- }
5538
- path;
5539
- };
5540
- function parseGoalState(filePath, raw) {
5541
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
5542
- throw new GoalStateError(filePath, "must be a JSON object");
5543
- }
5544
- const r = raw;
5545
- const stateValue = r.state;
5546
- if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
5547
- throw new GoalStateError(
5548
- filePath,
5549
- `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
5550
- );
5551
- }
5552
- const parsed = {
5553
- state: stateValue,
5554
- extra: {}
5555
- };
5556
- if (typeof r.goalIssueNumber === "number" && Number.isFinite(r.goalIssueNumber)) {
5557
- parsed.goalIssueNumber = r.goalIssueNumber;
5558
- }
5559
- if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
5560
- parsed.lastDispatchedIssue = r.lastDispatchedIssue;
5561
- }
5562
- if (typeof r.goalPrUrl === "string" && r.goalPrUrl.length > 0) {
5563
- parsed.goalPrUrl = r.goalPrUrl;
5564
- }
5565
- for (const ts of ["updatedAt", "createdAt", "startedAt", "completedAt"]) {
5566
- const v = r[ts];
5567
- if (typeof v === "string" && v.length > 0) parsed[ts] = v;
5568
- }
5569
- const known = /* @__PURE__ */ new Set([
5570
- "state",
5571
- "goalIssueNumber",
5572
- "lastDispatchedIssue",
5573
- "goalPrUrl",
5574
- "updatedAt",
5575
- "createdAt",
5576
- "startedAt",
5577
- "completedAt"
5578
- ]);
5579
- for (const [k, v] of Object.entries(r)) {
5580
- if (!known.has(k)) parsed.extra[k] = v;
5581
- }
5582
- return parsed;
5583
- }
5584
- function serializeGoalState(s) {
5585
- const obj = { ...s.extra, state: s.state };
5586
- if (s.goalIssueNumber !== void 0) obj.goalIssueNumber = s.goalIssueNumber;
5587
- if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
5588
- if (s.goalPrUrl !== void 0) obj.goalPrUrl = s.goalPrUrl;
5589
- if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
5590
- if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
5591
- if (s.completedAt !== void 0) obj.completedAt = s.completedAt;
5592
- if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
5593
- return `${JSON.stringify(obj, null, 2)}
5594
- `;
5595
- }
5596
- function goalStatePath(cwd, goalId) {
5597
- return path20.join(cwd, ".kody", "goals", goalId, "state.json");
5598
- }
5599
- function readGoalState(cwd, goalId) {
5600
- const file = goalStatePath(cwd, goalId);
5601
- if (!fs20.existsSync(file)) {
5602
- throw new GoalStateError(file, "file not found");
5603
- }
5604
- let raw;
5605
- try {
5606
- raw = JSON.parse(fs20.readFileSync(file, "utf-8"));
5607
- } catch (err) {
5608
- throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
5609
- }
5610
- return parseGoalState(file, raw);
5611
- }
5612
- function writeGoalState(cwd, goalId, state) {
5613
- const file = goalStatePath(cwd, goalId);
5614
- fs20.mkdirSync(path20.dirname(file), { recursive: true });
5615
- fs20.writeFileSync(file, serializeGoalState(state), "utf-8");
5616
- }
5617
- function nowIso() {
5618
- return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
5619
- }
5620
-
5621
5246
  // src/scripts/finalizeGoal.ts
5622
5247
  var finalizeGoal = async (ctx) => {
5623
5248
  const goal = ctx.data.goal;
5624
5249
  if (!goal) return;
5625
- process.stdout.write(`[goal-tick] all task(s) closed \u2014 finalising goal ${goal.id}
5626
- `);
5627
- if (!remoteBranchExists(goal.goalBranch, ctx.cwd)) {
5628
- process.stderr.write(`[goal-tick] goal branch ${goal.goalBranch} not found on origin \u2014 skipping final PR
5629
- `);
5630
- finishState(goal);
5631
- return;
5632
- }
5633
- const title = `goal: ${goal.id}`;
5634
- const closesLine = goal.goalIssueNumber ? `
5635
-
5636
- Closes #${goal.goalIssueNumber}
5637
- ` : "\n";
5638
- const body = `Final integration PR for goal **${goal.id}**.
5639
-
5640
- All task issues are closed and merged into \`${goal.goalBranch}\`. Ready for review.${closesLine}`;
5641
- const existing = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
5642
- if (existing.ok && existing.value && existing.value.length > 0) {
5643
- const pr = existing.value[0];
5644
- goal.goalPrUrl = pr.url;
5645
- const edit = editPrBody(pr.number, body, ctx.cwd);
5646
- if (!edit.ok) {
5647
- process.stderr.write(`[goal-tick] finalizeGoal: editPrBody failed: ${edit.error}
5250
+ process.stdout.write(`[goal-tick] all task(s) done \u2014 finalising goal ${goal.id}
5648
5251
  `);
5649
- }
5650
- if (pr.isDraft) {
5651
- process.stdout.write(`[goal-tick] promoting draft goal PR #${pr.number} to ready-for-review
5252
+ const leaf = goal.leafPr;
5253
+ if (leaf) {
5254
+ if (leaf.isDraft) {
5255
+ process.stdout.write(`[goal-tick] promoting draft leaf PR #${leaf.number} \u2192 ready
5652
5256
  `);
5653
- const ready = markPrReady(pr.number, ctx.cwd);
5257
+ const ready = markPrReady(leaf.number, ctx.cwd);
5654
5258
  if (!ready.ok) {
5655
5259
  process.stderr.write(`[goal-tick] finalizeGoal: markPrReady failed: ${ready.error}
5656
5260
  `);
5261
+ return;
5657
5262
  }
5658
5263
  }
5659
- } else {
5660
- const created = createPr(
5661
- {
5662
- head: goal.goalBranch,
5663
- base: goal.defaultBranch,
5664
- title,
5665
- body,
5666
- // ready-for-review (not draft) since we're finalizing.
5667
- draft: false
5668
- },
5669
- ctx.cwd
5670
- );
5671
- if (!created.ok) {
5672
- process.stderr.write(`[goal-tick] finalizeGoal: gh pr create failed: ${created.error}
5264
+ process.stdout.write(`[goal-tick] squash-merging leaf PR #${leaf.number}
5673
5265
  `);
5674
- } else {
5675
- goal.goalPrUrl = created.value;
5266
+ const merged = mergePrSquash(leaf.number, ctx.cwd);
5267
+ if (!merged.ok) {
5268
+ process.stderr.write(`[goal-tick] finalizeGoal: mergePrSquash failed: ${merged.error}
5269
+ `);
5270
+ return;
5676
5271
  }
5272
+ } else {
5273
+ process.stdout.write(`[goal-tick] no leaf PR \u2014 marking goal done without merge
5274
+ `);
5677
5275
  }
5678
- finishState(goal);
5679
- };
5680
- function finishState(goal) {
5681
5276
  goal.state = "done";
5682
- goal.completedAt = nowIso();
5683
- }
5277
+ };
5684
5278
 
5685
5279
  // src/scripts/finishFlow.ts
5686
- import { execFileSync as execFileSync15 } from "child_process";
5280
+ import { execFileSync as execFileSync14 } from "child_process";
5687
5281
  var API_TIMEOUT_MS6 = 3e4;
5688
5282
  var STATUS_ICON = {
5689
5283
  "review-passed": "\u2705",
@@ -5717,7 +5311,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
5717
5311
  **PR:** ${state.core.prUrl}` : "";
5718
5312
  const body = `${icon} kody flow \`${flowName}\` finished \u2014 \`${reason}\`${prSuffix}`;
5719
5313
  try {
5720
- execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5314
+ execFileSync14("gh", ["issue", "comment", String(issueNumber), "--body", body], {
5721
5315
  timeout: API_TIMEOUT_MS6,
5722
5316
  cwd: ctx.cwd,
5723
5317
  stdio: ["ignore", "pipe", "pipe"]
@@ -5731,7 +5325,7 @@ var finishFlow = async (ctx, _profile, _agentResult, args) => {
5731
5325
  };
5732
5326
 
5733
5327
  // src/branch.ts
5734
- import { execFileSync as execFileSync16 } from "child_process";
5328
+ import { execFileSync as execFileSync15 } from "child_process";
5735
5329
  var UncommittedChangesError = class extends Error {
5736
5330
  constructor(branch) {
5737
5331
  super(`Uncommitted changes on branch '${branch}' \u2014 refusing to run to protect work in progress`);
@@ -5741,7 +5335,7 @@ var UncommittedChangesError = class extends Error {
5741
5335
  branch;
5742
5336
  };
5743
5337
  function git2(args, cwd) {
5744
- return execFileSync16("git", args, {
5338
+ return execFileSync15("git", args, {
5745
5339
  encoding: "utf-8",
5746
5340
  timeout: 3e4,
5747
5341
  cwd,
@@ -5766,7 +5360,15 @@ function checkoutPrBranch(prNumber, cwd) {
5766
5360
  SKIP_HOOKS: "1",
5767
5361
  GH_TOKEN: process.env.GH_PAT?.trim() || process.env.GH_TOKEN || ""
5768
5362
  };
5769
- execFileSync16("gh", ["pr", "checkout", String(prNumber)], {
5363
+ try {
5364
+ execFileSync15("git", ["reset", "--hard", "HEAD"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5365
+ } catch {
5366
+ }
5367
+ try {
5368
+ execFileSync15("git", ["clean", "-fd"], { cwd, env, stdio: ["ignore", "pipe", "pipe"], timeout: 3e4 });
5369
+ } catch {
5370
+ }
5371
+ execFileSync15("gh", ["pr", "checkout", String(prNumber)], {
5770
5372
  cwd,
5771
5373
  env,
5772
5374
  stdio: ["ignore", "pipe", "pipe"],
@@ -5880,8 +5482,8 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd, baseBranch)
5880
5482
  }
5881
5483
 
5882
5484
  // src/gha.ts
5883
- import { execFileSync as execFileSync17 } from "child_process";
5884
- import * as fs21 from "fs";
5485
+ import { execFileSync as execFileSync16 } from "child_process";
5486
+ import * as fs20 from "fs";
5885
5487
  function getRunUrl() {
5886
5488
  const server = process.env.GITHUB_SERVER_URL;
5887
5489
  const repo = process.env.GITHUB_REPOSITORY;
@@ -5892,10 +5494,10 @@ function getRunUrl() {
5892
5494
  function reactToTriggerComment(cwd) {
5893
5495
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
5894
5496
  const eventPath = process.env.GITHUB_EVENT_PATH;
5895
- if (!eventPath || !fs21.existsSync(eventPath)) return;
5497
+ if (!eventPath || !fs20.existsSync(eventPath)) return;
5896
5498
  let event = null;
5897
5499
  try {
5898
- event = JSON.parse(fs21.readFileSync(eventPath, "utf-8"));
5500
+ event = JSON.parse(fs20.readFileSync(eventPath, "utf-8"));
5899
5501
  } catch {
5900
5502
  return;
5901
5503
  }
@@ -5923,7 +5525,7 @@ function reactToTriggerComment(cwd) {
5923
5525
  for (let attempt = 0; attempt < 3; attempt++) {
5924
5526
  if (attempt > 0) sleepMs(attempt === 1 ? 500 : 1500);
5925
5527
  try {
5926
- execFileSync17("gh", args, opts);
5528
+ execFileSync16("gh", args, opts);
5927
5529
  return;
5928
5530
  } catch (err) {
5929
5531
  lastErr = err;
@@ -5936,13 +5538,13 @@ function reactToTriggerComment(cwd) {
5936
5538
  }
5937
5539
  function sleepMs(ms) {
5938
5540
  try {
5939
- execFileSync17("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5541
+ execFileSync16("sleep", [(ms / 1e3).toString()], { stdio: "ignore", timeout: ms + 1e3 });
5940
5542
  } catch {
5941
5543
  }
5942
5544
  }
5943
5545
 
5944
5546
  // src/workflow.ts
5945
- import { execFileSync as execFileSync18 } from "child_process";
5547
+ import { execFileSync as execFileSync17 } from "child_process";
5946
5548
  var GH_TIMEOUT_MS = 3e4;
5947
5549
  function ghToken3() {
5948
5550
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
@@ -5950,7 +5552,7 @@ function ghToken3() {
5950
5552
  function gh3(args, cwd) {
5951
5553
  const token = ghToken3();
5952
5554
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
5953
- return execFileSync18("gh", args, {
5555
+ return execFileSync17("gh", args, {
5954
5556
  encoding: "utf-8",
5955
5557
  timeout: GH_TIMEOUT_MS,
5956
5558
  cwd,
@@ -6139,13 +5741,15 @@ var handleAbandonedGoal = async (ctx) => {
6139
5741
  if (!goal || goal.state !== "abandoned") return;
6140
5742
  process.stdout.write(`[goal-tick] ${goal.id} is abandoned \u2014 running cleanup
6141
5743
  `);
6142
- const issues = listGoalIssues(goal.id, goal.goalIssueNumber, ctx.cwd);
5744
+ const issues = listGoalIssues(goal.id, ctx.cwd);
6143
5745
  if (!issues.ok) {
6144
- process.stderr.write(`[goal-tick] handleAbandonedGoal: list failed: ${issues.error}
5746
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: list issues failed: ${issues.error}
6145
5747
  `);
6146
5748
  } else {
5749
+ const issueNumbers = /* @__PURE__ */ new Set();
6147
5750
  for (const i of issues.value ?? []) {
6148
5751
  if (i.state !== "OPEN") continue;
5752
+ issueNumbers.add(i.number);
6149
5753
  const r = closeIssue(
6150
5754
  i.number,
6151
5755
  {
@@ -6159,37 +5763,41 @@ var handleAbandonedGoal = async (ctx) => {
6159
5763
  `);
6160
5764
  }
6161
5765
  }
6162
- }
6163
- const goalPrs = listPrsByHead(goal.goalBranch, "open", ctx.cwd);
6164
- if (goalPrs.ok && goalPrs.value && goalPrs.value.length > 0) {
6165
- const pr = goalPrs.value[0];
6166
- const r = closePr(pr.number, "_Goal abandoned by operator \u2014 closing without merge._", ctx.cwd);
6167
- if (!r.ok) {
6168
- process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close goal PR #${pr.number}: ${r.error}
5766
+ const prs = listOpenPrs(ctx.cwd);
5767
+ if (prs.ok) {
5768
+ for (const pr of prs.value ?? []) {
5769
+ const headMatch = pr.headRefName.match(/^(\d+)-/);
5770
+ const headIssue = headMatch ? Number.parseInt(headMatch[1], 10) : NaN;
5771
+ if (!Number.isFinite(headIssue) || !issueNumbers.has(headIssue)) continue;
5772
+ const r = closePr(pr.number, "_Goal abandoned \u2014 closing stacked PR._", ctx.cwd);
5773
+ if (!r.ok) {
5774
+ process.stderr.write(`[goal-tick] handleAbandonedGoal: failed to close PR #${pr.number}: ${r.error}
6169
5775
  `);
5776
+ }
5777
+ }
6170
5778
  }
6171
5779
  }
6172
5780
  goal.state = "closed";
6173
5781
  };
6174
5782
 
6175
5783
  // src/scripts/initFlow.ts
6176
- import { execFileSync as execFileSync19 } from "child_process";
6177
- import * as fs23 from "fs";
6178
- import * as path22 from "path";
6179
-
6180
- // src/scripts/loadQaGuide.ts
5784
+ import { execFileSync as execFileSync18 } from "child_process";
6181
5785
  import * as fs22 from "fs";
6182
5786
  import * as path21 from "path";
5787
+
5788
+ // src/scripts/loadQaGuide.ts
5789
+ import * as fs21 from "fs";
5790
+ import * as path20 from "path";
6183
5791
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
6184
5792
  var loadQaGuide = async (ctx) => {
6185
- const full = path21.join(ctx.cwd, QA_GUIDE_REL_PATH);
6186
- if (!fs22.existsSync(full)) {
5793
+ const full = path20.join(ctx.cwd, QA_GUIDE_REL_PATH);
5794
+ if (!fs21.existsSync(full)) {
6187
5795
  ctx.data.qaGuide = "";
6188
5796
  ctx.data.qaGuidePath = "";
6189
5797
  return;
6190
5798
  }
6191
5799
  try {
6192
- ctx.data.qaGuide = fs22.readFileSync(full, "utf-8");
5800
+ ctx.data.qaGuide = fs21.readFileSync(full, "utf-8");
6193
5801
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
6194
5802
  } catch {
6195
5803
  ctx.data.qaGuide = "";
@@ -6199,9 +5807,9 @@ var loadQaGuide = async (ctx) => {
6199
5807
 
6200
5808
  // src/scripts/initFlow.ts
6201
5809
  function detectPackageManager(cwd) {
6202
- if (fs23.existsSync(path22.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
6203
- if (fs23.existsSync(path22.join(cwd, "yarn.lock"))) return "yarn";
6204
- if (fs23.existsSync(path22.join(cwd, "bun.lockb"))) return "bun";
5810
+ if (fs22.existsSync(path21.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
5811
+ if (fs22.existsSync(path21.join(cwd, "yarn.lock"))) return "yarn";
5812
+ if (fs22.existsSync(path21.join(cwd, "bun.lockb"))) return "bun";
6205
5813
  return "npm";
6206
5814
  }
6207
5815
  function qualityCommandsFor(pm) {
@@ -6214,7 +5822,7 @@ function qualityCommandsFor(pm) {
6214
5822
  function detectOwnerRepo(cwd) {
6215
5823
  let url;
6216
5824
  try {
6217
- url = execFileSync19("git", ["remote", "get-url", "origin"], {
5825
+ url = execFileSync18("git", ["remote", "get-url", "origin"], {
6218
5826
  cwd,
6219
5827
  encoding: "utf-8",
6220
5828
  stdio: ["ignore", "pipe", "pipe"]
@@ -6299,7 +5907,7 @@ jobs:
6299
5907
  `;
6300
5908
  function defaultBranchFromGit(cwd) {
6301
5909
  try {
6302
- const ref = execFileSync19("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
5910
+ const ref = execFileSync18("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
6303
5911
  cwd,
6304
5912
  encoding: "utf-8",
6305
5913
  stdio: ["ignore", "pipe", "pipe"]
@@ -6307,7 +5915,7 @@ function defaultBranchFromGit(cwd) {
6307
5915
  return ref.replace("refs/remotes/origin/", "");
6308
5916
  } catch {
6309
5917
  try {
6310
- return execFileSync19("git", ["branch", "--show-current"], {
5918
+ return execFileSync18("git", ["branch", "--show-current"], {
6311
5919
  cwd,
6312
5920
  encoding: "utf-8",
6313
5921
  stdio: ["ignore", "pipe", "pipe"]
@@ -6323,48 +5931,48 @@ function performInit(cwd, force) {
6323
5931
  const pm = detectPackageManager(cwd);
6324
5932
  const ownerRepo = detectOwnerRepo(cwd);
6325
5933
  const defaultBranch = defaultBranchFromGit(cwd);
6326
- const configPath = path22.join(cwd, "kody.config.json");
6327
- if (fs23.existsSync(configPath) && !force) {
5934
+ const configPath = path21.join(cwd, "kody.config.json");
5935
+ if (fs22.existsSync(configPath) && !force) {
6328
5936
  skipped.push("kody.config.json");
6329
5937
  } else {
6330
5938
  const cfg = makeConfig(pm, ownerRepo, defaultBranch);
6331
- fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
5939
+ fs22.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
6332
5940
  `);
6333
5941
  wrote.push("kody.config.json");
6334
5942
  }
6335
- const workflowDir = path22.join(cwd, ".github", "workflows");
6336
- const workflowPath = path22.join(workflowDir, "kody.yml");
6337
- if (fs23.existsSync(workflowPath) && !force) {
5943
+ const workflowDir = path21.join(cwd, ".github", "workflows");
5944
+ const workflowPath = path21.join(workflowDir, "kody.yml");
5945
+ if (fs22.existsSync(workflowPath) && !force) {
6338
5946
  skipped.push(".github/workflows/kody.yml");
6339
5947
  } else {
6340
- fs23.mkdirSync(workflowDir, { recursive: true });
6341
- fs23.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
5948
+ fs22.mkdirSync(workflowDir, { recursive: true });
5949
+ fs22.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
6342
5950
  wrote.push(".github/workflows/kody.yml");
6343
5951
  }
6344
- const hasUi = fs23.existsSync(path22.join(cwd, "src/app")) || fs23.existsSync(path22.join(cwd, "app")) || fs23.existsSync(path22.join(cwd, "pages"));
5952
+ const hasUi = fs22.existsSync(path21.join(cwd, "src/app")) || fs22.existsSync(path21.join(cwd, "app")) || fs22.existsSync(path21.join(cwd, "pages"));
6345
5953
  if (hasUi) {
6346
- const qaGuidePath = path22.join(cwd, QA_GUIDE_REL_PATH);
6347
- if (fs23.existsSync(qaGuidePath) && !force) {
5954
+ const qaGuidePath = path21.join(cwd, QA_GUIDE_REL_PATH);
5955
+ if (fs22.existsSync(qaGuidePath) && !force) {
6348
5956
  skipped.push(QA_GUIDE_REL_PATH);
6349
5957
  } else {
6350
- fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
5958
+ fs22.mkdirSync(path21.dirname(qaGuidePath), { recursive: true });
6351
5959
  const discovery = runQaDiscovery(cwd);
6352
- fs23.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
5960
+ fs22.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
6353
5961
  wrote.push(QA_GUIDE_REL_PATH);
6354
5962
  }
6355
5963
  }
6356
5964
  const builtinJobs = listBuiltinJobs();
6357
5965
  if (builtinJobs.length > 0) {
6358
- const jobsDir = path22.join(cwd, ".kody", "jobs");
6359
- fs23.mkdirSync(jobsDir, { recursive: true });
5966
+ const jobsDir = path21.join(cwd, ".kody", "jobs");
5967
+ fs22.mkdirSync(jobsDir, { recursive: true });
6360
5968
  for (const job of builtinJobs) {
6361
- const rel = path22.join(".kody", "jobs", `${job.slug}.md`);
6362
- const target = path22.join(cwd, rel);
6363
- if (fs23.existsSync(target) && !force) {
5969
+ const rel = path21.join(".kody", "jobs", `${job.slug}.md`);
5970
+ const target = path21.join(cwd, rel);
5971
+ if (fs22.existsSync(target) && !force) {
6364
5972
  skipped.push(rel);
6365
5973
  continue;
6366
5974
  }
6367
- fs23.writeFileSync(target, fs23.readFileSync(job.filePath, "utf-8"));
5975
+ fs22.writeFileSync(target, fs22.readFileSync(job.filePath, "utf-8"));
6368
5976
  wrote.push(rel);
6369
5977
  }
6370
5978
  }
@@ -6376,12 +5984,12 @@ function performInit(cwd, force) {
6376
5984
  continue;
6377
5985
  }
6378
5986
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
6379
- const target = path22.join(workflowDir, `kody-${exe.name}.yml`);
6380
- if (fs23.existsSync(target) && !force) {
5987
+ const target = path21.join(workflowDir, `kody-${exe.name}.yml`);
5988
+ if (fs22.existsSync(target) && !force) {
6381
5989
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
6382
5990
  continue;
6383
5991
  }
6384
- fs23.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
5992
+ fs22.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
6385
5993
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
6386
5994
  }
6387
5995
  let labels;
@@ -6469,6 +6077,82 @@ var loadCoverageRules = async (ctx) => {
6469
6077
  ctx.data.coverageRules = ctx.config.testRequirements ?? [];
6470
6078
  };
6471
6079
 
6080
+ // src/goal/state.ts
6081
+ import * as fs23 from "fs";
6082
+ import * as path22 from "path";
6083
+ var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
6084
+ var GoalStateError = class extends Error {
6085
+ constructor(path29, message) {
6086
+ super(`Invalid goal state at ${path29}:
6087
+ ${message}`);
6088
+ this.path = path29;
6089
+ this.name = "GoalStateError";
6090
+ }
6091
+ path;
6092
+ };
6093
+ function parseGoalState(filePath, raw) {
6094
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
6095
+ throw new GoalStateError(filePath, "must be a JSON object");
6096
+ }
6097
+ const r = raw;
6098
+ const stateValue = r.state;
6099
+ if (typeof stateValue !== "string" || !VALID_STATES.has(stateValue)) {
6100
+ throw new GoalStateError(
6101
+ filePath,
6102
+ `"state" is required and must be one of: ${[...VALID_STATES].join(" | ")} (got ${JSON.stringify(stateValue)})`
6103
+ );
6104
+ }
6105
+ const parsed = {
6106
+ state: stateValue,
6107
+ extra: {}
6108
+ };
6109
+ if (typeof r.lastDispatchedIssue === "number" && Number.isFinite(r.lastDispatchedIssue)) {
6110
+ parsed.lastDispatchedIssue = r.lastDispatchedIssue;
6111
+ }
6112
+ for (const ts of ["updatedAt", "createdAt", "startedAt"]) {
6113
+ const v = r[ts];
6114
+ if (typeof v === "string" && v.length > 0) parsed[ts] = v;
6115
+ }
6116
+ const known = /* @__PURE__ */ new Set(["state", "lastDispatchedIssue", "updatedAt", "createdAt", "startedAt"]);
6117
+ for (const [k, v] of Object.entries(r)) {
6118
+ if (!known.has(k)) parsed.extra[k] = v;
6119
+ }
6120
+ return parsed;
6121
+ }
6122
+ function serializeGoalState(s) {
6123
+ const obj = { ...s.extra, state: s.state };
6124
+ if (s.lastDispatchedIssue !== void 0) obj.lastDispatchedIssue = s.lastDispatchedIssue;
6125
+ if (s.createdAt !== void 0) obj.createdAt = s.createdAt;
6126
+ if (s.startedAt !== void 0) obj.startedAt = s.startedAt;
6127
+ if (s.updatedAt !== void 0) obj.updatedAt = s.updatedAt;
6128
+ return `${JSON.stringify(obj, null, 2)}
6129
+ `;
6130
+ }
6131
+ function goalStatePath(cwd, goalId) {
6132
+ return path22.join(cwd, ".kody", "goals", goalId, "state.json");
6133
+ }
6134
+ function readGoalState(cwd, goalId) {
6135
+ const file = goalStatePath(cwd, goalId);
6136
+ if (!fs23.existsSync(file)) {
6137
+ throw new GoalStateError(file, "file not found");
6138
+ }
6139
+ let raw;
6140
+ try {
6141
+ raw = JSON.parse(fs23.readFileSync(file, "utf-8"));
6142
+ } catch (err) {
6143
+ throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
6144
+ }
6145
+ return parseGoalState(file, raw);
6146
+ }
6147
+ function writeGoalState(cwd, goalId, state) {
6148
+ const file = goalStatePath(cwd, goalId);
6149
+ fs23.mkdirSync(path22.dirname(file), { recursive: true });
6150
+ fs23.writeFileSync(file, serializeGoalState(state), "utf-8");
6151
+ }
6152
+ function nowIso() {
6153
+ return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
6154
+ }
6155
+
6472
6156
  // src/scripts/loadGoalState.ts
6473
6157
  var loadGoalState = async (ctx) => {
6474
6158
  const goalId = ctx.args.goal;
@@ -6489,18 +6173,14 @@ var loadGoalState = async (ctx) => {
6489
6173
  ctx.data.goal = {
6490
6174
  id: goalId,
6491
6175
  state: state.state,
6492
- goalIssueNumber: state.goalIssueNumber,
6493
6176
  lastDispatchedIssue: state.lastDispatchedIssue,
6494
- goalPrUrl: state.goalPrUrl,
6495
6177
  // Cache the full parsed object so saveGoalState can preserve `extra`.
6496
6178
  raw: state,
6497
- // `phase` is populated by deriveGoalPhase later in the chain. Initialize
6498
- // to undefined so runWhen on `data.goal.phase` can match correctly.
6179
+ // `phase`, `childTasks`, `openTaskPrs`, `leafPr` are populated by
6180
+ // deriveGoalPhase later in the chain. Initialize to undefined so
6181
+ // runWhen on `data.goal.phase` can match correctly.
6499
6182
  phase: void 0,
6500
- // Populated by ensureGoalBranch / configured by config.git.defaultBranch.
6501
- defaultBranch: ctx.config.git.defaultBranch,
6502
- // Convenience derivations.
6503
- goalBranch: `goal-${goalId}`
6183
+ defaultBranch: ctx.config.git.defaultBranch
6504
6184
  };
6505
6185
  } catch (err) {
6506
6186
  process.stdout.write(`[goal-tick] ${err instanceof Error ? err.message : String(err)}
@@ -6854,32 +6534,8 @@ var markFlowSuccess = async (ctx) => {
6854
6534
  }
6855
6535
  };
6856
6536
 
6857
- // src/scripts/mergeReadyTaskPRs.ts
6858
- var mergeReadyTaskPRs = async (ctx) => {
6859
- const goal = ctx.data.goal;
6860
- if (!goal) return;
6861
- const open = listPrsByBase(goal.goalBranch, "open", ctx.cwd);
6862
- if (!open.ok) {
6863
- process.stderr.write(`[goal-tick] mergeReadyTaskPRs: list failed: ${open.error}
6864
- `);
6865
- return;
6866
- }
6867
- for (const pr of open.value ?? []) {
6868
- if (pr.isDraft) continue;
6869
- if (pr.mergeable !== "MERGEABLE") continue;
6870
- if (pr.mergeStateStatus !== "CLEAN") continue;
6871
- process.stdout.write(`[goal-tick] merging PR #${pr.number} into ${goal.goalBranch}
6872
- `);
6873
- const r = mergePrSquash(pr.number, ctx.cwd);
6874
- if (!r.ok) {
6875
- process.stderr.write(`[goal-tick] failed to merge PR #${pr.number}: ${r.error} (continuing)
6876
- `);
6877
- }
6878
- }
6879
- };
6880
-
6881
6537
  // src/scripts/mergeReleasePr.ts
6882
- import { execFileSync as execFileSync20 } from "child_process";
6538
+ import { execFileSync as execFileSync19 } from "child_process";
6883
6539
  var API_TIMEOUT_MS7 = 6e4;
6884
6540
  var mergeReleasePr = async (ctx) => {
6885
6541
  const state = ctx.data.taskState;
@@ -6898,7 +6554,7 @@ var mergeReleasePr = async (ctx) => {
6898
6554
  process.stderr.write(`[kody mergeReleasePr] merging PR #${prNumber} (${prUrl})
6899
6555
  `);
6900
6556
  try {
6901
- const out = execFileSync20("gh", ["pr", "merge", String(prNumber), "--merge"], {
6557
+ const out = execFileSync19("gh", ["pr", "merge", String(prNumber), "--merge"], {
6902
6558
  timeout: API_TIMEOUT_MS7,
6903
6559
  cwd: ctx.cwd,
6904
6560
  stdio: ["ignore", "pipe", "pipe"]
@@ -7016,7 +6672,7 @@ function buildIssueTitle(scope, verdict) {
7016
6672
  const verdictTag = verdict === "UNKNOWN" ? "REPORT" : verdict;
7017
6673
  return `QA [${verdictTag}]: ${focus} \u2014 ${date}`.slice(0, 240);
7018
6674
  }
7019
- function ensureLabel3(cwd) {
6675
+ function ensureLabel2(cwd) {
7020
6676
  try {
7021
6677
  gh(["label", "create", QA_LABEL, "--color", "8b5cf6", "--description", "kody: QA report", "--force"], { cwd });
7022
6678
  return true;
@@ -7076,7 +6732,7 @@ QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.gith
7076
6732
  }
7077
6733
  const scope = ctx.args.scope;
7078
6734
  const title = buildIssueTitle(scope, verdict);
7079
- const hasLabel = ensureLabel3(ctx.cwd);
6735
+ const hasLabel = ensureLabel2(ctx.cwd);
7080
6736
  let created;
7081
6737
  try {
7082
6738
  created = createQaIssue(title, reportBody, hasLabel, ctx.cwd);
@@ -7512,7 +7168,7 @@ ${body}`;
7512
7168
  }
7513
7169
 
7514
7170
  // src/scripts/recordClassification.ts
7515
- import { execFileSync as execFileSync21 } from "child_process";
7171
+ import { execFileSync as execFileSync20 } from "child_process";
7516
7172
  var API_TIMEOUT_MS8 = 3e4;
7517
7173
  var VALID_CLASSES3 = /* @__PURE__ */ new Set(["feature", "bug", "spec", "chore"]);
7518
7174
  var recordClassification = async (ctx) => {
@@ -7560,7 +7216,7 @@ function parseClassification(prSummary) {
7560
7216
  }
7561
7217
  function tryAuditComment(issueNumber, body, cwd) {
7562
7218
  try {
7563
- execFileSync21("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7219
+ execFileSync20("gh", ["issue", "comment", String(issueNumber), "--body", body], {
7564
7220
  cwd,
7565
7221
  timeout: API_TIMEOUT_MS8,
7566
7222
  stdio: ["ignore", "pipe", "pipe"]
@@ -7683,7 +7339,7 @@ var resolveArtifacts = async (ctx, profile) => {
7683
7339
  };
7684
7340
 
7685
7341
  // src/scripts/resolveFlow.ts
7686
- import { execFileSync as execFileSync22 } from "child_process";
7342
+ import { execFileSync as execFileSync21 } from "child_process";
7687
7343
  var CONFLICT_DIFF_MAX_BYTES = 4e4;
7688
7344
  var resolveFlow = async (ctx) => {
7689
7345
  const prNumber = ctx.args.pr;
@@ -7776,7 +7432,7 @@ function buildPreferBlock(prefer, baseBranch) {
7776
7432
  }
7777
7433
  function getConflictedFiles(cwd) {
7778
7434
  try {
7779
- const out = execFileSync22("git", ["diff", "--name-only", "--diff-filter=U"], {
7435
+ const out = execFileSync21("git", ["diff", "--name-only", "--diff-filter=U"], {
7780
7436
  encoding: "utf-8",
7781
7437
  cwd,
7782
7438
  env: { ...process.env, HUSKY: "0" }
@@ -7791,7 +7447,7 @@ function getConflictMarkersPreview(files, cwd, maxBytes = CONFLICT_DIFF_MAX_BYTE
7791
7447
  let total = 0;
7792
7448
  for (const f of files) {
7793
7449
  try {
7794
- const content = execFileSync22("cat", [f], { encoding: "utf-8", cwd }).toString();
7450
+ const content = execFileSync21("cat", [f], { encoding: "utf-8", cwd }).toString();
7795
7451
  const snippet = `### ${f}
7796
7452
 
7797
7453
  \`\`\`
@@ -7815,12 +7471,12 @@ function tryPostPr3(prNumber, body, cwd) {
7815
7471
  function pushEmptyCommit(branch, cwd) {
7816
7472
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
7817
7473
  try {
7818
- execFileSync22(
7474
+ execFileSync21(
7819
7475
  "git",
7820
7476
  ["commit", "--allow-empty", "-m", "chore: kody resolve refresh \u2014 empty commit to recompute mergeable status"],
7821
7477
  { cwd, env, stdio: ["ignore", "pipe", "pipe"] }
7822
7478
  );
7823
- execFileSync22("git", ["push", "-u", "origin", branch], {
7479
+ execFileSync21("git", ["push", "-u", "origin", branch], {
7824
7480
  cwd,
7825
7481
  env,
7826
7482
  stdio: ["ignore", "pipe", "pipe"]
@@ -7910,10 +7566,10 @@ var resolvePreviewUrl = async (ctx) => {
7910
7566
  };
7911
7567
 
7912
7568
  // src/scripts/resolveQaUrl.ts
7913
- import { execFileSync as execFileSync23 } from "child_process";
7569
+ import { execFileSync as execFileSync22 } from "child_process";
7914
7570
  function ghQuery(args, cwd) {
7915
7571
  try {
7916
- const out = execFileSync23("gh", args, {
7572
+ const out = execFileSync22("gh", args, {
7917
7573
  cwd,
7918
7574
  stdio: ["ignore", "pipe", "pipe"],
7919
7575
  encoding: "utf-8",
@@ -7983,7 +7639,7 @@ var resolveQaUrl = async (ctx) => {
7983
7639
  };
7984
7640
 
7985
7641
  // src/scripts/revertFlow.ts
7986
- import { execFileSync as execFileSync24 } from "child_process";
7642
+ import { execFileSync as execFileSync23 } from "child_process";
7987
7643
  var SHA_RE = /^[0-9a-f]{4,40}$/i;
7988
7644
  var revertFlow = async (ctx) => {
7989
7645
  const prNumber = ctx.args.pr;
@@ -8065,7 +7721,7 @@ function buildPrSummary(resolved) {
8065
7721
  return resolved.map((r) => `- Reverted \`${r.full.slice(0, 7)}\`${r.subject ? ` \u2014 ${r.subject}` : ""}`).join("\n");
8066
7722
  }
8067
7723
  function git3(args, cwd) {
8068
- return execFileSync24("git", args, {
7724
+ return execFileSync23("git", args, {
8069
7725
  encoding: "utf-8",
8070
7726
  timeout: 3e4,
8071
7727
  cwd,
@@ -8075,7 +7731,7 @@ function git3(args, cwd) {
8075
7731
  }
8076
7732
  function isAncestorOfHead(sha, cwd) {
8077
7733
  try {
8078
- execFileSync24("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
7734
+ execFileSync23("git", ["merge-base", "--is-ancestor", sha, "HEAD"], {
8079
7735
  cwd,
8080
7736
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
8081
7737
  stdio: ["ignore", "ignore", "ignore"]
@@ -8319,10 +7975,7 @@ var saveGoalState = async (ctx) => {
8319
7975
  const updated = {
8320
7976
  ...goal.raw ?? { state: goal.state, extra: {} },
8321
7977
  state: goal.state,
8322
- goalIssueNumber: goal.goalIssueNumber,
8323
7978
  lastDispatchedIssue: goal.lastDispatchedIssue,
8324
- goalPrUrl: goal.goalPrUrl,
8325
- completedAt: goal.completedAt ?? goal.raw?.completedAt,
8326
7979
  updatedAt: nowIso()
8327
7980
  };
8328
7981
  writeGoalState(ctx.cwd, goal.id, updated);
@@ -8404,11 +8057,11 @@ var skipAgent = async (ctx) => {
8404
8057
  };
8405
8058
 
8406
8059
  // src/scripts/stageMergeConflicts.ts
8407
- import { execFileSync as execFileSync25 } from "child_process";
8060
+ import { execFileSync as execFileSync24 } from "child_process";
8408
8061
  var stageMergeConflicts = async (ctx) => {
8409
8062
  if (ctx.data.agentDone === false) return;
8410
8063
  try {
8411
- execFileSync25("git", ["add", "-A"], {
8064
+ execFileSync24("git", ["add", "-A"], {
8412
8065
  cwd: ctx.cwd,
8413
8066
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" },
8414
8067
  stdio: "pipe"
@@ -8418,7 +8071,7 @@ var stageMergeConflicts = async (ctx) => {
8418
8071
  };
8419
8072
 
8420
8073
  // src/scripts/startFlow.ts
8421
- import { execFileSync as execFileSync26 } from "child_process";
8074
+ import { execFileSync as execFileSync25 } from "child_process";
8422
8075
  var API_TIMEOUT_MS9 = 3e4;
8423
8076
  var startFlow = async (ctx, profile, _agentResult, args) => {
8424
8077
  const entry = args?.entry;
@@ -8452,7 +8105,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
8452
8105
  const sub = target === "pr" && state?.core.prUrl ? "pr" : "issue";
8453
8106
  const body = `@kody ${next}`;
8454
8107
  try {
8455
- execFileSync26("gh", [sub, "comment", String(targetNumber), "--body", body], {
8108
+ execFileSync25("gh", [sub, "comment", String(targetNumber), "--body", body], {
8456
8109
  timeout: API_TIMEOUT_MS9,
8457
8110
  cwd,
8458
8111
  stdio: ["ignore", "pipe", "pipe"]
@@ -8466,7 +8119,7 @@ function postKodyComment(target, issueNumber, state, next, cwd) {
8466
8119
  }
8467
8120
 
8468
8121
  // src/scripts/syncFlow.ts
8469
- import { execFileSync as execFileSync27 } from "child_process";
8122
+ import { execFileSync as execFileSync26 } from "child_process";
8470
8123
  var syncFlow = async (ctx, _profile, args) => {
8471
8124
  const announceOnSuccess = Boolean(args?.announceOnSuccess);
8472
8125
  const prNumber = ctx.args.pr;
@@ -8538,7 +8191,7 @@ function bail2(ctx, prNumber, reason) {
8538
8191
  }
8539
8192
  function revParseHead(cwd) {
8540
8193
  try {
8541
- return execFileSync27("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
8194
+ return execFileSync26("git", ["rev-parse", "HEAD"], { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).toString().trim();
8542
8195
  } catch {
8543
8196
  return "";
8544
8197
  }
@@ -8546,9 +8199,9 @@ function revParseHead(cwd) {
8546
8199
  function pushBranch(branch, cwd) {
8547
8200
  const env = { ...process.env, HUSKY: "0", SKIP_HOOKS: "1" };
8548
8201
  try {
8549
- execFileSync27("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
8202
+ execFileSync26("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
8550
8203
  } catch {
8551
- execFileSync27("git", ["push", "--force-with-lease", "-u", "origin", branch], {
8204
+ execFileSync26("git", ["push", "--force-with-lease", "-u", "origin", branch], {
8552
8205
  cwd,
8553
8206
  env,
8554
8207
  stdio: ["ignore", "pipe", "pipe"]
@@ -8812,7 +8465,7 @@ function downgrade2(ctx, reason) {
8812
8465
  }
8813
8466
 
8814
8467
  // src/scripts/waitForCi.ts
8815
- import { execFileSync as execFileSync28 } from "child_process";
8468
+ import { execFileSync as execFileSync27 } from "child_process";
8816
8469
  var API_TIMEOUT_MS10 = 3e4;
8817
8470
  var waitForCi = async (ctx, _profile, _agentResult, args) => {
8818
8471
  const timeoutMinutes = numArg(args, "timeoutMinutes", 30);
@@ -8890,7 +8543,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
8890
8543
  };
8891
8544
  function fetchChecks(prNumber, cwd) {
8892
8545
  try {
8893
- const raw = execFileSync28("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
8546
+ const raw = execFileSync27("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
8894
8547
  encoding: "utf-8",
8895
8548
  timeout: API_TIMEOUT_MS10,
8896
8549
  cwd,
@@ -9212,13 +8865,7 @@ var preflightScripts = {
9212
8865
  runTickScript,
9213
8866
  loadGoalState,
9214
8867
  handleAbandonedGoal,
9215
- ensureLifecycleLabels,
9216
- ensureUmbrellaIssue,
9217
- ensureGoalPr,
9218
- mergeReadyTaskPRs,
9219
- closeMergedTaskIssues,
9220
8868
  deriveGoalPhase,
9221
- ensureGoalBranch,
9222
8869
  dispatchNextTask,
9223
8870
  finalizeGoal,
9224
8871
  saveGoalState
@@ -9269,7 +8916,7 @@ var allScriptNames = /* @__PURE__ */ new Set([
9269
8916
  ]);
9270
8917
 
9271
8918
  // src/tools.ts
9272
- import { execFileSync as execFileSync29 } from "child_process";
8919
+ import { execFileSync as execFileSync28 } from "child_process";
9273
8920
  function verifyCliTools(tools, cwd) {
9274
8921
  const out = [];
9275
8922
  for (const t of tools) out.push(verifyOne(t, cwd));
@@ -9302,7 +8949,7 @@ function verifyOne(tool, cwd) {
9302
8949
  }
9303
8950
  function runShell(cmd, cwd, timeoutMs = 3e4) {
9304
8951
  try {
9305
- execFileSync29("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
8952
+ execFileSync28("sh", ["-c", cmd], { cwd, stdio: "pipe", timeout: timeoutMs });
9306
8953
  return true;
9307
8954
  } catch {
9308
8955
  return false;
@@ -9858,7 +9505,7 @@ async function runContainerLoop(profile, ctx, input) {
9858
9505
  }
9859
9506
  function resetWorkingTree(cwd) {
9860
9507
  try {
9861
- execFileSync30("git", ["reset", "--hard", "HEAD"], {
9508
+ execFileSync29("git", ["reset", "--hard", "HEAD"], {
9862
9509
  cwd,
9863
9510
  stdio: ["ignore", "pipe", "pipe"],
9864
9511
  timeout: 3e4
@@ -10017,7 +9664,7 @@ function detectPackageManager2(cwd) {
10017
9664
  }
10018
9665
  function shellOut(cmd, args, cwd, stream = true) {
10019
9666
  try {
10020
- execFileSync31(cmd, args, {
9667
+ execFileSync30(cmd, args, {
10021
9668
  cwd,
10022
9669
  stdio: stream ? "inherit" : "pipe",
10023
9670
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" }
@@ -10030,7 +9677,7 @@ function shellOut(cmd, args, cwd, stream = true) {
10030
9677
  }
10031
9678
  function isOnPath(bin) {
10032
9679
  try {
10033
- execFileSync31("which", [bin], { stdio: "pipe" });
9680
+ execFileSync30("which", [bin], { stdio: "pipe" });
10034
9681
  return true;
10035
9682
  } catch {
10036
9683
  return false;
@@ -10071,7 +9718,7 @@ function installLitellmIfNeeded(cwd) {
10071
9718
  } catch {
10072
9719
  }
10073
9720
  try {
10074
- execFileSync31("python3", ["-c", "import litellm"], { stdio: "pipe" });
9721
+ execFileSync30("python3", ["-c", "import litellm"], { stdio: "pipe" });
10075
9722
  process.stdout.write("\u2192 kody: litellm already installed\n");
10076
9723
  return 0;
10077
9724
  } catch {
@@ -10081,16 +9728,16 @@ function installLitellmIfNeeded(cwd) {
10081
9728
  }
10082
9729
  function configureGitIdentity(cwd) {
10083
9730
  try {
10084
- const name = execFileSync31("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
9731
+ const name = execFileSync30("git", ["config", "user.name"], { cwd, stdio: "pipe", encoding: "utf-8" }).trim();
10085
9732
  if (name) return;
10086
9733
  } catch {
10087
9734
  }
10088
9735
  try {
10089
- execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
9736
+ execFileSync30("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
10090
9737
  } catch {
10091
9738
  }
10092
9739
  try {
10093
- execFileSync31("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
9740
+ execFileSync30("git", ["config", "user.email", "41898282+github-actions[bot]@users.noreply.github.com"], {
10094
9741
  cwd,
10095
9742
  stdio: "pipe"
10096
9743
  });
@@ -10391,9 +10038,9 @@ function commitChatFiles(cwd, sessionId, verbose) {
10391
10038
  if (paths.length === 0) return;
10392
10039
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
10393
10040
  try {
10394
- execFileSync32("git", ["add", "-f", ...paths], opts);
10395
- execFileSync32("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
10396
- execFileSync32("git", ["push", "--quiet", "origin", "HEAD"], opts);
10041
+ execFileSync31("git", ["add", "-f", ...paths], opts);
10042
+ execFileSync31("git", ["commit", "--quiet", "-m", `chat: reply for ${sessionId}`], opts);
10043
+ execFileSync31("git", ["push", "--quiet", "origin", "HEAD"], opts);
10397
10044
  } catch (err) {
10398
10045
  const msg = err instanceof Error ? err.message : String(err);
10399
10046
  process.stderr.write(`[kody:chat] commit/push skipped: ${msg}