@wardby/cli 0.8.0 → 0.9.0

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 (123) hide show
  1. package/deploy/local/docker-compose.quickstart-coding.yml +5 -0
  2. package/dist/claude-plugin/command.d.ts +4 -1
  3. package/dist/claude-plugin/command.js +7 -4
  4. package/dist/cli.js +24 -0
  5. package/dist/coding/transcript/recorder.js +2 -8
  6. package/dist/core/dispatch.js +3 -0
  7. package/dist/core/engine-native.d.ts +2 -0
  8. package/dist/core/engine-native.js +13 -0
  9. package/dist/core/error-code.js +1 -0
  10. package/dist/core/listen-address.d.ts +10 -0
  11. package/dist/core/listen-address.js +14 -0
  12. package/dist/core/power-idle.d.ts +8 -2
  13. package/dist/core/power-idle.js +17 -3
  14. package/dist/core/power.d.ts +5 -0
  15. package/dist/core/power.js +5 -0
  16. package/dist/core/retention-days.d.ts +5 -0
  17. package/dist/core/retention-days.js +14 -0
  18. package/dist/core/run-steps/config.d.ts +25 -0
  19. package/dist/core/run-steps/config.js +40 -0
  20. package/dist/core/run-steps/control-chars.d.ts +16 -0
  21. package/dist/core/run-steps/control-chars.js +26 -0
  22. package/dist/core/run-steps/detail.d.ts +32 -0
  23. package/dist/core/run-steps/detail.js +240 -0
  24. package/dist/core/run-steps/extract.d.ts +52 -0
  25. package/dist/core/run-steps/extract.js +467 -0
  26. package/dist/core/run-steps/ingress.d.ts +51 -0
  27. package/dist/core/run-steps/ingress.js +160 -0
  28. package/dist/core/run-steps/last-step.d.ts +15 -0
  29. package/dist/core/run-steps/last-step.js +35 -0
  30. package/dist/core/run-steps/native.d.ts +48 -0
  31. package/dist/core/run-steps/native.js +164 -0
  32. package/dist/core/run-steps/seen.d.ts +16 -0
  33. package/dist/core/run-steps/seen.js +35 -0
  34. package/dist/core/run-steps/summarize.d.ts +57 -0
  35. package/dist/core/run-steps/summarize.js +304 -0
  36. package/dist/core/run-steps/sweep.d.ts +21 -0
  37. package/dist/core/run-steps/sweep.js +41 -0
  38. package/dist/core/run-steps/types.d.ts +89 -0
  39. package/dist/core/run-steps/types.js +39 -0
  40. package/dist/core/run-steps/wire.d.ts +35 -0
  41. package/dist/core/run-steps/wire.js +47 -0
  42. package/dist/core/run-steps/writer.d.ts +49 -0
  43. package/dist/core/run-steps/writer.js +279 -0
  44. package/dist/core/runner.d.ts +4 -0
  45. package/dist/core/runner.js +15 -1
  46. package/dist/generated/prisma/browser.d.ts +8 -0
  47. package/dist/generated/prisma/client.d.ts +8 -0
  48. package/dist/generated/prisma/commonInputTypes.d.ts +30 -0
  49. package/dist/generated/prisma/enums.d.ts +6 -0
  50. package/dist/generated/prisma/enums.js +5 -0
  51. package/dist/generated/prisma/internal/class.d.ts +11 -0
  52. package/dist/generated/prisma/internal/class.js +4 -4
  53. package/dist/generated/prisma/internal/prismaNamespace.d.ts +103 -1
  54. package/dist/generated/prisma/internal/prismaNamespace.js +19 -1
  55. package/dist/generated/prisma/internal/prismaNamespaceBrowser.d.ts +19 -0
  56. package/dist/generated/prisma/internal/prismaNamespaceBrowser.js +19 -1
  57. package/dist/generated/prisma/models/Agent.d.ts +101 -1
  58. package/dist/generated/prisma/models/CodingProxySession.d.ts +41 -1
  59. package/dist/generated/prisma/models/Run.d.ts +503 -1
  60. package/dist/generated/prisma/models/RunStep.d.ts +1453 -0
  61. package/dist/generated/prisma/models/RunStep.js +1 -0
  62. package/dist/generated/prisma/models.d.ts +1 -0
  63. package/dist/help-index.json +102 -14
  64. package/dist/mcp/auth/access.d.ts +1 -0
  65. package/dist/mcp/context.d.ts +3 -0
  66. package/dist/mcp/index.js +10 -0
  67. package/dist/mcp/tools/agents.js +37 -7
  68. package/dist/mcp/tools/power.js +2 -2
  69. package/dist/mcp/tools/run-access.d.ts +7 -0
  70. package/dist/mcp/tools/run-access.js +14 -0
  71. package/dist/mcp/tools/run-steps.d.ts +16 -0
  72. package/dist/mcp/tools/run-steps.js +85 -0
  73. package/dist/mcp/tools/runs.js +10 -2
  74. package/dist/native-worker/gateway-server.d.ts +4 -1
  75. package/dist/native-worker/gateway-server.js +10 -2
  76. package/dist/native-worker/gateway.d.ts +3 -0
  77. package/dist/native-worker/gateway.js +8 -0
  78. package/dist/native-worker/http-server.d.ts +2 -4
  79. package/dist/native-worker/http-server.js +2 -8
  80. package/dist/providers/coding-proxy/memory-ledger.js +1 -0
  81. package/dist/providers/coding-proxy/prisma-ledger.js +9 -3
  82. package/dist/providers/coding-proxy/proxy.d.ts +26 -0
  83. package/dist/providers/coding-proxy/proxy.js +133 -42
  84. package/dist/providers/coding-proxy/run-step-forwarder.d.ts +42 -0
  85. package/dist/providers/coding-proxy/run-step-forwarder.js +206 -0
  86. package/dist/providers/coding-proxy/runtime.d.ts +7 -0
  87. package/dist/providers/coding-proxy/runtime.js +55 -3
  88. package/dist/providers/coding-proxy/types.d.ts +5 -0
  89. package/dist/providers/engine/types.d.ts +26 -0
  90. package/dist/providers/executor/container.d.ts +14 -0
  91. package/dist/providers/executor/container.js +8 -1
  92. package/dist/providers/jobs/kubernetes.d.ts +8 -0
  93. package/dist/providers/jobs/kubernetes.js +22 -4
  94. package/dist/quickstart-images.json +1 -1
  95. package/dist/serve.js +26 -18
  96. package/dist/viewer/api-schema.d.ts +66 -0
  97. package/dist/viewer/api-schema.js +31 -0
  98. package/dist/viewer/openapi.js +2 -1
  99. package/dist/viewer/run-detail.d.ts +2 -0
  100. package/dist/viewer/run-detail.js +26 -1
  101. package/docs/claude-code-plugin.md +57 -13
  102. package/docs/coding-agent-setup.md +17 -0
  103. package/docs/coding-worker-isolation.md +16 -0
  104. package/docs/debugging-agent-runs.md +112 -0
  105. package/docs/getting-started-gke.md +28 -7
  106. package/docs/mcp-servers.md +9 -0
  107. package/docs/native-sandbox.md +6 -0
  108. package/docs/security-deployment.md +51 -0
  109. package/docs/viewer-api.md +21 -12
  110. package/docs/viewer-api.openapi.yml +78 -2
  111. package/help/admin-viewer.md +1 -0
  112. package/help/claude-code-run-pane.md +27 -4
  113. package/help/deploy-gke.md +5 -0
  114. package/help/errors/coding-mcp-gateway-unavailable.md +46 -0
  115. package/help/low-power-mode.md +19 -8
  116. package/help/mcp.md +2 -0
  117. package/help/resume-runs-locally.md +4 -0
  118. package/help/run-steps.md +140 -0
  119. package/package.json +1 -1
  120. package/prisma/migrations/20261110000000_run_steps/migration.sql +26 -0
  121. package/prisma/migrations/20261111000000_run_steps_viewer_notify/migration.sql +59 -0
  122. package/prisma/migrations/20261112000000_run_steps_detail/migration.sql +9 -0
  123. package/prisma/schema.prisma +52 -0
