@kody-ade/kody-engine 0.4.79 → 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 CHANGED
@@ -49,17 +49,17 @@ function emitEvent(cwd, ev) {
49
49
  runId,
50
50
  ...ev
51
51
  };
52
- const eventsPath = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
53
- fs3.mkdirSync(path3.dirname(eventsPath), { recursive: true });
54
- fs3.appendFileSync(eventsPath, `${JSON.stringify(fullEvent)}
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 eventsPath = path3.join(cwd, ".kody", "runs", runId, "events.jsonl");
61
- if (!fs3.existsSync(eventsPath)) return [];
62
- const lines = fs3.readFileSync(eventsPath, "utf-8").split("\n");
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.79",
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 fs35 from "fs";
927
- import * as path32 from "path";
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
- "focused, technical when appropriate, and formatted in Markdown.",
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 fs34 from "fs";
2182
- import * as path31 from "path";
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 fs33 from "fs";
2505
- import * as path30 from "path";
2504
+ import * as fs34 from "fs";
2505
+ import * as path31 from "path";
2506
2506
  init_events();
2507
2507
 
2508
2508
  // src/lifecycleLabels.ts
@@ -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 ? "job-tick-scripted" : targetExecutable;
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(path33, message) {
7340
- super(`Invalid goal state at ${path33}:
7350
+ constructor(path34, message) {
7351
+ super(`Invalid goal state at ${path34}:
7341
7352
  ${message}`);
7342
- this.path = path33;
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
- var BrainSseSink = class {
9124
- constructor(res, chatId) {
9125
- this.res = res;
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
- res;
9327
+ emitToLog;
9129
9328
  chatId;
9130
9329
  async emit(event) {
9131
- switch (event.event) {
9132
- case "chat.message": {
9133
- const content = String(event.payload.content ?? "");
9134
- if (content.length > 0) {
9135
- emitSse(this.res, { type: "text", text: content, chatId: this.chatId });
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
- fs31.mkdirSync(path29.dirname(sessionFile), { recursive: true });
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
- writeSseHeaders(res);
9185
- emitSse(res, { type: "chat", chatId });
9186
- const sink = new BrainSseSink(res, chatId);
9187
- try {
9188
- await opts.runTurn({
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
- } catch (err) {
9197
- const errMsg2 = err instanceof Error ? err.message : String(err);
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
- try {
9201
- emitSse(res, { type: "error", error: errMsg2, chatId });
9202
- } catch {
9203
- }
9204
- } finally {
9205
- try {
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
  }
@@ -10224,7 +10442,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
10224
10442
  };
10225
10443
 
10226
10444
  // src/scripts/writeRunSummary.ts
10227
- import * as fs32 from "fs";
10445
+ import * as fs33 from "fs";
10228
10446
  var writeRunSummary = async (ctx, profile) => {
10229
10447
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
10230
10448
  if (!summaryPath) return;
@@ -10246,7 +10464,7 @@ var writeRunSummary = async (ctx, profile) => {
10246
10464
  if (reason) lines.push(`- **Reason:** ${reason}`);
10247
10465
  lines.push("");
10248
10466
  try {
10249
- fs32.appendFileSync(summaryPath, `${lines.join("\n")}
10467
+ fs33.appendFileSync(summaryPath, `${lines.join("\n")}
10250
10468
  `);
10251
10469
  } catch {
10252
10470
  }
@@ -10470,9 +10688,9 @@ async function runExecutable(profileName, input) {
10470
10688
  data: { ...input.preloadedData ?? {} },
10471
10689
  output: { exitCode: 0 }
10472
10690
  };
10473
- const ndjsonDir = path30.join(input.cwd, ".kody");
10691
+ const ndjsonDir = path31.join(input.cwd, ".kody");
10474
10692
  const invokeAgent = async (prompt) => {
10475
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path30.isAbsolute(p) ? p : path30.resolve(profile.dir, p)).filter((p) => p.length > 0);
10693
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path31.isAbsolute(p) ? p : path31.resolve(profile.dir, p)).filter((p) => p.length > 0);
10476
10694
  const syntheticPath = ctx.data.syntheticPluginPath;
10477
10695
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
10478
10696
  return runAgent({
@@ -10667,7 +10885,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
10667
10885
  function getProfileInputsForChild(profileName, _cwd) {
10668
10886
  try {
10669
10887
  const profilePath = resolveProfilePath(profileName);
10670
- if (!fs33.existsSync(profilePath)) return null;
10888
+ if (!fs34.existsSync(profilePath)) return null;
10671
10889
  return loadProfile(profilePath).inputs;
10672
10890
  } catch {
10673
10891
  return null;
@@ -10676,17 +10894,17 @@ function getProfileInputsForChild(profileName, _cwd) {
10676
10894
  function resolveProfilePath(profileName) {
10677
10895
  const found = resolveExecutable(profileName);
10678
10896
  if (found) return found;
10679
- const here = path30.dirname(new URL(import.meta.url).pathname);
10897
+ const here = path31.dirname(new URL(import.meta.url).pathname);
10680
10898
  const candidates = [
10681
- path30.join(here, "executables", profileName, "profile.json"),
10899
+ path31.join(here, "executables", profileName, "profile.json"),
10682
10900
  // same-dir sibling (dev)
10683
- path30.join(here, "..", "executables", profileName, "profile.json"),
10901
+ path31.join(here, "..", "executables", profileName, "profile.json"),
10684
10902
  // up one (prod: dist/bin → dist/executables)
10685
- path30.join(here, "..", "src", "executables", profileName, "profile.json")
10903
+ path31.join(here, "..", "src", "executables", profileName, "profile.json")
10686
10904
  // fallback
10687
10905
  ];
10688
10906
  for (const c of candidates) {
10689
- if (fs33.existsSync(c)) return c;
10907
+ if (fs34.existsSync(c)) return c;
10690
10908
  }
10691
10909
  return candidates[0];
10692
10910
  }
@@ -10786,8 +11004,8 @@ function resolveShellTimeoutMs(entry) {
10786
11004
  var SIGKILL_GRACE_MS = 5e3;
10787
11005
  async function runShellEntry(entry, ctx, profile) {
10788
11006
  const shellName = entry.shell;
10789
- const shellPath = path30.join(profile.dir, shellName);
10790
- if (!fs33.existsSync(shellPath)) {
11007
+ const shellPath = path31.join(profile.dir, shellName);
11008
+ if (!fs34.existsSync(shellPath)) {
10791
11009
  ctx.skipAgent = true;
10792
11010
  ctx.output.exitCode = 99;
10793
11011
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -11266,9 +11484,9 @@ function resolveAuthToken(env = process.env) {
11266
11484
  return token;
11267
11485
  }
11268
11486
  function detectPackageManager2(cwd) {
11269
- if (fs34.existsSync(path31.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11270
- if (fs34.existsSync(path31.join(cwd, "yarn.lock"))) return "yarn";
11271
- if (fs34.existsSync(path31.join(cwd, "bun.lockb"))) return "bun";
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";
11272
11490
  return "npm";
11273
11491
  }
11274
11492
  function shellOut(cmd, args, cwd, stream = true) {
@@ -11355,11 +11573,11 @@ function configureGitIdentity(cwd) {
11355
11573
  }
11356
11574
  function postFailureTail(issueNumber, cwd, reason) {
11357
11575
  if (!issueNumber) return;
11358
- const logPath = path31.join(cwd, ".kody", "last-run.jsonl");
11576
+ const logPath = path32.join(cwd, ".kody", "last-run.jsonl");
11359
11577
  let tail = "";
11360
11578
  try {
11361
- if (fs34.existsSync(logPath)) {
11362
- const content = fs34.readFileSync(logPath, "utf-8");
11579
+ if (fs35.existsSync(logPath)) {
11580
+ const content = fs35.readFileSync(logPath, "utf-8");
11363
11581
  tail = content.slice(-3e3);
11364
11582
  }
11365
11583
  } catch {
@@ -11384,7 +11602,7 @@ async function runCi(argv) {
11384
11602
  return 0;
11385
11603
  }
11386
11604
  const args = parseCiArgs(argv);
11387
- const cwd = args.cwd ? path31.resolve(args.cwd) : process.cwd();
11605
+ const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
11388
11606
  let earlyConfig;
11389
11607
  try {
11390
11608
  earlyConfig = loadConfig(cwd);
@@ -11394,9 +11612,9 @@ async function runCi(argv) {
11394
11612
  const eventName = process.env.GITHUB_EVENT_NAME;
11395
11613
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
11396
11614
  let manualWorkflowDispatch = false;
11397
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs34.existsSync(dispatchEventPath)) {
11615
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs35.existsSync(dispatchEventPath)) {
11398
11616
  try {
11399
- const evt = JSON.parse(fs34.readFileSync(dispatchEventPath, "utf-8"));
11617
+ const evt = JSON.parse(fs35.readFileSync(dispatchEventPath, "utf-8"));
11400
11618
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
11401
11619
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
11402
11620
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -11655,9 +11873,9 @@ function parseChatArgs(argv, env = process.env) {
11655
11873
  return result;
11656
11874
  }
11657
11875
  function commitChatFiles(cwd, sessionId, verbose) {
11658
- const sessionFile = path32.relative(cwd, sessionFilePath(cwd, sessionId));
11659
- const eventsFile = path32.relative(cwd, eventsFilePath(cwd, sessionId));
11660
- const paths = [sessionFile, eventsFile].filter((p) => fs35.existsSync(path32.join(cwd, 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)));
11661
11879
  if (paths.length === 0) return;
11662
11880
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
11663
11881
  try {
@@ -11695,7 +11913,7 @@ async function runChat(argv) {
11695
11913
  ${CHAT_HELP}`);
11696
11914
  return 64;
11697
11915
  }
11698
- const cwd = args.cwd ? path32.resolve(args.cwd) : process.cwd();
11916
+ const cwd = args.cwd ? path33.resolve(args.cwd) : process.cwd();
11699
11917
  const sessionId = args.sessionId;
11700
11918
  const unpackedSecrets = unpackAllSecrets();
11701
11919
  if (unpackedSecrets > 0) {
@@ -11747,7 +11965,7 @@ ${CHAT_HELP}`);
11747
11965
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
11748
11966
  const meta = readMeta(sessionFile);
11749
11967
  process.stdout.write(
11750
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs35.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11968
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs36.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
11751
11969
  `
11752
11970
  );
11753
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.79",
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
- "scripts": {
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
+ }