@kody-ade/kody-engine 0.4.197 → 0.4.199
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/README.md +1 -1
- package/dist/bin/kody.js +186 -111
- package/dist/executables/types.ts +9 -0
- package/package.json +3 -2
- package/templates/kody.yml +0 -102
package/README.md
CHANGED
|
@@ -92,7 +92,7 @@ Executable directories contain **only** three kinds of files: `profile.json` (de
|
|
|
92
92
|
npx -y -p @kody-ade/kody-engine@latest kody init
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
-
`kody init` scaffolds [kody.config.json](kody.config.schema.json),
|
|
95
|
+
`kody init` scaffolds [kody.config.json](kody.config.schema.json), `.github/workflows/kody.yml` (generated from `WORKFLOW_TEMPLATE` in [src/scripts/initFlow.ts](src/scripts/initFlow.ts)), and per-scheduled-executable workflow files. Idempotent — pass `--force` to overwrite.
|
|
96
96
|
|
|
97
97
|
Required repo secrets: at least one model provider key (e.g. `MINIMAX_API_KEY`, `ANTHROPIC_API_KEY`). Recommended: `KODY_TOKEN` PAT so kody's commits trigger downstream CI and can modify `.github/workflows/*`.
|
|
98
98
|
|
package/dist/bin/kody.js
CHANGED
|
@@ -605,8 +605,11 @@ function listRepairCandidates(repoSlug) {
|
|
|
605
605
|
function dispatchVerb(workflowFile, executable, prNumber) {
|
|
606
606
|
return dispatchWorkflow(workflowFile, executable, prNumber);
|
|
607
607
|
}
|
|
608
|
-
function postRecommendation(prNumber, mention, message) {
|
|
609
|
-
const
|
|
608
|
+
function postRecommendation(prNumber, mention, message, dutySlug) {
|
|
609
|
+
const mentioned = mention ? `${mention} ${message}` : message;
|
|
610
|
+
const body = dutySlug ? `${mentioned}
|
|
611
|
+
|
|
612
|
+
<!-- kody-duty: ${dutySlug} -->` : mentioned;
|
|
610
613
|
try {
|
|
611
614
|
gh(["pr", "comment", String(prNumber), "--body", body]);
|
|
612
615
|
return { ok: true };
|
|
@@ -761,7 +764,7 @@ function buildDutyMcpServer(opts) {
|
|
|
761
764
|
body: z3.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
|
|
762
765
|
},
|
|
763
766
|
async (args) => {
|
|
764
|
-
const result = postRecommendation(args.pr, opts.operatorMention, args.body);
|
|
767
|
+
const result = postRecommendation(args.pr, opts.operatorMention, args.body, opts.dutySlug);
|
|
765
768
|
const text = result.ok ? `Recommendation posted on PR #${args.pr}.` : `Recommendation failed on PR #${args.pr}: ${result.error}`;
|
|
766
769
|
return {
|
|
767
770
|
content: [{ type: "text", text }]
|
|
@@ -1428,7 +1431,7 @@ var init_loadPriorArt = __esm({
|
|
|
1428
1431
|
// package.json
|
|
1429
1432
|
var package_default = {
|
|
1430
1433
|
name: "@kody-ade/kody-engine",
|
|
1431
|
-
version: "0.4.
|
|
1434
|
+
version: "0.4.199",
|
|
1432
1435
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
1433
1436
|
license: "MIT",
|
|
1434
1437
|
type: "module",
|
|
@@ -1456,7 +1459,8 @@ var package_default = {
|
|
|
1456
1459
|
lint: "biome check",
|
|
1457
1460
|
"lint:fix": "biome check --write",
|
|
1458
1461
|
format: "biome format --write",
|
|
1459
|
-
|
|
1462
|
+
"brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner",
|
|
1463
|
+
prepublishOnly: "pnpm typecheck && pnpm test && pnpm build"
|
|
1460
1464
|
},
|
|
1461
1465
|
dependencies: {
|
|
1462
1466
|
"@actions/cache": "^6.0.0",
|
|
@@ -1770,7 +1774,8 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
1770
1774
|
repo: String(github.repo)
|
|
1771
1775
|
},
|
|
1772
1776
|
agent: {
|
|
1773
|
-
model: String(agent.model)
|
|
1777
|
+
model: String(agent.model),
|
|
1778
|
+
...parsePerExecutable(agent.perExecutable)
|
|
1774
1779
|
},
|
|
1775
1780
|
issueContext: parseIssueContext(raw.issueContext),
|
|
1776
1781
|
testRequirements: parseTestRequirements(raw.testRequirements),
|
|
@@ -1851,6 +1856,14 @@ function mergeAliases(raw) {
|
|
|
1851
1856
|
}
|
|
1852
1857
|
return out;
|
|
1853
1858
|
}
|
|
1859
|
+
function parsePerExecutable(raw) {
|
|
1860
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1861
|
+
const out = {};
|
|
1862
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
1863
|
+
if (typeof v === "string" && v.length > 0) out[k] = v;
|
|
1864
|
+
}
|
|
1865
|
+
return Object.keys(out).length > 0 ? { perExecutable: out } : {};
|
|
1866
|
+
}
|
|
1854
1867
|
function parseClassifyConfig(raw) {
|
|
1855
1868
|
if (!raw || typeof raw !== "object") return void 0;
|
|
1856
1869
|
const r = raw;
|
|
@@ -2136,7 +2149,8 @@ async function runAgent(opts) {
|
|
|
2136
2149
|
}
|
|
2137
2150
|
const dutyHandle = buildDutyMcpServer2({
|
|
2138
2151
|
repoSlug: opts.dutyRepoSlug,
|
|
2139
|
-
operatorMention: opts.dutyOperatorMention ?? ""
|
|
2152
|
+
operatorMention: opts.dutyOperatorMention ?? "",
|
|
2153
|
+
...opts.dutyDutySlug ? { dutySlug: opts.dutyDutySlug } : {}
|
|
2140
2154
|
});
|
|
2141
2155
|
mcpEntries.push(["kody-duty", dutyHandle.server]);
|
|
2142
2156
|
}
|
|
@@ -3408,7 +3422,7 @@ function autoDispatch(opts) {
|
|
|
3408
3422
|
const rawBody = String(event.comment?.body ?? "");
|
|
3409
3423
|
const authorLogin = String(event.comment?.user?.login ?? "");
|
|
3410
3424
|
const authorType = String(event.comment?.user?.type ?? "");
|
|
3411
|
-
if (!rawBody
|
|
3425
|
+
if (!hasKodyMention(rawBody)) return null;
|
|
3412
3426
|
const isBotAuthor = authorLogin === "kody-bot" || authorType === "Bot";
|
|
3413
3427
|
if (!associationAllowed(event, opts?.config)) return null;
|
|
3414
3428
|
const body = rawBody.toLowerCase();
|
|
@@ -3554,10 +3568,15 @@ function associationAllowed(event, config) {
|
|
|
3554
3568
|
const assoc = String(event.comment?.author_association ?? "").toUpperCase();
|
|
3555
3569
|
return allowed.includes(assoc);
|
|
3556
3570
|
}
|
|
3571
|
+
var KODY_MENTION_RE = /(?:^|\s)@kody(?=\s|$|[^\w-])/i;
|
|
3572
|
+
function hasKodyMention(body) {
|
|
3573
|
+
return KODY_MENTION_RE.test(body);
|
|
3574
|
+
}
|
|
3557
3575
|
function extractAfterTag(body) {
|
|
3558
|
-
const
|
|
3559
|
-
if (
|
|
3560
|
-
|
|
3576
|
+
const m = body.match(KODY_MENTION_RE);
|
|
3577
|
+
if (!m || m.index === void 0) return body;
|
|
3578
|
+
const at = body.indexOf("@kody", m.index);
|
|
3579
|
+
return body.slice(at + "@kody".length).trim();
|
|
3561
3580
|
}
|
|
3562
3581
|
function extractSubcommand(afterTag) {
|
|
3563
3582
|
const match = afterTag.match(/^([a-z][a-z0-9-]{1,40})\b/);
|
|
@@ -3936,13 +3955,32 @@ function loadProfile(profilePath) {
|
|
|
3936
3955
|
// Phase 5 in-process handoff opt-in. Default false; containers
|
|
3937
3956
|
// flip to true after end-to-end verification.
|
|
3938
3957
|
preloadContext: r.preloadContext === true,
|
|
3939
|
-
dir: path13.dirname(profilePath)
|
|
3958
|
+
dir: path13.dirname(profilePath),
|
|
3959
|
+
promptTemplates: readPromptTemplates(path13.dirname(profilePath))
|
|
3940
3960
|
};
|
|
3941
3961
|
if (lifecycle) {
|
|
3942
3962
|
applyLifecycle(profile, profilePath);
|
|
3943
3963
|
}
|
|
3944
3964
|
return profile;
|
|
3945
3965
|
}
|
|
3966
|
+
function readPromptTemplates(dir) {
|
|
3967
|
+
const out = {};
|
|
3968
|
+
const read = (p) => {
|
|
3969
|
+
try {
|
|
3970
|
+
out[p] = fs14.readFileSync(p, "utf-8");
|
|
3971
|
+
} catch {
|
|
3972
|
+
}
|
|
3973
|
+
};
|
|
3974
|
+
read(path13.join(dir, "prompt.md"));
|
|
3975
|
+
try {
|
|
3976
|
+
const promptsDir = path13.join(dir, "prompts");
|
|
3977
|
+
for (const ent of fs14.readdirSync(promptsDir)) {
|
|
3978
|
+
if (ent.endsWith(".md")) read(path13.join(promptsDir, ent));
|
|
3979
|
+
}
|
|
3980
|
+
} catch {
|
|
3981
|
+
}
|
|
3982
|
+
return out;
|
|
3983
|
+
}
|
|
3946
3984
|
function validateScriptReferences(profile, registeredScripts) {
|
|
3947
3985
|
const missing = [];
|
|
3948
3986
|
for (const e of [...profile.scripts.preflight, ...profile.scripts.postflight]) {
|
|
@@ -4387,10 +4425,22 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
4387
4425
|
}
|
|
4388
4426
|
};
|
|
4389
4427
|
const killChild = () => {
|
|
4428
|
+
const pid = child?.pid;
|
|
4429
|
+
if (typeof pid !== "number") return;
|
|
4390
4430
|
try {
|
|
4391
|
-
|
|
4431
|
+
process.kill(-pid, "SIGTERM");
|
|
4392
4432
|
} catch {
|
|
4433
|
+
try {
|
|
4434
|
+
child?.kill();
|
|
4435
|
+
} catch {
|
|
4436
|
+
}
|
|
4393
4437
|
}
|
|
4438
|
+
setTimeout(() => {
|
|
4439
|
+
try {
|
|
4440
|
+
process.kill(-pid, "SIGKILL");
|
|
4441
|
+
} catch {
|
|
4442
|
+
}
|
|
4443
|
+
}, 2e3).unref?.();
|
|
4394
4444
|
};
|
|
4395
4445
|
const ensureHealthy = async () => {
|
|
4396
4446
|
if (await checkLitellmHealth(url)) return true;
|
|
@@ -4639,7 +4689,7 @@ function pushWithRetry(opts = {}) {
|
|
|
4639
4689
|
attempts: attempt
|
|
4640
4690
|
};
|
|
4641
4691
|
}
|
|
4642
|
-
const rebase = runGit(["rebase", `origin/${branch}`], cwd);
|
|
4692
|
+
const rebase = runGit(["rebase", "--rebase-merges", `origin/${branch}`], cwd);
|
|
4643
4693
|
if (!rebase.ok) {
|
|
4644
4694
|
runGit(["rebase", "--abort"], cwd);
|
|
4645
4695
|
return {
|
|
@@ -4792,6 +4842,13 @@ function commitAndPush(branch, agentMessage, cwd) {
|
|
|
4792
4842
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
4793
4843
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
4794
4844
|
}
|
|
4845
|
+
const forbiddenFiles = allChanged.filter((f) => isForbiddenPath(f));
|
|
4846
|
+
for (const f of forbiddenFiles) {
|
|
4847
|
+
try {
|
|
4848
|
+
git(["reset", "-q", "--", f], cwd);
|
|
4849
|
+
} catch {
|
|
4850
|
+
}
|
|
4851
|
+
}
|
|
4795
4852
|
for (const f of allowedFiles) {
|
|
4796
4853
|
try {
|
|
4797
4854
|
git(["add", "--", f], cwd);
|
|
@@ -4892,29 +4949,43 @@ function findStateComment(target, number, cwd) {
|
|
|
4892
4949
|
}
|
|
4893
4950
|
return null;
|
|
4894
4951
|
}
|
|
4952
|
+
var CorruptStateError = class extends Error {
|
|
4953
|
+
constructor(message) {
|
|
4954
|
+
super(message);
|
|
4955
|
+
this.name = "CorruptStateError";
|
|
4956
|
+
}
|
|
4957
|
+
};
|
|
4895
4958
|
function parseStateComment(body) {
|
|
4896
4959
|
const beginIdx = body.indexOf(STATE_BEGIN);
|
|
4960
|
+
if (beginIdx < 0) return emptyState();
|
|
4897
4961
|
const endIdx = body.lastIndexOf(STATE_END);
|
|
4898
|
-
if (
|
|
4962
|
+
if (endIdx < 0 || endIdx <= beginIdx) {
|
|
4963
|
+
throw new CorruptStateError("STATE_BEGIN present but STATE_END missing or misordered (truncated comment?)");
|
|
4964
|
+
}
|
|
4899
4965
|
const between = body.slice(beginIdx + STATE_BEGIN.length, endIdx).trim();
|
|
4900
4966
|
const OPEN = "```json";
|
|
4901
4967
|
const CLOSE = "```";
|
|
4902
|
-
if (!between.startsWith(OPEN) || !between.endsWith(CLOSE))
|
|
4968
|
+
if (!between.startsWith(OPEN) || !between.endsWith(CLOSE)) {
|
|
4969
|
+
throw new CorruptStateError("state fence malformed (expected ```json\u2026``` between markers)");
|
|
4970
|
+
}
|
|
4903
4971
|
const jsonStr = between.slice(OPEN.length, between.length - CLOSE.length).trim();
|
|
4972
|
+
let parsed;
|
|
4904
4973
|
try {
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
artifacts: parsed.artifacts && typeof parsed.artifacts === "object" ? parsed.artifacts : {},
|
|
4912
|
-
history: Array.isArray(parsed.history) ? parsed.history : [],
|
|
4913
|
-
flow: parsed.flow
|
|
4914
|
-
};
|
|
4915
|
-
} catch {
|
|
4916
|
-
return emptyState();
|
|
4974
|
+
parsed = JSON.parse(jsonStr);
|
|
4975
|
+
} catch (err) {
|
|
4976
|
+
throw new CorruptStateError(`state JSON unparseable (truncated comment?): ${err instanceof Error ? err.message : String(err)}`);
|
|
4977
|
+
}
|
|
4978
|
+
if (parsed?.schemaVersion !== 1) {
|
|
4979
|
+
throw new CorruptStateError(`unexpected schemaVersion: ${JSON.stringify(parsed?.schemaVersion)}`);
|
|
4917
4980
|
}
|
|
4981
|
+
return {
|
|
4982
|
+
schemaVersion: 1,
|
|
4983
|
+
core: { ...emptyState().core, ...parsed.core },
|
|
4984
|
+
executables: parsed.executables ?? {},
|
|
4985
|
+
artifacts: parsed.artifacts && typeof parsed.artifacts === "object" ? parsed.artifacts : {},
|
|
4986
|
+
history: Array.isArray(parsed.history) ? parsed.history : [],
|
|
4987
|
+
flow: parsed.flow
|
|
4988
|
+
};
|
|
4918
4989
|
}
|
|
4919
4990
|
function reduce(state, executable, action, phase) {
|
|
4920
4991
|
if (!action) return state;
|
|
@@ -5293,6 +5364,12 @@ function getApiKey() {
|
|
|
5293
5364
|
}
|
|
5294
5365
|
return key;
|
|
5295
5366
|
}
|
|
5367
|
+
function isSafeChatId(id) {
|
|
5368
|
+
if (!id || id.length > 200) return false;
|
|
5369
|
+
if (id.startsWith("/") || id.includes("\\")) return false;
|
|
5370
|
+
if (/[^a-zA-Z0-9._/-]/.test(id)) return false;
|
|
5371
|
+
return id.split("/").every((seg) => seg !== "" && seg !== "." && seg !== "..");
|
|
5372
|
+
}
|
|
5296
5373
|
function authOk(req, expected) {
|
|
5297
5374
|
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
5298
5375
|
if (xApiKey && xApiKey === expected) return true;
|
|
@@ -5506,8 +5583,8 @@ function buildServer(opts) {
|
|
|
5506
5583
|
const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
|
|
5507
5584
|
if (req.method === "POST" && m) {
|
|
5508
5585
|
const chatId = decodeURIComponent(m[1] ?? "");
|
|
5509
|
-
if (!chatId) {
|
|
5510
|
-
sendJson(res, 400, { error: "chatId
|
|
5586
|
+
if (!chatId || !isSafeChatId(chatId)) {
|
|
5587
|
+
sendJson(res, 400, { error: "invalid chatId" });
|
|
5511
5588
|
return;
|
|
5512
5589
|
}
|
|
5513
5590
|
await handleChatTurn(req, res, chatId, {
|
|
@@ -5523,8 +5600,8 @@ function buildServer(opts) {
|
|
|
5523
5600
|
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
5524
5601
|
if (req.method === "GET" && sm) {
|
|
5525
5602
|
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
5526
|
-
if (!chatId) {
|
|
5527
|
-
sendJson(res, 400, { error: "chatId
|
|
5603
|
+
if (!chatId || !isSafeChatId(chatId)) {
|
|
5604
|
+
sendJson(res, 400, { error: "invalid chatId" });
|
|
5528
5605
|
return;
|
|
5529
5606
|
}
|
|
5530
5607
|
const sinceRaw = url.searchParams.get("since");
|
|
@@ -6038,6 +6115,12 @@ var composePrompt = async (ctx, profile) => {
|
|
|
6038
6115
|
let template = "";
|
|
6039
6116
|
const attempts = [];
|
|
6040
6117
|
for (const c of candidates) {
|
|
6118
|
+
const cached2 = profile.promptTemplates?.[c];
|
|
6119
|
+
if (cached2 !== void 0) {
|
|
6120
|
+
template = cached2;
|
|
6121
|
+
templatePath = c;
|
|
6122
|
+
break;
|
|
6123
|
+
}
|
|
6041
6124
|
try {
|
|
6042
6125
|
template = fs24.readFileSync(c, "utf-8");
|
|
6043
6126
|
templatePath = c;
|
|
@@ -9798,7 +9881,25 @@ var loadTaskState = async (ctx) => {
|
|
|
9798
9881
|
ctx.data.taskState = emptyState();
|
|
9799
9882
|
return;
|
|
9800
9883
|
}
|
|
9801
|
-
|
|
9884
|
+
try {
|
|
9885
|
+
ctx.data.taskState = readTaskState(target, number, ctx.cwd);
|
|
9886
|
+
} catch (err) {
|
|
9887
|
+
if (err instanceof CorruptStateError) {
|
|
9888
|
+
process.stderr.write(
|
|
9889
|
+
`[kody state] CORRUPT state on ${target} #${number}: ${err.message} \u2014 healing to empty and bailing so committed work isn't silently redone.
|
|
9890
|
+
`
|
|
9891
|
+
);
|
|
9892
|
+
try {
|
|
9893
|
+
writeTaskState(target, number, emptyState(), ctx.cwd);
|
|
9894
|
+
} catch {
|
|
9895
|
+
}
|
|
9896
|
+
ctx.skipAgent = true;
|
|
9897
|
+
ctx.output.exitCode = 99;
|
|
9898
|
+
ctx.output.reason = `corrupt task state on ${target} #${number}: ${err.message}`;
|
|
9899
|
+
return;
|
|
9900
|
+
}
|
|
9901
|
+
throw err;
|
|
9902
|
+
}
|
|
9802
9903
|
};
|
|
9803
9904
|
|
|
9804
9905
|
// src/scripts/loadWorkerAdhoc.ts
|
|
@@ -10256,72 +10357,32 @@ function makeAction2(type, payload) {
|
|
|
10256
10357
|
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10257
10358
|
}
|
|
10258
10359
|
|
|
10259
|
-
// src/scripts/
|
|
10360
|
+
// src/scripts/stateEnvelope.ts
|
|
10260
10361
|
function isPartialEnvelope(x) {
|
|
10261
10362
|
if (x === null || typeof x !== "object") return false;
|
|
10262
10363
|
const o = x;
|
|
10263
10364
|
return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
10264
10365
|
}
|
|
10265
|
-
var parseIssueStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10266
|
-
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10267
|
-
if (!fenceLabel) {
|
|
10268
|
-
throw new Error("parseIssueStateFromAgentResult: `with.fenceLabel` is required");
|
|
10269
|
-
}
|
|
10270
|
-
if (!agentResult) {
|
|
10271
|
-
ctx.data.nextStateParseError = "agent did not run";
|
|
10272
|
-
return;
|
|
10273
|
-
}
|
|
10274
|
-
const fenceRegex = new RegExp("```" + escapeRegex(fenceLabel) + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
10275
|
-
const match = fenceRegex.exec(agentResult.finalText);
|
|
10276
|
-
if (!match) {
|
|
10277
|
-
ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
|
|
10278
|
-
return;
|
|
10279
|
-
}
|
|
10280
|
-
let parsed;
|
|
10281
|
-
try {
|
|
10282
|
-
parsed = JSON.parse(match[1].trim());
|
|
10283
|
-
} catch (err) {
|
|
10284
|
-
ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
|
|
10285
|
-
return;
|
|
10286
|
-
}
|
|
10287
|
-
if (!isPartialEnvelope(parsed)) {
|
|
10288
|
-
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
10289
|
-
return;
|
|
10290
|
-
}
|
|
10291
|
-
const loaded = ctx.data.issueStateComment;
|
|
10292
|
-
const prevRev = loaded?.state.rev ?? 0;
|
|
10293
|
-
const next = {
|
|
10294
|
-
version: 1,
|
|
10295
|
-
rev: prevRev + 1,
|
|
10296
|
-
cursor: parsed.cursor,
|
|
10297
|
-
data: parsed.data,
|
|
10298
|
-
done: parsed.done
|
|
10299
|
-
};
|
|
10300
|
-
ctx.data.nextIssueState = next;
|
|
10301
|
-
};
|
|
10302
10366
|
function escapeRegex(s) {
|
|
10303
10367
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
10304
10368
|
}
|
|
10305
|
-
|
|
10306
|
-
|
|
10307
|
-
|
|
10308
|
-
|
|
10309
|
-
const o = x;
|
|
10310
|
-
return typeof o.cursor === "string" && o.cursor.length > 0 && typeof o.done === "boolean" && o.data !== null && typeof o.data === "object" && !Array.isArray(o.data);
|
|
10369
|
+
function extractFencedBlock(text, label) {
|
|
10370
|
+
const re = new RegExp(`\`\`\`${escapeRegex(label)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
|
|
10371
|
+
const m = re.exec(text);
|
|
10372
|
+
return m ? m[1].trim() : null;
|
|
10311
10373
|
}
|
|
10312
10374
|
function extractNextStateFromText(text, fenceLabel, prevRev) {
|
|
10313
|
-
const
|
|
10314
|
-
|
|
10315
|
-
if (!match) {
|
|
10375
|
+
const inner = extractFencedBlock(text, fenceLabel);
|
|
10376
|
+
if (inner === null) {
|
|
10316
10377
|
return { error: `missing \`${fenceLabel}\` fenced block` };
|
|
10317
10378
|
}
|
|
10318
10379
|
let parsed;
|
|
10319
10380
|
try {
|
|
10320
|
-
parsed = JSON.parse(
|
|
10381
|
+
parsed = JSON.parse(inner);
|
|
10321
10382
|
} catch (err) {
|
|
10322
10383
|
return { error: `state JSON parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
10323
10384
|
}
|
|
10324
|
-
if (!
|
|
10385
|
+
if (!isPartialEnvelope(parsed)) {
|
|
10325
10386
|
return { error: "state must be an object with string `cursor`, object `data`, and boolean `done`" };
|
|
10326
10387
|
}
|
|
10327
10388
|
const envelope = {
|
|
@@ -10333,6 +10394,28 @@ function extractNextStateFromText(text, fenceLabel, prevRev) {
|
|
|
10333
10394
|
};
|
|
10334
10395
|
return { envelope };
|
|
10335
10396
|
}
|
|
10397
|
+
|
|
10398
|
+
// src/scripts/parseIssueStateFromAgentResult.ts
|
|
10399
|
+
var parseIssueStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10400
|
+
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10401
|
+
if (!fenceLabel) {
|
|
10402
|
+
throw new Error("parseIssueStateFromAgentResult: `with.fenceLabel` is required");
|
|
10403
|
+
}
|
|
10404
|
+
if (!agentResult) {
|
|
10405
|
+
ctx.data.nextStateParseError = "agent did not run";
|
|
10406
|
+
return;
|
|
10407
|
+
}
|
|
10408
|
+
const loaded = ctx.data.issueStateComment;
|
|
10409
|
+
const prevRev = loaded?.state.rev ?? 0;
|
|
10410
|
+
const result = extractNextStateFromText(agentResult.finalText, fenceLabel, prevRev);
|
|
10411
|
+
if (result.error) {
|
|
10412
|
+
ctx.data.nextStateParseError = result.error.startsWith("missing `") ? `agent did not emit a \`${fenceLabel}\` fenced block` : result.error;
|
|
10413
|
+
return;
|
|
10414
|
+
}
|
|
10415
|
+
ctx.data.nextIssueState = result.envelope;
|
|
10416
|
+
};
|
|
10417
|
+
|
|
10418
|
+
// src/scripts/parseJobStateFromAgentResult.ts
|
|
10336
10419
|
var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10337
10420
|
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10338
10421
|
if (!fenceLabel) {
|
|
@@ -10373,9 +10456,6 @@ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
|
10373
10456
|
}
|
|
10374
10457
|
ctx.data.nextJobState = result.envelope;
|
|
10375
10458
|
};
|
|
10376
|
-
function escapeRegex2(s) {
|
|
10377
|
-
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
10378
|
-
}
|
|
10379
10459
|
|
|
10380
10460
|
// src/scripts/parseReproOutput.ts
|
|
10381
10461
|
var parseReproOutput = async (ctx, _profile, agentResult) => {
|
|
@@ -11299,17 +11379,25 @@ var poolServe = async (ctx) => {
|
|
|
11299
11379
|
});
|
|
11300
11380
|
};
|
|
11301
11381
|
|
|
11302
|
-
// src/scripts/
|
|
11382
|
+
// src/scripts/postAgentSummaryComment.ts
|
|
11303
11383
|
init_issue();
|
|
11304
|
-
|
|
11384
|
+
function postAgentSummaryComment(ctx, opts = {}) {
|
|
11305
11385
|
if (!ctx.data.agentDone) return;
|
|
11386
|
+
const targetType = ctx.data.commentTargetType;
|
|
11306
11387
|
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11307
|
-
const
|
|
11308
|
-
if (!targetNumber || !
|
|
11388
|
+
const body = ctx.data.prSummary?.trim();
|
|
11389
|
+
if (!targetNumber || !body) return;
|
|
11390
|
+
if (opts.issueOnly && targetType !== "issue") return;
|
|
11391
|
+
const rendered = opts.render ? opts.render(targetNumber, body) : body;
|
|
11309
11392
|
try {
|
|
11310
|
-
postIssueComment(targetNumber,
|
|
11393
|
+
postIssueComment(targetNumber, rendered, ctx.cwd);
|
|
11311
11394
|
} catch {
|
|
11312
11395
|
}
|
|
11396
|
+
}
|
|
11397
|
+
|
|
11398
|
+
// src/scripts/postAgentComment.ts
|
|
11399
|
+
var postAgentComment = async (ctx) => {
|
|
11400
|
+
postAgentSummaryComment(ctx);
|
|
11313
11401
|
};
|
|
11314
11402
|
|
|
11315
11403
|
// src/scripts/postIssueComment.ts
|
|
@@ -11450,19 +11538,12 @@ function postWith(type, n, body, cwd) {
|
|
|
11450
11538
|
}
|
|
11451
11539
|
|
|
11452
11540
|
// src/scripts/postPlanComment.ts
|
|
11453
|
-
init_issue();
|
|
11454
11541
|
var postPlanComment = async (ctx) => {
|
|
11455
|
-
if (!ctx.data.agentDone) return;
|
|
11456
|
-
const targetType = ctx.data.commentTargetType;
|
|
11457
|
-
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11458
|
-
const plan = ctx.data.prSummary?.trim();
|
|
11459
|
-
if (targetType !== "issue" || !targetNumber || !plan) return;
|
|
11460
11542
|
const flowActive = Boolean(ctx.data.taskState?.flow);
|
|
11461
|
-
|
|
11462
|
-
|
|
11463
|
-
|
|
11464
|
-
}
|
|
11465
|
-
}
|
|
11543
|
+
postAgentSummaryComment(ctx, {
|
|
11544
|
+
issueOnly: true,
|
|
11545
|
+
render: (n, plan) => renderPlanComment(n, plan, { flowActive })
|
|
11546
|
+
});
|
|
11466
11547
|
};
|
|
11467
11548
|
function renderPlanComment(issueNumber, plan, opts) {
|
|
11468
11549
|
const head = `## Plan for issue #${issueNumber}
|
|
@@ -11481,17 +11562,8 @@ Comment \`kody run\` (prefixed with \`@\`) to execute this plan.`;
|
|
|
11481
11562
|
}
|
|
11482
11563
|
|
|
11483
11564
|
// src/scripts/postResearchComment.ts
|
|
11484
|
-
init_issue();
|
|
11485
11565
|
var postResearchComment = async (ctx) => {
|
|
11486
|
-
|
|
11487
|
-
const targetType = ctx.data.commentTargetType;
|
|
11488
|
-
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11489
|
-
const body = ctx.data.prSummary?.trim();
|
|
11490
|
-
if (targetType !== "issue" || !targetNumber || !body) return;
|
|
11491
|
-
try {
|
|
11492
|
-
postIssueComment(targetNumber, renderResearchComment(targetNumber, body), ctx.cwd);
|
|
11493
|
-
} catch {
|
|
11494
|
-
}
|
|
11566
|
+
postAgentSummaryComment(ctx, { issueOnly: true, render: renderResearchComment });
|
|
11495
11567
|
};
|
|
11496
11568
|
function renderResearchComment(issueNumber, body) {
|
|
11497
11569
|
return `## Research for issue #${issueNumber}
|
|
@@ -14454,6 +14526,9 @@ async function runExecutable(profileName, input) {
|
|
|
14454
14526
|
// up the in-process `kody-duty` MCP server with the right context.
|
|
14455
14527
|
enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
|
|
14456
14528
|
dutyOperatorMention: typeof ctx.data.dutyOperatorMention === "string" ? ctx.data.dutyOperatorMention : void 0,
|
|
14529
|
+
// Stamp the running duty's slug onto recommendations so the dashboard
|
|
14530
|
+
// keys trust per duty (not per persona). `jobSlug` is set by loadJobFromFile.
|
|
14531
|
+
dutyDutySlug: typeof ctx.data.jobSlug === "string" ? ctx.data.jobSlug : void 0,
|
|
14457
14532
|
// owner/repo from kody.config.json; envelope falls back to GITHUB_REPOSITORY
|
|
14458
14533
|
// for tester repos that don't set config.github (the file isn't always
|
|
14459
14534
|
// checked in). Either way, dutyMcp needs "owner/name" to hit the compare API.
|
|
@@ -118,6 +118,15 @@ export interface Profile {
|
|
|
118
118
|
preloadContext?: boolean
|
|
119
119
|
/** Absolute directory the profile was loaded from. Used to resolve prompt.md. */
|
|
120
120
|
dir: string
|
|
121
|
+
/**
|
|
122
|
+
* Prompt template files captured (by absolute path) at load time, BEFORE any
|
|
123
|
+
* preflight runs. composePrompt prefers these over a fresh disk read so the
|
|
124
|
+
* template survives working-tree churn from runFlow's branch setup — on the CI
|
|
125
|
+
* runner a branch checkout can drop the tracked-but-ignore-negated
|
|
126
|
+
* `.kody/executables/<name>/` dir, and reading prompt.md afterwards fails with
|
|
127
|
+
* ENOENT even though profile.json (read here, earlier) loaded fine.
|
|
128
|
+
*/
|
|
129
|
+
promptTemplates?: Record<string, string>
|
|
121
130
|
}
|
|
122
131
|
|
|
123
132
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kody-ade/kody-engine",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.199",
|
|
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",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
51
|
"lint": "biome check",
|
|
52
52
|
"lint:fix": "biome check --write",
|
|
53
|
-
"format": "biome format --write"
|
|
53
|
+
"format": "biome format --write",
|
|
54
|
+
"brain:publish": "docker buildx build --platform linux/amd64 -f runner/Dockerfile.brain -t ghcr.io/${KODY_BRAIN_GHCR_OWNER:-aharonyaircohen}/kody-brain:latest --push runner"
|
|
54
55
|
}
|
|
55
56
|
}
|
package/templates/kody.yml
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
# Drop this file at .github/workflows/kody.yml in your repo.
|
|
2
|
-
#
|
|
3
|
-
# Triggers forward every relevant event to `kody`; the engine decides what
|
|
4
|
-
# (if anything) to do. The job runs `npx kody` — no shell branching, no
|
|
5
|
-
# routing logic in YAML. All orchestration lives in the kody npm package;
|
|
6
|
-
# future capabilities ship via `npm publish`, not by editing this file.
|
|
7
|
-
#
|
|
8
|
-
# Required repo secrets: at least one model provider key (e.g. MINIMAX_API_KEY,
|
|
9
|
-
# ANTHROPIC_API_KEY). kody reads any *_API_KEY secret automatically via
|
|
10
|
-
# toJSON(secrets) — no need to list them here.
|
|
11
|
-
#
|
|
12
|
-
# Recommended: KODY_TOKEN secret — a fine-grained PAT or GitHub App token
|
|
13
|
-
# with `repo` + `read:org` + `workflow` scopes. Without it, kody's
|
|
14
|
-
# commits/PR-creation still work via github.token, but three things degrade:
|
|
15
|
-
# 1. PR body updates fail with "token lacks read:org scope" (cosmetic).
|
|
16
|
-
# 2. Pushes from kody won't trigger downstream workflows.
|
|
17
|
-
# 3. Any commit that modifies `.github/workflows/*` is REJECTED by
|
|
18
|
-
# GitHub — the default GITHUB_TOKEN can't touch workflow files.
|
|
19
|
-
# Set KODY_TOKEN in repo Settings → Secrets → Actions.
|
|
20
|
-
|
|
21
|
-
name: kody
|
|
22
|
-
|
|
23
|
-
on:
|
|
24
|
-
workflow_dispatch:
|
|
25
|
-
inputs:
|
|
26
|
-
issue_number:
|
|
27
|
-
description: "GitHub issue number (agent mode)"
|
|
28
|
-
type: string
|
|
29
|
-
default: ""
|
|
30
|
-
sessionId:
|
|
31
|
-
description: "Chat session ID (chat mode, from Kody-Dashboard)"
|
|
32
|
-
type: string
|
|
33
|
-
default: ""
|
|
34
|
-
message:
|
|
35
|
-
description: "Initial chat message (optional)"
|
|
36
|
-
type: string
|
|
37
|
-
default: ""
|
|
38
|
-
model:
|
|
39
|
-
description: "Model override (optional, e.g. anthropic/claude-haiku-4-5-20251001)"
|
|
40
|
-
type: string
|
|
41
|
-
default: ""
|
|
42
|
-
dashboardUrl:
|
|
43
|
-
description: "Dashboard event ingest URL with inline ?token=... (chat mode)"
|
|
44
|
-
type: string
|
|
45
|
-
default: ""
|
|
46
|
-
executable:
|
|
47
|
-
description: "Stage to run for issue_number (default: run). goal-tick sets classify."
|
|
48
|
-
type: string
|
|
49
|
-
default: ""
|
|
50
|
-
base:
|
|
51
|
-
description: "Stacked-PR base branch for issue_number (goal-tick stacked dispatch)."
|
|
52
|
-
type: string
|
|
53
|
-
default: ""
|
|
54
|
-
issue_comment:
|
|
55
|
-
types: [created]
|
|
56
|
-
pull_request:
|
|
57
|
-
types: [closed]
|
|
58
|
-
schedule:
|
|
59
|
-
# Wakes every 30 minutes; kody fans out to whichever scheduled executables
|
|
60
|
-
# (job-scheduler, memorize, watch-stale-prs, …) match this tick.
|
|
61
|
-
#
|
|
62
|
-
# `memorize` writes to `.kody/vault/` and opens a daily PR. If your
|
|
63
|
-
# `.gitignore` ignores `.kody/*`, add `!.kody/vault/` and `!.kody/vault/**`
|
|
64
|
-
# so memorize's pages are tracked.
|
|
65
|
-
- cron: "*/15 * * * *"
|
|
66
|
-
|
|
67
|
-
jobs:
|
|
68
|
-
run:
|
|
69
|
-
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.merged == true }}
|
|
70
|
-
runs-on: ubuntu-latest
|
|
71
|
-
timeout-minutes: 360
|
|
72
|
-
concurrency:
|
|
73
|
-
group: kody-${{ inputs.sessionId || inputs.issue_number || github.event.issue.number || github.sha }}
|
|
74
|
-
cancel-in-progress: false
|
|
75
|
-
permissions:
|
|
76
|
-
issues: write
|
|
77
|
-
pull-requests: write
|
|
78
|
-
contents: write
|
|
79
|
-
actions: read
|
|
80
|
-
id-token: write # OIDC: lets preview-build federate into Namespace remote builders
|
|
81
|
-
steps:
|
|
82
|
-
- uses: actions/checkout@v4
|
|
83
|
-
with:
|
|
84
|
-
fetch-depth: 0
|
|
85
|
-
ref: ${{ github.event.pull_request.base.ref || github.ref }}
|
|
86
|
-
token: ${{ secrets.KODY_TOKEN || github.token }}
|
|
87
|
-
|
|
88
|
-
- uses: actions/setup-node@v4
|
|
89
|
-
with:
|
|
90
|
-
node-version: 22
|
|
91
|
-
|
|
92
|
-
- uses: actions/setup-python@v5
|
|
93
|
-
with:
|
|
94
|
-
python-version: "3.12"
|
|
95
|
-
|
|
96
|
-
- env:
|
|
97
|
-
ALL_SECRETS: ${{ toJSON(secrets) }}
|
|
98
|
-
SESSION_ID: ${{ inputs.sessionId }}
|
|
99
|
-
INIT_MESSAGE: ${{ inputs.message }}
|
|
100
|
-
MODEL: ${{ inputs.model }}
|
|
101
|
-
DASHBOARD_URL: ${{ inputs.dashboardUrl }}
|
|
102
|
-
run: npx -y -p @kody-ade/kody-engine@0.4.120 kody-engine
|