@yagni-app/code-staging 1.0.5-staging.1241.1 → 1.0.6-staging.1244.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.
package/README.md CHANGED
@@ -179,11 +179,16 @@ children and subagents — emits a per-prompt span tree: interaction → LLM
179
179
  request → tool calls, following the OTel GenAI semantic conventions. Nothing is
180
180
  exported unless you configure an endpoint.
181
181
 
182
- Enable it one of three ways (first match wins):
183
-
184
- - `OTEL_EXPORTER_OTLP_ENDPOINT=http://<collector>:4317` in the environment
185
- (a personal override — handy for pointing one session at a scratch
186
- collector), or
182
+ Enable it one of four ways (first match wins):
183
+
184
+ - `OTEL_EXPORTER_OTLP_ENDPOINT=…` in the environment (a personal override —
185
+ handy for pointing one session at a scratch collector), or
186
+ - `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=…` in the environment — the per-signal
187
+ form Claude Code setups use (e.g.
188
+ `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://otlp.datadoghq.com/v1/traces`
189
+ with `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf`); the launcher
190
+ bridges it to the generic var pi-otel reads, so a Claude Code OTel env
191
+ lights up YAGNI Code traces with no extra config, or
187
192
  - **workspace settings** (the zero-setup path): a workspace admin sets the
188
193
  endpoint, protocol, and any collector headers (e.g. a Datadog API key) once
189
194
  in the web app under Settings → YAGNI Code → Trace export. Every session in
@@ -197,7 +202,26 @@ Enable it one of three ways (first match wins):
197
202
  Standard OTel env vars are honored (`OTEL_EXPORTER_OTLP_PROTOCOL`,
198
203
  `OTEL_EXPORTER_OTLP_HEADERS`, `OTEL_SERVICE_NAME` — defaults to `yagni-code`),
199
204
  and `PI_OTEL_DISABLED=1` is the kill switch. `yagni doctor` shows the current
