@kody-ade/kody-engine 0.4.366 → 0.4.368

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 +158 -61
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -15,7 +15,7 @@ var init_package = __esm({
15
15
  "package.json"() {
16
16
  package_default = {
17
17
  name: "@kody-ade/kody-engine",
18
- version: "0.4.366",
18
+ version: "0.4.368",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -2666,10 +2666,34 @@ function readCheckRuns(repoSlug, ref, ignoreNames) {
2666
2666
  ],
2667
2667
  ghOptions
2668
2668
  );
2669
+ let rawStatuses = "";
2670
+ try {
2671
+ rawStatuses = gh(
2672
+ [
2673
+ "api",
2674
+ `repos/${repoSlug}/commits/${sha}/status`,
2675
+ "--jq",
2676
+ ".statuses[] | {context, state, target_url}"
2677
+ ],
2678
+ ghOptions
2679
+ );
2680
+ } catch {
2681
+ }
2669
2682
  const ignore = new Set(ignoreNames.map((n) => n.toLowerCase()));
2670
2683
  const checks = raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)).filter((c) => !ignore.has(String(c.name).toLowerCase()));
2684
+ const statuses = rawStatuses.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)).filter((status) => !ignore.has(String(status.context).toLowerCase()));
2671
2685
  const failing = checks.filter((c) => CHECK_FAIL_CONCLUSIONS.has(String(c.conclusion ?? "").toUpperCase())).map((c) => ({ name: c.name, conclusion: String(c.conclusion), detailsUrl: c.details_url }));
2686
+ failing.push(
2687
+ ...statuses.filter((status) => ["error", "failure"].includes(String(status.state).toLowerCase())).map((status) => ({
2688
+ name: status.context,
2689
+ conclusion: status.state,
2690
+ detailsUrl: status.target_url ?? ""
2691
+ }))
2692
+ );
2672
2693
  const pending = checks.filter((c) => String(c.status).toLowerCase() !== "completed").map((c) => ({ name: c.name, status: c.status }));
2694
+ pending.push(
2695
+ ...statuses.filter((status) => String(status.state).toLowerCase() === "pending").map((status) => ({ name: status.context, status: status.state }))
2696
+ );
2673
2697
  const state = failing.length > 0 ? "RED" : pending.length > 0 ? "PENDING" : "GREEN";
2674
2698
  return { sha, state, failing, pending };
2675
2699
  }
@@ -21803,6 +21827,26 @@ async function ghApp(jwt, apiPath2, method = "GET") {
21803
21827
  }
21804
21828
  return await res.json();
21805
21829
  }
