@kody-ade/kody-engine 0.4.72 → 0.4.73

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
@@ -864,7 +864,7 @@ var init_loadPriorArt = __esm({
864
864
  // package.json
865
865
  var package_default = {
866
866
  name: "@kody-ade/kody-engine",
867
- version: "0.4.72",
867
+ version: "0.4.73",
868
868
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
869
869
  license: "MIT",
870
870
  type: "module",
@@ -919,8 +919,8 @@ var package_default = {
919
919
 
920
920
  // src/chat-cli.ts
921
921
  import { execFileSync as execFileSync31 } from "child_process";
922
- import * as fs33 from "fs";
923
- import * as path31 from "path";
922
+ import * as fs34 from "fs";
923
+ import * as path32 from "path";
924
924
 
925
925
  // src/chat/events.ts
926
926
  import * as fs from "fs";
@@ -2013,8 +2013,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2013
2013
 
2014
2014
  // src/kody-cli.ts
2015
2015
  import { execFileSync as execFileSync30 } from "child_process";
2016
- import * as fs32 from "fs";
2017
- import * as path30 from "path";
2016
+ import * as fs33 from "fs";
2017
+ import * as path31 from "path";
2018
2018
 
2019
2019
  // src/dispatch.ts
2020
2020
  import * as fs8 from "fs";
@@ -2455,8 +2455,8 @@ init_issue();
2455
2455
 
2456
2456
  // src/executor.ts
2457
2457
  import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
2458
- import * as fs31 from "fs";
2459
- import * as path29 from "path";
2458
+ import * as fs32 from "fs";
2459
+ import * as path30 from "path";
2460
2460
  init_events();
2461
2461
 
2462
2462
  // src/lifecycleLabels.ts
@@ -6659,6 +6659,19 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
6659
6659
  git2(["pull", "origin", branchName], cwd);
6660
6660
  } catch {
6661
6661
  }
6662
+ if (!baseBranch || baseBranch === defaultBranch2) {
6663
+ try {
6664
+ git2(["merge", "--no-edit", `origin/${defaultBranch2}`], cwd);
6665
+ } catch {
6666
+ try {
6667
+ git2(["merge", "--abort"], cwd);
6668
+ } catch {
6669
+ }
6670
+ throw new Error(
6671
+ `Branch '${branchName}' has merge conflicts with 'origin/${defaultBranch2}'. Resolve manually or delete the branch to start fresh.`
6672
+ );
6673
+ }
6674
+ }
6662
6675
  return { branch: branchName, created: false };
6663
6676
  }
6664
6677
  try {
@@ -7277,10 +7290,10 @@ import * as fs25 from "fs";
7277
7290
  import * as path24 from "path";
7278
7291
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
7279
7292
  var GoalStateError = class extends Error {
7280
- constructor(path32, message) {
7281
- super(`Invalid goal state at ${path32}:
7293
+ constructor(path33, message) {
7294
+ super(`Invalid goal state at ${path33}:
7282
7295
  ${message}`);
7283
- this.path = path32;
7296
+ this.path = path33;
7284
7297
  this.name = "GoalStateError";
7285
7298
  }
7286
7299
  path;
@@ -9002,6 +9015,240 @@ function buildChildEnv(parent, force) {
9002
9015
  return out;
9003
9016
  }
9004
9017
 
9018
+ // src/scripts/brainServe.ts
9019
+ import { createServer } from "http";
9020
+ import * as fs30 from "fs";
9021
+ import * as path29 from "path";
9022
+ var DEFAULT_PORT = 8080;
9023
+ function getApiKey() {
9024
+ const key = (process.env.BRAIN_API_KEY ?? "").trim();
9025
+ if (!key) {
9026
+ throw new Error(
9027
+ "BRAIN_API_KEY env var is required \u2014 set it on the Fly machine before boot."
9028
+ );
9029
+ }
9030
+ return key;
9031
+ }
9032
+ function authOk(req, expected) {
9033
+ const xApiKey = req.headers["x-api-key"]?.trim();
9034
+ if (xApiKey && xApiKey === expected) return true;
9035
+ const auth = req.headers["authorization"]?.trim();
9036
+ if (auth && auth.toLowerCase().startsWith("bearer ")) {
9037
+ return auth.slice(7).trim() === expected;
9038
+ }
9039
+ return false;
9040
+ }
9041
+ function readJsonBody(req) {
9042
+ return new Promise((resolve4, reject) => {
9043
+ const chunks = [];
9044
+ req.on("data", (c) => chunks.push(c));
9045
+ req.on("end", () => {
9046
+ const raw = Buffer.concat(chunks).toString("utf-8");
9047
+ if (!raw.trim()) {
9048
+ resolve4({});
9049
+ return;
9050
+ }
9051
+ try {
9052
+ resolve4(JSON.parse(raw));
9053
+ } catch (err) {
9054
+ reject(err instanceof Error ? err : new Error(String(err)));
9055
+ }
9056
+ });
9057
+ req.on("error", reject);
9058
+ });
9059
+ }
9060
+ function sendJson(res, status, body) {
9061
+ res.writeHead(status, { "content-type": "application/json" });
9062
+ res.end(JSON.stringify(body));
9063
+ }
9064
+ function writeSseHeaders(res) {
9065
+ res.writeHead(200, {
9066
+ "content-type": "text/event-stream; charset=utf-8",
9067
+ "cache-control": "no-cache, no-transform",
9068
+ connection: "keep-alive",
9069
+ "x-accel-buffering": "no"
9070
+ });
9071
+ }
9072
+ function emitSse(res, event) {
9073
+ res.write(`data: ${JSON.stringify(event)}
9074
+
9075
+ `);
9076
+ }
9077
+ var BrainSseSink = class {
9078
+ constructor(res, chatId) {
9079
+ this.res = res;
9080
+ this.chatId = chatId;
9081
+ }
9082
+ res;
9083
+ chatId;
9084
+ async emit(event) {
9085
+ switch (event.event) {
9086
+ case "chat.message": {
9087
+ const content = String(event.payload.content ?? "");
9088
+ if (content.length > 0) {
9089
+ emitSse(this.res, { type: "text", text: content, chatId: this.chatId });
9090
+ }
9091
+ return;
9092
+ }
9093
+ case "chat.tool": {
9094
+ if (event.payload.phase !== "use") return;
9095
+ emitSse(this.res, {
9096
+ type: "tool_use",
9097
+ name: typeof event.payload.name === "string" ? event.payload.name : "tool",
9098
+ input: event.payload.input ?? {},
9099
+ chatId: this.chatId
9100
+ });
9101
+ return;
9102
+ }
9103
+ case "chat.done": {
9104
+ emitSse(this.res, { type: "done", chatId: this.chatId });
9105
+ return;
9106
+ }
9107
+ case "chat.error": {
9108
+ const errMsg2 = typeof event.payload.error === "string" ? event.payload.error : "agent error";
9109
+ emitSse(this.res, { type: "error", error: errMsg2, chatId: this.chatId });
9110
+ return;
9111
+ }
9112
+ // chat.thinking / chat.ready / chat.exit — not part of the Brain protocol.
9113
+ default:
9114
+ return;
9115
+ }
9116
+ }
9117
+ };
9118
+ async function handleChatTurn(req, res, chatId, opts) {
9119
+ let body;
9120
+ try {
9121
+ body = await readJsonBody(req);
9122
+ } catch {
9123
+ sendJson(res, 400, { error: "invalid JSON body" });
9124
+ return;
9125
+ }
9126
+ const message = typeof body === "object" && body !== null && "message" in body ? body.message : void 0;
9127
+ if (typeof message !== "string" || !message.trim()) {
9128
+ sendJson(res, 400, { error: "message required" });
9129
+ return;
9130
+ }
9131
+ const sessionFile = sessionFilePath(opts.cwd, chatId);
9132
+ fs30.mkdirSync(path29.dirname(sessionFile), { recursive: true });
9133
+ appendTurn(sessionFile, {
9134
+ role: "user",
9135
+ content: message,
9136
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
9137
+ });
9138
+ writeSseHeaders(res);
9139
+ emitSse(res, { type: "chat", chatId });
9140
+ const sink = new BrainSseSink(res, chatId);
9141
+ try {
9142
+ await opts.runTurn({
9143
+ sessionId: chatId,
9144
+ sessionFile,
9145
+ cwd: opts.cwd,
9146
+ model: opts.model,
9147
+ litellmUrl: opts.litellmUrl,
9148
+ sink
9149
+ });
9150
+ } catch (err) {
9151
+ const errMsg2 = err instanceof Error ? err.message : String(err);
9152
+ process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
9153
+ `);
9154
+ try {
9155
+ emitSse(res, { type: "error", error: errMsg2, chatId });
9156
+ } catch {
9157
+ }
9158
+ } finally {
9159
+ try {
9160
+ res.end();
9161
+ } catch {
9162
+ }
9163
+ }
9164
+ }
9165
+ function buildServer(opts) {
9166
+ const runTurn = opts.runTurn ?? runChatTurn;
9167
+ return createServer(async (req, res) => {
9168
+ if (!req.method || !req.url) {
9169
+ sendJson(res, 400, { error: "bad request" });
9170
+ return;
9171
+ }
9172
+ const url = new URL(req.url, `http://localhost`);
9173
+ if (req.method === "GET" && url.pathname === "/healthz") {
9174
+ sendJson(res, 200, { ok: true });
9175
+ return;
9176
+ }
9177
+ if (!authOk(req, opts.apiKey)) {
9178
+ sendJson(res, 401, { error: "unauthorized" });
9179
+ return;
9180
+ }
9181
+ const m = url.pathname.match(/^\/chats\/([^/]+)\/messages\/?$/);
9182
+ if (req.method === "POST" && m) {
9183
+ const chatId = decodeURIComponent(m[1] ?? "");
9184
+ if (!chatId) {
9185
+ sendJson(res, 400, { error: "chatId required" });
9186
+ return;
9187
+ }
9188
+ await handleChatTurn(req, res, chatId, {
9189
+ cwd: opts.cwd,
9190
+ model: opts.model,
9191
+ litellmUrl: opts.litellmUrl,
9192
+ runTurn
9193
+ });
9194
+ return;
9195
+ }
9196
+ sendJson(res, 404, { error: "not found" });
9197
+ });
9198
+ }
9199
+ var brainServe = async (ctx) => {
9200
+ ctx.skipAgent = true;
9201
+ const apiKey = getApiKey();
9202
+ const port = Number(process.env.PORT ?? DEFAULT_PORT);
9203
+ const model = parseProviderModel(ctx.config.agent.model);
9204
+ const usesProxy = needsLitellmProxy(model);
9205
+ let handle = null;
9206
+ if (usesProxy) {
9207
+ process.stdout.write(
9208
+ `[brain-serve] starting LiteLLM proxy for ${model.provider}/${model.model}...
9209
+ `
9210
+ );
9211
+ handle = await startLitellmIfNeeded(model, ctx.cwd);
9212
+ process.stdout.write(
9213
+ `[brain-serve] LiteLLM ready at ${handle?.url ?? LITELLM_DEFAULT_URL}
9214
+ `
9215
+ );
9216
+ }
9217
+ const litellmUrl = usesProxy ? handle?.url ?? LITELLM_DEFAULT_URL : null;
9218
+ const server = buildServer({
9219
+ apiKey,
9220
+ cwd: ctx.cwd,
9221
+ model,
9222
+ litellmUrl
9223
+ });
9224
+ await new Promise((resolve4) => {
9225
+ server.listen(port, "0.0.0.0", () => {
9226
+ process.stdout.write(
9227
+ `[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
9228
+ `
9229
+ );
9230
+ resolve4();
9231
+ });
9232
+ });
9233
+ const shutdown = (signal) => {
9234
+ process.stdout.write(`[brain-serve] ${signal} \u2014 shutting down
9235
+ `);
9236
+ server.close(() => {
9237
+ if (handle) {
9238
+ try {
9239
+ handle.kill();
9240
+ } catch {
9241
+ }
9242
+ }
9243
+ process.exit(0);
9244
+ });
9245
+ };
9246
+ process.once("SIGINT", () => shutdown("SIGINT"));
9247
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
9248
+ await new Promise(() => {
9249
+ });
9250
+ };
9251
+
9005
9252
  // src/scripts/serveFlow.ts
9006
9253
  import { spawn as spawn3 } from "child_process";
9007
9254
  function parseTarget(positional) {
@@ -9841,7 +10088,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
9841
10088
  };
9842
10089
 
9843
10090
  // src/scripts/writeRunSummary.ts
9844
- import * as fs30 from "fs";
10091
+ import * as fs31 from "fs";
9845
10092
  var writeRunSummary = async (ctx, profile) => {
9846
10093
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
9847
10094
  if (!summaryPath) return;
@@ -9863,7 +10110,7 @@ var writeRunSummary = async (ctx, profile) => {
9863
10110
  if (reason) lines.push(`- **Reason:** ${reason}`);
9864
10111
  lines.push("");
9865
10112
  try {
9866
- fs30.appendFileSync(summaryPath, `${lines.join("\n")}
10113
+ fs31.appendFileSync(summaryPath, `${lines.join("\n")}
9867
10114
  `);
9868
10115
  } catch {
9869
10116
  }
@@ -9905,6 +10152,7 @@ var preflightScripts = {
9905
10152
  dispatchJobFileTicks,
9906
10153
  runTickScript,
9907
10154
  serveFlow,
10155
+ brainServe,
9908
10156
  loadGoalState,
9909
10157
  handleAbandonedGoal,
9910
10158
  deriveGoalPhase,
@@ -10085,9 +10333,9 @@ async function runExecutable(profileName, input) {
10085
10333
  data: { ...input.preloadedData ?? {} },
10086
10334
  output: { exitCode: 0 }
10087
10335
  };
10088
- const ndjsonDir = path29.join(input.cwd, ".kody");
10336
+ const ndjsonDir = path30.join(input.cwd, ".kody");
10089
10337
  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);
10338
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path30.isAbsolute(p) ? p : path30.resolve(profile.dir, p)).filter((p) => p.length > 0);
10091
10339
  const syntheticPath = ctx.data.syntheticPluginPath;
10092
10340
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
10093
10341
  return runAgent({
@@ -10282,7 +10530,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
10282
10530
  function getProfileInputsForChild(profileName, _cwd) {
10283
10531
  try {
10284
10532
  const profilePath = resolveProfilePath(profileName);
10285
- if (!fs31.existsSync(profilePath)) return null;
10533
+ if (!fs32.existsSync(profilePath)) return null;
10286
10534
  return loadProfile(profilePath).inputs;
10287
10535
  } catch {
10288
10536
  return null;
@@ -10291,17 +10539,17 @@ function getProfileInputsForChild(profileName, _cwd) {
10291
10539
  function resolveProfilePath(profileName) {
10292
10540
  const found = resolveExecutable(profileName);
10293
10541
  if (found) return found;
10294
- const here = path29.dirname(new URL(import.meta.url).pathname);
10542
+ const here = path30.dirname(new URL(import.meta.url).pathname);
10295
10543
  const candidates = [
10296
- path29.join(here, "executables", profileName, "profile.json"),
10544
+ path30.join(here, "executables", profileName, "profile.json"),
10297
10545
  // same-dir sibling (dev)
10298
- path29.join(here, "..", "executables", profileName, "profile.json"),
10546
+ path30.join(here, "..", "executables", profileName, "profile.json"),
10299
10547
  // up one (prod: dist/bin → dist/executables)
10300
- path29.join(here, "..", "src", "executables", profileName, "profile.json")
10548
+ path30.join(here, "..", "src", "executables", profileName, "profile.json")
10301
10549
  // fallback
10302
10550
  ];
10303
10551
  for (const c of candidates) {
10304
- if (fs31.existsSync(c)) return c;
10552
+ if (fs32.existsSync(c)) return c;
10305
10553
  }
10306
10554
  return candidates[0];
10307
10555
  }
@@ -10401,8 +10649,8 @@ function resolveShellTimeoutMs(entry) {
10401
10649
  var SIGKILL_GRACE_MS = 5e3;
10402
10650
  async function runShellEntry(entry, ctx, profile) {
10403
10651
  const shellName = entry.shell;
10404
- const shellPath = path29.join(profile.dir, shellName);
10405
- if (!fs31.existsSync(shellPath)) {
10652
+ const shellPath = path30.join(profile.dir, shellName);
10653
+ if (!fs32.existsSync(shellPath)) {
10406
10654
  ctx.skipAgent = true;
10407
10655
  ctx.output.exitCode = 99;
10408
10656
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -10881,9 +11129,9 @@ function resolveAuthToken(env = process.env) {
10881
11129
  return token;
10882
11130
  }
10883
11131
  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";
11132
+ if (fs33.existsSync(path31.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11133
+ if (fs33.existsSync(path31.join(cwd, "yarn.lock"))) return "yarn";
11134
+ if (fs33.existsSync(path31.join(cwd, "bun.lockb"))) return "bun";
10887
11135
  return "npm";
10888
11136
  }
10889
11137
  function shellOut(cmd, args, cwd, stream = true) {
@@ -10970,11 +11218,11 @@ function configureGitIdentity(cwd) {
10970
11218
  }
10971
11219
  function postFailureTail(issueNumber, cwd, reason) {
10972
11220
  if (!issueNumber) return;
10973
- const logPath = path30.join(cwd, ".kody", "last-run.jsonl");
11221
+ const logPath = path31.join(cwd, ".kody", "last-run.jsonl");
10974
11222
  let tail = "";
10975
11223
  try {
10976
- if (fs32.existsSync(logPath)) {
10977
- const content = fs32.readFileSync(logPath, "utf-8");
11224
+ if (fs33.existsSync(logPath)) {
11225
+ const content = fs33.readFileSync(logPath, "utf-8");
10978
11226
  tail = content.slice(-3e3);
10979
11227
  }
10980
11228
  } catch {
@@ -10999,7 +11247,7 @@ async function runCi(argv) {
10999
11247
  return 0;
11000
11248
  }
11001
11249
  const args = parseCiArgs(argv);
11002
- const cwd = args.cwd ? path30.resolve(args.cwd) : process.cwd();
11250
+ const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
11003
11251
  let earlyConfig;
11004
11252
  try {
11005
11253
  earlyConfig = loadConfig(cwd);
@@ -11009,9 +11257,9 @@ async function runCi(argv) {
11009
11257
  const eventName = process.env.GITHUB_EVENT_NAME;
11010
11258
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
11011
11259
  let manualWorkflowDispatch = false;
11012
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs32.existsSync(dispatchEventPath)) {
11260
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs33.existsSync(dispatchEventPath)) {
11013
11261
  try {
11014
- const evt = JSON.parse(fs32.readFileSync(dispatchEventPath, "utf-8"));
11262
+ const evt = JSON.parse(fs33.readFileSync(dispatchEventPath, "utf-8"));
11015
11263
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
11016
11264
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
11017
11265
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -11270,9 +11518,9 @@ function parseChatArgs(argv, env = process.env) {
11270
11518
  return result;
11271
11519
  }
11272
11520
  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)));
11521
+ const sessionFile = path32.relative(cwd, sessionFilePath(cwd, sessionId));
11522
+ const eventsFile = path32.relative(cwd, eventsFilePath(cwd, sessionId));
11523
+ const paths = [sessionFile, eventsFile].filter((p) => fs34.existsSync(path32.join(cwd, p)));
11276
11524
  if (paths.length === 0) return;
11277
11525
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
11278
11526
  try {
@@ -11310,7 +11558,7 @@ async function runChat(argv) {
11310
11558
  ${CHAT_HELP}`);
11311
11559
  return 64;
11312
11560
  }
11313
- const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
11561
+ const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
11314
11562
  const sessionId = args.sessionId;
11315
11563
  const unpackedSecrets = unpackAllSecrets();
11316
11564
  if (unpackedSecrets > 0) {
@@ -11362,7 +11610,7 @@ ${CHAT_HELP}`);
11362
11610
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
11363
11611
  const meta = readMeta(sessionFile);
11364
11612
  process.stdout.write(
11365
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs33.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11613
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs34.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11366
11614
  `
11367
11615
  );
11368
11616
  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.73",
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",