@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 +395 -236
- package/dist/executables/fix/profile.json +4 -1
- package/dist/executables/fix/prompt.md +1 -1
- package/dist/executables/fix-ci/profile.json +4 -1
- package/dist/executables/fix-ci/prompt.md +1 -1
- package/dist/executables/goal-scheduler/scheduler.sh +0 -0
- package/dist/executables/release-deploy/deploy.sh +0 -0
- package/dist/executables/release-prepare/prepare.sh +0 -0
- package/dist/executables/release-publish/publish.sh +0 -0
- package/dist/executables/resolve/apply-prefer.sh +0 -0
- package/dist/executables/revert/revert.sh +0 -0
- package/dist/executables/run/profile.json +4 -1
- package/dist/executables/run/prompt.md +1 -1
- package/dist/executables/types.ts +15 -0
- package/package.json +16 -16
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.
|
|
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
|
|
123
|
-
import * as
|
|
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 ??
|
|
416
|
-
|
|
417
|
-
const ndjsonPath =
|
|
418
|
-
const fullLog =
|
|
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
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
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
|
|
592
|
-
import * as
|
|
904
|
+
import * as fs5 from "fs";
|
|
905
|
+
import * as path5 from "path";
|
|
593
906
|
function sessionFilePath(cwd, sessionId) {
|
|
594
|
-
return
|
|
907
|
+
return path5.join(cwd, ".kody", "sessions", `${sessionId}.jsonl`);
|
|
595
908
|
}
|
|
596
909
|
function readMeta(file) {
|
|
597
|
-
if (!
|
|
598
|
-
const raw =
|
|
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 (!
|
|
612
|
-
const raw =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
781
|
-
import * as
|
|
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 =
|
|
939
|
-
const eventsRel =
|
|
940
|
-
const paths = [sessionRel, eventsRel].filter((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
|
|
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
|
|
1086
|
-
import * as
|
|
1398
|
+
import * as fs7 from "fs";
|
|
1399
|
+
import * as path7 from "path";
|
|
1087
1400
|
function getExecutablesRoot() {
|
|
1088
|
-
const here =
|
|
1401
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
1089
1402
|
const candidates = [
|
|
1090
|
-
|
|
1403
|
+
path7.join(here, "executables"),
|
|
1091
1404
|
// dev: src/
|
|
1092
|
-
|
|
1405
|
+
path7.join(here, "..", "executables"),
|
|
1093
1406
|
// built: dist/bin → dist/executables
|
|
1094
|
-
|
|
1407
|
+
path7.join(here, "..", "src", "executables")
|
|
1095
1408
|
// fallback
|
|
1096
1409
|
];
|
|
1097
1410
|
for (const c of candidates) {
|
|
1098
|
-
if (
|
|
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
|
|
1416
|
+
return path7.join(process.cwd(), ".kody", "executables");
|
|
1104
1417
|
}
|
|
1105
1418
|
function getBuiltinJobsRoot() {
|
|
1106
|
-
const here =
|
|
1419
|
+
const here = path7.dirname(new URL(import.meta.url).pathname);
|
|
1107
1420
|
const candidates = [
|
|
1108
|
-
|
|
1421
|
+
path7.join(here, "jobs"),
|
|
1109
1422
|
// dev: src/
|
|
1110
|
-
|
|
1423
|
+
path7.join(here, "..", "jobs"),
|
|
1111
1424
|
// built: dist/bin → dist/jobs
|
|
1112
|
-
|
|
1425
|
+
path7.join(here, "..", "src", "jobs")
|
|
1113
1426
|
// fallback
|
|
1114
1427
|
];
|
|
1115
1428
|
for (const c of candidates) {
|
|
1116
|
-
if (
|
|
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 (!
|
|
1434
|
+
if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) return [];
|
|
1122
1435
|
const out = [];
|
|
1123
|
-
for (const ent of
|
|
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:
|
|
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 (!
|
|
1140
|
-
const entries =
|
|
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 =
|
|
1145
|
-
if (
|
|
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 =
|
|
1158
|
-
if (
|
|
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(
|
|
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 || !
|
|
1536
|
+
if (!eventName || !eventPath || !fs8.existsSync(eventPath)) return null;
|
|
1224
1537
|
let event = {};
|
|
1225
1538
|
try {
|
|
1226
|
-
event = JSON.parse(
|
|
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 || !
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
9219
|
-
const result = { name:
|
|
9220
|
-
let present = runShell(
|
|
9221
|
-
if (!present &&
|
|
9222
|
-
runShell(
|
|
9223
|
-
present = runShell(
|
|
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 "${
|
|
9381
|
+
result.error = `tool "${tool2.name}" not on PATH (check: ${tool2.install.checkCommand})`;
|
|
9228
9382
|
return result;
|
|
9229
9383
|
}
|
|
9230
|
-
const verified = runShell(
|
|
9384
|
+
const verified = runShell(tool2.verify, cwd);
|
|
9231
9385
|
result.verified = verified;
|
|
9232
|
-
if (!verified) result.error = `tool "${
|
|
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** —
|
|
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. **
|
|
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** —
|
|
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.
|
|
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
|
+
}
|