@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
@@ -50,6 +50,19 @@ export interface LoadedTool {
50
50
  export type StepRunner = <T>(name: string, fn: () => Promise<T>) => Promise<T>;
51
51
  /** The no-checkpoint default: run the step body directly. */
52
52
  export declare const runStepInline: StepRunner;
53
+ /**
54
+ * Live progress after a completed model turn: absolute cumulative totals,
55
+ * never deltas, so re-delivering one (a DBOS replay re-runs the loop over
56
+ * recorded steps) rewrites the same values instead of double counting.
57
+ */
58
+ export interface EngineProgress {
59
+ turns: number;
60
+ usage: {
61
+ tokensIn: number;
62
+ tokensOut: number;
63
+ costUsd: number;
64
+ };
65
+ }
53
66
  export interface EngineRunContext {
54
67
  agent: EngineAgent;
55
68
  tools: LoadedTool[];
@@ -57,6 +70,8 @@ export interface EngineRunContext {
57
70
  /** Host bridge: validates params (Zod-in-sandbox) then runs the tool body in the WASM sandbox. Never throws — a tool/validation failure is a JSON error result string, fed back to the model as the tool's result. */
58
71
  runSandboxTool(name: string, argsJson: string): Promise<string>;
59
72
  onText?: (delta: string) => void;
73
+ /** Called after each model turn that completed without error; the engine awaits it. */
74
+ onProgress?: (progress: EngineProgress) => Promise<void>;
60
75
  /** See StepRunner. Optional; the in-process executor leaves it unset. */
61
76
  step?: StepRunner;
62
77
  }
@@ -69,6 +84,10 @@ export interface EngineResult {
69
84
  tokensIn: number;
70
85
  tokensOut: number;
71
86
  costUsd: number;
87
+ /** Subset of tokensIn served from a prompt cache. */
88
+ cachedInputTokens?: number;
89
+ /** Tokens billed for writing prompt-cache entries (not part of tokensIn). */
90
+ cacheWriteTokens?: number;
72
91
  };
73
92
  error?: string;
74
93
  }
@@ -3,6 +3,7 @@ import { resolve } from "node:path";
3
3
  import { loadCodingConcurrencyConfig, loadContainerExecutorConfig, loadGitHubVcsConfig, loadKubernetesJobConfig, loadProviderConfig, } from "../../config/providers.js";
4
4
  import { summarizeRegistryFetches } from "../../coding/registry/report.js";
5
5
  import { drainCodingQueue } from "../../core/coding-queue.js";
6
+ import { prismaServiceStateReporter } from "../../core/coding-service-status.js";
6
7
  import { logger } from "../../core/logger.js";
7
8
  import { EnvironmentCredentialResolver } from "../coding-proxy/environment-credentials.js";
8
9
  import { CodingProxy } from "../coding-proxy/proxy.js";
@@ -17,6 +18,7 @@ import { buildVcsProvider } from "../vcs/index.js";
17
18
  import { ContainerExecutor, PrismaContainerExecutionStore, RunCapabilityVault } from "./container.js";
18
19
  import { PrismaExecutionKindResolver, RoutingExecutor } from "./routing.js";
19
20
  import { createRepoAccessGate } from "../../core/repo-access.js";
21
+ import { buildIssueTrackers } from "../issue-tracker/index.js";
20
22
  import { buildReviewHosts } from "../review-host/index.js";
21
23
  const compositionLog = logger.child({ module: "executor-composition" });
22
24
  /** Builds container execution only when Docker or Kubernetes is explicitly selected. */
@@ -41,6 +43,7 @@ export function buildConfiguredExecutor(options) {
41
43
  ledger: new PrismaProxyLedger(options.db),
42
44
  credentials: new EnvironmentCredentialResolver(env),
43
45
  });
46
+ const onServiceState = prismaServiceStateReporter(options.db);
44
47
  let jobs;
