@kody-ade/kody-engine 0.4.194 → 0.4.195-dev.0

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 +138 -19
  2. package/package.json +1 -1
package/dist/bin/kody.js CHANGED
@@ -545,7 +545,11 @@ var init_issue = __esm({
545
545
  var dutyMcp_exports = {};
546
546
  __export(dutyMcp_exports, {
547
547
  DUTY_MCP_TOOL_NAMES: () => DUTY_MCP_TOOL_NAMES,
548
- buildDutyMcpServer: () => buildDutyMcpServer
548
+ buildDutyMcpServer: () => buildDutyMcpServer,
549
+ dispatchWorkflow: () => dispatchWorkflow,
550
+ ensureComment: () => ensureComment,
551
+ ensureIssue: () => ensureIssue,
552
+ readCheckRuns: () => readCheckRuns
549
553
  });
550
554
  import { createSdkMcpServer as createSdkMcpServer3, tool as tool3 } from "@anthropic-ai/claude-agent-sdk";
551
555
  import { z as z3 } from "zod";
@@ -599,20 +603,7 @@ function listRepairCandidates(repoSlug) {
599
603
  });
600
604
  }
601
605
  function dispatchVerb(workflowFile, executable, prNumber) {
602
- try {
603
- gh([
604
- "workflow",
605
- "run",
606
- workflowFile,
607
- "-f",
608
- `executable=${executable}`,
609
- "-f",
610
- `issue_number=${prNumber}`
611
- ]);
612
- return { ok: true };
613
- } catch (err) {
614
- return { ok: false, error: err instanceof Error ? err.message : String(err) };
615
- }
606
+ return dispatchWorkflow(workflowFile, executable, prNumber);
616
607
  }
617
608
  function postRecommendation(prNumber, mention, message) {
618
609
  const body = mention ? `${mention} ${message}` : message;
@@ -660,6 +651,64 @@ function readLedger(label) {
660
651
  return { found: false, payload: { error: err instanceof Error ? err.message : String(err) } };
661
652
  }
662
653
  }
654
+ function readCheckRuns(repoSlug, ref, ignoreNames) {
655
+ const sha = gh(["api", `repos/${repoSlug}/commits/${ref}`, "--jq", ".sha"]).trim();
656
+ const raw = gh([
657
+ "api",
658
+ `repos/${repoSlug}/commits/${sha}/check-runs`,
659
+ "--paginate",
660
+ "--jq",
661
+ ".check_runs[] | {name, status, conclusion, details_url}"
662
+ ]);
663
+ const ignore = new Set(ignoreNames.map((n) => n.toLowerCase()));
664
+ const checks = raw.split("\n").map((l) => l.trim()).filter(Boolean).map((l) => JSON.parse(l)).filter((c) => !ignore.has(String(c.name).toLowerCase()));
665
+ 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 }));
666
+ const pending = checks.filter((c) => String(c.status).toLowerCase() !== "completed").map((c) => ({ name: c.name, status: c.status }));
667
+ const state = failing.length > 0 ? "RED" : pending.length > 0 ? "PENDING" : "GREEN";
668
+ return { sha, state, failing, pending };
669
+ }
670
+ function ensureIssue(repoSlug, key, title, body) {
671
+ const marker = trackMarker(key);
672
+ try {
673
+ const raw = gh(["issue", "list", "-R", repoSlug, "--state", "open", "--limit", "100", "--json", "number,body"]);
674
+ const issues = JSON.parse(raw);
675
+ const existing = issues.find((i) => (i.body ?? "").includes(marker));
676
+ if (existing) return { created: false, number: existing.number };
677
+ const url = gh(["issue", "create", "-R", repoSlug, "--title", title, "--body-file", "-"], {
678
+ input: `${body}
679
+
680
+ ${marker}`
681
+ });
682
+ const m = url.trim().match(/\/(\d+)\s*$/);
683
+ if (!m) return { error: `issue created but could not parse its number from: ${url}` };
684
+ return { created: true, number: Number(m[1]) };
685
+ } catch (err) {
686
+ return { error: err instanceof Error ? err.message : String(err) };
687
+ }
688
+ }
689
+ function ensureComment(repoSlug, issue, key, body) {
690
+ const marker = commentMarker(key);
691
+ try {
692
+ const raw = gh(["issue", "view", String(issue), "-R", repoSlug, "--json", "comments"]);
693
+ const parsed = JSON.parse(raw);
694
+ const already = (parsed.comments ?? []).some((c) => (c.body ?? "").includes(marker));
695
+ if (already) return { posted: false };
696
+ gh(["issue", "comment", String(issue), "-R", repoSlug, "--body-file", "-"], { input: `${body}
697
+
698
+ ${marker}` });
699
+ return { posted: true };
700
+ } catch (err) {
701
+ return { error: err instanceof Error ? err.message : String(err) };
702
+ }
703
+ }
704
+ function dispatchWorkflow(workflowFile, executable, issueNumber) {
705
+ try {
706
+ gh(["workflow", "run", workflowFile, "-f", `executable=${executable}`, "-f", `issue_number=${issueNumber}`]);
707
+ return { ok: true };
708
+ } catch (err) {
709
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
710
+ }
711
+ }
663
712
  function buildDutyMcpServer(opts) {
664
713
  const workflowFile = opts.workflowFile ?? "kody.yml";
665
714
  const listTool = tool3(
@@ -737,27 +786,97 @@ function buildDutyMcpServer(opts) {
737
786
  };
738
787
  }
739
788
  );
