@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
package/.env.example CHANGED
@@ -11,6 +11,9 @@ OPENAI_API_KEY=
11
11
  # Set LLM_PROVIDER=anthropic to select the Claude adapter (default: openai).
12
12
  LLM_PROVIDER=openai
13
13
  ANTHROPIC_API_KEY=
14
+ # How often (seconds) each process re-reads the model catalog from the database,
15
+ # so another process's set_model/disable_model/reset_model reaches it (docs/models.md).
16
+ # WARDBY_MODEL_CATALOG_REFRESH_SECONDS=45
14
17
 
15
18
  # Phase 4: MCP server (`wardby mcp`). stdio is the local/no-OAuth default.
16
19
  MCP_TRANSPORT=stdio
@@ -34,9 +37,9 @@ AUTH_ISSUER=
34
37
  AUTH_JWKS_URI=
35
38
  AUTH_AUDIENCE=
36
39
  # Optional, delegating mode: wardby roles from a signed access-token claim.
37
- # Privileged scopes (agents:admin, packages:approve, services:manage) are honoured only for a
38
- # role granting them (admin: all three; package-approver: packages:approve;
39
- # service-manager: services:manage). Set
40
+ # Privileged scopes (agents:admin, packages:approve, services:manage, admin:view, models:admin)
41
+ # are honoured only for a role granting them (admin: all five; package-approver: packages:approve;
42
+ # service-manager: services:manage; model-manager: models:admin). Set
40
43
  # both, or neither (then nobody has a role). The claim is an exact top-level
41
44
  # name first (e.g. groups, roles, or a namespaced https://… claim), else a
42
45
  # dotted path (e.g. realm_access.roles). The map is idpValue=wardbyRole pairs,
@@ -140,6 +143,22 @@ GITHUB_APP_WEBHOOK_SECRET=
140
143
  GITHUB_APP_CLIENT_ID=
141
144
  GITHUB_APP_CLIENT_SECRET=
142
145
 
146
+ # --- Jira Cloud (optional; see docs/jira-agents.md) ---
147
+ # wardby acts in Jira as an Atlassian service account, never a person's account:
148
+ # every comment and change an agent makes is attributed to this account.
149
+ # The site people browse (bare origin). Issue links in comments point here.
150
+ WARDBY_JIRA_SITE_URL=
151
+ # The service account's API token and the Atlassian API gateway for your site.
152
+ WARDBY_JIRA_API_TOKEN=
153
+ WARDBY_JIRA_API_BASE_URL=https://api.atlassian.com/ex/jira/<cloudId>
154
+ # Optional: the token's expiry date (YYYY-MM-DD); wardby warns 14 days ahead.
155
+ WARDBY_JIRA_API_TOKEN_EXPIRES_AT=
156
+ # Optional: field id of the legacy Epic Link field (company-managed projects), so
157
+ # cost reports group runs under their epic, e.g. customfield_10014.
158
+ # WARDBY_JIRA_EPIC_LINK_FIELD=
159
+ # The secret you set on the Jira webhook (20+ characters).
160
+ WARDBY_JIRA_WEBHOOK_SECRET=
161
+
143
162
  # EXECUTOR=in-process (default): heartbeat + reconciler durability; a run whose
144
163
  # process dies is reconciled to `lost`. EXECUTOR=dbos: each run is a DBOS
145
164
  # durable workflow that resumes from its last completed LLM turn / tool call
package/README.md CHANGED
@@ -225,14 +225,14 @@ These are example systems a team can build and manage through MCP:
225
225
  **Builders can vary. The controls do not.** Each system inherits the same
226
226
  budget, capability, identity, evidence, and review contract.
227
227
 