@@ -1,6 +1,7 @@
1
1
  import { loadDependencyCacheConfig } from "../../config/dependency-cache.js";
2
2
  import { createSimulatorModelFetch, createSimulatorRegistryFetch, startupTestSimulators, } from "../../config/test-simulators.js";
3
3
  import { logger } from "../../core/logger.js";
4
+ import { loadRunStepsLevel } from "../../core/run-steps/config.js";
4
5
  import { REGISTRY_ADAPTERS } from "../../coding/registry/adapters.js";
5
6
  import { CODING_PROXY_ALIAS, CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "../jobs/docker-isolation.js";
6
7
  import { createBlobStore } from "../storage/index.js";
@@ -14,6 +15,7 @@ import { createMcpRoute } from "./mcp-route.js";
14
15
  import { createMockUpstream, mockUpstreamFromEnv } from "./mock-upstream.js";
15
16
  import { CodingProxy } from "./proxy.js";
16
17
  import { PrismaProxyLedger } from "./prisma-ledger.js";
18
+ import { RunStepForwarder } from "./run-step-forwarder.js";
17
19
  import { OsvAudit } from "./registry/audit.js";
18
20
  import { PrismaRegistryStore } from "./registry/prisma-store.js";
19
21
  import { DEFAULT_PLAN_MAX_ENTRIES, DEFAULT_PLAN_TIMEOUT_MS } from "./registry/plan.js";
@@ -71,6 +73,34 @@ export function loadMcpGatewayUrl(env) {
71
73
  }
72
74
  return url.href.replace(/\/+$/, "");
73
75
  }
76
+ /**
77
+ * WARDBY_RUN_STEPS_URL: the control plane's run-steps listener (WARDBY_RUN_STEPS_LISTEN on the
78
+ * server), which the proxy posts coding runs' steps to. Unset or empty: coding runs record no steps.
79
+ * Anything but an http(s) origin without credentials, path, query or fragment stops start-up (the
80
+ * value is not echoed, so a credential in it never reaches a log).
81
+ */
82
+ export function loadRunStepsUrl(env) {
83
+ const value = env.WARDBY_RUN_STEPS_URL?.trim();
84
+ if (!value)
85
+ return undefined;
86
+ let url;
87
+ try {
88
+ url = new URL(value);
89
+ }
90
+ catch {
91
+ url = undefined;
92
+ }
93
+ if (!url ||
94
+ (url.protocol !== "http:" && url.protocol !== "https:") ||
95
+ url.username ||
96
+ url.password ||
97
+ url.pathname !== "/" ||
98
+ url.search ||
99
+ url.hash) {
100
+ throw new Error("WARDBY_RUN_STEPS_URL must be an http(s) URL without credentials, path, query or fragment");
101
+ }
102
+ return url.origin;
103
+ }
74
104
  /** Starts the trusted proxy with the fixed worker-only Docker endpoint, plus the deny port the enforcement witness probes. */
