@kody-ade/kody-engine 0.4.277 → 0.4.278

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 +560 -301
  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.277",
18
+ version: "0.4.278",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -156,14 +156,97 @@ function ghToken(preferRepoToken = false) {
156
156
  function gh(args, options) {
157
157
  const token = ghToken(options?.preferRepoToken);
158
158
  const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
159
- return execFileSync("gh", args, {
160
- encoding: "utf-8",
161
- timeout: API_TIMEOUT_MS,
162
- cwd: options?.cwd,
163
- env,
164
- input: options?.input,
165
- stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
166
- }).trim();
159
+ const maxRetries = envInt("KODY_GH_RATE_LIMIT_MAX_RETRIES", DEFAULT_RATE_LIMIT_RETRIES);
160
+ const maxWaitMs = envInt("KODY_GH_RATE_LIMIT_MAX_WAIT_MS", DEFAULT_RATE_LIMIT_MAX_WAIT_MS);
161
+ for (let attempt = 0; ; attempt += 1) {
162
+ try {
163
+ return execFileSync("gh", args, {
164
+ encoding: "utf-8",
165
+ timeout: API_TIMEOUT_MS,
166
+ cwd: options?.cwd,
167
+ env,
168
+ input: options?.input,
169
+ stdio: options?.input ? ["pipe", "pipe", "pipe"] : ["inherit", "pipe", "pipe"]
170
+ }).trim();
171
+ } catch (err) {
172
+ if (!isRateLimitError(err) || attempt >= maxRetries) throw err;
173
+ const waitMs = rateLimitWaitMs(token, options?.cwd, attempt + 1);
174
+ if (waitMs > maxWaitMs) {
175
+ process.stderr.write(
176
+ `[kody gh] rate limit on ${formatGhArgs(args)}; reset wait ${formatDuration(waitMs)} exceeds max ${formatDuration(maxWaitMs)}
177
+ `
178
+ );
179
+ throw err;
180
+ }
181
+ process.stderr.write(
182
+ `[kody gh] rate limit on ${formatGhArgs(args)}; retry ${attempt + 1}/${maxRetries} after ${formatDuration(waitMs)}
183
+ `
184
+ );
185
+ sleepSync(waitMs);
186
+ }
187
+ }
188
+ }
189
+ function envInt(name, fallback) {
190
+ const raw = process.env[name]?.trim();
191
+ if (!raw) return fallback;
192
+ const value = Number(raw);
193
+ return Number.isFinite(value) && value >= 0 ? Math.floor(value) : fallback;
194
+ }
195
+ function errorMessage(err) {
196
+ if (err instanceof Error) {
197
+ const stderr = err.stderr;
198
+ if (Buffer.isBuffer(stderr)) return `${err.message}
199
+ ${stderr.toString("utf-8")}`;
200
+ if (typeof stderr === "string") return `${err.message}
201
+ ${stderr}`;
202
+ return err.message;
203
+ }
204
+ return String(err);
205
+ }
206
+ function isRateLimitError(err) {
207
+ const msg = errorMessage(err);
208
+ return /rate limit exceeded|secondary rate limit|abuse detection/i.test(msg) && /HTTP 403|rate limit/i.test(msg);
209
+ }
210
+ function rateLimitWaitMs(token, cwd, retryNumber) {
211
+ const resetMs = readCoreRateLimitResetMs(token, cwd);
212
+ if (typeof resetMs === "number") {
213
+ return Math.max(0, resetMs - Date.now() + 1e3);
214
+ }
215
+ const base = envInt("KODY_GH_RATE_LIMIT_BASE_DELAY_MS", DEFAULT_RATE_LIMIT_BASE_DELAY_MS);
216
+ return base * 2 ** Math.max(0, retryNumber - 1);
217
+ }
218
+ function readCoreRateLimitResetMs(token, cwd) {
219
+ try {
220
+ const env = token ? { ...process.env, GH_TOKEN: token } : { ...process.env };
221
+ const raw = execFileSync("gh", ["api", "rate_limit", "--jq", ".resources.core.reset"], {
222
+ encoding: "utf-8",
223
+ timeout: API_TIMEOUT_MS,
224
+ cwd,
225
+ env,
226
+ stdio: ["ignore", "pipe", "pipe"]
227
+ }).trim();
228
+ const seconds = Number(raw);
229
+ return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : null;
230
+ } catch {
231
+ return null;
232
+ }
233
+ }
234
+ function sleepSync(ms) {
235
+ if (ms <= 0) return;
236
+ const buffer = new SharedArrayBuffer(4);
237
+ const view = new Int32Array(buffer);
238
+ Atomics.wait(view, 0, 0, ms);
239
+ }
240
+ function formatGhArgs(args) {
241
+ return `gh ${args.slice(0, 4).join(" ")}${args.length > 4 ? " ..." : ""}`;
242
+ }
243
+ function formatDuration(ms) {
244
+ if (ms < 1e3) return `${ms}ms`;
245
+ const seconds = Math.ceil(ms / 1e3);
246
+ if (seconds < 60) return `${seconds}s`;
247
+ const minutes = Math.floor(seconds / 60);
248
+ const rest = seconds % 60;
249
+ return rest ? `${minutes}m${rest}s` : `${minutes}m`;
167
250
  }
168
251
  function getIssue(issueNumber, cwd) {
169
252
  const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels,url"], {
@@ -333,11 +416,14 @@ function postPrReviewComment(prNumber, body, cwd) {
333
416
  );
334
417
  }
335
418
  }