228
- | Agent system | Typical cycle | Governed outcome |
229
- | ------------------------ | ------------------------------------------------------------------ | ------------------------------------ |
230
- | **Delivery pipeline** | Work request → plan → implementation → tests → review | Optional draft feature or bug-fix PR |
231
- | **Security maintenance** | Scheduled scan → assess → patch → verify | Report or optional remediation PR |
232
- | **Architecture review** | Inspect codebase → score risks → prioritize findings | Architecture and risk report |
233
- | **QA coverage** | Map journeys → rank gaps → add tests → run suite | Coverage report or optional test PR |
234
- | **System monitoring** | Receive signal → investigate → correlate → escalate | Actionable defect or incident report |
235
- | **Project tracking** | Read delivery data → compare plan, cost, and progress → flag drift | Portfolio or program update |
228
+ | Agent system | Typical cycle | Governed outcome |
229
+ | -------------------------------------------------------------------------------- | ------------------------------------------------------------------ | ------------------------------------ |
230
+ | **[Delivery pipeline](docs/agent-recipes.md#recipe-b-a-builder-per-language)** | Work request → plan → implementation → tests → review | Optional draft feature or bug-fix PR |
231
+ | **Security maintenance** | Scheduled scan → assess → patch → verify | Report or optional remediation PR |
232
+ | **[Architecture review](docs/agent-recipes.md#recipe-a-an-architecture-keeper)** | Inspect codebase → score risks → prioritize findings | Architecture and risk report |
233
+ | **QA coverage** | Map journeys → rank gaps → add tests → run suite | Coverage report or optional test PR |
234
+ | **System monitoring** | Receive signal → investigate → correlate → escalate | Actionable defect or incident report |
235
+ | **Project tracking** | Read delivery data → compare plan, cost, and progress → flag drift | Portfolio or program update |
236
236
 
237
237
  Agents can stand alone or be connected as bounded sub-agents. Shared budget
238
238
  groups can cap the combined spend of an ecosystem, while each agent retains its
@@ -364,7 +364,9 @@ for the repository's own dependency-override policy.
364
364
  - MCP-first agent, tool, schedule, budget-group, secret, datastore, webhook,
365
365
  run, and sub-agent management.
366
366
  - Scheduled and event-triggered native agents with OpenAI, direct Anthropic,
367
- and Bedrock-Claude model routing.
367
+ and Bedrock-Claude model routing, priced and routed from an admin-editable
368
+ model catalog (`list_models`, `get_model`, `set_model`, `disable_model`,
369
+ `reset_model`).
368
370
  - Per-run budgets, shared budget groups, usage accounting, and cancellation.
369
371
  - QuickJS tool isolation with host allowlists and secret bindings.
370
372
  - Containerized Codex and Claude Code executors with trusted GitHub draft-PR
@@ -1 +1 @@
1
- export declare const CLI_USAGE = "usage:\n wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]\n wardby doctor\n wardby status\n wardby logs [--tail N] [--follow]\n wardby down [--volumes]\n wardby help [list]\n wardby help search <terms>\n wardby help open <article-id>\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>] [--owner <subject>] [--public]\n (owned by --owner or LOCAL_PRINCIPAL; --public shares it with everyone at execute)\n wardby agent list\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name|tool-id> <agent-name> (grants the capabilities in the agent owner's name)\n wardby tool detach <tool-name|tool-id> <agent-name>\n wardby tool update <tool-name|tool-id> [--description <d>] [--params <file>] [--code <file>]\n wardby tool delete <tool-name|tool-id> [--detach]\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby grants migration-report [--json]\n wardby grants adopt-public --owner <subject> [--dry-run] [--keep-everyone-execute]\n wardby grants prune-bindings [--dry-run]\n wardby import <bundle-dir> [--owner <subject>] [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
1
+ export declare const CLI_USAGE = "usage:\n wardby quickstart [--provider openai|anthropic] [--model <m>] [--budget <usd>] [--client none|codex|claude|both] [--skip-demo] [--non-interactive --yes]\n wardby doctor\n wardby status\n wardby logs [--tail N] [--follow]\n wardby down [--volumes]\n wardby knowledge check [dir] [--root <dir>] [--strict] [--json] Validate a knowledge bundle (offline)\n wardby help [list]\n wardby help search <terms>\n wardby help open <article-id>\n wardby agent create --name <n> --model <m> --prompt <p> --budget <usd> [--schedule \"<cron>\"] [--timezone <tz>] [--max-turns <n>] [--owner <subject>] [--public]\n (owned by --owner or LOCAL_PRINCIPAL; --public shares it with everyone at execute)\n wardby agent list\n wardby agent schedule <name> --cron \"<expr>\" [--timezone <tz>] [--disable]\n wardby tool create --name <n> --description <d> --params <file> --code <file>\n wardby tool attach <tool-name|tool-id> <agent-name> (grants the capabilities in the agent owner's name)\n wardby tool detach <tool-name|tool-id> <agent-name>\n wardby tool update <tool-name|tool-id> [--description <d>] [--params <file>] [--code <file>]\n wardby tool delete <tool-name|tool-id> [--detach]\n wardby tool list [--agent <name>]\n wardby run <name>\n wardby runs [--agent <name>] [--limit N] [--status <s>]\n wardby coding preflight (JOB_LAUNCHER=docker or kubernetes)\n wardby coding cleanup --run-id <id>\n wardby scheduler [--scope default]\n wardby mcp (MCP_TRANSPORT=stdio|http selects the transport)\n wardby serve [--scope default] (mcp + scheduler + reconciler in one process; http only)\n wardby grants migration-report [--json]\n wardby grants adopt-public --owner <subject> [--dry-run] [--keep-everyone-execute]\n wardby grants prune-bindings [--dry-run]\n wardby import <bundle-dir> [--owner <subject>] [--public] [--include-secrets --transfer-key <pem>] [--default-budget <usd>] [--dry-run] [--prefix <p>] [--on-conflict fail|skip|rename] [--allow-open-fetch]\n\noptions:\n -h, --help Show this help\n -v, --version Show the installed Wardby version";
package/dist/cli-help.js CHANGED
@@ -4,6 +4,7 @@ export const CLI_USAGE = `usage:
4
4
  wardby status
5
5
  wardby logs [--tail N] [--follow]
6
6
  wardby down [--volumes]
7
+ wardby knowledge check [dir] [--root <dir>] [--strict] [--json] Validate a knowledge bundle (offline)
7
8
  wardby help [list]
8
9
  wardby help search <terms>
9
10
  wardby help open <article-id>
package/dist/cli.js CHANGED
@@ -26,6 +26,7 @@ import { isImmutableDockerImage } from "./providers/jobs/docker-isolation.js";
26
26
  import { ClientNodeKubernetesApi } from "./providers/jobs/kubernetes-client.js";
27
27
  import { describePreflightFailure, kubernetesPreflight } from "./providers/jobs/kubernetes-preflight.js";
28
28
  import { RoutingLlmProvider, isLlmEffort, modelSupportedEfforts, resolveLlmRegistrations, } from "./providers/llm/index.js";
29
+ import { startModelCatalog } from "./providers/llm/catalog-store.js";
29
30
  import { buildConfiguredExecutor, buildExecutor } from "./providers/executor/index.js";
30
31
  import { PostgresDatastore } from "./providers/datastore/index.js";
31
32
  import { PostgresAgentMemory } from "./providers/memory/index.js";
@@ -33,9 +34,11 @@ import { buildSecretCipher } from "./providers/secrets/index.js";
33
34
  import { prisma } from "./core/db.js";
34
35
  import { runAgent } from "./core/runner.js";
35
36
  import { cancelRunOnSignal } from "./core/run-heartbeat.js";
37
+ import { buildIssueTrackers } from "./providers/issue-tracker/index.js";
36
38
  import { buildReviewHosts } from "./providers/review-host/index.js";
37
39
  import { createRepoAccessGate } from "./core/repo-access.js";
38
40
  import { validateCronExpression } from "./core/cron.js";
41
+ import { assertAgentModelAvailable } from "./core/run-pricing.js";
39
42
  import { startScheduler } from "./core/scheduler.js";
40
43
  import { startReconciler } from "./core/reconciler.js";
41
44
  import { NativeEngine } from "./core/engine-native.js";
@@ -140,6 +143,22 @@ async function agentCreate(args) {
140
143
  fail(`invalid --schedule/--timezone: ${err instanceof Error ? err.message : String(err)}`);
141
144
  }
142
145
  }
146
+ // Started before validating the model so an admin override recorded in
147
+ // ModelCatalogEntry (a disabled model, a narrowed effort list) is
148
+ // respected here too, not just at run time.
149
+ let modelCatalog;
150
+ try {
151
+ modelCatalog = await startModelCatalog(prisma);
152
+ }
153
+ catch (err) {
154
+ fail(err instanceof Error ? err.message : String(err));
155
+ }
156
+ try {
157
+ assertAgentModelAvailable(values.model);
158
+ }
159
+ catch (err) {
160
+ fail(err instanceof Error ? err.message : String(err));
161
+ }
143
162
  if (values.effort !== undefined) {
144
163
  const accepted = modelSupportedEfforts(values.model);
145
164
  if (!isLlmEffort(values.effort) || !accepted.includes(values.effort)) {
@@ -147,6 +166,7 @@ async function agentCreate(args) {
147
166
  (accepted.length > 0 ? ` (accepted: ${accepted.join(", ")}).` : " (it accepts no effort setting)."));
148
167
  }
149
168
  }
169
+ modelCatalog.close();
150
170
  // Owned by --owner or the local operator; never owner-less (resource-
151
171
  // sharing grants spec §3.8). --public shares it with everyone at execute.
152
172
  let ownerId;
@@ -510,6 +530,13 @@ async function run(name) {
510
530
  if (agent.kind === "coding") {
511
531
  fail("Coding agents are MCP-first: use trigger_agent so task input and ownership are recorded safely.");
512
532
  }
533
+ let modelCatalog;
534
+ try {
535
+ modelCatalog = await startModelCatalog(prisma);
536
+ }
537
+ catch (err) {
538
+ fail(err instanceof Error ? err.message : String(err));
539
+ }
513
540
  const llm = buildLlmProvider();
514
541
  const engine = buildEngine();
515
542
  const secrets = buildSecrets();
@@ -520,7 +547,7 @@ async function run(name) {
520
547
  // `running` row behind.
521
548
  let removeSignalHandlers;
522
549
  try {
523
- run = await runAgent(name, { llm, engine, datastore, secrets, memory, reviewHosts: buildReviewHosts() }, prisma, (delta) => {
550
+ run = await runAgent(name, { llm, engine, datastore, secrets, memory, reviewHosts: buildReviewHosts(), issueTrackers: buildIssueTrackers() }, prisma, (delta) => {
524
551
  process.stdout.write(delta);
525
552
  }, (created) => {
526
553
  removeSignalHandlers = cancelRunOnSignal(prisma, created.id);
@@ -531,6 +558,7 @@ async function run(name) {
531
558
  }
532
559
  finally {
533
560
  removeSignalHandlers?.();
561
+ modelCatalog.close();
534
562
  }
535
563
  process.stdout.write("\n");
536
564
  const completedAgent = await prisma.agent.findUnique({ where: { id: run.agentId } });
@@ -657,6 +685,13 @@ async function scheduler(args) {
657
685
  // Parsed before anything starts, so a malformed CODING_MAX_CONCURRENT or
658
686
  // CODING_QUEUE_TIMEOUT_SEC fails fast.
659
687
  const concurrency = loadCodingConcurrencyConfig();
688
+ let modelCatalog;
689
+ try {
690
+ modelCatalog = await startModelCatalog(prisma);
691
+ }
692
+ catch (err) {
693
+ fail(err instanceof Error ? err.message : String(err));
694
+ }
660
695
  const config = loadProviderConfig();
661
696
  const llm = buildLlmProvider();
662
697
  const engine = buildEngine();
@@ -664,18 +699,21 @@ async function scheduler(args) {
664
699
  const datastore = buildDatastore(secrets);
665
700
  const memory = buildMemory();
666
701
  const reviewHosts = buildReviewHosts();
702
+ const issueTrackers = buildIssueTrackers();
667
703
  // One repository-access gate (and cache) for native repo_* calls and coding runs.
668
704
  const repoAccess = createRepoAccessGate({ db: prisma, hosts: reviewHosts });
669
- const nativeExecutor = buildExecutor(config, { llm, engine, datastore, secrets, memory, reviewHosts, repoAccess }, prisma);
705
+ const nativeExecutor = buildExecutor(config, { llm, engine, datastore, secrets, memory, reviewHosts, issueTrackers, repoAccess }, prisma);
670
706
  const executor = buildConfiguredExecutor({ native: nativeExecutor, db: prisma, providerConfig: config, repoAccess });
671
707
  await executor.launch?.();
672
- const reconciler = startReconciler({ db: prisma, executor, reviewHosts });
708
+ const reconciler = startReconciler({ db: prisma, executor, reviewHosts, issueTrackers });
709
+ const selfDefects = { db: prisma, issueTrackers };
673
710
  const sched = startScheduler({
674
711
  executor,
675
712
  db: prisma,
676
713
  scope,
714
+ selfDefects,
677
715
  onLeaderTick: async () => {
678
- await drainCodingQueue({ db: prisma, executor, ...concurrency });
716
+ await drainCodingQueue({ db: prisma, executor, ...concurrency, selfDefects });
679
717
  },
680
718
  });
681
719
  console.log(`wardby scheduler started (scope "${scope}"). Press Ctrl+C to stop.`);
@@ -686,7 +724,10 @@ async function scheduler(args) {
686
724
  reconciler.stop();
687
725
  void Promise.resolve(executor.close?.())
688
726
  .catch((err) => cliLog.warn({ err }, "executor close failed during scheduler shutdown"))
689
- .finally(resolve);
727
+ .finally(() => {
728
+ modelCatalog.close();
729
+ resolve();
730
+ });
690
731
  };
691
732
  process.once("SIGINT", shutdown);
692
733
  process.once("SIGTERM", shutdown);
@@ -734,11 +775,28 @@ async function serve(args) {
734
775
  }
735
776
  async function importCommand(rest) {
736
777
  const opts = parseImportArgs(rest);
737
- const { report, result } = await runImport({ ...opts, db: prisma, env: process.env });
738
- console.log(report);
739
- if (result) {
740
- for (const w of result.webhookSecrets)
741
- console.log(`webhook secret (${w.agentName}): ${w.secret}`);
778
+ // runImport builds an LLM provider and classifies each agent's model as
779
+ // routable/unroutable from currentModelCatalog() — without a store
780
+ // started here first, that falls back to the shipped catalog, so an
781
+ // admin-disabled model would be (wrongly) imported as routable and a
782
+ // DB-only custom model would be (wrongly) imported as disabled.
783
+ let modelCatalog;
784
+ try {
785
+ modelCatalog = await startModelCatalog(prisma);
786
+ }
787
+ catch (err) {
788
+ fail(err instanceof Error ? err.message : String(err));
789
+ }
790
+ try {
791
+ const { report, result } = await runImport({ ...opts, db: prisma, env: process.env });
792
+ console.log(report);
793
+ if (result) {
794
+ for (const w of result.webhookSecrets)
795
+ console.log(`webhook secret (${w.agentName}): ${w.secret}`);
796
+ }
797
+ }
798
+ finally {
799
+ modelCatalog.close();
742
800
  }
743
801
  }
744
802
  async function main() {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * The coding workspace has no git metadata, so the worker cannot learn which
3
+ * commit it is looking at. The executor appends it to the task when it writes
4
+ * the worker input (after cloning). Never grows a task past the limit.
5
+ */
6
+ export declare function withBaseCommit(task: string, baseCommit: string): string;
@@ -0,0 +1,12 @@
1
+ import { byteLength, MAX_CODING_TASK_BYTES } from "./protocol.js";
2
+ /**
3
+ * The coding workspace has no git metadata, so the worker cannot learn which
4
+ * commit it is looking at. The executor appends it to the task when it writes
5
+ * the worker input (after cloning). Never grows a task past the limit.
6
+ */
7
+ export function withBaseCommit(task, baseCommit) {
8
+ if (!/^[0-9a-f]{40}$/.test(baseCommit))
9
+ return task;
10
+ const withLine = `${task}\n\nBase commit: ${baseCommit} (the commit this workspace was checked out at; the workspace has no git metadata).`;
11
+ return byteLength(withLine) <= MAX_CODING_TASK_BYTES ? withLine : task;
12
+ }
@@ -1,7 +1,11 @@
1
1
  import { z } from "zod";
2
2
  export declare const CODING_PROTOCOL_VERSION: 1;
3
+ /** The code host every coding run's repository and pull request live on (see pullRequestUrlSchema); the one place to change when another is added. */
4
+ export declare const CODING_CODE_PROVIDER = "github";
3
5
  export declare const MAX_CODING_ARTIFACT_BYTES: number;
4
6
  export declare const MAX_CODING_TASK_BYTES: number;
7
+ /** Room reserved at the end of a composed task for lines the executor appends (e.g. the base commit). */
8
+ export declare const CODING_TASK_TRAILER_RESERVE_BYTES = 256;
5
9
  export declare const MAX_CODING_SUMMARY_BYTES: number;
6
10
  export declare const MAX_CODING_TESTS = 64;
7
11
  export declare const MAX_CODING_TEST_COMMAND_BYTES: number;
@@ -35,6 +39,15 @@ export declare function normalizeGitHubRepository(value: string): string;
35
39
  export declare function normalizeGitRef(value: string): string;
36
40
  export declare const CodingBaseRefSchema: z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>;
37
41
  export declare const CodingTaskOverrideSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>, string, string>;
42
+ /**
43
+ * A section rendered to fit whatever room the task leaves (the knowledge note:
44
+ * src/knowledge/note.ts). Declared structurally so this module, which coding
45
+ * workers ship as a single file, imports nothing new.
46
+ */
47
+ export interface FittedSection {
48
+ /** The section text within `maxBytes`, or undefined when it does not fit. */
49
+ render(maxBytes: number): string | undefined;
50
+ }
38
51
  /**
39
52
  * A coding worker receives only its task text, so a coding agent's own
40
53
  * instructions (its systemPrompt) travel inside that text, ahead of the
@@ -42,8 +55,11 @@ export declare const CodingTaskOverrideSchema: z.ZodEffects<z.ZodEffects<z.ZodEf
42
55
  * started: src/coding/services/note.ts). Blank instructions and no note leave
43
56
  * the task unchanged. The combination must still fit MAX_CODING_TASK_BYTES;
44
57
  * exceeding it is an error naming both parts rather than a silent truncation.
58
+ * A knowledge note, when given, goes between the standing instructions and the
59
+ * request and is fitted into whatever room is left: it is cut, or dropped,
60
+ * never the cause of an error.
45
61
  */
46
- export declare function composeCodingTask(instructions: string | null | undefined, task: string, note?: string): string;
62
+ export declare function composeCodingTask(instructions: string | null | undefined, task: string, note?: string, knowledge?: FittedSection): string;
47
63
  /**
48
64
  * The tag only labels a pull request title, so a model's malformed tag must not
49
65
  * sink an otherwise finished run. A tag that already passes is kept as-is; any
@@ -1,7 +1,11 @@
1
1
  import { z } from "zod";
2
2
  export const CODING_PROTOCOL_VERSION = 1;
3
+ /** The code host every coding run's repository and pull request live on (see pullRequestUrlSchema); the one place to change when another is added. */
4
+ export const CODING_CODE_PROVIDER = "github";
3
5
  export const MAX_CODING_ARTIFACT_BYTES = 64 * 1024;
4
6
  export const MAX_CODING_TASK_BYTES = 16 * 1024;
7
+ /** Room reserved at the end of a composed task for lines the executor appends (e.g. the base commit). */
8
+ export const CODING_TASK_TRAILER_RESERVE_BYTES = 256;
5
9
  export const MAX_CODING_SUMMARY_BYTES = 8 * 1024;
6
10
  export const MAX_CODING_TESTS = 64;
7
11
  export const MAX_CODING_TEST_COMMAND_BYTES = 2 * 1024;
@@ -201,17 +205,24 @@ export const CodingTaskOverrideSchema = boundedText(MAX_CODING_TASK_BYTES);
201
205
  * started: src/coding/services/note.ts). Blank instructions and no note leave
202
206
  * the task unchanged. The combination must still fit MAX_CODING_TASK_BYTES;
203
207
  * exceeding it is an error naming both parts rather than a silent truncation.
208
+ * A knowledge note, when given, goes between the standing instructions and the
209
+ * request and is fitted into whatever room is left: it is cut, or dropped,
210
+ * never the cause of an error.
204
211
  */
205
- export function composeCodingTask(instructions, task, note) {
212
+ export function composeCodingTask(instructions, task, note, knowledge) {
206
213
  const standing = [instructions?.trim(), note?.trim()].filter((part) => Boolean(part)).join("\n\n");
207
- if (!standing)
208
- return task;
209
- const composed = `Standing instructions for this coding agent:\n${standing}\n\nRequest:\n${task}`;
210
- if (byteLength(composed) > MAX_CODING_TASK_BYTES) {
214
+ const base = standing ? `Standing instructions for this coding agent:\n${standing}\n\nRequest:\n${task}` : task;
215
+ if (byteLength(base) > MAX_CODING_TASK_BYTES) {
211
216
  throw new Error(`The coding agent's instructions (${byteLength(standing)} bytes) plus this task (${byteLength(task)} bytes) ` +
212
217
  `exceed the ${MAX_CODING_TASK_BYTES}-byte coding task limit; shorten one of them.`);
213
218
  }