789
+ const checkRunsTool = tool3(
790
+ "read_check_runs",
791
+ "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/job-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.",
792
+ {
793
+ ref: z3.string().min(1).describe("Branch name or commit SHA to read CI for (e.g. 'dev')."),
794
+ ignoreNames: z3.array(z3.string()).optional().describe("Check names to exclude (default: Kody's own job names).")
795
+ },
796
+ async (args) => {
797
+ const result = readCheckRuns(opts.repoSlug, args.ref, args.ignoreNames ?? DEFAULT_IGNORE_CHECKS);
798
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
799
+ }
800
+ );
801
+ const ensureIssueTool = tool3(
802
+ "ensure_issue",
803
+ "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.",
804
+ {
805
+ key: z3.string().min(1).describe("Stable dedup identity for this finding (e.g. 'dev-ci-red', 'docs-drift:<feature>'). Same key across ticks = same issue."),
806
+ title: z3.string().min(1).describe("Issue title (used only on first creation)."),
807
+ body: z3.string().min(1).describe("Issue body markdown (used only on first creation). Include the operator mention verbatim if the duty body has one.")
808
+ },
809
+ async (args) => {
810
+ const result = ensureIssue(opts.repoSlug, args.key, args.title, args.body);
811
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
812
+ }
813
+ );
814
+ const ensureCommentTool = tool3(
815
+ "ensure_comment",
816
+ "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.",
817
+ {
818
+ issue: z3.number().int().positive().describe("Issue number to comment on."),
819
+ key: z3.string().min(1).describe("Stable dedup identity for this comment (e.g. 'dev-ci-red:dispatched')."),
820
+ body: z3.string().min(1).describe("Comment body markdown.")
821
+ },
822
+ async (args) => {
823
+ const result = ensureComment(opts.repoSlug, args.issue, args.key, args.body);
824
+ return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
825
+ }
826
+ );
827
+ const dispatchTool = tool3(
828
+ "dispatch_workflow",
829
+ "Dispatch a kody.yml workflow_dispatch run for an executable against an issue (the cross-run bot\u2192engine path; a bot `@kody` comment would be dropped). E.g. dispatch_workflow({executable:'run', issueNumber:<n>}) opens a fix PR from a tracking issue. Returns {ok} or {ok:false,error}.",
830
+ {
831
+ executable: z3.string().min(1).describe("Executable/stage to run (e.g. 'run')."),
832
+ issueNumber: z3.number().int().positive().describe("Issue (or PR) number forwarded as issue_number.")
833
+ },
834
+ async (args) => {
835
+ const result = dispatchWorkflow(workflowFile, args.executable, args.issueNumber);
836
+ const text = result.ok ? `Dispatched \`${args.executable}\` on #${args.issueNumber} via workflow_dispatch.` : `Dispatch failed for \`${args.executable}\` on #${args.issueNumber}: ${result.error}`;
837
+ return { content: [{ type: "text", text }] };
838
+ }
839
+ );
740
840
  const server = createSdkMcpServer3({
741
841
  name: "kody-duty",
742
842
  version: "0.1.0",
743
- tools: [listTool, syncTool, fixCiTool, resolveTool, recommendTool, ledgerTool]
843
+ tools: [
844
+ listTool,
845
+ syncTool,
846
+ fixCiTool,
847
+ resolveTool,
848
+ recommendTool,
849
+ ledgerTool,
850
+ checkRunsTool,
851
+ ensureIssueTool,
852
+ ensureCommentTool,
853
+ dispatchTool
854
+ ]
744
855
  });
