@cursor/july 0.1.54 → 0.1.61

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 (293) hide show
  1. package/dist/bin/agent-serve.js +30 -23
  2. package/dist/channels/github/github-channel.d.ts +12 -0
  3. package/dist/channels/github/github-channel.d.ts.map +1 -1
  4. package/dist/channels/github/github-channel.js +67 -15
  5. package/dist/channels/github/types.d.ts +27 -1
  6. package/dist/channels/github/types.d.ts.map +1 -1
  7. package/dist/channels/origin/cursor-account.d.ts +22 -0
  8. package/dist/channels/origin/cursor-account.d.ts.map +1 -0
  9. package/dist/channels/origin/cursor-account.js +35 -0
  10. package/dist/channels/origin/events.d.ts +10 -0
  11. package/dist/channels/origin/events.d.ts.map +1 -0
  12. package/dist/channels/origin/events.js +27 -0
  13. package/dist/channels/origin/index.d.ts +29 -0
  14. package/dist/channels/origin/index.d.ts.map +1 -0
  15. package/dist/channels/origin/index.js +28 -0
  16. package/dist/channels/origin/origin-channel.d.ts +68 -0
  17. package/dist/channels/origin/origin-channel.d.ts.map +1 -0
  18. package/dist/channels/origin/origin-channel.js +567 -0
  19. package/dist/channels/origin/origin-webhook.d.ts +38 -0
  20. package/dist/channels/origin/origin-webhook.d.ts.map +1 -0
  21. package/dist/channels/origin/origin-webhook.js +151 -0
  22. package/dist/channels/origin/types.d.ts +152 -0
  23. package/dist/channels/origin/types.d.ts.map +1 -0
  24. package/dist/channels/origin/types.js +1 -0
  25. package/dist/channels/slack/doctor.d.ts +17 -1
  26. package/dist/channels/slack/doctor.d.ts.map +1 -1
  27. package/dist/channels/slack/doctor.js +186 -10
  28. package/dist/docs/404.html +2 -2
  29. package/dist/docs/ab.html +4 -4
  30. package/dist/docs/assets/{app.DAKcDclS.js → app.CW3KJh72.js} +1 -1
  31. package/dist/docs/assets/chunks/@localSearchIndexroot.BuymfBAo.js +1 -0
  32. package/dist/docs/assets/chunks/{VPLocalSearchBox.BNIaCSfG.js → VPLocalSearchBox.Bk6EWpPj.js} +1 -1
  33. package/dist/docs/assets/chunks/{theme.B47GzJjc.js → theme.Bz9AodBA.js} +2 -2
  34. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.js → example-agents_fsd.md.ZeGEpAw_.js} +2 -2
  35. package/dist/docs/assets/{example-agents_fsd.md.DPz9ezO4.lean.js → example-agents_fsd.md.ZeGEpAw_.lean.js} +1 -1
  36. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.js → example-agents_security-reviewer.md.DBL1TwtP.js} +2 -2
  37. package/dist/docs/assets/{example-agents_security-reviewer.md.Dkf1gyo6.lean.js → example-agents_security-reviewer.md.DBL1TwtP.lean.js} +1 -1
  38. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.js +12 -0
  39. package/dist/docs/assets/guides_convert-automation.md.BhMzTkE5.lean.js +1 -0
  40. package/dist/docs/assets/{guides_github.md.B8QKtIpw.js → guides_github.md.CLLQJKRB.js} +4 -4
  41. package/dist/docs/assets/{guides_github.md.B8QKtIpw.lean.js → guides_github.md.CLLQJKRB.lean.js} +1 -1
  42. package/dist/docs/assets/index.md.DhRHS_-L.js +5 -0
  43. package/dist/docs/assets/{index.md.COeoSkcF.lean.js → index.md.DhRHS_-L.lean.js} +1 -1
  44. package/dist/docs/assets/{reference_cli.md.-V6J7Cl8.js → reference_cli.md.Cl14Jfo3.js} +10 -2
  45. package/dist/docs/assets/{reference_cli.md.-V6J7Cl8.lean.js → reference_cli.md.Cl14Jfo3.lean.js} +1 -1
  46. package/dist/docs/assets/scaffolding-agents.md.CrWSA0Ko.js +1 -0
  47. package/dist/docs/assets/{scaffolding-agents.md.mkc3B_ZW.lean.js → scaffolding-agents.md.CrWSA0Ko.lean.js} +1 -1
  48. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.js +14 -0
  49. package/dist/docs/assets/templates_agentic-owners.md.BkTLORaU.lean.js +1 -0
  50. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.js +4 -0
  51. package/dist/docs/assets/templates_demo.md.Bgd6MBaZ.lean.js +1 -0
  52. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.js +8 -0
  53. package/dist/docs/assets/templates_pr-autofixer.md.DcmoeUNZ.lean.js +1 -0
  54. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.js +2 -0
  55. package/dist/docs/assets/templates_security-reviewer.md.C0yIUaYs.lean.js +1 -0
  56. package/dist/docs/assets/templates_triage.md.DWuQ1bZz.js +9 -0
  57. package/dist/docs/assets/templates_triage.md.DWuQ1bZz.lean.js +1 -0
  58. package/dist/docs/building-with-agents.html +4 -4
  59. package/dist/docs/concepts.html +5 -5
  60. package/dist/docs/deployment.html +4 -4
  61. package/dist/docs/evals.html +4 -4
  62. package/dist/docs/example-agents/approval-buddy.html +4 -4
  63. package/dist/docs/example-agents/benny.html +4 -4
  64. package/dist/docs/example-agents/bugbot.html +4 -4
  65. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  66. package/dist/docs/example-agents/codeowners-review.html +4 -4
  67. package/dist/docs/example-agents/concierge.html +4 -4
  68. package/dist/docs/example-agents/fsd.html +5 -5
  69. package/dist/docs/example-agents/index.html +4 -4
  70. package/dist/docs/example-agents/knowledge-base.html +4 -4
  71. package/dist/docs/example-agents/oncall.html +4 -4
  72. package/dist/docs/example-agents/security-reviewer.html +5 -5
  73. package/dist/docs/example-agents/slack-agent.html +4 -4
  74. package/dist/docs/example-agents/weather-agent.html +4 -4
  75. package/dist/docs/guides/agent-to-agent.html +4 -4
  76. package/dist/docs/guides/cloud-runtime.html +4 -4
  77. package/dist/docs/guides/convert-automation.html +36 -0
  78. package/dist/docs/guides/github.html +7 -7
  79. package/dist/docs/guides/human-in-the-loop.html +4 -4
  80. package/dist/docs/guides/mcp-oauth.html +4 -4
  81. package/dist/docs/guides/opentelemetry.html +4 -4
  82. package/dist/docs/guides/slack.html +4 -4
  83. package/dist/docs/guides/webhooks.html +4 -4
  84. package/dist/docs/hashmap.json +1 -1
  85. package/dist/docs/hillclimbing.html +4 -4
  86. package/dist/docs/index.html +6 -6
  87. package/dist/docs/quickstart.html +4 -4
  88. package/dist/docs/reference/agent-config.html +4 -4
  89. package/dist/docs/reference/artifacts.html +4 -4
  90. package/dist/docs/reference/channels.html +4 -4
  91. package/dist/docs/reference/cli.html +14 -6
  92. package/dist/docs/reference/connections.html +4 -4
  93. package/dist/docs/reference/hooks.html +4 -4
  94. package/dist/docs/reference/http-api.html +4 -4
  95. package/dist/docs/reference/instructions.html +4 -4
  96. package/dist/docs/reference/playground.html +4 -4
  97. package/dist/docs/reference/project-layout.html +4 -4
  98. package/dist/docs/reference/prompt.html +4 -4
  99. package/dist/docs/reference/schedules.html +4 -4
  100. package/dist/docs/reference/sessions.html +4 -4
  101. package/dist/docs/reference/skills.html +4 -4
  102. package/dist/docs/reference/subagents.html +4 -4
  103. package/dist/docs/reference/tools.html +4 -4
  104. package/dist/docs/scaffolding-agents.html +5 -5
  105. package/dist/docs/storage.html +4 -4
  106. package/dist/docs/templates/agentic-owners.html +38 -0
  107. package/dist/docs/templates/demo.html +28 -0
  108. package/dist/docs/templates/pr-autofixer.html +32 -0
  109. package/dist/docs/templates/security-reviewer.html +26 -0
  110. package/dist/docs/templates/triage.html +14 -16
  111. package/dist/docs/troubleshooting.html +4 -4
  112. package/dist/index.d.ts +3 -3
  113. package/dist/index.d.ts.map +1 -1
  114. package/dist/index.js +2 -2
  115. package/dist/internal/cli-ax.d.ts +2 -0
  116. package/dist/internal/cli-ax.d.ts.map +1 -1
  117. package/dist/internal/cli-ax.js +30 -1
  118. package/dist/internal/cli-deploy.js +1 -1
  119. package/dist/internal/cli-slack.d.ts +4 -0
  120. package/dist/internal/cli-slack.d.ts.map +1 -1
  121. package/dist/internal/cli-slack.js +46 -20
  122. package/dist/internal/cursor-event-relay.d.ts +59 -11
  123. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  124. package/dist/internal/cursor-event-relay.js +133 -26
  125. package/dist/internal/discovery.d.ts.map +1 -1
  126. package/dist/internal/discovery.js +11 -2
  127. package/dist/internal/distribution.d.ts.map +1 -1
  128. package/dist/internal/distribution.js +1 -0
  129. package/dist/internal/handleAgentServeTrigger.d.ts +33 -6
  130. package/dist/internal/handleAgentServeTrigger.d.ts.map +1 -1
  131. package/dist/internal/handleAgentServeTrigger.js +97 -19
  132. package/dist/internal/init-interview.d.ts +67 -0
  133. package/dist/internal/init-interview.d.ts.map +1 -0
  134. package/dist/internal/init-interview.js +300 -0
  135. package/dist/internal/init-project.d.ts +7 -0
  136. package/dist/internal/init-project.d.ts.map +1 -1
  137. package/dist/internal/init-project.js +99 -43
  138. package/dist/internal/mcp-endpoint.d.ts +7 -0
  139. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  140. package/dist/internal/mcp-endpoint.js +7 -0
  141. package/dist/internal/server.d.ts.map +1 -1
  142. package/dist/internal/server.js +159 -57
  143. package/dist/internal/session-engine.d.ts.map +1 -1
  144. package/dist/internal/session-engine.js +20 -21
  145. package/dist/internal/session-links.d.ts +14 -0
  146. package/dist/internal/session-links.d.ts.map +1 -0
  147. package/dist/internal/session-links.js +69 -0
  148. package/dist/internal/sessions-client.d.ts.map +1 -1
  149. package/dist/internal/sessions-client.js +1 -0
  150. package/dist/internal/slack-provision-client.js +1 -1
  151. package/dist/playground/assets/index-ClbuaLpm.css +1 -0
  152. package/dist/playground/assets/index-SqkR13k_.js +85 -0
  153. package/dist/playground/index.html +2 -2
  154. package/dist/types.d.ts +19 -12
  155. package/dist/types.d.ts.map +1 -1
  156. package/docs/README.md +15 -0
  157. package/docs/example-agents/fsd.md +3 -0
  158. package/docs/example-agents/security-reviewer.md +3 -0
  159. package/docs/guides/convert-automation.md +176 -0
  160. package/docs/guides/github.md +6 -2
  161. package/docs/reference/cli.md +36 -10
  162. package/docs/scaffolding-agents.md +3 -0
  163. package/docs/templates/agentic-owners.md +97 -0
  164. package/docs/templates/demo.md +84 -0
  165. package/docs/templates/pr-autofixer.md +136 -0
  166. package/docs/templates/security-reviewer.md +89 -0
  167. package/docs/templates/triage.md +59 -101
  168. package/package.json +9 -1
  169. package/skills/github/SKILL.md +4 -5
  170. package/src/bin/agent-serve.ts +13 -6
  171. package/src/channels/github/github-channel.ts +95 -18
  172. package/src/channels/github/types.ts +31 -0
  173. package/src/channels/origin/cursor-account.ts +64 -0
  174. package/src/channels/origin/events.ts +41 -0
  175. package/src/channels/origin/index.ts +29 -0
  176. package/src/channels/origin/origin-channel.ts +784 -0
  177. package/src/channels/origin/origin-webhook.ts +202 -0
  178. package/src/channels/origin/types.ts +186 -0
  179. package/src/channels/slack/doctor.ts +218 -5
  180. package/src/index.ts +3 -1
  181. package/src/internal/cli-ax.ts +33 -1
  182. package/src/internal/cli-deploy.ts +2 -2
  183. package/src/internal/cli-slack.ts +68 -19
  184. package/src/internal/cursor-event-relay.ts +193 -31
  185. package/src/internal/discovery.ts +15 -5
  186. package/src/internal/distribution.ts +1 -0
  187. package/src/internal/handleAgentServeTrigger.ts +129 -19
  188. package/src/internal/init-interview.ts +384 -0
  189. package/src/internal/init-project.ts +146 -46
  190. package/src/internal/mcp-endpoint.ts +7 -0
  191. package/src/internal/server.ts +188 -58
  192. package/src/internal/session-engine.ts +5 -21
  193. package/src/internal/session-links.ts +88 -0
  194. package/src/internal/sessions-client.ts +1 -0
  195. package/src/internal/slack-provision-client.ts +1 -1
  196. package/src/types.ts +19 -12
  197. package/templates/agentic-owners/README.md +73 -0
  198. package/templates/agentic-owners/agent/agent.ts +15 -0
  199. package/templates/agentic-owners/agent/channels/github.ts +85 -0
  200. package/templates/agentic-owners/agent/instructions.md +34 -0
  201. package/templates/agentic-owners/agent/lib/config.ts +13 -0
  202. package/templates/agentic-owners/agent/lib/github.ts +241 -0
  203. package/templates/agentic-owners/agent/lib/owners/README.md +50 -0
  204. package/templates/agentic-owners/agent/lib/owners/constants.ts +22 -0
  205. package/templates/agentic-owners/agent/lib/owners/decision.ts +485 -0
  206. package/templates/agentic-owners/agent/lib/owners/handles.ts +54 -0
  207. package/templates/agentic-owners/agent/lib/owners/match.ts +177 -0
  208. package/templates/agentic-owners/agent/lib/owners/matching.ts +95 -0
  209. package/templates/agentic-owners/agent/lib/owners/parse.ts +589 -0
  210. package/templates/agentic-owners/agent/lib/owners/policyMode.ts +65 -0
  211. package/templates/agentic-owners/agent/lib/owners/prompt.ts +66 -0
  212. package/templates/agentic-owners/agent/lib/owners/prompts/base-review.md +105 -0
  213. package/templates/agentic-owners/agent/lib/owners/types.ts +45 -0
  214. package/templates/agentic-owners/agent/lib/review.ts +777 -0
  215. package/templates/agentic-owners/agent/tools/record_review.ts +69 -0
  216. package/templates/agentic-owners/evals/evals.config.ts +5 -0
  217. package/templates/agentic-owners/evals/owners.eval.ts +116 -0
  218. package/templates/agentic-owners/package.json +20 -0
  219. package/templates/agentic-owners/tsconfig.json +12 -0
  220. package/templates/demo/README.md +33 -0
  221. package/templates/demo/agent/agent.ts +15 -0
  222. package/templates/demo/agent/channels/github.ts +50 -0
  223. package/templates/demo/agent/channels/queue.ts +30 -0
  224. package/templates/demo/agent/instructions.md +8 -0
  225. package/templates/demo/agent/lib/collect.ts +78 -0
  226. package/templates/demo/agent/tools/record_demo.ts +40 -0
  227. package/templates/demo/evals/demo.eval.ts +33 -0
  228. package/templates/demo/evals/evals.config.ts +5 -0
  229. package/templates/demo/package.json +18 -0
  230. package/templates/demo/tsconfig.json +12 -0
  231. package/templates/pr-autofixer/README.md +44 -0
  232. package/templates/pr-autofixer/agent/agent.ts +22 -0
  233. package/templates/pr-autofixer/agent/channels/drive.ts +40 -0
  234. package/templates/pr-autofixer/agent/channels/github.ts +167 -0
  235. package/templates/pr-autofixer/agent/channels/slack.ts +17 -0
  236. package/templates/pr-autofixer/agent/hooks/bind-agent.ts +19 -0
  237. package/templates/pr-autofixer/agent/instructions.md +83 -0
  238. package/templates/pr-autofixer/agent/lib/affinity.ts +33 -0
  239. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +15 -0
  240. package/templates/pr-autofixer/agent/lib/drive.ts +79 -0
  241. package/templates/pr-autofixer/agent/lib/merge-watch.ts +123 -0
  242. package/templates/pr-autofixer/agent/lib/pr-state.ts +54 -0
  243. package/templates/pr-autofixer/agent/lib/repos.ts +19 -0
  244. package/templates/pr-autofixer/agent/lib/start-drive.ts +122 -0
  245. package/templates/pr-autofixer/agent/lib/webhook-buffer.ts +225 -0
  246. package/templates/pr-autofixer/agent/lib/webhook-routing.ts +231 -0
  247. package/templates/pr-autofixer/agent/storage.ts +7 -0
  248. package/templates/pr-autofixer/agent/tools/drive_pr.ts +22 -0
  249. package/templates/pr-autofixer/evals/coordinator.eval.ts +16 -0
  250. package/templates/pr-autofixer/evals/evals.config.ts +5 -0
  251. package/templates/pr-autofixer/init.json +25 -0
  252. package/templates/pr-autofixer/package.json +18 -0
  253. package/templates/pr-autofixer/tsconfig.json +12 -0
  254. package/templates/security-reviewer/README.md +31 -0
  255. package/templates/security-reviewer/agent/agent.ts +15 -0
  256. package/templates/security-reviewer/agent/channels/github.ts +46 -0
  257. package/templates/security-reviewer/agent/instructions.md +22 -0
  258. package/templates/security-reviewer/agent/lib/repos.ts +5 -0
  259. package/templates/security-reviewer/agent/tools/post_findings.ts +72 -0
  260. package/templates/security-reviewer/evals/evals.config.ts +5 -0
  261. package/templates/security-reviewer/evals/review.eval.ts +53 -0
  262. package/templates/security-reviewer/init.json +25 -0
  263. package/templates/security-reviewer/package.json +18 -0
  264. package/templates/security-reviewer/tsconfig.json +12 -0
  265. package/templates/triage/README.md +27 -29
  266. package/templates/triage/agent/agent.ts +1 -0
  267. package/templates/triage/agent/channels/queue.ts +47 -0
  268. package/templates/triage/agent/channels/webhook.ts +50 -0
  269. package/templates/triage/agent/instructions.md +24 -13
  270. package/templates/triage/agent/lib/actuate.ts +759 -0
  271. package/templates/triage/agent/lib/parse-queue-event.ts +168 -0
  272. package/templates/triage/agent/lib/tracker.ts +5 -0
  273. package/templates/triage/agent/lib/types.ts +52 -0
  274. package/templates/triage/agent/lib/verify-webhook.ts +81 -0
  275. package/templates/triage/agent/skills/triage-rubric.md +6 -5
  276. package/templates/triage/agent/tools/record_triage.ts +24 -5
  277. package/templates/triage/evals/triage.eval.ts +25 -3
  278. package/templates/triage/overlays/jira/README.md +21 -0
  279. package/templates/triage/overlays/jira/agent/lib/tracker.ts +8 -0
  280. package/templates/triage/overlays/jira/agent/mcp-connections/tracker.ts +7 -0
  281. package/templates/triage/overlays/linear/README.md +21 -0
  282. package/templates/triage/overlays/linear/agent/lib/tracker.ts +8 -0
  283. package/templates/triage/overlays/linear/agent/mcp-connections/tracker.ts +7 -0
  284. package/templates/triage/package.json +1 -1
  285. package/dist/docs/assets/chunks/@localSearchIndexroot.C4uxzYEU.js +0 -1
  286. package/dist/docs/assets/index.md.COeoSkcF.js +0 -5
  287. package/dist/docs/assets/scaffolding-agents.md.mkc3B_ZW.js +0 -1
  288. package/dist/docs/assets/templates_triage.md.CConZt1z.js +0 -11
  289. package/dist/docs/assets/templates_triage.md.CConZt1z.lean.js +0 -1
  290. package/dist/playground/assets/index-BHAPd9KT.js +0 -85
  291. package/dist/playground/assets/index-DpKH0HcT.css +0 -1
  292. package/templates/triage/agent/channels/intake.ts +0 -20
  293. package/templates/triage/agent/channels/slack.ts +0 -3
