@kody-ade/kody-engine 0.3.34 → 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 +474 -140
- 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";
|
|
@@ -501,11 +501,39 @@ function seedInitialMessage(file, message) {
|
|
|
501
501
|
|
|
502
502
|
// src/chat/loop.ts
|
|
503
503
|
var CHAT_SYSTEM_PROMPT = [
|
|
504
|
-
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the
|
|
505
|
-
"latest message using the full conversation below as context. Keep replies
|
|
506
|
-
"technical when appropriate, and formatted in Markdown.
|
|
507
|
-
"
|
|
508
|
-
"
|
|
504
|
+
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
|
|
505
|
+
"user's latest message using the full conversation below as context. Keep replies",
|
|
506
|
+
"focused, technical when appropriate, and formatted in Markdown.",
|
|
507
|
+
"",
|
|
508
|
+
"# Your environment and capabilities",
|
|
509
|
+
"You run inside a GitHub Actions job with a full clone of the user's repository",
|
|
510
|
+
"checked out at the current working directory. You have real tools \u2014 use them",
|
|
511
|
+
"before claiming you cannot do something. Never tell the user you lack repo,",
|
|
512
|
+
"filesystem, or GitHub access; you have all three.",
|
|
513
|
+
"",
|
|
514
|
+
"Tools you can call:",
|
|
515
|
+
"- Read, Edit, Write \u2014 full read/write access to every file in the repo (permission",
|
|
516
|
+
" mode is acceptEdits, so writes do not require confirmation).",
|
|
517
|
+
"- Glob, Grep \u2014 search the repo by filename pattern or content.",
|
|
518
|
+
"- Bash \u2014 run any shell command in the repo. The runner has:",
|
|
519
|
+
" - `git` (the repo is a real git checkout \u2014 `git log`, `git diff`,",
|
|
520
|
+
" `git show`, `git blame`, `git branch`, etc. all work).",
|
|
521
|
+
" - `gh` authenticated against this repository's GitHub via the Actions",
|
|
522
|
+
" `GITHUB_TOKEN` (read issues, PRs, workflows, runs, comments; query the API",
|
|
523
|
+
" with `gh api`).",
|
|
524
|
+
" - the repo's package manager and test/build/lint tooling (npm/pnpm/yarn,",
|
|
525
|
+
" pytest, go test, cargo, etc., whatever the project uses).",
|
|
526
|
+
" - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
|
|
527
|
+
"",
|
|
528
|
+
"# How to answer",
|
|
529
|
+
"If the user asks about repo content, code, history, issues, PRs, CI status, or",
|
|
530
|
+
"anything else knowable from the checkout or GitHub API, INVESTIGATE FIRST with",
|
|
531
|
+
"the tools above and answer from what you find. Do not say 'I don't have access'",
|
|
532
|
+
"\u2014 if you have not yet tried, try. Only fall back to a limitation statement after",
|
|
533
|
+
"a tool actually fails, and in that case quote the failing command and its error.",
|
|
534
|
+
"",
|
|
535
|
+
"Do not invent file paths, commit SHAs, line numbers, or command output. If you",
|
|
536
|
+
"cite something concrete, you must have just read or run it in this session."
|
|
509
537
|
].join("\n");
|
|
510
538
|
async function runChatTurn(opts) {
|
|
511
539
|
const turns = readSession(opts.sessionFile);
|
|
@@ -578,8 +606,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
|
|
|
578
606
|
|
|
579
607
|
// src/kody-cli.ts
|
|
580
608
|
import { execFileSync as execFileSync22 } from "child_process";
|
|
581
|
-
import * as
|
|
582
|
-
import * as
|
|
609
|
+
import * as fs23 from "fs";
|
|
610
|
+
import * as path20 from "path";
|
|
583
611
|
|
|
584
612
|
// src/dispatch.ts
|
|
585
613
|
import * as fs6 from "fs";
|
|
@@ -602,30 +630,51 @@ function getExecutablesRoot() {
|
|
|
602
630
|
}
|
|
603
631
|
return candidates[0];
|
|
604
632
|
}
|
|
605
|
-
function
|
|
606
|
-
|
|
607
|
-
|
|
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();
|
|
608
642
|
const out = [];
|
|
609
|
-
for (const
|
|
610
|
-
if (!
|
|
611
|
-
const
|
|
612
|
-
|
|
613
|
-
|
|
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
|
+
}
|
|
614
654
|
}
|
|
615
655
|
}
|
|
616
656
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
617
657
|
}
|
|
618
|
-
function
|
|
619
|
-
if (!isSafeName(name)) return
|
|
620
|
-
const
|
|
621
|
-
|
|
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;
|
|
622
671
|
}
|
|
623
672
|
function isSafeName(name) {
|
|
624
673
|
return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
|
|
625
674
|
}
|
|
626
|
-
function getProfileInputs(name,
|
|
627
|
-
|
|
628
|
-
|
|
675
|
+
function getProfileInputs(name, roots = getExecutableRoots()) {
|
|
676
|
+
const profilePath = resolveExecutable(name, roots);
|
|
677
|
+
if (!profilePath) return null;
|
|
629
678
|
try {
|
|
630
679
|
const raw = JSON.parse(fs5.readFileSync(profilePath, "utf-8"));
|
|
631
680
|
if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
|
|
@@ -806,8 +855,8 @@ function coerceBare(spec, value) {
|
|
|
806
855
|
|
|
807
856
|
// src/executor.ts
|
|
808
857
|
import { spawnSync } from "child_process";
|
|
809
|
-
import * as
|
|
810
|
-
import * as
|
|
858
|
+
import * as fs22 from "fs";
|
|
859
|
+
import * as path19 from "path";
|
|
811
860
|
|
|
812
861
|
// src/litellm.ts
|
|
813
862
|
import { execFileSync, spawn } from "child_process";
|
|
@@ -954,10 +1003,7 @@ function loadProfile(profilePath) {
|
|
|
954
1003
|
throw new ProfileError(profilePath, `kind: "scheduled" requires a "schedule" cron string`);
|
|
955
1004
|
}
|
|
956
1005
|
if (typeof r.role !== "string" || !VALID_ROLES.has(r.role)) {
|
|
957
|
-
throw new ProfileError(
|
|
958
|
-
profilePath,
|
|
959
|
-
`"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`
|
|
960
|
-
);
|
|
1006
|
+
throw new ProfileError(profilePath, `"role" is required and must be one of: ${[...VALID_ROLES].join(" | ")}`);
|
|
961
1007
|
}
|
|
962
1008
|
const role = r.role;
|
|
963
1009
|
let phase;
|
|
@@ -1148,7 +1194,10 @@ function parseScriptList(p, key, raw) {
|
|
|
1148
1194
|
const out = [];
|
|
1149
1195
|
for (const [i, item] of raw.entries()) {
|
|
1150
1196
|
if (!item || typeof item !== "object") {
|
|
1151
|
-
throw new ProfileError(
|
|
1197
|
+
throw new ProfileError(
|
|
1198
|
+
p,
|
|
1199
|
+
`scripts.${key}[${i}] must be an object like { script, runWhen? } or { shell, runWhen? }`
|
|
1200
|
+
);
|
|
1152
1201
|
}
|
|
1153
1202
|
const r = item;
|
|
1154
1203
|
const hasScript = typeof r.script === "string" && r.script.length > 0;
|
|
@@ -1157,7 +1206,10 @@ function parseScriptList(p, key, raw) {
|
|
|
1157
1206
|
throw new ProfileError(p, `scripts.${key}[${i}] cannot set both "script" and "shell" \u2014 pick one`);
|
|
1158
1207
|
}
|
|
1159
1208
|
if (!hasScript && !hasShell) {
|
|
1160
|
-
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
|
+
);
|
|
1161
1213
|
}
|
|
1162
1214
|
const entry = {};
|
|
1163
1215
|
if (hasScript) entry.script = r.script;
|
|
@@ -2065,11 +2117,11 @@ var diagMcp = async (_ctx) => {
|
|
|
2065
2117
|
process.stderr.write(`[kody diag] chromium present: ${hasChromium ? "yes" : "no"}
|
|
2066
2118
|
`);
|
|
2067
2119
|
try {
|
|
2068
|
-
const v = execFileSync6(
|
|
2069
|
-
"
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
).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();
|
|
2073
2125
|
process.stderr.write(`[kody diag] @playwright/mcp version: ${v}
|
|
2074
2126
|
`);
|
|
2075
2127
|
} catch (e) {
|
|
@@ -2245,7 +2297,9 @@ function walkApiRoutes(dir, prefix, cwd, out) {
|
|
|
2245
2297
|
if (routeFile) {
|
|
2246
2298
|
try {
|
|
2247
2299
|
const content = fs14.readFileSync(path13.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
|
|
2248
|
-
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
|
+
);
|
|
2249
2303
|
if (methods.length > 0) {
|
|
2250
2304
|
out.push({
|
|
2251
2305
|
path: prefix,
|
|
@@ -2620,6 +2674,64 @@ function parsePr(url) {
|
|
|
2620
2674
|
return Number.isFinite(n) ? n : null;
|
|
2621
2675
|
}
|
|
2622
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
|
+
|
|
2623
2735
|
// src/issue.ts
|
|
2624
2736
|
import { execFileSync as execFileSync8 } from "child_process";
|
|
2625
2737
|
var API_TIMEOUT_MS4 = 3e4;
|
|
@@ -2808,10 +2920,9 @@ var dispatchMissionTicks = async (ctx, _profile, args) => {
|
|
|
2808
2920
|
function listIssuesByLabel(label, cwd) {
|
|
2809
2921
|
let raw = "";
|
|
2810
2922
|
try {
|
|
2811
|
-
raw = gh2(
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
);
|
|
2923
|
+
raw = gh2(["issue", "list", "--state", "open", "--label", label, "--limit", "100", "--json", "number,title"], {
|
|
2924
|
+
cwd
|
|
2925
|
+
});
|
|
2815
2926
|
} catch {
|
|
2816
2927
|
return [];
|
|
2817
2928
|
}
|
|
@@ -3056,10 +3167,7 @@ function ensureLabels(cwd) {
|
|
|
3056
3167
|
}
|
|
3057
3168
|
function getIssueLabels(issueNumber, cwd) {
|
|
3058
3169
|
try {
|
|
3059
|
-
const output = gh2(
|
|
3060
|
-
["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"],
|
|
3061
|
-
{ cwd }
|
|
3062
|
-
);
|
|
3170
|
+
const output = gh2(["issue", "view", String(issueNumber), "--json", "labels", "--jq", ".labels[].name"], { cwd });
|
|
3063
3171
|
return output.split("\n").filter(Boolean);
|
|
3064
3172
|
} catch {
|
|
3065
3173
|
return [];
|
|
@@ -3083,10 +3191,8 @@ function createLabelInRepo(spec, cwd) {
|
|
|
3083
3191
|
function setKodyLabel(issueNumber, spec, cwd) {
|
|
3084
3192
|
const target = spec.label;
|
|
3085
3193
|
if (!target.startsWith(KODY_NAMESPACE)) {
|
|
3086
|
-
process.stderr.write(
|
|
3087
|
-
|
|
3088
|
-
`
|
|
3089
|
-
);
|
|
3194
|
+
process.stderr.write(`[kody] setKodyLabel: refusing to set non-kody label "${target}"
|
|
3195
|
+
`);
|
|
3090
3196
|
return;
|
|
3091
3197
|
}
|
|
3092
3198
|
const targetGroup = groupOf(target);
|
|
@@ -3112,10 +3218,8 @@ function setKodyLabel(issueNumber, spec, cwd) {
|
|
|
3112
3218
|
return;
|
|
3113
3219
|
}
|
|
3114
3220
|
}
|
|
3115
|
-
process.stderr.write(
|
|
3116
|
-
|
|
3117
|
-
`
|
|
3118
|
-
);
|
|
3221
|
+
process.stderr.write(`[kody] setKodyLabel: failed to add ${target} on #${issueNumber}: ${errMsg(err)}
|
|
3222
|
+
`);
|
|
3119
3223
|
}
|
|
3120
3224
|
}
|
|
3121
3225
|
function looksLikeMissingLabel(err) {
|
|
@@ -3284,7 +3388,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch, cwd) {
|
|
|
3284
3388
|
|
|
3285
3389
|
// src/gha.ts
|
|
3286
3390
|
import { execFileSync as execFileSync11 } from "child_process";
|
|
3287
|
-
import * as
|
|
3391
|
+
import * as fs17 from "fs";
|
|
3288
3392
|
function getRunUrl() {
|
|
3289
3393
|
const server = process.env.GITHUB_SERVER_URL;
|
|
3290
3394
|
const repo = process.env.GITHUB_REPOSITORY;
|
|
@@ -3295,10 +3399,10 @@ function getRunUrl() {
|
|
|
3295
3399
|
function reactToTriggerComment(cwd) {
|
|
3296
3400
|
if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
|
|
3297
3401
|
const eventPath = process.env.GITHUB_EVENT_PATH;
|
|
3298
|
-
if (!eventPath || !
|
|
3402
|
+
if (!eventPath || !fs17.existsSync(eventPath)) return;
|
|
3299
3403
|
let event = null;
|
|
3300
3404
|
try {
|
|
3301
|
-
event = JSON.parse(
|
|
3405
|
+
event = JSON.parse(fs17.readFileSync(eventPath, "utf-8"));
|
|
3302
3406
|
} catch {
|
|
3303
3407
|
return;
|
|
3304
3408
|
}
|
|
@@ -3538,22 +3642,22 @@ function tryPostPr2(prNumber, body, cwd) {
|
|
|
3538
3642
|
|
|
3539
3643
|
// src/scripts/initFlow.ts
|
|
3540
3644
|
import { execFileSync as execFileSync13 } from "child_process";
|
|
3541
|
-
import * as
|
|
3542
|
-
import * as
|
|
3645
|
+
import * as fs19 from "fs";
|
|
3646
|
+
import * as path17 from "path";
|
|
3543
3647
|
|
|
3544
3648
|
// src/scripts/loadQaGuide.ts
|
|
3545
|
-
import * as
|
|
3546
|
-
import * as
|
|
3649
|
+
import * as fs18 from "fs";
|
|
3650
|
+
import * as path16 from "path";
|
|
3547
3651
|
var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
|
|
3548
3652
|
var loadQaGuide = async (ctx) => {
|
|
3549
|
-
const full =
|
|
3550
|
-
if (!
|
|
3653
|
+
const full = path16.join(ctx.cwd, QA_GUIDE_REL_PATH);
|
|
3654
|
+
if (!fs18.existsSync(full)) {
|
|
3551
3655
|
ctx.data.qaGuide = "";
|
|
3552
3656
|
ctx.data.qaGuidePath = "";
|
|
3553
3657
|
return;
|
|
3554
3658
|
}
|
|
3555
3659
|
try {
|
|
3556
|
-
ctx.data.qaGuide =
|
|
3660
|
+
ctx.data.qaGuide = fs18.readFileSync(full, "utf-8");
|
|
3557
3661
|
ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
|
|
3558
3662
|
} catch {
|
|
3559
3663
|
ctx.data.qaGuide = "";
|
|
@@ -3563,9 +3667,9 @@ var loadQaGuide = async (ctx) => {
|
|
|
3563
3667
|
|
|
3564
3668
|
// src/scripts/initFlow.ts
|
|
3565
3669
|
function detectPackageManager(cwd) {
|
|
3566
|
-
if (
|
|
3567
|
-
if (
|
|
3568
|
-
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";
|
|
3569
3673
|
return "npm";
|
|
3570
3674
|
}
|
|
3571
3675
|
function qualityCommandsFor(pm) {
|
|
@@ -3687,33 +3791,33 @@ function performInit(cwd, force) {
|
|
|
3687
3791
|
const pm = detectPackageManager(cwd);
|
|
3688
3792
|
const ownerRepo = detectOwnerRepo(cwd);
|
|
3689
3793
|
const defaultBranch = defaultBranchFromGit(cwd);
|
|
3690
|
-
const configPath =
|
|
3691
|
-
if (
|
|
3794
|
+
const configPath = path17.join(cwd, "kody.config.json");
|
|
3795
|
+
if (fs19.existsSync(configPath) && !force) {
|
|
3692
3796
|
skipped.push("kody.config.json");
|
|
3693
3797
|
} else {
|
|
3694
3798
|
const cfg = makeConfig(pm, ownerRepo, defaultBranch);
|
|
3695
|
-
|
|
3799
|
+
fs19.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
|
|
3696
3800
|
`);
|
|
3697
3801
|
wrote.push("kody.config.json");
|
|
3698
3802
|
}
|
|
3699
|
-
const workflowDir =
|
|
3700
|
-
const workflowPath =
|
|
3701
|
-
if (
|
|
3803
|
+
const workflowDir = path17.join(cwd, ".github", "workflows");
|
|
3804
|
+
const workflowPath = path17.join(workflowDir, "kody.yml");
|
|
3805
|
+
if (fs19.existsSync(workflowPath) && !force) {
|
|
3702
3806
|
skipped.push(".github/workflows/kody.yml");
|
|
3703
3807
|
} else {
|
|
3704
|
-
|
|
3705
|
-
|
|
3808
|
+
fs19.mkdirSync(workflowDir, { recursive: true });
|
|
3809
|
+
fs19.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
|
|
3706
3810
|
wrote.push(".github/workflows/kody.yml");
|
|
3707
3811
|
}
|
|
3708
|
-
const hasUi =
|
|
3812
|
+
const hasUi = fs19.existsSync(path17.join(cwd, "src/app")) || fs19.existsSync(path17.join(cwd, "app")) || fs19.existsSync(path17.join(cwd, "pages"));
|
|
3709
3813
|
if (hasUi) {
|
|
3710
|
-
const qaGuidePath =
|
|
3711
|
-
if (
|
|
3814
|
+
const qaGuidePath = path17.join(cwd, QA_GUIDE_REL_PATH);
|
|
3815
|
+
if (fs19.existsSync(qaGuidePath) && !force) {
|
|
3712
3816
|
skipped.push(QA_GUIDE_REL_PATH);
|
|
3713
3817
|
} else {
|
|
3714
|
-
|
|
3818
|
+
fs19.mkdirSync(path17.dirname(qaGuidePath), { recursive: true });
|
|
3715
3819
|
const discovery = runQaDiscovery(cwd);
|
|
3716
|
-
|
|
3820
|
+
fs19.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
|
|
3717
3821
|
wrote.push(QA_GUIDE_REL_PATH);
|
|
3718
3822
|
}
|
|
3719
3823
|
}
|
|
@@ -3725,12 +3829,12 @@ function performInit(cwd, force) {
|
|
|
3725
3829
|
continue;
|
|
3726
3830
|
}
|
|
3727
3831
|
if (profile.kind !== "scheduled" || !profile.schedule) continue;
|
|
3728
|
-
const target =
|
|
3729
|
-
if (
|
|
3832
|
+
const target = path17.join(workflowDir, `kody-${exe.name}.yml`);
|
|
3833
|
+
if (fs19.existsSync(target) && !force) {
|
|
3730
3834
|
skipped.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
3731
3835
|
continue;
|
|
3732
3836
|
}
|
|
3733
|
-
|
|
3837
|
+
fs19.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
|
|
3734
3838
|
wrote.push(`.github/workflows/kody-${exe.name}.yml`);
|
|
3735
3839
|
}
|
|
3736
3840
|
let labels;
|
|
@@ -3788,10 +3892,8 @@ var initFlow = async (ctx) => {
|
|
|
3788
3892
|
`);
|
|
3789
3893
|
}
|
|
3790
3894
|
if (labels.failed.length > 0) {
|
|
3791
|
-
process.stdout.write(
|
|
3792
|
-
|
|
3793
|
-
`
|
|
3794
|
-
);
|
|
3895
|
+
process.stdout.write(` labels ${labels.failed.length} failed (gh auth missing? will self-heal on first run)
|
|
3896
|
+
`);
|
|
3795
3897
|
}
|
|
3796
3898
|
}
|
|
3797
3899
|
process.stdout.write(
|
|
@@ -3849,6 +3951,9 @@ function isStateEnvelope(x) {
|
|
|
3849
3951
|
const o = x;
|
|
3850
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);
|
|
3851
3953
|
}
|
|
3954
|
+
function initialStateEnvelope(cursor = "seed") {
|
|
3955
|
+
return { version: 1, rev: 0, cursor, data: {}, done: false };
|
|
3956
|
+
}
|
|
3852
3957
|
function formatStateCommentBody(marker, state) {
|
|
3853
3958
|
return `<!-- ${marker} -->
|
|
3854
3959
|
|
|
@@ -3896,10 +4001,10 @@ function findStateComment2(owner, repo, issueNumber, marker, cwd) {
|
|
|
3896
4001
|
}
|
|
3897
4002
|
function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
3898
4003
|
const body = formatStateCommentBody(marker, state);
|
|
3899
|
-
const raw = gh2(
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
);
|
|
4004
|
+
const raw = gh2(["api", "--method", "POST", `repos/${owner}/${repo}/issues/${issueNumber}/comments`, "--input", "-"], {
|
|
4005
|
+
cwd,
|
|
4006
|
+
input: JSON.stringify({ body })
|
|
4007
|
+
});
|
|
3903
4008
|
const parsed = JSON.parse(raw);
|
|
3904
4009
|
try {
|
|
3905
4010
|
minimizeComment(parsed.node_id, cwd);
|
|
@@ -3909,10 +4014,10 @@ function createStateComment(owner, repo, issueNumber, marker, state, cwd) {
|
|
|
3909
4014
|
}
|
|
3910
4015
|
function updateStateComment(owner, repo, commentId, commentNodeId, marker, state, cwd) {
|
|
3911
4016
|
const body = formatStateCommentBody(marker, state);
|
|
3912
|
-
gh2(
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
);
|
|
4017
|
+
gh2(["api", "--method", "PATCH", `repos/${owner}/${repo}/issues/comments/${commentId}`, "--input", "-"], {
|
|
4018
|
+
cwd,
|
|
4019
|
+
input: JSON.stringify({ body })
|
|
4020
|
+
});
|
|
3916
4021
|
try {
|
|
3917
4022
|
minimizeComment(commentNodeId, cwd);
|
|
3918
4023
|
} catch {
|
|
@@ -3949,6 +4054,168 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
|
|
|
3949
4054
|
ctx.data.issueStateJson = loaded ? JSON.stringify(loaded.state, null, 2) : "null";
|
|
3950
4055
|
};
|
|
3951
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
|
+
|
|
3952
4219
|
// src/scripts/loadPriorArt.ts
|
|
3953
4220
|
var PER_PR_DIFF_MAX_BYTES = 8e3;
|
|
3954
4221
|
var TOTAL_MAX_BYTES = 3e4;
|
|
@@ -4241,6 +4508,53 @@ function escapeRegex(s) {
|
|
|
4241
4508
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
4242
4509
|
}
|
|
4243
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
|
+
|
|
4244
4558
|
// src/scripts/persistArtifacts.ts
|
|
4245
4559
|
var persistArtifacts = async (ctx, profile) => {
|
|
4246
4560
|
if (profile.outputArtifacts.length === 0) return;
|
|
@@ -4306,16 +4620,16 @@ var postClassification = async (ctx) => {
|
|
|
4306
4620
|
}
|
|
4307
4621
|
if (!classification) {
|
|
4308
4622
|
ctx.data.action = failedAction("classification missing or invalid");
|
|
4309
|
-
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
|
+
);
|
|
4310
4628
|
ctx.output.exitCode = 1;
|
|
4311
4629
|
ctx.output.reason = "classify: no decision";
|
|
4312
4630
|
return;
|
|
4313
4631
|
}
|
|
4314
|
-
tryAuditComment(
|
|
4315
|
-
issueNumber,
|
|
4316
|
-
`\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`,
|
|
4317
|
-
ctx.cwd
|
|
4318
|
-
);
|
|
4632
|
+
tryAuditComment(issueNumber, `\u{1F50E} kody classified as \`${classification}\`${reason ? ` \u2014 ${reason}` : ""}`, ctx.cwd);
|
|
4319
4633
|
try {
|
|
4320
4634
|
execFileSync15("gh", ["issue", "comment", String(issueNumber), "--body", `@kody ${classification}`], {
|
|
4321
4635
|
cwd: ctx.cwd,
|
|
@@ -4605,7 +4919,9 @@ var requirePlanDeviations = async (ctx, profile) => {
|
|
|
4605
4919
|
ctx.data.planDeviationCount = bullets.length;
|
|
4606
4920
|
};
|
|
4607
4921
|
function isNoneSentinel(block) {
|
|
4608
|
-
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);
|
|
4609
4925
|
if (stripped.length !== 1) return false;
|
|
4610
4926
|
return stripped[0] === "none";
|
|
4611
4927
|
}
|
|
@@ -5237,16 +5553,12 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
|
|
|
5237
5553
|
};
|
|
5238
5554
|
function fetchChecks(prNumber, cwd) {
|
|
5239
5555
|
try {
|
|
5240
|
-
const raw = execFileSync20(
|
|
5241
|
-
"
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
|
|
5245
|
-
|
|
5246
|
-
cwd,
|
|
5247
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
5248
|
-
}
|
|
5249
|
-
);
|
|
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
|
+
});
|
|
5250
5562
|
const parsed = JSON.parse(raw);
|
|
5251
5563
|
return Array.isArray(parsed) ? parsed : [];
|
|
5252
5564
|
} catch (err) {
|
|
@@ -5325,10 +5637,7 @@ function formatStaleReport(stale, staleDays) {
|
|
|
5325
5637
|
if (stale.length === 0) {
|
|
5326
5638
|
return `\u{1F7E2} **kody watch-stale-prs** \u2014 no open PRs untouched for more than ${staleDays} days. \u2728`;
|
|
5327
5639
|
}
|
|
5328
|
-
const lines = [
|
|
5329
|
-
`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`,
|
|
5330
|
-
""
|
|
5331
|
-
];
|
|
5640
|
+
const lines = [`\u{1F7E1} **kody watch-stale-prs** \u2014 ${stale.length} PR(s) untouched for > ${staleDays} days:`, ""];
|
|
5332
5641
|
for (const pr of stale.slice(0, 50)) {
|
|
5333
5642
|
lines.push(`- [#${pr.number}](${pr.url}) \u2014 *${truncate2(pr.title, 80)}* (${pr.daysStale} days stale)`);
|
|
5334
5643
|
}
|
|
@@ -5392,8 +5701,29 @@ var writeIssueStateComment = async (ctx, _profile, _agentResult, args) => {
|
|
|
5392
5701
|
}
|
|
5393
5702
|
};
|
|
5394
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
|
+
|
|
5395
5725
|
// src/scripts/writeRunSummary.ts
|
|
5396
|
-
import * as
|
|
5726
|
+
import * as fs21 from "fs";
|
|
5397
5727
|
var writeRunSummary = async (ctx, profile) => {
|
|
5398
5728
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
5399
5729
|
if (!summaryPath) return;
|
|
@@ -5415,7 +5745,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
5415
5745
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
5416
5746
|
lines.push("");
|
|
5417
5747
|
try {
|
|
5418
|
-
|
|
5748
|
+
fs21.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
5419
5749
|
`);
|
|
5420
5750
|
} catch {
|
|
5421
5751
|
}
|
|
@@ -5434,6 +5764,7 @@ var preflightScripts = {
|
|
|
5434
5764
|
loadTaskState,
|
|
5435
5765
|
loadIssueContext,
|
|
5436
5766
|
loadIssueStateComment,
|
|
5767
|
+
loadMissionFromFile,
|
|
5437
5768
|
loadConventions,
|
|
5438
5769
|
loadCoverageRules,
|
|
5439
5770
|
loadPriorArt,
|
|
@@ -5448,12 +5779,15 @@ var preflightScripts = {
|
|
|
5448
5779
|
skipAgent,
|
|
5449
5780
|
classifyByLabel,
|
|
5450
5781
|
diagMcp,
|
|
5451
|
-
dispatchMissionTicks
|
|
5782
|
+
dispatchMissionTicks,
|
|
5783
|
+
dispatchMissionFileTicks
|
|
5452
5784
|
};
|
|
5453
5785
|
var postflightScripts = {
|
|
5454
5786
|
parseAgentResult: parseAgentResult2,
|
|
5455
5787
|
parseIssueStateFromAgentResult,
|
|
5788
|
+
parseMissionStateFromAgentResult,
|
|
5456
5789
|
writeIssueStateComment,
|
|
5790
|
+
writeMissionGistState,
|
|
5457
5791
|
requireFeedbackActions,
|
|
5458
5792
|
requirePlanDeviations,
|
|
5459
5793
|
verify,
|
|
@@ -5588,9 +5922,9 @@ async function runExecutable(profileName, input) {
|
|
|
5588
5922
|
data: {},
|
|
5589
5923
|
output: { exitCode: 0 }
|
|
5590
5924
|
};
|
|
5591
|
-
const ndjsonDir =
|
|
5925
|
+
const ndjsonDir = path19.join(input.cwd, ".kody");
|
|
5592
5926
|
const invokeAgent = async (prompt) => {
|
|
5593
|
-
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);
|
|
5594
5928
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
5595
5929
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
5596
5930
|
return runAgent({
|
|
@@ -5666,17 +6000,17 @@ async function runExecutable(profileName, input) {
|
|
|
5666
6000
|
}
|
|
5667
6001
|
}
|
|
5668
6002
|
function resolveProfilePath(profileName) {
|
|
5669
|
-
const here =
|
|
6003
|
+
const here = path19.dirname(new URL(import.meta.url).pathname);
|
|
5670
6004
|
const candidates = [
|
|
5671
|
-
|
|
6005
|
+
path19.join(here, "executables", profileName, "profile.json"),
|
|
5672
6006
|
// same-dir sibling (dev)
|
|
5673
|
-
|
|
6007
|
+
path19.join(here, "..", "executables", profileName, "profile.json"),
|
|
5674
6008
|
// up one (prod: dist/bin → dist/executables)
|
|
5675
|
-
|
|
6009
|
+
path19.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
5676
6010
|
// fallback
|
|
5677
6011
|
];
|
|
5678
6012
|
for (const c of candidates) {
|
|
5679
|
-
if (
|
|
6013
|
+
if (fs22.existsSync(c)) return c;
|
|
5680
6014
|
}
|
|
5681
6015
|
return candidates[0];
|
|
5682
6016
|
}
|
|
@@ -5769,8 +6103,8 @@ function finish(out) {
|
|
|
5769
6103
|
var SHELL_TIMEOUT_MS = 3e5;
|
|
5770
6104
|
function runShellEntry(entry, ctx, profile) {
|
|
5771
6105
|
const shellName = entry.shell;
|
|
5772
|
-
const shellPath =
|
|
5773
|
-
if (!
|
|
6106
|
+
const shellPath = path19.join(profile.dir, shellName);
|
|
6107
|
+
if (!fs22.existsSync(shellPath)) {
|
|
5774
6108
|
ctx.skipAgent = true;
|
|
5775
6109
|
ctx.output.exitCode = 99;
|
|
5776
6110
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -5920,9 +6254,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
5920
6254
|
return token;
|
|
5921
6255
|
}
|
|
5922
6256
|
function detectPackageManager2(cwd) {
|
|
5923
|
-
if (
|
|
5924
|
-
if (
|
|
5925
|
-
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";
|
|
5926
6260
|
return "npm";
|
|
5927
6261
|
}
|
|
5928
6262
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -6002,11 +6336,11 @@ function configureGitIdentity(cwd) {
|
|
|
6002
6336
|
}
|
|
6003
6337
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
6004
6338
|
if (!issueNumber) return;
|
|
6005
|
-
const logPath =
|
|
6339
|
+
const logPath = path20.join(cwd, ".kody", "last-run.jsonl");
|
|
6006
6340
|
let tail = "";
|
|
6007
6341
|
try {
|
|
6008
|
-
if (
|
|
6009
|
-
const content =
|
|
6342
|
+
if (fs23.existsSync(logPath)) {
|
|
6343
|
+
const content = fs23.readFileSync(logPath, "utf-8");
|
|
6010
6344
|
tail = content.slice(-3e3);
|
|
6011
6345
|
}
|
|
6012
6346
|
} catch {
|
|
@@ -6031,7 +6365,7 @@ async function runCi(argv) {
|
|
|
6031
6365
|
return 0;
|
|
6032
6366
|
}
|
|
6033
6367
|
const args = parseCiArgs(argv);
|
|
6034
|
-
const cwd = args.cwd ?
|
|
6368
|
+
const cwd = args.cwd ? path20.resolve(args.cwd) : process.cwd();
|
|
6035
6369
|
let earlyConfig;
|
|
6036
6370
|
try {
|
|
6037
6371
|
earlyConfig = loadConfig(cwd);
|
|
@@ -6169,9 +6503,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
6169
6503
|
return result;
|
|
6170
6504
|
}
|
|
6171
6505
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
6172
|
-
const sessionFile =
|
|
6173
|
-
const eventsFile =
|
|
6174
|
-
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)));
|
|
6175
6509
|
if (paths.length === 0) return;
|
|
6176
6510
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
6177
6511
|
try {
|
|
@@ -6209,7 +6543,7 @@ async function runChat(argv) {
|
|
|
6209
6543
|
${CHAT_HELP}`);
|
|
6210
6544
|
return 64;
|
|
6211
6545
|
}
|
|
6212
|
-
const cwd = args.cwd ?
|
|
6546
|
+
const cwd = args.cwd ? path21.resolve(args.cwd) : process.cwd();
|
|
6213
6547
|
const sessionId = args.sessionId;
|
|
6214
6548
|
const unpackedSecrets = unpackAllSecrets();
|
|
6215
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
|
+
}
|