@inflexa-ai/harness 0.2.0 → 0.4.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 (81) hide show
  1. package/README.md +48 -0
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +2 -2
  4. package/dist/agents/sandbox/shared.d.ts +3 -0
  5. package/dist/agents/sandbox/shared.js +2 -2
  6. package/dist/app/chat-turn.d.ts +3 -0
  7. package/dist/app/chat-turn.js +3 -1
  8. package/dist/app/synthesize-run.d.ts +3 -0
  9. package/dist/app/synthesize-run.js +3 -1
  10. package/dist/execution/artifact-metadata.d.ts +3 -0
  11. package/dist/execution/artifact-metadata.js +8 -2
  12. package/dist/execution/artifact-registration.d.ts +2 -1
  13. package/dist/execution/artifact-registration.js +8 -2
  14. package/dist/execution/ephemeral-runner.d.ts +3 -0
  15. package/dist/execution/ephemeral-runner.js +8 -4
  16. package/dist/execution/post-step-pipeline.d.ts +3 -0
  17. package/dist/execution/post-step-pipeline.js +24 -4
  18. package/dist/execution/reconcile-manifest.d.ts +7 -0
  19. package/dist/execution/reconcile-manifest.js +19 -5
  20. package/dist/execution/report-runner.d.ts +3 -0
  21. package/dist/execution/report-runner.js +4 -4
  22. package/dist/execution/step-summary.d.ts +3 -0
  23. package/dist/execution/step-summary.js +4 -2
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +5 -0
  26. package/dist/lib/chrome.d.ts +4 -1
  27. package/dist/lib/chrome.js +10 -7
  28. package/dist/lib/console-logger.d.ts +19 -0
  29. package/dist/lib/console-logger.js +62 -0
  30. package/dist/lib/logger.d.ts +88 -0
  31. package/dist/lib/logger.js +37 -0
  32. package/dist/lib/otel.d.ts +2 -1
  33. package/dist/lib/otel.js +8 -3
  34. package/dist/runtime/boot.d.ts +2 -2
  35. package/dist/runtime/boot.js +1 -1
  36. package/dist/runtime/connection-budget.d.ts +2 -2
  37. package/dist/runtime/connection-budget.js +5 -3
  38. package/dist/runtime/dbos.d.ts +7 -7
  39. package/dist/runtime/dbos.js +14 -11
  40. package/dist/runtime/shutdown.d.ts +2 -2
  41. package/dist/runtime/shutdown.js +4 -3
  42. package/dist/sandbox/create-sandbox.d.ts +41 -3
  43. package/dist/sandbox/create-sandbox.js +39 -19
  44. package/dist/sandbox/docker-client.d.ts +17 -2
  45. package/dist/sandbox/docker-client.js +39 -12
  46. package/dist/sandbox/k8s-client.d.ts +3 -0
  47. package/dist/sandbox/k8s-client.js +5 -4
  48. package/dist/sandbox/notification-sweep.d.ts +3 -3
  49. package/dist/sandbox/notification-sweep.js +4 -2
  50. package/dist/sandbox/reaper.d.ts +3 -3
  51. package/dist/sandbox/reaper.js +7 -4
  52. package/dist/sandbox/watchdog.d.ts +3 -3
  53. package/dist/sandbox/watchdog.js +8 -2
  54. package/dist/state/init.d.ts +2 -1
  55. package/dist/state/init.js +5 -6
  56. package/dist/tasks/data-profile.d.ts +5 -0
  57. package/dist/tasks/data-profile.js +24 -17
  58. package/dist/tools/bio/search-dgidb.d.ts +11 -1
  59. package/dist/tools/bio/search-dgidb.js +92 -78
  60. package/dist/tools/iterate-report.d.ts +3 -0
  61. package/dist/tools/iterate-report.js +3 -1
  62. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  63. package/dist/tools/research/literature-reviewer.js +2 -2
  64. package/dist/tools/workspace/execute-command.d.ts +3 -0
  65. package/dist/tools/workspace/execute-command.js +3 -1
  66. package/dist/workflows/execute-analysis.d.ts +3 -0
  67. package/dist/workflows/execute-analysis.js +11 -7
  68. package/dist/workflows/execute-target-assessment.d.ts +3 -0
  69. package/dist/workflows/execute-target-assessment.js +19 -15
  70. package/dist/workflows/sandbox-step.d.ts +9 -0
  71. package/dist/workflows/sandbox-step.js +39 -17
  72. package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
  73. package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
  74. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
  75. package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
  76. package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
  77. package/dist/workflows/target-assessment/phase5-persist.js +8 -3
  78. package/dist/workflows/target-assessment/progress.d.ts +2 -1
  79. package/dist/workflows/target-assessment/progress.js +4 -3
  80. package/dist/workspace/filesystem.js +163 -74
  81. package/package.json +95 -96
