@kody-ade/kody-engine 0.3.57 → 0.3.60
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 +598 -328
- package/dist/executables/run/profile.json +1 -1
- package/kody.config.schema.json +13 -0
- package/package.json +2 -1
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.60",
|
|
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",
|
|
@@ -606,8 +621,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
606
621
|
|
|
607
622
|
// src/kody-cli.ts
|
|
608
623
|
import { execFileSync as execFileSync25 } from "child_process";
|
|
609
|
-
import * as
|
|
610
|
-
import * as
|
|
624
|
+
import * as fs26 from "fs";
|
|
625
|
+
import * as path23 from "path";
|
|
611
626
|
|
|
612
627
|
// src/dispatch.ts
|
|
613
628
|
import * as fs6 from "fs";
|
|
@@ -959,9 +974,9 @@ function coerceBare(spec, value) {
|
|
|
959
974
|
}
|
|
960
975
|
|
|
961
976
|
// src/executor.ts
|
|
962
|
-
import {
|
|
963
|
-
import * as
|
|
964
|
-
import * as
|
|
977
|
+
import { spawn as spawn3 } from "child_process";
|
|
978
|
+
import * as fs25 from "fs";
|
|
979
|
+
import * as path22 from "path";
|
|
965
980
|
|
|
966
981
|
// src/litellm.ts
|
|
967
982
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -2851,62 +2866,8 @@ function failedAction(reason) {
|
|
|
2851
2866
|
}
|
|
2852
2867
|
|
|
2853
2868
|
// 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
|
-
}
|
|
2869
|
+
import * as fs17 from "fs";
|
|
2870
|
+
import * as path16 from "path";
|
|
2910
2871
|
|
|
2911
2872
|
// src/issue.ts
|
|
2912
2873
|
import { execFileSync as execFileSync9 } from "child_process";
|
|
@@ -3049,6 +3010,411 @@ function postPrReviewComment(prNumber, body, cwd) {
|
|
|
3049
3010
|
}
|
|
3050
3011
|
}
|
|
3051
3012
|
|
|
3013
|
+
// src/scripts/issueStateComment.ts
|
|
3014
|
+
function isStateEnvelope(x) {
|
|
3015
|
+
if (x === null || typeof x !== "object") return false;
|
|
3016
|
+
const o = x;
|
|
3017
|
+
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);
|
|
3018
|
+
}
|
|
3019
|
+
function initialStateEnvelope(cursor = "seed") {
|
|
3020
|
+
return { version: 1, rev: 0, cursor, data: {}, done: false };
|
|
3021
|
+
}
|
|
3022
|
+
function formatStateCommentBody(marker, state) {
|
|
3023
|
+
return `<!-- ${marker} -->
|
|
3024
|
+
|
|
3025
|
+
\`\`\`json
|
|
3026
|
+
${JSON.stringify(state, null, 2)}
|
|
3027
|
+
\`\`\`
|
|
3028
|
+
`;
|
|
3029
|
+
}
|
|
3030
|
+
function parseStateCommentBody(marker, body) {
|
|
3031
|
+
const markerLine = `<!-- ${marker} -->`;
|
|
3032
|
+
if (!body.trimStart().startsWith(markerLine)) return null;
|
|
3033
|
+
const fenceOpen = body.indexOf("```json");
|
|
3034
|
+
if (fenceOpen === -1) return null;
|
|
3035
|
+
const after = body.slice(fenceOpen + "```json".length);
|
|
3036
|
+
const fenceClose = after.indexOf("```");
|
|
3037
|
+
if (fenceClose === -1) return null;
|
|
3038
|
+
const jsonText = after.slice(0, fenceClose).trim();
|
|
3039
|
+
let parsed;
|
|
3040
|
+
try {
|
|
3041
|
+
parsed = JSON.parse(jsonText);
|
|
3042
|
+
} catch {
|
|
3043
|
+
return null;
|
|
3044
|
+
}
|
|
3045
|
+
return isStateEnvelope(parsed) ? parsed : null;
|
|
3046
|
+
}
|
|
3047
|
+
function listIssueComments(owner, repo, issueNumber, cwd) {
|
|
3048
|
+
const raw = gh2(["api", "--paginate", `repos/${owner}/${repo}/issues/${issueNumber}/comments`], { cwd });
|
|
3049
|
+
let parsed;
|
|
3050
|
+
try {
|
|
3051
|
+
parsed = JSON.parse(raw);
|
|
3052
|
+
} catch {
|
|
3053
|
+
return [];
|
|
3054
|
+
}
|
|
3055
|
+
if (!Array.isArray(parsed)) return [];
|
|
3056
|
+
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 }));
|
|
3057
|
+
}
|
|
3058
|
+
function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
3059
|
+
const comments = listIssueComments(owner, repo, issueNumber, cwd);
|
|
3060
|
+
for (const c of comments) {
|
|
3061
|
+
const state = parseStateCommentBody(marker, c.body);
|
|
3062
|
+
if (!state) continue;
|
|
3063
|
+
return { commentId: c.id, commentNodeId: c.node_id, state };
|
|
3064
|
+
}
|
|
3065
|
+
return null;
|
|
3066
|
+
}
|
|
3067
|
+
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
3068
|
+
const body = formatStateCommentBody(marker, state);
|
|
3069
|
+
const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
3070
|
+
cwd,
|
|
3071
|
+
input: JSON.stringify({ body })
|
|
3072
|
+
});
|
|
3073
|
+
const parsed = JSON.parse(raw);
|
|
3074
|
+
try {
|
|
3075
|
+
minimizeComment(parsed.node_id, cwd);
|
|
3076
|
+
} catch {
|
|
3077
|
+
}
|
|
3078
|
+
return { commentId: parsed.id, commentNodeId: parsed.node_id, state };
|
|
3079
|
+
}
|
|
3080
|
+
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
3081
|
+
const body = formatStateCommentBody(marker, state);
|
|
3082
|
+
gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
3083
|
+
cwd,
|
|
3084
|
+
input: JSON.stringify({ body })
|
|
3085
|
+
});
|
|
3086
|
+
try {
|
|
3087
|
+
minimizeComment(commentNodeId, cwd);
|
|
3088
|
+
} catch {
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
function minimizeComment(nodeId, cwd) {
|
|
3092
|
+
const mutation = "mutation($id: ID!) { minimizeComment(input: { classifier: OUTDATED, subjectId: $id }) { minimizedComment { isMinimized } } }";
|
|
3093
|
+
gh2(["api", "graphql", "-f", `query=${mutation}`, "-f", `id=${nodeId}`], { cwd });
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
// src/scripts/missionState/backend.ts
|
|
3097
|
+
function isStateUnchanged(prev, next) {
|
|
3098
|
+
if (prev.cursor !== next.cursor) return false;
|
|
3099
|
+
if (prev.done !== next.done) return false;
|
|
3100
|
+
return JSON.stringify(prev.data) === JSON.stringify(next.data);
|
|
3101
|
+
}
|
|
3102
|
+
function stateFilePath(missionsDir, slug) {
|
|
3103
|
+
return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
3104
|
+
}
|
|
3105
|
+
function slugFromStateFilePath(filePath) {
|
|
3106
|
+
const last = filePath.split("/").pop() ?? filePath;
|
|
3107
|
+
return last.replace(/\.state\.json$/i, "");
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
// src/scripts/missionState/contentsApiBackend.ts
|
|
3111
|
+
var ContentsApiBackend = class {
|
|
3112
|
+
name = "contents-api";
|
|
3113
|
+
owner;
|
|
3114
|
+
repo;
|
|
3115
|
+
missionsDir;
|
|
3116
|
+
cwd;
|
|
3117
|
+
constructor(opts) {
|
|
3118
|
+
if (!opts.owner || !opts.repo) {
|
|
3119
|
+
throw new Error("ContentsApiBackend: owner and repo are required");
|
|
3120
|
+
}
|
|
3121
|
+
this.owner = opts.owner;
|
|
3122
|
+
this.repo = opts.repo;
|
|
3123
|
+
this.missionsDir = opts.missionsDir;
|
|
3124
|
+
this.cwd = opts.cwd;
|
|
3125
|
+
}
|
|
3126
|
+
load(slug) {
|
|
3127
|
+
const filePath = stateFilePath(this.missionsDir, slug);
|
|
3128
|
+
let raw = "";
|
|
3129
|
+
try {
|
|
3130
|
+
raw = gh2(["api", `/repos/${this.owner}/${this.repo}/contents/${filePath}`], { cwd: this.cwd });
|
|
3131
|
+
} catch (err) {
|
|
3132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3133
|
+
if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
|
|
3134
|
+
return { path: filePath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
3135
|
+
}
|
|
3136
|
+
throw err;
|
|
3137
|
+
}
|
|
3138
|
+
let parsed;
|
|
3139
|
+
try {
|
|
3140
|
+
parsed = JSON.parse(raw);
|
|
3141
|
+
} catch {
|
|
3142
|
+
throw new Error(`ContentsApiBackend: contents API for ${filePath} did not return JSON`);
|
|
3143
|
+
}
|
|
3144
|
+
if (!parsed || typeof parsed !== "object") {
|
|
3145
|
+
throw new Error(`ContentsApiBackend: contents API for ${filePath} returned non-object`);
|
|
3146
|
+
}
|
|
3147
|
+
const o = parsed;
|
|
3148
|
+
if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
|
|
3149
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not a base64 file`);
|
|
3150
|
+
}
|
|
3151
|
+
const decoded = Buffer.from(o.content, "base64").toString("utf-8");
|
|
3152
|
+
let envelope;
|
|
3153
|
+
try {
|
|
3154
|
+
envelope = JSON.parse(decoded);
|
|
3155
|
+
} catch {
|
|
3156
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not valid JSON`);
|
|
3157
|
+
}
|
|
3158
|
+
if (!isStateEnvelope(envelope)) {
|
|
3159
|
+
throw new Error(`ContentsApiBackend: ${filePath} is not a StateEnvelope`);
|
|
3160
|
+
}
|
|
3161
|
+
return { path: filePath, handle: o.sha, state: envelope, created: false };
|
|
3162
|
+
}
|
|
3163
|
+
save(loaded, next) {
|
|
3164
|
+
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
3165
|
+
return false;
|
|
3166
|
+
}
|
|
3167
|
+
const slug = slugFromStateFilePath(loaded.path);
|
|
3168
|
+
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3169
|
+
const payload = {
|
|
3170
|
+
message: `chore(missions): update state for ${slug} (rev ${next.rev})`,
|
|
3171
|
+
content: Buffer.from(body, "utf-8").toString("base64")
|
|
3172
|
+
};
|
|
3173
|
+
if (typeof loaded.handle === "string") payload.sha = loaded.handle;
|
|
3174
|
+
gh2(["api", "--method", "PUT", `/repos/${this.owner}/${this.repo}/contents/${loaded.path}`, "--input", "-"], {
|
|
3175
|
+
cwd: this.cwd,
|
|
3176
|
+
input: JSON.stringify(payload)
|
|
3177
|
+
});
|
|
3178
|
+
return true;
|
|
3179
|
+
}
|
|
3180
|
+
};
|
|
3181
|
+
|
|
3182
|
+
// src/scripts/missionState/localFileBackend.ts
|
|
3183
|
+
import * as fs16 from "fs";
|
|
3184
|
+
import * as path15 from "path";
|
|
3185
|
+
var LocalFileBackend = class {
|
|
3186
|
+
name = "local-file";
|
|
3187
|
+
cwd;
|
|
3188
|
+
missionsDir;
|
|
3189
|
+
absDir;
|
|
3190
|
+
owner;
|
|
3191
|
+
repo;
|
|
3192
|
+
cache;
|
|
3193
|
+
constructor(opts) {
|
|
3194
|
+
if (!opts.cwd) throw new Error("LocalFileBackend: cwd is required");
|
|
3195
|
+
if (!opts.missionsDir) throw new Error("LocalFileBackend: missionsDir is required");
|
|
3196
|
+
if (!opts.owner || !opts.repo) throw new Error("LocalFileBackend: owner and repo are required");
|
|
3197
|
+
this.cwd = opts.cwd;
|
|
3198
|
+
this.missionsDir = opts.missionsDir;
|
|
3199
|
+
this.absDir = path15.join(opts.cwd, opts.missionsDir);
|
|
3200
|
+
this.owner = opts.owner;
|
|
3201
|
+
this.repo = opts.repo;
|
|
3202
|
+
this.cache = opts.cache ?? defaultCacheAdapter();
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Restore the mission directory from the most recent Actions cache entry
|
|
3206
|
+
* for this repo. No-op when not running in Actions or when no cache exists.
|
|
3207
|
+
*/
|
|
3208
|
+
async hydrate() {
|
|
3209
|
+
if (!this.cache.isAvailable()) {
|
|
3210
|
+
process.stdout.write(`[missions/state] hydrate skipped: actions cache unavailable
|
|
3211
|
+
`);
|
|
3212
|
+
return;
|
|
3213
|
+
}
|
|
3214
|
+
fs16.mkdirSync(this.absDir, { recursive: true });
|
|
3215
|
+
const prefix = this.cacheKeyPrefix();
|
|
3216
|
+
const probeKey = `${prefix}probe-${Date.now()}`;
|
|
3217
|
+
try {
|
|
3218
|
+
const matched = await this.cache.restore([this.absDir], probeKey, [prefix]);
|
|
3219
|
+
if (matched) {
|
|
3220
|
+
process.stdout.write(`[missions/state] hydrate hit: ${matched}
|
|
3221
|
+
`);
|
|
3222
|
+
} else {
|
|
3223
|
+
process.stdout.write(`[missions/state] hydrate miss (cold start)
|
|
3224
|
+
`);
|
|
3225
|
+
}
|
|
3226
|
+
} catch (err) {
|
|
3227
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3228
|
+
process.stderr.write(`[missions/state] hydrate failed (continuing): ${msg}
|
|
3229
|
+
`);
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
/**
|
|
3233
|
+
* Save the mission directory to the Actions cache under a unique key.
|
|
3234
|
+
* No-op when not running in Actions. Errors are logged, never thrown —
|
|
3235
|
+
* callers run this in a finally block and must not swallow real errors.
|
|
3236
|
+
*/
|
|
3237
|
+
async persist() {
|
|
3238
|
+
if (!this.cache.isAvailable()) {
|
|
3239
|
+
process.stdout.write(`[missions/state] persist skipped: actions cache unavailable
|
|
3240
|
+
`);
|
|
3241
|
+
return;
|
|
3242
|
+
}
|
|
3243
|
+
if (!fs16.existsSync(this.absDir)) {
|
|
3244
|
+
return;
|
|
3245
|
+
}
|
|
3246
|
+
const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
|
|
3247
|
+
try {
|
|
3248
|
+
await this.cache.save([this.absDir], key);
|
|
3249
|
+
process.stdout.write(`[missions/state] persist saved: ${key}
|
|
3250
|
+
`);
|
|
3251
|
+
} catch (err) {
|
|
3252
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3253
|
+
process.stderr.write(`[missions/state] persist failed (continuing): ${msg}
|
|
3254
|
+
`);
|
|
3255
|
+
}
|
|
3256
|
+
}
|
|
3257
|
+
load(slug) {
|
|
3258
|
+
const relPath = stateFilePath(this.missionsDir, slug);
|
|
3259
|
+
const absPath = path15.join(this.cwd, relPath);
|
|
3260
|
+
if (!fs16.existsSync(absPath)) {
|
|
3261
|
+
return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
|
|
3262
|
+
}
|
|
3263
|
+
const raw = fs16.readFileSync(absPath, "utf-8");
|
|
3264
|
+
let parsed;
|
|
3265
|
+
try {
|
|
3266
|
+
parsed = JSON.parse(raw);
|
|
3267
|
+
} catch (err) {
|
|
3268
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3269
|
+
throw new Error(`LocalFileBackend: ${relPath} is not valid JSON: ${msg}`);
|
|
3270
|
+
}
|
|
3271
|
+
if (!isStateEnvelope(parsed)) {
|
|
3272
|
+
throw new Error(`LocalFileBackend: ${relPath} is not a StateEnvelope`);
|
|
3273
|
+
}
|
|
3274
|
+
return { path: relPath, handle: null, state: parsed, created: false };
|
|
3275
|
+
}
|
|
3276
|
+
save(loaded, next) {
|
|
3277
|
+
if (!loaded.created && isStateUnchanged(loaded.state, next)) {
|
|
3278
|
+
return false;
|
|
3279
|
+
}
|
|
3280
|
+
const absPath = path15.join(this.cwd, loaded.path);
|
|
3281
|
+
fs16.mkdirSync(path15.dirname(absPath), { recursive: true });
|
|
3282
|
+
const body = JSON.stringify(next, null, 2) + "\n";
|
|
3283
|
+
fs16.writeFileSync(absPath, body, "utf-8");
|
|
3284
|
+
return true;
|
|
3285
|
+
}
|
|
3286
|
+
cacheKeyPrefix() {
|
|
3287
|
+
return `kody-mission-state-${sanitizeKey(this.owner)}-${sanitizeKey(this.repo)}-`;
|
|
3288
|
+
}
|
|
3289
|
+
};
|
|
3290
|
+
function sanitizeKey(s) {
|
|
3291
|
+
return s.replace(/[^A-Za-z0-9._-]/g, "-");
|
|
3292
|
+
}
|
|
3293
|
+
function defaultCacheAdapter() {
|
|
3294
|
+
let mod = null;
|
|
3295
|
+
const load = async () => {
|
|
3296
|
+
if (!mod) {
|
|
3297
|
+
mod = await import("@actions/cache");
|
|
3298
|
+
}
|
|
3299
|
+
return mod;
|
|
3300
|
+
};
|
|
3301
|
+
const available = () => {
|
|
3302
|
+
if (process.env.GITHUB_ACTIONS !== "true") return false;
|
|
3303
|
+
return Boolean(process.env.ACTIONS_CACHE_URL || process.env.ACTIONS_RESULTS_URL);
|
|
3304
|
+
};
|
|
3305
|
+
return {
|
|
3306
|
+
isAvailable: available,
|
|
3307
|
+
async restore(paths, primaryKey, restoreKeys) {
|
|
3308
|
+
if (!available()) return void 0;
|
|
3309
|
+
const m = await load();
|
|
3310
|
+
return m.restoreCache(paths, primaryKey, restoreKeys);
|
|
3311
|
+
},
|
|
3312
|
+
async save(paths, primaryKey) {
|
|
3313
|
+
if (!available()) return;
|
|
3314
|
+
const m = await load();
|
|
3315
|
+
try {
|
|
3316
|
+
await m.saveCache(paths, primaryKey);
|
|
3317
|
+
} catch (err) {
|
|
3318
|
+
const name = err?.name ?? "";
|
|
3319
|
+
if (name === "ReserveCacheError") return;
|
|
3320
|
+
throw err;
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3323
|
+
};
|
|
3324
|
+
}
|
|
3325
|
+
|
|
3326
|
+
// src/scripts/missionState/index.ts
|
|
3327
|
+
function resolveBackend(opts) {
|
|
3328
|
+
const owner = opts.config.github?.owner;
|
|
3329
|
+
const repo = opts.config.github?.repo;
|
|
3330
|
+
if (!owner || !repo) {
|
|
3331
|
+
throw new Error("resolveBackend: config.github.owner and config.github.repo must be set");
|
|
3332
|
+
}
|
|
3333
|
+
const requested = opts.config.missions?.stateBackend ?? "contents-api";
|
|
3334
|
+
switch (requested) {
|
|
3335
|
+
case "contents-api":
|
|
3336
|
+
return new ContentsApiBackend({ owner, repo, missionsDir: opts.missionsDir, cwd: opts.cwd });
|
|
3337
|
+
case "local-file":
|
|
3338
|
+
return new LocalFileBackend({ cwd: opts.cwd, missionsDir: opts.missionsDir, owner, repo });
|
|
3339
|
+
default: {
|
|
3340
|
+
const _exhaustive = requested;
|
|
3341
|
+
throw new Error(`resolveBackend: unknown stateBackend "${String(_exhaustive)}"`);
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
|
|
3346
|
+
// src/scripts/dispatchMissionFileTicks.ts
|
|
3347
|
+
var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
3348
|
+
ctx.skipAgent = true;
|
|
3349
|
+
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
3350
|
+
if (!targetExecutable) {
|
|
3351
|
+
throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
|
|
3352
|
+
}
|
|
3353
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
3354
|
+
const slugArg = String(args?.slugArg ?? "mission");
|
|
3355
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
3356
|
+
if (backend.hydrate) {
|
|
3357
|
+
await backend.hydrate();
|
|
3358
|
+
}
|
|
3359
|
+
try {
|
|
3360
|
+
const slugs = listMissionSlugs(path16.join(ctx.cwd, missionsDir));
|
|
3361
|
+
ctx.data.missionSlugCount = slugs.length;
|
|
3362
|
+
if (slugs.length === 0) {
|
|
3363
|
+
process.stdout.write(`[missions] no mission files in ${missionsDir}
|
|
3364
|
+
`);
|
|
3365
|
+
return;
|
|
3366
|
+
}
|
|
3367
|
+
process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
|
|
3368
|
+
`);
|
|
3369
|
+
const results = [];
|
|
3370
|
+
for (const slug of slugs) {
|
|
3371
|
+
process.stdout.write(`[missions] \u2192 tick ${slug}
|
|
3372
|
+
`);
|
|
3373
|
+
try {
|
|
3374
|
+
const out = await runExecutable(targetExecutable, {
|
|
3375
|
+
cliArgs: { [slugArg]: slug },
|
|
3376
|
+
cwd: ctx.cwd,
|
|
3377
|
+
config: ctx.config,
|
|
3378
|
+
verbose: ctx.verbose,
|
|
3379
|
+
quiet: ctx.quiet
|
|
3380
|
+
});
|
|
3381
|
+
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
3382
|
+
if (out.exitCode !== 0) {
|
|
3383
|
+
process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
3384
|
+
`);
|
|
3385
|
+
}
|
|
3386
|
+
} catch (err) {
|
|
3387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3388
|
+
process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
|
|
3389
|
+
`);
|
|
3390
|
+
results.push({ slug, exitCode: 99, reason: msg });
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
ctx.data.missionTickResults = results;
|
|
3394
|
+
ctx.output.exitCode = 0;
|
|
3395
|
+
} finally {
|
|
3396
|
+
if (backend.persist) {
|
|
3397
|
+
try {
|
|
3398
|
+
await backend.persist();
|
|
3399
|
+
} catch (err) {
|
|
3400
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3401
|
+
process.stderr.write(`[missions] backend persist failed: ${msg}
|
|
3402
|
+
`);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
};
|
|
3407
|
+
function listMissionSlugs(absDir) {
|
|
3408
|
+
if (!fs17.existsSync(absDir)) return [];
|
|
3409
|
+
let entries;
|
|
3410
|
+
try {
|
|
3411
|
+
entries = fs17.readdirSync(absDir, { withFileTypes: true });
|
|
3412
|
+
} catch {
|
|
3413
|
+
return [];
|
|
3414
|
+
}
|
|
3415
|
+
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();
|
|
3416
|
+
}
|
|
3417
|
+
|
|
3052
3418
|
// src/scripts/dispatchMissionTicks.ts
|
|
3053
3419
|
var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
3054
3420
|
ctx.skipAgent = true;
|
|
@@ -3681,7 +4047,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
3681
4047
|
|
|
3682
4048
|
// src/gha.ts
|
|
3683
4049
|
import { execFileSync as execFileSync12 } from "child_process";
|
|
3684
|
-
import * as
|
|
4050
|
+
import * as fs18 from "fs";
|
|
3685
4051
|
function getRunUrl() {
|
|
3686
4052
|
const server = process.env.GITHUB_SERVER_URL;
|
|
3687
4053
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -3692,10 +4058,10 @@ function getRunUrl() {
|
|
|
3692
4058
|
function reactToTriggerComment(cwd) {
|
|
3693
4059
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
3694
4060
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3695
|
-
if (!eventPath || !
|
|
4061
|
+
if (!eventPath || !fs18.existsSync(eventPath)) return;
|
|
3696
4062
|
let event = null;
|
|
3697
4063
|
try {
|
|
3698
|
-
event = JSON.parse(
|
|
4064
|
+
event = JSON.parse(fs18.readFileSync(eventPath, "utf-8"));
|
|
3699
4065
|
} catch {
|
|
3700
4066
|
return;
|
|
3701
4067
|
}
|
|
@@ -3935,22 +4301,22 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
3935
4301
|
|
|
3936
4302
|
// src/scripts/initFlow.ts
|
|
3937
4303
|
import { execFileSync as execFileSync14 } from "child_process";
|
|
3938
|
-
import * as
|
|
3939
|
-
import * as
|
|
4304
|
+
import * as fs20 from "fs";
|
|
4305
|
+
import * as path18 from "path";
|
|
3940
4306
|
|
|
3941
4307
|
// src/scripts/loadQaGuide.ts
|
|
3942
|
-
import * as
|
|
3943
|
-
import * as
|
|
4308
|
+
import * as fs19 from "fs";
|
|
4309
|
+
import * as path17 from "path";
|
|
3944
4310
|
var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
|
|
3945
4311
|
var loadQaGuide = async (ctx) => {
|
|
3946
|
-
const full =
|
|
3947
|
-
if (!
|
|
4312
|
+
const full = path17.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
4313
|
+
if (!fs19.existsSync(full)) {
|
|
3948
4314
|
ctx.data.qaGuide = "";
|
|
3949
4315
|
ctx.data.qaGuidePath = "";
|
|
3950
4316
|
return;
|
|
3951
4317
|
}
|
|
3952
4318
|
try {
|
|
3953
|
-
ctx.data.qaGuide =
|
|
4319
|
+
ctx.data.qaGuide = fs19.readFileSync(full, "utf-8");
|
|
3954
4320
|
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
3955
4321
|
} catch {
|
|
3956
4322
|
ctx.data.qaGuide = "";
|
|
@@ -3960,9 +4326,9 @@ var loadQaGuide = async (ctx) => {
|
|
|
3960
4326
|
|
|
3961
4327
|
// src/scripts/initFlow.ts
|
|
3962
4328
|
function detectPackageManager(cwd) {
|
|
3963
|
-
if (
|
|
3964
|
-
if (
|
|
3965
|
-
if (
|
|
4329
|
+
if (fs20.existsSync(path18.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
4330
|
+
if (fs20.existsSync(path18.join(cwd, "yarn.lock"))) return "yarn";
|
|
4331
|
+
if (fs20.existsSync(path18.join(cwd, "bun.lockb"))) return "bun";
|
|
3966
4332
|
return "npm";
|
|
3967
4333
|
}
|
|
3968
4334
|
function qualityCommandsFor(pm) {
|
|
@@ -4084,33 +4450,33 @@ function performInit(cwd, force) {
|
|
|
4084
4450
|
const pm = detectPackageManager(cwd);
|
|
4085
4451
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
4086
4452
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
4087
|
-
const configPath =
|
|
4088
|
-
if (
|
|
4453
|
+
const configPath = path18.join(cwd, "kody.config.json");
|
|
4454
|
+
if (fs20.existsSync(configPath) && !force) {
|
|
4089
4455
|
skipped.push("kody.config.json");
|
|
4090
4456
|
} else {
|
|
4091
4457
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
4092
|
-
|
|
4458
|
+
fs20.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
4093
4459
|
`);
|
|
4094
4460
|
wrote.push("kody.config.json");
|
|
4095
4461
|
}
|
|
4096
|
-
const workflowDir =
|
|
4097
|
-
const workflowPath =
|
|
4098
|
-
if (
|
|
4462
|
+
const workflowDir = path18.join(cwd, ".github", "workflows");
|
|
4463
|
+
const workflowPath = path18.join(workflowDir, "kody.yml");
|
|
4464
|
+
if (fs20.existsSync(workflowPath) && !force) {
|
|
4099
4465
|
skipped.push(".github/workflows/kody.yml");
|
|
4100
4466
|
} else {
|
|
4101
|
-
|
|
4102
|
-
|
|
4467
|
+
fs20.mkdirSync(workflowDir, { recursive: true });
|
|
4468
|
+
fs20.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
4103
4469
|
wrote.push(".github/workflows/kody.yml");
|
|
4104
4470
|
}
|
|
4105
|
-
const hasUi =
|
|
4471
|
+
const hasUi = fs20.existsSync(path18.join(cwd, "src/app")) || fs20.existsSync(path18.join(cwd, "app")) || fs20.existsSync(path18.join(cwd, "pages"));
|
|
4106
4472
|
if (hasUi) {
|
|
4107
|
-
const qaGuidePath =
|
|
4108
|
-
if (
|
|
4473
|
+
const qaGuidePath = path18.join(cwd, QA_GUIDE_REL_PATH);
|
|
4474
|
+
if (fs20.existsSync(qaGuidePath) && !force) {
|
|
4109
4475
|
skipped.push(QA_GUIDE_REL_PATH);
|
|
4110
4476
|
} else {
|
|
4111
|
-
|
|
4477
|
+
fs20.mkdirSync(path18.dirname(qaGuidePath), { recursive: true });
|
|
4112
4478
|
const discovery = runQaDiscovery(cwd);
|
|
4113
|
-
|
|
4479
|
+
fs20.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
4114
4480
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
4115
4481
|
}
|
|
4116
4482
|
}
|
|
@@ -4122,12 +4488,12 @@ function performInit(cwd, force) {
|
|
|
4122
4488
|
continue;
|
|
4123
4489
|
}
|
|
4124
4490
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
4125
|
-
const target =
|
|
4126
|
-
if (
|
|
4491
|
+
const target = path18.join(workflowDir, `kody-${exe.name}.yml`);
|
|
4492
|
+
if (fs20.existsSync(target) && !force) {
|
|
4127
4493
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
4128
4494
|
continue;
|
|
4129
4495
|
}
|
|
4130
|
-
|
|
4496
|
+
fs20.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
4131
4497
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
4132
4498
|
}
|
|
4133
4499
|
let labels;
|
|
@@ -4238,89 +4604,6 @@ var loadIssueContext = async (ctx) => {
|
|
|
4238
4604
|
ctx.data.commentTargetNumber = issueNumber;
|
|
4239
4605
|
};
|
|
4240
4606
|
|
|
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
4607
|
// src/scripts/loadIssueStateComment.ts
|
|
4325
4608
|
var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
4326
4609
|
const marker = String(args?.marker ?? "");
|
|
@@ -4348,75 +4631,8 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
4348
4631
|
};
|
|
4349
4632
|
|
|
4350
4633
|
// 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
|
|
4634
|
+
import * as fs21 from "fs";
|
|
4635
|
+
import * as path19 from "path";
|
|
4420
4636
|
var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
4421
4637
|
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
4422
4638
|
const slugArg = String(args?.slugArg ?? "mission");
|
|
@@ -4424,18 +4640,14 @@ var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
|
4424
4640
|
if (!slug) {
|
|
4425
4641
|
throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4426
4642
|
}
|
|
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)) {
|
|
4643
|
+
const absPath = path19.join(ctx.cwd, missionsDir, `${slug}.md`);
|
|
4644
|
+
if (!fs21.existsSync(absPath)) {
|
|
4434
4645
|
throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
|
|
4435
4646
|
}
|
|
4436
|
-
const raw =
|
|
4647
|
+
const raw = fs21.readFileSync(absPath, "utf-8");
|
|
4437
4648
|
const { title, body } = parseMissionFile(raw, slug);
|
|
4438
|
-
const
|
|
4649
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
4650
|
+
const loaded = await backend.load(slug);
|
|
4439
4651
|
ctx.data.missionSlug = slug;
|
|
4440
4652
|
ctx.data.missionTitle = title;
|
|
4441
4653
|
ctx.data.missionIntent = body;
|
|
@@ -4566,16 +4778,16 @@ var loadTaskState = async (ctx) => {
|
|
|
4566
4778
|
};
|
|
4567
4779
|
|
|
4568
4780
|
// src/scripts/loadVaultContext.ts
|
|
4569
|
-
import * as
|
|
4570
|
-
import * as
|
|
4781
|
+
import * as fs22 from "fs";
|
|
4782
|
+
import * as path20 from "path";
|
|
4571
4783
|
var VAULT_DIR_RELATIVE = ".kody/vault";
|
|
4572
4784
|
var MAX_PAGES = 8;
|
|
4573
4785
|
var PER_PAGE_MAX_BYTES = 4e3;
|
|
4574
4786
|
var TOTAL_MAX_BYTES2 = 24e3;
|
|
4575
4787
|
var TRUNCATED_SUFFIX2 = "\n\n\u2026 (truncated)";
|
|
4576
4788
|
var loadVaultContext = async (ctx) => {
|
|
4577
|
-
const vaultAbs =
|
|
4578
|
-
if (!
|
|
4789
|
+
const vaultAbs = path20.join(ctx.cwd, VAULT_DIR_RELATIVE);
|
|
4790
|
+
if (!fs22.existsSync(vaultAbs)) {
|
|
4579
4791
|
ctx.data.vaultContext = "";
|
|
4580
4792
|
return;
|
|
4581
4793
|
}
|
|
@@ -4600,21 +4812,21 @@ function collectPages(vaultAbs) {
|
|
|
4600
4812
|
walkMd(vaultAbs, (file) => {
|
|
4601
4813
|
let stat;
|
|
4602
4814
|
try {
|
|
4603
|
-
stat =
|
|
4815
|
+
stat = fs22.statSync(file);
|
|
4604
4816
|
} catch {
|
|
4605
4817
|
return;
|
|
4606
4818
|
}
|
|
4607
4819
|
let raw;
|
|
4608
4820
|
try {
|
|
4609
|
-
raw =
|
|
4821
|
+
raw = fs22.readFileSync(file, "utf-8");
|
|
4610
4822
|
} catch {
|
|
4611
4823
|
return;
|
|
4612
4824
|
}
|
|
4613
4825
|
const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
4614
|
-
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ??
|
|
4826
|
+
const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path20.basename(file, ".md");
|
|
4615
4827
|
const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
|
|
4616
4828
|
out.push({
|
|
4617
|
-
relPath:
|
|
4829
|
+
relPath: path20.relative(vaultAbs, file),
|
|
4618
4830
|
title,
|
|
4619
4831
|
updated,
|
|
4620
4832
|
content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX2 : raw,
|
|
@@ -4682,16 +4894,16 @@ function walkMd(root, visit) {
|
|
|
4682
4894
|
const dir = stack.pop();
|
|
4683
4895
|
let names;
|
|
4684
4896
|
try {
|
|
4685
|
-
names =
|
|
4897
|
+
names = fs22.readdirSync(dir);
|
|
4686
4898
|
} catch {
|
|
4687
4899
|
continue;
|
|
4688
4900
|
}
|
|
4689
4901
|
for (const name of names) {
|
|
4690
4902
|
if (name.startsWith(".")) continue;
|
|
4691
|
-
const full =
|
|
4903
|
+
const full = path20.join(dir, name);
|
|
4692
4904
|
let stat;
|
|
4693
4905
|
try {
|
|
4694
|
-
stat =
|
|
4906
|
+
stat = fs22.statSync(full);
|
|
4695
4907
|
} catch {
|
|
4696
4908
|
continue;
|
|
4697
4909
|
}
|
|
@@ -4714,15 +4926,15 @@ var markFlowSuccess = async (ctx) => {
|
|
|
4714
4926
|
|
|
4715
4927
|
// src/scripts/memorizeFlow.ts
|
|
4716
4928
|
import { execFileSync as execFileSync15 } from "child_process";
|
|
4717
|
-
import * as
|
|
4718
|
-
import * as
|
|
4929
|
+
import * as fs23 from "fs";
|
|
4930
|
+
import * as path21 from "path";
|
|
4719
4931
|
var VAULT_DIR_RELATIVE2 = ".kody/vault";
|
|
4720
4932
|
var DEFAULT_LOOKBACK_HOURS = 36;
|
|
4721
4933
|
var MAX_RECENT_PRS = 25;
|
|
4722
4934
|
var MAX_VAULT_INDEX_ENTRIES = 200;
|
|
4723
4935
|
var PR_BODY_TRUNC = 2e3;
|
|
4724
4936
|
var memorizeFlow = async (ctx) => {
|
|
4725
|
-
const vaultAbs =
|
|
4937
|
+
const vaultAbs = path21.join(ctx.cwd, VAULT_DIR_RELATIVE2);
|
|
4726
4938
|
ensureBranch(ctx, vaultAbs);
|
|
4727
4939
|
if (ctx.skipAgent) return;
|
|
4728
4940
|
const sinceIso = computeSinceIso(vaultAbs);
|
|
@@ -4732,8 +4944,8 @@ var memorizeFlow = async (ctx) => {
|
|
|
4732
4944
|
const recent = fetchRecentPrs(ctx.cwd, sinceIso);
|
|
4733
4945
|
ctx.data.recentPrs = formatRecentPrs(recent);
|
|
4734
4946
|
ctx.data.recentPrCount = recent.length;
|
|
4735
|
-
if (!
|
|
4736
|
-
|
|
4947
|
+
if (!fs23.existsSync(vaultAbs)) {
|
|
4948
|
+
fs23.mkdirSync(vaultAbs, { recursive: true });
|
|
4737
4949
|
}
|
|
4738
4950
|
ctx.data.vaultIndex = formatVaultIndex(vaultAbs);
|
|
4739
4951
|
if (recent.length === 0) {
|
|
@@ -4765,18 +4977,18 @@ function ensureBranch(ctx, vaultAbs) {
|
|
|
4765
4977
|
}
|
|
4766
4978
|
}
|
|
4767
4979
|
ctx.data.branch = branch;
|
|
4768
|
-
if (!
|
|
4769
|
-
|
|
4980
|
+
if (!fs23.existsSync(vaultAbs)) {
|
|
4981
|
+
fs23.mkdirSync(vaultAbs, { recursive: true });
|
|
4770
4982
|
}
|
|
4771
4983
|
}
|
|
4772
4984
|
function computeSinceIso(vaultAbs) {
|
|
4773
4985
|
const fallback = new Date(Date.now() - DEFAULT_LOOKBACK_HOURS * 60 * 60 * 1e3).toISOString();
|
|
4774
|
-
if (!
|
|
4986
|
+
if (!fs23.existsSync(vaultAbs)) return fallback;
|
|
4775
4987
|
let latest = "";
|
|
4776
4988
|
walkMd2(vaultAbs, (file) => {
|
|
4777
4989
|
let raw;
|
|
4778
4990
|
try {
|
|
4779
|
-
raw =
|
|
4991
|
+
raw = fs23.readFileSync(file, "utf-8");
|
|
4780
4992
|
} catch {
|
|
4781
4993
|
return;
|
|
4782
4994
|
}
|
|
@@ -4853,10 +5065,10 @@ function formatVaultIndex(vaultAbs) {
|
|
|
4853
5065
|
const entries = [];
|
|
4854
5066
|
walkMd2(vaultAbs, (file) => {
|
|
4855
5067
|
if (entries.length >= MAX_VAULT_INDEX_ENTRIES) return;
|
|
4856
|
-
const rel =
|
|
5068
|
+
const rel = path21.relative(vaultAbs, file);
|
|
4857
5069
|
let title = rel;
|
|
4858
5070
|
try {
|
|
4859
|
-
const raw =
|
|
5071
|
+
const raw = fs23.readFileSync(file, "utf-8");
|
|
4860
5072
|
const m = raw.match(/^---\s*\n([\s\S]*?)\n---/);
|
|
4861
5073
|
const titleMatch = m?.[1]?.match(/^title:\s*(.+)$/m);
|
|
4862
5074
|
if (titleMatch) title = `${titleMatch[1].trim()} (${rel})`;
|
|
@@ -4868,22 +5080,22 @@ function formatVaultIndex(vaultAbs) {
|
|
|
4868
5080
|
return entries.join("\n");
|
|
4869
5081
|
}
|
|
4870
5082
|
function walkMd2(root, visit) {
|
|
4871
|
-
if (!
|
|
5083
|
+
if (!fs23.existsSync(root)) return;
|
|
4872
5084
|
const stack = [root];
|
|
4873
5085
|
while (stack.length > 0) {
|
|
4874
5086
|
const dir = stack.pop();
|
|
4875
5087
|
let names;
|
|
4876
5088
|
try {
|
|
4877
|
-
names =
|
|
5089
|
+
names = fs23.readdirSync(dir);
|
|
4878
5090
|
} catch {
|
|
4879
5091
|
continue;
|
|
4880
5092
|
}
|
|
4881
5093
|
for (const name of names) {
|
|
4882
5094
|
if (name.startsWith(".")) continue;
|
|
4883
|
-
const full =
|
|
5095
|
+
const full = path21.join(dir, name);
|
|
4884
5096
|
let stat;
|
|
4885
5097
|
try {
|
|
4886
|
-
stat =
|
|
5098
|
+
stat = fs23.statSync(full);
|
|
4887
5099
|
} catch {
|
|
4888
5100
|
continue;
|
|
4889
5101
|
}
|
|
@@ -5212,7 +5424,15 @@ var postIssueComment2 = async (ctx) => {
|
|
|
5212
5424
|
const isFailure = failureReason.length > 0;
|
|
5213
5425
|
const justPushedToExistingPr = prAction === "updated" && commitResult?.committed === true;
|
|
5214
5426
|
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
|
|
5427
|
+
const branch = ctx.data.branch;
|
|
5428
|
+
const failurePrSuffix = computeFailureSuffix({
|
|
5429
|
+
prUrl,
|
|
5430
|
+
prAction,
|
|
5431
|
+
branch,
|
|
5432
|
+
branchPushed: commitResult?.committed === true,
|
|
5433
|
+
githubOwner: ctx.config.github?.owner,
|
|
5434
|
+
githubRepo: ctx.config.github?.repo
|
|
5435
|
+
});
|
|
5216
5436
|
const msg = isFailure ? `\u26A0\uFE0F kody FAILED: ${truncate2(failureReason, 1500)}${failurePrSuffix}` : successMsg;
|
|
5217
5437
|
postWith(targetType, targetNumber, msg, ctx.cwd);
|
|
5218
5438
|
let exitCode = 0;
|
|
@@ -5224,6 +5444,13 @@ var postIssueComment2 = async (ctx) => {
|
|
|
5224
5444
|
ctx.output.exitCode = exitCode;
|
|
5225
5445
|
ctx.output.reason = failureReason || void 0;
|
|
5226
5446
|
};
|
|
5447
|
+
function computeFailureSuffix(input) {
|
|
5448
|
+
if (input.prUrl) {
|
|
5449
|
+
return input.prAction === "updated" ? ` \u2014 PR: ${input.prUrl}` : ` \u2014 draft PR: ${input.prUrl}`;
|
|
5450
|
+
}
|
|
5451
|
+
if (!input.branchPushed || !input.branch || !input.githubOwner || !input.githubRepo) return "";
|
|
5452
|
+
return ` \u2014 branch: https://github.com/${input.githubOwner}/${input.githubRepo}/tree/${input.branch}`;
|
|
5453
|
+
}
|
|
5227
5454
|
function computeFailureReason2(ctx) {
|
|
5228
5455
|
const misses = ctx.data.coverageMisses ?? [];
|
|
5229
5456
|
if (misses.length > 0) return `missing tests: ${misses.map((m) => m.expectedTest).join(", ")}`;
|
|
@@ -6389,7 +6616,7 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
6389
6616
|
};
|
|
6390
6617
|
|
|
6391
6618
|
// src/scripts/writeMissionStateFile.ts
|
|
6392
|
-
var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
|
|
6619
|
+
var writeMissionStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
6393
6620
|
const parseError = ctx.data.nextStateParseError;
|
|
6394
6621
|
if (parseError) {
|
|
6395
6622
|
process.stderr.write(`[kody] mission state write skipped: ${parseError}
|
|
@@ -6406,16 +6633,13 @@ var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
|
|
|
6406
6633
|
if (!loaded) {
|
|
6407
6634
|
throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
|
|
6408
6635
|
}
|
|
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);
|
|
6636
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
6637
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, missionsDir });
|
|
6638
|
+
await backend.save(loaded, next);
|
|
6415
6639
|
};
|
|
6416
6640
|
|
|
6417
6641
|
// src/scripts/writeRunSummary.ts
|
|
6418
|
-
import * as
|
|
6642
|
+
import * as fs24 from "fs";
|
|
6419
6643
|
var writeRunSummary = async (ctx, profile) => {
|
|
6420
6644
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
6421
6645
|
if (!summaryPath) return;
|
|
@@ -6437,7 +6661,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
6437
6661
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
6438
6662
|
lines.push("");
|
|
6439
6663
|
try {
|
|
6440
|
-
|
|
6664
|
+
fs24.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
6441
6665
|
`);
|
|
6442
6666
|
} catch {
|
|
6443
6667
|
}
|
|
@@ -6620,9 +6844,9 @@ async function runExecutable(profileName, input) {
|
|
|
6620
6844
|
data: {},
|
|
6621
6845
|
output: { exitCode: 0 }
|
|
6622
6846
|
};
|
|
6623
|
-
const ndjsonDir =
|
|
6847
|
+
const ndjsonDir = path22.join(input.cwd, ".kody");
|
|
6624
6848
|
const invokeAgent = async (prompt) => {
|
|
6625
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
6849
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path22.isAbsolute(p) ? p : path22.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
6626
6850
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
6627
6851
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
6628
6852
|
return runAgent({
|
|
@@ -6648,7 +6872,7 @@ async function runExecutable(profileName, input) {
|
|
|
6648
6872
|
for (const entry of profile.scripts.preflight) {
|
|
6649
6873
|
if (!shouldRun(entry, ctx)) continue;
|
|
6650
6874
|
if (entry.shell) {
|
|
6651
|
-
runShellEntry(entry, ctx, profile);
|
|
6875
|
+
await runShellEntry(entry, ctx, profile);
|
|
6652
6876
|
} else {
|
|
6653
6877
|
const fn = preflightScripts[entry.script];
|
|
6654
6878
|
if (!fn) return finish({ exitCode: 99, reason: `preflight script not registered: ${entry.script}` });
|
|
@@ -6671,7 +6895,7 @@ async function runExecutable(profileName, input) {
|
|
|
6671
6895
|
const label = entry.script ?? entry.shell ?? "<unknown>";
|
|
6672
6896
|
try {
|
|
6673
6897
|
if (entry.shell) {
|
|
6674
|
-
runShellEntry(entry, ctx, profile);
|
|
6898
|
+
await runShellEntry(entry, ctx, profile);
|
|
6675
6899
|
} else {
|
|
6676
6900
|
const fn = postflightScripts[entry.script];
|
|
6677
6901
|
if (!fn) return finish({ exitCode: 99, reason: `postflight script not registered: ${entry.script}` });
|
|
@@ -6701,17 +6925,17 @@ async function runExecutable(profileName, input) {
|
|
|
6701
6925
|
function resolveProfilePath(profileName) {
|
|
6702
6926
|
const found = resolveExecutable(profileName);
|
|
6703
6927
|
if (found) return found;
|
|
6704
|
-
const here =
|
|
6928
|
+
const here = path22.dirname(new URL(import.meta.url).pathname);
|
|
6705
6929
|
const candidates = [
|
|
6706
|
-
|
|
6930
|
+
path22.join(here, "executables", profileName, "profile.json"),
|
|
6707
6931
|
// same-dir sibling (dev)
|
|
6708
|
-
|
|
6932
|
+
path22.join(here, "..", "executables", profileName, "profile.json"),
|
|
6709
6933
|
// up one (prod: dist/bin → dist/executables)
|
|
6710
|
-
|
|
6934
|
+
path22.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
6711
6935
|
// fallback
|
|
6712
6936
|
];
|
|
6713
6937
|
for (const c of candidates) {
|
|
6714
|
-
if (
|
|
6938
|
+
if (fs25.existsSync(c)) return c;
|
|
6715
6939
|
}
|
|
6716
6940
|
return candidates[0];
|
|
6717
6941
|
}
|
|
@@ -6812,10 +7036,11 @@ function resolveShellTimeoutMs(entry) {
|
|
|
6812
7036
|
}
|
|
6813
7037
|
return DEFAULT_SHELL_TIMEOUT_MS;
|
|
6814
7038
|
}
|
|
6815
|
-
|
|
7039
|
+
var SIGKILL_GRACE_MS = 5e3;
|
|
7040
|
+
async function runShellEntry(entry, ctx, profile) {
|
|
6816
7041
|
const shellName = entry.shell;
|
|
6817
|
-
const shellPath =
|
|
6818
|
-
if (!
|
|
7042
|
+
const shellPath = path22.join(profile.dir, shellName);
|
|
7043
|
+
if (!fs25.existsSync(shellPath)) {
|
|
6819
7044
|
ctx.skipAgent = true;
|
|
6820
7045
|
ctx.output.exitCode = 99;
|
|
6821
7046
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -6831,17 +7056,63 @@ function runShellEntry(entry, ctx, profile) {
|
|
|
6831
7056
|
env[`KODY_CFG_${k}`] = v;
|
|
6832
7057
|
}
|
|
6833
7058
|
const timeoutMs = resolveShellTimeoutMs(entry);
|
|
6834
|
-
const
|
|
7059
|
+
const child = spawn3("bash", [shellPath, ...positional], {
|
|
6835
7060
|
cwd: ctx.cwd,
|
|
6836
|
-
encoding: "utf-8",
|
|
6837
7061
|
env,
|
|
6838
7062
|
stdio: ["pipe", "pipe", "pipe"],
|
|
6839
|
-
|
|
7063
|
+
detached: true
|
|
7064
|
+
});
|
|
7065
|
+
let stdout = "";
|
|
7066
|
+
let stderr = "";
|
|
7067
|
+
child.stdout?.on("data", (chunk) => {
|
|
7068
|
+
const s = chunk.toString("utf-8");
|
|
7069
|
+
stdout += s;
|
|
7070
|
+
process.stdout.write(s);
|
|
7071
|
+
});
|
|
7072
|
+
child.stderr?.on("data", (chunk) => {
|
|
7073
|
+
const s = chunk.toString("utf-8");
|
|
7074
|
+
stderr += s;
|
|
7075
|
+
process.stderr.write(s);
|
|
6840
7076
|
});
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
7077
|
+
let timedOut = false;
|
|
7078
|
+
let killTimer;
|
|
7079
|
+
let escalateTimer;
|
|
7080
|
+
const result = await new Promise(
|
|
7081
|
+
(resolve4) => {
|
|
7082
|
+
let settled = false;
|
|
7083
|
+
const settle = (code, signal, spawnErr) => {
|
|
7084
|
+
if (settled) return;
|
|
7085
|
+
settled = true;
|
|
7086
|
+
if (killTimer) clearTimeout(killTimer);
|
|
7087
|
+
if (escalateTimer) clearTimeout(escalateTimer);
|
|
7088
|
+
resolve4({ code, signal, spawnErr });
|
|
7089
|
+
};
|
|
7090
|
+
child.on("error", (err) => settle(null, null, err));
|
|
7091
|
+
child.on("close", (code, signal) => settle(code, signal));
|
|
7092
|
+
if (typeof child.pid === "number") {
|
|
7093
|
+
const pgid = child.pid;
|
|
7094
|
+
killTimer = setTimeout(() => {
|
|
7095
|
+
timedOut = true;
|
|
7096
|
+
try {
|
|
7097
|
+
process.kill(-pgid, "SIGTERM");
|
|
7098
|
+
} catch {
|
|
7099
|
+
}
|
|
7100
|
+
escalateTimer = setTimeout(() => {
|
|
7101
|
+
try {
|
|
7102
|
+
process.kill(-pgid, "SIGKILL");
|
|
7103
|
+
} catch {
|
|
7104
|
+
}
|
|
7105
|
+
}, SIGKILL_GRACE_MS);
|
|
7106
|
+
}, timeoutMs);
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
);
|
|
7110
|
+
if (result.spawnErr) {
|
|
7111
|
+
ctx.skipAgent = true;
|
|
7112
|
+
ctx.output.exitCode = 99;
|
|
7113
|
+
ctx.output.reason = `shell '${shellName}' failed to spawn: ${result.spawnErr.message}`;
|
|
7114
|
+
return;
|
|
7115
|
+
}
|
|
6845
7116
|
if (/^KODY_SKIP_AGENT=true\s*$/m.test(stdout)) {
|
|
6846
7117
|
ctx.skipAgent = true;
|
|
6847
7118
|
if (ctx.output.exitCode === void 0) ctx.output.exitCode = 0;
|
|
@@ -6850,7 +7121,6 @@ function runShellEntry(entry, ctx, profile) {
|
|
|
6850
7121
|
if (prUrlMatch?.[1]) ctx.output.prUrl = prUrlMatch[1].trim();
|
|
6851
7122
|
const reasonMatch = stdout.match(/^KODY_REASON=(.+)$/m);
|
|
6852
7123
|
if (reasonMatch?.[1]) ctx.output.reason = reasonMatch[1].trim();
|
|
6853
|
-
const timedOut = r.status === null && r.signal !== null;
|
|
6854
7124
|
if (timedOut) {
|
|
6855
7125
|
ctx.skipAgent = true;
|
|
6856
7126
|
const seconds = Math.round(timeoutMs / 1e3);
|
|
@@ -6858,11 +7128,11 @@ function runShellEntry(entry, ctx, profile) {
|
|
|
6858
7128
|
ctx.output.exitCode = 124;
|
|
6859
7129
|
}
|
|
6860
7130
|
if (!ctx.output.reason) {
|
|
6861
|
-
ctx.output.reason = `shell '${shellName}' timed out after ${seconds}s (
|
|
7131
|
+
ctx.output.reason = `shell '${shellName}' timed out after ${seconds}s (process group signalled SIGTERM/SIGKILL)`;
|
|
6862
7132
|
}
|
|
6863
7133
|
return;
|
|
6864
7134
|
}
|
|
6865
|
-
const exit =
|
|
7135
|
+
const exit = result.code ?? -1;
|
|
6866
7136
|
if (exit !== 0) {
|
|
6867
7137
|
ctx.skipAgent = true;
|
|
6868
7138
|
if (ctx.output.exitCode === void 0 || ctx.output.exitCode === 0) {
|
|
@@ -6916,7 +7186,7 @@ Environment:
|
|
|
6916
7186
|
Exit codes (inherited from kody run):
|
|
6917
7187
|
0 success (PR opened, verify passed)
|
|
6918
7188
|
1 agent reported FAILED (draft PR opened)
|
|
6919
|
-
2 verify failed (
|
|
7189
|
+
2 verify failed (no PR opened \u2014 branch pushed for inspection)
|
|
6920
7190
|
3 no commits to ship
|
|
6921
7191
|
4 PR creation failed
|
|
6922
7192
|
5 uncommitted changes on target branch
|
|
@@ -6978,9 +7248,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
6978
7248
|
return token;
|
|
6979
7249
|
}
|
|
6980
7250
|
function detectPackageManager2(cwd) {
|
|
6981
|
-
if (
|
|
6982
|
-
if (
|
|
6983
|
-
if (
|
|
7251
|
+
if (fs26.existsSync(path23.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
7252
|
+
if (fs26.existsSync(path23.join(cwd, "yarn.lock"))) return "yarn";
|
|
7253
|
+
if (fs26.existsSync(path23.join(cwd, "bun.lockb"))) return "bun";
|
|
6984
7254
|
return "npm";
|
|
6985
7255
|
}
|
|
6986
7256
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -7060,11 +7330,11 @@ function configureGitIdentity(cwd) {
|
|
|
7060
7330
|
}
|
|
7061
7331
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
7062
7332
|
if (!issueNumber) return;
|
|
7063
|
-
const logPath =
|
|
7333
|
+
const logPath = path23.join(cwd, ".kody", "last-run.jsonl");
|
|
7064
7334
|
let tail = "";
|
|
7065
7335
|
try {
|
|
7066
|
-
if (
|
|
7067
|
-
const content =
|
|
7336
|
+
if (fs26.existsSync(logPath)) {
|
|
7337
|
+
const content = fs26.readFileSync(logPath, "utf-8");
|
|
7068
7338
|
tail = content.slice(-3e3);
|
|
7069
7339
|
}
|
|
7070
7340
|
} catch {
|
|
@@ -7089,7 +7359,7 @@ async function runCi(argv) {
|
|
|
7089
7359
|
return 0;
|
|
7090
7360
|
}
|
|
7091
7361
|
const args = parseCiArgs(argv);
|
|
7092
|
-
const cwd = args.cwd ?
|
|
7362
|
+
const cwd = args.cwd ? path23.resolve(args.cwd) : process.cwd();
|
|
7093
7363
|
let earlyConfig;
|
|
7094
7364
|
try {
|
|
7095
7365
|
earlyConfig = loadConfig(cwd);
|
|
@@ -7099,9 +7369,9 @@ async function runCi(argv) {
|
|
|
7099
7369
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
7100
7370
|
const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
|
|
7101
7371
|
let manualWorkflowDispatch = false;
|
|
7102
|
-
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath &&
|
|
7372
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs26.existsSync(dispatchEventPath)) {
|
|
7103
7373
|
try {
|
|
7104
|
-
const evt = JSON.parse(
|
|
7374
|
+
const evt = JSON.parse(fs26.readFileSync(dispatchEventPath, "utf-8"));
|
|
7105
7375
|
const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
|
|
7106
7376
|
const sessionInput = String(evt?.inputs?.sessionId ?? "");
|
|
7107
7377
|
manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
|
|
@@ -7316,9 +7586,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
7316
7586
|
return result;
|
|
7317
7587
|
}
|
|
7318
7588
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
7319
|
-
const sessionFile =
|
|
7320
|
-
const eventsFile =
|
|
7321
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
7589
|
+
const sessionFile = path24.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
7590
|
+
const eventsFile = path24.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
7591
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs27.existsSync(path24.join(cwd, p)));
|
|
7322
7592
|
if (paths.length === 0) return;
|
|
7323
7593
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
7324
7594
|
try {
|
|
@@ -7356,7 +7626,7 @@ async function runChat(argv) {
|
|
|
7356
7626
|
${CHAT_HELP}`);
|
|
7357
7627
|
return 64;
|
|
7358
7628
|
}
|
|
7359
|
-
const cwd = args.cwd ?
|
|
7629
|
+
const cwd = args.cwd ? path24.resolve(args.cwd) : process.cwd();
|
|
7360
7630
|
const sessionId = args.sessionId;
|
|
7361
7631
|
const unpackedSecrets = unpackAllSecrets();
|
|
7362
7632
|
if (unpackedSecrets > 0) {
|
|
@@ -7443,7 +7713,7 @@ directory to add a new command.
|
|
|
7443
7713
|
Exit codes:
|
|
7444
7714
|
0 success (PR opened, verify passed \u2014 or resolve produced a merge commit)
|
|
7445
7715
|
1 agent reported FAILED (draft PR opened)
|
|
7446
|
-
2 verify failed (
|
|
7716
|
+
2 verify failed (no PR opened \u2014 branch pushed for inspection) \u2014 skipped in resolve mode
|
|
7447
7717
|
3 no commits to ship (also the resolve clean-merge short-circuit)
|
|
7448
7718
|
4 PR creation failed
|
|
7449
7719
|
5 uncommitted changes on target branch
|