@kody-ade/kody-engine 0.4.138 → 0.4.140
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
|
@@ -93,7 +93,7 @@ var init_events = __esm({
|
|
|
93
93
|
// src/verify.ts
|
|
94
94
|
import { spawn } from "child_process";
|
|
95
95
|
function runCommand(command, cwd) {
|
|
96
|
-
return new Promise((
|
|
96
|
+
return new Promise((resolve5) => {
|
|
97
97
|
const start = Date.now();
|
|
98
98
|
const child = spawn(command, {
|
|
99
99
|
cwd,
|
|
@@ -122,11 +122,11 @@ function runCommand(command, cwd) {
|
|
|
122
122
|
child.on("exit", (code) => {
|
|
123
123
|
clearTimeout(timer);
|
|
124
124
|
const tail = Buffer.concat(buffers).toString("utf-8").slice(-TAIL_CHARS);
|
|
125
|
-
|
|
125
|
+
resolve5({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
|
|
126
126
|
});
|
|
127
127
|
child.on("error", (err) => {
|
|
128
128
|
clearTimeout(timer);
|
|
129
|
-
|
|
129
|
+
resolve5({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
}
|
|
@@ -312,6 +312,52 @@ var init_verifyMcp = __esm({
|
|
|
312
312
|
}
|
|
313
313
|
});
|
|
314
314
|
|
|
315
|
+
// src/submitMcp.ts
|
|
316
|
+
var submitMcp_exports = {};
|
|
317
|
+
__export(submitMcp_exports, {
|
|
318
|
+
buildSubmitMcpServer: () => buildSubmitMcpServer
|
|
319
|
+
});
|
|
320
|
+
import { createSdkMcpServer as createSdkMcpServer2, tool as tool2 } from "@anthropic-ai/claude-agent-sdk";
|
|
321
|
+
import { z as z2 } from "zod";
|
|
322
|
+
function buildSubmitMcpServer() {
|
|
323
|
+
let submitted;
|
|
324
|
+
const submitTool = tool2(
|
|
325
|
+
"submit_state",
|
|
326
|
+
"Persist this tick's next state. Call this EXACTLY ONCE, at the very end, when you've finished your work \u2014 it is the ONLY way your decision is saved. Pass your next `cursor` (string), your next `data` (object \u2014 carry prior data forward and mutate what you acted on), and `done` (boolean). After calling it you are finished; do not take further actions.",
|
|
327
|
+
{
|
|
328
|
+
cursor: z2.string().describe('The next cursor value (e.g. "idle"). Must be a non-empty string.'),
|
|
329
|
+
data: z2.record(z2.string(), z2.unknown()).describe("The next `data` object. Carry forward prior data and mutate only what you acted on this tick."),
|
|
330
|
+
done: z2.boolean().describe("true only if this duty is permanently finished; evergreen duties stay false.")
|
|
331
|
+
},
|
|
332
|
+
async (args) => {
|
|
333
|
+
submitted = {
|
|
334
|
+
cursor: String(args.cursor ?? ""),
|
|
335
|
+
data: args.data ?? {},
|
|
336
|
+
done: Boolean(args.done)
|
|
337
|
+
};
|
|
338
|
+
return {
|
|
339
|
+
content: [
|
|
340
|
+
{
|
|
341
|
+
type: "text",
|
|
342
|
+
text: "State recorded. You are done for this tick \u2014 no further action needed."
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
const server = createSdkMcpServer2({
|
|
349
|
+
name: "kody-submit",
|
|
350
|
+
version: "0.1.0",
|
|
351
|
+
tools: [submitTool]
|
|
352
|
+
});
|
|
353
|
+
return { server, getSubmitted: () => submitted };
|
|
354
|
+
}
|
|
355
|
+
var init_submitMcp = __esm({
|
|
356
|
+
"src/submitMcp.ts"() {
|
|
357
|
+
"use strict";
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
315
361
|
// src/issue.ts
|
|
316
362
|
import { execFileSync } from "child_process";
|
|
317
363
|
function ghToken() {
|
|
@@ -880,7 +926,7 @@ var init_loadPriorArt = __esm({
|
|
|
880
926
|
// package.json
|
|
881
927
|
var package_default = {
|
|
882
928
|
name: "@kody-ade/kody-engine",
|
|
883
|
-
version: "0.4.
|
|
929
|
+
version: "0.4.140",
|
|
884
930
|
description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
|
|
885
931
|
license: "MIT",
|
|
886
932
|
type: "module",
|
|
@@ -1514,6 +1560,7 @@ async function runAgent(opts) {
|
|
|
1514
1560
|
const turnTimeoutMs = resolveTurnTimeoutMs(opts);
|
|
1515
1561
|
let ndjsonWriteFailed = false;
|
|
1516
1562
|
let ndjsonWriteError;
|
|
1563
|
+
let getSubmitted;
|
|
1517
1564
|
try {
|
|
1518
1565
|
const queryOptions = {
|
|
1519
1566
|
model: opts.model.model,
|
|
@@ -1541,6 +1588,12 @@ async function runAgent(opts) {
|
|
|
1541
1588
|
});
|
|
1542
1589
|
mcpEntries.push(["kody-verify", verifyServer]);
|
|
1543
1590
|
}
|
|
1591
|
+
if (opts.enableSubmitTool) {
|
|
1592
|
+
const { buildSubmitMcpServer: buildSubmitMcpServer2 } = await Promise.resolve().then(() => (init_submitMcp(), submitMcp_exports));
|
|
1593
|
+
const submitHandle = buildSubmitMcpServer2();
|
|
1594
|
+
getSubmitted = submitHandle.getSubmitted;
|
|
1595
|
+
mcpEntries.push(["kody-submit", submitHandle.server]);
|
|
1596
|
+
}
|
|
1544
1597
|
if (mcpEntries.length > 0) {
|
|
1545
1598
|
queryOptions.mcpServers = Object.fromEntries(mcpEntries);
|
|
1546
1599
|
}
|
|
@@ -1588,10 +1641,10 @@ async function runAgent(opts) {
|
|
|
1588
1641
|
let timer;
|
|
1589
1642
|
let next;
|
|
1590
1643
|
if (turnTimeoutMs > 0) {
|
|
1591
|
-
const timeoutPromise = new Promise((
|
|
1644
|
+
const timeoutPromise = new Promise((resolve5) => {
|
|
1592
1645
|
timer = setTimeout(() => {
|
|
1593
1646
|
timedOut = true;
|
|
1594
|
-
|
|
1647
|
+
resolve5({ done: true, value: void 0 });
|
|
1595
1648
|
}, turnTimeoutMs);
|
|
1596
1649
|
});
|
|
1597
1650
|
next = await Promise.race([nextPromise, timeoutPromise]);
|
|
@@ -1706,10 +1759,12 @@ async function runAgent(opts) {
|
|
|
1706
1759
|
`);
|
|
1707
1760
|
}
|
|
1708
1761
|
const finalText = resultTexts.join("\n\n---\n\n");
|
|
1762
|
+
const submittedState = getSubmitted?.();
|
|
1709
1763
|
return {
|
|
1710
1764
|
outcome,
|
|
1711
1765
|
outcomeKind,
|
|
1712
1766
|
finalText,
|
|
1767
|
+
...submittedState ? { submittedState } : {},
|
|
1713
1768
|
error: errorMessage,
|
|
1714
1769
|
ndjsonPath,
|
|
1715
1770
|
durationMs: Date.now() - startedAt,
|
|
@@ -2263,7 +2318,7 @@ async function waitForNextUserMessage(opts) {
|
|
|
2263
2318
|
}
|
|
2264
2319
|
}
|
|
2265
2320
|
function sleep(ms) {
|
|
2266
|
-
return new Promise((
|
|
2321
|
+
return new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
2267
2322
|
}
|
|
2268
2323
|
function currentBranch(cwd) {
|
|
2269
2324
|
try {
|
|
@@ -2812,7 +2867,7 @@ function coerceBare(spec, value) {
|
|
|
2812
2867
|
init_issue();
|
|
2813
2868
|
|
|
2814
2869
|
// src/executor.ts
|
|
2815
|
-
import { execFileSync as execFileSync30, spawn as
|
|
2870
|
+
import { execFileSync as execFileSync30, spawn as spawn9 } from "child_process";
|
|
2816
2871
|
import * as fs40 from "fs";
|
|
2817
2872
|
import * as path37 from "path";
|
|
2818
2873
|
|
|
@@ -3186,6 +3241,7 @@ function parseClaudeCode(p, raw) {
|
|
|
3186
3241
|
systemPromptAppend: typeof r.systemPromptAppend === "string" ? r.systemPromptAppend : null,
|
|
3187
3242
|
cacheable: r.cacheable === true,
|
|
3188
3243
|
enableVerifyTool: r.enableVerifyTool === true,
|
|
3244
|
+
enableSubmitTool: r.enableSubmitTool === true,
|
|
3189
3245
|
verifyAttempts: typeof r.verifyAttempts === "number" && r.verifyAttempts > 0 ? r.verifyAttempts : null,
|
|
3190
3246
|
tools,
|
|
3191
3247
|
hooks: Array.isArray(r.hooks) ? r.hooks : [],
|
|
@@ -4235,6 +4291,7 @@ var advanceFlow = async (ctx, profile) => {
|
|
|
4235
4291
|
|
|
4236
4292
|
// src/scripts/brainServe.ts
|
|
4237
4293
|
import { createServer } from "http";
|
|
4294
|
+
import { spawn as spawn3, spawnSync } from "child_process";
|
|
4238
4295
|
import * as fs18 from "fs";
|
|
4239
4296
|
import * as path17 from "path";
|
|
4240
4297
|
|
|
@@ -4417,17 +4474,17 @@ function authOk(req, expected) {
|
|
|
4417
4474
|
return false;
|
|
4418
4475
|
}
|
|
4419
4476
|
function readJsonBody(req) {
|
|
4420
|
-
return new Promise((
|
|
4477
|
+
return new Promise((resolve5, reject) => {
|
|
4421
4478
|
const chunks = [];
|
|
4422
4479
|
req.on("data", (c) => chunks.push(c));
|
|
4423
4480
|
req.on("end", () => {
|
|
4424
4481
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
4425
4482
|
if (!raw.trim()) {
|
|
4426
|
-
|
|
4483
|
+
resolve5({});
|
|
4427
4484
|
return;
|
|
4428
4485
|
}
|
|
4429
4486
|
try {
|
|
4430
|
-
|
|
4487
|
+
resolve5(JSON.parse(raw));
|
|
4431
4488
|
} catch (err) {
|
|
4432
4489
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
4433
4490
|
}
|
|
@@ -4435,6 +4492,13 @@ function readJsonBody(req) {
|
|
|
4435
4492
|
req.on("error", reject);
|
|
4436
4493
|
});
|
|
4437
4494
|
}
|
|
4495
|
+
function strField(body, key) {
|
|
4496
|
+
if (typeof body === "object" && body !== null && key in body) {
|
|
4497
|
+
const v = body[key];
|
|
4498
|
+
if (typeof v === "string" && v.trim()) return v.trim();
|
|
4499
|
+
}
|
|
4500
|
+
return void 0;
|
|
4501
|
+
}
|
|
4438
4502
|
function sendJson(res, status, body) {
|
|
4439
4503
|
res.writeHead(status, { "content-type": "application/json" });
|
|
4440
4504
|
res.end(JSON.stringify(body));
|
|
@@ -4532,6 +4596,51 @@ function streamToRes(res, dir, chatId, since) {
|
|
|
4532
4596
|
);
|
|
4533
4597
|
res.on("close", unsubscribe);
|
|
4534
4598
|
}
|
|
4599
|
+
var REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
|
|
4600
|
+
var repoClones = /* @__PURE__ */ new Map();
|
|
4601
|
+
async function ensureRepoCwd(opts) {
|
|
4602
|
+
const repo = opts.repo?.trim();
|
|
4603
|
+
if (!repo || !REPO_RE.test(repo)) return opts.baseCwd;
|
|
4604
|
+
const root = path17.resolve(opts.reposRoot);
|
|
4605
|
+
const dir = path17.resolve(root, repo);
|
|
4606
|
+
if (dir !== root && !dir.startsWith(root + path17.sep)) return opts.baseCwd;
|
|
4607
|
+
if (fs18.existsSync(path17.join(dir, ".git"))) return dir;
|
|
4608
|
+
const inflight = repoClones.get(dir);
|
|
4609
|
+
if (inflight) {
|
|
4610
|
+
await inflight;
|
|
4611
|
+
return dir;
|
|
4612
|
+
}
|
|
4613
|
+
const p = opts.cloneRepo(repo, opts.repoToken, dir).finally(() => {
|
|
4614
|
+
if (repoClones.get(dir) === p) repoClones.delete(dir);
|
|
4615
|
+
});
|
|
4616
|
+
repoClones.set(dir, p);
|
|
4617
|
+
await p;
|
|
4618
|
+
return dir;
|
|
4619
|
+
}
|
|
4620
|
+
var defaultCloneRepo = (repo, token, dir) => {
|
|
4621
|
+
fs18.mkdirSync(path17.dirname(dir), { recursive: true });
|
|
4622
|
+
const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
|
|
4623
|
+
return new Promise((resolve5, reject) => {
|
|
4624
|
+
const child = spawn3("git", ["clone", "--depth=1", authUrl, dir], {
|
|
4625
|
+
stdio: "inherit"
|
|
4626
|
+
});
|
|
4627
|
+
child.on("exit", (code) => {
|
|
4628
|
+
if (code !== 0) {
|
|
4629
|
+
reject(new Error(`git clone ${repo} failed (exit ${code})`));
|
|
4630
|
+
return;
|
|
4631
|
+
}
|
|
4632
|
+
try {
|
|
4633
|
+
const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
|
|
4634
|
+
const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
|
|
4635
|
+
spawnSync("git", ["-C", dir, "config", "user.name", name]);
|
|
4636
|
+
spawnSync("git", ["-C", dir, "config", "user.email", email]);
|
|
4637
|
+
} catch {
|
|
4638
|
+
}
|
|
4639
|
+
resolve5();
|
|
4640
|
+
});
|
|
4641
|
+
child.on("error", reject);
|
|
4642
|
+
});
|
|
4643
|
+
};
|
|
4535
4644
|
async function handleChatTurn(req, res, chatId, opts) {
|
|
4536
4645
|
let body;
|
|
4537
4646
|
try {
|
|
@@ -4545,6 +4654,8 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
4545
4654
|
sendJson(res, 400, { error: "message required" });
|
|
4546
4655
|
return;
|
|
4547
4656
|
}
|
|
4657
|
+
const repo = strField(body, "repo");
|
|
4658
|
+
const repoToken = strField(body, "repoToken");
|
|
4548
4659
|
const sessionFile = sessionFilePath(opts.cwd, chatId);
|
|
4549
4660
|
fs18.mkdirSync(path17.dirname(sessionFile), { recursive: true });
|
|
4550
4661
|
appendTurn(sessionFile, {
|
|
@@ -4555,32 +4666,42 @@ async function handleChatTurn(req, res, chatId, opts) {
|
|
|
4555
4666
|
const sinceFloor = getLastSeq(opts.cwd, chatId);
|
|
4556
4667
|
const emitToLog = beginTurn(opts.cwd, chatId);
|
|
4557
4668
|
const sink = new BrokerSink(emitToLog, chatId);
|
|
4558
|
-
void enqueue(
|
|
4559
|
-
|
|
4560
|
-
|
|
4561
|
-
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4669
|
+
void enqueue(chatId, async () => {
|
|
4670
|
+
try {
|
|
4671
|
+
const agentCwd = await ensureRepoCwd({
|
|
4672
|
+
baseCwd: opts.cwd,
|
|
4673
|
+
reposRoot: opts.reposRoot,
|
|
4674
|
+
repo,
|
|
4675
|
+
repoToken,
|
|
4676
|
+
cloneRepo: opts.cloneRepo
|
|
4677
|
+
});
|
|
4678
|
+
await opts.runTurn({
|
|
4679
|
+
sessionId: chatId,
|
|
4680
|
+
sessionFile,
|
|
4681
|
+
cwd: agentCwd,
|
|
4682
|
+
model: opts.model,
|
|
4683
|
+
litellmUrl: opts.litellmUrl,
|
|
4684
|
+
sink
|
|
4685
|
+
});
|
|
4686
|
+
} catch (err) {
|
|
4568
4687
|
const errMsg3 = err instanceof Error ? err.message : String(err);
|
|
4569
4688
|
process.stderr.write(`[brain-serve] chat turn failed: ${errMsg3}
|
|
4570
4689
|
`);
|
|
4571
4690
|
endTurnIfUnterminated(opts.cwd, chatId, errMsg3);
|
|
4572
|
-
}
|
|
4691
|
+
} finally {
|
|
4573
4692
|
endTurnIfUnterminated(
|
|
4574
4693
|
opts.cwd,
|
|
4575
4694
|
chatId,
|
|
4576
4695
|
"Brain turn ended without a reply (the machine may have restarted mid-turn) \u2014 please resend your message"
|
|
4577
4696
|
);
|
|
4578
|
-
}
|
|
4579
|
-
);
|
|
4697
|
+
}
|
|
4698
|
+
});
|
|
4580
4699
|
streamToRes(res, opts.cwd, chatId, sinceFloor);
|
|
4581
4700
|
}
|
|
4582
4701
|
function buildServer(opts) {
|
|
4583
4702
|
const runTurn = opts.runTurn ?? runChatTurn;
|
|
4703
|
+
const cloneRepo = opts.cloneRepo ?? defaultCloneRepo;
|
|
4704
|
+
const reposRoot = opts.reposRoot ?? path17.join(path17.dirname(path17.resolve(opts.cwd)), "repos");
|
|
4584
4705
|
return createServer(async (req, res) => {
|
|
4585
4706
|
if (!req.method || !req.url) {
|
|
4586
4707
|
sendJson(res, 400, { error: "bad request" });
|
|
@@ -4604,6 +4725,8 @@ function buildServer(opts) {
|
|
|
4604
4725
|
}
|
|
4605
4726
|
await handleChatTurn(req, res, chatId, {
|
|
4606
4727
|
cwd: opts.cwd,
|
|
4728
|
+
reposRoot,
|
|
4729
|
+
cloneRepo,
|
|
4607
4730
|
model: opts.model,
|
|
4608
4731
|
litellmUrl: opts.litellmUrl,
|
|
4609
4732
|
runTurn
|
|
@@ -4654,16 +4777,18 @@ var brainServe = async (ctx) => {
|
|
|
4654
4777
|
const server = buildServer({
|
|
4655
4778
|
apiKey,
|
|
4656
4779
|
cwd: ctx.cwd,
|
|
4780
|
+
// Per-repo clones live here; defaults to a `repos` sibling of cwd.
|
|
4781
|
+
reposRoot: process.env.BRAIN_REPOS_ROOT?.trim() || void 0,
|
|
4657
4782
|
model,
|
|
4658
4783
|
litellmUrl
|
|
4659
4784
|
});
|
|
4660
|
-
await new Promise((
|
|
4785
|
+
await new Promise((resolve5) => {
|
|
4661
4786
|
server.listen(port, "0.0.0.0", () => {
|
|
4662
4787
|
process.stdout.write(
|
|
4663
4788
|
`[brain-serve] listening on 0.0.0.0:${port} (cwd=${ctx.cwd})
|
|
4664
4789
|
`
|
|
4665
4790
|
);
|
|
4666
|
-
|
|
4791
|
+
resolve5();
|
|
4667
4792
|
});
|
|
4668
4793
|
});
|
|
4669
4794
|
const shutdown = (signal) => {
|
|
@@ -5458,36 +5583,46 @@ var createQaGoal = async (ctx, _profile, agentResult) => {
|
|
|
5458
5583
|
ctx.data.action = failedAction2("empty report body");
|
|
5459
5584
|
return;
|
|
5460
5585
|
}
|
|
5586
|
+
const { markdown } = splitReport(finalText);
|
|
5587
|
+
const verdict = detectVerdict(markdown);
|
|
5588
|
+
const existingIssue = ctx.args.issue;
|
|
5589
|
+
if (typeof existingIssue === "number" && existingIssue > 0) {
|
|
5590
|
+
try {
|
|
5591
|
+
postIssueComment(existingIssue, finalText, ctx.cwd);
|
|
5592
|
+
} catch (err) {
|
|
5593
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5594
|
+
ctx.output.exitCode = 4;
|
|
5595
|
+
ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
|
|
5596
|
+
ctx.data.action = failedAction2(ctx.output.reason);
|
|
5597
|
+
return;
|
|
5598
|
+
}
|
|
5599
|
+
process.stdout.write(
|
|
5600
|
+
`
|
|
5601
|
+
QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
|
|
5602
|
+
`
|
|
5603
|
+
);
|
|
5604
|
+
ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
|
|
5605
|
+
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
5606
|
+
return;
|
|
5607
|
+
}
|
|
5608
|
+
await promoteReportToGoal(
|
|
5609
|
+
ctx,
|
|
5610
|
+
finalText,
|
|
5611
|
+
ctx.args.scope,
|
|
5612
|
+
ctx.args.goal
|
|
5613
|
+
);
|
|
5614
|
+
};
|
|
5615
|
+
async function promoteReportToGoal(ctx, finalText, scopeArg, explicitGoalArg) {
|
|
5461
5616
|
const { markdown, data, jsonError } = splitReport(finalText);
|
|
5462
5617
|
const verdict = detectVerdict(markdown);
|
|
5463
5618
|
const findings = data?.findings ?? [];
|
|
5464
|
-
const existingIssue = ctx.args.issue;
|
|
5465
5619
|
if (findings.length === 0 || jsonError) {
|
|
5466
5620
|
if (jsonError) {
|
|
5467
|
-
process.stderr.write(`[
|
|
5621
|
+
process.stderr.write(`[promoteReportToGoal] JSON parse: ${jsonError} \u2014 falling back to single-issue mode
|
|
5468
5622
|
`);
|
|
5469
5623
|
}
|
|
5470
|
-
if (typeof existingIssue === "number" && existingIssue > 0) {
|
|
5471
|
-
try {
|
|
5472
|
-
postIssueComment(existingIssue, finalText, ctx.cwd);
|
|
5473
|
-
} catch (err) {
|
|
5474
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
5475
|
-
ctx.output.exitCode = 4;
|
|
5476
|
-
ctx.output.reason = `failed to comment on issue #${existingIssue}: ${msg}`;
|
|
5477
|
-
ctx.data.action = failedAction2(ctx.output.reason);
|
|
5478
|
-
return;
|
|
5479
|
-
}
|
|
5480
|
-
process.stdout.write(
|
|
5481
|
-
`
|
|
5482
|
-
QA_REPORT_POSTED=https://github.com/${ctx.config.github.owner}/${ctx.config.github.repo}/issues/${existingIssue} (verdict: ${verdict})
|
|
5483
|
-
`
|
|
5484
|
-
);
|
|
5485
|
-
ctx.data.action = qaAction(verdict, { issueNumber: existingIssue, mode: "comment" });
|
|
5486
|
-
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
5487
|
-
return;
|
|
5488
|
-
}
|
|
5489
5624
|
ensureLabel(FINDING_LABEL, "ededed", "kody: QA finding", ctx.cwd);
|
|
5490
|
-
const scope2 =
|
|
5625
|
+
const scope2 = scopeArg;
|
|
5491
5626
|
const title = `QA [${verdict}]: ${scope2?.trim() || "smoke"} \u2014 ${todayIso()}`.slice(0, 240);
|
|
5492
5627
|
let url = "";
|
|
5493
5628
|
try {
|
|
@@ -5515,8 +5650,8 @@ QA_REPORT_POSTED=${url} (verdict: ${verdict})
|
|
|
5515
5650
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
5516
5651
|
return;
|
|
5517
5652
|
}
|
|
5518
|
-
const explicitGoal =
|
|
5519
|
-
const scope =
|
|
5653
|
+
const explicitGoal = explicitGoalArg?.trim();
|
|
5654
|
+
const scope = scopeArg;
|
|
5520
5655
|
let goalId;
|
|
5521
5656
|
let manifestIssueNumber = null;
|
|
5522
5657
|
let manifestCreated = false;
|
|
@@ -5614,7 +5749,7 @@ QA_GOAL_TARGETED=(no manifest issue) (id: ${goalId}, verdict: ${verdict})
|
|
|
5614
5749
|
mode: explicitGoal ? "goal-attach" : manifestCreated ? "goal-create" : "goal-append"
|
|
5615
5750
|
});
|
|
5616
5751
|
ctx.output.exitCode = verdict === "FAIL" ? 1 : 0;
|
|
5617
|
-
}
|
|
5752
|
+
}
|
|
5618
5753
|
|
|
5619
5754
|
// src/goal/operations.ts
|
|
5620
5755
|
init_issue();
|
|
@@ -9313,8 +9448,30 @@ var parseJobStateFromAgentResult = async (ctx, _profile, agentResult, args) => {
|
|
|
9313
9448
|
}
|
|
9314
9449
|
const loaded = ctx.data.jobState;
|
|
9315
9450
|
const prevRev = loaded?.state.rev ?? 0;
|
|
9451
|
+
const submitted = agentResult.submittedState;
|
|
9452
|
+
if (submitted && typeof submitted.cursor === "string" && submitted.cursor.length > 0) {
|
|
9453
|
+
ctx.data.nextJobState = {
|
|
9454
|
+
version: 1,
|
|
9455
|
+
rev: prevRev + 1,
|
|
9456
|
+
cursor: submitted.cursor,
|
|
9457
|
+
data: submitted.data ?? {},
|
|
9458
|
+
done: Boolean(submitted.done)
|
|
9459
|
+
};
|
|
9460
|
+
return;
|
|
9461
|
+
}
|
|
9316
9462
|
const result = extractNextStateFromText(agentResult.finalText, fenceLabel, prevRev);
|
|
9317
9463
|
if (result.error) {
|
|
9464
|
+
const cleanFinishNoBlock = result.error.startsWith("missing `") && agentResult.outcome === "completed" && loaded != null;
|
|
9465
|
+
if (cleanFinishNoBlock) {
|
|
9466
|
+
ctx.data.nextJobState = {
|
|
9467
|
+
version: 1,
|
|
9468
|
+
rev: prevRev + 1,
|
|
9469
|
+
cursor: loaded.state.cursor,
|
|
9470
|
+
data: loaded.state.data,
|
|
9471
|
+
done: loaded.state.done
|
|
9472
|
+
};
|
|
9473
|
+
return;
|
|
9474
|
+
}
|
|
9318
9475
|
ctx.data.nextStateParseError = result.error.startsWith("missing `") ? `agent did not emit a \`${fenceLabel}\` fenced block` : result.error;
|
|
9319
9476
|
return;
|
|
9320
9477
|
}
|
|
@@ -9435,7 +9592,7 @@ var persistFlowState = async (ctx) => {
|
|
|
9435
9592
|
};
|
|
9436
9593
|
|
|
9437
9594
|
// src/scripts/poolServe.ts
|
|
9438
|
-
import { spawn as
|
|
9595
|
+
import { spawn as spawn4 } from "child_process";
|
|
9439
9596
|
import { createServer as createServer2 } from "http";
|
|
9440
9597
|
|
|
9441
9598
|
// src/pool/fly.ts
|
|
@@ -10005,14 +10162,14 @@ function sendJson2(res, status, body) {
|
|
|
10005
10162
|
res.end(JSON.stringify(body));
|
|
10006
10163
|
}
|
|
10007
10164
|
function readJsonBody2(req) {
|
|
10008
|
-
return new Promise((
|
|
10165
|
+
return new Promise((resolve5, reject) => {
|
|
10009
10166
|
const chunks = [];
|
|
10010
10167
|
req.on("data", (c) => chunks.push(c));
|
|
10011
10168
|
req.on("end", () => {
|
|
10012
10169
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
10013
|
-
if (!raw.trim()) return
|
|
10170
|
+
if (!raw.trim()) return resolve5({});
|
|
10014
10171
|
try {
|
|
10015
|
-
|
|
10172
|
+
resolve5(JSON.parse(raw));
|
|
10016
10173
|
} catch (err) {
|
|
10017
10174
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
10018
10175
|
}
|
|
@@ -10054,7 +10211,7 @@ function superviseLitellm() {
|
|
|
10054
10211
|
let restarts = 0;
|
|
10055
10212
|
const start = () => {
|
|
10056
10213
|
log(`starting litellm child (port ${port}, host ${host})`);
|
|
10057
|
-
const child =
|
|
10214
|
+
const child = spawn4("litellm", ["--config", config, "--port", port, "--host", host], {
|
|
10058
10215
|
stdio: "inherit"
|
|
10059
10216
|
});
|
|
10060
10217
|
child.on("exit", (code) => {
|
|
@@ -10159,10 +10316,10 @@ var poolServe = async (ctx) => {
|
|
|
10159
10316
|
}
|
|
10160
10317
|
});
|
|
10161
10318
|
const apiHost = process.env.POOL_API_HOST ?? "::";
|
|
10162
|
-
await new Promise((
|
|
10319
|
+
await new Promise((resolve5) => {
|
|
10163
10320
|
server.listen(apiPort, apiHost, () => {
|
|
10164
10321
|
log(`listening on ${apiHost}:${apiPort} (min=${min}, app=${app}, region=${region})`);
|
|
10165
|
-
|
|
10322
|
+
resolve5();
|
|
10166
10323
|
});
|
|
10167
10324
|
});
|
|
10168
10325
|
const shutdown = (signal) => {
|
|
@@ -10675,6 +10832,46 @@ function pushEmptyCommit(branch, cwd) {
|
|
|
10675
10832
|
}
|
|
10676
10833
|
}
|
|
10677
10834
|
|
|
10835
|
+
// src/scripts/promoteQaGoal.ts
|
|
10836
|
+
init_issue();
|
|
10837
|
+
var REPORT_JSON_OPEN2 = "<!-- KODY_QA_REPORT_JSON";
|
|
10838
|
+
var promoteQaGoal = async (ctx) => {
|
|
10839
|
+
ctx.skipAgent = true;
|
|
10840
|
+
const issueNum = ctx.args.issue;
|
|
10841
|
+
if (typeof issueNum !== "number" || issueNum <= 0) {
|
|
10842
|
+
ctx.output.exitCode = 2;
|
|
10843
|
+
ctx.output.reason = "qa-goal requires --issue <n>";
|
|
10844
|
+
process.stderr.write("[qa-goal] missing --issue\n");
|
|
10845
|
+
return;
|
|
10846
|
+
}
|
|
10847
|
+
let report;
|
|
10848
|
+
try {
|
|
10849
|
+
const issue = getIssue(issueNum, ctx.cwd);
|
|
10850
|
+
const reportComment = [...issue.comments].reverse().find((c) => c.body.includes(REPORT_JSON_OPEN2));
|
|
10851
|
+
if (!reportComment) {
|
|
10852
|
+
ctx.output.exitCode = 3;
|
|
10853
|
+
ctx.output.reason = `no QA report (${REPORT_JSON_OPEN2} \u2026) found on issue #${issueNum}`;
|
|
10854
|
+
process.stderr.write(`[qa-goal] ${ctx.output.reason}
|
|
10855
|
+
`);
|
|
10856
|
+
return;
|
|
10857
|
+
}
|
|
10858
|
+
report = reportComment.body;
|
|
10859
|
+
} catch (err) {
|
|
10860
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10861
|
+
ctx.output.exitCode = 3;
|
|
10862
|
+
ctx.output.reason = `failed to read issue #${issueNum}: ${msg}`;
|
|
10863
|
+
process.stderr.write(`[qa-goal] ${ctx.output.reason}
|
|
10864
|
+
`);
|
|
10865
|
+
return;
|
|
10866
|
+
}
|
|
10867
|
+
await promoteReportToGoal(
|
|
10868
|
+
ctx,
|
|
10869
|
+
report,
|
|
10870
|
+
ctx.args.scope,
|
|
10871
|
+
ctx.args.goal
|
|
10872
|
+
);
|
|
10873
|
+
};
|
|
10874
|
+
|
|
10678
10875
|
// src/deployments.ts
|
|
10679
10876
|
init_issue();
|
|
10680
10877
|
function findPreviewDeploymentUrl(prNumber, cwd) {
|
|
@@ -11016,7 +11213,7 @@ function resolveBaseOverride(value) {
|
|
|
11016
11213
|
}
|
|
11017
11214
|
|
|
11018
11215
|
// src/scripts/runnerServe.ts
|
|
11019
|
-
import { spawn as
|
|
11216
|
+
import { spawn as spawn5 } from "child_process";
|
|
11020
11217
|
import { createServer as createServer3 } from "http";
|
|
11021
11218
|
import * as fs37 from "fs";
|
|
11022
11219
|
var DEFAULT_PORT2 = 8080;
|
|
@@ -11040,17 +11237,17 @@ function authOk2(req, expected) {
|
|
|
11040
11237
|
return false;
|
|
11041
11238
|
}
|
|
11042
11239
|
function readJsonBody3(req) {
|
|
11043
|
-
return new Promise((
|
|
11240
|
+
return new Promise((resolve5, reject) => {
|
|
11044
11241
|
const chunks = [];
|
|
11045
11242
|
req.on("data", (c) => chunks.push(c));
|
|
11046
11243
|
req.on("end", () => {
|
|
11047
11244
|
const raw = Buffer.concat(chunks).toString("utf-8");
|
|
11048
11245
|
if (!raw.trim()) {
|
|
11049
|
-
|
|
11246
|
+
resolve5({});
|
|
11050
11247
|
return;
|
|
11051
11248
|
}
|
|
11052
11249
|
try {
|
|
11053
|
-
|
|
11250
|
+
resolve5(JSON.parse(raw));
|
|
11054
11251
|
} catch (err) {
|
|
11055
11252
|
reject(err instanceof Error ? err : new Error(String(err)));
|
|
11056
11253
|
}
|
|
@@ -11125,13 +11322,13 @@ async function defaultRunJob(job) {
|
|
|
11125
11322
|
...interactive && job.idleExitMs ? { KODY_IDLE_EXIT_MS: String(job.idleExitMs) } : {},
|
|
11126
11323
|
...interactive && job.hardCapMs ? { KODY_HARD_CAP_MS: String(job.hardCapMs) } : {}
|
|
11127
11324
|
};
|
|
11128
|
-
const run = (cmd, args, cwd) => new Promise((
|
|
11129
|
-
const child =
|
|
11130
|
-
child.on("exit", (code) =>
|
|
11325
|
+
const run = (cmd, args, cwd) => new Promise((resolve5) => {
|
|
11326
|
+
const child = spawn5(cmd, args, { stdio: "inherit", env: childEnv, cwd });
|
|
11327
|
+
child.on("exit", (code) => resolve5(code ?? 0));
|
|
11131
11328
|
child.on("error", (err) => {
|
|
11132
11329
|
process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
|
|
11133
11330
|
`);
|
|
11134
|
-
|
|
11331
|
+
resolve5(1);
|
|
11135
11332
|
});
|
|
11136
11333
|
});
|
|
11137
11334
|
process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
|
|
@@ -11218,11 +11415,11 @@ var runnerServe = async (ctx) => {
|
|
|
11218
11415
|
const port = Number(process.env.PORT ?? DEFAULT_PORT2);
|
|
11219
11416
|
const server = buildServer2({ apiKey });
|
|
11220
11417
|
const host = process.env.RUNNER_HOST ?? "::";
|
|
11221
|
-
await new Promise((
|
|
11418
|
+
await new Promise((resolve5) => {
|
|
11222
11419
|
server.listen(port, host, () => {
|
|
11223
11420
|
process.stdout.write(`[runner-serve] listening on ${host}:${port} (idle, awaiting job)
|
|
11224
11421
|
`);
|
|
11225
|
-
|
|
11422
|
+
resolve5();
|
|
11226
11423
|
});
|
|
11227
11424
|
});
|
|
11228
11425
|
const shutdown = (signal) => {
|
|
@@ -11237,7 +11434,7 @@ var runnerServe = async (ctx) => {
|
|
|
11237
11434
|
};
|
|
11238
11435
|
|
|
11239
11436
|
// src/scripts/runTickScript.ts
|
|
11240
|
-
import { spawnSync } from "child_process";
|
|
11437
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
11241
11438
|
import * as fs38 from "fs";
|
|
11242
11439
|
import * as path36 from "path";
|
|
11243
11440
|
var runTickScript = async (ctx, _profile, args) => {
|
|
@@ -11283,7 +11480,7 @@ var runTickScript = async (ctx, _profile, args) => {
|
|
|
11283
11480
|
ctx.data.jobSlug = slug;
|
|
11284
11481
|
ctx.data.jobState = loaded;
|
|
11285
11482
|
const childEnv = buildChildEnv(process.env, Boolean(ctx.args.force));
|
|
11286
|
-
const result =
|
|
11483
|
+
const result = spawnSync2("bash", [scriptPath], {
|
|
11287
11484
|
cwd: ctx.cwd,
|
|
11288
11485
|
env: childEnv,
|
|
11289
11486
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -11416,7 +11613,7 @@ function synthesizeAction(ctx) {
|
|
|
11416
11613
|
}
|
|
11417
11614
|
|
|
11418
11615
|
// src/scripts/serveFlow.ts
|
|
11419
|
-
import { spawn as
|
|
11616
|
+
import { spawn as spawn6 } from "child_process";
|
|
11420
11617
|
function parseTarget(positional) {
|
|
11421
11618
|
if (!Array.isArray(positional) || positional.length === 0) return "none";
|
|
11422
11619
|
const first = String(positional[0]).toLowerCase();
|
|
@@ -11465,15 +11662,15 @@ var serveFlow = async (ctx) => {
|
|
|
11465
11662
|
if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
|
|
11466
11663
|
`);
|
|
11467
11664
|
const args = ["--dangerously-skip-permissions", "--model", model.model];
|
|
11468
|
-
const child =
|
|
11469
|
-
const exitCode = await new Promise((
|
|
11470
|
-
child.on("exit", (code) =>
|
|
11665
|
+
const child = spawn6("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
|
|
11666
|
+
const exitCode = await new Promise((resolve5) => {
|
|
11667
|
+
child.on("exit", (code) => resolve5(code ?? 0));
|
|
11471
11668
|
child.on("error", (err) => {
|
|
11472
11669
|
process.stderr.write(`[kody serve] failed to launch Claude Code: ${err.message}
|
|
11473
11670
|
`);
|
|
11474
11671
|
process.stderr.write(` Install: https://docs.anthropic.com/claude/docs/claude-code
|
|
11475
11672
|
`);
|
|
11476
|
-
|
|
11673
|
+
resolve5(1);
|
|
11477
11674
|
});
|
|
11478
11675
|
});
|
|
11479
11676
|
killProxy();
|
|
@@ -11486,7 +11683,7 @@ var serveFlow = async (ctx) => {
|
|
|
11486
11683
|
if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
|
|
11487
11684
|
`);
|
|
11488
11685
|
try {
|
|
11489
|
-
const code =
|
|
11686
|
+
const code = spawn6("code", [ctx.cwd], { stdio: "inherit", env: editorEnv, detached: true });
|
|
11490
11687
|
code.on("error", (err) => {
|
|
11491
11688
|
process.stderr.write(`[kody serve] failed to launch VS Code: ${err.message}
|
|
11492
11689
|
`);
|
|
@@ -11741,7 +11938,7 @@ var verify = async (ctx) => {
|
|
|
11741
11938
|
};
|
|
11742
11939
|
|
|
11743
11940
|
// src/scripts/verifyReproFails.ts
|
|
11744
|
-
import { spawn as
|
|
11941
|
+
import { spawn as spawn7 } from "child_process";
|
|
11745
11942
|
var TEST_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
11746
11943
|
var TAIL_CHARS2 = 8e3;
|
|
11747
11944
|
var ANSI_RE2 = /\x1B\[[0-?]*[ -/]*[@-~]/g;
|
|
@@ -11809,8 +12006,8 @@ function stripAnsi2(s) {
|
|
|
11809
12006
|
return s.replace(ANSI_RE2, "");
|
|
11810
12007
|
}
|
|
11811
12008
|
function runCommand2(command, cwd) {
|
|
11812
|
-
return new Promise((
|
|
11813
|
-
const child =
|
|
12009
|
+
return new Promise((resolve5) => {
|
|
12010
|
+
const child = spawn7(command, {
|
|
11814
12011
|
cwd,
|
|
11815
12012
|
shell: true,
|
|
11816
12013
|
env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
|
|
@@ -11836,11 +12033,11 @@ function runCommand2(command, cwd) {
|
|
|
11836
12033
|
}, TEST_TIMEOUT_MS);
|
|
11837
12034
|
child.on("exit", (code) => {
|
|
11838
12035
|
clearTimeout(timer);
|
|
11839
|
-
|
|
12036
|
+
resolve5({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
|
|
11840
12037
|
});
|
|
11841
12038
|
child.on("error", (err) => {
|
|
11842
12039
|
clearTimeout(timer);
|
|
11843
|
-
|
|
12040
|
+
resolve5({ exitCode: -1, output: err.message });
|
|
11844
12041
|
});
|
|
11845
12042
|
});
|
|
11846
12043
|
}
|
|
@@ -12139,7 +12336,7 @@ var appendCompanyActivity = async (ctx, _profile, agentResult) => {
|
|
|
12139
12336
|
};
|
|
12140
12337
|
|
|
12141
12338
|
// src/scripts/warmupMcp.ts
|
|
12142
|
-
import { spawn as
|
|
12339
|
+
import { spawn as spawn8 } from "child_process";
|
|
12143
12340
|
var PER_SERVER_TIMEOUT_MS = 6e4;
|
|
12144
12341
|
var PER_REQUEST_TIMEOUT_MS = 2e4;
|
|
12145
12342
|
var warmupMcp = async (_ctx, profile) => {
|
|
@@ -12161,7 +12358,7 @@ var warmupMcp = async (_ctx, profile) => {
|
|
|
12161
12358
|
}
|
|
12162
12359
|
};
|
|
12163
12360
|
async function warmupOne(command, args, env) {
|
|
12164
|
-
const child =
|
|
12361
|
+
const child = spawn8(command, args, {
|
|
12165
12362
|
stdio: ["pipe", "pipe", "pipe"],
|
|
12166
12363
|
env: env ? { ...process.env, ...env } : process.env
|
|
12167
12364
|
});
|
|
@@ -12262,20 +12459,20 @@ function lineStream(stream) {
|
|
|
12262
12459
|
tryDeliver();
|
|
12263
12460
|
});
|
|
12264
12461
|
return {
|
|
12265
|
-
next: (timeoutMs) => new Promise((
|
|
12462
|
+
next: (timeoutMs) => new Promise((resolve5) => {
|
|
12266
12463
|
if (queue.length > 0) {
|
|
12267
|
-
|
|
12464
|
+
resolve5(queue.shift());
|
|
12268
12465
|
return;
|
|
12269
12466
|
}
|
|
12270
12467
|
if (ended) {
|
|
12271
|
-
|
|
12468
|
+
resolve5(null);
|
|
12272
12469
|
return;
|
|
12273
12470
|
}
|
|
12274
|
-
waiter =
|
|
12471
|
+
waiter = resolve5;
|
|
12275
12472
|
const t = setTimeout(() => {
|
|
12276
|
-
if (waiter ===
|
|
12473
|
+
if (waiter === resolve5) {
|
|
12277
12474
|
waiter = null;
|
|
12278
|
-
|
|
12475
|
+
resolve5(null);
|
|
12279
12476
|
}
|
|
12280
12477
|
}, Math.max(0, timeoutMs));
|
|
12281
12478
|
t.unref?.();
|
|
@@ -12428,6 +12625,7 @@ var preflightScripts = {
|
|
|
12428
12625
|
discoverQaContext,
|
|
12429
12626
|
resolvePreviewUrl,
|
|
12430
12627
|
resolveQaUrl,
|
|
12628
|
+
promoteQaGoal,
|
|
12431
12629
|
composePrompt,
|
|
12432
12630
|
setCommentTarget,
|
|
12433
12631
|
setLifecycleLabel,
|
|
@@ -12512,21 +12710,21 @@ function firstRequiredFailure(results, tools) {
|
|
|
12512
12710
|
}
|
|
12513
12711
|
return null;
|
|
12514
12712
|
}
|
|
12515
|
-
function verifyOne(
|
|
12516
|
-
const result = { name:
|
|
12517
|
-
let present = runShell(
|
|
12518
|
-
if (!present &&
|
|
12519
|
-
runShell(
|
|
12520
|
-
present = runShell(
|
|
12713
|
+
function verifyOne(tool3, cwd) {
|
|
12714
|
+
const result = { name: tool3.name, present: false, verified: false };
|
|
12715
|
+
let present = runShell(tool3.install.checkCommand, cwd);
|
|
12716
|
+
if (!present && tool3.install.installCommand) {
|
|
12717
|
+
runShell(tool3.install.installCommand, cwd, 12e4);
|
|
12718
|
+
present = runShell(tool3.install.checkCommand, cwd);
|
|
12521
12719
|
}
|
|
12522
12720
|
result.present = present;
|
|
12523
12721
|
if (!present) {
|
|
12524
|
-
result.error = `tool "${
|
|
12722
|
+
result.error = `tool "${tool3.name}" not on PATH (check: ${tool3.install.checkCommand})`;
|
|
12525
12723
|
return result;
|
|
12526
12724
|
}
|
|
12527
|
-
const verified = runShell(
|
|
12725
|
+
const verified = runShell(tool3.verify, cwd);
|
|
12528
12726
|
result.verified = verified;
|
|
12529
|
-
if (!verified) result.error = `tool "${
|
|
12727
|
+
if (!verified) result.error = `tool "${tool3.name}" failed verify: ${tool3.verify}`;
|
|
12530
12728
|
return result;
|
|
12531
12729
|
}
|
|
12532
12730
|
function runShell(cmd, cwd, timeoutMs = 3e4) {
|
|
@@ -12666,6 +12864,7 @@ async function runExecutable(profileName, input) {
|
|
|
12666
12864
|
systemPromptAppend: [DISCIPLINE, profile.claudeCode.systemPromptAppend, taskArtifacts?.promptAddendum].filter((s) => typeof s === "string" && s.length > 0).join("\n\n") || void 0,
|
|
12667
12865
|
cacheable: profile.claudeCode.cacheable,
|
|
12668
12866
|
enableVerifyTool: profile.claudeCode.enableVerifyTool,
|
|
12867
|
+
enableSubmitTool: profile.claudeCode.enableSubmitTool,
|
|
12669
12868
|
verifyToolMaxAttempts: profile.claudeCode.verifyAttempts ?? null,
|
|
12670
12869
|
verifyConfig: profile.claudeCode.enableVerifyTool ? config : void 0,
|
|
12671
12870
|
executableName: profileName,
|
|
@@ -12988,7 +13187,7 @@ async function runShellEntry(entry, ctx, profile) {
|
|
|
12988
13187
|
env[`KODY_CFG_${k}`] = v;
|
|
12989
13188
|
}
|
|
12990
13189
|
const timeoutMs = resolveShellTimeoutMs(entry);
|
|
12991
|
-
const child =
|
|
13190
|
+
const child = spawn9("bash", [shellPath, ...positional], {
|
|
12992
13191
|
cwd: ctx.cwd,
|
|
12993
13192
|
env,
|
|
12994
13193
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -13010,14 +13209,14 @@ async function runShellEntry(entry, ctx, profile) {
|
|
|
13010
13209
|
let killTimer;
|
|
13011
13210
|
let escalateTimer;
|
|
13012
13211
|
const result = await new Promise(
|
|
13013
|
-
(
|
|
13212
|
+
(resolve5) => {
|
|
13014
13213
|
let settled = false;
|
|
13015
13214
|
const settle = (code, signal, spawnErr) => {
|
|
13016
13215
|
if (settled) return;
|
|
13017
13216
|
settled = true;
|
|
13018
13217
|
if (killTimer) clearTimeout(killTimer);
|
|
13019
13218
|
if (escalateTimer) clearTimeout(escalateTimer);
|
|
13020
|
-
|
|
13219
|
+
resolve5({ code, signal, spawnErr });
|
|
13021
13220
|
};
|
|
13022
13221
|
child.on("error", (err) => settle(null, null, err));
|
|
13023
13222
|
child.on("close", (code, signal) => settle(code, signal));
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"claudeCode": {
|
|
22
22
|
"model": "inherit",
|
|
23
23
|
"permissionMode": "default",
|
|
24
|
-
"maxTurns":
|
|
24
|
+
"maxTurns": 100,
|
|
25
25
|
"maxThinkingTokens": null,
|
|
26
26
|
"systemPromptAppend": null,
|
|
27
|
-
"
|
|
27
|
+
"enableSubmitTool": true,
|
|
28
|
+
"tools": ["Bash", "Read", "mcp__kody-submit"],
|
|
28
29
|
"hooks": [],
|
|
29
30
|
"skills": [],
|
|
30
31
|
"commands": [],
|
|
@@ -32,23 +32,25 @@ This is the state you wrote at the end of the previous tick (or `null` if this i
|
|
|
32
32
|
2. **Re-read the job body.** It may have changed since the last tick.
|
|
33
33
|
3. **Execute exactly the work the body's `## Job` section describes**, subject to its `## Allowed Commands` and `## Restrictions`. Use the `## State` section to interpret and update `data`.
|
|
34
34
|
4. **Optionally post a short narration** wherever the job tells you to (typically a PR comment alongside the action). Keep it terse.
|
|
35
|
-
5. **
|
|
35
|
+
5. **Submit the new state** by calling the `submit_state` tool (see contract below). Do not include `version` or `rev` — the postflight script manages those.
|
|
36
36
|
|
|
37
37
|
## Output contract (MANDATORY, exactly once, at the end)
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Call the **`submit_state`** tool exactly once, as the final step, with your next state:
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
"done": <true|false>
|
|
47
|
-
}
|
|
48
|
-
```
|
|
49
|
-
````
|
|
41
|
+
- `cursor` — your next cursor (string, e.g. `"idle"`).
|
|
42
|
+
- `data` — your next `data` object. Carry forward prior `data` and mutate only what you acted on this tick.
|
|
43
|
+
- `done` — `true` only if the duty is permanently finished; evergreen duties stay `false`.
|
|
44
|
+
|
|
45
|
+
This is the ONLY way your decision is saved. If you don't call it, the tick fails and the state is NOT updated — on the next wake you'll see the same prior state and can retry.
|
|
50
46
|
|
|
51
|
-
|
|
47
|
+
> Backstop (legacy): if the `submit_state` tool is unavailable, end your reply with the same JSON in a single fenced block tagged `kody-job-next-state` instead:
|
|
48
|
+
>
|
|
49
|
+
> ````
|
|
50
|
+
> ```kody-job-next-state
|
|
51
|
+
> { "cursor": "<next>", "data": { ... }, "done": <true|false> }
|
|
52
|
+
> ```
|
|
53
|
+
> ````
|
|
52
54
|
|
|
53
55
|
## Rules
|
|
54
56
|
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "qa-goal",
|
|
3
|
+
"role": "primitive",
|
|
4
|
+
"kind": "oneshot",
|
|
5
|
+
"describe": "Operator-gated half of QA: promotes a QA report (already posted on an issue by qa-engineer) into a goal — manifest entry + one fix-ticket per finding + a committed .kody/goals/<id>/state.json. Deterministic, no agent. The qa / qa-sweep duties surface a `@kody qa-goal --issue <n>` inbox rec; this runs when the operator approves it, so QA never auto-creates goals on its own.",
|
|
6
|
+
"inputs": [
|
|
7
|
+
{
|
|
8
|
+
"name": "issue",
|
|
9
|
+
"flag": "--issue",
|
|
10
|
+
"type": "int",
|
|
11
|
+
"required": true,
|
|
12
|
+
"describe": "Issue number carrying qa-engineer's QA report (the comment with the <!-- KODY_QA_REPORT_JSON --> block)."
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "scope",
|
|
16
|
+
"flag": "--scope",
|
|
17
|
+
"type": "string",
|
|
18
|
+
"required": false,
|
|
19
|
+
"describe": "Optional scope label for the goal name (e.g. the changelog entry title). Defaults to 'smoke'."
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"name": "goal",
|
|
23
|
+
"flag": "--goal",
|
|
24
|
+
"type": "string",
|
|
25
|
+
"required": false,
|
|
26
|
+
"describe": "Optional existing goal id to attach findings to instead of creating a new one."
|
|
27
|
+
}
|
|
28
|
+
],
|
|
29
|
+
"claudeCode": {
|
|
30
|
+
"model": "inherit",
|
|
31
|
+
"permissionMode": "default",
|
|
32
|
+
"maxTurns": null,
|
|
33
|
+
"maxThinkingTokens": null,
|
|
34
|
+
"systemPromptAppend": null,
|
|
35
|
+
"tools": [],
|
|
36
|
+
"hooks": [],
|
|
37
|
+
"skills": [],
|
|
38
|
+
"commands": [],
|
|
39
|
+
"subagents": [],
|
|
40
|
+
"plugins": [],
|
|
41
|
+
"mcpServers": []
|
|
42
|
+
},
|
|
43
|
+
"cliTools": [
|
|
44
|
+
{
|
|
45
|
+
"name": "gh",
|
|
46
|
+
"install": {
|
|
47
|
+
"required": true,
|
|
48
|
+
"checkCommand": "command -v gh"
|
|
49
|
+
},
|
|
50
|
+
"verify": "gh auth status",
|
|
51
|
+
"usage": "Reads the QA report (`gh issue view --json comments`), appends to the goals manifest, and opens fix-ticket issues.",
|
|
52
|
+
"allowedUses": ["issue", "api"]
|
|
53
|
+
}
|
|
54
|
+
],
|
|
55
|
+
"inputArtifacts": [],
|
|
56
|
+
"outputArtifacts": [],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"preflight": [
|
|
59
|
+
{
|
|
60
|
+
"script": "promoteQaGoal"
|
|
61
|
+
}
|
|
62
|
+
],
|
|
63
|
+
"postflight": []
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -228,6 +228,13 @@ export interface ClaudeCodeSpec {
|
|
|
228
228
|
* Default false.
|
|
229
229
|
*/
|
|
230
230
|
enableVerifyTool?: boolean
|
|
231
|
+
/**
|
|
232
|
+
* Opt-in: expose an in-process `submit_state` tool the agent calls to
|
|
233
|
+
* persist its next state, instead of relying on a trailing fenced
|
|
234
|
+
* `kody-job-next-state` block it must remember to emit. Used by job-tick.
|
|
235
|
+
* The fenced block stays supported as a fallback. Default false.
|
|
236
|
+
*/
|
|
237
|
+
enableSubmitTool?: boolean
|
|
231
238
|
/**
|
|
232
239
|
* Hard cap on verify-tool invocations per agent session when
|
|
233
240
|
* `enableVerifyTool` is true. Default 4 (≈3 fix iterations after the
|
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.140",
|
|
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",
|