@wardby/cli 0.3.0 → 0.4.1

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 (275) hide show
  1. package/.env.example +22 -3
  2. package/README.md +11 -9
  3. package/dist/cli-help.d.ts +1 -1
  4. package/dist/cli-help.js +1 -0
  5. package/dist/cli.js +68 -10
  6. package/dist/coding/base-commit.d.ts +6 -0
  7. package/dist/coding/base-commit.js +12 -0
  8. package/dist/coding/protocol.d.ts +17 -1
  9. package/dist/coding/protocol.js +17 -6
  10. package/dist/coding/provider.d.ts +6 -1
  11. package/dist/coding/provider.js +16 -9
  12. package/dist/config/providers.d.ts +25 -0
  13. package/dist/config/providers.js +71 -0
  14. package/dist/core/attribution.d.ts +101 -0
  15. package/dist/core/attribution.js +208 -0
  16. package/dist/core/budget-groups.d.ts +17 -10
  17. package/dist/core/budget-groups.js +15 -12
  18. package/dist/core/coding-queue.d.ts +3 -0
  19. package/dist/core/coding-queue.js +6 -2
  20. package/dist/core/coding-service-status.d.ts +11 -0
  21. package/dist/core/coding-service-status.js +17 -0
  22. package/dist/core/cost-report.d.ts +88 -0
  23. package/dist/core/cost-report.js +248 -0
  24. package/dist/core/dispatch.d.ts +42 -2
  25. package/dist/core/dispatch.js +144 -26
  26. package/dist/core/engine-native.js +17 -4
  27. package/dist/core/glob.d.ts +10 -0
  28. package/dist/core/glob.js +33 -0
  29. package/dist/core/host-events.d.ts +24 -1
  30. package/dist/core/host-events.js +341 -1
  31. package/dist/core/host-status.d.ts +19 -2
  32. package/dist/core/host-status.js +47 -30
  33. package/dist/core/issue-bridge.d.ts +60 -0
  34. package/dist/core/issue-bridge.js +189 -0
  35. package/dist/core/issue-dedupe.d.ts +70 -0
  36. package/dist/core/issue-dedupe.js +255 -0
  37. package/dist/core/issue-events.d.ts +42 -0
  38. package/dist/core/issue-events.js +155 -0
  39. package/dist/core/issue-status.d.ts +29 -0
  40. package/dist/core/issue-status.js +241 -0
  41. package/dist/core/issue-tracker-tools.d.ts +64 -0
  42. package/dist/core/issue-tracker-tools.js +850 -0
  43. package/dist/core/model-usage.d.ts +10 -0
  44. package/dist/core/model-usage.js +24 -0
  45. package/dist/core/reconciler.d.ts +8 -4
  46. package/dist/core/reconciler.js +15 -4
  47. package/dist/core/review-host-tools.js +10 -3
  48. package/dist/core/run-pricing.d.ts +61 -0
  49. package/dist/core/run-pricing.js +56 -0
  50. package/dist/core/runner.d.ts +5 -2
  51. package/dist/core/runner.js +178 -27
  52. package/dist/core/scheduler.d.ts +4 -1
  53. package/dist/core/scheduler.js +3 -2
  54. package/dist/core/self-defects.d.ts +80 -0
  55. package/dist/core/self-defects.js +180 -0
  56. package/dist/core/tool-names.js +3 -0
  57. package/dist/core/webhooks.d.ts +9 -1
  58. package/dist/core/webhooks.js +19 -1
  59. package/dist/env.js +6 -1
  60. package/dist/generated/prisma/browser.d.ts +66 -0
  61. package/dist/generated/prisma/client.d.ts +66 -0
  62. package/dist/generated/prisma/commonInputTypes.d.ts +122 -52
  63. package/dist/generated/prisma/enums.d.ts +7 -0
  64. package/dist/generated/prisma/enums.js +6 -0
  65. package/dist/generated/prisma/internal/class.d.ts +99 -0
  66. package/dist/generated/prisma/internal/class.js +4 -4
  67. package/dist/generated/prisma/internal/prismaNamespace.d.ts +826 -1
  68. package/dist/generated/prisma/internal/prismaNamespace.js +135 -2
  69. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +142 -0
  70. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +135 -2
  71. package/dist/generated/prisma/models/Agent.d.ts +389 -1
  72. package/dist/generated/prisma/models/AgentIssueProject.d.ts +1838 -0
  73. package/dist/generated/prisma/models/AgentIssueProject.js +1 -0
  74. package/dist/generated/prisma/models/AgentRepository.d.ts +1 -1
  75. package/dist/generated/prisma/models/AuthUser.d.ts +1 -1
  76. package/dist/generated/prisma/models/CodingProxySession.d.ts +73 -1
  77. package/dist/generated/prisma/models/CodingRun.d.ts +130 -1
  78. package/dist/generated/prisma/models/CodingRunServiceStatus.d.ts +1404 -0
  79. package/dist/generated/prisma/models/CodingRunServiceStatus.js +1 -0
  80. package/dist/generated/prisma/models/IssueFingerprint.d.ts +1183 -0
  81. package/dist/generated/prisma/models/IssueFingerprint.js +1 -0
  82. package/dist/generated/prisma/models/IssuePullRequest.d.ts +1255 -0
  83. package/dist/generated/prisma/models/IssuePullRequest.js +1 -0
  84. package/dist/generated/prisma/models/ModelCatalogEntry.d.ts +1322 -0
  85. package/dist/generated/prisma/models/ModelCatalogEntry.js +1 -0
  86. package/dist/generated/prisma/models/Run.d.ts +933 -1
  87. package/dist/generated/prisma/models/RunAttribution.d.ts +1259 -0
  88. package/dist/generated/prisma/models/RunAttribution.js +1 -0
  89. package/dist/generated/prisma/models/RunIssueStatus.d.ts +1199 -0
  90. package/dist/generated/prisma/models/RunIssueStatus.js +1 -0
  91. package/dist/generated/prisma/models/RunModelUsage.d.ts +1316 -0
  92. package/dist/generated/prisma/models/RunModelUsage.js +1 -0
  93. package/dist/generated/prisma/models/WorkItem.d.ts +1408 -0
  94. package/dist/generated/prisma/models/WorkItem.js +1 -0
  95. package/dist/generated/prisma/models.d.ts +9 -0
  96. package/dist/help-index.json +355 -16
  97. package/dist/import/neutral-schema.d.ts +16 -16
  98. package/dist/knowledge/check.d.ts +13 -0
  99. package/dist/knowledge/check.js +69 -0
  100. package/dist/knowledge/cli.d.ts +14 -0
  101. package/dist/knowledge/cli.js +67 -0
  102. package/dist/knowledge/concept.d.ts +54 -0
  103. package/dist/knowledge/concept.js +78 -0
  104. package/dist/knowledge/note.d.ts +11 -0
  105. package/dist/knowledge/note.js +39 -0
  106. package/dist/knowledge/relevance.d.ts +11 -0
  107. package/dist/knowledge/relevance.js +14 -0
  108. package/dist/knowledge/span-hash.d.ts +3 -0
  109. package/dist/knowledge/span-hash.js +16 -0
  110. package/dist/mcp/auth/access.d.ts +4 -2
  111. package/dist/mcp/auth/ownership.d.ts +9 -9
  112. package/dist/mcp/auth/resource-server.d.ts +3 -1
  113. package/dist/mcp/auth/resource-server.js +18 -3
  114. package/dist/mcp/auth/self-hosted/credentials.d.ts +3 -3
  115. package/dist/mcp/auth/self-hosted/session.d.ts +5 -5
  116. package/dist/mcp/context.d.ts +3 -0
  117. package/dist/mcp/host-events/deliveries.d.ts +9 -0
  118. package/dist/mcp/host-events/deliveries.js +17 -0
  119. package/dist/mcp/host-events/github-ingress.d.ts +4 -2
  120. package/dist/mcp/host-events/github-ingress.js +4 -13
  121. package/dist/mcp/host-events/jira-ingress.d.ts +29 -0
  122. package/dist/mcp/host-events/jira-ingress.js +92 -0
  123. package/dist/mcp/index.d.ts +2 -0
  124. package/dist/mcp/index.js +87 -9
  125. package/dist/mcp/server.js +5 -2
  126. package/dist/mcp/tools/agents.js +74 -3
  127. package/dist/mcp/tools/cost-report.d.ts +8 -0
  128. package/dist/mcp/tools/cost-report.js +60 -0
  129. package/dist/mcp/tools/issue-projects.d.ts +2 -0
  130. package/dist/mcp/tools/issue-projects.js +238 -0
  131. package/dist/mcp/tools/model-catalog.d.ts +22 -0
  132. package/dist/mcp/tools/model-catalog.js +423 -0
  133. package/dist/mcp/tools/repositories.js +2 -1
  134. package/dist/mcp/tools/tools.d.ts +2 -2
  135. package/dist/mcp/tools/trigger.js +33 -5
  136. package/dist/mcp/transport/streamable-http.d.ts +5 -0
  137. package/dist/mcp/transport/streamable-http.js +23 -1
  138. package/dist/mcp/webhooks/ingress.d.ts +2 -1
  139. package/dist/mcp/webhooks/ingress.js +9 -2
  140. package/dist/providers/auth/self-hosted.d.ts +8 -1
  141. package/dist/providers/auth/self-hosted.js +39 -2
  142. package/dist/providers/coding-proxy/memory-ledger.d.ts +1 -1
  143. package/dist/providers/coding-proxy/memory-ledger.js +10 -1
  144. package/dist/providers/coding-proxy/metering.d.ts +2 -1
  145. package/dist/providers/coding-proxy/metering.js +13 -2
  146. package/dist/providers/coding-proxy/prisma-ledger.js +59 -6
  147. package/dist/providers/coding-proxy/proxy.d.ts +12 -2
  148. package/dist/providers/coding-proxy/proxy.js +92 -30
  149. package/dist/providers/coding-proxy/types.d.ts +18 -1
  150. package/dist/providers/coding-proxy/types.js +12 -1
  151. package/dist/providers/engine/types.d.ts +19 -0
  152. package/dist/providers/executor/composition.js +9 -1
  153. package/dist/providers/executor/container.d.ts +30 -2
  154. package/dist/providers/executor/container.js +98 -17
  155. package/dist/providers/executor/dbos.d.ts +2 -0
  156. package/dist/providers/executor/dbos.js +7 -5
  157. package/dist/providers/executor/routing.d.ts +6 -0
  158. package/dist/providers/executor/routing.js +5 -0
  159. package/dist/providers/executor/types.d.ts +12 -0
  160. package/dist/providers/issue-tracker/adf.d.ts +31 -0
  161. package/dist/providers/issue-tracker/adf.js +181 -0
  162. package/dist/providers/issue-tracker/index.d.ts +5 -0
  163. package/dist/providers/issue-tracker/index.js +12 -0
  164. package/dist/providers/issue-tracker/jira-client.d.ts +41 -0
  165. package/dist/providers/issue-tracker/jira-client.js +151 -0
  166. package/dist/providers/issue-tracker/jira-events.d.ts +3 -0
  167. package/dist/providers/issue-tracker/jira-events.js +98 -0
  168. package/dist/providers/issue-tracker/jira.d.ts +116 -0
  169. package/dist/providers/issue-tracker/jira.js +502 -0
  170. package/dist/providers/issue-tracker/types.d.ts +269 -0
  171. package/dist/providers/issue-tracker/types.js +16 -0
  172. package/dist/providers/jobs/docker.d.ts +5 -1
  173. package/dist/providers/jobs/docker.js +61 -33
  174. package/dist/providers/jobs/kubernetes.d.ts +3 -0
  175. package/dist/providers/jobs/kubernetes.js +44 -4
  176. package/dist/providers/jobs/service-state.d.ts +22 -0
  177. package/dist/providers/jobs/service-state.js +17 -0
  178. package/dist/providers/llm/anthropic.d.ts +3 -3
  179. package/dist/providers/llm/anthropic.js +3 -9
  180. package/dist/providers/llm/bedrock.d.ts +3 -3
  181. package/dist/providers/llm/bedrock.js +3 -9
  182. package/dist/providers/llm/catalog-lookup.d.ts +10 -0
  183. package/dist/providers/llm/catalog-lookup.js +15 -0
  184. package/dist/providers/llm/catalog-shipped.d.ts +18 -0
  185. package/dist/providers/llm/catalog-shipped.js +197 -0
  186. package/dist/providers/llm/catalog-store.d.ts +58 -0
  187. package/dist/providers/llm/catalog-store.js +138 -0
  188. package/dist/providers/llm/catalog-types.d.ts +66 -0
  189. package/dist/providers/llm/catalog-types.js +64 -0
  190. package/dist/providers/llm/catalog.d.ts +61 -0
  191. package/dist/providers/llm/catalog.js +147 -0
  192. package/dist/providers/llm/claude-provider.d.ts +10 -14
  193. package/dist/providers/llm/claude-provider.js +11 -6
  194. package/dist/providers/llm/index.d.ts +9 -6
  195. package/dist/providers/llm/index.js +8 -5
  196. package/dist/providers/llm/openai.d.ts +14 -5
  197. package/dist/providers/llm/openai.js +24 -14
  198. package/dist/providers/llm/pricing-core.d.ts +5 -3
  199. package/dist/providers/llm/registration.js +8 -12
  200. package/dist/providers/llm/routing.d.ts +18 -17
  201. package/dist/providers/llm/routing.js +40 -24
  202. package/dist/providers/review-host/github-events.js +47 -1
  203. package/dist/providers/review-host/github.js +7 -6
  204. package/dist/providers/review-host/types.d.ts +25 -0
  205. package/dist/providers/vcs/git.js +2 -22
  206. package/dist/providers/vcs/github.d.ts +20 -0
  207. package/dist/providers/vcs/github.js +28 -2
  208. package/dist/providers/vcs/types.d.ts +6 -0
  209. package/dist/quickstart/index.d.ts +8 -0
  210. package/dist/quickstart/index.js +34 -34
  211. package/dist/serve.js +8 -2
  212. package/dist/viewer/api-schema.d.ts +2757 -0
  213. package/dist/viewer/api-schema.js +165 -0
  214. package/dist/viewer/build-schemas.d.ts +2 -0
  215. package/dist/viewer/build-schemas.js +18 -0
  216. package/dist/viewer/event-bus.d.ts +38 -0
  217. package/dist/viewer/event-bus.js +232 -0
  218. package/dist/viewer/graph.d.ts +40 -0
  219. package/dist/viewer/graph.js +243 -0
  220. package/dist/viewer/http.d.ts +30 -0
  221. package/dist/viewer/http.js +133 -0
  222. package/dist/viewer/run-detail.d.ts +4 -0
  223. package/dist/viewer/run-detail.js +61 -0
  224. package/dist/wardby-bin.js +5 -0
  225. package/docs/README.md +10 -0
  226. package/docs/agent-recipes.md +383 -0
  227. package/docs/code-review-agents.md +29 -2
  228. package/docs/coding-agent-setup.md +3 -0
  229. package/docs/coding-worker-isolation.md +39 -5
  230. package/docs/getting-started-gke.md +28 -11
  231. package/docs/getting-started-identity-provider.md +49 -38
  232. package/docs/getting-started.md +14 -0
  233. package/docs/jira-agents.md +649 -0
  234. package/docs/knowledge.md +387 -0
  235. package/docs/models.md +221 -0
  236. package/docs/security-deployment.md +19 -9
  237. package/docs/viewer-api.md +142 -0
  238. package/help/admin-viewer.md +39 -0
  239. package/help/agent-recipes.md +173 -0
  240. package/help/architecture-agent.md +189 -0
  241. package/help/builder-agent.md +80 -0
  242. package/help/code-review-agents.md +6 -0
  243. package/help/cost-attribution.md +67 -0
  244. package/help/creating-agents.md +22 -0
  245. package/help/deploy-gke.md +6 -0
  246. package/help/errors/model-unavailable.md +63 -0
  247. package/help/getting-started.md +1 -0
  248. package/help/github.md +18 -0
  249. package/help/identity-and-access.md +8 -3
  250. package/help/jira.md +135 -0
  251. package/help/knowledge.md +47 -0
  252. package/help/models.md +90 -0
  253. package/help/operating-agents.md +7 -1
  254. package/help/troubleshooting/budgets.md +6 -0
  255. package/package.json +5 -2
  256. package/prisma/migrations/20260930000000_jira_issue_projects/migration.sql +34 -0
  257. package/prisma/migrations/20261001000000_jira_phase2_allowlists/migration.sql +3 -0
  258. package/prisma/migrations/20261001010000_jira_link_types_allowlist/migration.sql +2 -0
  259. package/prisma/migrations/20261002000000_jira_coding_bridge/migration.sql +28 -0
  260. package/prisma/migrations/20261002010000_jira_issue_creation/migration.sql +25 -0
  261. package/prisma/migrations/20261003000000_issue_cost_attribution/migration.sql +56 -0
  262. package/prisma/migrations/20261003010000_coding_run_service_status/migration.sql +23 -0
  263. package/prisma/migrations/20261003020000_viewer_notify/migration.sql +54 -0
  264. package/prisma/migrations/20261003030000_viewer_notify_fixes/migration.sql +47 -0
  265. package/prisma/migrations/20261003040000_viewer_indexes/migration.sql +12 -0
  266. package/prisma/migrations/20261004000000_model_catalog/migration.sql +26 -0
  267. package/prisma/schema.prisma +258 -2
  268. package/dist/mcp/tools/models.d.ts +0 -8
  269. package/dist/mcp/tools/models.js +0 -15
  270. package/dist/providers/llm/pricing-anthropic.d.ts +0 -14
  271. package/dist/providers/llm/pricing-anthropic.js +0 -48
  272. package/dist/providers/llm/pricing-bedrock-claude.d.ts +0 -20
  273. package/dist/providers/llm/pricing-bedrock-claude.js +0 -46
  274. package/dist/providers/llm/pricing.d.ts +0 -30
  275. package/dist/providers/llm/pricing.js +0 -74
