@kody-ade/kody-engine 0.4.79 → 0.4.81
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 +345 -107
- package/dist/executables/fix-ci/profile.json +2 -2
- 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/ui-review/profile.json +1 -1
- 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/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();
|
|
@@ -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.81",
|
|
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,10 @@ 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
|
+
"When you diagnose something, answer in this shape: a few words on the issue, a",
|
|
1704
|
+
"few words on the fix, then a single question asking whether to proceed. Do not",
|
|
1705
|
+
"pad it with preamble, restated context, or a trailing summary.",
|
|
1703
1706
|
"",
|
|
1704
1707
|
"# Your environment and capabilities",
|
|
1705
1708
|
"You run inside a sandboxed runner with a full clone of the user's repository",
|
|
@@ -1724,6 +1727,22 @@ var CHAT_SYSTEM_PROMPT = [
|
|
|
1724
1727
|
" pytest, go test, cargo, etc., whatever the project uses).",
|
|
1725
1728
|
" - standard Unix utilities (curl, jq, sed, awk, find, etc.).",
|
|
1726
1729
|
"",
|
|
1730
|
+
"# Clarify before you act (HARD RULE)",
|
|
1731
|
+
"If the user's request is ambiguous or under-specified \u2014 you can read it two",
|
|
1732
|
+
"plausible ways, or you'd have to guess what they actually want \u2014 ask",
|
|
1733
|
+
"clarifying questions and stop. Ask as many as you genuinely need; do NOT",
|
|
1734
|
+
"pick an interpretation and run with it. This check comes first: confirm",
|
|
1735
|
+
"intent before you start investigating or making changes.",
|
|
1736
|
+
"",
|
|
1737
|
+
"# Answer first, act second (HARD RULE)",
|
|
1738
|
+
"If the user asked a question, answer it \u2014 do not start changing code, running",
|
|
1739
|
+
"mutating commands, or opening PRs. Investigating (read-only Glob/Grep/Read/`git",
|
|
1740
|
+
"log`/`gh ... view`) to ground the answer is expected; making changes is not.",
|
|
1741
|
+
"Before any mutating work (Edit/Write, `git commit`, `gh pr create`, anything",
|
|
1742
|
+
"that alters the repo or remote state) state the plan in a couple of bullets and",
|
|
1743
|
+
"stop for the user's go-ahead. Treat 'can we\u2026', 'is there a way\u2026', 'why does\u2026'",
|
|
1744
|
+
"as questions about what exists, not as instructions to build.",
|
|
1745
|
+
"",
|
|
1727
1746
|
"# Investigate before you answer (HARD RULE)",
|
|
1728
1747
|
"Do not answer from assumptions, training memory, or what the code 'probably'",
|
|
1729
1748
|
"does. Before replying to any question about this repo \u2014 its code, behavior,",
|
|
@@ -1854,6 +1873,11 @@ ${catalog}` : basePrompt;
|
|
|
1854
1873
|
return { exitCode: 99, error };
|
|
1855
1874
|
}
|
|
1856
1875
|
const reply = result.finalText.trim();
|
|
1876
|
+
if (reply.length === 0) {
|
|
1877
|
+
const error = "agent completed without producing a reply \u2014 please resend your message";
|
|
1878
|
+
await emit(opts.sink, "chat.error", opts.sessionId, "error", { error });
|
|
1879
|
+
return { exitCode: 99, error };
|
|
1880
|
+
}
|
|
1857
1881
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1858
1882
|
appendTurn(opts.sessionFile, {
|
|
1859
1883
|
role: "assistant",
|
|
@@ -2178,8 +2202,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
|
|
|
2178
2202
|
|
|
2179
2203
|
// src/kody-cli.ts
|
|
2180
2204
|
import { execFileSync as execFileSync30 } from "child_process";
|
|
2181
|
-
import * as
|
|
2182
|
-
import * as
|
|
2205
|
+
import * as fs35 from "fs";
|
|
2206
|
+
import * as path32 from "path";
|
|
2183
2207
|
|
|
2184
2208
|
// src/dispatch.ts
|
|
2185
2209
|
import * as fs9 from "fs";
|
|
@@ -2501,8 +2525,8 @@ init_issue();
|
|
|
2501
2525
|
|
|
2502
2526
|
// src/executor.ts
|
|
2503
2527
|
import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
|
|
2504
|
-
import * as
|
|
2505
|
-
import * as
|
|
2528
|
+
import * as fs34 from "fs";
|
|
2529
|
+
import * as path31 from "path";
|
|
2506
2530
|
init_events();
|
|
2507
2531
|
|
|
2508
2532
|
// src/lifecycleLabels.ts
|
|
@@ -5586,6 +5610,10 @@ function parseFlatYaml(text) {
|
|
|
5586
5610
|
out.every = value;
|
|
5587
5611
|
} else if (key === "tickScript" && value.length > 0) {
|
|
5588
5612
|
out.tickScript = value;
|
|
5613
|
+
} else if (key === "disabled") {
|
|
5614
|
+
const lower = value.toLowerCase();
|
|
5615
|
+
if (lower === "true") out.disabled = true;
|
|
5616
|
+
else if (lower === "false") out.disabled = false;
|
|
5589
5617
|
}
|
|
5590
5618
|
}
|
|
5591
5619
|
return out;
|
|
@@ -5946,6 +5974,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5946
5974
|
throw new Error("dispatchJobFileTicks: `with.targetExecutable` is required");
|
|
5947
5975
|
}
|
|
5948
5976
|
const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
|
|
5977
|
+
const scriptedExecutable = String(args?.scriptedExecutable ?? "job-tick-scripted");
|
|
5949
5978
|
const slugArg = String(args?.slugArg ?? "job");
|
|
5950
5979
|
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
5951
5980
|
if (backend.hydrate) {
|
|
@@ -5965,6 +5994,12 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5965
5994
|
const now = Date.now();
|
|
5966
5995
|
for (const slug of slugs) {
|
|
5967
5996
|
const frontmatter = readJobFrontmatter(ctx.cwd, jobsDir, slug);
|
|
5997
|
+
if (frontmatter.disabled === true) {
|
|
5998
|
+
process.stdout.write(`[jobs] \u23ED skip ${slug}: disabled in frontmatter
|
|
5999
|
+
`);
|
|
6000
|
+
results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
|
|
6001
|
+
continue;
|
|
6002
|
+
}
|
|
5968
6003
|
const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
|
|
5969
6004
|
if (decision.skip) {
|
|
5970
6005
|
process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
|
|
@@ -5972,7 +6007,7 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
|
|
|
5972
6007
|
results.push({ slug, exitCode: 0, skipped: true, reason: decision.reason });
|
|
5973
6008
|
continue;
|
|
5974
6009
|
}
|
|
5975
|
-
const slugTarget = frontmatter.tickScript ?
|
|
6010
|
+
const slugTarget = frontmatter.tickScript ? scriptedExecutable : targetExecutable;
|
|
5976
6011
|
process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
|
|
5977
6012
|
`);
|
|
5978
6013
|
try {
|
|
@@ -7336,10 +7371,10 @@ import * as fs26 from "fs";
|
|
|
7336
7371
|
import * as path24 from "path";
|
|
7337
7372
|
var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "done"]);
|
|
7338
7373
|
var GoalStateError = class extends Error {
|
|
7339
|
-
constructor(
|
|
7340
|
-
super(`Invalid goal state at ${
|
|
7374
|
+
constructor(path34, message) {
|
|
7375
|
+
super(`Invalid goal state at ${path34}:
|
|
7341
7376
|
${message}`);
|
|
7342
|
-
this.path =
|
|
7377
|
+
this.path = path34;
|
|
7343
7378
|
this.name = "GoalStateError";
|
|
7344
7379
|
}
|
|
7345
7380
|
path;
|
|
@@ -9063,8 +9098,168 @@ function buildChildEnv(parent, force) {
|
|
|
9063
9098
|
|
|
9064
9099
|
// src/scripts/brainServe.ts
|
|
9065
9100
|
import { createServer } from "http";
|
|
9101
|
+
import * as fs32 from "fs";
|
|
9102
|
+
import * as path30 from "path";
|
|
9103
|
+
|
|
9104
|
+
// src/scripts/brainTurnLog.ts
|
|
9066
9105
|
import * as fs31 from "fs";
|
|
9067
9106
|
import * as path29 from "path";
|
|
9107
|
+
var live = /* @__PURE__ */ new Map();
|
|
9108
|
+
function eventsPath(dir, chatId) {
|
|
9109
|
+
return path29.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
9110
|
+
}
|
|
9111
|
+
function lastPersistedSeq(dir, chatId) {
|
|
9112
|
+
const p = eventsPath(dir, chatId);
|
|
9113
|
+
if (!fs31.existsSync(p)) return 0;
|
|
9114
|
+
const lines = fs31.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
9115
|
+
if (lines.length === 0) return 0;
|
|
9116
|
+
try {
|
|
9117
|
+
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
9118
|
+
} catch {
|
|
9119
|
+
return 0;
|
|
9120
|
+
}
|
|
9121
|
+
}
|
|
9122
|
+
function readSince(dir, chatId, since) {
|
|
9123
|
+
const p = eventsPath(dir, chatId);
|
|
9124
|
+
if (!fs31.existsSync(p)) return [];
|
|
9125
|
+
const out = [];
|
|
9126
|
+
for (const line of fs31.readFileSync(p, "utf-8").split("\n")) {
|
|
9127
|
+
if (!line) continue;
|
|
9128
|
+
try {
|
|
9129
|
+
const rec = JSON.parse(line);
|
|
9130
|
+
if (rec.seq > since) out.push(rec);
|
|
9131
|
+
} catch {
|
|
9132
|
+
}
|
|
9133
|
+
}
|
|
9134
|
+
return out;
|
|
9135
|
+
}
|
|
9136
|
+
function isTerminal(event) {
|
|
9137
|
+
return event.type === "done" || event.type === "error";
|
|
9138
|
+
}
|
|
9139
|
+
function beginTurn(dir, chatId) {
|
|
9140
|
+
const existing = live.get(chatId);
|
|
9141
|
+
const seqFloor = existing ? existing.seq : lastPersistedSeq(dir, chatId);
|
|
9142
|
+
const turn = (existing?.turn ?? 0) + 1;
|
|
9143
|
+
const state = {
|
|
9144
|
+
seq: seqFloor,
|
|
9145
|
+
turn,
|
|
9146
|
+
status: "running",
|
|
9147
|
+
terminal: null,
|
|
9148
|
+
subscribers: /* @__PURE__ */ new Set()
|
|
9149
|
+
};
|
|
9150
|
+
live.set(chatId, state);
|
|
9151
|
+
const p = eventsPath(dir, chatId);
|
|
9152
|
+
fs31.mkdirSync(path29.dirname(p), { recursive: true });
|
|
9153
|
+
return (event) => {
|
|
9154
|
+
state.seq += 1;
|
|
9155
|
+
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
9156
|
+
try {
|
|
9157
|
+
fs31.appendFileSync(p, JSON.stringify(rec) + "\n");
|
|
9158
|
+
} catch (err) {
|
|
9159
|
+
process.stderr.write(
|
|
9160
|
+
`[brain-turn-log] append failed for ${chatId}: ${err instanceof Error ? err.message : String(err)}
|
|
9161
|
+
`
|
|
9162
|
+
);
|
|
9163
|
+
}
|
|
9164
|
+
for (const fn of state.subscribers) {
|
|
9165
|
+
try {
|
|
9166
|
+
fn(rec);
|
|
9167
|
+
} catch {
|
|
9168
|
+
}
|
|
9169
|
+
}
|
|
9170
|
+
if (isTerminal(event)) {
|
|
9171
|
+
state.status = "ended";
|
|
9172
|
+
state.terminal = rec;
|
|
9173
|
+
const subs = [...state.subscribers];
|
|
9174
|
+
state.subscribers.clear();
|
|
9175
|
+
for (const fn of subs) {
|
|
9176
|
+
try {
|
|
9177
|
+
fn(null);
|
|
9178
|
+
} catch {
|
|
9179
|
+
}
|
|
9180
|
+
}
|
|
9181
|
+
}
|
|
9182
|
+
};
|
|
9183
|
+
}
|
|
9184
|
+
function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
9185
|
+
const state = live.get(chatId);
|
|
9186
|
+
if (!state || state.status === "ended") return;
|
|
9187
|
+
state.seq += 1;
|
|
9188
|
+
const rec = {
|
|
9189
|
+
seq: state.seq,
|
|
9190
|
+
turn: state.turn,
|
|
9191
|
+
ts: Date.now(),
|
|
9192
|
+
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
9193
|
+
};
|
|
9194
|
+
try {
|
|
9195
|
+
fs31.appendFileSync(eventsPath(dir, chatId), JSON.stringify(rec) + "\n");
|
|
9196
|
+
} catch {
|
|
9197
|
+
}
|
|
9198
|
+
state.status = "ended";
|
|
9199
|
+
state.terminal = rec;
|
|
9200
|
+
const subs = [...state.subscribers];
|
|
9201
|
+
state.subscribers.clear();
|
|
9202
|
+
for (const fn of subs) {
|
|
9203
|
+
try {
|
|
9204
|
+
fn(rec);
|
|
9205
|
+
fn(null);
|
|
9206
|
+
} catch {
|
|
9207
|
+
}
|
|
9208
|
+
}
|
|
9209
|
+
}
|
|
9210
|
+
function subscribe(dir, chatId, since, onRecord, onClose) {
|
|
9211
|
+
const backlog = readSince(dir, chatId, since);
|
|
9212
|
+
for (const rec of backlog) onRecord(rec);
|
|
9213
|
+
const lastReplayed = backlog.length ? backlog[backlog.length - 1] : null;
|
|
9214
|
+
if (lastReplayed && isTerminal(lastReplayed.event)) {
|
|
9215
|
+
onClose();
|
|
9216
|
+
return () => {
|
|
9217
|
+
};
|
|
9218
|
+
}
|
|
9219
|
+
const state = live.get(chatId);
|
|
9220
|
+
if (state && state.status === "running") {
|
|
9221
|
+
const fn = (rec) => {
|
|
9222
|
+
if (rec === null) {
|
|
9223
|
+
state.subscribers.delete(fn);
|
|
9224
|
+
onClose();
|
|
9225
|
+
return;
|
|
9226
|
+
}
|
|
9227
|
+
if (rec.seq > since) onRecord(rec);
|
|
9228
|
+
};
|
|
9229
|
+
state.subscribers.add(fn);
|
|
9230
|
+
return () => {
|
|
9231
|
+
state.subscribers.delete(fn);
|
|
9232
|
+
};
|
|
9233
|
+
}
|
|
9234
|
+
if (state && state.status === "ended" && state.terminal) {
|
|
9235
|
+
if (state.terminal.seq > since && !lastReplayed) onRecord(state.terminal);
|
|
9236
|
+
onClose();
|
|
9237
|
+
return () => {
|
|
9238
|
+
};
|
|
9239
|
+
}
|
|
9240
|
+
if (lastReplayed) {
|
|
9241
|
+
onRecord({
|
|
9242
|
+
seq: lastReplayed.seq + 1,
|
|
9243
|
+
turn: lastReplayed.turn,
|
|
9244
|
+
ts: Date.now(),
|
|
9245
|
+
event: {
|
|
9246
|
+
type: "error",
|
|
9247
|
+
error: "stream interrupted (server restarted mid-reply) \u2014 resend your message",
|
|
9248
|
+
chatId
|
|
9249
|
+
}
|
|
9250
|
+
});
|
|
9251
|
+
}
|
|
9252
|
+
onClose();
|
|
9253
|
+
return () => {
|
|
9254
|
+
};
|
|
9255
|
+
}
|
|
9256
|
+
function getLastSeq(dir, chatId) {
|
|
9257
|
+
const state = live.get(chatId);
|
|
9258
|
+
if (state) return state.seq;
|
|
9259
|
+
return lastPersistedSeq(dir, chatId);
|
|
9260
|
+
}
|
|
9261
|
+
|
|
9262
|
+
// src/scripts/brainServe.ts
|
|
9068
9263
|
var DEFAULT_PORT = 8080;
|
|
9069
9264
|
function getApiKey() {
|
|
9070
9265
|
const key = (process.env.BRAIN_API_KEY ?? "").trim();
|
|
@@ -9120,47 +9315,86 @@ function emitSse(res, event) {
|
|
|
9120
9315
|
|
|
9121
9316
|
`);
|
|
9122
9317
|
}
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9318
|
+
function translateChatEvent(event, chatId) {
|
|
9319
|
+
switch (event.event) {
|
|
9320
|
+
case "chat.message": {
|
|
9321
|
+
const content = String(event.payload.content ?? "");
|
|
9322
|
+
if (content.length === 0) return null;
|
|
9323
|
+
return { type: "text", text: content, chatId };
|
|
9324
|
+
}
|
|
9325
|
+
case "chat.tool": {
|
|
9326
|
+
if (event.payload.phase !== "use") return null;
|
|
9327
|
+
return {
|
|
9328
|
+
type: "tool_use",
|
|
9329
|
+
name: typeof event.payload.name === "string" ? event.payload.name : "tool",
|
|
9330
|
+
input: event.payload.input ?? {},
|
|
9331
|
+
chatId
|
|
9332
|
+
};
|
|
9333
|
+
}
|
|
9334
|
+
case "chat.done":
|
|
9335
|
+
return { type: "done", chatId };
|
|
9336
|
+
case "chat.error":
|
|
9337
|
+
return {
|
|
9338
|
+
type: "error",
|
|
9339
|
+
error: typeof event.payload.error === "string" ? event.payload.error : "agent error",
|
|
9340
|
+
chatId
|
|
9341
|
+
};
|
|
9342
|
+
default:
|
|
9343
|
+
return null;
|
|
9344
|
+
}
|
|
9345
|
+
}
|
|
9346
|
+
var BrokerSink = class {
|
|
9347
|
+
constructor(emitToLog, chatId) {
|
|
9348
|
+
this.emitToLog = emitToLog;
|
|
9126
9349
|
this.chatId = chatId;
|
|
9127
9350
|
}
|
|
9128
|
-
|
|
9351
|
+
emitToLog;
|
|
9129
9352
|
chatId;
|
|
9130
9353
|
async emit(event) {
|
|
9131
|
-
|
|
9132
|
-
|
|
9133
|
-
|
|
9134
|
-
|
|
9135
|
-
|
|
9354
|
+
const be = translateChatEvent(event, this.chatId);
|
|
9355
|
+
if (be) this.emitToLog(be);
|
|
9356
|
+
}
|
|
9357
|
+
};
|
|
9358
|
+
var chatQueues = /* @__PURE__ */ new Map();
|
|
9359
|
+
function enqueue(chatId, fn) {
|
|
9360
|
+
const prev = chatQueues.get(chatId) ?? Promise.resolve();
|
|
9361
|
+
const next = prev.catch(() => {
|
|
9362
|
+
}).then(fn);
|
|
9363
|
+
chatQueues.set(
|
|
9364
|
+
chatId,
|
|
9365
|
+
next.finally(() => {
|
|
9366
|
+
if (chatQueues.get(chatId) === next) chatQueues.delete(chatId);
|
|
9367
|
+
})
|
|
9368
|
+
);
|
|
9369
|
+
return next;
|
|
9370
|
+
}
|
|
9371
|
+
function streamToRes(res, dir, chatId, since) {
|
|
9372
|
+
writeSseHeaders(res);
|
|
9373
|
+
emitSse(res, { type: "chat", chatId });
|
|
9374
|
+
let maxSent = since;
|
|
9375
|
+
const unsubscribe = subscribe(
|
|
9376
|
+
dir,
|
|
9377
|
+
chatId,
|
|
9378
|
+
since,
|
|
9379
|
+
(rec) => {
|
|
9380
|
+
if (rec.seq <= maxSent) return;
|
|
9381
|
+
maxSent = rec.seq;
|
|
9382
|
+
if (res.writableEnded) return;
|
|
9383
|
+
res.write(`data: ${JSON.stringify({ ...rec.event, seq: rec.seq })}
|
|
9384
|
+
|
|
9385
|
+
`);
|
|
9386
|
+
},
|
|
9387
|
+
() => {
|
|
9388
|
+
if (!res.writableEnded) {
|
|
9389
|
+
try {
|
|
9390
|
+
res.end();
|
|
9391
|
+
} catch {
|
|
9136
9392
|
}
|
|
9137
|
-
return;
|
|
9138
|
-
}
|
|
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
9393
|
}
|
|
9158
|
-
// chat.thinking / chat.ready / chat.exit — not part of the Brain protocol.
|
|
9159
|
-
default:
|
|
9160
|
-
return;
|
|
9161
9394
|
}
|
|
9162
|
-
|
|
9163
|
-
|
|
9395
|
+
);
|
|
9396
|
+
res.on("close", unsubscribe);
|
|
9397
|
+
}
|
|
9164
9398
|
async function handleChatTurn(req, res, chatId, opts) {
|
|
9165
9399
|
let body;
|
|
9166
9400
|
try {
|
|
@@ -9175,38 +9409,38 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
9175
9409
|
return;
|
|
9176
9410
|
}
|
|
9177
9411
|
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
9178
|
-
|
|
9412
|
+
fs32.mkdirSync(path30.dirname(sessionFile), { recursive: true });
|
|
9179
9413
|
appendTurn(sessionFile, {
|
|
9180
9414
|
role: "user",
|
|
9181
9415
|
content: message,
|
|
9182
9416
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
9183
9417
|
});
|
|
9184
|
-
|
|
9185
|
-
|
|
9186
|
-
const sink = new
|
|
9187
|
-
|
|
9188
|
-
|
|
9418
|
+
const sinceFloor = getLastSeq(opts.cwd, chatId);
|
|
9419
|
+
const emitToLog = beginTurn(opts.cwd, chatId);
|
|
9420
|
+
const sink = new BrokerSink(emitToLog, chatId);
|
|
9421
|
+
void enqueue(
|
|
9422
|
+
chatId,
|
|
9423
|
+
() => opts.runTurn({
|
|
9189
9424
|
sessionId: chatId,
|
|
9190
9425
|
sessionFile,
|
|
9191
9426
|
cwd: opts.cwd,
|
|
9192
9427
|
model: opts.model,
|
|
9193
9428
|
litellmUrl: opts.litellmUrl,
|
|
9194
9429
|
sink
|
|
9195
|
-
})
|
|
9196
|
-
|
|
9197
|
-
|
|
9198
|
-
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
|
|
9202
|
-
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9206
|
-
|
|
9207
|
-
|
|
9208
|
-
|
|
9209
|
-
}
|
|
9430
|
+
}).catch((err) => {
|
|
9431
|
+
const errMsg2 = err instanceof Error ? err.message : String(err);
|
|
9432
|
+
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
|
|
9433
|
+
`);
|
|
9434
|
+
endTurnIfUnterminated(opts.cwd, chatId, errMsg2);
|
|
9435
|
+
}).finally(() => {
|
|
9436
|
+
endTurnIfUnterminated(
|
|
9437
|
+
opts.cwd,
|
|
9438
|
+
chatId,
|
|
9439
|
+
"Brain turn ended without a reply (the machine may have restarted mid-turn) \u2014 please resend your message"
|
|
9440
|
+
);
|
|
9441
|
+
})
|
|
9442
|
+
);
|
|
9443
|
+
streamToRes(res, opts.cwd, chatId, sinceFloor);
|
|
9210
9444
|
}
|
|
9211
9445
|
function buildServer(opts) {
|
|
9212
9446
|
const runTurn = opts.runTurn ?? runChatTurn;
|
|
@@ -9239,6 +9473,18 @@ function buildServer(opts) {
|
|
|
9239
9473
|
});
|
|
9240
9474
|
return;
|
|
9241
9475
|
}
|
|
9476
|
+
const sm = url.pathname.match(/^\/chats\/([^/]+)\/stream\/?$/);
|
|
9477
|
+
if (req.method === "GET" && sm) {
|
|
9478
|
+
const chatId = decodeURIComponent(sm[1] ?? "");
|
|
9479
|
+
if (!chatId) {
|
|
9480
|
+
sendJson(res, 400, { error: "chatId required" });
|
|
9481
|
+
return;
|
|
9482
|
+
}
|
|
9483
|
+
const sinceRaw = url.searchParams.get("since");
|
|
9484
|
+
const since = Number.isFinite(Number(sinceRaw)) ? Number(sinceRaw) : 0;
|
|
9485
|
+
streamToRes(res, opts.cwd, chatId, since);
|
|
9486
|
+
return;
|
|
9487
|
+
}
|
|
9242
9488
|
sendJson(res, 404, { error: "not found" });
|
|
9243
9489
|
});
|
|
9244
9490
|
}
|
|
@@ -9591,7 +9837,6 @@ var syncFlow = async (ctx, _profile, args) => {
|
|
|
9591
9837
|
if (announceOnSuccess) {
|
|
9592
9838
|
ctx.output.exitCode = 0;
|
|
9593
9839
|
ctx.output.reason = `already up to date with origin/${baseBranch}`;
|
|
9594
|
-
tryPostPr6(prNumber, `\u2139\uFE0F kody sync: already up to date with origin/${baseBranch}`, ctx.cwd);
|
|
9595
9840
|
}
|
|
9596
9841
|
return;
|
|
9597
9842
|
}
|
|
@@ -9606,13 +9851,6 @@ var syncFlow = async (ctx, _profile, args) => {
|
|
|
9606
9851
|
if (announceOnSuccess) {
|
|
9607
9852
|
ctx.output.exitCode = 0;
|
|
9608
9853
|
ctx.output.reason = `merged origin/${baseBranch} into ${ctx.data.branch}`;
|
|
9609
|
-
const runUrl = getRunUrl();
|
|
9610
|
-
const runSuffix = runUrl ? ` ([logs](${runUrl}))` : "";
|
|
9611
|
-
tryPostPr6(
|
|
9612
|
-
prNumber,
|
|
9613
|
-
`\u2705 kody sync: merged \`origin/${baseBranch}\` into \`${ctx.data.branch}\`${runSuffix}`,
|
|
9614
|
-
ctx.cwd
|
|
9615
|
-
);
|
|
9616
9854
|
}
|
|
9617
9855
|
};
|
|
9618
9856
|
function bail2(ctx, prNumber, reason) {
|
|
@@ -10224,7 +10462,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
|
|
|
10224
10462
|
};
|
|
10225
10463
|
|
|
10226
10464
|
// src/scripts/writeRunSummary.ts
|
|
10227
|
-
import * as
|
|
10465
|
+
import * as fs33 from "fs";
|
|
10228
10466
|
var writeRunSummary = async (ctx, profile) => {
|
|
10229
10467
|
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
10230
10468
|
if (!summaryPath) return;
|
|
@@ -10246,7 +10484,7 @@ var writeRunSummary = async (ctx, profile) => {
|
|
|
10246
10484
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
10247
10485
|
lines.push("");
|
|
10248
10486
|
try {
|
|
10249
|
-
|
|
10487
|
+
fs33.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
10250
10488
|
`);
|
|
10251
10489
|
} catch {
|
|
10252
10490
|
}
|
|
@@ -10470,9 +10708,9 @@ async function runExecutable(profileName, input) {
|
|
|
10470
10708
|
data: { ...input.preloadedData ?? {} },
|
|
10471
10709
|
output: { exitCode: 0 }
|
|
10472
10710
|
};
|
|
10473
|
-
const ndjsonDir =
|
|
10711
|
+
const ndjsonDir = path31.join(input.cwd, ".kody");
|
|
10474
10712
|
const invokeAgent = async (prompt) => {
|
|
10475
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
10713
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path31.isAbsolute(p) ? p : path31.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
10476
10714
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
10477
10715
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
10478
10716
|
return runAgent({
|
|
@@ -10667,7 +10905,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
|
|
|
10667
10905
|
function getProfileInputsForChild(profileName, _cwd) {
|
|
10668
10906
|
try {
|
|
10669
10907
|
const profilePath = resolveProfilePath(profileName);
|
|
10670
|
-
if (!
|
|
10908
|
+
if (!fs34.existsSync(profilePath)) return null;
|
|
10671
10909
|
return loadProfile(profilePath).inputs;
|
|
10672
10910
|
} catch {
|
|
10673
10911
|
return null;
|
|
@@ -10676,17 +10914,17 @@ function getProfileInputsForChild(profileName, _cwd) {
|
|
|
10676
10914
|
function resolveProfilePath(profileName) {
|
|
10677
10915
|
const found = resolveExecutable(profileName);
|
|
10678
10916
|
if (found) return found;
|
|
10679
|
-
const here =
|
|
10917
|
+
const here = path31.dirname(new URL(import.meta.url).pathname);
|
|
10680
10918
|
const candidates = [
|
|
10681
|
-
|
|
10919
|
+
path31.join(here, "executables", profileName, "profile.json"),
|
|
10682
10920
|
// same-dir sibling (dev)
|
|
10683
|
-
|
|
10921
|
+
path31.join(here, "..", "executables", profileName, "profile.json"),
|
|
10684
10922
|
// up one (prod: dist/bin → dist/executables)
|
|
10685
|
-
|
|
10923
|
+
path31.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
10686
10924
|
// fallback
|
|
10687
10925
|
];
|
|
10688
10926
|
for (const c of candidates) {
|
|
10689
|
-
if (
|
|
10927
|
+
if (fs34.existsSync(c)) return c;
|
|
10690
10928
|
}
|
|
10691
10929
|
return candidates[0];
|
|
10692
10930
|
}
|
|
@@ -10786,8 +11024,8 @@ function resolveShellTimeoutMs(entry) {
|
|
|
10786
11024
|
var SIGKILL_GRACE_MS = 5e3;
|
|
10787
11025
|
async function runShellEntry(entry, ctx, profile) {
|
|
10788
11026
|
const shellName = entry.shell;
|
|
10789
|
-
const shellPath =
|
|
10790
|
-
if (!
|
|
11027
|
+
const shellPath = path31.join(profile.dir, shellName);
|
|
11028
|
+
if (!fs34.existsSync(shellPath)) {
|
|
10791
11029
|
ctx.skipAgent = true;
|
|
10792
11030
|
ctx.output.exitCode = 99;
|
|
10793
11031
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -11266,9 +11504,9 @@ function resolveAuthToken(env = process.env) {
|
|
|
11266
11504
|
return token;
|
|
11267
11505
|
}
|
|
11268
11506
|
function detectPackageManager2(cwd) {
|
|
11269
|
-
if (
|
|
11270
|
-
if (
|
|
11271
|
-
if (
|
|
11507
|
+
if (fs35.existsSync(path32.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
11508
|
+
if (fs35.existsSync(path32.join(cwd, "yarn.lock"))) return "yarn";
|
|
11509
|
+
if (fs35.existsSync(path32.join(cwd, "bun.lockb"))) return "bun";
|
|
11272
11510
|
return "npm";
|
|
11273
11511
|
}
|
|
11274
11512
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -11355,11 +11593,11 @@ function configureGitIdentity(cwd) {
|
|
|
11355
11593
|
}
|
|
11356
11594
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
11357
11595
|
if (!issueNumber) return;
|
|
11358
|
-
const logPath =
|
|
11596
|
+
const logPath = path32.join(cwd, ".kody", "last-run.jsonl");
|
|
11359
11597
|
let tail = "";
|
|
11360
11598
|
try {
|
|
11361
|
-
if (
|
|
11362
|
-
const content =
|
|
11599
|
+
if (fs35.existsSync(logPath)) {
|
|
11600
|
+
const content = fs35.readFileSync(logPath, "utf-8");
|
|
11363
11601
|
tail = content.slice(-3e3);
|
|
11364
11602
|
}
|
|
11365
11603
|
} catch {
|
|
@@ -11384,7 +11622,7 @@ async function runCi(argv) {
|
|
|
11384
11622
|
return 0;
|
|
11385
11623
|
}
|
|
11386
11624
|
const args = parseCiArgs(argv);
|
|
11387
|
-
const cwd = args.cwd ?
|
|
11625
|
+
const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
|
|
11388
11626
|
let earlyConfig;
|
|
11389
11627
|
try {
|
|
11390
11628
|
earlyConfig = loadConfig(cwd);
|
|
@@ -11394,9 +11632,9 @@ async function runCi(argv) {
|
|
|
11394
11632
|
const eventName = process.env.GITHUB_EVENT_NAME;
|
|
11395
11633
|
const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
|
|
11396
11634
|
let manualWorkflowDispatch = false;
|
|
11397
|
-
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath &&
|
|
11635
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs35.existsSync(dispatchEventPath)) {
|
|
11398
11636
|
try {
|
|
11399
|
-
const evt = JSON.parse(
|
|
11637
|
+
const evt = JSON.parse(fs35.readFileSync(dispatchEventPath, "utf-8"));
|
|
11400
11638
|
const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
|
|
11401
11639
|
const sessionInput = String(evt?.inputs?.sessionId ?? "");
|
|
11402
11640
|
manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
|
|
@@ -11655,9 +11893,9 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
11655
11893
|
return result;
|
|
11656
11894
|
}
|
|
11657
11895
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
11658
|
-
const sessionFile =
|
|
11659
|
-
const eventsFile =
|
|
11660
|
-
const paths = [sessionFile, eventsFile].filter((p) =>
|
|
11896
|
+
const sessionFile = path33.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
11897
|
+
const eventsFile = path33.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
11898
|
+
const paths = [sessionFile, eventsFile].filter((p) => fs36.existsSync(path33.join(cwd, p)));
|
|
11661
11899
|
if (paths.length === 0) return;
|
|
11662
11900
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
11663
11901
|
try {
|
|
@@ -11695,7 +11933,7 @@ async function runChat(argv) {
|
|
|
11695
11933
|
${CHAT_HELP}`);
|
|
11696
11934
|
return 64;
|
|
11697
11935
|
}
|
|
11698
|
-
const cwd = args.cwd ?
|
|
11936
|
+
const cwd = args.cwd ? path33.resolve(args.cwd) : process.cwd();
|
|
11699
11937
|
const sessionId = args.sessionId;
|
|
11700
11938
|
const unpackedSecrets = unpackAllSecrets();
|
|
11701
11939
|
if (unpackedSecrets > 0) {
|
|
@@ -11747,7 +11985,7 @@ ${CHAT_HELP}`);
|
|
|
11747
11985
|
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
11748
11986
|
const meta = readMeta(sessionFile);
|
|
11749
11987
|
process.stdout.write(
|
|
11750
|
-
`\u2192 kody:chat: session file=${sessionFile} exists=${
|
|
11988
|
+
`\u2192 kody:chat: session file=${sessionFile} exists=${fs36.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
|
|
11751
11989
|
`
|
|
11752
11990
|
);
|
|
11753
11991
|
try {
|
|
@@ -47,9 +47,9 @@
|
|
|
47
47
|
"lifecycle": "pr-branch",
|
|
48
48
|
"lifecycleConfig": {
|
|
49
49
|
"label": {
|
|
50
|
-
"name": "kody:fixing",
|
|
50
|
+
"name": "kody:fixing-ci",
|
|
51
51
|
"color": "e99695",
|
|
52
|
-
"description": "kody:
|
|
52
|
+
"description": "kody: fixing CI failures"
|
|
53
53
|
},
|
|
54
54
|
"context": "ci-fix",
|
|
55
55
|
"advance": false
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
"outputArtifacts": [],
|
|
68
68
|
"scripts": {
|
|
69
69
|
"preflight": [
|
|
70
|
-
{ "script": "setLifecycleLabel", "with": { "label": "kody:reviewing", "color": "d93f0b", "description": "kody: reviewing a PR" } },
|
|
70
|
+
{ "script": "setLifecycleLabel", "with": { "label": "kody:reviewing-ui", "color": "d93f0b", "description": "kody: UI-reviewing a PR" } },
|
|
71
71
|
{ "script": "reviewFlow" },
|
|
72
72
|
{ "script": "loadTaskState" },
|
|
73
73
|
{ "script": "loadConventions" },
|
|
@@ -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.81",
|
|
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
|
+
}
|