45
48
  if (providerConfig.jobs === "kubernetes") {
46
49
  if (!isRegistryDigest(config.workerImage)) {
@@ -65,6 +68,7 @@ export function buildConfiguredExecutor(options) {
65
68
  const api = options.kubernetesApi ?? new ClientNodeKubernetesApi({ context: kubernetes.context });
66
69
  const workerImage = config.workerImage;
67
70
  jobs = new KubernetesJobLauncher({
71
+ onServiceState,
68
72
  api,
69
73
  config: kubernetes,
70
74
  workspaceRoot,
@@ -87,6 +91,7 @@ export function buildConfiguredExecutor(options) {
87
91
  else {
88
92
  const stateRoot = resolve(config.stateRoot ?? resolve(tmpdir(), "wardby-docker-jobs"));
89
93
  jobs = new DockerJobLauncher({
94
+ onServiceState,
90
95
  stateRoot,
91
96
  workspaceRoot,
92
97
  proxyContainer: proxyContainer,
@@ -100,12 +105,13 @@ export function buildConfiguredExecutor(options) {
100
105
  },
101
106
  });
102
107
  }
108
+ const issueTrackers = buildIssueTrackers(env);
103
109
  const concurrency = loadCodingConcurrencyConfig(env);
104
110
  // The release hook needs the composed RoutingExecutor, which only exists
105
111
  // after the ContainerExecutor it wraps is built. The closure reads
106
112
  // `composed` only when a run finishes, which is after this function returns.
107
113
  const coding = new ContainerExecutor({
108
- store: new PrismaContainerExecutionStore(options.db, { maxConcurrent: concurrency.maxConcurrent }),
114
+ store: new PrismaContainerExecutionStore(options.db, { maxConcurrent: concurrency.maxConcurrent, issueTrackers }),
109
115
  jobs,
110
116
  vcs,
111
117
  sessions,
@@ -121,6 +127,7 @@ export function buildConfiguredExecutor(options) {
121
127
  limits: { cpus: config.cpus, memoryMb: config.memoryMb, pids: config.pids, diskMb: config.diskMb },
122
128
  maxDiskMb: config.maxDiskMb,
123
129
  repoAccess: options.repoAccess ?? createRepoAccessGate({ db: options.db, hosts: buildReviewHosts(env) }),
130
+ issueUrl: (provider, key) => (provider === "jira" ? issueTrackers.jira?.issueUrl(key) : undefined),
124
131
  registryReport: async (runId) => {
125
132
  const rows = await options.db.registryFetch.findMany({ where: { runId }, orderBy: { createdAt: "asc" } });
126
133
  return summarizeRegistryFetches(rows);
@@ -131,6 +138,7 @@ export function buildConfiguredExecutor(options) {
131
138
  executor: composed,
132
139
  maxConcurrent: concurrency.maxConcurrent,
133
140
  queueTimeoutSec: concurrency.queueTimeoutSec,
141
+ selfDefects: { db: options.db, issueTrackers },
134
142
  }).catch((err) => {
135
143
  // Never throws: the next scheduler tick drains again.
136
144
  compositionLog.warn({ err }, "coding queue drain after a released slot failed");
@@ -1,9 +1,10 @@
1
1
  import type { PrismaClient } from "#prisma";
2
- import type { CodingProvider } from "../../coding/provider.js";
2
+ import { type CodingProvider } from "../../coding/provider.js";
3
+ import type { IssueTrackerRegistry } from "../issue-tracker/types.js";
3
4
  import { type RepoAccessGate } from "../../core/repo-access.js";
4
5
  import { type CodingRunObserver } from "../../coding/observability.js";
5
6
  import { type CodingRunResult } from "../../coding/protocol.js";
6
- import type { ProxyProtocol } from "../coding-proxy/types.js";
7
+ import type { ProxyModelTerms, ProxyProtocol } from "../coding-proxy/types.js";
7
8
  import type { JobHandle, JobResourceLimits, WorkspaceJobLauncher } from "../jobs/types.js";
8
9
  import type { VcsProvider } from "../vcs/types.js";
9
10
  import type { CodingImageSelector, ExecutionRecoveryResult, Executor, PersistedExecutionHandle } from "./types.js";
@@ -32,6 +33,9 @@ export interface ContainerRunSnapshot {
32
33
  collectExclude: unknown;
33
34
  /** Revision-in-place: set when this run continues another run's branch/PR. See preflight(). */
34
35
  rootCodingRunId: string | null;
36
+ /** The issue that triggered this run (CodingRun.issueProvider/issueKey); null when none. */
37
+ issueProvider?: string | null;
38
+ issueKey?: string | null;
35
39
  budgetUsd: number;
36
40
  /** The agent's own per-run budget; `budgetUsd` is less when its budget group had less left. */
37
41
  agentBudgetUsd?: number;
@@ -57,6 +61,9 @@ export interface ContainerRunSnapshot {
57
61
  profileRepository: string | null;
58
62
  /** How the profile's repository was authorized (CodingAgentProfile.repositoryAuthorizedVia). */
59
63
  repositoryAuthorizedVia: string | null;
64
+ /** The run's catalog entry recorded at dispatch (Run.pricingSnapshot); null on runs from before the catalog. */
65
+ pricingVersion: string | null;
66
+ pricingSnapshot: unknown;
60
67
  }
61
68
  /**
62
69
  * claimed: this caller owns provisioning. unavailable: someone else does, or
@@ -82,7 +89,13 @@ export declare class PrismaContainerExecutionStore implements ContainerExecution
82
89
  private readonly options;
83
90
  constructor(db: PrismaClient, options?: {
84
91
  maxConcurrent?: number;
92
+ /** Configured issue trackers: a coding run this store ends failed/lost/budget_exhausted files a self-defect. */
93
+ issueTrackers?: IssueTrackerRegistry;
85
94
  });
95
+ /** Best effort (bounded, never throws): only called by the write that made the row terminal, so it files once. */
96
+ private selfDefect;
97
+ /** An empty registry (no Jira site configured) means no self-defect queries at all. */
98
+ private get hasTrackers();
86
99
  load(runId: string): Promise<ContainerRunSnapshot | null>;
87
100
  claimProvisioning(runId: string, claimId: string): Promise<ProvisioningClaim>;
88
101
  persistHandle(runId: string, claimId: string, handle: JobHandle): Promise<void>;
@@ -98,6 +111,8 @@ export interface CodingSessionController {
98
111
  allowedModels: string[];
99
112
  deadlineAt: Date;
100
113
  budgetUsd: number;
114
+ /** The run's catalog entry; the proxy prices and shapes the run's requests from it. */
115
+ terms?: ProxyModelTerms;
101
116
  }): Promise<{
102
117
  id: string;
103
118
  capability: string;
@@ -159,6 +174,8 @@ export interface ContainerExecutorOptions {
159
174
  * reporting failure must never fail the run itself.
160
175
  */
161
176
  registryReport?: (runId: string) => Promise<RegistryReport>;
177
+ /** Browse URL for an issue of a tracker provider (composition wires the issue-tracker registry); undefined when unconfigured. */
178
+ issueUrl?: (provider: string, key: string) => string | undefined;
162
179
  /**
163
180
  * Repository authorization: every run is checked, before its workspace is
164
181
  * prepared, against the agent owner's current GitHub access (or a recorded
@@ -224,6 +241,10 @@ export declare class ContainerExecutor implements Executor {
224
241
  * access. Throws RepoAccessError (refused before anything is cloned).
225
242
  */
226
243
  private authorizeRepository;
244
+ /** The originating issue for the PR title/body: control-plane data from the CodingRun row, key re-validated. */
245
+ private issueFor;
246
+ /** The run's stored model terms; a run from before the catalog resolves them from the current catalog. */
247
+ private modelTerms;
227
248
  private preflight;
228
249
  private finishTerminal;
229
250
  /** The package report is informational: any failure loading it (a
@@ -241,6 +262,13 @@ export declare class ContainerExecutor implements Executor {
241
262
  repository: string;
242
263
  baseRef: string;
243
264
  }): Promise<string | null>;
265
+ /** One repository file at the run's base ref (Executor.readCodingRepositoryFile). */
266
+ readCodingRepositoryFile(input: {
267
+ repository: string;
268
+ baseRef: string;
269
+ path: string;
270
+ maxBytes: number;
271
+ }): Promise<string | null>;
244
272
  /** The job launcher decides (Kubernetes and Docker start services for both providers). */
245
273
  supportsCodingServices(provider: CodingProvider): boolean;
246
274
  private requireCurrent;
@@ -1,23 +1,27 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { mkdir, realpath, rename, rm, writeFile } from "node:fs/promises";
3
3
  import { join, resolve, sep } from "node:path";
4
+ import { withBaseCommit } from "../../coding/base-commit.js";
4
5
  import { normalizeCollectExclusions } from "../../coding/collect-exclude.js";
6
+ import { codingProviderForModelProvider } from "../../coding/provider.js";
5
7
  import { CodingProfileSchema } from "../../coding/profile.js";
6
8
  import { parseStoredServices, storedServiceNames, workerServices } from "../../coding/services/catalog.js";
7
9
  import { MAX_SERVICE_DECLARATION_BYTES, SERVICE_DECLARATION_PATH } from "../../coding/services/declaration.js";
8
10
  import { SERVICE_UNREADY_CATEGORY, SERVICE_UNREADY_ERROR, serviceUnreadyName, serviceUnreadySentence, } from "../../coding/services/wording.js";
9
11
  import { PROTECTED_PATH_CATEGORY, protectedPathFromError, protectedPathSentence, } from "../../coding/protected-path-wording.js";
10
12
  import { budgetSentence } from "../../core/budget-wording.js";
13
+ import { fileSelfDefect } from "../../core/self-defects.js";
11
14
  import { classifyProviderFailure, providerClassOfCategory, providerSentence, } from "../../core/provider-wording.js";
12
15
  import { logger } from "../../core/logger.js";
13
16
  import { requiredLevel } from "../../core/repo-access.js";
14
17
  import { ensurePrivateDirectory } from "../../core/private-directory.js";
15
18
  import { codingRunObserver } from "../../coding/observability.js";
16
19
  import { CODING_PROTOCOL_VERSION, CodingRunResultSchema, CodingTaskInputSchema, parseCodingAgentOutputJson, redactAndTruncate, } from "../../coding/protocol.js";
17
- import { getModelPricing } from "../llm/pricing.js";
18
- import { getAnthropicPricing } from "../llm/pricing-anthropic.js";
20
+ import { parseStoredEntry } from "../llm/catalog-types.js";
21
+ import { resolveCodingEntry } from "../../core/run-pricing.js";
19
22
  import { CODING_PROXY_ALIAS, CODING_PROXY_PORT, isImmutableDockerImage } from "../jobs/docker-isolation.js";
20
23
  import { normalizeRegistryLockfiles } from "../../coding/registry/lockfiles.js";
24
+ import { ISSUE_KEY, ISSUE_TRACKER_NAMES } from "../issue-tracker/types.js";
21
25
  import { HEARTBEAT_TIMEOUT_MS } from "../../core/timing.js";
22
26
  const containerLog = logger.child({ module: "container-executor" });
23
27
  const TERMINAL_STATUSES = new Set(["succeeded", "failed", "refused", "lost", "budget_exhausted", "cancelled"]);
@@ -62,6 +66,15 @@ export class PrismaContainerExecutionStore {
62
66
  this.db = db;
63
67
  this.options = options;
64
68
  }
69
+ /** Best effort (bounded, never throws): only called by the write that made the row terminal, so it files once. */
70
+ async selfDefect(run) {
71
+ if (this.hasTrackers)
72
+ await fileSelfDefect(this.db, this.options.issueTrackers, run);
73
+ }
74
+ /** An empty registry (no Jira site configured) means no self-defect queries at all. */
75
+ get hasTrackers() {
76
+ return Object.values(this.options.issueTrackers ?? {}).some(Boolean);
77
+ }
65
78
  async load(runId) {
66
79
  const row = await this.db.run.findUnique({
67
80
  where: { id: runId },
@@ -88,6 +101,8 @@ export class PrismaContainerExecutionStore {
88
101
  protectedPaths: row.codingRun.protectedPaths,
89
102
  collectExclude: row.codingRun.collectExclude,
90
103
  rootCodingRunId: row.codingRun.rootCodingRunId,
104
+ issueProvider: row.codingRun.issueProvider,
105
+ issueKey: row.codingRun.issueKey,
91
106
  budgetUsd: Number(row.codingRun.budgetReservedUsd),
92
107
  agentBudgetUsd: Number(row.agent.budgetUsd),
93
108
  budgetGroupName: row.agent.budgetGroup?.name ?? null,
@@ -108,6 +123,8 @@ export class PrismaContainerExecutionStore {
108
123
  failureCategory: row.codingRun.failureCategory,
109
124
  profileRepository: row.agent.codingProfile?.repository ?? null,
110
125
  repositoryAuthorizedVia: row.agent.codingProfile?.repositoryAuthorizedVia ?? null,
126
+ pricingVersion: row.pricingVersion,
127
+ pricingSnapshot: row.pricingSnapshot,
111
128
  };
112
129
  }
113
130
  async claimProvisioning(runId, claimId) {
@@ -220,13 +237,14 @@ export class PrismaContainerExecutionStore {
220
237
  });
221
238
  }
222
239
  async complete(runId, status, result) {
223
- await this.db.$transaction(async (tx) => {
240
+ const finishedAt = new Date();
241
+ const finished = await this.db.$transaction(async (tx) => {
224
242
  const run = await tx.run.findUnique({ where: { id: runId }, include: { codingRun: true } });
225
243
  if (!run?.codingRun)
226
244
  throw new Error("coding_run_not_found");
227
245
  if (TERMINAL_STATUSES.has(run.status)) {
228
246
  if (run.codingRun.result && stableJson(run.codingRun.result) === stableJson(result))
229
- return;
247
+ return null;
230
248
  throw new Error("coding_run_terminal_conflict");
231
249
  }
232
250
  await tx.codingRun.update({
@@ -238,26 +256,40 @@ export class PrismaContainerExecutionStore {
238
256
  data: {
239
257
  status,
240
258
  finalText: result.summary,
241
- finishedAt: new Date(),
259
+ finishedAt,
242
260
  heartbeatAt: new Date(),
243
261
  error: null,
244
262
  },
245
263
  });
264
+ return { id: runId, agentId: run.agentId };
246
265
  });
266
+ if (finished && status === "budget_exhausted") {
267
+ await this.selfDefect({ ...finished, status, error: null, finishedAt });
268
+ }
247
269
  }
248
270
  async terminate(runId, status, error, audit) {
249
- await this.db.$transaction(async (tx) => {
271
+ const finishedAt = new Date();
272
+ const agentId = await this.db.$transaction(async (tx) => {
250
273
  const updated = await tx.run.updateMany({
251
274
  where: { id: runId, status: { in: ["pending", "running"] } },
252
- data: { status, error, finishedAt: new Date(), heartbeatAt: new Date() },
275
+ data: { status, error, finishedAt, heartbeatAt: new Date() },
253
276
  });
254
- if (updated.count && audit) {
277
+ if (!updated.count)
278
+ return null;
279
+ if (audit) {
255
280
  await tx.codingRun.update({
256
281
  where: { runId },
257
282
  data: { failureCategory: audit.failureCategory, diagnosticId: audit.diagnosticId },
258
283
  });
259
284
  }
285
+ if (!this.hasTrackers)
286
+ return null;
287
+ const row = await tx.run.findUnique({ where: { id: runId }, select: { agentId: true } });
288
+ return row?.agentId ?? null;
260
289
  });
290
+ // After commit, so the failure category is readable; cancelled/refused are skipped inside fileSelfDefect.
291
+ if (agentId)
292
+ await this.selfDefect({ id: runId, agentId, status, error, finishedAt });
261
293
  }
262
294
  }
263
295
  /** Plain capabilities live only for the short provisioning window. */
@@ -470,11 +502,12 @@ export class ContainerExecutor {
470
502
  allowedModels: [run.model],
471
503
  deadlineAt,
472
504
  budgetUsd: run.budgetUsd,
505
+ terms: this.modelTerms(run),
473
506
  });
474
507
  spendEnabled = true;
475
508
  sessionId = session.id;
476
509
  this.options.capabilities.set(runId, session.capability);
477
- const inputArtifact = await this.writeInput(run, deadlineAt);
510
+ const inputArtifact = await this.writeInput(run, deadlineAt, workspace.baseCommit);
478
511
  const beforeLaunch = await this.requireCurrent(runId);
479
512
  if (TERMINAL_STATUSES.has(beforeLaunch.status))
480
513
  throw new Error("coding_run_no_longer_active");
@@ -609,18 +642,50 @@ export class ContainerExecutor {
609
642
  if (!decision.ok)
610
643
  throw new RepoAccessError(`repo_access_${decision.reason}`);
611
644
  }
645
+ /** The originating issue for the PR title/body: control-plane data from the CodingRun row, key re-validated. */
646
+ issueFor(run) {
647
+ const { issueProvider, issueKey } = run;
648
+ if (!issueProvider || !issueKey || !ISSUE_KEY.test(issueKey))
649
+ return {};
650
+ let url;
651
+ try {
652
+ url = this.options.issueUrl?.(issueProvider, issueKey);
653
+ }
654
+ catch {
655
+ url = undefined;
656
+ }
657
+ const trackerName = Object.hasOwn(ISSUE_TRACKER_NAMES, issueProvider)
658
+ ? ISSUE_TRACKER_NAMES[issueProvider]
659
+ : undefined;
660
+ return {
661
+ issue: {
662
+ key: issueKey,
663
+ ...(url?.startsWith("https://") ? { url } : {}),
664
+ ...(trackerName ? { trackerName } : {}),
665
+ },
666
+ };
667
+ }
668
+ /** The run's stored model terms; a run from before the catalog resolves them from the current catalog. */
669
+ modelTerms(run) {
670
+ if (run.provider !== "codex" && run.provider !== "claude-code")
671
+ throw new Error("coding_provider_unsupported");
672
+ const stored = parseStoredEntry(run.pricingSnapshot);
673
+ if (stored && run.pricingVersion) {
674
+ if (stored.modelId !== run.model || codingProviderForModelProvider(stored.provider) !== run.provider) {
675
+ throw new Error("coding_run_pricing_mismatch");
676
+ }
677
+ return { version: run.pricingVersion, entry: stored };
678
+ }
679
+ const entry = resolveCodingEntry(run.provider, run.model);
680
+ return { version: entry.priceVersion, entry: parseStoredEntry(entry) };
681
+ }
612
682
  preflight(run) {
613
683
  try {
614
684
  if (run.status !== "pending" && run.status !== "running")
615
685
  throw new Error("coding_run_status_invalid");
616
686
  if (run.agentKind !== "coding" || !run.ownerId)
617
687
  throw new Error("coding_run_ownership_invalid");
618
- if (run.provider === "codex")
619
- getModelPricing(run.model);
620
- else if (run.provider === "claude-code")
621
- getAnthropicPricing(run.model);
622
- else
623
- throw new Error("coding_provider_unsupported");
688
+ this.modelTerms(run);
624
689
  if (!Number.isFinite(run.budgetUsd) || run.budgetUsd <= 0 || run.costUsd > run.budgetUsd) {
625
690
  throw new Error("coding_run_budget_invalid");
626
691
  }
@@ -744,6 +809,7 @@ export class ContainerExecutor {
744
809
  tests: output.tests,
745
810
  tag: output.tag,
746
811
  ...report,
812
+ ...this.issueFor(current),
747
813
  });
748
814
  const result = this.resultFor(output, current, finalized.outcome, finalized);
749
815
  await this.options.store.complete(run.runId, "succeeded", result);
@@ -884,6 +950,17 @@ export class ContainerExecutor {
884
950
  maxBytes: MAX_SERVICE_DECLARATION_BYTES,
885
951
  });
886
952
  }
953
+ /** One repository file at the run's base ref (Executor.readCodingRepositoryFile). */
954
+ async readCodingRepositoryFile(input) {
955
+ if (!this.options.vcs.readRepositoryFile)
956
+ return null;
957
+ return this.options.vcs.readRepositoryFile({
958
+ repository: input.repository,
959
+ ref: input.baseRef,
960
+ path: input.path,
961
+ maxBytes: input.maxBytes,
962
+ });
963
+ }
887
964
  /** The job launcher decides (Kubernetes and Docker start services for both providers). */
888
965
  supportsCodingServices(provider) {
889
966
  return this.options.jobs.supportsServicesFor?.(provider) === true;
@@ -929,7 +1006,7 @@ export class ContainerExecutor {
929
1006
  return this.options.anthropicCredentialRef;
930
1007
  throw new Error("coding_provider_not_configured:claude-code");
931
1008
  }
932
- async writeInput(run, deadlineAt) {
1009
+ async writeInput(run, deadlineAt, baseCommit) {
933
1010
  await ensurePrivateDirectory(this.artifactRoot);
934
1011
  const rootReal = await realpath(this.artifactRoot);
935
1012
  const directory = resolve(rootReal, run.runId);
@@ -941,13 +1018,17 @@ export class ContainerExecutor {
941
1018
  const destination = join(directory, "input.json");
942
1019
  const temporary = join(directory, `.input-${randomUUID()}.tmp`);
943
1020
  const services = parseStoredServices(run.services);
1021
+ const task = withBaseCommit(run.task, baseCommit);
1022
+ if (task === run.task && /^[0-9a-f]{40}$/.test(baseCommit)) {
1023
+ containerLog.warn({ event: "coding.base_commit_omitted", runId: run.runId }, "the coding task left no room for the base commit line");
1024
+ }
944
1025
  const input = CodingTaskInputSchema.parse({
945
1026
  schemaVersion: CODING_PROTOCOL_VERSION,
946
1027
  runId: run.runId,
947
1028
  repository: run.repository,
948
1029
  baseRef: run.baseRef,
949
1030
  headRef: run.headRef,
950
- task: run.task,
1031
+ task,
951
1032
  model: run.model,
952
1033
  budgetUsd: run.budgetUsd,
953
1034
  deadlineAt: deadlineAt.toISOString(),
@@ -38,6 +38,8 @@ export declare class DbosExecutor implements Executor {
38
38
  get executorId(): string;
39
39
  launch(): Promise<void>;
40
40
  close(): Promise<void>;
41
+ /** Where a run this executor marks failed files its self-defect (no-op without a configured issue tracker). */
42
+ private get selfDefects();
41
43
  start(runId: string): Promise<void>;
42
44
  stop(runId: string, reason?: string): Promise<void>;
43
45
  /**
@@ -133,6 +133,10 @@ export class DbosExecutor {
133
133
  setActiveExecutor(undefined);
134
134
  await DBOS.shutdown({ workflowCompletionTimeoutMS: 5_000 });
135
135
  }
136
+ /** Where a run this executor marks failed files its self-defect (no-op without a configured issue tracker). */
137
+ get selfDefects() {
138
+ return { db: this.db, issueTrackers: this.providers.issueTrackers };
139
+ }
136
140
  async start(runId) {
137
141
  if (!this.launched)
138
142
  await this.launch();
@@ -165,7 +169,7 @@ export class DbosExecutor {
165
169
  // the same failure — dispatchRun's own `.catch` around `start()` calls
166
170
  // it too — which is harmless precisely because it is conditional and
167
171
  // idempotent: the second call matches zero rows.
168
- await markRunFailedFromExecutorError(this.db, runId, err);
172
+ await markRunFailedFromExecutorError(this.db, runId, err, this.selfDefects);
169
173
  throw err;
170
174
  }
171
175
  }
@@ -241,10 +245,8 @@ export class DbosExecutor {
241
245
  return { state: "terminal" };
242
246
  case "mark-failed":
243
247
  this.resumeAttempts.delete(handle.runId);
244
- await this.db.run.updateMany({
245
- where: { id: handle.runId, status: { in: ["pending", "running"] } },
246
- data: { status: "failed", error: decision.error, finishedAt: new Date() },
247
- });
248
+ // Conditional (only a pending/running row), so a self-defect is filed only when this made the run failed.
249
+ await markRunFailedFromExecutorError(this.db, handle.runId, decision.error, this.selfDefects);
248
250
  return { state: "terminal" };
249
251
  case "lost":
250
252
  this.resumeAttempts.delete(handle.runId);
@@ -27,6 +27,12 @@ export declare class RoutingExecutor implements Executor {
27
27
  repository: string;
28
28
  baseRef: string;
29
29
  }): Promise<string | null>;
30
+ readCodingRepositoryFile(input: {
31
+ repository: string;
32
+ baseRef: string;
33
+ path: string;
34
+ maxBytes: number;
35
+ }): Promise<string | null>;
30
36
  supportsCodingServices(provider: CodingProvider): boolean;
31
37
  }
32
38
  export declare class PrismaExecutionKindResolver implements ExecutionKindResolver {
@@ -64,6 +64,11 @@ export class RoutingExecutor {
64
64
  return null;
65
65
  return this.coding.readCodingServiceDeclaration(input);
66
66
  }
67
+ async readCodingRepositoryFile(input) {
68
+ if (!this.coding.readCodingRepositoryFile)
69
+ return null;
70
+ return this.coding.readCodingRepositoryFile(input);
71
+ }
67
72
  supportsCodingServices(provider) {
68
73
  return this.coding.supportsCodingServices?.(provider) === true;
69
74
  }
@@ -68,6 +68,18 @@ export interface Executor {
68
68
  repository: string;
69
69
  baseRef: string;
70
70
  }): Promise<string | null>;
71
+ /**
72
+ * The raw text of one repository file at `baseRef`, or null when absent.
73
+ * Throws (github_file_too_large, github_file_not_a_file, github_file_not_utf8)
74
+ * when the file is oversized or not UTF-8 text. Called by dispatch before its transaction (network call).
75
+ * Used for the knowledge note (src/knowledge/note.ts).
76
+ */
77
+ readCodingRepositoryFile?(input: {
78
+ repository: string;
79
+ baseRef: string;
80
+ path: string;
81
+ maxBytes: number;
82
+ }): Promise<string | null>;
71
83
  /**
72
84
  * Whether this executor can start coding-run services for a run of `provider`: its job launcher
73
85
  * says so (Kubernetes and Docker, for Codex and Claude Code).
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Atlassian Document Format helpers. Jira Cloud's v3 API takes and returns
3
+ * rich text as ADF; webhook payloads use the v2 shape (wiki-markup strings),
4
+ * so the readers (adfToText, adfMentionIds) accept both. Agents write a
5
+ * small Markdown subset (markdownToAdf); the control plane reads issue text
6
+ * as plain text (adfToText). Nothing here
7
+ * produces HTML or mention nodes: a model can never ping a person.
8
+ */
9
+ export interface AdfNode {
10
+ type: string;
11
+ version?: number;
12
+ attrs?: Record<string, unknown>;
13
+ content?: AdfNode[];
14
+ text?: string;
15
+ marks?: Array<{
16
+ type: string;
17
+ attrs?: Record<string, unknown>;
18
+ }>;
19
+ }
20
+ /**
21
+ * Smart links (ADF inlineCard, what Jira's editor makes from a pasted issue link) for this site's
22
+ * issues: a bare `<siteUrl>/browse/KEY-1` URL, and a bare `KEY-1` whose project is in `projectKeys`
23
+ * (only those, so look-alikes such as UTF-8 or SHA-256 stay text).
24
+ */
25
+ export interface SmartLinkOptions {
26
+ siteUrl: string;
27
+ projectKeys?: readonly string[];
28
+ }
29
+ export declare function markdownToAdf(markdown: string, smart?: SmartLinkOptions): AdfNode;
30
+ export declare function adfToText(node: unknown, maxChars?: number): string;
31
+ export declare function adfMentionIds(node: unknown): string[];