745
856
  return { server };
746
857
  }
747
- var FAIL_CONCLUSIONS, RUNNING_STATUSES, DUTY_MCP_TOOL_NAMES;
858
+ var FAIL_CONCLUSIONS, RUNNING_STATUSES, CHECK_FAIL_CONCLUSIONS, DEFAULT_IGNORE_CHECKS, trackMarker, commentMarker, DUTY_MCP_TOOL_NAMES;
748
859
  var init_dutyMcp = __esm({
749
860
  "src/dutyMcp.ts"() {
750
861
  "use strict";
751
862
  init_issue();
752
863
  FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "CANCELLED"]);
753
864
  RUNNING_STATUSES = /* @__PURE__ */ new Set(["IN_PROGRESS", "QUEUED", "PENDING", "WAITING", "REQUESTED"]);
865
+ CHECK_FAIL_CONCLUSIONS = /* @__PURE__ */ new Set(["FAILURE", "TIMED_OUT", "STARTUP_FAILURE", "ACTION_REQUIRED"]);
866
+ DEFAULT_IGNORE_CHECKS = ["run", "kody", "job-tick", "goal-tick", "worker-ask", "chat"];
867
+ trackMarker = (key) => `<!-- kody-track:${key} -->`;
868
+ commentMarker = (key) => `<!-- kody-track-comment:${key} -->`;
754
869
  DUTY_MCP_TOOL_NAMES = [
755
870
  "list_prs_to_repair",
756
871
  "sync_pr",
757
872
  "fix_ci_pr",
758
873
  "resolve_pr",
759
874
  "recommend_to_operator",
760
- "read_ledger"
875
+ "read_ledger",
876
+ "read_check_runs",
877
+ "ensure_issue",
878
+ "ensure_comment",
879
+ "dispatch_workflow"
761
880
  ];
762
881
  }
763
882
  });
@@ -1309,7 +1428,7 @@ var init_loadPriorArt = __esm({
1309
1428
  // package.json
1310
1429
  var package_default = {
1311
1430
  name: "@kody-ade/kody-engine",
1312
- version: "0.4.194",
1431
+ version: "0.4.195-dev.0",
1313
1432
  description: "kody \u2014 autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
1314
1433
  license: "MIT",
1315
1434
  type: "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kody-ade/kody-engine",
3
- "version": "0.4.194",
3
+ "version": "0.4.195-dev.0",
4
4
  "description": "kody — autonomous development engine. Single-session Claude Code agent behind a generic executor + declarative executable profiles.",
5
5
  "license": "MIT",
6
6
  "type": "module",