214
- return composed;
219
+ if (!knowledge)
220
+ return base;
221
+ const prefix = standing ? `Standing instructions for this coding agent:\n${standing}\n\n` : "";
222
+ const suffix = `\n\nRequest:\n${task}`;
223
+ const room = MAX_CODING_TASK_BYTES - byteLength(prefix) - byteLength(suffix) - CODING_TASK_TRAILER_RESERVE_BYTES;
224
+ const rendered = room > 0 ? knowledge.render(room) : undefined;
225
+ return rendered ? `${prefix}${rendered}${suffix}` : base;
215
226
  }
216
227
  const runIdSchema = boundedText(MAX_RUN_ID_BYTES, true).refine((value) => /^[A-Za-z0-9][A-Za-z0-9_-]*$/.test(value), "must be an opaque identifier");
217
228
  const modelSchema = boundedText(MAX_MODEL_BYTES, true).refine((value) => /^[A-Za-z0-9][A-Za-z0-9._:-]*$/.test(value), "must be a model identifier");
@@ -1,4 +1,9 @@
1
+ import type { ModelCatalog } from "../providers/llm/catalog.js";
2
+ import type { ModelProvider } from "../providers/llm/catalog-types.js";
1
3
  export declare const CODING_PROVIDERS: readonly ["codex", "claude-code"];
