@kody-ade/kody-engine 0.3.58 → 0.3.61
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 +589 -346
- package/dist/executables/release-deploy/deploy.sh +0 -0
- package/dist/executables/release-prepare/prepare.sh +0 -0
- package/dist/executables/release-publish/publish.sh +0 -0
- package/dist/executables/resolve/apply-prefer.sh +0 -0
- package/dist/executables/revert/revert.sh +0 -0
- package/dist/executables/run/profile.json +1 -1
- package/kody.config.schema.json +13 -0
- package/package.json +16 -14
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.3.
|
|
6
|
+
version: "0.3.61",
|
|
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",
|
|
@@ -28,6 +28,7 @@ var package_default = {
|
|
|
28
28
|
prepublishOnly: "pnpm build"
|
|
29
29
|
},
|
|
30
30
|
dependencies: {
|
|
31
|
+
"@actions/cache": "^6.0.0",
|
|
31
32
|
"@anthropic-ai/claude-agent-sdk": "0.2.119"
|
|
32
33
|
},
|
|
33
34
|
devDependencies: {
|
|
@@ -51,8 +52,8 @@ var package_default = {
|
|
|
51
52
|
|
|
52
53
|
// src/chat-cli.ts
|
|
53
54
|
import { execFileSync as execFileSync26 } from "child_process";
|
|
54
|
-
import * as
|
|
55
|
-
import * as
|
|
55
|
+
import * as fs27 from "fs";
|
|
56
|
+
import * as path24 from "path";
|
|
56
57
|
|
|
57
58
|
// src/chat/events.ts
|
|
58
59
|
import * as fs from "fs";
|
|
@@ -186,9 +187,23 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
186
187
|
defaultPrExecutable: typeof raw.defaultPrExecutable === "string" && raw.defaultPrExecutable.length > 0 ? raw.defaultPrExecutable : "fix",
|
|
187
188
|
aliases: mergeAliases(raw.aliases),
|
|
188
189
|
classify: parseClassifyConfig(raw.classify),
|
|
189
|
-
release: parseReleaseConfig(raw.release)
|
|
190
|
+
release: parseReleaseConfig(raw.release),
|
|
191
|
+
missions: parseMissionsConfig(raw.missions)
|
|
190
192
|
};
|
|
191
193
|
}
|
|
194
|
+
function parseMissionsConfig(raw) {
|
|
195
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
196
|
+
const r = raw;
|
|
197
|
+
const out = {};
|
|
198
|
+
if (r.stateBackend === "contents-api" || r.stateBackend === "local-file") {
|
|
199
|
+
out.stateBackend = r.stateBackend;
|
|
200
|
+
} else if (typeof r.stateBackend === "string") {
|
|
201
|
+
throw new Error(
|
|
202
|
+
`kody.config.json: missions.stateBackend must be "contents-api" or "local-file", got "${r.stateBackend}"`
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
206
|
+
}
|
|
192
207
|
var BUILTIN_ALIASES = {
|
|
193
208
|
build: "run",
|
|
194
209
|
orchestrate: "bug",
|
|
@@ -525,12 +540,35 @@ var CHAT_SYSTEM_PROMPT = [
|
|
|
525
540
|
" pytest, go test, cargo, etc., whatever the project uses).",
|
|
526
541
|
" - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
|
|
527
542
|
"",
|
|
528
|
-
"#
|
|
529
|
-
"
|
|
530
|
-
"
|
|
531
|
-
"
|
|
532
|
-
"
|
|
533
|
-
"
|
|
543
|
+
"# Investigate before you answer (HARD RULE)",
|
|
544
|
+
"Do not answer from assumptions, training memory, or what the code 'probably'",
|
|
545
|
+
"does. Before replying to any question about this repo \u2014 its code, behavior,",
|
|
546
|
+
"config, history, issues, PRs, CI, or dependencies \u2014 you MUST first ground the",
|
|
547
|
+
"answer in concrete evidence collected in THIS session.",
|
|
548
|
+
"",
|
|
549
|
+
"Required pre-reply protocol for every non-trivial question:",
|
|
550
|
+
"1. Locate the relevant code with Glob/Grep. Don't guess paths.",
|
|
551
|
+
"2. Read the actual files end-to-end (or the relevant ranges). Read more than",
|
|
552
|
+
" you think you need \u2014 adjacent files, callers, tests, types.",
|
|
553
|
+
"3. If behavior depends on runtime state (CI, PRs, issues, git history), run",
|
|
554
|
+
" the matching `gh` / `git` / shell command and look at the real output.",
|
|
555
|
+
"4. Only after steps 1\u20133 do you compose the reply.",
|
|
556
|
+
"",
|
|
557
|
+
"Every factual claim about this repo in your reply must be backed by something",
|
|
558
|
+
"you actually read or executed in this session. Cite the source inline:",
|
|
559
|
+
"`path/to/file.ts:42`, `git show <sha>`, `gh pr view 123`, etc. If you cannot",
|
|
560
|
+
"produce a citation, you have not investigated enough \u2014 go back to step 1.",
|
|
561
|
+
"",
|
|
562
|
+
"Forbidden phrasings unless preceded by an actual tool failure quoted in your",
|
|
563
|
+
"reply: 'I don't have access', 'I can't see', 'it likely', 'it probably',",
|
|
564
|
+
"'typically this would', 'based on common patterns'. These are tells that you",
|
|
565
|
+
"skipped investigation \u2014 replace them with the result of the investigation.",
|
|
566
|
+
"",
|
|
567
|
+
"Speed is not the goal \u2014 correctness grounded in this specific codebase is.",
|
|
568
|
+
"Spend the tool calls. A short answer with three citations beats a long answer",
|
|
569
|
+
"with zero. If a question is genuinely trivial (greeting, clarification,",
|
|
570
|
+
"definition of a generic term unrelated to this repo), you may answer without",
|
|
571
|
+
"tools \u2014 but err on the side of investigating.",
|
|
534
572
|
"",
|
|
535
573
|
"Do not invent file paths, commit SHAs, line numbers, or command output. If you",
|
|
536
574
|
"cite something concrete, you must have just read or run it in this session."
|
|
@@ -606,8 +644,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
606
644
|
|
|
607
645
|
// src/kody-cli.ts
|
|
608
646
|
import { execFileSync as execFileSync25 } from "child_process";
|
|
609
|
-
import * as
|
|
610
|
-
import * as
|
|
647
|
+
import * as fs26 from "fs";
|
|
648
|
+
import * as path23 from "path";
|
|
611
649
|
|
|
612
650
|
// src/dispatch.ts
|
|
613
651
|
import * as fs6 from "fs";
|
|
@@ -960,8 +998,8 @@ function coerceBare(spec, value) {
|
|
|
960
998
|
|
|
961
999
|
// src/executor.ts
|
|
962
1000
|
import { spawn as spawn3 } from "child_process";
|
|
963
|
-
import * as
|
|
964
|
-
import * as
|
|
1001
|
+
import * as fs25 from "fs";
|
|
1002
|
+
import * as path22 from "path";
|
|
965
1003
|
|
|
966
1004
|
// src/litellm.ts
|
|
967
1005
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -1981,7 +2019,7 @@ function parseAgentResult(finalText) {
|
|
|
1981
2019
|
};
|
|
1982
2020
|
}
|
|
1983
2021
|
const hasDoneMarker = DONE_RE.test(text);
|
|
1984
|
-
const hasCommitMsg = /^[\s>*_
|
|
2022
|
+
const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
|
|
1985
2023
|
if (!hasDoneMarker && !hasCommitMsg) {
|
|
1986
2024
|
return {
|
|
1987
2025
|
done: false,
|
|
@@ -1993,7 +2031,7 @@ function parseAgentResult(finalText) {
|
|
|
1993
2031
|
failureReason: "no DONE or FAILED marker in agent output"
|
|
1994
2032
|
};
|
|
1995
2033
|
}
|
|
1996
|
-
const commitMatch = text.match(/^[\s>*_
|
|
2034
|
+
const commitMatch = text.match(/^[\s>*_#`~-]*COMMIT_MSG[\s>*_#`~-]*\s*:\s*(.+)$/im);
|
|
1997
2035
|
const commitMessage = commitMatch ? stripMarkdownEmphasis(commitMatch[1]) : "";
|
|
1998
2036
|
const feedbackActions = extractBlock(
|
|
1999
2037
|
text,
|
|
@@ -2851,62 +2889,8 @@ function failedAction(reason) {
|
|
|
2851
2889
|
}
|
|
2852
2890
|
|
|
2853
2891
|
// src/scripts/dispatchMissionFileTicks.ts
|
|
2854
|
-
import * as
|
|
2855
|
-
import * as
|
|
2856
|
-
var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
2857
|
-
ctx.skipAgent = true;
|
|
2858
|
-
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
2859
|
-
if (!targetExecutable) {
|
|
2860
|
-
throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
|
|
2861
|
-
}
|
|
2862
|
-
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
2863
|
-
const slugArg = String(args?.slugArg ?? "mission");
|
|
2864
|
-
const slugs = listMissionSlugs(path15.join(ctx.cwd, missionsDir));
|
|
2865
|
-
ctx.data.missionSlugCount = slugs.length;
|
|
2866
|
-
if (slugs.length === 0) {
|
|
2867
|
-
process.stdout.write(`[missions] no mission files in ${missionsDir}
|
|
2868
|
-
`);
|
|
2869
|
-
return;
|
|
2870
|
-
}
|
|
2871
|
-
process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
|
|
2872
|
-
`);
|
|
2873
|
-
const results = [];
|
|
2874
|
-
for (const slug of slugs) {
|
|
2875
|
-
process.stdout.write(`[missions] \u2192 tick ${slug}
|
|
2876
|
-
`);
|
|
2877
|
-
try {
|
|
2878
|
-
const out = await runExecutable(targetExecutable, {
|
|
2879
|
-
cliArgs: { [slugArg]: slug },
|
|
2880
|
-
cwd: ctx.cwd,
|
|
2881
|
-
config: ctx.config,
|
|
2882
|
-
verbose: ctx.verbose,
|
|
2883
|
-
quiet: ctx.quiet
|
|
2884
|
-
});
|
|
2885
|
-
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
2886
|
-
if (out.exitCode !== 0) {
|
|
2887
|
-
process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
2888
|
-
`);
|
|
2889
|
-
}
|
|
2890
|
-
} catch (err) {
|
|
2891
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
2892
|
-
process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
|
|
2893
|
-
`);
|
|
2894
|
-
results.push({ slug, exitCode: 99, reason: msg });
|
|
2895
|
-
}
|
|
2896
|
-
}
|
|
2897
|
-
ctx.data.missionTickResults = results;
|
|
2898
|
-
ctx.output.exitCode = 0;
|
|
2899
|
-
};
|
|
2900
|
-
function listMissionSlugs(absDir) {
|
|
2901
|
-
if (!fs16.existsSync(absDir)) return [];
|
|
2902
|
-
let entries;
|
|
2903
|
-
try {
|
|
2904
|
-
entries = fs16.readdirSync(absDir, { withFileTypes: true });
|
|
2905
|
-
} catch {
|
|
2906
|
-
return [];
|
|
2907
|
-
}
|
|
2908
|
-
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).filter((slug) => slug.length > 0 && !slug.startsWith("_") && !slug.startsWith(".")).sort();
|
|
2909
|
-
}
|
|
2892
|
+
import * as fs17 from "fs";
|
|
2893
|
+
import * as path16 from "path";
|
|
2910
2894
|
|
|
2911
2895
|
// src/issue.ts
|
|
2912
2896
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
@@ -3049,6 +3033,411 @@ function postPrReviewComment(prNumber, body, cwd) {
|
|
|
3049
3033
|
}
|
|
3050
3034
|
}
|
|
3051
3035
|
|
|
3036
|
+
// src/scripts/issueStateComment.ts
|
|
3037
|
+
function isStateEnvelope(x) {
|
|
3038
|
+
if (x === null || typeof x !== "object") return false;
|
|
3039
|
+
const o = x;
|
|
3040
|
+
return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
3041
|
+
}
|
|
3042
|
+
function initialStateEnvelope(cursor = "seed") {
|
|
3043
|
+
return { version: 1, rev: 0, cursor, data: {}, done: false };
|
|
3044
|
+
}
|
|
3045
|
+
function formatStateCommentBody(marker, state) {
|
|
3046
|
+
return `<!-- ${marker} -->
|
|
3047
|
+
|
|
3048
|
+
\`\`\`json
|
|
3049
|
+
${JSON.stringify(state, null, 2)}
|
|
3050
|
+
\`\`\`
|
|
3051
|
+
`;
|
|
3052
|
+
}
|
|
3053
|
+
function parseStateCommentBody(marker, body) {
|
|
3054
|
+
const markerLine = `<!-- ${marker} -->`;
|
|
3055
|
+
if (!body.trimStart().startsWith(markerLine)) return null;
|
|
3056
|
+
const fenceOpen = body.indexOf("```json");
|
|
3057
|
+
if (fenceOpen === -1) return null;
|
|
3058
|
+
const after = body.slice(fenceOpen + "```json".length);
|
|
3059
|
+
const fenceClose = after.indexOf("```");
|
|
3060
|
+
if (fenceClose === -1) return null;
|
|
3061
|
+
const jsonText = after.slice(0, fenceClose).trim();
|
|
3062
|
+
let parsed;
|
|
3063
|
+
try {
|
|
3064
|
+
parsed = JSON.parse(jsonText);
|
|
3065
|
+
} catch {
|
|
3066
|
+
return null;
|
|
3067
|
+
}
|
|
3068
|
+
return isStateEnvelope(parsed) ? parsed : null;
|
|
3069
|
+
}
|
|
3070
|
+
function listIssueComments(owner, repo, issueNumber, cwd) {
|
|
3071
|
+
const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
|
|
3072
|
+
let parsed;
|
|
3073
|
+
try {
|
|
3074
|
+
parsed = JSON.parse(raw);
|
|
3075
|
+
} catch {
|
|
3076
|
+
return [];
|
|
3077
|
+
}
|
|
3078
|
+
if (!Array.isArray(parsed)) return [];
|
|
3079
|
+
return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
|
|
3080
|
+
}
|
|
3081
|
+
function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
3082
|
+
const comments = listIssueComments(owner, repo, issueNumber, cwd);
|
|
3083
|
+
for (const c of comments) {
|
|
3084
|
+
const state = parseStateCommentBody(marker, c.body);
|
|
3085
|
+
if (!state) continue;
|
|
3086
|
+
return { commentId: c.id, commentNodeId: c.node_id, state };
|
|
3087
|
+
}
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
3091
|
+
const body = formatStateCommentBody(marker, state);
|
|
3092
|
+
const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
3093
|
+
cwd,
|
|
3094
|
+
input: JSON.stringify({ body })
|
|
3095
|
+
});
|
|
3096
|
+
const parsed = JSON.parse(raw);
|
|
3097
|
+
try {
|
|
3098
|
+
minimizeComment(parsed.node_id, cwd);
|
|
3099
|
+
} catch {
|
|
3100
|
+
}
|
|
3101
|
+
return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
|
|
3102
|
+
}
|
|
3103
|
+
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
3104
|
+
const body = formatStateCommentBody(marker, state);
|
|
3105
|
+
gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
3106
|
+
cwd,
|
|
3107
|
+
input: JSON.stringify({ body })
|
|
3108
|
+
});
|
|
3109
|
+
try {
|
|
3110
|
+
minimizeComment(commentNodeId, cwd);
|
|
3111
|
+
} catch {
|
|
3112
|
+
}
|
|
3113
|
+
}
|
|
3114
|
+
function minimizeComment(nodeId, cwd) {
|
|
3115
|
+
const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
|
|
3116
|
+
gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
// src/scripts/missionState/backend.ts
|
|
3120
|
+
function isStateUnchanged(prev, next) {
|
|
3121
|
+
if (prev.cursor !== next.cursor) return false;
|
|
3122
|
+
if (prev.done !== next.done) return false;
|
|
3123
|
+
return JSON.stringify(prev.data) === JSON.stringify(next.data);
|
|
3124
|
+
}
|
|
3125
|
+
function stateFilePath(missionsDir, slug) {
|
|
3126
|
+
return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
3127
|
+
}
|
|
3128
|
+
function slugFromStateFilePath(filePath) {
|
|
3129
|
+
const last = filePath.split("/").pop() ?? filePath;
|
|
3130
|
+
return last.replace(/\.state\.json$/i, "");
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
// src/scripts/missionState/contentsApiBackend.ts
|
|
3134
|
+
var ContentsApiBackend = class {
|
|
3135
|
+
name = "contents-api";
|
|
3136
|
+
owner;
|
|
3137
|
+
repo;
|
|
3138
|
+
missionsDir;
|
|
3139
|
+
cwd;
|
|
3140
|
+
constructor(opts) {
|
|
3141
|
+
if (!opts.owner || !opts.repo) {
|
|
3142
|
+
throw new Error("ContentsApiBackend: owner and repo are required");
|
|
3143
|
+
}
|
|
3144
|
+
this.owner = opts.owner;
|
|
3145
|
+
this.repo = opts.repo;
|
|
3146
|
+
this.missionsDir = opts.missionsDir;
|
|
3147
|
+
this.cwd = opts.cwd;
|
|
3148
|
+
}
|
|
3149
|
+
load(slug) {
|
|
3150
|
+
const filePath = stateFilePath(this.missionsDir, slug);
|
|
3151
|
+
let raw = "";
|
|
3152
|
+
try {
|
|
3153
|
+
raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
|
|
3154
|
+
} catch (err) {
|
|
3155
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3156
|
+
if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
|
|
3157
|
+
return { path: filePath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
3158
|
+
}
|
|
3159
|
+
throw err;
|
|
3160
|
+
}
|
|
3161
|
+
let parsed;
|
|
3162
|
+
try {
|
|
3163
|
+
parsed = JSON.parse(raw);
|
|
3164
|
+
} catch {
|
|
3165
|
+
throw new Error(`ContentsApiBackend: contents API for ${filePath} did not return JSON`);
|
|
3166
|
+
}
|
|
3167
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3168
|
+
throw new Error(`ContentsApiBackend: contents API for ${filePath} returned non-object`);
|
|
3169
|
+
}
|
|
3170
|
+
const o = parsed;
|
|
3171
|
+
if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
|
|
3172
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not a base64 file`);
|
|
3173
|
+
}
|
|
3174
|
+
const decoded = Buffer.from(o.content, "base64").toString("utf-8");
|
|
3175
|
+
let envelope;
|
|
3176
|
+
try {
|
|
3177
|
+
envelope = JSON.parse(decoded);
|
|
3178
|
+
} catch {
|
|
3179
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not valid JSON`);
|
|
3180
|
+
}
|
|
3181
|
+
if (!isStateEnvelope(envelope)) {
|
|
3182
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not a StateEnvelope`);
|
|
3183
|
+
}
|
|
3184
|
+
return { path: filePath, handle: o.sha, state: envelope, created: false };
|
|
3185
|
+
}
|
|
3186
|
+
save(loaded, next) {
|
|
3187
|
+
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
3188
|
+
return false;
|
|
3189
|
+
}
|
|
3190
|
+
const slug = slugFromStateFilePath(loaded.path);
|
|
3191
|
+
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3192
|
+
const payload = {
|
|
3193
|
+
message: `chore(missions): update state for ${slug} (rev ${next.rev})`,
|
|
3194
|
+
content: Buffer.from(body, "utf-8").toString("base64")
|
|
3195
|
+
};
|
|
3196
|
+
if (typeof loaded.handle === "string") payload.sha = loaded.handle;
|
|
3197
|
+
gh2(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
|
|
3198
|
+
cwd: this.cwd,
|
|
3199
|
+
input: JSON.stringify(payload)
|
|
3200
|
+
});
|
|
3201
|
+
return true;
|
|
3202
|
+
}
|
|
3203
|
+
};
|
|
3204
|
+
|
|
3205
|
+
// src/scripts/missionState/localFileBackend.ts
|
|
3206
|
+
import * as fs16 from "fs";
|
|
3207
|
+
import * as path15 from "path";
|
|
3208
|
+
var LocalFileBackend = class {
|
|
3209
|
+
name = "local-file";
|
|
3210
|
+
cwd;
|
|
3211
|
+
missionsDir;
|
|
3212
|
+
absDir;
|
|
3213
|
+
owner;
|
|
3214
|
+
repo;
|
|
3215
|
+
cache;
|
|
3216
|
+
constructor(opts) {
|
|
3217
|
+
if (!opts.cwd) throw new Error("LocalFileBackend: cwd is required");
|
|
3218
|
+
if (!opts.missionsDir) throw new Error("LocalFileBackend: missionsDir is required");
|
|
3219
|
+
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
3220
|
+
this.cwd = opts.cwd;
|
|
3221
|
+
this.missionsDir = opts.missionsDir;
|
|
3222
|
+
this.absDir = path15.join(opts.cwd, opts.missionsDir);
|
|
3223
|
+
this.owner = opts.owner;
|
|
3224
|
+
this.repo = opts.repo;
|
|
3225
|
+
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
3226
|
+
}
|
|
3227
|
+
/**
|
|
3228
|
+
* Restore the mission directory from the most recent Actions cache entry
|
|
3229
|
+
* for this repo. No-op when not running in Actions or when no cache exists.
|
|
3230
|
+
*/
|
|
3231
|
+
async hydrate() {
|
|
3232
|
+
if (!this.cache.isAvailable()) {
|
|
3233
|
+
process.stdout.write(`[missions/state] hydrate skipped: actions cache unavailable
|
|
3234
|
+
`);
|
|
3235
|
+
return;
|
|
3236
|
+
}
|
|
3237
|
+
fs16.mkdirSync(this.absDir, { recursive: true });
|
|
3238
|
+
const prefix = this.cacheKeyPrefix();
|
|
3239
|
+
const probeKey = `${prefix}probe-${Date.now()}`;
|
|
3240
|
+
try {
|
|
3241
|
+
const matched = await this.cache.restore([this.absDir], probeKey, [prefix]);
|
|
3242
|
+
if (matched) {
|
|
3243
|
+
process.stdout.write(`[missions/state] hydrate hit: ${matched}
|
|
3244
|
+
`);
|
|
3245
|
+
} else {
|
|
3246
|
+
process.stdout.write(`[missions/state] hydrate miss (cold start)
|
|
3247
|
+
`);
|
|
3248
|
+
}
|
|
3249
|
+
} catch (err) {
|
|
3250
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3251
|
+
process.stderr.write(`[missions/state] hydrate failed (continuing): ${msg}
|
|
3252
|
+
`);
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Save the mission directory to the Actions cache under a unique key.
|
|
3257
|
+
* No-op when not running in Actions. Errors are logged, never thrown —
|
|
3258
|
+
* callers run this in a finally block and must not swallow real errors.
|
|
3259
|
+
*/
|
|
3260
|
+
async persist() {
|
|
3261
|
+
if (!this.cache.isAvailable()) {
|
|
3262
|
+
process.stdout.write(`[missions/state] persist skipped: actions cache unavailable
|
|
3263
|
+
`);
|
|
3264
|
+
return;
|
|
3265
|
+
}
|
|
3266
|
+
if (!fs16.existsSync(this.absDir)) {
|
|
3267
|
+
return;
|
|
3268
|
+
}
|
|
3269
|
+
const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
|
|
3270
|
+
try {
|
|
3271
|
+
await this.cache.save([this.absDir], key);
|
|
3272
|
+
process.stdout.write(`[missions/state] persist saved: ${key}
|
|
3273
|
+
`);
|
|
3274
|
+
} catch (err) {
|
|
3275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3276
|
+
process.stderr.write(`[missions/state] persist failed (continuing): ${msg}
|
|
3277
|
+
`);
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
load(slug) {
|
|
3281
|
+
const relPath = stateFilePath(this.missionsDir, slug);
|
|
3282
|
+
const absPath = path15.join(this.cwd, relPath);
|
|
3283
|
+
if (!fs16.existsSync(absPath)) {
|
|
3284
|
+
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
3285
|
+
}
|
|
3286
|
+
const raw = fs16.readFileSync(absPath, "utf-8");
|
|
3287
|
+
let parsed;
|
|
3288
|
+
try {
|
|
3289
|
+
parsed = JSON.parse(raw);
|
|
3290
|
+
} catch (err) {
|
|
3291
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3292
|
+
throw new Error(`LocalFileBackend: ${relPath} is not valid JSON: ${msg}`);
|
|
3293
|
+
}
|
|
3294
|
+
if (!isStateEnvelope(parsed)) {
|
|
3295
|
+
throw new Error(`LocalFileBackend: ${relPath} is not a StateEnvelope`);
|
|
3296
|
+
}
|
|
3297
|
+
return { path: relPath, handle: null, state: parsed, created: false };
|
|
3298
|
+
}
|
|
3299
|
+
save(loaded, next) {
|
|
3300
|
+
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
3301
|
+
return false;
|
|
3302
|
+
}
|
|
3303
|
+
const absPath = path15.join(this.cwd, loaded.path);
|
|
3304
|
+
fs16.mkdirSync(path15.dirname(absPath), { recursive: true });
|
|
3305
|
+
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3306
|
+
fs16.writeFileSync(absPath, body, "utf-8");
|
|
3307
|
+
return true;
|
|
3308
|
+
}
|
|
3309
|
+
cacheKeyPrefix() {
|
|
3310
|
+
return `kody-mission-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
|
|
3311
|
+
}
|
|
3312
|
+
};
|
|
3313
|
+
function sanitizeKey(s) {
|
|
3314
|
+
return s.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
3315
|
+
}
|
|
3316
|
+
function defaultCacheAdapter() {
|
|
3317
|
+
let mod = null;
|
|
3318
|
+
const load = async () => {
|
|
3319
|
+
if (!mod) {
|
|
3320
|
+
mod = await import("@actions/cache");
|
|
3321
|
+
}
|
|
3322
|
+
return mod;
|
|
3323
|
+
};
|
|
3324
|
+
const available = () => {
|
|
3325
|
+
if (process.env.GITHUB_ACTIONS !== "true") return false;
|
|
3326
|
+
return Boolean(process.env.ACTIONS_CACHE_URL || process.env.ACTIONS_RESULTS_URL);
|
|
3327
|
+
};
|
|
3328
|
+
return {
|
|
3329
|
+
isAvailable: available,
|
|
3330
|
+
async restore(paths, primaryKey, restoreKeys) {
|
|
3331
|
+
if (!available()) return void 0;
|
|
3332
|
+
const m = await load();
|
|
3333
|
+
return m.restoreCache(paths, primaryKey, restoreKeys);
|
|
3334
|
+
},
|
|
3335
|
+
async save(paths, primaryKey) {
|
|
3336
|
+
if (!available()) return;
|
|
3337
|
+
const m = await load();
|
|
3338
|
+
try {
|
|
3339
|
+
await m.saveCache(paths, primaryKey);
|
|
3340
|
+
} catch (err) {
|
|
3341
|
+
const name = err?.name ?? "";
|
|
3342
|
+
if (name === "ReserveCacheError") return;
|
|
3343
|
+
throw err;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
// src/scripts/missionState/index.ts
|
|
3350
|
+
function resolveBackend(opts) {
|
|
3351
|
+
const owner = opts.config.github?.owner;
|
|
3352
|
+
const repo = opts.config.github?.repo;
|
|
3353
|
+
if (!owner || !repo) {
|
|
3354
|
+
throw new Error("resolveBackend: config.github.owner and config.github.repo must be set");
|
|
3355
|
+
}
|
|
3356
|
+
const requested = opts.config.missions?.stateBackend ?? "contents-api";
|
|
3357
|
+
switch (requested) {
|
|
3358
|
+
case "contents-api":
|
|
3359
|
+
return new ContentsApiBackend({ owner, repo, missionsDir: opts.missionsDir, cwd: opts.cwd });
|
|
3360
|
+
case "local-file":
|
|
3361
|
+
return new LocalFileBackend({ cwd: opts.cwd, missionsDir: opts.missionsDir, owner, repo });
|
|
3362
|
+
default: {
|
|
3363
|
+
const _exhaustive = requested;
|
|
3364
|
+
throw new Error(`resolveBackend: unknown stateBackend "${String(_exhaustive)}"`);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
// src/scripts/dispatchMissionFileTicks.ts
|
|
3370
|
+
var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
3371
|
+
ctx.skipAgent = true;
|
|
3372
|
+
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3373
|
+
if (!targetExecutable) {
|
|
3374
|
+
throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
|
|
3375
|
+
}
|
|
3376
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
3377
|
+
const slugArg = String(args?.slugArg ?? "mission");
|
|
3378
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
3379
|
+
if (backend.hydrate) {
|
|
3380
|
+
await backend.hydrate();
|
|
3381
|
+
}
|
|
3382
|
+
try {
|
|
3383
|
+
const slugs = listMissionSlugs(path16.join(ctx.cwd, missionsDir));
|
|
3384
|
+
ctx.data.missionSlugCount = slugs.length;
|
|
3385
|
+
if (slugs.length === 0) {
|
|
3386
|
+
process.stdout.write(`[missions] no mission files in ${missionsDir}
|
|
3387
|
+
`);
|
|
3388
|
+
return;
|
|
3389
|
+
}
|
|
3390
|
+
process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
|
|
3391
|
+
`);
|
|
3392
|
+
const results = [];
|
|
3393
|
+
for (const slug of slugs) {
|
|
3394
|
+
process.stdout.write(`[missions] \u2192 tick ${slug}
|
|
3395
|
+
`);
|
|
3396
|
+
try {
|
|
3397
|
+
const out = await runExecutable(targetExecutable, {
|
|
3398
|
+
cliArgs: { [slugArg]: slug },
|
|
3399
|
+
cwd: ctx.cwd,
|
|
3400
|
+
config: ctx.config,
|
|
3401
|
+
verbose: ctx.verbose,
|
|
3402
|
+
quiet: ctx.quiet
|
|
3403
|
+
});
|
|
3404
|
+
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
3405
|
+
if (out.exitCode !== 0) {
|
|
3406
|
+
process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3407
|
+
`);
|
|
3408
|
+
}
|
|
3409
|
+
} catch (err) {
|
|
3410
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3411
|
+
process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
|
|
3412
|
+
`);
|
|
3413
|
+
results.push({ slug, exitCode: 99, reason: msg });
|
|
3414
|
+
}
|
|
3415
|
+
}
|
|
3416
|
+
ctx.data.missionTickResults = results;
|
|
3417
|
+
ctx.output.exitCode = 0;
|
|
3418
|
+
} finally {
|
|
3419
|
+
if (backend.persist) {
|
|
3420
|
+
try {
|
|
3421
|
+
await backend.persist();
|
|
3422
|
+
} catch (err) {
|
|
3423
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3424
|
+
process.stderr.write(`[missions] backend persist failed: ${msg}
|
|
3425
|
+
`);
|
|
3426
|
+
}
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
};
|
|
3430
|
+
function listMissionSlugs(absDir) {
|
|
3431
|
+
if (!fs17.existsSync(absDir)) return [];
|
|
3432
|
+
let entries;
|
|
3433
|
+
try {
|
|
3434
|
+
entries = fs17.readdirSync(absDir, { withFileTypes: true });
|
|
3435
|
+
} catch {
|
|
3436
|
+
return [];
|
|
3437
|
+
}
|
|
3438
|
+
return entries.filter((e) => e.isFile() && e.name.endsWith(".md")).map((e) => e.name.replace(/\.md$/, "")).filter((slug) => slug.length > 0 && !slug.startsWith("_") && !slug.startsWith(".")).sort();
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3052
3441
|
// src/scripts/dispatchMissionTicks.ts
|
|
3053
3442
|
var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
3054
3443
|
ctx.skipAgent = true;
|
|
@@ -3681,7 +4070,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
3681
4070
|
|
|
3682
4071
|
// src/gha.ts
|
|
3683
4072
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
3684
|
-
import * as
|
|
4073
|
+
import * as fs18 from "fs";
|
|
3685
4074
|
function getRunUrl() {
|
|
3686
4075
|
const server = process.env.GITHUB_SERVER_URL;
|
|
3687
4076
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -3692,10 +4081,10 @@ function getRunUrl() {
|
|
|
3692
4081
|
function reactToTriggerComment(cwd) {
|
|
3693
4082
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
3694
4083
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3695
|
-
if (!eventPath || !
|
|
4084
|
+
if (!eventPath || !fs18.existsSync(eventPath)) return;
|
|
3696
4085
|
let event = null;
|
|
3697
4086
|
try {
|
|
3698
|
-
event = JSON.parse(
|
|
4087
|
+
event = JSON.parse(fs18.readFileSync(eventPath, "utf-8"));
|
|
3699
4088
|
} catch {
|
|
3700
4089
|
return;
|
|
3701
4090
|
}
|
|
@@ -3935,22 +4324,22 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
3935
4324
|
|
|
3936
4325
|
// src/scripts/initFlow.ts
|
|
3937
4326
|
import { execFileSync as execFileSync14 } from "child_process";
|
|
3938
|
-
import * as
|
|
3939
|
-
import * as
|
|
4327
|
+
import * as fs20 from "fs";
|
|
4328
|
+
import * as path18 from "path";
|
|
3940
4329
|
|
|
3941
4330
|
// src/scripts/loadQaGuide.ts
|
|
3942
|
-
import * as
|
|
3943
|
-
import * as
|
|
4331
|
+
import * as fs19 from "fs";
|
|
4332
|
+
import * as path17 from "path";
|
|
3944
4333
|
var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
|
|
3945
4334
|
var loadQaGuide = async (ctx) => {
|
|
3946
|
-
const full =
|
|
3947
|
-
if (!
|
|
4335
|
+
const full = path17.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
4336
|
+
if (!fs19.existsSync(full)) {
|
|
3948
4337
|
ctx.data.qaGuide = "";
|
|
3949
4338
|
ctx.data.qaGuidePath = "";
|
|
3950
4339
|
return;
|
|
3951
4340
|
}
|
|
3952
4341
|
try {
|
|
3953
|
-
ctx.data.qaGuide =
|
|
4342
|
+
ctx.data.qaGuide = fs19.readFileSync(full, "utf-8");
|
|
3954
4343
|
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
3955
4344
|
} catch {
|
|
3956
4345
|
ctx.data.qaGuide = "";
|
|
@@ -3960,9 +4349,9 @@ var loadQaGuide = async (ctx) => {
|
|
|
3960
4349
|
|
|
3961
4350
|
// src/scripts/initFlow.ts
|
|
3962
4351
|
function detectPackageManager(cwd) {
|
|
3963
|
-
if (
|
|
3964
|
-
if (
|
|
3965
|
-
if (
|
|
4352
|
+
if (fs20.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4353
|
+
if (fs20.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
|
|
4354
|
+
if (fs20.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
|
|
3966
4355
|
return "npm";
|
|
3967
4356
|
}
|
|
3968
4357
|
function qualityCommandsFor(pm) {
|
|
@@ -4084,33 +4473,33 @@ function performInit(cwd, force) {
|
|
|
4084
4473
|
const pm = detectPackageManager(cwd);
|
|
4085
4474
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
4086
4475
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
4087
|
-
const configPath =
|
|
4088
|
-
if (
|
|
4476
|
+
const configPath = path18.join(cwd, "kody.config.json");
|
|
4477
|
+
if (fs20.existsSync(configPath) && !force) {
|
|
4089
4478
|
skipped.push("kody.config.json");
|
|
4090
4479
|
} else {
|
|
4091
4480
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
4092
|
-
|
|
4481
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
4093
4482
|
`);
|
|
4094
4483
|
wrote.push("kody.config.json");
|
|
4095
4484
|
}
|
|
4096
|
-
const workflowDir =
|
|
4097
|
-
const workflowPath =
|
|
4098
|
-
if (
|
|
4485
|
+
const workflowDir = path18.join(cwd, ".github", "workflows");
|
|
4486
|
+
const workflowPath = path18.join(workflowDir, "kody.yml");
|
|
4487
|
+
if (fs20.existsSync(workflowPath) && !force) {
|
|
4099
4488
|
skipped.push(".github/workflows/kody.yml");
|
|
4100
4489
|
} else {
|
|
4101
|
-
|
|
4102
|
-
|
|
4490
|
+
fs20.mkdirSync(workflowDir, { recursive: true });
|
|
4491
|
+
fs20.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
4103
4492
|
wrote.push(".github/workflows/kody.yml");
|
|
4104
4493
|
}
|
|
4105
|
-
const hasUi =
|
|
4494
|
+
const hasUi = fs20.existsSync(path18.join(cwd, "src/app")) || fs20.existsSync(path18.join(cwd, "app")) || fs20.existsSync(path18.join(cwd, "pages"));
|
|
4106
4495
|
if (hasUi) {
|
|
4107
|
-
const qaGuidePath =
|
|
4108
|
-
if (
|
|
4496
|
+
const qaGuidePath = path18.join(cwd, QA_GUIDE_REL_PATH);
|
|
4497
|
+
if (fs20.existsSync(qaGuidePath) && !force) {
|
|
4109
4498
|
skipped.push(QA_GUIDE_REL_PATH);
|
|
4110
4499
|
} else {
|
|
4111
|
-
|
|
4500
|
+
fs20.mkdirSync(path18.dirname(qaGuidePath), { recursive: true });
|
|
4112
4501
|
const discovery = runQaDiscovery(cwd);
|
|
4113
|
-
|
|
4502
|
+
fs20.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
4114
4503
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
4115
4504
|
}
|
|
4116
4505
|
}
|
|
@@ -4122,12 +4511,12 @@ function performInit(cwd, force) {
|
|
|
4122
4511
|
continue;
|
|
4123
4512
|
}
|
|
4124
4513
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
4125
|
-
const target =
|
|
4126
|
-
if (
|
|
4514
|
+
const target = path18.join(workflowDir, `kody-${exe.name}.yml`);
|
|
4515
|
+
if (fs20.existsSync(target) && !force) {
|
|
4127
4516
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
4128
4517
|
continue;
|
|
4129
4518
|
}
|
|
4130
|
-
|
|
4519
|
+
fs20.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
4131
4520
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
4132
4521
|
}
|
|
4133
4522
|
let labels;
|
|
@@ -4238,89 +4627,6 @@ var loadIssueContext = async (ctx) => {
|
|
|
4238
4627
|
ctx.data.commentTargetNumber = issueNumber;
|
|
4239
4628
|
};
|
|
4240
4629
|
|
|
4241
|
-
// src/scripts/issueStateComment.ts
|
|
4242
|
-
function isStateEnvelope(x) {
|
|
4243
|
-
if (x === null || typeof x !== "object") return false;
|
|
4244
|
-
const o = x;
|
|
4245
|
-
return o.version === 1 && typeof o.rev === "number" && Number.isInteger(o.rev) && o.rev >= 0 && typeof o.cursor === "string" && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
4246
|
-
}
|
|
4247
|
-
function initialStateEnvelope(cursor = "seed") {
|
|
4248
|
-
return { version: 1, rev: 0, cursor, data: {}, done: false };
|
|
4249
|
-
}
|
|
4250
|
-
function formatStateCommentBody(marker, state) {
|
|
4251
|
-
return `<!-- ${marker} -->
|
|
4252
|
-
|
|
4253
|
-
\`\`\`json
|
|
4254
|
-
${JSON.stringify(state, null, 2)}
|
|
4255
|
-
\`\`\`
|
|
4256
|
-
`;
|
|
4257
|
-
}
|
|
4258
|
-
function parseStateCommentBody(marker, body) {
|
|
4259
|
-
const markerLine = `<!-- ${marker} -->`;
|
|
4260
|
-
if (!body.trimStart().startsWith(markerLine)) return null;
|
|
4261
|
-
const fenceOpen = body.indexOf("```json");
|
|
4262
|
-
if (fenceOpen === -1) return null;
|
|
4263
|
-
const after = body.slice(fenceOpen + "```json".length);
|
|
4264
|
-
const fenceClose = after.indexOf("```");
|
|
4265
|
-
if (fenceClose === -1) return null;
|
|
4266
|
-
const jsonText = after.slice(0, fenceClose).trim();
|
|
4267
|
-
let parsed;
|
|
4268
|
-
try {
|
|
4269
|
-
parsed = JSON.parse(jsonText);
|
|
4270
|
-
} catch {
|
|
4271
|
-
return null;
|
|
4272
|
-
}
|
|
4273
|
-
return isStateEnvelope(parsed) ? parsed : null;
|
|
4274
|
-
}
|
|
4275
|
-
function listIssueComments(owner, repo, issueNumber, cwd) {
|
|
4276
|
-
const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
|
|
4277
|
-
let parsed;
|
|
4278
|
-
try {
|
|
4279
|
-
parsed = JSON.parse(raw);
|
|
4280
|
-
} catch {
|
|
4281
|
-
return [];
|
|
4282
|
-
}
|
|
4283
|
-
if (!Array.isArray(parsed)) return [];
|
|
4284
|
-
return parsed.filter((c) => typeof c.id === "number" && typeof c.node_id === "string" && typeof c.body === "string").map((c) => ({ id: c.id, node_id: c.node_id, body: c.body }));
|
|
4285
|
-
}
|
|
4286
|
-
function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
4287
|
-
const comments = listIssueComments(owner, repo, issueNumber, cwd);
|
|
4288
|
-
for (const c of comments) {
|
|
4289
|
-
const state = parseStateCommentBody(marker, c.body);
|
|
4290
|
-
if (!state) continue;
|
|
4291
|
-
return { commentId: c.id, commentNodeId: c.node_id, state };
|
|
4292
|
-
}
|
|
4293
|
-
return null;
|
|
4294
|
-
}
|
|
4295
|
-
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
4296
|
-
const body = formatStateCommentBody(marker, state);
|
|
4297
|
-
const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
4298
|
-
cwd,
|
|
4299
|
-
input: JSON.stringify({ body })
|
|
4300
|
-
});
|
|
4301
|
-
const parsed = JSON.parse(raw);
|
|
4302
|
-
try {
|
|
4303
|
-
minimizeComment(parsed.node_id, cwd);
|
|
4304
|
-
} catch {
|
|
4305
|
-
}
|
|
4306
|
-
return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
|
|
4307
|
-
}
|
|
4308
|
-
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
4309
|
-
const body = formatStateCommentBody(marker, state);
|
|
4310
|
-
gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
4311
|
-
cwd,
|
|
4312
|
-
input: JSON.stringify({ body })
|
|
4313
|
-
});
|
|
4314
|
-
try {
|
|
4315
|
-
minimizeComment(commentNodeId, cwd);
|
|
4316
|
-
} catch {
|
|
4317
|
-
}
|
|
4318
|
-
}
|
|
4319
|
-
function minimizeComment(nodeId, cwd) {
|
|
4320
|
-
const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
|
|
4321
|
-
gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
4322
|
-
}
|
|
4323
|
-
|
|
4324
4630
|
// src/scripts/loadIssueStateComment.ts
|
|
4325
4631
|
var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
4326
4632
|
const marker = String(args?.marker ?? "");
|
|
@@ -4348,75 +4654,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
4348
4654
|
};
|
|
4349
4655
|
|
|
4350
4656
|
// src/scripts/loadMissionFromFile.ts
|
|
4351
|
-
import * as
|
|
4352
|
-
import * as
|
|
4353
|
-
|
|
4354
|
-
// src/scripts/missionStateFile.ts
|
|
4355
|
-
function stateFilePath(missionsDir, slug) {
|
|
4356
|
-
return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
4357
|
-
}
|
|
4358
|
-
function loadMissionState(owner, repo, filePath, cwd) {
|
|
4359
|
-
let raw = "";
|
|
4360
|
-
try {
|
|
4361
|
-
raw = gh2(["api", `/repos/${owner}/${repo}/contents/${filePath}`], { cwd });
|
|
4362
|
-
} catch (err) {
|
|
4363
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
4364
|
-
if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
|
|
4365
|
-
return { path: filePath, sha: null, state: initialStateEnvelope("seed"), created: true };
|
|
4366
|
-
}
|
|
4367
|
-
throw err;
|
|
4368
|
-
}
|
|
4369
|
-
let parsed;
|
|
4370
|
-
try {
|
|
4371
|
-
parsed = JSON.parse(raw);
|
|
4372
|
-
} catch {
|
|
4373
|
-
throw new Error(`loadMissionState: contents API for ${filePath} did not return JSON`);
|
|
4374
|
-
}
|
|
4375
|
-
if (!parsed || typeof parsed !== "object") {
|
|
4376
|
-
throw new Error(`loadMissionState: contents API for ${filePath} returned non-object`);
|
|
4377
|
-
}
|
|
4378
|
-
const o = parsed;
|
|
4379
|
-
if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
|
|
4380
|
-
throw new Error(`loadMissionState: ${filePath} is not a base64 file`);
|
|
4381
|
-
}
|
|
4382
|
-
const decoded = Buffer.from(o.content, "base64").toString("utf-8");
|
|
4383
|
-
let envelope;
|
|
4384
|
-
try {
|
|
4385
|
-
envelope = JSON.parse(decoded);
|
|
4386
|
-
} catch {
|
|
4387
|
-
throw new Error(`loadMissionState: ${filePath} is not valid JSON`);
|
|
4388
|
-
}
|
|
4389
|
-
if (!isStateEnvelope(envelope)) {
|
|
4390
|
-
throw new Error(`loadMissionState: ${filePath} is not a StateEnvelope`);
|
|
4391
|
-
}
|
|
4392
|
-
return { path: filePath, sha: o.sha, state: envelope, created: false };
|
|
4393
|
-
}
|
|
4394
|
-
function writeMissionState(owner, repo, loaded, next, cwd) {
|
|
4395
|
-
if (!loaded.created && deepEqualsState(loaded.state, next)) {
|
|
4396
|
-
return false;
|
|
4397
|
-
}
|
|
4398
|
-
const body = JSON.stringify(next, null, 2) + "\n";
|
|
4399
|
-
const payload = {
|
|
4400
|
-
message: `chore(missions): update state for ${stateFileSlug(loaded.path)} (rev ${next.rev})`,
|
|
4401
|
-
content: Buffer.from(body, "utf-8").toString("base64")
|
|
4402
|
-
};
|
|
4403
|
-
if (loaded.sha) payload.sha = loaded.sha;
|
|
4404
|
-
gh2(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${loaded.path}`, "--input", "-"], {
|
|
4405
|
-
cwd,
|
|
4406
|
-
input: JSON.stringify(payload)
|
|
4407
|
-
});
|
|
4408
|
-
return true;
|
|
4409
|
-
}
|
|
4410
|
-
function deepEqualsState(a, b) {
|
|
4411
|
-
if (a.cursor !== b.cursor || a.done !== b.done) return false;
|
|
4412
|
-
return JSON.stringify(a.data) === JSON.stringify(b.data);
|
|
4413
|
-
}
|
|
4414
|
-
function stateFileSlug(filePath) {
|
|
4415
|
-
const last = filePath.split("/").pop() ?? filePath;
|
|
4416
|
-
return last.replace(/\.state\.json$/i, "");
|
|
4417
|
-
}
|
|
4418
|
-
|
|
4419
|
-
// src/scripts/loadMissionFromFile.ts
|
|
4657
|
+
import * as fs21 from "fs";
|
|
4658
|
+
import * as path19 from "path";
|
|
4420
4659
|
var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
4421
4660
|
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
4422
4661
|
const slugArg = String(args?.slugArg ?? "mission");
|
|
@@ -4424,18 +4663,14 @@ var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
|
4424
4663
|
if (!slug) {
|
|
4425
4664
|
throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4426
4665
|
}
|
|
4427
|
-
const
|
|
4428
|
-
|
|
4429
|
-
if (!owner || !repo) {
|
|
4430
|
-
throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
|
|
4431
|
-
}
|
|
4432
|
-
const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
|
|
4433
|
-
if (!fs20.existsSync(absPath)) {
|
|
4666
|
+
const absPath = path19.join(ctx.cwd, missionsDir, `${slug}.md`);
|
|
4667
|
+
if (!fs21.existsSync(absPath)) {
|
|
4434
4668
|
throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
|
|
4435
4669
|
}
|
|
4436
|
-
const raw =
|
|
4670
|
+
const raw = fs21.readFileSync(absPath, "utf-8");
|
|
4437
4671
|
const { title, body } = parseMissionFile(raw, slug);
|
|
4438
|
-
const
|
|
4672
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
4673
|
+
const loaded = await backend.load(slug);
|
|
4439
4674
|
ctx.data.missionSlug = slug;
|
|
4440
4675
|
ctx.data.missionTitle = title;
|
|
4441
4676
|
ctx.data.missionIntent = body;
|
|
@@ -4566,16 +4801,16 @@ var loadTaskState = async (ctx) => {
|
|
|
4566
4801
|
};
|
|
4567
4802
|
|
|
4568
4803
|
// src/scripts/loadVaultContext.ts
|
|
4569
|
-
import * as
|
|
4570
|
-
import * as
|
|
4804
|
+
import * as fs22 from "fs";
|
|
4805
|
+
import * as path20 from "path";
|
|
4571
4806
|
var VAULT_DIR_RELATIVE = ".kody/vault";
|
|
4572
4807
|
var MAX_PAGES = 8;
|
|
4573
4808
|
var PER_PAGE_MAX_BYTES = 4e3;
|
|
4574
4809
|
var TOTAL_MAX_BYTES2 = 24e3;
|
|
4575
4810
|
var TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
|
|
4576
4811
|
var loadVaultContext = async (ctx) => {
|
|
4577
|
-
const vaultAbs =
|
|
4578
|
-
if (!
|
|
4812
|
+
const vaultAbs = path20.join(ctx.cwd, VAULT_DIR_RELATIVE);
|
|
4813
|
+
if (!fs22.existsSync(vaultAbs)) {
|
|
4579
4814
|
ctx.data.vaultContext = "";
|
|
4580
4815
|
return;
|
|
4581
4816
|
}
|
|
@@ -4600,21 +4835,21 @@ function collectPages(vaultAbs) {
|
|
|
4600
4835
|
walkMd(vaultAbs, (file) => {
|
|
4601
4836
|
let stat;
|
|
4602
4837
|
try {
|
|
4603
|
-
stat =
|
|
4838
|
+
stat = fs22.statSync(file);
|
|
4604
4839
|
} catch {
|
|
4605
4840
|
return;
|
|
4606
4841
|
}
|
|
4607
4842
|
let raw;
|
|
4608
4843
|
try {
|
|
4609
|
-
raw =
|
|
4844
|
+
raw = fs22.readFileSync(file, "utf-8");
|
|
4610
4845
|
} catch {
|
|
4611
4846
|
return;
|
|
4612
4847
|
}
|
|
4613
4848
|
const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
4614
|
-
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ??
|
|
4849
|
+
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path20.basename(file, ".md");
|
|
4615
4850
|
const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
|
|
4616
4851
|
out.push({
|
|
4617
|
-
relPath:
|
|
4852
|
+
relPath: path20.relative(vaultAbs, file),
|
|
4618
4853
|
title,
|
|
4619
4854
|
updated,
|
|
4620
4855
|
content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
|
|
@@ -4682,16 +4917,16 @@ function walkMd(root, visit) {
|
|
|
4682
4917
|
const dir = stack.pop();
|
|
4683
4918
|
let names;
|
|
4684
4919
|
try {
|
|
4685
|
-
names =
|
|
4920
|
+
names = fs22.readdirSync(dir);
|
|
4686
4921
|
} catch {
|
|
4687
4922
|
continue;
|
|
4688
4923
|
}
|
|
4689
4924
|
for (const name of names) {
|
|
4690
4925
|
if (name.startsWith(".")) continue;
|
|
4691
|
-
const full =
|
|
4926
|
+
const full = path20.join(dir, name);
|
|
4692
4927
|
let stat;
|
|
4693
4928
|
try {
|
|
4694
|
-
stat =
|
|
4929
|
+
stat = fs22.statSync(full);
|
|
4695
4930
|
} catch {
|
|
4696
4931
|
continue;
|
|
4697
4932
|
}
|
|
@@ -4714,15 +4949,15 @@ var markFlowSuccess = async (ctx) => {
|
|
|
4714
4949
|
|
|
4715
4950
|
// src/scripts/memorizeFlow.ts
|
|
4716
4951
|
import { execFileSync as execFileSync15 } from "child_process";
|
|
4717
|
-
import * as
|
|
4718
|
-
import * as
|
|
4952
|
+
import * as fs23 from "fs";
|
|
4953
|
+
import * as path21 from "path";
|
|
4719
4954
|
var VAULT_DIR_RELATIVE2 = ".kody/vault";
|
|
4720
4955
|
var DEFAULT_LOOKBACK_HOURS = 36;
|
|
4721
4956
|
var MAX_RECENT_PRS = 25;
|
|
4722
4957
|
var MAX_VAULT_INDEX_ENTRIES = 200;
|
|
4723
4958
|
var PR_BODY_TRUNC = 2e3;
|
|
4724
4959
|
var memorizeFlow = async (ctx) => {
|
|
4725
|
-
const vaultAbs =
|
|
4960
|
+
const vaultAbs = path21.join(ctx.cwd, VAULT_DIR_RELATIVE2);
|
|
4726
4961
|
ensureBranch(ctx, vaultAbs);
|
|
4727
4962
|
if (ctx.skipAgent) return;
|
|
4728
4963
|
const sinceIso = computeSinceIso(vaultAbs);
|
|
@@ -4732,8 +4967,8 @@ var memorizeFlow = async (ctx) => {
|
|
|
4732
4967
|
const recent = fetchRecentPrs(ctx.cwd, sinceIso);
|
|
4733
4968
|
ctx.data.recentPrs = formatRecentPrs(recent);
|
|
4734
4969
|
ctx.data.recentPrCount = recent.length;
|
|
4735
|
-
if (!
|
|
4736
|
-
|
|
4970
|
+
if (!fs23.existsSync(vaultAbs)) {
|
|
4971
|
+
fs23.mkdirSync(vaultAbs, { recursive: true });
|
|
4737
4972
|
}
|
|
4738
4973
|
ctx.data.vaultIndex = formatVaultIndex(vaultAbs);
|
|
4739
4974
|
if (recent.length === 0) {
|
|
@@ -4765,18 +5000,18 @@ function ensureBranch(ctx, vaultAbs) {
|
|
|
4765
5000
|
}
|
|
4766
5001
|
}
|
|
4767
5002
|
ctx.data.branch = branch;
|
|
4768
|
-
if (!
|
|
4769
|
-
|
|
5003
|
+
if (!fs23.existsSync(vaultAbs)) {
|
|
5004
|
+
fs23.mkdirSync(vaultAbs, { recursive: true });
|
|
4770
5005
|
}
|
|
4771
5006
|
}
|
|
4772
5007
|
function computeSinceIso(vaultAbs) {
|
|
4773
5008
|
const fallback = new Date(Date.now() - DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1e3).toISOString();
|
|
4774
|
-
if (!
|
|
5009
|
+
if (!fs23.existsSync(vaultAbs)) return fallback;
|
|
4775
5010
|
let latest = "";
|
|
4776
5011
|
walkMd2(vaultAbs, (file) => {
|
|
4777
5012
|
let raw;
|
|
4778
5013
|
try {
|
|
4779
|
-
raw =
|
|
5014
|
+
raw = fs23.readFileSync(file, "utf-8");
|
|
4780
5015
|
} catch {
|
|
4781
5016
|
return;
|
|
4782
5017
|
}
|
|
@@ -4853,10 +5088,10 @@ function formatVaultIndex(vaultAbs) {
|
|
|
4853
5088
|
const entries = [];
|
|
4854
5089
|
walkMd2(vaultAbs, (file) => {
|
|
4855
5090
|
if (entries.length >= MAX_VAULT_INDEX_ENTRIES) return;
|
|
4856
|
-
const rel =
|
|
5091
|
+
const rel = path21.relative(vaultAbs, file);
|
|
4857
5092
|
let title = rel;
|
|
4858
5093
|
try {
|
|
4859
|
-
const raw =
|
|
5094
|
+
const raw = fs23.readFileSync(file, "utf-8");
|
|
4860
5095
|
const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
4861
5096
|
const titleMatch = m?.[1]?.match(/^title:\s*(.+)$/m);
|
|
4862
5097
|
if (titleMatch) title = `${titleMatch[1].trim()} (${rel})`;
|
|
@@ -4868,22 +5103,22 @@ function formatVaultIndex(vaultAbs) {
|
|
|
4868
5103
|
return entries.join("\n");
|
|
4869
5104
|
}
|
|
4870
5105
|
function walkMd2(root, visit) {
|
|
4871
|
-
if (!
|
|
5106
|
+
if (!fs23.existsSync(root)) return;
|
|
4872
5107
|
const stack = [root];
|
|
4873
5108
|
while (stack.length > 0) {
|
|
4874
5109
|
const dir = stack.pop();
|
|
4875
5110
|
let names;
|
|
4876
5111
|
try {
|
|
4877
|
-
names =
|
|
5112
|
+
names = fs23.readdirSync(dir);
|
|
4878
5113
|
} catch {
|
|
4879
5114
|
continue;
|
|
4880
5115
|
}
|
|
4881
5116
|
for (const name of names) {
|
|
4882
5117
|
if (name.startsWith(".")) continue;
|
|
4883
|
-
const full =
|
|
5118
|
+
const full = path21.join(dir, name);
|
|
4884
5119
|
let stat;
|
|
4885
5120
|
try {
|
|
4886
|
-
stat =
|
|
5121
|
+
stat = fs23.statSync(full);
|
|
4887
5122
|
} catch {
|
|
4888
5123
|
continue;
|
|
4889
5124
|
}
|
|
@@ -5212,7 +5447,15 @@ var postIssueComment2 = async (ctx) => {
|
|
|
5212
5447
|
const isFailure = failureReason.length > 0;
|
|
5213
5448
|
const justPushedToExistingPr = prAction === "updated" && commitResult?.committed === true;
|
|
5214
5449
|
const successMsg = justPushedToExistingPr ? `\u2705 kody pushed to ${prUrl}` : prAction === "updated" ? `\u2139\uFE0F kody made no changes \u2014 PR: ${prUrl}` : `\u2705 kody PR opened: ${prUrl}`;
|
|
5215
|
-
const
|
|
5450
|
+
const branch = ctx.data.branch;
|
|
5451
|
+
const failurePrSuffix = computeFailureSuffix({
|
|
5452
|
+
prUrl,
|
|
5453
|
+
prAction,
|
|
5454
|
+
branch,
|
|
5455
|
+
branchPushed: commitResult?.committed === true,
|
|
5456
|
+
githubOwner: ctx.config.github?.owner,
|
|
5457
|
+
githubRepo: ctx.config.github?.repo
|
|
5458
|
+
});
|
|
5216
5459
|
const msg = isFailure ? `\u26A0\uFE0F kody FAILED: ${truncate2(failureReason, 1500)}${failurePrSuffix}` : successMsg;
|
|
5217
5460
|
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
5218
5461
|
let exitCode = 0;
|
|
@@ -5224,6 +5467,13 @@ var postIssueComment2 = async (ctx) => {
|
|
|
5224
5467
|
ctx.output.exitCode = exitCode;
|
|
5225
5468
|
ctx.output.reason = failureReason || void 0;
|
|
5226
5469
|
};
|
|
5470
|
+
function computeFailureSuffix(input) {
|
|
5471
|
+
if (input.prUrl) {
|
|
5472
|
+
return input.prAction === "updated" ? ` \u2014 PR: ${input.prUrl}` : ` \u2014 draft PR: ${input.prUrl}`;
|
|
5473
|
+
}
|
|
5474
|
+
if (!input.branchPushed || !input.branch || !input.githubOwner || !input.githubRepo) return "";
|
|
5475
|
+
return ` \u2014 branch: https://github.com/${input.githubOwner}/${input.githubRepo}/tree/${input.branch}`;
|
|
5476
|
+
}
|
|
5227
5477
|
function computeFailureReason2(ctx) {
|
|
5228
5478
|
const misses = ctx.data.coverageMisses ?? [];
|
|
5229
5479
|
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
@@ -5638,6 +5888,25 @@ function tryPostPr3(prNumber, body, cwd) {
|
|
|
5638
5888
|
}
|
|
5639
5889
|
}
|
|
5640
5890
|
|
|
5891
|
+
// src/scripts/resolvePreviewUrl.ts
|
|
5892
|
+
var DEFAULT_PREVIEW_URL = "http://localhost:3000";
|
|
5893
|
+
var resolvePreviewUrl = async (ctx) => {
|
|
5894
|
+
const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
|
|
5895
|
+
if (fromFlag.length > 0) {
|
|
5896
|
+
ctx.data.previewUrl = fromFlag;
|
|
5897
|
+
ctx.data.previewUrlSource = "flag";
|
|
5898
|
+
return;
|
|
5899
|
+
}
|
|
5900
|
+
const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
|
|
5901
|
+
if (fromEnv.length > 0) {
|
|
5902
|
+
ctx.data.previewUrl = fromEnv;
|
|
5903
|
+
ctx.data.previewUrlSource = "env";
|
|
5904
|
+
return;
|
|
5905
|
+
}
|
|
5906
|
+
ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
|
|
5907
|
+
ctx.data.previewUrlSource = "default";
|
|
5908
|
+
};
|
|
5909
|
+
|
|
5641
5910
|
// src/scripts/revertFlow.ts
|
|
5642
5911
|
import { execFileSync as execFileSync19 } from "child_process";
|
|
5643
5912
|
var SHA_RE = /^[0-9a-f]{4,40}$/i;
|
|
@@ -5707,11 +5976,7 @@ var revertFlow = async (ctx) => {
|
|
|
5707
5976
|
const runUrl = getRunUrl();
|
|
5708
5977
|
const runSuffix = runUrl ? `, run ${runUrl}` : "";
|
|
5709
5978
|
const shaList = resolved.map((r) => `\`${r.full.slice(0, 7)}\``).join(", ");
|
|
5710
|
-
tryPostPr4(
|
|
5711
|
-
prNumber,
|
|
5712
|
-
`\u2699\uFE0F kody revert started on \`${ctx.data.branch}\`${runSuffix} \u2014 reverting ${shaList}`,
|
|
5713
|
-
ctx.cwd
|
|
5714
|
-
);
|
|
5979
|
+
tryPostPr4(prNumber, `\u2699\uFE0F kody revert started on \`${ctx.data.branch}\`${runSuffix} \u2014 reverting ${shaList}`, ctx.cwd);
|
|
5715
5980
|
};
|
|
5716
5981
|
function buildCommitMessage(resolved) {
|
|
5717
5982
|
if (resolved.length === 1) {
|
|
@@ -5755,25 +6020,6 @@ function tryPostPr4(prNumber, body, cwd) {
|
|
|
5755
6020
|
}
|
|
5756
6021
|
}
|
|
5757
6022
|
|
|
5758
|
-
// src/scripts/resolvePreviewUrl.ts
|
|
5759
|
-
var DEFAULT_PREVIEW_URL = "http://localhost:3000";
|
|
5760
|
-
var resolvePreviewUrl = async (ctx) => {
|
|
5761
|
-
const fromFlag = typeof ctx.args.previewUrl === "string" ? ctx.args.previewUrl.trim() : "";
|
|
5762
|
-
if (fromFlag.length > 0) {
|
|
5763
|
-
ctx.data.previewUrl = fromFlag;
|
|
5764
|
-
ctx.data.previewUrlSource = "flag";
|
|
5765
|
-
return;
|
|
5766
|
-
}
|
|
5767
|
-
const fromEnv = (process.env.PREVIEW_URL ?? "").trim();
|
|
5768
|
-
if (fromEnv.length > 0) {
|
|
5769
|
-
ctx.data.previewUrl = fromEnv;
|
|
5770
|
-
ctx.data.previewUrlSource = "env";
|
|
5771
|
-
return;
|
|
5772
|
-
}
|
|
5773
|
-
ctx.data.previewUrl = DEFAULT_PREVIEW_URL;
|
|
5774
|
-
ctx.data.previewUrlSource = "default";
|
|
5775
|
-
};
|
|
5776
|
-
|
|
5777
6023
|
// src/scripts/reviewFlow.ts
|
|
5778
6024
|
var reviewFlow = async (ctx) => {
|
|
5779
6025
|
const prNumber = ctx.args.pr;
|
|
@@ -6389,7 +6635,7 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
6389
6635
|
};
|
|
6390
6636
|
|
|
6391
6637
|
// src/scripts/writeMissionStateFile.ts
|
|
6392
|
-
var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
|
|
6638
|
+
var writeMissionStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
6393
6639
|
const parseError = ctx.data.nextStateParseError;
|
|
6394
6640
|
if (parseError) {
|
|
6395
6641
|
process.stderr.write(`[kody] mission state write skipped: ${parseError}
|
|
@@ -6406,16 +6652,13 @@ var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
|
|
|
6406
6652
|
if (!loaded) {
|
|
6407
6653
|
throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
|
|
6408
6654
|
}
|
|
6409
|
-
const
|
|
6410
|
-
const
|
|
6411
|
-
|
|
6412
|
-
throw new Error("writeMissionStateFile: ctx.config.github.owner/repo must be set");
|
|
6413
|
-
}
|
|
6414
|
-
writeMissionState(owner, repo, loaded, next, ctx.cwd);
|
|
6655
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
6656
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
6657
|
+
await backend.save(loaded, next);
|
|
6415
6658
|
};
|
|
6416
6659
|
|
|
6417
6660
|
// src/scripts/writeRunSummary.ts
|
|
6418
|
-
import * as
|
|
6661
|
+
import * as fs24 from "fs";
|
|
6419
6662
|
var writeRunSummary = async (ctx, profile) => {
|
|
6420
6663
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
6421
6664
|
if (!summaryPath) return;
|
|
@@ -6437,7 +6680,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
6437
6680
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
6438
6681
|
lines.push("");
|
|
6439
6682
|
try {
|
|
6440
|
-
|
|
6683
|
+
fs24.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
6441
6684
|
`);
|
|
6442
6685
|
} catch {
|
|
6443
6686
|
}
|
|
@@ -6620,9 +6863,9 @@ async function runExecutable(profileName, input) {
|
|
|
6620
6863
|
data: {},
|
|
6621
6864
|
output: { exitCode: 0 }
|
|
6622
6865
|
};
|
|
6623
|
-
const ndjsonDir =
|
|
6866
|
+
const ndjsonDir = path22.join(input.cwd, ".kody");
|
|
6624
6867
|
const invokeAgent = async (prompt) => {
|
|
6625
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
6868
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path22.isAbsolute(p) ? p : path22.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
6626
6869
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
6627
6870
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
6628
6871
|
return runAgent({
|
|
@@ -6701,17 +6944,17 @@ async function runExecutable(profileName, input) {
|
|
|
6701
6944
|
function resolveProfilePath(profileName) {
|
|
6702
6945
|
const found = resolveExecutable(profileName);
|
|
6703
6946
|
if (found) return found;
|
|
6704
|
-
const here =
|
|
6947
|
+
const here = path22.dirname(new URL(import.meta.url).pathname);
|
|
6705
6948
|
const candidates = [
|
|
6706
|
-
|
|
6949
|
+
path22.join(here, "executables", profileName, "profile.json"),
|
|
6707
6950
|
// same-dir sibling (dev)
|
|
6708
|
-
|
|
6951
|
+
path22.join(here, "..", "executables", profileName, "profile.json"),
|
|
6709
6952
|
// up one (prod: dist/bin → dist/executables)
|
|
6710
|
-
|
|
6953
|
+
path22.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
6711
6954
|
// fallback
|
|
6712
6955
|
];
|
|
6713
6956
|
for (const c of candidates) {
|
|
6714
|
-
if (
|
|
6957
|
+
if (fs25.existsSync(c)) return c;
|
|
6715
6958
|
}
|
|
6716
6959
|
return candidates[0];
|
|
6717
6960
|
}
|
|
@@ -6815,8 +7058,8 @@ function resolveShellTimeoutMs(entry) {
|
|
|
6815
7058
|
var SIGKILL_GRACE_MS = 5e3;
|
|
6816
7059
|
async function runShellEntry(entry, ctx, profile) {
|
|
6817
7060
|
const shellName = entry.shell;
|
|
6818
|
-
const shellPath =
|
|
6819
|
-
if (!
|
|
7061
|
+
const shellPath = path22.join(profile.dir, shellName);
|
|
7062
|
+
if (!fs25.existsSync(shellPath)) {
|
|
6820
7063
|
ctx.skipAgent = true;
|
|
6821
7064
|
ctx.output.exitCode = 99;
|
|
6822
7065
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -6962,7 +7205,7 @@ Environment:
|
|
|
6962
7205
|
Exit codes (inherited from kody run):
|
|
6963
7206
|
0 success (PR opened, verify passed)
|
|
6964
7207
|
1 agent reported FAILED (draft PR opened)
|
|
6965
|
-
2 verify failed (
|
|
7208
|
+
2 verify failed (no PR opened \u2014 branch pushed for inspection)
|
|
6966
7209
|
3 no commits to ship
|
|
6967
7210
|
4 PR creation failed
|
|
6968
7211
|
5 uncommitted changes on target branch
|
|
@@ -7024,9 +7267,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
7024
7267
|
return token;
|
|
7025
7268
|
}
|
|
7026
7269
|
function detectPackageManager2(cwd) {
|
|
7027
|
-
if (
|
|
7028
|
-
if (
|
|
7029
|
-
if (
|
|
7270
|
+
if (fs26.existsSync(path23.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
7271
|
+
if (fs26.existsSync(path23.join(cwd, "yarn.lock"))) return "yarn";
|
|
7272
|
+
if (fs26.existsSync(path23.join(cwd, "bun.lockb"))) return "bun";
|
|
7030
7273
|
return "npm";
|
|
7031
7274
|
}
|
|
7032
7275
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -7106,11 +7349,11 @@ function configureGitIdentity(cwd) {
|
|
|
7106
7349
|
}
|
|
7107
7350
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
7108
7351
|
if (!issueNumber) return;
|
|
7109
|
-
const logPath =
|
|
7352
|
+
const logPath = path23.join(cwd, ".kody", "last-run.jsonl");
|
|
7110
7353
|
let tail = "";
|
|
7111
7354
|
try {
|
|
7112
|
-
if (
|
|
7113
|
-
const content =
|
|
7355
|
+
if (fs26.existsSync(logPath)) {
|
|
7356
|
+
const content = fs26.readFileSync(logPath, "utf-8");
|
|
7114
7357
|
tail = content.slice(-3e3);
|
|
7115
7358
|
}
|
|
7116
7359
|
} catch {
|
|
@@ -7135,7 +7378,7 @@ async function runCi(argv) {
|
|
|
7135
7378
|
return 0;
|
|
7136
7379
|
}
|
|
7137
7380
|
const args = parseCiArgs(argv);
|
|
7138
|
-
const cwd = args.cwd ?
|
|
7381
|
+
const cwd = args.cwd ? path23.resolve(args.cwd) : process.cwd();
|
|
7139
7382
|
let earlyConfig;
|
|
7140
7383
|
try {
|
|
7141
7384
|
earlyConfig = loadConfig(cwd);
|
|
@@ -7145,9 +7388,9 @@ async function runCi(argv) {
|
|
|
7145
7388
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
7146
7389
|
const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
|
|
7147
7390
|
let manualWorkflowDispatch = false;
|
|
7148
|
-
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath &&
|
|
7391
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs26.existsSync(dispatchEventPath)) {
|
|
7149
7392
|
try {
|
|
7150
|
-
const evt = JSON.parse(
|
|
7393
|
+
const evt = JSON.parse(fs26.readFileSync(dispatchEventPath, "utf-8"));
|
|
7151
7394
|
const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
|
|
7152
7395
|
const sessionInput = String(evt?.inputs?.sessionId ?? "");
|
|
7153
7396
|
manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
|
|
@@ -7362,9 +7605,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
7362
7605
|
return result;
|
|
7363
7606
|
}
|
|
7364
7607
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
7365
|
-
const sessionFile =
|
|
7366
|
-
const eventsFile =
|
|
7367
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
7608
|
+
const sessionFile = path24.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
7609
|
+
const eventsFile = path24.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
7610
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs27.existsSync(path24.join(cwd, p)));
|
|
7368
7611
|
if (paths.length === 0) return;
|
|
7369
7612
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
7370
7613
|
try {
|
|
@@ -7402,7 +7645,7 @@ async function runChat(argv) {
|
|
|
7402
7645
|
${CHAT_HELP}`);
|
|
7403
7646
|
return 64;
|
|
7404
7647
|
}
|
|
7405
|
-
const cwd = args.cwd ?
|
|
7648
|
+
const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
|
|
7406
7649
|
const sessionId = args.sessionId;
|
|
7407
7650
|
const unpackedSecrets = unpackAllSecrets();
|
|
7408
7651
|
if (unpackedSecrets > 0) {
|
|
@@ -7489,7 +7732,7 @@ directory to add a new command.
|
|
|
7489
7732
|
Exit codes:
|
|
7490
7733
|
0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
|
|
7491
7734
|
1 agent reported FAILED (draft PR opened)
|
|
7492
|
-
2 verify failed (
|
|
7735
|
+
2 verify failed (no PR opened \u2014 branch pushed for inspection) \u2014 skipped in resolve mode
|
|
7493
7736
|
3 no commits to ship (also the resolve clean-merge short-circuit)
|
|
7494
7737
|
4 PR creation failed
|
|
7495
7738
|
5 uncommitted changes on target branch
|