@kody-ade/kody-engine 0.4.222 → 0.4.223
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
|
@@ -15,7 +15,7 @@ var init_package = __esm({
|
|
|
15
15
|
"package.json"() {
|
|
16
16
|
package_default = {
|
|
17
17
|
name: "@kody-ade/kody-engine",
|
|
18
|
-
version: "0.4.
|
|
18
|
+
version: "0.4.223",
|
|
19
19
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
20
20
|
license: "MIT",
|
|
21
21
|
type: "module",
|
|
@@ -146,6 +146,12 @@ var init_claudeBinary = __esm({
|
|
|
146
146
|
// src/config.ts
|
|
147
147
|
import * as fs2 from "fs";
|
|
148
148
|
import * as path2 from "path";
|
|
149
|
+
function parseReasoningEffort(raw) {
|
|
150
|
+
if (!raw) return null;
|
|
151
|
+
const v = raw.trim().toLowerCase();
|
|
152
|
+
if (REASONING_EFFORTS.includes(v)) return v;
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
149
155
|
function parseProviderModel(s) {
|
|
150
156
|
const slash = s.indexOf("/");
|
|
151
157
|
if (slash <= 0 || slash === s.length - 1) {
|
|
@@ -198,7 +204,8 @@ function loadConfig(projectDir = process.cwd()) {
|
|
|
198
204
|
},
|
|
199
205
|
agent: {
|
|
200
206
|
model: String(agent.model),
|
|
201
|
-
...parsePerExecutable(agent.perExecutable)
|
|
207
|
+
...parsePerExecutable(agent.perExecutable),
|
|
208
|
+
...parseAgentReasoningEffort(agent.reasoningEffort)
|
|
202
209
|
},
|
|
203
210
|
issueContext: parseIssueContext(raw.issueContext),
|
|
204
211
|
testRequirements: parseTestRequirements(raw.testRequirements),
|
|
@@ -275,6 +282,10 @@ function parsePerExecutable(raw) {
|
|
|
275
282
|
}
|
|
276
283
|
return Object.keys(out).length > 0 ? { perExecutable: out } : {};
|
|
277
284
|
}
|
|
285
|
+
function parseAgentReasoningEffort(raw) {
|
|
286
|
+
if (typeof raw !== "string") return {};
|
|
287
|
+
return { reasoningEffort: parseReasoningEffort(raw) ?? void 0 };
|
|
288
|
+
}
|
|
278
289
|
function parseClassifyConfig(raw) {
|
|
279
290
|
if (!raw || typeof raw !== "object") return void 0;
|
|
280
291
|
const r = raw;
|
|
@@ -327,10 +338,16 @@ function parseTestRequirements(raw) {
|
|
|
327
338
|
function getAnthropicApiKeyOrDummy() {
|
|
328
339
|
return process.env.ANTHROPIC_API_KEY || `sk-ant-api03-${"0".repeat(64)}`;
|
|
329
340
|
}
|
|
330
|
-
var LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
|
|
341
|
+
var REASONING_EFFORTS, REASONING_BUDGETS, LITELLM_DEFAULT_PORT, LITELLM_DEFAULT_URL, GITHUB_AUTHOR_ASSOCIATIONS, DEFAULT_ALLOWED_ASSOCIATIONS, BUILTIN_ALIASES;
|
|
331
342
|
var init_config = __esm({
|
|
332
343
|
"src/config.ts"() {
|
|
333
344
|
"use strict";
|
|
345
|
+
REASONING_EFFORTS = ["off", "low", "medium", "high"];
|
|
346
|
+
REASONING_BUDGETS = {
|
|
347
|
+
low: 2048,
|
|
348
|
+
medium: 1e4,
|
|
349
|
+
high: 32e3
|
|
350
|
+
};
|
|
334
351
|
LITELLM_DEFAULT_PORT = 4e3;
|
|
335
352
|
LITELLM_DEFAULT_URL = `http://localhost:${LITELLM_DEFAULT_PORT}`;
|
|
336
353
|
GITHUB_AUTHOR_ASSOCIATIONS = [
|
|
@@ -1664,15 +1681,19 @@ async function runAgent(opts) {
|
|
|
1664
1681
|
let finalText = "";
|
|
1665
1682
|
let getSubmitted;
|
|
1666
1683
|
for (let attempt = 0; ; attempt++) {
|
|
1684
|
+
let ndjsonWriteFailed = false;
|
|
1685
|
+
let ndjsonWriteError;
|
|
1667
1686
|
const fullLog = fs5.createWriteStream(ndjsonPath, { flags: "w" });
|
|
1687
|
+
fullLog.on("error", (err) => {
|
|
1688
|
+
ndjsonWriteFailed = true;
|
|
1689
|
+
ndjsonWriteError = err instanceof Error ? err.message : String(err);
|
|
1690
|
+
});
|
|
1668
1691
|
const resultTexts = [];
|
|
1669
1692
|
outcome = "failed";
|
|
1670
1693
|
outcomeKind = "generic_failed";
|
|
1671
1694
|
errorMessage = void 0;
|
|
1672
1695
|
tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
|
|
1673
1696
|
messageCount = 0;
|
|
1674
|
-
let ndjsonWriteFailed = false;
|
|
1675
|
-
let ndjsonWriteError;
|
|
1676
1697
|
let sawMutatingTool = false;
|
|
1677
1698
|
let sawTerminalSuccess = false;
|
|
1678
1699
|
let noWorkSuccess = false;
|
|
@@ -1747,7 +1768,13 @@ async function runAgent(opts) {
|
|
|
1747
1768
|
if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
|
|
1748
1769
|
queryOptions.maxTurns = opts.maxTurns;
|
|
1749
1770
|
}
|
|
1750
|
-
if (
|
|
1771
|
+
if (opts.reasoningEffort !== void 0 && opts.reasoningEffort !== null) {
|
|
1772
|
+
if (opts.reasoningEffort === "off") {
|
|
1773
|
+
} else {
|
|
1774
|
+
const budget = REASONING_BUDGETS[opts.reasoningEffort];
|
|
1775
|
+
if (budget) queryOptions.maxThinkingTokens = budget;
|
|
1776
|
+
}
|
|
1777
|
+
} else if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
|
|
1751
1778
|
queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
|
|
1752
1779
|
}
|
|
1753
1780
|
if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
|
|
@@ -1808,12 +1835,14 @@ async function runAgent(opts) {
|
|
|
1808
1835
|
if (next.done) break;
|
|
1809
1836
|
const msg = next.value;
|
|
1810
1837
|
messageCount++;
|
|
1811
|
-
|
|
1812
|
-
|
|
1838
|
+
if (!ndjsonWriteFailed) {
|
|
1839
|
+
try {
|
|
1840
|
+
fullLog.write(`${JSON.stringify(msg)}
|
|
1813
1841
|
`);
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1842
|
+
} catch (e) {
|
|
1843
|
+
ndjsonWriteFailed = true;
|
|
1844
|
+
ndjsonWriteError = e instanceof Error ? e.message : String(e);
|
|
1845
|
+
}
|
|
1817
1846
|
}
|
|
1818
1847
|
const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
|
|
1819
1848
|
if (line) process.stdout.write(`${line}
|
|
@@ -2945,7 +2974,7 @@ function loadProfile(profilePath) {
|
|
|
2945
2974
|
const preNames = new Set(profile.scripts.preflight.map((e) => e.script).filter(Boolean));
|
|
2946
2975
|
const postNames = profile.scripts.postflight.map((e) => e.script).filter(Boolean);
|
|
2947
2976
|
const needsState = postNames.includes("writeJobStateFile") || postNames.includes("parseJobStateFromAgentResult");
|
|
2948
|
-
const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript"];
|
|
2977
|
+
const STATE_LOADERS = ["loadDutyState", "loadJobFromFile", "runTickScript", "runScheduledExecutableTick"];
|
|
2949
2978
|
if (needsState && !STATE_LOADERS.some((s) => preNames.has(s))) {
|
|
2950
2979
|
throw new ProfileError(
|
|
2951
2980
|
profilePath,
|
|
@@ -5574,10 +5603,10 @@ var init_state2 = __esm({
|
|
|
5574
5603
|
"use strict";
|
|
5575
5604
|
VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
|
|
5576
5605
|
GoalStateError = class extends Error {
|
|
5577
|
-
constructor(
|
|
5578
|
-
super(`Invalid goal state at ${
|
|
5606
|
+
constructor(path45, message) {
|
|
5607
|
+
super(`Invalid goal state at ${path45}:
|
|
5579
5608
|
${message}`);
|
|
5580
|
-
this.path =
|
|
5609
|
+
this.path = path45;
|
|
5581
5610
|
this.name = "GoalStateError";
|
|
5582
5611
|
}
|
|
5583
5612
|
path;
|
|
@@ -7891,9 +7920,9 @@ var init_dispatchDutyFileTicks = __esm({
|
|
|
7891
7920
|
init_dutyFolders();
|
|
7892
7921
|
init_issue();
|
|
7893
7922
|
init_job();
|
|
7894
|
-
init_scheduleEvery();
|
|
7895
7923
|
init_jobState();
|
|
7896
7924
|
init_planTaskJobs();
|
|
7925
|
+
init_scheduleEvery();
|
|
7897
7926
|
dispatchDutyFileTicks = async (ctx, _profile, args) => {
|
|
7898
7927
|
ctx.skipAgent = true;
|
|
7899
7928
|
const targetExecutable = String(args?.targetExecutable ?? "");
|
|
@@ -7986,7 +8015,7 @@ var init_dispatchDutyFileTicks = __esm({
|
|
|
7986
8015
|
continue;
|
|
7987
8016
|
}
|
|
7988
8017
|
const slugTarget = config.tickScript ? scriptedExecutable : config.executable ?? targetExecutable;
|
|
7989
|
-
const cliArgs =
|
|
8018
|
+
const cliArgs = { [slugArg]: slug };
|
|
7990
8019
|
process.stdout.write(`[jobs] \u2192 tick ${slug} (${slugTarget})
|
|
7991
8020
|
`);
|
|
7992
8021
|
try {
|
|
@@ -12352,11 +12381,46 @@ var init_runPreviewBuild = __esm({
|
|
|
12352
12381
|
}
|
|
12353
12382
|
});
|
|
12354
12383
|
|
|
12355
|
-
// src/scripts/
|
|
12384
|
+
// src/scripts/tickShellRunner.ts
|
|
12356
12385
|
import { spawnSync as spawnSync2 } from "child_process";
|
|
12357
|
-
|
|
12358
|
-
|
|
12359
|
-
|
|
12386
|
+
function runTickShellAndParse(opts) {
|
|
12387
|
+
const childEnv = buildTickChildEnv(process.env, opts.force);
|
|
12388
|
+
const result = spawnSync2("bash", [opts.scriptPath], {
|
|
12389
|
+
cwd: opts.ctx.cwd,
|
|
12390
|
+
env: childEnv,
|
|
12391
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
12392
|
+
encoding: "utf-8",
|
|
12393
|
+
timeout: 5 * 60 * 1e3,
|
|
12394
|
+
maxBuffer: 16 * 1024 * 1024
|
|
12395
|
+
});
|
|
12396
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
12397
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
12398
|
+
if (result.error) {
|
|
12399
|
+
opts.ctx.output.exitCode = 99;
|
|
12400
|
+
opts.ctx.output.reason = `${opts.displayName}: spawn error: ${result.error.message}`;
|
|
12401
|
+
return;
|
|
12402
|
+
}
|
|
12403
|
+
if (result.signal) {
|
|
12404
|
+
opts.ctx.output.exitCode = 124;
|
|
12405
|
+
opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}killed by ${result.signal} (likely 5min timeout)`;
|
|
12406
|
+
return;
|
|
12407
|
+
}
|
|
12408
|
+
if (result.status !== 0) {
|
|
12409
|
+
opts.ctx.output.exitCode = result.status ?? 99;
|
|
12410
|
+
opts.ctx.output.reason = `${opts.displayName}: ${opts.reasonSubject ? `${opts.reasonSubject} ` : ""}exited ${result.status}`;
|
|
12411
|
+
return;
|
|
12412
|
+
}
|
|
12413
|
+
const prevRev = opts.loaded.state.rev ?? 0;
|
|
12414
|
+
const parsed = extractNextStateFromText(result.stdout ?? "", opts.fenceLabel, prevRev);
|
|
12415
|
+
if (parsed.error) {
|
|
12416
|
+
opts.ctx.data.nextStateParseError = parsed.error;
|
|
12417
|
+
opts.ctx.output.exitCode = 1;
|
|
12418
|
+
opts.ctx.output.reason = `${opts.displayName}: ${parsed.error}`;
|
|
12419
|
+
return;
|
|
12420
|
+
}
|
|
12421
|
+
opts.ctx.data.nextJobState = parsed.envelope;
|
|
12422
|
+
}
|
|
12423
|
+
function buildTickChildEnv(parent, force) {
|
|
12360
12424
|
const allow = /* @__PURE__ */ new Set([
|
|
12361
12425
|
"PATH",
|
|
12362
12426
|
"HOME",
|
|
@@ -12367,45 +12431,104 @@ function buildChildEnv(parent, force) {
|
|
|
12367
12431
|
"LANG",
|
|
12368
12432
|
"LC_ALL",
|
|
12369
12433
|
"TERM",
|
|
12370
|
-
// GitHub auth — `gh` reads these.
|
|
12371
12434
|
"GH_TOKEN",
|
|
12372
12435
|
"GH_PAT",
|
|
12373
12436
|
"GITHUB_TOKEN",
|
|
12374
|
-
// CI metadata commonly read by tick scripts (`gh repo view`,
|
|
12375
|
-
// workflow run links, etc.). All public values from GitHub Actions.
|
|
12376
|
-
"GITHUB_ACTIONS",
|
|
12377
|
-
"GITHUB_ACTOR",
|
|
12378
12437
|
"GITHUB_REPOSITORY",
|
|
12379
|
-
"GITHUB_REPOSITORY_OWNER",
|
|
12380
12438
|
"GITHUB_REF",
|
|
12381
12439
|
"GITHUB_SHA",
|
|
12382
12440
|
"GITHUB_RUN_ID",
|
|
12383
|
-
"
|
|
12441
|
+
"GITHUB_RUN_ATTEMPT",
|
|
12384
12442
|
"GITHUB_WORKFLOW",
|
|
12385
|
-
"
|
|
12386
|
-
"
|
|
12387
|
-
"
|
|
12388
|
-
"
|
|
12389
|
-
"RUNNER_OS",
|
|
12390
|
-
"RUNNER_ARCH"
|
|
12443
|
+
"GITHUB_ACTIONS",
|
|
12444
|
+
"CI",
|
|
12445
|
+
"KODY_DRY_RUN",
|
|
12446
|
+
"KODY_NO_COMMIT"
|
|
12391
12447
|
]);
|
|
12392
|
-
const
|
|
12448
|
+
const env = {};
|
|
12393
12449
|
for (const [key, value] of Object.entries(parent)) {
|
|
12394
12450
|
if (value === void 0) continue;
|
|
12395
|
-
if (allow.has(key) || key.startsWith("KODY_PUBLIC_"))
|
|
12396
|
-
out[key] = value;
|
|
12397
|
-
}
|
|
12451
|
+
if (allow.has(key) || key.startsWith("KODY_PUBLIC_")) env[key] = value;
|
|
12398
12452
|
}
|
|
12399
|
-
if (force)
|
|
12400
|
-
return
|
|
12453
|
+
if (force) env.KODY_FORCE = "1";
|
|
12454
|
+
return env;
|
|
12401
12455
|
}
|
|
12456
|
+
var init_tickShellRunner = __esm({
|
|
12457
|
+
"src/scripts/tickShellRunner.ts"() {
|
|
12458
|
+
"use strict";
|
|
12459
|
+
init_parseJobStateFromAgentResult();
|
|
12460
|
+
}
|
|
12461
|
+
});
|
|
12462
|
+
|
|
12463
|
+
// src/scripts/runScheduledExecutableTick.ts
|
|
12464
|
+
import * as fs36 from "fs";
|
|
12465
|
+
import * as path35 from "path";
|
|
12466
|
+
var runScheduledExecutableTick;
|
|
12467
|
+
var init_runScheduledExecutableTick = __esm({
|
|
12468
|
+
"src/scripts/runScheduledExecutableTick.ts"() {
|
|
12469
|
+
"use strict";
|
|
12470
|
+
init_dutyFolders();
|
|
12471
|
+
init_jobState();
|
|
12472
|
+
init_tickShellRunner();
|
|
12473
|
+
runScheduledExecutableTick = async (ctx, profile, args) => {
|
|
12474
|
+
ctx.skipAgent = true;
|
|
12475
|
+
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
12476
|
+
const slugArg = String(args?.slugArg ?? "duty");
|
|
12477
|
+
const fenceLabel = String(args?.fenceLabel ?? "kody-job-next-state");
|
|
12478
|
+
const shell = String(args?.shell ?? "tick.sh");
|
|
12479
|
+
const slug = String(ctx.args[slugArg] ?? "").trim();
|
|
12480
|
+
if (!slug) {
|
|
12481
|
+
ctx.output.exitCode = 99;
|
|
12482
|
+
ctx.output.reason = `runScheduledExecutableTick: ctx.args.${slugArg} must be non-empty duty slug`;
|
|
12483
|
+
return;
|
|
12484
|
+
}
|
|
12485
|
+
const duty = readDutyFolder(path35.join(ctx.cwd, jobsDir), slug);
|
|
12486
|
+
if (!duty) {
|
|
12487
|
+
ctx.output.exitCode = 99;
|
|
12488
|
+
ctx.output.reason = `runScheduledExecutableTick: duty folder not found or incomplete: ${path35.join(jobsDir, slug)}`;
|
|
12489
|
+
return;
|
|
12490
|
+
}
|
|
12491
|
+
const shellPath = path35.join(profile.dir, shell);
|
|
12492
|
+
if (!fs36.existsSync(shellPath)) {
|
|
12493
|
+
ctx.output.exitCode = 99;
|
|
12494
|
+
ctx.output.reason = `runScheduledExecutableTick: shell not found: ${shell} (looked in ${profile.dir})`;
|
|
12495
|
+
return;
|
|
12496
|
+
}
|
|
12497
|
+
const backend = resolveBackend({ config: ctx.config, cwd: ctx.cwd, jobsDir });
|
|
12498
|
+
let loaded;
|
|
12499
|
+
try {
|
|
12500
|
+
loaded = await backend.load(slug);
|
|
12501
|
+
} catch (err) {
|
|
12502
|
+
ctx.output.exitCode = 99;
|
|
12503
|
+
ctx.output.reason = `runScheduledExecutableTick: state load failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
12504
|
+
return;
|
|
12505
|
+
}
|
|
12506
|
+
ctx.data.jobSlug = slug;
|
|
12507
|
+
ctx.data.dutySlug = slug;
|
|
12508
|
+
ctx.data.executableSlug = profile.name;
|
|
12509
|
+
ctx.data.jobState = loaded;
|
|
12510
|
+
runTickShellAndParse({
|
|
12511
|
+
ctx,
|
|
12512
|
+
loaded,
|
|
12513
|
+
scriptPath: shellPath,
|
|
12514
|
+
displayName: `runScheduledExecutableTick: ${shell}`,
|
|
12515
|
+
fenceLabel,
|
|
12516
|
+
force: Boolean(ctx.args.force)
|
|
12517
|
+
});
|
|
12518
|
+
};
|
|
12519
|
+
}
|
|
12520
|
+
});
|
|
12521
|
+
|
|
12522
|
+
// src/scripts/runTickScript.ts
|
|
12523
|
+
import * as fs37 from "fs";
|
|
12524
|
+
import * as path36 from "path";
|
|
12402
12525
|
var runTickScript;
|
|
12403
12526
|
var init_runTickScript = __esm({
|
|
12404
12527
|
"src/scripts/runTickScript.ts"() {
|
|
12405
12528
|
"use strict";
|
|
12406
12529
|
init_dutyFolders();
|
|
12407
12530
|
init_jobState();
|
|
12408
|
-
|
|
12531
|
+
init_tickShellRunner();
|
|
12409
12532
|
runTickScript = async (ctx, _profile, args) => {
|
|
12410
12533
|
ctx.skipAgent = true;
|
|
12411
12534
|
const jobsDir = String(args?.jobsDir ?? ".kody/duties");
|
|
@@ -12417,10 +12540,10 @@ var init_runTickScript = __esm({
|
|
|
12417
12540
|
ctx.output.reason = `runTickScript: ctx.args.${slugArg} must be a non-empty slug`;
|
|
12418
12541
|
return;
|
|
12419
12542
|
}
|
|
12420
|
-
const duty = readDutyFolder(
|
|
12543
|
+
const duty = readDutyFolder(path36.join(ctx.cwd, jobsDir), slug);
|
|
12421
12544
|
if (!duty) {
|
|
12422
12545
|
ctx.output.exitCode = 99;
|
|
12423
|
-
ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${
|
|
12546
|
+
ctx.output.reason = `runTickScript: duty folder not found or incomplete: ${path36.join(ctx.cwd, jobsDir, slug)}`;
|
|
12424
12547
|
return;
|
|
12425
12548
|
}
|
|
12426
12549
|
const tickScript = duty.config.tickScript;
|
|
@@ -12429,8 +12552,8 @@ var init_runTickScript = __esm({
|
|
|
12429
12552
|
ctx.output.reason = `runTickScript: duty ${slug} has no \`tickScript\` in profile.json \u2014 route via duty-tick instead`;
|
|
12430
12553
|
return;
|
|
12431
12554
|
}
|
|
12432
|
-
const scriptPath =
|
|
12433
|
-
if (!
|
|
12555
|
+
const scriptPath = path36.isAbsolute(tickScript) ? tickScript : path36.join(ctx.cwd, tickScript);
|
|
12556
|
+
if (!fs37.existsSync(scriptPath)) {
|
|
12434
12557
|
ctx.output.exitCode = 99;
|
|
12435
12558
|
ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
|
|
12436
12559
|
return;
|
|
@@ -12446,46 +12569,15 @@ var init_runTickScript = __esm({
|
|
|
12446
12569
|
}
|
|
12447
12570
|
ctx.data.jobSlug = slug;
|
|
12448
12571
|
ctx.data.jobState = loaded;
|
|
12449
|
-
|
|
12450
|
-
|
|
12451
|
-
|
|
12452
|
-
|
|
12453
|
-
|
|
12454
|
-
|
|
12455
|
-
|
|
12456
|
-
|
|
12457
|
-
// busy repo (or an accidental `set -x`) can blow that and silently
|
|
12458
|
-
// truncate stdout, which is the exact "silent state drop" failure
|
|
12459
|
-
// mode this whole executable was written to prevent. 16MB is well
|
|
12460
|
-
// above any realistic tick output.
|
|
12461
|
-
maxBuffer: 16 * 1024 * 1024
|
|
12572
|
+
runTickShellAndParse({
|
|
12573
|
+
ctx,
|
|
12574
|
+
loaded,
|
|
12575
|
+
scriptPath,
|
|
12576
|
+
displayName: "runTickScript",
|
|
12577
|
+
reasonSubject: tickScript,
|
|
12578
|
+
fenceLabel,
|
|
12579
|
+
force: Boolean(ctx.args.force)
|
|
12462
12580
|
});
|
|
12463
|
-
if (result.stdout) process.stdout.write(result.stdout);
|
|
12464
|
-
if (result.stderr) process.stderr.write(result.stderr);
|
|
12465
|
-
if (result.error) {
|
|
12466
|
-
ctx.output.exitCode = 99;
|
|
12467
|
-
ctx.output.reason = `runTickScript: spawn error: ${result.error.message}`;
|
|
12468
|
-
return;
|
|
12469
|
-
}
|
|
12470
|
-
if (result.signal) {
|
|
12471
|
-
ctx.output.exitCode = 124;
|
|
12472
|
-
ctx.output.reason = `runTickScript: ${tickScript} killed by ${result.signal} (likely 5min timeout)`;
|
|
12473
|
-
return;
|
|
12474
|
-
}
|
|
12475
|
-
if (result.status !== 0) {
|
|
12476
|
-
ctx.output.exitCode = result.status ?? 99;
|
|
12477
|
-
ctx.output.reason = `runTickScript: ${tickScript} exited ${result.status}`;
|
|
12478
|
-
return;
|
|
12479
|
-
}
|
|
12480
|
-
const prevRev = loaded.state.rev ?? 0;
|
|
12481
|
-
const parsed = extractNextStateFromText(result.stdout ?? "", fenceLabel, prevRev);
|
|
12482
|
-
if (parsed.error) {
|
|
12483
|
-
ctx.data.nextStateParseError = parsed.error;
|
|
12484
|
-
ctx.output.exitCode = 1;
|
|
12485
|
-
ctx.output.reason = `runTickScript: ${parsed.error}`;
|
|
12486
|
-
return;
|
|
12487
|
-
}
|
|
12488
|
-
ctx.data.nextJobState = parsed.envelope;
|
|
12489
12581
|
};
|
|
12490
12582
|
}
|
|
12491
12583
|
});
|
|
@@ -13394,7 +13486,7 @@ var init_writeJobStateFile = __esm({
|
|
|
13394
13486
|
});
|
|
13395
13487
|
|
|
13396
13488
|
// src/scripts/writeRunSummary.ts
|
|
13397
|
-
import * as
|
|
13489
|
+
import * as fs38 from "fs";
|
|
13398
13490
|
var writeRunSummary;
|
|
13399
13491
|
var init_writeRunSummary = __esm({
|
|
13400
13492
|
"src/scripts/writeRunSummary.ts"() {
|
|
@@ -13420,7 +13512,7 @@ var init_writeRunSummary = __esm({
|
|
|
13420
13512
|
if (reason) lines.push(`- **Reason:** ${reason}`);
|
|
13421
13513
|
lines.push("");
|
|
13422
13514
|
try {
|
|
13423
|
-
|
|
13515
|
+
fs38.appendFileSync(summaryPath, `${lines.join("\n")}
|
|
13424
13516
|
`);
|
|
13425
13517
|
} catch {
|
|
13426
13518
|
}
|
|
@@ -13507,6 +13599,7 @@ var init_scripts = __esm({
|
|
|
13507
13599
|
init_reviewFlow();
|
|
13508
13600
|
init_runFlow();
|
|
13509
13601
|
init_runPreviewBuild();
|
|
13602
|
+
init_runScheduledExecutableTick();
|
|
13510
13603
|
init_runTickScript();
|
|
13511
13604
|
init_saveGoalState();
|
|
13512
13605
|
init_saveTaskState();
|
|
@@ -13565,6 +13658,7 @@ var init_scripts = __esm({
|
|
|
13565
13658
|
dispatchDutyFileTicks,
|
|
13566
13659
|
planTaskJobs,
|
|
13567
13660
|
dispatchNextTaskJob,
|
|
13661
|
+
runScheduledExecutableTick,
|
|
13568
13662
|
runTickScript,
|
|
13569
13663
|
runPreviewBuild,
|
|
13570
13664
|
loadGoalState,
|
|
@@ -13627,8 +13721,8 @@ var init_scripts = __esm({
|
|
|
13627
13721
|
});
|
|
13628
13722
|
|
|
13629
13723
|
// src/staff.ts
|
|
13630
|
-
import * as
|
|
13631
|
-
import * as
|
|
13724
|
+
import * as fs39 from "fs";
|
|
13725
|
+
import * as path37 from "path";
|
|
13632
13726
|
function stripFrontmatter(raw) {
|
|
13633
13727
|
const match = /^---\n[\s\S]*?\n---\n?([\s\S]*)$/.exec(raw);
|
|
13634
13728
|
return (match ? match[1] : raw).trim();
|
|
@@ -13636,9 +13730,9 @@ function stripFrontmatter(raw) {
|
|
|
13636
13730
|
function loadStaffPersona(cwd, slug, staffDir = DEFAULT_STAFF_DIR) {
|
|
13637
13731
|
const trimmed = slug.trim();
|
|
13638
13732
|
if (!trimmed) throw new Error("loadStaffPersona: empty staff slug");
|
|
13639
|
-
const staffPath =
|
|
13640
|
-
if (
|
|
13641
|
-
const body = stripFrontmatter(
|
|
13733
|
+
const staffPath = path37.join(cwd, staffDir, `${trimmed}.md`);
|
|
13734
|
+
if (fs39.existsSync(staffPath)) {
|
|
13735
|
+
const body = stripFrontmatter(fs39.readFileSync(staffPath, "utf-8"));
|
|
13642
13736
|
if (body) return body;
|
|
13643
13737
|
const builtinForEmpty = BUILTIN_PERSONAS[trimmed];
|
|
13644
13738
|
if (builtinForEmpty) return builtinForEmpty;
|
|
@@ -13751,8 +13845,8 @@ var init_tools = __esm({
|
|
|
13751
13845
|
|
|
13752
13846
|
// src/executor.ts
|
|
13753
13847
|
import { spawn as spawn7 } from "child_process";
|
|
13754
|
-
import * as
|
|
13755
|
-
import * as
|
|
13848
|
+
import * as fs40 from "fs";
|
|
13849
|
+
import * as path38 from "path";
|
|
13756
13850
|
function isMutatingPostflight(scriptName) {
|
|
13757
13851
|
return MUTATING_POSTFLIGHTS.has(scriptName ?? "");
|
|
13758
13852
|
}
|
|
@@ -13865,17 +13959,7 @@ async function runExecutable(profileName, input) {
|
|
|
13865
13959
|
reason: `agent.model invalid: ${err instanceof Error ? err.message : String(err)}`
|
|
13866
13960
|
});
|
|
13867
13961
|
}
|
|
13868
|
-
let litellm
|
|
13869
|
-
if (profileName !== "preview-build") {
|
|
13870
|
-
try {
|
|
13871
|
-
litellm = await startLitellmIfNeeded(model, input.cwd);
|
|
13872
|
-
} catch (err) {
|
|
13873
|
-
return finishAndEnd({
|
|
13874
|
-
exitCode: 99,
|
|
13875
|
-
reason: `litellm startup failed: ${err instanceof Error ? err.message : String(err)}`
|
|
13876
|
-
});
|
|
13877
|
-
}
|
|
13878
|
-
}
|
|
13962
|
+
let litellm;
|
|
13879
13963
|
const ctx = {
|
|
13880
13964
|
args,
|
|
13881
13965
|
cwd: input.cwd,
|
|
@@ -13903,16 +13987,23 @@ async function runExecutable(profileName, input) {
|
|
|
13903
13987
|
})
|
|
13904
13988
|
};
|
|
13905
13989
|
})() : null;
|
|
13906
|
-
const ndjsonDir =
|
|
13990
|
+
const ndjsonDir = path38.join(input.cwd, ".kody");
|
|
13907
13991
|
const personaSlug = typeof profile.staff === "string" && profile.staff.length > 0 ? profile.staff : typeof ctx.data.jobPersona === "string" && ctx.data.jobPersona.length > 0 ? ctx.data.jobPersona : null;
|
|
13908
13992
|
const staffPersona = personaSlug ? framePersona(personaSlug, loadStaffPersona(input.cwd, personaSlug)) : null;
|
|
13909
13993
|
const jobWhyBlock = typeof ctx.data.jobWhy === "string" ? operatorRequestBlock(ctx.data.jobWhy) : null;
|
|
13910
13994
|
const jobRefBlock = jobReferenceBlock(profileName, profile, ctx.data);
|
|
13911
13995
|
const invokeAgent = async (prompt) => {
|
|
13912
|
-
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) =>
|
|
13996
|
+
const externalPlugins = (profile.claudeCode.plugins ?? []).map((p) => path38.isAbsolute(p) ? p : path38.resolve(profile.dir, p)).filter((p) => p.length > 0);
|
|
13913
13997
|
const syntheticPath = ctx.data.syntheticPluginPath;
|
|
13914
13998
|
const pluginPaths = [...externalPlugins, ...syntheticPath ? [syntheticPath] : []];
|
|
13915
13999
|
const agents = loadSubagents(profile);
|
|
14000
|
+
if (litellm === void 0) {
|
|
14001
|
+
try {
|
|
14002
|
+
litellm = await startLitellmIfNeeded(model, input.cwd);
|
|
14003
|
+
} catch (err) {
|
|
14004
|
+
throw new Error(`litellm startup failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
14005
|
+
}
|
|
14006
|
+
}
|
|
13916
14007
|
const lm = litellm;
|
|
13917
14008
|
return runAgent({
|
|
13918
14009
|
prompt,
|
|
@@ -14020,7 +14111,14 @@ async function runExecutable(profileName, input) {
|
|
|
14020
14111
|
});
|
|
14021
14112
|
}
|
|
14022
14113
|
emitEvent(input.cwd, { executable: profileName, kind: "agent_start" });
|
|
14023
|
-
|
|
14114
|
+
try {
|
|
14115
|
+
agentResult = await invokeAgent(prompt);
|
|
14116
|
+
} catch (err) {
|
|
14117
|
+
return finishAndEnd({
|
|
14118
|
+
exitCode: 99,
|
|
14119
|
+
reason: err instanceof Error ? err.message : String(err)
|
|
14120
|
+
});
|
|
14121
|
+
}
|
|
14024
14122
|
emitEvent(input.cwd, {
|
|
14025
14123
|
executable: profileName,
|
|
14026
14124
|
kind: "agent_end",
|
|
@@ -14274,17 +14372,17 @@ function clearStampedLifecycleLabels(profile, ctx) {
|
|
|
14274
14372
|
function resolveProfilePath(profileName) {
|
|
14275
14373
|
const found = resolveExecutable(profileName);
|
|
14276
14374
|
if (found) return found;
|
|
14277
|
-
const here =
|
|
14375
|
+
const here = path38.dirname(new URL(import.meta.url).pathname);
|
|
14278
14376
|
const candidates = [
|
|
14279
|
-
|
|
14377
|
+
path38.join(here, "executables", profileName, "profile.json"),
|
|
14280
14378
|
// same-dir sibling (dev)
|
|
14281
|
-
|
|
14379
|
+
path38.join(here, "..", "executables", profileName, "profile.json"),
|
|
14282
14380
|
// up one (prod: dist/bin → dist/executables)
|
|
14283
|
-
|
|
14381
|
+
path38.join(here, "..", "src", "executables", profileName, "profile.json")
|
|
14284
14382
|
// fallback
|
|
14285
14383
|
];
|
|
14286
14384
|
for (const c of candidates) {
|
|
14287
|
-
if (
|
|
14385
|
+
if (fs40.existsSync(c)) return c;
|
|
14288
14386
|
}
|
|
14289
14387
|
return candidates[0];
|
|
14290
14388
|
}
|
|
@@ -14382,8 +14480,8 @@ function resolveShellTimeoutMs(entry) {
|
|
|
14382
14480
|
}
|
|
14383
14481
|
async function runShellEntry(entry, ctx, profile) {
|
|
14384
14482
|
const shellName = entry.shell;
|
|
14385
|
-
const shellPath =
|
|
14386
|
-
if (!
|
|
14483
|
+
const shellPath = path38.join(profile.dir, shellName);
|
|
14484
|
+
if (!fs40.existsSync(shellPath)) {
|
|
14387
14485
|
ctx.skipAgent = true;
|
|
14388
14486
|
ctx.output.exitCode = 99;
|
|
14389
14487
|
ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
|
|
@@ -14542,7 +14640,7 @@ __export(job_exports, {
|
|
|
14542
14640
|
stableJobKey: () => stableJobKey,
|
|
14543
14641
|
validateJob: () => validateJob
|
|
14544
14642
|
});
|
|
14545
|
-
import * as
|
|
14643
|
+
import * as path39 from "path";
|
|
14546
14644
|
function newJobId(flavor) {
|
|
14547
14645
|
localJobSeq += 1;
|
|
14548
14646
|
const runId = process.env.GITHUB_RUN_ID;
|
|
@@ -14579,7 +14677,7 @@ function validateJob(input) {
|
|
|
14579
14677
|
async function runJob(job, base) {
|
|
14580
14678
|
const valid = validateJob(job);
|
|
14581
14679
|
const action = valid.action ?? valid.duty;
|
|
14582
|
-
const projectDutiesRoot =
|
|
14680
|
+
const projectDutiesRoot = path39.join(base.cwd, ".kody", "duties");
|
|
14583
14681
|
const resolvedDuty = action ? resolveDutyAction(action, projectDutiesRoot) : null;
|
|
14584
14682
|
const dutyIdentity = valid.duty ?? resolvedDuty?.duty;
|
|
14585
14683
|
const dutyContext = loadDutyContext(dutyIdentity, base.cwd);
|
|
@@ -14635,7 +14733,7 @@ async function runJob(job, base) {
|
|
|
14635
14733
|
}
|
|
14636
14734
|
function loadDutyContext(slug, cwd) {
|
|
14637
14735
|
if (!slug) return null;
|
|
14638
|
-
return readDutyFolder(
|
|
14736
|
+
return readDutyFolder(path39.join(cwd, ".kody", "duties"), slug) ?? readDutyFolder(getProjectDutiesRoot(), slug) ?? readDutyFolder(getBuiltinDutiesRoot(), slug);
|
|
14639
14737
|
}
|
|
14640
14738
|
function mintInstantJob(dispatch2, opts) {
|
|
14641
14739
|
return {
|
|
@@ -14787,9 +14885,9 @@ function translateOpenAISseToBrain(opts) {
|
|
|
14787
14885
|
}
|
|
14788
14886
|
|
|
14789
14887
|
// src/servers/brain-serve.ts
|
|
14790
|
-
import * as
|
|
14888
|
+
import * as fs43 from "fs";
|
|
14791
14889
|
import { createServer } from "http";
|
|
14792
|
-
import * as
|
|
14890
|
+
import * as path42 from "path";
|
|
14793
14891
|
|
|
14794
14892
|
// src/chat/loop.ts
|
|
14795
14893
|
init_agent();
|
|
@@ -15158,6 +15256,7 @@ async function runChatTurn(opts) {
|
|
|
15158
15256
|
verbose: opts.verbose,
|
|
15159
15257
|
quiet: opts.quiet,
|
|
15160
15258
|
systemPromptAppend: systemPrompt,
|
|
15259
|
+
...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {},
|
|
15161
15260
|
// Let the agent clone + work on OTHER repos mid-conversation (a
|
|
15162
15261
|
// repo-less Brain serves many). Enabled whenever we know where repos
|
|
15163
15262
|
// live; grants read access to that root via additionalDirectories.
|
|
@@ -15330,8 +15429,8 @@ init_config();
|
|
|
15330
15429
|
|
|
15331
15430
|
// src/kody-cli.ts
|
|
15332
15431
|
import { execFileSync as execFileSync26 } from "child_process";
|
|
15333
|
-
import * as
|
|
15334
|
-
import * as
|
|
15432
|
+
import * as fs41 from "fs";
|
|
15433
|
+
import * as path40 from "path";
|
|
15335
15434
|
|
|
15336
15435
|
// src/app-auth.ts
|
|
15337
15436
|
import { createSign } from "crypto";
|
|
@@ -15926,9 +16025,9 @@ async function resolveAuthToken(env = process.env) {
|
|
|
15926
16025
|
return void 0;
|
|
15927
16026
|
}
|
|
15928
16027
|
function detectPackageManager2(cwd) {
|
|
15929
|
-
if (
|
|
15930
|
-
if (
|
|
15931
|
-
if (
|
|
16028
|
+
if (fs41.existsSync(path40.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
16029
|
+
if (fs41.existsSync(path40.join(cwd, "yarn.lock"))) return "yarn";
|
|
16030
|
+
if (fs41.existsSync(path40.join(cwd, "bun.lockb"))) return "bun";
|
|
15932
16031
|
return "npm";
|
|
15933
16032
|
}
|
|
15934
16033
|
function shellOut(cmd, args, cwd, stream = true) {
|
|
@@ -16015,11 +16114,11 @@ function configureGitIdentity(cwd) {
|
|
|
16015
16114
|
}
|
|
16016
16115
|
function postFailureTail(issueNumber, cwd, reason) {
|
|
16017
16116
|
if (!issueNumber) return;
|
|
16018
|
-
const logPath =
|
|
16117
|
+
const logPath = path40.join(cwd, ".kody", "last-run.jsonl");
|
|
16019
16118
|
let tail = "";
|
|
16020
16119
|
try {
|
|
16021
|
-
if (
|
|
16022
|
-
const content =
|
|
16120
|
+
if (fs41.existsSync(logPath)) {
|
|
16121
|
+
const content = fs41.readFileSync(logPath, "utf-8");
|
|
16023
16122
|
tail = content.slice(-3e3);
|
|
16024
16123
|
}
|
|
16025
16124
|
} catch {
|
|
@@ -16044,7 +16143,7 @@ async function runCi(argv) {
|
|
|
16044
16143
|
return 0;
|
|
16045
16144
|
}
|
|
16046
16145
|
const args = parseCiArgs(argv);
|
|
16047
|
-
const cwd = args.cwd ?
|
|
16146
|
+
const cwd = args.cwd ? path40.resolve(args.cwd) : process.cwd();
|
|
16048
16147
|
let earlyConfig;
|
|
16049
16148
|
let earlyConfigError;
|
|
16050
16149
|
try {
|
|
@@ -16057,9 +16156,9 @@ async function runCi(argv) {
|
|
|
16057
16156
|
const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
|
|
16058
16157
|
let manualWorkflowDispatch = false;
|
|
16059
16158
|
let forceRunAction = null;
|
|
16060
|
-
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath &&
|
|
16159
|
+
if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs41.existsSync(dispatchEventPath)) {
|
|
16061
16160
|
try {
|
|
16062
|
-
const evt = JSON.parse(
|
|
16161
|
+
const evt = JSON.parse(fs41.readFileSync(dispatchEventPath, "utf-8"));
|
|
16063
16162
|
const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
|
|
16064
16163
|
const sessionInput = String(evt?.inputs?.sessionId ?? "");
|
|
16065
16164
|
const dutyInput = String(evt?.inputs?.duty ?? evt?.inputs?.executable ?? "").trim();
|
|
@@ -16359,16 +16458,16 @@ init_litellm();
|
|
|
16359
16458
|
init_repoWorkspace();
|
|
16360
16459
|
|
|
16361
16460
|
// src/scripts/brainTurnLog.ts
|
|
16362
|
-
import * as
|
|
16363
|
-
import * as
|
|
16461
|
+
import * as fs42 from "fs";
|
|
16462
|
+
import * as path41 from "path";
|
|
16364
16463
|
var live = /* @__PURE__ */ new Map();
|
|
16365
16464
|
function eventsPath(dir, chatId) {
|
|
16366
|
-
return
|
|
16465
|
+
return path41.join(dir, ".kody", "brain-events", `${chatId}.jsonl`);
|
|
16367
16466
|
}
|
|
16368
16467
|
function lastPersistedSeq(dir, chatId) {
|
|
16369
16468
|
const p = eventsPath(dir, chatId);
|
|
16370
|
-
if (!
|
|
16371
|
-
const lines =
|
|
16469
|
+
if (!fs42.existsSync(p)) return 0;
|
|
16470
|
+
const lines = fs42.readFileSync(p, "utf-8").split("\n").filter(Boolean);
|
|
16372
16471
|
if (lines.length === 0) return 0;
|
|
16373
16472
|
try {
|
|
16374
16473
|
return JSON.parse(lines[lines.length - 1]).seq || 0;
|
|
@@ -16378,9 +16477,9 @@ function lastPersistedSeq(dir, chatId) {
|
|
|
16378
16477
|
}
|
|
16379
16478
|
function readSince(dir, chatId, since) {
|
|
16380
16479
|
const p = eventsPath(dir, chatId);
|
|
16381
|
-
if (!
|
|
16480
|
+
if (!fs42.existsSync(p)) return [];
|
|
16382
16481
|
const out = [];
|
|
16383
|
-
for (const line of
|
|
16482
|
+
for (const line of fs42.readFileSync(p, "utf-8").split("\n")) {
|
|
16384
16483
|
if (!line) continue;
|
|
16385
16484
|
try {
|
|
16386
16485
|
const rec = JSON.parse(line);
|
|
@@ -16406,12 +16505,12 @@ function beginTurn(dir, chatId) {
|
|
|
16406
16505
|
};
|
|
16407
16506
|
live.set(chatId, state);
|
|
16408
16507
|
const p = eventsPath(dir, chatId);
|
|
16409
|
-
|
|
16508
|
+
fs42.mkdirSync(path41.dirname(p), { recursive: true });
|
|
16410
16509
|
return (event) => {
|
|
16411
16510
|
state.seq += 1;
|
|
16412
16511
|
const rec = { seq: state.seq, turn, ts: Date.now(), event };
|
|
16413
16512
|
try {
|
|
16414
|
-
|
|
16513
|
+
fs42.appendFileSync(p, `${JSON.stringify(rec)}
|
|
16415
16514
|
`);
|
|
16416
16515
|
} catch (err) {
|
|
16417
16516
|
process.stderr.write(
|
|
@@ -16450,7 +16549,7 @@ function endTurnIfUnterminated(dir, chatId, errMessage) {
|
|
|
16450
16549
|
event: { type: "error", error: errMessage || "turn ended unexpectedly", chatId }
|
|
16451
16550
|
};
|
|
16452
16551
|
try {
|
|
16453
|
-
|
|
16552
|
+
fs42.appendFileSync(eventsPath(dir, chatId), `${JSON.stringify(rec)}
|
|
16454
16553
|
`);
|
|
16455
16554
|
} catch {
|
|
16456
16555
|
}
|
|
@@ -16681,7 +16780,7 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
16681
16780
|
const repo = strField(body, "repo");
|
|
16682
16781
|
const repoToken = strField(body, "repoToken");
|
|
16683
16782
|
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
16684
|
-
|
|
16783
|
+
fs43.mkdirSync(path42.dirname(sessionFile), { recursive: true });
|
|
16685
16784
|
appendTurn(sessionFile, {
|
|
16686
16785
|
role: "user",
|
|
16687
16786
|
content: message,
|
|
@@ -16728,7 +16827,7 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
16728
16827
|
function buildServer(opts) {
|
|
16729
16828
|
const runTurn = opts.runTurn ?? runChatTurn;
|
|
16730
16829
|
const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
|
|
16731
|
-
const reposRoot = opts.reposRoot ??
|
|
16830
|
+
const reposRoot = opts.reposRoot ?? path42.join(path42.dirname(path42.resolve(opts.cwd)), "repos");
|
|
16732
16831
|
return createServer(async (req, res) => {
|
|
16733
16832
|
if (!req.method || !req.url) {
|
|
16734
16833
|
sendJson(res, 400, { error: "bad request" });
|
|
@@ -17321,14 +17420,14 @@ async function loadConfigSafe() {
|
|
|
17321
17420
|
|
|
17322
17421
|
// src/chat-cli.ts
|
|
17323
17422
|
import { execFileSync as execFileSync29 } from "child_process";
|
|
17324
|
-
import * as
|
|
17325
|
-
import * as
|
|
17423
|
+
import * as fs45 from "fs";
|
|
17424
|
+
import * as path44 from "path";
|
|
17326
17425
|
|
|
17327
17426
|
// src/chat/modes/interactive.ts
|
|
17328
17427
|
init_issue();
|
|
17329
17428
|
import { execFileSync as execFileSync28 } from "child_process";
|
|
17330
|
-
import * as
|
|
17331
|
-
import * as
|
|
17429
|
+
import * as fs44 from "fs";
|
|
17430
|
+
import * as path43 from "path";
|
|
17332
17431
|
|
|
17333
17432
|
// src/chat/inbox.ts
|
|
17334
17433
|
import { execFileSync as execFileSync27 } from "child_process";
|
|
@@ -17461,7 +17560,8 @@ async function runInteractiveMode(opts) {
|
|
|
17461
17560
|
sink: opts.sink,
|
|
17462
17561
|
verbose: opts.verbose,
|
|
17463
17562
|
quiet: opts.quiet,
|
|
17464
|
-
invokeAgent: opts.invokeAgent
|
|
17563
|
+
invokeAgent: opts.invokeAgent,
|
|
17564
|
+
...opts.reasoningEffort ? { reasoningEffort: opts.reasoningEffort } : {}
|
|
17465
17565
|
});
|
|
17466
17566
|
} catch (err) {
|
|
17467
17567
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -17487,9 +17587,9 @@ function findNextUserTurn(turns, fromIdx) {
|
|
|
17487
17587
|
return -1;
|
|
17488
17588
|
}
|
|
17489
17589
|
function commitTurn(cwd, sessionId, _verbose) {
|
|
17490
|
-
const sessionRel =
|
|
17491
|
-
const eventsRel =
|
|
17492
|
-
const rels = [sessionRel, eventsRel].filter((p) =>
|
|
17590
|
+
const sessionRel = path43.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
17591
|
+
const eventsRel = path43.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
17592
|
+
const rels = [sessionRel, eventsRel].filter((p) => fs44.existsSync(path43.join(cwd, p)));
|
|
17493
17593
|
if (rels.length === 0) return;
|
|
17494
17594
|
const repository = process.env.GITHUB_REPOSITORY;
|
|
17495
17595
|
if (!repository) {
|
|
@@ -17501,8 +17601,8 @@ function commitTurn(cwd, sessionId, _verbose) {
|
|
|
17501
17601
|
}
|
|
17502
17602
|
const branch = defaultBranch(cwd) ?? "main";
|
|
17503
17603
|
for (const rel of rels) {
|
|
17504
|
-
const repoPath = rel.split(
|
|
17505
|
-
const localText =
|
|
17604
|
+
const repoPath = rel.split(path43.sep).join("/");
|
|
17605
|
+
const localText = fs44.readFileSync(path43.join(cwd, rel), "utf-8");
|
|
17506
17606
|
putJsonlViaContents(repository, branch, repoPath, localText, sessionId, cwd);
|
|
17507
17607
|
}
|
|
17508
17608
|
}
|
|
@@ -17600,11 +17700,17 @@ var CHAT_HELP = `kody chat \u2014 dashboard-driven chat session
|
|
|
17600
17700
|
|
|
17601
17701
|
Usage:
|
|
17602
17702
|
kody chat [--session <id>] [--message <text>] [--model <provider/model>]
|
|
17703
|
+
[--reasoning-effort <off|low|medium|high>]
|
|
17603
17704
|
[--dashboard-url <url>] [--cwd <path>] [--verbose|--quiet]
|
|
17604
17705
|
|
|
17605
|
-
All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, DASHBOARD_URL.
|
|
17706
|
+
All inputs may also come from env: SESSION_ID, INIT_MESSAGE, MODEL, REASONING_EFFORT, DASHBOARD_URL.
|
|
17606
17707
|
CLI flags take precedence over env. SESSION_ID is required.
|
|
17607
17708
|
|
|
17709
|
+
Thinking level maps to the Claude Agent SDK's maxThinkingTokens (Anthropic
|
|
17710
|
+
extended thinking). Default is unset (no thinking \u2014 cheapest). Set via the
|
|
17711
|
+
dashboard's chat-level thinking dropdown, the REASONING_EFFORT env var,
|
|
17712
|
+
or this flag.
|
|
17713
|
+
|
|
17608
17714
|
Exit codes:
|
|
17609
17715
|
0 reply emitted successfully
|
|
17610
17716
|
64 bad inputs (missing session, empty history)
|
|
@@ -17617,6 +17723,7 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
17617
17723
|
if (arg === "--session") result.sessionId = argv[++i];
|
|
17618
17724
|
else if (arg === "--message") result.initMessage = argv[++i];
|
|
17619
17725
|
else if (arg === "--model") result.model = argv[++i];
|
|
17726
|
+
else if (arg === "--reasoning-effort") result.reasoningEffort = parseReasoningEffort(argv[++i]) ?? void 0;
|
|
17620
17727
|
else if (arg === "--dashboard-url") result.dashboardUrl = argv[++i];
|
|
17621
17728
|
else if (arg === "--cwd") result.cwd = argv[++i];
|
|
17622
17729
|
else if (arg === "--verbose") result.verbose = true;
|
|
@@ -17629,22 +17736,23 @@ function parseChatArgs(argv, env = process.env) {
|
|
|
17629
17736
|
result.initMessage = result.initMessage ?? env.INIT_MESSAGE ?? void 0;
|
|
17630
17737
|
result.model = result.model ?? env.MODEL ?? void 0;
|
|
17631
17738
|
result.dashboardUrl = result.dashboardUrl ?? env.DASHBOARD_URL ?? void 0;
|
|
17739
|
+
result.reasoningEffort = result.reasoningEffort ?? parseReasoningEffort(env.REASONING_EFFORT) ?? void 0;
|
|
17632
17740
|
for (const key of ["sessionId", "initMessage", "model", "dashboardUrl"]) {
|
|
17633
17741
|
const v = result[key];
|
|
17634
17742
|
if (typeof v === "string" && v.trim() === "") result[key] = void 0;
|
|
17635
17743
|
}
|
|
17636
17744
|
if (!result.sessionId && !result.errors.includes("__HELP__")) {
|
|
17637
|
-
result.errors.push("--session <id> (or SESSION_ID env) is required");
|
|
17745
|
+
result.errors.push("--session <id> (or SESSION_ID env) is required)");
|
|
17638
17746
|
}
|
|
17639
17747
|
return result;
|
|
17640
17748
|
}
|
|
17641
17749
|
function commitChatFiles(cwd, sessionId, verbose) {
|
|
17642
|
-
const sessionFile =
|
|
17643
|
-
const eventsFile =
|
|
17750
|
+
const sessionFile = path44.relative(cwd, sessionFilePath(cwd, sessionId));
|
|
17751
|
+
const eventsFile = path44.relative(cwd, eventsFilePath(cwd, sessionId));
|
|
17644
17752
|
const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
17645
|
-
const tasksDir =
|
|
17753
|
+
const tasksDir = path44.join(".kody", "tasks", safeSession);
|
|
17646
17754
|
const candidatePaths = [sessionFile, eventsFile, tasksDir];
|
|
17647
|
-
const paths = candidatePaths.filter((p) =>
|
|
17755
|
+
const paths = candidatePaths.filter((p) => fs45.existsSync(path44.join(cwd, p)));
|
|
17648
17756
|
if (paths.length === 0) return;
|
|
17649
17757
|
const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
|
|
17650
17758
|
try {
|
|
@@ -17688,7 +17796,7 @@ async function runChat(argv) {
|
|
|
17688
17796
|
${CHAT_HELP}`);
|
|
17689
17797
|
return 64;
|
|
17690
17798
|
}
|
|
17691
|
-
const cwd = args.cwd ?
|
|
17799
|
+
const cwd = args.cwd ? path44.resolve(args.cwd) : process.cwd();
|
|
17692
17800
|
const sessionId = args.sessionId;
|
|
17693
17801
|
const unpackedSecrets = unpackAllSecrets();
|
|
17694
17802
|
if (unpackedSecrets > 0) {
|
|
@@ -17699,6 +17807,7 @@ ${CHAT_HELP}`);
|
|
|
17699
17807
|
configureGitIdentity(cwd);
|
|
17700
17808
|
const config = tryLoadConfig(cwd);
|
|
17701
17809
|
const modelSpec = args.model ?? config?.agent.model ?? DEFAULT_MODEL2;
|
|
17810
|
+
const reasoningEffort = args.reasoningEffort ?? config?.agent.reasoningEffort ?? void 0;
|
|
17702
17811
|
let model;
|
|
17703
17812
|
try {
|
|
17704
17813
|
model = parseProviderModel(modelSpec);
|
|
@@ -17740,7 +17849,7 @@ ${CHAT_HELP}`);
|
|
|
17740
17849
|
const sink = buildSink(cwd, sessionId, args.dashboardUrl);
|
|
17741
17850
|
const meta = readMeta(sessionFile);
|
|
17742
17851
|
process.stdout.write(
|
|
17743
|
-
`\u2192 kody:chat: session file=${sessionFile} exists=${
|
|
17852
|
+
`\u2192 kody:chat: session file=${sessionFile} exists=${fs45.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
|
|
17744
17853
|
`
|
|
17745
17854
|
);
|
|
17746
17855
|
try {
|
|
@@ -17753,7 +17862,8 @@ ${CHAT_HELP}`);
|
|
|
17753
17862
|
sink,
|
|
17754
17863
|
meta,
|
|
17755
17864
|
verbose: args.verbose,
|
|
17756
|
-
quiet: args.quiet
|
|
17865
|
+
quiet: args.quiet,
|
|
17866
|
+
...reasoningEffort ? { reasoningEffort } : {}
|
|
17757
17867
|
});
|
|
17758
17868
|
return result2.exitCode;
|
|
17759
17869
|
}
|
|
@@ -17765,7 +17875,8 @@ ${CHAT_HELP}`);
|
|
|
17765
17875
|
litellmUrl: litellm?.url ?? null,
|
|
17766
17876
|
sink,
|
|
17767
17877
|
verbose: args.verbose,
|
|
17768
|
-
quiet: args.quiet
|
|
17878
|
+
quiet: args.quiet,
|
|
17879
|
+
...reasoningEffort ? { reasoningEffort } : {}
|
|
17769
17880
|
});
|
|
17770
17881
|
commitChatFiles(cwd, sessionId, args.verbose ?? false);
|
|
17771
17882
|
return result.exitCode;
|
|
@@ -17896,8 +18007,8 @@ var FlyClient = class {
|
|
|
17896
18007
|
get fetch() {
|
|
17897
18008
|
return this.opts.fetchImpl ?? fetch;
|
|
17898
18009
|
}
|
|
17899
|
-
async call(
|
|
17900
|
-
const res = await this.fetch(`${FLY_API_BASE}${
|
|
18010
|
+
async call(path45, init = {}) {
|
|
18011
|
+
const res = await this.fetch(`${FLY_API_BASE}${path45}`, {
|
|
17901
18012
|
method: init.method ?? "GET",
|
|
17902
18013
|
headers: {
|
|
17903
18014
|
Authorization: `Bearer ${this.opts.token}`,
|
|
@@ -17908,7 +18019,7 @@ var FlyClient = class {
|
|
|
17908
18019
|
if (res.status === 404 && init.allow404) return null;
|
|
17909
18020
|
if (!res.ok) {
|
|
17910
18021
|
const text = await res.text().catch(() => "");
|
|
17911
|
-
throw new Error(`Fly API ${res.status} on ${
|
|
18022
|
+
throw new Error(`Fly API ${res.status} on ${path45}: ${text.slice(0, 200) || res.statusText}`);
|
|
17912
18023
|
}
|
|
17913
18024
|
if (res.status === 204) return null;
|
|
17914
18025
|
const raw = await res.text();
|
|
@@ -18134,7 +18245,9 @@ var PoolManager = class {
|
|
|
18134
18245
|
const destroyedSurplus = await this.destroySurplus(surplus);
|
|
18135
18246
|
const destroyed = destroyedTracked + destroyedSurplus;
|
|
18136
18247
|
if (pruned > 0 || adopted > 0 || destroyed > 0) {
|
|
18137
|
-
this.log(
|
|
18248
|
+
this.log(
|
|
18249
|
+
`resync: pruned ${pruned} stale, adopted ${adopted}, destroyed ${destroyed} surplus (free=${this.free.length})`
|
|
18250
|
+
);
|
|
18138
18251
|
}
|
|
18139
18252
|
await this.refill();
|
|
18140
18253
|
}
|
|
@@ -18631,7 +18744,7 @@ async function poolServe() {
|
|
|
18631
18744
|
|
|
18632
18745
|
// src/servers/runner-serve.ts
|
|
18633
18746
|
import { spawn as spawn8 } from "child_process";
|
|
18634
|
-
import * as
|
|
18747
|
+
import * as fs46 from "fs";
|
|
18635
18748
|
import { createServer as createServer5 } from "http";
|
|
18636
18749
|
var DEFAULT_PORT2 = 8080;
|
|
18637
18750
|
var DEFAULT_WORKDIR = "/workspace/repo";
|
|
@@ -18711,8 +18824,8 @@ async function defaultRunJob(job) {
|
|
|
18711
18824
|
const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
|
|
18712
18825
|
const branch = job.ref ?? "main";
|
|
18713
18826
|
const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
|
|
18714
|
-
|
|
18715
|
-
|
|
18827
|
+
fs46.rmSync(workdir, { recursive: true, force: true });
|
|
18828
|
+
fs46.mkdirSync(workdir, { recursive: true });
|
|
18716
18829
|
const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
|
|
18717
18830
|
const interactive = job.mode === "interactive";
|
|
18718
18831
|
const scheduled = job.mode === "scheduled";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "preview-health",
|
|
3
|
+
"role": "watch",
|
|
4
|
+
"kind": "oneshot",
|
|
5
|
+
"describe": "Scan open PRs and dispatch mechanical preview repairs.",
|
|
6
|
+
"inputs": [],
|
|
7
|
+
"claudeCode": {
|
|
8
|
+
"model": "inherit",
|
|
9
|
+
"permissionMode": "default",
|
|
10
|
+
"maxTurns": 0,
|
|
11
|
+
"maxThinkingTokens": null,
|
|
12
|
+
"systemPromptAppend": null,
|
|
13
|
+
"tools": [],
|
|
14
|
+
"hooks": [],
|
|
15
|
+
"skills": [],
|
|
16
|
+
"commands": [],
|
|
17
|
+
"subagents": [],
|
|
18
|
+
"plugins": [],
|
|
19
|
+
"mcpServers": []
|
|
20
|
+
},
|
|
21
|
+
"cliTools": [
|
|
22
|
+
{
|
|
23
|
+
"name": "gh",
|
|
24
|
+
"install": {
|
|
25
|
+
"required": true,
|
|
26
|
+
"checkCommand": "command -v gh"
|
|
27
|
+
},
|
|
28
|
+
"verify": "gh auth status",
|
|
29
|
+
"usage": "Reads open PRs, reads the CTO trust ledger, dispatches Kody workflows, and posts PR audit/recommendation comments.",
|
|
30
|
+
"allowedUses": ["pr", "api", "issue", "workflow"]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
33
|
+
"inputArtifacts": [],
|
|
34
|
+
"outputArtifacts": [],
|
|
35
|
+
"scripts": {
|
|
36
|
+
"preflight": [
|
|
37
|
+
{
|
|
38
|
+
"shell": "tick.sh",
|
|
39
|
+
"with": {
|
|
40
|
+
"jobsDir": ".kody/duties",
|
|
41
|
+
"duty": "preview-health"
|
|
42
|
+
},
|
|
43
|
+
"timeoutSec": 300
|
|
44
|
+
}
|
|
45
|
+
],
|
|
46
|
+
"postflight": []
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
jobs_dir="${1:-.kody/duties}"
|
|
5
|
+
duty="${2:-preview-health}"
|
|
6
|
+
|
|
7
|
+
export KODY_PREVIEW_HEALTH_JOBS_DIR="$jobs_dir"
|
|
8
|
+
export KODY_PREVIEW_HEALTH_DUTY="$duty"
|
|
9
|
+
|
|
10
|
+
python3 <<'PY'
|
|
11
|
+
import base64
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import re
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
from datetime import datetime, timezone
|
|
18
|
+
|
|
19
|
+
DECISIONS_LABEL = "kody:cto-decisions"
|
|
20
|
+
LEDGER_START = "<!-- kody-cto-decisions:start -->"
|
|
21
|
+
LEDGER_END = "<!-- kody-cto-decisions:end -->"
|
|
22
|
+
VERBS = ("fix-ci", "sync", "resolve")
|
|
23
|
+
AUTO_VERBS = {"resolve"}
|
|
24
|
+
MAX_ACTIONS_PER_TICK = 5
|
|
25
|
+
FAIL_CONCLUSIONS = {"FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"}
|
|
26
|
+
RUNNING_STATUSES = {"IN_PROGRESS", "QUEUED"}
|
|
27
|
+
STALE_THRESHOLD = 10
|
|
28
|
+
STATE_BRANCH = "kody-state"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def log(message):
|
|
32
|
+
print(f"[preview-health] {message}", file=sys.stderr)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def now_iso():
|
|
36
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def run_gh(args, input_text=None, check=True):
|
|
40
|
+
result = subprocess.run(
|
|
41
|
+
["gh", *args],
|
|
42
|
+
input=input_text,
|
|
43
|
+
text=True,
|
|
44
|
+
stdout=subprocess.PIPE,
|
|
45
|
+
stderr=subprocess.PIPE,
|
|
46
|
+
cwd=os.getcwd(),
|
|
47
|
+
)
|
|
48
|
+
if check and result.returncode != 0:
|
|
49
|
+
raise RuntimeError(result.stderr.strip() or result.stdout.strip() or f"gh {' '.join(args)} failed")
|
|
50
|
+
return result.stdout
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def repo_slug():
|
|
54
|
+
owner = os.environ.get("KODY_CFG_GITHUB_OWNER", "").strip()
|
|
55
|
+
repo = os.environ.get("KODY_CFG_GITHUB_REPO", "").strip()
|
|
56
|
+
if owner and repo:
|
|
57
|
+
return f"{owner}/{repo}"
|
|
58
|
+
return run_gh(["repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner"]).strip()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def default_branch(slug):
|
|
62
|
+
configured = os.environ.get("KODY_CFG_GIT_DEFAULTBRANCH", "").strip()
|
|
63
|
+
if configured:
|
|
64
|
+
return configured
|
|
65
|
+
return run_gh(["repo", "view", slug, "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"]).strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def ensure_state_branch(slug):
|
|
69
|
+
if run_gh(["api", f"/repos/{slug}/git/ref/heads/{STATE_BRANCH}"], check=False).strip():
|
|
70
|
+
return
|
|
71
|
+
branch = default_branch(slug)
|
|
72
|
+
sha = run_gh(["api", f"/repos/{slug}/git/ref/heads/{branch}", "--jq", ".object.sha"]).strip()
|
|
73
|
+
payload = json.dumps({"ref": f"refs/heads/{STATE_BRANCH}", "sha": sha})
|
|
74
|
+
run_gh(["api", "--method", "POST", f"/repos/{slug}/git/refs", "--input", "-"], input_text=payload)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def initial_state():
|
|
78
|
+
return {"version": 1, "rev": 0, "cursor": "seed", "data": {}, "done": False}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_state(slug, path):
|
|
82
|
+
result = run_gh(
|
|
83
|
+
["api", f"/repos/{slug}/contents/{path}?ref={STATE_BRANCH}"],
|
|
84
|
+
check=False,
|
|
85
|
+
)
|
|
86
|
+
if not result.strip():
|
|
87
|
+
return {"path": path, "sha": None, "state": initial_state(), "created": True}
|
|
88
|
+
obj = json.loads(result)
|
|
89
|
+
raw = base64.b64decode(obj["content"]).decode("utf-8")
|
|
90
|
+
return {"path": path, "sha": obj.get("sha"), "state": json.loads(raw), "created": False}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def state_unchanged(prev, next_state):
|
|
94
|
+
return (
|
|
95
|
+
prev.get("cursor") == next_state.get("cursor")
|
|
96
|
+
and prev.get("done") == next_state.get("done")
|
|
97
|
+
and prev.get("data") == next_state.get("data")
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def save_state(slug, loaded, next_state):
|
|
102
|
+
if not loaded["created"] and state_unchanged(loaded["state"], next_state):
|
|
103
|
+
return False
|
|
104
|
+
ensure_state_branch(slug)
|
|
105
|
+
body = json.dumps(next_state, indent=2) + "\n"
|
|
106
|
+
payload = {
|
|
107
|
+
"message": f"chore(jobs): update state for {os.environ['KODY_PREVIEW_HEALTH_DUTY']} (rev {next_state['rev']})",
|
|
108
|
+
"content": base64.b64encode(body.encode("utf-8")).decode("ascii"),
|
|
109
|
+
"branch": STATE_BRANCH,
|
|
110
|
+
}
|
|
111
|
+
if loaded.get("sha"):
|
|
112
|
+
payload["sha"] = loaded["sha"]
|
|
113
|
+
result = run_gh(
|
|
114
|
+
["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"],
|
|
115
|
+
input_text=json.dumps(payload),
|
|
116
|
+
check=False,
|
|
117
|
+
)
|
|
118
|
+
if result.strip():
|
|
119
|
+
return True
|
|
120
|
+
reloaded = load_state(slug, loaded["path"])
|
|
121
|
+
if not reloaded["created"] and state_unchanged(reloaded["state"], next_state):
|
|
122
|
+
return False
|
|
123
|
+
if reloaded.get("sha"):
|
|
124
|
+
payload["sha"] = reloaded["sha"]
|
|
125
|
+
else:
|
|
126
|
+
payload.pop("sha", None)
|
|
127
|
+
run_gh(["api", "--method", "PUT", f"/repos/{slug}/contents/{loaded['path']}", "--input", "-"], input_text=json.dumps(payload))
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def read_ledger_modes():
|
|
132
|
+
modes = {verb: "ask" for verb in VERBS}
|
|
133
|
+
try:
|
|
134
|
+
raw = run_gh(["issue", "list", "--label", DECISIONS_LABEL, "--state", "open", "--limit", "20", "--json", "number,body"])
|
|
135
|
+
issues = json.loads(raw or "[]")
|
|
136
|
+
if not issues:
|
|
137
|
+
return modes
|
|
138
|
+
body = sorted(issues, key=lambda x: x.get("number", 10**9))[0].get("body", "")
|
|
139
|
+
if LEDGER_START not in body or LEDGER_END not in body:
|
|
140
|
+
return modes
|
|
141
|
+
inner = body.split(LEDGER_START, 1)[1].split(LEDGER_END, 1)[0]
|
|
142
|
+
match = re.search(r"```(?:json)?\s*(\{[\s\S]*?\})\s*```", inner)
|
|
143
|
+
if not match:
|
|
144
|
+
return modes
|
|
145
|
+
ledger = json.loads(match.group(1))
|
|
146
|
+
cto = ledger.get("staff", {}).get("cto", {})
|
|
147
|
+
for verb in VERBS:
|
|
148
|
+
if cto.get(verb, {}).get("mode") == "auto":
|
|
149
|
+
modes[verb] = "auto"
|
|
150
|
+
except Exception as err:
|
|
151
|
+
log(f"ledger read failed; treating all verbs as ask: {err}")
|
|
152
|
+
return modes
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def ci_failing(rollup):
|
|
156
|
+
if not isinstance(rollup, list):
|
|
157
|
+
return False
|
|
158
|
+
has_fail = any(str(check.get("conclusion", "")) in FAIL_CONCLUSIONS for check in rollup if isinstance(check, dict))
|
|
159
|
+
running = any(str(check.get("status", "")) in RUNNING_STATUSES for check in rollup if isinstance(check, dict))
|
|
160
|
+
return has_fail and not running
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def behind_by(slug, base, head):
|
|
164
|
+
try:
|
|
165
|
+
raw = run_gh(["api", f"repos/{slug}/compare/{base}...{head}", "--jq", ".behind_by"])
|
|
166
|
+
return int(raw.strip() or "0")
|
|
167
|
+
except Exception as err:
|
|
168
|
+
log(f"compare {base}...{head} failed: {err}")
|
|
169
|
+
return 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def detect_repair(slug, pr):
|
|
173
|
+
mergeable = str(pr.get("mergeable") or "").upper()
|
|
174
|
+
if mergeable == "UNKNOWN":
|
|
175
|
+
return ("defer", f"PR #{pr['number']} mergeability still UNKNOWN; retry next tick.")
|
|
176
|
+
if mergeable == "CONFLICTING":
|
|
177
|
+
return ("resolve", f"PR #{pr['number']} has merge conflicts with `{pr['baseRefName']}`.")
|
|
178
|
+
if ci_failing(pr.get("statusCheckRollup")):
|
|
179
|
+
return ("fix-ci", f"PR #{pr['number']} has failing CI checks.")
|
|
180
|
+
drift = behind_by(slug, pr["baseRefName"], pr["headRefName"])
|
|
181
|
+
if drift >= STALE_THRESHOLD:
|
|
182
|
+
return ("sync", f"PR #{pr['number']}'s branch is {drift} commits behind `{pr['baseRefName']}`.")
|
|
183
|
+
return None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def duty_operator(jobs_dir, duty):
|
|
187
|
+
try:
|
|
188
|
+
with open(os.path.join(os.getcwd(), jobs_dir, duty, "profile.json"), "r", encoding="utf-8") as f:
|
|
189
|
+
profile = json.load(f)
|
|
190
|
+
mentions = profile.get("mentions", [])
|
|
191
|
+
return mentions[0] if isinstance(mentions, list) and mentions else ""
|
|
192
|
+
except Exception:
|
|
193
|
+
return ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def post_comment(pr_number, body):
|
|
197
|
+
if os.environ.get("KODY_DRY_RUN") == "1":
|
|
198
|
+
log(f"[dry-run] would comment on #{pr_number}: {body.splitlines()[0]}")
|
|
199
|
+
return True
|
|
200
|
+
try:
|
|
201
|
+
run_gh(["pr", "comment", str(pr_number), "--body", body])
|
|
202
|
+
return True
|
|
203
|
+
except Exception as err:
|
|
204
|
+
log(f"comment failed on #{pr_number}: {err}")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def recommend(pr_number, verb, reason, operator):
|
|
209
|
+
mention = f"@{operator} " if operator else ""
|
|
210
|
+
body = (
|
|
211
|
+
f"{mention}**CTO recommendation** - `{verb}`\n\n"
|
|
212
|
+
f"{reason} Recommended action: `{verb}` for PR #{pr_number}.\n\n"
|
|
213
|
+
"_Confirm in dashboard inbox or run the action manually. CTO will not act on its own._"
|
|
214
|
+
)
|
|
215
|
+
return post_comment(pr_number, body)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def auto_run(pr_number, verb, reason):
|
|
219
|
+
if os.environ.get("KODY_DRY_RUN") == "1":
|
|
220
|
+
log(f"[dry-run] would dispatch kody.yml executable={verb} issue_number={pr_number}")
|
|
221
|
+
else:
|
|
222
|
+
try:
|
|
223
|
+
run_gh(["workflow", "run", "kody.yml", "-f", f"executable={verb}", "-f", f"issue_number={pr_number}"])
|
|
224
|
+
except Exception as err:
|
|
225
|
+
log(f"workflow_dispatch failed #{pr_number} ({verb}): {err}")
|
|
226
|
+
return False
|
|
227
|
+
auto_reason = (
|
|
228
|
+
"Policy: preview-health auto-runs `resolve` for merge conflicts."
|
|
229
|
+
if verb in AUTO_VERBS
|
|
230
|
+
else f"Graduated: operator approved `{verb}` repeatedly. A **Reject** on any `{verb}` returns me asking."
|
|
231
|
+
)
|
|
232
|
+
return post_comment(pr_number, f"**CTO action** - `{verb}` dispatched\n\n{reason}\n\n{auto_reason}")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def print_row(pr, verb, fp, action, note):
|
|
236
|
+
print(f"| #{pr} | {verb} | {fp} | {action} | {note} |")
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def main():
|
|
240
|
+
slug = repo_slug()
|
|
241
|
+
jobs_dir = os.environ["KODY_PREVIEW_HEALTH_JOBS_DIR"].rstrip("/")
|
|
242
|
+
duty = os.environ["KODY_PREVIEW_HEALTH_DUTY"]
|
|
243
|
+
state_path = f"{jobs_dir}/{duty}/state.json"
|
|
244
|
+
loaded = load_state(slug, state_path)
|
|
245
|
+
modes = read_ledger_modes()
|
|
246
|
+
operator = duty_operator(jobs_dir, duty)
|
|
247
|
+
|
|
248
|
+
prs = json.loads(
|
|
249
|
+
run_gh([
|
|
250
|
+
"pr",
|
|
251
|
+
"list",
|
|
252
|
+
"--state",
|
|
253
|
+
"open",
|
|
254
|
+
"--limit",
|
|
255
|
+
"100",
|
|
256
|
+
"--json",
|
|
257
|
+
"number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt",
|
|
258
|
+
])
|
|
259
|
+
or "[]"
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
prior = loaded["state"].get("data", {}).get("prs", {})
|
|
263
|
+
open_numbers = {str(pr["number"]) for pr in prs}
|
|
264
|
+
next_prs = {key: value for key, value in prior.items() if key in open_numbers and isinstance(value, dict)}
|
|
265
|
+
|
|
266
|
+
print("| PR | verb | fingerprint | action | note |")
|
|
267
|
+
print("|----|------|-------------|--------|------|")
|
|
268
|
+
|
|
269
|
+
priority = {"resolve": 0, "fix-ci": 1, "sync": 2}
|
|
270
|
+
queue = []
|
|
271
|
+
for pr in prs:
|
|
272
|
+
if pr.get("isDraft"):
|
|
273
|
+
print_row(pr["number"], "-", "-", "skip", "draft")
|
|
274
|
+
continue
|
|
275
|
+
repair = detect_repair(slug, pr)
|
|
276
|
+
if not repair:
|
|
277
|
+
print_row(pr["number"], "-", "-", "skip", "healthy")
|
|
278
|
+
continue
|
|
279
|
+
verb, reason = repair
|
|
280
|
+
if verb == "defer":
|
|
281
|
+
print_row(pr["number"], "-", "-", "defer", "mergeable=UNKNOWN")
|
|
282
|
+
continue
|
|
283
|
+
queue.append((priority[verb], pr["number"], pr, verb, reason))
|
|
284
|
+
|
|
285
|
+
actions_taken = 0
|
|
286
|
+
for _, number, pr, verb, reason in sorted(queue):
|
|
287
|
+
key = str(number)
|
|
288
|
+
fp = f"{verb}|{pr.get('headRefOid', '')}"
|
|
289
|
+
graduated = verb in AUTO_VERBS or modes.get(verb) == "auto"
|
|
290
|
+
intended_stage = f"{verb}-auto" if graduated else f"{verb}-recommended"
|
|
291
|
+
prev = next_prs.get(key, {})
|
|
292
|
+
if prev.get("fp") == fp and prev.get("stage") == intended_stage:
|
|
293
|
+
print_row(number, verb, fp[:24], "skip", "dedup")
|
|
294
|
+
continue
|
|
295
|
+
if actions_taken >= MAX_ACTIONS_PER_TICK:
|
|
296
|
+
print_row(number, verb, fp[:24], "defer", "tick cap")
|
|
297
|
+
continue
|
|
298
|
+
|
|
299
|
+
ok = auto_run(number, verb, reason) if graduated else recommend(number, verb, reason, operator)
|
|
300
|
+
action = "auto-ran" if graduated and ok else "auto-failed" if graduated else "recommended" if ok else "recommend-failed"
|
|
301
|
+
if ok:
|
|
302
|
+
actions_taken += 1
|
|
303
|
+
next_prs[key] = {"fp": fp, "stage": intended_stage, "lastActAt": now_iso()}
|
|
304
|
+
print_row(number, verb, fp[:24], action, "auto" if graduated else "advisory")
|
|
305
|
+
|
|
306
|
+
next_state = {
|
|
307
|
+
"version": 1,
|
|
308
|
+
"rev": int(loaded["state"].get("rev", 0)) + 1,
|
|
309
|
+
"cursor": "idle",
|
|
310
|
+
"data": {"prs": next_prs, "lastFiredAt": now_iso()},
|
|
311
|
+
"done": False,
|
|
312
|
+
}
|
|
313
|
+
save_state(slug, loaded, next_state)
|
|
314
|
+
log(f"tick complete: {actions_taken} action(s), {len(next_prs)} tracked PR(s)")
|
|
315
|
+
print("KODY_SKIP_AGENT=true")
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
main()
|
|
319
|
+
PY
|
|
@@ -261,8 +261,10 @@ open_prepare_pr() {
|
|
|
261
261
|
fi
|
|
262
262
|
|
|
263
263
|
# Bump version files.
|
|
264
|
-
local files
|
|
265
|
-
|
|
264
|
+
local files=()
|
|
265
|
+
while IFS= read -r f; do
|
|
266
|
+
files+=("$f")
|
|
267
|
+
done < <(resolve_version_files)
|
|
266
268
|
local touched=()
|
|
267
269
|
for f in "${files[@]}"; do
|
|
268
270
|
local res
|
package/kody.config.schema.json
CHANGED
|
@@ -63,6 +63,11 @@
|
|
|
63
63
|
"pattern": "^[^/]+/.+$",
|
|
64
64
|
"description": "Base provider/model string, for example minimax/MiniMax-M3 or claude/claude-sonnet-4-6."
|
|
65
65
|
},
|
|
66
|
+
"reasoningEffort": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"enum": ["off", "low", "medium", "high"],
|
|
69
|
+
"description": "Default thinking effort for the Claude Agent SDK. Maps to maxThinkingTokens. 'off' (default) skips the thinking block entirely — no reasoning preamble, no extra tokens. 'low'/'medium'/'high' enable extended thinking with budgets 2_048/10_000/32_000. Overridable per-session via the REASONING_EFFORT env var or the dashboard's chat-level thinking dropdown."
|
|
70
|
+
},
|
|
66
71
|
"perExecutable": {
|
|
67
72
|
"type": "object",
|
|
68
73
|
"description": "Optional provider/model override by executable name.",
|
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.223",
|
|
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",
|