@kody-ade/kody-engine 0.4.108 → 0.4.110

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
@@ -469,16 +469,16 @@ var init_issue = __esm({
469
469
  });
470
470
 
471
471
  // src/prompt.ts
472
- import * as fs19 from "fs";
472
+ import * as fs20 from "fs";
473
473
  import * as path18 from "path";
474
474
  function loadProjectConventions(projectDir) {
475
475
  const out = [];
476
476
  for (const rel of CONVENTION_FILES) {
477
477
  const abs = path18.join(projectDir, rel);
478
- if (!fs19.existsSync(abs)) continue;
478
+ if (!fs20.existsSync(abs)) continue;
479
479
  let content;
480
480
  try {
481
- content = fs19.readFileSync(abs, "utf-8");
481
+ content = fs20.readFileSync(abs, "utf-8");
482
482
  } catch {
483
483
  continue;
484
484
  }
@@ -626,20 +626,20 @@ var loadMemoryContext_exports = {};
626
626
  __export(loadMemoryContext_exports, {
627
627
  loadMemoryContext: () => loadMemoryContext
628
628
  });
629
- import * as fs34 from "fs";
629
+ import * as fs35 from "fs";
630
630
  import * as path33 from "path";
631
631
  function collectPages(memoryAbs) {
632
632
  const out = [];
633
633
  walkMd(memoryAbs, (file) => {
634
634
  let stat;
635
635
  try {
636
- stat = fs34.statSync(file);
636
+ stat = fs35.statSync(file);
637
637
  } catch {
638
638
  return;
639
639
  }
640
640
  let raw;
641
641
  try {
642
- raw = fs34.readFileSync(file, "utf-8");
642
+ raw = fs35.readFileSync(file, "utf-8");
643
643
  } catch {
644
644
  return;
645
645
  }
@@ -715,7 +715,7 @@ function walkMd(root, visit) {
715
715
  const dir = stack.pop();
716
716
  let names;
717
717
  try {
718
- names = fs34.readdirSync(dir);
718
+ names = fs35.readdirSync(dir);
719
719
  } catch {
720
720
  continue;
721
721
  }
@@ -724,7 +724,7 @@ function walkMd(root, visit) {
724
724
  const full = path33.join(dir, name);
725
725
  let stat;
726
726
  try {
727
- stat = fs34.statSync(full);
727
+ stat = fs35.statSync(full);
728
728
  } catch {
729
729
  continue;
730
730
  }
@@ -748,7 +748,7 @@ var init_loadMemoryContext = __esm({
748
748
  loadMemoryContext = async (ctx) => {
749
749
  if (typeof ctx.data.memoryContext === "string") return;
750
750
  const memoryAbs = path33.join(ctx.cwd, MEMORY_DIR_RELATIVE);
751
- if (!fs34.existsSync(memoryAbs)) {
751
+ if (!fs35.existsSync(memoryAbs)) {
752
752
  ctx.data.memoryContext = "";
753
753
  return;
754
754
  }
@@ -880,7 +880,7 @@ var init_loadPriorArt = __esm({
880
880
  // package.json
881
881
  var package_default = {
882
882
  name: "@kody-ade/kody-engine",
883
- version: "0.4.108",
883
+ version: "0.4.110",
884
884
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
885
885
  license: "MIT",
886
886
  type: "module",
@@ -935,7 +935,7 @@ var package_default = {
935
935
 
936
936
  // src/chat-cli.ts
937
937
  import { execFileSync as execFileSync32 } from "child_process";
938
- import * as fs40 from "fs";
938
+ import * as fs41 from "fs";
939
939
  import * as path38 from "path";
940
940
 
941
941
  // src/chat/events.ts
@@ -2386,7 +2386,7 @@ async function emit2(sink, type, sessionId, suffix, payload) {
2386
2386
 
2387
2387
  // src/kody-cli.ts
2388
2388
  import { execFileSync as execFileSync31 } from "child_process";
2389
- import * as fs39 from "fs";
2389
+ import * as fs40 from "fs";
2390
2390
  import * as path37 from "path";
2391
2391
 
2392
2392
  // src/dispatch.ts
@@ -2708,8 +2708,8 @@ function coerceBare(spec, value) {
2708
2708
  init_issue();
2709
2709
 
2710
2710
  // src/executor.ts
2711
- import { execFileSync as execFileSync30, spawn as spawn6 } from "child_process";
2712
- import * as fs38 from "fs";
2711
+ import { execFileSync as execFileSync30, spawn as spawn8 } from "child_process";
2712
+ import * as fs39 from "fs";
2713
2713
  import * as path36 from "path";
2714
2714
 
2715
2715
  // src/discipline.ts
@@ -4461,10 +4461,10 @@ async function handleChatTurn(req, res, chatId, opts) {
4461
4461
  litellmUrl: opts.litellmUrl,
4462
4462
  sink
4463
4463
  }).catch((err) => {
4464
- const errMsg2 = err instanceof Error ? err.message : String(err);
4465
- process.stderr.write(`[brain-serve] chat turn failed: ${errMsg2}
4464
+ const errMsg3 = err instanceof Error ? err.message : String(err);
4465
+ process.stderr.write(`[brain-serve] chat turn failed: ${errMsg3}
4466
4466
  `);
4467
- endTurnIfUnterminated(opts.cwd, chatId, errMsg2);
4467
+ endTurnIfUnterminated(opts.cwd, chatId, errMsg3);
4468
4468
  }).finally(() => {
4469
4469
  endTurnIfUnterminated(
4470
4470
  opts.cwd,
@@ -4581,6 +4581,657 @@ var brainServe = async (ctx) => {
4581
4581
  });
4582
4582
  };
4583
4583
 
4584
+ // src/scripts/runnerServe.ts
4585
+ import { spawn as spawn3 } from "child_process";
4586
+ import { createServer as createServer2 } from "http";
4587
+ import * as fs19 from "fs";
4588
+ var DEFAULT_PORT2 = 8080;
4589
+ var DEFAULT_WORKDIR = "/workspace/repo";
4590
+ function getApiKey2() {
4591
+ const key = (process.env.RUNNER_API_KEY ?? "").trim();
4592
+ if (!key) {
4593
+ throw new Error(
4594
+ "RUNNER_API_KEY env var is required \u2014 set it on the pooled machine before boot."
4595
+ );
4596
+ }
4597
+ return key;
4598
+ }
4599
+ function authOk2(req, expected) {
4600
+ const xApiKey = req.headers["x-api-key"]?.trim();
4601
+ if (xApiKey && xApiKey === expected) return true;
4602
+ const auth = req.headers["authorization"]?.trim();
4603
+ if (auth && auth.toLowerCase().startsWith("bearer ")) {
4604
+ return auth.slice(7).trim() === expected;
4605
+ }
4606
+ return false;
4607
+ }
4608
+ function readJsonBody2(req) {
4609
+ return new Promise((resolve4, reject) => {
4610
+ const chunks = [];
4611
+ req.on("data", (c) => chunks.push(c));
4612
+ req.on("end", () => {
4613
+ const raw = Buffer.concat(chunks).toString("utf-8");
4614
+ if (!raw.trim()) {
4615
+ resolve4({});
4616
+ return;
4617
+ }
4618
+ try {
4619
+ resolve4(JSON.parse(raw));
4620
+ } catch (err) {
4621
+ reject(err instanceof Error ? err : new Error(String(err)));
4622
+ }
4623
+ });
4624
+ req.on("error", reject);
4625
+ });
4626
+ }
4627
+ function sendJson2(res, status, body) {
4628
+ res.writeHead(status, { "content-type": "application/json" });
4629
+ res.end(JSON.stringify(body));
4630
+ }
4631
+ function parseJob(body) {
4632
+ if (typeof body !== "object" || body === null) return { error: "body must be an object" };
4633
+ const b = body;
4634
+ const jobId = typeof b.jobId === "string" ? b.jobId.trim() : "";
4635
+ if (!jobId) return { error: "jobId required" };
4636
+ const repo = typeof b.repo === "string" ? b.repo.trim() : "";
4637
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
4638
+ const issueNumber = Number(b.issueNumber);
4639
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
4640
+ return { error: "issueNumber (positive integer) required" };
4641
+ }
4642
+ const githubToken = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
4643
+ if (!githubToken) return { error: "githubToken required" };
4644
+ const job = { jobId, repo, issueNumber, githubToken };
4645
+ if (typeof b.ref === "string" && b.ref.trim()) job.ref = b.ref.trim();
4646
+ if (typeof b.model === "string" && b.model.trim()) job.model = b.model.trim();
4647
+ if (typeof b.sessionId === "string" && b.sessionId.trim()) job.sessionId = b.sessionId.trim();
4648
+ if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) job.dashboardUrl = b.dashboardUrl.trim();
4649
+ if (b.allSecrets && (typeof b.allSecrets === "object" || typeof b.allSecrets === "string")) {
4650
+ job.allSecrets = b.allSecrets;
4651
+ }
4652
+ return { job };
4653
+ }
4654
+ async function defaultRunJob(job) {
4655
+ const workdir = process.env.RUNNER_WORKDIR ?? DEFAULT_WORKDIR;
4656
+ const branch = job.ref ?? "main";
4657
+ const authUrl = `https://x-access-token:${job.githubToken}@github.com/${job.repo}.git`;
4658
+ fs19.rmSync(workdir, { recursive: true, force: true });
4659
+ fs19.mkdirSync(workdir, { recursive: true });
4660
+ const allSecrets = typeof job.allSecrets === "string" ? job.allSecrets : JSON.stringify(job.allSecrets ?? {});
4661
+ const childEnv = {
4662
+ ...process.env,
4663
+ REPO: job.repo,
4664
+ REF: branch,
4665
+ GITHUB_TOKEN: job.githubToken,
4666
+ ISSUE_NUMBER: String(job.issueNumber),
4667
+ ALL_SECRETS: allSecrets,
4668
+ SESSION_ID: job.sessionId ?? "",
4669
+ DASHBOARD_URL: job.dashboardUrl ?? "",
4670
+ MODEL: job.model ?? ""
4671
+ };
4672
+ const run = (cmd, args, cwd) => new Promise((resolve4) => {
4673
+ const child = spawn3(cmd, args, { stdio: "inherit", env: childEnv, cwd });
4674
+ child.on("exit", (code) => resolve4(code ?? 0));
4675
+ child.on("error", (err) => {
4676
+ process.stderr.write(`[runner-serve] ${cmd} failed: ${err.message}
4677
+ `);
4678
+ resolve4(1);
4679
+ });
4680
+ });
4681
+ process.stdout.write(`[runner-serve] job ${job.jobId}: cloning ${job.repo}@${branch}
4682
+ `);
4683
+ const cloneCode = await run("git", [
4684
+ "clone",
4685
+ "--depth=1",
4686
+ "--single-branch",
4687
+ "--branch",
4688
+ branch,
4689
+ authUrl,
4690
+ workdir
4691
+ ]);
4692
+ if (cloneCode !== 0) {
4693
+ process.stderr.write(`[runner-serve] job ${job.jobId}: clone failed (${cloneCode})
4694
+ `);
4695
+ process.exit(cloneCode);
4696
+ return;
4697
+ }
4698
+ process.stdout.write(`[runner-serve] job ${job.jobId}: running issue #${job.issueNumber}
4699
+ `);
4700
+ const runCode = await run("kody", ["run", "--issue", String(job.issueNumber)], workdir);
4701
+ process.stdout.write(`[runner-serve] job ${job.jobId}: finished (exit ${runCode})
4702
+ `);
4703
+ process.exit(runCode);
4704
+ }
4705
+ function buildServer2(opts) {
4706
+ const runJob = opts.runJob ?? defaultRunJob;
4707
+ let busy = false;
4708
+ return createServer2(async (req, res) => {
4709
+ if (!req.method || !req.url) {
4710
+ sendJson2(res, 400, { error: "bad request" });
4711
+ return;
4712
+ }
4713
+ const url = new URL(req.url, "http://localhost");
4714
+ if (req.method === "GET" && url.pathname === "/healthz") {
4715
+ sendJson2(res, 200, { ok: true, busy });
4716
+ return;
4717
+ }
4718
+ if (!authOk2(req, opts.apiKey)) {
4719
+ sendJson2(res, 401, { error: "unauthorized" });
4720
+ return;
4721
+ }
4722
+ if (req.method === "POST" && url.pathname === "/run") {
4723
+ if (busy) {
4724
+ sendJson2(res, 409, { error: "runner busy" });
4725
+ return;
4726
+ }
4727
+ let body;
4728
+ try {
4729
+ body = await readJsonBody2(req);
4730
+ } catch {
4731
+ sendJson2(res, 400, { error: "invalid JSON body" });
4732
+ return;
4733
+ }
4734
+ const parsed = parseJob(body);
4735
+ if ("error" in parsed) {
4736
+ sendJson2(res, 400, { error: parsed.error });
4737
+ return;
4738
+ }
4739
+ busy = true;
4740
+ sendJson2(res, 202, { ok: true, jobId: parsed.job.jobId, started: true });
4741
+ void runJob(parsed.job).catch((err) => {
4742
+ const msg = err instanceof Error ? err.message : String(err);
4743
+ process.stderr.write(`[runner-serve] job ${parsed.job.jobId} crashed: ${msg}
4744
+ `);
4745
+ process.exit(1);
4746
+ });
4747
+ return;
4748
+ }
4749
+ sendJson2(res, 404, { error: "not found" });
4750
+ });
4751
+ }
4752
+ var runnerServe = async (ctx) => {
4753
+ ctx.skipAgent = true;
4754
+ const apiKey = getApiKey2();
4755
+ const port = Number(process.env.PORT ?? DEFAULT_PORT2);
4756
+ const server = buildServer2({ apiKey });
4757
+ await new Promise((resolve4) => {
4758
+ server.listen(port, "0.0.0.0", () => {
4759
+ process.stdout.write(`[runner-serve] listening on 0.0.0.0:${port} (idle, awaiting job)
4760
+ `);
4761
+ resolve4();
4762
+ });
4763
+ });
4764
+ const shutdown = (signal) => {
4765
+ process.stdout.write(`[runner-serve] ${signal} \u2014 shutting down
4766
+ `);
4767
+ server.close(() => process.exit(0));
4768
+ };
4769
+ process.once("SIGINT", () => shutdown("SIGINT"));
4770
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
4771
+ await new Promise(() => {
4772
+ });
4773
+ };
4774
+
4775
+ // src/scripts/poolServe.ts
4776
+ import { spawn as spawn4 } from "child_process";
4777
+ import { createServer as createServer3 } from "http";
4778
+
4779
+ // src/pool/fly.ts
4780
+ var FLY_API_BASE = "https://api.machines.dev/v1";
4781
+ var POOL_METADATA_KEY = "kody_pool";
4782
+ var POOL_METADATA_VALUE = "1";
4783
+ var FlyClient = class {
4784
+ constructor(opts) {
4785
+ this.opts = opts;
4786
+ if (!opts.token?.trim()) throw new Error("FlyClient: token required");
4787
+ if (!opts.app?.trim()) throw new Error("FlyClient: app required");
4788
+ }
4789
+ opts;
4790
+ get fetch() {
4791
+ return this.opts.fetchImpl ?? fetch;
4792
+ }
4793
+ async call(path39, init = {}) {
4794
+ const res = await this.fetch(`${FLY_API_BASE}${path39}`, {
4795
+ method: init.method ?? "GET",
4796
+ headers: {
4797
+ Authorization: `Bearer ${this.opts.token}`,
4798
+ "Content-Type": "application/json"
4799
+ },
4800
+ body: init.body !== void 0 ? JSON.stringify(init.body) : void 0
4801
+ });
4802
+ if (res.status === 404 && init.allow404) return null;
4803
+ if (!res.ok) {
4804
+ const text = await res.text().catch(() => "");
4805
+ throw new Error(`Fly API ${res.status} on ${path39}: ${text.slice(0, 200) || res.statusText}`);
4806
+ }
4807
+ if (res.status === 204) return null;
4808
+ const raw = await res.text();
4809
+ if (!raw.trim()) return null;
4810
+ return JSON.parse(raw);
4811
+ }
4812
+ /** Create + start a pooled machine in serve mode. */
4813
+ async createPooled(input) {
4814
+ const body = {
4815
+ region: input.region,
4816
+ config: {
4817
+ image: input.image,
4818
+ guest: input.guest,
4819
+ auto_destroy: true,
4820
+ restart: { policy: "no" },
4821
+ init: { entrypoint: ["/usr/local/bin/entrypoint-serve.sh"] },
4822
+ metadata: { [POOL_METADATA_KEY]: POOL_METADATA_VALUE },
4823
+ env: {
4824
+ RUNNER_API_KEY: input.runnerApiKey,
4825
+ KODY_LITELLM_URL: input.litellmUrl,
4826
+ PORT: String(input.port ?? 8080)
4827
+ }
4828
+ }
4829
+ };
4830
+ const m = await this.call(`/apps/${enc(this.opts.app)}/machines`, { method: "POST", body });
4831
+ if (!m?.id) throw new Error("Fly API: createPooled returned no machine id");
4832
+ return m;
4833
+ }
4834
+ async get(id) {
4835
+ return this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}`, { allow404: true });
4836
+ }
4837
+ /** List the app's pooled (kody_pool) machines, excluding destroyed/destroying. */
4838
+ async listPooled() {
4839
+ const all = await this.call(`/apps/${enc(this.opts.app)}/machines`, { allow404: true }) ?? [];
4840
+ return all.filter(
4841
+ (m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.state !== "destroyed" && m.state !== "destroying"
4842
+ );
4843
+ }
4844
+ /** Suspend (freeze) a machine — wakes in ~1s from the snapshot. */
4845
+ async suspend(id) {
4846
+ await this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}/suspend`, { method: "POST", allow404: true });
4847
+ }
4848
+ /** Start (wake) a suspended/stopped machine. */
4849
+ async start(id) {
4850
+ await this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}/start`, { method: "POST", allow404: true });
4851
+ }
4852
+ async destroy(id) {
4853
+ await this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}?force=true`, { method: "DELETE", allow404: true });
4854
+ }
4855
+ /**
4856
+ * Wait for `GET <baseUrl>/healthz` to return 200, polling until timeout.
4857
+ * baseUrl is the machine's private 6PN address (http://[ip]:port).
4858
+ */
4859
+ async waitHealthy(baseUrl, opts = {}) {
4860
+ const timeoutMs = opts.timeoutMs ?? 12e4;
4861
+ const intervalMs = opts.intervalMs ?? 1e3;
4862
+ const deadline = Date.now() + timeoutMs;
4863
+ while (Date.now() < deadline) {
4864
+ try {
4865
+ const res = await this.fetch(`${baseUrl}/healthz`, { signal: AbortSignal.timeout(4e3) });
4866
+ if (res.ok) return true;
4867
+ } catch {
4868
+ }
4869
+ await sleep2(intervalMs);
4870
+ }
4871
+ return false;
4872
+ }
4873
+ };
4874
+ function enc(s) {
4875
+ return encodeURIComponent(s);
4876
+ }
4877
+ function sleep2(ms) {
4878
+ return new Promise((r) => setTimeout(r, ms));
4879
+ }
4880
+
4881
+ // src/pool/manager.ts
4882
+ var PoolManager = class {
4883
+ constructor(deps) {
4884
+ this.deps = deps;
4885
+ this.postRun = deps.postRun ?? defaultPostRun;
4886
+ this.log = deps.log ?? (() => {
4887
+ });
4888
+ }
4889
+ deps;
4890
+ free = [];
4891
+ booting = 0;
4892
+ claimsInFlight = 0;
4893
+ refilling = false;
4894
+ postRun;
4895
+ log;
4896
+ status() {
4897
+ return {
4898
+ min: this.deps.config.min,
4899
+ free: this.free.length,
4900
+ booting: this.booting,
4901
+ claimsInFlight: this.claimsInFlight,
4902
+ total: this.free.length + this.booting + this.claimsInFlight
4903
+ };
4904
+ }
4905
+ /**
4906
+ * Adopt existing pooled machines on owner (re)start: suspended ones become
4907
+ * free; anything else is left to finish/auto-destroy. Then refill to `min`.
4908
+ */
4909
+ async reconcile() {
4910
+ const machines = await this.deps.fly.listPooled();
4911
+ this.free = [];
4912
+ for (const m of machines) {
4913
+ if ((m.state === "suspended" || m.state === "suspending") && m.private_ip) {
4914
+ this.free.push({ id: m.id, privateIp: m.private_ip });
4915
+ }
4916
+ }
4917
+ this.log(`reconcile: adopted ${this.free.length} suspended machine(s)`);
4918
+ await this.refill();
4919
+ }
4920
+ /**
4921
+ * Claim a warm machine for a job. Returns ok:false (caller falls back to
4922
+ * create-fresh) when the pool is empty or the woken machine fails to take
4923
+ * the job. The pick is synchronous — the atomic step.
4924
+ */
4925
+ async claim(job) {
4926
+ const machine = this.free.shift();
4927
+ if (!machine) {
4928
+ this.log("claim: pool empty");
4929
+ void this.refill();
4930
+ return { ok: false, reason: "pool empty" };
4931
+ }
4932
+ this.claimsInFlight++;
4933
+ try {
4934
+ await this.deps.fly.start(machine.id);
4935
+ const base = this.baseUrl(machine);
4936
+ const healthy = await this.deps.fly.waitHealthy(base, { timeoutMs: this.deps.config.healthTimeoutMs });
4937
+ if (!healthy) {
4938
+ this.log(`claim: machine ${machine.id} unhealthy after wake \u2014 destroying`);
4939
+ await this.safeDestroy(machine.id);
4940
+ return { ok: false, reason: "woken machine unhealthy" };
4941
+ }
4942
+ const accepted = await this.postRun(machine, job, this.deps.config);
4943
+ if (!accepted) {
4944
+ this.log(`claim: machine ${machine.id} rejected job \u2014 destroying`);
4945
+ await this.safeDestroy(machine.id);
4946
+ return { ok: false, reason: "machine rejected job" };
4947
+ }
4948
+ this.log(`claim: machine ${machine.id} took job ${job.jobId}`);
4949
+ return { ok: true, machineId: machine.id };
4950
+ } catch (err) {
4951
+ this.log(`claim: error on ${machine.id}: ${errMsg2(err)} \u2014 destroying`);
4952
+ await this.safeDestroy(machine.id);
4953
+ return { ok: false, reason: errMsg2(err) };
4954
+ } finally {
4955
+ this.claimsInFlight--;
4956
+ void this.refill();
4957
+ }
4958
+ }
4959
+ /** Top up free machines to `min`. Serialized so it never overshoots. */
4960
+ async refill() {
4961
+ if (this.refilling) return;
4962
+ this.refilling = true;
4963
+ try {
4964
+ while (this.free.length + this.booting < this.deps.config.min) {
4965
+ this.booting++;
4966
+ try {
4967
+ await this.bootOne();
4968
+ } catch (err) {
4969
+ this.log(`refill: boot failed: ${errMsg2(err)}`);
4970
+ this.booting--;
4971
+ break;
4972
+ }
4973
+ this.booting--;
4974
+ }
4975
+ } finally {
4976
+ this.refilling = false;
4977
+ }
4978
+ }
4979
+ async bootOne() {
4980
+ const cfg = this.deps.config;
4981
+ const m = await this.deps.fly.createPooled({
4982
+ image: cfg.image,
4983
+ region: cfg.region,
4984
+ guest: cfg.guest,
4985
+ runnerApiKey: cfg.runnerApiKey,
4986
+ litellmUrl: cfg.litellmUrl,
4987
+ port: cfg.port
4988
+ });
4989
+ if (!m.private_ip) {
4990
+ const refreshed = await this.deps.fly.get(m.id);
4991
+ if (refreshed?.private_ip) m.private_ip = refreshed.private_ip;
4992
+ }
4993
+ if (!m.private_ip) {
4994
+ await this.safeDestroy(m.id);
4995
+ throw new Error(`machine ${m.id} has no private_ip`);
4996
+ }
4997
+ const free = { id: m.id, privateIp: m.private_ip };
4998
+ const healthy = await this.deps.fly.waitHealthy(this.baseUrl(free), { timeoutMs: cfg.healthTimeoutMs });
4999
+ if (!healthy) {
5000
+ await this.safeDestroy(m.id);
5001
+ throw new Error(`machine ${m.id} never became healthy`);
5002
+ }
5003
+ await this.deps.fly.suspend(m.id);
5004
+ this.free.push(free);
5005
+ this.log(`refill: machine ${m.id} booted, frozen, ready (free=${this.free.length})`);
5006
+ }
5007
+ baseUrl(m) {
5008
+ return `http://[${m.privateIp}]:${this.deps.config.port}`;
5009
+ }
5010
+ async safeDestroy(id) {
5011
+ try {
5012
+ await this.deps.fly.destroy(id);
5013
+ } catch (err) {
5014
+ this.log(`destroy ${id} failed: ${errMsg2(err)}`);
5015
+ }
5016
+ }
5017
+ };
5018
+ async function defaultPostRun(m, job, cfg) {
5019
+ const res = await fetch(`http://[${m.privateIp}]:${cfg.port}/run`, {
5020
+ method: "POST",
5021
+ headers: {
5022
+ "content-type": "application/json",
5023
+ "x-api-key": cfg.runnerApiKey
5024
+ },
5025
+ body: JSON.stringify(job),
5026
+ signal: AbortSignal.timeout(15e3)
5027
+ });
5028
+ return res.status === 202;
5029
+ }
5030
+ function errMsg2(err) {
5031
+ return err instanceof Error ? err.message : String(err);
5032
+ }
5033
+
5034
+ // src/pool/keys.ts
5035
+ import { hkdfSync } from "crypto";
5036
+ var POOL_API_KEY_INFO = "kody-pool-api:v1";
5037
+ var RUNNER_API_KEY_INFO = "kody-runner-api:v1";
5038
+ function masterKeyBytes(raw) {
5039
+ const v = raw.trim();
5040
+ if (!v) throw new Error("KODY_MASTER_KEY is empty");
5041
+ if (/^[0-9a-fA-F]+$/.test(v) && v.length === 64) {
5042
+ return Buffer.from(v, "hex");
5043
+ }
5044
+ return Buffer.from(v.replace(/-/g, "+").replace(/_/g, "/"), "base64");
5045
+ }
5046
+ function deriveKey(master, info, length = 32) {
5047
+ return Buffer.from(hkdfSync("sha256", master, Buffer.alloc(0), info, length)).toString("hex");
5048
+ }
5049
+ function derivePoolApiKey(master) {
5050
+ return deriveKey(master, POOL_API_KEY_INFO);
5051
+ }
5052
+ function deriveRunnerApiKey(master) {
5053
+ return deriveKey(master, RUNNER_API_KEY_INFO);
5054
+ }
5055
+ function bearerOk(headerAuth, xApiKey, expected) {
5056
+ const x = (xApiKey ?? "").trim();
5057
+ if (x && timingEqual(x, expected)) return true;
5058
+ const a = (headerAuth ?? "").trim();
5059
+ if (a.toLowerCase().startsWith("bearer ")) {
5060
+ return timingEqual(a.slice(7).trim(), expected);
5061
+ }
5062
+ return false;
5063
+ }
5064
+ function timingEqual(a, b) {
5065
+ if (a.length !== b.length) return false;
5066
+ let diff = 0;
5067
+ for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
5068
+ return diff === 0;
5069
+ }
5070
+
5071
+ // src/scripts/poolServe.ts
5072
+ var PERF_GUEST = {
5073
+ low: { cpu_kind: "shared", cpus: 2, memory_mb: 2048 },
5074
+ medium: { cpu_kind: "performance", cpus: 1, memory_mb: 2048 },
5075
+ high: { cpu_kind: "performance", cpus: 2, memory_mb: 4096 }
5076
+ };
5077
+ function envInt(name, dflt) {
5078
+ const v = Number(process.env[name]);
5079
+ return Number.isFinite(v) && v > 0 ? v : dflt;
5080
+ }
5081
+ function log(msg) {
5082
+ process.stdout.write(`[pool-serve] ${msg}
5083
+ `);
5084
+ }
5085
+ function sendJson3(res, status, body) {
5086
+ res.writeHead(status, { "content-type": "application/json" });
5087
+ res.end(JSON.stringify(body));
5088
+ }
5089
+ function readJsonBody3(req) {
5090
+ return new Promise((resolve4, reject) => {
5091
+ const chunks = [];
5092
+ req.on("data", (c) => chunks.push(c));
5093
+ req.on("end", () => {
5094
+ const raw = Buffer.concat(chunks).toString("utf-8");
5095
+ if (!raw.trim()) return resolve4({});
5096
+ try {
5097
+ resolve4(JSON.parse(raw));
5098
+ } catch (err) {
5099
+ reject(err instanceof Error ? err : new Error(String(err)));
5100
+ }
5101
+ });
5102
+ req.on("error", reject);
5103
+ });
5104
+ }
5105
+ function parsePoolJob(body) {
5106
+ if (typeof body !== "object" || body === null) return { error: "body must be an object" };
5107
+ const b = body;
5108
+ const jobId = typeof b.jobId === "string" ? b.jobId.trim() : "";
5109
+ if (!jobId) return { error: "jobId required" };
5110
+ const repo = typeof b.repo === "string" ? b.repo.trim() : "";
5111
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
5112
+ const issueNumber = Number(b.issueNumber);
5113
+ if (!Number.isInteger(issueNumber) || issueNumber <= 0) return { error: "issueNumber required" };
5114
+ const githubToken = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
5115
+ if (!githubToken) return { error: "githubToken required" };
5116
+ const job = { jobId, repo, issueNumber, githubToken };
5117
+ if (typeof b.ref === "string" && b.ref.trim()) job.ref = b.ref.trim();
5118
+ if (typeof b.model === "string" && b.model.trim()) job.model = b.model.trim();
5119
+ if (typeof b.sessionId === "string" && b.sessionId.trim()) job.sessionId = b.sessionId.trim();
5120
+ if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) job.dashboardUrl = b.dashboardUrl.trim();
5121
+ if (b.allSecrets && typeof b.allSecrets === "object") job.allSecrets = b.allSecrets;
5122
+ return { job };
5123
+ }
5124
+ function superviseLitellm() {
5125
+ if (process.env.POOL_DISABLE_LITELLM === "1") return null;
5126
+ const port = String(envInt("LITELLM_PORT", 4e3));
5127
+ const config = process.env.LITELLM_CONFIG ?? "/app/config.yaml";
5128
+ let restarts = 0;
5129
+ const start = () => {
5130
+ log(`starting litellm child (port ${port})`);
5131
+ const child = spawn4("litellm", ["--config", config, "--port", port, "--host", "0.0.0.0"], {
5132
+ stdio: "inherit"
5133
+ });
5134
+ child.on("exit", (code) => {
5135
+ restarts++;
5136
+ if (restarts > 50) {
5137
+ process.stderr.write("[pool-serve] litellm restarted too many times \u2014 giving up\n");
5138
+ return;
5139
+ }
5140
+ log(`litellm exited (${code}) \u2014 restarting in 2s`);
5141
+ setTimeout(start, 2e3);
5142
+ });
5143
+ child.on("error", (err) => process.stderr.write(`[pool-serve] litellm spawn error: ${err.message}
5144
+ `));
5145
+ return child;
5146
+ };
5147
+ return start();
5148
+ }
5149
+ var poolServe = async (ctx) => {
5150
+ ctx.skipAgent = true;
5151
+ const masterRaw = process.env.KODY_MASTER_KEY?.trim();
5152
+ if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
5153
+ const flyToken = process.env.FLY_API_TOKEN?.trim();
5154
+ if (!flyToken) throw new Error("FLY_API_TOKEN required for pool-serve");
5155
+ const master = masterKeyBytes(masterRaw);
5156
+ const poolApiKey = derivePoolApiKey(master);
5157
+ const runnerApiKey = deriveRunnerApiKey(master);
5158
+ const app = process.env.FLY_APP_NAME ?? "kody-runner";
5159
+ const region = process.env.FLY_REGION ?? "fra";
5160
+ const perf = process.env.POOL_PERF ?? "medium";
5161
+ const guest = PERF_GUEST[perf] ?? PERF_GUEST.medium;
5162
+ const litellmUrl = process.env.KODY_LITELLM_URL ?? "http://kody-litellm.internal:4000";
5163
+ const min = envInt("POOL_MIN", 2);
5164
+ const runnerPort = envInt("RUNNER_PORT", 8080);
5165
+ const apiPort = envInt("POOL_API_PORT", 4100);
5166
+ const healthTimeoutMs = envInt("POOL_HEALTH_TIMEOUT_MS", 12e4);
5167
+ const litellm = superviseLitellm();
5168
+ const fly = new FlyClient({ token: flyToken, app });
5169
+ const manager = new PoolManager({
5170
+ fly,
5171
+ config: { min, image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest", region, guest, runnerApiKey, litellmUrl, port: runnerPort, healthTimeoutMs },
5172
+ log
5173
+ });
5174
+ manager.reconcile().catch((err) => log(`reconcile failed: ${err instanceof Error ? err.message : String(err)}`));
5175
+ const refillMs = envInt("POOL_REFILL_INTERVAL_MS", 6e4);
5176
+ const tick = setInterval(() => {
5177
+ manager.refill().catch((err) => log(`refill tick failed: ${err instanceof Error ? err.message : String(err)}`));
5178
+ }, refillMs);
5179
+ const server = createServer3(async (req, res) => {
5180
+ try {
5181
+ if (!req.method || !req.url) return sendJson3(res, 400, { error: "bad request" });
5182
+ const url = new URL(req.url, "http://localhost");
5183
+ if (req.method === "GET" && url.pathname === "/healthz") {
5184
+ return sendJson3(res, 200, { ok: true, litellm: litellm ? "supervised" : "off", pool: manager.status() });
5185
+ }
5186
+ const authed = bearerOk(
5187
+ req.headers["authorization"],
5188
+ req.headers["x-api-key"],
5189
+ poolApiKey
5190
+ );
5191
+ if (!authed) return sendJson3(res, 401, { error: "unauthorized" });
5192
+ if (req.method === "GET" && url.pathname === "/pool/status") {
5193
+ return sendJson3(res, 200, manager.status());
5194
+ }
5195
+ if (req.method === "POST" && url.pathname === "/pool/claim") {
5196
+ let body;
5197
+ try {
5198
+ body = await readJsonBody3(req);
5199
+ } catch {
5200
+ return sendJson3(res, 400, { error: "invalid JSON body" });
5201
+ }
5202
+ const parsed = parsePoolJob(body);
5203
+ if ("error" in parsed) return sendJson3(res, 400, { error: parsed.error });
5204
+ const result = await manager.claim(parsed.job);
5205
+ if (result.ok) return sendJson3(res, 200, { ok: true, machineId: result.machineId });
5206
+ return sendJson3(res, 503, { ok: false, reason: result.reason ?? "pool unavailable" });
5207
+ }
5208
+ return sendJson3(res, 404, { error: "not found" });
5209
+ } catch (err) {
5210
+ process.stderr.write(`[pool-serve] handler error: ${err instanceof Error ? err.message : String(err)}
5211
+ `);
5212
+ try {
5213
+ sendJson3(res, 500, { error: "internal error" });
5214
+ } catch {
5215
+ }
5216
+ }
5217
+ });
5218
+ await new Promise((resolve4) => {
5219
+ server.listen(apiPort, "0.0.0.0", () => {
5220
+ log(`listening on 0.0.0.0:${apiPort} (min=${min}, app=${app}, region=${region})`);
5221
+ resolve4();
5222
+ });
5223
+ });
5224
+ const shutdown = (signal) => {
5225
+ log(`${signal} \u2014 shutting down`);
5226
+ clearInterval(tick);
5227
+ server.close(() => process.exit(0));
5228
+ };
5229
+ process.once("SIGINT", () => shutdown("SIGINT"));
5230
+ process.once("SIGTERM", () => shutdown("SIGTERM"));
5231
+ await new Promise(() => {
5232
+ });
5233
+ };
5234
+
4584
5235
  // src/coverage.ts
4585
5236
  import { execFileSync as execFileSync9 } from "child_process";
4586
5237
  function patternToRegex(pattern) {
@@ -4727,7 +5378,7 @@ function defaultLabelMap() {
4727
5378
  }
4728
5379
 
4729
5380
  // src/scripts/commitAndPush.ts
4730
- import * as fs20 from "fs";
5381
+ import * as fs21 from "fs";
4731
5382
  import * as path19 from "path";
4732
5383
  init_events();
4733
5384
  var DEFAULT_COMMIT_MESSAGE = "chore: kody changes";
@@ -4743,9 +5394,9 @@ var commitAndPush2 = async (ctx, profile) => {
4743
5394
  }
4744
5395
  const idempotencyEnabled = process.env.KODY_COMMIT_IDEMPOTENCY !== "0";
4745
5396
  const sentinel = idempotencyEnabled ? sentinelPathForStage(ctx.cwd, profile.name) : null;
4746
- if (sentinel && fs20.existsSync(sentinel)) {
5397
+ if (sentinel && fs21.existsSync(sentinel)) {
4747
5398
  try {
4748
- const replay = JSON.parse(fs20.readFileSync(sentinel, "utf-8"));
5399
+ const replay = JSON.parse(fs21.readFileSync(sentinel, "utf-8"));
4749
5400
  ctx.data.commitResult = replay.commitResult ?? { committed: false, pushed: false };
4750
5401
  if (Array.isArray(replay.changedFiles)) ctx.data.changedFiles = replay.changedFiles;
4751
5402
  if (typeof replay.hasCommitsAhead === "boolean") ctx.data.hasCommitsAhead = replay.hasCommitsAhead;
@@ -4798,8 +5449,8 @@ var commitAndPush2 = async (ctx, profile) => {
4798
5449
  const result = ctx.data.commitResult;
4799
5450
  if (sentinel && result?.committed) {
4800
5451
  try {
4801
- fs20.mkdirSync(path19.dirname(sentinel), { recursive: true });
4802
- fs20.writeFileSync(
5452
+ fs21.mkdirSync(path19.dirname(sentinel), { recursive: true });
5453
+ fs21.writeFileSync(
4803
5454
  sentinel,
4804
5455
  JSON.stringify(
4805
5456
  {
@@ -4869,7 +5520,7 @@ function describeCommitMessage(goal) {
4869
5520
  }
4870
5521
 
4871
5522
  // src/scripts/composePrompt.ts
4872
- import * as fs21 from "fs";
5523
+ import * as fs22 from "fs";
4873
5524
  import * as path21 from "path";
4874
5525
  var MUSTACHE = /\{\{\s*([a-zA-Z0-9_.-]+)\s*\}\}/g;
4875
5526
  var composePrompt = async (ctx, profile) => {
@@ -4882,7 +5533,7 @@ var composePrompt = async (ctx, profile) => {
4882
5533
  ].filter(Boolean);
4883
5534
  let templatePath = "";
4884
5535
  for (const c of candidates) {
4885
- if (fs21.existsSync(c)) {
5536
+ if (fs22.existsSync(c)) {
4886
5537
  templatePath = c;
4887
5538
  break;
4888
5539
  }
@@ -4890,7 +5541,7 @@ var composePrompt = async (ctx, profile) => {
4890
5541
  if (!templatePath) {
4891
5542
  throw new Error(`profile at ${profile.dir}: no prompt template found (tried ${candidates.join(", ")})`);
4892
5543
  }
4893
- const template = fs21.readFileSync(templatePath, "utf-8");
5544
+ const template = fs22.readFileSync(templatePath, "utf-8");
4894
5545
  const tokens = {
4895
5546
  ...stringifyAll(ctx.args, "args."),
4896
5547
  ...stringifyAll(ctx.data, ""),
@@ -4969,7 +5620,7 @@ function formatToolsUsage(profile) {
4969
5620
  // src/scripts/createQaGoal.ts
4970
5621
  init_issue();
4971
5622
  import { execFileSync as execFileSync11 } from "child_process";
4972
- import * as fs22 from "fs";
5623
+ import * as fs23 from "fs";
4973
5624
  import * as path22 from "path";
4974
5625
 
4975
5626
  // src/scripts/postReviewResult.ts
@@ -5224,7 +5875,7 @@ function createOrUpdateManifestIssue(number, manifest, cwd) {
5224
5875
  }
5225
5876
  function writeStateFile(cwd, goalId, lastDispatchedIssue) {
5226
5877
  const dir = path22.join(cwd, ".kody", "goals", goalId);
5227
- fs22.mkdirSync(dir, { recursive: true });
5878
+ fs23.mkdirSync(dir, { recursive: true });
5228
5879
  const state = {
5229
5880
  version: 1,
5230
5881
  state: "active",
@@ -5233,7 +5884,7 @@ function writeStateFile(cwd, goalId, lastDispatchedIssue) {
5233
5884
  ...typeof lastDispatchedIssue === "number" ? { lastDispatchedIssue } : {}
5234
5885
  };
5235
5886
  const filePath = path22.join(dir, "state.json");
5236
- fs22.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
5887
+ fs23.writeFileSync(filePath, `${JSON.stringify(state, null, 2)}
5237
5888
  `);
5238
5889
  return filePath;
5239
5890
  }
@@ -5742,7 +6393,7 @@ function filterGoalTaskPrs(prs, taskIssueNumbers) {
5742
6393
 
5743
6394
  // src/scripts/diagMcp.ts
5744
6395
  import { execFileSync as execFileSync12 } from "child_process";
5745
- import * as fs23 from "fs";
6396
+ import * as fs24 from "fs";
5746
6397
  import * as os4 from "os";
5747
6398
  import * as path23 from "path";
5748
6399
  var diagMcp = async (_ctx) => {
@@ -5750,7 +6401,7 @@ var diagMcp = async (_ctx) => {
5750
6401
  const cacheDir = path23.join(home, ".cache", "ms-playwright");
5751
6402
  let entries = [];
5752
6403
  try {
5753
- entries = fs23.readdirSync(cacheDir);
6404
+ entries = fs24.readdirSync(cacheDir);
5754
6405
  } catch {
5755
6406
  }
5756
6407
  const hasChromium = entries.some((e) => e.startsWith("chromium"));
@@ -5776,17 +6427,17 @@ var diagMcp = async (_ctx) => {
5776
6427
  };
5777
6428
 
5778
6429
  // src/scripts/discoverQaContext.ts
5779
- import * as fs25 from "fs";
6430
+ import * as fs26 from "fs";
5780
6431
  import * as path25 from "path";
5781
6432
 
5782
6433
  // src/scripts/frameworkDetectors.ts
5783
- import * as fs24 from "fs";
6434
+ import * as fs25 from "fs";
5784
6435
  import * as path24 from "path";
5785
6436
  function detectFrameworks(cwd) {
5786
6437
  const out = [];
5787
6438
  let deps = {};
5788
6439
  try {
5789
- const pkg = JSON.parse(fs24.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
6440
+ const pkg = JSON.parse(fs25.readFileSync(path24.join(cwd, "package.json"), "utf-8"));
5790
6441
  deps = { ...pkg.dependencies, ...pkg.devDependencies };
5791
6442
  } catch {
5792
6443
  return out;
@@ -5823,7 +6474,7 @@ function detectFrameworks(cwd) {
5823
6474
  }
5824
6475
  function findFile(cwd, candidates) {
5825
6476
  for (const c of candidates) {
5826
- if (fs24.existsSync(path24.join(cwd, c))) return c;
6477
+ if (fs25.existsSync(path24.join(cwd, c))) return c;
5827
6478
  }
5828
6479
  return null;
5829
6480
  }
@@ -5837,17 +6488,17 @@ function discoverPayloadCollections(cwd) {
5837
6488
  const out = [];
5838
6489
  for (const dir of COLLECTION_DIRS) {
5839
6490
  const full = path24.join(cwd, dir);
5840
- if (!fs24.existsSync(full)) continue;
6491
+ if (!fs25.existsSync(full)) continue;
5841
6492
  let files;
5842
6493
  try {
5843
- files = fs24.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6494
+ files = fs25.readdirSync(full).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
5844
6495
  } catch {
5845
6496
  continue;
5846
6497
  }
5847
6498
  for (const file of files) {
5848
6499
  try {
5849
6500
  const filePath = path24.join(full, file);
5850
- const content = fs24.readFileSync(filePath, "utf-8").slice(0, 1e4);
6501
+ const content = fs25.readFileSync(filePath, "utf-8").slice(0, 1e4);
5851
6502
  const slugMatch = content.match(/slug:\s*['"]([a-z0-9-]+)['"]/);
5852
6503
  if (!slugMatch) continue;
5853
6504
  const slug = slugMatch[1];
@@ -5876,10 +6527,10 @@ function discoverAdminComponents(cwd, collections) {
5876
6527
  const out = [];
5877
6528
  for (const dir of ADMIN_COMPONENT_DIRS) {
5878
6529
  const full = path24.join(cwd, dir);
5879
- if (!fs24.existsSync(full)) continue;
6530
+ if (!fs25.existsSync(full)) continue;
5880
6531
  let entries;
5881
6532
  try {
5882
- entries = fs24.readdirSync(full, { withFileTypes: true });
6533
+ entries = fs25.readdirSync(full, { withFileTypes: true });
5883
6534
  } catch {
5884
6535
  continue;
5885
6536
  }
@@ -5889,7 +6540,7 @@ function discoverAdminComponents(cwd, collections) {
5889
6540
  let filePath;
5890
6541
  if (entry.isDirectory()) {
5891
6542
  const indexFile = ["index.tsx", "index.ts", "index.jsx", "index.js"].find(
5892
- (f) => fs24.existsSync(path24.join(entryPath, f))
6543
+ (f) => fs25.existsSync(path24.join(entryPath, f))
5893
6544
  );
5894
6545
  if (!indexFile) continue;
5895
6546
  name = entry.name;
@@ -5904,7 +6555,7 @@ function discoverAdminComponents(cwd, collections) {
5904
6555
  if (collections) {
5905
6556
  for (const col of collections) {
5906
6557
  try {
5907
- const colContent = fs24.readFileSync(path24.join(cwd, col.filePath), "utf-8");
6558
+ const colContent = fs25.readFileSync(path24.join(cwd, col.filePath), "utf-8");
5908
6559
  if (colContent.includes(name)) {
5909
6560
  usedInCollection = col.slug;
5910
6561
  break;
@@ -5924,7 +6575,7 @@ function scanApiRoutes(cwd) {
5924
6575
  const appDirs = ["src/app", "app"];
5925
6576
  for (const appDir of appDirs) {
5926
6577
  const apiDir = path24.join(cwd, appDir, "api");
5927
- if (!fs24.existsSync(apiDir)) continue;
6578
+ if (!fs25.existsSync(apiDir)) continue;
5928
6579
  walkApiRoutes(apiDir, "/api", cwd, out);
5929
6580
  break;
5930
6581
  }
@@ -5933,14 +6584,14 @@ function scanApiRoutes(cwd) {
5933
6584
  function walkApiRoutes(dir, prefix, cwd, out) {
5934
6585
  let entries;
5935
6586
  try {
5936
- entries = fs24.readdirSync(dir, { withFileTypes: true });
6587
+ entries = fs25.readdirSync(dir, { withFileTypes: true });
5937
6588
  } catch {
5938
6589
  return;
5939
6590
  }
5940
6591
  const routeFile = entries.find((e) => e.isFile() && /^route\.(ts|js|tsx|jsx)$/.test(e.name));
5941
6592
  if (routeFile) {
5942
6593
  try {
5943
- const content = fs24.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
6594
+ const content = fs25.readFileSync(path24.join(dir, routeFile.name), "utf-8").slice(0, 5e3);
5944
6595
  const methods = HTTP_METHODS.filter(
5945
6596
  (m) => new RegExp(`export\\s+(?:async\\s+)?function\\s+${m}\\b`).test(content)
5946
6597
  );
@@ -5988,9 +6639,9 @@ function scanEnvVars(cwd) {
5988
6639
  const candidates = [".env.example", ".env.local.example", ".env.template"];
5989
6640
  for (const envFile of candidates) {
5990
6641
  const envPath = path24.join(cwd, envFile);
5991
- if (!fs24.existsSync(envPath)) continue;
6642
+ if (!fs25.existsSync(envPath)) continue;
5992
6643
  try {
5993
- const content = fs24.readFileSync(envPath, "utf-8");
6644
+ const content = fs25.readFileSync(envPath, "utf-8");
5994
6645
  const vars = [];
5995
6646
  for (const line of content.split("\n")) {
5996
6647
  const trimmed = line.trim();
@@ -6038,9 +6689,9 @@ function runQaDiscovery(cwd) {
6038
6689
  }
6039
6690
  function detectDevServer(cwd, out) {
6040
6691
  try {
6041
- const pkg = JSON.parse(fs25.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
6692
+ const pkg = JSON.parse(fs26.readFileSync(path25.join(cwd, "package.json"), "utf-8"));
6042
6693
  const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
6043
- const pm = fs25.existsSync(path25.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs25.existsSync(path25.join(cwd, "yarn.lock")) ? "yarn" : fs25.existsSync(path25.join(cwd, "bun.lockb")) ? "bun" : "npm";
6694
+ const pm = fs26.existsSync(path25.join(cwd, "pnpm-lock.yaml")) ? "pnpm" : fs26.existsSync(path25.join(cwd, "yarn.lock")) ? "yarn" : fs26.existsSync(path25.join(cwd, "bun.lockb")) ? "bun" : "npm";
6044
6695
  if (pkg.scripts?.dev) out.devCommand = `${pm} dev`;
6045
6696
  if (allDeps.next || allDeps.nuxt) out.devPort = 3e3;
6046
6697
  else if (allDeps.vite) out.devPort = 5173;
@@ -6051,7 +6702,7 @@ function scanFrontendRoutes(cwd, out) {
6051
6702
  const appDirs = ["src/app", "app"];
6052
6703
  for (const appDir of appDirs) {
6053
6704
  const full = path25.join(cwd, appDir);
6054
- if (!fs25.existsSync(full)) continue;
6705
+ if (!fs26.existsSync(full)) continue;
6055
6706
  walkFrontendRoutes(full, "", out);
6056
6707
  break;
6057
6708
  }
@@ -6059,7 +6710,7 @@ function scanFrontendRoutes(cwd, out) {
6059
6710
  function walkFrontendRoutes(dir, prefix, out) {
6060
6711
  let entries;
6061
6712
  try {
6062
- entries = fs25.readdirSync(dir, { withFileTypes: true });
6713
+ entries = fs26.readdirSync(dir, { withFileTypes: true });
6063
6714
  } catch {
6064
6715
  return;
6065
6716
  }
@@ -6101,23 +6752,23 @@ function detectAuthFiles(cwd, out) {
6101
6752
  "src/app/api/oauth"
6102
6753
  ];
6103
6754
  for (const c of candidates) {
6104
- if (fs25.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
6755
+ if (fs26.existsSync(path25.join(cwd, c))) out.authFiles.push(c);
6105
6756
  }
6106
6757
  }
6107
6758
  function detectRoles(cwd, out) {
6108
6759
  const rolePaths = ["src/types", "src/lib", "src/utils", "src/constants", "src/access", "src/collections"];
6109
6760
  for (const rp of rolePaths) {
6110
6761
  const dir = path25.join(cwd, rp);
6111
- if (!fs25.existsSync(dir)) continue;
6762
+ if (!fs26.existsSync(dir)) continue;
6112
6763
  let files;
6113
6764
  try {
6114
- files = fs25.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6765
+ files = fs26.readdirSync(dir).filter((f) => f.endsWith(".ts") || f.endsWith(".tsx"));
6115
6766
  } catch {
6116
6767
  continue;
6117
6768
  }
6118
6769
  for (const f of files) {
6119
6770
  try {
6120
- const content = fs25.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
6771
+ const content = fs26.readFileSync(path25.join(dir, f), "utf-8").slice(0, 5e3);
6121
6772
  const roleMatches = content.match(/(?:role|Role|ROLE)\s*[=:]\s*['"](\w+)['"]/g);
6122
6773
  if (roleMatches) {
6123
6774
  for (const m of roleMatches) {
@@ -6363,7 +7014,7 @@ function failedAction3(reason) {
6363
7014
  }
6364
7015
 
6365
7016
  // src/scripts/dispatchJobFileTicks.ts
6366
- import * as fs27 from "fs";
7017
+ import * as fs28 from "fs";
6367
7018
  import * as path27 from "path";
6368
7019
 
6369
7020
  // src/scripts/jobFrontmatter.ts
@@ -6625,7 +7276,7 @@ var ContentsApiBackend = class {
6625
7276
  };
6626
7277
 
6627
7278
  // src/scripts/jobState/localFileBackend.ts
6628
- import * as fs26 from "fs";
7279
+ import * as fs27 from "fs";
6629
7280
  import * as path26 from "path";
6630
7281
  var LocalFileBackend = class {
6631
7282
  name = "local-file";
@@ -6656,7 +7307,7 @@ var LocalFileBackend = class {
6656
7307
  `);
6657
7308
  return;
6658
7309
  }
6659
- fs26.mkdirSync(this.absDir, { recursive: true });
7310
+ fs27.mkdirSync(this.absDir, { recursive: true });
6660
7311
  const prefix = this.cacheKeyPrefix();
6661
7312
  const probeKey = `${prefix}probe-${Date.now()}`;
6662
7313
  try {
@@ -6685,7 +7336,7 @@ var LocalFileBackend = class {
6685
7336
  `);
6686
7337
  return;
6687
7338
  }
6688
- if (!fs26.existsSync(this.absDir)) {
7339
+ if (!fs27.existsSync(this.absDir)) {
6689
7340
  return;
6690
7341
  }
6691
7342
  const key = `${this.cacheKeyPrefix()}${process.env.GITHUB_RUN_ID ?? "norunid"}-${Date.now()}`;
@@ -6702,10 +7353,10 @@ var LocalFileBackend = class {
6702
7353
  load(slug) {
6703
7354
  const relPath = stateFilePath(this.jobsDir, slug);
6704
7355
  const absPath = path26.join(this.cwd, relPath);
6705
- if (!fs26.existsSync(absPath)) {
7356
+ if (!fs27.existsSync(absPath)) {
6706
7357
  return { path: relPath, handle: null, state: initialStateEnvelope("seed"), created: true };
6707
7358
  }
6708
- const raw = fs26.readFileSync(absPath, "utf-8");
7359
+ const raw = fs27.readFileSync(absPath, "utf-8");
6709
7360
  let parsed;
6710
7361
  try {
6711
7362
  parsed = JSON.parse(raw);
@@ -6723,9 +7374,9 @@ var LocalFileBackend = class {
6723
7374
  return false;
6724
7375
  }
6725
7376
  const absPath = path26.join(this.cwd, loaded.path);
6726
- fs26.mkdirSync(path26.dirname(absPath), { recursive: true });
7377
+ fs27.mkdirSync(path26.dirname(absPath), { recursive: true });
6727
7378
  const body = JSON.stringify(next, null, 2) + "\n";
6728
- fs26.writeFileSync(absPath, body, "utf-8");
7379
+ fs27.writeFileSync(absPath, body, "utf-8");
6729
7380
  return true;
6730
7381
  }
6731
7382
  cacheKeyPrefix() {
@@ -6916,17 +7567,17 @@ function formatAgo(ms) {
6916
7567
  }
6917
7568
  function readJobFrontmatter(cwd, jobsDir, slug) {
6918
7569
  try {
6919
- const raw = fs27.readFileSync(path27.join(cwd, jobsDir, `${slug}.md`), "utf-8");
7570
+ const raw = fs28.readFileSync(path27.join(cwd, jobsDir, `${slug}.md`), "utf-8");
6920
7571
  return splitFrontmatter2(raw).frontmatter;
6921
7572
  } catch {
6922
7573
  return {};
6923
7574
  }
6924
7575
  }
6925
7576
  function listJobSlugs(absDir) {
6926
- if (!fs27.existsSync(absDir)) return [];
7577
+ if (!fs28.existsSync(absDir)) return [];
6927
7578
  let entries;
6928
7579
  try {
6929
- entries = fs27.readdirSync(absDir, { withFileTypes: true });
7580
+ entries = fs28.readdirSync(absDir, { withFileTypes: true });
6930
7581
  } catch {
6931
7582
  return [];
6932
7583
  }
@@ -7679,7 +8330,7 @@ function ensureFeatureBranch(issueNumber, title, defaultBranch2, cwd, baseBranch
7679
8330
 
7680
8331
  // src/gha.ts
7681
8332
  import { execFileSync as execFileSync17 } from "child_process";
7682
- import * as fs28 from "fs";
8333
+ import * as fs29 from "fs";
7683
8334
  function getRunUrl() {
7684
8335
  const server = process.env.GITHUB_SERVER_URL;
7685
8336
  const repo = process.env.GITHUB_REPOSITORY;
@@ -7690,10 +8341,10 @@ function getRunUrl() {
7690
8341
  function reactToTriggerComment(cwd) {
7691
8342
  if (process.env.GITHUB_EVENT_NAME !== "issue_comment") return;
7692
8343
  const eventPath = process.env.GITHUB_EVENT_PATH;
7693
- if (!eventPath || !fs28.existsSync(eventPath)) return;
8344
+ if (!eventPath || !fs29.existsSync(eventPath)) return;
7694
8345
  let event = null;
7695
8346
  try {
7696
- event = JSON.parse(fs28.readFileSync(eventPath, "utf-8"));
8347
+ event = JSON.parse(fs29.readFileSync(eventPath, "utf-8"));
7697
8348
  } catch {
7698
8349
  return;
7699
8350
  }
@@ -7986,22 +8637,22 @@ var handleAbandonedGoal = async (ctx) => {
7986
8637
 
7987
8638
  // src/scripts/initFlow.ts
7988
8639
  import { execFileSync as execFileSync19 } from "child_process";
7989
- import * as fs30 from "fs";
8640
+ import * as fs31 from "fs";
7990
8641
  import * as path29 from "path";
7991
8642
 
7992
8643
  // src/scripts/loadQaGuide.ts
7993
- import * as fs29 from "fs";
8644
+ import * as fs30 from "fs";
7994
8645
  import * as path28 from "path";
7995
8646
  var QA_GUIDE_REL_PATH = ".kody/qa-guide.md";
7996
8647
  var loadQaGuide = async (ctx) => {
7997
8648
  const full = path28.join(ctx.cwd, QA_GUIDE_REL_PATH);
7998
- if (!fs29.existsSync(full)) {
8649
+ if (!fs30.existsSync(full)) {
7999
8650
  ctx.data.qaGuide = "";
8000
8651
  ctx.data.qaGuidePath = "";
8001
8652
  return;
8002
8653
  }
8003
8654
  try {
8004
- ctx.data.qaGuide = fs29.readFileSync(full, "utf-8");
8655
+ ctx.data.qaGuide = fs30.readFileSync(full, "utf-8");
8005
8656
  ctx.data.qaGuidePath = QA_GUIDE_REL_PATH;
8006
8657
  } catch {
8007
8658
  ctx.data.qaGuide = "";
@@ -8011,9 +8662,9 @@ var loadQaGuide = async (ctx) => {
8011
8662
 
8012
8663
  // src/scripts/initFlow.ts
8013
8664
  function detectPackageManager(cwd) {
8014
- if (fs30.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8015
- if (fs30.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
8016
- if (fs30.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
8665
+ if (fs31.existsSync(path29.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
8666
+ if (fs31.existsSync(path29.join(cwd, "yarn.lock"))) return "yarn";
8667
+ if (fs31.existsSync(path29.join(cwd, "bun.lockb"))) return "bun";
8017
8668
  return "npm";
8018
8669
  }
8019
8670
  function qualityCommandsFor(pm) {
@@ -8136,47 +8787,47 @@ function performInit(cwd, force) {
8136
8787
  const ownerRepo = detectOwnerRepo(cwd);
8137
8788
  const defaultBranch2 = defaultBranchFromGit(cwd);
8138
8789
  const configPath = path29.join(cwd, "kody.config.json");
8139
- if (fs30.existsSync(configPath) && !force) {
8790
+ if (fs31.existsSync(configPath) && !force) {
8140
8791
  skipped.push("kody.config.json");
8141
8792
  } else {
8142
8793
  const cfg = makeConfig(pm, ownerRepo, defaultBranch2);
8143
- fs30.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
8794
+ fs31.writeFileSync(configPath, `${JSON.stringify(cfg, null, 2)}
8144
8795
  `);
8145
8796
  wrote.push("kody.config.json");
8146
8797
  }
8147
8798
  const workflowDir = path29.join(cwd, ".github", "workflows");
8148
8799
  const workflowPath = path29.join(workflowDir, "kody.yml");
8149
- if (fs30.existsSync(workflowPath) && !force) {
8800
+ if (fs31.existsSync(workflowPath) && !force) {
8150
8801
  skipped.push(".github/workflows/kody.yml");
8151
8802
  } else {
8152
- fs30.mkdirSync(workflowDir, { recursive: true });
8153
- fs30.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
8803
+ fs31.mkdirSync(workflowDir, { recursive: true });
8804
+ fs31.writeFileSync(workflowPath, WORKFLOW_TEMPLATE);
8154
8805
  wrote.push(".github/workflows/kody.yml");
8155
8806
  }
8156
- const hasUi = fs30.existsSync(path29.join(cwd, "src/app")) || fs30.existsSync(path29.join(cwd, "app")) || fs30.existsSync(path29.join(cwd, "pages"));
8807
+ const hasUi = fs31.existsSync(path29.join(cwd, "src/app")) || fs31.existsSync(path29.join(cwd, "app")) || fs31.existsSync(path29.join(cwd, "pages"));
8157
8808
  if (hasUi) {
8158
8809
  const qaGuidePath = path29.join(cwd, QA_GUIDE_REL_PATH);
8159
- if (fs30.existsSync(qaGuidePath) && !force) {
8810
+ if (fs31.existsSync(qaGuidePath) && !force) {
8160
8811
  skipped.push(QA_GUIDE_REL_PATH);
8161
8812
  } else {
8162
- fs30.mkdirSync(path29.dirname(qaGuidePath), { recursive: true });
8813
+ fs31.mkdirSync(path29.dirname(qaGuidePath), { recursive: true });
8163
8814
  const discovery = runQaDiscovery(cwd);
8164
- fs30.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
8815
+ fs31.writeFileSync(qaGuidePath, generateQaGuideTemplate(discovery));
8165
8816
  wrote.push(QA_GUIDE_REL_PATH);
8166
8817
  }
8167
8818
  }
8168
8819
  const builtinJobs = listBuiltinJobs();
8169
8820
  if (builtinJobs.length > 0) {
8170
8821
  const jobsDir = path29.join(cwd, ".kody", "jobs");
8171
- fs30.mkdirSync(jobsDir, { recursive: true });
8822
+ fs31.mkdirSync(jobsDir, { recursive: true });
8172
8823
  for (const job of builtinJobs) {
8173
8824
  const rel = path29.join(".kody", "jobs", `${job.slug}.md`);
8174
8825
  const target = path29.join(cwd, rel);
8175
- if (fs30.existsSync(target) && !force) {
8826
+ if (fs31.existsSync(target) && !force) {
8176
8827
  skipped.push(rel);
8177
8828
  continue;
8178
8829
  }
8179
- fs30.writeFileSync(target, fs30.readFileSync(job.filePath, "utf-8"));
8830
+ fs31.writeFileSync(target, fs31.readFileSync(job.filePath, "utf-8"));
8180
8831
  wrote.push(rel);
8181
8832
  }
8182
8833
  }
@@ -8189,11 +8840,11 @@ function performInit(cwd, force) {
8189
8840
  }
8190
8841
  if (profile.kind !== "scheduled" || !profile.schedule) continue;
8191
8842
  const target = path29.join(workflowDir, `kody-${exe.name}.yml`);
8192
- if (fs30.existsSync(target) && !force) {
8843
+ if (fs31.existsSync(target) && !force) {
8193
8844
  skipped.push(`.github/workflows/kody-${exe.name}.yml`);
8194
8845
  continue;
8195
8846
  }
8196
- fs30.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
8847
+ fs31.writeFileSync(target, renderScheduledWorkflow(exe.name, profile.schedule));
8197
8848
  wrote.push(`.github/workflows/kody-${exe.name}.yml`);
8198
8849
  }
8199
8850
  let labels;
@@ -8271,7 +8922,7 @@ init_loadConventions();
8271
8922
  init_loadCoverageRules();
8272
8923
 
8273
8924
  // src/goal/state.ts
8274
- import * as fs31 from "fs";
8925
+ import * as fs32 from "fs";
8275
8926
  import * as path30 from "path";
8276
8927
  var VALID_STATES = /* @__PURE__ */ new Set(["active", "abandoned", "closed", "awaiting-merge", "done"]);
8277
8928
  var GoalStateError = class extends Error {
@@ -8330,12 +8981,12 @@ function goalStatePath(cwd, goalId) {
8330
8981
  }
8331
8982
  function readGoalState(cwd, goalId) {
8332
8983
  const file = goalStatePath(cwd, goalId);
8333
- if (!fs31.existsSync(file)) {
8984
+ if (!fs32.existsSync(file)) {
8334
8985
  throw new GoalStateError(file, "file not found");
8335
8986
  }
8336
8987
  let raw;
8337
8988
  try {
8338
- raw = JSON.parse(fs31.readFileSync(file, "utf-8"));
8989
+ raw = JSON.parse(fs32.readFileSync(file, "utf-8"));
8339
8990
  } catch (err) {
8340
8991
  throw new GoalStateError(file, `invalid JSON: ${err instanceof Error ? err.message : String(err)}`);
8341
8992
  }
@@ -8343,8 +8994,8 @@ function readGoalState(cwd, goalId) {
8343
8994
  }
8344
8995
  function writeGoalState(cwd, goalId, state) {
8345
8996
  const file = goalStatePath(cwd, goalId);
8346
- fs31.mkdirSync(path30.dirname(file), { recursive: true });
8347
- fs31.writeFileSync(file, serializeGoalState(state), "utf-8");
8997
+ fs32.mkdirSync(path30.dirname(file), { recursive: true });
8998
+ fs32.writeFileSync(file, serializeGoalState(state), "utf-8");
8348
8999
  }
8349
9000
  function nowIso() {
8350
9001
  return (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
@@ -8441,7 +9092,7 @@ var loadIssueStateComment = async (ctx, _profile, args) => {
8441
9092
  };
8442
9093
 
8443
9094
  // src/scripts/loadJobFromFile.ts
8444
- import * as fs32 from "fs";
9095
+ import * as fs33 from "fs";
8445
9096
  import * as path31 from "path";
8446
9097
  var loadJobFromFile = async (ctx, _profile, args) => {
8447
9098
  const jobsDir = String(args?.jobsDir ?? ".kody/jobs");
@@ -8452,22 +9103,22 @@ var loadJobFromFile = async (ctx, _profile, args) => {
8452
9103
  throw new Error(`loadJobFromFile: ctx.args.${slugArg} must be a non-empty slug`);
8453
9104
  }
8454
9105
  const absPath = path31.join(ctx.cwd, jobsDir, `${slug}.md`);
8455
- if (!fs32.existsSync(absPath)) {
9106
+ if (!fs33.existsSync(absPath)) {
8456
9107
  throw new Error(`loadJobFromFile: job file not found: ${absPath}`);
8457
9108
  }
8458
- const raw = fs32.readFileSync(absPath, "utf-8");
9109
+ const raw = fs33.readFileSync(absPath, "utf-8");
8459
9110
  const { title, body } = parseJobFile(raw, slug);
8460
9111
  const workerSlug = (splitFrontmatter2(raw).frontmatter.worker ?? "").trim();
8461
9112
  let workerTitle = "";
8462
9113
  let workerPersona = "";
8463
9114
  if (workerSlug) {
8464
9115
  const workerPath = path31.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8465
- if (!fs32.existsSync(workerPath)) {
9116
+ if (!fs33.existsSync(workerPath)) {
8466
9117
  throw new Error(
8467
9118
  `loadJobFromFile: job '${slug}' declares worker '${workerSlug}' but ${workerPath} does not exist`
8468
9119
  );
8469
9120
  }
8470
- const workerRaw = fs32.readFileSync(workerPath, "utf-8");
9121
+ const workerRaw = fs33.readFileSync(workerPath, "utf-8");
8471
9122
  const parsed = parseJobFile(workerRaw, workerSlug);
8472
9123
  workerTitle = parsed.title;
8473
9124
  workerPersona = parsed.body;
@@ -8505,7 +9156,7 @@ function humanizeSlug(slug) {
8505
9156
  }
8506
9157
 
8507
9158
  // src/scripts/loadWorkerAdhoc.ts
8508
- import * as fs33 from "fs";
9159
+ import * as fs34 from "fs";
8509
9160
  import * as path32 from "path";
8510
9161
  var loadWorkerAdhoc = async (ctx, _profile, args) => {
8511
9162
  const workersDir = String(args?.workersDir ?? ".kody/workers");
@@ -8514,10 +9165,10 @@ var loadWorkerAdhoc = async (ctx, _profile, args) => {
8514
9165
  throw new Error("loadWorkerAdhoc: ctx.args.worker must be a non-empty slug");
8515
9166
  }
8516
9167
  const workerPath = path32.join(ctx.cwd, workersDir, `${workerSlug}.md`);
8517
- if (!fs33.existsSync(workerPath)) {
9168
+ if (!fs34.existsSync(workerPath)) {
8518
9169
  throw new Error(`loadWorkerAdhoc: worker persona not found: ${workerPath}`);
8519
9170
  }
8520
- const { title, body } = parsePersona(fs33.readFileSync(workerPath, "utf-8"), workerSlug);
9171
+ const { title, body } = parsePersona(fs34.readFileSync(workerPath, "utf-8"), workerSlug);
8521
9172
  const message = resolveMessage(ctx.args.message);
8522
9173
  if (!message) {
8523
9174
  throw new Error(
@@ -8537,9 +9188,9 @@ function resolveMessage(messageArg) {
8537
9188
  }
8538
9189
  function readCommentBody() {
8539
9190
  const eventPath = process.env.GITHUB_EVENT_PATH;
8540
- if (!eventPath || !fs33.existsSync(eventPath)) return "";
9191
+ if (!eventPath || !fs34.existsSync(eventPath)) return "";
8541
9192
  try {
8542
- const event = JSON.parse(fs33.readFileSync(eventPath, "utf-8"));
9193
+ const event = JSON.parse(fs34.readFileSync(eventPath, "utf-8"));
8543
9194
  return String(event.comment?.body ?? "");
8544
9195
  } catch {
8545
9196
  return "";
@@ -8585,7 +9236,7 @@ init_loadPriorArt();
8585
9236
  init_events();
8586
9237
 
8587
9238
  // src/taskContext.ts
8588
- import * as fs35 from "fs";
9239
+ import * as fs36 from "fs";
8589
9240
  import * as path34 from "path";
8590
9241
  var TASK_CONTEXT_SCHEMA_VERSION = 1;
8591
9242
  function buildTaskContext(args) {
@@ -8603,9 +9254,9 @@ function buildTaskContext(args) {
8603
9254
  function persistTaskContext(cwd, ctx) {
8604
9255
  try {
8605
9256
  const dir = path34.join(cwd, ".kody", "runs", ctx.runId);
8606
- fs35.mkdirSync(dir, { recursive: true });
9257
+ fs36.mkdirSync(dir, { recursive: true });
8607
9258
  const file = path34.join(dir, "task-context.json");
8608
- fs35.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
9259
+ fs36.writeFileSync(file, `${JSON.stringify(ctx, null, 2)}
8609
9260
  `);
8610
9261
  return file;
8611
9262
  } catch (err) {
@@ -9968,7 +10619,7 @@ function resolveBaseOverride(value) {
9968
10619
 
9969
10620
  // src/scripts/runTickScript.ts
9970
10621
  import { spawnSync } from "child_process";
9971
- import * as fs36 from "fs";
10622
+ import * as fs37 from "fs";
9972
10623
  import * as path35 from "path";
9973
10624
  var runTickScript = async (ctx, _profile, args) => {
9974
10625
  ctx.skipAgent = true;
@@ -9982,12 +10633,12 @@ var runTickScript = async (ctx, _profile, args) => {
9982
10633
  return;
9983
10634
  }
9984
10635
  const jobPath = path35.join(ctx.cwd, jobsDir, `${slug}.md`);
9985
- if (!fs36.existsSync(jobPath)) {
10636
+ if (!fs37.existsSync(jobPath)) {
9986
10637
  ctx.output.exitCode = 99;
9987
10638
  ctx.output.reason = `runTickScript: job file not found: ${jobPath}`;
9988
10639
  return;
9989
10640
  }
9990
- const raw = fs36.readFileSync(jobPath, "utf-8");
10641
+ const raw = fs37.readFileSync(jobPath, "utf-8");
9991
10642
  const { frontmatter } = splitFrontmatter2(raw);
9992
10643
  const tickScript = frontmatter.tickScript;
9993
10644
  if (!tickScript) {
@@ -9996,7 +10647,7 @@ var runTickScript = async (ctx, _profile, args) => {
9996
10647
  return;
9997
10648
  }
9998
10649
  const scriptPath = path35.isAbsolute(tickScript) ? tickScript : path35.join(ctx.cwd, tickScript);
9999
- if (!fs36.existsSync(scriptPath)) {
10650
+ if (!fs37.existsSync(scriptPath)) {
10000
10651
  ctx.output.exitCode = 99;
10001
10652
  ctx.output.reason = `runTickScript: tickScript not found: ${scriptPath}`;
10002
10653
  return;
@@ -10144,7 +10795,7 @@ function synthesizeAction(ctx) {
10144
10795
  }
10145
10796
 
10146
10797
  // src/scripts/serveFlow.ts
10147
- import { spawn as spawn3 } from "child_process";
10798
+ import { spawn as spawn5 } from "child_process";
10148
10799
  function parseTarget(positional) {
10149
10800
  if (!Array.isArray(positional) || positional.length === 0) return "none";
10150
10801
  const first = String(positional[0]).toLowerCase();
@@ -10193,7 +10844,7 @@ var serveFlow = async (ctx) => {
10193
10844
  if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
10194
10845
  `);
10195
10846
  const args = ["--dangerously-skip-permissions", "--model", model.model];
10196
- const child = spawn3("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
10847
+ const child = spawn5("claude", args, { stdio: "inherit", env: editorEnv, cwd: ctx.cwd });
10197
10848
  const exitCode = await new Promise((resolve4) => {
10198
10849
  child.on("exit", (code) => resolve4(code ?? 0));
10199
10850
  child.on("error", (err) => {
@@ -10214,7 +10865,7 @@ var serveFlow = async (ctx) => {
10214
10865
  if (usesProxy) process.stdout.write(` ANTHROPIC_BASE_URL=${url}
10215
10866
  `);
10216
10867
  try {
10217
- const code = spawn3("code", [ctx.cwd], { stdio: "inherit", env: editorEnv, detached: true });
10868
+ const code = spawn5("code", [ctx.cwd], { stdio: "inherit", env: editorEnv, detached: true });
10218
10869
  code.on("error", (err) => {
10219
10870
  process.stderr.write(`[kody serve] failed to launch VS Code: ${err.message}
10220
10871
  `);
@@ -10469,7 +11120,7 @@ var verify = async (ctx) => {
10469
11120
  };
10470
11121
 
10471
11122
  // src/scripts/verifyReproFails.ts
10472
- import { spawn as spawn4 } from "child_process";
11123
+ import { spawn as spawn6 } from "child_process";
10473
11124
  var TEST_TIMEOUT_MS = 10 * 60 * 1e3;
10474
11125
  var TAIL_CHARS2 = 8e3;
10475
11126
  var ANSI_RE2 = /\x1B\[[0-?]*[ -/]*[@-~]/g;
@@ -10538,7 +11189,7 @@ function stripAnsi2(s) {
10538
11189
  }
10539
11190
  function runCommand2(command, cwd) {
10540
11191
  return new Promise((resolve4) => {
10541
- const child = spawn4(command, {
11192
+ const child = spawn6(command, {
10542
11193
  cwd,
10543
11194
  shell: true,
10544
11195
  env: { ...process.env, HUSKY: "0", SKIP_HOOKS: "1", CI: process.env.CI ?? "1" },
@@ -10691,17 +11342,17 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
10691
11342
  return;
10692
11343
  }
10693
11344
  const fixCiAttempts = state?.core.attempts?.["fix-ci"] ?? 0;
10694
- await sleep2(initialWaitSeconds * 1e3);
11345
+ await sleep3(initialWaitSeconds * 1e3);
10695
11346
  const deadline = Date.now() + timeoutMinutes * 6e4;
10696
11347
  let lastSummary = "";
10697
11348
  while (Date.now() < deadline) {
10698
11349
  const rows = fetchChecks(prNumber, ctx.cwd);
10699
11350
  if (rows === null) {
10700
- await sleep2(pollSeconds * 1e3);
11351
+ await sleep3(pollSeconds * 1e3);
10701
11352
  continue;
10702
11353
  }
10703
11354
  if (rows.length === 0) {
10704
- await sleep2(pollSeconds * 1e3);
11355
+ await sleep3(pollSeconds * 1e3);
10705
11356
  continue;
10706
11357
  }
10707
11358
  const summary = summarize(rows);
@@ -10744,7 +11395,7 @@ var waitForCi = async (ctx, _profile, _agentResult, args) => {
10744
11395
  tryPostPr7(prNumber, `\u2705 kody waitForCi: all ${rows.length} checks green on PR #${prNumber}`, ctx.cwd);
10745
11396
  return;
10746
11397
  }
10747
- await sleep2(pollSeconds * 1e3);
11398
+ await sleep3(pollSeconds * 1e3);
10748
11399
  }
10749
11400
  ctx.data.action = mkAction("CI_TIMEOUT", {
10750
11401
  reason: `CI did not complete within ${timeoutMinutes} minutes`,
@@ -10796,12 +11447,12 @@ function tryPostPr7(prNumber, body, cwd) {
10796
11447
  } catch {
10797
11448
  }
10798
11449
  }
10799
- function sleep2(ms) {
11450
+ function sleep3(ms) {
10800
11451
  return new Promise((res) => setTimeout(res, ms));
10801
11452
  }
10802
11453
 
10803
11454
  // src/scripts/warmupMcp.ts
10804
- import { spawn as spawn5 } from "child_process";
11455
+ import { spawn as spawn7 } from "child_process";
10805
11456
  var PER_SERVER_TIMEOUT_MS = 6e4;
10806
11457
  var PER_REQUEST_TIMEOUT_MS = 2e4;
10807
11458
  var warmupMcp = async (_ctx, profile) => {
@@ -10823,7 +11474,7 @@ var warmupMcp = async (_ctx, profile) => {
10823
11474
  }
10824
11475
  };
10825
11476
  async function warmupOne(command, args, env) {
10826
- const child = spawn5(command, args, {
11477
+ const child = spawn7(command, args, {
10827
11478
  stdio: ["pipe", "pipe", "pipe"],
10828
11479
  env: env ? { ...process.env, ...env } : process.env
10829
11480
  });
@@ -11012,7 +11663,7 @@ var writeJobStateFile = async (ctx, _profile, _agentResult, args) => {
11012
11663
  };
11013
11664
 
11014
11665
  // src/scripts/writeRunSummary.ts
11015
- import * as fs37 from "fs";
11666
+ import * as fs38 from "fs";
11016
11667
  var writeRunSummary = async (ctx, profile) => {
11017
11668
  const summaryPath = process.env.GITHUB_STEP_SUMMARY;
11018
11669
  if (!summaryPath) return;
@@ -11034,7 +11685,7 @@ var writeRunSummary = async (ctx, profile) => {
11034
11685
  if (reason) lines.push(`- **Reason:** ${reason}`);
11035
11686
  lines.push("");
11036
11687
  try {
11037
- fs37.appendFileSync(summaryPath, `${lines.join("\n")}
11688
+ fs38.appendFileSync(summaryPath, `${lines.join("\n")}
11038
11689
  `);
11039
11690
  } catch {
11040
11691
  }
@@ -11078,6 +11729,8 @@ var preflightScripts = {
11078
11729
  runTickScript,
11079
11730
  serveFlow,
11080
11731
  brainServe,
11732
+ runnerServe,
11733
+ poolServe,
11081
11734
  loadGoalState,
11082
11735
  handleAbandonedGoal,
11083
11736
  deriveGoalPhase,
@@ -11487,7 +12140,7 @@ function clearStampedLifecycleLabels(profile, ctx) {
11487
12140
  function getProfileInputsForChild(profileName, _cwd) {
11488
12141
  try {
11489
12142
  const profilePath = resolveProfilePath(profileName);
11490
- if (!fs38.existsSync(profilePath)) return null;
12143
+ if (!fs39.existsSync(profilePath)) return null;
11491
12144
  return loadProfile(profilePath).inputs;
11492
12145
  } catch {
11493
12146
  return null;
@@ -11506,7 +12159,7 @@ function resolveProfilePath(profileName) {
11506
12159
  // fallback
11507
12160
  ];
11508
12161
  for (const c of candidates) {
11509
- if (fs38.existsSync(c)) return c;
12162
+ if (fs39.existsSync(c)) return c;
11510
12163
  }
11511
12164
  return candidates[0];
11512
12165
  }
@@ -11607,7 +12260,7 @@ var SIGKILL_GRACE_MS = 5e3;
11607
12260
  async function runShellEntry(entry, ctx, profile) {
11608
12261
  const shellName = entry.shell;
11609
12262
  const shellPath = path36.join(profile.dir, shellName);
11610
- if (!fs38.existsSync(shellPath)) {
12263
+ if (!fs39.existsSync(shellPath)) {
11611
12264
  ctx.skipAgent = true;
11612
12265
  ctx.output.exitCode = 99;
11613
12266
  ctx.output.reason = `shell script not found: ${shellName} (looked in ${profile.dir})`;
@@ -11623,7 +12276,7 @@ async function runShellEntry(entry, ctx, profile) {
11623
12276
  env[`KODY_CFG_${k}`] = v;
11624
12277
  }
11625
12278
  const timeoutMs = resolveShellTimeoutMs(entry);
11626
- const child = spawn6("bash", [shellPath, ...positional], {
12279
+ const child = spawn8("bash", [shellPath, ...positional], {
11627
12280
  cwd: ctx.cwd,
11628
12281
  env,
11629
12282
  stdio: ["pipe", "pipe", "pipe"],
@@ -12086,9 +12739,9 @@ function resolveAuthToken(env = process.env) {
12086
12739
  return token;
12087
12740
  }
12088
12741
  function detectPackageManager2(cwd) {
12089
- if (fs39.existsSync(path37.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
12090
- if (fs39.existsSync(path37.join(cwd, "yarn.lock"))) return "yarn";
12091
- if (fs39.existsSync(path37.join(cwd, "bun.lockb"))) return "bun";
12742
+ if (fs40.existsSync(path37.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
12743
+ if (fs40.existsSync(path37.join(cwd, "yarn.lock"))) return "yarn";
12744
+ if (fs40.existsSync(path37.join(cwd, "bun.lockb"))) return "bun";
12092
12745
  return "npm";
12093
12746
  }
12094
12747
  function shellOut(cmd, args, cwd, stream = true) {
@@ -12178,8 +12831,8 @@ function postFailureTail(issueNumber, cwd, reason) {
12178
12831
  const logPath = path37.join(cwd, ".kody", "last-run.jsonl");
12179
12832
  let tail = "";
12180
12833
  try {
12181
- if (fs39.existsSync(logPath)) {
12182
- const content = fs39.readFileSync(logPath, "utf-8");
12834
+ if (fs40.existsSync(logPath)) {
12835
+ const content = fs40.readFileSync(logPath, "utf-8");
12183
12836
  tail = content.slice(-3e3);
12184
12837
  }
12185
12838
  } catch {
@@ -12214,9 +12867,9 @@ async function runCi(argv) {
12214
12867
  const eventName = process.env.GITHUB_EVENT_NAME;
12215
12868
  const dispatchEventPath = process.env.GITHUB_EVENT_PATH;
12216
12869
  let manualWorkflowDispatch = false;
12217
- if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs39.existsSync(dispatchEventPath)) {
12870
+ if (!args.issueNumber && !autoFallback && eventName === "workflow_dispatch" && dispatchEventPath && fs40.existsSync(dispatchEventPath)) {
12218
12871
  try {
12219
- const evt = JSON.parse(fs39.readFileSync(dispatchEventPath, "utf-8"));
12872
+ const evt = JSON.parse(fs40.readFileSync(dispatchEventPath, "utf-8"));
12220
12873
  const issueInput = parseInt(String(evt?.inputs?.issue_number ?? ""), 10);
12221
12874
  const sessionInput = String(evt?.inputs?.sessionId ?? "");
12222
12875
  manualWorkflowDispatch = !sessionInput && !(Number.isFinite(issueInput) && issueInput > 0);
@@ -12480,7 +13133,7 @@ function commitChatFiles(cwd, sessionId, verbose) {
12480
13133
  const safeSession = sessionId.replace(/[^a-zA-Z0-9._-]/g, "_");
12481
13134
  const tasksDir = path38.join(".kody", "tasks", safeSession);
12482
13135
  const candidatePaths = [sessionFile, eventsFile, tasksDir];
12483
- const paths = candidatePaths.filter((p) => fs40.existsSync(path38.join(cwd, p)));
13136
+ const paths = candidatePaths.filter((p) => fs41.existsSync(path38.join(cwd, p)));
12484
13137
  if (paths.length === 0) return;
12485
13138
  const opts = { cwd, stdio: verbose ? "inherit" : "pipe" };
12486
13139
  try {
@@ -12576,7 +13229,7 @@ ${CHAT_HELP}`);
12576
13229
  const sink = buildSink(cwd, sessionId, args.dashboardUrl);
12577
13230
  const meta = readMeta(sessionFile);
12578
13231
  process.stdout.write(
12579
- `\u2192 kody:chat: session file=${sessionFile} exists=${fs40.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
13232
+ `\u2192 kody:chat: session file=${sessionFile} exists=${fs41.existsSync(sessionFile)} meta=${meta ? meta.mode : "none"}
12580
13233
  `
12581
13234
  );
12582
13235
  try {