@@ -32,6 +32,7 @@ import { composeSystemPrompt } from "../agents/system-prompt.js";
32
32
  import { createReadFileTool } from "../tools/workspace/read-file.js";
33
33
  import { StepSummarySchema } from "../schemas/step-summary.js";
34
34
  import { incrementSummaryNullCount } from "./step-summary-metrics.js";
35
+ import { createNoopLogger } from "../lib/console-logger.js";
35
36
  const SYSTEM_PROMPT = `You are a sandbox agent writing a post-step interpretive summary of work you just completed.
36
37
 
37
38
  Your prior conversation — tool calls, code output, intermediate results — is in the message history above. Use it for narrative and intent.
@@ -69,6 +70,7 @@ function sanitizeTranscript(messages) {
69
70
  * without `summary.md` in that case.
70
71
  */
71
72
  export async function generateStepSummary(opts) {
73
+ const logger = (opts.logger ?? createNoopLogger()).named("step-summary").with({ runId: opts.runId, stepId: opts.stepId, agentId: opts.agentId });
72
74
  const transcript = sanitizeTranscript(opts.messages);
73
75
  const userPrompt = stepSummaryPrompt(opts.artifactPaths.join("\n"));
74
76
  const writer = {
@@ -89,13 +91,13 @@ export async function generateStepSummary(opts) {
89
91
  });
90
92
  }
91
93
  catch (err) {
92
- console.warn(`[step-summary] Summary loop failed runId=${opts.runId} stepId=${opts.stepId} agentId=${opts.agentId}: ${err instanceof Error ? err.message : err}`);
94
+ logger.warn("summary loop failed", logger.errorFields(err));
93
95
  incrementSummaryNullCount(opts.agentId, "throw");
94
96
  return undefined;
95
97
  }
96
98
  const markdown = finalText(result.messages);
97
99
  if (!markdown || markdown.trim().length === 0) {
98
- console.warn(`[step-summary] Empty markdown runId=${opts.runId} stepId=${opts.stepId} agentId=${opts.agentId}`);
100
+ logger.warn("empty markdown");
99
101
  incrementSummaryNullCount(opts.agentId, "empty");
100
102
  return undefined;
101
103
  }
package/dist/index.d.ts CHANGED
@@ -108,6 +108,9 @@ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parse
108
108
  export type { ReferenceInstallReceipt, ReferenceReceiptArtifact } from "./reference-data/receipt.js";
109
109
  export { queryActiveSandboxes } from "./state/active-sandboxes.js";
110
110
  export { runStepDir } from "./workspace/paths.js";
111
+ export type { LogFields, LogLevel, Logger } from "./lib/logger.js";
112
+ export { defaultErrorFields } from "./lib/logger.js";
113
+ export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
111
114
  export { registerSandboxReaper } from "./sandbox/reaper.js";
112
115
  export type { RegisterReaperDeps } from "./sandbox/reaper.js";
113
116
  export { registerWatchdog } from "./sandbox/watchdog.js";
package/dist/index.js CHANGED
@@ -162,6 +162,11 @@ export { queryActiveSandboxes } from "./state/active-sandboxes.js";
162
162
  // Workspace-tree path convention — the root-relative step directory each
163
163
  // sandbox step confines its artifact writes to (join onto the resolved root).
164
164
  export { runStepDir } from "./workspace/paths.js";
165
+ // `Logger.errorFields` is on the interface so a realization can defer to its
166
+ // sink's native error handling (pino's `err` serializer, OTel's `exception.*`);
167
+ // `defaultErrorFields` is the shipped mapping for one with no opinion.
168
+ export { defaultErrorFields } from "./lib/logger.js";
169
+ export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
165
170
  // Sandbox-hygiene scheduled workflows. Reaper reclaims a dead host's orphaned
166
171
  // containers; watchdog converts a dead sandbox into a prompt step failure
167
172
  // instead of a deadline-long `DBOS.recv` hang; the sweep prunes stale
@@ -7,9 +7,12 @@
7
7
  * semaphore so the sidecar isn't overwhelmed by concurrent reports.
8
8
  */
9
9
  import type { Browser, BrowserContext, Page } from "puppeteer-core";
10
+ import type { Logger } from "./logger.js";
10
11
  export interface ChromeConfig {
12
+ /** Operational logging seam; omitted falls back to no-op. */
13
+ readonly logger?: Logger;
11
14
  readonly browserUrl?: string;
12
15
  readonly maxPages?: number;
13
16
  }
14
- export declare function getBrowser(browserUrl?: string): Promise<Browser>;
17
+ export declare function getBrowser(browserUrl?: string, injected?: Logger): Promise<Browser>;
15
18
  export declare function withPage<T>(cfg: ChromeConfig, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
@@ -7,6 +7,7 @@
7
7
  * semaphore so the sidecar isn't overwhelmed by concurrent reports.
8
8
  */
9
9
  import puppeteer from "puppeteer-core";
10
+ import { createNoopLogger } from "./console-logger.js";
10
11
  let browser;
11
12
  let connecting;
12
13
  let semaphore;
@@ -41,7 +42,8 @@ function getSemaphore(maxPages) {
41
42
  }
42
43
  return semaphore;
43
44
  }
44
- export async function getBrowser(browserUrl) {
45
+ export async function getBrowser(browserUrl, injected) {
46
+ const logger = (injected ?? createNoopLogger()).named("chrome");
45
47
  if (browser && browser.connected)
46
48
  return browser;
47
49
  if (connecting)
@@ -50,21 +52,21 @@ export async function getBrowser(browserUrl) {
50
52
  if (!browserURL) {
51
53
  throw new Error("CHROME_BROWSER_URL is not set — Cortex requires the chrome sidecar to be reachable");
52
54
  }
53
- console.log(`[chrome] connecting to browser at ${browserURL}`);
55
+ logger.info("connecting to browser", { browserURL });
54
56
  connecting = puppeteer
55
57
  .connect({ browserURL })
56
58
  .then((b) => {
57
59
  browser = b;
58
60
  b.on("disconnected", () => {
59
- console.log("[chrome] browser disconnected; will reconnect on next request");
61
+ logger.info("browser disconnected; will reconnect on next request");
60
62
  if (browser === b)
61
63
  browser = undefined;
62
64
  });
63
- console.log(`[chrome] connected (wsEndpoint=${b.wsEndpoint()})`);
65
+ logger.info("connected", { wsEndpoint: b.wsEndpoint() });
64
66
  return b;
65
67
  })
66
68
  .catch((err) => {
67
- console.error("[chrome] failed to connect to browser:", err);
69
+ logger.error("failed to connect to browser", logger.errorFields(err));
68
70
  throw err;
69
71
  })
70
72
  .finally(() => {
@@ -76,7 +78,7 @@ export async function withPage(cfg, fn) {
76
78
  const release = await getSemaphore(cfg.maxPages).acquire();
77
79
  let context;
78
80
  try {
79
- const b = await getBrowser(cfg.browserUrl);
81
+ const b = await getBrowser(cfg.browserUrl, cfg.logger);
80
82
  context = await b.createBrowserContext();
81
83
  const page = await context.newPage();
82
84
  return await fn(page, context);
@@ -87,7 +89,8 @@ export async function withPage(cfg, fn) {
87
89
  await context.close();
88
90
  }
89
91
  catch (err) {
90
- console.error("[chrome] error closing browser context:", err);
92
+ const logger = (cfg.logger ?? createNoopLogger()).named("chrome");
93
+ logger.error("error closing browser context", logger.errorFields(err));
91
94
  }
92
95
  }
93
96
  release();
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The trivial `Logger` realizations: one that writes to `console`, and the
3
+ * silent fallback the harness substitutes when an embedder wires nothing.
4
+ *
5
+ * `createConsoleLogger` is offered, never defaulted. A host whose UI owns
6
+ * stdout (an alternate-screen TUI, for instance) discards console output, so
7
+ * defaulting to it would quietly reintroduce the invisible-diagnostics failure
8
+ * the seam exists to prevent — the default is `createNoopLogger`, which is at
9
+ * least honest about saying nothing.
10
+ */
11
+ import { type Logger } from "./logger.js";
12
+ /** Build a `Logger` that writes each record to the matching `console` method. */
13
+ export declare function createConsoleLogger(): Logger;
14
+ /**
15
+ * Build a `Logger` that discards every record. This is what the harness falls
16
+ * back to when an embedder injects no logger, so internal call sites can log
17
+ * unconditionally instead of threading `?.` through every diagnostic.
18
+ */
19
+ export declare function createNoopLogger(): Logger;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The trivial `Logger` realizations: one that writes to `console`, and the
3
+ * silent fallback the harness substitutes when an embedder wires nothing.
4
+ *
5
+ * `createConsoleLogger` is offered, never defaulted. A host whose UI owns
6
+ * stdout (an alternate-screen TUI, for instance) discards console output, so
7
+ * defaulting to it would quietly reintroduce the invisible-diagnostics failure
8
+ * the seam exists to prevent — the default is `createNoopLogger`, which is at
9
+ * least honest about saying nothing.
10
+ */
11
+ import { defaultErrorFields } from "./logger.js";
12
+ /** Renders accumulated namespace segments as the message's `[a.b]` prefix. */
13
+ function prefixed(names, msg) {
14
+ return names.length > 0 ? `[${names.join(".")}] ${msg}` : msg;
15
+ }
16
+ /**
17
+ * `with()` accumulates into `bindings` and `named()` into `names`; each record
18
+ * merges call-site fields over the bindings, so a narrower call-site key
19
+ * deliberately wins over inherited context.
20
+ */
21
+ function makeConsoleLogger(names, bindings) {
22
+ const emit = (level) => (msg, fields) => {
23
+ // The one sanctioned console site in the harness — writing to console is
24
+ // this function's entire contract. `no-console` exempts this file by path
25
+ // in eslint.config.js rather than by an inline disable here.
26
+ console[level](prefixed(names, msg), { ...bindings, ...fields });
27
+ };
28
+ return {
29
+ debug: emit("debug"),
30
+ info: emit("info"),
31
+ warn: emit("warn"),
32
+ error: emit("error"),
33
+ with: (extra) => makeConsoleLogger(names, { ...bindings, ...extra }),
34
+ named: (name) => makeConsoleLogger([...names, name], bindings),
35
+ // console renders whatever it is handed; there is no native error
36
+ // serializer here to defer to, so the shipped default is the right shape.
37
+ errorFields: defaultErrorFields,
38
+ };
39
+ }
40
+ /** Build a `Logger` that writes each record to the matching `console` method. */
41
+ export function createConsoleLogger() {
42
+ return makeConsoleLogger([], {});
43
+ }
44
+ /**
45
+ * Build a `Logger` that discards every record. This is what the harness falls
46
+ * back to when an embedder injects no logger, so internal call sites can log
47
+ * unconditionally instead of threading `?.` through every diagnostic.
48
+ */
49
+ export function createNoopLogger() {
50
+ const noop = {
51
+ debug: () => { },
52
+ info: () => { },
53
+ warn: () => { },
54
+ error: () => { },
55
+ with: () => noop,
56
+ named: () => noop,
57
+ // Still the real mapping, not `{}`: callers spread the result into fields
58
+ // they may pass elsewhere, so it must stay honest even when nothing is emitted.
59
+ errorFields: defaultErrorFields,
60
+ };
61
+ return noop;
62
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * The harness's operational logging seam.
3
+ *
4
+ * The harness ships as a published package embedded by hosts it does not
5
+ * control, so it declares the shape it logs through and lets the embedder bind
6
+ * a realization at its composition root — the same inversion the capability
7
+ * seams use. Naming a concrete logger here would push that library onto every
8
+ * consumer.
9
+ *
10
+ * The alternative that reads as obvious — take the embedder's logger type
11
+ * directly — was rejected for exactly that reason. `@opentelemetry/api-logs`
12
+ * was the other candidate, being the nearest thing to a neutral standard, but
13
+ * it is still pre-1.0 and would force consumers into an OTel SDK to read a log
14
+ * line. JS has no `log/slog` equivalent to conform to, so the harness defines
15
+ * its own minimum and adapts at the edges.
16
+ *
17
+ * Diagnostics MUST go through here, never `console`: an embedder whose UI owns
18
+ * stdout discards console output entirely, which is silent data loss precisely
19
+ * when something has gone wrong. `createConsoleLogger` (`./console-logger.ts`)
20
+ * is the one sanctioned console site.
21
+ */
22
+ /** Severity of a log record, ascending. */
23
+ export type LogLevel = "debug" | "info" | "warn" | "error";
24
+ /**
25
+ * Structured key/value context attached to a record. Identifiers ride here
26
+ * rather than interpolated into the message so records stay queryable and any
27
+ * field-name redaction the host configures can actually match them.
28
+ */
29
+ export type LogFields = Record<string, unknown>;
30
+ /**
31
+ * Operational logger. Message-first (`error(msg, fields?)`) — the convention
32
+ * `slog`, winston, and `console` share. Object-first loggers exist (pino,
33
+ * bunyan) and an embedder using one adapts at its composition root; shaping
34
+ * this contract to match one of them would model the published surface on a
35
+ * single consumer's choice.
36
+ */
37
+ export interface Logger {
38
+ debug(msg: string, fields?: LogFields): void;
39
+ info(msg: string, fields?: LogFields): void;
40
+ warn(msg: string, fields?: LogFields): void;
41
+ error(msg: string, fields?: LogFields): void;
42
+ /**
43
+ * Bind `fields` onto every record the returned logger emits — `slog`'s
44
+ * `With`. Lets a workflow body fix `{ runId, stepId }` once instead of
45
+ * repeating it at each call site.
46
+ */
47
+ with(fields: LogFields): Logger;
48
+ /**
49
+ * Bind a namespace onto every record the returned logger emits, rendered as
50
+ * a bracketed prefix on the message: `named("boot")` turns
51
+ * `info("harness booted")` into `[boot] harness booted`.
52
+ *
53
+ * The namespace is a first-class part of a record, so it is declared once at
54
+ * the seam rather than hand-typed into every message string — hand-typing is
55
+ * what lets one module drift to `[Boot]` or drop the tag entirely. Nesting
56
+ * composes with a dot (`named("post-step").named("reconcile")` →
57
+ * `[post-step.reconcile]`), matching the tag convention already in use.
58
+ */
59
+ named(name: string): Logger;
60
+ /**
61
+ * Normalize a thrown value into fields to spread onto a record:
62
+ * `logger.error("step failure", { ...logger.errorFields(err), stepId })`.
63
+ *
64
+ * Every catch block would otherwise hand-roll
65
+ * `err instanceof Error ? err.message : String(err)` — repetition that
66
+ * reliably drifts, and that silently drops the stack from whichever site
67
+ * forgot it.
68
+ *
69
+ * It lives on the interface rather than as a free function because how an
70
+ * error is best represented is a property of the SINK, not of the harness: a
71
+ * pino-backed realization may defer to pino's `err` serializer, an
72
+ * OTel-backed one to the `exception.*` semantic conventions. Owning the
73
+ * mapping here would foreclose both. `defaultErrorFields` is the shipped
74
+ * implementation — a realization with no opinion just references it.
75
+ */
76
+ errorFields(err: unknown): LogFields;
77
+ }
78
+ /**
79
+ * The default `Logger.errorFields`: `err` carries the message, `stack` rides as
80
+ * its own field when there is one.
81
+ *
82
+ * A raw `Error` is deliberately not passed through as a field value: it is
83
+ * assignable to `unknown` and so type-checks, but `JSON.stringify(new Error())`
84
+ * is `{}` — a sink that serializes to JSON would drop the message entirely,
85
+ * which is the silent-loss failure this seam exists to prevent. Normalizing to
86
+ * strings means any sink gets something printable regardless of its serializer.
87
+ */
88
+ export declare function defaultErrorFields(err: unknown): LogFields;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The harness's operational logging seam.
3
+ *
4
+ * The harness ships as a published package embedded by hosts it does not
5
+ * control, so it declares the shape it logs through and lets the embedder bind
6
+ * a realization at its composition root — the same inversion the capability
7
+ * seams use. Naming a concrete logger here would push that library onto every
8
+ * consumer.
9
+ *
10
+ * The alternative that reads as obvious — take the embedder's logger type
11
+ * directly — was rejected for exactly that reason. `@opentelemetry/api-logs`
12
+ * was the other candidate, being the nearest thing to a neutral standard, but
13
+ * it is still pre-1.0 and would force consumers into an OTel SDK to read a log
14
+ * line. JS has no `log/slog` equivalent to conform to, so the harness defines
15
+ * its own minimum and adapts at the edges.
16
+ *
17
+ * Diagnostics MUST go through here, never `console`: an embedder whose UI owns
18
+ * stdout discards console output entirely, which is silent data loss precisely
19
+ * when something has gone wrong. `createConsoleLogger` (`./console-logger.ts`)
20
+ * is the one sanctioned console site.
21
+ */
22
+ /**
23
+ * The default `Logger.errorFields`: `err` carries the message, `stack` rides as
24
+ * its own field when there is one.
25
+ *
26
+ * A raw `Error` is deliberately not passed through as a field value: it is
27
+ * assignable to `unknown` and so type-checks, but `JSON.stringify(new Error())`
28
+ * is `{}` — a sink that serializes to JSON would drop the message entirely,
29
+ * which is the silent-loss failure this seam exists to prevent. Normalizing to
30
+ * strings means any sink gets something printable regardless of its serializer.
31
+ */
32
+ export function defaultErrorFields(err) {
33
+ if (err instanceof Error) {
34
+ return err.stack ? { err: err.message, stack: err.stack } : { err: err.message };
35
+ }
36
+ return { err: String(err) };
37
+ }
@@ -15,13 +15,14 @@
15
15
  * so inbound/outbound trace-context propagation across those paths is
16
16
  * not currently wired.
17
17
  */
18
+ import type { Logger } from "./logger.js";
18
19
  /**
19
20
  * Initialize the OpenTelemetry SDK. Must be called before any code that
20
21
  * calls trace.getTracer() or metrics.getMeter().
21
22
  *
22
23
  * Safe to call multiple times — only the first call has effect.
23
24
  */
24
- export declare function initOtel(): void;
25
+ export declare function initOtel(injected?: Logger): void;
25
26
  /**
26
27
  * Flush and shut down the OTel exporters. Called by the graceful-shutdown
27
28
  * sequence so in-flight batch spans/metrics make it to the collector
package/dist/lib/otel.js CHANGED
@@ -16,6 +16,7 @@
16
16
  * not currently wired.
17
17
  */
18
18
  import { propagation, metrics, trace } from "@opentelemetry/api";
19
+ import { createNoopLogger } from "./console-logger.js";
19
20
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
20
21
  import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
21
22
  import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
@@ -33,10 +34,11 @@ let registeredMeterProvider;
33
34
  *
34
35
  * Safe to call multiple times — only the first call has effect.
35
36
  */
36
- export function initOtel() {
37
+ export function initOtel(injected) {
37
38
  if (initialized)
38
39
  return;
39
40
  initialized = true;
41
+ const logger = (injected ?? createNoopLogger()).named("otel");
40
42
  // ── W3C Trace Context propagation ──────────────────────────────────
41
43
  propagation.setGlobalPropagator(new W3CTraceContextPropagator());
42
44
  const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
@@ -65,8 +67,11 @@ export function initOtel() {
65
67
  const testSpan = testTracer.startSpan("init-check");
66
68
  const ctx = testSpan.spanContext();
67
69
  const isNoop = ctx.spanId === "0000000000000000";
68
- console.log(`[otel] TracerProvider registered: spanId=${ctx.spanId}, noop=${isNoop}, ` +
69
- `endpoint=${endpoint ?? "(none)"}, processors=${spanProcessors.length}`);
70
+ // A `console.log` here is what forced the CLI embedder to pass
71
+ // `initTelemetry: () => {}` — its TUI owns stdout, so the banner corrupted
72
+ // the screen and the whole of the harness's traces + metrics were switched
73
+ // off to avoid it. At debug through the injected seam it costs nothing.
74
+ logger.debug("TracerProvider registered", { spanId: ctx.spanId, noop: isNoop, endpoint: endpoint ?? null, processors: spanProcessors.length });
70
75
  testSpan.end();
71
76
  }
72
77
  // ── Metrics ────────────────────────────────────────────────────────
@@ -31,7 +31,7 @@
31
31
  * once shutdown starts, exiting takes precedence (see `runShutdownSequence`).
32
32
  */
33
33
  import type { Pool } from "pg";
34
- import type pino from "pino";
34
+ import type { Logger } from "../lib/logger.js";
35
35
  import { type CoreRuntime, type CoreRuntimeDeps } from "./assemble.js";
36
36
  import { type ConnectionBudgetConfig } from "./connection-budget.js";
37
37
  import { type DbosConfig } from "./dbos.js";
@@ -46,7 +46,7 @@ export interface BootHarnessDeps {
46
46
  readonly dbos: DbosConfig;
47
47
  /** Connection-budget guard config. */
48
48
  readonly connectionBudget: ConnectionBudgetConfig;
49
- readonly logger: pino.Logger;
49
+ readonly logger: Logger;
50
50
  /**
51
51
  * Host-specific work that must run AFTER workflow registration and BEFORE
52
52
  * DBOS launch (scheduled sweeps, ephemeral reap, agent-switch install). The
@@ -49,7 +49,7 @@ export async function bootHarness(deps) {
49
49
  const runtime = assembleCoreRuntime(core);
50
50
  await deps.beforeLaunch?.();
51
51
  await launchDbos({ config: deps.dbos, logger });
52
- logger.info({ executorId: deps.dbos.executorId }, "[boot] harness booted");
52
+ logger.named("boot").info("harness booted", { executorId: deps.dbos.executorId });
53
53
  const shutdown = (signal) => runShutdownSequence({
54
54
  signal,
55
55
  logger,
@@ -21,7 +21,7 @@
21
21
  * misconfigured pod fails boot loudly instead of degrading under load.
22
22
  */
23
23
  import type { Pool } from "pg";
24
- import type pino from "pino";
24
+ import type { Logger } from "../lib/logger.js";
25
25
  /**
26
26
  * Narrow config slice the budget guard reads — only the optional app-pool
27
27
  * size override. Composition roots map `Env.DB_POOL_MAX` onto this.
@@ -31,7 +31,7 @@ export interface ConnectionBudgetConfig {
31
31
  }
32
32
  export interface AssertConnectionBudgetOptions {
33
33
  pool: Pool;
34
- logger: pino.Logger;
34
+ logger: Logger;
35
35
  config: ConnectionBudgetConfig;
36
36
  }
37
37
  /**
@@ -38,15 +38,17 @@ export async function assertConnectionBudget({ pool, logger, config }) {
38
38
  `Lower DB_POOL_MAX or raise max_connections.`);
39
39
  }
40
40
  const supportedReplicas = Math.floor((maxConnections - SAFETY_MARGIN) / podFootprint);
41
- logger.info({
41
+ logger
42
+ .named("boot")
43
+ .info(`postgres connection budget OK — this cluster's max_connections ` +
44
+ `supports ~${supportedReplicas} cortex replicas; size replicas/pooler accordingly`, {
42
45
  maxConnections,
43
46
  appPoolSize,
44
47
  dbosPoolSize,
45
48
  podFootprint,
46
49
  safetyMargin: SAFETY_MARGIN,
47
50
  supportedReplicas,
48
- }, `[boot] postgres connection budget OK — this cluster's max_connections ` +
49
- `supports ~${supportedReplicas} cortex replicas; size replicas/pooler accordingly`);
51
+ });
50
52
  return {
51
53
  maxConnections,
52
54
  appPoolSize,
@@ -12,8 +12,8 @@
12
12
  *
13
13
  * See `openspec/changes/harness-dbos-runtime`.
14
14
  */
15
- import type pino from "pino";
16
15
  import type { Pool } from "pg";
16
+ import type { Logger } from "../lib/logger.js";
17
17
  /**
18
18
  * Narrow config slice the DBOS bootstrap reads. Composition roots map their
19
19
  * validated `Env` onto this and pass the executor id in (the harness reads no env).
@@ -50,9 +50,9 @@ interface State {
50
50
  * must resolve before the HTTP listener accepts traffic (otherwise the
51
51
  * readiness probe could 200 against a runtime that can't own workflows).
52
52
  */
53
- export declare function launchDbos({ config, logger }: {
53
+ export declare function launchDbos({ config, logger: injected }: {
54
54
  config: DbosConfig;
55
- logger: pino.Logger;
55
+ logger: Logger;
56
56
  }): Promise<void>;
57
57
  /**
58
58
  * Shut DBOS down. In-flight workflows are marked recoverable so another
@@ -63,8 +63,8 @@ export declare function launchDbos({ config, logger }: {
63
63
  * Must run after HTTP has drained (so no inbound request is orphaned) and
64
64
  * before the application pool closes (DBOS needs the system DB).
65
65
  */
66
- export declare function shutdownDbos({ logger }: {
67
- logger: pino.Logger;
66
+ export declare function shutdownDbos({ logger: injected }: {
67
+ logger: Logger;
68
68
  }): Promise<void>;
69
69
  /**
70
70
  * Cancel any `ephemeral:`-prefixed PENDING workflow this executor owns —
@@ -79,9 +79,9 @@ export declare function shutdownDbos({ logger }: {
79
79
  * pre-launch timing, and this is the sole module that owns DBOS. The system
80
80
  * DB is the same database as the app pool, so the pool reaches it directly.
81
81
  */
82
- export declare function sweepEphemeralWorkflows({ pool, logger, executorId, }: {
82
+ export declare function sweepEphemeralWorkflows({ pool, logger: injectedLogger, executorId, }: {
83
83
  pool: Pool;
84
- logger: pino.Logger;
84
+ logger: Logger;
85
85
  /** Stable per-pod executor id — must match `launchDbos`'s `executorId`. */
86
86
  executorId: string;
87
87
  }): Promise<void>;
@@ -53,9 +53,10 @@ function systemDatabaseUrl(config) {
53
53
  * must resolve before the HTTP listener accepts traffic (otherwise the
54
54
  * readiness probe could 200 against a runtime that can't own workflows).
55
55
  */
56
- export async function launchDbos({ config, logger }) {
56
+ export async function launchDbos({ config, logger: injected }) {
57
57
  if (state.launched)
58
58
  return;
59
+ const logger = injected.named("dbos");
59
60
  const executorID = config.executorId;
60
61
  const adminPort = parseInt(config.adminPort, 10);
61
62
  DBOS.setConfig({
@@ -71,12 +72,12 @@ export async function launchDbos({ config, logger }) {
71
72
  await DBOS.launch();
72
73
  state.launched = true;
73
74
  state.recoveryStarted = true;
74
- logger.info({
75
+ logger.info("launched", {
75
76
  executorID,
76
77
  applicationVersion: config.applicationVersion,
77
78
  adminPort,
78
79
  durationMs: Math.round(performance.now() - start),
79
- }, "[dbos] launched");
80
+ });
80
81
  }
81
82
  /**
82
83
  * Shut DBOS down. In-flight workflows are marked recoverable so another
@@ -87,19 +88,20 @@ export async function launchDbos({ config, logger }) {
87
88
  * Must run after HTTP has drained (so no inbound request is orphaned) and
88
89
  * before the application pool closes (DBOS needs the system DB).
89
90
  */
90
- export async function shutdownDbos({ logger }) {
91
+ export async function shutdownDbos({ logger: injected }) {
91
92
  if (!state.launched)
92
93
  return;
94
+ const logger = injected.named("dbos");
93
95
  const start = performance.now();
94
96
  try {
95
97
  await DBOS.shutdown();
96
- logger.info({ durationMs: Math.round(performance.now() - start) }, "[dbos] shutdown");
98
+ logger.info("shutdown", { durationMs: Math.round(performance.now() - start) });
97
99
  }
98
100
  catch (err) {
99
- logger.error({
100
- err: err instanceof Error ? err.message : String(err),
101
+ logger.error("shutdown failed", {
102
+ ...logger.errorFields(err),
101
103
  durationMs: Math.round(performance.now() - start),
102
- }, "[dbos] shutdown failed");
104
+ });
103
105
  }
104
106
  finally {
105
107
  state.launched = false;
@@ -119,7 +121,8 @@ export async function shutdownDbos({ logger }) {
119
121
  * pre-launch timing, and this is the sole module that owns DBOS. The system
120
122
  * DB is the same database as the app pool, so the pool reaches it directly.
121
123
  */
122
- export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
124
+ export async function sweepEphemeralWorkflows({ pool, logger: injectedLogger, executorId, }) {
125
+ const logger = injectedLogger.named("dbos");
123
126
  const executorID = executorId;
124
127
  try {
125
128
  const { rowCount } = await pool.query({
@@ -131,7 +134,7 @@ export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
131
134
  values: [Date.now(), executorID],
132
135
  });
133
136
  if (rowCount && rowCount > 0) {
134
- logger.info({ executorID, swept: rowCount }, "[dbos] swept orphaned ephemeral workflows");
137
+ logger.info("swept orphaned ephemeral workflows", { executorID, swept: rowCount });
135
138
  }
136
139
  }
137
140
  catch (err) {
@@ -139,7 +142,7 @@ export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
139
142
  if (err && typeof err === "object" && "code" in err && err.code === "42P01") {
140
143
  return;
141
144
  }
142
- logger.error({ executorID, err: err instanceof Error ? err.message : String(err) }, "[dbos] ephemeral sweep failed");
145
+ logger.error("ephemeral sweep failed", { executorID, ...logger.errorFields(err) });
143
146
  }
144
147
  }
145
148
  /** Snapshot of DBOS lifecycle state — read by the readiness probe. */
@@ -18,10 +18,10 @@
18
18
  * swallowed — once shutdown has started, exiting takes precedence over
19
19
  * any single step's failure.
20
20
  */
21
- import type pino from "pino";
21
+ import type { Logger } from "../lib/logger.js";
22
22
  export interface ShutdownDeps {
23
23
  signal: string;
24
- logger: pino.Logger;
24
+ logger: Logger;
25
25
  markDraining: () => void;
26
26
  closeHttpServer: () => Promise<void>;
27
27
  shutdownDbos: () => Promise<void>;
@@ -23,12 +23,13 @@ async function safely(logger, step, fn) {
23
23
  await fn();
24
24
  }
25
25
  catch (err) {
26
- logger.error({ err: err instanceof Error ? err.message : String(err), step }, `[shutdown] ${step} failed`);
26
+ logger.error(`${step} failed`, { ...logger.errorFields(err), step });
27
27
  }
28
28
  }
29
29
  export async function runShutdownSequence(deps) {
30
- const { logger, signal } = deps;
31
- logger.info({ signal }, "[shutdown] requested");
30
+ const { signal } = deps;
31
+ const logger = deps.logger.named("shutdown");
32
+ logger.info("requested", { signal });
32
33
  deps.markDraining();
33
34
  await safely(logger, "http-drain", deps.closeHttpServer);
34
35
  await safely(logger, "dbos-shutdown", deps.shutdownDbos);