@kody-ade/kody-engine 0.4.250 → 0.4.251

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 +1422 -1372
  2. package/package.json +22 -23
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.250",
18
+ version: "0.4.251",
19
19
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative agentAction profiles.",
20
20
  license: "MIT",
21
21
  type: "module",
@@ -165,15 +165,18 @@ function gh(args, options) {
165
165
  }).trim();
166
166
  }
167
167
  function getIssue(issueNumber, cwd) {
168
- const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels"], { cwd });
168
+ const output = gh(["issue", "view", String(issueNumber), "--json", "number,title,body,comments,labels,url"], { cwd });
169
169
  const parsed = JSON.parse(output);
170
170
  if (typeof parsed?.title !== "string") {
171
171
  throw new Error(`Issue #${issueNumber}: unexpected response shape`);
172
172
  }
173
+ const url = typeof parsed.url === "string" ? parsed.url : void 0;
173
174
  return {
174
175
  number: parsed.number ?? issueNumber,
175
176
  title: parsed.title,
176
177
  body: parsed.body ?? "",
178
+ url,
179
+ isPullRequest: typeof url === "string" && /\/pull\/\d+$/.test(url),
177
180
  comments: (parsed.comments ?? []).map((c) => ({
178
181
  body: c.body ?? "",
179
182
  author: c.author?.login ?? "unknown",
@@ -1336,1482 +1339,1521 @@ var init_submitMcp = __esm({
1336
1339
  }
1337
1340
  });
1338
1341
 
1339
- // src/agent-responsibilityMcp.ts
1340
- var agent_responsibilityMcp_exports = {};
1341
- __export(agent_responsibilityMcp_exports, {
1342
- AGENT_RESPONSIBILITY_MCP_TOOL_NAMES: () => AGENT_RESPONSIBILITY_MCP_TOOL_NAMES,
1343
- agentResponsibilityToolDefinitions: () => agentResponsibilityToolDefinitions,
1344
- buildAgentResponsibilityMcpServer: () => buildAgentResponsibilityMcpServer,
1345
- dispatchWorkflow: () => dispatchWorkflow,
1346
- ensureComment: () => ensureComment,
1347
- ensureIssue: () => ensureIssue,
1348
- isDispatchGated: () => isDispatchGated,
1349
- parseAgentResponsibilityTrustMode: () => parseAgentResponsibilityTrustMode,
1350
- readAgentResponsibilityTrustMode: () => readAgentResponsibilityTrustMode,
1351
- readCheckRuns: () => readCheckRuns,
1352
- readThread: () => readThread
1353
- });
1354
- import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
1355
- import { z as z2 } from "zod";
1356
- function summarizeCiStatus(rollup) {
1357
- if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
1358
- let hasRunning = false;
1359
- for (const check of rollup) {
1360
- const status = String(check.status ?? "").toUpperCase();
1361
- const conclusion = String(check.conclusion ?? "").toUpperCase();
1362
- if (FAIL_CONCLUSIONS.has(conclusion)) return "FAILING";
1363
- if (!conclusion && RUNNING_STATUSES.has(status)) hasRunning = true;
1364
- }
1365
- return hasRunning ? "RUNNING" : "PASSING";
1366
- }
1367
- function computeBehindBy(repoSlug, base, head) {
1342
+ // src/agent-responsibilityFolders.ts
1343
+ import * as fs4 from "fs";
1344
+ import * as path6 from "path";
1345
+ function listAgentResponsibilityFolderSlugs(absDir) {
1346
+ if (!fs4.existsSync(absDir)) return [];
1347
+ let entries;
1368
1348
  try {
1369
- const raw = gh(["api", `repos/${repoSlug}/compare/${base}...${head}`, "--jq", ".behind_by"]);
1370
- const n = Number(raw.trim());
1371
- return Number.isFinite(n) ? n : -1;
1349
+ entries = fs4.readdirSync(absDir, { withFileTypes: true });
1372
1350
  } catch {
1373
- return -1;
1351
+ return [];
1374
1352
  }
1353
+ return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).filter((e) => isAgentResponsibilityFolder(path6.join(absDir, e.name))).map((e) => e.name).sort();
1375
1354
  }
1376
- function listRepairCandidates(repoSlug) {
1377
- const raw = gh([
1378
- "pr",
1379
- "list",
1380
- "--state",
1381
- "open",
1382
- "--limit",
1383
- "100",
1384
- "--json",
1385
- "number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt"
1386
- ]);
1387
- const prs = JSON.parse(raw);
1388
- return prs.filter((p) => !p.isDraft).map((p) => {
1389
- const ciStatus = summarizeCiStatus(p.statusCheckRollup);
1390
- const mergeable = String(p.mergeable ?? "UNKNOWN").toUpperCase();
1391
- const behindBy = mergeable === "CONFLICTING" || ciStatus === "FAILING" ? 0 : computeBehindBy(repoSlug, p.baseRefName, p.headRefName);
1355
+ function isAgentResponsibilityFolder(dir) {
1356
+ return fs4.existsSync(path6.join(dir, AGENT_RESPONSIBILITY_PROFILE_FILE)) && fs4.existsSync(path6.join(dir, AGENT_RESPONSIBILITY_BODY_FILE));
1357
+ }
1358
+ function readAgentResponsibilityFolder(root, slug2) {
1359
+ const dir = path6.join(root, slug2);
1360
+ const profilePath = path6.join(dir, AGENT_RESPONSIBILITY_PROFILE_FILE);
1361
+ const bodyPath = path6.join(dir, AGENT_RESPONSIBILITY_BODY_FILE);
1362
+ if (!fs4.existsSync(profilePath) || !fs4.statSync(profilePath).isFile()) return null;
1363
+ if (!fs4.existsSync(bodyPath) || !fs4.statSync(bodyPath).isFile()) return null;
1364
+ try {
1365
+ const rawProfile = JSON.parse(fs4.readFileSync(profilePath, "utf-8"));
1366
+ const rawBody = fs4.readFileSync(bodyPath, "utf-8");
1367
+ const { title, body } = parseAgentResponsibilityBody(rawBody, slug2);
1392
1368
  return {
1393
- number: p.number,
1394
- title: p.title,
1395
- headSha: p.headRefOid,
1396
- baseRef: p.baseRefName,
1397
- isDraft: false,
1398
- mergeable,
1399
- ciStatus,
1400
- behindBy,
1401
- updatedAt: p.updatedAt
1369
+ slug: slug2,
1370
+ dir,
1371
+ profilePath,
1372
+ bodyPath,
1373
+ title,
1374
+ body,
1375
+ rawBody,
1376
+ config: parseAgentResponsibilityConfig(rawProfile),
1377
+ rawProfile
1402
1378
  };
1403
- });
1379
+ } catch {
1380
+ return null;
1381
+ }
1404
1382
  }
1405
- function dispatchVerb(workflowFile, agentResponsibility, prNumber) {
1406
- return dispatchWorkflow(workflowFile, agentResponsibility, prNumber);
1383
+ function parseAgentResponsibilityConfig(raw) {
1384
+ const tools = stringList(raw.tools ?? raw.agentResponsibilityTools);
1385
+ return {
1386
+ action: stringField(raw.action),
1387
+ agentAction: stringField(raw.agentAction),
1388
+ tickScript: stringField(raw.tickScript),
1389
+ disabled: typeof raw.disabled === "boolean" ? raw.disabled : void 0,
1390
+ agent: stringField(raw.agent),
1391
+ mentions: stringList(raw.mentions).map((m) => m.replace(/^@/, "")),
1392
+ tools,
1393
+ agentActions: stringList(raw.agentActions),
1394
+ capabilityKind: capabilityKindField(raw.capabilityKind),
1395
+ describe: stringField(raw.describe),
1396
+ stage: stringField(raw.stage),
1397
+ readsFrom: stringList(raw.readsFrom ?? raw.reads_from),
1398
+ writesTo: stringList(raw.writesTo ?? raw.writes_to)
1399
+ };
1407
1400
  }
1408
- function postRecommendation(prNumber, mention, message, agentResponsibilitySlug) {
1409
- const mentioned = mention ? `${mention} ${message}` : message;
1410
- const body = agentResponsibilitySlug ? `${mentioned}
1411
-
1412
- <!-- kody-agentResponsibility: ${agentResponsibilitySlug} -->` : mentioned;
1413
- try {
1414
- gh(["pr", "comment", String(prNumber), "--body", body]);
1415
- return { ok: true };
1416
- } catch (err) {
1417
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
1418
- }
1401
+ function parseAgentResponsibilityBody(raw, slug2) {
1402
+ const trimmed = raw.trim();
1403
+ const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
1404
+ const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
1405
+ const title = h1 ? h1[1].trim() : humanizeSlug(slug2);
1406
+ const body = stripLeadingH1(raw);
1407
+ return { title, body };
1419
1408
  }
1420
- function readLedger(label) {
1421
- const startTag = `<!-- ${label}:start -->`;
1422
- const endTag = `<!-- ${label}:end -->`;
1423
- try {
1424
- const raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "5", "--json", "number,body"]);
1425
- const issues = JSON.parse(raw);
1426
- if (issues.length === 0) return { found: false, payload: null };
1427
- const issue = issues.sort((a, b) => a.number - b.number)[0];
1428
- const body = issue?.body ?? "";
1429
- const startIdx = body.indexOf(startTag);
1430
- const endIdx = body.indexOf(endTag);
1431
- if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
1432
- return { found: true, issueNumber: issue?.number, payload: null };
1433
- }
1434
- const between = body.slice(startIdx + startTag.length, endIdx);
1435
- const fenceMatch = between.match(/```json\s*([\s\S]*?)```/);
1436
- if (!fenceMatch) return { found: true, issueNumber: issue?.number, payload: null };
1437
- try {
1438
- return { found: true, issueNumber: issue?.number, payload: JSON.parse(fenceMatch[1]) };
1439
- } catch {
1440
- return { found: true, issueNumber: issue?.number, payload: null };
1441
- }
1442
- } catch (err) {
1443
- return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
1409
+ function stripLeadingH1(raw) {
1410
+ const lines = raw.replace(/^\uFEFF/, "").split("\n");
1411
+ let i = 0;
1412
+ for (; ; ) {
1413
+ while (i < lines.length && lines[i].trim() === "") i++;
1414
+ if (i < lines.length && /^#\s+.+/.test(lines[i])) i++;
1415
+ else break;
1444
1416
  }
1417
+ return lines.slice(i).join("\n");
1445
1418
  }
1446
- function parseAgentResponsibilityTrustMode(rawJson, agentResponsibilitySlug) {
1447
- try {
1448
- const parsed = JSON.parse(rawJson);
1449
- return parsed?.agentResponsibilities?.[agentResponsibilitySlug]?.mode === "auto" ? "auto" : "ask";
1450
- } catch {
1451
- return "ask";
1419
+ function humanizeSlug(slug2) {
1420
+ return slug2.split(/[-_]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
1421
+ }
1422
+ function stringField(value) {
1423
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
1424
+ }
1425
+ function stringList(value) {
1426
+ if (Array.isArray(value)) {
1427
+ return value.map((v) => String(v).trim()).filter(Boolean);
1452
1428
  }
1429
+ if (typeof value === "string") {
1430
+ return value.split(",").map((v) => v.trim()).filter(Boolean);
1431
+ }
1432
+ return [];
1453
1433
  }
1454
- function defaultStateForRepoSlug(repoSlug) {
1455
- const [owner, repo] = repoSlug.split("/");
1456
- return { repo: `${owner}/kody-state`, path: repo ?? repoSlug };
1434
+ function capabilityKindField(value) {
1435
+ if (value === "observe" || value === "act" || value === "verify") return value;
1436
+ return void 0;
1457
1437
  }
1458
- function readAgentResponsibilityTrustMode(stateOrRepoSlug, repoSlugOrAgentResponsibilitySlug, maybeAgentResponsibilitySlug) {
1459
- const state = typeof stateOrRepoSlug === "string" ? void 0 : stateOrRepoSlug;
1460
- const repoSlug = typeof stateOrRepoSlug === "string" ? stateOrRepoSlug : repoSlugOrAgentResponsibilitySlug ?? "";
1461
- const agentResponsibilitySlug = typeof stateOrRepoSlug === "string" ? repoSlugOrAgentResponsibilitySlug : maybeAgentResponsibilitySlug;
1462
- if (!agentResponsibilitySlug) return "ask";
1463
- try {
1464
- const loaded = readStateText({ state: state ?? defaultStateForRepoSlug(repoSlug) }, void 0, TRUST_FILE_PATH);
1465
- return loaded ? parseAgentResponsibilityTrustMode(loaded.content, agentResponsibilitySlug) : "ask";
1466
- } catch {
1467
- return "ask";
1438
+ var AGENT_RESPONSIBILITY_PROFILE_FILE, AGENT_RESPONSIBILITY_BODY_FILE;
1439
+ var init_agent_responsibilityFolders = __esm({
1440
+ "src/agent-responsibilityFolders.ts"() {
1441
+ "use strict";
1442
+ AGENT_RESPONSIBILITY_PROFILE_FILE = "profile.json";
1443
+ AGENT_RESPONSIBILITY_BODY_FILE = "agent-responsibility.md";
1468
1444
  }
1445
+ });
1446
+
1447
+ // src/companyStore.ts
1448
+ import { execFileSync as execFileSync2 } from "child_process";
1449
+ import * as crypto3 from "crypto";
1450
+ import * as fs5 from "fs";
1451
+ import * as os3 from "os";
1452
+ import * as path7 from "path";
1453
+ function getCompanyStoreRoot() {
1454
+ const store = resolveCompanyStore();
1455
+ if (!store) return null;
1456
+ const key = `${store.repo}#${store.ref}`;
1457
+ if (memo?.key === key) return memo.root;
1458
+ const root = fetchCompanyStore(store.repo, store.ref);
1459
+ memo = { key, root };
1460
+ return root;
1469
1461
  }
1470
- function readThread(repoSlug, number, limit = 10) {
1471
- const meta = JSON.parse(gh(["api", `repos/${repoSlug}/issues/${number}`]));
1472
- const rawComments = JSON.parse(gh(["api", `repos/${repoSlug}/issues/${number}/comments?per_page=100`]));
1473
- const comments = rawComments.slice(-Math.max(1, limit)).map((c) => ({
1474
- author: c.user?.login ?? "?",
1475
- createdAt: c.created_at ?? "",
1476
- body: (c.body ?? "").slice(0, THREAD_BODY_MAX)
1477
- }));
1478
- return {
1479
- number,
1480
- title: meta.title ?? "",
1481
- state: meta.state ?? "",
1482
- labels: (meta.labels ?? []).map((l) => l.name ?? "").filter(Boolean),
1483
- comments
1462
+ function getCompanyStoreAssetRoot(kind) {
1463
+ const root = getCompanyStoreRoot();
1464
+ if (!root) return null;
1465
+ const folderByKind = {
1466
+ agentResponsibilities: "agent-responsibilities",
1467
+ agentActions: "agent-actions",
1468
+ goals: "goals",
1469
+ agents: "agents"
1484
1470
  };
1471
+ return path7.join(root, ".kody", folderByKind[kind]);
1485
1472
  }
1486
- function readCheckRuns(repoSlug, ref, ignoreNames) {
1487
- const ghOptions = { preferRepoToken: true };
1488
- const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"], ghOptions).trim();
1489
- const raw = gh(
1490
- [
1491
- "api",
1492
- `repos/${repoSlug}/commits/${sha}/check-runs`,
1493
- "--paginate",
1494
- "--jq",
1495
- ".check_runs[] | {name, status, conclusion, details_url}"
1496
- ],
1497
- ghOptions
1498
- );
1499
- const ignore = new Set(ignoreNames.map((n) => n.toLowerCase()));
1500
- const checks = raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)).filter((c) => !ignore.has(String(c.name).toLowerCase()));
1501
- 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 }));
1502
- const pending = checks.filter((c) => String(c.status).toLowerCase() !== "completed").map((c) => ({ name: c.name, status: c.status }));
1503
- const state = failing.length > 0 ? "RED" : pending.length > 0 ? "PENDING" : "GREEN";
1504
- return { sha, state, failing, pending };
1473
+ function resolveCompanyStore() {
1474
+ const envStore = process.env[STORE_ENV]?.trim();
1475
+ if (!envStore && process.env.VITEST) return null;
1476
+ const repo = envStore || DEFAULT_COMPANY_STORE;
1477
+ if (repo === "0" || repo === "false" || repo === "off") return null;
1478
+ const ref = process.env[REF_ENV]?.trim() || DEFAULT_COMPANY_STORE_REF;
1479
+ if (!ref) return null;
1480
+ return { repo, ref };
1505
1481
  }
1506
- function ensureIssue(repoSlug, key, title, body) {
1507
- const marker = trackMarker(key);
1482
+ function fetchCompanyStore(repo, ref) {
1483
+ const localRoot = localStoreRoot(repo);
1484
+ if (localRoot) return localRoot;
1485
+ const url = repoToGitUrl(repo);
1486
+ const cacheDir = path7.join(cacheRoot(), cacheKey(repo, ref));
1508
1487
  try {
1509
- const raw = gh(["issue", "list", "-R", repoSlug, "--state", "open", "--limit", "100", "--json", "number,body"]);
1510
- const issues = JSON.parse(raw);
1511
- const existing = issues.find((i) => (i.body ?? "").includes(marker));
1512
- if (existing) return { created: false, number: existing.number };
1513
- const url = gh(["issue", "create", "-R", repoSlug, "--title", title, "--body-file", "-"], {
1514
- input: `${body}
1515
-
1516
- ${marker}`
1517
- });
1518
- const m = url.trim().match(/\/(\d+)\s*$/);
1519
- if (!m) return { error: `issue created but could not parse its number from: ${url}` };
1520
- return { created: true, number: Number(m[1]) };
1488
+ if (isGitHubShorthand(repo)) setupGithubGitAuth();
1489
+ fs5.mkdirSync(path7.dirname(cacheDir), { recursive: true });
1490
+ if (!fs5.existsSync(path7.join(cacheDir, ".git"))) {
1491
+ fs5.rmSync(cacheDir, { recursive: true, force: true });
1492
+ runGit(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
1493
+ }
1494
+ runGit(["-C", cacheDir, "remote", "set-url", "origin", url]);
1495
+ runGit(["-C", cacheDir, "fetch", "--depth=1", "origin", ref]);
1496
+ runGit(["-C", cacheDir, "checkout", "--detach", "FETCH_HEAD"]);
1497
+ return cacheDir;
1521
1498
  } catch (err) {
1522
- return { error: err instanceof Error ? err.message : String(err) };
1499
+ const msg = err instanceof Error ? err.message : String(err);
1500
+ process.stderr.write(`[company-store] failed to fetch ${repo}#${ref}: ${msg}
1501
+ `);
1502
+ return null;
1523
1503
  }
1524
1504
  }
1525
- function ensureComment(repoSlug, issue, key, body) {
1526
- const marker = commentMarker(key);
1505
+ function localStoreRoot(repo) {
1506
+ if (!path7.isAbsolute(repo) && !repo.startsWith(".")) return null;
1507
+ const root = path7.resolve(repo);
1508
+ if (!fs5.existsSync(path7.join(root, ".kody"))) return null;
1509
+ return root;
1510
+ }
1511
+ function repoToGitUrl(repo) {
1512
+ if (/^(https?:|ssh:|git@|file:)/.test(repo)) return repo;
1513
+ if (isGitHubShorthand(repo)) {
1514
+ return `https://github.com/${repo}.git`;
1515
+ }
1516
+ return repo;
1517
+ }
1518
+ function isGitHubShorthand(repo) {
1519
+ return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo);
1520
+ }
1521
+ function cacheRoot() {
1522
+ return process.env[CACHE_ENV]?.trim() || path7.join(os3.homedir(), ".cache", "kody", "company-store");
1523
+ }
1524
+ function cacheKey(repo, ref) {
1525
+ return crypto3.createHash("sha256").update(`${repo}#${ref}`).digest("hex").slice(0, 24);
1526
+ }
1527
+ function runGit(args) {
1528
+ execFileSync2("git", args, { stdio: ["ignore", "ignore", "pipe"] });
1529
+ }
1530
+ function setupGithubGitAuth() {
1531
+ const token = process.env.KODY_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.GH_PAT;
1532
+ if (!token) return;
1527
1533
  try {
1528
- const raw = gh(["issue", "view", String(issue), "-R", repoSlug, "--json", "comments"]);
1529
- const parsed = JSON.parse(raw);
1530
- const already = (parsed.comments ?? []).some((c) => (c.body ?? "").includes(marker));
1531
- if (already) return { posted: false };
1532
- gh(["issue", "comment", String(issue), "-R", repoSlug, "--body-file", "-"], { input: `${body}
1534
+ execFileSync2("gh", ["auth", "setup-git", "--hostname", "github.com"], {
1535
+ env: { ...process.env, GH_TOKEN: token },
1536
+ stdio: ["ignore", "ignore", "pipe"]
1537
+ });
1538
+ } catch {
1539
+ }
1540
+ }
1541
+ var DEFAULT_COMPANY_STORE, DEFAULT_COMPANY_STORE_REF, STORE_ENV, REF_ENV, CACHE_ENV, memo;
1542
+ var init_companyStore = __esm({
1543
+ "src/companyStore.ts"() {
1544
+ "use strict";
1545
+ DEFAULT_COMPANY_STORE = "aharonyaircohen/kody-company-store";
1546
+ DEFAULT_COMPANY_STORE_REF = "stable";
1547
+ STORE_ENV = "KODY_COMPANY_STORE";
1548
+ REF_ENV = "KODY_COMPANY_STORE_REF";
1549
+ CACHE_ENV = "KODY_COMPANY_STORE_CACHE";
1550
+ memo = null;
1551
+ }
1552
+ });
1533
1553
 
1534
- ${marker}` });
1535
- return { posted: true };
1536
- } catch (err) {
1537
- return { error: err instanceof Error ? err.message : String(err) };
1554
+ // src/registry.ts
1555
+ import * as fs6 from "fs";
1556
+ import * as path8 from "path";
1557
+ function getAgentActionsRoot() {
1558
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1559
+ const candidates = [
1560
+ path8.join(here, "agent-actions"),
1561
+ // dev: src/
1562
+ path8.join(here, "..", "agent-actions"),
1563
+ // built: dist/bin → dist/agent-actions
1564
+ path8.join(here, "..", "src", "agent-actions")
1565
+ // fallback
1566
+ ];
1567
+ for (const c of candidates) {
1568
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
1538
1569
  }
1570
+ return candidates[0];
1539
1571
  }
1540
- function dispatchWorkflow(workflowFile, agentResponsibility, issueNumber) {
1541
- try {
1542
- gh([
1543
- "workflow",
1544
- "run",
1545
- workflowFile,
1546
- "-f",
1547
- `agentResponsibility=${agentResponsibility}`,
1548
- "-f",
1549
- `issue_number=${issueNumber}`
1550
- ]);
1551
- return { ok: true };
1552
- } catch (err) {
1553
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
1572
+ function getProjectAgentActionsRoot() {
1573
+ return path8.join(process.cwd(), ".kody", "agent-actions");
1574
+ }
1575
+ function getProjectAgentResponsibilitiesRoot() {
1576
+ return path8.join(process.cwd(), ".kody", "agent-responsibilities");
1577
+ }
1578
+ function getCompanyStoreAgentActionsRoot() {
1579
+ return getCompanyStoreAssetRoot("agentActions");
1580
+ }
1581
+ function getCompanyStoreAgentResponsibilitiesRoot() {
1582
+ return getCompanyStoreAssetRoot("agentResponsibilities");
1583
+ }
1584
+ function getBuiltinAgentResponsibilitiesRoot() {
1585
+ const here = path8.dirname(new URL(import.meta.url).pathname);
1586
+ const candidates = [
1587
+ path8.join(here, "agent-responsibilities"),
1588
+ // dev: src/
1589
+ path8.join(here, "..", "agent-responsibilities"),
1590
+ // built: dist/bin → dist/agent-responsibilities
1591
+ path8.join(here, "..", "src", "agent-responsibilities")
1592
+ // fallback
1593
+ ];
1594
+ for (const c of candidates) {
1595
+ if (fs6.existsSync(c) && fs6.statSync(c).isDirectory()) return c;
1554
1596
  }
1597
+ return candidates[0];
1555
1598
  }
1556
- function isDispatchGated(agentResponsibility, mode) {
1557
- if (mode === "auto") return false;
1558
- if (agentResponsibility && GATE_EXEMPT_DUTIES.has(agentResponsibility)) return false;
1559
- return true;
1599
+ function getAgentActionRoots() {
1600
+ const storeRoot = getCompanyStoreAgentActionsRoot();
1601
+ return [getProjectAgentActionsRoot(), ...storeRoot ? [storeRoot] : [], getAgentActionsRoot()];
1560
1602
  }
1561
- function trustRefusal(agentResponsibilitySlug) {
1562
- return `Not dispatched: agentResponsibility \`${agentResponsibilitySlug ?? "?"}\` 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 agentResponsibility act on its own, grant it Auto on the dashboard Trust page.`;
1603
+ function getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
1604
+ const storeRoot = getCompanyStoreAgentResponsibilitiesRoot();
1605
+ return [projectAgentResponsibilitiesRoot, ...storeRoot ? [storeRoot] : [], getBuiltinAgentResponsibilitiesRoot()];
1563
1606
  }
1564
- function agentResponsibilityToolDefinitions(opts) {
1565
- const workflowFile = opts.workflowFile ?? "kody.yml";
1566
- const listTool = {
1567
- name: "list_prs_to_repair",
1568
- description: "Return open non-draft PRs with the signals you need to pick a repair: number, title, headSha, baseRef, mergeable (CONFLICTING/MERGEABLE/UNKNOWN), ciStatus (PASSING/FAILING/RUNNING/UNKNOWN), behindBy (commits behind base; 0 for PRs that already match conflicts or CI-failure rules), updatedAt. Drafts are excluded. One call returns everything \u2014 do not iterate or paginate.",
1569
- inputSchema: {},
1570
- handler: async () => {
1571
- const candidates = listRepairCandidates(opts.repoSlug);
1572
- return {
1573
- content: [
1574
- {
1575
- type: "text",
1576
- text: JSON.stringify({ prs: candidates }, null, 2)
1577
- }
1578
- ]
1579
- };
1580
- }
1581
- };
1582
- const makeDispatch = (verb, describe) => ({
1583
- name: `${verb.replace("-", "_")}_pr`,
1584
- description: describe,
1585
- inputSchema: {
1586
- pr: z2.number().int().positive().describe("PR number to repair.")
1587
- },
1588
- handler: async (args) => {
1589
- const pr = Number(args.pr);
1590
- if (isDispatchGated(verb, readAgentResponsibilityTrustMode(opts.state, opts.repoSlug, opts.agentResponsibilitySlug))) {
1591
- return { content: [{ type: "text", text: trustRefusal(opts.agentResponsibilitySlug) }] };
1607
+ function listAgentActions(roots = getAgentActionRoots()) {
1608
+ const rootList = typeof roots === "string" ? [roots] : roots;
1609
+ const seen = /* @__PURE__ */ new Set();
1610
+ const out = [];
1611
+ for (const root of rootList) {
1612
+ if (!fs6.existsSync(root)) continue;
1613
+ const entries = fs6.readdirSync(root, { withFileTypes: true });
1614
+ for (const ent of entries) {
1615
+ if (!ent.isDirectory()) continue;
1616
+ if (seen.has(ent.name)) continue;
1617
+ const profilePath = path8.join(root, ent.name, AGENT_RESPONSIBILITY_PROFILE_FILE);
1618
+ if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
1619
+ out.push({ name: ent.name, profilePath });
1620
+ seen.add(ent.name);
1592
1621
  }
1593
- const result = dispatchVerb(workflowFile, verb, pr);
1594
- const text = result.ok ? `Dispatched \`${verb}\` on PR #${pr}. The repair runs in its own workflow_dispatch \u2014 wait for the next tick to see the new headSha.` : `Dispatch failed for \`${verb}\` on PR #${pr}: ${result.error}`;
1595
- return { content: [{ type: "text", text }] };
1596
- }
1597
- });
1598
- const syncTool = makeDispatch(
1599
- "sync",
1600
- "Bring a stale PR up to date with its base branch (merges base \u2192 head + pushes). Use when behindBy > 10 AND mergeable !== CONFLICTING AND ciStatus !== FAILING. Returns immediately \u2014 the actual merge runs in a separate workflow."
1601
- );
1602
- const fixCiTool = makeDispatch(
1603
- "fix-ci",
1604
- "Repair a PR whose CI is failing. Use when ciStatus === FAILING. The repair runs in a separate workflow."
1605
- );
1606
- const resolveTool = makeDispatch(
1607
- "resolve",
1608
- "Resolve merge conflicts on a PR. Use when mergeable === CONFLICTING. The repair runs in a separate workflow."
1609
- );
1610
- const recommendTool = {
1611
- name: "recommend_to_operator",
1612
- description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a agentResponsibility 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.",
1613
- inputSchema: {
1614
- pr: z2.number().int().positive().describe("PR number to comment on."),
1615
- body: z2.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
1616
- },
1617
- handler: async (args) => {
1618
- const pr = Number(args.pr);
1619
- const body = String(args.body ?? "");
1620
- const result = postRecommendation(pr, opts.operatorMention, body, opts.agentResponsibilitySlug);
1621
- const text = result.ok ? `Recommendation posted on PR #${pr}.` : `Recommendation failed on PR #${pr}: ${result.error}`;
1622
- return { content: [{ type: "text", text }] };
1623
- }
1624
- };
1625
- const ledgerTool = {
1626
- name: "read_ledger",
1627
- 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.",
1628
- inputSchema: {
1629
- label: z2.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
1630
- },
1631
- handler: async (args) => {
1632
- const label = String(args.label ?? "");
1633
- const result = readLedger(label);
1634
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1635
1622
  }
1636
- };
1637
- const checkRunsTool = {
1638
- name: "read_check_runs",
1639
- 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/agent-responsibility-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.",
1640
- inputSchema: {
1641
- ref: z2.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
1642
- ignoreNames: z2.array(z2.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
1643
- },
1644
- handler: async (args) => {
1645
- const ref = String(args.ref ?? "");
1646
- const ignoreNames = Array.isArray(args.ignoreNames) ? args.ignoreNames : DEFAULT_IGNORE_CHECKS;
1647
- const result = readCheckRuns(opts.repoSlug, ref, ignoreNames);
1648
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1623
+ }
1624
+ return out.sort((a, b) => a.name.localeCompare(b.name));
1625
+ }
1626
+ function resolveAgentAction(name, roots = getAgentActionRoots()) {
1627
+ if (!isSafeName(name)) return null;
1628
+ const rootList = typeof roots === "string" ? [roots] : roots;
1629
+ for (const root of rootList) {
1630
+ const profilePath = path8.join(root, name, "profile.json");
1631
+ if (fs6.existsSync(profilePath) && fs6.statSync(profilePath).isFile()) {
1632
+ return profilePath;
1649
1633
  }
1634
+ }
1635
+ return null;
1636
+ }
1637
+ function listAgentResponsibilityActions(projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
1638
+ const seen = /* @__PURE__ */ new Set();
1639
+ const out = [];
1640
+ const add = (action) => {
1641
+ if (!isSafeName(action.action) || !isSafeName(action.agentResponsibility) || !isSafeName(action.agentAction)) return;
1642
+ if (seen.has(action.action)) return;
1643
+ seen.add(action.action);
1644
+ out.push(action);
1650
1645
  };
1651
- const readThreadTool = {
1652
- name: "read_thread",
1653
- 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.",
1654
- inputSchema: {
1655
- number: z2.number().int().positive().describe("Issue or PR number to read."),
1656
- limit: z2.number().int().positive().optional().describe("Max recent comments to return (default 10).")
1657
- },
1658
- handler: async (args) => {
1659
- const number = Number(args.number);
1660
- const limit = args.limit ? Number(args.limit) : 10;
1661
- const result = readThread(opts.repoSlug, number, limit);
1662
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1663
- }
1664
- };
1665
- const ensureIssueTool = {
1666
- name: "ensure_issue",
1667
- 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.",
1668
- inputSchema: {
1669
- key: z2.string().min(1).describe(
1670
- "Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
1671
- ),
1672
- title: z2.string().min(1).describe("Issue title (used only on first creation)."),
1673
- body: z2.string().min(1).describe(
1674
- "Issue body markdown (used only on first creation). Include the operator mention verbatim if the agentResponsibility body has one."
1675
- )
1676
- },
1677
- handler: async (args) => {
1678
- const key = String(args.key ?? "");
1679
- const title = String(args.title ?? "");
1680
- const body = String(args.body ?? "");
1681
- const result = ensureIssue(opts.repoSlug, key, title, body);
1682
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1683
- }
1684
- };
1685
- const ensureCommentTool = {
1686
- name: "ensure_comment",
1687
- 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.",
1688
- inputSchema: {
1689
- issue: z2.number().int().positive().describe("Issue number to comment on."),
1690
- key: z2.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
1691
- body: z2.string().min(1).describe("Comment body markdown.")
1692
- },
1693
- handler: async (args) => {
1694
- const issue = Number(args.issue);
1695
- const key = String(args.key ?? "");
1696
- const body = String(args.body ?? "");
1697
- const result = ensureComment(opts.repoSlug, issue, key, body);
1698
- return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
1699
- }
1700
- };
1701
- const dispatchTool = {
1702
- name: "dispatch_workflow",
1703
- description: "Dispatch a kody.yml workflow_dispatch run for a agentResponsibility action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({agentResponsibility:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
1704
- inputSchema: {
1705
- agentResponsibility: z2.string().min(1).optional().describe("AgentResponsibility action to run (e.g. 'run')."),
1706
- agentAction: z2.string().min(1).optional().describe("Deprecated alias for agentResponsibility."),
1707
- issueNumber: z2.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
1708
- },
1709
- handler: async (args) => {
1710
- const agentResponsibility = String(args.agentResponsibility ?? args.agentAction ?? "");
1711
- const issueNumber = Number(args.issueNumber);
1712
- if (isDispatchGated(
1713
- agentResponsibility,
1714
- readAgentResponsibilityTrustMode(opts.state, opts.repoSlug, opts.agentResponsibilitySlug)
1715
- )) {
1716
- return { content: [{ type: "text", text: trustRefusal(opts.agentResponsibilitySlug) }] };
1717
- }
1718
- const result = dispatchWorkflow(workflowFile, agentResponsibility, issueNumber);
1719
- const text = result.ok ? `Dispatched agentResponsibility \`${agentResponsibility}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for agentResponsibility \`${agentResponsibility}\` on #${issueNumber}: ${result.error}`;
1720
- return { content: [{ type: "text", text }] };
1721
- }
1722
- };
1723
- return [
1724
- listTool,
1725
- syncTool,
1726
- fixCiTool,
1727
- resolveTool,
1728
- recommendTool,
1729
- ledgerTool,
1730
- checkRunsTool,
1731
- readThreadTool,
1732
- ensureIssueTool,
1733
- ensureCommentTool,
1734
- dispatchTool
1735
- ];
1646
+ const roots = getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot);
1647
+ const executableRoots = getAgentActionRoots();
1648
+ for (const action of listFolderAgentResponsibilityActions(roots[0], "project-folder")) add(action);
1649
+ for (const action of listAgentActionResponsibilityActions(executableRoots[0], "project-agentAction")) add(action);
1650
+ if (roots.length === 3) {
1651
+ for (const action of listFolderAgentResponsibilityActions(roots[1], "company-store")) add(action);
1652
+ for (const action of listAgentActionResponsibilityActions(executableRoots[1], "company-store-agentAction"))
1653
+ add(action);
1654
+ for (const action of listBuiltinAgentResponsibilityActions(roots[2])) add(action);
1655
+ } else {
1656
+ for (const action of listBuiltinAgentResponsibilityActions(roots[1])) add(action);
1657
+ }
1658
+ return out.sort((a, b) => a.action.localeCompare(b.action));
1736
1659
  }
1737
- function buildAgentResponsibilityMcpServer(opts) {
1738
- const definitions = agentResponsibilityToolDefinitions(opts);
1739
- const tools = definitions.map(
1740
- (def) => tool3(
1741
- def.name,
1742
- def.description,
1743
- def.inputSchema,
1744
- async (args) => def.handler(args)
1745
- )
1746
- );
1747
- const server = createSdkMcpServer3({
1748
- name: "kody-agentResponsibility",
1749
- version: "0.1.0",
1750
- tools
1751
- });
1752
- return { server };
1660
+ function resolveAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
1661
+ if (!isSafeName(action)) return null;
1662
+ return listAgentResponsibilityActions(projectAgentResponsibilitiesRoot).find((d) => d.action === action) ?? null;
1753
1663
  }
1754
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, THREAD_BODY_MAX, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, GATE_EXEMPT_DUTIES, AGENT_RESPONSIBILITY_MCP_TOOL_NAMES;
1755
- var init_agent_responsibilityMcp = __esm({
1756
- "src/agent-responsibilityMcp.ts"() {
1757
- "use strict";
1758
- init_issue();
1759
- init_stateRepo();
1760
- FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
1761
- RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
1762
- TRUST_FILE_PATH = "state/trust.json";
1763
- THREAD_BODY_MAX = 4e3;
1764
- CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
1765
- DEFAULT_IGNORE_CHECKS = ["run", "kody", "agent-responsibility-tick", "agent-ask", "chat"];
1766
- trackMarker = (key) => `<!-- kody-track:${key} -->`;
1767
- commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
1768
- GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
1769
- AGENT_RESPONSIBILITY_MCP_TOOL_NAMES = [
1770
- "list_prs_to_repair",
1771
- "sync_pr",
1772
- "fix_ci_pr",
1773
- "resolve_pr",
1774
- "recommend_to_operator",
1775
- "read_ledger",
1776
- "read_check_runs",
1777
- "read_thread",
1778
- "ensure_issue",
1779
- "ensure_comment",
1780
- "dispatch_workflow"
1781
- ];
1782
- }
1783
- });
1784
-
1785
- // src/repoWorkspace.ts
1786
- import { spawn as spawn2, spawnSync } from "child_process";
1787
- import * as fs4 from "fs";
1788
- import * as path6 from "path";
1789
- async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
1790
- const name = repo?.trim();
1791
- if (!name || !REPO_RE.test(name)) return null;
1792
- const root = path6.resolve(reposRoot);
1793
- const dir = path6.resolve(root, name);
1794
- if (dir !== root && !dir.startsWith(root + path6.sep)) return null;
1795
- if (fs4.existsSync(path6.join(dir, ".git"))) return dir;
1796
- const inflight = repoClones.get(dir);
1797
- if (inflight) {
1798
- await inflight;
1799
- return dir;
1664
+ function hasAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
1665
+ return resolveAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot) !== null;
1666
+ }
1667
+ function resolveAgentResponsibilityFolder(slug2, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
1668
+ if (!isSafeName(slug2)) return null;
1669
+ for (const root of getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot)) {
1670
+ const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
1671
+ if (agentResponsibility) return agentResponsibility;
1800
1672
  }
1801
- const p = cloneRepo(name, repoToken, dir).finally(() => {
1802
- if (repoClones.get(dir) === p) repoClones.delete(dir);
1803
- });
1804
- repoClones.set(dir, p);
1805
- await p;
1806
- return dir;
1673
+ return null;
1807
1674
  }
1808
- async function ensureRepoCwd(opts) {
1809
- const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo);
1810
- return dir ?? opts.baseCwd;
1675
+ function resolveAgentResponsibilityExecution(agentResponsibility) {
1676
+ const agentAction = agentResponsibility.config.agentAction ?? agentResponsibility.config.agentActions?.[0] ?? (agentResponsibility.config.tickScript ? "agent-responsibility-tick-scripted" : "agent-responsibility-tick");
1677
+ const cliArgs = agentActionDeclaresInput(agentAction, "agentResponsibility") ? { agentResponsibility: agentResponsibility.slug } : {};
1678
+ return { agentAction, cliArgs };
1811
1679
  }
1812
- async function fetchRepo(opts) {
1813
- const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo ?? defaultCloneRepo);
1814
- if (!dir) {
1815
- throw new Error(`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`);
1680
+ function agentActionDeclaresInput(agentAction, inputName) {
1681
+ const profilePath = resolveAgentAction(agentAction);
1682
+ if (!profilePath) return false;
1683
+ try {
1684
+ const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
1685
+ if (!Array.isArray(raw.inputs)) return false;
1686
+ return raw.inputs.some((entry) => {
1687
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
1688
+ const input = entry;
1689
+ return input.name === inputName || input.flag === `--${inputName}`;
1690
+ });
1691
+ } catch {
1692
+ return false;
1816
1693
  }
1817
- return dir;
1818
1694
  }
1819
- var REPO_RE, repoClones, defaultCloneRepo;
1820
- var init_repoWorkspace = __esm({
1821
- "src/repoWorkspace.ts"() {
1822
- "use strict";
1823
- REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
1824
- repoClones = /* @__PURE__ */ new Map();
1825
- defaultCloneRepo = (repo, token, dir) => {
1826
- fs4.mkdirSync(path6.dirname(dir), { recursive: true });
1827
- const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
1828
- return new Promise((resolve7, reject) => {
1829
- const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
1830
- stdio: "inherit"
1831
- });
1832
- child.on("exit", (code) => {
1833
- if (code !== 0) {
1834
- reject(new Error(`git clone ${repo} failed (exit ${code})`));
1835
- return;
1836
- }
1837
- try {
1838
- const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
1839
- const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
1840
- spawnSync("git", ["-C", dir, "config", "user.name", name]);
1841
- spawnSync("git", ["-C", dir, "config", "user.email", email]);
1842
- } catch {
1843
- }
1844
- resolve7();
1845
- });
1846
- child.on("error", reject);
1695
+ function isSafeName(name) {
1696
+ return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
1697
+ }
1698
+ function listAgentActionResponsibilityActions(root, source) {
1699
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
1700
+ const out = [];
1701
+ for (const ent of fs6.readdirSync(root, { withFileTypes: true })) {
1702
+ if (!ent.isDirectory() || !isSafeName(ent.name)) continue;
1703
+ const profilePath = path8.join(root, ent.name, AGENT_RESPONSIBILITY_PROFILE_FILE);
1704
+ if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) continue;
1705
+ try {
1706
+ const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
1707
+ const action = typeof raw.action === "string" && raw.action.trim() ? raw.action.trim() : "";
1708
+ if (!action) continue;
1709
+ if (!PUBLIC_AGENT_ACTION_ACTION_ROLES.has(String(raw.role))) continue;
1710
+ if (!PUBLIC_AGENT_ACTION_CAPABILITY_KINDS.has(String(raw.capabilityKind))) continue;
1711
+ if (!Array.isArray(raw.inputs)) continue;
1712
+ out.push({
1713
+ action,
1714
+ agentResponsibility: ent.name,
1715
+ agentAction: ent.name,
1716
+ cliArgs: {},
1717
+ source,
1718
+ describe: typeof raw.describe === "string" ? raw.describe : void 0,
1719
+ profilePath
1847
1720
  });
1848
- };
1721
+ } catch {
1722
+ }
1849
1723
  }
1850
- });
1851
-
1852
- // src/fetchRepoMcp.ts
1853
- var fetchRepoMcp_exports = {};
1854
- __export(fetchRepoMcp_exports, {
1855
- buildFetchRepoMcpServer: () => buildFetchRepoMcpServer,
1856
- fetchRepoToolDefinition: () => fetchRepoToolDefinition
1857
- });
1858
- import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
1859
- import { z as z3 } from "zod";
1860
- function fetchRepoToolDefinition(opts) {
1861
- return {
1862
- name: "fetch_repo",
1863
- description: DESCRIPTION3,
1864
- inputSchema: INPUT_SCHEMA3,
1865
- handler: async (args) => {
1866
- const repo = String(args.repo ?? "").trim();
1867
- try {
1868
- const dir = await fetchRepo({
1869
- reposRoot: opts.reposRoot,
1870
- repo,
1871
- repoToken: opts.repoToken
1872
- });
1873
- return {
1874
- content: [
1875
- {
1876
- type: "text",
1877
- text: `Cloned ${repo} \u2192 ${dir}
1878
- Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in your workspace alongside any other repos you've fetched.`
1879
- }
1880
- ]
1881
- };
1882
- } catch (err) {
1883
- const msg = err instanceof Error ? err.message : String(err);
1884
- return {
1885
- content: [{ type: "text", text: `Could not fetch ${repo}: ${msg}` }],
1886
- isError: true
1887
- };
1888
- }
1889
- }
1890
- };
1724
+ return out.sort((a, b) => a.action.localeCompare(b.action));
1891
1725
  }
1892
- function buildFetchRepoMcpServer(opts) {
1893
- const def = fetchRepoToolDefinition(opts);
1894
- const fetchTool = tool4(def.name, def.description, def.inputSchema, async (args) => {
1895
- return def.handler({ repo: String(args.repo ?? "") });
1896
- });
1897
- return createSdkMcpServer4({
1898
- name: "kody-fetch-repo",
1899
- version: "0.1.0",
1900
- tools: [fetchTool]
1901
- });
1726
+ function listFolderAgentResponsibilityActions(root, source) {
1727
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
1728
+ const out = [];
1729
+ for (const slug2 of listAgentResponsibilityFolderSlugs(root)) {
1730
+ if (!isSafeName(slug2)) continue;
1731
+ const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
1732
+ if (!agentResponsibility) continue;
1733
+ const action = agentResponsibility.config.action ?? slug2;
1734
+ const { agentAction, cliArgs } = resolveAgentResponsibilityExecution(agentResponsibility);
1735
+ out.push({
1736
+ action,
1737
+ agentResponsibility: slug2,
1738
+ agentAction,
1739
+ cliArgs,
1740
+ source,
1741
+ describe: agentResponsibility.config.describe ?? agentResponsibility.title,
1742
+ capabilityKind: agentResponsibility.config.capabilityKind,
1743
+ profilePath: agentResponsibility.profilePath,
1744
+ bodyPath: agentResponsibility.bodyPath
1745
+ });
1746
+ }
1747
+ return out.sort((a, b) => a.action.localeCompare(b.action));
1902
1748
  }
1903
- var DESCRIPTION3, INPUT_SCHEMA3;
1904
- var init_fetchRepoMcp = __esm({
1905
- "src/fetchRepoMcp.ts"() {
1749
+ function listBuiltinAgentResponsibilityActions(root = getBuiltinAgentResponsibilitiesRoot()) {
1750
+ if (!fs6.existsSync(root) || !fs6.statSync(root).isDirectory()) return [];
1751
+ const out = [];
1752
+ for (const slug2 of listAgentResponsibilityFolderSlugs(root)) {
1753
+ if (!isSafeName(slug2)) continue;
1754
+ const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
1755
+ if (!agentResponsibility) continue;
1756
+ const action = agentResponsibility.config.action ?? slug2;
1757
+ const agentAction = agentResponsibility.config.agentAction ?? slug2;
1758
+ out.push({
1759
+ action,
1760
+ agentResponsibility: slug2,
1761
+ agentAction,
1762
+ cliArgs: {},
1763
+ source: "builtin",
1764
+ describe: agentResponsibility.config.describe ?? agentResponsibility.title,
1765
+ capabilityKind: agentResponsibility.config.capabilityKind,
1766
+ profilePath: agentResponsibility.profilePath,
1767
+ bodyPath: agentResponsibility.bodyPath
1768
+ });
1769
+ }
1770
+ return out.sort((a, b) => a.action.localeCompare(b.action));
1771
+ }
1772
+ function getProfileInputs(name, roots = getAgentActionRoots()) {
1773
+ const profilePath = resolveAgentAction(name, roots);
1774
+ if (!profilePath) return null;
1775
+ try {
1776
+ const raw = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
1777
+ if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
1778
+ return raw.inputs;
1779
+ } catch {
1780
+ return null;
1781
+ }
1782
+ }
1783
+ function parseGenericFlags(argv) {
1784
+ const args = {};
1785
+ const positional = [];
1786
+ for (let i = 0; i < argv.length; i++) {
1787
+ const arg = argv[i];
1788
+ if (!arg.startsWith("--")) {
1789
+ positional.push(arg);
1790
+ continue;
1791
+ }
1792
+ const key = arg.slice(2);
1793
+ const next = argv[i + 1];
1794
+ let value = true;
1795
+ if (next !== void 0 && !next.startsWith("--")) {
1796
+ value = next;
1797
+ i++;
1798
+ }
1799
+ args[key] = value;
1800
+ if (key.includes("-")) {
1801
+ const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
1802
+ if (camel !== key && args[camel] === void 0) args[camel] = value;
1803
+ }
1804
+ }
1805
+ if (positional.length > 0) args._ = positional;
1806
+ return args;
1807
+ }
1808
+ var PUBLIC_AGENT_ACTION_ACTION_ROLES, PUBLIC_AGENT_ACTION_CAPABILITY_KINDS;
1809
+ var init_registry = __esm({
1810
+ "src/registry.ts"() {
1906
1811
  "use strict";
1907
- init_repoWorkspace();
1908
- 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.';
1909
- INPUT_SCHEMA3 = {
1910
- repo: z3.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
1911
- };
1812
+ init_agent_responsibilityFolders();
1813
+ init_companyStore();
1814
+ PUBLIC_AGENT_ACTION_ACTION_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
1815
+ PUBLIC_AGENT_ACTION_CAPABILITY_KINDS = /* @__PURE__ */ new Set(["observe", "act", "verify"]);
1912
1816
  }
1913
1817
  });
1914
1818
 
1915
- // src/agent.ts
1916
- import * as fs5 from "fs";
1917
- import * as path7 from "path";
1918
- import { query } from "@anthropic-ai/claude-agent-sdk";
1919
- function classifySubtype(subtype) {
1920
- if (!subtype) return "generic_failed";
1921
- const lower = subtype.toLowerCase();
1922
- if (lower === "success") return "ok";
1923
- if (lower.includes("max_turns") || lower.includes("max-turns")) return "out_of_turns";
1924
- if (lower.includes("rate_limit") || lower.includes("rate-limit")) return "rate_limit";
1925
- if (lower.includes("tool")) return "tool_error";
1926
- if (lower.includes("error")) return "model_error";
1927
- return "generic_failed";
1928
- }
1929
- function isClaudeLoginRequiredText(text) {
1930
- const normalized = text.toLowerCase();
1931
- return normalized.includes("not logged in") && normalized.includes("/login");
1819
+ // src/agent-responsibilityMcp.ts
1820
+ var agent_responsibilityMcp_exports = {};
1821
+ __export(agent_responsibilityMcp_exports, {
1822
+ AGENT_RESPONSIBILITY_MCP_TOOL_NAMES: () => AGENT_RESPONSIBILITY_MCP_TOOL_NAMES,
1823
+ agentResponsibilityToolDefinitions: () => agentResponsibilityToolDefinitions,
1824
+ buildAgentResponsibilityMcpServer: () => buildAgentResponsibilityMcpServer,
1825
+ dispatchWorkflow: () => dispatchWorkflow,
1826
+ ensureComment: () => ensureComment,
1827
+ ensureIssue: () => ensureIssue,
1828
+ isDispatchGated: () => isDispatchGated,
1829
+ parseAgentResponsibilityTrustMode: () => parseAgentResponsibilityTrustMode,
1830
+ readAgentResponsibilityTrustMode: () => readAgentResponsibilityTrustMode,
1831
+ readCheckRuns: () => readCheckRuns,
1832
+ readThread: () => readThread
1833
+ });
1834
+ import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
1835
+ import { z as z2 } from "zod";
1836
+ function summarizeCiStatus(rollup) {
1837
+ if (!Array.isArray(rollup) || rollup.length === 0) return "UNKNOWN";
1838
+ let hasRunning = false;
1839
+ for (const check of rollup) {
1840
+ const status = String(check.status ?? "").toUpperCase();
1841
+ const conclusion = String(check.conclusion ?? "").toUpperCase();
1842
+ if (FAIL_CONCLUSIONS.has(conclusion)) return "FAILING";
1843
+ if (!conclusion && RUNNING_STATUSES.has(status)) hasRunning = true;
1844
+ }
1845
+ return hasRunning ? "RUNNING" : "PASSING";
1932
1846
  }
1933
- function resolveTurnTimeoutMs(opts) {
1934
- if (opts.maxTurnTimeoutMs !== void 0 && opts.maxTurnTimeoutMs !== null) {
1935
- return opts.maxTurnTimeoutMs > 0 ? opts.maxTurnTimeoutMs : 0;
1847
+ function computeBehindBy(repoSlug, base, head) {
1848
+ try {
1849
+ const raw = gh(["api", `repos/${repoSlug}/compare/${base}...${head}`, "--jq", ".behind_by"]);
1850
+ const n = Number(raw.trim());
1851
+ return Number.isFinite(n) ? n : -1;
1852
+ } catch {
1853
+ return -1;
1936
1854
  }
1937
- const envSec = Number(process.env.KODY_TURN_TIMEOUT_SEC);
1938
- if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
1939
- if (Number.isFinite(envSec) && envSec <= 0) return 0;
1940
- return DEFAULT_TURN_TIMEOUT_MS;
1941
1855
  }
1942
- function isTransientConnectionError(msg) {
1943
- if (!msg) return false;
1944
- return /ConnectionRefused|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH|ENOTFOUND|socket hang up|Unable to connect to API|fetch failed/i.test(
1945
- msg
1946
- );
1856
+ function listRepairCandidates(repoSlug) {
1857
+ const raw = gh([
1858
+ "pr",
1859
+ "list",
1860
+ "--state",
1861
+ "open",
1862
+ "--limit",
1863
+ "100",
1864
+ "--json",
1865
+ "number,title,headRefName,headRefOid,baseRefName,isDraft,mergeable,statusCheckRollup,updatedAt"
1866
+ ]);
1867
+ const prs = JSON.parse(raw);
1868
+ return prs.filter((p) => !p.isDraft).map((p) => {
1869
+ const ciStatus = summarizeCiStatus(p.statusCheckRollup);
1870
+ const mergeable = String(p.mergeable ?? "UNKNOWN").toUpperCase();
1871
+ const behindBy = mergeable === "CONFLICTING" || ciStatus === "FAILING" ? 0 : computeBehindBy(repoSlug, p.baseRefName, p.headRefName);
1872
+ return {
1873
+ number: p.number,
1874
+ title: p.title,
1875
+ headSha: p.headRefOid,
1876
+ baseRef: p.baseRefName,
1877
+ isDraft: false,
1878
+ mergeable,
1879
+ ciStatus,
1880
+ behindBy,
1881
+ updatedAt: p.updatedAt
1882
+ };
1883
+ });
1947
1884
  }
1948
- function toolMayMutate(name, input) {
1949
- if (!name) return false;
1950
- if (MUTATING_FILE_TOOLS.has(name)) return true;
1951
- if (name === "Bash") return BASH_WRITE_VERB.test(String(input?.command ?? ""));
1952
- return false;
1885
+ function dispatchVerb(workflowFile, repoSlug, agentResponsibility, prNumber) {
1886
+ return dispatchWorkflow(workflowFile, agentResponsibility, prNumber, repoSlug);
1953
1887
  }
1954
- function stripAgentSecrets(env) {
1955
- const out = { ...env };
1956
- const raw = out.ALL_SECRETS;
1957
- delete out.ALL_SECRETS;
1958
- if (!raw) return out;
1888
+ function postRecommendation(prNumber, mention, message, agentResponsibilitySlug) {
1889
+ const mentioned = mention ? `${mention} ${message}` : message;
1890
+ const body = agentResponsibilitySlug ? `${mentioned}
1891
+
1892
+ <!-- kody-agentResponsibility: ${agentResponsibilitySlug} -->` : mentioned;
1959
1893
  try {
1960
- const parsed = JSON.parse(raw);
1961
- for (const key of Object.keys(parsed)) {
1962
- if (!AGENT_KEEP_SECRETS.has(key)) delete out[key];
1963
- }
1964
- } catch {
1894
+ gh(["pr", "comment", String(prNumber), "--body", body]);
1895
+ return { ok: true };
1896
+ } catch (err) {
1897
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
1965
1898
  }
1966
- return out;
1967
1899
  }
1968
- async function runAgent(opts) {
1969
- const ndjsonDir = opts.ndjsonDir ?? path7.join(opts.cwd, ".kody");
1970
- fs5.mkdirSync(ndjsonDir, { recursive: true });
1971
- const ndjsonPath = path7.join(ndjsonDir, "last-run.jsonl");
1972
- const env = stripAgentSecrets({
1973
- ...process.env,
1974
- SKIP_HOOKS: "1",
1975
- HUSKY: "0",
1976
- CI: process.env.CI ?? "1",
1977
- // MCP servers are spawned asynchronously by the SDK. With the default
1978
- // non-blocking behavior, the SDK announces its tool list at session
1979
- // init while servers are still in `pending`, so their tools never
1980
- // reach the model. Block until each MCP completes its handshake (or
1981
- // the timeout below elapses) so the tool list is complete on first
1982
- // turn.
1983
- MCP_CONNECTION_NONBLOCKING: process.env.MCP_CONNECTION_NONBLOCKING ?? "false",
1984
- MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "60000"
1985
- });
1986
- if (opts.litellmUrl) {
1987
- env.ANTHROPIC_BASE_URL = opts.litellmUrl;
1988
- env.ANTHROPIC_API_KEY = getAnthropicApiKeyOrDummy();
1900
+ function readLedger(label) {
1901
+ const startTag = `<!-- ${label}:start -->`;
1902
+ const endTag = `<!-- ${label}:end -->`;
1903
+ try {
1904
+ const raw = gh(["issue", "list", "--state", "open", "--label", label, "--limit", "5", "--json", "number,body"]);
1905
+ const issues = JSON.parse(raw);
1906
+ if (issues.length === 0) return { found: false, payload: null };
1907
+ const issue = issues.sort((a, b) => a.number - b.number)[0];
1908
+ const body = issue?.body ?? "";
1909
+ const startIdx = body.indexOf(startTag);
1910
+ const endIdx = body.indexOf(endTag);
1911
+ if (startIdx === -1 || endIdx === -1 || endIdx <= startIdx) {
1912
+ return { found: true, issueNumber: issue?.number, payload: null };
1913
+ }
1914
+ const between = body.slice(startIdx + startTag.length, endIdx);
1915
+ const fenceMatch = between.match(/```json\s*([\s\S]*?)```/);
1916
+ if (!fenceMatch) return { found: true, issueNumber: issue?.number, payload: null };
1917
+ try {
1918
+ return { found: true, issueNumber: issue?.number, payload: JSON.parse(fenceMatch[1]) };
1919
+ } catch {
1920
+ return { found: true, issueNumber: issue?.number, payload: null };
1921
+ }
1922
+ } catch (err) {
1923
+ return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
1989
1924
  }
1990
- const startedAt = Date.now();
1991
- const turnTimeoutMs = resolveTurnTimeoutMs(opts);
1992
- let outcome = "failed";
1993
- let outcomeKind = "generic_failed";
1994
- let errorMessage;
1995
- let tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
1996
- let messageCount = 0;
1997
- let finalText = "";
1998
- let getSubmitted;
1999
- for (let attempt = 0; ; attempt++) {
2000
- let ndjsonWriteFailed = false;
2001
- let ndjsonWriteError;
2002
- const fullLog = fs5.createWriteStream(ndjsonPath, { flags: "w" });
2003
- fullLog.on("error", (err) => {
2004
- ndjsonWriteFailed = true;
2005
- ndjsonWriteError = err instanceof Error ? err.message : String(err);
1925
+ }
1926
+ function parseAgentResponsibilityTrustMode(rawJson, agentResponsibilitySlug) {
1927
+ try {
1928
+ const parsed = JSON.parse(rawJson);
1929
+ return parsed?.agentResponsibilities?.[agentResponsibilitySlug]?.mode === "auto" ? "auto" : "ask";
1930
+ } catch {
1931
+ return "ask";
1932
+ }
1933
+ }
1934
+ function defaultStateForRepoSlug(repoSlug) {
1935
+ const [owner, repo] = repoSlug.split("/");
1936
+ return { repo: `${owner}/kody-state`, path: repo ?? repoSlug };
1937
+ }
1938
+ function readAgentResponsibilityTrustMode(stateOrRepoSlug, repoSlugOrAgentResponsibilitySlug, maybeAgentResponsibilitySlug) {
1939
+ const state = typeof stateOrRepoSlug === "string" ? void 0 : stateOrRepoSlug;
1940
+ const repoSlug = typeof stateOrRepoSlug === "string" ? stateOrRepoSlug : repoSlugOrAgentResponsibilitySlug ?? "";
1941
+ const agentResponsibilitySlug = typeof stateOrRepoSlug === "string" ? repoSlugOrAgentResponsibilitySlug : maybeAgentResponsibilitySlug;
1942
+ if (!agentResponsibilitySlug) return "ask";
1943
+ try {
1944
+ const loaded = readStateText({ state: state ?? defaultStateForRepoSlug(repoSlug) }, void 0, TRUST_FILE_PATH);
1945
+ return loaded ? parseAgentResponsibilityTrustMode(loaded.content, agentResponsibilitySlug) : "ask";
1946
+ } catch {
1947
+ return "ask";
1948
+ }
1949
+ }
1950
+ function readThread(repoSlug, number, limit = 10) {
1951
+ const meta = JSON.parse(gh(["api", `repos/${repoSlug}/issues/${number}`]));
1952
+ const rawComments = JSON.parse(gh(["api", `repos/${repoSlug}/issues/${number}/comments?per_page=100`]));
1953
+ const comments = rawComments.slice(-Math.max(1, limit)).map((c) => ({
1954
+ author: c.user?.login ?? "?",
1955
+ createdAt: c.created_at ?? "",
1956
+ body: (c.body ?? "").slice(0, THREAD_BODY_MAX)
1957
+ }));
1958
+ return {
1959
+ number,
1960
+ title: meta.title ?? "",
1961
+ state: meta.state ?? "",
1962
+ labels: (meta.labels ?? []).map((l) => l.name ?? "").filter(Boolean),
1963
+ comments
1964
+ };
1965
+ }
1966
+ function readCheckRuns(repoSlug, ref, ignoreNames) {
1967
+ const ghOptions = { preferRepoToken: true };
1968
+ const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"], ghOptions).trim();
1969
+ const raw = gh(
1970
+ [
1971
+ "api",
1972
+ `repos/${repoSlug}/commits/${sha}/check-runs`,
1973
+ "--paginate",
1974
+ "--jq",
1975
+ ".check_runs[] | {name, status, conclusion, details_url}"
1976
+ ],
1977
+ ghOptions
1978
+ );
1979
+ const ignore = new Set(ignoreNames.map((n) => n.toLowerCase()));
1980
+ const checks = raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)).filter((c) => !ignore.has(String(c.name).toLowerCase()));
1981
+ 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 }));
1982
+ const pending = checks.filter((c) => String(c.status).toLowerCase() !== "completed").map((c) => ({ name: c.name, status: c.status }));
1983
+ const state = failing.length > 0 ? "RED" : pending.length > 0 ? "PENDING" : "GREEN";
1984
+ return { sha, state, failing, pending };
1985
+ }
1986
+ function ensureIssue(repoSlug, key, title, body) {
1987
+ const marker = trackMarker(key);
1988
+ try {
1989
+ const raw = gh(["issue", "list", "-R", repoSlug, "--state", "open", "--limit", "100", "--json", "number,body"]);
1990
+ const issues = JSON.parse(raw);
1991
+ const existing = issues.find((i) => (i.body ?? "").includes(marker));
1992
+ if (existing) return { created: false, number: existing.number };
1993
+ const url = gh(["issue", "create", "-R", repoSlug, "--title", title, "--body-file", "-"], {
1994
+ input: `${body}
1995
+
1996
+ ${marker}`
2006
1997
  });
2007
- const resultTexts = [];
2008
- outcome = "failed";
2009
- outcomeKind = "generic_failed";
2010
- errorMessage = void 0;
2011
- tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
2012
- messageCount = 0;
2013
- let sawMutatingTool = false;
2014
- let sawTerminalSuccess = false;
2015
- let sawLoginRequired = false;
2016
- let noWorkSuccess = false;
2017
- try {
2018
- const queryOptions = {
2019
- model: opts.model.model,
2020
- cwd: opts.cwd,
2021
- // Fresh array (never mutate the shared DEFAULT_ALLOWED_TOOLS const) so
2022
- // opt-in tools like fetch_repo can be appended below.
2023
- allowedTools: [...opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS],
2024
- permissionMode: opts.permissionModeOverride ?? "acceptEdits",
2025
- env
1998
+ const m = url.trim().match(/\/(\d+)\s*$/);
1999
+ if (!m) return { error: `issue created but could not parse its number from: ${url}` };
2000
+ return { created: true, number: Number(m[1]) };
2001
+ } catch (err) {
2002
+ return { error: err instanceof Error ? err.message : String(err) };
2003
+ }
2004
+ }
2005
+ function ensureComment(repoSlug, issue, key, body) {
2006
+ const marker = commentMarker(key);
2007
+ try {
2008
+ const raw = gh(["issue", "view", String(issue), "-R", repoSlug, "--json", "comments"]);
2009
+ const parsed = JSON.parse(raw);
2010
+ const already = (parsed.comments ?? []).some((c) => (c.body ?? "").includes(marker));
2011
+ if (already) return { posted: false };
2012
+ gh(["issue", "comment", String(issue), "-R", repoSlug, "--body-file", "-"], { input: `${body}
2013
+
2014
+ ${marker}` });
2015
+ return { posted: true };
2016
+ } catch (err) {
2017
+ return { error: err instanceof Error ? err.message : String(err) };
2018
+ }
2019
+ }
2020
+ function dispatchWorkflow(workflowFile, agentResponsibility, issueNumber, repoSlug) {
2021
+ const expected = expectedDispatchTarget(agentResponsibility);
2022
+ if (repoSlug && expected) {
2023
+ const target = readDispatchTargetKind(repoSlug, issueNumber);
2024
+ if (!target.ok) return target;
2025
+ if (expected === "issue" && target.kind === "pr") {
2026
+ return {
2027
+ ok: false,
2028
+ error: `refusing to dispatch ${agentResponsibility} on PR #${issueNumber}; dispatch the source issue or use a PR action`
2026
2029
  };
2027
- const mcpEntries = [];
2028
- if (opts.mcpServers && opts.mcpServers.length > 0) {
2029
- for (const s of opts.mcpServers) {
2030
- const cfg = { command: s.command };
2031
- if (s.args) cfg.args = s.args;
2032
- if (s.env) cfg.env = s.env;
2033
- mcpEntries.push([s.name, cfg]);
2034
- }
2035
- }
2036
- if (opts.enableVerifyTool && opts.verifyConfig) {
2037
- const { buildVerifyMcpServer: buildVerifyMcpServer2 } = await Promise.resolve().then(() => (init_verifyMcp(), verifyMcp_exports));
2038
- const verifyServer = buildVerifyMcpServer2({
2039
- config: opts.verifyConfig,
2040
- cwd: opts.cwd,
2041
- agentAction: opts.agentActionName ?? "agent",
2042
- maxAttempts: typeof opts.verifyToolMaxAttempts === "number" && opts.verifyToolMaxAttempts > 0 ? opts.verifyToolMaxAttempts : void 0
2043
- });
2044
- mcpEntries.push(["kody-verify", verifyServer]);
2045
- }
2046
- if (opts.enableSubmitTool) {
2047
- const { buildSubmitMcpServer: buildSubmitMcpServer2 } = await Promise.resolve().then(() => (init_submitMcp(), submitMcp_exports));
2048
- const submitHandle = buildSubmitMcpServer2();
2049
- getSubmitted = submitHandle.getSubmitted;
2050
- mcpEntries.push(["kody-submit", submitHandle.server]);
2030
+ }
2031
+ if (expected === "pr" && target.kind === "issue") {
2032
+ return {
2033
+ ok: false,
2034
+ error: `refusing to dispatch ${agentResponsibility} on issue #${issueNumber}; expected a PR target`
2035
+ };
2036
+ }
2037
+ }
2038
+ try {
2039
+ gh([
2040
+ "workflow",
2041
+ "run",
2042
+ workflowFile,
2043
+ "-f",
2044
+ `agentResponsibility=${agentResponsibility}`,
2045
+ "-f",
2046
+ `issue_number=${issueNumber}`
2047
+ ]);
2048
+ return { ok: true };
2049
+ } catch (err) {
2050
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2051
+ }
2052
+ }
2053
+ function expectedDispatchTarget(agentResponsibility) {
2054
+ const route = resolveAgentResponsibilityAction(agentResponsibility);
2055
+ if (!route) return null;
2056
+ const inputs = getProfileInputs(route.agentAction);
2057
+ const numeric = inputs?.find((input) => input.type === "int" && input.required);
2058
+ if (numeric?.name === "issue") return "issue";
2059
+ if (numeric?.name === "pr") return "pr";
2060
+ return null;
2061
+ }
2062
+ function readDispatchTargetKind(repoSlug, issueNumber) {
2063
+ try {
2064
+ const raw = gh(["api", `repos/${repoSlug}/issues/${issueNumber}`]);
2065
+ const parsed = JSON.parse(raw);
2066
+ return { ok: true, kind: parsed.pull_request ? "pr" : "issue" };
2067
+ } catch (err) {
2068
+ return {
2069
+ ok: false,
2070
+ error: `could not verify target #${issueNumber}: ${err instanceof Error ? err.message : String(err)}`
2071
+ };
2072
+ }
2073
+ }
2074
+ function isDispatchGated(agentResponsibility, mode) {
2075
+ if (mode === "auto") return false;
2076
+ if (agentResponsibility && GATE_EXEMPT_DUTIES.has(agentResponsibility)) return false;
2077
+ return true;
2078
+ }
2079
+ function trustRefusal(agentResponsibilitySlug) {
2080
+ return `Not dispatched: agentResponsibility \`${agentResponsibilitySlug ?? "?"}\` 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 agentResponsibility act on its own, grant it Auto on the dashboard Trust page.`;
2081
+ }
2082
+ function agentResponsibilityToolDefinitions(opts) {
2083
+ const workflowFile = opts.workflowFile ?? "kody.yml";
2084
+ const listTool = {
2085
+ name: "list_prs_to_repair",
2086
+ description: "Return open non-draft PRs with the signals you need to pick a repair: number, title, headSha, baseRef, mergeable (CONFLICTING/MERGEABLE/UNKNOWN), ciStatus (PASSING/FAILING/RUNNING/UNKNOWN), behindBy (commits behind base; 0 for PRs that already match conflicts or CI-failure rules), updatedAt. Drafts are excluded. One call returns everything \u2014 do not iterate or paginate.",
2087
+ inputSchema: {},
2088
+ handler: async () => {
2089
+ const candidates = listRepairCandidates(opts.repoSlug);
2090
+ return {
2091
+ content: [
2092
+ {
2093
+ type: "text",
2094
+ text: JSON.stringify({ prs: candidates }, null, 2)
2095
+ }
2096
+ ]
2097
+ };
2098
+ }
2099
+ };
2100
+ const makeDispatch = (verb, describe) => ({
2101
+ name: `${verb.replace("-", "_")}_pr`,
2102
+ description: describe,
2103
+ inputSchema: {
2104
+ pr: z2.number().int().positive().describe("PR number to repair.")
2105
+ },
2106
+ handler: async (args) => {
2107
+ const pr = Number(args.pr);
2108
+ if (isDispatchGated(verb, readAgentResponsibilityTrustMode(opts.state, opts.repoSlug, opts.agentResponsibilitySlug))) {
2109
+ return { content: [{ type: "text", text: trustRefusal(opts.agentResponsibilitySlug) }] };
2051
2110
  }
2052
- if (opts.enableAgentResponsibilityTool) {
2053
- const { buildAgentResponsibilityMcpServer: buildAgentResponsibilityMcpServer2 } = await Promise.resolve().then(() => (init_agent_responsibilityMcp(), agent_responsibilityMcp_exports));
2054
- if (!opts.dutyRepoSlug) {
2055
- throw new Error(
2056
- "enableAgentResponsibilityTool requires dutyRepoSlug (owner/name) \u2014 set kody.config.json github.{owner,repo} or GITHUB_REPOSITORY env var"
2057
- );
2058
- }
2059
- const dutyHandle = buildAgentResponsibilityMcpServer2({
2060
- repoSlug: opts.dutyRepoSlug,
2061
- state: opts.agentResponsibilityState,
2062
- operatorMention: opts.agentResponsibilityOperatorMention ?? "",
2063
- ...opts.agentResponsibilitySlug ? { agentResponsibilitySlug: opts.agentResponsibilitySlug } : {}
2064
- });
2065
- mcpEntries.push(["kody-agentResponsibility", dutyHandle.server]);
2066
- }
2067
- if (opts.enableFetchRepoTool && opts.reposRoot) {
2068
- const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
2069
- const fetchServer = buildFetchRepoMcpServer2({
2070
- reposRoot: opts.reposRoot,
2071
- repoToken: opts.repoToken
2072
- });
2073
- mcpEntries.push(["kody-fetch-repo", fetchServer]);
2074
- queryOptions.allowedTools.push("mcp__kody-fetch-repo__fetch_repo");
2075
- queryOptions.additionalDirectories = [opts.reposRoot];
2076
- }
2077
- if (mcpEntries.length > 0) {
2078
- queryOptions.mcpServers = Object.fromEntries(mcpEntries);
2079
- }
2080
- if (opts.pluginPaths && opts.pluginPaths.length > 0) {
2081
- queryOptions.plugins = opts.pluginPaths.map((p) => ({ type: "local", path: p }));
2082
- }
2083
- if (opts.agents && Object.keys(opts.agents).length > 0) {
2084
- queryOptions.agents = opts.agents;
2085
- }
2086
- if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
2087
- queryOptions.maxTurns = opts.maxTurns;
2088
- }
2089
- if (opts.reasoningEffort !== void 0 && opts.reasoningEffort !== null) {
2090
- if (opts.reasoningEffort === "off") {
2091
- } else {
2092
- const budget = REASONING_BUDGETS[opts.reasoningEffort];
2093
- if (budget) queryOptions.maxThinkingTokens = budget;
2094
- }
2095
- } else if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
2096
- queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
2097
- }
2098
- if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
2099
- const systemPrompt = {
2100
- type: "preset",
2101
- preset: "claude_code",
2102
- append: opts.systemPromptAppend
2103
- };
2104
- if (opts.cacheable) systemPrompt.excludeDynamicSections = true;
2105
- queryOptions.systemPrompt = systemPrompt;
2106
- } else if (opts.cacheable) {
2107
- queryOptions.systemPrompt = {
2108
- type: "preset",
2109
- preset: "claude_code",
2110
- excludeDynamicSections: true
2111
- };
2112
- }
2113
- queryOptions.settingSources = opts.settingSources ?? ["project", "local"];
2114
- const stableBinary = ensureStableClaudeBinary();
2115
- if (stableBinary) {
2116
- queryOptions.pathToClaudeCodeExecutable = stableBinary;
2117
- }
2118
- const result = query({
2119
- prompt: opts.prompt,
2120
- // biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
2121
- options: queryOptions
2122
- });
2123
- const iterator = typeof result[Symbol.asyncIterator] === "function" ? result[Symbol.asyncIterator]() : result;
2124
- while (true) {
2125
- const nextPromise = iterator.next();
2126
- let timedOut = false;
2127
- let timer;
2128
- let next;
2129
- if (turnTimeoutMs > 0) {
2130
- const timeoutPromise = new Promise((resolve7) => {
2131
- timer = setTimeout(() => {
2132
- timedOut = true;
2133
- resolve7({ done: true, value: void 0 });
2134
- }, turnTimeoutMs);
2135
- });
2136
- next = await Promise.race([nextPromise, timeoutPromise]);
2137
- if (timer) clearTimeout(timer);
2138
- } else {
2139
- next = await nextPromise;
2140
- }
2141
- if (timedOut) {
2142
- outcome = "failed";
2143
- outcomeKind = "stalled";
2144
- errorMessage = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
2145
- if (typeof iterator.return === "function") {
2146
- try {
2147
- await iterator.return(void 0);
2148
- } catch {
2149
- }
2150
- }
2151
- break;
2152
- }
2153
- if (next.done) break;
2154
- const msg = next.value;
2155
- messageCount++;
2156
- if (!ndjsonWriteFailed) {
2157
- try {
2158
- fullLog.write(`${JSON.stringify(msg)}
2159
- `);
2160
- } catch (e) {
2161
- ndjsonWriteFailed = true;
2162
- ndjsonWriteError = e instanceof Error ? e.message : String(e);
2163
- }
2164
- }
2165
- const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
2166
- if (line) {
2167
- if (isClaudeLoginRequiredText(line)) sawLoginRequired = true;
2168
- process.stdout.write(`${line}
2169
- `);
2170
- }
2171
- const m = msg;
2172
- if (opts.onProgress) {
2173
- const blocks = m.message?.content ?? [];
2174
- for (const block of blocks) {
2175
- try {
2176
- if (block.type === "thinking") {
2177
- const t = block.thinking;
2178
- if (typeof t === "string" && t.length > 0) {
2179
- await opts.onProgress({ kind: "thinking", thinking: t });
2180
- }
2181
- } else if (block.type === "tool_use") {
2182
- const b = block;
2183
- await opts.onProgress({
2184
- kind: "tool_use",
2185
- name: b.name ?? "tool",
2186
- input: b.input,
2187
- id: b.id
2188
- });
2189
- } else if (block.type === "tool_result") {
2190
- const b = block;
2191
- const content = typeof b.content === "string" ? b.content : (() => {
2192
- try {
2193
- return JSON.stringify(b.content);
2194
- } catch {
2195
- return "";
2196
- }
2197
- })();
2198
- await opts.onProgress({
2199
- kind: "tool_result",
2200
- toolUseId: b.tool_use_id,
2201
- content,
2202
- isError: b.is_error
2203
- });
2204
- } else if (block.type === "text") {
2205
- const b = block;
2206
- if (typeof b.text === "string" && b.text.length > 0) {
2207
- await opts.onProgress({ kind: "text", text: b.text });
2208
- }
2209
- }
2210
- } catch {
2211
- }
2212
- }
2213
- }
2214
- const usage = m.usage;
2215
- if (usage && typeof usage === "object") {
2216
- const i = Number(usage.input_tokens ?? 0);
2217
- const o = Number(usage.output_tokens ?? 0);
2218
- const cr = Number(usage.cache_read_input_tokens ?? 0);
2219
- const cc = Number(usage.cache_creation_input_tokens ?? 0);
2220
- if (Number.isFinite(i)) tokens.input += i;
2221
- if (Number.isFinite(o)) tokens.output += o;
2222
- if (Number.isFinite(cr)) tokens.cacheRead += cr;
2223
- if (Number.isFinite(cc)) tokens.cacheCreate += cc;
2224
- }
2225
- if (!sawMutatingTool) {
2226
- const blocks = m.message?.content ?? [];
2227
- for (const block of blocks) {
2228
- if (block.type === "tool_use") {
2229
- const b = block;
2230
- if (toolMayMutate(b.name, b.input)) {
2231
- sawMutatingTool = true;
2232
- break;
2233
- }
2234
- }
2235
- }
2236
- }
2237
- if (m.type === "result") {
2238
- if (m.subtype === "success") {
2239
- outcome = "completed";
2240
- outcomeKind = "ok";
2241
- sawTerminalSuccess = true;
2242
- const text = (typeof m.result === "string" ? m.result : "").trim();
2243
- if (isClaudeLoginRequiredText(text)) sawLoginRequired = true;
2244
- if (text) resultTexts.push(text);
2245
- } else {
2246
- outcome = "failed";
2247
- outcomeKind = classifySubtype(m.subtype);
2248
- errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
2249
- }
2250
- }
2251
- }
2252
- } catch (e) {
2253
- if (sawTerminalSuccess) {
2254
- errorMessage = e instanceof Error ? e.message : String(e);
2255
- } else {
2256
- outcome = "failed";
2257
- outcomeKind = "model_error";
2258
- errorMessage = e instanceof Error ? e.message : String(e);
2259
- }
2260
- } finally {
2261
- try {
2262
- fullLog.end();
2263
- } catch {
2264
- }
2265
- }
2266
- if (ndjsonWriteFailed) {
2267
- process.stderr.write(
2268
- `[kody agent] NDJSON write failed (post-mortem may be incomplete): ${ndjsonWriteError ?? "unknown error"}
2269
- `
2270
- );
2111
+ const result = dispatchVerb(workflowFile, opts.repoSlug, verb, pr);
2112
+ const text = result.ok ? `Dispatched \`${verb}\` on PR #${pr}. The repair runs in its own workflow_dispatch \u2014 wait for the next tick to see the new headSha.` : `Dispatch failed for \`${verb}\` on PR #${pr}: ${result.error}`;
2113
+ return { content: [{ type: "text", text }] };
2271
2114
  }
2272
- finalText = resultTexts.join("\n\n---\n\n");
2273
- if (outcome === "completed" && sawLoginRequired) {
2274
- outcome = "failed";
2275
- outcomeKind = "model_error";
2276
- errorMessage = "Claude Code reported it is not logged in; refusing to mark agent run successful";
2115
+ });
2116
+ const syncTool = makeDispatch(
2117
+ "sync",
2118
+ "Bring a stale PR up to date with its base branch (merges base \u2192 head + pushes). Use when behindBy > 10 AND mergeable !== CONFLICTING AND ciStatus !== FAILING. Returns immediately \u2014 the actual merge runs in a separate workflow."
2119
+ );
2120
+ const fixCiTool = makeDispatch(
2121
+ "fix-ci",
2122
+ "Repair a PR whose CI is failing. Use when ciStatus === FAILING. The repair runs in a separate workflow."
2123
+ );
2124
+ const resolveTool = makeDispatch(
2125
+ "resolve",
2126
+ "Resolve merge conflicts on a PR. Use when mergeable === CONFLICTING. The repair runs in a separate workflow."
2127
+ );
2128
+ const recommendTool = {
2129
+ name: "recommend_to_operator",
2130
+ description: "Post ONE comment on a PR with the operator @-mention prepended. Use this when a agentResponsibility 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.",
2131
+ inputSchema: {
2132
+ pr: z2.number().int().positive().describe("PR number to comment on."),
2133
+ body: z2.string().min(1).describe("Comment body (markdown). Do not include the operator mention \u2014 the engine prepends it.")
2134
+ },
2135
+ handler: async (args) => {
2136
+ const pr = Number(args.pr);
2137
+ const body = String(args.body ?? "");
2138
+ const result = postRecommendation(pr, opts.operatorMention, body, opts.agentResponsibilitySlug);
2139
+ const text = result.ok ? `Recommendation posted on PR #${pr}.` : `Recommendation failed on PR #${pr}: ${result.error}`;
2140
+ return { content: [{ type: "text", text }] };
2277
2141
  }
2278
- if (outcome === "completed" && !sawMutatingTool) {
2279
- const backendDead = opts.isBackendHealthy ? !await opts.isBackendHealthy() : false;
2280
- const zeroOutput = tokens.output === 0 && finalText === "";
2281
- if (backendDead || zeroOutput) {
2282
- outcome = "failed";
2283
- outcomeKind = "model_error";
2284
- noWorkSuccess = true;
2285
- 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");
2286
- }
2142
+ };
2143
+ const ledgerTool = {
2144
+ name: "read_ledger",
2145
+ 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.",
2146
+ inputSchema: {
2147
+ label: z2.string().min(1).describe("GitHub issue label that identifies the manifest issue.")
2148
+ },
2149
+ handler: async (args) => {
2150
+ const label = String(args.label ?? "");
2151
+ const result = readLedger(label);
2152
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2287
2153
  }
2288
- const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage) || noWorkSuccess);
2289
- if (!shouldRetry) break;
2290
- const delayMs = CONNECTION_RETRY_BASE_MS * 2 ** attempt;
2291
- process.stderr.write(
2292
- `[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage}
2293
- `
2294
- );
2295
- if (opts.ensureBackend) {
2296
- try {
2297
- await opts.ensureBackend();
2298
- } catch (e) {
2299
- process.stderr.write(`[kody agent] backend recovery failed: ${e instanceof Error ? e.message : String(e)}
2300
- `);
2154
+ };
2155
+ const checkRunsTool = {
2156
+ name: "read_check_runs",
2157
+ 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/agent-responsibility-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.",
2158
+ inputSchema: {
2159
+ ref: z2.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
2160
+ ignoreNames: z2.array(z2.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
2161
+ },
2162
+ handler: async (args) => {
2163
+ const ref = String(args.ref ?? "");
2164
+ const ignoreNames = Array.isArray(args.ignoreNames) ? args.ignoreNames : DEFAULT_IGNORE_CHECKS;
2165
+ const result = readCheckRuns(opts.repoSlug, ref, ignoreNames);
2166
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2167
+ }
2168
+ };
2169
+ const readThreadTool = {
2170
+ name: "read_thread",
2171
+ 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.",
2172
+ inputSchema: {
2173
+ number: z2.number().int().positive().describe("Issue or PR number to read."),
2174
+ limit: z2.number().int().positive().optional().describe("Max recent comments to return (default 10).")
2175
+ },
2176
+ handler: async (args) => {
2177
+ const number = Number(args.number);
2178
+ const limit = args.limit ? Number(args.limit) : 10;
2179
+ const result = readThread(opts.repoSlug, number, limit);
2180
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2181
+ }
2182
+ };
2183
+ const ensureIssueTool = {
2184
+ name: "ensure_issue",
2185
+ 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.",
2186
+ inputSchema: {
2187
+ key: z2.string().min(1).describe(
2188
+ "Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."
2189
+ ),
2190
+ title: z2.string().min(1).describe("Issue title (used only on first creation)."),
2191
+ body: z2.string().min(1).describe(
2192
+ "Issue body markdown (used only on first creation). Include the operator mention verbatim if the agentResponsibility body has one."
2193
+ )
2194
+ },
2195
+ handler: async (args) => {
2196
+ const key = String(args.key ?? "");
2197
+ const title = String(args.title ?? "");
2198
+ const body = String(args.body ?? "");
2199
+ const result = ensureIssue(opts.repoSlug, key, title, body);
2200
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2201
+ }
2202
+ };
2203
+ const ensureCommentTool = {
2204
+ name: "ensure_comment",
2205
+ 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.",
2206
+ inputSchema: {
2207
+ issue: z2.number().int().positive().describe("Issue number to comment on."),
2208
+ key: z2.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
2209
+ body: z2.string().min(1).describe("Comment body markdown.")
2210
+ },
2211
+ handler: async (args) => {
2212
+ const issue = Number(args.issue);
2213
+ const key = String(args.key ?? "");
2214
+ const body = String(args.body ?? "");
2215
+ const result = ensureComment(opts.repoSlug, issue, key, body);
2216
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
2217
+ }
2218
+ };
2219
+ const dispatchTool = {
2220
+ name: "dispatch_workflow",
2221
+ description: "Dispatch a kody.yml workflow_dispatch run for a agentResponsibility action against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({agentResponsibility:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
2222
+ inputSchema: {
2223
+ agentResponsibility: z2.string().min(1).optional().describe("AgentResponsibility action to run (e.g. 'run')."),
2224
+ agentAction: z2.string().min(1).optional().describe("Deprecated alias for agentResponsibility."),
2225
+ issueNumber: z2.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
2226
+ },
2227
+ handler: async (args) => {
2228
+ const agentResponsibility = String(args.agentResponsibility ?? args.agentAction ?? "");
2229
+ const issueNumber = Number(args.issueNumber);
2230
+ if (isDispatchGated(
2231
+ agentResponsibility,
2232
+ readAgentResponsibilityTrustMode(opts.state, opts.repoSlug, opts.agentResponsibilitySlug)
2233
+ )) {
2234
+ return { content: [{ type: "text", text: trustRefusal(opts.agentResponsibilitySlug) }] };
2301
2235
  }
2236
+ const result = dispatchWorkflow(workflowFile, agentResponsibility, issueNumber, opts.repoSlug);
2237
+ const text = result.ok ? `Dispatched agentResponsibility \`${agentResponsibility}\` on #${issueNumber} via workflow_dispatch.` : `Dispatch failed for agentResponsibility \`${agentResponsibility}\` on #${issueNumber}: ${result.error}`;
2238
+ return { content: [{ type: "text", text }] };
2302
2239
  }
2303
- await new Promise((r) => setTimeout(r, delayMs));
2304
- }
2305
- const submittedState = getSubmitted?.();
2306
- return {
2307
- outcome,
2308
- outcomeKind,
2309
- finalText,
2310
- ...submittedState ? { submittedState } : {},
2311
- error: errorMessage,
2312
- ndjsonPath,
2313
- durationMs: Date.now() - startedAt,
2314
- tokens,
2315
- messageCount
2316
2240
  };
2241
+ return [
2242
+ listTool,
2243
+ syncTool,
2244
+ fixCiTool,
2245
+ resolveTool,
2246
+ recommendTool,
2247
+ ledgerTool,
2248
+ checkRunsTool,
2249
+ readThreadTool,
2250
+ ensureIssueTool,
2251
+ ensureCommentTool,
2252
+ dispatchTool
2253
+ ];
2317
2254
  }
2318
- var DEFAULT_ALLOWED_TOOLS, DEFAULT_TURN_TIMEOUT_MS, MAX_CONNECTION_RETRIES, CONNECTION_RETRY_BASE_MS, MUTATING_FILE_TOOLS, BASH_WRITE_VERB, AGENT_KEEP_SECRETS;
2319
- var init_agent = __esm({
2320
- "src/agent.ts"() {
2255
+ function buildAgentResponsibilityMcpServer(opts) {
2256
+ const definitions = agentResponsibilityToolDefinitions(opts);
2257
+ const tools = definitions.map(
2258
+ (def) => tool3(
2259
+ def.name,
2260
+ def.description,
2261
+ def.inputSchema,
2262
+ async (args) => def.handler(args)
2263
+ )
2264
+ );
2265
+ const server = createSdkMcpServer3({
2266
+ name: "kody-agentResponsibility",
2267
+ version: "0.1.0",
2268
+ tools
2269
+ });
2270
+ return { server };
2271
+ }
2272
+ var FAIL_CONCLUSIONS, RUNNING_STATUSES, TRUST_FILE_PATH, THREAD_BODY_MAX, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, GATE_EXEMPT_DUTIES, AGENT_RESPONSIBILITY_MCP_TOOL_NAMES;
2273
+ var init_agent_responsibilityMcp = __esm({
2274
+ "src/agent-responsibilityMcp.ts"() {
2321
2275
  "use strict";
2322
- init_claudeBinary();
2323
- init_config();
2324
- init_format();
2325
- DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
2326
- DEFAULT_TURN_TIMEOUT_MS = 6e5;
2327
- MAX_CONNECTION_RETRIES = 2;
2328
- CONNECTION_RETRY_BASE_MS = 2e3;
2329
- MUTATING_FILE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
2330
- BASH_WRITE_VERB = /\b(git\s+(commit|push|merge|rebase|tag|reset|cherry-pick)|gh\s+(pr|issue|release)\s+(create|comment|edit|close|merge|review|reopen)|gh\s+api\b[^|&]*-X\s*(POST|PUT|PATCH|DELETE)|npm\s+publish)\b/i;
2331
- AGENT_KEEP_SECRETS = /* @__PURE__ */ new Set([
2332
- "ANTHROPIC_API_KEY",
2333
- "ANTHROPIC_AUTH_TOKEN",
2334
- "ANTHROPIC_BASE_URL",
2335
- "GH_TOKEN",
2336
- "GITHUB_TOKEN"
2337
- ]);
2276
+ init_issue();
2277
+ init_registry();
2278
+ init_stateRepo();
2279
+ FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
2280
+ RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
2281
+ TRUST_FILE_PATH = "state/trust.json";
2282
+ THREAD_BODY_MAX = 4e3;
2283
+ CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
2284
+ DEFAULT_IGNORE_CHECKS = ["run", "kody", "agent-responsibility-tick", "agent-ask", "chat"];
2285
+ trackMarker = (key) => `<!-- kody-track:${key} -->`;
2286
+ commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
2287
+ GATE_EXEMPT_DUTIES = /* @__PURE__ */ new Set(["qa-engineer", "ui-review"]);
2288
+ AGENT_RESPONSIBILITY_MCP_TOOL_NAMES = [
2289
+ "list_prs_to_repair",
2290
+ "sync_pr",
2291
+ "fix_ci_pr",
2292
+ "resolve_pr",
2293
+ "recommend_to_operator",
2294
+ "read_ledger",
2295
+ "read_check_runs",
2296
+ "read_thread",
2297
+ "ensure_issue",
2298
+ "ensure_comment",
2299
+ "dispatch_workflow"
2300
+ ];
2338
2301
  }
2339
2302
  });
2340
2303
 
2341
- // src/agent-responsibilityFolders.ts
2342
- import * as fs6 from "fs";
2343
- import * as path8 from "path";
2344
- function listAgentResponsibilityFolderSlugs(absDir) {
2345
- if (!fs6.existsSync(absDir)) return [];
2346
- let entries;
2347
- try {
2348
- entries = fs6.readdirSync(absDir, { withFileTypes: true });
2349
- } catch {
2350
- return [];
2304
+ // src/repoWorkspace.ts
2305
+ import { spawn as spawn2, spawnSync } from "child_process";
2306
+ import * as fs7 from "fs";
2307
+ import * as path9 from "path";
2308
+ async function resolveAndClone(reposRoot, repo, repoToken, cloneRepo) {
2309
+ const name = repo?.trim();
2310
+ if (!name || !REPO_RE.test(name)) return null;
2311
+ const root = path9.resolve(reposRoot);
2312
+ const dir = path9.resolve(root, name);
2313
+ if (dir !== root && !dir.startsWith(root + path9.sep)) return null;
2314
+ if (fs7.existsSync(path9.join(dir, ".git"))) return dir;
2315
+ const inflight = repoClones.get(dir);
2316
+ if (inflight) {
2317
+ await inflight;
2318
+ return dir;
2351
2319
  }
2352
- return entries.filter((e) => e.isDirectory() && !e.name.startsWith("_") && !e.name.startsWith(".")).filter((e) => isAgentResponsibilityFolder(path8.join(absDir, e.name))).map((e) => e.name).sort();
2320
+ const p = cloneRepo(name, repoToken, dir).finally(() => {
2321
+ if (repoClones.get(dir) === p) repoClones.delete(dir);
2322
+ });
2323
+ repoClones.set(dir, p);
2324
+ await p;
2325
+ return dir;
2353
2326
  }
2354
- function isAgentResponsibilityFolder(dir) {
2355
- return fs6.existsSync(path8.join(dir, AGENT_RESPONSIBILITY_PROFILE_FILE)) && fs6.existsSync(path8.join(dir, AGENT_RESPONSIBILITY_BODY_FILE));
2327
+ async function ensureRepoCwd(opts) {
2328
+ const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo);
2329
+ return dir ?? opts.baseCwd;
2356
2330
  }
2357
- function readAgentResponsibilityFolder(root, slug2) {
2358
- const dir = path8.join(root, slug2);
2359
- const profilePath = path8.join(dir, AGENT_RESPONSIBILITY_PROFILE_FILE);
2360
- const bodyPath = path8.join(dir, AGENT_RESPONSIBILITY_BODY_FILE);
2361
- if (!fs6.existsSync(profilePath) || !fs6.statSync(profilePath).isFile()) return null;
2362
- if (!fs6.existsSync(bodyPath) || !fs6.statSync(bodyPath).isFile()) return null;
2363
- try {
2364
- const rawProfile = JSON.parse(fs6.readFileSync(profilePath, "utf-8"));
2365
- const rawBody = fs6.readFileSync(bodyPath, "utf-8");
2366
- const { title, body } = parseAgentResponsibilityBody(rawBody, slug2);
2367
- return {
2368
- slug: slug2,
2369
- dir,
2370
- profilePath,
2371
- bodyPath,
2372
- title,
2373
- body,
2374
- rawBody,
2375
- config: parseAgentResponsibilityConfig(rawProfile),
2376
- rawProfile
2377
- };
2378
- } catch {
2379
- return null;
2331
+ async function fetchRepo(opts) {
2332
+ const dir = await resolveAndClone(opts.reposRoot, opts.repo, opts.repoToken, opts.cloneRepo ?? defaultCloneRepo);
2333
+ if (!dir) {
2334
+ throw new Error(`invalid repo "${opts.repo}" \u2014 expected "owner/name" with no path escapes`);
2380
2335
  }
2336
+ return dir;
2381
2337
  }
2382
- function parseAgentResponsibilityConfig(raw) {
2383
- const tools = stringList(raw.tools ?? raw.agentResponsibilityTools);
2384
- return {
2385
- action: stringField(raw.action),
2386
- agentAction: stringField(raw.agentAction),
2387
- tickScript: stringField(raw.tickScript),
2388
- disabled: typeof raw.disabled === "boolean" ? raw.disabled : void 0,
2389
- agent: stringField(raw.agent),
2390
- mentions: stringList(raw.mentions).map((m) => m.replace(/^@/, "")),
2391
- tools,
2392
- agentActions: stringList(raw.agentActions),
2393
- capabilityKind: capabilityKindField(raw.capabilityKind),
2394
- describe: stringField(raw.describe),
2395
- stage: stringField(raw.stage),
2396
- readsFrom: stringList(raw.readsFrom ?? raw.reads_from),
2397
- writesTo: stringList(raw.writesTo ?? raw.writes_to)
2398
- };
2399
- }
2400
- function parseAgentResponsibilityBody(raw, slug2) {
2401
- const trimmed = raw.trim();
2402
- const firstLine2 = trimmed.split("\n", 1)[0] ?? "";
2403
- const h1 = /^#\s+(.+?)\s*$/.exec(firstLine2);
2404
- const title = h1 ? h1[1].trim() : humanizeSlug(slug2);
2405
- const body = stripLeadingH1(raw);
2406
- return { title, body };
2407
- }
2408
- function stripLeadingH1(raw) {
2409
- const lines = raw.replace(/^\uFEFF/, "").split("\n");
2410
- let i = 0;
2411
- for (; ; ) {
2412
- while (i < lines.length && lines[i].trim() === "") i++;
2413
- if (i < lines.length && /^#\s+.+/.test(lines[i])) i++;
2414
- else break;
2415
- }
2416
- return lines.slice(i).join("\n");
2417
- }
2418
- function humanizeSlug(slug2) {
2419
- return slug2.split(/[-_]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join(" ");
2420
- }
2421
- function stringField(value) {
2422
- return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
2423
- }
2424
- function stringList(value) {
2425
- if (Array.isArray(value)) {
2426
- return value.map((v) => String(v).trim()).filter(Boolean);
2427
- }
2428
- if (typeof value === "string") {
2429
- return value.split(",").map((v) => v.trim()).filter(Boolean);
2430
- }
2431
- return [];
2432
- }
2433
- function capabilityKindField(value) {
2434
- if (value === "observe" || value === "act" || value === "verify") return value;
2435
- return void 0;
2436
- }
2437
- var AGENT_RESPONSIBILITY_PROFILE_FILE, AGENT_RESPONSIBILITY_BODY_FILE;
2438
- var init_agent_responsibilityFolders = __esm({
2439
- "src/agent-responsibilityFolders.ts"() {
2338
+ var REPO_RE, repoClones, defaultCloneRepo;
2339
+ var init_repoWorkspace = __esm({
2340
+ "src/repoWorkspace.ts"() {
2440
2341
  "use strict";
2441
- AGENT_RESPONSIBILITY_PROFILE_FILE = "profile.json";
2442
- AGENT_RESPONSIBILITY_BODY_FILE = "agent-responsibility.md";
2342
+ REPO_RE = /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/;
2343
+ repoClones = /* @__PURE__ */ new Map();
2344
+ defaultCloneRepo = (repo, token, dir) => {
2345
+ fs7.mkdirSync(path9.dirname(dir), { recursive: true });
2346
+ const authUrl = token ? `https://x-access-token:${token}@github.com/${repo}.git` : `https://github.com/${repo}.git`;
2347
+ return new Promise((resolve7, reject) => {
2348
+ const child = spawn2("git", ["clone", "--depth=1", authUrl, dir], {
2349
+ stdio: "inherit"
2350
+ });
2351
+ child.on("exit", (code) => {
2352
+ if (code !== 0) {
2353
+ reject(new Error(`git clone ${repo} failed (exit ${code})`));
2354
+ return;
2355
+ }
2356
+ try {
2357
+ const name = process.env.GIT_AUTHOR_NAME ?? "Kody Bot";
2358
+ const email = process.env.GIT_AUTHOR_EMAIL ?? "kody-bot@users.noreply.github.com";
2359
+ spawnSync("git", ["-C", dir, "config", "user.name", name]);
2360
+ spawnSync("git", ["-C", dir, "config", "user.email", email]);
2361
+ } catch {
2362
+ }
2363
+ resolve7();
2364
+ });
2365
+ child.on("error", reject);
2366
+ });
2367
+ };
2443
2368
  }
2444
2369
  });
2445
2370
 
2446
- // src/companyStore.ts
2447
- import { execFileSync as execFileSync2 } from "child_process";
2448
- import * as crypto3 from "crypto";
2449
- import * as fs7 from "fs";
2450
- import * as os3 from "os";
2451
- import * as path9 from "path";
2452
- function getCompanyStoreRoot() {
2453
- const store = resolveCompanyStore();
2454
- if (!store) return null;
2455
- const key = `${store.repo}#${store.ref}`;
2456
- if (memo?.key === key) return memo.root;
2457
- const root = fetchCompanyStore(store.repo, store.ref);
2458
- memo = { key, root };
2459
- return root;
2460
- }
2461
- function getCompanyStoreAssetRoot(kind) {
2462
- const root = getCompanyStoreRoot();
2463
- if (!root) return null;
2464
- const folderByKind = {
2465
- agentResponsibilities: "agent-responsibilities",
2466
- agentActions: "agent-actions",
2467
- goals: "goals",
2468
- agents: "agents"
2371
+ // src/fetchRepoMcp.ts
2372
+ var fetchRepoMcp_exports = {};
2373
+ __export(fetchRepoMcp_exports, {
2374
+ buildFetchRepoMcpServer: () => buildFetchRepoMcpServer,
2375
+ fetchRepoToolDefinition: () => fetchRepoToolDefinition
2376
+ });
2377
+ import { createSdkMcpServer as createSdkMcpServer4, tool as tool4 } from "@anthropic-ai/claude-agent-sdk";
2378
+ import { z as z3 } from "zod";
2379
+ function fetchRepoToolDefinition(opts) {
2380
+ return {
2381
+ name: "fetch_repo",
2382
+ description: DESCRIPTION3,
2383
+ inputSchema: INPUT_SCHEMA3,
2384
+ handler: async (args) => {
2385
+ const repo = String(args.repo ?? "").trim();
2386
+ try {
2387
+ const dir = await fetchRepo({
2388
+ reposRoot: opts.reposRoot,
2389
+ repo,
2390
+ repoToken: opts.repoToken
2391
+ });
2392
+ return {
2393
+ content: [
2394
+ {
2395
+ type: "text",
2396
+ text: `Cloned ${repo} \u2192 ${dir}
2397
+ Use Read/Grep/Glob/Bash at that absolute path to explore it. It now lives in your workspace alongside any other repos you've fetched.`
2398
+ }
2399
+ ]
2400
+ };
2401
+ } catch (err) {
2402
+ const msg = err instanceof Error ? err.message : String(err);
2403
+ return {
2404
+ content: [{ type: "text", text: `Could not fetch ${repo}: ${msg}` }],
2405
+ isError: true
2406
+ };
2407
+ }
2408
+ }
2469
2409
  };
2470
- return path9.join(root, ".kody", folderByKind[kind]);
2471
2410
  }
2472
- function resolveCompanyStore() {
2473
- const envStore = process.env[STORE_ENV]?.trim();
2474
- if (!envStore && process.env.VITEST) return null;
2475
- const repo = envStore || DEFAULT_COMPANY_STORE;
2476
- if (repo === "0" || repo === "false" || repo === "off") return null;
2477
- const ref = process.env[REF_ENV]?.trim() || DEFAULT_COMPANY_STORE_REF;
2478
- if (!ref) return null;
2479
- return { repo, ref };
2411
+ function buildFetchRepoMcpServer(opts) {
2412
+ const def = fetchRepoToolDefinition(opts);
2413
+ const fetchTool = tool4(def.name, def.description, def.inputSchema, async (args) => {
2414
+ return def.handler({ repo: String(args.repo ?? "") });
2415
+ });
2416
+ return createSdkMcpServer4({
2417
+ name: "kody-fetch-repo",
2418
+ version: "0.1.0",
2419
+ tools: [fetchTool]
2420
+ });
2480
2421
  }
2481
- function fetchCompanyStore(repo, ref) {
2482
- const localRoot = localStoreRoot(repo);
2483
- if (localRoot) return localRoot;
2484
- const url = repoToGitUrl(repo);
2485
- const cacheDir = path9.join(cacheRoot(), cacheKey(repo, ref));
2486
- try {
2487
- if (isGitHubShorthand(repo)) setupGithubGitAuth();
2488
- fs7.mkdirSync(path9.dirname(cacheDir), { recursive: true });
2489
- if (!fs7.existsSync(path9.join(cacheDir, ".git"))) {
2490
- fs7.rmSync(cacheDir, { recursive: true, force: true });
2491
- runGit(["clone", "--no-checkout", "--filter=blob:none", url, cacheDir]);
2492
- }
2493
- runGit(["-C", cacheDir, "remote", "set-url", "origin", url]);
2494
- runGit(["-C", cacheDir, "fetch", "--depth=1", "origin", ref]);
2495
- runGit(["-C", cacheDir, "checkout", "--detach", "FETCH_HEAD"]);
2496
- return cacheDir;
2497
- } catch (err) {
2498
- const msg = err instanceof Error ? err.message : String(err);
2499
- process.stderr.write(`[company-store] failed to fetch ${repo}#${ref}: ${msg}
2500
- `);
2501
- return null;
2422
+ var DESCRIPTION3, INPUT_SCHEMA3;
2423
+ var init_fetchRepoMcp = __esm({
2424
+ "src/fetchRepoMcp.ts"() {
2425
+ "use strict";
2426
+ init_repoWorkspace();
2427
+ 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.';
2428
+ INPUT_SCHEMA3 = {
2429
+ repo: z3.string().describe('GitHub repository as "owner/name", e.g. "A-Guy-educ/A-Guy".')
2430
+ };
2502
2431
  }
2432
+ });
2433
+
2434
+ // src/agent.ts
2435
+ import * as fs8 from "fs";
2436
+ import * as path10 from "path";
2437
+ import { query } from "@anthropic-ai/claude-agent-sdk";
2438
+ function classifySubtype(subtype) {
2439
+ if (!subtype) return "generic_failed";
2440
+ const lower = subtype.toLowerCase();
2441
+ if (lower === "success") return "ok";
2442
+ if (lower.includes("max_turns") || lower.includes("max-turns")) return "out_of_turns";
2443
+ if (lower.includes("rate_limit") || lower.includes("rate-limit")) return "rate_limit";
2444
+ if (lower.includes("tool")) return "tool_error";
2445
+ if (lower.includes("error")) return "model_error";
2446
+ return "generic_failed";
2503
2447
  }
2504
- function localStoreRoot(repo) {
2505
- if (!path9.isAbsolute(repo) && !repo.startsWith(".")) return null;
2506
- const root = path9.resolve(repo);
2507
- if (!fs7.existsSync(path9.join(root, ".kody"))) return null;
2508
- return root;
2448
+ function isClaudeLoginRequiredText(text) {
2449
+ const normalized = text.toLowerCase();
2450
+ return normalized.includes("not logged in") && normalized.includes("/login");
2509
2451
  }
2510
- function repoToGitUrl(repo) {
2511
- if (/^(https?:|ssh:|git@|file:)/.test(repo)) return repo;
2512
- if (isGitHubShorthand(repo)) {
2513
- return `https://github.com/${repo}.git`;
2452
+ function resolveTurnTimeoutMs(opts) {
2453
+ if (opts.maxTurnTimeoutMs !== void 0 && opts.maxTurnTimeoutMs !== null) {
2454
+ return opts.maxTurnTimeoutMs > 0 ? opts.maxTurnTimeoutMs : 0;
2514
2455
  }
2515
- return repo;
2516
- }
2517
- function isGitHubShorthand(repo) {
2518
- return /^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo);
2519
- }
2520
- function cacheRoot() {
2521
- return process.env[CACHE_ENV]?.trim() || path9.join(os3.homedir(), ".cache", "kody", "company-store");
2456
+ const envSec = Number(process.env.KODY_TURN_TIMEOUT_SEC);
2457
+ if (Number.isFinite(envSec) && envSec > 0) return Math.floor(envSec * 1e3);
2458
+ if (Number.isFinite(envSec) && envSec <= 0) return 0;
2459
+ return DEFAULT_TURN_TIMEOUT_MS;
2522
2460
  }
2523
- function cacheKey(repo, ref) {
2524
- return crypto3.createHash("sha256").update(`${repo}#${ref}`).digest("hex").slice(0, 24);
2461
+ function isTransientConnectionError(msg) {
2462
+ if (!msg) return false;
2463
+ return /ConnectionRefused|ECONNREFUSED|ECONNRESET|ETIMEDOUT|EHOSTUNREACH|ENOTFOUND|socket hang up|Unable to connect to API|fetch failed/i.test(
2464
+ msg
2465
+ );
2525
2466
  }
2526
- function runGit(args) {
2527
- execFileSync2("git", args, { stdio: ["ignore", "ignore", "pipe"] });
2467
+ function toolMayMutate(name, input) {
2468
+ if (!name) return false;
2469
+ if (MUTATING_FILE_TOOLS.has(name)) return true;
2470
+ if (name === "Bash") return BASH_WRITE_VERB.test(String(input?.command ?? ""));
2471
+ return false;
2528
2472
  }
2529
- function setupGithubGitAuth() {
2530
- const token = process.env.KODY_TOKEN || process.env.GH_TOKEN || process.env.GITHUB_TOKEN || process.env.GH_PAT;
2531
- if (!token) return;
2473
+ function stripAgentSecrets(env) {
2474
+ const out = { ...env };
2475
+ const raw = out.ALL_SECRETS;
2476
+ delete out.ALL_SECRETS;
2477
+ if (!raw) return out;
2532
2478
  try {
2533
- execFileSync2("gh", ["auth", "setup-git", "--hostname", "github.com"], {
2534
- env: { ...process.env, GH_TOKEN: token },
2535
- stdio: ["ignore", "ignore", "pipe"]
2536
- });
2479
+ const parsed = JSON.parse(raw);
2480
+ for (const key of Object.keys(parsed)) {
2481
+ if (!AGENT_KEEP_SECRETS.has(key)) delete out[key];
2482
+ }
2537
2483
  } catch {
2538
2484
  }
2485
+ return out;
2539
2486
  }
2540
- var DEFAULT_COMPANY_STORE, DEFAULT_COMPANY_STORE_REF, STORE_ENV, REF_ENV, CACHE_ENV, memo;
2541
- var init_companyStore = __esm({
2542
- "src/companyStore.ts"() {
2543
- "use strict";
2544
- DEFAULT_COMPANY_STORE = "aharonyaircohen/kody-company-store";
2545
- DEFAULT_COMPANY_STORE_REF = "stable";
2546
- STORE_ENV = "KODY_COMPANY_STORE";
2547
- REF_ENV = "KODY_COMPANY_STORE_REF";
2548
- CACHE_ENV = "KODY_COMPANY_STORE_CACHE";
2549
- memo = null;
2550
- }
2551
- });
2552
-
2553
- // src/registry.ts
2554
- import * as fs8 from "fs";
2555
- import * as path10 from "path";
2556
- function getAgentActionsRoot() {
2557
- const here = path10.dirname(new URL(import.meta.url).pathname);
2558
- const candidates = [
2559
- path10.join(here, "agent-actions"),
2560
- // dev: src/
2561
- path10.join(here, "..", "agent-actions"),
2562
- // built: dist/bin → dist/agent-actions
2563
- path10.join(here, "..", "src", "agent-actions")
2564
- // fallback
2565
- ];
2566
- for (const c of candidates) {
2567
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
2568
- }
2569
- return candidates[0];
2570
- }
2571
- function getProjectAgentActionsRoot() {
2572
- return path10.join(process.cwd(), ".kody", "agent-actions");
2573
- }
2574
- function getProjectAgentResponsibilitiesRoot() {
2575
- return path10.join(process.cwd(), ".kody", "agent-responsibilities");
2576
- }
2577
- function getCompanyStoreAgentActionsRoot() {
2578
- return getCompanyStoreAssetRoot("agentActions");
2579
- }
2580
- function getCompanyStoreAgentResponsibilitiesRoot() {
2581
- return getCompanyStoreAssetRoot("agentResponsibilities");
2582
- }
2583
- function getBuiltinAgentResponsibilitiesRoot() {
2584
- const here = path10.dirname(new URL(import.meta.url).pathname);
2585
- const candidates = [
2586
- path10.join(here, "agent-responsibilities"),
2587
- // dev: src/
2588
- path10.join(here, "..", "agent-responsibilities"),
2589
- // built: dist/bin → dist/agent-responsibilities
2590
- path10.join(here, "..", "src", "agent-responsibilities")
2591
- // fallback
2592
- ];
2593
- for (const c of candidates) {
2594
- if (fs8.existsSync(c) && fs8.statSync(c).isDirectory()) return c;
2487
+ async function runAgent(opts) {
2488
+ const ndjsonDir = opts.ndjsonDir ?? path10.join(opts.cwd, ".kody");
2489
+ fs8.mkdirSync(ndjsonDir, { recursive: true });
2490
+ const ndjsonPath = path10.join(ndjsonDir, "last-run.jsonl");
2491
+ const env = stripAgentSecrets({
2492
+ ...process.env,
2493
+ SKIP_HOOKS: "1",
2494
+ HUSKY: "0",
2495
+ CI: process.env.CI ?? "1",
2496
+ // MCP servers are spawned asynchronously by the SDK. With the default
2497
+ // non-blocking behavior, the SDK announces its tool list at session
2498
+ // init while servers are still in `pending`, so their tools never
2499
+ // reach the model. Block until each MCP completes its handshake (or
2500
+ // the timeout below elapses) so the tool list is complete on first
2501
+ // turn.
2502
+ MCP_CONNECTION_NONBLOCKING: process.env.MCP_CONNECTION_NONBLOCKING ?? "false",
2503
+ MCP_TIMEOUT: process.env.MCP_TIMEOUT ?? "60000"
2504
+ });
2505
+ if (opts.litellmUrl) {
2506
+ env.ANTHROPIC_BASE_URL = opts.litellmUrl;
2507
+ env.ANTHROPIC_API_KEY = getAnthropicApiKeyOrDummy();
2595
2508
  }
2596
- return candidates[0];
2597
- }
2598
- function getAgentActionRoots() {
2599
- const storeRoot = getCompanyStoreAgentActionsRoot();
2600
- return [getProjectAgentActionsRoot(), ...storeRoot ? [storeRoot] : [], getAgentActionsRoot()];
2601
- }
2602
- function getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
2603
- const storeRoot = getCompanyStoreAgentResponsibilitiesRoot();
2604
- return [projectAgentResponsibilitiesRoot, ...storeRoot ? [storeRoot] : [], getBuiltinAgentResponsibilitiesRoot()];
2605
- }
2606
- function listAgentActions(roots = getAgentActionRoots()) {
2607
- const rootList = typeof roots === "string" ? [roots] : roots;
2608
- const seen = /* @__PURE__ */ new Set();
2609
- const out = [];
2610
- for (const root of rootList) {
2611
- if (!fs8.existsSync(root)) continue;
2612
- const entries = fs8.readdirSync(root, { withFileTypes: true });
2613
- for (const ent of entries) {
2614
- if (!ent.isDirectory()) continue;
2615
- if (seen.has(ent.name)) continue;
2616
- const profilePath = path10.join(root, ent.name, AGENT_RESPONSIBILITY_PROFILE_FILE);
2617
- if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
2618
- out.push({ name: ent.name, profilePath });
2619
- seen.add(ent.name);
2509
+ const startedAt = Date.now();
2510
+ const turnTimeoutMs = resolveTurnTimeoutMs(opts);
2511
+ let outcome = "failed";
2512
+ let outcomeKind = "generic_failed";
2513
+ let errorMessage;
2514
+ let tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
2515
+ let messageCount = 0;
2516
+ let finalText = "";
2517
+ let getSubmitted;
2518
+ for (let attempt = 0; ; attempt++) {
2519
+ let ndjsonWriteFailed = false;
2520
+ let ndjsonWriteError;
2521
+ const fullLog = fs8.createWriteStream(ndjsonPath, { flags: "w" });
2522
+ fullLog.on("error", (err) => {
2523
+ ndjsonWriteFailed = true;
2524
+ ndjsonWriteError = err instanceof Error ? err.message : String(err);
2525
+ });
2526
+ const resultTexts = [];
2527
+ outcome = "failed";
2528
+ outcomeKind = "generic_failed";
2529
+ errorMessage = void 0;
2530
+ tokens = { input: 0, output: 0, cacheRead: 0, cacheCreate: 0 };
2531
+ messageCount = 0;
2532
+ let sawMutatingTool = false;
2533
+ let sawTerminalSuccess = false;
2534
+ let sawLoginRequired = false;
2535
+ let noWorkSuccess = false;
2536
+ try {
2537
+ const queryOptions = {
2538
+ model: opts.model.model,
2539
+ cwd: opts.cwd,
2540
+ // Fresh array (never mutate the shared DEFAULT_ALLOWED_TOOLS const) so
2541
+ // opt-in tools like fetch_repo can be appended below.
2542
+ allowedTools: [...opts.allowedToolsOverride ?? DEFAULT_ALLOWED_TOOLS],
2543
+ permissionMode: opts.permissionModeOverride ?? "acceptEdits",
2544
+ env
2545
+ };
2546
+ const mcpEntries = [];
2547
+ if (opts.mcpServers && opts.mcpServers.length > 0) {
2548
+ for (const s of opts.mcpServers) {
2549
+ const cfg = { command: s.command };
2550
+ if (s.args) cfg.args = s.args;
2551
+ if (s.env) cfg.env = s.env;
2552
+ mcpEntries.push([s.name, cfg]);
2553
+ }
2554
+ }
2555
+ if (opts.enableVerifyTool && opts.verifyConfig) {
2556
+ const { buildVerifyMcpServer: buildVerifyMcpServer2 } = await Promise.resolve().then(() => (init_verifyMcp(), verifyMcp_exports));
2557
+ const verifyServer = buildVerifyMcpServer2({
2558
+ config: opts.verifyConfig,
2559
+ cwd: opts.cwd,
2560
+ agentAction: opts.agentActionName ?? "agent",
2561
+ maxAttempts: typeof opts.verifyToolMaxAttempts === "number" && opts.verifyToolMaxAttempts > 0 ? opts.verifyToolMaxAttempts : void 0
2562
+ });
2563
+ mcpEntries.push(["kody-verify", verifyServer]);
2564
+ }
2565
+ if (opts.enableSubmitTool) {
2566
+ const { buildSubmitMcpServer: buildSubmitMcpServer2 } = await Promise.resolve().then(() => (init_submitMcp(), submitMcp_exports));
2567
+ const submitHandle = buildSubmitMcpServer2();
2568
+ getSubmitted = submitHandle.getSubmitted;
2569
+ mcpEntries.push(["kody-submit", submitHandle.server]);
2570
+ }
2571
+ if (opts.enableAgentResponsibilityTool) {
2572
+ const { buildAgentResponsibilityMcpServer: buildAgentResponsibilityMcpServer2 } = await Promise.resolve().then(() => (init_agent_responsibilityMcp(), agent_responsibilityMcp_exports));
2573
+ if (!opts.dutyRepoSlug) {
2574
+ throw new Error(
2575
+ "enableAgentResponsibilityTool requires dutyRepoSlug (owner/name) \u2014 set kody.config.json github.{owner,repo} or GITHUB_REPOSITORY env var"
2576
+ );
2577
+ }
2578
+ const dutyHandle = buildAgentResponsibilityMcpServer2({
2579
+ repoSlug: opts.dutyRepoSlug,
2580
+ state: opts.agentResponsibilityState,
2581
+ operatorMention: opts.agentResponsibilityOperatorMention ?? "",
2582
+ ...opts.agentResponsibilitySlug ? { agentResponsibilitySlug: opts.agentResponsibilitySlug } : {}
2583
+ });
2584
+ mcpEntries.push(["kody-agentResponsibility", dutyHandle.server]);
2585
+ }
2586
+ if (opts.enableFetchRepoTool && opts.reposRoot) {
2587
+ const { buildFetchRepoMcpServer: buildFetchRepoMcpServer2 } = await Promise.resolve().then(() => (init_fetchRepoMcp(), fetchRepoMcp_exports));
2588
+ const fetchServer = buildFetchRepoMcpServer2({
2589
+ reposRoot: opts.reposRoot,
2590
+ repoToken: opts.repoToken
2591
+ });
2592
+ mcpEntries.push(["kody-fetch-repo", fetchServer]);
2593
+ queryOptions.allowedTools.push("mcp__kody-fetch-repo__fetch_repo");
2594
+ queryOptions.additionalDirectories = [opts.reposRoot];
2595
+ }
2596
+ if (mcpEntries.length > 0) {
2597
+ queryOptions.mcpServers = Object.fromEntries(mcpEntries);
2598
+ }
2599
+ if (opts.pluginPaths && opts.pluginPaths.length > 0) {
2600
+ queryOptions.plugins = opts.pluginPaths.map((p) => ({ type: "local", path: p }));
2601
+ }
2602
+ if (opts.agents && Object.keys(opts.agents).length > 0) {
2603
+ queryOptions.agents = opts.agents;
2604
+ }
2605
+ if (typeof opts.maxTurns === "number" && opts.maxTurns > 0) {
2606
+ queryOptions.maxTurns = opts.maxTurns;
2607
+ }
2608
+ if (opts.reasoningEffort !== void 0 && opts.reasoningEffort !== null) {
2609
+ if (opts.reasoningEffort === "off") {
2610
+ } else {
2611
+ const budget = REASONING_BUDGETS[opts.reasoningEffort];
2612
+ if (budget) queryOptions.maxThinkingTokens = budget;
2613
+ }
2614
+ } else if (typeof opts.maxThinkingTokens === "number" && opts.maxThinkingTokens > 0) {
2615
+ queryOptions.maxThinkingTokens = opts.maxThinkingTokens;
2616
+ }
2617
+ if (typeof opts.systemPromptAppend === "string" && opts.systemPromptAppend.length > 0) {
2618
+ const systemPrompt = {
2619
+ type: "preset",
2620
+ preset: "claude_code",
2621
+ append: opts.systemPromptAppend
2622
+ };
2623
+ if (opts.cacheable) systemPrompt.excludeDynamicSections = true;
2624
+ queryOptions.systemPrompt = systemPrompt;
2625
+ } else if (opts.cacheable) {
2626
+ queryOptions.systemPrompt = {
2627
+ type: "preset",
2628
+ preset: "claude_code",
2629
+ excludeDynamicSections: true
2630
+ };
2631
+ }
2632
+ queryOptions.settingSources = opts.settingSources ?? ["project", "local"];
2633
+ const stableBinary = ensureStableClaudeBinary();
2634
+ if (stableBinary) {
2635
+ queryOptions.pathToClaudeCodeExecutable = stableBinary;
2636
+ }
2637
+ const result = query({
2638
+ prompt: opts.prompt,
2639
+ // biome-ignore lint/suspicious/noExplicitAny: SDK options type is narrow; mcpServers is runtime-passthrough.
2640
+ options: queryOptions
2641
+ });
2642
+ const iterator = typeof result[Symbol.asyncIterator] === "function" ? result[Symbol.asyncIterator]() : result;
2643
+ while (true) {
2644
+ const nextPromise = iterator.next();
2645
+ let timedOut = false;
2646
+ let timer;
2647
+ let next;
2648
+ if (turnTimeoutMs > 0) {
2649
+ const timeoutPromise = new Promise((resolve7) => {
2650
+ timer = setTimeout(() => {
2651
+ timedOut = true;
2652
+ resolve7({ done: true, value: void 0 });
2653
+ }, turnTimeoutMs);
2654
+ });
2655
+ next = await Promise.race([nextPromise, timeoutPromise]);
2656
+ if (timer) clearTimeout(timer);
2657
+ } else {
2658
+ next = await nextPromise;
2659
+ }
2660
+ if (timedOut) {
2661
+ outcome = "failed";
2662
+ outcomeKind = "stalled";
2663
+ errorMessage = `agent stalled: no SDK message in ${Math.round(turnTimeoutMs / 1e3)}s`;
2664
+ if (typeof iterator.return === "function") {
2665
+ try {
2666
+ await iterator.return(void 0);
2667
+ } catch {
2668
+ }
2669
+ }
2670
+ break;
2671
+ }
2672
+ if (next.done) break;
2673
+ const msg = next.value;
2674
+ messageCount++;
2675
+ if (!ndjsonWriteFailed) {
2676
+ try {
2677
+ fullLog.write(`${JSON.stringify(msg)}
2678
+ `);
2679
+ } catch (e) {
2680
+ ndjsonWriteFailed = true;
2681
+ ndjsonWriteError = e instanceof Error ? e.message : String(e);
2682
+ }
2683
+ }
2684
+ const line = renderEvent(msg, { verbose: opts.verbose, quiet: opts.quiet });
2685
+ if (line) {
2686
+ if (isClaudeLoginRequiredText(line)) sawLoginRequired = true;
2687
+ process.stdout.write(`${line}
2688
+ `);
2689
+ }
2690
+ const m = msg;
2691
+ if (opts.onProgress) {
2692
+ const blocks = m.message?.content ?? [];
2693
+ for (const block of blocks) {
2694
+ try {
2695
+ if (block.type === "thinking") {
2696
+ const t = block.thinking;
2697
+ if (typeof t === "string" && t.length > 0) {
2698
+ await opts.onProgress({ kind: "thinking", thinking: t });
2699
+ }
2700
+ } else if (block.type === "tool_use") {
2701
+ const b = block;
2702
+ await opts.onProgress({
2703
+ kind: "tool_use",
2704
+ name: b.name ?? "tool",
2705
+ input: b.input,
2706
+ id: b.id
2707
+ });
2708
+ } else if (block.type === "tool_result") {
2709
+ const b = block;
2710
+ const content = typeof b.content === "string" ? b.content : (() => {
2711
+ try {
2712
+ return JSON.stringify(b.content);
2713
+ } catch {
2714
+ return "";
2715
+ }
2716
+ })();
2717
+ await opts.onProgress({
2718
+ kind: "tool_result",
2719
+ toolUseId: b.tool_use_id,
2720
+ content,
2721
+ isError: b.is_error
2722
+ });
2723
+ } else if (block.type === "text") {
2724
+ const b = block;
2725
+ if (typeof b.text === "string" && b.text.length > 0) {
2726
+ await opts.onProgress({ kind: "text", text: b.text });
2727
+ }
2728
+ }
2729
+ } catch {
2730
+ }
2731
+ }
2732
+ }
2733
+ const usage = m.usage;
2734
+ if (usage && typeof usage === "object") {
2735
+ const i = Number(usage.input_tokens ?? 0);
2736
+ const o = Number(usage.output_tokens ?? 0);
2737
+ const cr = Number(usage.cache_read_input_tokens ?? 0);
2738
+ const cc = Number(usage.cache_creation_input_tokens ?? 0);
2739
+ if (Number.isFinite(i)) tokens.input += i;
2740
+ if (Number.isFinite(o)) tokens.output += o;
2741
+ if (Number.isFinite(cr)) tokens.cacheRead += cr;
2742
+ if (Number.isFinite(cc)) tokens.cacheCreate += cc;
2743
+ }
2744
+ if (!sawMutatingTool) {
2745
+ const blocks = m.message?.content ?? [];
2746
+ for (const block of blocks) {
2747
+ if (block.type === "tool_use") {
2748
+ const b = block;
2749
+ if (toolMayMutate(b.name, b.input)) {
2750
+ sawMutatingTool = true;
2751
+ break;
2752
+ }
2753
+ }
2754
+ }
2755
+ }
2756
+ if (m.type === "result") {
2757
+ if (m.subtype === "success") {
2758
+ outcome = "completed";
2759
+ outcomeKind = "ok";
2760
+ sawTerminalSuccess = true;
2761
+ const text = (typeof m.result === "string" ? m.result : "").trim();
2762
+ if (isClaudeLoginRequiredText(text)) sawLoginRequired = true;
2763
+ if (text) resultTexts.push(text);
2764
+ } else {
2765
+ outcome = "failed";
2766
+ outcomeKind = classifySubtype(m.subtype);
2767
+ errorMessage = `result subtype: ${m.subtype ?? "unknown"}`;
2768
+ }
2769
+ }
2770
+ }
2771
+ } catch (e) {
2772
+ if (sawTerminalSuccess) {
2773
+ errorMessage = e instanceof Error ? e.message : String(e);
2774
+ } else {
2775
+ outcome = "failed";
2776
+ outcomeKind = "model_error";
2777
+ errorMessage = e instanceof Error ? e.message : String(e);
2778
+ }
2779
+ } finally {
2780
+ try {
2781
+ fullLog.end();
2782
+ } catch {
2620
2783
  }
2621
2784
  }
2622
- }
2623
- return out.sort((a, b) => a.name.localeCompare(b.name));
2624
- }
2625
- function resolveAgentAction(name, roots = getAgentActionRoots()) {
2626
- if (!isSafeName(name)) return null;
2627
- const rootList = typeof roots === "string" ? [roots] : roots;
2628
- for (const root of rootList) {
2629
- const profilePath = path10.join(root, name, "profile.json");
2630
- if (fs8.existsSync(profilePath) && fs8.statSync(profilePath).isFile()) {
2631
- return profilePath;
2632
- }
2633
- }
2634
- return null;
2635
- }
2636
- function listAgentResponsibilityActions(projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
2637
- const seen = /* @__PURE__ */ new Set();
2638
- const out = [];
2639
- const add = (action) => {
2640
- if (!isSafeName(action.action) || !isSafeName(action.agentResponsibility) || !isSafeName(action.agentAction)) return;
2641
- if (seen.has(action.action)) return;
2642
- seen.add(action.action);
2643
- out.push(action);
2644
- };
2645
- const roots = getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot);
2646
- const executableRoots = getAgentActionRoots();
2647
- for (const action of listFolderAgentResponsibilityActions(roots[0], "project-folder")) add(action);
2648
- for (const action of listAgentActionResponsibilityActions(executableRoots[0], "project-agentAction")) add(action);
2649
- if (roots.length === 3) {
2650
- for (const action of listFolderAgentResponsibilityActions(roots[1], "company-store")) add(action);
2651
- for (const action of listAgentActionResponsibilityActions(executableRoots[1], "company-store-agentAction"))
2652
- add(action);
2653
- for (const action of listBuiltinAgentResponsibilityActions(roots[2])) add(action);
2654
- } else {
2655
- for (const action of listBuiltinAgentResponsibilityActions(roots[1])) add(action);
2656
- }
2657
- return out.sort((a, b) => a.action.localeCompare(b.action));
2658
- }
2659
- function resolveAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
2660
- if (!isSafeName(action)) return null;
2661
- return listAgentResponsibilityActions(projectAgentResponsibilitiesRoot).find((d) => d.action === action) ?? null;
2662
- }
2663
- function hasAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
2664
- return resolveAgentResponsibilityAction(action, projectAgentResponsibilitiesRoot) !== null;
2665
- }
2666
- function resolveAgentResponsibilityFolder(slug2, projectAgentResponsibilitiesRoot = getProjectAgentResponsibilitiesRoot()) {
2667
- if (!isSafeName(slug2)) return null;
2668
- for (const root of getAgentResponsibilityRoots(projectAgentResponsibilitiesRoot)) {
2669
- const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
2670
- if (agentResponsibility) return agentResponsibility;
2671
- }
2672
- return null;
2673
- }
2674
- function resolveAgentResponsibilityExecution(agentResponsibility) {
2675
- const agentAction = agentResponsibility.config.agentAction ?? agentResponsibility.config.agentActions?.[0] ?? (agentResponsibility.config.tickScript ? "agent-responsibility-tick-scripted" : "agent-responsibility-tick");
2676
- const cliArgs = agentActionDeclaresInput(agentAction, "agentResponsibility") ? { agentResponsibility: agentResponsibility.slug } : {};
2677
- return { agentAction, cliArgs };
2678
- }
2679
- function agentActionDeclaresInput(agentAction, inputName) {
2680
- const profilePath = resolveAgentAction(agentAction);
2681
- if (!profilePath) return false;
2682
- try {
2683
- const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
2684
- if (!Array.isArray(raw.inputs)) return false;
2685
- return raw.inputs.some((entry) => {
2686
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
2687
- const input = entry;
2688
- return input.name === inputName || input.flag === `--${inputName}`;
2689
- });
2690
- } catch {
2691
- return false;
2692
- }
2693
- }
2694
- function isSafeName(name) {
2695
- return /^[a-z][a-z0-9-]*$/.test(name) && !name.includes("..");
2696
- }
2697
- function listAgentActionResponsibilityActions(root, source) {
2698
- if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2699
- const out = [];
2700
- for (const ent of fs8.readdirSync(root, { withFileTypes: true })) {
2701
- if (!ent.isDirectory() || !isSafeName(ent.name)) continue;
2702
- const profilePath = path10.join(root, ent.name, AGENT_RESPONSIBILITY_PROFILE_FILE);
2703
- if (!fs8.existsSync(profilePath) || !fs8.statSync(profilePath).isFile()) continue;
2704
- try {
2705
- const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
2706
- const action = typeof raw.action === "string" && raw.action.trim() ? raw.action.trim() : "";
2707
- if (!action) continue;
2708
- if (!PUBLIC_AGENT_ACTION_ACTION_ROLES.has(String(raw.role))) continue;
2709
- if (!PUBLIC_AGENT_ACTION_CAPABILITY_KINDS.has(String(raw.capabilityKind))) continue;
2710
- if (!Array.isArray(raw.inputs)) continue;
2711
- out.push({
2712
- action,
2713
- agentResponsibility: ent.name,
2714
- agentAction: ent.name,
2715
- cliArgs: {},
2716
- source,
2717
- describe: typeof raw.describe === "string" ? raw.describe : void 0,
2718
- profilePath
2719
- });
2720
- } catch {
2785
+ if (ndjsonWriteFailed) {
2786
+ process.stderr.write(
2787
+ `[kody agent] NDJSON write failed (post-mortem may be incomplete): ${ndjsonWriteError ?? "unknown error"}
2788
+ `
2789
+ );
2721
2790
  }
2722
- }
2723
- return out.sort((a, b) => a.action.localeCompare(b.action));
2724
- }
2725
- function listFolderAgentResponsibilityActions(root, source) {
2726
- if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2727
- const out = [];
2728
- for (const slug2 of listAgentResponsibilityFolderSlugs(root)) {
2729
- if (!isSafeName(slug2)) continue;
2730
- const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
2731
- if (!agentResponsibility) continue;
2732
- const action = agentResponsibility.config.action ?? slug2;
2733
- const { agentAction, cliArgs } = resolveAgentResponsibilityExecution(agentResponsibility);
2734
- out.push({
2735
- action,
2736
- agentResponsibility: slug2,
2737
- agentAction,
2738
- cliArgs,
2739
- source,
2740
- describe: agentResponsibility.config.describe ?? agentResponsibility.title,
2741
- capabilityKind: agentResponsibility.config.capabilityKind,
2742
- profilePath: agentResponsibility.profilePath,
2743
- bodyPath: agentResponsibility.bodyPath
2744
- });
2745
- }
2746
- return out.sort((a, b) => a.action.localeCompare(b.action));
2747
- }
2748
- function listBuiltinAgentResponsibilityActions(root = getBuiltinAgentResponsibilitiesRoot()) {
2749
- if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) return [];
2750
- const out = [];
2751
- for (const slug2 of listAgentResponsibilityFolderSlugs(root)) {
2752
- if (!isSafeName(slug2)) continue;
2753
- const agentResponsibility = readAgentResponsibilityFolder(root, slug2);
2754
- if (!agentResponsibility) continue;
2755
- const action = agentResponsibility.config.action ?? slug2;
2756
- const agentAction = agentResponsibility.config.agentAction ?? slug2;
2757
- out.push({
2758
- action,
2759
- agentResponsibility: slug2,
2760
- agentAction,
2761
- cliArgs: {},
2762
- source: "builtin",
2763
- describe: agentResponsibility.config.describe ?? agentResponsibility.title,
2764
- capabilityKind: agentResponsibility.config.capabilityKind,
2765
- profilePath: agentResponsibility.profilePath,
2766
- bodyPath: agentResponsibility.bodyPath
2767
- });
2768
- }
2769
- return out.sort((a, b) => a.action.localeCompare(b.action));
2770
- }
2771
- function getProfileInputs(name, roots = getAgentActionRoots()) {
2772
- const profilePath = resolveAgentAction(name, roots);
2773
- if (!profilePath) return null;
2774
- try {
2775
- const raw = JSON.parse(fs8.readFileSync(profilePath, "utf-8"));
2776
- if (!raw || typeof raw !== "object" || !Array.isArray(raw.inputs)) return [];
2777
- return raw.inputs;
2778
- } catch {
2779
- return null;
2780
- }
2781
- }
2782
- function parseGenericFlags(argv) {
2783
- const args = {};
2784
- const positional = [];
2785
- for (let i = 0; i < argv.length; i++) {
2786
- const arg = argv[i];
2787
- if (!arg.startsWith("--")) {
2788
- positional.push(arg);
2789
- continue;
2791
+ finalText = resultTexts.join("\n\n---\n\n");
2792
+ if (outcome === "completed" && sawLoginRequired) {
2793
+ outcome = "failed";
2794
+ outcomeKind = "model_error";
2795
+ errorMessage = "Claude Code reported it is not logged in; refusing to mark agent run successful";
2790
2796
  }
2791
- const key = arg.slice(2);
2792
- const next = argv[i + 1];
2793
- let value = true;
2794
- if (next !== void 0 && !next.startsWith("--")) {
2795
- value = next;
2796
- i++;
2797
+ if (outcome === "completed" && !sawMutatingTool) {
2798
+ const backendDead = opts.isBackendHealthy ? !await opts.isBackendHealthy() : false;
2799
+ const zeroOutput = tokens.output === 0 && finalText === "";
2800
+ if (backendDead || zeroOutput) {
2801
+ outcome = "failed";
2802
+ outcomeKind = "model_error";
2803
+ noWorkSuccess = true;
2804
+ 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");
2805
+ }
2797
2806
  }
2798
- args[key] = value;
2799
- if (key.includes("-")) {
2800
- const camel = key.replace(/-([a-z0-9])/g, (_, c) => c.toUpperCase());
2801
- if (camel !== key && args[camel] === void 0) args[camel] = value;
2807
+ const shouldRetry = outcome === "failed" && attempt < MAX_CONNECTION_RETRIES && !sawMutatingTool && (isTransientConnectionError(errorMessage) || noWorkSuccess);
2808
+ if (!shouldRetry) break;
2809
+ const delayMs = CONNECTION_RETRY_BASE_MS * 2 ** attempt;
2810
+ process.stderr.write(
2811
+ `[kody agent] transient connection error (attempt ${attempt + 1}/${MAX_CONNECTION_RETRIES + 1}); retrying in ${Math.round(delayMs / 1e3)}s: ${errorMessage}
2812
+ `
2813
+ );
2814
+ if (opts.ensureBackend) {
2815
+ try {
2816
+ await opts.ensureBackend();
2817
+ } catch (e) {
2818
+ process.stderr.write(`[kody agent] backend recovery failed: ${e instanceof Error ? e.message : String(e)}
2819
+ `);
2820
+ }
2802
2821
  }
2822
+ await new Promise((r) => setTimeout(r, delayMs));
2803
2823
  }
2804
- if (positional.length > 0) args._ = positional;
2805
- return args;
2824
+ const submittedState = getSubmitted?.();
2825
+ return {
2826
+ outcome,
2827
+ outcomeKind,
2828
+ finalText,
2829
+ ...submittedState ? { submittedState } : {},
2830
+ error: errorMessage,
2831
+ ndjsonPath,
2832
+ durationMs: Date.now() - startedAt,
2833
+ tokens,
2834
+ messageCount
2835
+ };
2806
2836
  }
2807
- var PUBLIC_AGENT_ACTION_ACTION_ROLES, PUBLIC_AGENT_ACTION_CAPABILITY_KINDS;
2808
- var init_registry = __esm({
2809
- "src/registry.ts"() {
2837
+ var DEFAULT_ALLOWED_TOOLS, DEFAULT_TURN_TIMEOUT_MS, MAX_CONNECTION_RETRIES, CONNECTION_RETRY_BASE_MS, MUTATING_FILE_TOOLS, BASH_WRITE_VERB, AGENT_KEEP_SECRETS;
2838
+ var init_agent = __esm({
2839
+ "src/agent.ts"() {
2810
2840
  "use strict";
2811
- init_agent_responsibilityFolders();
2812
- init_companyStore();
2813
- PUBLIC_AGENT_ACTION_ACTION_ROLES = /* @__PURE__ */ new Set(["primitive", "orchestrator", "container", "watch", "utility"]);
2814
- PUBLIC_AGENT_ACTION_CAPABILITY_KINDS = /* @__PURE__ */ new Set(["observe", "act", "verify"]);
2841
+ init_claudeBinary();
2842
+ init_config();
2843
+ init_format();
2844
+ DEFAULT_ALLOWED_TOOLS = ["Bash", "Edit", "Read", "Write", "Glob", "Grep"];
2845
+ DEFAULT_TURN_TIMEOUT_MS = 6e5;
2846
+ MAX_CONNECTION_RETRIES = 2;
2847
+ CONNECTION_RETRY_BASE_MS = 2e3;
2848
+ MUTATING_FILE_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
2849
+ BASH_WRITE_VERB = /\b(git\s+(commit|push|merge|rebase|tag|reset|cherry-pick)|gh\s+(pr|issue|release)\s+(create|comment|edit|close|merge|review|reopen)|gh\s+api\b[^|&]*-X\s*(POST|PUT|PATCH|DELETE)|npm\s+publish)\b/i;
2850
+ AGENT_KEEP_SECRETS = /* @__PURE__ */ new Set([
2851
+ "ANTHROPIC_API_KEY",
2852
+ "ANTHROPIC_AUTH_TOKEN",
2853
+ "ANTHROPIC_BASE_URL",
2854
+ "GH_TOKEN",
2855
+ "GITHUB_TOKEN"
2856
+ ]);
2815
2857
  }
2816
2858
  });
2817
2859
 
@@ -13227,6 +13269,14 @@ var init_runFlow = __esm({
13227
13269
  cfgCtx.commentMaxBytes ?? DEFAULT_COMMENT_MAX_BYTES
13228
13270
  );
13229
13271
  ctx.data.issue = { ...issue, commentsFormatted };
13272
+ if (issue.isPullRequest) {
13273
+ ctx.data.commentTargetType = "pr";
13274
+ ctx.data.commentTargetNumber = issueNumber;
13275
+ ctx.skipAgent = true;
13276
+ ctx.output.exitCode = 1;
13277
+ ctx.output.reason = `run target #${issueNumber} is a pull request; dispatch a PR action or the source issue instead`;
13278
+ return;
13279
+ }
13230
13280
  ctx.data.commentTargetType = "issue";
13231
13281
  ctx.data.commentTargetNumber = issueNumber;
13232
13282
  const argBase = resolveBaseOverride(ctx.args.base);