@kody-ade/kody-engine 0.4.78 → 0.4.80
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 +410 -101
- 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/worker-scheduler/profile.json +50 -0
- package/dist/executables/worker-tick/profile.json +74 -0
- package/dist/executables/worker-tick/prompt.md +65 -0
- package/dist/executables/worker-tick-scripted/profile.json +69 -0
- package/package.json +20 -19
- package/templates/kody.yml +1 -1
package/dist/bin/kody.js
CHANGED
|
@@ -49,17 +49,17 @@ function emitEvent(cwd, ev) {
|
|
|
49
49
|
runId,
|
|
50
50
|
...ev
|
|
51
51
|
};
|
|
52
|
-
const
|
|
53
|
-
fs3.mkdirSync(path3.dirname(
|
|
54
|
-
fs3.appendFileSync(
|
|
52
|
+
const eventsPath2 = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
|
|
53
|
+
fs3.mkdirSync(path3.dirname(eventsPath2), { recursive: true });
|
|
54
|
+
fs3.appendFileSync(eventsPath2, `${JSON.stringify(fullEvent)}
|
|
55
55
|
`);
|
|
56
56
|
} catch {
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
function readEvents(cwd, runId) {
|
|
60
|
-
const
|
|
61
|
-
if (!fs3.existsSync(
|
|
62
|
-
const lines = fs3.readFileSync(
|
|
60
|
+
const eventsPath2 = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
|
|
61
|
+
if (!fs3.existsSync(eventsPath2)) return [];
|
|
62
|
+
const lines = fs3.readFileSync(eventsPath2, "utf-8").split("\n");
|
|
63
63
|
const out = [];
|
|
64
64
|
for (const line of lines) {
|
|
65
65
|
const trimmed = line.trim();
|
|
@@ -213,7 +213,7 @@ function buildVerifyMcpServer(opts) {
|
|
|
213
213
|
attempts: 0,
|
|
214
214
|
maxAttempts: opts.maxAttempts ?? DEFAULT_MAX_VERIFY_ATTEMPTS
|
|
215
215
|
};
|
|
216
|
-
const
|
|
216
|
+
const runVerify2 = opts.__runVerify ?? verifyAllWithRetry;
|
|
217
217
|
const verifyTool = tool(
|
|
218
218
|
"verify",
|
|
219
219
|
"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.",
|
|
@@ -243,7 +243,7 @@ function buildVerifyMcpServer(opts) {
|
|
|
243
243
|
};
|
|
244
244
|
}
|
|
245
245
|
const startedAt = Date.now();
|
|
246
|
-
const result = await
|
|
246
|
+
const result = await runVerify2(opts.config, opts.cwd);
|
|
247
247
|
const durationMs = Date.now() - startedAt;
|
|
248
248
|
emitEvent(opts.cwd, {
|
|
249
249
|
executable: opts.executable,
|
|
@@ -868,7 +868,7 @@ var init_loadPriorArt = __esm({
|
|
|
868
868
|
// package.json
|
|
869
869
|
var package_default = {
|
|
870
870
|
name: "@kody-ade/kody-engine",
|
|
871
|
-
version: "0.4.
|
|
871
|
+
version: "0.4.80",
|
|
872
872
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
873
873
|
license: "MIT",
|
|
874
874
|
type: "module",
|
|
@@ -923,8 +923,8 @@ var package_default = {
|
|
|
923
923
|
|
|
924
924
|
// src/chat-cli.ts
|
|
925
925
|
import { execFileSync as execFileSync31 } from "child_process";
|
|
926
|
-
import * as
|
|
927
|
-
import * as
|
|
926
|
+
import * as fs36 from "fs";
|
|
927
|
+
import * as path33 from "path";
|
|
928
928
|
|
|
929
929
|
// src/chat/events.ts
|
|
930
930
|
import * as fs from "fs";
|
|
@@ -1699,7 +1699,7 @@ function seedInitialMessage(file, message) {
|
|
|
1699
1699
|
var CHAT_SYSTEM_PROMPT = [
|
|
1700
1700
|
"You are Kody, an AI assistant for the Kody Operations Dashboard. Reply to the",
|
|
1701
1701
|
"user's latest message using the full conversation below as context. Keep replies",
|
|
1702
|
-
"
|
|
1702
|
+
"short and simple. Prefer one-liners and short paragraphs. Use plain terms, not jargon.",
|
|
1703
1703
|
"",
|
|
1704
1704
|
"# Your environment and capabilities",
|
|
1705
1705
|
"You run inside a sandboxed runner with a full clone of the user's repository",
|
|
@@ -2178,8 +2178,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
|
|
|
2178
2178
|
|
|
2179
2179
|
// src/kody-cli.ts
|
|
2180
2180
|
import { execFileSync as execFileSync30 } from "child_process";
|
|
2181
|
-
import * as
|
|
2182
|
-
import * as
|
|
2181
|
+
import * as fs35 from "fs";
|
|
2182
|
+
import * as path32 from "path";
|
|
2183
2183
|
|
|
2184
2184
|
// src/dispatch.ts
|
|
2185
2185
|
import * as fs9 from "fs";
|
|
@@ -2501,8 +2501,8 @@ init_issue();
|
|
|
2501
2501
|
|
|
2502
2502
|
// src/executor.ts
|
|
2503
2503
|
import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
|
|
2504
|
-
import * as
|
|
2505
|
-
import * as
|
|
2504
|
+
import * as fs34 from "fs";
|
|
2505
|
+
import * as path31 from "path";
|
|
2506
2506
|
init_events();
|
|
2507
2507
|
|
|
2508
2508
|
// src/lifecycleLabels.ts
|
|
@@ -2546,7 +2546,7 @@ function prBranchLifecycle(profile, profilePath) {
|
|
|
2546
2546
|
const afterPreflight = cfg.context === "minimal" && cfg.contextExtras.length === 0 ? [{ script: "composePrompt" }] : [...contextBundle, { script: "composePrompt" }];
|
|
2547
2547
|
profile.scripts.preflight = [...before, ...profile.scripts.preflight, ...afterPreflight];
|
|
2548
2548
|
const beforePostflight = [{ script: "parseAgentResult" }];
|
|
2549
|
-
const verifyChain = cfg.verify ? [{ script: "
|
|
2549
|
+
const verifyChain = cfg.verify ? [{ script: "verifyWithRetry" }, { script: "checkCoverageWithRetry" }, { script: "abortUnfinishedGitOps" }] : [];
|
|
2550
2550
|
const tail = [
|
|
2551
2551
|
...verifyChain,
|
|
2552
2552
|
{ script: "commitAndPush" },
|
|
@@ -5586,6 +5586,10 @@ function parseFlatYaml(text) {
|
|
|
5586
5586
|
out.every = value;
|
|
5587
5587
|
} else if (key === "tickScript" && value.length > 0) {
|
|
5588
5588
|
out.tickScript = value;
|
|
5589
|
+
} else if (key === "disabled") {
|
|
5590
|
+
const lower = value.toLowerCase();
|
|
5591
|
+
if (lower === "true") out.disabled = true;
|
|
5592
|
+
else if (lower === "false") out.disabled = false;
|
|
5589
5593
|
}
|
|
5590
5594
|
}
|
|
5591
5595
|
return out;
|
|
@@ -5946,6 +5950,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5946
5950
|
throw new Error("dispatchJobFileTicks: `with.targetExecutable` is required");
|
|
5947
5951
|
}
|
|
5948
5952
|
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
5953
|
+
const scriptedExecutable = String(args?.scriptedExecutable ?? "job-tick-scripted");
|
|
5949
5954
|
const slugArg = String(args?.slugArg ?? "job");
|
|
5950
5955
|
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
5951
5956
|
if (backend.hydrate) {
|
|
@@ -5965,6 +5970,12 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5965
5970
|
const now = Date.now();
|
|
5966
5971
|
for (const slug of slugs) {
|
|
5967
5972
|
const frontmatter = readJobFrontmatter(ctx.cwd, jobsDir, slug);
|
|
5973
|
+
if (frontmatter.disabled === true) {
|
|
5974
|
+
process.stdout.write(`[jobs] \u23ED skip ${slug}: disabled in frontmatter
|
|
5975
|
+
`);
|
|
5976
|
+
results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
|
|
5977
|
+
continue;
|
|
5978
|
+
}
|
|
5968
5979
|
const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
|
|
5969
5980
|
if (decision.skip) {
|
|
5970
5981
|
process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
|
|
@@ -5972,7 +5983,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5972
5983
|
results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
|
|
5973
5984
|
continue;
|
|
5974
5985
|
}
|
|
5975
|
-
const slugTarget = frontmatter.tickScript ?
|
|
5986
|
+
const slugTarget = frontmatter.tickScript ? scriptedExecutable : targetExecutable;
|
|
5976
5987
|
process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
|
|
5977
5988
|
`);
|
|
5978
5989
|
try {
|
|
@@ -7336,10 +7347,10 @@ import * as fs26 from "fs";
|
|
|
7336
7347
|
import * as path24 from "path";
|
|
7337
7348
|
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
|
|
7338
7349
|
var GoalStateError = class extends Error {
|
|
7339
|
-
constructor(
|
|
7340
|
-
super(`Invalid goal state at ${
|
|
7350
|
+
constructor(path34, message) {
|
|
7351
|
+
super(`Invalid goal state at ${path34}:
|
|
7341
7352
|
${message}`);
|
|
7342
|
-
this.path =
|
|
7353
|
+
this.path = path34;
|
|
7343
7354
|
this.name = "GoalStateError";
|
|
7344
7355
|
}
|
|
7345
7356
|
path;
|
|
@@ -9063,8 +9074,168 @@ function buildChildEnv(parent, force) {
|
|
|
9063
9074
|
|
|
9064
9075
|
// src/scripts/brainServe.ts
|
|
9065
9076
|
import { createServer } from "http";
|
|
9077
|
+
import * as fs32 from "fs";
|
|
9078
|
+
import * as path30 from "path";
|
|
9079
|
+
|
|
9080
|
+
// src/scripts/brainTurnLog.ts
|
|
9066
9081
|
import * as fs31 from "fs";
|
|
9067
9082
|
import * as path29 from "path";
|
|
9083
|
+
var live = /* @__PURE__ */ new Map();
|
|
9084
|
+
function eventsPath(dir, chatId) {
|
|
9085
|
+
return path29.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
9086
|
+
}
|
|
9087
|
+
function lastPersistedSeq(dir, chatId) {
|
|
9088
|
+
const p = eventsPath(dir, chatId);
|
|
9089
|
+
if (!fs31.existsSync(p)) return 0;
|
|
9090
|
+
const lines = fs31.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
9091
|
+
if (lines.length === 0) return 0;
|
|
9092
|
+
try {
|
|
9093
|
+
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
9094
|
+
} catch {
|
|
9095
|
+
return 0;
|
|
9096
|
+
}
|
|
9097
|
+
}
|
|
9098
|
+
function readSince(dir, chatId, since) {
|
|
9099
|
+
const p = eventsPath(dir, chatId);
|
|
9100
|
+
if (!fs31.existsSync(p)) return [];
|
|
9101
|
+
const out = [];
|
|
9102
|
+
for (const line of fs31.readFileSync(p, "utf-8").split("\n")) {
|
|
9103
|
+
if (!line) continue;
|
|
9104
|
+
try {
|
|
9105
|
+
const rec = JSON.parse(line);
|
|
9106
|
+
if (rec.seq > since) out.push(rec);
|
|
9107
|
+
} catch {
|
|
9108
|
+
}
|
|
9109
|
+
}
|
|
9110
|
+
return out;
|
|
9111
|
+
}
|
|
9112
|
+
function isTerminal(event) {
|
|
9113
|
+
return event.type === "done" || event.type === "error";
|
|
9114
|
+
}
|
|
9115
|
+
function beginTurn(dir, chatId) {
|
|
9116
|
+
const existing = live.get(chatId);
|
|
9117
|
+
const seqFloor = existing ? existing.seq : lastPersistedSeq(dir, chatId);
|
|
9118
|
+
const turn = (existing?.turn ?? 0) + 1;
|
|
9119
|
+
const state = {
|
|
9120
|
+
seq: seqFloor,
|
|
9121
|
+
turn,
|
|
9122
|
+
status: "running",
|
|
9123
|
+
terminal: null,
|
|
9124
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
9125
|
+
};
|
|
9126
|
+
live.set(chatId, state);
|
|
9127
|
+
const p = eventsPath(dir, chatId);
|
|
9128
|
+
fs31.mkdirSync(path29.dirname(p), { recursive: true });
|
|
9129
|
+
return (event) => {
|
|
9130
|
+
state.seq += 1;
|
|
9131
|
+
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
9132
|
+
try {
|
|
9133
|
+
fs31.appendFileSync(p, JSON.stringify(rec) + "\n");
|
|
9134
|
+
} catch (err) {
|
|
9135
|
+
process.stderr.write(
|
|
9136
|
+
`[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
|
|
9137
|
+
`
|
|
9138
|
+
);
|
|
9139
|
+
}
|
|
9140
|
+
for (const fn of state.subscribers) {
|
|
9141
|
+
try {
|
|
9142
|
+
fn(rec);
|
|
9143
|
+
} catch {
|
|
9144
|
+
}
|
|
9145
|
+
}
|
|
9146
|
+
if (isTerminal(event)) {
|
|
9147
|
+
state.status = "ended";
|
|
9148
|
+
state.terminal = rec;
|
|
9149
|
+
const subs = [...state.subscribers];
|
|
9150
|
+
state.subscribers.clear();
|
|
9151
|
+
for (const fn of subs) {
|
|
9152
|
+
try {
|
|
9153
|
+
fn(null);
|
|
9154
|
+
} catch {
|
|
9155
|
+
}
|
|
9156
|
+
}
|
|
9157
|
+
}
|
|
9158
|
+
};
|
|
9159
|
+
}
|
|
9160
|
+
function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
9161
|
+
const state = live.get(chatId);
|
|
9162
|
+
if (!state || state.status === "ended") return;
|
|
9163
|
+
state.seq += 1;
|
|
9164
|
+
const rec = {
|
|
9165
|
+
seq: state.seq,
|
|
9166
|
+
turn: state.turn,
|
|
9167
|
+
ts: Date.now(),
|
|
9168
|
+
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
9169
|
+
};
|
|
9170
|
+
try {
|
|
9171
|
+
fs31.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
|
|
9172
|
+
} catch {
|
|
9173
|
+
}
|
|
9174
|
+
state.status = "ended";
|
|
9175
|
+
state.terminal = rec;
|
|
9176
|
+
const subs = [...state.subscribers];
|
|
9177
|
+
state.subscribers.clear();
|
|
9178
|
+
for (const fn of subs) {
|
|
9179
|
+
try {
|
|
9180
|
+
fn(rec);
|
|
9181
|
+
fn(null);
|
|
9182
|
+
} catch {
|
|
9183
|
+
}
|
|
9184
|
+
}
|
|
9185
|
+
}
|
|
9186
|
+
function subscribe(dir, chatId, since, onRecord, onClose) {
|
|
9187
|
+
const backlog = readSince(dir, chatId, since);
|
|
9188
|
+
for (const rec of backlog) onRecord(rec);
|
|
9189
|
+
const lastReplayed = backlog.length ? backlog[backlog.length - 1] : null;
|
|
9190
|
+
if (lastReplayed && isTerminal(lastReplayed.event)) {
|
|
9191
|
+
onClose();
|
|
9192
|
+
return () => {
|
|
9193
|
+
};
|
|
9194
|
+
}
|
|
9195
|
+
const state = live.get(chatId);
|
|
9196
|
+
if (state && state.status === "running") {
|
|
9197
|
+
const fn = (rec) => {
|
|
9198
|
+
if (rec === null) {
|
|
9199
|
+
state.subscribers.delete(fn);
|
|
9200
|
+
onClose();
|
|
9201
|
+
return;
|
|
9202
|
+
}
|
|
9203
|
+
if (rec.seq > since) onRecord(rec);
|
|
9204
|
+
};
|
|
9205
|
+
state.subscribers.add(fn);
|
|
9206
|
+
return () => {
|
|
9207
|
+
state.subscribers.delete(fn);
|
|
9208
|
+
};
|
|
9209
|
+
}
|
|
9210
|
+
if (state && state.status === "ended" && state.terminal) {
|
|
9211
|
+
if (state.terminal.seq > since && !lastReplayed) onRecord(state.terminal);
|
|
9212
|
+
onClose();
|
|
9213
|
+
return () => {
|
|
9214
|
+
};
|
|
9215
|
+
}
|
|
9216
|
+
if (lastReplayed) {
|
|
9217
|
+
onRecord({
|
|
9218
|
+
seq: lastReplayed.seq + 1,
|
|
9219
|
+
turn: lastReplayed.turn,
|
|
9220
|
+
ts: Date.now(),
|
|
9221
|
+
event: {
|
|
9222
|
+
type: "error",
|
|
9223
|
+
error: "stream interrupted (server restarted mid-reply) \u2014 resend your message",
|
|
9224
|
+
chatId
|
|
9225
|
+
}
|
|
9226
|
+
});
|
|
9227
|
+
}
|
|
9228
|
+
onClose();
|
|
9229
|
+
return () => {
|
|
9230
|
+
};
|
|
9231
|
+
}
|
|
9232
|
+
function getLastSeq(dir, chatId) {
|
|
9233
|
+
const state = live.get(chatId);
|
|
9234
|
+
if (state) return state.seq;
|
|
9235
|
+
return lastPersistedSeq(dir, chatId);
|
|
9236
|
+
}
|
|
9237
|
+
|
|
9238
|
+
// src/scripts/brainServe.ts
|
|
9068
9239
|
var DEFAULT_PORT = 8080;
|
|
9069
9240
|
function getApiKey() {
|
|
9070
9241
|
const key = (process.env.BRAIN_API_KEY ?? "").trim();
|
|
@@ -9120,47 +9291,86 @@ function emitSse(res, event) {
|
|
|
9120
9291
|
|
|
9121
9292
|
`);
|
|
9122
9293
|
}
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9294
|
+
function translateChatEvent(event, chatId) {
|
|
9295
|
+
switch (event.event) {
|
|
9296
|
+
case "chat.message": {
|
|
9297
|
+
const content = String(event.payload.content ?? "");
|
|
9298
|
+
if (content.length === 0) return null;
|
|
9299
|
+
return { type: "text", text: content, chatId };
|
|
9300
|
+
}
|
|
9301
|
+
case "chat.tool": {
|
|
9302
|
+
if (event.payload.phase !== "use") return null;
|
|
9303
|
+
return {
|
|
9304
|
+
type: "tool_use",
|
|
9305
|
+
name: typeof event.payload.name === "string" ? event.payload.name : "tool",
|
|
9306
|
+
input: event.payload.input ?? {},
|
|
9307
|
+
chatId
|
|
9308
|
+
};
|
|
9309
|
+
}
|
|
9310
|
+
case "chat.done":
|
|
9311
|
+
return { type: "done", chatId };
|
|
9312
|
+
case "chat.error":
|
|
9313
|
+
return {
|
|
9314
|
+
type: "error",
|
|
9315
|
+
error: typeof event.payload.error === "string" ? event.payload.error : "agent error",
|
|
9316
|
+
chatId
|
|
9317
|
+
};
|
|
9318
|
+
default:
|
|
9319
|
+
return null;
|
|
9320
|
+
}
|
|
9321
|
+
}
|
|
9322
|
+
var BrokerSink = class {
|
|
9323
|
+
constructor(emitToLog, chatId) {
|
|
9324
|
+
this.emitToLog = emitToLog;
|
|
9126
9325
|
this.chatId = chatId;
|
|
9127
9326
|
}
|
|
9128
|
-
|
|
9327
|
+
emitToLog;
|
|
9129
9328
|
chatId;
|
|
9130
9329
|
async emit(event) {
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9330
|
+
const be = translateChatEvent(event, this.chatId);
|
|
9331
|
+
if (be) this.emitToLog(be);
|
|
9332
|
+
}
|
|
9333
|
+
};
|
|
9334
|
+
var chatQueues = /* @__PURE__ */ new Map();
|
|
9335
|
+
function enqueue(chatId, fn) {
|
|
9336
|
+
const prev = chatQueues.get(chatId) ?? Promise.resolve();
|
|
9337
|
+
const next = prev.catch(() => {
|
|
9338
|
+
}).then(fn);
|
|
9339
|
+
chatQueues.set(
|
|
9340
|
+
chatId,
|
|
9341
|
+
next.finally(() => {
|
|
9342
|
+
if (chatQueues.get(chatId) === next) chatQueues.delete(chatId);
|
|
9343
|
+
})
|
|
9344
|
+
);
|
|
9345
|
+
return next;
|
|
9346
|
+
}
|
|
9347
|
+
function streamToRes(res, dir, chatId, since) {
|
|
9348
|
+
writeSseHeaders(res);
|
|
9349
|
+
emitSse(res, { type: "chat", chatId });
|
|
9350
|
+
let maxSent = since;
|
|
9351
|
+
const unsubscribe = subscribe(
|
|
9352
|
+
dir,
|
|
9353
|
+
chatId,
|
|
9354
|
+
since,
|
|
9355
|
+
(rec) => {
|
|
9356
|
+
if (rec.seq <= maxSent) return;
|
|
9357
|
+
maxSent = rec.seq;
|
|
9358
|
+
if (res.writableEnded) return;
|
|
9359
|
+
res.write(`data: ${JSON.stringify({ ...rec.event, seq: rec.seq })}
|
|
9360
|
+
|
|
9361
|
+
`);
|
|
9362
|
+
},
|
|
9363
|
+
() => {
|
|
9364
|
+
if (!res.writableEnded) {
|
|
9365
|
+
try {
|
|
9366
|
+
res.end();
|
|
9367
|
+
} catch {
|
|
9136
9368
|
}
|
|
9137
|
-
return;
|
|
9138
9369
|
}
|
|
9139
|
-
case "chat.tool": {
|
|
9140
|
-
if (event.payload.phase !== "use") return;
|
|
9141
|
-
emitSse(this.res, {
|
|
9142
|
-
type: "tool_use",
|
|
9143
|
-
name: typeof event.payload.name === "string" ? event.payload.name : "tool",
|
|
9144
|
-
input: event.payload.input ?? {},
|
|
9145
|
-
chatId: this.chatId
|
|
9146
|
-
});
|
|
9147
|
-
return;
|
|
9148
|
-
}
|
|
9149
|
-
case "chat.done": {
|
|
9150
|
-
emitSse(this.res, { type: "done", chatId: this.chatId });
|
|
9151
|
-
return;
|
|
9152
|
-
}
|
|
9153
|
-
case "chat.error": {
|
|
9154
|
-
const errMsg2 = typeof event.payload.error === "string" ? event.payload.error : "agent error";
|
|
9155
|
-
emitSse(this.res, { type: "error", error: errMsg2, chatId: this.chatId });
|
|
9156
|
-
return;
|
|
9157
|
-
}
|
|
9158
|
-
// chat.thinking / chat.ready / chat.exit — not part of the Brain protocol.
|
|
9159
|
-
default:
|
|
9160
|
-
return;
|
|
9161
9370
|
}
|
|
9162
|
-
|
|
9163
|
-
|
|
9371
|
+
);
|
|
9372
|
+
res.on("close", unsubscribe);
|
|
9373
|
+
}
|
|
9164
9374
|
async function handleChatTurn(req, res, chatId, opts) {
|
|
9165
9375
|
let body;
|
|
9166
9376
|
try {
|
|
@@ -9175,38 +9385,34 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
9175
9385
|
return;
|
|
9176
9386
|
}
|
|
9177
9387
|
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
9178
|
-
|
|
9388
|
+
fs32.mkdirSync(path30.dirname(sessionFile), { recursive: true });
|
|
9179
9389
|
appendTurn(sessionFile, {
|
|
9180
9390
|
role: "user",
|
|
9181
9391
|
content: message,
|
|
9182
9392
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9183
9393
|
});
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
const sink = new
|
|
9187
|
-
|
|
9188
|
-
|
|
9394
|
+
const sinceFloor = getLastSeq(opts.cwd, chatId);
|
|
9395
|
+
const emitToLog = beginTurn(opts.cwd, chatId);
|
|
9396
|
+
const sink = new BrokerSink(emitToLog, chatId);
|
|
9397
|
+
void enqueue(
|
|
9398
|
+
chatId,
|
|
9399
|
+
() => opts.runTurn({
|
|
9189
9400
|
sessionId: chatId,
|
|
9190
9401
|
sessionFile,
|
|
9191
9402
|
cwd: opts.cwd,
|
|
9192
9403
|
model: opts.model,
|
|
9193
9404
|
litellmUrl: opts.litellmUrl,
|
|
9194
9405
|
sink
|
|
9195
|
-
})
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
|
|
9406
|
+
}).catch((err) => {
|
|
9407
|
+
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
9408
|
+
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
|
|
9199
9409
|
`);
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
}
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
res.end();
|
|
9207
|
-
} catch {
|
|
9208
|
-
}
|
|
9209
|
-
}
|
|
9410
|
+
endTurnIfUnterminated(opts.cwd, chatId, errMsg2);
|
|
9411
|
+
}).finally(() => {
|
|
9412
|
+
endTurnIfUnterminated(opts.cwd, chatId, "turn ended without a result");
|
|
9413
|
+
})
|
|
9414
|
+
);
|
|
9415
|
+
streamToRes(res, opts.cwd, chatId, sinceFloor);
|
|
9210
9416
|
}
|
|
9211
9417
|
function buildServer(opts) {
|
|
9212
9418
|
const runTurn = opts.runTurn ?? runChatTurn;
|
|
@@ -9239,6 +9445,18 @@ function buildServer(opts) {
|
|
|
9239
9445
|
});
|
|
9240
9446
|
return;
|
|
9241
9447
|
}
|
|
9448
|
+
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
9449
|
+
if (req.method === "GET" && sm) {
|
|
9450
|
+
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
9451
|
+
if (!chatId) {
|
|
9452
|
+
sendJson(res, 400, { error: "chatId required" });
|
|
9453
|
+
return;
|
|
9454
|
+
}
|
|
9455
|
+
const sinceRaw = url.searchParams.get("since");
|
|
9456
|
+
const since = Number.isFinite(Number(sinceRaw)) ? Number(sinceRaw) : 0;
|
|
9457
|
+
streamToRes(res, opts.cwd, chatId, since);
|
|
9458
|
+
return;
|
|
9459
|
+
}
|
|
9242
9460
|
sendJson(res, 404, { error: "not found" });
|
|
9243
9461
|
});
|
|
9244
9462
|
}
|
|
@@ -9796,6 +10014,96 @@ function downgrade2(ctx, reason) {
|
|
|
9796
10014
|
ctx.data.agentDone = false;
|
|
9797
10015
|
}
|
|
9798
10016
|
|
|
10017
|
+
// src/scripts/verifyWithRetry.ts
|
|
10018
|
+
init_prompt();
|
|
10019
|
+
init_verify();
|
|
10020
|
+
async function runVerify(ctx) {
|
|
10021
|
+
try {
|
|
10022
|
+
const result = await verifyAllWithRetry(ctx.config, ctx.cwd);
|
|
10023
|
+
ctx.data.verifyOk = result.ok;
|
|
10024
|
+
ctx.data.verifyReason = result.ok ? "" : summarizeFailure(result);
|
|
10025
|
+
ctx.data.verifyRecovered = result.recovered ?? [];
|
|
10026
|
+
if (result.recovered && result.recovered.length > 0) {
|
|
10027
|
+
process.stderr.write(
|
|
10028
|
+
`[kody verify] caught flake on: ${result.recovered.join(", ")} (passed on retry)
|
|
10029
|
+
`
|
|
10030
|
+
);
|
|
10031
|
+
}
|
|
10032
|
+
} catch (err) {
|
|
10033
|
+
ctx.data.verifyOk = false;
|
|
10034
|
+
ctx.data.verifyReason = `verify crashed: ${err instanceof Error ? err.message : String(err)}`;
|
|
10035
|
+
}
|
|
10036
|
+
}
|
|
10037
|
+
function downgradeActionOnFailure(ctx) {
|
|
10038
|
+
if (ctx.data.verifyOk !== false) return;
|
|
10039
|
+
const action = ctx.data.action;
|
|
10040
|
+
if (!action || !action.type.endsWith("_COMPLETED")) return;
|
|
10041
|
+
const reason = ctx.data.verifyReason || "verify failed";
|
|
10042
|
+
ctx.data.action = {
|
|
10043
|
+
type: action.type.replace(/_COMPLETED$/, "_FAILED"),
|
|
10044
|
+
payload: { reason, downgradedFrom: action.type },
|
|
10045
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10046
|
+
};
|
|
10047
|
+
}
|
|
10048
|
+
function upgradeActionOnPass(ctx) {
|
|
10049
|
+
if (ctx.data.verifyOk !== true) return;
|
|
10050
|
+
const action = ctx.data.action;
|
|
10051
|
+
if (!action || !action.type.endsWith("_FAILED")) return;
|
|
10052
|
+
const downgradedFrom = action.payload?.downgradedFrom;
|
|
10053
|
+
if (!downgradedFrom || !downgradedFrom.endsWith("_COMPLETED")) return;
|
|
10054
|
+
ctx.data.action = {
|
|
10055
|
+
type: downgradedFrom,
|
|
10056
|
+
payload: {},
|
|
10057
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
10058
|
+
};
|
|
10059
|
+
}
|
|
10060
|
+
var verifyWithRetry = async (ctx) => {
|
|
10061
|
+
await runVerify(ctx);
|
|
10062
|
+
if (ctx.data.verifyOk !== false) return;
|
|
10063
|
+
if (!ctx.data.agentDone) {
|
|
10064
|
+
downgradeActionOnFailure(ctx);
|
|
10065
|
+
return;
|
|
10066
|
+
}
|
|
10067
|
+
const invoker = ctx.data.__invokeAgent;
|
|
10068
|
+
const basePrompt = ctx.data.prompt;
|
|
10069
|
+
if (!invoker || !basePrompt) {
|
|
10070
|
+
downgradeActionOnFailure(ctx);
|
|
10071
|
+
return;
|
|
10072
|
+
}
|
|
10073
|
+
const reason = ctx.data.verifyReason || "verify failed";
|
|
10074
|
+
process.stderr.write(`[kody] verify failed; retrying agent once with verify output as feedback
|
|
10075
|
+
`);
|
|
10076
|
+
const retryPrompt = [
|
|
10077
|
+
basePrompt,
|
|
10078
|
+
"",
|
|
10079
|
+
"# Verify failure (retry)",
|
|
10080
|
+
"",
|
|
10081
|
+
"The quality gate failed after your previous attempt. Read the output below,",
|
|
10082
|
+
"fix the underlying issue (do NOT relax the gate or skip tests), then re-emit",
|
|
10083
|
+
"your result. You have one retry \u2014 make it count.",
|
|
10084
|
+
"",
|
|
10085
|
+
reason
|
|
10086
|
+
].join("\n");
|
|
10087
|
+
try {
|
|
10088
|
+
const retry = await invoker(retryPrompt);
|
|
10089
|
+
const parsed = parseAgentResult(retry.finalText);
|
|
10090
|
+
if (retry.outcome === "completed" && parsed.done) {
|
|
10091
|
+
ctx.data.agentDone = true;
|
|
10092
|
+
ctx.data.commitMessage = parsed.commitMessage || ctx.data.commitMessage;
|
|
10093
|
+
ctx.data.prSummary = parsed.prSummary || ctx.data.prSummary;
|
|
10094
|
+
}
|
|
10095
|
+
} catch (err) {
|
|
10096
|
+
process.stderr.write(`[kody] verify retry crashed: ${err instanceof Error ? err.message : String(err)}
|
|
10097
|
+
`);
|
|
10098
|
+
}
|
|
10099
|
+
await runVerify(ctx);
|
|
10100
|
+
if (ctx.data.verifyOk === true) {
|
|
10101
|
+
upgradeActionOnPass(ctx);
|
|
10102
|
+
} else {
|
|
10103
|
+
downgradeActionOnFailure(ctx);
|
|
10104
|
+
}
|
|
10105
|
+
};
|
|
10106
|
+
|
|
9799
10107
|
// src/scripts/waitForCi.ts
|
|
9800
10108
|
init_issue();
|
|
9801
10109
|
import { execFileSync as execFileSync27 } from "child_process";
|
|
@@ -10134,7 +10442,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
|
10134
10442
|
};
|
|
10135
10443
|
|
|
10136
10444
|
// src/scripts/writeRunSummary.ts
|
|
10137
|
-
import * as
|
|
10445
|
+
import * as fs33 from "fs";
|
|
10138
10446
|
var writeRunSummary = async (ctx, profile) => {
|
|
10139
10447
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
10140
10448
|
if (!summaryPath) return;
|
|
@@ -10156,7 +10464,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
10156
10464
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
10157
10465
|
lines.push("");
|
|
10158
10466
|
try {
|
|
10159
|
-
|
|
10467
|
+
fs33.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
10160
10468
|
`);
|
|
10161
10469
|
} catch {
|
|
10162
10470
|
}
|
|
@@ -10216,6 +10524,7 @@ var postflightScripts = {
|
|
|
10216
10524
|
requireFeedbackActions,
|
|
10217
10525
|
requirePlanDeviations,
|
|
10218
10526
|
verify,
|
|
10527
|
+
verifyWithRetry,
|
|
10219
10528
|
verifyReproFails,
|
|
10220
10529
|
checkCoverageWithRetry,
|
|
10221
10530
|
abortUnfinishedGitOps: abortUnfinishedGitOps2,
|
|
@@ -10379,9 +10688,9 @@ async function runExecutable(profileName, input) {
|
|
|
10379
10688
|
data: { ...input.preloadedData ?? {} },
|
|
10380
10689
|
output: { exitCode: 0 }
|
|
10381
10690
|
};
|
|
10382
|
-
const ndjsonDir =
|
|
10691
|
+
const ndjsonDir = path31.join(input.cwd, ".kody");
|
|
10383
10692
|
const invokeAgent = async (prompt) => {
|
|
10384
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
10693
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path31.isAbsolute(p) ? p : path31.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
10385
10694
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
10386
10695
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
10387
10696
|
return runAgent({
|
|
@@ -10576,7 +10885,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
|
|
|
10576
10885
|
function getProfileInputsForChild(profileName, _cwd) {
|
|
10577
10886
|
try {
|
|
10578
10887
|
const profilePath = resolveProfilePath(profileName);
|
|
10579
|
-
if (!
|
|
10888
|
+
if (!fs34.existsSync(profilePath)) return null;
|
|
10580
10889
|
return loadProfile(profilePath).inputs;
|
|
10581
10890
|
} catch {
|
|
10582
10891
|
return null;
|
|
@@ -10585,17 +10894,17 @@ function getProfileInputsForChild(profileName, _cwd) {
|
|
|
10585
10894
|
function resolveProfilePath(profileName) {
|
|
10586
10895
|
const found = resolveExecutable(profileName);
|
|
10587
10896
|
if (found) return found;
|
|
10588
|
-
const here =
|
|
10897
|
+
const here = path31.dirname(new URL(import.meta.url).pathname);
|
|
10589
10898
|
const candidates = [
|
|
10590
|
-
|
|
10899
|
+
path31.join(here, "executables", profileName, "profile.json"),
|
|
10591
10900
|
// same-dir sibling (dev)
|
|
10592
|
-
|
|
10901
|
+
path31.join(here, "..", "executables", profileName, "profile.json"),
|
|
10593
10902
|
// up one (prod: dist/bin → dist/executables)
|
|
10594
|
-
|
|
10903
|
+
path31.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
10595
10904
|
// fallback
|
|
10596
10905
|
];
|
|
10597
10906
|
for (const c of candidates) {
|
|
10598
|
-
if (
|
|
10907
|
+
if (fs34.existsSync(c)) return c;
|
|
10599
10908
|
}
|
|
10600
10909
|
return candidates[0];
|
|
10601
10910
|
}
|
|
@@ -10695,8 +11004,8 @@ function resolveShellTimeoutMs(entry) {
|
|
|
10695
11004
|
var SIGKILL_GRACE_MS = 5e3;
|
|
10696
11005
|
async function runShellEntry(entry, ctx, profile) {
|
|
10697
11006
|
const shellName = entry.shell;
|
|
10698
|
-
const shellPath =
|
|
10699
|
-
if (!
|
|
11007
|
+
const shellPath = path31.join(profile.dir, shellName);
|
|
11008
|
+
if (!fs34.existsSync(shellPath)) {
|
|
10700
11009
|
ctx.skipAgent = true;
|
|
10701
11010
|
ctx.output.exitCode = 99;
|
|
10702
11011
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -11175,9 +11484,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
11175
11484
|
return token;
|
|
11176
11485
|
}
|
|
11177
11486
|
function detectPackageManager2(cwd) {
|
|
11178
|
-
if (
|
|
11179
|
-
if (
|
|
11180
|
-
if (
|
|
11487
|
+
if (fs35.existsSync(path32.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
11488
|
+
if (fs35.existsSync(path32.join(cwd, "yarn.lock"))) return "yarn";
|
|
11489
|
+
if (fs35.existsSync(path32.join(cwd, "bun.lockb"))) return "bun";
|
|
11181
11490
|
return "npm";
|
|
11182
11491
|
}
|
|
11183
11492
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -11264,11 +11573,11 @@ function configureGitIdentity(cwd) {
|
|
|
11264
11573
|
}
|
|
11265
11574
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
11266
11575
|
if (!issueNumber) return;
|
|
11267
|
-
const logPath =
|
|
11576
|
+
const logPath = path32.join(cwd, ".kody", "last-run.jsonl");
|
|
11268
11577
|
let tail = "";
|
|
11269
11578
|
try {
|
|
11270
|
-
if (
|
|
11271
|
-
const content =
|
|
11579
|
+
if (fs35.existsSync(logPath)) {
|
|
11580
|
+
const content = fs35.readFileSync(logPath, "utf-8");
|
|
11272
11581
|
tail = content.slice(-3e3);
|
|
11273
11582
|
}
|
|
11274
11583
|
} catch {
|
|
@@ -11293,7 +11602,7 @@ async function runCi(argv) {
|
|
|
11293
11602
|
return 0;
|
|
11294
11603
|
}
|
|
11295
11604
|
const args = parseCiArgs(argv);
|
|
11296
|
-
const cwd = args.cwd ?
|
|
11605
|
+
const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
|
|
11297
11606
|
let earlyConfig;
|
|
11298
11607
|
try {
|
|
11299
11608
|
earlyConfig = loadConfig(cwd);
|
|
@@ -11303,9 +11612,9 @@ async function runCi(argv) {
|
|
|
11303
11612
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
11304
11613
|
const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
|
|
11305
11614
|
let manualWorkflowDispatch = false;
|
|
11306
|
-
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath &&
|
|
11615
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs35.existsSync(dispatchEventPath)) {
|
|
11307
11616
|
try {
|
|
11308
|
-
const evt = JSON.parse(
|
|
11617
|
+
const evt = JSON.parse(fs35.readFileSync(dispatchEventPath, "utf-8"));
|
|
11309
11618
|
const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
|
|
11310
11619
|
const sessionInput = String(evt?.inputs?.sessionId ?? "");
|
|
11311
11620
|
manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
|
|
@@ -11564,9 +11873,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
11564
11873
|
return result;
|
|
11565
11874
|
}
|
|
11566
11875
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
11567
|
-
const sessionFile =
|
|
11568
|
-
const eventsFile =
|
|
11569
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
11876
|
+
const sessionFile = path33.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
11877
|
+
const eventsFile = path33.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
11878
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs36.existsSync(path33.join(cwd, p)));
|
|
11570
11879
|
if (paths.length === 0) return;
|
|
11571
11880
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
11572
11881
|
try {
|
|
@@ -11604,7 +11913,7 @@ async function runChat(argv) {
|
|
|
11604
11913
|
${CHAT_HELP}`);
|
|
11605
11914
|
return 64;
|
|
11606
11915
|
}
|
|
11607
|
-
const cwd = args.cwd ?
|
|
11916
|
+
const cwd = args.cwd ? path33.resolve(args.cwd) : process.cwd();
|
|
11608
11917
|
const sessionId = args.sessionId;
|
|
11609
11918
|
const unpackedSecrets = unpackAllSecrets();
|
|
11610
11919
|
if (unpackedSecrets > 0) {
|
|
@@ -11656,7 +11965,7 @@ ${CHAT_HELP}`);
|
|
|
11656
11965
|
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
11657
11966
|
const meta = readMeta(sessionFile);
|
|
11658
11967
|
process.stdout.write(
|
|
11659
|
-
`\u2192 kody:chat: session file=${sessionFile} exists=${
|
|
11968
|
+
`\u2192 kody:chat: session file=${sessionFile} exists=${fs36.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
|
|
11660
11969
|
`
|
|
11661
11970
|
);
|
|
11662
11971
|
try {
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worker-scheduler",
|
|
3
|
+
"role": "watch",
|
|
4
|
+
"describe": "Scheduled: for every worker file under .kody/workers/, invoke worker-tick once. No agent on the scheduler itself. Parallel to job-scheduler.",
|
|
5
|
+
"kind": "scheduled",
|
|
6
|
+
"schedule": "*/5 * * * *",
|
|
7
|
+
"inputs": [],
|
|
8
|
+
"claudeCode": {
|
|
9
|
+
"model": "inherit",
|
|
10
|
+
"permissionMode": "default",
|
|
11
|
+
"maxTurns": null,
|
|
12
|
+
"maxThinkingTokens": null,
|
|
13
|
+
"systemPromptAppend": null,
|
|
14
|
+
"tools": [],
|
|
15
|
+
"hooks": [],
|
|
16
|
+
"skills": [],
|
|
17
|
+
"commands": [],
|
|
18
|
+
"subagents": [],
|
|
19
|
+
"plugins": [],
|
|
20
|
+
"mcpServers": []
|
|
21
|
+
},
|
|
22
|
+
"cliTools": [
|
|
23
|
+
{
|
|
24
|
+
"name": "gh",
|
|
25
|
+
"install": {
|
|
26
|
+
"required": true,
|
|
27
|
+
"checkCommand": "command -v gh"
|
|
28
|
+
},
|
|
29
|
+
"verify": "gh auth status",
|
|
30
|
+
"usage": "",
|
|
31
|
+
"allowedUses": ["api", "issue", "pr"]
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"inputArtifacts": [],
|
|
35
|
+
"outputArtifacts": [],
|
|
36
|
+
"scripts": {
|
|
37
|
+
"preflight": [
|
|
38
|
+
{
|
|
39
|
+
"script": "dispatchJobFileTicks",
|
|
40
|
+
"with": {
|
|
41
|
+
"jobsDir": ".kody/workers",
|
|
42
|
+
"targetExecutable": "worker-tick",
|
|
43
|
+
"scriptedExecutable": "worker-tick-scripted",
|
|
44
|
+
"slugArg": "job"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
"postflight": []
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worker-tick",
|
|
3
|
+
"role": "primitive",
|
|
4
|
+
"describe": "One classifier tick for one worker file: read intent + state, decide and execute via gh, emit next state. Parallel to job-tick but reads .kody/workers/.",
|
|
5
|
+
"kind": "oneshot",
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"name": "job",
|
|
9
|
+
"flag": "--job",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"describe": "Worker slug — basename (without .md) of the file under .kody/workers/."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "force",
|
|
16
|
+
"flag": "--force",
|
|
17
|
+
"type": "bool",
|
|
18
|
+
"describe": "When true, the agent ignores the worker body's cadence guard and executes the work this tick. All other body rules (allowed commands, restrictions, state schema) still apply. Used for manual triggers from the dashboard's 'Run now' button."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"claudeCode": {
|
|
22
|
+
"model": "inherit",
|
|
23
|
+
"permissionMode": "default",
|
|
24
|
+
"maxTurns": 20,
|
|
25
|
+
"maxThinkingTokens": null,
|
|
26
|
+
"systemPromptAppend": null,
|
|
27
|
+
"tools": ["Bash", "Read"],
|
|
28
|
+
"hooks": [],
|
|
29
|
+
"skills": [],
|
|
30
|
+
"commands": [],
|
|
31
|
+
"subagents": [],
|
|
32
|
+
"plugins": [],
|
|
33
|
+
"mcpServers": []
|
|
34
|
+
},
|
|
35
|
+
"cliTools": [
|
|
36
|
+
{
|
|
37
|
+
"name": "gh",
|
|
38
|
+
"install": {
|
|
39
|
+
"required": true,
|
|
40
|
+
"checkCommand": "command -v gh"
|
|
41
|
+
},
|
|
42
|
+
"verify": "gh auth status",
|
|
43
|
+
"usage": "Use `gh` for all GitHub actions: `gh pr list ...` to enumerate candidate PRs, `gh pr comment <n> --body \"...\"` to issue a Kody command, `gh pr view <n> --json mergeable,statusCheckRollup,headRefOid` to inspect state, `gh api ...` for anything else. NEVER edit files in the working tree.",
|
|
44
|
+
"allowedUses": ["pr", "api", "issue"]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"inputArtifacts": [],
|
|
48
|
+
"outputArtifacts": [],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"preflight": [
|
|
51
|
+
{
|
|
52
|
+
"script": "loadJobFromFile",
|
|
53
|
+
"with": {
|
|
54
|
+
"jobsDir": ".kody/workers",
|
|
55
|
+
"slugArg": "job"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
"script": "composePrompt"
|
|
60
|
+
}
|
|
61
|
+
],
|
|
62
|
+
"postflight": [
|
|
63
|
+
{
|
|
64
|
+
"script": "parseJobStateFromAgentResult",
|
|
65
|
+
"with": {
|
|
66
|
+
"fenceLabel": "kody-job-next-state"
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"script": "writeJobStateFile"
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
You are **kody worker-tick**, the coordinator for one file-based worker. You do **not** touch code, do **not** commit, and do **not** edit files. You coordinate by inspecting GitHub state and issuing Kody commands as PR comments.
|
|
2
|
+
|
|
3
|
+
## The worker
|
|
4
|
+
|
|
5
|
+
Slug **`{{jobSlug}}`** — *{{jobTitle}}*. The worker body below is authoritative: it states what success looks like, allowed commands, and restrictions. The worker file is human-edited — re-read it every tick.
|
|
6
|
+
|
|
7
|
+
### Worker body
|
|
8
|
+
|
|
9
|
+
{{jobIntent}}
|
|
10
|
+
|
|
11
|
+
## Current state
|
|
12
|
+
|
|
13
|
+
This is the state you wrote at the end of the previous tick (or `null` if this is the first tick):
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{{jobStateJson}}
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
`cursor` is *your* enum — pick whatever labels map cleanly to your worker's phases. `data` is where you stash anything you need on the next tick (per-PR attempt counters, last-seen SHAs, etc). `done: true` is how you signal that the worker is permanently over — for evergreen workers this should always remain `false`.
|
|
20
|
+
|
|
21
|
+
## What to do on this tick
|
|
22
|
+
|
|
23
|
+
`forceRun = {{args.force}}` — set to `true` when an operator clicked "Run now" on the dashboard. When `forceRun` is `true`, ignore the worker body's `**Cadence guard.**` paragraph (or any equivalent "skip if last run was within X" rule) and execute the work as if the guard had passed. All other body rules — allowed commands, restrictions, state schema — still apply. Force only overrides cadence.
|
|
24
|
+
|
|
25
|
+
1. **Check `done`.** If the prior state has `done: true`, emit the same state back unchanged and exit without any action.
|
|
26
|
+
2. **Re-read the worker body.** It may have changed since the last tick.
|
|
27
|
+
3. **Execute exactly the work the body's `## Worker` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
28
|
+
4. **Optionally post a short narration** wherever the worker tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
29
|
+
5. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
|
|
30
|
+
|
|
31
|
+
## Output contract (MANDATORY, exactly once, at the end)
|
|
32
|
+
|
|
33
|
+
End your response with a single fenced block using the `kody-job-next-state` language tag:
|
|
34
|
+
|
|
35
|
+
````
|
|
36
|
+
```kody-job-next-state
|
|
37
|
+
{
|
|
38
|
+
"cursor": "<your-next-cursor>",
|
|
39
|
+
"data": { ... },
|
|
40
|
+
"done": <true|false>
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
````
|
|
44
|
+
|
|
45
|
+
If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
|
|
46
|
+
|
|
47
|
+
## Rules
|
|
48
|
+
|
|
49
|
+
- Never edit, create, or delete files in the working tree.
|
|
50
|
+
- Never commit or push via `git`. The only permitted commit path is `gh api -X PUT` against the report file (see exception below).
|
|
51
|
+
- Only shell calls allowed: `gh`. Everything must go through it.
|
|
52
|
+
- Keep each tick focused: do one action per candidate per wake. The cron will call you again.
|
|
53
|
+
- If state says you're waiting on something, just check and re-emit — don't spawn a duplicate.
|
|
54
|
+
- Honour the worker body's `## Restrictions` over any inferred shortcut.
|
|
55
|
+
|
|
56
|
+
### Single permitted write: the worker's report file
|
|
57
|
+
|
|
58
|
+
A worker MAY (optionally — only if its body asks for it) write a single
|
|
59
|
+
markdown report file at the canonical path:
|
|
60
|
+
|
|
61
|
+
```
|
|
62
|
+
.kody/reports/{{jobSlug}}.md
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Only that exact path. Only via `gh api -X PUT /repos/<owner>/<repo>/contents/.kody/reports/{{jobSlug}}.md` (with base64 content + `sha` of the existing file when updating). All other writes — code files, other report paths, other slugs — remain forbidden. The dashboard's `/reports` page surfaces these files automatically; this is the canonical channel for a worker's diagnostic output when an issue comment isn't expressive enough.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "worker-tick-scripted",
|
|
3
|
+
"role": "utility",
|
|
4
|
+
"describe": "Deterministic worker tick: runs the slug's `tickScript:` (declared in worker frontmatter), parses next-state from its stdout, persists. No agent. Parallel to job-tick-scripted but reads .kody/workers/.",
|
|
5
|
+
"kind": "oneshot",
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"name": "job",
|
|
9
|
+
"flag": "--job",
|
|
10
|
+
"type": "string",
|
|
11
|
+
"required": true,
|
|
12
|
+
"describe": "Worker slug — basename (without .md) of the file under .kody/workers/."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "force",
|
|
16
|
+
"flag": "--force",
|
|
17
|
+
"type": "bool",
|
|
18
|
+
"describe": "Accepted for parity with `worker-tick`. Scripted ticks have no agent cadence guard to bypass — the dispatcher already gated on frontmatter `every:`. Forwarded to the script via env if it cares."
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"claudeCode": {
|
|
22
|
+
"model": "inherit",
|
|
23
|
+
"permissionMode": "default",
|
|
24
|
+
"maxTurns": null,
|
|
25
|
+
"maxThinkingTokens": null,
|
|
26
|
+
"systemPromptAppend": null,
|
|
27
|
+
"tools": [],
|
|
28
|
+
"hooks": [],
|
|
29
|
+
"skills": [],
|
|
30
|
+
"commands": [],
|
|
31
|
+
"subagents": [],
|
|
32
|
+
"plugins": [],
|
|
33
|
+
"mcpServers": []
|
|
34
|
+
},
|
|
35
|
+
"cliTools": [
|
|
36
|
+
{
|
|
37
|
+
"name": "gh",
|
|
38
|
+
"install": {
|
|
39
|
+
"required": true,
|
|
40
|
+
"checkCommand": "command -v gh"
|
|
41
|
+
},
|
|
42
|
+
"verify": "gh auth status",
|
|
43
|
+
"usage": "Available to the tickScript via PATH; this executable shells out to `bash <tickScript>`. Scripts use `gh pr list`, `gh pr comment`, etc.",
|
|
44
|
+
"allowedUses": ["pr", "api", "issue"]
|
|
45
|
+
}
|
|
46
|
+
],
|
|
47
|
+
"inputArtifacts": [],
|
|
48
|
+
"outputArtifacts": [],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"preflight": [
|
|
51
|
+
{
|
|
52
|
+
"script": "runTickScript",
|
|
53
|
+
"with": {
|
|
54
|
+
"jobsDir": ".kody/workers",
|
|
55
|
+
"slugArg": "job",
|
|
56
|
+
"fenceLabel": "kody-job-next-state"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
],
|
|
60
|
+
"postflight": [
|
|
61
|
+
{
|
|
62
|
+
"script": "writeJobStateFile",
|
|
63
|
+
"with": {
|
|
64
|
+
"jobsDir": ".kody/workers"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
}
|
|
69
|
+
}
|
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.80",
|
|
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,6 +12,23 @@
|
|
|
12
12
|
"templates",
|
|
13
13
|
"kody.config.schema.json"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"kody:run": "tsx bin/kody.ts",
|
|
17
|
+
"serve": "tsx bin/kody.ts serve",
|
|
18
|
+
"serve:vscode": "tsx bin/kody.ts serve vscode",
|
|
19
|
+
"serve:claude": "tsx bin/kody.ts serve claude",
|
|
20
|
+
"build": "tsup && node scripts/copy-assets.cjs",
|
|
21
|
+
"check:modularity": "tsx scripts/check-script-modularity.ts",
|
|
22
|
+
"pretest": "pnpm check:modularity",
|
|
23
|
+
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
24
|
+
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
25
|
+
"test:all": "vitest run tests --no-coverage",
|
|
26
|
+
"typecheck": "tsc --noEmit",
|
|
27
|
+
"lint": "biome check",
|
|
28
|
+
"lint:fix": "biome check --write",
|
|
29
|
+
"format": "biome format --write",
|
|
30
|
+
"prepublishOnly": "pnpm build"
|
|
31
|
+
},
|
|
15
32
|
"dependencies": {
|
|
16
33
|
"@actions/cache": "^6.0.0",
|
|
17
34
|
"@anthropic-ai/claude-agent-sdk": "0.2.119",
|
|
@@ -33,21 +50,5 @@
|
|
|
33
50
|
"url": "git+https://github.com/aharonyaircohen/kody-engine.git"
|
|
34
51
|
},
|
|
35
52
|
"homepage": "https://github.com/aharonyaircohen/kody-engine",
|
|
36
|
-
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
37
|
-
|
|
38
|
-
"kody:run": "tsx bin/kody.ts",
|
|
39
|
-
"serve": "tsx bin/kody.ts serve",
|
|
40
|
-
"serve:vscode": "tsx bin/kody.ts serve vscode",
|
|
41
|
-
"serve:claude": "tsx bin/kody.ts serve claude",
|
|
42
|
-
"build": "tsup && node scripts/copy-assets.cjs",
|
|
43
|
-
"check:modularity": "tsx scripts/check-script-modularity.ts",
|
|
44
|
-
"pretest": "pnpm check:modularity",
|
|
45
|
-
"test": "vitest run tests/unit tests/int --no-coverage",
|
|
46
|
-
"test:e2e": "vitest run tests/e2e --no-coverage",
|
|
47
|
-
"test:all": "vitest run tests --no-coverage",
|
|
48
|
-
"typecheck": "tsc --noEmit",
|
|
49
|
-
"lint": "biome check",
|
|
50
|
-
"lint:fix": "biome check --write",
|
|
51
|
-
"format": "biome format --write"
|
|
52
|
-
}
|
|
53
|
-
}
|
|
53
|
+
"bugs": "https://github.com/aharonyaircohen/kody-engine/issues"
|
|
54
|
+
}
|
package/templates/kody.yml
CHANGED