@@ -0,0 +1,759 @@
1
+ import type {
2
+ HostMcpCallResult,
3
+ HostMcpRegistry,
4
+ HostMcpToolInfo,
5
+ } from "@cursor/july";
6
+ import {
7
+ ISSUE_ID,
8
+ JIRA_URL,
9
+ LINEAR_URL,
10
+ pinIssueIdentifier,
11
+ richTextValue,
12
+ } from "./parse-queue-event.js";
13
+ import type {
14
+ QueueAction,
15
+ RecordTriageResult,
16
+ TrackerConfig,
17
+ TrackerIssue,
18
+ TrackerResult,
19
+ TriageSeverity,
20
+ TriageVerdict,
21
+ } from "./types.js";
22
+
23
+ const CONNECTION_NAME = "tracker";
24
+
25
+ const SEARCH_ISSUE_TOOL = /(?:search|list|find)[_-]?issues?$/i;
26
+ const COMMENT_TOOL = /(?:add|create|save)[_-]?comment$/i;
27
+ const LIST_COMMENT_TOOL = /(?:list|get|read)[_-]?comments?$/i;
28
+ const SAVE_OR_UPDATE_ISSUE_TOOL = /(?:save|update)[_-]?issue$/i;
29
+ const UPDATE_ISSUE_TOOL = /update[_-]?issue$/i;
30
+
31
+ const FIELD_ALIASES = {
32
+ title: ["title", "name", "issueTitle", "summary"],
33
+ query: ["query", "q", "search", "term", "jql", "text"],
34
+ comment: ["comment", "body", "text", "message"],
35
+ issue: ["id", "issueId", "issue_id", "issueKey", "issue_key", "identifier"],
36
+ commentIssue: [
37
+ "issueId",
38
+ "issue_id",
39
+ "issueKey",
40
+ "issue_key",
41
+ "identifier",
42
+ ],
43
+ priority: ["priority", "priorityId", "priority_id"],
44
+ } as const;
45
+
46
+ const PRIORITY_BY_SEVERITY: Record<TriageSeverity, number> = {
47
+ urgent: 1,
48
+ high: 2,
49
+ medium: 3,
50
+ low: 4,
51
+ };
52
+
53
+ export interface ActuateInput {
54
+ verdict: TriageVerdict;
55
+ mcp: HostMcpRegistry;
56
+ tracker: TrackerConfig;
57
+ purpose?: "live" | "eval";
58
+ now?: () => string;
59
+ issueKey?: string | null;
60
+ /**
61
+ * Host-trusted inbound title. Live `record_triage` omits this and looks
62
+ * up the pinned issue. `session.title` is the harness display title and
63
+ * is overwritten by the turn summary.
64
+ */
65
+ issueTitle?: string | null;
66
+ }
67
+
68
+ export async function actuate(input: ActuateInput): Promise<RecordTriageResult> {
69
+ const evaluatedAt = (input.now ?? (() => new Date().toISOString()))();
70
+ const verdict: TriageVerdict = {
71
+ ...input.verdict,
72
+ identifier: pinIssueIdentifier(input.issueKey, input.verdict.identifier),
73
+ };
74
+ const tracker = await resolveTracker({ ...input, verdict });
75
+ return { verdict, evaluatedAt, tracker };
76
+ }
77
+
78
+ async function resolveTracker(input: ActuateInput): Promise<TrackerResult> {
79
+ if (input.purpose === "eval") {
80
+ return { status: "skipped", reason: "eval" };
81
+ }
82
+ if (input.tracker.kind === "none") {
83
+ return { status: "not_connected" };
84
+ }
85
+
86
+ const connectionName = input.tracker.connectionName || CONNECTION_NAME;
87
+ if (!input.mcp.names().includes(connectionName)) {
88
+ return { status: "not_connected" };
89
+ }
90
+
91
+ let tools: HostMcpToolInfo[];
92
+ try {
93
+ tools = await input.mcp.listTools(connectionName);
94
+ } catch (error) {
95
+ return { status: "failed", message: errorText(error) };
96
+ }
97
+ if (tools.length === 0) {
98
+ return { status: "not_connected" };
99
+ }
100
+
101
+ const issue: TrackerIssue = {
102
+ identifier: input.verdict.identifier,
103
+ url: "",
104
+ };
105
+ const searchTool = tools.find((tool) => SEARCH_ISSUE_TOOL.test(tool.name));
106
+ let search: SearchResult = {
107
+ candidates: [],
108
+ };
109
+ if (searchTool !== undefined) {
110
+ const hostTitle = await resolveHostTitle({
111
+ mcp: input.mcp,
112
+ connectionName,
113
+ tool: searchTool,
114
+ identifier: issue.identifier,
115
+ issueTitle: input.issueTitle,
116
+ });
117
+ if (hostTitle !== "") {
118
+ search = await searchExisting({
119
+ mcp: input.mcp,
120
+ connectionName,
121
+ tool: searchTool,
122
+ query: hostTitle,
123
+ matchTitle: hostTitle,
124
+ excludeIdentifier: issue.identifier,
125
+ });
126
+ }
127
+ }
128
+
129
+ const duplicate = search.duplicate;
130
+ const commentTool = tools.find((tool) => COMMENT_TOOL.test(tool.name));
131
+ if (commentTool === undefined) {
132
+ return {
133
+ status: "failed",
134
+ message: "the tracker connector exposes no comment tool",
135
+ };
136
+ }
137
+
138
+ const listed = await loadComments({
139
+ mcp: input.mcp,
140
+ connectionName,
141
+ tools,
142
+ identifier: issue.identifier,
143
+ });
144
+
145
+ const actions: QueueAction[] = [];
146
+ if (listed.status !== "failed") {
147
+ if (
148
+ !alreadyCommented(
149
+ listed,
150
+ search.candidates,
151
+ issue.identifier,
152
+ "Triage:"
153
+ )
154
+ ) {
155
+ const comment = await commentOn({
156
+ mcp: input.mcp,
157
+ connectionName,
158
+ tool: commentTool,
159
+ issue,
160
+ body: triageComment(input.verdict, duplicate),
161
+ });
162
+ if (!comment.ok) {
163
+ return { status: "failed", message: comment.message };
164
+ }
165
+ }
166
+ actions.push({ op: "commented" });
167
+ }
168
+ if (input.verdict.needsInfo === true) {
169
+ actions.push({ op: "needs_info" });
170
+ }
171
+
172
+ if (duplicate !== undefined) {
173
+ const duplicateComment = `Triage: duplicate inbound ${issue.identifier}`;
174
+ const duplicateListed = await loadComments({
175
+ mcp: input.mcp,
176
+ connectionName,
177
+ tools,
178
+ identifier: duplicate.identifier,
179
+ });
180
+ if (duplicateListed.status !== "failed") {
181
+ if (
182
+ !alreadyCommented(
183
+ duplicateListed,
184
+ search.candidates,
185
+ duplicate.identifier,
186
+ duplicateComment
187
+ )
188
+ ) {
189
+ const comment = await commentOn({
190
+ mcp: input.mcp,
191
+ connectionName,
192
+ tool: commentTool,
193
+ issue: duplicate,
194
+ body: duplicateComment,
195
+ });
196
+ if (!comment.ok) {
197
+ return { status: "failed", message: comment.message };
198
+ }
199
+ }
200
+ actions.push({ op: "duplicate_of", of: duplicate.identifier });
201
+ }
202
+ }
203
+
204
+ if (input.verdict.kind === "bug") {
205
+ let updateTool: HostMcpToolInfo | undefined;
206
+ let updateArgs: Record<string, unknown> | undefined;
207
+ for (const tool of tools) {
208
+ if (!SAVE_OR_UPDATE_ISSUE_TOOL.test(tool.name)) {
209
+ continue;
210
+ }
211
+ const args = buildPriorityArgs(
212
+ input.verdict,
213
+ tool,
214
+ input.tracker.kind
215
+ );
216
+ if (args !== undefined) {
217
+ updateTool = tool;
218
+ updateArgs = args;
219
+ break;
220
+ }
221
+ }
222
+ if (updateTool !== undefined && updateArgs !== undefined) {
223
+ const update = await callTool({
224
+ mcp: input.mcp,
225
+ connectionName,
226
+ tool: updateTool.name,
227
+ args: updateArgs,
228
+ });
229
+ if (!update.ok) {
230
+ return { status: "failed", message: update.message };
231
+ }
232
+ actions.push({
233
+ op: "prioritized",
234
+ severity: input.verdict.severity,
235
+ });
236
+ }
237
+ }
238
+
239
+ return { status: "acted", issue, actions };
240
+ }
241
+
242
+ type SearchResult = {
243
+ candidates: CandidateIssue[];
244
+ duplicate?: TrackerIssue;
245
+ };
246
+
247
+ async function resolveHostTitle(input: {
248
+ mcp: HostMcpRegistry;
249
+ connectionName: string;
250
+ tool: HostMcpToolInfo;
251
+ identifier: string;
252
+ issueTitle?: string | null;
253
+ }): Promise<string> {
254
+ if (typeof input.issueTitle === "string") {
255
+ const title = input.issueTitle.trim();
256
+ if (title !== "") {
257
+ return title;
258
+ }
259
+ }
260
+ const listed = await searchIssues({
261
+ mcp: input.mcp,
262
+ connectionName: input.connectionName,
263
+ tool: input.tool,
264
+ query: input.identifier,
265
+ });
266
+ const self = listed.find(
267
+ (candidate) => issueIdentifier(candidate) === input.identifier
268
+ );
269
+ return self?.title.trim() ?? "";
270
+ }
271
+
272
+ async function searchExisting(input: {
273
+ mcp: HostMcpRegistry;
274
+ connectionName: string;
275
+ tool: HostMcpToolInfo;
276
+ query: string;
277
+ matchTitle: string;
278
+ excludeIdentifier: string;
279
+ }): Promise<SearchResult> {
280
+ const candidates = await searchIssues({
281
+ mcp: input.mcp,
282
+ connectionName: input.connectionName,
283
+ tool: input.tool,
284
+ query: input.query,
285
+ });
286
+ return {
287
+ candidates,
288
+ duplicate: matchingIssue(
289
+ candidates,
290
+ input.matchTitle,
291
+ input.excludeIdentifier
292
+ ),
293
+ };
294
+ }
295
+
296
+ async function searchIssues(input: {
297
+ mcp: HostMcpRegistry;
298
+ connectionName: string;
299
+ tool: HostMcpToolInfo;
300
+ query: string;
301
+ }): Promise<CandidateIssue[]> {
302
+ try {
303
+ const result = await input.mcp.callTool(
304
+ input.connectionName,
305
+ input.tool.name,
306
+ buildSearchArgs(input.query, input.tool)
307
+ );
308
+ if (result.isError === true) {
309
+ return [];
310
+ }
311
+ return issuesFromSearch(result);
312
+ } catch {
313
+ return [];
314
+ }
315
+ }
316
+
317
+ type ListedComments =
318
+ | { status: "listed"; bodies: string[] }
319
+ | { status: "unavailable" }
320
+ | { status: "failed" };
321
+
322
+ async function loadComments(input: {
323
+ mcp: HostMcpRegistry;
324
+ connectionName: string;
325
+ tools: HostMcpToolInfo[];
326
+ identifier: string;
327
+ }): Promise<ListedComments> {
328
+ const tool = input.tools.find((candidate) =>
329
+ LIST_COMMENT_TOOL.test(candidate.name)
330
+ );
331
+ if (tool === undefined) {
332
+ return { status: "unavailable" };
333
+ }
334
+ try {
335
+ const result = await input.mcp.callTool(
336
+ input.connectionName,
337
+ tool.name,
338
+ {
339
+ [pickProperty(tool, FIELD_ALIASES.commentIssue)]: input.identifier,
340
+ }
341
+ );
342
+ if (result.isError === true) {
343
+ return { status: "failed" };
344
+ }
345
+ return { status: "listed", bodies: commentBodiesFrom(result) };
346
+ } catch {
347
+ return { status: "failed" };
348
+ }
349
+ }
350
+
351
+ function commentBodiesFrom(result: HostMcpCallResult): string[] {
352
+ const fromStructured = collectCommentBodies(result.structuredContent);
353
+ if (fromStructured.length > 0) {
354
+ return fromStructured;
355
+ }
356
+ const text = resultText(result);
357
+ const fromJson = collectCommentBodies(tryParseJson(text));
358
+ if (fromJson.length > 0) {
359
+ return fromJson;
360
+ }
361
+ return text === "" ? [] : [text];
362
+ }
363
+
364
+ function alreadyCommented(
365
+ listed: ListedComments,
366
+ candidates: CandidateIssue[],
367
+ identifier: string,
368
+ prefix: string
369
+ ): boolean {
370
+ if (listed.status === "listed") {
371
+ return listed.bodies.some((comment) => hasSentinel(comment, prefix));
372
+ }
373
+ return hasCommentStarting(candidates, identifier, prefix);
374
+ }
375
+
376
+ async function commentOn(input: {
377
+ mcp: HostMcpRegistry;
378
+ connectionName: string;
379
+ tool: HostMcpToolInfo;
380
+ issue: TrackerIssue;
381
+ body: string;
382
+ }): Promise<{ ok: true } | { ok: false; message: string }> {
383
+ const result = await callTool({
384
+ mcp: input.mcp,
385
+ connectionName: input.connectionName,
386
+ tool: input.tool.name,
387
+ args: buildCommentArgs(input.issue, input.body, input.tool),
388
+ });
389
+ if (!result.ok) {
390
+ return {
391
+ ok: false,
392
+ message: `could not comment on ${input.issue.identifier}: ${result.message}`,
393
+ };
394
+ }
395
+ return result;
396
+ }
397
+
398
+ async function callTool(input: {
399
+ mcp: HostMcpRegistry;
400
+ connectionName: string;
401
+ tool: string;
402
+ args: Record<string, unknown>;
403
+ }): Promise<{ ok: true } | { ok: false; message: string }> {
404
+ try {
405
+ const result = await input.mcp.callTool(
406
+ input.connectionName,
407
+ input.tool,
408
+ input.args
409
+ );
410
+ if (result.isError === true) {
411
+ return { ok: false, message: resultText(result) };
412
+ }
413
+ return { ok: true };
414
+ } catch (error) {
415
+ return { ok: false, message: errorText(error) };
416
+ }
417
+ }
418
+
419
+ function buildSearchArgs(
420
+ query: string,
421
+ tool: HostMcpToolInfo
422
+ ): Record<string, unknown> {
423
+ return {
424
+ [pickProperty(tool, FIELD_ALIASES.query)]: query,
425
+ };
426
+ }
427
+
428
+ function buildCommentArgs(
429
+ issue: TrackerIssue,
430
+ body: string,
431
+ tool: HostMcpToolInfo
432
+ ): Record<string, unknown> {
433
+ return {
434
+ [pickProperty(tool, FIELD_ALIASES.commentIssue)]: issue.identifier,
435
+ [pickProperty(tool, FIELD_ALIASES.comment)]: body,
436
+ };
437
+ }
438
+
439
+ function buildPriorityArgs(
440
+ verdict: TriageVerdict,
441
+ tool: HostMcpToolInfo,
442
+ kind: TrackerConfig["kind"]
443
+ ): Record<string, unknown> | undefined {
444
+ if (kind !== "linear") {
445
+ return undefined;
446
+ }
447
+ const known = propertyNames(tool);
448
+ const issueProperty =
449
+ FIELD_ALIASES.issue.find((alias) => known.has(alias)) ??
450
+ (UPDATE_ISSUE_TOOL.test(tool.name) ? "id" : undefined);
451
+ if (issueProperty === undefined) {
452
+ return undefined;
453
+ }
454
+ const priorityProperty = FIELD_ALIASES.priority.find((alias) =>
455
+ known.has(alias)
456
+ );
457
+ if (priorityProperty === undefined) {
458
+ return undefined;
459
+ }
460
+ return {
461
+ [issueProperty]: verdict.identifier,
462
+ [priorityProperty]: PRIORITY_BY_SEVERITY[verdict.severity],
463
+ };
464
+ }
465
+
466
+ function pickProperty(
467
+ tool: HostMcpToolInfo,
468
+ aliases: readonly string[]
469
+ ): string {
470
+ const known = propertyNames(tool);
471
+ return aliases.find((alias) => known.has(alias)) ?? aliases[0]!;
472
+ }
473
+
474
+ function propertyNames(tool: HostMcpToolInfo): Set<string> {
475
+ const properties = tool.inputSchema?.["properties"];
476
+ if (!isRecord(properties)) {
477
+ return new Set();
478
+ }
479
+ return new Set(Object.keys(properties));
480
+ }
481
+
482
+ function triageComment(
483
+ verdict: TriageVerdict,
484
+ duplicate: TrackerIssue | undefined
485
+ ): string {
486
+ const lines = [
487
+ `Triage: ${verdict.kind}`,
488
+ `Severity: ${verdict.severity}`,
489
+ `Summary: ${verdict.summary}`,
490
+ `Next step: ${verdict.nextStep}`,
491
+ ];
492
+ if (duplicate !== undefined) {
493
+ lines.push(`Duplicate of: ${duplicate.identifier}`);
494
+ }
495
+ if (verdict.needsInfo === true) {
496
+ lines.push(`Needs info: ${verdict.nextStep}`);
497
+ }
498
+ if (verdict.reporter !== undefined && verdict.reporter !== "") {
499
+ lines.push(`Reported by: ${verdict.reporter}`);
500
+ }
501
+ return lines.join("\n");
502
+ }
503
+
504
+ interface CandidateIssue {
505
+ identifier: string;
506
+ url: string;
507
+ title: string;
508
+ comments: string[];
509
+ }
510
+
511
+ function matchingIssue(
512
+ candidates: CandidateIssue[],
513
+ title: string,
514
+ excludeIdentifier: string
515
+ ): TrackerIssue | undefined {
516
+ const match = candidates.find(
517
+ (candidate) =>
518
+ issueIdentifier(candidate) !== excludeIdentifier &&
519
+ titlesMatch(candidate.title, title)
520
+ );
521
+ if (match === undefined) {
522
+ return undefined;
523
+ }
524
+ const identifier = issueIdentifier(match);
525
+ if (identifier === "") {
526
+ return undefined;
527
+ }
528
+ return { identifier, url: match.url };
529
+ }
530
+
531
+ function issuesFromSearch(result: HostMcpCallResult): CandidateIssue[] {
532
+ const fromStructured = collectIssues(result.structuredContent);
533
+ if (fromStructured.length > 0) {
534
+ return fromStructured;
535
+ }
536
+ const text = resultText(result);
537
+ const fromJson = collectIssues(tryParseJson(text));
538
+ return fromJson.length > 0 ? fromJson : issuesFromLines(text);
539
+ }
540
+
541
+ function collectIssues(
542
+ value: unknown,
543
+ found: CandidateIssue[] = []
544
+ ): CandidateIssue[] {
545
+ if (Array.isArray(value)) {
546
+ for (const entry of value) {
547
+ collectIssues(entry, found);
548
+ }
549
+ return found;
550
+ }
551
+ if (!isRecord(value)) {
552
+ return found;
553
+ }
554
+ const url = firstIssueUrl(value);
555
+ const identifier = firstIssueId(value) || issueKeyIn(url);
556
+ const title = firstString(value, FIELD_ALIASES.title);
557
+ if (identifier !== "" || url !== "") {
558
+ found.push({
559
+ identifier,
560
+ url,
561
+ title,
562
+ comments: [
563
+ ...collectCommentBodies(value["comments"]),
564
+ ...collectCommentBodies(value["comment"]),
565
+ ],
566
+ });
567
+ return found;
568
+ }
569
+ for (const nested of Object.values(value)) {
570
+ if (typeof nested === "object" && nested !== null) {
571
+ collectIssues(nested, found);
572
+ }
573
+ }
574
+ return found;
575
+ }
576
+
577
+ function issuesFromLines(text: string): CandidateIssue[] {
578
+ return text.split(/\n/).flatMap((line) => {
579
+ const identifier = line.match(ISSUE_ID)?.[0] ?? "";
580
+ const url = line.match(LINEAR_URL)?.[0] ?? line.match(JIRA_URL)?.[0] ?? "";
581
+ if (identifier === "" && url === "") {
582
+ return [];
583
+ }
584
+ const title = line
585
+ .replace(LINEAR_URL, "")
586
+ .replace(JIRA_URL, "")
587
+ .replace(ISSUE_ID, "")
588
+ .replace(/\s+/g, " ")
589
+ .trim();
590
+ const triage = line.indexOf("Triage:");
591
+ return [
592
+ {
593
+ identifier,
594
+ url,
595
+ title,
596
+ comments: triage === -1 ? [] : [line.slice(triage)],
597
+ },
598
+ ];
599
+ });
600
+ }
601
+
602
+ function hasCommentStarting(
603
+ candidates: CandidateIssue[],
604
+ identifier: string,
605
+ prefix: string
606
+ ): boolean {
607
+ return candidates.some(
608
+ (candidate) =>
609
+ issueIdentifier(candidate) === identifier &&
610
+ candidate.comments.some((comment) => hasSentinel(comment, prefix))
611
+ );
612
+ }
613
+
614
+ function hasSentinel(text: string, prefix: string): boolean {
615
+ return text.split(/\n/).some((line) => {
616
+ const trimmed = line
617
+ .trimStart()
618
+ .replace(/^[-*]\s+/, "")
619
+ .replace(/^\d+\.\s+/, "");
620
+ return trimmed.startsWith(prefix);
621
+ });
622
+ }
623
+
624
+ const COMMENT_WRAP_KEYS = [
625
+ "comments",
626
+ "comment",
627
+ "nodes",
628
+ "items",
629
+ "results",
630
+ "data",
631
+ "content",
632
+ "replies",
633
+ ] as const;
634
+
635
+ function collectCommentBodies(value: unknown, found: string[] = []): string[] {
636
+ if (typeof value === "string") {
637
+ if (value.trim() !== "") {
638
+ found.push(value);
639
+ }
640
+ return found;
641
+ }
642
+ if (Array.isArray(value)) {
643
+ for (const entry of value) {
644
+ collectCommentBodies(entry, found);
645
+ }
646
+ return found;
647
+ }
648
+ if (!isRecord(value)) {
649
+ return found;
650
+ }
651
+ const body = firstString(value, FIELD_ALIASES.comment);
652
+ if (body !== "") {
653
+ found.push(body);
654
+ collectCommentBodies(value["comments"], found);
655
+ collectCommentBodies(value["replies"], found);
656
+ return found;
657
+ }
658
+ for (const key of FIELD_ALIASES.comment) {
659
+ const nested = value[key];
660
+ if (nested === undefined || typeof nested === "string") {
661
+ continue;
662
+ }
663
+ const flattened = richTextValue(nested);
664
+ if (flattened !== "") {
665
+ found.push(flattened);
666
+ collectCommentBodies(value["comments"], found);
667
+ collectCommentBodies(value["replies"], found);
668
+ return found;
669
+ }
670
+ }
671
+ for (const key of COMMENT_WRAP_KEYS) {
672
+ if (value[key] !== undefined) {
673
+ collectCommentBodies(value[key], found);
674
+ }
675
+ }
676
+ return found;
677
+ }
678
+
679
+ function issueIdentifier(issue: CandidateIssue): string {
680
+ return issue.identifier || issueKeyIn(issue.url);
681
+ }
682
+
683
+ function issueKeyIn(value: string): string {
684
+ return value.match(ISSUE_ID)?.[0] ?? "";
685
+ }
686
+
687
+ function firstIssueId(record: Record<string, unknown>): string {
688
+ for (const key of ["identifier", "issueKey", "issue_key", "key", "id"]) {
689
+ const value = record[key];
690
+ if (typeof value === "string") {
691
+ const match = value.match(ISSUE_ID);
692
+ if (match !== null) {
693
+ return match[0];
694
+ }
695
+ }
696
+ }
697
+ return "";
698
+ }
699
+
700
+ function firstIssueUrl(record: Record<string, unknown>): string {
701
+ for (const key of ["url", "htmlUrl", "issueUrl", "link"]) {
702
+ const value = record[key];
703
+ if (typeof value === "string") {
704
+ const url =
705
+ value.match(LINEAR_URL)?.[0] ?? value.match(JIRA_URL)?.[0] ?? "";
706
+ if (url !== "") {
707
+ return url;
708
+ }
709
+ }
710
+ }
711
+ return "";
712
+ }
713
+
714
+ function firstString(
715
+ record: Record<string, unknown>,
716
+ keys: readonly string[]
717
+ ): string {
718
+ for (const key of keys) {
719
+ const value = record[key];
720
+ if (typeof value === "string" && value.trim() !== "") {
721
+ return value;
722
+ }
723
+ }
724
+ return "";
725
+ }
726
+
727
+ function titlesMatch(candidate: string, title: string): boolean {
728
+ const hay = normalizeTitle(candidate);
729
+ const needle = normalizeTitle(title);
730
+ return hay !== "" && hay === needle;
731
+ }
732
+
733
+ function normalizeTitle(value: string): string {
734
+ return value.toLowerCase().replace(/\s+/g, " ").trim();
735
+ }
736
+
737
+ function tryParseJson(text: string): unknown {
738
+ try {
739
+ return JSON.parse(text);
740
+ } catch {
741
+ return undefined;
742
+ }
743
+ }
744
+
745
+ function resultText(result: HostMcpCallResult): string {
746
+ return result.content
747
+ .map((part) => ("text" in part && typeof part.text === "string" ? part.text : ""))
748
+ .filter((part) => part !== "")
749
+ .join("\n")
750
+ .trim();
751
+ }
752
+
753
+ function errorText(error: unknown): string {
754
+ return error instanceof Error ? error.message : String(error);
755
+ }
756
+
757
+ function isRecord(value: unknown): value is Record<string, unknown> {
758
+ return value !== null && typeof value === "object" && !Array.isArray(value);
759
+ }