21830
+ async function ghAppPage(authToken, apiPath2) {
21831
+ const res = await fetch(`${GH_API}${apiPath2}`, {
21832
+ headers: {
21833
+ Authorization: `Bearer ${authToken}`,
21834
+ Accept: "application/vnd.github+json",
21835
+ "X-GitHub-Api-Version": "2022-11-28",
21836
+ "User-Agent": "kody-engine"
21837
+ }
21838
+ });
21839
+ if (!res.ok) {
21840
+ const body = await res.text().catch(() => "");
21841
+ throw new Error(
21842
+ `GitHub App API GET ${apiPath2} \u2192 ${res.status} ${res.statusText}${body ? `: ${body.slice(0, 200)}` : ""}`
21843
+ );
21844
+ }
21845
+ return {
21846
+ data: await res.json(),
21847
+ hasNext: /rel="next"/.test(res.headers.get("link") ?? "")
21848
+ };
21849
+ }
21806
21850
  function readAppCreds(env = process.env) {
21807
21851
  const appId = env.KODY_APP_ID?.trim();
21808
21852
  const privateKey = env.KODY_APP_PRIVATE_KEY;
@@ -21827,6 +21871,36 @@ async function mintAppInstallationToken(creds) {
21827
21871
  const tok = await ghApp(jwt, `/app/installations/${installationId}/access_tokens`, "POST");
21828
21872
  return tok.token;
21829
21873
  }
21874
+ async function discoverAppRepositories(creds) {
21875
+ const jwt = buildAppJwt(creds.appId, creds.privateKey);
21876
+ const installations = [];
21877
+ for (let page = 1; ; page++) {
21878
+ const result = await ghAppPage(jwt, `/app/installations?per_page=100&page=${page}`);
21879
+ installations.push(...result.data.filter((item) => Number.isInteger(item.id) && item.id > 0));
21880
+ if (!result.hasNext) break;
21881
+ }
21882
+ const byRepo = /* @__PURE__ */ new Map();
21883
+ for (const installation of installations) {
21884
+ const token = await mintAppInstallationToken({
21885
+ appId: creds.appId,
21886
+ privateKey: creds.privateKey,
21887
+ installationId: String(installation.id)
21888
+ });
21889
+ for (let page = 1; ; page++) {
21890
+ const result = await ghAppPage(
21891
+ token,
21892
+ `/installation/repositories?per_page=100&page=${page}`
21893
+ );
21894
+ for (const repository of result.data.repositories ?? []) {
21895
+ const repo = repository.full_name?.trim();
21896
+ if (!repo || !/^[^/\s]+\/[^/\s]+$/.test(repo)) continue;
21897
+ byRepo.set(repo.toLowerCase(), { repo, token });
21898
+ }
21899
+ if (!result.hasNext) break;
21900
+ }
21901
+ }
21902
+ return [...byRepo.values()].sort((left, right) => left.repo.localeCompare(right.repo));
21903
+ }
21830
21904
 
21831
21905
  // src/kody-cli.ts
21832
21906
  init_companyStore();
@@ -24643,67 +24717,49 @@ init_registry();
24643
24717
  // src/servers/pool-serve.ts
24644
24718
  import { createServer as createServer5 } from "http";
24645
24719
 
24646
- // src/github-health.ts
24647
- var STATUS_URL = "https://www.githubstatus.com/api/v2/components.json";
24648
- var STATUS_CACHE_TTL_MS = 3e4;
24649
- var statusCache = null;
24650
- async function probeActionsStatus(fetchImpl = fetch) {
24651
- if (statusCache && statusCache.expiresAt > Date.now()) return statusCache.probe;
24652
- try {
24653
- const res = await fetchImpl(STATUS_URL, { headers: { "User-Agent": "kody-engine" } });
24654
- if (!res.ok) return { degraded: false, label: `http_${res.status}` };
24655
- const body = await res.json();
24656
- const actions = (body.components ?? []).find((c) => (c.name ?? "").trim().toLowerCase() === "actions");
24657
- const label = actions?.status ?? "unknown";
24658
- const degraded = !!actions && label !== "operational";
24659
- const probe = { degraded, label };
24660
- statusCache = { probe, expiresAt: Date.now() + STATUS_CACHE_TTL_MS };
24661
- return probe;
24662
- } catch {
24663
- return { degraded: false, label: "probe_error" };
24720
+ // src/pool/agency-loop-tick.ts
24721
+ function normalizeRepositories(repositories) {
24722
+ const unique = /* @__PURE__ */ new Set();
24723
+ for (const raw of repositories) {
24724
+ const repo = raw.trim().toLowerCase();
24725
+ if (/^[^/\s]+\/[^/\s]+$/.test(repo)) unique.add(repo);
24664
24726
  }
24727
+ return [...unique].sort();
24665
24728
  }
24666
- async function gitHubActionsDegraded(fetchImpl = fetch) {
24667
- return (await probeActionsStatus(fetchImpl)).degraded;
24668
- }
24669
-
24670
- // src/pool/capability-fallback-tick.ts
24671
- async function runCapabilityFallbackTick(deps) {
24672
- if (!await deps.isDegraded()) {
24673
- return { ran: false, claimed: 0 };
24674
- }
24675
- const repos = deps.activeRepos();
24676
- if (repos.length === 0) {
24677
- deps.log("GitHub Actions degraded but no active repo pools \u2014 nothing to tick");
24678
- return { ran: true, claimed: 0 };
24729
+ async function runAgencyLoopTick(deps) {
24730
+ const repositories = normalizeRepositories(await deps.discover());
24731
+ if (repositories.length === 0) {
24732
+ deps.log("no consumer agencies discovered \u2014 nothing to tick");
24733
+ return { discovered: 0, claimed: 0 };
24679
24734
  }
24680
- deps.log(`GitHub Actions degraded \u2014 running scheduled fan-out on Fly for ${repos.length} repo(s)`);
24735
+ deps.log(
24736
+ `running scheduled fan-out for ${repositories.length} consumer agenc${repositories.length === 1 ? "y" : "ies"}`
24737
+ );
24681
24738
  const clock = deps.now ?? Date.now;
24682
24739
  let claimed = 0;
24683
- for (const tag of repos) {
24684
- const [owner, repo] = tag.split("/");
24685
- if (!owner || !repo) continue;
24740
+ for (const repository of repositories) {
24741
+ const [owner, repo] = repository.split("/");
24686
24742
  try {
24687
- const res = await deps.claim(owner, repo, {
24743
+ const result = await deps.claim(owner, repo, {
24688
24744
  jobId: `sched-${owner}-${repo}-${clock()}`,
24689
- repo: tag,
24745
+ repo: repository,
24690
24746
  runRequest: {
24691
24747
  target: { type: "workflow", id: "scheduled-fanout" },
24692
24748
  intent: "tick",
24693
24749
  source: "schedule"
24694
24750
  }
24695
24751
  });
24696
- if (res.ok) {
24752
+ if (result.ok) {
24697
24753
  claimed++;
24698
- deps.log(`[${tag}] scheduled fan-out claimed ${res.machineId}`);
24754
+ deps.log(`[${repository}] scheduled fan-out claimed ${result.machineId}`);
24699
24755
  } else {
24700
- deps.log(`[${tag}] scheduled fan-out skipped: ${res.reason ?? "pool unavailable"}`);
24756
+ deps.log(`[${repository}] scheduled fan-out skipped: ${result.reason ?? "runner unavailable"}`);
24701
24757
  }
24702
- } catch (err) {
24703
- deps.log(`[${tag}] scheduled fan-out error: ${err instanceof Error ? err.message : String(err)}`);
24758
+ } catch (error) {
24759
+ deps.log(`[${repository}] scheduled fan-out error: ${error instanceof Error ? error.message : String(error)}`);
24704
24760
  }
24705
24761
  }
24706
- return { ran: true, claimed };
24762
+ return { discovered: repositories.length, claimed };
24707
24763
  }
24708
24764
 
24709
24765
  // src/servers/pool-serve.ts
@@ -25089,8 +25145,9 @@ var PoolRegistry = class {
25089
25145
  this.cfg = cfg;
25090
25146
  this.log = cfg.log ?? (() => {
25091
25147
  });
25092
- this.resolveFlyToken = cfg.resolveFlyToken ?? ((owner, repo) => readRepoSecret({
25093
- githubToken: cfg.githubToken,
25148
+ this.resolveGithubToken = cfg.resolveGithubToken ?? (async () => cfg.githubToken);
25149
+ this.resolveFlyToken = cfg.resolveFlyToken ?? (async (owner, repo) => readRepoSecret({
25150
+ githubToken: await this.resolveGithubToken(owner, repo),
25094
25151
  masterKey: cfg.masterKey,
25095
25152
  owner,
25096
25153
  repo,
@@ -25098,7 +25155,7 @@ var PoolRegistry = class {
25098
25155
  }));
25099
25156
  this.resolvePoolMin = cfg.resolvePoolMin ?? (async (owner, repo) => parsePoolMin(
25100
25157
  await readRepoSecret({
25101
- githubToken: cfg.githubToken,
25158
+ githubToken: await this.resolveGithubToken(owner, repo),
25102
25159
  masterKey: cfg.masterKey,
25103
25160
  owner,
25104
25161
  repo,
@@ -25110,6 +25167,7 @@ var PoolRegistry = class {
25110
25167
  cfg;
25111
25168
  pools = /* @__PURE__ */ new Map();
25112
25169
  poolCreates = /* @__PURE__ */ new Map();
25170
+ resolveGithubToken;
25113
25171
  resolveFlyToken;
25114
25172
  resolvePoolMin;
25115
25173
  log;
@@ -25162,10 +25220,17 @@ var PoolRegistry = class {
25162
25220
  async claim(owner, repo, req) {
25163
25221
  const pm = await this.getPool(owner, repo);
25164
25222
  if (!pm) return { ok: false, reason: "repo has no FLY_API_TOKEN (no pool)" };
25223
+ let githubToken2;
25224
+ try {
25225
+ githubToken2 = await this.resolveGithubToken(owner, repo);
25226
+ } catch (err) {
25227
+ this.log(`[${this.key(owner, repo)}] repository auth failed: ${err instanceof Error ? err.message : String(err)}`);
25228
+ return { ok: false, reason: "repository authentication failed" };
25229
+ }
25165
25230
  let allSecrets = {};
25166
25231
  try {
25167
25232
  const vault = await readRepoSecrets({
25168
- githubToken: this.cfg.githubToken,
25233
+ githubToken: githubToken2,
25169
25234
  masterKey: this.cfg.masterKey,
25170
25235
  owner,
25171
25236
  repo
@@ -25182,7 +25247,7 @@ var PoolRegistry = class {
25182
25247
  const job = {
25183
25248
  jobId: req.jobId,
25184
25249
  repo: `${owner}/${repo}`,
25185
- githubToken: this.cfg.githubToken,
25250
+ githubToken: githubToken2,
25186
25251
  runRequest: req.runRequest,
25187
25252
  issueNumber: req.issueNumber,
25188
25253
  sessionId: req.sessionId,
@@ -25344,8 +25409,24 @@ function synthesizeLegacyClaimRequest(input) {
25344
25409
  async function poolServe() {
25345
25410
  const masterRaw = process.env.KODY_MASTER_KEY?.trim();
25346
25411
  if (!masterRaw) throw new Error("KODY_MASTER_KEY required for pool-serve");
25347
- const githubToken2 = process.env.GITHUB_TOKEN?.trim();
25348
- if (!githubToken2) throw new Error("GITHUB_TOKEN required for pool-serve (reads per-repo vaults)");
25412
+ const appCreds = readAppCreds();
25413
+ const fallbackGithubToken = process.env.GITHUB_TOKEN?.trim() ?? "";
25414
+ if (!appCreds && !fallbackGithubToken) {
25415
+ throw new Error("GitHub App credentials or GITHUB_TOKEN required for pool-serve");
25416
+ }
25417
+ const repoTokens = /* @__PURE__ */ new Map();
25418
+ const resolveGithubToken = async (owner, repo) => {
25419
+ const key = `${owner}/${repo}`.toLowerCase();
25420
+ const discovered = repoTokens.get(key);
25421
+ if (discovered) return discovered;
25422
+ if (appCreds) {
25423
+ const token = await mintAppInstallationToken({ ...appCreds, repo: `${owner}/${repo}` });
25424
+ repoTokens.set(key, token);
25425
+ return token;
25426
+ }
25427
+ if (fallbackGithubToken) return fallbackGithubToken;
25428
+ throw new Error(`no unattended GitHub token for ${key}`);
25429
+ };
25349
25430
  const master = masterKeyBytes(masterRaw);
25350
25431
  const poolApiKey = derivePoolApiKey(master);
25351
25432
  const runnerApiKey = deriveRunnerApiKey(master);
@@ -25358,7 +25439,8 @@ async function poolServe() {
25358
25439
  const apiPort = envInt2("POOL_API_PORT", 4100);
25359
25440
  const healthTimeoutMs = envInt2("POOL_HEALTH_TIMEOUT_MS", 12e4);
25360
25441
  const registry = new PoolRegistry({
25361
- githubToken: githubToken2,
25442
+ githubToken: fallbackGithubToken,
25443
+ resolveGithubToken,
25362
25444
  masterKey: master,
25363
25445
  base: {
25364
25446
  min,
@@ -25376,16 +25458,30 @@ async function poolServe() {
25376
25458
  const tick = setInterval(() => {
25377
25459
  registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
25378
25460
  }, refillMs);
25379
- const capabilityTickEnabled = (process.env.POOL_CAPABILITY_TICK ?? "1") !== "0";
25380
- const capabilityTickMs = envInt2("POOL_CAPABILITY_TICK_MS", 15 * 6e4);
25381
- const capabilityTick = capabilityTickEnabled ? setInterval(() => {
25382
- runCapabilityFallbackTick({
25383
- isDegraded: () => gitHubActionsDegraded(),
25384
- activeRepos: () => registry.activeRepos(),
25461
+ const discoverAgencies = async () => {
25462
+ if (!appCreds) return registry.activeRepos();
25463
+ const repositories = await discoverAppRepositories(appCreds);
25464
+ for (const access of repositories) repoTokens.set(access.repo.toLowerCase(), access.token);
25465
+ return [.../* @__PURE__ */ new Set([...repositories.map((access) => access.repo), ...registry.activeRepos()])];
25466
+ };
25467
+ let agencyTickInFlight = null;
25468
+ const runLoopTick = () => {
25469
+ if (agencyTickInFlight) return agencyTickInFlight;
25470
+ agencyTickInFlight = runAgencyLoopTick({
25471
+ discover: discoverAgencies,
25385
25472
  claim: (owner, repo, req) => registry.claim(owner, repo, req),
25386
25473
  log
25387
- }).catch((err) => log(`capability fallback tick failed: ${err instanceof Error ? err.message : String(err)}`));
25388
- }, capabilityTickMs) : null;
25474
+ }).catch((err) => log(`agency Loop tick failed: ${err instanceof Error ? err.message : String(err)}`)).finally(() => {
25475
+ agencyTickInFlight = null;
25476
+ });
25477
+ return agencyTickInFlight;
25478
+ };
25479
+ const loopTickEnabled = (process.env.POOL_LOOP_TICK ?? process.env.POOL_CAPABILITY_TICK ?? "1") !== "0";
25480
+ const loopTickMs = envInt2(
25481
+ process.env.POOL_LOOP_TICK_MS ? "POOL_LOOP_TICK_MS" : "POOL_CAPABILITY_TICK_MS",
25482
+ 15 * 6e4
25483
+ );
25484
+ const loopTick = loopTickEnabled ? setInterval(() => void runLoopTick(), loopTickMs) : null;
25389
25485
  const server = createServer5(async (req, res) => {
25390
25486
  try {
25391
25487
  if (!req.method || !req.url) return sendJson2(res, 400, { error: "bad request" });
@@ -25439,10 +25535,11 @@ async function poolServe() {
25439
25535
  resolve10();
25440
25536
  });
25441
25537
  });
25538
+ if (loopTickEnabled) void runLoopTick();
25442
25539
  const shutdown = (signal) => {
25443
25540
  log(`${signal} \u2014 shutting down`);
25444
25541
  clearInterval(tick);
25445
- if (capabilityTick) clearInterval(capabilityTick);
25542
+ if (loopTick) clearInterval(loopTick);
25446
25543
  server.close(() => process.exit(0));
25447
25544
  };
25448
25545
  process.once("SIGINT", () => shutdown("SIGINT"));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.366",
3
+ "version": "0.4.368",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative implementation profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",