@kody-ade/kody-engine 0.4.72 → 0.4.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin/kody.js CHANGED
@@ -495,9 +495,10 @@ function parseAgentResult(finalText) {
495
495
  markerMissing: false
496
496
  };
497
497
  const MARKDOWN_PREFIX = "[\\s>*_#`~\\-]*";
498
- const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "is");
499
- const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`, "i");
500
- const failedMatch = text.match(FAILED_RE);
498
+ const FAILED_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}FAILED${MARKDOWN_PREFIX}\\s*:\\s*(.+?)\\s*$`, "s");
499
+ const DONE_RE = new RegExp(`(?:^|\\n)${MARKDOWN_PREFIX}DONE\\b`);
500
+ const scanText = stripFencedCodeBlocks(text);
501
+ const failedMatch = scanText.match(FAILED_RE);
501
502
  if (failedMatch) {
502
503
  return {
503
504
  done: false,
@@ -510,7 +511,7 @@ function parseAgentResult(finalText) {
510
511
  markerMissing: false
511
512
  };
512
513
  }
513
- const hasDoneMarker = DONE_RE.test(text);
514
+ const hasDoneMarker = DONE_RE.test(scanText);
514
515
  const hasCommitMsg = /^[\s>*_#`~-]*COMMIT_MSG\s*:/im.test(text);
515
516
  const hasPrSummary = /^[\s>*_#`~-]*PR_SUMMARY\s*:/im.test(text);
516
517
  const markerMissing = !hasDoneMarker && !hasCommitMsg && !hasPrSummary;
@@ -553,6 +554,9 @@ function parseAgentResult(finalText) {
553
554
  function stripMarkdownEmphasis(s) {
554
555
  return s.trim().replace(/^[*_`~]+|[*_`~]+$/g, "").trim();
555
556
  }
557
+ function stripFencedCodeBlocks(s) {
558
+ return s.replace(/```[\s\S]*?```/g, "").replace(/~~~[\s\S]*?~~~/g, "");
559
+ }
556
560
  function extractBlock(text, startMarker, endMarker) {
557
561
  const startIdx = text.search(startMarker);
558
562
  if (startIdx === -1) return "";
@@ -864,7 +868,7 @@ var init_loadPriorArt = __esm({
864
868
  // package.json
865
869
  var package_default = {
866
870
  name: "@kody-ade/kody-engine",
867
- version: "0.4.72",
871
+ version: "0.4.74",
868
872
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
869
873
  license: "MIT",
870
874
  type: "module",
@@ -919,8 +923,8 @@ var package_default = {
919
923
 
920
924
  // src/chat-cli.ts
921
925
  import { execFileSync as execFileSync31 } from "child_process";
922
- import * as fs33 from "fs";
923
- import * as path31 from "path";
926
+ import * as fs34 from "fs";
927
+ import * as path32 from "path";
924
928
 
925
929
  // src/chat/events.ts
926
930
  import * as fs from "fs";
@@ -2013,8 +2017,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2013
2017
 
2014
2018
  // src/kody-cli.ts
2015
2019
  import { execFileSync as execFileSync30 } from "child_process";
2016
- import * as fs32 from "fs";
2017
- import * as path30 from "path";
2020
+ import * as fs33 from "fs";
2021
+ import * as path31 from "path";
2018
2022
 
2019
2023
  // src/dispatch.ts
2020
2024
  import * as fs8 from "fs";
@@ -2455,8 +2459,8 @@ init_issue();
2455
2459
 
2456
2460
  // src/executor.ts
2457
2461
  import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
2458
- import * as fs31 from "fs";
2459
- import * as path29 from "path";
2462
+ import * as fs32 from "fs";
2463
+ import * as path30 from "path";
2460
2464
  init_events();
2461
2465
 
2462
2466
  // src/lifecycleLabels.ts
@@ -6659,6 +6663,19 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
6659
6663
  git2(["pull", "origin", branchName], cwd);
6660
6664
  } catch {
6661
6665
  }
6666
+ if (!baseBranch || baseBranch === defaultBranch2) {
6667
+ try {
6668
+ git2(["merge", "--no-edit", `origin/${defaultBranch2}`], cwd);
6669
+ } catch {
6670
+ try {
6671
+ git2(["merge", "--abort"], cwd);
6672
+ } catch {
6673
+ }
6674
+ throw new Error(
6675
+ `Branch '${branchName}' has merge conflicts with 'origin/${defaultBranch2}'. Resolve manually or delete the branch to start fresh.`
6676
+ );
6677
+ }
6678
+ }
6662
6679
  return { branch: branchName, created: false };
6663
6680
  }
6664
6681
  try {
@@ -7277,10 +7294,10 @@ import * as fs25 from "fs";
7277
7294
  import * as path24 from "path";
7278
7295
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
7279
7296
  var GoalStateError = class extends Error {
7280
- constructor(path32, message) {
7281
- super(`Invalid goal state at ${path32}:
7297
+ constructor(path33, message) {
7298
+ super(`Invalid goal state at ${path33}:
7282
7299
  ${message}`);
7283
- this.path = path32;
7300
+ this.path = path33;
7284
7301
  this.name = "GoalStateError";
7285
7302
  }
7286
7303
  path;
@@ -9002,6 +9019,240 @@ function buildChildEnv(parent, force) {
9002
9019
  return out;
9003
9020
  }
9004
9021
 
9022
+ // src/scripts/brainServe.ts
9023
+ import { createServer } from "http";
9024
+ import * as fs30 from "fs";
9025
+ import * as path29 from "path";
9026
+ var DEFAULT_PORT = 8080;
9027
+ function getApiKey() {
9028
+ const key = (process.env.BRAIN_API_KEY ?? "").trim();
9029
+ if (!key) {
9030
+ throw new Error(
9031
+ "BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
9032
+ );
9033
+ }
9034
+ return key;
9035
+ }
9036
+ function authOk(req, expected) {
9037
+ const xApiKey = req.headers["x-api-key"]?.trim();
9038
+ if (xApiKey && xApiKey === expected) return true;
9039
+ const auth = req.headers["authorization"]?.trim();
9040
+ if (auth && auth.toLowerCase().startsWith("bearer ")) {
9041
+ return auth.slice(7).trim() === expected;
9042
+ }
9043
+ return false;
9044
+ }
9045
+ function readJsonBody(req) {
9046
+ return new Promise((resolve4, reject) => {
9047
+ const chunks = [];
9048
+ req.on("data", (c) => chunks.push(c));
9049
+ req.on("end", () => {
9050
+ const raw = Buffer.concat(chunks).toString("utf-8");
9051
+ if (!raw.trim()) {
9052
+ resolve4({});
9053
+ return;
9054
+ }
9055
+ try {
9056
+ resolve4(JSON.parse(raw));
9057
+ } catch (err) {
9058
+ reject(err instanceof Error ? err : new Error(String(err)));
9059
+ }
9060
+ });
9061
+ req.on("error", reject);
9062
+ });
9063
+ }
9064
+ function sendJson(res, status, body) {
9065
+ res.writeHead(status, { "content-type": "application/json" });
9066
+ res.end(JSON.stringify(body));
9067
+ }
9068
+ function writeSseHeaders(res) {
9069
+ res.writeHead(200, {
9070
+ "content-type": "text/event-stream; charset=utf-8",
9071
+ "cache-control": "no-cache, no-transform",
9072
+ connection: "keep-alive",
9073
+ "x-accel-buffering": "no"
9074
+ });
9075
+ }
9076
+ function emitSse(res, event) {
9077
+ res.write(`data: ${JSON.stringify(event)}
9078
+
9079
+ `);
9080
+ }
9081
+ var BrainSseSink = class {
9082
+ constructor(res, chatId) {
9083
+ this.res = res;
9084
+ this.chatId = chatId;
9085
+ }
9086
+ res;
9087
+ chatId;
9088
+ async emit(event) {
9089
+ switch (event.event) {
9090
+ case "chat.message": {
9091
+ const content = String(event.payload.content ?? "");
9092
+ if (content.length > 0) {
9093
+ emitSse(this.res, { type: "text", text: content, chatId: this.chatId });
9094
+ }
9095
+ return;
9096
+ }
9097
+ case "chat.tool": {
9098
+ if (event.payload.phase !== "use") return;
9099
+ emitSse(this.res, {
9100
+ type: "tool_use",
9101
+ name: typeof event.payload.name === "string" ? event.payload.name : "tool",
9102
+ input: event.payload.input ?? {},
9103
+ chatId: this.chatId
9104
+ });
9105
+ return;
9106
+ }
9107
+ case "chat.done": {
9108
+ emitSse(this.res, { type: "done", chatId: this.chatId });
9109
+ return;
9110
+ }
9111
+ case "chat.error": {
9112
+ const errMsg2 = typeof event.payload.error === "string" ? event.payload.error : "agent error";
9113
+ emitSse(this.res, { type: "error", error: errMsg2, chatId: this.chatId });
9114
+ return;
9115
+ }
9116
+ // chat.thinking / chat.ready / chat.exit — not part of the Brain protocol.
9117
+ default:
9118
+ return;
9119
+ }
9120
+ }
9121
+ };
9122
+ async function handleChatTurn(req, res, chatId, opts) {
9123
+ let body;
9124
+ try {
9125
+ body = await readJsonBody(req);
9126
+ } catch {
9127
+ sendJson(res, 400, { error: "invalid JSON body" });
9128
+ return;
9129
+ }
9130
+ const message = typeof body === "object" && body !== null && "message" in body ? body.message : void 0;
9131
+ if (typeof message !== "string" || !message.trim()) {
9132
+ sendJson(res, 400, { error: "message required" });
9133
+ return;
9134
+ }
9135
+ const sessionFile = sessionFilePath(opts.cwd, chatId);
9136
+ fs30.mkdirSync(path29.dirname(sessionFile), { recursive: true });
9137
+ appendTurn(sessionFile, {
9138
+ role: "user",
9139
+ content: message,
9140
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9141
+ });
9142
+ writeSseHeaders(res);
9143
+ emitSse(res, { type: "chat", chatId });
9144
+ const sink = new BrainSseSink(res, chatId);
9145
+ try {
9146
+ await opts.runTurn({
9147
+ sessionId: chatId,
9148
+ sessionFile,
9149
+ cwd: opts.cwd,
9150
+ model: opts.model,
9151
+ litellmUrl: opts.litellmUrl,
9152
+ sink
9153
+ });
9154
+ } catch (err) {
9155
+ const errMsg2 = err instanceof Error ? err.message : String(err);
9156
+ process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
9157
+ `);
9158
+ try {
9159
+ emitSse(res, { type: "error", error: errMsg2, chatId });
9160
+ } catch {
9161
+ }
9162
+ } finally {
9163
+ try {
9164
+ res.end();
9165
+ } catch {
9166
+ }
9167
+ }
9168
+ }
9169
+ function buildServer(opts) {
9170
+ const runTurn = opts.runTurn ?? runChatTurn;
9171
+ return createServer(async (req, res) => {
9172
+ if (!req.method || !req.url) {
9173
+ sendJson(res, 400, { error: "bad request" });
9174
+ return;
9175
+ }
9176
+ const url = new URL(req.url, `http://localhost`);
9177
+ if (req.method === "GET" && url.pathname === "/healthz") {
9178
+ sendJson(res, 200, { ok: true });
9179
+ return;
9180
+ }
9181
+ if (!authOk(req, opts.apiKey)) {
9182
+ sendJson(res, 401, { error: "unauthorized" });
9183
+ return;
9184
+ }
9185
+ const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
9186
+ if (req.method === "POST" && m) {
9187
+ const chatId = decodeURIComponent(m[1] ?? "");
9188
+ if (!chatId) {
9189
+ sendJson(res, 400, { error: "chatId required" });
9190
+ return;
9191
+ }
9192
+ await handleChatTurn(req, res, chatId, {
9193
+ cwd: opts.cwd,
9194
+ model: opts.model,
9195
+ litellmUrl: opts.litellmUrl,
9196
+ runTurn
9197
+ });
9198
+ return;
9199
+ }
9200
+ sendJson(res, 404, { error: "not found" });
9201
+ });
9202
+ }
9203
+ var brainServe = async (ctx) => {
9204
+ ctx.skipAgent = true;
9205
+ const apiKey = getApiKey();
9206
+ const port = Number(process.env.PORT ?? DEFAULT_PORT);
9207
+ const model = parseProviderModel(ctx.config.agent.model);
9208
+ const usesProxy = needsLitellmProxy(model);
9209
+ let handle = null;
9210
+ if (usesProxy) {
9211
+ process.stdout.write(
9212
+ `[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
9213
+ `
9214
+ );
9215
+ handle = await startLitellmIfNeeded(model, ctx.cwd);
9216
+ process.stdout.write(
9217
+ `[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
9218
+ `
9219
+ );
9220
+ }
9221
+ const litellmUrl = usesProxy ? handle?.url ?? LITELLM_DEFAULT_URL : null;
9222
+ const server = buildServer({
9223
+ apiKey,
9224
+ cwd: ctx.cwd,
9225
+ model,
9226
+ litellmUrl
9227
+ });
9228
+ await new Promise((resolve4) => {
9229
+ server.listen(port, "0.0.0.0", () => {
9230
+ process.stdout.write(
9231
+ `[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
9232
+ `
9233
+ );
9234
+ resolve4();
9235
+ });
9236
+ });
9237
+ const shutdown = (signal) => {
9238
+ process.stdout.write(`[brain-serve] ${signal} \u2014 shutting down
9239
+ `);
9240
+ server.close(() => {
9241
+ if (handle) {
9242
+ try {
9243
+ handle.kill();
9244
+ } catch {
9245
+ }
9246
+ }
9247
+ process.exit(0);
9248
+ });
9249
+ };
9250
+ process.once("SIGINT", () => shutdown("SIGINT"));
9251
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
9252
+ await new Promise(() => {
9253
+ });
9254
+ };
9255
+
9005
9256
  // src/scripts/serveFlow.ts
9006
9257
  import { spawn as spawn3 } from "child_process";
9007
9258
  function parseTarget(positional) {
@@ -9841,7 +10092,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
9841
10092
  };
9842
10093
 
9843
10094
  // src/scripts/writeRunSummary.ts
9844
- import * as fs30 from "fs";
10095
+ import * as fs31 from "fs";
9845
10096
  var writeRunSummary = async (ctx, profile) => {
9846
10097
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
9847
10098
  if (!summaryPath) return;
@@ -9863,7 +10114,7 @@ var writeRunSummary = async (ctx, profile) => {
9863
10114
  if (reason) lines.push(`- **Reason:** ${reason}`);
9864
10115
  lines.push("");
9865
10116
  try {
9866
- fs30.appendFileSync(summaryPath, `${lines.join("\n")}
10117
+ fs31.appendFileSync(summaryPath, `${lines.join("\n")}
9867
10118
  `);
9868
10119
  } catch {
9869
10120
  }
@@ -9905,6 +10156,7 @@ var preflightScripts = {
9905
10156
  dispatchJobFileTicks,
9906
10157
  runTickScript,
9907
10158
  serveFlow,
10159
+ brainServe,
9908
10160
  loadGoalState,
9909
10161
  handleAbandonedGoal,
9910
10162
  deriveGoalPhase,
@@ -10085,9 +10337,9 @@ async function runExecutable(profileName, input) {
10085
10337
  data: { ...input.preloadedData ?? {} },
10086
10338
  output: { exitCode: 0 }
10087
10339
  };
10088
- const ndjsonDir = path29.join(input.cwd, ".kody");
10340
+ const ndjsonDir = path30.join(input.cwd, ".kody");
10089
10341
  const invokeAgent = async (prompt) => {
10090
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path29.isAbsolute(p) ? p : path29.resolve(profile.dir, p)).filter((p) => p.length > 0);
10342
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path30.isAbsolute(p) ? p : path30.resolve(profile.dir, p)).filter((p) => p.length > 0);
10091
10343
  const syntheticPath = ctx.data.syntheticPluginPath;
10092
10344
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
10093
10345
  return runAgent({
@@ -10282,7 +10534,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
10282
10534
  function getProfileInputsForChild(profileName, _cwd) {
10283
10535
  try {
10284
10536
  const profilePath = resolveProfilePath(profileName);
10285
- if (!fs31.existsSync(profilePath)) return null;
10537
+ if (!fs32.existsSync(profilePath)) return null;
10286
10538
  return loadProfile(profilePath).inputs;
10287
10539
  } catch {
10288
10540
  return null;
@@ -10291,17 +10543,17 @@ function getProfileInputsForChild(profileName, _cwd) {
10291
10543
  function resolveProfilePath(profileName) {
10292
10544
  const found = resolveExecutable(profileName);
10293
10545
  if (found) return found;
10294
- const here = path29.dirname(new URL(import.meta.url).pathname);
10546
+ const here = path30.dirname(new URL(import.meta.url).pathname);
10295
10547
  const candidates = [
10296
- path29.join(here, "executables", profileName, "profile.json"),
10548
+ path30.join(here, "executables", profileName, "profile.json"),
10297
10549
  // same-dir sibling (dev)
10298
- path29.join(here, "..", "executables", profileName, "profile.json"),
10550
+ path30.join(here, "..", "executables", profileName, "profile.json"),
10299
10551
  // up one (prod: dist/bin → dist/executables)
10300
- path29.join(here, "..", "src", "executables", profileName, "profile.json")
10552
+ path30.join(here, "..", "src", "executables", profileName, "profile.json")
10301
10553
  // fallback
10302
10554
  ];
10303
10555
  for (const c of candidates) {
10304
- if (fs31.existsSync(c)) return c;
10556
+ if (fs32.existsSync(c)) return c;
10305
10557
  }
10306
10558
  return candidates[0];
10307
10559
  }
@@ -10401,8 +10653,8 @@ function resolveShellTimeoutMs(entry) {
10401
10653
  var SIGKILL_GRACE_MS = 5e3;
10402
10654
  async function runShellEntry(entry, ctx, profile) {
10403
10655
  const shellName = entry.shell;
10404
- const shellPath = path29.join(profile.dir, shellName);
10405
- if (!fs31.existsSync(shellPath)) {
10656
+ const shellPath = path30.join(profile.dir, shellName);
10657
+ if (!fs32.existsSync(shellPath)) {
10406
10658
  ctx.skipAgent = true;
10407
10659
  ctx.output.exitCode = 99;
10408
10660
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -10881,9 +11133,9 @@ function resolveAuthToken(env = process.env) {
10881
11133
  return token;
10882
11134
  }
10883
11135
  function detectPackageManager2(cwd) {
10884
- if (fs32.existsSync(path30.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
10885
- if (fs32.existsSync(path30.join(cwd, "yarn.lock"))) return "yarn";
10886
- if (fs32.existsSync(path30.join(cwd, "bun.lockb"))) return "bun";
11136
+ if (fs33.existsSync(path31.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11137
+ if (fs33.existsSync(path31.join(cwd, "yarn.lock"))) return "yarn";
11138
+ if (fs33.existsSync(path31.join(cwd, "bun.lockb"))) return "bun";
10887
11139
  return "npm";
10888
11140
  }
10889
11141
  function shellOut(cmd, args, cwd, stream = true) {
@@ -10970,11 +11222,11 @@ function configureGitIdentity(cwd) {
10970
11222
  }
10971
11223
  function postFailureTail(issueNumber, cwd, reason) {
10972
11224
  if (!issueNumber) return;
10973
- const logPath = path30.join(cwd, ".kody", "last-run.jsonl");
11225
+ const logPath = path31.join(cwd, ".kody", "last-run.jsonl");
10974
11226
  let tail = "";
10975
11227
  try {
10976
- if (fs32.existsSync(logPath)) {
10977
- const content = fs32.readFileSync(logPath, "utf-8");
11228
+ if (fs33.existsSync(logPath)) {
11229
+ const content = fs33.readFileSync(logPath, "utf-8");
10978
11230
  tail = content.slice(-3e3);
10979
11231
  }
10980
11232
  } catch {
@@ -10999,7 +11251,7 @@ async function runCi(argv) {
10999
11251
  return 0;
11000
11252
  }
11001
11253
  const args = parseCiArgs(argv);
11002
- const cwd = args.cwd ? path30.resolve(args.cwd) : process.cwd();
11254
+ const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
11003
11255
  let earlyConfig;
11004
11256
  try {
11005
11257
  earlyConfig = loadConfig(cwd);
@@ -11009,9 +11261,9 @@ async function runCi(argv) {
11009
11261
  const eventName = process.env.GITHUB_EVENT_NAME;
11010
11262
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
11011
11263
  let manualWorkflowDispatch = false;
11012
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs32.existsSync(dispatchEventPath)) {
11264
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs33.existsSync(dispatchEventPath)) {
11013
11265
  try {
11014
- const evt = JSON.parse(fs32.readFileSync(dispatchEventPath, "utf-8"));
11266
+ const evt = JSON.parse(fs33.readFileSync(dispatchEventPath, "utf-8"));
11015
11267
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
11016
11268
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
11017
11269
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -11270,9 +11522,9 @@ function parseChatArgs(argv, env = process.env) {
11270
11522
  return result;
11271
11523
  }
11272
11524
  function commitChatFiles(cwd, sessionId, verbose) {
11273
- const sessionFile = path31.relative(cwd, sessionFilePath(cwd, sessionId));
11274
- const eventsFile = path31.relative(cwd, eventsFilePath(cwd, sessionId));
11275
- const paths = [sessionFile, eventsFile].filter((p) => fs33.existsSync(path31.join(cwd, p)));
11525
+ const sessionFile = path32.relative(cwd, sessionFilePath(cwd, sessionId));
11526
+ const eventsFile = path32.relative(cwd, eventsFilePath(cwd, sessionId));
11527
+ const paths = [sessionFile, eventsFile].filter((p) => fs34.existsSync(path32.join(cwd, p)));
11276
11528
  if (paths.length === 0) return;
11277
11529
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
11278
11530
  try {
@@ -11310,7 +11562,7 @@ async function runChat(argv) {
11310
11562
  ${CHAT_HELP}`);
11311
11563
  return 64;
11312
11564
  }
11313
- const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
11565
+ const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
11314
11566
  const sessionId = args.sessionId;
11315
11567
  const unpackedSecrets = unpackAllSecrets();
11316
11568
  if (unpackedSecrets > 0) {
@@ -11362,7 +11614,7 @@ ${CHAT_HELP}`);
11362
11614
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
11363
11615
  const meta = readMeta(sessionFile);
11364
11616
  process.stdout.write(
11365
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs33.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11617
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs34.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11366
11618
  `
11367
11619
  );
11368
11620
  try {
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "brain-serve",
3
+ "role": "utility",
4
+ "describe": "Long-lived HTTP server that wraps the Kody chat loop and speaks the Brain SSE protocol. Listens on $PORT (default 8080); auth via $BRAIN_API_KEY. Pairs with the Kody-Dashboard /api/kody/chat/brain proxy. Usage: `kody brain-serve` (runs until SIGINT/SIGTERM).",
5
+ "inputs": [],
6
+ "claudeCode": {
7
+ "model": "inherit",
8
+ "permissionMode": "acceptEdits",
9
+ "maxTurns": null,
10
+ "systemPromptAppend": null,
11
+ "tools": [],
12
+ "hooks": [],
13
+ "skills": [],
14
+ "commands": [],
15
+ "subagents": [],
16
+ "plugins": [],
17
+ "mcpServers": []
18
+ },
19
+ "cliTools": [],
20
+ "scripts": {
21
+ "preflight": [
22
+ {
23
+ "script": "brainServe"
24
+ }
25
+ ],
26
+ "postflight": []
27
+ }
28
+ }
@@ -89,7 +89,19 @@ For EACH file you will change or create, include:
89
89
  - Target state — what will be there after the change, at the same level of specificity.
90
90
  - Exact locations of edits (function name, line range if stable, or anchor like "after the `meta` group field, before the closing `fields: []`").
91
91
  - For new files: rough shape including exports, key functions with signatures, and top-level module comment. **Do not paste full function bodies** — signatures and 1–2 sentence intent per export are enough for an implementer to write the body. Single-line type/interface declarations and short config snippets are fine.
92
- - Dependencies touched (imports added/removed, new packages) — call out if anything needs installing.
92
+ - Dependencies touched (imports added/removed) — call out per file; list any new third-party packages here AND aggregate them in the `## Dependencies to install` section below.
93
+
94
+ ## Dependencies to install
95
+ REQUIRED if the plan introduces any third-party import not already present in
96
+ the repo's manifest (`package.json` / `requirements.txt` / `go.mod` / etc).
97
+ One line per dep with the exact install command the implementer should run,
98
+ picked from the repo's lockfile (`pnpm-lock.yaml` → pnpm, `package-lock.json`
99
+ → npm, `yarn.lock` → yarn, `bun.lockb` → bun):
100
+ - `pnpm add <pkg>` — runtime dep, used by `<file>`
101
+ - `pnpm add -D <pkg>` — dev/types-only dep (`@types/*`, test tooling)
102
+ If no new deps are needed, write the single line `- none`. Omitting this
103
+ section when new deps ARE needed is a planning failure — the implementer will
104
+ hit a `Cannot find module` typecheck error and have to recover blind.
93
105
 
94
106
  ## Algorithms & pseudocode
95
107
  REQUIRED for any non-trivial logic (sorting, diffing, state transitions, concurrency, batching, caching, conflict resolution).
@@ -41,6 +41,8 @@ If a prior-art block is present above, READ THE DIFFS — those are failed or su
41
41
  3. **Build** — Edit/Write to implement the change. Stay within the plan; if you discover the plan was wrong, briefly say so and adjust.
42
42
  4. **Test** — for every new module you added and every behavior you changed, write or update tests. If the plan above contains a "Test plan" section, treat it as authoritative: every item there must produce a corresponding test. Before writing a test, open the newest existing file in the same test directory (`tests/int/`, `tests/unit/`, `tests/e2e/`, or sibling `*.test.ts`) and copy its imports, setup hooks, and auth pattern **verbatim**. Do NOT introduce a new test infrastructure (own testcontainers, `fetch` against relative URLs, alternate auth headers) when a working pattern already exists in that directory — divergence from the established pattern is a hard failure even if the test passes locally. Cover at least one happy path and one failure path per change. Skipping tests is a hard failure. A change may only be declared untestable if you can name the specific blocker (e.g., "no fake exists for the X SDK and stubbing it would mock the entire call surface"); vague "this is just config" claims are rejected. Untestable changes go in `PLAN_DEVIATIONS:` with the named blocker.
43
43
  5. **Verify** — before declaring DONE, call the `verify` tool (mcp__kody-verify__verify). It runs typecheck/lint/tests with the project's configured commands and returns `{ ok, failures, attemptsRemaining }`. If `ok: true`, you may proceed to DONE. If `ok: false`, read the truncated `failures` list, fix the root cause, commit-equivalent edits, and call `verify` again. You have up to 4 total attempts; the tool will return `locked: true` after that and you must wrap up with FAILED. The postflight verifier runs again after this session ends and is the final ratifier — but it's also the gate that downgrades a self-reported DONE to FAILED if you skipped this step, so calling the tool is strictly cheaper than not.
44
+
45
+ **Allowed fixes between attempts** include installing missing third-party dependencies. If `failures` contains `Cannot find module 'X'` / `Cannot find package 'X'` / `error TS2307` for a NON-relative import (i.e. not `./` or `../`), the fix is to install it with the repo's package manager before the next verify call — `pnpm add X` (runtime) or `pnpm add -D X` (types-only, `@types/*`, dev tooling). Pick the package manager from the repo's lockfile: `pnpm-lock.yaml` → pnpm, `package-lock.json` → npm, `yarn.lock` → yarn, `bun.lockb` → bun. If the plan provided a `## Dependencies to install` section, prefer the exact command it specifies. Do NOT install a dep just to silence a relative-path resolution error — that's a code bug, fix the import path instead.
44
46
  6. Your FINAL message must use this exact format (or a single `FAILED: <reason>` line on failure). The `PLAN_DEVIATIONS:` block is REQUIRED whenever a plan was provided.
45
47
 
46
48
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.72",
3
+ "version": "0.4.74",
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",