@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
@@ -40,13 +40,15 @@ function firstUserMessage(untrustedContext) {
40
40
  `${wrapUntrusted(UNTRUSTED_CONTEXT_TAG, untrustedContext)}\n\nBegin.`);
41
41
  }
42
42
  function zeroUsage() {
43
- return { tokensIn: 0, tokensOut: 0, costUsd: 0 };
43
+ return { tokensIn: 0, tokensOut: 0, costUsd: 0, cachedInputTokens: 0, cacheWriteTokens: 0 };
44
44
  }
45
45
  function addUsage(a, b) {
46
46
  return {
47
47
  tokensIn: a.tokensIn + b.inputTokens,
48
48
  tokensOut: a.tokensOut + b.outputTokens,
49
49
  costUsd: a.costUsd + b.costUsd,
50
+ cachedInputTokens: a.cachedInputTokens + b.cachedInputTokens,
51
+ cacheWriteTokens: a.cacheWriteTokens + b.cacheWriteTokens,
50
52
  };
51
53
  }
52
54
  export class NativeEngine {
@@ -102,6 +104,10 @@ export class NativeEngine {
102
104
  return this.finish("failed", lastText, turns, addUsage(cumulative, turn.usage), turn.error);
103
105
  }
104
106
  cumulative = addUsage(cumulative, turn.usage);
107
+ await ctx.onProgress?.({
108
+ turns,
109
+ usage: { tokensIn: cumulative.tokensIn, tokensOut: cumulative.tokensOut, costUsd: cumulative.costUsd },
110
+ });
105
111
  lastCacheRatio = turn.usage.inputTokens > 0 ? turn.usage.cachedInputTokens / turn.usage.inputTokens : 0;
106
112
  if (turn.budgetExceededMidStream) {
107
113
  return this.windDown(ctx, messages, cumulative, turns, turn.text || lastText);
@@ -219,7 +225,7 @@ export class NativeEngine {
219
225
  }
220
226
  catch (err) {
221
227
  return {
222
- usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, costUsd: 0 },
228
+ usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0, costUsd: 0 },
223
229
  text,
224
230
  toolCalls,
225
231
  budgetExceededMidStream: false,
@@ -233,7 +239,13 @@ export class NativeEngine {
233
239
  cachedInputTokens: estimatedCachedInput,
234
240
  });
235
241
  return {
236
- usage: { inputTokens, outputTokens: outputTokensSoFar, cachedInputTokens: estimatedCachedInput, costUsd },
242
+ usage: {
243
+ inputTokens,
244
+ outputTokens: outputTokensSoFar,
245
+ cachedInputTokens: estimatedCachedInput,
246
+ cacheWriteTokens: 0,
247
+ costUsd,
248
+ },
237
249
  text,
238
250
  toolCalls,
239
251
  budgetExceededMidStream: true,
@@ -241,7 +253,7 @@ export class NativeEngine {
241
253
  }
242
254
  if (!usage) {
243
255
  return {
244
- usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, costUsd: 0 },
256
+ usage: { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, cacheWriteTokens: 0, costUsd: 0 },
245
257
  text,
246
258
  toolCalls,
247
259
  budgetExceededMidStream: false,
@@ -266,6 +278,7 @@ export class NativeEngine {
266
278
  inputTokens: usage.inputTokens,
267
279
  outputTokens: usage.outputTokens,
268
280
  cachedInputTokens: usage.cachedInputTokens ?? 0,
281
+ cacheWriteTokens: usage.cacheWriteTokens ?? 0,
269
282
  costUsd: usage.costUsd,
270
283
  },
271
284
  text,
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Converts a glob pattern (with *, **, and ? support) to a regular expression.
3
+ * - `*` matches any characters except `/` (one path segment)
4
+ * - `**` matches any characters including `/` (any depth)
5
+ * - `?` matches any single character except `/`
6
+ * - Other regex metacharacters are escaped
7
+ */
8
+ export declare function globRegex(pattern: string): RegExp;
9
+ /** Whether a repo-relative POSIX path matches a protectedPaths-style glob (*, **, ?). */
10
+ export declare function matchesGlob(path: string, pattern: string): boolean;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Converts a glob pattern (with *, **, and ? support) to a regular expression.
3
+ * - `*` matches any characters except `/` (one path segment)
4
+ * - `**` matches any characters including `/` (any depth)
5
+ * - `?` matches any single character except `/`
6
+ * - Other regex metacharacters are escaped
7
+ */
8
+ export function globRegex(pattern) {
9
+ let source = "^";
10
+ for (let index = 0; index < pattern.length; index += 1) {
11
+ const character = pattern[index];
12
+ if (character === "*") {
13
+ if (pattern[index + 1] === "*") {
14
+ source += ".*";
15
+ index += 1;
16
+ }
17
+ else {
18
+ source += "[^/]*";
19
+ }
20
+ }
21
+ else if (character === "?") {
22
+ source += "[^/]";
23
+ }
24
+ else {
25
+ source += character.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
26
+ }
27
+ }
28
+ return new RegExp(`${source}$`);
29
+ }
30
+ /** Whether a repo-relative POSIX path matches a protectedPaths-style glob (*, **, ?). */
31
+ export function matchesGlob(path, pattern) {
32
+ return globRegex(pattern).test(path);
33
+ }
@@ -7,10 +7,12 @@
7
7
  */
8
8
  import type { PrismaClient } from "#prisma";
9
9
  import type { Executor } from "../providers/executor/types.js";
10
+ import type { IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
10
11
  import type { HostEvent, ReviewHostRegistry } from "../providers/review-host/types.js";
11
12
  import { type RepoAccessGate } from "./repo-access.js";
13
+ import { type DriftScope } from "../knowledge/relevance.js";
12
14
  export type { HostEvent };
13
- export type HostEventDb = Pick<PrismaClient, "agentRepository" | "agent" | "run" | "runHostCheck" | "runHostStatus" | "codingRun" | "task" | "webhook" | "budgetGroup" | "$transaction" | "$queryRaw">;
15
+ export type HostEventDb = Pick<PrismaClient, "agentRepository" | "agent" | "run" | "runHostCheck" | "runHostStatus" | "codingRun" | "task" | "webhook" | "budgetGroup" | "issuePullRequest" | "workItem" | "runAttribution" | "agentIssueProject" | "$transaction" | "$queryRaw">;
14
16
  export interface RouteHostEventDeps {
15
17
  db: HostEventDb;
16
18
  executor: Executor;
@@ -19,6 +21,8 @@ export interface RouteHostEventDeps {
19
21
  mentionHandle: string;
20
22
  /** Re-checks each link's authorization, and a mention author's own permission, before anything runs. */
21
23
  repoAccess: RepoAccessGate;
24
+ /** Issue trackers for moving/commenting on the issue a closed PR was opened for; absent → pr_closed is ignored. */
25
+ issueTrackers?: IssueTrackerRegistry;
22
26
  }
23
27
  export interface RouteResult {
24
28
  runIds: string[];
@@ -26,6 +30,24 @@ export interface RouteResult {
26
30
  followUps: Array<() => Promise<void>>;
27
31
  }
28
32
  export declare function isReviewCommand(body: string, mentionHandle: string): boolean;
33
+ type PushEvent = Extract<HostEvent, {
34
+ kind: "push";
35
+ }>;
36
+ type ConceptCitationStatus = {
37
+ state: "verified";
38
+ } | {
39
+ state: "stale";
40
+ spans: string[];
41
+ } | {
42
+ state: "unverified";
43
+ };
44
+ /**
45
+ * The merge watcher's task text. The task holds only trusted facts (repository,
46
+ * branch, short shas, a fixed instruction); the changed paths came from pushed
47
+ * commits, so they and the affected concept paths travel as untrusted context.
48
+ * Commit messages and author names are never included.
49
+ */
50
+ export declare function mergeTaskText(event: PushEvent, scope: DriftScope | null, bundleComplete?: boolean, citations?: ReadonlyMap<string, ConceptCitationStatus>): string;
29
51
  type MentionEvent = Extract<HostEvent, {
30
52
  kind: "mention";
31
53
  }>;
@@ -46,4 +68,5 @@ type MentionEvent = Extract<HostEvent, {
46
68
  * taskOverride column.
47
69
  */
48
70
  export declare function mentionTaskText(event: MentionEvent): string;
71
+ export declare function unknownPriorRunBody(priorRunId: string): string;
49
72
  export declare function routeHostEvent(event: HostEvent, deps: RouteHostEventDeps): Promise<RouteResult>;
@@ -1,8 +1,13 @@
1
- import { dispatchRun } from "./dispatch.js";
1
+ import { linkedPullRequestAttribution, RESPONSE_PATH_SNAPSHOT_BUDGET } from "./attribution.js";
2
+ import { checkContinuation, dispatchRun } from "./dispatch.js";
2
3
  import { mentionStatusRow, postMentionStatus } from "./host-status.js";
4
+ import { handlePullRequestClosed } from "./issue-bridge.js";
3
5
  import { logger } from "./logger.js";
4
6
  import { requiredLevel } from "./repo-access.js";
5
7
  import { composeTaskOverride } from "./untrusted-content.js";
8
+ import { DEFAULT_KNOWLEDGE_BUNDLE_PATH, parseConcept, RESERVED_BUNDLE_FILES, } from "../knowledge/concept.js";
9
+ import { driftScope } from "../knowledge/relevance.js";
10
+ import { spanHash } from "../knowledge/span-hash.js";
6
11
  const log = logger.child({ module: "host-events" });
7
12
  const MAX_TASK_BODY = 8000;
8
13
  const MAX_TITLE = 256;
@@ -13,6 +18,221 @@ function escapeRegExp(value) {
13
18
  export function isReviewCommand(body, mentionHandle) {
14
19
  return new RegExp(`(^|[^\\w-])@${escapeRegExp(mentionHandle)}\\s+review(?![\\w-])`, "i").test(body);
15
20
  }
21
+ const MAX_BUNDLE_FILES = 200;
22
+ const BUNDLE_FILE_LINES = 2000;
23
+ const MAX_LISTED_CHANGES = 200;
24
+ /** The whole bundle load (listing plus reads) must finish inside GitHub's ~10 s webhook window. */
25
+ const BUNDLE_LOAD_DEADLINE_MS = 4000;
26
+ const BUNDLE_READ_CONCURRENCY = 8;
27
+ /** Most lines read from one cited file; a citation reaching past it cannot be verified. */
28
+ const CITATION_READ_MAX_LINES = 5000;
29
+ const BUNDLE_INCOMPLETE_TEXT = "The knowledge bundle could not be fully read; treat every knowledge concept as possibly affected.";
30
+ /**
31
+ * The merge watcher's task text. The task holds only trusted facts (repository,
32
+ * branch, short shas, a fixed instruction); the changed paths came from pushed
33
+ * commits, so they and the affected concept paths travel as untrusted context.
34
+ * Commit messages and author names are never included.
35
+ */
36
+ export function mergeTaskText(event, scope, bundleComplete = true, citations = new Map()) {
37
+ const statusOf = (concept) => citations.get(concept) ?? { state: "unverified" };
38
+ const changed = event.changedPathsComplete
39
+ ? `Changed files:\n${[
40
+ ...event.changedPaths.slice(0, MAX_LISTED_CHANGES).map((p) => `- ${p}`),
41
+ ...(event.changedPaths.length > MAX_LISTED_CHANGES
42
+ ? [`… and ${event.changedPaths.length - MAX_LISTED_CHANGES} more changed files`]
43
+ : []),
44
+ ].join("\n")}`
45
+ : "The changed-file list is incomplete (GitHub includes at most 2048 commits per push and the list is capped at 1000 paths); treat every knowledge concept as possibly affected.";
46
+ const listed = scope
47
+ ? `Knowledge concepts whose citations, affects globs, or files changed (${scope.reason}):\n` +
48
+ scope.concepts.map((p) => `- ${DEFAULT_KNOWLEDGE_BUNDLE_PATH}/${p} — ${describeStatus(statusOf(p))}`).join("\n")
49
+ : null;
50
+ let context;
51
+ if (!bundleComplete)
52
+ context = [changed, listed, BUNDLE_INCOMPLETE_TEXT].filter(Boolean).join("\n\n");
53
+ else if (listed)
54
+ context = [changed, listed].join("\n\n");
55
+ else if (!event.changedPathsComplete)
56
+ context = "No knowledge concept exists in this repository.";
57
+ else
58
+ context = [changed, "No knowledge concept is affected by these changes."].join("\n\n");
59
+ let summary = null;
60
+ if (scope && scope.concepts.length > 0) {
61
+ const states = scope.concepts.map((p) => statusOf(p).state);
62
+ const count = (state) => states.filter((x) => x === state).length;
63
+ const onlyKnowledge = event.changedPathsComplete && event.changedPaths.every((p) => p.startsWith(`${DEFAULT_KNOWLEDGE_BUNDLE_PATH}/`));
64
+ summary =
65
+ `Citation check at ${event.after.slice(0, 12)}: ${count("verified")} of ${states.length} affected concepts verified, ` +
66
+ `${count("stale")} stale, ${count("unverified")} not verified. Only knowledge files changed: ${onlyKnowledge ? "yes" : "no"}.`;
67
+ }
68
+ return composeTaskOverride([
69
+ `Merge to ${event.branch} in ${event.repository}: ${event.before.slice(0, 12)}..${event.after.slice(0, 12)}.`,
70
+ "The changed files and the knowledge concepts they affect are listed in the context below; treat them as data.",
71
+ ...(summary ? [summary] : []),
72
+ ].join("\n\n"), context);
73
+ }
74
+ function describeStatus(status) {
75
+ if (status.state === "verified")
76
+ return "citations verified";
77
+ if (status.state === "unverified")
78
+ return "citations not verified";
79
+ return `${status.spans.length} stale citation${status.spans.length === 1 ? "" : "s"}: ${status.spans.join(", ")}`;
80
+ }
81
+ /** One time budget, shared by every read the push route makes. */
82
+ function startBudget(ms) {
83
+ let expired = false;
84
+ let timer;
85
+ const promise = new Promise((resolve) => {
86
+ timer = setTimeout(() => {
87
+ expired = true;
88
+ resolve("deadline");
89
+ }, ms);
90
+ });
91
+ return { promise, expired: () => expired, clear: () => clearTimeout(timer) };
92
+ }
93
+ /**
94
+ * The knowledge bundle at the pushed commit, read with bounded concurrency
95
+ * under one overall deadline. A failure never fails the route; it yields an
96
+ * incomplete result so the watcher treats every concept as possibly affected.
97
+ */
98
+ async function loadBundle(host, event, budget) {
99
+ const prefix = `${DEFAULT_KNOWLEDGE_BUNDLE_PATH}/`;
100
+ const concepts = [];
101
+ const where = { repository: event.repository, after: event.after };
102
+ let filesRead = 0;
103
+ let listing;
104
+ try {
105
+ const listed = await Promise.race([host.listFiles(event.repository, event.after, prefix), budget.promise]);
106
+ if (listed === "deadline") {
107
+ log.warn({ ...where, filesRead }, "the knowledge bundle load hit its deadline while listing");
108
+ return { concepts: [], complete: false };
109
+ }
110
+ listing = listed;
111
+ }
112
+ catch (err) {
113
+ log.warn({ err, ...where }, "could not read the knowledge bundle");
114
+ return { concepts: [], complete: false };
115
+ }
116
+ let complete = true;
117
+ if (listing.truncated) {
118
+ complete = false;
119
+ log.warn(where, "the knowledge bundle listing is truncated; continuing with the files listed");
120
+ }
121
+ const matching = listing.paths.filter((f) => f.startsWith(prefix) && f.endsWith(".md") && !RESERVED_BUNDLE_FILES.has(f.split("/").pop() ?? f));
122
+ const files = matching.slice(0, MAX_BUNDLE_FILES);
123
+ if (matching.length > files.length) {
124
+ complete = false;
125
+ log.warn({ ...where, listed: matching.length, cap: MAX_BUNDLE_FILES }, "the knowledge bundle exceeds the file cap");
126
+ }
127
+ let skipped = 0;
128
+ let lastError;
129
+ let next = 0;
130
+ const worker = async () => {
131
+ while (!budget.expired() && next < files.length) {
132
+ const file = files[next++];
133
+ try {
134
+ const read = await host.readFile(event.repository, file, event.after, {
135
+ startLine: 1,
136
+ maxLines: BUNDLE_FILE_LINES,
137
+ });
138
+ if (budget.expired())
139
+ return;
140
+ filesRead += 1;
141
+ if (read.kind !== "file" || read.truncated) {
142
+ skipped += 1;
143
+ continue;
144
+ }
145
+ const parsed = parseConcept(file.slice(prefix.length), read.text);
146
+ if (parsed.ok)
147
+ concepts.push(parsed.concept);
148
+ else {
149
+ // A concept that cannot be parsed might be one the push affects: do not claim the bundle was fully read.
150
+ skipped += 1;
151
+ lastError = new Error(parsed.error);
152
+ }
153
+ }
154
+ catch (err) {
155
+ skipped += 1;
156
+ lastError = err;
157
+ }
158
+ }
159
+ };
160
+ const workers = Promise.all(Array.from({ length: Math.min(BUNDLE_READ_CONCURRENCY, files.length) }, worker));
161
+ const outcome = await Promise.race([workers, budget.promise]);
162
+ if (outcome === "deadline") {
163
+ log.warn({ ...where, filesRead }, "the knowledge bundle load hit its deadline; continuing with the files read");
164
+ return { concepts: [...concepts], complete: false };
165
+ }
166
+ if (skipped > 0) {
167
+ complete = false;
168
+ log.warn({ err: lastError, ...where, skipped }, "skipped knowledge files that could not be read");
169
+ }
170
+ return { concepts, complete };
171
+ }
172
+ /**
173
+ * Whether each concept's citations still match the code at the pushed commit.
174
+ * Each distinct cited path is read once (raw text, up to the largest cited end
175
+ * line), with bounded concurrency, inside the budget the bundle load shares.
176
+ * Anything not checked by the deadline, or that cannot be read, is
177
+ * "unverified"; this never throws.
178
+ */
179
+ async function verifyCitations(host, event, concepts, budget) {
180
+ const needed = new Map();
181
+ for (const concept of concepts)
182
+ for (const c of concept.citations) {
183
+ const lines = c.lines ? Math.min(c.lines[1], CITATION_READ_MAX_LINES) : CITATION_READ_MAX_LINES;
184
+ needed.set(c.path, Math.max(needed.get(c.path) ?? 0, lines));
185
+ }
186
+ const fetched = new Map();
187
+ const paths = [...needed.keys()];
188
+ let next = 0;
189
+ const worker = async () => {
190
+ while (!budget.expired() && next < paths.length) {
191
+ const path = paths[next++];
192
+ try {
193
+ const read = await host.readFile(event.repository, path, event.after, {
194
+ startLine: 1,
195
+ maxLines: needed.get(path) ?? CITATION_READ_MAX_LINES,
196
+ });
197
+ if (budget.expired())
198
+ return;
199
+ if (read.kind === "file")
200
+ fetched.set(path, { text: read.text, truncated: read.truncated });
201
+ }
202
+ catch (err) {
203
+ log.warn({ err, repository: event.repository, after: event.after, path }, "could not read a cited file");
204
+ }
205
+ }
206
+ };
207
+ try {
208
+ const workers = Promise.all(Array.from({ length: Math.min(BUNDLE_READ_CONCURRENCY, paths.length) }, worker));
209
+ if ((await Promise.race([workers, budget.promise])) === "deadline") {
210
+ log.warn({ repository: event.repository, after: event.after, read: fetched.size, of: paths.length }, "the citation check hit its deadline; the remaining concepts are unverified");
211
+ }
212
+ }
213
+ catch (err) {
214
+ log.warn({ err, repository: event.repository, after: event.after }, "the citation check failed");
215
+ }
216
+ const statuses = new Map();
217
+ for (const concept of concepts) {
218
+ const spans = [];
219
+ let unverified = concept.citations.length === 0;
220
+ for (const c of concept.citations) {
221
+ const file = fetched.get(c.path);
222
+ const beyondRead = c.lines ? c.lines[1] > CITATION_READ_MAX_LINES : true;
223
+ // A truncated read is only usable for a cited span that fits inside the window read.
224
+ if (!file || (file.truncated && beyondRead)) {
225
+ unverified = true;
226
+ continue;
227
+ }
228
+ if (spanHash(file.text, c.lines) !== c.spanHash) {
229
+ spans.push(c.lines ? `${c.path}#L${c.lines[0]}-L${c.lines[1]}` : c.path);
230
+ }
231
+ }
232
+ statuses.set(concept.path, spans.length > 0 ? { state: "stale", spans } : unverified ? { state: "unverified" } : { state: "verified" });
233
+ }
234
+ return statuses;
235
+ }
16
236
  /**
17
237
  * The mention agent's task text. Deterministic so a prompt can parse it.
18
238
  *
@@ -63,6 +283,23 @@ export function mentionTaskText(event) {
63
283
  }
64
284
  return composeTaskOverride(sections.join("\n\n"), context.length > 0 ? context.join("\n\n") : undefined);
65
285
  }
286
+ /**
287
+ * Whether the run the PR's marker names is one this deployment can continue on
288
+ * this PR: known here, in this repository, and the one that opened this PR.
289
+ * The marker is trusted only on a PR the App authored, but every deployment
290
+ * sharing the App authors such PRs, and the id is only a hint until checked.
291
+ */
292
+ async function priorRunContinues(deps, event) {
293
+ if (!event.priorRunId)
294
+ return false;
295
+ const check = await checkContinuation(deps.db, event.priorRunId, event.repository);
296
+ return check.ok && check.root.pullRequestNumber === event.number;
297
+ }
298
+ export function unknownPriorRunBody(priorRunId) {
299
+ return (`❌ I can't continue this pull request. Its description names wardby run \`${priorRunId}\`, but this wardby ` +
300
+ "deployment has no record of that run opening this pull request (another wardby deployment may have opened it). " +
301
+ "Ask the deployment that opened it, or make the change on this branch by hand.");
302
+ }
66
303
  /** Run states that already cover a commit: a review of it is under way or done. */
67
304
  const COVERING_REVIEW_STATUSES = ["pending", "running", "succeeded"];
68
305
  /**
@@ -89,6 +326,10 @@ async function startReviews(deps, host, repository, prNumber, headSha, targets,
89
326
  /** True for automatic triggers; an explicit re-run or `@wardby review` always runs. */
90
327
  skipReviewedCommits = false) {
91
328
  const runIds = [];
329
+ // One lookup per PR, not per reviewer, and none at all when every reviewer
330
+ // skips this commit: taken at the first actual dispatch.
331
+ let attribution;
332
+ const linkedAttribution = () => (attribution ??= linkedPullRequestAttribution(deps.db, deps.issueTrackers, { codeProvider: host.provider, repository, number: prNumber }, RESPONSE_PATH_SNAPSHOT_BUDGET));
92
333
  for (const target of targets) {
93
334
  if (skipReviewedCommits && (await alreadyReviewed(deps, repository, prNumber, headSha, target.agentId))) {
94
335
  log.info({ repository, prNumber, agentId: target.agentId, headSha }, "review skipped: this commit was already reviewed");
@@ -106,9 +347,11 @@ skipReviewedCommits = false) {
106
347
  const dispatched = await dispatchRun({
107
348
  db: deps.db,
108
349
  executor: deps.executor,
350
+ selfDefects: { db: deps.db, issueTrackers: deps.issueTrackers },
109
351
  agentId: target.agentId,
110
352
  trigger: "host_event",
111
353
  taskOverride: `Review pull request #${prNumber} in ${repository} (head ${headSha}).`,
354
+ attribution: await linkedAttribution(),
112
355
  afterPersist: checkId
113
356
  ? async (tx, run) => {
114
357
  await tx.runHostCheck.create({
@@ -165,6 +408,16 @@ async function authorizedLinks(deps, event, links) {
165
408
  const reviewTargets = (links) => links.map((l) => ({ agentId: l.agentId, checkName: l.checkName }));
166
409
  export async function routeHostEvent(event, deps) {
167
410
  const none = { runIds: [], followUps: [] };
411
+ if (event.kind === "pr_closed") {
412
+ // Bookkeeping only: no agent runs, so no repository link is consulted.
413
+ await handlePullRequestClosed(deps.db, deps.issueTrackers, {
414
+ codeProvider: event.provider,
415
+ repository: event.repository,
416
+ number: event.prNumber,
417
+ merged: event.merged,
418
+ });
419
+ return none;
420
+ }
168
421
  const host = deps.hosts[event.provider];
169
422
  if (!host)
170
423
  return none;
@@ -174,6 +427,71 @@ export async function routeHostEvent(event, deps) {
174
427
  }));
175
428
  const reviewerLinks = links.filter((l) => l.access === "write" && l.triggers.includes("pull_request") && l.checkName);
176
429
  switch (event.kind) {
430
+ case "push": {
431
+ const watchers = await authorizedLinks(deps, event, links.filter((l) => l.access === "write" && l.triggers.includes("push")));
432
+ if (watchers.length === 0)
433
+ return none;
434
+ // Loading the bundle is the slow part of the response path; skip it when
435
+ // no dispatch can happen. The per-dispatch guard below stays authoritative.
436
+ const busy = new Set((await deps.db.run.findMany({
437
+ where: { agentId: { in: watchers.map((w) => w.agentId) }, status: { in: ["pending", "running"] } },
438
+ select: { agentId: true },
439
+ })).map((r) => r.agentId));
440
+ if (watchers.every((w) => busy.has(w.agentId))) {
441
+ log.info({ repository: event.repository, after: event.after }, "push skipped: every watcher has a run in flight");
442
+ return none;
443
+ }
444
+ const budget = startBudget(BUNDLE_LOAD_DEADLINE_MS);
445
+ let taskOverride;
446
+ try {
447
+ const { concepts, complete } = await loadBundle(host, event, budget);
448
+ const scope = driftScope({
449
+ changedPaths: event.changedPaths,
450
+ changedPathsComplete: event.changedPathsComplete,
451
+ bundlePath: DEFAULT_KNOWLEDGE_BUNDLE_PATH,
452
+ concepts,
453
+ });
454
+ // Only the concepts the merge affects are checked, and only against a bundle that was fully read.
455
+ const affected = new Set(scope?.concepts);
456
+ const citations = complete
457
+ ? await verifyCitations(host, event, concepts.filter((c) => affected.has(c.path)), budget)
458
+ : new Map();
459
+ taskOverride = mergeTaskText(event, scope, complete, citations);
460
+ }
461
+ finally {
462
+ budget.clear();
463
+ }
464
+ const runIds = [];
465
+ for (const watcher of watchers) {
466
+ let dispatched;
467
+ try {
468
+ dispatched = await dispatchRun({
469
+ db: deps.db,
470
+ executor: deps.executor,
471
+ selfDefects: { db: deps.db, issueTrackers: deps.issueTrackers },
472
+ agentId: watcher.agentId,
473
+ trigger: "host_event",
474
+ taskOverride,
475
+ lockAgent: true,
476
+ // D3: one merge run at a time per watcher; a push during a run is skipped.
477
+ beforePersist: async (tx, agent) => !(await tx.run.findFirst({
478
+ where: { agentId: agent.id, status: { in: ["pending", "running"] } },
479
+ select: { id: true },
480
+ })),
481
+ });
482
+ }
483
+ catch (err) {
484
+ log.warn({ err, agentId: watcher.agentId, repository: event.repository, after: event.after }, "merge watcher run could not be dispatched");
485
+ continue;
486
+ }
487
+ if (dispatched)
488
+ runIds.push(dispatched.run.id);
489
+ else {
490
+ log.info({ repository: event.repository, after: event.after, agentId: watcher.agentId }, "push skipped: the watcher has a run in flight");
491
+ }
492
+ }
493
+ return { runIds, followUps: [] };
494
+ }
177
495
  case "pr_updated": {
178
496
  if (event.isFork || reviewerLinks.length === 0)
179
497
  return none;
@@ -234,12 +552,34 @@ export async function routeHostEvent(event, deps) {
234
552
  followUps: [react],
235
553
  };
236
554
  }
555
+ if (event.priorRunId && !(await priorRunContinues(deps, event))) {
556
+ // The PR's marker names a run this deployment cannot continue (most
557
+ // often one another deployment sharing the App opened). A run would
558
+ // only fail, or open a second PR, so say why instead of starting one.
559
+ log.info({ repository: event.repository, number: event.number, priorRunId: event.priorRunId }, "mention refused: the PR's wardby run cannot be continued here");
560
+ const reply = async () => {
561
+ await host
562
+ .comment(event.repository, {
563
+ number: event.number,
564
+ body: unknownPriorRunBody(event.priorRunId ?? ""),
565
+ ...(event.replyToReviewCommentId ? { replyToReviewCommentId: event.replyToReviewCommentId } : {}),
566
+ })
567
+ .catch((err) => {
568
+ log.warn({ err, repository: event.repository, number: event.number }, "could not post the refusal");
569
+ });
570
+ };
571
+ return { runIds: [], followUps: [react, reply] };
572
+ }
237
573
  const dispatched = await dispatchRun({
238
574
  db: deps.db,
239
575
  executor: deps.executor,
576
+ selfDefects: { db: deps.db, issueTrackers: deps.issueTrackers },
240
577
  agentId: allowed[0].agentId,
241
578
  trigger: "host_event",
242
579
  taskOverride: mentionTaskText(event),
580
+ attribution: event.isPullRequest
581
+ ? await linkedPullRequestAttribution(deps.db, deps.issueTrackers, { codeProvider: host.provider, repository: event.repository, number: event.number }, RESPONSE_PATH_SNAPSHOT_BUDGET)
582
+ : undefined,
243
583
  // Where to report, written with the run: even if this instance dies
244
584
  // before the follow-up below, the run's outcome still gets a comment.
245
585
  afterPersist: async (tx, run) => {
@@ -11,12 +11,18 @@
11
11
  import type { Prisma, PrismaClient, Run } from "#prisma";
12
12
  import type { CodeReviewHost, ReviewHostProvider, ReviewHostRegistry } from "../providers/review-host/types.js";
13
13
  export type HostStatusDb = Pick<PrismaClient, "runHostStatus" | "run">;
14
- type FinishedRun = Pick<Run, "id" | "status" | "finalText">;
14
+ export type FinishedRun = Pick<Run, "id" | "status" | "finalText">;
15
15
  /** The parts of a CodingRun result this comment uses. */
16
16
  export interface PullRequestOutcome {
17
17
  outcome: "pull_request_opened" | "pull_request_updated";
18
18
  repository: string;
19
19
  pullRequestNumber: number;
20
+ /** The result's own pull request URL; absent from a malformed result. */
21
+ pullRequestUrl?: string;
22
+ /** The code host the repository is on. */
23
+ codeProvider: string;
24
+ /** The coding sub-run that produced it; set by collectRunOutcome. */
25
+ runId?: string;
20
26
  }
21
27
  /** A sub-run that ended without succeeding. */
22
28
  export interface FailedChild {
@@ -29,6 +35,7 @@ export interface FailedChild {
29
35
  /** The service names on the coding sub-run (CodingRun.services), for one that never became ready. */
30
36
  services?: string[];
31
37
  }
38
+ export declare const runLine: (runId: string) => string;
32
39
  export declare function workingBody(runId: string): string;
33
40
  /** The status row for a mention run, created in the same transaction as the run. */
34
41
  export declare function mentionStatusRow(provider: ReviewHostProvider, event: {
@@ -38,7 +45,18 @@ export declare function mentionStatusRow(provider: ReviewHostProvider, event: {
38
45
  }, runId: string): Prisma.RunHostStatusUncheckedCreateInput;
39
46
  export declare function outcomeBody(run: FinishedRun, repository: string, pullRequests: PullRequestOutcome[], failedChildren?: FailedChild[], opts?: {
40
47
  budgetSentence?: string;
48
+ noPullRequestText?: string;
49
+ /** The agent's reply IS the answer (no separate reply comment): shown as written, not quoted. */
50
+ replyAsAnswer?: boolean;
41
51
  }): string;
52
+ /** What collectRunOutcome reads: a run's children and, when out of budget, its budget facts. */
53
+ export type RunOutcomeDb = Pick<PrismaClient, "run">;
54
+ /** The parts of a finished run's outcome a status comment reports; shared by GitHub and Jira status comments. */
55
+ export declare function collectRunOutcome(db: RunOutcomeDb, run: FinishedRun): Promise<{
56
+ pullRequests: PullRequestOutcome[];
57
+ failedChildren: FailedChild[];
58
+ budgetSentence?: string;
59
+ }>;
42
60
  /**
43
61
  * Writes a finished run's outcome to its status comment and marks it
44
62
  * complete: edits the comment when it exists; when it does not yet, posts the
@@ -58,4 +76,3 @@ export declare function completeHostStatus(db: HostStatusDb, run: FinishedRun, h
58
76
  * already complete. Never throws.
59
77
  */
60
78
  export declare function postMentionStatus(db: HostStatusDb, host: CodeReviewHost, runId: string, hosts: ReviewHostRegistry | undefined): Promise<void>;
61
- export {};