@kody-ade/kody-engine 0.4.57 → 0.4.59

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
@@ -1,9 +1,309 @@
1
1
  #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __esm = (fn, res) => function __init() {
5
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
6
+ };
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+
12
+ // src/events.ts
13
+ import * as crypto from "crypto";
14
+ import * as fs3 from "fs";
15
+ import * as path3 from "path";
16
+ function resolveRunId() {
17
+ if (cachedRunId) return cachedRunId;
18
+ if (process.env.KODY_RUN_ID) {
19
+ cachedRunId = process.env.KODY_RUN_ID;
20
+ return cachedRunId;
21
+ }
22
+ if (process.env.GITHUB_RUN_ID) {
23
+ const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
24
+ cachedRunId = `gh-${process.env.GITHUB_RUN_ID}-${attempt}`;
25
+ } else {
26
+ cachedRunId = `${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
27
+ }
28
+ process.env.KODY_RUN_ID = cachedRunId;
29
+ return cachedRunId;
30
+ }
31
+ function emitEvent(cwd, ev) {
32
+ if (process.env.KODY_EVENTS === "0") return;
33
+ try {
34
+ const runId = resolveRunId();
35
+ const fullEvent = {
36
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
37
+ runId,
38
+ ...ev
39
+ };
40
+ const eventsPath = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
41
+ fs3.mkdirSync(path3.dirname(eventsPath), { recursive: true });
42
+ fs3.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}
43
+ `);
44
+ } catch {
45
+ }
46
+ }
47
+ function readEvents(cwd, runId) {
48
+ const eventsPath = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
49
+ if (!fs3.existsSync(eventsPath)) return [];
50
+ const lines = fs3.readFileSync(eventsPath, "utf-8").split("\n");
51
+ const out = [];
52
+ for (const line of lines) {
53
+ const trimmed = line.trim();
54
+ if (!trimmed) continue;
55
+ try {
56
+ out.push(JSON.parse(trimmed));
57
+ } catch {
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+ function listRuns(cwd) {
63
+ const runsDir = path3.join(cwd, ".kody", "runs");
64
+ if (!fs3.existsSync(runsDir)) return [];
65
+ return fs3.readdirSync(runsDir).filter((name) => {
66
+ try {
67
+ return fs3.statSync(path3.join(runsDir, name)).isDirectory();
68
+ } catch {
69
+ return false;
70
+ }
71
+ }).sort();
72
+ }
73
+ var cachedRunId;
74
+ var init_events = __esm({
75
+ "src/events.ts"() {
76
+ "use strict";
77
+ cachedRunId = null;
78
+ }
79
+ });
80
+
81
+ // src/verify.ts
82
+ import { spawn } from "child_process";
83
+ function runCommand(command, cwd) {
84
+ return new Promise((resolve4) => {
85
+ const start = Date.now();
86
+ const child = spawn(command, {
87
+ cwd,
88
+ shell: true,
89
+ env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
90
+ stdio: ["ignore", "pipe", "pipe"]
91
+ });
92
+ const buffers = [];
93
+ let totalSize = 0;
94
+ const collect = (chunk) => {
95
+ buffers.push(chunk);
96
+ totalSize += chunk.length;
97
+ while (totalSize > TAIL_CHARS * 4 && buffers.length > 1) {
98
+ totalSize -= buffers[0].length;
99
+ buffers.shift();
100
+ }
101
+ };
102
+ child.stdout?.on("data", collect);
103
+ child.stderr?.on("data", collect);
104
+ const timer = setTimeout(() => {
105
+ child.kill("SIGTERM");
106
+ setTimeout(() => {
107
+ if (!child.killed) child.kill("SIGKILL");
108
+ }, 5e3);
109
+ }, COMMAND_TIMEOUT_MS);
110
+ child.on("exit", (code) => {
111
+ clearTimeout(timer);
112
+ const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
113
+ resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
114
+ });
115
+ child.on("error", (err) => {
116
+ clearTimeout(timer);
117
+ resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
118
+ });
119
+ });
120
+ }
121
+ async function verifyAll(config, cwd) {
122
+ const commands = [];
123
+ if (config.quality.typecheck) commands.push({ name: "typecheck", cmd: config.quality.typecheck });
124
+ if (config.quality.testUnit) commands.push({ name: "test", cmd: config.quality.testUnit });
125
+ if (config.quality.lint) commands.push({ name: "lint", cmd: config.quality.lint });
126
+ if (config.quality.format) commands.push({ name: "format", cmd: config.quality.format });
127
+ const failed = [];
128
+ const details = {};
129
+ for (const { name, cmd } of commands) {
130
+ const result = await runCommand(cmd, cwd);
131
+ details[name] = result;
132
+ if (result.exitCode !== 0) failed.push(name);
133
+ }
134
+ return { ok: failed.length === 0, failed, details };
135
+ }
136
+ async function applyTestRetries(initial, testCommand, cwd, runner, testRetries = DEFAULT_TEST_RETRIES) {
137
+ if (initial.ok) return { ...initial, recovered: [] };
138
+ const recovered = [];
139
+ const details = { ...initial.details };
140
+ let failed = [...initial.failed];
141
+ if (failed.includes("test") && testCommand && testRetries > 0) {
142
+ for (let attempt = 1; attempt <= testRetries; attempt++) {
143
+ const retry = await runner(testCommand, cwd);
144
+ details[`test (retry ${attempt})`] = retry;
145
+ if (retry.exitCode === 0) {
146
+ failed = failed.filter((f) => f !== "test");
147
+ recovered.push("test");
148
+ break;
149
+ }
150
+ }
151
+ }
152
+ return { ok: failed.length === 0, failed, details, recovered };
153
+ }
154
+ async function verifyAllWithRetry(config, cwd, opts) {
155
+ const initial = await verifyAll(config, cwd);
156
+ return applyTestRetries(initial, config.quality.testUnit, cwd, runCommand, opts?.testRetries);
157
+ }
158
+ function stripAnsi(s) {
159
+ return s.replace(ANSI_RE, "");
160
+ }
161
+ function summarizeFailure(result) {
162
+ const lines = [`verify failed: ${result.failed.join(", ")}`];
163
+ for (const name of result.failed) {
164
+ const d = result.details[name];
165
+ if (!d) continue;
166
+ lines.push(`
167
+ --- ${name} (exit ${d.exitCode}, ${(d.durationMs / 1e3).toFixed(1)}s) ---`);
168
+ lines.push(stripAnsi(d.tail));
169
+ for (let attempt = 1; ; attempt++) {
170
+ const retry = result.details[`${name} (retry ${attempt})`];
171
+ if (!retry) break;
172
+ lines.push(`
173
+ --- ${name} (retry ${attempt}: exit ${retry.exitCode}, ${(retry.durationMs / 1e3).toFixed(1)}s) ---`);
174
+ lines.push(stripAnsi(retry.tail));
175
+ }
176
+ }
177
+ return lines.join("\n");
178
+ }
179
+ var TAIL_CHARS, COMMAND_TIMEOUT_MS, DEFAULT_TEST_RETRIES, ANSI_RE;
180
+ var init_verify = __esm({
181
+ "src/verify.ts"() {
182
+ "use strict";
183
+ TAIL_CHARS = 4e3;
184
+ COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
185
+ DEFAULT_TEST_RETRIES = 2;
186
+ ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
187
+ }
188
+ });
189
+
190
+ // src/verifyMcp.ts
191
+ var verifyMcp_exports = {};
192
+ __export(verifyMcp_exports, {
193
+ DEFAULT_MAX_VERIFY_ATTEMPTS: () => DEFAULT_MAX_VERIFY_ATTEMPTS,
194
+ buildVerifyMcpServer: () => buildVerifyMcpServer,
195
+ truncateVerifyResult: () => truncateVerifyResult
196
+ });
197
+ import { createSdkMcpServer, tool } from "@anthropic-ai/claude-agent-sdk";
198
+ import { z } from "zod";
199
+ function buildVerifyMcpServer(opts) {
200
+ const state = {
201
+ attempts: 0,
202
+ maxAttempts: opts.maxAttempts ?? DEFAULT_MAX_VERIFY_ATTEMPTS
203
+ };
204
+ const runVerify = opts.__runVerify ?? verifyAllWithRetry;
205
+ const verifyTool = tool(
206
+ "verify",
207
+ "Run the project's quality gates (typecheck, lint, tests). Returns ok=true with empty failures when everything passes. Call this before declaring DONE. If ok=false, read the truncated failures, fix the code, commit, and call verify() again. You have a bounded number of attempts; after that the tool stops accepting calls and you must wrap up with whatever state is current.",
208
+ {},
209
+ async (_args, _extra) => {
210
+ state.attempts++;
211
+ const attempt = state.attempts;
212
+ if (attempt > state.maxAttempts) {
213
+ emitEvent(opts.cwd, {
214
+ executable: opts.executable,
215
+ kind: "error",
216
+ name: "verify_tool",
217
+ outcome: "failed",
218
+ meta: { reason: "budget exhausted", attempts: attempt, maxAttempts: state.maxAttempts }
219
+ });
220
+ return {
221
+ content: [
222
+ {
223
+ type: "text",
224
+ text: JSON.stringify({
225
+ ok: false,
226
+ locked: true,
227
+ reason: `verify budget exhausted (${state.maxAttempts} attempts used)`
228
+ })
229
+ }
230
+ ]
231
+ };
232
+ }
233
+ const startedAt = Date.now();
234
+ const result = await runVerify(opts.config, opts.cwd);
235
+ const durationMs = Date.now() - startedAt;
236
+ emitEvent(opts.cwd, {
237
+ executable: opts.executable,
238
+ kind: "postflight",
239
+ name: `verify_attempt_${attempt}`,
240
+ durationMs,
241
+ outcome: result.ok ? "ok" : "failed",
242
+ meta: {
243
+ attempt,
244
+ failureCount: result.failed.length,
245
+ recovered: result.recovered ?? []
246
+ }
247
+ });
248
+ const payload = truncateVerifyResult(result, state, attempt);
249
+ return {
250
+ content: [
251
+ {
252
+ type: "text",
253
+ text: JSON.stringify(payload)
254
+ }
255
+ ]
256
+ };
257
+ }
258
+ );
259
+ return createSdkMcpServer({
260
+ name: "kody-verify",
261
+ version: "0.1.0",
262
+ tools: [verifyTool]
263
+ });
264
+ }
265
+ function truncateVerifyResult(result, state, attempt) {
266
+ const failures = result.failed.slice(0, 5).map((name) => {
267
+ const detail = result.details[name];
268
+ const tail = detail?.tail ?? "";
269
+ return {
270
+ name,
271
+ exitCode: detail?.exitCode ?? -1,
272
+ tail: tail.length > PER_FAILURE_TAIL_CHARS ? `\u2026${tail.slice(-PER_FAILURE_TAIL_CHARS)}` : tail
273
+ };
274
+ });
275
+ const payload = {
276
+ ok: result.ok,
277
+ attempt,
278
+ attemptsRemaining: Math.max(0, state.maxAttempts - attempt),
279
+ failures,
280
+ ...result.recovered && result.recovered.length > 0 ? { recovered: result.recovered } : {}
281
+ };
282
+ const json = JSON.stringify(payload);
283
+ if (json.length <= MAX_RESULT_BYTES) return payload;
284
+ for (const f of payload.failures) {
285
+ f.tail = f.tail.slice(-Math.max(120, Math.floor(PER_FAILURE_TAIL_CHARS / 4)));
286
+ if (JSON.stringify(payload).length <= MAX_RESULT_BYTES) return payload;
287
+ }
288
+ for (const f of payload.failures) f.tail = "";
289
+ return payload;
290
+ }
291
+ var DEFAULT_MAX_VERIFY_ATTEMPTS, MAX_RESULT_BYTES, PER_FAILURE_TAIL_CHARS;
292
+ var init_verifyMcp = __esm({
293
+ "src/verifyMcp.ts"() {
294
+ "use strict";
295
+ init_events();
296
+ init_verify();
297
+ DEFAULT_MAX_VERIFY_ATTEMPTS = 4;
298
+ MAX_RESULT_BYTES = 2048;
299
+ PER_FAILURE_TAIL_CHARS = 600;
300
+ }
301
+ });
2
302
 
3
303
  // package.json
4
304
  var package_default = {
5
305
  name: "@kody-ade/kody-engine",
6
- version: "0.4.57",
306
+ version: "0.4.59",
7
307
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
8
308
  license: "MIT",
9
309
  type: "module",
@@ -29,7 +329,8 @@ var package_default = {
29
329
  },
30
330
  dependencies: {
31
331
  "@actions/cache": "^6.0.0",
32
- "@anthropic-ai/claude-agent-sdk": "0.2.119"
332
+ "@anthropic-ai/claude-agent-sdk": "0.2.119",
333
+ zod: "^4.0.0"
33
334
  },
34
335
  devDependencies: {
35
336
  "@biomejs/biome": "^2.4.12",
@@ -119,8 +420,8 @@ function makeRunId(sessionId, suffix) {
119
420
  }
120
421
 
121
422
  // src/agent.ts
122
- import * as fs3 from "fs";
123
- import * as path3 from "path";
423
+ import * as fs4 from "fs";
424
+ import * as path4 from "path";
124
425
  import { query } from "@anthropic-ai/claude-agent-sdk";
125
426
 
126
427
  // src/config.ts
@@ -412,10 +713,10 @@ function resolveTurnTimeoutMs(opts) {
412
713
  return DEFAULT_TURN_TIMEOUT_MS;
413
714
  }
414
715
  async function runAgent(opts) {
415
- const ndjsonDir = opts.ndjsonDir ?? path3.join(opts.cwd, ".kody");
416
- fs3.mkdirSync(ndjsonDir, { recursive: true });
417
- const ndjsonPath = path3.join(ndjsonDir, "last-run.jsonl");
418
- const fullLog = fs3.createWriteStream(ndjsonPath, { flags: "w" });
716
+ const ndjsonDir = opts.ndjsonDir ?? path4.join(opts.cwd, ".kody");
717
+ fs4.mkdirSync(ndjsonDir, { recursive: true });
718
+ const ndjsonPath = path4.join(ndjsonDir, "last-run.jsonl");
719
+ const fullLog = fs4.createWriteStream(ndjsonPath, { flags: "w" });
419
720
  const env = {
420
721
  ...process.env,
421
722
  SKIP_HOOKS: "1",
@@ -452,15 +753,27 @@ async function runAgent(opts) {
452
753
  permissionMode: opts.permissionModeOverride ?? "acceptEdits",
453
754
  env
454
755
  };
756
+ const mcpEntries = [];
455
757
  if (opts.mcpServers && opts.mcpServers.length > 0) {
456
- queryOptions.mcpServers = Object.fromEntries(
457
- opts.mcpServers.map((s) => {
458
- const cfg = { command: s.command };
459
- if (s.args) cfg.args = s.args;
460
- if (s.env) cfg.env = s.env;
461
- return [s.name, cfg];
462
- })
463
- );
758
+ for (const s of opts.mcpServers) {
759
+ const cfg = { command: s.command };
760
+ if (s.args) cfg.args = s.args;
761
+ if (s.env) cfg.env = s.env;
762
+ mcpEntries.push([s.name, cfg]);
763
+ }
764
+ }
765
+ if (opts.enableVerifyTool && opts.verifyConfig) {
766
+ const { buildVerifyMcpServer: buildVerifyMcpServer2 } = await Promise.resolve().then(() => (init_verifyMcp(), verifyMcp_exports));
767
+ const verifyServer = buildVerifyMcpServer2({
768
+ config: opts.verifyConfig,
769
+ cwd: opts.cwd,
770
+ executable: opts.executableName ?? "agent",
771
+ maxAttempts: typeof opts.verifyToolMaxAttempts === "number" && opts.verifyToolMaxAttempts > 0 ? opts.verifyToolMaxAttempts : void 0
772
+ });
773
+ mcpEntries.push(["kody-verify", verifyServer]);
774
+ }
775
+ if (mcpEntries.length > 0) {
776
+ queryOptions.mcpServers = Object.fromEntries(mcpEntries);
464
777
  }
465
778
  if (opts.pluginPaths && opts.pluginPaths.length > 0) {
466
779
  queryOptions.plugins = opts.pluginPaths.map((p) => ({ type: "local", path: p }));
@@ -588,14 +901,14 @@ async function runAgent(opts) {
588
901
  }
589
902
 
590
903
  // src/chat/session.ts
591
- import * as fs4 from "fs";
592
- import * as path4 from "path";
904
+ import * as fs5 from "fs";
905
+ import * as path5 from "path";
593
906
  function sessionFilePath(cwd, sessionId) {
594
- return path4.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
907
+ return path5.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
595
908
  }
596
909
  function readMeta(file) {
597
- if (!fs4.existsSync(file)) return null;
598
- const raw = fs4.readFileSync(file, "utf-8");
910
+ if (!fs5.existsSync(file)) return null;
911
+ const raw = fs5.readFileSync(file, "utf-8");
599
912
  const firstLine2 = raw.split("\n", 1)[0]?.trim();
600
913
  if (!firstLine2) return null;
601
914
  try {
@@ -608,8 +921,8 @@ function readMeta(file) {
608
921
  }
609
922
  }
610
923
  function readSession(file) {
611
- if (!fs4.existsSync(file)) return [];
612
- const raw = fs4.readFileSync(file, "utf-8").trim();
924
+ if (!fs5.existsSync(file)) return [];
925
+ const raw = fs5.readFileSync(file, "utf-8").trim();
613
926
  if (!raw) return [];
614
927
  const turns = [];
615
928
  for (const line of raw.split("\n")) {
@@ -625,14 +938,14 @@ function readSession(file) {
625
938
  return turns;
626
939
  }
627
940
  function appendTurn(file, turn) {
628
- fs4.mkdirSync(path4.dirname(file), { recursive: true });
941
+ fs5.mkdirSync(path5.dirname(file), { recursive: true });
629
942
  const line = JSON.stringify({
630
943
  role: turn.role,
631
944
  content: turn.content,
632
945
  timestamp: turn.timestamp,
633
946
  toolCalls: turn.toolCalls ?? []
634
947
  });
635
- fs4.appendFileSync(file, `${line}
948
+ fs5.appendFileSync(file, `${line}
636
949
  `);
637
950
  }
638
951
  function seedInitialMessage(file, message) {
@@ -777,8 +1090,8 @@ async function emit(sink, type, sessionId, suffix, payload) {
777
1090
 
778
1091
  // src/chat/modes/interactive.ts
779
1092
  import { execFileSync as execFileSync2 } from "child_process";
780
- import * as fs5 from "fs";
781
- import * as path5 from "path";
1093
+ import * as fs6 from "fs";
1094
+ import * as path6 from "path";
782
1095
 
783
1096
  // src/chat/inbox.ts
784
1097
  import { execFileSync } from "child_process";
@@ -935,9 +1248,9 @@ function findNextUserTurn(turns, fromIdx) {
935
1248
  return -1;
936
1249
  }
937
1250
  function commitTurn(cwd, sessionId, verbose) {
938
- const sessionRel = path5.relative(cwd, sessionFilePath(cwd, sessionId));
939
- const eventsRel = path5.relative(cwd, eventsFilePath(cwd, sessionId));
940
- const paths = [sessionRel, eventsRel].filter((p) => fs5.existsSync(path5.join(cwd, p)));
1251
+ const sessionRel = path6.relative(cwd, sessionFilePath(cwd, sessionId));
1252
+ const eventsRel = path6.relative(cwd, eventsFilePath(cwd, sessionId));
1253
+ const paths = [sessionRel, eventsRel].filter((p) => fs6.existsSync(path6.join(cwd, p)));
941
1254
  if (paths.length === 0) return;
942
1255
  const stdio = verbose ? "inherit" : "pipe";
943
1256
  const exec = (args) => execFileSync2("git", args, { cwd, stdio });
@@ -1017,7 +1330,7 @@ import * as fs31 from "fs";
1017
1330
  import * as path29 from "path";
1018
1331
 
1019
1332
  // src/dispatch.ts
1020
- import * as fs7 from "fs";
1333
+ import * as fs8 from "fs";
1021
1334
 
1022
1335
  // src/cron-match.ts
1023
1336
  var FIELD_BOUNDS = [
@@ -1082,48 +1395,48 @@ function cronMatchesInWindow(spec, end, windowSec) {
1082
1395
  }
1083
1396
 
1084
1397
  // src/registry.ts
1085
- import * as fs6 from "fs";
1086
- import * as path6 from "path";
1398
+ import * as fs7 from "fs";
1399
+ import * as path7 from "path";
1087
1400
  function getExecutablesRoot() {
1088
- const here = path6.dirname(new URL(import.meta.url).pathname);
1401
+ const here = path7.dirname(new URL(import.meta.url).pathname);
1089
1402
  const candidates = [
1090
- path6.join(here, "executables"),
1403
+ path7.join(here, "executables"),
1091
1404
  // dev: src/
1092
- path6.join(here, "..", "executables"),
1405
+ path7.join(here, "..", "executables"),
1093
1406
  // built: dist/bin → dist/executables
1094
- path6.join(here, "..", "src", "executables")
1407
+ path7.join(here, "..", "src", "executables")
1095
1408
  // fallback
1096
1409
  ];
1097
1410
  for (const c of candidates) {
1098
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
1411
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
1099
1412
  }
1100
1413
  return candidates[0];
1101
1414
  }
1102
1415
  function getProjectExecutablesRoot() {
1103
- return path6.join(process.cwd(), ".kody", "executables");
1416
+ return path7.join(process.cwd(), ".kody", "executables");
1104
1417
  }
1105
1418
  function getBuiltinJobsRoot() {
1106
- const here = path6.dirname(new URL(import.meta.url).pathname);
1419
+ const here = path7.dirname(new URL(import.meta.url).pathname);
1107
1420
  const candidates = [
1108
- path6.join(here, "jobs"),
1421
+ path7.join(here, "jobs"),
1109
1422
  // dev: src/
1110
- path6.join(here, "..", "jobs"),
1423
+ path7.join(here, "..", "jobs"),
1111
1424
  // built: dist/bin → dist/jobs
1112
- path6.join(here, "..", "src", "jobs")
1425
+ path7.join(here, "..", "src", "jobs")
1113
1426
  // fallback
1114
1427
  ];
1115
1428
  for (const c of candidates) {
1116
- if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
1429
+ if (fs7.existsSync(c) && fs7.statSync(c).isDirectory()) return c;
1117
1430
  }
1118
1431
  return candidates[0];
1119
1432
  }
1120
1433
  function listBuiltinJobs(root = getBuiltinJobsRoot()) {
1121
- if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
1434
+ if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
1122
1435
  const out = [];
1123
- for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
1436
+ for (const ent of fs7.readdirSync(root, { withFileTypes: true })) {
1124
1437
  if (!ent.isFile() || !ent.name.endsWith(".md")) continue;
1125
1438
  const slug = ent.name.slice(0, -3);
1126
- out.push({ slug, filePath: path6.join(root, ent.name) });
1439
+ out.push({ slug, filePath: path7.join(root, ent.name) });
1127
1440
  }
1128
1441
  out.sort((a, b) => a.slug.localeCompare(b.slug));
1129
1442
  return out;
@@ -1136,13 +1449,13 @@ function listExecutables(roots = getExecutableRoots()) {
1136
1449
  const seen = /* @__PURE__ */ new Set();
1137
1450
  const out = [];
1138
1451
  for (const root of rootList) {
1139
- if (!fs6.existsSync(root)) continue;
1140
- const entries = fs6.readdirSync(root, { withFileTypes: true });
1452
+ if (!fs7.existsSync(root)) continue;
1453
+ const entries = fs7.readdirSync(root, { withFileTypes: true });
1141
1454
  for (const ent of entries) {
1142
1455
  if (!ent.isDirectory()) continue;
1143
1456
  if (seen.has(ent.name)) continue;
1144
- const profilePath = path6.join(root, ent.name, "profile.json");
1145
- if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
1457
+ const profilePath = path7.join(root, ent.name, "profile.json");
1458
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
1146
1459
  out.push({ name: ent.name, profilePath });
1147
1460
  seen.add(ent.name);
1148
1461
  }
@@ -1154,8 +1467,8 @@ function resolveExecutable(name, roots = getExecutableRoots()) {
1154
1467
  if (!isSafeName(name)) return null;
1155
1468
  const rootList = typeof roots === "string" ? [roots] : roots;
1156
1469
  for (const root of rootList) {
1157
- const profilePath = path6.join(root, name, "profile.json");
1158
- if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
1470
+ const profilePath = path7.join(root, name, "profile.json");
1471
+ if (fs7.existsSync(profilePath) && fs7.statSync(profilePath).isFile()) {
1159
1472
  return profilePath;
1160
1473
  }
1161
1474
  }
@@ -1171,7 +1484,7 @@ function getProfileInputs(name, roots = getExecutableRoots()) {
1171
1484
  const profilePath = resolveExecutable(name, roots);
1172
1485
  if (!profilePath) return null;
1173
1486
  try {
1174
- const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
1487
+ const raw = JSON.parse(fs7.readFileSync(profilePath, "utf-8"));
1175
1488
  if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
1176
1489
  return raw.inputs;
1177
1490
  } catch {
@@ -1220,10 +1533,10 @@ function autoDispatch(opts) {
1220
1533
  }
1221
1534
  const eventName = process.env.GITHUB_EVENT_NAME;
1222
1535
  const eventPath = process.env.GITHUB_EVENT_PATH;
1223
- if (!eventName || !eventPath || !fs7.existsSync(eventPath)) return null;
1536
+ if (!eventName || !eventPath || !fs8.existsSync(eventPath)) return null;
1224
1537
  let event = {};
1225
1538
  try {
1226
- event = JSON.parse(fs7.readFileSync(eventPath, "utf-8"));
1539
+ event = JSON.parse(fs8.readFileSync(eventPath, "utf-8"));
1227
1540
  } catch {
1228
1541
  return null;
1229
1542
  }
@@ -1296,7 +1609,7 @@ function autoDispatchTyped(opts) {
1296
1609
  if (legacy) return { kind: "route", ...legacy };
1297
1610
  const eventName = process.env.GITHUB_EVENT_NAME;
1298
1611
  const eventPath = process.env.GITHUB_EVENT_PATH;
1299
- if (!eventName || !eventPath || !fs7.existsSync(eventPath)) {
1612
+ if (!eventName || !eventPath || !fs8.existsSync(eventPath)) {
1300
1613
  return { kind: "silent", reason: "no GHA event context" };
1301
1614
  }
1302
1615
  if (eventName !== "issue_comment") {
@@ -1304,7 +1617,7 @@ function autoDispatchTyped(opts) {
1304
1617
  }
1305
1618
  let event = {};
1306
1619
  try {
1307
- event = JSON.parse(fs7.readFileSync(eventPath, "utf-8"));
1620
+ event = JSON.parse(fs8.readFileSync(eventPath, "utf-8"));
1308
1621
  } catch {
1309
1622
  return { kind: "silent", reason: "GHA event payload unreadable" };
1310
1623
  }
@@ -1338,7 +1651,7 @@ function dispatchScheduledWatches(opts) {
1338
1651
  for (const exe of listExecutables()) {
1339
1652
  let raw;
1340
1653
  try {
1341
- raw = fs7.readFileSync(exe.profilePath, "utf-8");
1654
+ raw = fs8.readFileSync(exe.profilePath, "utf-8");
1342
1655
  } catch {
1343
1656
  continue;
1344
1657
  }
@@ -1595,69 +1908,7 @@ function postPrReviewComment(prNumber, body, cwd) {
1595
1908
  import { execFileSync as execFileSync29, spawn as spawn5 } from "child_process";
1596
1909
  import * as fs30 from "fs";
1597
1910
  import * as path28 from "path";
1598
-
1599
- // src/events.ts
1600
- import * as crypto from "crypto";
1601
- import * as fs8 from "fs";
1602
- import * as path7 from "path";
1603
- var cachedRunId = null;
1604
- function resolveRunId() {
1605
- if (cachedRunId) return cachedRunId;
1606
- if (process.env.KODY_RUN_ID) {
1607
- cachedRunId = process.env.KODY_RUN_ID;
1608
- return cachedRunId;
1609
- }
1610
- if (process.env.GITHUB_RUN_ID) {
1611
- const attempt = process.env.GITHUB_RUN_ATTEMPT ?? "1";
1612
- cachedRunId = `gh-${process.env.GITHUB_RUN_ID}-${attempt}`;
1613
- } else {
1614
- cachedRunId = `${Date.now().toString(36)}-${crypto.randomBytes(4).toString("hex")}`;
1615
- }
1616
- process.env.KODY_RUN_ID = cachedRunId;
1617
- return cachedRunId;
1618
- }
1619
- function emitEvent(cwd, ev) {
1620
- if (process.env.KODY_EVENTS === "0") return;
1621
- try {
1622
- const runId = resolveRunId();
1623
- const fullEvent = {
1624
- ts: (/* @__PURE__ */ new Date()).toISOString(),
1625
- runId,
1626
- ...ev
1627
- };
1628
- const eventsPath = path7.join(cwd, ".kody", "runs", runId, "events.jsonl");
1629
- fs8.mkdirSync(path7.dirname(eventsPath), { recursive: true });
1630
- fs8.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}
1631
- `);
1632
- } catch {
1633
- }
1634
- }
1635
- function readEvents(cwd, runId) {
1636
- const eventsPath = path7.join(cwd, ".kody", "runs", runId, "events.jsonl");
1637
- if (!fs8.existsSync(eventsPath)) return [];
1638
- const lines = fs8.readFileSync(eventsPath, "utf-8").split("\n");
1639
- const out = [];
1640
- for (const line of lines) {
1641
- const trimmed = line.trim();
1642
- if (!trimmed) continue;
1643
- try {
1644
- out.push(JSON.parse(trimmed));
1645
- } catch {
1646
- }
1647
- }
1648
- return out;
1649
- }
1650
- function listRuns(cwd) {
1651
- const runsDir = path7.join(cwd, ".kody", "runs");
1652
- if (!fs8.existsSync(runsDir)) return [];
1653
- return fs8.readdirSync(runsDir).filter((name) => {
1654
- try {
1655
- return fs8.statSync(path7.join(runsDir, name)).isDirectory();
1656
- } catch {
1657
- return false;
1658
- }
1659
- }).sort();
1660
- }
1911
+ init_events();
1661
1912
 
1662
1913
  // src/profile.ts
1663
1914
  import * as fs9 from "fs";
@@ -1792,6 +2043,8 @@ function parseClaudeCode(p, raw) {
1792
2043
  maxTurnTimeoutSec: typeof r.maxTurnTimeoutSec === "number" ? r.maxTurnTimeoutSec : null,
1793
2044
  systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
1794
2045
  cacheable: r.cacheable === true,
2046
+ enableVerifyTool: r.enableVerifyTool === true,
2047
+ verifyAttempts: typeof r.verifyAttempts === "number" && r.verifyAttempts > 0 ? r.verifyAttempts : null,
1795
2048
  tools,
1796
2049
  hooks: Array.isArray(r.hooks) ? r.hooks : [],
1797
2050
  skills: Array.isArray(r.skills) ? r.skills : [],
@@ -2085,7 +2338,7 @@ function errMsg(err) {
2085
2338
  }
2086
2339
 
2087
2340
  // src/litellm.ts
2088
- import { execFileSync as execFileSync4, spawn } from "child_process";
2341
+ import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
2089
2342
  import * as fs10 from "fs";
2090
2343
  import * as os from "os";
2091
2344
  import * as path9 from "path";
@@ -2143,7 +2396,7 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
2143
2396
  const dotenvVars = readDotenvApiKeys(projectDir);
2144
2397
  const logPath = path9.join(os.tmpdir(), `kody-litellm-${Date.now()}.log`);
2145
2398
  const outFd = fs10.openSync(logPath, "w");
2146
- const child = spawn(cmd, args, {
2399
+ const child = spawn2(cmd, args, {
2147
2400
  stdio: ["ignore", outFd, outFd],
2148
2401
  detached: true,
2149
2402
  env: stripBlockingEnv({ ...process.env, ...dotenvVars })
@@ -6744,6 +6997,9 @@ function formatReviewComments(raw) {
6744
6997
  }
6745
6998
  }
6746
6999
 
7000
+ // src/scripts/loadTaskContext.ts
7001
+ init_events();
7002
+
6747
7003
  // src/taskContext.ts
6748
7004
  import * as fs27 from "fs";
6749
7005
  import * as path26 from "path";
@@ -8499,110 +8755,8 @@ function tryPostPr6(prNumber, body, cwd) {
8499
8755
  }
8500
8756
  }
8501
8757
 
8502
- // src/verify.ts
8503
- import { spawn as spawn2 } from "child_process";
8504
- var TAIL_CHARS = 4e3;
8505
- var COMMAND_TIMEOUT_MS = 10 * 60 * 1e3;
8506
- var DEFAULT_TEST_RETRIES = 2;
8507
- function runCommand(command, cwd) {
8508
- return new Promise((resolve4) => {
8509
- const start = Date.now();
8510
- const child = spawn2(command, {
8511
- cwd,
8512
- shell: true,
8513
- env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
8514
- stdio: ["ignore", "pipe", "pipe"]
8515
- });
8516
- const buffers = [];
8517
- let totalSize = 0;
8518
- const collect = (chunk) => {
8519
- buffers.push(chunk);
8520
- totalSize += chunk.length;
8521
- while (totalSize > TAIL_CHARS * 4 && buffers.length > 1) {
8522
- totalSize -= buffers[0].length;
8523
- buffers.shift();
8524
- }
8525
- };
8526
- child.stdout?.on("data", collect);
8527
- child.stderr?.on("data", collect);
8528
- const timer = setTimeout(() => {
8529
- child.kill("SIGTERM");
8530
- setTimeout(() => {
8531
- if (!child.killed) child.kill("SIGKILL");
8532
- }, 5e3);
8533
- }, COMMAND_TIMEOUT_MS);
8534
- child.on("exit", (code) => {
8535
- clearTimeout(timer);
8536
- const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
8537
- resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
8538
- });
8539
- child.on("error", (err) => {
8540
- clearTimeout(timer);
8541
- resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
8542
- });
8543
- });
8544
- }
8545
- async function verifyAll(config, cwd) {
8546
- const commands = [];
8547
- if (config.quality.typecheck) commands.push({ name: "typecheck", cmd: config.quality.typecheck });
8548
- if (config.quality.testUnit) commands.push({ name: "test", cmd: config.quality.testUnit });
8549
- if (config.quality.lint) commands.push({ name: "lint", cmd: config.quality.lint });
8550
- if (config.quality.format) commands.push({ name: "format", cmd: config.quality.format });
8551
- const failed = [];
8552
- const details = {};
8553
- for (const { name, cmd } of commands) {
8554
- const result = await runCommand(cmd, cwd);
8555
- details[name] = result;
8556
- if (result.exitCode !== 0) failed.push(name);
8557
- }
8558
- return { ok: failed.length === 0, failed, details };
8559
- }
8560
- async function applyTestRetries(initial, testCommand, cwd, runner, testRetries = DEFAULT_TEST_RETRIES) {
8561
- if (initial.ok) return { ...initial, recovered: [] };
8562
- const recovered = [];
8563
- const details = { ...initial.details };
8564
- let failed = [...initial.failed];
8565
- if (failed.includes("test") && testCommand && testRetries > 0) {
8566
- for (let attempt = 1; attempt <= testRetries; attempt++) {
8567
- const retry = await runner(testCommand, cwd);
8568
- details[`test (retry ${attempt})`] = retry;
8569
- if (retry.exitCode === 0) {
8570
- failed = failed.filter((f) => f !== "test");
8571
- recovered.push("test");
8572
- break;
8573
- }
8574
- }
8575
- }
8576
- return { ok: failed.length === 0, failed, details, recovered };
8577
- }
8578
- async function verifyAllWithRetry(config, cwd, opts) {
8579
- const initial = await verifyAll(config, cwd);
8580
- return applyTestRetries(initial, config.quality.testUnit, cwd, runCommand, opts?.testRetries);
8581
- }
8582
- var ANSI_RE = /\x1B\[[0-?]*[ -/]*[@-~]/g;
8583
- function stripAnsi(s) {
8584
- return s.replace(ANSI_RE, "");
8585
- }
8586
- function summarizeFailure(result) {
8587
- const lines = [`verify failed: ${result.failed.join(", ")}`];
8588
- for (const name of result.failed) {
8589
- const d = result.details[name];
8590
- if (!d) continue;
8591
- lines.push(`
8592
- --- ${name} (exit ${d.exitCode}, ${(d.durationMs / 1e3).toFixed(1)}s) ---`);
8593
- lines.push(stripAnsi(d.tail));
8594
- for (let attempt = 1; ; attempt++) {
8595
- const retry = result.details[`${name} (retry ${attempt})`];
8596
- if (!retry) break;
8597
- lines.push(`
8598
- --- ${name} (retry ${attempt}: exit ${retry.exitCode}, ${(retry.durationMs / 1e3).toFixed(1)}s) ---`);
8599
- lines.push(stripAnsi(retry.tail));
8600
- }
8601
- }
8602
- return lines.join("\n");
8603
- }
8604
-
8605
8758
  // src/scripts/verify.ts
8759
+ init_verify();
8606
8760
  var verify = async (ctx) => {
8607
8761
  try {
8608
8762
  const result = await verifyAllWithRetry(ctx.config, ctx.cwd);
@@ -9215,21 +9369,21 @@ function firstRequiredFailure(results, tools) {
9215
9369
  }
9216
9370
  return null;
9217
9371
  }
9218
- function verifyOne(tool, cwd) {
9219
- const result = { name: tool.name, present: false, verified: false };
9220
- let present = runShell(tool.install.checkCommand, cwd);
9221
- if (!present && tool.install.installCommand) {
9222
- runShell(tool.install.installCommand, cwd, 12e4);
9223
- present = runShell(tool.install.checkCommand, cwd);
9372
+ function verifyOne(tool2, cwd) {
9373
+ const result = { name: tool2.name, present: false, verified: false };
9374
+ let present = runShell(tool2.install.checkCommand, cwd);
9375
+ if (!present && tool2.install.installCommand) {
9376
+ runShell(tool2.install.installCommand, cwd, 12e4);
9377
+ present = runShell(tool2.install.checkCommand, cwd);
9224
9378
  }
9225
9379
  result.present = present;
9226
9380
  if (!present) {
9227
- result.error = `tool "${tool.name}" not on PATH (check: ${tool.install.checkCommand})`;
9381
+ result.error = `tool "${tool2.name}" not on PATH (check: ${tool2.install.checkCommand})`;
9228
9382
  return result;
9229
9383
  }
9230
- const verified = runShell(tool.verify, cwd);
9384
+ const verified = runShell(tool2.verify, cwd);
9231
9385
  result.verified = verified;
9232
- if (!verified) result.error = `tool "${tool.name}" failed verify: ${tool.verify}`;
9386
+ if (!verified) result.error = `tool "${tool2.name}" failed verify: ${tool2.verify}`;
9233
9387
  return result;
9234
9388
  }
9235
9389
  function runShell(cmd, cwd, timeoutMs = 3e4) {
@@ -9346,6 +9500,10 @@ async function runExecutable(profileName, input) {
9346
9500
  maxTurnTimeoutMs: typeof profile.claudeCode.maxTurnTimeoutSec === "number" ? Math.floor(profile.claudeCode.maxTurnTimeoutSec * 1e3) : void 0,
9347
9501
  systemPromptAppend: profile.claudeCode.systemPromptAppend,
9348
9502
  cacheable: profile.claudeCode.cacheable,
9503
+ enableVerifyTool: profile.claudeCode.enableVerifyTool,
9504
+ verifyToolMaxAttempts: profile.claudeCode.verifyAttempts ?? null,
9505
+ verifyConfig: profile.claudeCode.enableVerifyTool ? config : void 0,
9506
+ executableName: profileName,
9349
9507
  settingSources: profile.claudeCode.settingSources
9350
9508
  });
9351
9509
  };
@@ -10571,6 +10729,7 @@ ${CHAT_HELP}`);
10571
10729
  }
10572
10730
 
10573
10731
  // src/stats.ts
10732
+ init_events();
10574
10733
  function parseStatsArgs(argv) {
10575
10734
  const out = { cwd: process.cwd() };
10576
10735
  for (let i = 0; i < argv.length; i++) {
@@ -26,6 +26,8 @@
26
26
  "maxTurnTimeoutSec": 1200,
27
27
  "systemPromptAppend": null,
28
28
  "cacheable": true,
29
+ "enableVerifyTool": true,
30
+ "verifyAttempts": 4,
29
31
  "tools": [
30
32
  "Read",
31
33
  "Write",
@@ -33,7 +35,8 @@
33
35
  "Bash",
34
36
  "Grep",
35
37
  "Glob",
36
- "mcp__playwright"
38
+ "mcp__playwright",
39
+ "mcp__kody-verify"
37
40
  ],
38
41
  "hooks": ["block-git"],
39
42
  "skills": [],
@@ -32,7 +32,7 @@ If a prior-art block is present above, scan it before editing — those are earl
32
32
  - Read the **full** contents of every file you intend to change.
33
33
  - Read the test file for each of those files, if one exists.
34
34
  - Skipping the floor on the assumption "feedback says exactly what to change" is a hard failure when the change touches code with non-obvious invariants.
35
- 4. **Verify** — run each quality command with Bash. Fix the root cause of any failure you introduced by this round of edits.
35
+ 4. **Verify** — before declaring DONE, call the `verify` tool (mcp__kody-verify__verify). It runs the project's typecheck/lint/test gates and returns `{ ok, failures, attemptsRemaining }`. If `ok: true`, proceed to DONE. If `ok: false`, read the truncated `failures` list, fix what you introduced this round (not pre-existing breakages unrelated to the feedback), and call `verify` again. Bounded by 4 attempts; after that the tool returns `locked: true` and you must wrap up. The postflight verifier still runs after this session as the final ratifier.
36
36
  5. Your FINAL message MUST use this exact format (or a single `FAILED: <reason>` line on failure). The `FEEDBACK_ACTIONS:` block is REQUIRED — omitting it or leaving it empty makes your DONE invalid.
37
37
 
38
38
  ```
@@ -25,13 +25,16 @@
25
25
  "maxTurnTimeoutSec": 1200,
26
26
  "systemPromptAppend": null,
27
27
  "cacheable": true,
28
+ "enableVerifyTool": true,
29
+ "verifyAttempts": 4,
28
30
  "tools": [
29
31
  "Read",
30
32
  "Write",
31
33
  "Edit",
32
34
  "Bash",
33
35
  "Grep",
34
- "Glob"
36
+ "Glob",
37
+ "mcp__kody-verify"
35
38
  ],
36
39
  "hooks": ["block-git"],
37
40
  "skills": [],
@@ -36,7 +36,7 @@ You are Kody, an autonomous engineer. A CI workflow on PR #{{pr.number}} (`{{bra
36
36
 
37
37
  2. **Make the minimum edits to fix the root cause.** Do not bundle unrelated cleanups into a CI fix.
38
38
 
39
- 3. **Re-run the relevant quality command locally with Bash and confirm exit 0.**
39
+ 3. **Confirm green via the `verify` tool** — call `mcp__kody-verify__verify` to run the project's typecheck/lint/test gates. If `ok: false`, read the truncated `failures`, fix the root cause, and call `verify` again. Bounded by 4 attempts; after that the tool returns `locked: true` and you must wrap up with FAILED. The postflight verifier still runs after this session as the final ratifier.
40
40
 
41
41
  4. **Final message format** (or `FAILED: <reason>` on failure):
42
42
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -25,13 +25,16 @@
25
25
  "maxTurnTimeoutSec": 1200,
26
26
  "systemPromptAppend": null,
27
27
  "cacheable": true,
28
+ "enableVerifyTool": true,
29
+ "verifyAttempts": 4,
28
30
  "tools": [
29
31
  "Read",
30
32
  "Write",
31
33
  "Edit",
32
34
  "Bash",
33
35
  "Grep",
34
- "Glob"
36
+ "Glob",
37
+ "mcp__kody-verify"
35
38
  ],
36
39
  "hooks": ["block-git"],
37
40
  "skills": [],
@@ -40,7 +40,7 @@ If a prior-art block is present above, READ THE DIFFS — those are failed or su
40
40
  2. **Plan** — before any Edit/Write, output a short plan (5–10 lines): what files you'll change, the approach, what could go wrong. No fluff.
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
- 5. **Verify** — run each quality command with Bash. On failure, fix the root cause and re-run. When reporting that a command passed, you MUST have just run it and seen exit code 0 in this session — do not paraphrase prior output.
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
44
  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
45
 
46
46
  ```
@@ -177,6 +177,21 @@ export interface ClaudeCodeSpec {
177
177
  * not support `excludeDynamicSections` (forward-compatible).
178
178
  */
179
179
  cacheable?: boolean
180
+ /**
181
+ * Phase 3 opt-in: expose an in-process `verify` MCP tool to the agent
182
+ * so it can iterate on typecheck/lint/test failures inside one SDK
183
+ * session instead of needing a `fix-ci` round trip. The tool is
184
+ * bounded by `verifyAttempts` (default 4). The postflight `verify`
185
+ * script still runs after the agent finishes as the final ratifier.
186
+ * Default false.
187
+ */
188
+ enableVerifyTool?: boolean
189
+ /**
190
+ * Hard cap on verify-tool invocations per agent session when
191
+ * `enableVerifyTool` is true. Default 4 (≈3 fix iterations after the
192
+ * first attempt). Set to 0 or omit to use the default.
193
+ */
194
+ verifyAttempts?: number | null
180
195
  /** SDK built-in tools this executable is allowed to use (capability pack). */
181
196
  tools: string[]
182
197
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.57",
3
+ "version": "0.4.59",
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",
@@ -12,21 +12,10 @@
12
12
  "templates",
13
13
  "kody.config.schema.json"
14
14
  ],
15
- "scripts": {
16
- "kody": "tsx bin/kody.ts",
17
- "build": "tsup && node scripts/copy-assets.cjs",
18
- "test": "vitest run tests/unit tests/int --no-coverage",
19
- "test:e2e": "vitest run tests/e2e --no-coverage",
20
- "test:all": "vitest run tests --no-coverage",
21
- "typecheck": "tsc --noEmit",
22
- "lint": "biome check",
23
- "lint:fix": "biome check --write",
24
- "format": "biome format --write",
25
- "prepublishOnly": "pnpm build"
26
- },
27
15
  "dependencies": {
28
16
  "@actions/cache": "^6.0.0",
29
- "@anthropic-ai/claude-agent-sdk": "0.2.119"
17
+ "@anthropic-ai/claude-agent-sdk": "0.2.119",
18
+ "zod": "^4.0.0"
30
19
  },
31
20
  "devDependencies": {
32
21
  "@biomejs/biome": "^2.4.12",
@@ -44,5 +33,16 @@
44
33
  "url": "git+https://github.com/aharonyaircohen/kody-engine.git"
45
34
  },
46
35
  "homepage": "https://github.com/aharonyaircohen/kody-engine",
47
- "bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
48
- }
36
+ "bugs": "https://github.com/aharonyaircohen/kody-engine/issues",
37
+ "scripts": {
38
+ "kody": "tsx bin/kody.ts",
39
+ "build": "tsup && node scripts/copy-assets.cjs",
40
+ "test": "vitest run tests/unit tests/int --no-coverage",
41
+ "test:e2e": "vitest run tests/e2e --no-coverage",
42
+ "test:all": "vitest run tests --no-coverage",
43
+ "typecheck": "tsc --noEmit",
44
+ "lint": "biome check",
45
+ "lint:fix": "biome check --write",
46
+ "format": "biome format --write"
47
+ }
48
+ }