@kody-ade/kody-engine 0.4.94 → 0.4.95

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,
@@ -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.95",
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",
@@ -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
  }
@@ -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}
@@ -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;
@@ -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
 
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.95",
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
- }