@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
@@ -9,8 +9,12 @@ import { Prisma } from "#prisma";
9
9
  import { z } from "zod";
10
10
  import { CodingProfilePatchSchema, CodingProfileSchema } from "../../coding/profile.js";
11
11
  import { codingProviderSupportsModel } from "../../coding/provider.js";
12
+ import { assertAgentModelAvailable } from "../../core/run-pricing.js";
13
+ import { ModelUnavailableError } from "../../providers/llm/catalog-types.js";
14
+ import { currentModelCatalog } from "../../providers/llm/catalog-store.js";
12
15
  import { validateCronExpression } from "../../core/cron.js";
13
16
  import { modelSupportedEfforts } from "../../providers/llm/routing.js";
17
+ import { PROJECT_KEY } from "../../providers/issue-tracker/types.js";
14
18
  import { LLM_EFFORT_LEVELS, isLlmEffort } from "../../providers/llm/types.js";
15
19
  import { requireReadableBudgetGroup } from "../auth/ownership.js";
16
20
  import { agentAccessResolver, agentNotFound, assertAgentAccess, readableAgentsWhere, requireAgentAccess, } from "../auth/access.js";
@@ -57,7 +61,23 @@ const agentFields = {
57
61
  budgetGroupId: z.string().min(1).max(128),
58
62
  memoryEnabled: z.boolean(),
59
63
  effort: z.enum(LLM_EFFORT_LEVELS),
64
+ defectProjectKey: z.string().regex(PROJECT_KEY, "must be an upper-case Jira project key"),
65
+ defectIssueType: z.string().trim().min(1).max(100),
60
66
  };
67
+ /** Self-defect config is both-or-neither; on update, both null clears it. */
68
+ function refineDefectPair(value, ctx) {
69
+ const key = value.defectProjectKey;
70
+ const type = value.defectIssueType;
71
+ if ((key === undefined) !== (type === undefined) || (key === null) !== (type === null)) {
72
+ ctx.addIssue({
73
+ code: z.ZodIssueCode.custom,
74
+ path: ["defectProjectKey"],
75
+ message: "defectProjectKey and defectIssueType must be set together (or both null to clear on update)",
76
+ });
77
+ }
78
+ }
79
+ const DEFECT_DESCRIPTION = "Self-defects (optional, both or neither with defectIssueType): the Jira project key wardby files this agent's own failures into. " +
80
+ "Filing needs a write link (link_issue_project) to that project whose creatableIssueTypes includes defectIssueType; it is checked at filing time and fails closed.";
61
81
  const CreateAgentSchema = z
62
82
  .object({
63
83
  name: agentFields.name,
@@ -72,11 +92,14 @@ const CreateAgentSchema = z
72
92
  budgetGroupId: agentFields.budgetGroupId.optional(),
73
93
  memoryEnabled: agentFields.memoryEnabled.default(false),
74
94
  effort: agentFields.effort.optional(),
95
+ defectProjectKey: agentFields.defectProjectKey.optional(),
96
+ defectIssueType: agentFields.defectIssueType.optional(),
75
97
  codingProfile: CodingProfileSchema.optional(),
76
98
  repositoryAdminOverride: z.boolean().optional(),
77
99
  })
78
100
  .strict()
79
101
  .superRefine((value, ctx) => {
102
+ refineDefectPair(value, ctx);
80
103
  if (value.kind === "coding" && !value.codingProfile) {
81
104
  ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["codingProfile"], message: "is required for coding agents" });
82
105
  }
@@ -87,8 +110,11 @@ const CreateAgentSchema = z
87
110
  message: "is only valid for coding agents",
88
111
  });
89
112
  }
