@kody-ade/kody-engine 0.3.35 → 0.3.39
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 +400 -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.39",
|
|
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,122 @@ 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/missionStateFile.ts
|
|
4062
|
+
function stateFilePath(missionsDir, slug) {
|
|
4063
|
+
return `${missionsDir.replace(/\/+$/, "")}/${slug}.state.json`;
|
|
4064
|
+
}
|
|
4065
|
+
function loadMissionState(owner, repo, filePath, cwd) {
|
|
4066
|
+
let raw = "";
|
|
4067
|
+
try {
|
|
4068
|
+
raw = gh2(["api", `/repos/${owner}/${repo}/contents/${filePath}`], { cwd });
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4071
|
+
if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) {
|
|
4072
|
+
return { path: filePath, sha: null, state: initialStateEnvelope("seed"), created: true };
|
|
4073
|
+
}
|
|
4074
|
+
throw err;
|
|
4075
|
+
}
|
|
4076
|
+
let parsed;
|
|
4077
|
+
try {
|
|
4078
|
+
parsed = JSON.parse(raw);
|
|
4079
|
+
} catch {
|
|
4080
|
+
throw new Error(`loadMissionState: contents API for ${filePath} did not return JSON`);
|
|
4081
|
+
}
|
|
4082
|
+
if (!parsed || typeof parsed !== "object") {
|
|
4083
|
+
throw new Error(`loadMissionState: contents API for ${filePath} returned non-object`);
|
|
4084
|
+
}
|
|
4085
|
+
const o = parsed;
|
|
4086
|
+
if (o.type !== "file" || o.encoding !== "base64" || typeof o.content !== "string") {
|
|
4087
|
+
throw new Error(`loadMissionState: ${filePath} is not a base64 file`);
|
|
4088
|
+
}
|
|
4089
|
+
const decoded = Buffer.from(o.content, "base64").toString("utf-8");
|
|
4090
|
+
let envelope;
|
|
4091
|
+
try {
|
|
4092
|
+
envelope = JSON.parse(decoded);
|
|
4093
|
+
} catch {
|
|
4094
|
+
throw new Error(`loadMissionState: ${filePath} is not valid JSON`);
|
|
4095
|
+
}
|
|
4096
|
+
if (!isStateEnvelope(envelope)) {
|
|
4097
|
+
throw new Error(`loadMissionState: ${filePath} is not a StateEnvelope`);
|
|
4098
|
+
}
|
|
4099
|
+
return { path: filePath, sha: o.sha, state: envelope, created: false };
|
|
4100
|
+
}
|
|
4101
|
+
function writeMissionState(owner, repo, loaded, next, cwd) {
|
|
4102
|
+
if (!loaded.created && deepEqualsState(loaded.state, next)) {
|
|
4103
|
+
return false;
|
|
4104
|
+
}
|
|
4105
|
+
const body = JSON.stringify(next, null, 2) + "\n";
|
|
4106
|
+
const payload = {
|
|
4107
|
+
message: `chore(missions): update state for ${stateFileSlug(loaded.path)} (rev ${next.rev})`,
|
|
4108
|
+
content: Buffer.from(body, "utf-8").toString("base64")
|
|
4109
|
+
};
|
|
4110
|
+
if (loaded.sha) payload.sha = loaded.sha;
|
|
4111
|
+
gh2(["api", "--method", "PUT", `/repos/${owner}/${repo}/contents/${loaded.path}`, "--input", "-"], {
|
|
4112
|
+
cwd,
|
|
4113
|
+
input: JSON.stringify(payload)
|
|
4114
|
+
});
|
|
4115
|
+
return true;
|
|
4116
|
+
}
|
|
4117
|
+
function deepEqualsState(a, b) {
|
|
4118
|
+
if (a.cursor !== b.cursor || a.done !== b.done) return false;
|
|
4119
|
+
return JSON.stringify(a.data) === JSON.stringify(b.data);
|
|
4120
|
+
}
|
|
4121
|
+
function stateFileSlug(filePath) {
|
|
4122
|
+
const last = filePath.split("/").pop() ?? filePath;
|
|
4123
|
+
return last.replace(/\.state\.json$/i, "");
|
|
4124
|
+
}
|
|
4125
|
+
|
|
4126
|
+
// src/scripts/loadMissionFromFile.ts
|
|
4127
|
+
var loadMissionFromFile = async (ctx, _profile, args) => {
|
|
4128
|
+
const missionsDir = String(args?.missionsDir ?? ".kody/missions");
|
|
4129
|
+
const slugArg = String(args?.slugArg ?? "mission");
|
|
4130
|
+
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
4131
|
+
if (!slug) {
|
|
4132
|
+
throw new Error(`loadMissionFromFile: ctx.args.${slugArg} must be a non-empty slug`);
|
|
4133
|
+
}
|
|
4134
|
+
const owner = ctx.config.github.owner;
|
|
4135
|
+
const repo = ctx.config.github.repo;
|
|
4136
|
+
if (!owner || !repo) {
|
|
4137
|
+
throw new Error("loadMissionFromFile: ctx.config.github.owner/repo must be set");
|
|
4138
|
+
}
|
|
4139
|
+
const absPath = path18.join(ctx.cwd, missionsDir, `${slug}.md`);
|
|
4140
|
+
if (!fs20.existsSync(absPath)) {
|
|
4141
|
+
throw new Error(`loadMissionFromFile: mission file not found: ${absPath}`);
|
|
4142
|
+
}
|
|
4143
|
+
const raw = fs20.readFileSync(absPath, "utf-8");
|
|
4144
|
+
const { title, body } = parseMissionFile(raw, slug);
|
|
4145
|
+
const loaded = loadMissionState(owner, repo, stateFilePath(missionsDir, slug), ctx.cwd);
|
|
4146
|
+
ctx.data.missionSlug = slug;
|
|
4147
|
+
ctx.data.missionTitle = title;
|
|
4148
|
+
ctx.data.missionIntent = body;
|
|
4149
|
+
ctx.data.missionState = loaded;
|
|
4150
|
+
ctx.data.missionStateJson = JSON.stringify(loaded.state, null, 2);
|
|
4151
|
+
};
|
|
4152
|
+
function parseMissionFile(raw, slug) {
|
|
4153
|
+
let stripped = raw;
|
|
4154
|
+
if (stripped.startsWith("---\n")) {
|
|
4155
|
+
const end = stripped.indexOf("\n---\n", 4);
|
|
4156
|
+
if (end !== -1) {
|
|
4157
|
+
stripped = stripped.slice(end + 5);
|
|
4158
|
+
}
|
|
4159
|
+
}
|
|
4160
|
+
const trimmed = stripped.trim();
|
|
4161
|
+
const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
|
|
4162
|
+
const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
|
|
4163
|
+
if (h1) {
|
|
4164
|
+
const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
|
|
4165
|
+
return { title: h1[1].trim(), body: rest };
|
|
4166
|
+
}
|
|
4167
|
+
return { title: humanizeSlug(slug), body: trimmed };
|
|
4168
|
+
}
|
|
4169
|
+
function humanizeSlug(slug) {
|
|
4170
|
+
return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
|
|
4171
|
+
}
|
|
4172
|
+
|
|
3980
4173
|
// src/scripts/loadPriorArt.ts
|
|
3981
4174
|
var PER_PR_DIFF_MAX_BYTES = 8e3;
|
|
3982
4175
|
var TOTAL_MAX_BYTES = 3e4;
|
|
@@ -4269,6 +4462,53 @@ function escapeRegex(s) {
|
|
|
4269
4462
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
4270
4463
|
}
|
|
4271
4464
|
|
|
4465
|
+
// src/scripts/parseMissionStateFromAgentResult.ts
|
|
4466
|
+
function isPartialEnvelope2(x) {
|
|
4467
|
+
if (x === null || typeof x !== "object") return false;
|
|
4468
|
+
const o = x;
|
|
4469
|
+
return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
4470
|
+
}
|
|
4471
|
+
var parseMissionStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
4472
|
+
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
4473
|
+
if (!fenceLabel) {
|
|
4474
|
+
throw new Error("parseMissionStateFromAgentResult: `with.fenceLabel` is required");
|
|
4475
|
+
}
|
|
4476
|
+
if (!agentResult) {
|
|
4477
|
+
ctx.data.nextStateParseError = "agent did not run";
|
|
4478
|
+
return;
|
|
4479
|
+
}
|
|
4480
|
+
const fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
|
|
4481
|
+
const match = fenceRegex.exec(agentResult.finalText);
|
|
4482
|
+
if (!match) {
|
|
4483
|
+
ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
|
|
4484
|
+
return;
|
|
4485
|
+
}
|
|
4486
|
+
let parsed;
|
|
4487
|
+
try {
|
|
4488
|
+
parsed = JSON.parse(match[1].trim());
|
|
4489
|
+
} catch (err) {
|
|
4490
|
+
ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
|
|
4491
|
+
return;
|
|
4492
|
+
}
|
|
4493
|
+
if (!isPartialEnvelope2(parsed)) {
|
|
4494
|
+
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
4495
|
+
return;
|
|
4496
|
+
}
|
|
4497
|
+
const loaded = ctx.data.missionState;
|
|
4498
|
+
const prevRev = loaded?.state.rev ?? 0;
|
|
4499
|
+
const next = {
|
|
4500
|
+
version: 1,
|
|
4501
|
+
rev: prevRev + 1,
|
|
4502
|
+
cursor: parsed.cursor,
|
|
4503
|
+
data: parsed.data,
|
|
4504
|
+
done: parsed.done
|
|
4505
|
+
};
|
|
4506
|
+
ctx.data.nextMissionState = next;
|
|
4507
|
+
};
|
|
4508
|
+
function escapeRegex2(s) {
|
|
4509
|
+
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
4510
|
+
}
|
|
4511
|
+
|
|
4272
4512
|
// src/scripts/persistArtifacts.ts
|
|
4273
4513
|
var persistArtifacts = async (ctx, profile) => {
|
|
4274
4514
|
if (profile.outputArtifacts.length === 0) return;
|
|
@@ -4334,16 +4574,16 @@ var postClassification = async (ctx) => {
|
|
|
4334
4574
|
}
|
|
4335
4575
|
if (!classification) {
|
|
4336
4576
|
ctx.data.action = failedAction("classification missing or invalid");
|
|
4337
|
-
tryAuditComment(
|
|
4577
|
+
tryAuditComment(
|
|
4578
|
+
issueNumber,
|
|
4579
|
+
"\u26A0\uFE0F kody classifier could not decide \u2014 please re-run with an explicit `@kody <type>`.",
|
|
4580
|
+
ctx.cwd
|
|
4581
|
+
);
|
|
4338
4582
|
ctx.output.exitCode = 1;
|
|
4339
4583
|
ctx.output.reason = "classify: no decision";
|
|
4340
4584
|
return;
|
|
4341
4585
|
}
|
|
4342
|
-
tryAuditComment(
|
|
4343
|
-
issueNumber,
|
|
4344
|
-
`\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
|
|
4345
|
-
ctx.cwd
|
|
4346
|
-
);
|
|
4586
|
+
tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
|
|
4347
4587
|
try {
|
|
4348
4588
|
execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
|
|
4349
4589
|
cwd: ctx.cwd,
|
|
@@ -4633,7 +4873,9 @@ var requirePlanDeviations = async (ctx, profile) => {
|
|
|
4633
4873
|
ctx.data.planDeviationCount = bullets.length;
|
|
4634
4874
|
};
|
|
4635
4875
|
function isNoneSentinel(block) {
|
|
4636
|
-
const stripped = block.split("\n").map(
|
|
4876
|
+
const stripped = block.split("\n").map(
|
|
4877
|
+
(l) => l.replace(/^\s*[-*]\s*/, "").trim().toLowerCase()
|
|
4878
|
+
).filter((l) => l.length > 0);
|
|
4637
4879
|
if (stripped.length !== 1) return false;
|
|
4638
4880
|
return stripped[0] === "none";
|
|
4639
4881
|
}
|
|
@@ -5265,16 +5507,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
|
|
|
5265
5507
|
};
|
|
5266
5508
|
function fetchChecks(prNumber, cwd) {
|
|
5267
5509
|
try {
|
|
5268
|
-
const raw = execFileSync20(
|
|
5269
|
-
"
|
|
5270
|
-
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
cwd,
|
|
5275
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5276
|
-
}
|
|
5277
|
-
);
|
|
5510
|
+
const raw = execFileSync20("gh", ["pr", "checks", String(prNumber), "--json", "bucket,state,name,workflow,link"], {
|
|
5511
|
+
encoding: "utf-8",
|
|
5512
|
+
timeout: API_TIMEOUT_MS9,
|
|
5513
|
+
cwd,
|
|
5514
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
5515
|
+
});
|
|
5278
5516
|
const parsed = JSON.parse(raw);
|
|
5279
5517
|
return Array.isArray(parsed) ? parsed : [];
|
|
5280
5518
|
} catch (err) {
|
|
@@ -5353,10 +5591,7 @@ function formatStaleReport(stale, staleDays) {
|
|
|
5353
5591
|
if (stale.length === 0) {
|
|
5354
5592
|
return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
|
|
5355
5593
|
}
|
|
5356
|
-
const lines = [
|
|
5357
|
-
`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
|
|
5358
|
-
""
|
|
5359
|
-
];
|
|
5594
|
+
const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
|
|
5360
5595
|
for (const pr of stale.slice(0, 50)) {
|
|
5361
5596
|
lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
|
|
5362
5597
|
}
|
|
@@ -5420,8 +5655,34 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
5420
5655
|
}
|
|
5421
5656
|
};
|
|
5422
5657
|
|
|
5658
|
+
// src/scripts/writeMissionStateFile.ts
|
|
5659
|
+
var writeMissionStateFile = async (ctx, _profile, _agentResult) => {
|
|
5660
|
+
const parseError = ctx.data.nextStateParseError;
|
|
5661
|
+
if (parseError) {
|
|
5662
|
+
process.stderr.write(`[kody] mission state write skipped: ${parseError}
|
|
5663
|
+
`);
|
|
5664
|
+
if (ctx.output.exitCode === 0) ctx.output.exitCode = 1;
|
|
5665
|
+
if (!ctx.output.reason) ctx.output.reason = `next-state parse failed: ${parseError}`;
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
const next = ctx.data.nextMissionState;
|
|
5669
|
+
if (!next) {
|
|
5670
|
+
return;
|
|
5671
|
+
}
|
|
5672
|
+
const loaded = ctx.data.missionState;
|
|
5673
|
+
if (!loaded) {
|
|
5674
|
+
throw new Error("writeMissionStateFile: ctx.data.missionState missing \u2014 preflight must run first");
|
|
5675
|
+
}
|
|
5676
|
+
const owner = ctx.config.github.owner;
|
|
5677
|
+
const repo = ctx.config.github.repo;
|
|
5678
|
+
if (!owner || !repo) {
|
|
5679
|
+
throw new Error("writeMissionStateFile: ctx.config.github.owner/repo must be set");
|
|
5680
|
+
}
|
|
5681
|
+
writeMissionState(owner, repo, loaded, next, ctx.cwd);
|
|
5682
|
+
};
|
|
5683
|
+
|
|
5423
5684
|
// src/scripts/writeRunSummary.ts
|
|
5424
|
-
import * as
|
|
5685
|
+
import * as fs21 from "fs";
|
|
5425
5686
|
var writeRunSummary = async (ctx, profile) => {
|
|
5426
5687
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
5427
5688
|
if (!summaryPath) return;
|
|
@@ -5443,7 +5704,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
5443
5704
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
5444
5705
|
lines.push("");
|
|
5445
5706
|
try {
|
|
5446
|
-
|
|
5707
|
+
fs21.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
5447
5708
|
`);
|
|
5448
5709
|
} catch {
|
|
5449
5710
|
}
|
|
@@ -5462,6 +5723,7 @@ var preflightScripts = {
|
|
|
5462
5723
|
loadTaskState,
|
|
5463
5724
|
loadIssueContext,
|
|
5464
5725
|
loadIssueStateComment,
|
|
5726
|
+
loadMissionFromFile,
|
|
5465
5727
|
loadConventions,
|
|
5466
5728
|
loadCoverageRules,
|
|
5467
5729
|
loadPriorArt,
|
|
@@ -5476,12 +5738,15 @@ var preflightScripts = {
|
|
|
5476
5738
|
skipAgent,
|
|
5477
5739
|
classifyByLabel,
|
|
5478
5740
|
diagMcp,
|
|
5479
|
-
dispatchMissionTicks
|
|
5741
|
+
dispatchMissionTicks,
|
|
5742
|
+
dispatchMissionFileTicks
|
|
5480
5743
|
};
|
|
5481
5744
|
var postflightScripts = {
|
|
5482
5745
|
parseAgentResult: parseAgentResult2,
|
|
5483
5746
|
parseIssueStateFromAgentResult,
|
|
5747
|
+
parseMissionStateFromAgentResult,
|
|
5484
5748
|
writeIssueStateComment,
|
|
5749
|
+
writeMissionStateFile,
|
|
5485
5750
|
requireFeedbackActions,
|
|
5486
5751
|
requirePlanDeviations,
|
|
5487
5752
|
verify,
|
|
@@ -5616,9 +5881,9 @@ async function runExecutable(profileName, input) {
|
|
|
5616
5881
|
data: {},
|
|
5617
5882
|
output: { exitCode: 0 }
|
|
5618
5883
|
};
|
|
5619
|
-
const ndjsonDir =
|
|
5884
|
+
const ndjsonDir = path19.join(input.cwd, ".kody");
|
|
5620
5885
|
const invokeAgent = async (prompt) => {
|
|
5621
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
5886
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path19.isAbsolute(p) ? p : path19.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
5622
5887
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
5623
5888
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
5624
5889
|
return runAgent({
|
|
@@ -5694,17 +5959,17 @@ async function runExecutable(profileName, input) {
|
|
|
5694
5959
|
}
|
|
5695
5960
|
}
|
|
5696
5961
|
function resolveProfilePath(profileName) {
|
|
5697
|
-
const here =
|
|
5962
|
+
const here = path19.dirname(new URL(import.meta.url).pathname);
|
|
5698
5963
|
const candidates = [
|
|
5699
|
-
|
|
5964
|
+
path19.join(here, "executables", profileName, "profile.json"),
|
|
5700
5965
|
// same-dir sibling (dev)
|
|
5701
|
-
|
|
5966
|
+
path19.join(here, "..", "executables", profileName, "profile.json"),
|
|
5702
5967
|
// up one (prod: dist/bin → dist/executables)
|
|
5703
|
-
|
|
5968
|
+
path19.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
5704
5969
|
// fallback
|
|
5705
5970
|
];
|
|
5706
5971
|
for (const c of candidates) {
|
|
5707
|
-
if (
|
|
5972
|
+
if (fs22.existsSync(c)) return c;
|
|
5708
5973
|
}
|
|
5709
5974
|
return candidates[0];
|
|
5710
5975
|
}
|
|
@@ -5797,8 +6062,8 @@ function finish(out) {
|
|
|
5797
6062
|
var SHELL_TIMEOUT_MS = 3e5;
|
|
5798
6063
|
function runShellEntry(entry, ctx, profile) {
|
|
5799
6064
|
const shellName = entry.shell;
|
|
5800
|
-
const shellPath =
|
|
5801
|
-
if (!
|
|
6065
|
+
const shellPath = path19.join(profile.dir, shellName);
|
|
6066
|
+
if (!fs22.existsSync(shellPath)) {
|
|
5802
6067
|
ctx.skipAgent = true;
|
|
5803
6068
|
ctx.output.exitCode = 99;
|
|
5804
6069
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -5948,9 +6213,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
5948
6213
|
return token;
|
|
5949
6214
|
}
|
|
5950
6215
|
function detectPackageManager2(cwd) {
|
|
5951
|
-
if (
|
|
5952
|
-
if (
|
|
5953
|
-
if (
|
|
6216
|
+
if (fs23.existsSync(path20.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
6217
|
+
if (fs23.existsSync(path20.join(cwd, "yarn.lock"))) return "yarn";
|
|
6218
|
+
if (fs23.existsSync(path20.join(cwd, "bun.lockb"))) return "bun";
|
|
5954
6219
|
return "npm";
|
|
5955
6220
|
}
|
|
5956
6221
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -6030,11 +6295,11 @@ function configureGitIdentity(cwd) {
|
|
|
6030
6295
|
}
|
|
6031
6296
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
6032
6297
|
if (!issueNumber) return;
|
|
6033
|
-
const logPath =
|
|
6298
|
+
const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
|
|
6034
6299
|
let tail = "";
|
|
6035
6300
|
try {
|
|
6036
|
-
if (
|
|
6037
|
-
const content =
|
|
6301
|
+
if (fs23.existsSync(logPath)) {
|
|
6302
|
+
const content = fs23.readFileSync(logPath, "utf-8");
|
|
6038
6303
|
tail = content.slice(-3e3);
|
|
6039
6304
|
}
|
|
6040
6305
|
} catch {
|
|
@@ -6059,7 +6324,7 @@ async function runCi(argv) {
|
|
|
6059
6324
|
return 0;
|
|
6060
6325
|
}
|
|
6061
6326
|
const args = parseCiArgs(argv);
|
|
6062
|
-
const cwd = args.cwd ?
|
|
6327
|
+
const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
|
|
6063
6328
|
let earlyConfig;
|
|
6064
6329
|
try {
|
|
6065
6330
|
earlyConfig = loadConfig(cwd);
|
|
@@ -6197,9 +6462,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
6197
6462
|
return result;
|
|
6198
6463
|
}
|
|
6199
6464
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
6200
|
-
const sessionFile =
|
|
6201
|
-
const eventsFile =
|
|
6202
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
6465
|
+
const sessionFile = path21.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
6466
|
+
const eventsFile = path21.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
6467
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs24.existsSync(path21.join(cwd, p)));
|
|
6203
6468
|
if (paths.length === 0) return;
|
|
6204
6469
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
6205
6470
|
try {
|
|
@@ -6237,7 +6502,7 @@ async function runChat(argv) {
|
|
|
6237
6502
|
${CHAT_HELP}`);
|
|
6238
6503
|
return 64;
|
|
6239
6504
|
}
|
|
6240
|
-
const cwd = args.cwd ?
|
|
6505
|
+
const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
|
|
6241
6506
|
const sessionId = args.sessionId;
|
|
6242
6507
|
const unpackedSecrets = unpackAllSecrets();
|
|
6243
6508
|
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": "writeMissionStateFile"
|
|
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.39",
|
|
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
|
+
}
|