@kody-ade/kody-engine 0.4.198 → 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 +160 -110
- 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/);
|
|
@@ -4406,10 +4425,22 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
|
|
|
4406
4425
|
}
|
|
4407
4426
|
};
|
|
4408
4427
|
const killChild = () => {
|
|
4428
|
+
const pid = child?.pid;
|
|
4429
|
+
if (typeof pid !== "number") return;
|
|
4409
4430
|
try {
|
|
4410
|
-
|
|
4431
|
+
process.kill(-pid, "SIGTERM");
|
|
4411
4432
|
} catch {
|
|
4433
|
+
try {
|
|
4434
|
+
child?.kill();
|
|
4435
|
+
} catch {
|
|
4436
|
+
}
|
|
4412
4437
|
}
|
|
4438
|
+
setTimeout(() => {
|
|
4439
|
+
try {
|
|
4440
|
+
process.kill(-pid, "SIGKILL");
|
|
4441
|
+
} catch {
|
|
4442
|
+
}
|
|
4443
|
+
}, 2e3).unref?.();
|
|
4413
4444
|
};
|
|
4414
4445
|
const ensureHealthy = async () => {
|
|
4415
4446
|
if (await checkLitellmHealth(url)) return true;
|
|
@@ -4658,7 +4689,7 @@ function pushWithRetry(opts = {}) {
|
|
|
4658
4689
|
attempts: attempt
|
|
4659
4690
|
};
|
|
4660
4691
|
}
|
|
4661
|
-
const rebase = runGit(["rebase", `origin/${branch}`], cwd);
|
|
4692
|
+
const rebase = runGit(["rebase", "--rebase-merges", `origin/${branch}`], cwd);
|
|
4662
4693
|
if (!rebase.ok) {
|
|
4663
4694
|
runGit(["rebase", "--abort"], cwd);
|
|
4664
4695
|
return {
|
|
@@ -4811,6 +4842,13 @@ function commitAndPush(branch, agentMessage, cwd) {
|
|
|
4811
4842
|
if (allowedFiles.length === 0 && !mergeHeadExists) {
|
|
4812
4843
|
return { committed: false, pushed: false, sha: "", message: "" };
|
|
4813
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
|
+
}
|
|
4814
4852
|
for (const f of allowedFiles) {
|
|
4815
4853
|
try {
|
|
4816
4854
|
git(["add", "--", f], cwd);
|
|
@@ -4911,29 +4949,43 @@ function findStateComment(target, number, cwd) {
|
|
|
4911
4949
|
}
|
|
4912
4950
|
return null;
|
|
4913
4951
|
}
|
|
4952
|
+
var CorruptStateError = class extends Error {
|
|
4953
|
+
constructor(message) {
|
|
4954
|
+
super(message);
|
|
4955
|
+
this.name = "CorruptStateError";
|
|
4956
|
+
}
|
|
4957
|
+
};
|
|
4914
4958
|
function parseStateComment(body) {
|
|
4915
4959
|
const beginIdx = body.indexOf(STATE_BEGIN);
|
|
4960
|
+
if (beginIdx < 0) return emptyState();
|
|
4916
4961
|
const endIdx = body.lastIndexOf(STATE_END);
|
|
4917
|
-
if (
|
|
4962
|
+
if (endIdx < 0 || endIdx <= beginIdx) {
|
|
4963
|
+
throw new CorruptStateError("STATE_BEGIN present but STATE_END missing or misordered (truncated comment?)");
|
|
4964
|
+
}
|
|
4918
4965
|
const between = body.slice(beginIdx + STATE_BEGIN.length, endIdx).trim();
|
|
4919
4966
|
const OPEN = "```json";
|
|
4920
4967
|
const CLOSE = "```";
|
|
4921
|
-
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
|
+
}
|
|
4922
4971
|
const jsonStr = between.slice(OPEN.length, between.length - CLOSE.length).trim();
|
|
4972
|
+
let parsed;
|
|
4923
4973
|
try {
|
|
4924
|
-
|
|
4925
|
-
|
|
4926
|
-
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
artifacts: parsed.artifacts && typeof parsed.artifacts === "object" ? parsed.artifacts : {},
|
|
4931
|
-
history: Array.isArray(parsed.history) ? parsed.history : [],
|
|
4932
|
-
flow: parsed.flow
|
|
4933
|
-
};
|
|
4934
|
-
} catch {
|
|
4935
|
-
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)}`);
|
|
4936
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
|
+
};
|
|
4937
4989
|
}
|
|
4938
4990
|
function reduce(state, executable, action, phase) {
|
|
4939
4991
|
if (!action) return state;
|
|
@@ -5312,6 +5364,12 @@ function getApiKey() {
|
|
|
5312
5364
|
}
|
|
5313
5365
|
return key;
|
|
5314
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
|
+
}
|
|
5315
5373
|
function authOk(req, expected) {
|
|
5316
5374
|
const xApiKey = req.headers["x-api-key"]?.trim();
|
|
5317
5375
|
if (xApiKey && xApiKey === expected) return true;
|
|
@@ -5525,8 +5583,8 @@ function buildServer(opts) {
|
|
|
5525
5583
|
const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
|
|
5526
5584
|
if (req.method === "POST" && m) {
|
|
5527
5585
|
const chatId = decodeURIComponent(m[1] ?? "");
|
|
5528
|
-
if (!chatId) {
|
|
5529
|
-
sendJson(res, 400, { error: "chatId
|
|
5586
|
+
if (!chatId || !isSafeChatId(chatId)) {
|
|
5587
|
+
sendJson(res, 400, { error: "invalid chatId" });
|
|
5530
5588
|
return;
|
|
5531
5589
|
}
|
|
5532
5590
|
await handleChatTurn(req, res, chatId, {
|
|
@@ -5542,8 +5600,8 @@ function buildServer(opts) {
|
|
|
5542
5600
|
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
5543
5601
|
if (req.method === "GET" && sm) {
|
|
5544
5602
|
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
5545
|
-
if (!chatId) {
|
|
5546
|
-
sendJson(res, 400, { error: "chatId
|
|
5603
|
+
if (!chatId || !isSafeChatId(chatId)) {
|
|
5604
|
+
sendJson(res, 400, { error: "invalid chatId" });
|
|
5547
5605
|
return;
|
|
5548
5606
|
}
|
|
5549
5607
|
const sinceRaw = url.searchParams.get("since");
|
|
@@ -9823,7 +9881,25 @@ var loadTaskState = async (ctx) => {
|
|
|
9823
9881
|
ctx.data.taskState = emptyState();
|
|
9824
9882
|
return;
|
|
9825
9883
|
}
|
|
9826
|
-
|
|
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
|
+
}
|
|
9827
9903
|
};
|
|
9828
9904
|
|
|
9829
9905
|
// src/scripts/loadWorkerAdhoc.ts
|
|
@@ -10281,72 +10357,32 @@ function makeAction2(type, payload) {
|
|
|
10281
10357
|
return { type, payload, timestamp: (/* @__PURE__ */ new Date()).toISOString() };
|
|
10282
10358
|
}
|
|
10283
10359
|
|
|
10284
|
-
// src/scripts/
|
|
10360
|
+
// src/scripts/stateEnvelope.ts
|
|
10285
10361
|
function isPartialEnvelope(x) {
|
|
10286
10362
|
if (x === null || typeof x !== "object") return false;
|
|
10287
10363
|
const o = x;
|
|
10288
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);
|
|
10289
10365
|
}
|
|
10290
|
-
var parseIssueStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10291
|
-
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10292
|
-
if (!fenceLabel) {
|
|
10293
|
-
throw new Error("parseIssueStateFromAgentResult: `with.fenceLabel` is required");
|
|
10294
|
-
}
|
|
10295
|
-
if (!agentResult) {
|
|
10296
|
-
ctx.data.nextStateParseError = "agent did not run";
|
|
10297
|
-
return;
|
|
10298
|
-
}
|
|
10299
|
-
const fenceRegex = new RegExp("```" + escapeRegex(fenceLabel) + "\\s*\\n([\\s\\S]*?)\\n```", "m");
|
|
10300
|
-
const match = fenceRegex.exec(agentResult.finalText);
|
|
10301
|
-
if (!match) {
|
|
10302
|
-
ctx.data.nextStateParseError = `agent did not emit a \`${fenceLabel}\` fenced block`;
|
|
10303
|
-
return;
|
|
10304
|
-
}
|
|
10305
|
-
let parsed;
|
|
10306
|
-
try {
|
|
10307
|
-
parsed = JSON.parse(match[1].trim());
|
|
10308
|
-
} catch (err) {
|
|
10309
|
-
ctx.data.nextStateParseError = `state JSON parse error: ${err instanceof Error ? err.message : String(err)}`;
|
|
10310
|
-
return;
|
|
10311
|
-
}
|
|
10312
|
-
if (!isPartialEnvelope(parsed)) {
|
|
10313
|
-
ctx.data.nextStateParseError = "state must be an object with string `cursor`, object `data`, and boolean `done`";
|
|
10314
|
-
return;
|
|
10315
|
-
}
|
|
10316
|
-
const loaded = ctx.data.issueStateComment;
|
|
10317
|
-
const prevRev = loaded?.state.rev ?? 0;
|
|
10318
|
-
const next = {
|
|
10319
|
-
version: 1,
|
|
10320
|
-
rev: prevRev + 1,
|
|
10321
|
-
cursor: parsed.cursor,
|
|
10322
|
-
data: parsed.data,
|
|
10323
|
-
done: parsed.done
|
|
10324
|
-
};
|
|
10325
|
-
ctx.data.nextIssueState = next;
|
|
10326
|
-
};
|
|
10327
10366
|
function escapeRegex(s) {
|
|
10328
10367
|
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
10329
10368
|
}
|
|
10330
|
-
|
|
10331
|
-
|
|
10332
|
-
|
|
10333
|
-
|
|
10334
|
-
const o = x;
|
|
10335
|
-
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;
|
|
10336
10373
|
}
|
|
10337
10374
|
function extractNextStateFromText(text, fenceLabel, prevRev) {
|
|
10338
|
-
const
|
|
10339
|
-
|
|
10340
|
-
if (!match) {
|
|
10375
|
+
const inner = extractFencedBlock(text, fenceLabel);
|
|
10376
|
+
if (inner === null) {
|
|
10341
10377
|
return { error: `missing \`${fenceLabel}\` fenced block` };
|
|
10342
10378
|
}
|
|
10343
10379
|
let parsed;
|
|
10344
10380
|
try {
|
|
10345
|
-
parsed = JSON.parse(
|
|
10381
|
+
parsed = JSON.parse(inner);
|
|
10346
10382
|
} catch (err) {
|
|
10347
10383
|
return { error: `state JSON parse error: ${err instanceof Error ? err.message : String(err)}` };
|
|
10348
10384
|
}
|
|
10349
|
-
if (!
|
|
10385
|
+
if (!isPartialEnvelope(parsed)) {
|
|
10350
10386
|
return { error: "state must be an object with string `cursor`, object `data`, and boolean `done`" };
|
|
10351
10387
|
}
|
|
10352
10388
|
const envelope = {
|
|
@@ -10358,6 +10394,28 @@ function extractNextStateFromText(text, fenceLabel, prevRev) {
|
|
|
10358
10394
|
};
|
|
10359
10395
|
return { envelope };
|
|
10360
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
|
|
10361
10419
|
var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
10362
10420
|
const fenceLabel = String(args?.fenceLabel ?? "");
|
|
10363
10421
|
if (!fenceLabel) {
|
|
@@ -10398,9 +10456,6 @@ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
|
10398
10456
|
}
|
|
10399
10457
|
ctx.data.nextJobState = result.envelope;
|
|
10400
10458
|
};
|
|
10401
|
-
function escapeRegex2(s) {
|
|
10402
|
-
return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&");
|
|
10403
|
-
}
|
|
10404
10459
|
|
|
10405
10460
|
// src/scripts/parseReproOutput.ts
|
|
10406
10461
|
var parseReproOutput = async (ctx, _profile, agentResult) => {
|
|
@@ -11324,17 +11379,25 @@ var poolServe = async (ctx) => {
|
|
|
11324
11379
|
});
|
|
11325
11380
|
};
|
|
11326
11381
|
|
|
11327
|
-
// src/scripts/
|
|
11382
|
+
// src/scripts/postAgentSummaryComment.ts
|
|
11328
11383
|
init_issue();
|
|
11329
|
-
|
|
11384
|
+
function postAgentSummaryComment(ctx, opts = {}) {
|
|
11330
11385
|
if (!ctx.data.agentDone) return;
|
|
11386
|
+
const targetType = ctx.data.commentTargetType;
|
|
11331
11387
|
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11332
|
-
const
|
|
11333
|
-
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;
|
|
11334
11392
|
try {
|
|
11335
|
-
postIssueComment(targetNumber,
|
|
11393
|
+
postIssueComment(targetNumber, rendered, ctx.cwd);
|
|
11336
11394
|
} catch {
|
|
11337
11395
|
}
|
|
11396
|
+
}
|
|
11397
|
+
|
|
11398
|
+
// src/scripts/postAgentComment.ts
|
|
11399
|
+
var postAgentComment = async (ctx) => {
|
|
11400
|
+
postAgentSummaryComment(ctx);
|
|
11338
11401
|
};
|
|
11339
11402
|
|
|
11340
11403
|
// src/scripts/postIssueComment.ts
|
|
@@ -11475,19 +11538,12 @@ function postWith(type, n, body, cwd) {
|
|
|
11475
11538
|
}
|
|
11476
11539
|
|
|
11477
11540
|
// src/scripts/postPlanComment.ts
|
|
11478
|
-
init_issue();
|
|
11479
11541
|
var postPlanComment = async (ctx) => {
|
|
11480
|
-
if (!ctx.data.agentDone) return;
|
|
11481
|
-
const targetType = ctx.data.commentTargetType;
|
|
11482
|
-
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11483
|
-
const plan = ctx.data.prSummary?.trim();
|
|
11484
|
-
if (targetType !== "issue" || !targetNumber || !plan) return;
|
|
11485
11542
|
const flowActive = Boolean(ctx.data.taskState?.flow);
|
|
11486
|
-
|
|
11487
|
-
|
|
11488
|
-
|
|
11489
|
-
}
|
|
11490
|
-
}
|
|
11543
|
+
postAgentSummaryComment(ctx, {
|
|
11544
|
+
issueOnly: true,
|
|
11545
|
+
render: (n, plan) => renderPlanComment(n, plan, { flowActive })
|
|
11546
|
+
});
|
|
11491
11547
|
};
|
|
11492
11548
|
function renderPlanComment(issueNumber, plan, opts) {
|
|
11493
11549
|
const head = `## Plan for issue #${issueNumber}
|
|
@@ -11506,17 +11562,8 @@ Comment \`kody run\` (prefixed with \`@\`) to execute this plan.`;
|
|
|
11506
11562
|
}
|
|
11507
11563
|
|
|
11508
11564
|
// src/scripts/postResearchComment.ts
|
|
11509
|
-
init_issue();
|
|
11510
11565
|
var postResearchComment = async (ctx) => {
|
|
11511
|
-
|
|
11512
|
-
const targetType = ctx.data.commentTargetType;
|
|
11513
|
-
const targetNumber = Number(ctx.data.commentTargetNumber ?? 0);
|
|
11514
|
-
const body = ctx.data.prSummary?.trim();
|
|
11515
|
-
if (targetType !== "issue" || !targetNumber || !body) return;
|
|
11516
|
-
try {
|
|
11517
|
-
postIssueComment(targetNumber, renderResearchComment(targetNumber, body), ctx.cwd);
|
|
11518
|
-
} catch {
|
|
11519
|
-
}
|
|
11566
|
+
postAgentSummaryComment(ctx, { issueOnly: true, render: renderResearchComment });
|
|
11520
11567
|
};
|
|
11521
11568
|
function renderResearchComment(issueNumber, body) {
|
|
11522
11569
|
return `## Research for issue #${issueNumber}
|
|
@@ -14479,6 +14526,9 @@ async function runExecutable(profileName, input) {
|
|
|
14479
14526
|
// up the in-process `kody-duty` MCP server with the right context.
|
|
14480
14527
|
enableDutyTool: Array.isArray(ctx.data.dutyTools) && ctx.data.dutyTools.length > 0,
|
|
14481
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,
|
|
14482
14532
|
// owner/repo from kody.config.json; envelope falls back to GITHUB_REPOSITORY
|
|
14483
14533
|
// for tester repos that don't set config.github (the file isn't always
|
|
14484
14534
|
// checked in). Either way, dutyMcp needs "owner/name" to hit the compare API.
|
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
|