@@ -1,3 +1,4 @@
1
+ import { CODING_CODE_PROVIDER } from "../coding/protocol.js";
1
2
  import { storedServiceNames } from "../coding/services/catalog.js";
2
3
  import { SERVICE_UNREADY_CATEGORY, serviceRefusalSentence, serviceUnreadySentence, } from "../coding/services/wording.js";
3
4
  import { PROTECTED_PATH_CATEGORY, PROTECTED_PATH_HOST_LINE } from "../coding/protected-path-wording.js";
@@ -8,7 +9,7 @@ const log = logger.child({ module: "host-status" });
8
9
  /** Keeps the edited comment readable; the full reply stays on the run. */
9
10
  const MAX_REPLY_CHARS = 2000;
10
11
  const TERMINAL = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
11
- const runLine = (runId) => `<sub>wardby run \`${runId}\`</sub>`;
12
+ export const runLine = (runId) => `<sub>wardby run \`${runId}\`</sub>`;
12
13
  export function workingBody(runId) {
13
14
  return `👀 Working on it.\n\n${runLine(runId)}`;
14
15
  }
@@ -33,12 +34,18 @@ function pullRequestOutcome(result) {
33
34
  return null;
34
35
  if (!Number.isInteger(r.pullRequestNumber) || r.pullRequestNumber <= 0)
35
36
  return null;
36
- return { outcome: r.outcome, repository: r.repository, pullRequestNumber: r.pullRequestNumber };
37
+ return {
38
+ outcome: r.outcome,
39
+ repository: r.repository,
40
+ pullRequestNumber: r.pullRequestNumber,
41
+ codeProvider: CODING_CODE_PROVIDER,
42
+ ...(typeof r.pullRequestUrl === "string" ? { pullRequestUrl: r.pullRequestUrl } : {}),
43
+ };
37
44
  }
