@kody-ade/kody-engine 0.4.93 → 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 +110 -107
- package/dist/executables/job-tick/prompt.md +8 -2
- package/package.json +1 -1
- package/dist/executables/worker-scheduler/profile.json +0 -50
- package/dist/executables/worker-tick/profile.json +0 -74
- package/dist/executables/worker-tick/prompt.md +0 -65
- package/dist/executables/worker-tick-scripted/profile.json +0 -69
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
2015
|
-
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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,
|
|
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
|
|
2154
|
-
if (
|
|
2155
|
-
const
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
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
|
|
2162
|
-
const
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
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
|
|
2194
|
-
|
|
2195
|
-
|
|
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
|
-
|
|
2198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2212
|
-
if (!
|
|
2213
|
-
process.stderr.write(`[kody:chat:interactive]
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
5467
|
+
import * as os4 from "os";
|
|
5494
5468
|
import * as path20 from "path";
|
|
5495
5469
|
var diagMcp = async (_ctx) => {
|
|
5496
|
-
const home =
|
|
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}
|
|
@@ -7246,7 +7230,7 @@ function git2(args, cwd) {
|
|
|
7246
7230
|
}
|
|
7247
7231
|
function deriveBranchName(issueNumber, title) {
|
|
7248
7232
|
const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").slice(0, 50).replace(/-$/, "");
|
|
7249
|
-
return slug ? `${issueNumber}-${slug}` : `${issueNumber}`;
|
|
7233
|
+
return slug ? `${issueNumber}-${slug}` : `${issueNumber}-task`;
|
|
7250
7234
|
}
|
|
7251
7235
|
function getCurrentBranch(cwd) {
|
|
7252
7236
|
return git2(["branch", "--show-current"], cwd);
|
|
@@ -7319,7 +7303,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
|
|
|
7319
7303
|
}
|
|
7320
7304
|
let originBranchExists = false;
|
|
7321
7305
|
try {
|
|
7322
|
-
git2(["rev-parse", "--verify", `origin/${branchName}`], cwd);
|
|
7306
|
+
git2(["rev-parse", "--verify", "--quiet", `refs/remotes/origin/${branchName}`], cwd);
|
|
7323
7307
|
originBranchExists = true;
|
|
7324
7308
|
} catch {
|
|
7325
7309
|
}
|
|
@@ -7380,7 +7364,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
|
|
|
7380
7364
|
return { branch: branchName, created: false };
|
|
7381
7365
|
}
|
|
7382
7366
|
try {
|
|
7383
|
-
git2(["rev-parse", "--verify", branchName], cwd);
|
|
7367
|
+
git2(["rev-parse", "--verify", "--quiet", `refs/heads/${branchName}`], cwd);
|
|
7384
7368
|
git2(["checkout", branchName], cwd);
|
|
7385
7369
|
return { branch: branchName, created: false };
|
|
7386
7370
|
} catch {
|
|
@@ -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
|
|
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}}
|
|
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.
|
|
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
|
-
}
|