2
4
  export type CodingProvider = (typeof CODING_PROVIDERS)[number];
3
- export declare function codingProviderSupportsModel(provider: CodingProvider, model: string): boolean;
5
+ /** Which coding provider runs a model provider's models; Bedrock models are never coding models. */
6
+ export declare function codingProviderForModelProvider(provider: ModelProvider): CodingProvider | undefined;
7
+ export declare function codingProviderSupportsModel(provider: CodingProvider, model: string, catalog?: ModelCatalog): boolean;
8
+ export declare function assertCodingProvider(provider: string): asserts provider is CodingProvider;
4
9
  export declare function assertCodingProviderModel(provider: string, model: string): asserts provider is CodingProvider;
@@ -1,17 +1,24 @@
1
- import { anthropicSupportedModels } from "../providers/llm/pricing-anthropic.js";
2
- import { supportedModels as openaiSupportedModels } from "../providers/llm/pricing.js";
1
+ import { currentModelCatalog } from "../providers/llm/catalog-store.js";
3
2
  export const CODING_PROVIDERS = ["codex", "claude-code"];
4
- const MODELS_BY_PROVIDER = {
5
- codex: new Set(openaiSupportedModels()),
6
- "claude-code": new Set(anthropicSupportedModels()),
7
- };
8
- export function codingProviderSupportsModel(provider, model) {
9
- return MODELS_BY_PROVIDER[provider].has(model);
3
+ /** Which coding provider runs a model provider's models; Bedrock models are never coding models. */
4
+ export function codingProviderForModelProvider(provider) {
5
+ if (provider === "openai")
6
+ return "codex";
7
+ if (provider === "anthropic")
8
+ return "claude-code";
9
+ return undefined;
10
10
  }
11
- export function assertCodingProviderModel(provider, model) {
11
+ export function codingProviderSupportsModel(provider, model, catalog = currentModelCatalog()) {
12
+ const entry = catalog.get(model);
13
+ return entry !== undefined && codingProviderForModelProvider(entry.provider) === provider;
14
+ }
15
+ export function assertCodingProvider(provider) {
12
16
  if (!CODING_PROVIDERS.includes(provider)) {
13
17
  throw new Error(`Unsupported coding provider "${provider}".`);
14
18
  }
19
+ }
20
+ export function assertCodingProviderModel(provider, model) {
21
+ assertCodingProvider(provider);
15
22
  if (!codingProviderSupportsModel(provider, model)) {
16
23
  throw new Error(`Model "${model}" is not supported by coding provider "${provider}".`);
17
24
  }
@@ -52,6 +52,31 @@ export declare function loadGitHubUserAuthConfig(env?: NodeJS.ProcessEnv): {
52
52
  clientId?: string;
53
53
  clientSecret?: string;
54
54
  };
55
+ export interface JiraConfig {
56
+ /** The site people browse, e.g. https://your-site.atlassian.net — used for issue links. A bare https origin. */
57
+ siteUrl: string;
58
+ /** Where REST calls go: always https://api.atlassian.com/ex/jira/<cloudId>, the only base a service-account token works against. */
59
+ apiBaseUrl: string;
60
+ /** A service account's API token, sent as a Bearer token. Personal (Basic email:token) auth is not supported. */
61
+ auth: {
62
+ kind: "bearer";
63
+ token: string;
64
+ };
65
+ webhookSecret: string;
66
+ /** Optional, for expiry warnings; Atlassian API tokens last at most a year. */
67
+ tokenExpiresAt?: Date;
68
+ /** Company-managed sites still on the legacy Epic Link field: its id (customfield_N). Optional. */
69
+ epicLinkField?: string;
70
+ }
71
+ /**
72
+ * Jira Cloud issue-tracker settings (docs/jira-agents.md). Unset = Jira is
73
+ * disabled. wardby acts in Jira only as an Atlassian service account, so
74
+ * everything an agent does is attributed to that account: the token is sent
75
+ * as a Bearer token and REST calls go through the api.atlassian.com gateway
76
+ * (WARDBY_JIRA_API_BASE_URL, required). Personal tokens (Basic email:token)
77
+ * are rejected.
78
+ */
79
+ export declare function loadJiraConfig(env?: NodeJS.ProcessEnv): JiraConfig | null;
55
80
  export interface ContainerExecutorConfig {
56
81
  workerImage?: string;
57
82
  claudeWorkerImage?: string;
@@ -64,6 +64,77 @@ export function loadGitHubUserAuthConfig(env = process.env) {
64
64
  }
65
65
  return { clientId, clientSecret };
66
66
  }
67
+ const JIRA_GATEWAY = /^https:\/\/api\.atlassian\.com\/ex\/jira\/[0-9a-f-]{36}$/i;
68
+ function httpsOrigin(value, name) {
69
+ let url;
70
+ try {
71
+ url = new URL(value);
72
+ }
73
+ catch {
74
+ throw new Error(`${name} is not a valid URL.`);
75
+ }
76
+ if (url.protocol !== "https:")
77
+ throw new Error(`${name} must be an https URL.`);
78
+ return value.replace(/\/+$/, "");
79
+ }
80
+ /**
81
+ * Jira Cloud issue-tracker settings (docs/jira-agents.md). Unset = Jira is
82
+ * disabled. wardby acts in Jira only as an Atlassian service account, so
83
+ * everything an agent does is attributed to that account: the token is sent
84
+ * as a Bearer token and REST calls go through the api.atlassian.com gateway
85
+ * (WARDBY_JIRA_API_BASE_URL, required). Personal tokens (Basic email:token)
86
+ * are rejected.
87
+ */
88
+ export function loadJiraConfig(env = process.env) {
89
+ const site = env.WARDBY_JIRA_SITE_URL?.trim();
90
+ const token = env.WARDBY_JIRA_API_TOKEN?.trim();
91
+ const secret = env.WARDBY_JIRA_WEBHOOK_SECRET?.trim();
92
+ const base = env.WARDBY_JIRA_API_BASE_URL?.trim();
93
+ const expires = env.WARDBY_JIRA_API_TOKEN_EXPIRES_AT?.trim() || undefined;
94
+ if (env.WARDBY_JIRA_API_EMAIL?.trim()) {
95
+ throw new Error("WARDBY_JIRA_API_EMAIL is no longer supported: personal (Basic email:token) Jira tokens are not accepted. " +
96
+ "Use an Atlassian service account's API token with WARDBY_JIRA_API_BASE_URL=https://api.atlassian.com/ex/jira/<cloudId>.");
97
+ }
98
+ if (!site && !token && !secret && !base)
99
+ return null;
100
+ if (!site || !token || !secret || !base) {
101
+ throw new Error("Set WARDBY_JIRA_SITE_URL, WARDBY_JIRA_API_BASE_URL, WARDBY_JIRA_API_TOKEN and WARDBY_JIRA_WEBHOOK_SECRET together, or none of them.");
102
+ }
103
+ if (secret.length < 20)
104
+ throw new Error("WARDBY_JIRA_WEBHOOK_SECRET must be at least 20 characters.");
105
+ const siteUrl = httpsOrigin(site, "WARDBY_JIRA_SITE_URL");
106
+ const siteParsed = new URL(siteUrl);
107
+ if (siteParsed.pathname !== "/" ||
108
+ siteParsed.search ||
109
+ siteParsed.hash ||
110
+ siteParsed.username ||
111
+ siteParsed.password) {
112
+ throw new Error("WARDBY_JIRA_SITE_URL must be a bare https origin such as https://your-site.atlassian.net.");
113
+ }
114
+ const apiBaseUrl = httpsOrigin(base, "WARDBY_JIRA_API_BASE_URL");
115
+ if (!JIRA_GATEWAY.test(apiBaseUrl)) {
116
+ throw new Error("WARDBY_JIRA_API_BASE_URL must be https://api.atlassian.com/ex/jira/<cloudId>, the only base a service " +
117
+ "account's API token works against.");
118
+ }
119
+ let tokenExpiresAt;
120
+ if (expires) {
121
+ tokenExpiresAt = new Date(expires);
122
+ if (Number.isNaN(tokenExpiresAt.getTime()))
123
+ throw new Error("WARDBY_JIRA_API_TOKEN_EXPIRES_AT must be an ISO date.");
124
+ }
125
+ const epicLinkField = env.WARDBY_JIRA_EPIC_LINK_FIELD?.trim() || undefined;
126
+ if (epicLinkField && !/^customfield_\d+$/.test(epicLinkField)) {
127
+ throw new Error("WARDBY_JIRA_EPIC_LINK_FIELD must be a custom field id such as customfield_10014.");
128
+ }
129
+ return {
130
+ siteUrl,
131
+ apiBaseUrl,
132
+ auth: { kind: "bearer", token },
133
+ webhookSecret: secret,
134
+ ...(tokenExpiresAt ? { tokenExpiresAt } : {}),
135
+ ...(epicLinkField ? { epicLinkField } : {}),
136
+ };
137
+ }
67
138
  function optionalPositiveNumber(value, name, fallback) {
68
139
  if (value === undefined)
69
140
  return fallback;
@@ -0,0 +1,101 @@
1
+ /**
2
+ * Cost attribution (docs/private/2026-10-01-issue-cost-attribution-design.md):
3
+ * every run in an issue-related run tree points at one WorkItem, with the
4
+ * item's parent frozen as of dispatch. Attribution comes from control-plane
5
+ * data only (the event, a stored PR link, a validated explicit key, or the
6
+ * parent run), never from model output. A snapshot is a network call, so it
7
+ * is taken before dispatch's persist transaction and never fails a dispatch.
8
+ */
9
+ import type { Prisma, PrismaClient } from "#prisma";
10
+ import { type IssueSnapshot, type IssueTrackerProvider, type IssueTrackerRegistry } from "../providers/issue-tracker/types.js";
11
+ export type AttributionSource = "issue_event" | "linked_pr" | "explicit";
12
+ /** Resolved before the persist transaction; never contains a network call. */
13
+ export interface ResolvedWorkItem {
14
+ provider: string;
15
+ key: string;
16
+ scopeKey: string;
17
+ /** null = key-only (snapshot failed or no tracker); then existing WorkItem fields are kept. */
18
+ snapshot: IssueSnapshot | null;
19
+ }
20
+ export interface AttributionIntent {
21
+ source: AttributionSource;
22
+ item: ResolvedWorkItem;
23
+ }
24
+ /** A WorkItem snapshotted this recently is reused as-is, without calling the tracker. */
25
+ export declare const SNAPSHOT_CACHE_MS: number;
26
+ export declare const SNAPSHOT_TIMEOUT_MS = 3000;
27
+ /**
28
+ * Snapshot budget for a caller that is answering a request (a webhook, an
29
+ * MCP tool call, a host event): a short timeout and no 429 retry, so a slow
30
+ * or rate-limited tracker degrades to key-only attribution instead of
31
+ * stalling the response.
32
+ */
33
+ export declare const RESPONSE_PATH_SNAPSHOT_BUDGET: {
34
+ readonly timeoutMs: 2000;
35
+ readonly retryOn429: false;
36
+ };
37
+ /**
38
+ * The work item a new attribution points at, snapshotted from the tracker
39
+ * unless a fresh snapshot is already stored. Never throws: a failed lookup or
40
+ * snapshot attributes by key only.
41
+ */
42
+ export declare function resolveWorkItem(db: Pick<PrismaClient, "workItem">, trackers: IssueTrackerRegistry | undefined, provider: string, key: string, opts?: {
43
+ timeoutMs?: number;
44
+ retryOn429?: boolean;
45
+ now?: Date;
46
+ }): Promise<ResolvedWorkItem>;
47
+ export type AttributionTx = Pick<Prisma.TransactionClient, "workItem" | "runAttribution" | "codingRun" | "runIssueStatus">;
48
+ /**
49
+ * Writes the run's attribution inside dispatch's persist transaction.
50
+ * Precedence: the parent run's attribution, then the continued coding run's,
51
+ * then a pre-attribution issue (legacyIssue), then the caller's intent. Returns the item's provider/key (for
52
+ * CodingRun.issueProvider/issueKey), or null when the run is unattributed.
53
+ */
54
+ export declare function attributeRun(tx: AttributionTx, runId: string, from: {
55
+ parentRunId?: string;
56
+ continuesCodingRunId?: string;
57
+ intent?: AttributionIntent;
58
+ }, now?: Date): Promise<{
59
+ provider: string;
60
+ key: string;
61
+ } | null>;
62
+ /** The issue a pull request was opened for (phase-3 IssuePullRequest); the earliest link when several exist. */
63
+ export declare function linkedPullRequestIssue(db: Pick<PrismaClient, "issuePullRequest">, pr: {
64
+ codeProvider: string;
65
+ repository: string;
66
+ number: number;
67
+ }): Promise<{
68
+ provider: string;
69
+ key: string;
70
+ } | null>;
71
+ /** Attribution for a run on a pull request that was opened for an issue; undefined when it has none. */
72
+ export declare function linkedPullRequestAttribution(db: Pick<PrismaClient, "issuePullRequest" | "workItem">, trackers: IssueTrackerRegistry | undefined, pr: {
73
+ codeProvider: string;
74
+ repository: string;
75
+ number: number;
76
+ }, opts?: {
77
+ timeoutMs?: number;
78
+ retryOn429?: boolean;
79
+ }): Promise<AttributionIntent | undefined>;
80
+ /** A refusal whose message is safe to return to the caller. */
81
+ export declare class AttributionError extends Error {
82
+ constructor(message: string);
83
+ }
84
+ /**
85
+ * An issue a caller names on trigger_agent or a webhook. It decides whose
86
+ * cost this run counts toward, so it must be well formed and in a project the
87
+ * agent is linked to (AgentIssueProject) — otherwise the dispatch is refused.
88
+ * Issue keys are uppercase, so a key that differs only by case or surrounding
89
+ * spaces ("pay-241") is corrected rather than refused. `field` is the caller's
90
+ * name for the value (trigger_agent: issue; webhooks: wardbyIssue), so
91
+ * refusals name the field the caller actually sent.
92
+ */
93
+ export declare function validateExplicitIssue(db: Pick<PrismaClient, "agentIssueProject">, agentId: string, issue: unknown, field?: string): Promise<{
94
+ provider: IssueTrackerProvider;
95
+ key: string;
96
+ }>;
97
+ export declare function explicitAttribution(db: Pick<PrismaClient, "agentIssueProject" | "workItem">, trackers: IssueTrackerRegistry | undefined, agentId: string, issue: unknown, opts?: {
98
+ timeoutMs?: number;
99
+ retryOn429?: boolean;
100
+ field?: string;
101
+ }): Promise<AttributionIntent>;