@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
@@ -0,0 +1,243 @@
1
+ import { periodStart } from "../core/budget-groups.js";
2
+ import { publicCodingRunResult } from "../coding/protocol.js";
3
+ export const DEFAULT_GRAPH_LIMIT = 500;
4
+ export const MAX_GRAPH_LIMIT = 2000;
5
+ const DEFAULT_SINCE_MS = 60 * 60 * 1000;
6
+ const RELATIVE_SINCE_MS = {
7
+ "15m": 15 * 60 * 1000,
8
+ "1h": 60 * 60 * 1000,
9
+ "6h": 6 * 60 * 60 * 1000,
10
+ "24h": 24 * 60 * 60 * 1000,
11
+ "7d": 7 * 24 * 60 * 60 * 1000,
12
+ };
13
+ /** Accepts "15m" | "1h" | "6h" | "24h" | "7d" or an ISO-8601 timestamp; null defaults to 1h. Throws otherwise. */
14
+ export function parseSince(value, now) {
15
+ if (value === null)
16
+ return new Date(now.getTime() - DEFAULT_SINCE_MS);
17
+ if (Object.hasOwn(RELATIVE_SINCE_MS, value))
18
+ return new Date(now.getTime() - RELATIVE_SINCE_MS[value]);
19
+ if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
20
+ const parsed = new Date(value);
21
+ if (!Number.isNaN(parsed.getTime()))
22
+ return parsed;
23
+ }
24
+ throw new Error(`invalid since "${value}": use 15m, 1h, 6h, 24h, 7d or an ISO-8601 timestamp`);
25
+ }
26
+ const runInclude = {
27
+ agent: true,
28
+ codingRun: { select: { result: true, provider: true, model: true, services: true } },
29
+ hostCheck: true,
30
+ hostStatus: true,
31
+ issueStatus: true,
32
+ serviceStatuses: { orderBy: { createdAt: "asc" } },
33
+ };
34
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
35
+ /** Name and version of each service in a coding run's resolved services snapshot. */
36
+ function declaredServices(snapshot) {
37
+ if (!Array.isArray(snapshot))
38
+ return [];
39
+ return snapshot.filter(isRecord).map((s) => ({ name: String(s.name), version: String(s.version) }));
40
+ }
41
+ const iso = (date) => (date ? date.toISOString() : null);
42
+ function triggerFor(row) {
43
+ switch (row.trigger) {
44
+ case "scheduled":
45
+ return { kind: "scheduled", schedule: row.agent.schedule };
46
+ case "webhook":
47
+ return { kind: "webhook" };
48
+ case "subagent":
49
+ return { kind: "subagent" };
50
+ case "manual":
51
+ return { kind: "manual" };
52
+ case "host_event":
53
+ if (row.issueStatus) {
54
+ return { kind: "issue", provider: row.issueStatus.provider, issueKey: row.issueStatus.issueKey };
55
+ }
56
+ if (row.hostStatus) {
57
+ return {
58
+ kind: "code_host",
59
+ provider: row.hostStatus.provider,
60
+ repository: row.hostStatus.repository,
61
+ number: row.hostStatus.number,
62
+ event: "mention",
63
+ };
64
+ }
65
+ if (row.hostCheck) {
66
+ return {
67
+ kind: "code_host",
68
+ provider: row.hostCheck.provider,
69
+ repository: row.hostCheck.repository,
70
+ number: row.hostCheck.prNumber,
71
+ event: "review",
72
+ };
73
+ }
74
+ return { kind: "host_event" };
75
+ }
76
+ }
77
+ function outcomesFor(row, pullRequests) {
78
+ const outcomes = pullRequests.map((pr) => ({
79
+ kind: "pull_request",
80
+ provider: pr.codeProvider,
81
+ repository: pr.repository,
82
+ number: pr.number,
83
+ url: pr.url,
84
+ state: pr.state,
85
+ at: pr.createdAt.toISOString(),
86
+ }));
87
+ const result = publicCodingRunResult(row.codingRun?.result);
88
+ if (result?.pullRequestUrl &&
89
+ result.pullRequestNumber &&
90
+ !pullRequests.some((pr) => pr.url === result.pullRequestUrl)) {
91
+ outcomes.push({
92
+ kind: "pull_request",
93
+ provider: "github",
94
+ repository: result.repository,
95
+ number: result.pullRequestNumber,
96
+ url: result.pullRequestUrl,
97
+ state: null,
98
+ at: iso(row.finishedAt),
99
+ });
100
+ }
101
+ if (row.hostStatus?.commentId) {
102
+ outcomes.push({
103
+ kind: "code_host_comment",
104
+ provider: row.hostStatus.provider,
105
+ repository: row.hostStatus.repository,
106
+ number: row.hostStatus.number,
107
+ at: (row.hostStatus.completedAt ?? row.hostStatus.createdAt).toISOString(),
108
+ });
109
+ }
110
+ if (row.issueStatus?.commentId) {
111
+ outcomes.push({
112
+ kind: "issue_comment",
113
+ provider: row.issueStatus.provider,
114
+ issueKey: row.issueStatus.issueKey,
115
+ at: (row.issueStatus.completedAt ?? row.issueStatus.createdAt).toISOString(),
116
+ });
117
+ }
118
+ if (row.hostCheck) {
119
+ outcomes.push({
120
+ kind: "check",
121
+ provider: row.hostCheck.provider,
122
+ repository: row.hostCheck.repository,
123
+ number: row.hostCheck.prNumber,
124
+ completed: row.hostCheck.completedAt !== null,
125
+ at: iso(row.hostCheck.completedAt),
126
+ });
127
+ }
128
+ return outcomes;
129
+ }
130
+ /** The one row -> GraphRun mapping (also used by run detail). `pullRequests` are the IssuePullRequest rows this run opened. */
131
+ export function toGraphRun(row, pullRequests) {
132
+ return {
133
+ id: row.id,
134
+ parentRunId: row.parentRunId,
135
+ agentId: row.agentId,
136
+ agentName: row.agent.name,
137
+ agentKind: row.agent.kind,
138
+ model: row.codingRun?.model ?? row.agent.model,
139
+ codingProvider: row.codingRun?.provider ?? null,
140
+ status: row.status,
141
+ trigger: triggerFor(row),
142
+ turns: row.turns,
143
+ tokensIn: row.tokensIn,
144
+ tokensOut: row.tokensOut,
145
+ costUsd: Number(row.costUsd),
146
+ budgetUsd: Number(row.agent.budgetUsd),
147
+ startedAt: row.startedAt.toISOString(),
148
+ finishedAt: iso(row.finishedAt),
149
+ heartbeatAt: iso(row.heartbeatAt),
150
+ outcomes: outcomesFor(row, pullRequests),
151
+ declaredServices: declaredServices(row.codingRun?.services),
152
+ services: row.serviceStatuses.map((s) => ({
153
+ name: s.name,
154
+ state: s.state,
155
+ attempts: s.attempts,
156
+ reason: s.reason,
157
+ readyAt: iso(s.readyAt),
158
+ failedAt: iso(s.failedAt),
159
+ createdAt: s.createdAt.toISOString(),
160
+ })),
161
+ };
162
+ }
163
+ async function mapRows(db, rows) {
164
+ if (rows.length === 0)
165
+ return [];
166
+ const pullRequests = await db.issuePullRequest.findMany({
167
+ where: { openedByRunId: { in: rows.map((r) => r.id) } },
168
+ orderBy: { createdAt: "asc" },
169
+ });
170
+ const byRun = new Map();
171
+ for (const pr of pullRequests)
172
+ byRun.set(pr.openedByRunId, [...(byRun.get(pr.openedByRunId) ?? []), pr]);
173
+ return rows.map((row) => toGraphRun(row, byRun.get(row.id) ?? []));
174
+ }
175
+ /** Loads the given runs as GraphRuns (shared with run detail); unknown ids are skipped. */
176
+ export async function loadGraphRuns(db, runIds) {
177
+ if (runIds.length === 0)
178
+ return [];
179
+ const rows = await db.run.findMany({ where: { id: { in: [...runIds] } }, include: runInclude });
180
+ return mapRows(db, rows);
181
+ }
182
+ export async function loadGraph(db, options) {
183
+ const now = options.now ?? new Date();
184
+ // Window rule: started inside the window, or still in flight (a long run started earlier stays visible).
185
+ const windowed = await db.run.findMany({
186
+ where: { OR: [{ startedAt: { gte: options.since } }, { status: { in: ["pending", "running"] } }] },
187
+ orderBy: [{ startedAt: "desc" }, { id: "desc" }],
188
+ take: options.limit + 1,
189
+ include: runInclude,
190
+ });
191
+ const truncated = windowed.length > options.limit;
192
+ const rows = windowed.slice(0, options.limit);
193
+ // Ancestor rule: every ancestor of a windowed run is included, one batch per level, never capped.
194
+ const known = new Set(rows.map((r) => r.id));
195
+ let frontier = rows;
196
+ for (;;) {
197
+ const missing = [
198
+ ...new Set(frontier.map((r) => r.parentRunId).filter((p) => p !== null && !known.has(p))),
199
+ ];
200
+ if (missing.length === 0)
201
+ break;
202
+ frontier = await db.run.findMany({ where: { id: { in: missing } }, include: runInclude });
203
+ for (const r of frontier) {
204
+ known.add(r.id);
205
+ rows.push(r);
206
+ }
207
+ }
208
+ const dayStart = periodStart("day", now);
209
+ const [runs, today, groups] = await Promise.all([
210
+ mapRows(db, rows),
211
+ db.run.aggregate({ where: { startedAt: { gte: dayStart } }, _sum: { costUsd: true } }),
212
+ db.budgetGroup.findMany({
213
+ where: { dailyBudgetUsd: { not: null } },
214
+ select: { id: true, name: true, dailyBudgetUsd: true, agents: { select: { id: true } } },
215
+ orderBy: { name: "asc" },
216
+ }),
217
+ ]);
218
+ const memberAgents = groups.flatMap((g) => g.agents.map((a) => a.id));
219
+ const spendByAgent = memberAgents.length
220
+ ? await db.run.groupBy({
221
+ by: ["agentId"],
222
+ where: { agentId: { in: memberAgents }, startedAt: { gte: dayStart } },
223
+ _sum: { costUsd: true },
224
+ })
225
+ : [];
226
+ const spent = new Map(spendByAgent.map((s) => [s.agentId, Number(s._sum.costUsd ?? 0)]));
227
+ return {
228
+ generatedAt: now.toISOString(),
229
+ since: options.since.toISOString(),
230
+ limit: options.limit,
231
+ truncated,
232
+ runs,
233
+ spend: {
234
+ todayUsd: Number(today._sum.costUsd ?? 0),
235
+ groups: groups.map((g) => ({
236
+ id: g.id,
237
+ name: g.name,
238
+ dailyBudgetUsd: g.dailyBudgetUsd === null ? null : Number(g.dailyBudgetUsd),
239
+ spentTodayUsd: g.agents.reduce((sum, a) => sum + (spent.get(a.id) ?? 0), 0),
240
+ })),
241
+ },
242
+ };
243
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Viewer HTTP routes: GET /admin/api/graph, /admin/api/runs/:id and the
3
+ * /admin/api/events SSE stream. Read-only and deployment-wide, so every route
4
+ * authenticates and then requires the privileged `admin:view` scope (admin role
5
+ * only) BEFORE touching the database or the event bus. McpErrors propagate to
6
+ * the server's catch (401/403 + WWW-Authenticate).
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from "node:http";
9
+ import type { PrismaClient } from "#prisma";
10
+ import type { McpRequestContext } from "../mcp/context.js";
11
+ import type { ViewerEventBus } from "./event-bus.js";
12
+ export declare const VIEWER_API_PREFIX = "/admin/api/";
13
+ export declare const SSE_HEARTBEAT_MS = 15000;
14
+ /** A stream whose unsent backlog exceeds this is ended; the client reconnects and resyncs. */
15
+ export declare const SSE_MAX_BUFFERED_BYTES: number;
16
+ export interface ViewerApiDeps {
17
+ db: PrismaClient;
18
+ bus: ViewerEventBus;
19
+ authenticate: (authorization: string | undefined) => Promise<McpRequestContext>;
20
+ canonicalUri: string;
21
+ heartbeatMs?: number;
22
+ maxBufferedBytes?: number;
23
+ }
24
+ export interface ViewerApi {
25
+ /** Returns false when the path is not a viewer route (caller falls through to 404). */
26
+ handle(req: IncomingMessage, res: ServerResponse, url: URL): Promise<boolean>;
27
+ /** Ends every open event stream (server shutdown). */
28
+ closeStreams(): void;
29
+ }
30
+ export declare function createViewerApi(deps: ViewerApiDeps): ViewerApi;
@@ -0,0 +1,133 @@
1
+ import { requireScope } from "../mcp/auth/resource-server.js";
2
+ import { sendJson } from "../mcp/transport/streamable-http.js";
3
+ import { DEFAULT_GRAPH_LIMIT, MAX_GRAPH_LIMIT, loadGraph, parseSince } from "./graph.js";
4
+ import { loadRunDetail } from "./run-detail.js";
5
+ export const VIEWER_API_PREFIX = "/admin/api/";
6
+ export const SSE_HEARTBEAT_MS = 15_000;
7
+ /** A stream whose unsent backlog exceeds this is ended; the client reconnects and resyncs. */
8
+ export const SSE_MAX_BUFFERED_BYTES = 1024 * 1024;
9
+ const RUN_ID = /^[A-Za-z0-9_-]{1,64}$/;
10
+ export function createViewerApi(deps) {
11
+ const heartbeatMs = deps.heartbeatMs ?? SSE_HEARTBEAT_MS;
12
+ const maxBufferedBytes = deps.maxBufferedBytes ?? SSE_MAX_BUFFERED_BYTES;
13
+ const streams = new Set();
14
+ let nextEventId = 0;
15
+ function openStream(req, res) {
16
+ // The client may have gone while we awaited authentication; its close
17
+ // events already fired and will not fire again, so subscribing now would leak.
18
+ if (req.destroyed || res.destroyed || res.writableEnded)
19
+ return;
20
+ res.writeHead(200, {
21
+ "content-type": "text/event-stream",
22
+ "cache-control": "no-store",
23
+ connection: "keep-alive",
24
+ "x-accel-buffering": "no",
25
+ });
26
+ let done = false;
27
+ // A client that stops reading (or reads too slowly) must not grow server
28
+ // memory without bound: past the limit, drop the stream (its backlog with
29
+ // it); the client reconnects and resyncs.
30
+ const send = (frame) => {
31
+ if (done)
32
+ return;
33
+ res.write(frame);
34
+ if (res.writableLength > maxBufferedBytes) {
35
+ cleanup();
36
+ res.destroy();
37
+ }
38
+ };
39
+ res.write("retry: 3000\n\n");
40
+ res.write(`event: hello\ndata: ${JSON.stringify({ connected: deps.bus.connected() })}\n\n`);
41
+ const unsubscribe = deps.bus.subscribe((event) => {
42
+ send(`id: ${++nextEventId}\nevent: ${event.kind}\ndata: ${JSON.stringify(event)}\n\n`);
43
+ });
44
+ // Every transition to live (including the bus's first connect, which
45
+ // usually lands just after this stream opens) may follow missed events, so
46
+ // tell the client to refetch the graph. Going down only changes status.
47
+ const unstate = deps.bus.onState((live) => {
48
+ send(`event: status\ndata: ${JSON.stringify({ connected: live })}\n\n`);
49
+ if (live)
50
+ send("event: resync\ndata: {}\n\n");
51
+ });
52
+ const heartbeat = setInterval(() => send(": ping\n\n"), heartbeatMs);
53
+ const cleanup = () => {
54
+ if (done)
55
+ return;
56
+ done = true;
57
+ clearInterval(heartbeat);
58
+ unsubscribe();
59
+ unstate();
60
+ streams.delete(close);
61
+ };
62
+ const close = () => {
63
+ cleanup();
64
+ // Close the connection with the stream, so shutdown never waits for the
65
+ // keep-alive timeout.
66
+ res.shouldKeepAlive = false;
67
+ res.end();
68
+ };
69
+ streams.add(close);
70
+ req.on("close", cleanup);
71
+ res.on("close", cleanup);
72
+ res.on("error", cleanup);
73
+ }
74
+ return {
75
+ async handle(req, res, url) {
76
+ const path = url.pathname;
77
+ const runMatch = /^\/admin\/api\/runs\/([^/]*)$/.exec(path);
78
+ const isGraph = path === "/admin/api/graph";
79
+ const isEvents = path === "/admin/api/events";
80
+ if (!isGraph && !isEvents && !runMatch)
81
+ return false;
82
+ const ctx = await deps.authenticate(req.headers.authorization);
83
+ requireScope(ctx, deps.canonicalUri, "admin:view");
84
+ if (req.method !== "GET") {
85
+ sendJson(res, 405, { error: "method_not_allowed" }, { allow: "GET" });
86
+ return true;
87
+ }
88
+ if (isGraph) {
89
+ const limitParam = url.searchParams.get("limit");
90
+ let limit = DEFAULT_GRAPH_LIMIT;
91
+ if (limitParam !== null) {
92
+ limit = /^\d{1,6}$/.test(limitParam) ? Number(limitParam) : 0;
93
+ if (limit < 1 || limit > MAX_GRAPH_LIMIT) {
94
+ sendJson(res, 400, { error: "invalid_limit" });
95
+ return true;
96
+ }
97
+ }
98
+ const now = new Date();
99
+ let since;
100
+ try {
101
+ since = parseSince(url.searchParams.get("since"), now);
102
+ }
103
+ catch {
104
+ sendJson(res, 400, { error: "invalid_since" });
105
+ return true;
106
+ }
107
+ sendJson(res, 200, await loadGraph(deps.db, { since, limit, now }));
108
+ return true;
109
+ }
110
+ if (runMatch) {
111
+ let id = "";
112
+ try {
113
+ id = decodeURIComponent(runMatch[1]);
114
+ }
115
+ catch {
116
+ /* malformed escape: fails the id check below */
117
+ }
118
+ const detail = RUN_ID.test(id) ? await loadRunDetail(deps.db, id) : null;
119
+ if (!detail)
120
+ sendJson(res, 404, { error: "not_found" });
121
+ else
122
+ sendJson(res, 200, detail);
123
+ return true;
124
+ }
125
+ openStream(req, res);
126
+ return true;
127
+ },
128
+ closeStreams() {
129
+ for (const close of [...streams])
130
+ close();
131
+ },
132
+ };
133
+ }
@@ -0,0 +1,4 @@
1
+ /** The run detail read model behind GET /admin/api/runs/:id (docs/viewer-api.md). */
2
+ import type { PrismaClient } from "#prisma";
3
+ import type { RunDetail } from "./api-schema.js";
4
+ export declare function loadRunDetail(db: PrismaClient, runId: string): Promise<RunDetail | null>;
@@ -0,0 +1,61 @@
1
+ import { loadGraphRuns } from "./graph.js";
2
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
3
+ /** Reads the CodingRun.services snapshot. Env values are never copied: names only, and serviceEnv is never read. */
4
+ function serviceDetails(snapshot) {
5
+ if (!Array.isArray(snapshot))
6
+ return [];
7
+ return snapshot.filter(isRecord).map((s) => ({
8
+ name: String(s.name),
9
+ version: String(s.version),
10
+ image: String(s.image),
11
+ envNames: isRecord(s.testEnv) ? Object.keys(s.testEnv).sort() : [],
12
+ }));
13
+ }
14
+ export async function loadRunDetail(db, runId) {
15
+ const [graphRun] = await loadGraphRuns(db, [runId]);
16
+ if (!graphRun)
17
+ return null;
18
+ const [row, children] = await Promise.all([
19
+ db.run.findUniqueOrThrow({
20
+ where: { id: runId },
21
+ select: {
22
+ error: true,
23
+ finalText: true,
24
+ codingRun: {
25
+ select: {
26
+ provider: true,
27
+ repository: true,
28
+ baseRef: true,
29
+ headRef: true,
30
+ queuedAt: true,
31
+ failureCategory: true,
32
+ services: true,
33
+ },
34
+ },
35
+ },
36
+ }),
37
+ db.run.findMany({
38
+ where: { parentRunId: runId },
39
+ select: { id: true },
40
+ orderBy: [{ startedAt: "asc" }, { id: "asc" }],
41
+ }),
42
+ ]);
43
+ const coding = row.codingRun;
44
+ return {
45
+ ...graphRun,
46
+ error: row.error,
47
+ finalText: row.finalText,
48
+ childRunIds: children.map((c) => c.id),
49
+ coding: coding
50
+ ? {
51
+ provider: coding.provider,
52
+ repository: coding.repository,
53
+ baseRef: coding.baseRef,
54
+ headRef: coding.headRef,
55
+ queuedAt: coding.queuedAt ? coding.queuedAt.toISOString() : null,
56
+ failureCategory: coding.failureCategory,
57
+ services: serviceDetails(coding.services),
58
+ }
59
+ : null,
60
+ };
61
+ }
@@ -42,6 +42,11 @@ async function main() {
42
42
  const { helpCommand } = await import("./help/cli.js");
43
43
  await helpCommand(process.argv.slice(3));
44
44
  }
45
+ else if (command === "knowledge") {
46
+ // Offline bundle validation: no environment, database, or HTTP runtime.
47
+ const { knowledgeCommand } = await import("./knowledge/cli.js");
48
+ await knowledgeCommand(process.argv.slice(3));
49
+ }
45
50
  else {
46
51
  await import("./cli.js");
47
52
  }
package/docs/README.md CHANGED
@@ -8,8 +8,16 @@
8
8
  configures an existing OAuth/OIDC provider to protect remote MCP access.
9
9
  - [Runtime architecture](architecture-runtime.md) explains the control-plane,
10
10
  provider, persistence, and execution boundaries.
11
+ - [Models and pricing](models.md) covers the model catalog: the shipped models,
12
+ adding or overriding one with `set_model`, disabling and resetting, and how
13
+ runs are billed.
14
+ - [Agent recipes](agent-recipes.md) gives two copyable setups, an architecture
15
+ keeper and a per-language builder, with the prerequisites each needs.
11
16
  - [Coding-agent setup](coding-agent-setup.md) configures the local trusted proxy
12
17
  and isolated Codex or Claude Code workers.
18
+ - [Architecture knowledge bundles](knowledge.md) covers `docs/knowledge/`: the
19
+ concept format, how coding runs and reviewers use it, `wardby knowledge check`,
20
+ and the scheduled architecture agent.
13
21
  - [Coding-worker isolation](coding-worker-isolation.md) documents the threat
14
22
  model and enforced worker boundary.
15
23
  - [Bring-your-own worker images](coding-worker-byo-images.md) explains how to
@@ -19,6 +27,8 @@
19
27
  - [Services for coding runs](coding-services.md) gives coding runs fresh
20
28
  PostgreSQL, Redis or MySQL instances declared in `.wardby/services.yaml`:
21
29
  the catalog, per-agent permission, variables, and errors.
30
+ - [Jira agents](jira-agents.md) connects wardby to Jira Cloud with a service
31
+ account and webhook, and links agents to projects.
22
32
  - [Observability](observability.md) covers Prometheus metrics, Grafana, cloud
23
33
  collectors, retention, and production ownership.
24
34
  - [Release verification](release-verification.md) lists the automated and live