@kody-ade/kody-engine 0.4.115 → 0.4.117

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.
Files changed (2) hide show
  1. package/dist/bin/kody.js +278 -60
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -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.115",
883
+ version: "0.4.117",
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",
@@ -4785,6 +4785,7 @@ import { createServer as createServer3 } from "http";
4785
4785
  var FLY_API_BASE = "https://api.machines.dev/v1";
4786
4786
  var POOL_METADATA_KEY = "kody_pool";
4787
4787
  var POOL_METADATA_VALUE = "1";
4788
+ var POOL_REPO_METADATA_KEY = "kody_pool_repo";
4788
4789
  var FlyClient = class {
4789
4790
  constructor(opts) {
4790
4791
  this.opts = opts;
@@ -4814,7 +4815,7 @@ var FlyClient = class {
4814
4815
  if (!raw.trim()) return null;
4815
4816
  return JSON.parse(raw);
4816
4817
  }
4817
- /** Create + start a pooled machine in serve mode. */
4818
+ /** Create + start a pooled machine in serve mode, tagged for `repoTag`. */
4818
4819
  async createPooled(input) {
4819
4820
  const body = {
4820
4821
  region: input.region,
@@ -4824,7 +4825,10 @@ var FlyClient = class {
4824
4825
  auto_destroy: true,
4825
4826
  restart: { policy: "no" },
4826
4827
  init: { entrypoint: ["/usr/local/bin/entrypoint-serve.sh"] },
4827
- metadata: { [POOL_METADATA_KEY]: POOL_METADATA_VALUE },
4828
+ metadata: {
4829
+ [POOL_METADATA_KEY]: POOL_METADATA_VALUE,
4830
+ [POOL_REPO_METADATA_KEY]: input.repoTag
4831
+ },
4828
4832
  env: {
4829
4833
  RUNNER_API_KEY: input.runnerApiKey,
4830
4834
  KODY_LITELLM_URL: input.litellmUrl,
@@ -4839,11 +4843,14 @@ var FlyClient = class {
4839
4843
  async get(id) {
4840
4844
  return this.call(`/apps/${enc(this.opts.app)}/machines/${enc(id)}`, { allow404: true });
4841
4845
  }
4842
- /** List the app's pooled (kody_pool) machines, excluding destroyed/destroying. */
4843
- async listPooled() {
4846
+ /**
4847
+ * List pooled machines for `repoTag` (kody_pool + matching repo tag),
4848
+ * excluding destroyed/destroying. Each repo's pool sees only its own.
4849
+ */
4850
+ async listPooled(repoTag) {
4844
4851
  const all = await this.call(`/apps/${enc(this.opts.app)}/machines`, { allow404: true }) ?? [];
4845
4852
  return all.filter(
4846
- (m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.state !== "destroyed" && m.state !== "destroying"
4853
+ (m) => m.config?.metadata?.[POOL_METADATA_KEY] === POOL_METADATA_VALUE && m.config?.metadata?.[POOL_REPO_METADATA_KEY] === repoTag && m.state !== "destroyed" && m.state !== "destroying"
4847
4854
  );
4848
4855
  }
4849
4856
  /** Suspend (freeze) a machine — wakes in ~1s from the snapshot. */
@@ -4884,6 +4891,7 @@ function sleep2(ms) {
4884
4891
  }
4885
4892
 
4886
4893
  // src/pool/manager.ts
4894
+ var MAX_CLAIM_ATTEMPTS = 3;
4887
4895
  var PoolManager = class {
4888
4896
  constructor(deps) {
4889
4897
  this.deps = deps;
@@ -4912,7 +4920,7 @@ var PoolManager = class {
4912
4920
  * free; anything else is left to finish/auto-destroy. Then refill to `min`.
4913
4921
  */
4914
4922
  async reconcile() {
4915
- const machines = await this.deps.fly.listPooled();
4923
+ const machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
4916
4924
  this.free = [];
4917
4925
  for (const m of machines) {
4918
4926
  if ((m.state === "suspended" || m.state === "suspending") && m.private_ip) {
@@ -4923,43 +4931,84 @@ var PoolManager = class {
4923
4931
  await this.refill();
4924
4932
  }
4925
4933
  /**
4926
- * Claim a warm machine for a job. Returns ok:false (caller falls back to
4927
- * create-fresh) when the pool is empty or the woken machine fails to take
4928
- * the job. The pick is synchronous the atomic step.
4934
+ * Claim a warm machine for a job. Tries free machines in turn: if a woken
4935
+ * machine is stale/unhealthy/rejecting (e.g. it vanished out-of-band), it's
4936
+ * destroyed and the next free one is tried, up to MAX_CLAIM_ATTEMPTS. Only
4937
+ * when none work (or the pool is empty) does it return ok:false so the
4938
+ * caller falls back to create-fresh. The pick (shift) is synchronous — the
4939
+ * atomic step that prevents two concurrent claims grabbing the same machine.
4929
4940
  */
4930
4941
  async claim(job) {
4931
- const machine = this.free.shift();
4932
- if (!machine) {
4933
- this.log("claim: pool empty");
4934
- void this.refill();
4935
- return { ok: false, reason: "pool empty" };
4936
- }
4937
- this.claimsInFlight++;
4938
- try {
4939
- await this.deps.fly.start(machine.id);
4940
- const base = this.baseUrl(machine);
4941
- const healthy = await this.deps.fly.waitHealthy(base, { timeoutMs: this.deps.config.healthTimeoutMs });
4942
- if (!healthy) {
4943
- this.log(`claim: machine ${machine.id} unhealthy after wake \u2014 destroying`);
4944
- await this.safeDestroy(machine.id);
4945
- return { ok: false, reason: "woken machine unhealthy" };
4946
- }
4947
- const accepted = await this.postRun(machine, job, this.deps.config);
4948
- if (!accepted) {
4949
- this.log(`claim: machine ${machine.id} rejected job \u2014 destroying`);
4942
+ let lastReason = "pool empty";
4943
+ for (let attempt = 0; attempt < MAX_CLAIM_ATTEMPTS; attempt++) {
4944
+ const machine = this.free.shift();
4945
+ if (!machine) break;
4946
+ this.claimsInFlight++;
4947
+ try {
4948
+ await this.deps.fly.start(machine.id);
4949
+ const healthy = await this.deps.fly.waitHealthy(this.baseUrl(machine), {
4950
+ timeoutMs: this.deps.config.healthTimeoutMs
4951
+ });
4952
+ if (!healthy) {
4953
+ this.log(`claim: machine ${machine.id} unhealthy after wake \u2014 destroying, trying next`);
4954
+ await this.safeDestroy(machine.id);
4955
+ lastReason = "woken machine unhealthy";
4956
+ continue;
4957
+ }
4958
+ const accepted = await this.postRun(machine, job, this.deps.config);
4959
+ if (!accepted) {
4960
+ this.log(`claim: machine ${machine.id} rejected job \u2014 destroying, trying next`);
4961
+ await this.safeDestroy(machine.id);
4962
+ lastReason = "machine rejected job";
4963
+ continue;
4964
+ }
4965
+ this.log(`claim: machine ${machine.id} took job ${job.jobId}`);
4966
+ void this.refill();
4967
+ return { ok: true, machineId: machine.id };
4968
+ } catch (err) {
4969
+ this.log(`claim: error on ${machine.id}: ${errMsg2(err)} \u2014 destroying, trying next`);
4950
4970
  await this.safeDestroy(machine.id);
4951
- return { ok: false, reason: "machine rejected job" };
4971
+ lastReason = errMsg2(err);
4972
+ } finally {
4973
+ this.claimsInFlight--;
4952
4974
  }
4953
- this.log(`claim: machine ${machine.id} took job ${job.jobId}`);
4954
- return { ok: true, machineId: machine.id };
4975
+ }
4976
+ void this.refill();
4977
+ return { ok: false, reason: lastReason };
4978
+ }
4979
+ /**
4980
+ * Periodic self-heal: reconcile the in-memory free list against actual Fly
4981
+ * state. Prunes free entries whose machine vanished out-of-band (auto-destroy
4982
+ * after a job, manual ops) so a later claim never tries a dead machine, and
4983
+ * adopts any suspended machines we lost track of. Then tops up. Unlike
4984
+ * reconcile() this MERGES rather than rebuilds, so it won't drop a machine
4985
+ * that's momentarily not yet reflected as suspended by Fly's eventual
4986
+ * consistency.
4987
+ */
4988
+ async resync() {
4989
+ let machines;
4990
+ try {
4991
+ machines = await this.deps.fly.listPooled(this.deps.config.repoTag);
4955
4992
  } catch (err) {
4956
- this.log(`claim: error on ${machine.id}: ${errMsg2(err)} \u2014 destroying`);
4957
- await this.safeDestroy(machine.id);
4958
- return { ok: false, reason: errMsg2(err) };
4959
- } finally {
4960
- this.claimsInFlight--;
4961
- void this.refill();
4993
+ this.log(`resync: listPooled failed: ${errMsg2(err)}`);
4994
+ return;
4962
4995
  }
4996
+ const liveIds = new Set(machines.map((m) => m.id));
4997
+ const before = this.free.length;
4998
+ this.free = this.free.filter((f) => liveIds.has(f.id));
4999
+ const pruned = before - this.free.length;
5000
+ const tracked = new Set(this.free.map((f) => f.id));
5001
+ let adopted = 0;
5002
+ for (const m of machines) {
5003
+ if ((m.state === "suspended" || m.state === "suspending") && m.private_ip && !tracked.has(m.id)) {
5004
+ this.free.push({ id: m.id, privateIp: m.private_ip });
5005
+ adopted++;
5006
+ }
5007
+ }
5008
+ if (pruned > 0 || adopted > 0) {
5009
+ this.log(`resync: pruned ${pruned} stale, adopted ${adopted} (free=${this.free.length})`);
5010
+ }
5011
+ await this.refill();
4963
5012
  }
4964
5013
  /** Top up free machines to `min`. Serialized so it never overshoots. */
4965
5014
  async refill() {
@@ -4989,6 +5038,7 @@ var PoolManager = class {
4989
5038
  guest: cfg.guest,
4990
5039
  runnerApiKey: cfg.runnerApiKey,
4991
5040
  litellmUrl: cfg.litellmUrl,
5041
+ repoTag: cfg.repoTag,
4992
5042
  port: cfg.port
4993
5043
  });
4994
5044
  if (!m.private_ip) {
@@ -5036,6 +5086,160 @@ function errMsg2(err) {
5036
5086
  return err instanceof Error ? err.message : String(err);
5037
5087
  }
5038
5088
 
5089
+ // src/pool/vault.ts
5090
+ import { createDecipheriv } from "crypto";
5091
+ var GITHUB_API = "https://api.github.com";
5092
+ var VAULT_PATH = ".kody/secrets.enc";
5093
+ var CACHE_TTL_MS = 6e4;
5094
+ var cache = /* @__PURE__ */ new Map();
5095
+ function decryptVault(payload, masterKey) {
5096
+ const parts = payload.split(":");
5097
+ if (parts.length !== 4 || parts[0] !== "v1") {
5098
+ throw new Error("invalid vault payload format");
5099
+ }
5100
+ const [, ivB64, ctB64, tagB64] = parts;
5101
+ const iv = Buffer.from(ivB64, "base64");
5102
+ const ct = Buffer.from(ctB64, "base64");
5103
+ const tag = Buffer.from(tagB64, "base64");
5104
+ const decipher = createDecipheriv("aes-256-gcm", masterKey, iv);
5105
+ decipher.setAuthTag(tag);
5106
+ return Buffer.concat([decipher.update(ct), decipher.final()]).toString("utf8");
5107
+ }
5108
+ async function readVaultSecrets(opts) {
5109
+ const key = `${opts.owner}/${opts.repo}`.toLowerCase();
5110
+ const hit = cache.get(key);
5111
+ if (hit && hit.expiresAt > Date.now()) return hit.secrets;
5112
+ const doFetch = opts.fetchImpl ?? fetch;
5113
+ const res = await doFetch(
5114
+ `${GITHUB_API}/repos/${encodeURIComponent(opts.owner)}/${encodeURIComponent(opts.repo)}/contents/${VAULT_PATH}`,
5115
+ {
5116
+ headers: {
5117
+ Authorization: `Bearer ${opts.githubToken}`,
5118
+ Accept: "application/vnd.github+json",
5119
+ "User-Agent": "kody-pool-serve"
5120
+ }
5121
+ }
5122
+ );
5123
+ if (res.status === 404) {
5124
+ cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
5125
+ return {};
5126
+ }
5127
+ if (!res.ok) {
5128
+ throw new Error(`vault read ${res.status} for ${key}: ${(await res.text().catch(() => "")).slice(0, 160)}`);
5129
+ }
5130
+ const body = await res.json();
5131
+ if (!body.content) {
5132
+ cache.set(key, { secrets: {}, expiresAt: Date.now() + CACHE_TTL_MS });
5133
+ return {};
5134
+ }
5135
+ const ciphertext = Buffer.from(body.content, body.encoding ?? "base64").toString("utf8").trim();
5136
+ const doc = JSON.parse(decryptVault(ciphertext, opts.masterKey));
5137
+ const flat = {};
5138
+ for (const [name, entry] of Object.entries(doc.secrets ?? {})) {
5139
+ if (entry && typeof entry.value === "string") flat[name] = entry.value;
5140
+ }
5141
+ cache.set(key, { secrets: flat, expiresAt: Date.now() + CACHE_TTL_MS });
5142
+ return flat;
5143
+ }
5144
+ async function readRepoSecret(opts) {
5145
+ const secrets = await readVaultSecrets(opts);
5146
+ const v = secrets[opts.name];
5147
+ return v && v.trim() ? v : null;
5148
+ }
5149
+ async function readRepoSecrets(opts) {
5150
+ return readVaultSecrets(opts);
5151
+ }
5152
+
5153
+ // src/pool/registry.ts
5154
+ var PoolRegistry = class {
5155
+ constructor(cfg) {
5156
+ this.cfg = cfg;
5157
+ this.log = cfg.log ?? (() => {
5158
+ });
5159
+ this.resolveFlyToken = cfg.resolveFlyToken ?? ((owner, repo) => readRepoSecret({
5160
+ githubToken: cfg.githubToken,
5161
+ masterKey: cfg.masterKey,
5162
+ owner,
5163
+ repo,
5164
+ name: "FLY_API_TOKEN"
5165
+ }));
5166
+ }
5167
+ cfg;
5168
+ pools = /* @__PURE__ */ new Map();
5169
+ resolveFlyToken;
5170
+ log;
5171
+ key(owner, repo) {
5172
+ return `${owner}/${repo}`.toLowerCase();
5173
+ }
5174
+ /** Get-or-create the pool for a repo, or null if the repo has no Fly token. */
5175
+ async getPool(owner, repo) {
5176
+ const repoTag = this.key(owner, repo);
5177
+ const existing = this.pools.get(repoTag);
5178
+ if (existing) return existing;
5179
+ let flyToken;
5180
+ try {
5181
+ flyToken = await this.resolveFlyToken(owner, repo);
5182
+ } catch (err) {
5183
+ this.log(`registry: vault read failed for ${repoTag}: ${err instanceof Error ? err.message : String(err)}`);
5184
+ return null;
5185
+ }
5186
+ if (!flyToken) {
5187
+ this.log(`registry: ${repoTag} has no FLY_API_TOKEN \u2014 no pool`);
5188
+ return null;
5189
+ }
5190
+ const fly = new FlyClient({ token: flyToken, app: this.cfg.base.app });
5191
+ const pm = new PoolManager({
5192
+ fly,
5193
+ config: { ...this.cfg.base, repoTag },
5194
+ log: (m) => this.log(`[${repoTag}] ${m}`)
5195
+ });
5196
+ this.pools.set(repoTag, pm);
5197
+ void pm.reconcile().catch((err) => this.log(`[${repoTag}] reconcile: ${err instanceof Error ? err.message : String(err)}`));
5198
+ return pm;
5199
+ }
5200
+ async claim(owner, repo, req) {
5201
+ const pm = await this.getPool(owner, repo);
5202
+ if (!pm) return { ok: false, reason: "repo has no FLY_API_TOKEN (no pool)" };
5203
+ let allSecrets = {};
5204
+ try {
5205
+ const vault = await readRepoSecrets({
5206
+ githubToken: this.cfg.githubToken,
5207
+ masterKey: this.cfg.masterKey,
5208
+ owner,
5209
+ repo
5210
+ });
5211
+ allSecrets = Object.fromEntries(Object.entries(vault).filter(([k]) => k !== "FLY_API_TOKEN"));
5212
+ } catch (err) {
5213
+ this.log(`[${this.key(owner, repo)}] vault secrets read failed: ${err instanceof Error ? err.message : String(err)}`);
5214
+ }
5215
+ const job = {
5216
+ jobId: req.jobId,
5217
+ repo: `${owner}/${repo}`,
5218
+ issueNumber: req.issueNumber,
5219
+ githubToken: this.cfg.githubToken,
5220
+ ref: req.ref,
5221
+ allSecrets,
5222
+ model: req.model,
5223
+ sessionId: req.sessionId,
5224
+ dashboardUrl: req.dashboardUrl
5225
+ };
5226
+ return pm.claim(job);
5227
+ }
5228
+ /** Status for a single repo's pool, or null if none exists yet. */
5229
+ status(owner, repo) {
5230
+ return this.pools.get(this.key(owner, repo))?.status() ?? null;
5231
+ }
5232
+ /** Resync every active repo pool (periodic self-heal). */
5233
+ async resyncAll() {
5234
+ for (const [repoTag, pm] of this.pools) {
5235
+ await pm.resync().catch((err) => this.log(`[${repoTag}] resync: ${err instanceof Error ? err.message : String(err)}`));
5236
+ }
5237
+ }
5238
+ activeRepos() {
5239
+ return [...this.pools.keys()];
5240
+ }
5241
+ };
5242
+
5039
5243
  // src/pool/keys.ts
5040
5244
  import { hkdfSync } from "crypto";
5041
5245
  var POOL_API_KEY_INFO = "kody-pool-api:v1";
@@ -5107,7 +5311,7 @@ function readJsonBody3(req) {
5107
5311
  req.on("error", reject);
5108
5312
  });
5109
5313
  }
5110
- function parsePoolJob(body) {
5314
+ function parseClaimRequest(body) {
5111
5315
  if (typeof body !== "object" || body === null) return { error: "body must be an object" };
5112
5316
  const b = body;
5113
5317
  const jobId = typeof b.jobId === "string" ? b.jobId.trim() : "";
@@ -5116,15 +5320,12 @@ function parsePoolJob(body) {
5116
5320
  if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) return { error: "repo must be 'owner/name'" };
5117
5321
  const issueNumber = Number(b.issueNumber);
5118
5322
  if (!Number.isInteger(issueNumber) || issueNumber <= 0) return { error: "issueNumber required" };
5119
- const githubToken = typeof b.githubToken === "string" ? b.githubToken.trim() : "";
5120
- if (!githubToken) return { error: "githubToken required" };
5121
- const job = { jobId, repo, issueNumber, githubToken };
5122
- if (typeof b.ref === "string" && b.ref.trim()) job.ref = b.ref.trim();
5123
- if (typeof b.model === "string" && b.model.trim()) job.model = b.model.trim();
5124
- if (typeof b.sessionId === "string" && b.sessionId.trim()) job.sessionId = b.sessionId.trim();
5125
- if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) job.dashboardUrl = b.dashboardUrl.trim();
5126
- if (b.allSecrets && typeof b.allSecrets === "object") job.allSecrets = b.allSecrets;
5127
- return { job };
5323
+ const req = { jobId, repo, issueNumber };
5324
+ if (typeof b.ref === "string" && b.ref.trim()) req.ref = b.ref.trim();
5325
+ if (typeof b.model === "string" && b.model.trim()) req.model = b.model.trim();
5326
+ if (typeof b.sessionId === "string" && b.sessionId.trim()) req.sessionId = b.sessionId.trim();
5327
+ if (typeof b.dashboardUrl === "string" && b.dashboardUrl.trim()) req.dashboardUrl = b.dashboardUrl.trim();
5328
+ return { req };
5128
5329
  }
5129
5330
  function superviseLitellm() {
5130
5331
  if (process.env.POOL_DISABLE_LITELLM === "1") return null;
@@ -5156,8 +5357,8 @@ var poolServe = async (ctx) => {
5156
5357
  ctx.skipAgent = true;
5157
5358
  const masterRaw = process.env.KODY_MASTER_KEY?.trim();
5158
5359
  if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
5159
- const flyToken = process.env.FLY_API_TOKEN?.trim();
5160
- if (!flyToken) throw new Error("FLY_API_TOKEN required for pool-serve");
5360
+ const githubToken = process.env.GITHUB_TOKEN?.trim();
5361
+ if (!githubToken) throw new Error("GITHUB_TOKEN required for pool-serve (reads per-repo vaults)");
5161
5362
  const master = masterKeyBytes(masterRaw);
5162
5363
  const poolApiKey = derivePoolApiKey(master);
5163
5364
  const runnerApiKey = deriveRunnerApiKey(master);
@@ -5171,23 +5372,36 @@ var poolServe = async (ctx) => {
5171
5372
  const apiPort = envInt("POOL_API_PORT", 4100);
5172
5373
  const healthTimeoutMs = envInt("POOL_HEALTH_TIMEOUT_MS", 12e4);
5173
5374
  const litellm = superviseLitellm();
5174
- const fly = new FlyClient({ token: flyToken, app });
5175
- const manager = new PoolManager({
5176
- fly,
5177
- config: { min, image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest", region, guest, runnerApiKey, litellmUrl, port: runnerPort, healthTimeoutMs },
5375
+ const registry = new PoolRegistry({
5376
+ githubToken,
5377
+ masterKey: master,
5378
+ base: {
5379
+ min,
5380
+ image: process.env.FLY_RUNNER_IMAGE ?? "registry.fly.io/kody-runner:latest",
5381
+ region,
5382
+ guest,
5383
+ runnerApiKey,
5384
+ litellmUrl,
5385
+ port: runnerPort,
5386
+ healthTimeoutMs,
5387
+ app
5388
+ },
5178
5389
  log
5179
5390
  });
5180
- manager.reconcile().catch((err) => log(`reconcile failed: ${err instanceof Error ? err.message : String(err)}`));
5181
5391
  const refillMs = envInt("POOL_REFILL_INTERVAL_MS", 6e4);
5182
5392
  const tick = setInterval(() => {
5183
- manager.refill().catch((err) => log(`refill tick failed: ${err instanceof Error ? err.message : String(err)}`));
5393
+ registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
5184
5394
  }, refillMs);
5185
5395
  const server = createServer3(async (req, res) => {
5186
5396
  try {
5187
5397
  if (!req.method || !req.url) return sendJson3(res, 400, { error: "bad request" });
5188
5398
  const url = new URL(req.url, "http://localhost");
5189
5399
  if (req.method === "GET" && url.pathname === "/healthz") {
5190
- return sendJson3(res, 200, { ok: true, litellm: litellm ? "supervised" : "off", pool: manager.status() });
5400
+ return sendJson3(res, 200, {
5401
+ ok: true,
5402
+ litellm: litellm ? "supervised" : "off",
5403
+ repos: registry.activeRepos()
5404
+ });
5191
5405
  }
5192
5406
  const authed = bearerOk(
5193
5407
  req.headers["authorization"],
@@ -5196,7 +5410,10 @@ var poolServe = async (ctx) => {
5196
5410
  );
5197
5411
  if (!authed) return sendJson3(res, 401, { error: "unauthorized" });
5198
5412
  if (req.method === "GET" && url.pathname === "/pool/status") {
5199
- return sendJson3(res, 200, manager.status());
5413
+ const repoParam = (url.searchParams.get("repo") ?? "").trim();
5414
+ const [owner, repo] = repoParam.split("/");
5415
+ if (!owner || !repo) return sendJson3(res, 400, { error: "repo query (owner/name) required" });
5416
+ return sendJson3(res, 200, { status: registry.status(owner, repo) });
5200
5417
  }
5201
5418
  if (req.method === "POST" && url.pathname === "/pool/claim") {
5202
5419
  let body;
@@ -5205,9 +5422,10 @@ var poolServe = async (ctx) => {
5205
5422
  } catch {
5206
5423
  return sendJson3(res, 400, { error: "invalid JSON body" });
5207
5424
  }
5208
- const parsed = parsePoolJob(body);
5425
+ const parsed = parseClaimRequest(body);
5209
5426
  if ("error" in parsed) return sendJson3(res, 400, { error: parsed.error });
5210
- const result = await manager.claim(parsed.job);
5427
+ const [owner, repo] = parsed.req.repo.split("/");
5428
+ const result = await registry.claim(owner, repo, parsed.req);
5211
5429
  if (result.ok) return sendJson3(res, 200, { ok: true, machineId: result.machineId });
5212
5430
  return sendJson3(res, 503, { ok: false, reason: result.reason ?? "pool unavailable" });
5213
5431
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.115",
3
+ "version": "0.4.117",
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",