75
105
  export async function startConfiguredCodingProxy(options) {
76
106
  const env = options.env ?? process.env;
@@ -86,6 +116,10 @@ export async function startConfiguredCodingProxy(options) {
86
116
  : options.audit;
87
117
  // Throws on an invalid setting, before anything listens.
88
118
  const mcpGatewayUrl = loadMcpGatewayUrl(env);
119
+ // Throws on an invalid setting, before anything listens.
120
+ const runStepsUrl = loadRunStepsUrl(env);
121
+ // Throws on an invalid WARDBY_RUN_STEPS. At off no step text leaves the proxy.
122
+ const runStepsCeiling = loadRunStepsLevel(env);
89
123
  // Throws (test_simulators_refused) on any refusal, before anything listens.
90
124
  const simulators = startupTestSimulators("coding-proxy", env);
91
125
  const buildPinnedFetch = options.createPinnedFetch ?? createPinnedProxyFetch;
@@ -152,6 +186,10 @@ export async function startConfiguredCodingProxy(options) {
152
186
  logger.warn({ module: "coding-proxy-runtime", event: "proxy.dependency_cache_no_blob_store" }, "coding proxy: WARDBY_DEPENDENCY_CACHE=true but no BLOB_STORE is configured; the dependency cache is off");
153
187
  }
154
188
  const seal = modelRequestSeal;
189
+ // Coding runs' steps go to the control plane only when WARDBY_RUN_STEPS_URL is set and
190
+ // WARDBY_RUN_STEPS is not off; otherwise no
191
+ // submitter is passed and the proxy does no step extraction.
192
+ const steps = runStepsUrl && runStepsCeiling !== "off" ? new RunStepForwarder({ url: runStepsUrl }) : undefined;
155
193
  const ledger = new PrismaProxyLedger(options.db);
156
194
  const proxy = new CodingProxy({
157
195
  ledger,
@@ -166,6 +204,8 @@ export async function startConfiguredCodingProxy(options) {
166
204
  : {}),
167
205
  // The run's first model request closes its dependency cache (no-op for a run with it off).
168
206
  ...(seal ? { onModelRequest: (runId) => seal.onModelRequest(runId) } : {}),
207
+ ...(steps ? { steps } : {}),
208
+ runStepsCeiling,
169
209
  });
170
210
  const startServer = options.startServer ?? startCodingProxyServer;
171
211
  let server;
@@ -182,6 +222,7 @@ export async function startConfiguredCodingProxy(options) {
182
222
  }
183
223
  catch (error) {
184
224
  eviction?.stop();
225
+ await steps?.close();
185
226
  throw error;
186
227
  }
187
228
  let deny;
@@ -192,15 +233,26 @@ export async function startConfiguredCodingProxy(options) {
192
233
  // A proxy without its witness must not stay up: the launcher's gate would never be able to
193
234
  // prove a policy is enforced, and every launch would fail at the gate instead of at startup.
194
235
  eviction?.stop();
195
- await server.close();
236
+ try {
237
+ await server.close();
238
+ }
239
+ finally {
240
+ await steps?.close();
241
+ }
196
242
  throw error;
197
243
  }
198
244
  return {
199
245
  port: server.port,
200
246
  close: async () => {
201
247
  eviction?.stop();
202
- await deny.close();
203
- await server.close();
248
+ try {
249
+ await deny.close();
250
+ await server.close();
251
+ }
252
+ finally {
253
+ // Last: requests are done, so this is the final chance to hand over queued steps (bounded).
254
+ await steps?.close();
255
+ }
204
256
  },
205
257
  };
206
258
  }
@@ -1,4 +1,5 @@
1
1
  import { type CatalogEntry } from "../llm/catalog-types.js";
2
+ import type { RunStepsLevel } from "../../core/run-steps/config.js";
2
3
  import type { ModelPricing } from "../llm/pricing-core.js";
3
4
  import type { ProxyRefusalDetails } from "../../core/budget-events.js";
4
5
  import type { McpToolPin } from "../../core/mcp-tools.js";
@@ -53,6 +54,8 @@ export interface ProxySession {
53
54
  mcpTokenHash: string | null;
54
55
  /** The run's MCP tools/call count so far. */
55
56
  mcpCallCount: number;
57
+ /** The run's Run.runStepsLevel, copied at session creation; null on sessions from before run steps detail. */
58
+ runStepsLevel: RunStepsLevel | null;
56
59
  }
57
60
  export interface ProxyRequest {
58
61
  id: string;
@@ -100,6 +103,8 @@ export interface CreateProxySessionInput {
100
103
  mcpTools?: McpToolPin[];
101
104
  /** SHA-256 (base64url) of the run's MCP token (mcpTokenHash in src/coding/mcp-token.ts). */
102
105
  mcpTokenHash?: string;
106
+ /** The run's level, fixed at dispatch; absent counts as summary. */
107
+ runStepsLevel?: RunStepsLevel;
103
108
  }
104
109
  export interface ReserveProxyRequestInput {
105
110
  id: string;
@@ -117,9 +117,35 @@ export interface EngineRunContext {
117
117
  onText?: (delta: string) => void;
118
118
  /** Called after each model turn that completed without error; the engine awaits it. */
119
119
  onProgress?: (progress: EngineProgress) => Promise<void>;
120
+ /**
121
+ * Receives each model turn's text and tool calls, then the turn's tool results, as the engine saw
122
+ * them. The runner turns them into the run's steps (src/core/run-steps); the engine never imports
123
+ * that code, so the sandbox worker image does not ship it. Synchronous and not awaited. A replay
124
+ * delivers the same call ids again; the writer drops the repeats.
125
+ */
126
+ onTurn?: (event: EngineTurnEvent) => void;
120
127
  /** See StepRunner. Optional; the in-process executor leaves it unset. */
121
128
  step?: StepRunner;
122
129
  }
130
+ /** What EngineRunContext.onTurn receives: a turn's output, then (if it called tools) their results in call order. */
131
+ export type EngineTurnEvent = {
132
+ kind: "turn";
133
+ /** 1-based turn number. */
134
+ turn: number;
135
+ text: string;
136
+ toolCalls: readonly {
137
+ id: string;
138
+ name: string;
139
+ argsJson: string;
140
+ }[];
141
+ } | {
142
+ kind: "results";
143
+ turn: number;
144
+ toolCalls: readonly {
145
+ id: string;
146
+ }[];
147
+ results: readonly string[];
148
+ };
123
149
  export type EngineStatus = "succeeded" | "budget_exhausted" | "refused" | "failed";
124
150
  export interface EngineResult {
125
151
  status: EngineStatus;
@@ -1,3 +1,4 @@
1
+ import type { RunStepsLevel } from "../../core/run-steps/config.js";
1
2
  import { type PackageAuditEntry, type PackageAuditMode } from "../../coding/registry/audit-mode.js";
2
3
  import type { PrismaClient } from "#prisma";
3
4
  import type { RunTranscriptRecorder } from "../../coding/transcript/recorder.js";
@@ -101,6 +102,8 @@ export type StoredDescriptionBaseline = DescriptionBaseline & {
101
102
  legacySource?: LegacyDescriptionSource;
102
103
  };
103
104
  export interface ContainerRunSnapshot {
105
+ /** Run.runStepsLevel, fixed at dispatch; absent on older snapshots = summary. */
106
+ runStepsLevel?: RunStepsLevel | null;
104
107
  runId: string;
105
108
  agentId: string;
106
109
  status: string;
@@ -338,6 +341,8 @@ export interface CodingSessionController {
338
341
  mcpTools?: McpToolPin[];
339
342
  /** The hash of the run's MCP token (src/coding/mcp-token.ts); the token itself stays in the vault. */
340
343
  mcpTokenHash?: string;
344
+ /** The run's run-steps level (Run.runStepsLevel); the proxy reads tool output only at detail. */
345
+ runStepsLevel?: RunStepsLevel;
341
346
  }): Promise<{
342
347
  id: string;
343
348
  capability: string;
@@ -757,3 +762,12 @@ export declare function describeFailure(error: unknown, depth?: number): string;
757
762
  export declare const MCP_PINS_UNAVAILABLE_ERROR = "coding_mcp_pins_unavailable";
758
763
  /** Its category: the run's error is coding_failure_mcp_pins_unavailable:<diagnostic id>. */
759
764
  export declare const MCP_PINS_UNAVAILABLE_CATEGORY = "mcp_pins_unavailable";
765
+ /** Low-power mode: the native gateway serving a run's MCP tools did not wake; nothing launched. */
766
+ export declare const MCP_GATEWAY_UNAVAILABLE_CATEGORY = "mcp_gateway_unavailable";
767
+ /**
768
+ * Error-code prefixes that name their category outright, checked before the
769
+ * substring heuristics below. Those heuristics filed every `github_*` error
770
+ * (a failed PR lookup or create after the push, a token mint) under
771
+ * "workspace" because "github" contains "git".
772
+ */
773
+ export declare const CATEGORY_BY_PREFIX: ReadonlyArray<readonly [prefix: string, category: string]>;
@@ -5,6 +5,7 @@ import { join, resolve, sep } from "node:path";
5
5
  import { withBaseCommit } from "../../coding/base-commit.js";
6
6
  import { mcpTokenHash, mintMcpToken } from "../../coding/mcp-token.js";
7
7
  import { isCodexCoding } from "../../core/mcp-tools.js";
8
+ import { CODING_MCP_GATEWAY_UNAVAILABLE } from "../../core/power.js";
8
9
  import { codeProviderOf } from "../../coding/code-host.js";
9
10
  import { withPullRequestDescription } from "../../coding/pr-description-note.js";
10
11
  import { withLinkedIssue } from "../../coding/linked-issue-note.js";
@@ -185,6 +186,7 @@ export class PrismaContainerExecutionStore {
185
186
  : null;
186
187
  return {
187
188
  runId: row.id,
189
+ runStepsLevel: row.runStepsLevel,
188
190
  agentId: row.agentId,
189
191
  status: row.status,
190
192
  agentKind: row.agent.kind,
@@ -882,6 +884,7 @@ export class ContainerExecutor {
882
884
  budgetUsd: run.budgetUsd,
883
885
  terms: this.modelTerms(run),
884
886
  ...(mcpToken ? { mcpTools, mcpTokenHash: mcpTokenHash(mcpToken) } : {}),
887
+ ...(run.runStepsLevel ? { runStepsLevel: run.runStepsLevel } : {}),
885
888
  });
886
889
  spendEnabled = true;
887
890
  sessionId = session.id;
@@ -2485,13 +2488,15 @@ function safeError(error) {
2485
2488
  export const MCP_PINS_UNAVAILABLE_ERROR = "coding_mcp_pins_unavailable";
2486
2489
  /** Its category: the run's error is coding_failure_mcp_pins_unavailable:<diagnostic id>. */
2487
2490
  export const MCP_PINS_UNAVAILABLE_CATEGORY = "mcp_pins_unavailable";
2491
+ /** Low-power mode: the native gateway serving a run's MCP tools did not wake; nothing launched. */
2492
+ export const MCP_GATEWAY_UNAVAILABLE_CATEGORY = "mcp_gateway_unavailable";
2488
2493
  /**
2489
2494
  * Error-code prefixes that name their category outright, checked before the
2490
2495
  * substring heuristics below. Those heuristics filed every `github_*` error
2491
2496
  * (a failed PR lookup or create after the push, a token mint) under
2492
2497
  * "workspace" because "github" contains "git".
2493
2498
  */
2494
- const CATEGORY_BY_PREFIX = [
2499
+ export const CATEGORY_BY_PREFIX = [
2495
2500
  // Claude Code reached the agent's turn limit (codingProfile.maxTurns) before finishing.
2496
2501
  ["job_coding_turn_limit", "turn_limit"],
2497
2502
  // A service sidecar never became ready (src/providers/jobs/kubernetes.ts).
@@ -2523,6 +2528,8 @@ const CATEGORY_BY_PREFIX = [
2523
2528
  ["vcs_", "workspace"],
2524
2529
  // The agent's MCP tool pins could not be read at launch: nothing was launched.
2525
2530
  [MCP_PINS_UNAVAILABLE_ERROR, MCP_PINS_UNAVAILABLE_CATEGORY],
2531
+ // Low-power mode: the native gateway serving the run's MCP tools did not wake (jobs/kubernetes.ts).
2532
+ [CODING_MCP_GATEWAY_UNAVAILABLE, MCP_GATEWAY_UNAVAILABLE_CATEGORY],
2526
2533
  // Local repositories (LocalRepoError codes): the same family as the vcs_ errors.
2527
2534
  ["local_", "workspace"],
2528
2535
  ["git_", "workspace"],
@@ -100,6 +100,14 @@ export declare class KubernetesJobLauncher implements WorkspaceJobLauncher {
100
100
  hasCapacityFor(spec: JobSpec): Promise<boolean>;
101
101
  private readonly warnedOnce;
102
102
  private warnOnce;
103
+ /**
104
+ * Wakes what the run needs before anything is created: the coding proxy always, and the native
105
+ * gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
106
+ * worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
107
+ * the slower of the two, not their sum. A gateway that does not wake fails the launch with
108
+ * coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
109
+ */
110
+ private wakeForRun;
103
111
  launch(spec: JobSpec): Promise<JobHandle>;
104
112
  status(handle: JobHandle): Promise<JobStatus>;
105
113
  collect(handle: JobHandle): Promise<JobResult>;
@@ -29,7 +29,7 @@ import { finished } from "node:stream/promises";
29
29
  import { createHash } from "node:crypto";
30
30
  import { isIP } from "node:net";
31
31
  import { logger } from "../../core/logger.js";
32
- import { DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
32
+ import { CODING_MCP_GATEWAY_UNAVAILABLE, DISABLED_POWER, wakeTimeoutMs } from "../../core/power.js";
33
33
  import { MAX_CODING_ARTIFACT_BYTES, MAX_CODING_INPUT_BYTES, MAX_DEBUG_TRACE_LINE_BYTES, parseCodingWorkerOutputJson, } from "../../coding/protocol.js";
34
34
  import { CODING_PROXY_DENY_PORT, CODING_PROXY_PORT } from "./docker-isolation.js";
35
35
  import { parseWorkerDiagnosticLine } from "./docker.js";
@@ -500,6 +500,25 @@ export class KubernetesJobLauncher {
500
500
  this.warnedOnce.add(message);
501
501
  this.warn(message);
502
502
  }
503
+ /**
504
+ * Wakes what the run needs before anything is created: the coding proxy always, and the native
505
+ * gateway too for a run with MCP tools (its MCP calls go proxy -> gateway; without a gateway the
506
+ * worker would start with its MCP tools missing). Both wake at once, so a cold start waits for
507
+ * the slower of the two, not their sum. A gateway that does not wake fails the launch with
508
+ * coding_mcp_gateway_unavailable; the proxy's own wake error is reported first.
509
+ */
510
+ async wakeForRun(spec) {
511
+ const opts = { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId };
512
+ const [proxy, gateway] = await Promise.allSettled([
513
+ this.power.ensureAwake("proxy", opts),
514
+ spec.mcpTools ? this.power.ensureAwake("gateway", opts) : Promise.resolve(),
515
+ ]);
516
+ if (proxy.status === "rejected")
517
+ throw proxy.reason;
518
+ if (gateway.status === "rejected") {
519
+ throw new Error(`${CODING_MCP_GATEWAY_UNAVAILABLE}: the native gateway, which serves this run's MCP tools, did not wake.`, { cause: gateway.reason });
520
+ }
521
+ }
503
522
  async launch(spec) {
504
523
  try {
505
524
  validateKubernetesSpec(spec);
@@ -516,9 +535,8 @@ export class KubernetesJobLauncher {
516
535
  }
517
536
  // Low-power mode: the proxy may be scaled to zero; wake it before the preflight reads it. Guarded
518
537
  // (not just a no-op await) so mode off keeps launch() joining an in-flight warm-up synchronously.
519
- if (this.power.enabled) {
520
- await this.power.ensureAwake("proxy", { timeoutMs: wakeTimeoutMs(this.readyTimeoutMs), runId: spec.runId });
521
- }
538
+ if (this.power.enabled)
539
+ await this.wakeForRun(spec);
522
540
  await this.runPreflight();
523
541
  const names = kubernetesRunNames(spec.runId);
524
542
  const handle = { backend: BACKEND, id: `${this.namespace}/${names.token}` };
@@ -1 +1 @@
1
- {"runtime":"ghcr.io/wardby/wardby/wardby-runtime@sha256:8efd21e571c4da6529494ef1a422f208b3d81764dab83e581abe2e5a2eaf08d3","worker":"ghcr.io/wardby/wardby/wardby-coding-worker@sha256:93c8191a4d89077f0aa60eb698543a9c390bba9cc5319bf814734e2075d39c20","claudeWorker":"ghcr.io/wardby/wardby/wardby-claude-coding-worker@sha256:464176af56a8692ca1d1202d04fc0311ea98cd0ba9d0671433f91912c2699492","claudeToolRunner":"ghcr.io/wardby/wardby/wardby-claude-tool-runner@sha256:e475ef7c7070c4a913bde2665bfe65a65fc0ff0db82a2c58953b9b4a453b277d","workerNodePython":"ghcr.io/wardby/wardby/wardby-coding-worker-node-python@sha256:02b04ccf6348706240c27a28615a3e17b3d8c0f828f20da0ae8d3af75e3eb9e5","claudeToolRunnerNodePython":"ghcr.io/wardby/wardby/wardby-claude-tool-runner-node-python@sha256:868eb258552ac23f17bc275e2bfe0619b9cf70e4bef075f93fa75d085ee735b4","nativeWorker":"ghcr.io/wardby/wardby/wardby-native-worker@sha256:8dd742964dcaea1a6da078b12fd2091d0b6e1783c3deb1f45d691727c63b4dd0","driver":"ghcr.io/wardby/wardby/wardby-coding-worker-driver@sha256:281a7cabc53d6ff1977d1b68c1420b2ef598f90da3995cb13df27395a58a60ef"}
1
+ {"runtime":"ghcr.io/wardby/wardby/wardby-runtime@sha256:d5cb8fcfbfd43c230418374e9a5e203528b7d7fd372c01c8545284bc261edd58","worker":"ghcr.io/wardby/wardby/wardby-coding-worker@sha256:943a4023bcdc86d84fb923ebd97e3b0b0b5c0055ea81542cb9972ddc6ee36e69","claudeWorker":"ghcr.io/wardby/wardby/wardby-claude-coding-worker@sha256:861ef8f23bd70c329cde7013191371737d266b854678942d328906b29242d08f","claudeToolRunner":"ghcr.io/wardby/wardby/wardby-claude-tool-runner@sha256:247a870862a3b20d02b6d0a2a87ef7b3d2ae57a30478b6d517ae58535bf54260","workerNodePython":"ghcr.io/wardby/wardby/wardby-coding-worker-node-python@sha256:9684306a84da26ea057b6ccdaaa73f7377820a74bb30fe1740afb8c734da4300","claudeToolRunnerNodePython":"ghcr.io/wardby/wardby/wardby-claude-tool-runner-node-python@sha256:71bd16fd9635eee62b561771da9568120db25ec259807aa070c2eb7865dbbf0f","nativeWorker":"ghcr.io/wardby/wardby/wardby-native-worker@sha256:33606de7284c11dee354a44b6563cc7999fd135d71545c1e63244d867f1a7b2c","driver":"ghcr.io/wardby/wardby/wardby-coding-worker-driver@sha256:281a7cabc53d6ff1977d1b68c1420b2ef598f90da3995cb13df27395a58a60ef"}
package/dist/serve.js CHANGED
@@ -1,21 +1,3 @@
1
- /**
2
- * `wardby serve`: the complete wardby process. Runs the MCP server, the
3
- * scheduler, and the reconciler together on ONE provider set and ONE
4
- * executor, which is what a single-container deployment (Cloud Run, a lone
5
- * VM) needs - `wardby mcp` alone never fires a schedule.
6
- *
7
- * Why one executor: DbosExecutor is a per-process singleton and refuses to
8
- * launch twice under different ids (providers/executor/dbos.ts), and its
9
- * recovery decisions key on that id. Building providers once and sharing
10
- * them is the whole trick; it also gives scheduled native runs the same
11
- * `providers.executor` wiring MCP-triggered runs get, so sub-agent dispatch
12
- * from a scheduled run works (it does not from `wardby scheduler`).
13
- *
14
- * Why the scheduler and reconciler can run in every replica: the scheduler
15
- * elects one ticker via a Postgres lease and enforces at-most-once with row
16
- * locks regardless (core/scheduler.ts); the reconciler is deliberately not
17
- * lease-gated and is safe under concurrency (core/reconciler.ts).
18
- */
19
1
  import { loadCodingConcurrencyConfig, loadMcpConfig } from "./config/providers.js";
20
2
  import { drainCodingQueue } from "./core/coding-queue.js";
21
3
  import { prisma } from "./core/db.js";
@@ -28,6 +10,8 @@ import { markRunFailedFromExecutorError } from "./core/dispatch.js";
28
10
  import { startScheduler } from "./core/scheduler.js";
29
11
  import { drainDeferredRuns } from "./providers/executor/deferred.js";
30
12
  import { resolveDeferredCodingRun } from "./core/deferred-coding.js";
13
+ import { RunStepSweeper } from "./core/run-steps/sweep.js";
14
+ import { parseRunStepsListen, startRunStepIngress } from "./core/run-steps/ingress.js";
31
15
  import { buildMcpProviders, startMcp } from "./mcp/index.js";
32
16
  const serveLog = logger.child({ module: "serve" });
33
17
  export async function startServe(options = {}) {
@@ -39,6 +23,8 @@ export async function startServe(options = {}) {
39
23
  // Parsed before anything starts, so a malformed CODING_MAX_CONCURRENT or
40
24
  // CODING_QUEUE_TIMEOUT_SEC fails fast instead of after HTTP is listening.
41
25
  const concurrency = loadCodingConcurrencyConfig();
26
+ // Run steps from the coding proxy (POST /internal/run-steps); parsed early so a bad value stops start-up.
27
+ const runStepsListen = parseRunStepsListen(process.env.WARDBY_RUN_STEPS_LISTEN);
42
28
  const providers = options.providers ?? buildMcpProviders().providers;
43
29
  const power = providers.power ?? DISABLED_POWER;
44
30
  if (power.enabled) {
@@ -48,6 +34,21 @@ export async function startServe(options = {}) {
48
34
  }, (err) => serveLog.warn({ err }, "low_power_access_check_failed"));
49
35
  }
50
36
  const mcp = await startMcp({ providers, schedulerAttached: true });
37
+ // Its own port (not the MCP listener's): only the coding proxy reaches it.
38
+ let runStepIngress;
39
+ if (runStepsListen && providers.runSteps) {
40
+ try {
41
+ runStepIngress = await startRunStepIngress(runStepsListen, { db: prisma, sink: providers.runSteps });
42
+ }
43
+ catch (err) {
44
+ await mcp.close();
45
+ throw err;
46
+ }
47
+ serveLog.info({ host: runStepsListen.host, port: runStepsListen.port }, "run steps listener started");
48
+ }
49
+ else if (runStepsListen) {
50
+ serveLog.warn("WARDBY_RUN_STEPS_LISTEN is set but this server has no run-steps writer; not listening");
51
+ }
51
52
  const reviewHosts = providers.reviewHosts;
52
53
  const reconciler = startReconciler({
53
54
  executor: providers.executor,
@@ -67,6 +68,8 @@ export async function startServe(options = {}) {
67
68
  });
68
69
  const notifications = await startNotifications({ db: prisma, chat: providers.chat ?? {} });
69
70
  const selfDefects = { db: prisma, issueTrackers: providers.issueTrackers };
71
+ // Expired run steps (RUN_STEP_RETENTION_DAYS), from the scheduler leader only.
72
+ const runStepSweeper = new RunStepSweeper(prisma);
70
73
  const scheduler = startScheduler({
71
74
  executor: providers.executor,
72
75
  scope: options.scope,
@@ -82,6 +85,7 @@ export async function startServe(options = {}) {
82
85
  });
83
86
  // Scale idle coding-proxy/native-gateway Deployments to zero (no-op unless WARDBY_LOW_POWER=1).
84
87
  await power.sleepIfIdle().catch((err) => serveLog.warn({ err }, "power_sleep_failed"));
88
+ await runStepSweeper.maybeSweep();
85
89
  },
86
90
  });
87
91
  return {
@@ -92,6 +96,10 @@ export async function startServe(options = {}) {
92
96
  scheduler.stop();
93
97
  reconciler.stop();
94
98
  notifications.stop();
99
+ // Before mcp.close(), which flushes the run-steps writer: no batch arrives after the flush.
100
+ const ingress = runStepIngress;
101
+ if (ingress)
102
+ await new Promise((resolve) => ingress.close(() => resolve()));
95
103
  await mcp.close();
96
104
  },
97
105
  };
@@ -320,6 +320,21 @@ export declare const GraphSnapshotSchema: z.ZodObject<{
320
320
  }, z.core.$strip>>;
321
321
  }, z.core.$strip>;
322
322
  }, z.core.$strip>;
323
+ /**
324
+ * One recorded run step (docs/viewer-api.md): a tool call or what the model said, one line. Stored
325
+ * already redacted (src/core/run-steps/writer.ts); a step's call id and detail text are never sent.
326
+ */
327
+ export declare const RunStepSchema: z.ZodObject<{
328
+ seq: z.ZodNumber;
329
+ at: z.ZodString;
330
+ kind: z.ZodEnum<{
331
+ tool: "tool";
332
+ said: "said";
333
+ }>;
334
+ tool: z.ZodNullable<z.ZodString>;
335
+ summary: z.ZodString;
336
+ status: z.ZodNullable<z.ZodString>;
337
+ }, z.core.$strip>;
323
338
  export declare const RunDetailSchema: z.ZodObject<{
324
339
  id: z.ZodString;
325
340
  parentRunId: z.ZodNullable<z.ZodString>;
@@ -429,6 +444,19 @@ export declare const RunDetailSchema: z.ZodObject<{
429
444
  error: z.ZodNullable<z.ZodString>;
430
445
  finalText: z.ZodNullable<z.ZodString>;
431
446
  childRunIds: z.ZodArray<z.ZodString>;
447
+ steps: z.ZodArray<z.ZodObject<{
448
+ seq: z.ZodNumber;
449
+ at: z.ZodString;
450
+ kind: z.ZodEnum<{
451
+ tool: "tool";
452
+ said: "said";
453
+ }>;
454
+ tool: z.ZodNullable<z.ZodString>;
455
+ summary: z.ZodString;
456
+ status: z.ZodNullable<z.ZodString>;
457
+ }, z.core.$strip>>;
458
+ stepsOmitted: z.ZodNumber;
459
+ stepsDropped: z.ZodNumber;
432
460
  coding: z.ZodNullable<z.ZodObject<{
433
461
  provider: z.ZodString;
434
462
  repository: z.ZodString;
@@ -484,6 +512,18 @@ export declare const ViewerEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
484
512
  issue_status: "issue_status";
485
513
  host_check: "host_check";
486
514
  }>;
515
+ }, z.core.$strip>, z.ZodObject<{
516
+ kind: z.ZodLiteral<"step">;
517
+ runId: z.ZodString;
518
+ seq: z.ZodNumber;
519
+ stepKind: z.ZodEnum<{
520
+ tool: "tool";
521
+ said: "said";
522
+ }>;
523
+ tool: z.ZodNullable<z.ZodString>;
524
+ summary: z.ZodString;
525
+ status: z.ZodNullable<z.ZodString>;
526
+ at: z.ZodString;
487
527
  }, z.core.$strip>], "kind">;
488
528
  export type RunTriggerInfo = z.infer<typeof RunTriggerSchema>;
489
529
  export type Outcome = z.infer<typeof OutcomeSchema>;
@@ -491,6 +531,7 @@ export type ServiceStatus = z.infer<typeof ServiceStatusSchema>;
491
531
  export type GraphRun = z.infer<typeof GraphRunSchema>;
492
532
  export type GraphSnapshot = z.infer<typeof GraphSnapshotSchema>;
493
533
  export type RunDetail = z.infer<typeof RunDetailSchema>;
534
+ export type RunStepView = z.infer<typeof RunStepSchema>;
494
535
  export type ViewerEvent = z.infer<typeof ViewerEventSchema>;
495
536
  /** How this deployment runs coding jobs, for the desktop viewer's Infrastructure view (GET /admin/api/infra). */
496
537
  export declare const InfraInfoSchema: z.ZodObject<{
@@ -758,6 +799,19 @@ export declare const VIEWER_SCHEMAS: {
758
799
  error: z.ZodNullable<z.ZodString>;
759
800
  finalText: z.ZodNullable<z.ZodString>;
760
801
  childRunIds: z.ZodArray<z.ZodString>;
802
+ steps: z.ZodArray<z.ZodObject<{
803
+ seq: z.ZodNumber;
804
+ at: z.ZodString;
805
+ kind: z.ZodEnum<{
806
+ tool: "tool";
807
+ said: "said";
808
+ }>;
809
+ tool: z.ZodNullable<z.ZodString>;
810
+ summary: z.ZodString;
811
+ status: z.ZodNullable<z.ZodString>;
812
+ }, z.core.$strip>>;
813
+ stepsOmitted: z.ZodNumber;
814
+ stepsDropped: z.ZodNumber;
761
815
  coding: z.ZodNullable<z.ZodObject<{
762
816
  provider: z.ZodString;
763
817
  repository: z.ZodString;
@@ -813,6 +867,18 @@ export declare const VIEWER_SCHEMAS: {
813
867
  issue_status: "issue_status";
814
868
  host_check: "host_check";
815
869
  }>;
870
+ }, z.core.$strip>, z.ZodObject<{
871
+ kind: z.ZodLiteral<"step">;
872
+ runId: z.ZodString;
873
+ seq: z.ZodNumber;
874
+ stepKind: z.ZodEnum<{
875
+ tool: "tool";
876
+ said: "said";
877
+ }>;
878
+ tool: z.ZodNullable<z.ZodString>;
879
+ summary: z.ZodString;
880
+ status: z.ZodNullable<z.ZodString>;
881
+ at: z.ZodString;
816
882
  }, z.core.$strip>], "kind">;
817
883
  readonly "infra-info": z.ZodObject<{
818
884
  launcher: z.ZodEnum<{
@@ -123,10 +123,30 @@ export const GraphSnapshotSchema = z.object({
123
123
  runs: z.array(GraphRunSchema),
124
124
  spend: z.object({ todayUsd: z.number(), groups: z.array(BudgetGroupSpendSchema) }),
125
125
  });
126
+ /**
127
+ * One recorded run step (docs/viewer-api.md): a tool call or what the model said, one line. Stored
128
+ * already redacted (src/core/run-steps/writer.ts); a step's call id and detail text are never sent.
129
+ */
130
+ export const RunStepSchema = z.object({
131
+ seq: z.number().int(),
132
+ at: z.string(),
133
+ kind: z.enum(["tool", "said"]),
134
+ /** The tool's display name (`Bash`, `Edit`, `mcp:<tool>`); null for said. */
135
+ tool: z.string().nullable(),
136
+ summary: z.string(),
137
+ /** `ok`, `error` or `exit N` once the result is back; null until then and for said. */
138
+ status: z.string().nullable(),
139
+ });
126
140
  export const RunDetailSchema = GraphRunSchema.extend({
127
141
  error: z.string().nullable(),
128
142
  finalText: z.string().nullable(),
129
143
  childRunIds: z.array(z.string()),
144
+ /** The run's newest steps (at most 200), oldest first; see `stepsOmitted` for earlier ones. */
145
+ steps: z.array(RunStepSchema),
146
+ /** Earlier recorded steps not included in `steps`. */
147
+ stepsOmitted: z.number().int(),
148
+ /** Steps past the per-run limit, never recorded. */
149
+ stepsDropped: z.number().int(),
130
150
  coding: z
131
151
  .object({
132
152
  provider: z.string(),
@@ -170,6 +190,17 @@ export const ViewerEventSchema = z.discriminatedUnion("kind", [
170
190
  /** Which table changed (the viewer NOTIFY trigger names them). */
171
191
  source: z.enum(["pull_request", "host_status", "issue_status", "host_check"]),
172
192
  }),
193
+ /** A run recorded a step, or a step's status arrived (the RunStep NOTIFY trigger). */
194
+ z.object({
195
+ kind: z.literal("step"),
196
+ runId: z.string(),
197
+ seq: RunStepSchema.shape.seq,
198
+ stepKind: RunStepSchema.shape.kind,
199
+ tool: RunStepSchema.shape.tool,
200
+ summary: RunStepSchema.shape.summary,
201
+ status: RunStepSchema.shape.status,
202
+ at: RunStepSchema.shape.at,
203
+ }),
173
204
  ]);
174
205
  /** How this deployment runs coding jobs, for the desktop viewer's Infrastructure view (GET /admin/api/infra). */
175
206
  export const InfraInfoSchema = z.object({
@@ -59,7 +59,7 @@ export const VIEWER_ROUTES = [
59
59
  path: "/admin/api/runs/{id}",
60
60
  operationId: "getRun",
61
61
  summary: "Run detail",
62
- description: "Full detail for one run, including final text.",
62
+ description: "Full detail for one run, including final text and its newest steps.",
63
63
  parameters: [{ name: "id", in: "path", required: true, description: "Run id.", schema: { type: "string" } }],
64
64
  responses: {
65
65
  "200": { description: "The run.", content: { "application/json": { schema: ref("run-detail") } } },
@@ -85,6 +85,7 @@ export const VIEWER_ROUTES = [
85
85
  "- `retry: 3000`: suggested client reconnect delay in milliseconds.",
86
86
  '- `event: hello`: sent first. Data is `{"connected": <bool>}`, whether the server\'s live-event connection is up.',
87
87
  "- `event: run`, `event: service`, `event: outcome`: a run, coding-run service, or outcome changed. Each carries an `id:` line. Payloads match the `viewer-event` schema (see `x-event-schema`).",
88
+ "- `event: step`: a run recorded a step, or a step's status arrived. Carries an `id:` line. Data: the step's `seq`, `stepKind` (`tool` or `said`), `tool`, `summary` (redacted when it was recorded), `status` and `at`, never tool output.",
88
89
  '- `event: status`: the server\'s live-event connection changed. Data is `{"connected": <bool>}`.',
89
90
  "- `event: resync`: the live-event connection is up (again). Events may have been missed: refetch the graph.",
90
91
  `- \`: ping\`: comment sent every ${SSE_HEARTBEAT_MS / 1000} seconds to keep the connection open.`,
@@ -2,4 +2,6 @@
2
2
  import type { PrismaClient } from "#prisma";
3
3
  import type { RunDetail } from "./api-schema.js";
4
4
  import { type IssueSites } from "./graph.js";
5
+ /** The most steps a run detail carries: the newest, oldest first. */
6
+ export declare const RUN_DETAIL_STEPS = 200;
5
7
  export declare function loadRunDetail(db: PrismaClient, runId: string, issueSites?: IssueSites): Promise<RunDetail | null>;
@@ -11,16 +11,19 @@ function serviceDetails(snapshot) {
11
11
  envNames: isRecord(s.testEnv) ? Object.keys(s.testEnv).sort() : [],
12
12
  }));
13
13
  }
14
+ /** The most steps a run detail carries: the newest, oldest first. */
15
+ export const RUN_DETAIL_STEPS = 200;
14
16
  export async function loadRunDetail(db, runId, issueSites = {}) {
15
17
  const [graphRun] = await loadGraphRuns(db, [runId], issueSites);
16
18
  if (!graphRun)
17
19
  return null;
18
- const [row, children] = await Promise.all([
20
+ const [row, children, newest, stepCount] = await Promise.all([
19
21
  db.run.findUniqueOrThrow({
20
22
  where: { id: runId },
21
23
  select: {
22
24
  error: true,
23
25
  finalText: true,
26
+ stepsDropped: true,
24
27
  codingRun: {
25
28
  select: {
26
29
  provider: true,
@@ -39,13 +42,35 @@ export async function loadRunDetail(db, runId, issueSites = {}) {
39
42
  select: { id: true },
40
43
  orderBy: [{ startedAt: "asc" }, { id: "asc" }],
41
44
  }),
45
+ // Stored already redacted; the call id and the detail text are never selected.
46
+ db.runStep.findMany({
47
+ where: { runId },
48
+ orderBy: { seq: "desc" },
49
+ take: RUN_DETAIL_STEPS,
50
+ select: { seq: true, at: true, kind: true, tool: true, summary: true, status: true },
51
+ }),
52
+ db.runStep.count({ where: { runId } }),
42
53
  ]);
54
+ const steps = newest
55
+ .reverse()
56
+ .filter((s) => s.kind === "tool" || s.kind === "said")
57
+ .map((s) => ({
58
+ seq: s.seq,
59
+ at: s.at.toISOString(),
60
+ kind: s.kind,
61
+ tool: s.tool,
62
+ summary: s.summary,
63
+ status: s.status,
64
+ }));
43
65
  const coding = row.codingRun;
44
66
  return {
45
67
  ...graphRun,
46
68
  error: row.error,
47
69
  finalText: row.finalText,
48
70
  childRunIds: children.map((c) => c.id),
71
+ steps,
72
+ stepsOmitted: Math.max(0, stepCount - newest.length),
73
+ stepsDropped: row.stepsDropped,
49
74
  coding: coding
50
75
  ? {
51
76
  provider: coding.provider,