@kody-ade/kody-engine 0.3.35 → 0.3.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/kody.js +441 -135
- package/dist/executables/mission-scheduler/profile.json +5 -7
- package/dist/executables/mission-tick/profile.json +12 -16
- package/dist/executables/mission-tick/prompt.md +15 -18
- 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/package.json +14 -15
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.38",
|
|
7
7
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
8
8
|
license: "MIT",
|
|
9
9
|
type: "module",
|
|
@@ -51,8 +51,8 @@ var package_default = {
|
|
|
51
51
|
|
|
52
52
|
// src/chat-cli.ts
|
|
53
53
|
import { execFileSync as execFileSync23 } from "child_process";
|
|
54
|
-
import * as
|
|
55
|
-
import * as
|
|
54
|
+
import * as fs24 from "fs";
|
|
55
|
+
import * as path21 from "path";
|
|
56
56
|
|
|
57
57
|
// src/chat/events.ts
|
|
58
58
|
import * as fs from "fs";
|
|
@@ -606,8 +606,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
606
606
|
|
|
607
607
|
// src/kody-cli.ts
|
|
608
608
|
import { execFileSync as execFileSync22 } from "child_process";
|
|
609
|
-
import * as
|
|
610
|
-
import * as
|
|
609
|
+
import * as fs23 from "fs";
|
|
610
|
+
import * as path20 from "path";
|
|
611
611
|
|
|
612
612
|
// src/dispatch.ts
|
|
613
613
|
import * as fs6 from "fs";
|
|
@@ -630,30 +630,51 @@ function getExecutablesRoot() {
|
|
|
630
630
|
}
|
|
631
631
|
return candidates[0];
|
|
632
632
|
}
|
|
633
|
-
function
|
|
634
|
-
|
|
635
|
-
|
|
633
|
+
function getProjectExecutablesRoot() {
|
|
634
|
+
return path5.join(process.cwd(), ".kody", "executables");
|
|
635
|
+
}
|
|
636
|
+
function getExecutableRoots() {
|
|
637
|
+
return [getProjectExecutablesRoot(), getExecutablesRoot()];
|
|
638
|
+
}
|
|
639
|
+
function listExecutables(roots = getExecutableRoots()) {
|
|
640
|
+
const rootList = typeof roots === "string" ? [roots] : roots;
|
|
641
|
+
const seen = /* @__PURE__ */ new Set();
|
|
636
642
|
const out = [];
|
|
637
|
-
for (const
|
|
638
|
-
if (!
|
|
639
|
-
const
|
|
640
|
-
|
|
641
|
-
|
|
643
|
+
for (const root of rootList) {
|
|
644
|
+
if (!fs5.existsSync(root)) continue;
|
|
645
|
+
const entries = fs5.readdirSync(root, { withFileTypes: true });
|
|
646
|
+
for (const ent of entries) {
|
|
647
|
+
if (!ent.isDirectory()) continue;
|
|
648
|
+
if (seen.has(ent.name)) continue;
|
|
649
|
+
const profilePath = path5.join(root, ent.name, "profile.json");
|
|
650
|
+
if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
|
|
651
|
+
out.push({ name: ent.name, profilePath });
|
|
652
|
+
seen.add(ent.name);
|
|
653
|
+
}
|
|
642
654
|
}
|
|
643
655
|
}
|
|
644
656
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
645
657
|
}
|
|
646
|
-
function
|
|
647
|
-
if (!isSafeName(name)) return
|
|
648
|
-
const
|
|
649
|
-
|
|
658
|
+
function resolveExecutable(name, roots = getExecutableRoots()) {
|
|
659
|
+
if (!isSafeName(name)) return null;
|
|
660
|
+
const rootList = typeof roots === "string" ? [roots] : roots;
|
|
661
|
+
for (const root of rootList) {
|
|
662
|
+
const profilePath = path5.join(root, name, "profile.json");
|
|
663
|
+
if (fs5.existsSync(profilePath) && fs5.statSync(profilePath).isFile()) {
|
|
664
|
+
return profilePath;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return null;
|
|
668
|
+
}
|
|
669
|
+
function hasExecutable(name, roots = getExecutableRoots()) {
|
|
670
|
+
return resolveExecutable(name, roots) !== null;
|
|
650
671
|
}
|
|
651
672
|
function isSafeName(name) {
|
|
652
673
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
653
674
|
}
|
|
654
|
-
function getProfileInputs(name,
|
|
655
|
-
|
|
656
|
-
|
|
675
|
+
function getProfileInputs(name, roots = getExecutableRoots()) {
|
|
676
|
+
const profilePath = resolveExecutable(name, roots);
|
|
677
|
+
if (!profilePath) return null;
|
|
657
678
|
try {
|
|
658
679
|
const raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
|
|
659
680
|
if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
|
|
@@ -834,8 +855,8 @@ function coerceBare(spec, value) {
|
|
|
834
855
|
|
|
835
856
|
// src/executor.ts
|
|
836
857
|
import { spawnSync } from "child_process";
|
|
837
|
-
import * as
|
|
838
|
-
import * as
|
|
858
|
+
import * as fs22 from "fs";
|
|
859
|
+
import * as path19 from "path";
|
|
839
860
|
|
|
840
861
|
// src/litellm.ts
|
|
841
862
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -982,10 +1003,7 @@ function loadProfile(profilePath) {
|
|
|
982
1003
|
throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
|
|
983
1004
|
}
|
|
984
1005
|
if (typeof r.role !== "string" || !VALID_ROLES.has(r.role)) {
|
|
985
|
-
throw new ProfileError(
|
|
986
|
-
profilePath,
|
|
987
|
-
`"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`
|
|
988
|
-
);
|
|
1006
|
+
throw new ProfileError(profilePath, `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`);
|
|
989
1007
|
}
|
|
990
1008
|
const role = r.role;
|
|
991
1009
|
let phase;
|
|
@@ -1176,7 +1194,10 @@ function parseScriptList(p, key, raw) {
|
|
|
1176
1194
|
const out = [];
|
|
1177
1195
|
for (const [i, item] of raw.entries()) {
|
|
1178
1196
|
if (!item || typeof item !== "object") {
|
|
1179
|
-
throw new ProfileError(
|
|
1197
|
+
throw new ProfileError(
|
|
1198
|
+
p,
|
|
1199
|
+
`scripts.${key}[${i}] must be an object like { script, runWhen? } or { shell, runWhen? }`
|
|
1200
|
+
);
|
|
1180
1201
|
}
|
|
1181
1202
|
const r = item;
|
|
1182
1203
|
const hasScript = typeof r.script === "string" && r.script.length > 0;
|
|
@@ -1185,7 +1206,10 @@ function parseScriptList(p, key, raw) {
|
|
|
1185
1206
|
throw new ProfileError(p, `scripts.${key}[${i}] cannot set both "script" and "shell" \u2014 pick one`);
|
|
1186
1207
|
}
|
|
1187
1208
|
if (!hasScript && !hasShell) {
|
|
1188
|
-
throw new ProfileError(
|
|
1209
|
+
throw new ProfileError(
|
|
1210
|
+
p,
|
|
1211
|
+
`scripts.${key}[${i}] must set "script" (registered TS function) or "shell" (filename in executable dir)`
|
|
1212
|
+
);
|
|
1189
1213
|
}
|
|
1190
1214
|
const entry = {};
|
|
1191
1215
|
if (hasScript) entry.script = r.script;
|
|
@@ -2093,11 +2117,11 @@ var diagMcp = async (_ctx) => {
|
|
|
2093
2117
|
process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
|
|
2094
2118
|
`);
|
|
2095
2119
|
try {
|
|
2096
|
-
const v = execFileSync6(
|
|
2097
|
-
"
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
).trim();
|
|
2120
|
+
const v = execFileSync6("npx", ["-y", "--package=@playwright/mcp@latest", "--", "playwright-mcp", "--version"], {
|
|
2121
|
+
stdio: "pipe",
|
|
2122
|
+
timeout: 6e4,
|
|
2123
|
+
encoding: "utf8"
|
|
2124
|
+
}).trim();
|
|
2101
2125
|
process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
|
|
2102
2126
|
`);
|
|
2103
2127
|
} catch (e) {
|
|
@@ -2273,7 +2297,9 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
2273
2297
|
if (routeFile) {
|
|
2274
2298
|
try {
|
|
2275
2299
|
const content = fs14.readFileSync(path13.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
|
|
2276
|
-
const methods = HTTP_METHODS.filter(
|
|
2300
|
+
const methods = HTTP_METHODS.filter(
|
|
2301
|
+
(m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
|
|
2302
|
+
);
|
|
2277
2303
|
if (methods.length > 0) {
|
|
2278
2304
|
out.push({
|
|
2279
2305
|
path: prefix,
|
|
@@ -2648,6 +2674,64 @@ function parsePr(url) {
|
|
|
2648
2674
|
return Number.isFinite(n) ? n : null;
|
|
2649
2675
|
}
|
|
2650
2676
|
|
|
2677
|
+
// src/scripts/dispatchMissionFileTicks.ts
|
|
2678
|
+
import * as fs16 from "fs";
|
|
2679
|
+
import * as path15 from "path";
|
|
2680
|
+
var dispatchMissionFileTicks = async (ctx, _profile, args) => {
|
|
2681
|
+
ctx.skipAgent = true;
|
|
2682
|
+
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
2683
|
+
if (!targetExecutable) {
|
|
2684
|
+
throw new Error("dispatchMissionFileTicks: `with.targetExecutable` is required");
|
|
2685
|
+
}
|
|
2686
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
2687
|
+
const slugArg = String(args?.slugArg ?? "mission");
|
|
2688
|
+
const slugs = listMissionSlugs(path15.join(ctx.cwd, missionsDir));
|
|
2689
|
+
ctx.data.missionSlugCount = slugs.length;
|
|
2690
|
+
if (slugs.length === 0) {
|
|
2691
|
+
process.stdout.write(`[missions] no mission files in ${missionsDir}
|
|
2692
|
+
`);
|
|
2693
|
+
return;
|
|
2694
|
+
}
|
|
2695
|
+
process.stdout.write(`[missions] ticking ${slugs.length} mission(s) via ${targetExecutable}
|
|
2696
|
+
`);
|
|
2697
|
+
const results = [];
|
|
2698
|
+
for (const slug of slugs) {
|
|
2699
|
+
process.stdout.write(`[missions] \u2192 tick ${slug}
|
|
2700
|
+
`);
|
|
2701
|
+
try {
|
|
2702
|
+
const out = await runExecutable(targetExecutable, {
|
|
2703
|
+
cliArgs: { [slugArg]: slug },
|
|
2704
|
+
cwd: ctx.cwd,
|
|
2705
|
+
config: ctx.config,
|
|
2706
|
+
verbose: ctx.verbose,
|
|
2707
|
+
quiet: ctx.quiet
|
|
2708
|
+
});
|
|
2709
|
+
results.push({ slug, exitCode: out.exitCode, reason: out.reason });
|
|
2710
|
+
if (out.exitCode !== 0) {
|
|
2711
|
+
process.stderr.write(`[missions] tick ${slug} failed (exit ${out.exitCode}): ${out.reason ?? ""}
|
|
2712
|
+
`);
|
|
2713
|
+
}
|
|
2714
|
+
} catch (err) {
|
|
2715
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2716
|
+
process.stderr.write(`[missions] tick ${slug} crashed: ${msg}
|
|
2717
|
+
`);
|
|
2718
|
+
results.push({ slug, exitCode: 99, reason: msg });
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
ctx.data.missionTickResults = results;
|
|
2722
|
+
ctx.output.exitCode = 0;
|
|
2723
|
+
};
|
|
2724
|
+
function listMissionSlugs(absDir) {
|
|
2725
|
+
if (!fs16.existsSync(absDir)) return [];
|
|
2726
|
+
let entries;
|
|
2727
|
+
try {
|
|
2728
|
+
entries = fs16.readdirSync(absDir, { withFileTypes: true });
|
|
2729
|
+
} catch {
|
|
2730
|
+
return [];
|
|
2731
|
+
}
|
|
2732
|
+
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();
|
|
2733
|
+
}
|
|
2734
|
+
|
|
2651
2735
|
// src/issue.ts
|
|
2652
2736
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2653
2737
|
var API_TIMEOUT_MS4 = 3e4;
|
|
@@ -2836,10 +2920,9 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
|
2836
2920
|
function listIssuesByLabel(label, cwd) {
|
|
2837
2921
|
let raw = "";
|
|
2838
2922
|
try {
|
|
2839
|
-
raw = gh2(
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
);
|
|
2923
|
+
raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
|
|
2924
|
+
cwd
|
|
2925
|
+
});
|
|
2843
2926
|
} catch {
|
|
2844
2927
|
return [];
|
|
2845
2928
|
}
|
|
@@ -3084,10 +3167,7 @@ function ensureLabels(cwd) {
|
|
|
3084
3167
|
}
|
|
3085
3168
|
function getIssueLabels(issueNumber, cwd) {
|
|
3086
3169
|
try {
|
|
3087
|
-
const output = gh2(
|
|
3088
|
-
["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"],
|
|
3089
|
-
{ cwd }
|
|
3090
|
-
);
|
|
3170
|
+
const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
3091
3171
|
return output.split("\n").filter(Boolean);
|
|
3092
3172
|
} catch {
|
|
3093
3173
|
return [];
|
|
@@ -3111,10 +3191,8 @@ function createLabelInRepo(spec, cwd) {
|
|
|
3111
3191
|
function setKodyLabel(issueNumber, spec, cwd) {
|
|
3112
3192
|
const target = spec.label;
|
|
3113
3193
|
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
3114
|
-
process.stderr.write(
|
|
3115
|
-
|
|
3116
|
-
`
|
|
3117
|
-
);
|
|
3194
|
+
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
3195
|
+
`);
|
|
3118
3196
|
return;
|
|
3119
3197
|
}
|
|
3120
3198
|
const targetGroup = groupOf(target);
|
|
@@ -3140,10 +3218,8 @@ function setKodyLabel(issueNumber, spec, cwd) {
|
|
|
3140
3218
|
return;
|
|
3141
3219
|
}
|
|
3142
3220
|
}
|
|
3143
|
-
process.stderr.write(
|
|
3144
|
-
|
|
3145
|
-
`
|
|
3146
|
-
);
|
|
3221
|
+
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
3222
|
+
`);
|
|
3147
3223
|
}
|
|
3148
3224
|
}
|
|
3149
3225
|
function looksLikeMissingLabel(err) {
|
|
@@ -3312,7 +3388,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
3312
3388
|
|
|
3313
3389
|
// src/gha.ts
|
|
3314
3390
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
3315
|
-
import * as
|
|
3391
|
+
import * as fs17 from "fs";
|
|
3316
3392
|
function getRunUrl() {
|
|
3317
3393
|
const server = process.env.GITHUB_SERVER_URL;
|
|
3318
3394
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -3323,10 +3399,10 @@ function getRunUrl() {
|
|
|
3323
3399
|
function reactToTriggerComment(cwd) {
|
|
3324
3400
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
3325
3401
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3326
|
-
if (!eventPath || !
|
|
3402
|
+
if (!eventPath || !fs17.existsSync(eventPath)) return;
|
|
3327
3403
|
let event = null;
|
|
3328
3404
|
try {
|
|
3329
|
-
event = JSON.parse(
|
|
3405
|
+
event = JSON.parse(fs17.readFileSync(eventPath, "utf-8"));
|
|
3330
3406
|
} catch {
|
|
3331
3407
|
return;
|
|
3332
3408
|
}
|
|
@@ -3566,22 +3642,22 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
3566
3642
|
|
|
3567
3643
|
// src/scripts/initFlow.ts
|
|
3568
3644
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
3569
|
-
import * as
|
|
3570
|
-
import * as
|
|
3645
|
+
import * as fs19 from "fs";
|
|
3646
|
+
import * as path17 from "path";
|
|
3571
3647
|
|
|
3572
3648
|
// src/scripts/loadQaGuide.ts
|
|
3573
|
-
import * as
|
|
3574
|
-
import * as
|
|
3649
|
+
import * as fs18 from "fs";
|
|
3650
|
+
import * as path16 from "path";
|
|
3575
3651
|
var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
|
|
3576
3652
|
var loadQaGuide = async (ctx) => {
|
|
3577
|
-
const full =
|
|
3578
|
-
if (!
|
|
3653
|
+
const full = path16.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
3654
|
+
if (!fs18.existsSync(full)) {
|
|
3579
3655
|
ctx.data.qaGuide = "";
|
|
3580
3656
|
ctx.data.qaGuidePath = "";
|
|
3581
3657
|
return;
|
|
3582
3658
|
}
|
|
3583
3659
|
try {
|
|
3584
|
-
ctx.data.qaGuide =
|
|
3660
|
+
ctx.data.qaGuide = fs18.readFileSync(full, "utf-8");
|
|
3585
3661
|
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
3586
3662
|
} catch {
|
|
3587
3663
|
ctx.data.qaGuide = "";
|
|
@@ -3591,9 +3667,9 @@ var loadQaGuide = async (ctx) => {
|
|
|
3591
3667
|
|
|
3592
3668
|
// src/scripts/initFlow.ts
|
|
3593
3669
|
function detectPackageManager(cwd) {
|
|
3594
|
-
if (
|
|
3595
|
-
if (
|
|
3596
|
-
if (
|
|
3670
|
+
if (fs19.existsSync(path17.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
3671
|
+
if (fs19.existsSync(path17.join(cwd, "yarn.lock"))) return "yarn";
|
|
3672
|
+
if (fs19.existsSync(path17.join(cwd, "bun.lockb"))) return "bun";
|
|
3597
3673
|
return "npm";
|
|
3598
3674
|
}
|
|
3599
3675
|
function qualityCommandsFor(pm) {
|
|
@@ -3715,33 +3791,33 @@ function performInit(cwd, force) {
|
|
|
3715
3791
|
const pm = detectPackageManager(cwd);
|
|
3716
3792
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
3717
3793
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
3718
|
-
const configPath =
|
|
3719
|
-
if (
|
|
3794
|
+
const configPath = path17.join(cwd, "kody.config.json");
|
|
3795
|
+
if (fs19.existsSync(configPath) && !force) {
|
|
3720
3796
|
skipped.push("kody.config.json");
|
|
3721
3797
|
} else {
|
|
3722
3798
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
3723
|
-
|
|
3799
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
3724
3800
|
`);
|
|
3725
3801
|
wrote.push("kody.config.json");
|
|
3726
3802
|
}
|
|
3727
|
-
const workflowDir =
|
|
3728
|
-
const workflowPath =
|
|
3729
|
-
if (
|
|
3803
|
+
const workflowDir = path17.join(cwd, ".github", "workflows");
|
|
3804
|
+
const workflowPath = path17.join(workflowDir, "kody.yml");
|
|
3805
|
+
if (fs19.existsSync(workflowPath) && !force) {
|
|
3730
3806
|
skipped.push(".github/workflows/kody.yml");
|
|
3731
3807
|
} else {
|
|
3732
|
-
|
|
3733
|
-
|
|
3808
|
+
fs19.mkdirSync(workflowDir, { recursive: true });
|
|
3809
|
+
fs19.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
3734
3810
|
wrote.push(".github/workflows/kody.yml");
|
|
3735
3811
|
}
|
|
3736
|
-
const hasUi =
|
|
3812
|
+
const hasUi = fs19.existsSync(path17.join(cwd, "src/app")) || fs19.existsSync(path17.join(cwd, "app")) || fs19.existsSync(path17.join(cwd, "pages"));
|
|
3737
3813
|
if (hasUi) {
|
|
3738
|
-
const qaGuidePath =
|
|
3739
|
-
if (
|
|
3814
|
+
const qaGuidePath = path17.join(cwd, QA_GUIDE_REL_PATH);
|
|
3815
|
+
if (fs19.existsSync(qaGuidePath) && !force) {
|
|
3740
3816
|
skipped.push(QA_GUIDE_REL_PATH);
|
|
3741
3817
|
} else {
|
|
3742
|
-
|
|
3818
|
+
fs19.mkdirSync(path17.dirname(qaGuidePath), { recursive: true });
|
|
3743
3819
|
const discovery = runQaDiscovery(cwd);
|
|
3744
|
-
|
|
3820
|
+
fs19.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
3745
3821
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
3746
3822
|
}
|
|
3747
3823
|
}
|
|
@@ -3753,12 +3829,12 @@ function performInit(cwd, force) {
|
|
|
3753
3829
|
continue;
|
|
3754
3830
|
}
|
|
3755
3831
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
3756
|
-
const target =
|
|
3757
|
-
if (
|
|
3832
|
+
const target = path17.join(workflowDir, `kody-${exe.name}.yml`);
|
|
3833
|
+
if (fs19.existsSync(target) && !force) {
|
|
3758
3834
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
3759
3835
|
continue;
|
|
3760
3836
|
}
|
|
3761
|
-
|
|
3837
|
+
fs19.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
3762
3838
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
3763
3839
|
}
|
|
3764
3840
|
let labels;
|
|
@@ -3816,10 +3892,8 @@ var initFlow = async (ctx) => {
|
|
|
3816
3892
|
`);
|
|
3817
3893
|
}
|
|
3818
3894
|
if (labels.failed.length > 0) {
|
|
3819
|
-
process.stdout.write(
|
|
3820
|
-
|
|
3821
|
-
`
|
|
3822
|
-
);
|
|
3895
|
+
process.stdout.write(` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
|
|
3896
|
+
`);
|
|
3823
3897
|
}
|
|
3824
3898
|
}
|
|
3825
3899
|
process.stdout.write(
|
|
@@ -3877,6 +3951,9 @@ function isStateEnvelope(x) {
|
|
|
3877
3951
|
const o = x;
|
|
3878
3952
|
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);
|
|
3879
3953
|
}
|
|
3954
|
+
function initialStateEnvelope(cursor = "seed") {
|
|
3955
|
+
return { version: 1, rev: 0, cursor, data: {}, done: false };
|
|
3956
|
+
}
|
|
3880
3957
|
function formatStateCommentBody(marker, state) {
|
|
3881
3958
|
return `<!-- ${marker} -->
|
|
3882
3959
|
|
|
@@ -3924,10 +4001,10 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
|
3924
4001
|
}
|
|
3925
4002
|
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
3926
4003
|
const body = formatStateCommentBody(marker, state);
|
|
3927
|
-
const raw = gh2(
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
);
|
|
4004
|
+
const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
4005
|
+
cwd,
|
|
4006
|
+
input: JSON.stringify({ body })
|
|
4007
|
+
});
|
|
3931
4008
|
const parsed = JSON.parse(raw);
|
|
3932
4009
|
try {
|
|
3933
4010
|
minimizeComment(parsed.node_id, cwd);
|
|
@@ -3937,10 +4014,10 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
|
3937
4014
|
}
|
|
3938
4015
|
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
3939
4016
|
const body = formatStateCommentBody(marker, state);
|
|
3940
|
-
gh2(
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
);
|
|
4017
|
+
gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
4018
|
+
cwd,
|
|
4019
|
+
input: JSON.stringify({ body })
|
|
4020
|
+
});
|
|
3944
4021
|
try {
|
|
3945
4022
|
minimizeComment(commentNodeId, cwd);
|
|
3946
4023
|
} catch {
|
|
@@ -3977,6 +4054,168 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
3977
4054
|
ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
|
|
3978
4055
|
};
|
|
3979
4056
|
|
|
4057
|
+
// src/scripts/loadMissionFromFile.ts
|
|
4058
|
+
import * as fs20 from "fs";
|
|
4059
|
+
import * as path18 from "path";
|
|
4060
|
+
|
|
4061
|
+
// src/scripts/missionGist.ts
|
|
4062
|
+
function gistDescription(owner, repo, slug) {
|
|
4063
|
+
return `kody-mission:${owner}/${repo}:${slug}`;
|
|
4064
|
+
}
|
|
4065
|
+
function listGists(cwd) {
|
|
4066
|
+
let raw = "";
|
|
4067
|
+
try {
|
|
4068
|
+
raw = gh2(["api", "--paginate", "/gists?per_page=100"], { cwd });
|
|
4069
|
+
} catch {
|
|
4070
|
+
return [];
|
|
4071
|
+
}
|
|
4072
|
+
let parsed;
|
|
4073
|
+
try {
|
|
4074
|
+
parsed = JSON.parse(raw);
|
|
4075
|
+
} catch {
|
|
4076
|
+
return [];
|
|
4077
|
+
}
|
|
4078
|
+
if (!Array.isArray(parsed)) return [];
|
|
4079
|
+
return parsed.filter((g) => typeof g.id === "string").map((g) => ({
|
|
4080
|
+
id: g.id,
|
|
4081
|
+
description: typeof g.description === "string" ? g.description : null,
|
|
4082
|
+
files: g.files ?? {}
|
|
4083
|
+
}));
|
|
4084
|
+
}
|
|
4085
|
+
function getGist(gistId, cwd) {
|
|
4086
|
+
let raw = "";
|
|
4087
|
+
try {
|
|
4088
|
+
raw = gh2(["api", `/gists/${gistId}`], { cwd });
|
|
4089
|
+
} catch {
|
|
4090
|
+
return null;
|
|
4091
|
+
}
|
|
4092
|
+
let parsed;
|
|
4093
|
+
try {
|
|
4094
|
+
parsed = JSON.parse(raw);
|
|
4095
|
+
} catch {
|
|
4096
|
+
return null;
|
|
4097
|
+
}
|
|
4098
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
4099
|
+
const o = parsed;
|
|
4100
|
+
if (typeof o.id !== "string") return null;
|
|
4101
|
+
return {
|
|
4102
|
+
id: o.id,
|
|
4103
|
+
description: typeof o.description === "string" ? o.description : null,
|
|
4104
|
+
files: o.files ?? {}
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
function findGistByDescription(description, cwd) {
|
|
4108
|
+
const all = listGists(cwd);
|
|
4109
|
+
return all.find((g) => g.description === description) ?? null;
|
|
4110
|
+
}
|
|
4111
|
+
function readEnvelope(gist) {
|
|
4112
|
+
const file = gist.files["state.json"];
|
|
4113
|
+
if (!file?.content) return null;
|
|
4114
|
+
let parsed;
|
|
4115
|
+
try {
|
|
4116
|
+
parsed = JSON.parse(file.content);
|
|
4117
|
+
} catch {
|
|
4118
|
+
const fallback = parseStateCommentBody("kody-mission-state", file.content);
|
|
4119
|
+
return fallback ?? null;
|
|
4120
|
+
}
|
|
4121
|
+
return isStateEnvelope(parsed) ? parsed : null;
|
|
4122
|
+
}
|
|
4123
|
+
function findMissionGist(owner, repo, slug, cwd) {
|
|
4124
|
+
const desc = gistDescription(owner, repo, slug);
|
|
4125
|
+
const gist = findGistByDescription(desc, cwd);
|
|
4126
|
+
if (!gist) return null;
|
|
4127
|
+
const full = getGist(gist.id, cwd) ?? gist;
|
|
4128
|
+
const envelope = readEnvelope(full);
|
|
4129
|
+
if (!envelope) return null;
|
|
4130
|
+
return { gistId: full.id, state: envelope };
|
|
4131
|
+
}
|
|
4132
|
+
function createMissionGist(owner, repo, slug, cursor = "seed", cwd) {
|
|
4133
|
+
const description = gistDescription(owner, repo, slug);
|
|
4134
|
+
const initial = initialStateEnvelope(cursor);
|
|
4135
|
+
const payload = {
|
|
4136
|
+
description,
|
|
4137
|
+
public: false,
|
|
4138
|
+
files: {
|
|
4139
|
+
"state.json": { content: JSON.stringify(initial, null, 2) + "\n" }
|
|
4140
|
+
}
|
|
4141
|
+
};
|
|
4142
|
+
const raw = gh2(["api", "--method", "POST", "/gists", "--input", "-"], {
|
|
4143
|
+
cwd,
|
|
4144
|
+
input: JSON.stringify(payload)
|
|
4145
|
+
});
|
|
4146
|
+
let parsed;
|
|
4147
|
+
try {
|
|
4148
|
+
parsed = JSON.parse(raw);
|
|
4149
|
+
} catch {
|
|
4150
|
+
throw new Error(`createMissionGist: gh did not return JSON: ${raw.slice(0, 200)}`);
|
|
4151
|
+
}
|
|
4152
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.id !== "string") {
|
|
4153
|
+
throw new Error("createMissionGist: gist creation response missing id");
|
|
4154
|
+
}
|
|
4155
|
+
return { gistId: parsed.id, state: initial };
|
|
4156
|
+
}
|
|
4157
|
+
function writeMissionGist(gistId, next, cwd) {
|
|
4158
|
+
const payload = {
|
|
4159
|
+
files: {
|
|
4160
|
+
"state.json": { content: JSON.stringify(next, null, 2) + "\n" }
|
|
4161
|
+
}
|
|
4162
|
+
};
|
|
4163
|
+
gh2(["api", "--method", "PATCH", `/gists/${gistId}`, "--input", "-"], {
|
|
4164
|
+
cwd,
|
|
4165
|
+
input: JSON.stringify(payload)
|
|
4166
|
+
});
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
// src/scripts/loadMissionFromFile.ts
|
|
4170
|
+
var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
4171
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
4172
|
+
const slugArg = String(args?.slugArg ?? "mission");
|
|
4173
|
+
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
4174
|
+
if (!slug) {
|
|
4175
|
+
throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4176
|
+
}
|
|
4177
|
+
const owner = ctx.config.github.owner;
|
|
4178
|
+
const repo = ctx.config.github.repo;
|
|
4179
|
+
if (!owner || !repo) {
|
|
4180
|
+
throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
|
|
4181
|
+
}
|
|
4182
|
+
const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
|
|
4183
|
+
if (!fs20.existsSync(absPath)) {
|
|
4184
|
+
throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
|
|
4185
|
+
}
|
|
4186
|
+
const raw = fs20.readFileSync(absPath, "utf-8");
|
|
4187
|
+
const { title, body } = parseMissionFile(raw, slug);
|
|
4188
|
+
let loaded = findMissionGist(owner, repo, slug, ctx.cwd);
|
|
4189
|
+
if (!loaded) {
|
|
4190
|
+
loaded = createMissionGist(owner, repo, slug, "seed", ctx.cwd);
|
|
4191
|
+
}
|
|
4192
|
+
ctx.data.missionSlug = slug;
|
|
4193
|
+
ctx.data.missionTitle = title;
|
|
4194
|
+
ctx.data.missionIntent = body;
|
|
4195
|
+
ctx.data.missionGist = loaded;
|
|
4196
|
+
ctx.data.missionStateJson = JSON.stringify(loaded.state, null, 2);
|
|
4197
|
+
};
|
|
4198
|
+
function parseMissionFile(raw, slug) {
|
|
4199
|
+
let stripped = raw;
|
|
4200
|
+
if (stripped.startsWith("---\n")) {
|
|
4201
|
+
const end = stripped.indexOf("\n---\n", 4);
|
|
4202
|
+
if (end !== -1) {
|
|
4203
|
+
stripped = stripped.slice(end + 5);
|
|
4204
|
+
}
|
|
4205
|
+
}
|
|
4206
|
+
const trimmed = stripped.trim();
|
|
4207
|
+
const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
|
|
4208
|
+
const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
|
|
4209
|
+
if (h1) {
|
|
4210
|
+
const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
|
|
4211
|
+
return { title: h1[1].trim(), body: rest };
|
|
4212
|
+
}
|
|
4213
|
+
return { title: humanizeSlug(slug), body: trimmed };
|
|
4214
|
+
}
|
|
4215
|
+
function humanizeSlug(slug) {
|
|
4216
|
+
return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
|
|
4217
|
+
}
|
|
4218
|
+
|
|
3980
4219
|
// src/scripts/loadPriorArt.ts
|
|
3981
4220
|
var PER_PR_DIFF_MAX_BYTES = 8e3;
|
|
3982
4221
|
var TOTAL_MAX_BYTES = 3e4;
|
|
@@ -4269,6 +4508,53 @@ function escapeRegex(s) {
|
|
|
4269
4508
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
4270
4509
|
}
|
|
4271
4510
|
|
|
4511
|
+
// src/scripts/parseMissionStateFromAgentResult.ts
|
|
4512
|
+
function isPartialEnvelope2(x) {
|
|
4513
|
+
if (x === null || typeof x !== "object") return false;
|
|
4514
|
+
const o = x;
|
|
4515
|
+
return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
4516
|
+
}
|
|
4517
|
+
var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
4518
|
+
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
4519
|
+
if (!fenceLabel) {
|
|
4520
|
+
throw new Error("parseMissionStateFromAgentResult: `with.fenceLabel` is required");
|
|
4521
|
+
}
|
|
4522
|
+
if (!agentResult) {
|
|
4523
|
+
ctx.data.nextStateParseError = "agent did not run";
|
|
4524
|
+
return;
|
|
4525
|
+
}
|
|
4526
|
+
const fenceRegex = new RegExp("```" + escapeRegex2(fenceLabel) + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
4527
|
+
const match = fenceRegex.exec(agentResult.finalText);
|
|
4528
|
+
if (!match) {
|
|
4529
|
+
ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
|
|
4530
|
+
return;
|
|
4531
|
+
}
|
|
4532
|
+
let parsed;
|
|
4533
|
+
try {
|
|
4534
|
+
parsed = JSON.parse(match[1].trim());
|
|
4535
|
+
} catch (err) {
|
|
4536
|
+
ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4537
|
+
return;
|
|
4538
|
+
}
|
|
4539
|
+
if (!isPartialEnvelope2(parsed)) {
|
|
4540
|
+
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
4541
|
+
return;
|
|
4542
|
+
}
|
|
4543
|
+
const loaded = ctx.data.missionGist;
|
|
4544
|
+
const prevRev = loaded?.state.rev ?? 0;
|
|
4545
|
+
const next = {
|
|
4546
|
+
version: 1,
|
|
4547
|
+
rev: prevRev + 1,
|
|
4548
|
+
cursor: parsed.cursor,
|
|
4549
|
+
data: parsed.data,
|
|
4550
|
+
done: parsed.done
|
|
4551
|
+
};
|
|
4552
|
+
ctx.data.nextMissionState = next;
|
|
4553
|
+
};
|
|
4554
|
+
function escapeRegex2(s) {
|
|
4555
|
+
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
4556
|
+
}
|
|
4557
|
+
|
|
4272
4558
|
// src/scripts/persistArtifacts.ts
|
|
4273
4559
|
var persistArtifacts = async (ctx, profile) => {
|
|
4274
4560
|
if (profile.outputArtifacts.length === 0) return;
|
|
@@ -4334,16 +4620,16 @@ var postClassification = async (ctx) => {
|
|
|
4334
4620
|
}
|
|
4335
4621
|
if (!classification) {
|
|
4336
4622
|
ctx.data.action = failedAction("classification missing or invalid");
|
|
4337
|
-
tryAuditComment(
|
|
4623
|
+
tryAuditComment(
|
|
4624
|
+
issueNumber,
|
|
4625
|
+
"\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
|
|
4626
|
+
ctx.cwd
|
|
4627
|
+
);
|
|
4338
4628
|
ctx.output.exitCode = 1;
|
|
4339
4629
|
ctx.output.reason = "classify: no decision";
|
|
4340
4630
|
return;
|
|
4341
4631
|
}
|
|
4342
|
-
tryAuditComment(
|
|
4343
|
-
issueNumber,
|
|
4344
|
-
`\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
|
|
4345
|
-
ctx.cwd
|
|
4346
|
-
);
|
|
4632
|
+
tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
|
|
4347
4633
|
try {
|
|
4348
4634
|
execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
|
|
4349
4635
|
cwd: ctx.cwd,
|
|
@@ -4633,7 +4919,9 @@ var requirePlanDeviations = async (ctx, profile) => {
|
|
|
4633
4919
|
ctx.data.planDeviationCount = bullets.length;
|
|
4634
4920
|
};
|
|
4635
4921
|
function isNoneSentinel(block) {
|
|
4636
|
-
const stripped = block.split("\n").map(
|
|
4922
|
+
const stripped = block.split("\n").map(
|
|
4923
|
+
(l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()
|
|
4924
|
+
).filter((l) => l.length > 0);
|
|
4637
4925
|
if (stripped.length !== 1) return false;
|
|
4638
4926
|
return stripped[0] === "none";
|
|
4639
4927
|
}
|
|
@@ -5265,16 +5553,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
|
|
|
5265
5553
|
};
|
|
5266
5554
|
function fetchChecks(prNumber, cwd) {
|
|
5267
5555
|
try {
|
|
5268
|
-
const raw = execFileSync20(
|
|
5269
|
-
"
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
cwd,
|
|
5275
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5276
|
-
}
|
|
5277
|
-
);
|
|
5556
|
+
const raw = execFileSync20("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
|
|
5557
|
+
encoding: "utf-8",
|
|
5558
|
+
timeout: API_TIMEOUT_MS9,
|
|
5559
|
+
cwd,
|
|
5560
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5561
|
+
});
|
|
5278
5562
|
const parsed = JSON.parse(raw);
|
|
5279
5563
|
return Array.isArray(parsed) ? parsed : [];
|
|
5280
5564
|
} catch (err) {
|
|
@@ -5353,10 +5637,7 @@ function formatStaleReport(stale, staleDays) {
|
|
|
5353
5637
|
if (stale.length === 0) {
|
|
5354
5638
|
return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
|
|
5355
5639
|
}
|
|
5356
|
-
const lines = [
|
|
5357
|
-
`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
|
|
5358
|
-
""
|
|
5359
|
-
];
|
|
5640
|
+
const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
|
|
5360
5641
|
for (const pr of stale.slice(0, 50)) {
|
|
5361
5642
|
lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
|
|
5362
5643
|
}
|
|
@@ -5420,8 +5701,29 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
5420
5701
|
}
|
|
5421
5702
|
};
|
|
5422
5703
|
|
|
5704
|
+
// src/scripts/writeMissionGistState.ts
|
|
5705
|
+
var writeMissionGistState = async (ctx, _profile, _agentResult) => {
|
|
5706
|
+
const parseError = ctx.data.nextStateParseError;
|
|
5707
|
+
if (parseError) {
|
|
5708
|
+
process.stderr.write(`[kody] mission state write skipped: ${parseError}
|
|
5709
|
+
`);
|
|
5710
|
+
if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
|
|
5711
|
+
if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
|
|
5712
|
+
return;
|
|
5713
|
+
}
|
|
5714
|
+
const next = ctx.data.nextMissionState;
|
|
5715
|
+
if (!next) {
|
|
5716
|
+
return;
|
|
5717
|
+
}
|
|
5718
|
+
const loaded = ctx.data.missionGist;
|
|
5719
|
+
if (!loaded) {
|
|
5720
|
+
throw new Error("writeMissionGistState: ctx.data.missionGist missing \u2014 preflight must run first");
|
|
5721
|
+
}
|
|
5722
|
+
writeMissionGist(loaded.gistId, next, ctx.cwd);
|
|
5723
|
+
};
|
|
5724
|
+
|
|
5423
5725
|
// src/scripts/writeRunSummary.ts
|
|
5424
|
-
import * as
|
|
5726
|
+
import * as fs21 from "fs";
|
|
5425
5727
|
var writeRunSummary = async (ctx, profile) => {
|
|
5426
5728
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
5427
5729
|
if (!summaryPath) return;
|
|
@@ -5443,7 +5745,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
5443
5745
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
5444
5746
|
lines.push("");
|
|
5445
5747
|
try {
|
|
5446
|
-
|
|
5748
|
+
fs21.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
5447
5749
|
`);
|
|
5448
5750
|
} catch {
|
|
5449
5751
|
}
|
|
@@ -5462,6 +5764,7 @@ var preflightScripts = {
|
|
|
5462
5764
|
loadTaskState,
|
|
5463
5765
|
loadIssueContext,
|
|
5464
5766
|
loadIssueStateComment,
|
|
5767
|
+
loadMissionFromFile,
|
|
5465
5768
|
loadConventions,
|
|
5466
5769
|
loadCoverageRules,
|
|
5467
5770
|
loadPriorArt,
|
|
@@ -5476,12 +5779,15 @@ var preflightScripts = {
|
|
|
5476
5779
|
skipAgent,
|
|
5477
5780
|
classifyByLabel,
|
|
5478
5781
|
diagMcp,
|
|
5479
|
-
dispatchMissionTicks
|
|
5782
|
+
dispatchMissionTicks,
|
|
5783
|
+
dispatchMissionFileTicks
|
|
5480
5784
|
};
|
|
5481
5785
|
var postflightScripts = {
|
|
5482
5786
|
parseAgentResult: parseAgentResult2,
|
|
5483
5787
|
parseIssueStateFromAgentResult,
|
|
5788
|
+
parseMissionStateFromAgentResult,
|
|
5484
5789
|
writeIssueStateComment,
|
|
5790
|
+
writeMissionGistState,
|
|
5485
5791
|
requireFeedbackActions,
|
|
5486
5792
|
requirePlanDeviations,
|
|
5487
5793
|
verify,
|
|
@@ -5616,9 +5922,9 @@ async function runExecutable(profileName, input) {
|
|
|
5616
5922
|
data: {},
|
|
5617
5923
|
output: { exitCode: 0 }
|
|
5618
5924
|
};
|
|
5619
|
-
const ndjsonDir =
|
|
5925
|
+
const ndjsonDir = path19.join(input.cwd, ".kody");
|
|
5620
5926
|
const invokeAgent = async (prompt) => {
|
|
5621
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
5927
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path19.isAbsolute(p) ? p : path19.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
5622
5928
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
5623
5929
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
5624
5930
|
return runAgent({
|
|
@@ -5694,17 +6000,17 @@ async function runExecutable(profileName, input) {
|
|
|
5694
6000
|
}
|
|
5695
6001
|
}
|
|
5696
6002
|
function resolveProfilePath(profileName) {
|
|
5697
|
-
const here =
|
|
6003
|
+
const here = path19.dirname(new URL(import.meta.url).pathname);
|
|
5698
6004
|
const candidates = [
|
|
5699
|
-
|
|
6005
|
+
path19.join(here, "executables", profileName, "profile.json"),
|
|
5700
6006
|
// same-dir sibling (dev)
|
|
5701
|
-
|
|
6007
|
+
path19.join(here, "..", "executables", profileName, "profile.json"),
|
|
5702
6008
|
// up one (prod: dist/bin → dist/executables)
|
|
5703
|
-
|
|
6009
|
+
path19.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
5704
6010
|
// fallback
|
|
5705
6011
|
];
|
|
5706
6012
|
for (const c of candidates) {
|
|
5707
|
-
if (
|
|
6013
|
+
if (fs22.existsSync(c)) return c;
|
|
5708
6014
|
}
|
|
5709
6015
|
return candidates[0];
|
|
5710
6016
|
}
|
|
@@ -5797,8 +6103,8 @@ function finish(out) {
|
|
|
5797
6103
|
var SHELL_TIMEOUT_MS = 3e5;
|
|
5798
6104
|
function runShellEntry(entry, ctx, profile) {
|
|
5799
6105
|
const shellName = entry.shell;
|
|
5800
|
-
const shellPath =
|
|
5801
|
-
if (!
|
|
6106
|
+
const shellPath = path19.join(profile.dir, shellName);
|
|
6107
|
+
if (!fs22.existsSync(shellPath)) {
|
|
5802
6108
|
ctx.skipAgent = true;
|
|
5803
6109
|
ctx.output.exitCode = 99;
|
|
5804
6110
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -5948,9 +6254,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
5948
6254
|
return token;
|
|
5949
6255
|
}
|
|
5950
6256
|
function detectPackageManager2(cwd) {
|
|
5951
|
-
if (
|
|
5952
|
-
if (
|
|
5953
|
-
if (
|
|
6257
|
+
if (fs23.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
6258
|
+
if (fs23.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
|
|
6259
|
+
if (fs23.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
|
|
5954
6260
|
return "npm";
|
|
5955
6261
|
}
|
|
5956
6262
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -6030,11 +6336,11 @@ function configureGitIdentity(cwd) {
|
|
|
6030
6336
|
}
|
|
6031
6337
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
6032
6338
|
if (!issueNumber) return;
|
|
6033
|
-
const logPath =
|
|
6339
|
+
const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
|
|
6034
6340
|
let tail = "";
|
|
6035
6341
|
try {
|
|
6036
|
-
if (
|
|
6037
|
-
const content =
|
|
6342
|
+
if (fs23.existsSync(logPath)) {
|
|
6343
|
+
const content = fs23.readFileSync(logPath, "utf-8");
|
|
6038
6344
|
tail = content.slice(-3e3);
|
|
6039
6345
|
}
|
|
6040
6346
|
} catch {
|
|
@@ -6059,7 +6365,7 @@ async function runCi(argv) {
|
|
|
6059
6365
|
return 0;
|
|
6060
6366
|
}
|
|
6061
6367
|
const args = parseCiArgs(argv);
|
|
6062
|
-
const cwd = args.cwd ?
|
|
6368
|
+
const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
|
|
6063
6369
|
let earlyConfig;
|
|
6064
6370
|
try {
|
|
6065
6371
|
earlyConfig = loadConfig(cwd);
|
|
@@ -6197,9 +6503,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
6197
6503
|
return result;
|
|
6198
6504
|
}
|
|
6199
6505
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
6200
|
-
const sessionFile =
|
|
6201
|
-
const eventsFile =
|
|
6202
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
6506
|
+
const sessionFile = path21.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
6507
|
+
const eventsFile = path21.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
6508
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs24.existsSync(path21.join(cwd, p)));
|
|
6203
6509
|
if (paths.length === 0) return;
|
|
6204
6510
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
6205
6511
|
try {
|
|
@@ -6237,7 +6543,7 @@ async function runChat(argv) {
|
|
|
6237
6543
|
${CHAT_HELP}`);
|
|
6238
6544
|
return 64;
|
|
6239
6545
|
}
|
|
6240
|
-
const cwd = args.cwd ?
|
|
6546
|
+
const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
|
|
6241
6547
|
const sessionId = args.sessionId;
|
|
6242
6548
|
const unpackedSecrets = unpackAllSecrets();
|
|
6243
6549
|
if (unpackedSecrets > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mission-scheduler",
|
|
3
3
|
"role": "watch",
|
|
4
|
-
"describe": "Scheduled: for every
|
|
4
|
+
"describe": "Scheduled: for every mission file under .kody/missions/, invoke mission-tick once. No agent on the scheduler itself.",
|
|
5
5
|
"kind": "scheduled",
|
|
6
6
|
"schedule": "*/5 * * * *",
|
|
7
7
|
"inputs": [],
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"verify": "gh auth status",
|
|
30
30
|
"usage": "",
|
|
31
|
-
"allowedUses": ["issue"]
|
|
31
|
+
"allowedUses": ["api", "issue", "pr"]
|
|
32
32
|
}
|
|
33
33
|
],
|
|
34
34
|
"inputArtifacts": [],
|
|
@@ -36,13 +36,11 @@
|
|
|
36
36
|
"scripts": {
|
|
37
37
|
"preflight": [
|
|
38
38
|
{
|
|
39
|
-
"script": "
|
|
39
|
+
"script": "dispatchMissionFileTicks",
|
|
40
40
|
"with": {
|
|
41
|
-
"
|
|
42
|
-
"color": "1d76db",
|
|
43
|
-
"description": "kody: issue is a mission — scheduler ticks it every cron wake",
|
|
41
|
+
"missionsDir": ".kody/missions",
|
|
44
42
|
"targetExecutable": "mission-tick",
|
|
45
|
-
"
|
|
43
|
+
"slugArg": "mission"
|
|
46
44
|
}
|
|
47
45
|
}
|
|
48
46
|
],
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mission-tick",
|
|
3
3
|
"role": "primitive",
|
|
4
|
-
"describe": "One classifier tick for one mission
|
|
4
|
+
"describe": "One classifier tick for one mission file: read intent + state, decide and execute via gh, emit next state.",
|
|
5
5
|
"kind": "oneshot",
|
|
6
6
|
"inputs": [
|
|
7
7
|
{
|
|
8
|
-
"name": "
|
|
9
|
-
"flag": "--
|
|
10
|
-
"type": "
|
|
8
|
+
"name": "mission",
|
|
9
|
+
"flag": "--mission",
|
|
10
|
+
"type": "string",
|
|
11
11
|
"required": true,
|
|
12
|
-
"describe": "
|
|
12
|
+
"describe": "Mission slug — basename (without .md) of the file under .kody/missions/."
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"claudeCode": {
|
|
@@ -34,8 +34,8 @@
|
|
|
34
34
|
"checkCommand": "command -v gh"
|
|
35
35
|
},
|
|
36
36
|
"verify": "gh auth status",
|
|
37
|
-
"usage": "Use `gh` for all GitHub actions: `gh
|
|
38
|
-
"allowedUses": ["
|
|
37
|
+
"usage": "Use `gh` for all GitHub actions: `gh pr list ...` to enumerate candidate PRs, `gh pr comment <n> --body \"...\"` to issue a Kody command, `gh pr view <n> --json mergeable,statusCheckRollup,headRefOid` to inspect state, `gh api ...` for anything else. NEVER edit files in the working tree.",
|
|
38
|
+
"allowedUses": ["pr", "api", "issue"]
|
|
39
39
|
}
|
|
40
40
|
],
|
|
41
41
|
"inputArtifacts": [],
|
|
@@ -43,10 +43,10 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"preflight": [
|
|
45
45
|
{
|
|
46
|
-
"script": "
|
|
46
|
+
"script": "loadMissionFromFile",
|
|
47
47
|
"with": {
|
|
48
|
-
"
|
|
49
|
-
"
|
|
48
|
+
"missionsDir": ".kody/missions",
|
|
49
|
+
"slugArg": "mission"
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
{
|
|
@@ -55,17 +55,13 @@
|
|
|
55
55
|
],
|
|
56
56
|
"postflight": [
|
|
57
57
|
{
|
|
58
|
-
"script": "
|
|
58
|
+
"script": "parseMissionStateFromAgentResult",
|
|
59
59
|
"with": {
|
|
60
60
|
"fenceLabel": "kody-mission-next-state"
|
|
61
61
|
}
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
|
-
"script": "
|
|
65
|
-
"with": {
|
|
66
|
-
"marker": "kody-mission-state",
|
|
67
|
-
"issueArg": "issue"
|
|
68
|
-
}
|
|
64
|
+
"script": "writeMissionGistState"
|
|
69
65
|
}
|
|
70
66
|
]
|
|
71
67
|
}
|
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
You are **kody mission-tick**, the coordinator for one
|
|
1
|
+
You are **kody mission-tick**, the coordinator for one file-based mission. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state and issuing Kody commands as PR comments.
|
|
2
2
|
|
|
3
3
|
## The mission
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Slug **`{{missionSlug}}`** — *{{missionTitle}}*. The mission body below is authoritative: it states what success looks like, allowed commands, and restrictions. The mission file is human-edited — re-read it every tick.
|
|
6
6
|
|
|
7
|
-
### Mission
|
|
7
|
+
### Mission body
|
|
8
8
|
|
|
9
|
-
{{
|
|
9
|
+
{{missionIntent}}
|
|
10
10
|
|
|
11
11
|
## Current state
|
|
12
12
|
|
|
13
13
|
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
14
14
|
|
|
15
15
|
```json
|
|
16
|
-
{{
|
|
16
|
+
{{missionStateJson}}
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`cursor` is *your* enum — pick whatever labels map cleanly to your mission's phases
|
|
19
|
+
`cursor` is *your* enum — pick whatever labels map cleanly to your mission's phases. `data` is where you stash anything you need on the next tick (per-PR attempt counters, last-seen SHAs, etc). `done: true` is how you signal that the mission is permanently over — for evergreen missions this should always remain `false`.
|
|
20
20
|
|
|
21
21
|
## What to do on this tick
|
|
22
22
|
|
|
23
|
-
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any
|
|
24
|
-
2. **Re-read the mission.**
|
|
25
|
-
3. **
|
|
26
|
-
|
|
27
|
-
- If you're waiting on a child run, check its status via `gh run view <id> --json status,conclusion`. If still running, just update cursor/data minimally and exit — the next cron wake will check again. If succeeded, advance. If failed, record the failure and either spawn a remediation child or mark `done: true` with an error.
|
|
28
|
-
- If it's time to spawn a child executable, use `gh workflow run kody.yml -f issue_number=<N>` (or the appropriate workflow + inputs for the consumer repo). Capture the dispatched run's ID via `gh run list --workflow=kody.yml --limit 1 --json databaseId --jq '.[0].databaseId'` and stash it in `data`.
|
|
29
|
-
- If the mission is complete, set `done: true` and a terminal cursor like `done`.
|
|
30
|
-
4. **Optionally post a human-readable narration comment** on the issue summarizing what you just did (spawned run #12345, waiting on CI, etc.). Keep it short. Use `gh issue comment {{issueNumber}} --body "..."`.
|
|
23
|
+
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
24
|
+
2. **Re-read the mission body.** It may have changed since the last tick.
|
|
25
|
+
3. **Execute exactly the work the body's `## Mission` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
26
|
+
4. **Optionally post a short narration** wherever the mission tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
31
27
|
5. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
|
|
32
28
|
|
|
33
29
|
## Output contract (MANDATORY, exactly once, at the end)
|
|
@@ -44,12 +40,13 @@ End your response with a single fenced block using the `kody-mission-next-state`
|
|
|
44
40
|
```
|
|
45
41
|
````
|
|
46
42
|
|
|
47
|
-
If you fail to emit this block, or the JSON is invalid, the tick fails and the state
|
|
43
|
+
If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
|
|
48
44
|
|
|
49
45
|
## Rules
|
|
50
46
|
|
|
51
47
|
- Never edit, create, or delete files in the working tree.
|
|
52
48
|
- Never commit or push.
|
|
53
|
-
- Only shell calls allowed: `gh
|
|
54
|
-
- Keep each tick focused: do one
|
|
55
|
-
- If
|
|
49
|
+
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
50
|
+
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
51
|
+
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
52
|
+
- Honour the mission body's `## Restrictions` over any inferred shortcut.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.38",
|
|
4
4
|
"description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -12,18 +12,6 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"kody.config.schema.json"
|
|
14
14
|
],
|
|
15
|
-
"scripts": {
|
|
16
|
-
"kody": "tsx bin/kody.ts",
|
|
17
|
-
"build": "tsup && node scripts/copy-assets.cjs",
|
|
18
|
-
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
19
|
-
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
20
|
-
"test:all": "vitest run tests --no-coverage",
|
|
21
|
-
"typecheck": "tsc --noEmit",
|
|
22
|
-
"lint": "biome check",
|
|
23
|
-
"lint:fix": "biome check --write",
|
|
24
|
-
"format": "biome format --write",
|
|
25
|
-
"prepublishOnly": "pnpm build"
|
|
26
|
-
},
|
|
27
15
|
"dependencies": {
|
|
28
16
|
"@anthropic-ai/claude-agent-sdk": "0.2.119"
|
|
29
17
|
},
|
|
@@ -43,5 +31,16 @@
|
|
|
43
31
|
"url": "git+https://github.com/aharonyaircohen/kody-engine.git"
|
|
44
32
|
},
|
|
45
33
|
"homepage": "https://github.com/aharonyaircohen/kody-engine",
|
|
46
|
-
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
47
|
-
|
|
34
|
+
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
|
|
35
|
+
"scripts": {
|
|
36
|
+
"kody": "tsx bin/kody.ts",
|
|
37
|
+
"build": "tsup && node scripts/copy-assets.cjs",
|
|
38
|
+
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
39
|
+
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
40
|
+
"test:all": "vitest run tests --no-coverage",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"lint": "biome check",
|
|
43
|
+
"lint:fix": "biome check --write",
|
|
44
|
+
"format": "biome format --write"
|
|
45
|
+
}
|
|
46
|
+
}
|