@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 +1059 -208
- package/dist/executables/goal-tick/profile.json +49 -2
- package/package.json +1 -1
- package/dist/executables/goal-tick/tick.sh +0 -596
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.
|
|
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
|
|
55
|
-
import * as
|
|
56
|
-
import * as
|
|
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
|
|
916
|
-
import * as
|
|
917
|
-
import * as
|
|
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
|
|
1303
|
-
import * as
|
|
1304
|
-
import * as
|
|
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
|
|
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 ?
|
|
2804
|
-
mode ?
|
|
2805
|
-
|
|
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
|
|
3241
|
+
import { execFileSync as execFileSync11 } from "child_process";
|
|
2895
3242
|
import * as fs14 from "fs";
|
|
2896
|
-
import * as
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
3248
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
3831
|
+
import * as path15 from "path";
|
|
3442
3832
|
var diagMcp = async (_ctx) => {
|
|
3443
3833
|
const home = os3.homedir();
|
|
3444
|
-
const cacheDir =
|
|
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 =
|
|
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
|
|
3864
|
+
import * as path17 from "path";
|
|
3475
3865
|
|
|
3476
3866
|
// src/scripts/frameworkDetectors.ts
|
|
3477
3867
|
import * as fs16 from "fs";
|
|
3478
|
-
import * as
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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(
|
|
3976
|
+
(f) => fs16.existsSync(path16.join(entryPath, f))
|
|
3587
3977
|
);
|
|
3588
3978
|
if (!indexFile) continue;
|
|
3589
3979
|
name = entry.name;
|
|
3590
|
-
filePath =
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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:
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
4395
|
-
fs18.mkdirSync(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
5065
|
-
import * as
|
|
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 || !
|
|
5789
|
+
if (!eventPath || !fs21.existsSync(eventPath)) return;
|
|
5077
5790
|
let event = null;
|
|
5078
5791
|
try {
|
|
5079
|
-
event = JSON.parse(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
5319
|
-
import * as
|
|
5320
|
-
import * as
|
|
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
|
|
5324
|
-
import * as
|
|
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 =
|
|
5328
|
-
if (!
|
|
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 =
|
|
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 (
|
|
5345
|
-
if (
|
|
5346
|
-
if (
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
5469
|
-
if (
|
|
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
|
-
|
|
6225
|
+
fs23.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
5474
6226
|
`);
|
|
5475
6227
|
wrote.push("kody.config.json");
|
|
5476
6228
|
}
|
|
5477
|
-
const workflowDir =
|
|
5478
|
-
const workflowPath =
|
|
5479
|
-
if (
|
|
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
|
-
|
|
5483
|
-
|
|
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 =
|
|
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 =
|
|
5489
|
-
if (
|
|
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
|
-
|
|
6244
|
+
fs23.mkdirSync(path22.dirname(qaGuidePath), { recursive: true });
|
|
5493
6245
|
const discovery = runQaDiscovery(cwd);
|
|
5494
|
-
|
|
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 =
|
|
5501
|
-
|
|
6252
|
+
const jobsDir = path22.join(cwd, ".kody", "jobs");
|
|
6253
|
+
fs23.mkdirSync(jobsDir, { recursive: true });
|
|
5502
6254
|
for (const job of builtinJobs) {
|
|
5503
|
-
const rel =
|
|
5504
|
-
const target =
|
|
5505
|
-
if (
|
|
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
|
-
|
|
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 =
|
|
5522
|
-
if (
|
|
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
|
-
|
|
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
|
|
5665
|
-
import * as
|
|
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 =
|
|
5674
|
-
if (!
|
|
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 =
|
|
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
|
|
5710
|
-
import * as
|
|
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 =
|
|
5718
|
-
if (!
|
|
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 =
|
|
6537
|
+
stat = fs25.statSync(file);
|
|
5744
6538
|
} catch {
|
|
5745
6539
|
return;
|
|
5746
6540
|
}
|
|
5747
6541
|
let raw;
|
|
5748
6542
|
try {
|
|
5749
|
-
raw =
|
|
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() ??
|
|
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:
|
|
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 =
|
|
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 =
|
|
6625
|
+
const full = path24.join(dir, name);
|
|
5832
6626
|
let stat;
|
|
5833
6627
|
try {
|
|
5834
|
-
stat =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
7735
|
+
import { execFileSync as execFileSync23 } from "child_process";
|
|
6918
7736
|
function ghQuery(args, cwd) {
|
|
6919
7737
|
try {
|
|
6920
|
-
const out =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
7179
|
-
if (!
|
|
7180
|
-
const goalId =
|
|
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
|
|
7188
|
-
import * as
|
|
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 =
|
|
7201
|
-
if (!
|
|
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 =
|
|
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 =
|
|
7215
|
-
if (!
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
8371
|
+
execFileSync27("git", ["push", "-u", "origin", branch], { cwd, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
7534
8372
|
} catch {
|
|
7535
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
9159
|
+
const ndjsonDir = path26.join(input.cwd, ".kody");
|
|
8309
9160
|
const invokeAgent = async (prompt) => {
|
|
8310
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
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 =
|
|
9270
|
+
const here = path26.dirname(new URL(import.meta.url).pathname);
|
|
8420
9271
|
const candidates = [
|
|
8421
|
-
|
|
9272
|
+
path26.join(here, "executables", profileName, "profile.json"),
|
|
8422
9273
|
// same-dir sibling (dev)
|
|
8423
|
-
|
|
9274
|
+
path26.join(here, "..", "executables", profileName, "profile.json"),
|
|
8424
9275
|
// up one (prod: dist/bin → dist/executables)
|
|
8425
|
-
|
|
9276
|
+
path26.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
8426
9277
|
// fallback
|
|
8427
9278
|
];
|
|
8428
9279
|
for (const c of candidates) {
|
|
8429
|
-
if (
|
|
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 =
|
|
8534
|
-
if (!
|
|
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
|
-
|
|
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 (
|
|
8948
|
-
if (
|
|
8949
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
9874
|
+
execFileSync31("git", ["config", "user.name", "github-actions[bot]"], { cwd, stdio: "pipe" });
|
|
9024
9875
|
} catch {
|
|
9025
9876
|
}
|
|
9026
9877
|
try {
|
|
9027
|
-
|
|
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 =
|
|
9887
|
+
const logPath = path27.join(cwd, ".kody", "last-run.jsonl");
|
|
9037
9888
|
let tail = "";
|
|
9038
9889
|
try {
|
|
9039
|
-
if (
|
|
9040
|
-
const content =
|
|
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 ?
|
|
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 &&
|
|
9926
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs29.existsSync(dispatchEventPath)) {
|
|
9076
9927
|
try {
|
|
9077
|
-
const evt = JSON.parse(
|
|
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 =
|
|
9293
|
-
const eventsFile =
|
|
9294
|
-
const paths = [sessionFile, eventsFile].filter((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
|
-
|
|
9299
|
-
|
|
9300
|
-
|
|
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 ?
|
|
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=${
|
|
10235
|
+
`\u2192 kody:chat: session file=${sessionFile} exists=${fs30.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
|
|
9385
10236
|
`
|
|
9386
10237
|
);
|
|
9387
10238
|
try {
|