200
- export state.
205
+ export state and probes the endpoint live (unreachable → a warning with a
206
+ hint, never an exit-code flip).
207
+
208
+ Two recipes for common collectors:
209
+
210
+ - **Datadog, direct SaaS (no agent):** endpoint
211
+ `https://otlp.datadoghq.com/v1/traces` (US1; other sites have their own
212
+ host — `otlp.us3.datadoghq.com`, `otlp.us5.datadoghq.com`,
213
+ `otlp.datadoghq.eu`, `otlp.ap1.datadoghq.com`), protocol `http/protobuf`
214
+ (Datadog's direct trace intake does not accept gRPC), header
215
+ `dd-api-key=<your Datadog API key>`. Use the full path, not `/v1/` — the
216
+ endpoint is passed through verbatim, no path is appended for you.
217
+ - **Datadog Agent:** endpoint `http://<agent-host>:4317`, protocol `grpc`, no
218
+ key (the agent forwards with its own). OTLP ingest ships in the agent but
219
+ is off by default — enable it with
220
+ `otlp_config.receiver.protocols.grpc.endpoint: 0.0.0.0:4317` in
221
+ `datadog.yaml`.
222
+ - **Grafana Cloud:** endpoint `https://otlp-gateway-prod-…-0.grafana.net:443/otlp`,
223
+ protocol `http/protobuf`, header `authorization=<the raw MTgx… value>` (the
224
+ `Basic ` scheme is added for you).
201
225
 
202
226
  Two things are enforced and not configurable:
203
227
 
package/dist/doctor.d.ts CHANGED
@@ -13,7 +13,7 @@
13
13
  * Advisory checks (loose perms, missing `gh`) never flip the exit code.
14
14
  */
15
15
  import { type TokenExpiryStatus } from "./launch.js";
16
- import { type OtelLaunchConfig } from "./otel.js";
16
+ import { probeOtelEndpoint, type OtelLaunchConfig } from "./otel.js";
17
17
  import { type Profile } from "./profiles.js";
18
18
  export type CheckStatus = "ok" | "warn" | "fail";
19
19
  export interface CheckResult {
@@ -63,10 +63,15 @@ export declare function checkCliUpdate(probe: {
63
63
  export declare function checkGh(onPath: boolean): CheckResult;
64
64
  /**
65
65
  * Advisory OTel-export line: says whether sessions will stream traces to an
66
- * OTLP collector, and from which config source. Never flips the exit code
66
+ * OTLP collector, from which config source, and when a collector is
67
+ * configured — whether the endpoint answers TCP right now (scheme-default
68
+ * ports included: https with no port probes 443). Never flips the exit code —
67
69
  * most machines have no collector, and that is the healthy default.
68
70
  */
69
- export declare function checkOtelExport(config: OtelLaunchConfig | undefined): CheckResult;
71
+ export declare function checkOtelExport(config: OtelLaunchConfig | undefined, deps?: {
72
+ probeEndpoint?: typeof probeOtelEndpoint;
73
+ probeTimeoutMs?: number;
74
+ }): Promise<CheckResult>;
70
75
  /** Config-only MCP snapshot (no connections — health checks live in `mcp list`/`get`). */
71
76
  export interface McpProbe {
72
77
  /** YAGNI_CODE_MCP_DISABLED kill-switch is active. */
package/dist/doctor.js CHANGED
@@ -17,7 +17,7 @@ import { delimiter, join } from "node:path";
17
17
  import { credentialsDir } from "./credentials.js";
18
18
  import { currentCliVersion, fetchLatestVersion, isNewerVersion } from "./upgrade.js";
19
19
  import { classifyTokenExpiry } from "./launch.js";
20
- import { resolveOtelLaunchWithWorkspace } from "./otel.js";
20
+ import { probeOtelEndpoint, resolveOtelLaunchWithWorkspace, } from "./otel.js";
21
21
  import { resolveExtensionPath, resolvePiCliPath, resolvePiPackageDir } from "./paths.js";
22
22
  import { readActiveProfile } from "./profiles.js";
23
23
  import { resolveMcpConfigPath } from "./mcpCommand.js";
@@ -202,10 +202,12 @@ export function checkGh(onPath) {
202
202
  }
203
203
  /**
204
204
  * Advisory OTel-export line: says whether sessions will stream traces to an
205
- * OTLP collector, and from which config source. Never flips the exit code
205
+ * OTLP collector, from which config source, and when a collector is
206
+ * configured — whether the endpoint answers TCP right now (scheme-default
207
+ * ports included: https with no port probes 443). Never flips the exit code —
206
208
  * most machines have no collector, and that is the healthy default.
207
209
  */
208
- export function checkOtelExport(config) {
210
+ export async function checkOtelExport(config, deps = {}) {
209
211
  if (!config) {
210
212
  return {
211
213
  name: "otel export (optional)",
@@ -215,16 +217,41 @@ export function checkOtelExport(config) {
215
217
  };
216
218
  }
217
219
  const source = config.source === "env"
218
- ? "OTEL_EXPORTER_OTLP_ENDPOINT"
220
+ ? (config.envVar ?? "OTEL_EXPORTER_OTLP_ENDPOINT")
219
221
  : config.source === "workspace"
220
222
  ? "workspace settings"
221
223
  : ".pi/settings.json";
222
- return {
223
- name: "otel export (optional)",
224
- status: "ok",
225
- detail: `on ${config.endpoint} (${source}, metadata-only)`,
226
- required: false,
227
- };
224
+ const base = `on → ${config.endpoint} (${source}, metadata-only)`;
225
+ const doProbe = deps.probeEndpoint ?? probeOtelEndpoint;
226
+ try {
227
+ const reachable = await doProbe(config.endpoint, deps.probeTimeoutMs ?? 1_000);
228
+ return reachable
229
+ ? {
230
+ name: "otel export (optional)",
231
+ status: "ok",
232
+ detail: `${base}, TCP reachable`,
233
+ required: false,
234
+ }
235
+ : {
236
+ name: "otel export (optional)",
237
+ status: "warn",
238
+ detail: `${base}, endpoint not reachable`,
239
+ hint: "the collector is down, unreachable from this machine, or the URL is wrong — traces will not export until it answers",
240
+ required: false,
241
+ };
242
+ }
243
+ catch {
244
+ // The probe itself errored (not "endpoint down" — the check could not
245
+ // run). Distinct from a verified-healthy line so nobody reads an
246
+ // unverified config as checked-and-good.
247
+ return {
248
+ name: "otel export (optional)",
249
+ status: "warn",
250
+ detail: `${base}, reachability probe failed`,
251
+ hint: "the reachability check could not run (network error) — export is configured but unverified; traces may still export normally",
252
+ required: false,
253
+ };
254
+ }
228
255
  }
229
256
  /**
230
257
  * Advisory (never required) MCP line: reports config health without connecting
@@ -489,7 +516,7 @@ export async function gatherChecks(deps = {}) {
489
516
  checks.push(checkStateDir(probeStateDir()));
490
517
  checks.push(checkGh(ghOnPath()));
491
518
  checks.push(checkMcpConfig(await probeMcp()));
492
- checks.push(checkOtelExport(await resolveOtelLaunchWithWorkspace({
519
+ checks.push(await checkOtelExport(await resolveOtelLaunchWithWorkspace({
493
520
  env: process.env,
494
521
  cwd: process.cwd(),
495
522
  creds: profile.token ? { baseUrl: profile.baseUrl, token: profile.token } : null,
package/dist/otel.d.ts CHANGED
@@ -43,13 +43,23 @@ export interface OtelLaunchConfig {
43
43
  extensionPath: string;
44
44
  /**
45
45
  * Where the enablement signal came from. "workspace" additionally carries
46
- * protocol/headers/serviceName, delivered as env to the session (the other
47
- * sources leave those to whatever the user/repo already configured).
46
+ * protocol/headers/serviceName, delivered as env to the session; "env" may
47
+ * carry a protocol when the endpoint arrived via the per-signal
48
+ * `OTEL_EXPORTER_OTLP_TRACES_(ENDPOINT|PROTOCOL)` bridge (pi-otel reads only
49
+ * the generic var, so the child env translates); "project-settings" leaves
50
+ * those to whatever the user/repo already configured.
48
51
  */
49
52
  source: "env" | "workspace" | "project-settings";
53
+ /**
54
+ * The env var the endpoint actually came from — the generic
55
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` or the per-signal
56
+ * `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. Doctor surfaces it so a user who set
57
+ * the per-signal var sees their own var named, not the generic one.
58
+ */
59
+ envVar?: "OTEL_EXPORTER_OTLP_ENDPOINT" | "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT";
50
60
  /** The configured OTLP endpoint (for doctor display; env for workspace source). */
51
61
  endpoint: string;
52
- /** Workspace-configured OTLP protocol (workspace source only). */
62
+ /** OTLP protocol: workspace config, or a per-signal env bridge. */
53
63
  protocol?: string;
54
64
  /** Workspace-configured collector headers (workspace source only; carries secrets). */
55
65
  headers?: Record<string, string>;
@@ -72,6 +82,15 @@ export interface WorkspaceOtelConfig {
72
82
  * Decide whether this launch exports OTel traces, and with which pi-otel entry.
73
83
  * Returns undefined when export stays off: no endpoint configured anywhere,
74
84
  * `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
85
+ *
86
+ * Env sources, in precedence order: the generic `OTEL_EXPORTER_OTLP_ENDPOINT`,
87
+ * then the per-signal `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. pi-otel only reads
88
+ * the generic var, so the per-signal one (the standard OTel env shape, and
89
+ * what Claude Code setups configure — e.g. Updater's) is bridged: the traces
90
+ * endpoint becomes the generic one, and a per-signal TRACES_PROTOCOL carries
91
+ * over the same way. Logs/metrics per-signal vars are deliberately NOT read:
92
+ * sessions export traces only, and pi-otel's metrics/logs signals are opt-in
93
+ * elsewhere.
75
94
  */
76
95
  export declare function resolveOtelLaunch(opts: {
77
96
  env: NodeJS.ProcessEnv;
@@ -80,13 +99,21 @@ export declare function resolveOtelLaunch(opts: {
80
99
  resolveExtension?: () => string;
81
100
  readFile?: typeof readFileSync;
82
101
  }): OtelLaunchConfig | undefined;
83
- /**
84
- * Absolute path to pi-otel's extension entry (its package main IS the pi
102
+ /** Absolute path to pi-otel's extension entry (its package main IS the pi
85
103
  * extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
86
104
  * for pi itself. Throws when the package is missing — callers treat that as
87
105
  * "export off", not an error.
88
106
  */
89
107
  export declare function resolveOtelExtensionPath(): string;
108
+ /**
109
+ * TCP reachability probe for an OTLP endpoint URL, with scheme-default ports:
110
+ * WHATWG `URL` strips `:443`/`:80` as defaults, so an https endpoint with no
111
+ * port probes 443 (the agentless SaaS shape). Returns false on any parse or
112
+ * connect failure — reachability is advisory, never fatal. Mirrors the probe
113
+ * inside (patched) pi-otel but lives here so `doctor` can use it without
114
+ * importing the extension's SDK graph.
115
+ */
116
+ export declare function probeOtelEndpoint(endpoint: string, timeoutMs?: number): Promise<boolean>;
90
117
  /**
91
118
  * The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
92
119
  * is pinned unconditionally (policy 2 above); the service name only fills in
@@ -125,10 +152,12 @@ export declare function fetchWorkspaceOtel(creds: {
125
152
  token: string;
126
153
  }, profileName: string, deps?: WorkspaceOtelFetchDeps): Promise<WorkspaceOtelConfig | null>;
127
154
  /**
128
- * Full launch-time resolution, all three sources in precedence order:
155
+ * Full launch-time resolution, all four sources in precedence order:
129
156
  *
130
157
  * 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
131
- * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` the user's own setup, untouched.
158
+ * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
159
+ * — the user's own setup, untouched (generic wins
160
+ * over per-signal).
132
161
  * 3. workspace config — admin-set in the web app, fetched/cached.
133
162
  * 4. repo `.pi/settings.json` — committed per-repo config.
134
163
  *
package/dist/otel.js CHANGED
@@ -29,6 +29,7 @@
29
29
  * missing package or unreadable settings file resolves to "disabled".
30
30
  */
31
31
  import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
32
+ import { createConnection } from "node:net";
32
33
  import { join } from "node:path";
33
34
  import { fileURLToPath } from "node:url";
34
35
  import { credentialsDir } from "./credentials.js";
@@ -71,16 +72,25 @@ function envDisabled(env) {
71
72
  * Decide whether this launch exports OTel traces, and with which pi-otel entry.
72
73
  * Returns undefined when export stays off: no endpoint configured anywhere,
73
74
  * `PI_OTEL_DISABLED` set, or the pi-otel package unresolvable (never fatal).
75
+ *
76
+ * Env sources, in precedence order: the generic `OTEL_EXPORTER_OTLP_ENDPOINT`,
77
+ * then the per-signal `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`. pi-otel only reads
78
+ * the generic var, so the per-signal one (the standard OTel env shape, and
79
+ * what Claude Code setups configure — e.g. Updater's) is bridged: the traces
80
+ * endpoint becomes the generic one, and a per-signal TRACES_PROTOCOL carries
81
+ * over the same way. Logs/metrics per-signal vars are deliberately NOT read:
82
+ * sessions export traces only, and pi-otel's metrics/logs signals are opt-in
83
+ * elsewhere.
74
84
  */
75
85
  export function resolveOtelLaunch(opts) {
76
86
  const { env, cwd } = opts;
77
87
  if (envDisabled(env))
78
88
  return undefined;
79
89
  const envEndpoint = env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
80
- const settingsEndpoint = envEndpoint
81
- ? undefined
82
- : projectOtelEndpoint(cwd, opts.readFile ?? readFileSync);
83
- if (!envEndpoint && !settingsEndpoint)
90
+ const tracesEndpoint = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
91
+ const usingPerSignal = !envEndpoint && !!tracesEndpoint;
92
+ const settingsEndpoint = envEndpoint || tracesEndpoint ? undefined : projectOtelEndpoint(cwd, opts.readFile ?? readFileSync);
93
+ if (!envEndpoint && !tracesEndpoint && !settingsEndpoint)
84
94
  return undefined;
85
95
  let extensionPath;
86
96
  try {
@@ -89,12 +99,28 @@ export function resolveOtelLaunch(opts) {
89
99
  catch {
90
100
  return undefined;
91
101
  }
92
- return envEndpoint
93
- ? { extensionPath, source: "env", endpoint: envEndpoint }
94
- : { extensionPath, source: "project-settings", endpoint: settingsEndpoint };
102
+ if (envEndpoint) {
103
+ return {
104
+ extensionPath,
105
+ source: "env",
106
+ envVar: "OTEL_EXPORTER_OTLP_ENDPOINT",
107
+ endpoint: envEndpoint,
108
+ };
109
+ }
110
+ if (usingPerSignal) {
111
+ return {
112
+ extensionPath,
113
+ source: "env",
114
+ envVar: "OTEL_EXPORTER_OTLP_TRACES_ENDPOINT",
115
+ endpoint: tracesEndpoint,
116
+ ...(env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL?.trim()
117
+ ? { protocol: env.OTEL_EXPORTER_OTLP_TRACES_PROTOCOL.trim() }
118
+ : {}),
119
+ };
120
+ }
121
+ return { extensionPath, source: "project-settings", endpoint: settingsEndpoint };
95
122
  }
96
- /**
97
- * Absolute path to pi-otel's extension entry (its package main IS the pi
123
+ /** Absolute path to pi-otel's extension entry (its package main IS the pi
98
124
  * extension entry, `dist/index.js`). Resolved ESM-native like `paths.ts` does
99
125
  * for pi itself. Throws when the package is missing — callers treat that as
100
126
  * "export off", not an error.
@@ -106,6 +132,45 @@ export function resolveOtelExtensionPath() {
106
132
  }
107
133
  return path;
108
134
  }
135
+ /**
136
+ * TCP reachability probe for an OTLP endpoint URL, with scheme-default ports:
137
+ * WHATWG `URL` strips `:443`/`:80` as defaults, so an https endpoint with no
138
+ * port probes 443 (the agentless SaaS shape). Returns false on any parse or
139
+ * connect failure — reachability is advisory, never fatal. Mirrors the probe
140
+ * inside (patched) pi-otel but lives here so `doctor` can use it without
141
+ * importing the extension's SDK graph.
142
+ */
143
+ export async function probeOtelEndpoint(endpoint, timeoutMs = 1_000) {
144
+ let host;
145
+ let port;
146
+ try {
147
+ const u = new URL(endpoint);
148
+ host = u.hostname;
149
+ port = u.port
150
+ ? Number(u.port)
151
+ : u.protocol === "https:"
152
+ ? 443
153
+ : u.protocol === "http:"
154
+ ? 80
155
+ : null;
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ if (!host || !port)
161
+ return false;
162
+ return new Promise((resolve) => {
163
+ const sock = createConnection({ host, port });
164
+ const done = (ok) => {
165
+ sock.destroy();
166
+ resolve(ok);
167
+ };
168
+ sock.setTimeout(timeoutMs);
169
+ sock.once("connect", () => done(true));
170
+ sock.once("timeout", () => done(false));
171
+ sock.once("error", () => done(false));
172
+ });
173
+ }
109
174
  /**
110
175
  * The env keys an OTel-exporting child must carry. `PI_OTEL_CAPTURE_CONTENT`
111
176
  * is pinned unconditionally (policy 2 above); the service name only fills in
@@ -135,6 +200,17 @@ export function otelChildEnv(config, baseEnv) {
135
200
  workspace.OTEL_SERVICE_NAME = config.serviceName;
136
201
  }
137
202
  }
203
+ else if (config.source === "env" && config.protocol) {
204
+ // The per-signal bridge: the endpoint came from
205
+ // OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (+ TRACES_PROTOCOL), which pi-otel
206
+ // does not read — deliver them as the generic vars the extension resolves.
207
+ if (!baseEnv.OTEL_EXPORTER_OTLP_ENDPOINT) {
208
+ workspace.OTEL_EXPORTER_OTLP_ENDPOINT = config.endpoint;
209
+ }
210
+ if (!baseEnv.OTEL_EXPORTER_OTLP_PROTOCOL) {
211
+ workspace.OTEL_EXPORTER_OTLP_PROTOCOL = config.protocol;
212
+ }
213
+ }
138
214
  return {
139
215
  ...workspace,
140
216
  [OTEL_EXTENSION_PATH_ENV]: config.extensionPath,
@@ -235,10 +311,12 @@ function readWorkspaceOtelCache(cachePath) {
235
311
  }
236
312
  }
237
313
  /**
238
- * Full launch-time resolution, all three sources in precedence order:
314
+ * Full launch-time resolution, all four sources in precedence order:
239
315
  *
240
316
  * 1. `PI_OTEL_DISABLED` — personal kill switch, beats everything.
241
- * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` the user's own setup, untouched.
317
+ * 2. env `OTEL_EXPORTER_OTLP_ENDPOINT` / `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`
318
+ * — the user's own setup, untouched (generic wins
319
+ * over per-signal).
242
320
  * 3. workspace config — admin-set in the web app, fetched/cached.
243
321
  * 4. repo `.pi/settings.json` — committed per-repo config.
244
322
  *
@@ -249,7 +327,7 @@ export async function resolveOtelLaunchWithWorkspace(opts) {
249
327
  if (envDisabled(env))
250
328
  return undefined;
251
329
  // Source 2: the user's own env config short-circuits — no fetch needed.
252
- if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim()) {
330
+ if (env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim()) {
253
331
  return resolveOtelLaunch({
254
332
  env,
255
333
  cwd,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yagni-app/code-staging",
3
- "version": "1.0.5-staging.1241.1",
3
+ "version": "1.0.6-staging.1244.1",
4
4
  "description": "YAGNI Code: a terminal coding agent that already knows your company. One YAGNI login routes the model and grounds the agent in your team's context.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": "YAGNI, Inc. <jack@yagni.app> (https://yagni.app)",
@@ -42,5 +42,5 @@
42
42
  "turndown": "^7.2.4",
43
43
  "typebox": "^1.3.15"
44
44
  },
45
- "yagniSourceSha": "9328192141ab3d96cbeb324d748df1e571fe5eb2"
45
+ "yagniSourceSha": "09cd57add6ba5c3028cc4f971459d5c7f9d4da97"
46
46
  }