113
+ // A model the catalog doesn't offer is left to the handler, which reports
114
+ // model_unavailable with its reason instead of a provider mismatch.
90
115
  if (value.kind === "coding" &&
91
116
  value.codingProfile &&
117
+ currentModelCatalog().get(value.model) !== undefined &&
92
118
  !codingProviderSupportsModel(value.codingProfile.provider, value.model)) {
93
119
  ctx.addIssue({
94
120
  code: z.ZodIssueCode.custom,
@@ -120,13 +146,16 @@ const UpdateAgentSchema = z
120
146
  budgetGroupId: agentFields.budgetGroupId.nullable().optional(),
121
147
  memoryEnabled: agentFields.memoryEnabled.optional(),
122
148
  effort: agentFields.effort.nullable().optional(),
149
+ defectProjectKey: agentFields.defectProjectKey.nullable().optional(),
150
+ defectIssueType: agentFields.defectIssueType.nullable().optional(),
123
151
  codingProfile: CodingProfilePatchSchema.extend({
124
152
  /** Not a stored profile field: sets CodingAgentProfile.debugTraceUntil = now + minutes (null clears). */
125
153
  debugTraceMinutes: z.number().int().min(1).max(MAX_DEBUG_TRACE_MINUTES).nullable().optional(),
126
154
  }).optional(),
127
155
  repositoryAdminOverride: z.boolean().optional(),
128
156
  })
129
- .strict();
157
+ .strict()
158
+ .superRefine(refineDefectPair);
130
159
  const REPOSITORY_ADMIN_OVERRIDE = {
131
160
  type: "boolean",
132
161
  description: "Admins only (agents:admin with the admin role): approve codingProfile.repository without checking GitHub access, recorded as an admin approval. On an agent the admin doesn't own, only codingProfile.repository may change.",
@@ -231,6 +260,25 @@ function validateEffort(kind, model, effort) {
231
260
  : "It accepts no effort setting; leave effort unset (or null)."));
232
261
  }
233
262
  }