38
45
  /** The agent's reply as a quote, cut to MAX_REPLY_CHARS, with @-mentions defused so nobody is pinged. */
46
+ const cutReply = (text) => text.length > MAX_REPLY_CHARS ? `${text.slice(0, MAX_REPLY_CHARS)}…` : text;
39
47
  function quoteReply(text) {
40
- const cut = text.length > MAX_REPLY_CHARS ? `${text.slice(0, MAX_REPLY_CHARS)}…` : text;
41
- return cut
48
+ return cutReply(text)
42
49
  .replace(/@(?=[\w-])/g, "@​")
43
50
  .split("\n")
44
51
  .map((line) => `> ${line}`)
@@ -106,7 +113,41 @@ export function outcomeBody(run, repository, pullRequests, failedChildren = [],
106
113
  }
107
114
  if (links.length > 0)
108
115
  return `✅ ${links.join(", ")}.\n\n${footer}`;
109
- return `✅ Finished without opening a pull request.${quoted}\n\n${footer}`;
116
+ if (opts.replyAsAnswer && reply)
117
+ return `✅ ${cutReply(reply)}\n\n${footer}`;
118
+ return `✅ ${opts.noPullRequestText ?? "Finished without opening a pull request."}${quoted}\n\n${footer}`;
119
+ }
120
+ /** The parts of a finished run's outcome a status comment reports; shared by GitHub and Jira status comments. */
121
+ export async function collectRunOutcome(db, run) {
122
+ const children = await db.run.findMany({
123
+ where: { parentRunId: run.id },
124
+ select: {
125
+ id: true,
126
+ status: true,
127
+ error: true,
128
+ codingRun: { select: { result: true, failureCategory: true, services: true } },
129
+ },
130
+ orderBy: { startedAt: "asc" },
131
+ });
132
+ const pullRequests = children
133
+ .map((c) => {
134
+ const pr = pullRequestOutcome(c.codingRun?.result);
135
+ return pr ? { ...pr, runId: c.id } : null;
136
+ })
137
+ .filter((pr) => pr !== null);
138
+ const failedChildren = children
139
+ .filter((c) => TERMINAL.has(c.status) && c.status !== "succeeded")
140
+ .map((c) => ({
141
+ id: c.id,
142
+ status: c.status,
143
+ failureCategory: c.codingRun?.failureCategory ?? null,
144
+ error: c.error ?? null,
145
+ services: storedServiceNames(c.codingRun?.services),
146
+ }));
147
+ const budgetSentence = run.status === "budget_exhausted" || run.status === "refused"
148
+ ? await loadBudgetSentence(db, run.id, run.status)
149
+ : undefined;
150
+ return { pullRequests, failedChildren, ...(budgetSentence ? { budgetSentence } : {}) };
110
151
  }