336
- var API_TIMEOUT_MS, BotDispatchCommentError, DEFAULT_COMMENT_LIMIT, DEFAULT_COMMENT_MAX_BYTES, VERDICT_HEADING;
419
+ var API_TIMEOUT_MS, DEFAULT_RATE_LIMIT_RETRIES, DEFAULT_RATE_LIMIT_BASE_DELAY_MS, DEFAULT_RATE_LIMIT_MAX_WAIT_MS, BotDispatchCommentError, DEFAULT_COMMENT_LIMIT, DEFAULT_COMMENT_MAX_BYTES, VERDICT_HEADING;
337
420
  var init_issue = __esm({
338
421
  "src/issue.ts"() {
339
422
  "use strict";
340
423
  API_TIMEOUT_MS = 3e4;
424
+ DEFAULT_RATE_LIMIT_RETRIES = 2;
425
+ DEFAULT_RATE_LIMIT_BASE_DELAY_MS = 1e3;
426
+ DEFAULT_RATE_LIMIT_MAX_WAIT_MS = 65 * 60 * 1e3;
341
427
  BotDispatchCommentError = class extends Error {
342
428
  constructor(slug2) {
343
429
  super(
@@ -446,8 +532,11 @@ function branchApiPath(config, targetPath) {
446
532
  }
447
533
  function ensureStateBranch(config, cwd) {
448
534
  const parsed = parseStateRepo(config);
535
+ const cacheKey2 = `${parsed.owner}/${parsed.repo}:${STATE_BRANCH}`;
536
+ if (ensuredStateBranches.has(cacheKey2)) return;
449
537
  try {
450
538
  gh(["api", `/repos/${parsed.owner}/${parsed.repo}/git/ref/heads/${STATE_BRANCH}`], { cwd });
539
+ ensuredStateBranches.add(cacheKey2);
451
540
  return;
452
541
  } catch (err) {
453
542
  if (!is404(err)) throw err;
@@ -466,6 +555,7 @@ function ensureStateBranch(config, cwd) {
466
555
  } catch (err) {
467
556
  if (!isAlreadyExists(err)) throw err;
468
557
  }
558
+ ensuredStateBranches.add(cacheKey2);
469
559
  }
470
560
  function readStateText(config, cwd, filePath) {
471
561
  const targetPath = stateRepoPath(config, filePath);
@@ -539,11 +629,13 @@ function listStateDirectory(config, cwd, dirPath) {
539
629
  const parsed = JSON.parse(raw);
540
630
  return Array.isArray(parsed) ? parsed : [];
541
631
  }
632
+ var ensuredStateBranches;
542
633
  var init_stateRepo = __esm({
543
634
  "src/stateRepo.ts"() {
544
635
  "use strict";
545
636
  init_issue();
546
637
  init_stateBranch();
638
+ ensuredStateBranches = /* @__PURE__ */ new Set();
547
639
  }
548
640
  });
549
641
 
@@ -1413,6 +1505,300 @@ var init_submitMcp = __esm({
1413
1505
  }
1414
1506
  });
1415
1507
 
1508
+ // src/dashboardCmsMcp.ts
1509
+ var dashboardCmsMcp_exports = {};
1510
+ __export(dashboardCmsMcp_exports, {
1511
+ DASHBOARD_CMS_MCP_TOOL_NAMES: () => DASHBOARD_CMS_MCP_TOOL_NAMES,
1512
+ buildDashboardCmsMcpServer: () => buildDashboardCmsMcpServer,
1513
+ dashboardCmsToolDefinitions: () => dashboardCmsToolDefinitions
1514
+ });
1515
+ import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
1516
+ import { z as z2 } from "zod";
1517
+ function dashboardBaseUrl(opts) {
1518
+ const raw = opts.dashboardUrl?.trim() || process.env.KODY_CMS_DASHBOARD_URL?.trim() || process.env.KODY_DASHBOARD_URL?.trim() || process.env.DASHBOARD_URL?.trim() || "";
1519
+ if (!raw) return null;
1520
+ try {
1521
+ return new URL(raw).origin;
1522
+ } catch {
1523
+ return null;
1524
+ }
1525
+ }
1526
+ function dashboardCmsToken(opts) {
1527
+ return opts.token?.trim() || process.env.KODY_CMS_TOKEN?.trim() || process.env.KODY_DASHBOARD_TOKEN?.trim() || process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_PAT?.trim() || null;
1528
+ }
1529
+ function cmsHeaders(opts) {
1530
+ const token = dashboardCmsToken(opts);
1531
+ const [owner, repo] = opts.repoSlug.split("/");
1532
+ if (!token) {
1533
+ return {
1534
+ ok: false,
1535
+ error: "missing_cms_token",
1536
+ message: "Set KODY_CMS_TOKEN, KODY_DASHBOARD_TOKEN, KODY_TOKEN, GH_TOKEN, GITHUB_TOKEN, or GH_PAT."
1537
+ };
1538
+ }
1539
+ if (!owner || !repo) {
1540
+ return { ok: false, error: "invalid_repo", message: `Invalid repo slug: ${opts.repoSlug}` };
1541
+ }
1542
+ return {
1543
+ ok: true,
1544
+ headers: {
1545
+ "Content-Type": "application/json",
1546
+ "x-kody-token": token,
1547
+ "x-kody-owner": owner,
1548
+ "x-kody-repo": repo,
1549
+ ...opts.storeRepoUrl?.trim() ? { "x-kody-store-repo-url": opts.storeRepoUrl.trim() } : {},
1550
+ ...opts.storeRef?.trim() ? { "x-kody-store-ref": opts.storeRef.trim() } : {}
1551
+ }
1552
+ };
1553
+ }
1554
+ async function callDashboardCms(opts, path50, init = {}) {
1555
+ const baseUrl = dashboardBaseUrl(opts);
1556
+ if (!baseUrl) {
1557
+ return {
1558
+ ok: false,
1559
+ error: "missing_dashboard_url",
1560
+ message: "Set KODY_CMS_DASHBOARD_URL or KODY_DASHBOARD_URL to the Dashboard origin."
1561
+ };
1562
+ }
1563
+ const headerResult = cmsHeaders(opts);
1564
+ if (!headerResult.ok) return headerResult;
1565
+ try {
1566
+ const res = await fetch(`${baseUrl}${path50}`, {
1567
+ ...init,
1568
+ headers: {
1569
+ ...headerResult.headers,
1570
+ ...init.headers
1571
+ }
1572
+ });
1573
+ const contentType = res.headers.get("content-type") ?? "";
1574
+ const data = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
1575
+ if (!res.ok) {
1576
+ return {
1577
+ ok: false,
1578
+ status: res.status,
1579
+ error: stringFieldFromRecord(data, "error") ?? "cms_request_failed",
1580
+ message: stringFieldFromRecord(data, "message") ?? `Dashboard CMS request failed with ${res.status}.`,
1581
+ data
1582
+ };
1583
+ }
1584
+ return { ok: true, status: res.status, data };
1585
+ } catch (err) {
1586
+ return {
1587
+ ok: false,
1588
+ error: "cms_request_error",
1589
+ message: err instanceof Error ? err.message : String(err)
1590
+ };
1591
+ }
1592
+ }
1593
+ function cmsToolResponse(result) {
1594
+ return {
1595
+ content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
1596
+ ...result.ok ? {} : { isError: true }
1597
+ };
1598
+ }
1599
+ function cmsQuery(args) {
1600
+ const params = new URLSearchParams();
1601
+ const q = stringArg(args.q);
1602
+ if (q) params.set("q", q);
1603
+ const limit = numberArg(args.limit);
1604
+ if (limit !== void 0) params.set("limit", String(limit));
1605
+ const offset = numberArg(args.offset);
1606
+ if (offset !== void 0) params.set("offset", String(offset));
1607
+ if (args.filters && typeof args.filters === "object" && !Array.isArray(args.filters)) {
1608
+ params.set("filters", JSON.stringify(args.filters));
1609
+ }
1610
+ if (Array.isArray(args.sort)) {
1611
+ const sort = args.sort.flatMap((entry) => {
1612
+ if (!entry || typeof entry !== "object") return [];
1613
+ const field = stringArg(entry.field);
1614
+ if (!field) return [];
1615
+ const direction = entry.direction === "asc" ? "asc" : "desc";
1616
+ return [`${field}:${direction}`];
1617
+ }).join(",");
1618
+ if (sort) params.set("sort", sort);
1619
+ }
1620
+ const value = params.toString();
1621
+ return value ? `?${value}` : "";
1622
+ }
1623
+ function stringArg(value) {
1624
+ return typeof value === "string" ? value.trim() : "";
1625
+ }
1626
+ function numberArg(value) {
1627
+ if (value === void 0 || value === null || value === "") return void 0;
1628
+ const n = Number(value);
1629
+ return Number.isFinite(n) ? n : void 0;
1630
+ }
1631
+ function documentArg(value) {
1632
+ if (!value || typeof value !== "object" || Array.isArray(value)) return {};
1633
+ return value;
1634
+ }
1635
+ function normalizeCmsDocumentIdInput(input) {
1636
+ const trimmed = stripWrappingQuotes(input.trim());
1637
+ const withoutQuery = trimmed.split(/[?#]/, 1)[0] ?? trimmed;
1638
+ const path50 = parseDocumentPath(withoutQuery);
1639
+ return path50 ?? parseDocumentIdSegment(withoutQuery) ?? withoutQuery;
1640
+ }
1641
+ function stripWrappingQuotes(value) {
1642
+ let current = value;
1643
+ for (; ; ) {
1644
+ const next = current.replace(/^[`'"]+|[`'"]+$/g, "").trim();
1645
+ if (next === current) return current;
1646
+ current = next;
1647
+ }
1648
+ }
1649
+ function parseDocumentPath(value) {
1650
+ const path50 = value.startsWith("http://") || value.startsWith("https://") ? urlPathname(value) : value;
1651
+ if (!path50?.includes("/content/entries/")) return null;
1652
+ const parts = path50.split("/").filter(Boolean).map(decodePathPart);
1653
+ const entriesIndex = parts.findIndex((part, index) => part === "content" && parts[index + 1] === "entries");
1654
+ const idPart = parts[entriesIndex + 3];
1655
+ if (!idPart || idPart === "new") return null;
1656
+ return idPart === "edit" ? parts[entriesIndex + 2] ?? null : idPart;
1657
+ }
1658
+ function parseDocumentIdSegment(value) {
1659
+ const parts = value.split("/").filter(Boolean).map(decodePathPart);
1660
+ if (parts.length < 2) return null;
1661
+ const lastPart = parts[parts.length - 1];
1662
+ if (!lastPart || lastPart === "new") return null;
1663
+ return lastPart === "edit" ? parts[parts.length - 2] ?? null : lastPart;
1664
+ }
1665
+ function urlPathname(value) {
1666
+ try {
1667
+ return new URL(value).pathname;
1668
+ } catch {
1669
+ return null;
1670
+ }
1671
+ }
1672
+ function decodePathPart(value) {
1673
+ try {
1674
+ return decodeURIComponent(value);
1675
+ } catch {
1676
+ return value;
1677
+ }
1678
+ }
1679
+ function stringFieldFromRecord(value, field) {
1680
+ return value && typeof value === "object" && !Array.isArray(value) && typeof value[field] === "string" ? String(value[field]) : void 0;
1681
+ }
1682
+ function dashboardCmsToolDefinitions(opts) {
1683
+ const cmsListCollectionsTool = {
1684
+ name: "cms_list_collections",
1685
+ description: "List configured Dashboard CMS collections and their supported operations. Read-only.",
1686
+ inputSchema: {},
1687
+ handler: async () => cmsToolResponse(await callDashboardCms(opts, "/api/kody/cms"))
1688
+ };
1689
+ const cmsListDocumentsTool = {
1690
+ name: "cms_list_documents",
1691
+ description: "List or search Dashboard CMS documents from one configured collection. Read-only.",
1692
+ inputSchema: {
1693
+ collection: z2.string().min(1).describe("CMS collection name."),
1694
+ q: z2.string().optional().describe("Optional search query."),
1695
+ filters: z2.record(z2.string(), z2.unknown()).optional().describe("Optional filter object keyed by field."),
1696
+ sort: z2.array(
1697
+ z2.object({
1698
+ field: z2.string().min(1),
1699
+ direction: z2.enum(["asc", "desc"]).default("desc")
1700
+ })
1701
+ ).optional(),
1702
+ limit: z2.number().int().min(1).max(100).optional(),
1703
+ offset: z2.number().int().min(0).optional()
1704
+ },
1705
+ handler: async (args) => {
1706
+ const collection = encodeURIComponent(stringArg(args.collection));
1707
+ return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}${cmsQuery(args)}`));
1708
+ }
1709
+ };
1710
+ const cmsGetDocumentTool = {
1711
+ name: "cms_get_document",
1712
+ description: "Get one Dashboard CMS document by collection and id. Use a raw id or the content entry URL copied from the Dashboard.",
1713
+ inputSchema: {
1714
+ collection: z2.string().min(1).describe("CMS collection name."),
1715
+ id: z2.string().min(1).describe("Document id. Use cmsDocumentId from cms_list_documents when available.")
1716
+ },
1717
+ handler: async (args) => {
1718
+ const collection = encodeURIComponent(stringArg(args.collection));
1719
+ const id = encodeURIComponent(normalizeCmsDocumentIdInput(stringArg(args.id)));
1720
+ return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`));
1721
+ }
1722
+ };
1723
+ const cmsCreateDocumentTool = {
1724
+ name: "cms_create_document",
1725
+ description: "Create one Dashboard CMS document when the caller allows writes and the CMS collection allows create.",
1726
+ inputSchema: {
1727
+ collection: z2.string().min(1).describe("CMS collection name."),
1728
+ data: z2.record(z2.string(), z2.unknown()).describe("Document fields to create.")
1729
+ },
1730
+ handler: async (args) => {
1731
+ const refusal = opts.assertWriteAllowed?.();
1732
+ if (refusal) return { content: [{ type: "text", text: refusal }] };
1733
+ const collection = encodeURIComponent(stringArg(args.collection));
1734
+ return cmsToolResponse(
1735
+ await callDashboardCms(opts, `/api/kody/cms/${collection}`, {
1736
+ method: "POST",
1737
+ body: JSON.stringify(documentArg(args.data))
1738
+ })
1739
+ );
1740
+ }
1741
+ };
1742
+ const cmsUpdateDocumentTool = {
1743
+ name: "cms_update_document",
1744
+ description: "Update one Dashboard CMS document when the caller allows writes and the CMS collection allows update.",
1745
+ inputSchema: {
1746
+ collection: z2.string().min(1).describe("CMS collection name."),
1747
+ id: z2.string().min(1).describe("Document id."),
1748
+ data: z2.record(z2.string(), z2.unknown()).describe("Partial document fields to update.")
1749
+ },
1750
+ handler: async (args) => {
1751
+ const refusal = opts.assertWriteAllowed?.();
1752
+ if (refusal) return { content: [{ type: "text", text: refusal }] };
1753
+ const collection = encodeURIComponent(stringArg(args.collection));
1754
+ const id = encodeURIComponent(normalizeCmsDocumentIdInput(stringArg(args.id)));
1755
+ return cmsToolResponse(
1756
+ await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`, {
1757
+ method: "PATCH",
1758
+ body: JSON.stringify(documentArg(args.data))
1759
+ })
1760
+ );
1761
+ }
1762
+ };
1763
+ return [
1764
+ cmsListCollectionsTool,
1765
+ cmsListDocumentsTool,
1766
+ cmsGetDocumentTool,
1767
+ cmsCreateDocumentTool,
1768
+ cmsUpdateDocumentTool
1769
+ ];
1770
+ }
1771
+ function buildDashboardCmsMcpServer(opts) {
1772
+ const tools = dashboardCmsToolDefinitions(opts).map(
1773
+ (def) => tool3(
1774
+ def.name,
1775
+ def.description,
1776
+ def.inputSchema,
1777
+ async (args) => def.handler(args)
1778
+ )
1779
+ );
1780
+ return {
1781
+ server: createSdkMcpServer3({
1782
+ name: "kody-cms",
1783
+ version: "0.1.0",
1784
+ tools
1785
+ })
1786
+ };
1787
+ }
1788
+ var DASHBOARD_CMS_MCP_TOOL_NAMES;
1789
+ var init_dashboardCmsMcp = __esm({
1790
+ "src/dashboardCmsMcp.ts"() {
1791
+ "use strict";
1792
+ DASHBOARD_CMS_MCP_TOOL_NAMES = [
1793
+ "cms_list_collections",
1794
+ "cms_list_documents",
1795
+ "cms_get_document",
1796
+ "cms_create_document",
1797
+ "cms_update_document"
1798
+ ];
1799
+ }
1800
+ });
1801
+
1416
1802
  // src/capabilityFolders.ts
1417
1803
  import * as fs4 from "fs";
1418
1804
  import * as path6 from "path";
@@ -1983,8 +2369,8 @@ __export(capabilityMcp_exports, {
1983
2369
  readCheckRuns: () => readCheckRuns,
1984
2370
  readThread: () => readThread
1985
2371
  });
1986
- import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
1987
- import { z as z2 } from "zod";
2372
+ import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2373
+ import { z as z3 } from "zod";
1988
2374
  function summarizeCiStatus(rollup) {
1989
2375
  if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
1990
2376
  let hasRunning = false;
@@ -2223,132 +2609,12 @@ function isDispatchGated(capability, mode) {
2223
2609
  function trustRefusal(capabilitySlug) {
2224
2610
  return `Not dispatched: capability \`${capabilitySlug ?? "?"}\` is in ASK mode (not trusted for autonomy). Do NOT retry the dispatch. Instead notify the operator (use recommend_to_operator, or rely on the tracking issue that already @-mentions them), then submit_state. To let this capability act on its own, grant it Auto on the dashboard Trust page.`;
2225
2611
  }
2226
- function dashboardBaseUrl() {
2227
- const raw = process.env.KODY_CMS_DASHBOARD_URL?.trim() || process.env.KODY_DASHBOARD_URL?.trim() || process.env.DASHBOARD_URL?.trim() || "";
2228
- if (!raw) return null;
2229
- try {
2230
- const parsed = new URL(raw);
2231
- return parsed.origin;
2232
- } catch {
2233
- return null;
2234
- }
2235
- }
2236
- function dashboardCmsToken() {
2237
- return process.env.KODY_CMS_TOKEN?.trim() || process.env.KODY_DASHBOARD_TOKEN?.trim() || process.env.KODY_TOKEN?.trim() || process.env.GH_TOKEN?.trim() || process.env.GITHUB_TOKEN?.trim() || process.env.GH_PAT?.trim() || null;
2238
- }
2239
- function cmsHeaders(opts) {
2240
- const token = dashboardCmsToken();
2241
- const [owner, repo] = opts.repoSlug.split("/");
2242
- if (!token) {
2243
- return {
2244
- ok: false,
2245
- error: "missing_cms_token",
2246
- message: "Set KODY_CMS_TOKEN, KODY_DASHBOARD_TOKEN, KODY_TOKEN, GH_TOKEN, GITHUB_TOKEN, or GH_PAT."
2247
- };
2248
- }
2249
- if (!owner || !repo) {
2250
- return { ok: false, error: "invalid_repo", message: `Invalid repo slug: ${opts.repoSlug}` };
2251
- }
2252
- return {
2253
- ok: true,
2254
- headers: {
2255
- "Content-Type": "application/json",
2256
- "x-kody-token": token,
2257
- "x-kody-owner": owner,
2258
- "x-kody-repo": repo
2259
- }
2260
- };
2261
- }
2262
- async function callDashboardCms(opts, path50, init = {}) {
2263
- const baseUrl = dashboardBaseUrl();
2264
- if (!baseUrl) {
2265
- return {
2266
- ok: false,
2267
- error: "missing_dashboard_url",
2268
- message: "Set KODY_CMS_DASHBOARD_URL or KODY_DASHBOARD_URL to the Dashboard origin."
2269
- };
2270
- }
2271
- const headerResult = cmsHeaders(opts);
2272
- if (!headerResult.ok) return headerResult;
2273
- try {
2274
- const res = await fetch(`${baseUrl}${path50}`, {
2275
- ...init,
2276
- headers: {
2277
- ...headerResult.headers,
2278
- ...init.headers
2279
- }
2280
- });
2281
- const contentType = res.headers.get("content-type") ?? "";
2282
- const data = contentType.includes("application/json") ? await res.json().catch(() => null) : await res.text().catch(() => "");
2283
- if (!res.ok) {
2284
- return {
2285
- ok: false,
2286
- status: res.status,
2287
- error: stringFieldFromRecord(data, "error") ?? "cms_request_failed",
2288
- message: stringFieldFromRecord(data, "message") ?? `Dashboard CMS request failed with ${res.status}.`,
2289
- data
2290
- };
2291
- }
2292
- return { ok: true, status: res.status, data };
2293
- } catch (err) {
2294
- return {
2295
- ok: false,
2296
- error: "cms_request_error",
2297
- message: err instanceof Error ? err.message : String(err)
2298
- };
2299
- }
2300
- }
2301
- function cmsToolResponse(result) {
2302
- return {
2303
- content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
2304
- ...result.ok ? {} : { isError: true }
2305
- };
2306
- }
2307
2612
  function assertCmsWriteAllowed(opts) {
2308
2613
  if (isDispatchGated(opts.capabilitySlug, readCapabilityTrustMode(opts.state, opts.repoSlug, opts.capabilitySlug))) {
2309
2614
  return trustRefusal(opts.capabilitySlug);
2310
2615
  }
2311
2616
  return null;
2312
2617
  }
2313
- function cmsQuery(args) {
2314
- const params = new URLSearchParams();
2315
- const q = stringArg(args.q);
2316
- if (q) params.set("q", q);
2317
- const limit = numberArg(args.limit);
2318
- if (limit !== void 0) params.set("limit", String(limit));
2319
- const offset = numberArg(args.offset);
2320
- if (offset !== void 0) params.set("offset", String(offset));
2321
- if (args.filters && typeof args.filters === "object" && !Array.isArray(args.filters)) {
2322
- params.set("filters", JSON.stringify(args.filters));
2323
- }
2324
- if (Array.isArray(args.sort)) {
2325
- const sort = args.sort.flatMap((entry) => {
2326
- if (!entry || typeof entry !== "object") return [];
2327
- const field = stringArg(entry.field);
2328
- if (!field) return [];
2329
- const direction = entry.direction === "asc" ? "asc" : "desc";
2330
- return [`${field}:${direction}`];
2331
- }).join(",");
2332
- if (sort) params.set("sort", sort);
2333
- }
2334
- const value = params.toString();
2335
- return value ? `?${value}` : "";
2336
- }
2337
- function stringArg(value) {
2338
- return typeof value === "string" ? value.trim() : "";
2339
- }
2340
- function numberArg(value) {
2341
- if (value === void 0 || value === null || value === "") return void 0;
2342
- const n = Number(value);
2343
- return Number.isFinite(n) ? n : void 0;
2344
- }
2345
- function documentArg(value) {
2346
- if (!value || typeof value !== "object" || Array.isArray(value)) return {};
2347
- return value;
2348
- }
2349
- function stringFieldFromRecord(value, field) {
2350
- return value && typeof value === "object" && !Array.isArray(value) && typeof value[field] === "string" ? String(value[field]) : void 0;
2351
- }
2352
2618
  function capabilityToolDefinitions(opts) {
2353
2619
  const workflowFile = opts.workflowFile ?? "kody.yml";
2354
2620
  const listTool = {
@@ -2371,7 +2637,7 @@ function capabilityToolDefinitions(opts) {
2371
2637
  name: `${verb.replace("-", "_")}_pr`,
2372
2638
  description: describe,
2373
2639
  inputSchema: {
2374
- pr: z2.number().int().positive().describe("PR number to repair.")
2640
+ pr: z3.number().int().positive().describe("PR number to repair.")
2375
2641
  },
2376
2642
  handler: async (args) => {
2377
2643
  const pr = Number(args.pr);
@@ -2399,8 +2665,8 @@ function capabilityToolDefinitions(opts) {
2399
2665
  name: "recommend_to_operator",
2400
2666
  description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a capability is in ASK mode and you want the operator to confirm via the dashboard inbox. The mention handle is substituted from kody.config.json `github.operators` \u2014 do not type it yourself.",
2401
2667
  inputSchema: {
2402
- pr: z2.number().int().positive().describe("PR number to comment on."),
2403
- body: z2.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
2668
+ pr: z3.number().int().positive().describe("PR number to comment on."),
2669
+ body: z3.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
2404
2670
  },
2405
2671
  handler: async (args) => {
2406
2672
  const pr = Number(args.pr);
@@ -2414,7 +2680,7 @@ function capabilityToolDefinitions(opts) {
2414
2680
  name: "read_ledger",
2415
2681
  description: "Read any sentinel-fenced JSON manifest stored on a labeled issue. Returns `{found, issueNumber, payload}` where payload is the parsed JSON between `<!-- <label>:start -->` and `<!-- <label>:end -->` sentinels.",
2416
2682
  inputSchema: {
2417
- label: z2.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
2683
+ label: z3.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
2418
2684
  },
2419
2685
  handler: async (args) => {
2420
2686
  const label = String(args.label ?? "");
@@ -2426,8 +2692,8 @@ function capabilityToolDefinitions(opts) {
2426
2692
  name: "read_check_runs",
2427
2693
  description: "Read CI for a branch or commit ref (e.g. 'dev'). Returns {sha, state, failing:[{name,conclusion,detailsUrl}], pending:[{name,status}]}. state is RED (\u22651 check has a terminal-failure conclusion: failure/timed_out/startup_failure/action_required), PENDING (none failed but some still running), or GREEN (all completed, none failed). Kody's own job check-runs (run/kody/capability-tick/\u2026) are excluded by default. This reads the commit's authoritative check-runs \u2014 use it instead of guessing CI health from a run list.",
2428
2694
  inputSchema: {
2429
- ref: z2.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
2430
- ignoreNames: z2.array(z2.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
2695
+ ref: z3.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
2696
+ ignoreNames: z3.array(z3.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
2431
2697
  },
2432
2698
  handler: async (args) => {
2433
2699
  const ref = String(args.ref ?? "");
@@ -2440,8 +2706,8 @@ function capabilityToolDefinitions(opts) {
2440
2706
  name: "read_thread",
2441
2707
  description: "Read an issue or PR's recent comments + labels + title/state. Returns {number, title, state, labels:[...], comments:[{author, createdAt, body}]} (newest last, body truncated). Use this to read a verdict a dispatched check posted back \u2014 e.g. qa-engineer's report or ui-review's PASS/CONCERNS/FAIL \u2014 on a later tick. Read-only; works for both issues AND PRs.",
2442
2708
  inputSchema: {
2443
- number: z2.number().int().positive().describe("Issue or PR number to read."),
2444
- limit: z2.number().int().positive().optional().describe("Max recent comments to return (default 10).")
2709
+ number: z3.number().int().positive().describe("Issue or PR number to read."),
2710
+ limit: z3.number().int().positive().optional().describe("Max recent comments to return (default 10).")
2445
2711
  },
2446
2712
  handler: async (args) => {
2447
2713
  const number = Number(args.number);
@@ -2454,11 +2720,11 @@ function capabilityToolDefinitions(opts) {
2454
2720
  name: "ensure_issue",
2455
2721
  description: "Idempotently ensure ONE open tracking issue exists for `key`. Searches OPEN issues (issues API, not the laggy search index) for `key`'s hidden marker; if found, returns {created:false, number} and creates NOTHING; otherwise creates the issue (title + body, marker appended) and returns {created:true, number}. This is the anti-duplication primitive: use one stable `key` per recurring finding so re-ticks reuse the same issue. Only take follow-up actions (dispatch/comment) when created===true.",
2456
2722
  inputSchema: {
2457
- key: z2.string().min(1).describe(
2723
+ key: z3.string().min(1).describe(
2458
2724
  "Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
2459
2725
  ),
2460
- title: z2.string().min(1).describe("Issue title (used only on first creation)."),
2461
- body: z2.string().min(1).describe(
2726
+ title: z3.string().min(1).describe("Issue title (used only on first creation)."),
2727
+ body: z3.string().min(1).describe(
2462
2728
  "Issue body markdown (used only on first creation). Include the operator mention verbatim if the capability body has one."
2463
2729
  )
2464
2730
  },
@@ -2474,9 +2740,9 @@ function capabilityToolDefinitions(opts) {
2474
2740
  name: "ensure_comment",
2475
2741
  description: "Idempotently post ONE comment on an issue for `key`. If a comment carrying `key`'s marker already exists on the issue, returns {posted:false}; otherwise posts the comment (marker appended) and returns {posted:true}. Use for a notify/audit comment that must appear exactly once.",
2476
2742
  inputSchema: {
2477
- issue: z2.number().int().positive().describe("Issue number to comment on."),
2478
- key: z2.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
2479
- body: z2.string().min(1).describe("Comment body markdown.")
2743
+ issue: z3.number().int().positive().describe("Issue number to comment on."),
2744
+ key: z3.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
2745
+ body: z3.string().min(1).describe("Comment body markdown.")
2480
2746
  },
2481
2747
  handler: async (args) => {
2482
2748
  const issue = Number(args.issue);
@@ -2490,9 +2756,9 @@ function capabilityToolDefinitions(opts) {
2490
2756
  name: "dispatch_workflow",
2491
2757
  description: "Dispatch a kody.yml workflow_dispatch run for a capability action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({capability:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
2492
2758
  inputSchema: {
2493
- capability: z2.string().min(1).optional().describe("Capability action to run (e.g. 'run')."),
2494
- executable: z2.string().min(1).optional().describe("Deprecated alias for capability."),
2495
- issueNumber: z2.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
2759
+ capability: z3.string().min(1).optional().describe("Capability action to run (e.g. 'run')."),
2760
+ executable: z3.string().min(1).optional().describe("Deprecated alias for capability."),
2761
+ issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
2496
2762
  },
2497
2763
  handler: async (args) => {
2498
2764
  const capability = String(args.capability ?? args.executable ?? "");
@@ -2505,86 +2771,10 @@ function capabilityToolDefinitions(opts) {
2505
2771
  return { content: [{ type: "text", text }] };
2506
2772
  }
2507
2773
  };
2508
- const cmsListCollectionsTool = {
2509
- name: "cms_list_collections",
2510
- description: "List configured Dashboard CMS collections and their supported operations. Read-only.",
2511
- inputSchema: {},
2512
- handler: async () => cmsToolResponse(await callDashboardCms(opts, "/api/kody/cms"))
2513
- };
2514
- const cmsListDocumentsTool = {
2515
- name: "cms_list_documents",
2516
- description: "List or search Dashboard CMS documents from one configured collection. Read-only.",
2517
- inputSchema: {
2518
- collection: z2.string().min(1).describe("CMS collection name."),
2519
- q: z2.string().optional().describe("Optional search query."),
2520
- filters: z2.record(z2.string(), z2.unknown()).optional().describe("Optional filter object keyed by field."),
2521
- sort: z2.array(
2522
- z2.object({
2523
- field: z2.string().min(1),
2524
- direction: z2.enum(["asc", "desc"]).default("desc")
2525
- })
2526
- ).optional(),
2527
- limit: z2.number().int().min(1).max(100).optional(),
2528
- offset: z2.number().int().min(0).optional()
2529
- },
2530
- handler: async (args) => {
2531
- const collection = encodeURIComponent(stringArg(args.collection));
2532
- return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}${cmsQuery(args)}`));
2533
- }
2534
- };
2535
- const cmsGetDocumentTool = {
2536
- name: "cms_get_document",
2537
- description: "Get one Dashboard CMS document by collection and id. Read-only.",
2538
- inputSchema: {
2539
- collection: z2.string().min(1).describe("CMS collection name."),
2540
- id: z2.string().min(1).describe("Document id.")
2541
- },
2542
- handler: async (args) => {
2543
- const collection = encodeURIComponent(stringArg(args.collection));
2544
- const id = encodeURIComponent(stringArg(args.id));
2545
- return cmsToolResponse(await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`));
2546
- }
2547
- };
2548
- const cmsCreateDocumentTool = {
2549
- name: "cms_create_document",
2550
- description: "Create one Dashboard CMS document when the capability is trusted and the CMS collection allows create.",
2551
- inputSchema: {
2552
- collection: z2.string().min(1).describe("CMS collection name."),
2553
- data: z2.record(z2.string(), z2.unknown()).describe("Document fields to create.")
2554
- },
2555
- handler: async (args) => {
2556
- const refusal = assertCmsWriteAllowed(opts);
2557
- if (refusal) return { content: [{ type: "text", text: refusal }] };
2558
- const collection = encodeURIComponent(stringArg(args.collection));
2559
- return cmsToolResponse(
2560
- await callDashboardCms(opts, `/api/kody/cms/${collection}`, {
2561
- method: "POST",
2562
- body: JSON.stringify(documentArg(args.data))
2563
- })
2564
- );
2565
- }
2566
- };
2567
- const cmsUpdateDocumentTool = {
2568
- name: "cms_update_document",
2569
- description: "Update one Dashboard CMS document when the capability is trusted and the CMS collection allows update.",
2570
- inputSchema: {
2571
- collection: z2.string().min(1).describe("CMS collection name."),
2572
- id: z2.string().min(1).describe("Document id."),
2573
- data: z2.record(z2.string(), z2.unknown()).describe("Partial document fields to update.")
2574
- },
2575
- handler: async (args) => {
2576
- const refusal = assertCmsWriteAllowed(opts);
2577
- if (refusal) return { content: [{ type: "text", text: refusal }] };
2578
- const collection = encodeURIComponent(stringArg(args.collection));
2579
- const id = encodeURIComponent(stringArg(args.id));
2580
- return cmsToolResponse(
2581
- await callDashboardCms(opts, `/api/kody/cms/${collection}/${id}`, {
2582
- method: "PATCH",
2583
- body: JSON.stringify(documentArg(args.data))
2584
- })
2585
- );
2586
- }
2587
- };
2774
+ const cmsTools = dashboardCmsToolDefinitions({
2775
+ repoSlug: opts.repoSlug,
2776
+ assertWriteAllowed: () => assertCmsWriteAllowed(opts)
2777
+ });
2588
2778
  return [
2589
2779
  listTool,
2590
2780
  syncTool,
@@ -2597,24 +2787,20 @@ function capabilityToolDefinitions(opts) {
2597
2787
  ensureIssueTool,
2598
2788
  ensureCommentTool,
2599
2789
  dispatchTool,
2600
- cmsListCollectionsTool,
2601
- cmsListDocumentsTool,
2602
- cmsGetDocumentTool,
2603
- cmsCreateDocumentTool,
2604
- cmsUpdateDocumentTool
2790
+ ...cmsTools
2605
2791
  ];
2606
2792
  }
2607
2793
  function buildCapabilityMcpServer(opts) {
2608
2794
  const definitions = capabilityToolDefinitions(opts);
2609
2795
  const tools = definitions.map(
2610
- (def) => tool3(
2796
+ (def) => tool4(
2611
2797
  def.name,
2612
2798
  def.description,
2613
2799
  def.inputSchema,
2614
2800
  async (args) => def.handler(args)
2615
2801
  )
2616
2802
  );
2617
- const server = createSdkMcpServer3({
2803
+ const server = createSdkMcpServer4({
2618
2804
  name: "kody-capability",
2619
2805
  version: "0.1.0",
2620
2806
  tools
@@ -2625,6 +2811,7 @@ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, THREAD_BODY_MAX, CHECK_
2625
2811
  var init_capabilityMcp = __esm({
2626
2812
  "src/capabilityMcp.ts"() {
2627
2813
  "use strict";
2814
+ init_dashboardCmsMcp();
2628
2815
  init_issue();
2629
2816
  init_registry();
2630
2817
  init_stateRepo();
@@ -2649,11 +2836,7 @@ var init_capabilityMcp = __esm({
2649
2836
  "ensure_issue",
2650
2837
  "ensure_comment",
2651
2838
  "dispatch_workflow",
2652
- "cms_list_collections",
2653
- "cms_list_documents",
2654
- "cms_get_document",
2655
- "cms_create_document",
2656
- "cms_update_document"
2839
+ ...DASHBOARD_CMS_MCP_TOOL_NAMES
2657
2840
  ];
2658
2841
  }
2659
2842
  });
@@ -2731,8 +2914,8 @@ __export(fetchRepoMcp_exports, {
2731
2914
  buildFetchRepoMcpServer: () => buildFetchRepoMcpServer,
2732
2915
  fetchRepoToolDefinition: () => fetchRepoToolDefinition
2733
2916
  });
2734
- import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2735
- import { z as z3 } from "zod";
2917
+ import { createSdkMcpServer as createSdkMcpServer5, tool as tool5 } from "@anthropic-ai/claude-agent-sdk";
2918
+ import { z as z4 } from "zod";
2736
2919
  function fetchRepoToolDefinition(opts) {
2737
2920
  return {
2738
2921
  name: "fetch_repo",
@@ -2767,10 +2950,10 @@ Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in you
2767
2950
  }
2768
2951
  function buildFetchRepoMcpServer(opts) {
2769
2952
  const def = fetchRepoToolDefinition(opts);
2770
- const fetchTool = tool4(def.name, def.description, def.inputSchema, async (args) => {
2953
+ const fetchTool = tool5(def.name, def.description, def.inputSchema, async (args) => {
2771
2954
  return def.handler({ repo: String(args.repo ?? "") });
2772
2955
  });
2773
- return createSdkMcpServer4({
2956
+ return createSdkMcpServer5({
2774
2957
  name: "kody-fetch-repo",
2775
2958
  version: "0.1.0",
2776
2959
  tools: [fetchTool]
@@ -2783,7 +2966,7 @@ var init_fetchRepoMcp = __esm({
2783
2966
  init_repoWorkspace();
2784
2967
  DESCRIPTION3 = 'Clone another GitHub repository into your workspace so you can read and work on it. Pass `repo` as "owner/name" (e.g. "A-Guy-educ/A-Guy"). Returns the absolute path of the clone \u2014 then use your Read/Grep/Glob/Bash tools at that path to inspect it. Already-fetched repos are reused instantly. Use this whenever the user asks about a repository other than your current one \u2014 you are NOT limited to a single repo.';
2785
2968
  INPUT_SCHEMA3 = {
2786
- repo: z3.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
2969
+ repo: z4.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
2787
2970
  };
2788
2971
  }
2789
2972
  });
@@ -2867,7 +3050,7 @@ async function runAgent(opts) {
2867
3050
  const turnTimeoutMs = resolveTurnTimeoutMs(opts);
2868
3051
  let outcome = "failed";
2869
3052
  let outcomeKind = "generic_failed";
2870
- let errorMessage;
3053
+ let errorMessage2;
2871
3054
  let tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
2872
3055
  let messageCount = 0;
2873
3056
  let finalText = "";
@@ -2883,7 +3066,7 @@ async function runAgent(opts) {
2883
3066
  const resultTexts = [];
2884
3067
  outcome = "failed";
2885
3068
  outcomeKind = "generic_failed";
2886
- errorMessage = void 0;
3069
+ errorMessage2 = void 0;
2887
3070
  tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
2888
3071
  messageCount = 0;
2889
3072
  let sawMutatingTool = false;
@@ -2941,6 +3124,24 @@ async function runAgent(opts) {
2941
3124
  });
2942
3125
  mcpEntries.push(["kody-capability", dutyHandle.server]);
2943
3126
  }
3127
+ if (opts.enableDashboardCmsTool) {
3128
+ const { buildDashboardCmsMcpServer: buildDashboardCmsMcpServer2, DASHBOARD_CMS_MCP_TOOL_NAMES: DASHBOARD_CMS_MCP_TOOL_NAMES2 } = await Promise.resolve().then(() => (init_dashboardCmsMcp(), dashboardCmsMcp_exports));
3129
+ if (!opts.cmsRepoSlug) {
3130
+ throw new Error("enableDashboardCmsTool requires cmsRepoSlug (owner/name)");
3131
+ }
3132
+ const cmsHandle = buildDashboardCmsMcpServer2({
3133
+ repoSlug: opts.cmsRepoSlug,
3134
+ dashboardUrl: opts.cmsDashboardUrl,
3135
+ token: opts.cmsToken,
3136
+ storeRepoUrl: opts.cmsStoreRepoUrl,
3137
+ storeRef: opts.cmsStoreRef
3138
+ });
3139
+ mcpEntries.push(["kody-cms", cmsHandle.server]);
3140
+ for (const toolName of DASHBOARD_CMS_MCP_TOOL_NAMES2) {
3141
+ ;
3142
+ queryOptions.allowedTools.push(`mcp__kody-cms__${toolName}`);
3143
+ }
3144
+ }
2944
3145
  if (opts.enableFetchRepoTool && opts.reposRoot) {
2945
3146
  const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
2946
3147
  const fetchServer = buildFetchRepoMcpServer2({
@@ -3021,7 +3222,7 @@ async function runAgent(opts) {
3021
3222
  if (timedOut) {
3022
3223
  outcome = "failed";
3023
3224
  outcomeKind = "stalled";
3024
- errorMessage = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
3225
+ errorMessage2 = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
3025
3226
  if (typeof iterator.return === "function") {
3026
3227
  try {
3027
3228
  await iterator.return(void 0);
@@ -3125,17 +3326,17 @@ async function runAgent(opts) {
3125
3326
  } else {
3126
3327
  outcome = "failed";
3127
3328
  outcomeKind = classifySubtype(m.subtype);
3128
- errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
3329
+ errorMessage2 = `result subtype: ${m.subtype ?? "unknown"}`;
3129
3330
  }
3130
3331
  }
3131
3332
  }
3132
3333
  } catch (e) {
3133
3334
  if (sawTerminalSuccess) {
3134
- errorMessage = e instanceof Error ? e.message : String(e);
3335
+ errorMessage2 = e instanceof Error ? e.message : String(e);
3135
3336
  } else {
3136
3337
  outcome = "failed";
3137
3338
  outcomeKind = "model_error";
3138
- errorMessage = e instanceof Error ? e.message : String(e);
3339
+ errorMessage2 = e instanceof Error ? e.message : String(e);
3139
3340
  }
3140
3341
  } finally {
3141
3342
  try {
@@ -3153,7 +3354,7 @@ async function runAgent(opts) {
3153
3354
  if (outcome === "completed" && sawLoginRequired) {
3154
3355
  outcome = "failed";
3155
3356
  outcomeKind = "model_error";
3156
- errorMessage = "Claude Code reported it is not logged in; refusing to mark agent run successful";
3357
+ errorMessage2 = "Claude Code reported it is not logged in; refusing to mark agent run successful";
3157
3358
  }
3158
3359
  if (outcome === "completed" && !sawMutatingTool) {
3159
3360
  const backendDead = opts.isBackendHealthy ? !await opts.isBackendHealthy() : false;
@@ -3162,14 +3363,14 @@ async function runAgent(opts) {
3162
3363
  outcome = "failed";
3163
3364
  outcomeKind = "model_error";
3164
3365
  noWorkSuccess = true;
3165
- errorMessage = errorMessage ?? (backendDead ? "model backend unreachable after a reported success \u2014 proxy crashed mid-request (hollow success)" : "session reported success but produced no model output (0 output tokens) \u2014 backend likely unreachable");
3366
+ errorMessage2 = errorMessage2 ?? (backendDead ? "model backend unreachable after a reported success \u2014 proxy crashed mid-request (hollow success)" : "session reported success but produced no model output (0 output tokens) \u2014 backend likely unreachable");
3166
3367
  }
3167
3368
  }
3168
- const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage) || noWorkSuccess);
3369
+ const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage2) || noWorkSuccess);
3169
3370
  if (!shouldRetry) break;
3170
3371
  const delayMs = CONNECTION_RETRY_BASE_MS * 2 ** attempt;
3171
3372
  process.stderr.write(
3172
- `[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage}
3373
+ `[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage2}
3173
3374
  `
3174
3375
  );
3175
3376
  if (opts.ensureBackend) {
@@ -3188,7 +3389,7 @@ async function runAgent(opts) {
3188
3389
  outcomeKind,
3189
3390
  finalText,
3190
3391
  ...submittedState ? { submittedState } : {},
3191
- error: errorMessage,
3392
+ error: errorMessage2,
3192
3393
  ndjsonPath,
3193
3394
  durationMs: Date.now() - startedAt,
3194
3395
  tokens,
@@ -5813,7 +6014,7 @@ var init_litellm = __esm({
5813
6014
 
5814
6015
  // src/pushWithRetry.ts
5815
6016
  import { execFileSync as execFileSync6 } from "child_process";
5816
- function sleepSync(ms) {
6017
+ function sleepSync2(ms) {
5817
6018
  if (ms <= 0) return;
5818
6019
  Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
5819
6020
  }
@@ -5874,7 +6075,7 @@ function pushWithRetry(opts = {}) {
5874
6075
  };
5875
6076
  }
5876
6077
  const delay = Math.min(baseBackoff * 2 ** (attempt - 1), MAX_BACKOFF_MS);
5877
- sleepSync(delay);
6078
+ sleepSync2(delay);
5878
6079
  }
5879
6080
  return {
5880
6081
  ok: false,
@@ -6119,6 +6320,13 @@ function jobMetaFromData(data) {
6119
6320
  why: typeof data.jobWhy === "string" ? data.jobWhy : void 0
6120
6321
  };
6121
6322
  }
6323
+ function applyStandaloneTerminalState(state, ctx, profile) {
6324
+ if (profile.lifecycleConfig?.finalize !== true || state.flow?.issueNumber) return;
6325
+ const delivered = ctx.output.exitCode === 0 && !!state.core.prUrl;
6326
+ state.core.phase = delivered ? "shipped" : "failed";
6327
+ state.core.status = delivered ? "succeeded" : "failed";
6328
+ state.core.currentExecutable = null;
6329
+ }
6122
6330
  function synthesizeAction(ctx) {
6123
6331
  const ok = ctx.output.exitCode === 0;
6124
6332
  return {
@@ -6149,6 +6357,7 @@ var init_saveTaskState = __esm({
6149
6357
  });
6150
6358
  if (ctx.output.prUrl) next.core.prUrl = ctx.output.prUrl;
6151
6359
  if (typeof ctx.data.runUrl === "string") next.core.runUrl = ctx.data.runUrl;
6360
+ applyStandaloneTerminalState(next, ctx, profile);
6152
6361
  writeTaskState(target, number, next, ctx.cwd, ctx.config);
6153
6362
  ctx.data.taskState = next;
6154
6363
  ctx.data.taskStateRendered = renderStateComment(next);
@@ -11478,12 +11687,9 @@ var init_finalizeTerminal = __esm({
11478
11687
  const issueNumber = ctx.args.issue;
11479
11688
  const targetNumber = ctx.data.commentTargetNumber ?? issueNumber;
11480
11689
  if (!targetNumber) return;
11481
- let prUrl;
11482
- try {
11483
- prUrl = readTaskState(target, targetNumber, ctx.cwd, ctx.config).core.prUrl;
11484
- } catch {
11485
- prUrl = void 0;
11486
- }
11690
+ const cachedState = ctx.data.taskState;
11691
+ let state = cachedState;
11692
+ const prUrl = cachedState?.core.prUrl ?? ctx.output.prUrl ?? ctx.data.prResult?.url;
11487
11693
  const delivered = ctx.output.exitCode === 0 && !!prUrl;
11488
11694
  const spec = delivered ? DONE : FAILED;
11489
11695
  const phase = delivered ? "shipped" : "failed";
@@ -11491,12 +11697,28 @@ var init_finalizeTerminal = __esm({
11491
11697
  if (issueNumber) setKodyLabel(issueNumber, spec, ctx.cwd);
11492
11698
  const prNumber = prUrl ? parsePrNumber(prUrl) : null;
11493
11699
  if (prNumber && prNumber !== issueNumber) setKodyLabel(prNumber, spec, ctx.cwd);
11700
+ if (!state) {
11701
+ try {
11702
+ state = readTaskState(target, targetNumber, ctx.cwd, ctx.config);
11703
+ } catch {
11704
+ state = void 0;
11705
+ }
11706
+ }
11707
+ if (!state) return;
11708
+ const alreadyTerminal = state.core.phase === phase && state.core.status === status && state.core.currentExecutable === null;
11709
+ if (alreadyTerminal) return;
11710
+ const next = {
11711
+ ...state,
11712
+ core: {
11713
+ ...state.core,
11714
+ phase,
11715
+ status,
11716
+ currentExecutable: null
11717
+ }
11718
+ };
11719
+ ctx.data.taskState = next;
11494
11720
  try {
11495
- const state = readTaskState(target, targetNumber, ctx.cwd, ctx.config);
11496
- state.core.phase = phase;
11497
- state.core.status = status;
11498
- state.core.currentExecutable = null;
11499
- writeTaskState(target, targetNumber, state, ctx.cwd, ctx.config);
11721
+ writeTaskState(target, targetNumber, next, ctx.cwd, ctx.config);
11500
11722
  } catch (err) {
11501
11723
  process.stderr.write(
11502
11724
  `[kody finalizeTerminal] failed to write terminal state on ${target} #${targetNumber}: ${err instanceof Error ? err.message : String(err)}
@@ -13161,17 +13383,22 @@ var init_mirrorStateToPr = __esm({
13161
13383
  const issueNumber = ctx.data.commentTargetNumber;
13162
13384
  const issueTarget = ctx.data.commentTargetType;
13163
13385
  if (!issueNumber || issueTarget !== "issue") return;
13164
- const prUrl = ctx.output.prUrl ?? ctx.data.prResult?.url;
13386
+ let state = ctx.data.taskState;
13387
+ const prUrl = ctx.output.prUrl ?? ctx.data.prResult?.url ?? state?.core.prUrl;
13165
13388
  if (!prUrl) return;
13166
13389
  const prNumber = parsePrNumber4(prUrl);
13167
13390
  if (!prNumber) return;
13168
- let state;
13169
- try {
13170
- state = readTaskState("issue", issueNumber, ctx.cwd, ctx.config);
13171
- } catch {
13172
- return;
13391
+ if (!state) {
13392
+ try {
13393
+ state = readTaskState("issue", issueNumber, ctx.cwd, ctx.config);
13394
+ } catch {
13395
+ return;
13396
+ }
13397
+ }
13398
+ if (prUrl && !state.core.prUrl) {
13399
+ state = { ...state, core: { ...state.core, prUrl } };
13400
+ ctx.data.taskState = state;
13173
13401
  }
13174
- if (prUrl && !state.core.prUrl) state.core.prUrl = prUrl;
13175
13402
  try {
13176
13403
  writeTaskState("pr", prNumber, state, ctx.cwd, ctx.config);
13177
13404
  } catch (err) {
@@ -17135,24 +17362,24 @@ function firstRequiredFailure(results, tools) {
17135
17362
  }
17136
17363
  return null;
17137
17364
  }
17138
- function verifyOne(tool5, cwd) {
17139
- const result = { name: tool5.name, present: false, verified: false };
17140
- const checkRes = runShell(tool5.install.checkCommand, cwd);
17365
+ function verifyOne(tool6, cwd) {
17366
+ const result = { name: tool6.name, present: false, verified: false };
17367
+ const checkRes = runShell(tool6.install.checkCommand, cwd);
17141
17368
  let present = checkRes.ok;
17142
- if (!present && tool5.install.installCommand) {
17143
- runShell(tool5.install.installCommand, cwd, 12e4);
17144
- present = runShell(tool5.install.checkCommand, cwd).ok;
17369
+ if (!present && tool6.install.installCommand) {
17370
+ runShell(tool6.install.installCommand, cwd, 12e4);
17371
+ present = runShell(tool6.install.checkCommand, cwd).ok;
17145
17372
  }
17146
17373
  result.present = present;
17147
17374
  if (!present) {
17148
- result.error = `tool "${tool5.name}" not on PATH (check: ${tool5.install.checkCommand})`;
17375
+ result.error = `tool "${tool6.name}" not on PATH (check: ${tool6.install.checkCommand})`;
17149
17376
  return result;
17150
17377
  }
17151
- const verifyRes = runShell(tool5.verify, cwd);
17378
+ const verifyRes = runShell(tool6.verify, cwd);
17152
17379
  result.verified = verifyRes.ok;
17153
17380
  if (!verifyRes.ok) {
17154
17381
  const tail = formatStderrTail(verifyRes.stderr, verifyRes.stdout);
17155
- result.error = `tool "${tool5.name}" failed verify: ${tool5.verify}${tail ? ` \u2014 ${tail}` : ""}`;
17382
+ result.error = `tool "${tool6.name}" failed verify: ${tool6.verify}${tail ? ` \u2014 ${tail}` : ""}`;
17156
17383
  }
17157
17384
  return result;
17158
17385
  }
@@ -18864,6 +19091,12 @@ var CROSS_REPO_PROMPT = [
18864
19091
  "instantly. When the user asks about a different repo \u2014 or to compare repos \u2014",
18865
19092
  "fetch it instead of saying you are scoped to a single repo."
18866
19093
  ].join("\n");
19094
+ var DASHBOARD_CMS_PROMPT = [
19095
+ "# Dashboard CMS",
19096
+ "Use Dashboard CMS tools for CMS content. The Dashboard is the source of truth.",
19097
+ "Do not query Mongo directly for CMS content and do not infer CMS data from repository files.",
19098
+ "Use cms_list_documents first when you need to find content, then pass the raw id or cmsDocumentId to cms_get_document."
19099
+ ].join("\n");
18867
19100
  function buildExecutableCatalog() {
18868
19101
  let discovered;
18869
19102
  try {
@@ -18922,6 +19155,8 @@ async function runChatTurn(opts) {
18922
19155
  const memoryBlock = readMemoryIndexBlock(opts.cwd);
18923
19156
  const instructionsBlock = readInstructionsBlock(opts.cwd);
18924
19157
  const crossRepoBlock = opts.reposRoot ? CROSS_REPO_PROMPT : null;
19158
+ const dashboardCmsEnabled = Boolean(opts.cmsDashboardUrl && opts.cmsRepoSlug && opts.cmsToken);
19159
+ const dashboardCmsBlock = dashboardCmsEnabled ? DASHBOARD_CMS_PROMPT : null;
18925
19160
  const imageBlock = imagePaths.length > 0 ? [
18926
19161
  "# Attached images",
18927
19162
  "The user attached one or more images on this turn. They are saved as",
@@ -18936,6 +19171,7 @@ async function runChatTurn(opts) {
18936
19171
  memoryBlock,
18937
19172
  instructionsBlock,
18938
19173
  crossRepoBlock,
19174
+ dashboardCmsBlock,
18939
19175
  imageBlock,
18940
19176
  catalog,
18941
19177
  artifactAddendum
@@ -18963,6 +19199,14 @@ async function runChatTurn(opts) {
18963
19199
  reposRoot: opts.reposRoot,
18964
19200
  repoToken: opts.repoToken
18965
19201
  } : {},
19202
+ ...dashboardCmsEnabled ? {
19203
+ enableDashboardCmsTool: true,
19204
+ cmsDashboardUrl: opts.cmsDashboardUrl,
19205
+ cmsRepoSlug: opts.cmsRepoSlug,
19206
+ cmsToken: opts.cmsToken,
19207
+ cmsStoreRepoUrl: opts.cmsStoreRepoUrl,
19208
+ cmsStoreRef: opts.cmsStoreRef
19209
+ } : {},
18966
19210
  onProgress: async (ev) => {
18967
19211
  progressSeq += 1;
18968
19212
  if (ev.kind === "thinking") {
@@ -19061,7 +19305,9 @@ function readMemoryIndexBlock(cwd) {
19061
19305
  }
19062
19306
  const trimmed = raw.trim();
19063
19307
  if (!trimmed) return "";
19064
- const body = trimmed.length > MAX_INDEX_BYTES ? trimmed.slice(0, MAX_INDEX_BYTES) + "\n\n_\u2026 (memory index truncated; use recall_search to read more)_" : trimmed;
19308
+ const body = trimmed.length > MAX_INDEX_BYTES ? `${trimmed.slice(0, MAX_INDEX_BYTES)}
19309
+
19310
+ _\u2026 (memory index truncated; use recall_search to read more)_` : trimmed;
19065
19311
  return [
19066
19312
  "# Project memory index (state repo `memory/INDEX.md`)",
19067
19313
  "",
@@ -20689,6 +20935,9 @@ async function handleChatTurn(req, res, chatId, opts) {
20689
20935
  }
20690
20936
  const repo = strField(body, "repo");
20691
20937
  const repoToken = strField(body, "repoToken");
20938
+ const dashboardUrl = strField(body, "dashboardUrl");
20939
+ const storeRepoUrl = strField(body, "storeRepoUrl");
20940
+ const storeRef = strField(body, "storeRef");
20692
20941
  let agentCwd;
20693
20942
  try {
20694
20943
  agentCwd = await ensureRepoCwd({
@@ -20765,7 +21014,14 @@ async function handleChatTurn(req, res, chatId, opts) {
20765
21014
  sink,
20766
21015
  // Let the agent clone + work on OTHER repos via the fetch_repo tool.
20767
21016
  reposRoot: opts.reposRoot,
20768
- repoToken
21017
+ repoToken,
21018
+ ...dashboardUrl && repo && stateToken ? {
21019
+ cmsDashboardUrl: dashboardUrl,
21020
+ cmsRepoSlug: repo,
21021
+ cmsToken: stateToken,
21022
+ cmsStoreRepoUrl: storeRepoUrl,
21023
+ cmsStoreRef: storeRef
21024
+ } : {}
20769
21025
  });
20770
21026
  } catch (err) {
20771
21027
  const errMsg3 = err instanceof Error ? err.message : String(err);
@@ -21022,7 +21278,10 @@ async function proxyToBrainServe(args) {
21022
21278
  body: JSON.stringify({
21023
21279
  message: args.body.message,
21024
21280
  ...args.body.repo ? { repo: args.body.repo } : {},
21025
- ...args.body.repoToken ? { repoToken: args.body.repoToken } : {}
21281
+ ...args.body.repoToken ? { repoToken: args.body.repoToken } : {},
21282
+ ...args.body.dashboardUrl ? { dashboardUrl: args.body.dashboardUrl } : {},
21283
+ ...args.body.storeRepoUrl ? { storeRepoUrl: args.body.storeRepoUrl } : {},
21284
+ ...args.body.storeRef ? { storeRef: args.body.storeRef } : {}
21026
21285
  })
21027
21286
  });
21028
21287
  if (!upstream.ok || !upstream.body) {
@@ -21237,15 +21496,15 @@ function buildMcpHttpServer(opts) {
21237
21496
  transports.set(route.path, transport);
21238
21497
  routes.set(route.path, route.name);
21239
21498
  const mcpServer = new McpServer({ name: route.name, version: route.version });
21240
- for (const tool5 of route.tools) {
21499
+ for (const tool6 of route.tools) {
21241
21500
  mcpServer.registerTool(
21242
- tool5.name,
21501
+ tool6.name,
21243
21502
  {
21244
- description: tool5.description,
21245
- inputSchema: tool5.inputSchema
21503
+ description: tool6.description,
21504
+ inputSchema: tool6.inputSchema
21246
21505
  },
21247
21506
  async (args) => {
21248
- const result = await tool5.handler(args);
21507
+ const result = await tool6.handler(args);
21249
21508
  return {
21250
21509
  content: result.content,
21251
21510
  ...result.isError ? { isError: true } : {}
@@ -22551,7 +22810,7 @@ var PERF_GUEST = {
22551
22810
  medium: { cpu_kind: "performance", cpus: 1, memory_mb: 2048 },
22552
22811
  high: { cpu_kind: "performance", cpus: 2, memory_mb: 4096 }
22553
22812
  };
22554
- function envInt(name, dflt) {
22813
+ function envInt2(name, dflt) {
22555
22814
  const v = Number(process.env[name]);
22556
22815
  return Number.isFinite(v) && v > 0 ? v : dflt;
22557
22816
  }
@@ -22671,10 +22930,10 @@ async function poolServe() {
22671
22930
  const region = process.env.POOL_REGION ?? "fra";
22672
22931
  const perf = process.env.POOL_PERF ?? "medium";
22673
22932
  const guest = PERF_GUEST[perf] ?? PERF_GUEST.medium;
22674
- const min = envInt("POOL_MIN", 2);
22675
- const runnerPort = envInt("RUNNER_PORT", 8080);
22676
- const apiPort = envInt("POOL_API_PORT", 4100);
22677
- const healthTimeoutMs = envInt("POOL_HEALTH_TIMEOUT_MS", 12e4);
22933
+ const min = envInt2("POOL_MIN", 2);
22934
+ const runnerPort = envInt2("RUNNER_PORT", 8080);
22935
+ const apiPort = envInt2("POOL_API_PORT", 4100);
22936
+ const healthTimeoutMs = envInt2("POOL_HEALTH_TIMEOUT_MS", 12e4);
22678
22937
  const registry = new PoolRegistry({
22679
22938
  githubToken,
22680
22939
  masterKey: master,
@@ -22690,12 +22949,12 @@ async function poolServe() {
22690
22949
  },
22691
22950
  log
22692
22951
  });
22693
- const refillMs = envInt("POOL_REFILL_INTERVAL_MS", 6e4);
22952
+ const refillMs = envInt2("POOL_REFILL_INTERVAL_MS", 6e4);
22694
22953
  const tick = setInterval(() => {
22695
22954
  registry.resyncAll().catch((err) => log(`resync tick failed: ${err instanceof Error ? err.message : String(err)}`));
22696
22955
  }, refillMs);
22697
22956
  const dutyTickEnabled = (process.env.POOL_CAPABILITY_TICK ?? "1") !== "0";
22698
- const dutyTickMs = envInt("POOL_CAPABILITY_TICK_MS", 15 * 6e4);
22957
+ const dutyTickMs = envInt2("POOL_CAPABILITY_TICK_MS", 15 * 6e4);
22699
22958
  const dutyTick = dutyTickEnabled ? setInterval(() => {
22700
22959
  runCapabilityFallbackTick({
22701
22960
  isDegraded: () => gitHubActionsDegraded(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.277",
3
+ "version": "0.4.278",
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",