263
+ /**
264
+ * A model being set (create, or update with `model`) must be runnable here:
265
+ * in the catalog and enabled; for a native agent, its provider must also be
266
+ * configured on the native router. A coding agent is checked against the
267
+ * catalog only: its runs go through the coding proxy, whose credentials
268
+ * (CODING_*_CREDENTIAL_REF) are not the native router's. An existing agent's
269
+ * unchanged model is not re-checked: if an admin later disabled it, the agent
270
+ * fails at its next run, not at an unrelated edit.
271
+ */
272
+ function requireModelAvailable(ctx, model, kind) {
273
+ try {
274
+ assertAgentModelAvailable(model, kind === "native" ? ctx.providers.llm : undefined);
275
+ }
276
+ catch (err) {
277
+ if (err instanceof ModelUnavailableError)
278
+ throw new McpError(400, err.message);
279
+ throw err;
280
+ }
281
+ }
234
282
  function storedProfile(profile) {
235
283
  return CodingProfileSchema.parse({
236
284
  provider: profile.provider,
@@ -284,6 +332,8 @@ export function registerAgentTools(mcp) {
284
332
  enum: [...LLM_EFFORT_LEVELS],
285
333
  description: "Reasoning effort for native agents. Unset uses the provider default.",
286
334
  },
335
+ defectProjectKey: { type: "string", pattern: PROJECT_KEY.source, description: DEFECT_DESCRIPTION },
336
+ defectIssueType: { type: "string", minLength: 1, maxLength: 100, description: DEFECT_DESCRIPTION },
287
337
  codingProfile: { ...profileJsonSchema, required: ["repository"] },
288
338
  repositoryAdminOverride: REPOSITORY_ADMIN_OVERRIDE,
289
339
  },
@@ -299,6 +349,7 @@ export function registerAgentTools(mcp) {
299
349
  Object.keys(packages.packagePolicy ?? {}).length > 0))
300
350
  requirePackageApproval(ctx);
301
351
  validateSchedule(args.schedule, args.timezone ?? "UTC");
352
+ requireModelAvailable(ctx, args.model, args.kind);
302
353
  validateEffort(args.kind, args.model, args.effort);
303
354
  if (args.codingProfile)
304
355
  await requireCatalogServiceNames(ctx.db, args.codingProfile.services);
@@ -356,6 +407,17 @@ export function registerAgentTools(mcp) {
356
407
  enum: [...LLM_EFFORT_LEVELS, null],
357
408
  description: "Reasoning effort for native agents. Null clears it back to the provider default.",
358
409
  },
410
+ defectProjectKey: {
411
+ type: ["string", "null"],
412
+ pattern: PROJECT_KEY.source,
413
+ description: `${DEFECT_DESCRIPTION} Null (together with defectIssueType) clears it.`,
414
+ },
415
+ defectIssueType: {
416
+ type: ["string", "null"],
417
+ minLength: 1,
418
+ maxLength: 100,
419
+ description: `${DEFECT_DESCRIPTION} Null (together with defectProjectKey) clears it.`,
420
+ },
359
421
  codingProfile: {
360
422
  ...profileJsonSchema,
361
423
  properties: {
@@ -474,7 +536,13 @@ export function registerAgentTools(mcp) {
474
536
  if (nextKind === "native" && args.codingProfile) {
475
537
  throw new McpError(400, "A coding profile is only valid for coding agents.");
476
538
  }
477
- validateEffort(nextKind, args.model ?? existing.model, args.effort !== undefined ? args.effort : existing.effort);
539
+ if (args.model !== undefined)
540
+ requireModelAvailable(ctx, args.model, nextKind);
541
+ // Only re-checked when the model, effort or kind changes: an agent whose
542
+ // model was since disabled can still take unrelated edits.
543
+ if (args.model !== undefined || args.effort !== undefined || nextKind !== existing.kind) {
544
+ validateEffort(nextKind, args.model ?? existing.model, args.effort !== undefined ? args.effort : existing.effort);
545
+ }
478
546
  let nextProfile = null;
479
547
  if (nextKind === "coding") {
480
548
  const attachedTools = await tx.agentTool.count({ where: { agentId: args.id } });
@@ -490,7 +558,10 @@ export function registerAgentTools(mcp) {
490
558
  if (args.codingProfile?.services !== undefined)
491
559
  await requireCatalogServiceNames(tx, nextProfile.services);
492
560
  const nextModel = args.model ?? existing.model;
493
- if (!codingProviderSupportsModel(nextProfile.provider, nextModel)) {
561
+ const pairingChanges = args.model !== undefined ||
562
+ existing.kind !== "coding" ||
563
+ existing.codingProfile?.provider !== nextProfile.provider;
564
+ if (pairingChanges && !codingProviderSupportsModel(nextProfile.provider, nextModel)) {
494
565
  throw new McpError(400, `Model "${nextModel}" is not supported by coding provider "${nextProfile.provider}".`);
495
566
  }
496
567
  }
@@ -0,0 +1,8 @@
1
+ import type { WardbyMcpServer } from "../server.js";
2
+ /**
3
+ * Cost rolled up by issue, parent (epic, as of each run's dispatch), scope
4
+ * (project/team), agent, model, or run. Same visibility as list_runs/get_run:
5
+ * the agents the caller owns (Agent.ownerId), plus runs the caller
6
+ * triggered. USD only; tokens by priced kind.
7
+ */
8
+ export declare function registerCostReportTools(mcp: WardbyMcpServer): void;
@@ -0,0 +1,60 @@
1
+ import { McpError } from "../errors.js";
2
+ import { agentAccessResolver, readableAgentsWhere } from "../auth/access.js";
3
+ import { costReport, CostReportInputError, parseCostReportQuery } from "../../core/cost-report.js";
4
+ import { textResult } from "./text-result.js";
5
+ /**
6
+ * Cost rolled up by issue, parent (epic, as of each run's dispatch), scope
7
+ * (project/team), agent, model, or run. Same visibility as list_runs/get_run:
8
+ * the agents the caller owns (Agent.ownerId), plus runs the caller
9
+ * triggered. USD only; tokens by priced kind.
10
+ */
11
+ export function registerCostReportTools(mcp) {
12
+ mcp.registerTool({
13
+ name: "cost_report",
14
+ scope: "agents:read",
15
+ description: "Agent spend (USD) and tokens by kind, grouped by issue, parent (epic), scope (project/team), agent, model, or run, " +
16
+ "over a time window (default: last 30 days). Combine filters with groupBy to drill down, e.g. groupBy=issue with parentKey. " +
17
+ "Totals always sum each run's full cost over attributed runs, whatever the grouping; with groupBy=model, rows come from " +
18
+ "per-model usage and can add up to less than the total when some runs have no per-model record. " +
19
+ "`unattributed` covers visible runs in the window that have no issue; only agentId narrows it " +
20
+ "(provider/scope/parent/issue filters do not).",
21
+ inputSchema: {
22
+ type: "object",
23
+ properties: {
24
+ groupBy: { type: "string", enum: ["issue", "parent", "scope", "agent", "model", "run"] },
25
+ from: { type: "string", description: "ISO date (inclusive)" },
26
+ to: { type: "string", description: "ISO date (exclusive)" },
27
+ provider: { type: "string" },
28
+ scopeKey: { type: "string" },
29
+ parentKey: { type: "string" },
30
+ issueKey: { type: "string" },
31
+ agentId: { type: "string" },
32
+ limit: { type: "number" },
33
+ },
34
+ },
35
+ handler: async (args, ctx) => {
36
+ let query;
37
+ try {
38
+ query = parseCostReportQuery(args);
39
+ }
40
+ catch (err) {
41
+ if (err instanceof CostReportInputError)
42
+ throw new McpError(400, err.message);
43
+ throw err;
44
+ }
45
+ let visibility = null;
46
+ if (!ctx.operator) {
47
+ const resolve = await agentAccessResolver(ctx);
48
+ const agents = await ctx.db.agent.findMany({
49
+ where: await readableAgentsWhere(ctx),
50
+ select: { id: true, ownerId: true },
51
+ });
52
+ visibility = {
53
+ ownedAgentIds: agents.filter((a) => resolve(a) === "owner").map((a) => a.id),
54
+ principalId: ctx.principal.id,
55
+ };
56
+ }
57
+ return textResult(await costReport(ctx.db, query, visibility));
58
+ },
59
+ });
60
+ }
@@ -0,0 +1,2 @@
1
+ import type { WardbyMcpServer } from "../server.js";
2
+ export declare function registerIssueProjectTools(mcp: WardbyMcpServer): void;
@@ -0,0 +1,238 @@
1
+ /**
2
+ * AgentIssueProject CRUD — which Jira projects a native agent may act on,
3
+ * and which issue events trigger it. Linking is admin-approved: wardby cannot
4
+ * verify an agent owner's own Jira access (the integration is one service
5
+ * account, no per-user OAuth), so link/unlink need agents:admin with the
6
+ * admin role, and the approver is stamped on the link. Triggers that carry
7
+ * text from a person (mention/assigned) are gated by trustedAccountIds.
8
+ * See docs/private/2026-09-30-jira-issue-tracker-design.md.
9
+ */
10
+ import { requireAgentAccess } from "../auth/access.js";
11
+ import { requireScope } from "../auth/resource-server.js";
12
+ import { McpError } from "../errors.js";
13
+ import { ISSUE_TRACKER_PROVIDERS, PROJECT_KEY } from "../../providers/issue-tracker/types.js";
14
+ import { textResult } from "./text-result.js";
15
+ const PROVIDERS = [...ISSUE_TRACKER_PROVIDERS];
16
+ const TRIGGERS = ["created", "transitioned", "labeled", "assigned", "mention"];
17
+ const ACCOUNT_ID = /^[A-Za-z0-9:_-]{1,128}$/;
18
+ const WRITABLE_FIELD = /^(labels|components|priority|customfield_\d{1,10})$/;
19
+ /** Trimmed names, 1-100 characters each, de-duplicated case-insensitively (first spelling kept). */
20
+ function nameList(field, raw) {
21
+ const seen = new Set();
22
+ const names = [];
23
+ for (const entry of raw ?? []) {
24
+ const name = entry.trim();
25
+ if (name.length === 0 || name.length > 100)
26
+ throw new McpError(400, `invalid ${field} entry (1-100 characters)`);
27
+ if (!seen.has(name.toLowerCase())) {
28
+ seen.add(name.toLowerCase());
29
+ names.push(name);
30
+ }
31
+ }
32
+ return names;
33
+ }
34
+ /** An optional single status name, trimmed, 1-100 characters; undefined/absent means none. */
35
+ function optionalStatus(field, raw) {
36
+ if (raw === undefined)
37
+ return null;
38
+ const name = raw.trim();
39
+ if (name.length === 0 || name.length > 100)
40
+ throw new McpError(400, `invalid ${field} (1-100 characters)`);
41
+ return name;
42
+ }
43
+ function normalizeProjectKey(projectKey) {
44
+ const key = projectKey.trim().toUpperCase();
45
+ if (!PROJECT_KEY.test(key))
46
+ throw new McpError(400, `invalid projectKey "${projectKey}"`);
47
+ return key;
48
+ }
49
+ export function registerIssueProjectTools(mcp) {
50
+ mcp.registerTool({
51
+ name: "link_issue_project",
52
+ scope: "agents:write",
53
+ description: "Links a native agent to a Jira project, with its access and issue-event triggers. A wardby admin approves Jira links " +
54
+ "(agents:admin with the admin role); the agent acts as the deployment's Jira service account, not as any person. " +
55
+ "triggers (write access only): created, transitioned (needs triggerStatuses), labeled (needs triggerLabels), assigned and " +
56
+ "mention (each needs trustedAccountIds). trustedAccountIds are Jira accountIds (from a Jira profile URL or jira_get_issue " +
57
+ "output) whose mentions/assignments may trigger the agent; triggered text from anyone else is treated as untrusted. " +
58
+ "jqlFilter optionally narrows which issues trigger; commentVisibilityRole restricts the agent's comments to a project role. " +
59
+ "Re-linking a project replaces its access, triggers, and filters (an omitted field is cleared, not kept) — always send " +
60
+ "the full desired state. " +
61
+ "allowedTransitions (write access only): the target status names jira_transition may move issues to (matched by target " +
62
+ "status name, case-insensitive). writableFields (write access only): the field ids jira_update_fields may change " +
63
+ "(labels, components, priority, or customfield_NNNNN). Both allowlists fail closed: empty or omitted means the agent " +
64
+ "cannot transition issues or edit fields at all. " +
65
+ 'allowedLinkTypes (write access only): the issue link type names jira_link_issues may create (e.g. "Duplicate", ' +
66
+ "case-insensitive, at most 20); linking also needs write access to both issues' projects, each allowlisting the type. " +
67
+ "It fails closed too: empty or omitted means the agent cannot link issues. Issue properties (jira_set_property) are " +
68
+ "not allowlisted: any write link may set them. " +
69
+ "onPullRequestOpened / onPullRequestMerged (write access only, optional): the status name to move the issue to when a " +
70
+ "pull request wardby opened for it opens or merges. These are control-plane moves, not model actions, and are not gated " +
71
+ "by allowedTransitions; the name is matched in the Jira service account's language, and omitting one means no move. " +
72
+ "creatableIssueTypes (write access only): the issue type names (case-insensitive, at most 20) the agent may create issues " +
73
+ "of in this project; it fails closed too: empty or omitted means the agent cannot create issues here. " +
74
+ "maxNewIssuesPerRun (write access only, optional integer 1-1000): the most issues one run may create in this project; " +
75
+ "omitted means no cap. The cap is best-effort across resumed or concurrent attempts of the same run.",
76
+ inputSchema: {
77
+ type: "object",
78
+ additionalProperties: false,
79
+ properties: {
80
+ agentId: { type: "string" },
81
+ provider: { type: "string", enum: PROVIDERS },
82
+ projectKey: { type: "string", minLength: 1, maxLength: 255 },
83
+ access: { type: "string", enum: ["read", "write"] },
84
+ triggers: { type: "array", items: { type: "string", enum: [...TRIGGERS] }, uniqueItems: true },
85
+ triggerStatuses: { type: "array", items: { type: "string", minLength: 1, maxLength: 100 }, maxItems: 50 },
86
+ triggerLabels: { type: "array", items: { type: "string", minLength: 1, maxLength: 255 }, maxItems: 50 },
87
+ jqlFilter: { type: "string", minLength: 1, maxLength: 1000 },
88
+ trustedAccountIds: {
89
+ type: "array",
90
+ items: { type: "string", pattern: ACCOUNT_ID.source },
91
+ maxItems: 50,
92
+ },
93
+ commentVisibilityRole: { type: "string", minLength: 1, maxLength: 100 },
94
+ allowedTransitions: { type: "array", items: { type: "string", minLength: 1, maxLength: 100 }, maxItems: 50 },
95
+ writableFields: { type: "array", items: { type: "string", pattern: WRITABLE_FIELD.source }, maxItems: 50 },
96
+ allowedLinkTypes: { type: "array", items: { type: "string", minLength: 1, maxLength: 100 }, maxItems: 20 },
97
+ onPullRequestOpened: { type: "string", minLength: 1, maxLength: 100 },
98
+ onPullRequestMerged: { type: "string", minLength: 1, maxLength: 100 },
99
+ creatableIssueTypes: { type: "array", items: { type: "string", minLength: 1, maxLength: 100 }, maxItems: 20 },
100
+ maxNewIssuesPerRun: { type: "integer", minimum: 1, maximum: 1000 },
101
+ },
102
+ required: ["agentId", "projectKey", "access"],
103
+ },
104
+ handler: async (args, ctx) => {
105
+ requireScope(ctx, ctx.canonicalUri, "agents:admin");
106
+ const agent = await ctx.db.agent.findUnique({ where: { id: args.agentId } });
107
+ if (!agent)
108
+ throw new McpError(404, `Agent "${args.agentId}" not found.`);
109
+ if (agent.kind !== "native")
110
+ throw new McpError(400, "Issue-project links are for native agents only.");
111
+ if (agent.ownerId === null) {
112
+ throw new McpError(400, `Agent "${agent.id}" has no owner; assign one with make_owner before linking.`);
113
+ }
114
+ const provider = args.provider ?? "jira";
115
+ const projectKey = normalizeProjectKey(args.projectKey);
116
+ const triggers = [...new Set(args.triggers ?? [])];
117
+ const triggerStatuses = args.triggerStatuses ?? [];
118
+ const triggerLabels = args.triggerLabels ?? [];
119
+ const trustedAccountIds = [...new Set(args.trustedAccountIds ?? [])];
120
+ if (trustedAccountIds.some((id) => !ACCOUNT_ID.test(id))) {
121
+ throw new McpError(400, "invalid trustedAccountIds entry (letters, digits, ':', '_', '-'; up to 128 characters)");
122
+ }
123
+ const allowedTransitions = nameList("allowedTransitions", args.allowedTransitions);
124
+ const allowedLinkTypes = nameList("allowedLinkTypes", args.allowedLinkTypes);
125
+ if (allowedLinkTypes.length > 20)
126
+ throw new McpError(400, "allowedLinkTypes takes at most 20 entries");
127
+ const creatableIssueTypes = nameList("creatableIssueTypes", args.creatableIssueTypes);
128
+ if (creatableIssueTypes.length > 20)
129
+ throw new McpError(400, "creatableIssueTypes takes at most 20 entries");
130
+ const maxNewIssuesPerRun = args.maxNewIssuesPerRun ?? null;
131
+ if (maxNewIssuesPerRun !== null &&
132
+ (!Number.isInteger(maxNewIssuesPerRun) || maxNewIssuesPerRun < 1 || maxNewIssuesPerRun > 1000)) {
133
+ throw new McpError(400, "maxNewIssuesPerRun must be an integer from 1 to 1000");
134
+ }
135
+ if ((creatableIssueTypes.length > 0 || maxNewIssuesPerRun !== null) && args.access !== "write") {
136
+ throw new McpError(400, "creatableIssueTypes and maxNewIssuesPerRun need write access.");
137
+ }
138
+ const writableFields = [...new Set(args.writableFields ?? [])];
139
+ if (writableFields.some((f) => !WRITABLE_FIELD.test(f))) {
140
+ throw new McpError(400, "invalid writableFields entry (labels, components, priority, or customfield_NNNNN)");
141
+ }
142
+ const onPullRequestOpened = optionalStatus("onPullRequestOpened", args.onPullRequestOpened);
143
+ const onPullRequestMerged = optionalStatus("onPullRequestMerged", args.onPullRequestMerged);
144
+ if ((onPullRequestOpened !== null || onPullRequestMerged !== null) && args.access !== "write") {
145
+ throw new McpError(400, "onPullRequestOpened and onPullRequestMerged need write access.");
146
+ }
147
+ if ((allowedTransitions.length > 0 || writableFields.length > 0 || allowedLinkTypes.length > 0) &&
148
+ args.access !== "write") {
149
+ throw new McpError(400, "allowedTransitions, writableFields, and allowedLinkTypes need write access.");
150
+ }
151
+ if (triggers.length > 0 && args.access !== "write")
152
+ throw new McpError(400, "Event triggers need write access.");
153
+ if (triggers.includes("transitioned") && triggerStatuses.length === 0) {
154
+ throw new McpError(400, "The transitioned trigger needs a non-empty triggerStatuses.");
155
+ }
156
+ if (triggers.includes("labeled") && triggerLabels.length === 0) {
157
+ throw new McpError(400, "The labeled trigger needs a non-empty triggerLabels.");
158
+ }
159
+ if ((triggers.includes("mention") || triggers.includes("assigned")) && trustedAccountIds.length === 0) {
160
+ throw new McpError(400, "The mention and assigned triggers need a non-empty trustedAccountIds.");
161
+ }
162
+ if (!ctx.providers.issueTrackers?.[provider]) {
163
+ throw new McpError(503, "Jira is not configured on this deployment (see docs/jira-agents.md).");
164
+ }
165
+ const fields = {
166
+ access: args.access,
167
+ triggers,
168
+ triggerStatuses,
169
+ triggerLabels,
170
+ jqlFilter: args.jqlFilter ?? null,
171
+ trustedAccountIds,
172
+ commentVisibilityRole: args.commentVisibilityRole ?? null,
173
+ allowedTransitions,
174
+ writableFields,
175
+ allowedLinkTypes,
176
+ onPullRequestOpened,
177
+ onPullRequestMerged,
178
+ creatableIssueTypes,
179
+ maxNewIssuesPerRun,
180
+ authorizedById: ctx.principal.id,
181
+ authorizedAt: new Date(),
182
+ };
183
+ const link = await ctx.db.agentIssueProject.upsert({
184
+ where: { agentId_provider_projectKey: { agentId: args.agentId, provider, projectKey } },
185
+ create: { agentId: args.agentId, provider, projectKey, ...fields },
186
+ update: fields,
187
+ });
188
+ return textResult({ linked: true, link });
189
+ },
190
+ });
191
+ mcp.registerTool({
192
+ name: "unlink_issue_project",
193
+ scope: "agents:write",
194
+ description: "Removes a native agent's link to a Jira project. Admin only (agents:admin with the admin role).",
195
+ inputSchema: {
196
+ type: "object",
197
+ additionalProperties: false,
198
+ properties: {
199
+ agentId: { type: "string" },
200
+ provider: { type: "string", enum: PROVIDERS },
201
+ projectKey: { type: "string", minLength: 1, maxLength: 255 },
202
+ },
203
+ required: ["agentId", "projectKey"],
204
+ },
205
+ handler: async (args, ctx) => {
206
+ requireScope(ctx, ctx.canonicalUri, "agents:admin");
207
+ const agent = await ctx.db.agent.findUnique({ where: { id: args.agentId } });
208
+ if (!agent)
209
+ throw new McpError(404, `Agent "${args.agentId}" not found.`);
210
+ const { count } = await ctx.db.agentIssueProject.deleteMany({
211
+ where: {
212
+ agentId: args.agentId,
213
+ provider: args.provider ?? "jira",
214
+ projectKey: normalizeProjectKey(args.projectKey),
215
+ },
216
+ });
217
+ return textResult({ unlinked: count > 0 });
218
+ },
219
+ });
220
+ mcp.registerTool({
221
+ name: "list_issue_projects",
222
+ scope: "agents:read",
223
+ inputSchema: {
224
+ type: "object",
225
+ additionalProperties: false,
226
+ properties: { agentId: { type: "string" } },
227
+ required: ["agentId"],
228
+ },
229
+ handler: async (args, ctx) => {
230
+ await requireAgentAccess(ctx, args.agentId, "read");
231
+ const links = await ctx.db.agentIssueProject.findMany({
232
+ where: { agentId: args.agentId },
233
+ orderBy: { createdAt: "asc" },
234
+ });
235
+ return textResult({ issueProjects: links });
236
+ },
237
+ });
238
+ }
@@ -0,0 +1,22 @@
1
+ import { McpError } from "../errors.js";
2
+ import type { WardbyMcpServer } from "../server.js";
3
+ export interface ModelCatalogToolDeps {
4
+ /** Rebuilds the process's installed CatalogStore after a write; omitted in contexts with no store. */
5
+ refresh?: () => Promise<void>;
6
+ }
7
+ /**
8
+ * The post-write ownership-race error: our own row for `inputProvider` was
9
+ * written, but a refresh right after showed `nowOwner` winning the merge
10
+ * instead (the first-claim-wins rule — see `ownerOf` in catalog.ts). Named
11
+ * after the thing that actually fixes it (reset_model, which clears every
12
+ * row for the id, `nowOwner`'s included) rather than telling the caller to
13
+ * "retry set_model", which would just hit the pre-write conflict check again
14
+ * (or, if the race repeats, loop). Exported so its exact wording can be unit
15
+ * tested directly — forcing the genuine two-process write race that triggers
16
+ * this through the public tool-call surface isn't practical: it requires a
17
+ * competing write to land in the narrow window between this handler's own
18
+ * pre-write ownership check and its own write, which a single-process test
19
+ * harness has no way to inject mid-call.
20
+ */
21
+ export declare function raceLostError(modelId: string, inputProvider: string, nowOwner: string): McpError;
22
+ export declare function registerModelCatalogTools(mcp: WardbyMcpServer, deps?: ModelCatalogToolDeps): void;