111
152
  /**
112
153
  * Writes a finished run's outcome to its status comment and marks it
@@ -128,31 +169,7 @@ export async function completeHostStatus(db, run, hosts, opts = {}) {
128
169
  const host = hosts[status.provider];
129
170
  if (!host)
130
171
  return;
131
- const children = await db.run.findMany({
132
- where: { parentRunId: run.id },
133
- select: {
134
- id: true,
135
- status: true,
136
- error: true,
137
- codingRun: { select: { result: true, failureCategory: true, services: true } },
138
- },
139
- orderBy: { startedAt: "asc" },
140
- });
141
- const pullRequests = children
142
- .map((c) => pullRequestOutcome(c.codingRun?.result))
143
- .filter((pr) => pr !== null);
144
- const failedChildren = children
145
- .filter((c) => TERMINAL.has(c.status) && c.status !== "succeeded")
146
- .map((c) => ({
147
- id: c.id,
148
- status: c.status,
149
- failureCategory: c.codingRun?.failureCategory ?? null,
150
- error: c.error ?? null,
151
- services: storedServiceNames(c.codingRun?.services),
152
- }));
153
- const budgetSentence = run.status === "budget_exhausted" || run.status === "refused"
154
- ? await loadBudgetSentence(db, run.id, run.status)
155
- : undefined;
172
+ const { pullRequests, failedChildren, budgetSentence } = await collectRunOutcome(db, run);
156
173
  const body = outcomeBody(run, status.repository, pullRequests, failedChildren, { budgetSentence });
157
174
  let commentId = status.commentId;
158
175
  if (commentId) {
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Links a run's pull requests to the issue that started it: records each
3
+ * issue/PR pair, puts a web link on the issue, and applies the link's
4
+ * configured status move. Provider-neutral: a code host is only ever
5
+ * { provider, repository, number }. The issue key comes from the run tree
6
+ * (RunIssueStatus), never from model output. The status move is a control-plane
7
+ * action (not gated by allowedTransitions). Nothing here throws, except
8
+ * handlePullRequestClosed's lookup before it has claimed anything (so the
9
+ * webhook delivery is rolled back and redelivered).
10
+ */
11
+ import { type PrismaClient } from "#prisma";
12
+ import { type IssueTracker, type IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
13
+ export type IssueBridgeDb = Pick<PrismaClient, "issuePullRequest">;
14
+ export type PullRequestClosedDb = Pick<PrismaClient, "issuePullRequest" | "agentIssueProject">;
15
+ export interface BridgedPullRequest {
16
+ codeProvider: string;
17
+ repository: string;
18
+ number: number;
19
+ url: string;
20
+ openedByRunId: string;
21
+ outcome: "pull_request_opened" | "pull_request_updated";
22
+ }
23
+ export interface RecordPullRequestsInput {
24
+ issueKey: string;
25
+ issueProvider: string;
26
+ agentId: string;
27
+ /** The status the link moves the issue to when a pull request is opened, if configured. */
28
+ onPullRequestOpened?: string | null;
29
+ pullRequests: BridgedPullRequest[];
30
+ }
31
+ export declare const pullRequestGlobalId: (pr: Pick<BridgedPullRequest, "codeProvider" | "repository" | "number">) => string;
32
+ /**
33
+ * Returns sentences for the outcome comment (a refused status move); empty
34
+ * when there is nothing to say. Safe to repeat (a retried completion): each
35
+ * pair's side effects happen once — the status move only when this call newly
36
+ * recorded an opened pull request, and a merged/closed pair is never re-linked
37
+ * (re-posting its web link would un-resolve it). An unreadable store skips
38
+ * both rather than risk repeating them.
39
+ */
40
+ export declare function recordPullRequests(db: IssueBridgeDb, tracker: IssueTracker, input: RecordPullRequestsInput): Promise<{
41
+ notes: string[];
42
+ }>;
43
+ export interface ClosedPullRequest {
44
+ codeProvider: string;
45
+ repository: string;
46
+ number: number;
47
+ merged: boolean;
48
+ }
49
+ /**
50
+ * A pull request wardby recorded against an issue was merged or closed: marks
51
+ * each still-open pair, and, where the agent's link to the issue's project is
52
+ * still write, comments on the issue (and on merge resolves the web link and
53
+ * applies the link's onPullRequestMerged move). Only rows wardby wrote are
54
+ * touched, matched by the host's own identity for the PR — nothing from the
55
+ * PR's text. The row is claimed first (open → merged/closed), so a duplicate
56
+ * or concurrent delivery finds nothing to do. Throws only when the lookup of
57
+ * linked rows fails — before anything is claimed — so the caller's webhook
58
+ * delivery is rolled back and redelivered; never throws after that.
59
+ */
60
+ export declare function handlePullRequestClosed(db: PullRequestClosedDb, trackers: IssueTrackerRegistry | undefined, pr: ClosedPullRequest): Promise<void>;
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Links a run's pull requests to the issue that started it: records each
3
+ * issue/PR pair, puts a web link on the issue, and applies the link's
4
+ * configured status move. Provider-neutral: a code host is only ever
5
+ * { provider, repository, number }. The issue key comes from the run tree
6
+ * (RunIssueStatus), never from model output. The status move is a control-plane
7
+ * action (not gated by allowedTransitions). Nothing here throws, except
8
+ * handlePullRequestClosed's lookup before it has claimed anything (so the
9
+ * webhook delivery is rolled back and redelivered).
10
+ */
11
+ import { Prisma } from "#prisma";
12
+ import { projectOf, } from "../providers/issue-tracker/types.js";
13
+ import { logger } from "./logger.js";
14
+ const log = logger.child({ module: "issue-bridge" });
15
+ export const pullRequestGlobalId = (pr) => `wardby:pr:${pr.codeProvider}:${pr.repository}#${pr.number}`;
16
+ /**
17
+ * Makes sure the issue/PR pair has a row, without ever overwriting one: a new
18
+ * row is "created"; an existing open one gets its url refreshed ("open"); a
19
+ * merged or closed one is left alone ("settled"). A lost create race (P2002)
20
+ * reads back the winner's row. "unknown" when the store could not be read.
21
+ */
22
+ async function ensurePair(db, input, pr) {
23
+ const where = {
24
+ codeProvider_repository_number_issueProvider_issueKey: {
25
+ codeProvider: pr.codeProvider,
26
+ repository: pr.repository,
27
+ number: pr.number,
28
+ issueProvider: input.issueProvider,
29
+ issueKey: input.issueKey,
30
+ },
31
+ };
32
+ try {
33
+ let existing = await db.issuePullRequest.findUnique({ where, select: { state: true } });
34
+ if (!existing) {
35
+ try {
36
+ await db.issuePullRequest.create({
37
+ data: {
38
+ issueProvider: input.issueProvider,
39
+ issueKey: input.issueKey,
40
+ codeProvider: pr.codeProvider,
41
+ repository: pr.repository,
42
+ number: pr.number,
43
+ url: pr.url,
44
+ agentId: input.agentId,
45
+ openedByRunId: pr.openedByRunId,
46
+ },
47
+ });
48
+ return "created";
49
+ }
50
+ catch (err) {
51
+ if (!(err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002"))
52
+ throw err;
53
+ existing = await db.issuePullRequest.findUnique({ where, select: { state: true } });
54
+ if (!existing)
55
+ return "unknown";
56
+ }
57
+ }
58
+ if (existing.state !== "open")
59
+ return "settled";
60
+ await db.issuePullRequest.update({ where, data: { url: pr.url } });
61
+ return "open";
62
+ }
63
+ catch (err) {
64
+ log.warn({ err, issueKey: input.issueKey, repository: pr.repository, number: pr.number }, "could not record the issue/PR pair");
65
+ return "unknown";
66
+ }
67
+ }
68
+ /**
69
+ * Returns sentences for the outcome comment (a refused status move); empty
70
+ * when there is nothing to say. Safe to repeat (a retried completion): each
71
+ * pair's side effects happen once — the status move only when this call newly
72
+ * recorded an opened pull request, and a merged/closed pair is never re-linked
73
+ * (re-posting its web link would un-resolve it). An unreadable store skips
74
+ * both rather than risk repeating them.
75
+ */
76
+ export async function recordPullRequests(db, tracker, input) {
77
+ const notes = [];
78
+ try {
79
+ let newlyOpened = false;
80
+ for (const pr of input.pullRequests) {
81
+ const state = await ensurePair(db, input, pr);
82
+ if (state === "created" && pr.outcome === "pull_request_opened")
83
+ newlyOpened = true;
84
+ if (state !== "created" && state !== "open")
85
+ continue;
86
+ try {
87
+ await tracker.addRemoteLink(input.issueKey, {
88
+ globalId: pullRequestGlobalId(pr),
89
+ url: pr.url,
90
+ title: `${pr.repository}#${pr.number}`,
91
+ });
92
+ }
93
+ catch (err) {
94
+ log.warn({ err, issueKey: input.issueKey, repository: pr.repository, number: pr.number }, "could not link the pull request on the issue");
95
+ }
96
+ }
97
+ const target = input.onPullRequestOpened;
98
+ if (target && newlyOpened) {
99
+ try {
100
+ await tracker.transitionTo(input.issueKey, target);
101
+ }
102
+ catch (err) {
103
+ log.warn({ err, issueKey: input.issueKey, target }, "could not move the issue after a pull request opened");
104
+ notes.push(`Could not move ${input.issueKey} to "${target}"; it may need a manual move.`);
105
+ }
106
+ }
107
+ }
108
+ catch (err) {
109
+ log.warn({ err, issueKey: input.issueKey }, "could not bridge the pull requests to the issue");
110
+ }
111
+ return { notes };
112
+ }
113
+ /**
114
+ * A pull request wardby recorded against an issue was merged or closed: marks
115
+ * each still-open pair, and, where the agent's link to the issue's project is
116
+ * still write, comments on the issue (and on merge resolves the web link and
117
+ * applies the link's onPullRequestMerged move). Only rows wardby wrote are
118
+ * touched, matched by the host's own identity for the PR — nothing from the
119
+ * PR's text. The row is claimed first (open → merged/closed), so a duplicate
120
+ * or concurrent delivery finds nothing to do. Throws only when the lookup of
121
+ * linked rows fails — before anything is claimed — so the caller's webhook
122
+ * delivery is rolled back and redelivered; never throws after that.
123
+ */
124
+ export async function handlePullRequestClosed(db, trackers, pr) {
125
+ if (!trackers)
126
+ return;
127
+ const where = { codeProvider: pr.codeProvider, repository: pr.repository, number: pr.number };
128
+ // Not caught: nothing is claimed yet, and a throw rolls back the delivery so it is redelivered.
129
+ const rows = await db.issuePullRequest.findMany({ where: { ...where, state: "open" } });
130
+ for (const row of rows) {
131
+ try {
132
+ const claimed = await db.issuePullRequest.updateMany({
133
+ where: { id: row.id, state: "open" },
134
+ data: { state: pr.merged ? "merged" : "closed" },
135
+ });
136
+ if (claimed.count === 0)
137
+ continue;
138
+ const tracker = trackers[row.issueProvider];
139
+ if (!tracker)
140
+ continue;
141
+ const link = await db.agentIssueProject.findUnique({
142
+ where: {
143
+ agentId_provider_projectKey: {
144
+ agentId: row.agentId,
145
+ provider: row.issueProvider,
146
+ projectKey: projectOf(row.issueKey),
147
+ },
148
+ },
149
+ select: { access: true, commentVisibilityRole: true, onPullRequestMerged: true },
150
+ });
151
+ if (link?.access !== "write")
152
+ continue;
153
+ const ref = `[${row.repository}#${row.number}](${row.url})`;
154
+ let markdown;
155
+ if (pr.merged) {
156
+ await tracker
157
+ .addRemoteLink(row.issueKey, {
158
+ globalId: pullRequestGlobalId(row),
159
+ url: row.url,
160
+ title: `${row.repository}#${row.number}`,
161
+ status: { resolved: true },
162
+ })
163
+ .catch((err) => log.warn({ err, issueKey: row.issueKey, ...where }, "could not resolve the pull request's web link"));
164
+ markdown = `✅ Pull request ${ref} was merged.`;
165
+ const target = link.onPullRequestMerged;
166
+ if (target) {
167
+ try {
168
+ const moved = await tracker.transitionTo(row.issueKey, target);
169
+ markdown += ` Moved to ${moved.toStatus}.`;
170
+ }
171
+ catch (err) {
172
+ log.warn({ err, issueKey: row.issueKey, target }, "could not move the issue after its pull request merged");
173
+ markdown += ` Could not move ${row.issueKey} to "${target}"; it may need a manual move.`;
174
+ }
175
+ }
176
+ }
177
+ else {
178
+ markdown = `Pull request ${ref} was closed without merging.`;
179
+ }
180
+ await tracker.comment(row.issueKey, {
181
+ markdown,
182
+ ...(link.commentVisibilityRole ? { visibilityRole: link.commentVisibilityRole } : {}),
183
+ });
184
+ }
185
+ catch (err) {
186
+ log.warn({ err, issueKey: row.issueKey, ...where }, "could not bridge the closed pull request to the issue");
187
+ }
188
+ }
189
+ }
@@ -0,0 +1,70 @@
1
+ import type { PrismaClient } from "#prisma";
2
+ import { type CreateIssueInput, type IssueTracker, type TrackerCallOptions } from "../providers/issue-tracker/types.js";
3
+ export declare const FINGERPRINT_MAX_LENGTH = 200;
4
+ /**
5
+ * Each tracker call inside the lock: a whole-call cap, and a 429 fails at once rather than sleeping. Jira
6
+ * Cloud calls through the api.atlassian.com gateway routinely take several seconds each, and one tracker
7
+ * call can be a few requests (a create resolves the issue type and its fields first), so the cap is generous.
8
+ */
9
+ export declare const TRACKER_CALL_OPTIONS: Readonly<TrackerCallOptions>;
10
+ /** How long a sighting waits for another sighting of the same fingerprint before answering "busy". */
11
+ export declare const DEDUPE_LOCK_TIMEOUT_MS = 5000;
12
+ /**
13
+ * The critical section is the lock wait (5 s) plus at most two bounded tracker calls (getIssue, then
14
+ * createIssue or comment: 2 x 30 s) plus a few row reads/writes; 90 s leaves ample margin so a create is
15
+ * never in flight when the transaction (and with it the advisory lock) is torn down. The cost: each
16
+ * sighting in progress holds one pool connection for up to this long, and waiters hold one for up to
17
+ * the lock timeout (callers waiting for a free connection give up after maxWait).
18
+ */
19
+ export declare const DEDUPE_TRANSACTION_TIMEOUT_MS = 90000;
20
+ export type IssueDedupeDb = Pick<PrismaClient, "$transaction">;
21
+ export interface IssueDedupeLink {
22
+ provider: string;
23
+ projectKey: string;
24
+ access: string;
25
+ commentVisibilityRole?: string | null;
26
+ }
27
+ export interface FileIssueInput {
28
+ agentId: string;
29
+ runId?: string | null;
30
+ link: IssueDedupeLink;
31
+ tracker: IssueTracker;
32
+ fingerprint?: string | null;
33
+ /** Everything but the project, which comes from the link. */
34
+ create: Omit<CreateIssueInput, "projectKey">;
35
+ /** Appended after the description (and after any "Regression of" line), so it stays the last line. */
36
+ footerMarkdown?: string;
37
+ /** Posted on the existing issue when the fingerprint is seen again. */
38
+ seenAgainMarkdown: string;
39
+ /** false: only a seen-again update may happen; a create or regression returns issue_cap_reached. Default true. */
40
+ createAllowed?: boolean;
41
+ }
42
+ export type FileIssueOutcome = "created" | "seen_again" | "regression";
43
+ export type FileIssueSuccess = {
44
+ outcome: FileIssueOutcome;
45
+ issueKey: string;
46
+ url: string;
47
+ seenCount: number;
48
+ };
49
+ export type FileIssueResult = FileIssueSuccess | {
50
+ error: string;
51
+ message: string;
52
+ };
53
+ export declare const BUSY_RESULT: {
54
+ readonly error: "busy";
55
+ readonly message: "another sighting of this fingerprint is being filed; try again";
56
+ };
57
+ /**
58
+ * The part of a fingerprint that identifies it: letters and digits only, lower-cased (Unicode-normalised),
59
+ * so a model's small slips in case, punctuation or spacing ("checkout-api" vs "checkoutapi") still match.
60
+ */
61
+ export declare function normalizeFingerprint(fingerprint: string): string;
62
+ export declare function fingerprintHash(fingerprint: string): string;
63
+ /** A stable signed 64-bit advisory-lock key for one (provider, project, hash). */
64
+ export declare function dedupeLockKey(provider: string, projectKey: string, hash: string): bigint;
65
+ export declare function fileIssue(deps: {
66
+ db: IssueDedupeDb;
67
+ lockTimeoutMs?: number;
68
+ }, input: FileIssueInput): Promise<FileIssueResult>;
69
+ /** Postgres lock_not_available (55P03), as raised when lock_timeout expires, however the driver wraps it. */
70
+ export declare function isLockTimeout(err: unknown): boolean;