@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 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.197",
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/);
@@ -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
- child?.kill();
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 (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
+ }
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)) return emptyState();
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
- const parsed = JSON.parse(jsonStr);
4906
- if (parsed?.schemaVersion !== 1) return emptyState();
4907
- return {
4908
- schemaVersion: 1,
4909
- core: { ...emptyState().core, ...parsed.core },
4910
- executables: parsed.executables ?? {},
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 required" });
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 required" });
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
- 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
+ }
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/parseIssueStateFromAgentResult.ts
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
- // src/scripts/parseJobStateFromAgentResult.ts
10307
- function isPartialEnvelope2(x) {
10308
- if (x === null || typeof x !== "object") return false;
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 fenceRegex = new RegExp(`\`\`\`${escapeRegex2(fenceLabel)}\\s*\\n([\\s\\S]*?)\\n\`\`\``, "m");
10314
- const match = fenceRegex.exec(text);
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(match[1].trim());
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 (!isPartialEnvelope2(parsed)) {
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/postAgentComment.ts
11382
+ // src/scripts/postAgentSummaryComment.ts
11303
11383
  init_issue();
11304
- var postAgentComment = async (ctx) => {
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 answer = ctx.data.prSummary?.trim();
11308
- 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;
11309
11392
  try {
11310
- postIssueComment(targetNumber, answer, ctx.cwd);
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
- const body = renderPlanComment(targetNumber, plan, { flowActive });
11462
- try {
11463
- postIssueComment(targetNumber, body, ctx.cwd);
11464
- } catch {
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
- if (!ctx.data.agentDone) return;
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.197",
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