@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 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), [.github/workflows/kody.yml](templates/kody.yml), and per-scheduled-executable workflow files. Idempotent — pass `--force` to overwrite.
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 body = mention ? `${mention} ${message}` : message;
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.198",
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
- prepublishOnly: "pnpm build"
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.toLowerCase().includes("@kody")) return null;
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 idx = body.indexOf("@kody");
3559
- if (idx === -1) return body;
3560
- return body.slice(idx + "@kody".length).trim();
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
- child?.kill();
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 (beginIdx < 0 || endIdx < 0 || endIdx <= beginIdx) return emptyState();
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)) return emptyState();
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
- const parsed = JSON.parse(jsonStr);
4925
- if (parsed?.schemaVersion !== 1) return emptyState();
4926
- return {
4927
- schemaVersion: 1,
4928
- core: { ...emptyState().core, ...parsed.core },
4929
- executables: parsed.executables ?? {},
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 required" });
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 required" });
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
- ctx.data.taskState = readTaskState(target, number, ctx.cwd);
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/parseIssueStateFromAgentResult.ts
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
- // src/scripts/parseJobStateFromAgentResult.ts
10332
- function isPartialEnvelope2(x) {
10333
- if (x === null || typeof x !== "object") return false;
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 fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
10339
- const match = fenceRegex.exec(text);
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(match[1].trim());
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 (!isPartialEnvelope2(parsed)) {
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/postAgentComment.ts
11382
+ // src/scripts/postAgentSummaryComment.ts
11328
11383
  init_issue();
11329
- var postAgentComment = async (ctx) => {
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 answer = ctx.data.prSummary?.trim();
11333
- if (!targetNumber || !answer) return;
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, answer, ctx.cwd);
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
- const body = renderPlanComment(targetNumber, plan, { flowActive });
11487
- try {
11488
- postIssueComment(targetNumber, body, ctx.cwd);
11489
- } catch {
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
- if (!ctx.data.agentDone) return;
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.198",
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
  }
@@ -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