@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((resolve4) => {
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
- resolve4({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
125
+ resolve5({ exitCode: code ?? -1, durationMs: Date.now() - start, tail });
126
126
  });
127
127
  child.on("error", (err) => {
128
128
  clearTimeout(timer);
129
- resolve4({ exitCode: -1, durationMs: Date.now() - start, tail: err.message });
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.138",
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((resolve4) => {
1644
+ const timeoutPromise = new Promise((resolve5) => {
1592
1645
  timer = setTimeout(() => {
1593
1646
  timedOut = true;
1594
- resolve4({ done: true, value: void 0 });
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((resolve4) => setTimeout(resolve4, ms));
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 spawn8 } from "child_process";
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((resolve4, reject) => {
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
- resolve4({});
4483
+ resolve5({});
4427
4484
  return;
4428
4485
  }
4429
4486
  try {
4430
- resolve4(JSON.parse(raw));
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
- chatId,
4560
- () => opts.runTurn({
4561
- sessionId: chatId,
4562
- sessionFile,
4563
- cwd: opts.cwd,
4564
- model: opts.model,
4565
- litellmUrl: opts.litellmUrl,
4566
- sink
4567
- }).catch((err) => {
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
- }).finally(() => {
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((resolve4) => {
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
- resolve4();
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(`[createQaGoal] JSON parse: ${jsonError} \u2014 falling back to single-issue mode
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 = ctx.args.scope;
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 = ctx.args.goal?.trim();
5519
- const scope = ctx.args.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 spawn3 } from "child_process";
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((resolve4, reject) => {
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 resolve4({});
10170
+ if (!raw.trim()) return resolve5({});
10014
10171
  try {
10015
- resolve4(JSON.parse(raw));
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 = spawn3("litellm", ["--config", config, "--port", port, "--host", host], {
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((resolve4) => {
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
- resolve4();
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 spawn4 } from "child_process";
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((resolve4, reject) => {
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
- resolve4({});
11246
+ resolve5({});
11050
11247
  return;
11051
11248
  }
11052
11249
  try {
11053
- resolve4(JSON.parse(raw));
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((resolve4) => {
11129
- const child = spawn4(cmd, args, { stdio: "inherit", env: childEnv, cwd });
11130
- child.on("exit", (code) => resolve4(code ?? 0));
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
- resolve4(1);
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((resolve4) => {
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
- resolve4();
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 = spawnSync("bash", [scriptPath], {
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 spawn5 } from "child_process";
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 = spawn5("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
11469
- const exitCode = await new Promise((resolve4) => {
11470
- child.on("exit", (code) => resolve4(code ?? 0));
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
- resolve4(1);
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 = spawn5("code", [ctx.cwd], { stdio: "inherit", env: editorEnv, detached: true });
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 spawn6 } from "child_process";
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((resolve4) => {
11813
- const child = spawn6(command, {
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
- resolve4({ exitCode: code ?? -1, output: Buffer.concat(buffers).toString("utf-8") });
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
- resolve4({ exitCode: -1, output: err.message });
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 spawn7 } from "child_process";
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 = spawn7(command, args, {
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((resolve4) => {
12462
+ next: (timeoutMs) => new Promise((resolve5) => {
12266
12463
  if (queue.length > 0) {
12267
- resolve4(queue.shift());
12464
+ resolve5(queue.shift());
12268
12465
  return;
12269
12466
  }
12270
12467
  if (ended) {
12271
- resolve4(null);
12468
+ resolve5(null);
12272
12469
  return;
12273
12470
  }
12274
- waiter = resolve4;
12471
+ waiter = resolve5;
12275
12472
  const t = setTimeout(() => {
12276
- if (waiter === resolve4) {
12473
+ if (waiter === resolve5) {
12277
12474
  waiter = null;
12278
- resolve4(null);
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(tool2, cwd) {
12516
- const result = { name: tool2.name, present: false, verified: false };
12517
- let present = runShell(tool2.install.checkCommand, cwd);
12518
- if (!present && tool2.install.installCommand) {
12519
- runShell(tool2.install.installCommand, cwd, 12e4);
12520
- present = runShell(tool2.install.checkCommand, cwd);
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 "${tool2.name}" not on PATH (check: ${tool2.install.checkCommand})`;
12722
+ result.error = `tool "${tool3.name}" not on PATH (check: ${tool3.install.checkCommand})`;
12525
12723
  return result;
12526
12724
  }
12527
- const verified = runShell(tool2.verify, cwd);
12725
+ const verified = runShell(tool3.verify, cwd);
12528
12726
  result.verified = verified;
12529
- if (!verified) result.error = `tool "${tool2.name}" failed verify: ${tool2.verify}`;
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 = spawn8("bash", [shellPath, ...positional], {
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
- (resolve4) => {
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
- resolve4({ code, signal, spawnErr });
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": 20,
24
+ "maxTurns": 100,
25
25
  "maxThinkingTokens": null,
26
26
  "systemPromptAppend": null,
27
- "tools": ["Bash", "Read"],
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. **Emit the new state** at the very end of your response using the fenced block below. Do not include `version` or `rev` — the postflight script manages those.
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
- End your response with a single fenced block using the `kody-job-next-state` language tag:
39
+ Call the **`submit_state`** tool exactly once, as the final step, with your next state:
40
40
 
41
- ````
42
- ```kody-job-next-state
43
- {
44
- "cursor": "<your-next-cursor>",
45
- "data": { ... },
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
- If you fail to emit this block, or the JSON is invalid, the tick fails and the gist state is NOT updated. On the next wake you'll see the same prior state and can retry.
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.138",
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",