@kody-ade/kody-engine 0.4.94 → 0.4.96

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
@@ -313,14 +313,14 @@ var init_verifyMcp = __esm({
313
313
  });
314
314
 
315
315
  // src/issue.ts
316
- import { execFileSync as execFileSync3 } from "child_process";
316
+ import { execFileSync } from "child_process";
317
317
  function ghToken() {
318
318
  return process.env.GH_PAT?.trim() || process.env.GH_TOKEN;
319
319
  }
320
320
  function gh(args, options) {
321
321
  const token = ghToken();
322
322
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
323
- return execFileSync3("gh", args, {
323
+ return execFileSync("gh", args, {
324
324
  encoding: "utf-8",
325
325
  timeout: API_TIMEOUT_MS,
326
326
  cwd: options?.cwd,
@@ -626,28 +626,28 @@ var loadMemoryContext_exports = {};
626
626
  __export(loadMemoryContext_exports, {
627
627
  loadMemoryContext: () => loadMemoryContext
628
628
  });
629
- import * as fs31 from "fs";
630
- import * as path29 from "path";
629
+ import * as fs32 from "fs";
630
+ import * as path30 from "path";
631
631
  function collectPages(memoryAbs) {
632
632
  const out = [];
633
633
  walkMd(memoryAbs, (file) => {
634
634
  let stat;
635
635
  try {
636
- stat = fs31.statSync(file);
636
+ stat = fs32.statSync(file);
637
637
  } catch {
638
638
  return;
639
639
  }
640
640
  let raw;
641
641
  try {
642
- raw = fs31.readFileSync(file, "utf-8");
642
+ raw = fs32.readFileSync(file, "utf-8");
643
643
  } catch {
644
644
  return;
645
645
  }
646
646
  const fm = raw.match(/^---\s*\n([\s\S]*?)\n---/);
647
- const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path29.basename(file, ".md");
647
+ const title = fm?.[1]?.match(/^title:\s*(.+)$/m)?.[1]?.trim() ?? path30.basename(file, ".md");
648
648
  const updated = fm?.[1]?.match(/^updated:\s*([0-9T:.+\-Z]+)/m)?.[1]?.trim() ?? "";
649
649
  out.push({
650
- relPath: path29.relative(memoryAbs, file),
650
+ relPath: path30.relative(memoryAbs, file),
651
651
  title,
652
652
  updated,
653
653
  content: raw.length > PER_PAGE_MAX_BYTES ? raw.slice(0, PER_PAGE_MAX_BYTES) + TRUNCATED_SUFFIX : raw,
@@ -715,16 +715,16 @@ function walkMd(root, visit) {
715
715
  const dir = stack.pop();
716
716
  let names;
717
717
  try {
718
- names = fs31.readdirSync(dir);
718
+ names = fs32.readdirSync(dir);
719
719
  } catch {
720
720
  continue;
721
721
  }
722
722
  for (const name of names) {
723
723
  if (name.startsWith(".")) continue;
724
- const full = path29.join(dir, name);
724
+ const full = path30.join(dir, name);
725
725
  let stat;
726
726
  try {
727
- stat = fs31.statSync(full);
727
+ stat = fs32.statSync(full);
728
728
  } catch {
729
729
  continue;
730
730
  }
@@ -747,8 +747,8 @@ var init_loadMemoryContext = __esm({
747
747
  TRUNCATED_SUFFIX = "\n\n\u2026 (truncated)";
748
748
  loadMemoryContext = async (ctx) => {
749
749
  if (typeof ctx.data.memoryContext === "string") return;
750
- const memoryAbs = path29.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
- if (!fs31.existsSync(memoryAbs)) {
750
+ const memoryAbs = path30.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
+ if (!fs32.existsSync(memoryAbs)) {
752
752
  ctx.data.memoryContext = "";
753
753
  return;
754
754
  }
@@ -877,7 +877,7 @@ var init_loadPriorArt = __esm({
877
877
  // package.json
878
878
  var package_default = {
879
879
  name: "@kody-ade/kody-engine",
880
- version: "0.4.94",
880
+ version: "0.4.96",
881
881
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
882
882
  license: "MIT",
883
883
  type: "module",
@@ -932,8 +932,8 @@ var package_default = {
932
932
 
933
933
  // src/chat-cli.ts
934
934
  import { execFileSync as execFileSync31 } from "child_process";
935
- import * as fs37 from "fs";
936
- import * as path34 from "path";
935
+ import * as fs38 from "fs";
936
+ import * as path35 from "path";
937
937
 
938
938
  // src/chat/events.ts
939
939
  import * as fs from "fs";
@@ -1988,13 +1988,13 @@ async function emit(sink, type, sessionId, suffix, payload) {
1988
1988
  }
1989
1989
 
1990
1990
  // src/chat/modes/interactive.ts
1991
- import { execFileSync as execFileSync2 } from "child_process";
1991
+ init_issue();
1992
+ import { execFileSync as execFileSync3 } from "child_process";
1992
1993
  import * as fs9 from "fs";
1993
- import * as os2 from "os";
1994
1994
  import * as path8 from "path";
1995
1995
 
1996
1996
  // src/chat/inbox.ts
1997
- import { execFileSync } from "child_process";
1997
+ import { execFileSync as execFileSync2 } from "child_process";
1998
1998
  var DEFAULT_POLL_MS = 3e3;
1999
1999
  async function waitForNextUserMessage(opts) {
2000
2000
  const pollMs = opts.pollIntervalMs ?? DEFAULT_POLL_MS;
@@ -2011,13 +2011,13 @@ async function waitForNextUserMessage(opts) {
2011
2011
  try {
2012
2012
  const branch = currentBranch(opts.cwd);
2013
2013
  if (branch) {
2014
- execFileSync("git", ["fetch", "--quiet", "origin", branch], { cwd: opts.cwd, stdio: "pipe" });
2015
- execFileSync("git", ["merge", "--ff-only", "--quiet", `origin/${branch}`], {
2014
+ execFileSync2("git", ["fetch", "--quiet", "origin", branch], { cwd: opts.cwd, stdio: "pipe" });
2015
+ execFileSync2("git", ["merge", "--ff-only", "--quiet", `origin/${branch}`], {
2016
2016
  cwd: opts.cwd,
2017
2017
  stdio: "pipe"
2018
2018
  });
2019
2019
  } else {
2020
- execFileSync("git", ["fetch", "--quiet", "--all"], { cwd: opts.cwd, stdio: "pipe" });
2020
+ execFileSync2("git", ["fetch", "--quiet", "--all"], { cwd: opts.cwd, stdio: "pipe" });
2021
2021
  }
2022
2022
  } catch (err) {
2023
2023
  const msg = err instanceof Error ? err.message : String(err);
@@ -2043,7 +2043,7 @@ function sleep(ms) {
2043
2043
  }
2044
2044
  function currentBranch(cwd) {
2045
2045
  try {
2046
- const out = execFileSync("git", ["symbolic-ref", "--short", "HEAD"], {
2046
+ const out = execFileSync2("git", ["symbolic-ref", "--short", "HEAD"], {
2047
2047
  cwd,
2048
2048
  stdio: ["ignore", "pipe", "ignore"]
2049
2049
  });
@@ -2068,8 +2068,10 @@ async function runInteractiveMode(opts) {
2068
2068
  const repository = process.env.GITHUB_REPOSITORY;
2069
2069
  const serverUrl = process.env.GITHUB_SERVER_URL ?? "https://github.com";
2070
2070
  const runUrl = runId && repository ? `${serverUrl}/${repository}/actions/runs/${runId}` : void 0;
2071
- process.stdout.write(`\u2192 kody:chat:interactive: emitting chat.ready (idleExitMs=${idleExitMs}, hardCapMs=${hardCapMs}, runUrl=${runUrl ?? "n/a"})
2072
- `);
2071
+ process.stdout.write(
2072
+ `\u2192 kody:chat:interactive: emitting chat.ready (idleExitMs=${idleExitMs}, hardCapMs=${hardCapMs}, runUrl=${runUrl ?? "n/a"})
2073
+ `
2074
+ );
2073
2075
  await emit2(opts.sink, "chat.ready", opts.sessionId, "ready", {
2074
2076
  sessionId: opts.sessionId,
2075
2077
  startedAt: new Date(startedAt).toISOString(),
@@ -2147,117 +2149,89 @@ function findNextUserTurn(turns, fromIdx) {
2147
2149
  if (turns.length > 0 && turns[turns.length - 1].role === "user") return turns.length - 1;
2148
2150
  return -1;
2149
2151
  }
2150
- function commitTurn(cwd, sessionId, verbose) {
2152
+ function commitTurn(cwd, sessionId, _verbose) {
2151
2153
  const sessionRel = path8.relative(cwd, sessionFilePath(cwd, sessionId));
2152
2154
  const eventsRel = path8.relative(cwd, eventsFilePath(cwd, sessionId));
2153
- const paths = [sessionRel, eventsRel].filter((p) => fs9.existsSync(path8.join(cwd, p)));
2154
- if (paths.length === 0) return;
2155
- const startBranch = currentBranch2(cwd);
2156
- const eventsBranch = defaultBranch(cwd) ?? "main";
2157
- if (startBranch === eventsBranch) {
2158
- commitPathsAndPush(cwd, paths, sessionId, verbose, "HEAD");
2155
+ const rels = [sessionRel, eventsRel].filter((p) => fs9.existsSync(path8.join(cwd, p)));
2156
+ if (rels.length === 0) return;
2157
+ const repository = process.env.GITHUB_REPOSITORY;
2158
+ if (!repository) {
2159
+ process.stderr.write(
2160
+ `[kody:chat:interactive] GITHUB_REPOSITORY unset; cannot persist session/events via Contents API
2161
+ `
2162
+ );
2159
2163
  return;
2160
2164
  }
2161
- const stdio = verbose ? "inherit" : "pipe";
2162
- const exec = (args) => execFileSync2("git", args, { cwd, stdio });
2163
- const worktreeDir = fs9.mkdtempSync(path8.join(os2.tmpdir(), "kody-events-"));
2164
- let worktreeAdded = false;
2165
- try {
2166
- exec(["fetch", "--quiet", "origin", eventsBranch]);
2167
- exec(["worktree", "add", "--detach", "--quiet", worktreeDir, `origin/${eventsBranch}`]);
2168
- worktreeAdded = true;
2169
- for (const rel of paths) {
2170
- const src = path8.join(cwd, rel);
2171
- const dst = path8.join(worktreeDir, rel);
2172
- fs9.mkdirSync(path8.dirname(dst), { recursive: true });
2173
- fs9.copyFileSync(src, dst);
2174
- }
2175
- commitPathsAndPush(worktreeDir, paths, sessionId, verbose, `HEAD:${eventsBranch}`);
2176
- } catch (err) {
2177
- const msg = err instanceof Error ? err.message : String(err);
2178
- process.stderr.write(`[kody:chat:interactive] worktree commit failed: ${msg}
2179
- `);
2180
- } finally {
2181
- if (worktreeAdded) {
2182
- try {
2183
- exec(["worktree", "remove", "--force", "--quiet", worktreeDir]);
2184
- } catch {
2185
- }
2186
- }
2187
- try {
2188
- fs9.rmSync(worktreeDir, { recursive: true, force: true });
2189
- } catch {
2190
- }
2165
+ const branch = defaultBranch(cwd) ?? "main";
2166
+ for (const rel of rels) {
2167
+ const repoPath = rel.split(path8.sep).join("/");
2168
+ const localText = fs9.readFileSync(path8.join(cwd, rel), "utf-8");
2169
+ putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
2191
2170
  }
2192
2171
  }
2193
- function commitPathsAndPush(cwd, paths, sessionId, verbose, pushSpec) {
2194
- const stdio = verbose ? "inherit" : "pipe";
2195
- const exec = (args) => execFileSync2("git", args, { cwd, stdio });
2172
+ function jsonlLines(text) {
2173
+ return text.split("\n").filter((l) => l.length > 0);
2174
+ }
2175
+ function getRemoteBlob(repository, branch, repoPath, cwd) {
2196
2176
  try {
2197
- exec(["add", "-f", ...paths]);
2198
- exec(["commit", "--quiet", "-m", `chat: interactive turn for ${sessionId}`]);
2177
+ const raw = gh(["api", `/repos/${repository}/contents/${repoPath}?ref=${encodeURIComponent(branch)}`], { cwd });
2178
+ const o = JSON.parse(raw);
2179
+ if (o.type === "file" && o.encoding === "base64" && typeof o.content === "string" && typeof o.sha === "string") {
2180
+ return { sha: o.sha, lines: jsonlLines(Buffer.from(o.content, "base64").toString("utf-8")) };
2181
+ }
2182
+ return { sha: null, lines: [] };
2199
2183
  } catch (err) {
2200
2184
  const msg = err instanceof Error ? err.message : String(err);
2201
- process.stderr.write(`[kody:chat:interactive] commit failed: ${msg}
2202
- `);
2203
- return;
2185
+ if (/HTTP 404/i.test(msg) || /Not Found/i.test(msg)) return { sha: null, lines: [] };
2186
+ throw err;
2204
2187
  }
2188
+ }
2189
+ function putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd) {
2205
2190
  for (let attempt = 1; attempt <= 3; attempt++) {
2191
+ const remote = getRemoteBlob(repository, branch, repoPath, cwd);
2192
+ let body = localText;
2193
+ if (attempt > 1 && remote.lines.length > 0) {
2194
+ const localLines = jsonlLines(localText);
2195
+ const localSet = new Set(localLines);
2196
+ const extra = remote.lines.filter((l) => !localSet.has(l));
2197
+ if (extra.length > 0) body = [...localLines, ...extra].join("\n") + "\n";
2198
+ }
2199
+ const payload = {
2200
+ message: `chat: interactive turn for ${sessionId}`,
2201
+ content: Buffer.from(body, "utf-8").toString("base64"),
2202
+ branch
2203
+ };
2204
+ if (remote.sha) payload.sha = remote.sha;
2206
2205
  try {
2207
- exec(["push", "--quiet", "origin", pushSpec]);
2206
+ gh(["api", "--method", "PUT", `/repos/${repository}/contents/${repoPath}`, "--input", "-"], {
2207
+ cwd,
2208
+ input: JSON.stringify(payload)
2209
+ });
2208
2210
  return;
2209
2211
  } catch (err) {
2210
2212
  const msg = err instanceof Error ? err.message : String(err);
2211
- const isNonFf = /non-fast-forward|fetch first|rejected/i.test(msg);
2212
- if (!isNonFf || attempt === 3) {
2213
- process.stderr.write(`[kody:chat:interactive] push failed (attempt ${attempt}): ${msg}
2214
- `);
2215
- return;
2216
- }
2217
- process.stderr.write(`[kody:chat:interactive] push rejected (attempt ${attempt}); fetch+rebase+retry
2218
- `);
2219
- try {
2220
- exec(["fetch", "--quiet", "origin"]);
2221
- const upstream = pushSpec.includes(":") ? `origin/${pushSpec.split(":")[1]}` : (() => {
2222
- const branch = currentBranch2(cwd);
2223
- return branch ? `origin/${branch}` : null;
2224
- })();
2225
- if (!upstream) {
2226
- process.stderr.write(`[kody:chat:interactive] cannot rebase: no upstream resolved
2227
- `);
2228
- return;
2229
- }
2230
- exec(["rebase", "--quiet", upstream]);
2231
- } catch (rebaseErr) {
2232
- const rmsg = rebaseErr instanceof Error ? rebaseErr.message : String(rebaseErr);
2233
- process.stderr.write(`[kody:chat:interactive] rebase failed: ${rmsg}
2213
+ const isConflict = /HTTP 409/i.test(msg) || /HTTP 422/i.test(msg) || /does not match|but expected/i.test(msg);
2214
+ if (!isConflict || attempt === 3) {
2215
+ process.stderr.write(`[kody:chat:interactive] Contents PUT failed (${repoPath}, attempt ${attempt}): ${msg}
2234
2216
  `);
2235
2217
  return;
2236
2218
  }
2219
+ process.stderr.write(
2220
+ `[kody:chat:interactive] Contents PUT conflict (${repoPath}, attempt ${attempt}); refetch+union+retry
2221
+ `
2222
+ );
2237
2223
  }
2238
2224
  }
2239
2225
  }
2240
2226
  function defaultBranch(cwd) {
2241
2227
  try {
2242
- const out = execFileSync2(
2243
- "git",
2244
- ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"],
2245
- { cwd, stdio: ["ignore", "pipe", "ignore"] }
2246
- );
2247
- const symbolic = out.toString("utf-8").trim();
2248
- if (symbolic.startsWith("origin/")) return symbolic.slice("origin/".length);
2249
- return symbolic || null;
2250
- } catch {
2251
- return null;
2252
- }
2253
- }
2254
- function currentBranch2(cwd) {
2255
- try {
2256
- const out = execFileSync2("git", ["symbolic-ref", "--short", "HEAD"], {
2228
+ const out = execFileSync3("git", ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], {
2257
2229
  cwd,
2258
2230
  stdio: ["ignore", "pipe", "ignore"]
2259
2231
  });
2260
- return out.toString("utf-8").trim() || null;
2232
+ const symbolic = out.toString("utf-8").trim();
2233
+ if (symbolic.startsWith("origin/")) return symbolic.slice("origin/".length);
2234
+ return symbolic || null;
2261
2235
  } catch {
2262
2236
  return null;
2263
2237
  }
@@ -2281,8 +2255,8 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2281
2255
 
2282
2256
  // src/kody-cli.ts
2283
2257
  import { execFileSync as execFileSync30 } from "child_process";
2284
- import * as fs36 from "fs";
2285
- import * as path33 from "path";
2258
+ import * as fs37 from "fs";
2259
+ import * as path34 from "path";
2286
2260
 
2287
2261
  // src/dispatch.ts
2288
2262
  import * as fs10 from "fs";
@@ -2604,8 +2578,8 @@ init_issue();
2604
2578
 
2605
2579
  // src/executor.ts
2606
2580
  import { execFileSync as execFileSync29, spawn as spawn6 } from "child_process";
2607
- import * as fs35 from "fs";
2608
- import * as path32 from "path";
2581
+ import * as fs36 from "fs";
2582
+ import * as path33 from "path";
2609
2583
  init_events();
2610
2584
 
2611
2585
  // src/lifecycleLabels.ts
@@ -3229,7 +3203,7 @@ function errMsg(err) {
3229
3203
  // src/litellm.ts
3230
3204
  import { execFileSync as execFileSync4, spawn as spawn2 } from "child_process";
3231
3205
  import * as fs12 from "fs";
3232
- import * as os3 from "os";
3206
+ import * as os2 from "os";
3233
3207
  import * as path10 from "path";
3234
3208
  async function checkLitellmHealth(url) {
3235
3209
  try {
@@ -3277,13 +3251,13 @@ async function startLitellmIfNeeded(model, projectDir, url = LITELLM_DEFAULT_URL
3277
3251
  throw new Error("litellm not installed \u2014 run: pip install 'litellm[proxy]'");
3278
3252
  }
3279
3253
  }
3280
- const configPath = path10.join(os3.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3254
+ const configPath = path10.join(os2.tmpdir(), `kody-litellm-${Date.now()}.yaml`);
3281
3255
  fs12.writeFileSync(configPath, generateLitellmConfigYaml(model));
3282
3256
  const portMatch = url.match(/:(\d+)/);
3283
3257
  const port = portMatch ? portMatch[1] : "4000";
3284
3258
  const args = cmd === "litellm" ? ["--config", configPath, "--port", port] : ["-m", "litellm", "--config", configPath, "--port", port];
3285
3259
  const dotenvVars = readDotenvApiKeys(projectDir);
3286
- const logPath = path10.join(os3.tmpdir(), `kody-litellm-${Date.now()}.log`);
3260
+ const logPath = path10.join(os2.tmpdir(), `kody-litellm-${Date.now()}.log`);
3287
3261
  const outFd = fs12.openSync(logPath, "w");
3288
3262
  const child = spawn2(cmd, args, {
3289
3263
  stdio: ["ignore", outFd, outFd],
@@ -4244,7 +4218,7 @@ var brainServe = async (ctx) => {
4244
4218
 
4245
4219
  // src/scripts/buildSyntheticPlugin.ts
4246
4220
  import * as fs16 from "fs";
4247
- import * as os4 from "os";
4221
+ import * as os3 from "os";
4248
4222
  import * as path14 from "path";
4249
4223
  function getPluginsCatalogRoot() {
4250
4224
  const here = path14.dirname(new URL(import.meta.url).pathname);
@@ -4267,7 +4241,7 @@ var buildSyntheticPlugin = async (ctx, profile) => {
4267
4241
  if (!needsSynthetic) return;
4268
4242
  const catalog = getPluginsCatalogRoot();
4269
4243
  const runId = `${profile.name}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
4270
- const root = path14.join(os4.tmpdir(), `kody-synth-${runId}`);
4244
+ const root = path14.join(os3.tmpdir(), `kody-synth-${runId}`);
4271
4245
  fs16.mkdirSync(path14.join(root, ".claude-plugin"), { recursive: true });
4272
4246
  const resolvePart = (bucket, entry) => {
4273
4247
  const local = path14.join(profile.dir, bucket, entry);
@@ -5490,10 +5464,10 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
5490
5464
  // src/scripts/diagMcp.ts
5491
5465
  import { execFileSync as execFileSync11 } from "child_process";
5492
5466
  import * as fs21 from "fs";
5493
- import * as os5 from "os";
5467
+ import * as os4 from "os";
5494
5468
  import * as path20 from "path";
5495
5469
  var diagMcp = async (_ctx) => {
5496
- const home = os5.homedir();
5470
+ const home = os4.homedir();
5497
5471
  const cacheDir = path20.join(home, ".cache", "ms-playwright");
5498
5472
  let entries = [];
5499
5473
  try {
@@ -6168,6 +6142,8 @@ function parseFlatYaml(text) {
6168
6142
  const lower = value.toLowerCase();
6169
6143
  if (lower === "true") out.disabled = true;
6170
6144
  else if (lower === "false") out.disabled = false;
6145
+ } else if (key === "worker" && value.length > 0) {
6146
+ out.worker = value;
6171
6147
  }
6172
6148
  }
6173
6149
  return out;
@@ -6554,6 +6530,14 @@ var dispatchJobFileTicks = async (ctx, _profile, args) => {
6554
6530
  results.push({ slug, exitCode: 0, skipped: true, reason: "disabled" });
6555
6531
  continue;
6556
6532
  }
6533
+ if (!frontmatter.worker || frontmatter.worker.trim().length === 0) {
6534
+ process.stderr.write(
6535
+ `[jobs] \u23ED skip ${slug}: no worker assigned (add 'worker: <slug>' frontmatter)
6536
+ `
6537
+ );
6538
+ results.push({ slug, exitCode: 0, skipped: true, reason: "no worker assigned" });
6539
+ continue;
6540
+ }
6557
6541
  const decision = await decideShouldFire(frontmatter.every, slug, backend, now);
6558
6542
  if (decision.skip) {
6559
6543
  process.stdout.write(`[jobs] \u23ED skip ${slug}: ${decision.reason}
@@ -7999,10 +7983,10 @@ import * as fs29 from "fs";
7999
7983
  import * as path27 from "path";
8000
7984
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
8001
7985
  var GoalStateError = class extends Error {
8002
- constructor(path35, message) {
8003
- super(`Invalid goal state at ${path35}:
7986
+ constructor(path36, message) {
7987
+ super(`Invalid goal state at ${path36}:
8004
7988
  ${message}`);
8005
- this.path = path35;
7989
+ this.path = path36;
8006
7990
  this.name = "GoalStateError";
8007
7991
  }
8008
7992
  path;
@@ -8169,6 +8153,7 @@ import * as fs30 from "fs";
8169
8153
  import * as path28 from "path";
8170
8154
  var loadJobFromFile = async (ctx, _profile, args) => {
8171
8155
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
8156
+ const workersDir = String(args?.workersDir ?? ".kody/workers");
8172
8157
  const slugArg = String(args?.slugArg ?? "job");
8173
8158
  const slug = String(ctx.args[slugArg] ?? "").trim();
8174
8159
  if (!slug) {
@@ -8180,6 +8165,21 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8180
8165
  }
8181
8166
  const raw = fs30.readFileSync(absPath, "utf-8");
8182
8167
  const { title, body } = parseJobFile(raw, slug);
8168
+ const workerSlug = (splitFrontmatter(raw).frontmatter.worker ?? "").trim();
8169
+ let workerTitle = "";
8170
+ let workerPersona = "";
8171
+ if (workerSlug) {
8172
+ const workerPath = path28.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8173
+ if (!fs30.existsSync(workerPath)) {
8174
+ throw new Error(
8175
+ `loadJobFromFile: job '${slug}' declares worker '${workerSlug}' but ${workerPath} does not exist`
8176
+ );
8177
+ }
8178
+ const workerRaw = fs30.readFileSync(workerPath, "utf-8");
8179
+ const parsed = parseJobFile(workerRaw, workerSlug);
8180
+ workerTitle = parsed.title;
8181
+ workerPersona = parsed.body;
8182
+ }
8183
8183
  const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
8184
8184
  const loaded = await backend.load(slug);
8185
8185
  ctx.data.jobSlug = slug;
@@ -8187,6 +8187,9 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8187
8187
  ctx.data.jobIntent = body;
8188
8188
  ctx.data.jobState = loaded;
8189
8189
  ctx.data.jobStateJson = JSON.stringify(loaded.state, null, 2);
8190
+ ctx.data.workerSlug = workerSlug;
8191
+ ctx.data.workerTitle = workerTitle;
8192
+ ctx.data.workerPersona = workerPersona;
8190
8193
  };
8191
8194
  function parseJobFile(raw, slug) {
8192
8195
  let stripped = raw;
@@ -8209,6 +8212,79 @@ function humanizeSlug(slug) {
8209
8212
  return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
8210
8213
  }
8211
8214
 
8215
+ // src/scripts/loadWorkerAdhoc.ts
8216
+ import * as fs31 from "fs";
8217
+ import * as path29 from "path";
8218
+ var loadWorkerAdhoc = async (ctx, _profile, args) => {
8219
+ const workersDir = String(args?.workersDir ?? ".kody/workers");
8220
+ const workerSlug = String(ctx.args.worker ?? "").trim();
8221
+ if (!workerSlug) {
8222
+ throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
8223
+ }
8224
+ const workerPath = path29.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8225
+ if (!fs31.existsSync(workerPath)) {
8226
+ throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
8227
+ }
8228
+ const { title, body } = parsePersona(fs31.readFileSync(workerPath, "utf-8"), workerSlug);
8229
+ const message = resolveMessage(ctx.args.message);
8230
+ if (!message) {
8231
+ throw new Error(
8232
+ "loadWorkerAdhoc: no message \u2014 neither the dispatching comment body nor ctx.args.message provided one"
8233
+ );
8234
+ }
8235
+ ctx.data.workerSlug = workerSlug;
8236
+ ctx.data.workerTitle = title;
8237
+ ctx.data.workerPersona = body;
8238
+ ctx.data.message = message;
8239
+ ctx.data.thread = String(ctx.args.thread ?? "").trim();
8240
+ };
8241
+ function resolveMessage(messageArg) {
8242
+ const fromComment = readCommentBody();
8243
+ if (fromComment) return stripDirective(fromComment);
8244
+ return String(messageArg ?? "").trim();
8245
+ }
8246
+ function readCommentBody() {
8247
+ const eventPath = process.env.GITHUB_EVENT_PATH;
8248
+ if (!eventPath || !fs31.existsSync(eventPath)) return "";
8249
+ try {
8250
+ const event = JSON.parse(fs31.readFileSync(eventPath, "utf-8"));
8251
+ return String(event.comment?.body ?? "");
8252
+ } catch {
8253
+ return "";
8254
+ }
8255
+ }
8256
+ function stripDirective(body) {
8257
+ const lines = body.split("\n");
8258
+ let start = 0;
8259
+ while (start < lines.length) {
8260
+ const line = lines[start].trim();
8261
+ if (line.length === 0) {
8262
+ start++;
8263
+ continue;
8264
+ }
8265
+ if (/@kody\s+worker-ask\b/i.test(line)) {
8266
+ start++;
8267
+ continue;
8268
+ }
8269
+ break;
8270
+ }
8271
+ return lines.slice(start).join("\n").trim();
8272
+ }
8273
+ function parsePersona(raw, slug) {
8274
+ const stripped = splitFrontmatter(raw).body;
8275
+ const trimmed = stripped.trim();
8276
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
8277
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
8278
+ if (h1) {
8279
+ const rest = trimmed.slice(firstLine2.length).replace(/^\n+/, "");
8280
+ return { title: h1[1].trim(), body: rest };
8281
+ }
8282
+ return { title: humanizeSlug2(slug), body: trimmed };
8283
+ }
8284
+ function humanizeSlug2(slug) {
8285
+ return slug.split(/[-_]+/).filter((s) => s.length > 0).map((s) => s[0].toUpperCase() + s.slice(1)).join(" ");
8286
+ }
8287
+
8212
8288
  // src/scripts/index.ts
8213
8289
  init_loadMemoryContext();
8214
8290
  init_loadPriorArt();
@@ -8217,8 +8293,8 @@ init_loadPriorArt();
8217
8293
  init_events();
8218
8294
 
8219
8295
  // src/taskContext.ts
8220
- import * as fs32 from "fs";
8221
- import * as path30 from "path";
8296
+ import * as fs33 from "fs";
8297
+ import * as path31 from "path";
8222
8298
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
8223
8299
  function buildTaskContext(args) {
8224
8300
  return {
@@ -8234,10 +8310,10 @@ function buildTaskContext(args) {
8234
8310
  }
8235
8311
  function persistTaskContext(cwd, ctx) {
8236
8312
  try {
8237
- const dir = path30.join(cwd, ".kody", "runs", ctx.runId);
8238
- fs32.mkdirSync(dir, { recursive: true });
8239
- const file = path30.join(dir, "task-context.json");
8240
- fs32.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8313
+ const dir = path31.join(cwd, ".kody", "runs", ctx.runId);
8314
+ fs33.mkdirSync(dir, { recursive: true });
8315
+ const file = path31.join(dir, "task-context.json");
8316
+ fs33.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8241
8317
  `);
8242
8318
  return file;
8243
8319
  } catch (err) {
@@ -9600,8 +9676,8 @@ function resolveBaseOverride(value) {
9600
9676
 
9601
9677
  // src/scripts/runTickScript.ts
9602
9678
  import { spawnSync } from "child_process";
9603
- import * as fs33 from "fs";
9604
- import * as path31 from "path";
9679
+ import * as fs34 from "fs";
9680
+ import * as path32 from "path";
9605
9681
  var runTickScript = async (ctx, _profile, args) => {
9606
9682
  ctx.skipAgent = true;
9607
9683
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -9613,13 +9689,13 @@ var runTickScript = async (ctx, _profile, args) => {
9613
9689
  ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
9614
9690
  return;
9615
9691
  }
9616
- const jobPath = path31.join(ctx.cwd, jobsDir, `${slug}.md`);
9617
- if (!fs33.existsSync(jobPath)) {
9692
+ const jobPath = path32.join(ctx.cwd, jobsDir, `${slug}.md`);
9693
+ if (!fs34.existsSync(jobPath)) {
9618
9694
  ctx.output.exitCode = 99;
9619
9695
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
9620
9696
  return;
9621
9697
  }
9622
- const raw = fs33.readFileSync(jobPath, "utf-8");
9698
+ const raw = fs34.readFileSync(jobPath, "utf-8");
9623
9699
  const { frontmatter } = splitFrontmatter(raw);
9624
9700
  const tickScript = frontmatter.tickScript;
9625
9701
  if (!tickScript) {
@@ -9627,8 +9703,8 @@ var runTickScript = async (ctx, _profile, args) => {
9627
9703
  ctx.output.reason = `runTickScript: job ${slug} has no \`tickScript:\` frontmatter \u2014 route via job-tick instead`;
9628
9704
  return;
9629
9705
  }
9630
- const scriptPath = path31.isAbsolute(tickScript) ? tickScript : path31.join(ctx.cwd, tickScript);
9631
- if (!fs33.existsSync(scriptPath)) {
9706
+ const scriptPath = path32.isAbsolute(tickScript) ? tickScript : path32.join(ctx.cwd, tickScript);
9707
+ if (!fs34.existsSync(scriptPath)) {
9632
9708
  ctx.output.exitCode = 99;
9633
9709
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
9634
9710
  return;
@@ -10650,7 +10726,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
10650
10726
  };
10651
10727
 
10652
10728
  // src/scripts/writeRunSummary.ts
10653
- import * as fs34 from "fs";
10729
+ import * as fs35 from "fs";
10654
10730
  var writeRunSummary = async (ctx, profile) => {
10655
10731
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
10656
10732
  if (!summaryPath) return;
@@ -10672,7 +10748,7 @@ var writeRunSummary = async (ctx, profile) => {
10672
10748
  if (reason) lines.push(`- **Reason:** ${reason}`);
10673
10749
  lines.push("");
10674
10750
  try {
10675
- fs34.appendFileSync(summaryPath, `${lines.join("\n")}
10751
+ fs35.appendFileSync(summaryPath, `${lines.join("\n")}
10676
10752
  `);
10677
10753
  } catch {
10678
10754
  }
@@ -10693,6 +10769,7 @@ var preflightScripts = {
10693
10769
  loadIssueContext,
10694
10770
  loadIssueStateComment,
10695
10771
  loadJobFromFile,
10772
+ loadWorkerAdhoc,
10696
10773
  loadConventions,
10697
10774
  loadCoverageRules,
10698
10775
  loadMemoryContext,
@@ -10897,9 +10974,9 @@ async function runExecutable(profileName, input) {
10897
10974
  data: { ...input.preloadedData ?? {} },
10898
10975
  output: { exitCode: 0 }
10899
10976
  };
10900
- const ndjsonDir = path32.join(input.cwd, ".kody");
10977
+ const ndjsonDir = path33.join(input.cwd, ".kody");
10901
10978
  const invokeAgent = async (prompt) => {
10902
- const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path32.isAbsolute(p) ? p : path32.resolve(profile.dir, p)).filter((p) => p.length > 0);
10979
+ const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path33.isAbsolute(p) ? p : path33.resolve(profile.dir, p)).filter((p) => p.length > 0);
10903
10980
  const syntheticPath = ctx.data.syntheticPluginPath;
10904
10981
  const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
10905
10982
  return runAgent({
@@ -11094,7 +11171,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
11094
11171
  function getProfileInputsForChild(profileName, _cwd) {
11095
11172
  try {
11096
11173
  const profilePath = resolveProfilePath(profileName);
11097
- if (!fs35.existsSync(profilePath)) return null;
11174
+ if (!fs36.existsSync(profilePath)) return null;
11098
11175
  return loadProfile(profilePath).inputs;
11099
11176
  } catch {
11100
11177
  return null;
@@ -11103,17 +11180,17 @@ function getProfileInputsForChild(profileName, _cwd) {
11103
11180
  function resolveProfilePath(profileName) {
11104
11181
  const found = resolveExecutable(profileName);
11105
11182
  if (found) return found;
11106
- const here = path32.dirname(new URL(import.meta.url).pathname);
11183
+ const here = path33.dirname(new URL(import.meta.url).pathname);
11107
11184
  const candidates = [
11108
- path32.join(here, "executables", profileName, "profile.json"),
11185
+ path33.join(here, "executables", profileName, "profile.json"),
11109
11186
  // same-dir sibling (dev)
11110
- path32.join(here, "..", "executables", profileName, "profile.json"),
11187
+ path33.join(here, "..", "executables", profileName, "profile.json"),
11111
11188
  // up one (prod: dist/bin → dist/executables)
11112
- path32.join(here, "..", "src", "executables", profileName, "profile.json")
11189
+ path33.join(here, "..", "src", "executables", profileName, "profile.json")
11113
11190
  // fallback
11114
11191
  ];
11115
11192
  for (const c of candidates) {
11116
- if (fs35.existsSync(c)) return c;
11193
+ if (fs36.existsSync(c)) return c;
11117
11194
  }
11118
11195
  return candidates[0];
11119
11196
  }
@@ -11213,8 +11290,8 @@ function resolveShellTimeoutMs(entry) {
11213
11290
  var SIGKILL_GRACE_MS = 5e3;
11214
11291
  async function runShellEntry(entry, ctx, profile) {
11215
11292
  const shellName = entry.shell;
11216
- const shellPath = path32.join(profile.dir, shellName);
11217
- if (!fs35.existsSync(shellPath)) {
11293
+ const shellPath = path33.join(profile.dir, shellName);
11294
+ if (!fs36.existsSync(shellPath)) {
11218
11295
  ctx.skipAgent = true;
11219
11296
  ctx.output.exitCode = 99;
11220
11297
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -11693,9 +11770,9 @@ function resolveAuthToken(env = process.env) {
11693
11770
  return token;
11694
11771
  }
11695
11772
  function detectPackageManager2(cwd) {
11696
- if (fs36.existsSync(path33.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11697
- if (fs36.existsSync(path33.join(cwd, "yarn.lock"))) return "yarn";
11698
- if (fs36.existsSync(path33.join(cwd, "bun.lockb"))) return "bun";
11773
+ if (fs37.existsSync(path34.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
11774
+ if (fs37.existsSync(path34.join(cwd, "yarn.lock"))) return "yarn";
11775
+ if (fs37.existsSync(path34.join(cwd, "bun.lockb"))) return "bun";
11699
11776
  return "npm";
11700
11777
  }
11701
11778
  function shellOut(cmd, args, cwd, stream = true) {
@@ -11782,11 +11859,11 @@ function configureGitIdentity(cwd) {
11782
11859
  }
11783
11860
  function postFailureTail(issueNumber, cwd, reason) {
11784
11861
  if (!issueNumber) return;
11785
- const logPath = path33.join(cwd, ".kody", "last-run.jsonl");
11862
+ const logPath = path34.join(cwd, ".kody", "last-run.jsonl");
11786
11863
  let tail = "";
11787
11864
  try {
11788
- if (fs36.existsSync(logPath)) {
11789
- const content = fs36.readFileSync(logPath, "utf-8");
11865
+ if (fs37.existsSync(logPath)) {
11866
+ const content = fs37.readFileSync(logPath, "utf-8");
11790
11867
  tail = content.slice(-3e3);
11791
11868
  }
11792
11869
  } catch {
@@ -11811,7 +11888,7 @@ async function runCi(argv) {
11811
11888
  return 0;
11812
11889
  }
11813
11890
  const args = parseCiArgs(argv);
11814
- const cwd = args.cwd ? path33.resolve(args.cwd) : process.cwd();
11891
+ const cwd = args.cwd ? path34.resolve(args.cwd) : process.cwd();
11815
11892
  let earlyConfig;
11816
11893
  try {
11817
11894
  earlyConfig = loadConfig(cwd);
@@ -11821,9 +11898,9 @@ async function runCi(argv) {
11821
11898
  const eventName = process.env.GITHUB_EVENT_NAME;
11822
11899
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
11823
11900
  let manualWorkflowDispatch = false;
11824
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs36.existsSync(dispatchEventPath)) {
11901
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs37.existsSync(dispatchEventPath)) {
11825
11902
  try {
11826
- const evt = JSON.parse(fs36.readFileSync(dispatchEventPath, "utf-8"));
11903
+ const evt = JSON.parse(fs37.readFileSync(dispatchEventPath, "utf-8"));
11827
11904
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
11828
11905
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
11829
11906
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -12082,9 +12159,9 @@ function parseChatArgs(argv, env = process.env) {
12082
12159
  return result;
12083
12160
  }
12084
12161
  function commitChatFiles(cwd, sessionId, verbose) {
12085
- const sessionFile = path34.relative(cwd, sessionFilePath(cwd, sessionId));
12086
- const eventsFile = path34.relative(cwd, eventsFilePath(cwd, sessionId));
12087
- const paths = [sessionFile, eventsFile].filter((p) => fs37.existsSync(path34.join(cwd, p)));
12162
+ const sessionFile = path35.relative(cwd, sessionFilePath(cwd, sessionId));
12163
+ const eventsFile = path35.relative(cwd, eventsFilePath(cwd, sessionId));
12164
+ const paths = [sessionFile, eventsFile].filter((p) => fs38.existsSync(path35.join(cwd, p)));
12088
12165
  if (paths.length === 0) return;
12089
12166
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
12090
12167
  try {
@@ -12122,7 +12199,7 @@ async function runChat(argv) {
12122
12199
  ${CHAT_HELP}`);
12123
12200
  return 64;
12124
12201
  }
12125
- const cwd = args.cwd ? path34.resolve(args.cwd) : process.cwd();
12202
+ const cwd = args.cwd ? path35.resolve(args.cwd) : process.cwd();
12126
12203
  const sessionId = args.sessionId;
12127
12204
  const unpackedSecrets = unpackAllSecrets();
12128
12205
  if (unpackedSecrets > 0) {
@@ -12174,7 +12251,7 @@ ${CHAT_HELP}`);
12174
12251
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
12175
12252
  const meta = readMeta(sessionFile);
12176
12253
  process.stdout.write(
12177
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs37.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
12254
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs38.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
12178
12255
  `
12179
12256
  );
12180
12257
  try {
@@ -1,8 +1,14 @@
1
- You are **kody job-tick**, the coordinator for one file-based job. 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.
1
+ You are **{{workerTitle}}** (worker `{{workerSlug}}`), operating through **kody job-tick** the coordinator for one file-based job. 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
+ ## Who you are — worker persona (authoritative identity)
4
+
5
+ The job below assigns you, worker **`{{workerSlug}}`**, as its executor. This persona defines *who* runs the job: your authority, doctrine, voice, and hard limits. Where the persona's restrictions are stricter than the job body, **the persona wins** — a job can never grant you authority your worker persona withholds.
6
+
7
+ {{workerPersona}}
2
8
 
3
9
  ## The job
4
10
 
5
- Slug **`{{jobSlug}}`** — *{{jobTitle}}*. The job body below is authoritative: it states what success looks like, allowed commands, and restrictions. The job file is human-edited — re-read it every tick.
11
+ Slug **`{{jobSlug}}`** — *{{jobTitle}}*, assigned to worker **`{{workerSlug}}`**. The job body below is authoritative for *what* to do, *when* (cadence), allowed commands, and state schema. It is human-edited — re-read it every tick. Execute it **as** the persona above.
6
12
 
7
13
  ### Job body
8
14
 
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "worker-ask",
3
+ "role": "primitive",
4
+ "describe": "Ad-hoc one-shot: run a worker persona against an inline message + context (from a dashboard @worker mention). Stateless — no job file, no state, no commit. Replies into the originating thread.",
5
+ "kind": "oneshot",
6
+ "inputs": [
7
+ {
8
+ "name": "worker",
9
+ "flag": "--worker",
10
+ "type": "string",
11
+ "required": true,
12
+ "describe": "Worker slug — basename (without .md) of the persona file under .kody/workers/."
13
+ },
14
+ {
15
+ "name": "thread",
16
+ "flag": "--thread",
17
+ "type": "string",
18
+ "describe": "Where to post the reply: `discussion:<n>` (or a bare number) for a Discussion, or `issue:<n>` for an issue/PR. When set, the worker answers as a comment on that thread."
19
+ },
20
+ {
21
+ "name": "message",
22
+ "flag": "--message",
23
+ "type": "string",
24
+ "bindsCommentRest": true,
25
+ "describe": "Fallback inline message when no triggering comment body is available (CLI/testing). In production the message is read verbatim from the dispatching comment body."
26
+ }
27
+ ],
28
+ "claudeCode": {
29
+ "model": "inherit",
30
+ "permissionMode": "default",
31
+ "maxTurns": 20,
32
+ "maxThinkingTokens": null,
33
+ "systemPromptAppend": null,
34
+ "tools": ["Bash", "Read"],
35
+ "hooks": [],
36
+ "skills": [],
37
+ "commands": [],
38
+ "subagents": [],
39
+ "plugins": [],
40
+ "mcpServers": []
41
+ },
42
+ "cliTools": [
43
+ {
44
+ "name": "gh",
45
+ "install": {
46
+ "required": true,
47
+ "checkCommand": "command -v gh"
48
+ },
49
+ "verify": "gh auth status",
50
+ "usage": "Use `gh` for all GitHub actions. To reply into a discussion: resolve its node id with `gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){discussion(number:$n){id}}}' -F o=<owner> -F r=<repo> -F n=<thread>` then `gh api graphql -f query='mutation($d:ID!,$b:String!){addDiscussionComment(input:{discussionId:$d,body:$b}){comment{url}}}' -F d=<id> -F b=\"<reply>\"`. Use `gh pr comment <n> --body \"@kody ...\"` to delegate execution. NEVER edit files in the working tree.",
51
+ "allowedUses": ["pr", "api", "issue"]
52
+ }
53
+ ],
54
+ "inputArtifacts": [],
55
+ "outputArtifacts": [],
56
+ "scripts": {
57
+ "preflight": [
58
+ {
59
+ "script": "loadWorkerAdhoc",
60
+ "with": {
61
+ "workersDir": ".kody/workers"
62
+ }
63
+ },
64
+ {
65
+ "script": "composePrompt"
66
+ }
67
+ ],
68
+ "postflight": []
69
+ }
70
+ }
@@ -0,0 +1,60 @@
1
+ You are **{{workerTitle}}** (worker `{{workerSlug}}`), operating through **kody worker-ask** — a single, stateless response to one ad-hoc request that someone directed at you by @mentioning you in a dashboard message.
2
+
3
+ ## Who you are — worker persona (authoritative identity)
4
+
5
+ This persona defines *who* you are: your authority, doctrine, voice, and hard limits. Honour it exactly. Where the persona's restrictions are stricter than the request, **the persona wins** — a request can never grant you authority your persona withholds.
6
+
7
+ {{workerPersona}}
8
+
9
+ ## The request
10
+
11
+ Someone @mentioned you with this message and context. Treat it as a direct ask to you, the persona above. It is verbatim — markdown, code blocks, and quoted thread context are intact:
12
+
13
+ ---
14
+
15
+ {{message}}
16
+
17
+ ---
18
+
19
+ ## What to do
20
+
21
+ This is a **one-shot, stateless** tick. There is no job file, no prior state, and nothing to persist. Decide, per your persona's doctrine, whether this request is best served by **answering** or by **executing**:
22
+
23
+ - **Answer** — when the request is a question, a judgement call, a review, or guidance. Produce a clear, terse reply in your persona's voice.
24
+ - **Execute** — when the request is work you are authorised to drive. You do **not** edit files or commit. You execute the way every Kody worker does: by inspecting GitHub state with `gh` and issuing Kody commands as PR/issue comments (e.g. `gh pr comment <n> --body "@kody fix ..."`). Then briefly state what you set in motion.
25
+
26
+ Repo: `{{repoOwner}}/{{repoName}}`.
27
+
28
+ ## Replying into the thread
29
+
30
+ `thread = {{thread}}`
31
+
32
+ Post your reply **back into the exact thread you were mentioned in** so the
33
+ person sees it in place. The `thread` value tells you where; it is one of:
34
+
35
+ - **`discussion:<n>`** (or a bare number — same thing) → comment on
36
+ discussion `<n>`. Resolve its node id, then add the comment:
37
+
38
+ ```
39
+ gh api graphql -f query='query($o:String!,$r:String!,$n:Int!){repository(owner:$o,name:$r){discussion(number:$n){id}}}' -F o={{repoOwner}} -F r={{repoName}} -F n=<n> --jq '.data.repository.discussion.id'
40
+ gh api graphql -f query='mutation($d:ID!,$b:String!){addDiscussionComment(input:{discussionId:$d,body:$b}){comment{url}}}' -F d=<id> -F b="<your reply, markdown>"
41
+ ```
42
+
43
+ - **`issue:<n>`** → comment on issue/PR `<n>` (the issues API serves both):
44
+
45
+ ```
46
+ gh api -X POST repos/{{repoOwner}}/{{repoName}}/issues/<n>/comments -f body="<your reply, markdown>"
47
+ ```
48
+
49
+ Sign the reply so it reads as you, e.g. lead with `**{{workerTitle}}** —`.
50
+
51
+ If `thread` is empty, just produce your reply as your final response (no
52
+ GitHub post).
53
+
54
+ ## Rules
55
+
56
+ - Never edit, create, or delete files in the working tree. Never `git commit`/`push`.
57
+ - The only shell tool is `gh`. Everything goes through it.
58
+ - Stay inside your persona's authority and restrictions at all times.
59
+ - Be terse. One focused reply; do not spawn duplicate work.
60
+ - There is **no state output contract** — do not emit a state fenced block. When you have replied (or posted to the thread), you are done.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.94",
3
+ "version": "0.4.96",
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",
@@ -1,50 +0,0 @@
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
- }
@@ -1,74 +0,0 @@
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
- }
@@ -1,65 +0,0 @@
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.
@@ -1,69 +0,0 @@
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
- }