@infinitedusky/indusk-mcp 1.5.14 → 1.6.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.
@@ -597,9 +597,16 @@ function printMcpInstructions(name, manifest) {
597
597
  ];
598
598
  const cmd = `claude ${args.map(a => a.includes(" ") ? `"${a}"` : a).join(" ")}`;
599
599
  console.info(`\n ${name}: adding MCP server with credentials from .env...`);
600
+ // Remove existing entry first so we always write fresh credentials
601
+ try {
602
+ execSync(`claude mcp remove -s project ${name}`, { cwd: process.cwd(), timeout: 10000, stdio: ["ignore", "pipe", "pipe"] });
603
+ }
604
+ catch {
605
+ // Ignore — may not exist
606
+ }
600
607
  try {
601
608
  execSync(cmd, { cwd: process.cwd(), timeout: 15000, stdio: ["ignore", "pipe", "pipe"] });
602
- console.info(` ${name}: MCP server added (restart Claude Code to load)`);
609
+ console.info(` ${name}: MCP server configured (restart Claude Code to load)`);
603
610
  return;
604
611
  }
605
612
  catch (e) {
@@ -607,6 +614,7 @@ function printMcpInstructions(name, manifest) {
607
614
  const stderr = err.stderr ? String(err.stderr).trim() : "";
608
615
  console.info(` ${name}: auto-add failed. ${stderr || err.message || ""}`);
609
616
  console.info(` command was: ${cmd}`);
617
+ return;
610
618
  }
611
619
  }
612
620
  }
@@ -190,6 +190,9 @@ export async function init(projectRoot, options = {}) {
190
190
  existsSync(join(projectRoot, "next.config.mjs"));
191
191
  const isPython = existsSync(join(projectRoot, "requirements.txt")) ||
192
192
  existsSync(join(projectRoot, "pyproject.toml"));
193
+ const isReactSPA = !isNextJs &&
194
+ (existsSync(join(projectRoot, "vite.config.ts")) ||
195
+ existsSync(join(projectRoot, "vite.config.js")));
193
196
  if (isPython) {
194
197
  const pyTemplate = "instrumentation.py";
195
198
  const targetFile = join(projectRoot, pyTemplate);
@@ -203,9 +206,64 @@ export async function init(projectRoot, options = {}) {
203
206
  console.info(" run: opentelemetry-instrument python your_app.py");
204
207
  }
205
208
  }
209
+ else if (isNextJs) {
210
+ // Next.js — use @vercel/otel
211
+ const instrTarget = join(projectRoot, "instrumentation.ts");
212
+ if (existsSync(instrTarget) && !force) {
213
+ console.info(" skip: instrumentation.ts (already exists)");
214
+ }
215
+ else {
216
+ cpSync(join(packageRoot, "templates/instrumentation.next.ts"), instrTarget);
217
+ const content = readFileSync(instrTarget, "utf-8");
218
+ writeFileSync(instrTarget, content.replace('"unknown-service"', `"${projectName}"`));
219
+ console.info(" create: instrumentation.ts (Next.js — @vercel/otel)");
220
+ }
221
+ // Logger
222
+ const loggerTarget = join(projectRoot, "src/logger.ts");
223
+ if (existsSync(loggerTarget) && !force) {
224
+ console.info(" skip: src/logger.ts (already exists)");
225
+ }
226
+ else {
227
+ const loggerDir = join(projectRoot, "src");
228
+ mkdirSync(loggerDir, { recursive: true });
229
+ cpSync(join(packageRoot, "templates/logger.ts"), loggerTarget);
230
+ console.info(" create: src/logger.ts");
231
+ }
232
+ // Client-side browser instrumentation
233
+ const webInstrTarget = join(projectRoot, "src/instrumentation.web.ts");
234
+ if (existsSync(webInstrTarget) && !force) {
235
+ console.info(" skip: src/instrumentation.web.ts (already exists)");
236
+ }
237
+ else {
238
+ const srcDir = join(projectRoot, "src");
239
+ mkdirSync(srcDir, { recursive: true });
240
+ cpSync(join(packageRoot, "templates/instrumentation.web.ts"), webInstrTarget);
241
+ const content = readFileSync(webInstrTarget, "utf-8");
242
+ writeFileSync(webInstrTarget, content.replace('"unknown-service"', `"${projectName}"`));
243
+ console.info(" create: src/instrumentation.web.ts (browser — OTel Web SDK)");
244
+ }
245
+ console.info(" install: pnpm add @vercel/otel pino pino-opentelemetry-transport @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction");
246
+ console.info(" wire (server): Next.js loads instrumentation.ts automatically");
247
+ console.info(" wire (client): import './instrumentation.web' in your root client component or layout");
248
+ }
249
+ else if (isReactSPA) {
250
+ // React SPA (Vite) — browser instrumentation via Dash0 Web SDK
251
+ const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
252
+ const instrTarget = join(srcDir, "instrumentation.ts");
253
+ if (existsSync(instrTarget) && !force) {
254
+ console.info(" skip: instrumentation.ts (already exists)");
255
+ }
256
+ else {
257
+ cpSync(join(packageRoot, "templates/instrumentation.web.ts"), instrTarget);
258
+ const content = readFileSync(instrTarget, "utf-8");
259
+ writeFileSync(instrTarget, content.replace('"unknown-service"', `"${projectName}"`));
260
+ console.info(" create: src/instrumentation.ts (React SPA — @dash0/sdk-web)");
261
+ }
262
+ console.info(" install: pnpm add @opentelemetry/sdk-trace-web @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions @opentelemetry/instrumentation @opentelemetry/instrumentation-fetch @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction");
263
+ console.info(" wire: import './instrumentation' at the top of your main.tsx or App.tsx");
264
+ }
206
265
  else {
207
- // Node.js or Next.js copy TypeScript templates
208
- const targetDir = isNextJs ? projectRoot : join(projectRoot, "src");
266
+ // Node.js full SDK with auto-instrumentation
209
267
  const srcDir = existsSync(join(projectRoot, "src")) ? join(projectRoot, "src") : projectRoot;
210
268
  for (const template of otelTemplates) {
211
269
  const targetFile = join(srcDir, template);
@@ -222,7 +280,6 @@ export async function init(projectRoot, options = {}) {
222
280
  const content = readFileSync(instrPath, "utf-8");
223
281
  writeFileSync(instrPath, content.replace('"unknown-service"', `"${projectName}"`));
224
282
  }
225
- // Print instructions
226
283
  const packages = [
227
284
  "@opentelemetry/sdk-node",
228
285
  "@opentelemetry/auto-instrumentations-node",
@@ -235,12 +292,7 @@ export async function init(projectRoot, options = {}) {
235
292
  "pino-opentelemetry-transport",
236
293
  ];
237
294
  console.info(` install: pnpm add ${packages.join(" ")}`);
238
- if (isNextJs) {
239
- console.info(" wire: Next.js instrumentation hook (instrumentation.ts in app root)");
240
- }
241
- else {
242
- console.info(" wire: node -r ./src/instrumentation.ts src/index.ts");
243
- }
295
+ console.info(" wire: node --import ./src/instrumentation.ts src/index.ts");
244
296
  }
245
297
  } // end isInduskMcp else
246
298
  // 8. Install gate enforcement hooks
@@ -1,13 +1,24 @@
1
+ ---
2
+ name: otel
3
+ description: OpenTelemetry instrumentation patterns — span naming, categories, structured logging, error propagation, sensitive data, and validation
4
+ ---
5
+
1
6
  # OpenTelemetry
2
7
 
3
- Every service is instrumented from day one. `init` creates the instrumentation files. This skill teaches you how to use them.
8
+ Every service is instrumented from day one. `init` creates the instrumentation files. This skill teaches you how to produce high-quality telemetry.
4
9
 
5
10
  ## What's Already Set Up
6
11
 
7
- After `init`, your project has:
8
- - `instrumentation.ts` — auto-instruments HTTP, database, and framework calls
9
- - `filtering-exporter.ts` controls which span categories get exported
10
- - `logger.ts` — Pino structured logger with stdout + OTLP dual output
12
+ After `init`, your project has instrumentation files for your runtime. See the OTel reference docs in the indusk-docs site for the full list per runtime.
13
+
14
+ ## Core Principle: Signal Density Over Volume
15
+
16
+ Every telemetry item should serve one of three purposes:
17
+ - **Detect** — help identify that something is wrong
18
+ - **Localize** — help pinpoint where the problem is
19
+ - **Explain** — help understand why it happened
20
+
21
+ If it doesn't serve one of these purposes, don't emit it.
11
22
 
12
23
  ## When to Add Manual Spans
13
24
 
@@ -22,20 +33,61 @@ If it's a meaningful action that you'd want to see in a trace, add a span.
22
33
 
23
34
  ## Span Naming
24
35
 
25
- Use `{domain}.{entity}.{action}`:
36
+ Span names MUST be low-cardinality. The number of unique span names should be bounded and small.
26
37
 
27
- ```typescript
28
- const tracer = trace.getTracer('my-service');
38
+ ### General pattern: `{domain}.{entity}.{action}`
29
39
 
30
- // Good
40
+ ```typescript
41
+ // Good — low cardinality
31
42
  tracer.startSpan('poker.hand.deal');
32
43
  tracer.startSpan('auth.session.create');
33
44
  tracer.startSpan('settlement.receipt.sign');
34
45
 
35
- // Bad
36
- tracer.startSpan('processRequest');
37
- tracer.startSpan('handle');
38
- tracer.startSpan('doThing');
46
+ // Bad — high cardinality (contains variable data)
47
+ tracer.startSpan(`process_payment_for_${userId}`); // user ID in name!
48
+ tracer.startSpan(`GET /api/users/${id}`); // path param in name!
49
+ ```
50
+
51
+ Variable data (user IDs, order numbers, room codes) goes in **attributes**, never in the span name.
52
+
53
+ ### Per-signal naming
54
+
55
+ | Signal | Format | Example |
56
+ |--------|--------|---------|
57
+ | HTTP server | `{method} {route}` | `GET /api/users/:id` |
58
+ | HTTP client | `{method} {template}` | `POST /checkout` |
59
+ | Database | `{operation} {collection}` | `SELECT orders` |
60
+ | Messaging | `{operation} {destination}` | `publish shop.orders` |
61
+ | Business logic | `{domain}.{entity}.{action}` | `poker.hand.deal` |
62
+
63
+ ## Span Kind
64
+
65
+ Each span has exactly one kind. Choose based on the communication pattern:
66
+
67
+ | Kind | Use When | Examples |
68
+ |------|----------|---------|
69
+ | `SERVER` | Handling an inbound sync request | HTTP handler, gRPC handler |
70
+ | `CLIENT` | Making an outbound sync request | HTTP call, database query, outbound RPC |
71
+ | `PRODUCER` | Initiating an async operation | Publishing to a queue |
72
+ | `CONSUMER` | Processing an async operation | Processing a queued message |
73
+ | `INTERNAL` | Internal operation, no remote peer | In-memory computation, business logic |
74
+
75
+ Common mistakes:
76
+ - Using `INTERNAL` for everything — database calls are `CLIENT`, HTTP handlers are `SERVER`
77
+ - Using `CLIENT` for message publishing — that's `PRODUCER` (async, no response expected)
78
+
79
+ ## Span Status
80
+
81
+ Only set error status explicitly. Success is the default.
82
+
83
+ ```typescript
84
+ import { SpanStatusCode } from '@opentelemetry/api';
85
+
86
+ // Only set on error
87
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
88
+
89
+ // Don't set OK explicitly — it's the default
90
+ // span.setStatus({ code: SpanStatusCode.OK }); // unnecessary
39
91
  ```
40
92
 
41
93
  ## Custom Attributes
@@ -43,38 +95,43 @@ tracer.startSpan('doThing');
43
95
  Always add domain-specific data to spans:
44
96
 
45
97
  ```typescript
46
- const span = tracer.startSpan('poker.hand.deal');
47
- span.setAttribute('otel.category', 'business');
48
- span.setAttribute('room.code', roomCode);
49
- span.setAttribute('hand.number', handNumber);
50
- span.setAttribute('player.count', players.length);
51
- // ... do work ...
52
- span.end();
98
+ const span = tracer.startSpan('poker.hand.deal', {
99
+ attributes: {
100
+ 'otel.category': 'business',
101
+ 'room.code': roomCode,
102
+ 'hand.number': handNumber,
103
+ 'player.count': players.length,
104
+ },
105
+ });
53
106
  ```
54
107
 
108
+ ### Attribute hygiene
109
+
110
+ - Use snake_case with dots for namespacing: `room.code`, `player.count`
111
+ - Keep values low-cardinality where possible
112
+ - Never put variable-length user input in attributes without truncation
113
+
55
114
  ## Categories
56
115
 
57
- Every manual span should get an `otel.category` attribute. This controls whether it gets exported:
116
+ Every manual span should get an `otel.category` attribute:
58
117
 
59
118
  | Category | What it covers | Examples |
60
119
  |----------|---------------|----------|
61
- | `http` | HTTP server/client requests | Auto-instrumented |
120
+ | `http` | HTTP requests | Auto-instrumented |
62
121
  | `db` | Database queries | Auto-instrumented |
63
122
  | `business` | Domain events | hand dealt, user registered, payment processed |
64
123
  | `inference` | LLM/AI calls | Gemini generate, embedding create |
65
- | `state` | State transitions | game started, order status changed |
66
- | `system` | Infrastructure | health checks, cron jobs, queue processing |
124
+ | `state` | State transitions | game started, order shipped |
125
+ | `system` | Infrastructure | health checks, cron, queue processing |
67
126
 
68
- Control which categories are exported via `OTEL_ENABLED_CATEGORIES`:
127
+ Control export via `OTEL_ENABLED_CATEGORIES`:
69
128
 
70
129
  ```bash
71
- # Export only HTTP and business spans
72
- OTEL_ENABLED_CATEGORIES=http,business node -r ./instrumentation.ts src/index.ts
73
-
74
- # Export everything (default)
75
- node -r ./instrumentation.ts src/index.ts
130
+ OTEL_ENABLED_CATEGORIES=http,business # only these categories exported
76
131
  ```
77
132
 
133
+ Unset = all categories exported. Spans without a category are always exported.
134
+
78
135
  ## Structured Logging with Pino
79
136
 
80
137
  Use the project logger, not `console.log`:
@@ -84,70 +141,155 @@ import { logger } from './logger';
84
141
 
85
142
  // Good — structured, with context
86
143
  logger.info({ roomCode, players: players.length }, 'hand started');
87
- logger.error({ err, traceId: span.spanContext().traceId }, 'settlement failed');
88
- logger.warn({ queueDepth: 150, threshold: 100 }, 'queue depth approaching limit');
144
+ logger.error({ err, orderId }, 'payment failed');
89
145
 
90
146
  // Bad — unstructured string
91
147
  console.log('Hand started for room ' + roomCode);
92
- console.log('Error: ' + err.message);
93
148
  ```
94
149
 
95
- ### Log Levels
150
+ ### Log levels
96
151
 
97
152
  | Level | Meaning | When to use |
98
153
  |-------|---------|------------|
99
- | `error` | Something is broken | Unhandled errors, failed operations that should succeed |
100
- | `warn` | Degraded but functional | Approaching limits, fallback behavior triggered |
101
- | `info` | Business events | State transitions, user actions, completed operations |
102
- | `debug` | Development details | Variable values, flow tracing — disable in production |
154
+ | `error` | Something is broken | Unhandled errors, failed operations |
155
+ | `warn` | Degraded but functional | Approaching limits, fallback behavior |
156
+ | `info` | Business events | State transitions, completed operations |
157
+ | `debug` | Development details | Disable in production |
158
+
159
+ ### Structured logging safeguards
160
+
161
+ Never spread entire objects — explicitly pick safe fields:
162
+
163
+ ```typescript
164
+ // BAD: spreads entire request body — may contain passwords, tokens, PII
165
+ logger.info({ ...req.body }, 'user signup');
166
+
167
+ // GOOD: explicitly select safe fields
168
+ logger.info({ userId: req.body.userId, plan: req.body.plan }, 'user signup');
169
+ ```
170
+
171
+ ## Trace Correlation in Logs
172
+
173
+ Every log inside an active span should carry trace context:
174
+
175
+ ```typescript
176
+ import { trace, context } from '@opentelemetry/api';
177
+
178
+ function getTraceContext() {
179
+ const span = trace.getSpan(context.active());
180
+ if (!span) return {};
181
+ const ctx = span.spanContext();
182
+ return { traceId: ctx.traceId, spanId: ctx.spanId };
183
+ }
184
+
185
+ logger.info({ ...getTraceContext(), orderId }, 'order placed');
186
+ ```
187
+
188
+ Wrap this in a helper so every log call includes trace context automatically. Without it, logs are isolated events that can't be connected to the request that produced them.
103
189
 
104
190
  ## Error Propagation
105
191
 
106
- Errors must always include trace context:
192
+ Errors must always include trace context. Never swallow silently:
107
193
 
108
194
  ```typescript
109
195
  try {
110
196
  await processSettlement(hand);
111
197
  } catch (err) {
198
+ // 1. Record on span
112
199
  span.recordException(err);
113
200
  span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
114
- logger.error({ err, traceId: span.spanContext().traceId }, 'settlement failed');
115
- throw err; // re-throw with context preserved
201
+
202
+ // 2. Log with correlation
203
+ logger.error({ err, ...getTraceContext() }, 'settlement failed');
204
+
205
+ // 3. Re-throw — don't swallow
206
+ throw err;
116
207
  }
117
208
  ```
118
209
 
119
- Never swallow errors silently. Every catch block should either:
120
- 1. Re-throw with context
210
+ Every catch block should either:
211
+ 1. Re-throw with context preserved
121
212
  2. Log with trace correlation and handle completely
122
213
 
214
+ ## Sensitive Data
215
+
216
+ Never attach these to spans, logs, or metrics:
217
+
218
+ | Category | Examples |
219
+ |----------|----------|
220
+ | Auth credentials | Passwords, API keys, bearer tokens, session cookies |
221
+ | Financial instruments | Credit card numbers, bank accounts, CVVs |
222
+ | Government IDs | SSN, passport numbers, tax IDs |
223
+ | Full auth headers | `Authorization`, `Cookie` header values |
224
+
225
+ For URLs, strip query parameters that carry tokens or user input:
226
+
227
+ ```typescript
228
+ // BAD
229
+ span.setAttribute('http.url', 'https://example.com/callback?token=eyJhbG...');
230
+
231
+ // GOOD
232
+ span.setAttribute('http.url', sanitizeUrl(req.url));
233
+ ```
234
+
235
+ For database queries, never include literal parameter values — use parameterized queries.
236
+
237
+ ## Validation Checklist
238
+
239
+ After instrumenting a service, verify:
240
+
241
+ 1. **Service appears in backend** — search for your `OTEL_SERVICE_NAME` in Dash0
242
+ 2. **Resource attributes present** — `service.name`, `service.version`, `deployment.environment`
243
+ 3. **Expected span names appear** — list your endpoints and business operations, verify each produces a span
244
+ 4. **Attributes are populated** — check that domain attributes (`room.code`, `player.count`) are present
245
+ 5. **Errors are tracked** — trigger an error, verify it appears with exception details and trace context
246
+ 6. **Logs correlate with traces** — verify `traceId` and `spanId` appear in log records
247
+
123
248
  ## Framework-Specific Notes
124
249
 
250
+ ### Node.js (ESM)
251
+ - Load instrumentation with `--import`: `node --import ./src/instrumentation.ts src/index.ts`
252
+ - ESM requires `--import`, not `--require`
253
+
125
254
  ### Next.js
126
- The `instrumentation.ts` file in the app root is loaded via the Next.js instrumentation hook (13.4+). No `-r` flag needed.
255
+ - Server: `instrumentation.ts` at app root, loaded automatically by Next.js (13.4+)
256
+ - Client: `src/instrumentation.web.ts`, import in your root client component
257
+ - Both server and client need separate instrumentation — they run in different environments
258
+
259
+ ### React SPA (Vite)
260
+ - `import './instrumentation'` at the top of `main.tsx`
261
+ - Browser env vars use `VITE_` prefix: `VITE_OTEL_EXPORTER_OTLP_ENDPOINT`
127
262
 
128
263
  ### Python
129
- Use the `opentelemetry-instrument` CLI wrapper:
130
- ```bash
131
- opentelemetry-instrument python your_app.py
132
- ```
133
- Or import the instrumentation module before your app code.
264
+ - Use `opentelemetry-instrument` CLI: `opentelemetry-instrument python your_app.py`
265
+ - Or import `instrumentation.py` before app code
134
266
 
135
267
  ## Environment Variables
136
268
 
269
+ ### Server
270
+
137
271
  | Variable | Purpose | Default |
138
272
  |----------|---------|---------|
139
- | `OTEL_SERVICE_NAME` | Service name in traces | Project name (set by init) |
140
- | `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL | Not set (uses console exporter) |
141
- | `OTEL_EXPORTER_OTLP_HEADERS` | Auth headers for backend | Not set |
142
- | `OTEL_ENABLED_CATEGORIES` | Comma-separated active categories | All enabled |
273
+ | `OTEL_SERVICE_NAME` | Service name | Set by init |
274
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL | Not set (console) |
275
+ | `OTEL_EXPORTER_OTLP_HEADERS` | Auth headers | Not set |
276
+ | `OTEL_ENABLED_CATEGORIES` | Active categories | All |
143
277
  | `LOG_LEVEL` | Pino log level | `info` |
144
278
 
145
- When no `OTEL_EXPORTER_OTLP_ENDPOINT` is set, traces go to the console exporter (stdout). Enable a backend (like Dash0) to send traces remotely.
279
+ ### Browser
280
+
281
+ | Variable | Purpose |
282
+ |----------|---------|
283
+ | `VITE_OTEL_EXPORTER_OTLP_ENDPOINT` / `NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT` | Backend URL |
284
+ | `VITE_OTEL_EXPORTER_OTLP_HEADERS` / `NEXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS` | Auth headers |
146
285
 
147
- ## Verification
286
+ ## During `/work`
148
287
 
149
- During `/work`, check:
150
- - Are new endpoints/business logic instrumented?
151
- - Do manual spans have meaningful names and category attributes?
152
- - Are errors recorded with trace context?
153
- - Is structured logging used (not console.log)?
288
+ When implementing or reviewing code, check:
289
+ - Are new endpoints and business logic instrumented with manual spans?
290
+ - Do spans have meaningful names (low-cardinality, `{domain}.{entity}.{action}`)?
291
+ - Do spans have the right kind (SERVER, CLIENT, INTERNAL)?
292
+ - Do spans have `otel.category` and domain-specific attributes?
293
+ - Are errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log?
294
+ - Is structured logging used (not `console.log`)?
295
+ - Is sensitive data excluded from attributes and logs?
@@ -117,8 +117,8 @@ function detectWorkflow(content) {
117
117
 
118
118
  // Which gate types are required per workflow
119
119
  const WORKFLOW_GATES = {
120
- feature: ["verification", "context", "document"],
121
- refactor: ["verification", "context", "document"],
120
+ feature: ["verification", "otel", "context", "document"],
121
+ refactor: ["verification", "otel", "context", "document"],
122
122
  bugfix: ["verification", "document"],
123
123
  spike: [],
124
124
  };
@@ -147,7 +147,7 @@ function parsePhases(content) {
147
147
  continue;
148
148
  }
149
149
 
150
- const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|Context|Document)\b/);
150
+ const gateMatch = line.match(/^####\s+Phase\s+\d+\s+(Verification|OTel|Context|Document)\b/);
151
151
  if (gateMatch) {
152
152
  currentGateType = gateMatch[1].toLowerCase();
153
153
  continue;
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * PreToolUse hook: validates that impl phases have all four gate sections.
4
4
  *
5
- * Every phase must have: implementation items, Verification, Context, Document.
5
+ * Every phase must have: implementation items, Verification, OTel, Context, Document.
6
6
  * Sections can opt out with (none needed), (not applicable), or skip-reason: {why}.
7
7
  *
8
8
  * Exit 0 = allow the edit
@@ -102,10 +102,10 @@ const workflow = workflowMatch ? workflowMatch[1] : "feature";
102
102
 
103
103
  // Different workflows have different requirements
104
104
  const requirements = {
105
- feature: { verification: true, context: true, document: true },
106
- refactor: { verification: true, context: true, document: true },
107
- bugfix: { verification: true, context: false, document: true },
108
- spike: { verification: false, context: false, document: false },
105
+ feature: { verification: true, otel: true, context: true, document: true },
106
+ refactor: { verification: true, otel: true, context: true, document: true },
107
+ bugfix: { verification: true, otel: false, context: false, document: true },
108
+ spike: { verification: false, otel: false, context: false, document: false },
109
109
  }[workflow];
110
110
  const lines = body.split("\n");
111
111
 
@@ -122,9 +122,11 @@ for (const line of lines) {
122
122
  name: phaseMatch[2].trim(),
123
123
  hasImplementation: false,
124
124
  hasVerification: false,
125
+ hasOtel: false,
125
126
  hasContext: false,
126
127
  hasDocument: false,
127
128
  verificationIsOptOut: false,
129
+ otelIsOptOut: false,
128
130
  contextIsOptOut: false,
129
131
  documentIsOptOut: false,
130
132
  };
@@ -142,6 +144,13 @@ for (const line of lines) {
142
144
  continue;
143
145
  }
144
146
 
147
+ const otelMatch = line.match(/^####\s+Phase\s+\d+\s+OTel\b/);
148
+ if (otelMatch) {
149
+ currentPhase.hasOtel = true;
150
+ currentSection = "otel";
151
+ continue;
152
+ }
153
+
145
154
  const ctxMatch = line.match(/^####\s+Phase\s+\d+\s+Context\b/);
146
155
  if (ctxMatch) {
147
156
  currentPhase.hasContext = true;
@@ -168,6 +177,7 @@ for (const line of lines) {
168
177
  line.includes("skip-reason:");
169
178
  if (currentPhase && isOptOutLine) {
170
179
  if (currentSection === "verification") currentPhase.verificationIsOptOut = true;
180
+ if (currentSection === "otel") currentPhase.otelIsOptOut = true;
171
181
  if (currentSection === "context") currentPhase.contextIsOptOut = true;
172
182
  if (currentSection === "document") currentPhase.documentIsOptOut = true;
173
183
  }
@@ -186,6 +196,7 @@ for (const phase of phases) {
186
196
 
187
197
  const missing = [];
188
198
  if (requirements.verification && !phase.hasVerification) missing.push("Verification");
199
+ if (requirements.otel && !phase.hasOtel) missing.push("OTel");
189
200
  if (requirements.context && !phase.hasContext) missing.push("Context");
190
201
  if (requirements.document && !phase.hasDocument) missing.push("Document");
191
202
 
@@ -200,6 +211,8 @@ for (const phase of phases) {
200
211
  const optOuts = [];
201
212
  if (requirements.verification && phase.hasVerification && phase.verificationIsOptOut)
202
213
  optOuts.push("Verification");
214
+ if (requirements.otel && phase.hasOtel && phase.otelIsOptOut)
215
+ optOuts.push("OTel");
203
216
  if (requirements.context && phase.hasContext && phase.contextIsOptOut) optOuts.push("Context");
204
217
  if (requirements.document && phase.hasDocument && phase.documentIsOptOut)
205
218
  optOuts.push("Document");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@infinitedusky/indusk-mcp",
3
- "version": "1.5.14",
3
+ "version": "1.6.0",
4
4
  "description": "InDusk development system — skills, MCP tools, and CLI for structured AI-assisted development",
5
5
  "type": "module",
6
6
  "files": [
package/skills/plan.md CHANGED
@@ -256,8 +256,11 @@ For multi-phase impls, include a boundary map showing what each phase produces a
256
256
  function withdrawFor(wallet: address, player: address, amount: uint256, historyHash: bytes32)
257
257
  ```
258
258
 
259
+ #### Phase 1 OTel
260
+ - [ ] {Instrumentation check — are new code paths observable? See the OTel skill for patterns. Example items: "New endpoints have manual spans with `otel.category` and domain attributes", "Errors recorded with `recordException` + `setStatus(ERROR)` + trace-correlated log". Ask: "did this phase add endpoints, business logic, state transitions, or error paths?" If not, this section can be opted out per gate policy.}
261
+
259
262
  #### Phase 1 Verification
260
- - [ ] {Verification step — prove this phase works. Must be a specific runnable command with expected output, not "verify it works." See the verify skill for guidance on what checks a phase needs based on what changed.}
263
+ - [ ] {Verification step — prove this phase works. Must be a specific runnable command with expected output, not "verify it works." See the verify skill for guidance on what checks a phase needs based on what changed. Can include trace verification if OTel was added.}
261
264
 
262
265
  #### Phase 1 Context
263
266
  - [ ] {Concrete CLAUDE.md edit this phase produces — e.g., "Add to Architecture: ...", "Add to Conventions: ...", "Update Current State: ...". Ask: "what does this phase change about how the project works?" If nothing, omit this section.}
@@ -48,6 +48,77 @@ When a new session begins:
48
48
  3. Call `list_plans` — understand what plans exist, their stages, and what's in progress.
49
49
  4. Call `get_context` — read the project's CLAUDE.md to understand architecture, conventions, and current state.
50
50
 
51
+ ## Creating a New App
52
+
53
+ When starting a new service or app in the monorepo, follow this sequence:
54
+
55
+ ### 1. Create the app
56
+
57
+ | Runtime | Command |
58
+ |---------|---------|
59
+ | **Next.js** | `npx create-next-app apps/my-app` |
60
+ | **React SPA** (Vite) | `pnpm create vite apps/my-app --template react-ts` |
61
+ | **Node.js** (Express/Fastify) | Create `apps/my-app/` with `package.json`, `src/index.ts` |
62
+ | **Python** | Create `apps/my-app/` with `pyproject.toml` or `requirements.txt` |
63
+
64
+ ### 2. Initialize indusk-mcp
65
+
66
+ ```bash
67
+ cd apps/my-app
68
+ npx @infinitedusky/indusk-mcp init
69
+ ```
70
+
71
+ This detects the runtime and scaffolds:
72
+ - Skills, hooks, lessons, CLAUDE.md
73
+ - **Next.js**: `instrumentation.ts` (app root) using `@vercel/otel`, `src/logger.ts` using Pino
74
+ - **Node.js**: `src/instrumentation.ts` (full OTel SDK + FilteringExporter), `src/filtering-exporter.ts`, `src/logger.ts`
75
+ - **Python**: `instrumentation.py` (OTel SDK with auto-instrumentation)
76
+ - Biome config, VS Code settings, `.cgcignore`
77
+
78
+ ### 3. Install the printed packages
79
+
80
+ `init` prints the install command — run it:
81
+ - **Next.js**: `pnpm add @vercel/otel pino pino-opentelemetry-transport`
82
+ - **Node.js**: `pnpm add @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node ...` (full list printed by init)
83
+ - **Python**: `pip install opentelemetry-distro opentelemetry-instrumentation opentelemetry-exporter-otlp`
84
+
85
+ ### 4. Wire instrumentation into the entry point
86
+
87
+ - **Next.js**: automatic — Next.js loads `instrumentation.ts` from the app root
88
+ - **Node.js**: `node --import ./src/instrumentation.ts src/index.ts`
89
+ - **Python**: `opentelemetry-instrument python your_app.py`
90
+
91
+ ### 5. Create a composable.env contract (if using composable.env)
92
+
93
+ Create `env/contracts/my-app.contract.json`:
94
+ ```json
95
+ {
96
+ "name": "my-app",
97
+ "location": "apps/my-app",
98
+ "vars": {
99
+ "OTEL_SERVICE_NAME": "my-app",
100
+ "OTEL_EXPORTER_OTLP_ENDPOINT": "${dash0.HTTP_ENDPOINT}",
101
+ "OTEL_EXPORTER_OTLP_HEADERS": "${dash0.OTLP_HEADERS}"
102
+ }
103
+ }
104
+ ```
105
+
106
+ Then `pnpm ce env:build` generates the `.env` files.
107
+
108
+ ### 6. Enable extensions
109
+
110
+ ```bash
111
+ npx @infinitedusky/indusk-mcp extensions enable dash0
112
+ ```
113
+
114
+ If `.indusk/extensions/dash0/.env` has credentials, this auto-configures the MCP server. Otherwise it tells you what to create.
115
+
116
+ ### 7. Verify
117
+
118
+ - Start the app, hit an endpoint
119
+ - Check Dash0 for traces: `mcp__dash0__getSpans` with `service.name = my-app`
120
+ - Check health: `mcp__indusk__check_health` should show OTel extension healthy
121
+
51
122
  ## Before Modifying Code
52
123
 
53
124
  Before touching any file:
package/skills/work.md CHANGED
@@ -52,14 +52,15 @@ Implementation plans live in `planning/{plan-name}/impl.md` as checklists. Your
52
52
  - Add it as a new item in the appropriate phase
53
53
  - Then do it and check it off
54
54
 
55
- 10. **Per-phase completion order.** Each phase has up to four types of items. Complete them in this order:
55
+ 10. **Per-phase completion order.** Each phase has up to five types of items. Complete them in this order:
56
56
 
57
57
  **Implementation items** → build the thing
58
- **Verification items** → prove it works (tests, type checks, commands)
58
+ **OTel items** → instrument it (spans, categories, error recording — see OTel skill)
59
+ **Verification items** → prove it works (tests, type checks, commands — can include trace verification)
59
60
  **Context items** → capture what changed (concrete CLAUDE.md edits)
60
61
  **Document items** → write or update docs pages (see document skill)
61
62
 
62
- A phase is not complete until all four are done. **Enforced by hooks:** if you try to check off a Phase N+1 implementation item while Phase N has unchecked gates, the edit will be blocked with a message listing what's missing. Complete the gates first.
63
+ A phase is not complete until all five are done. **Enforced by hooks:** if you try to check off a Phase N+1 implementation item while Phase N has unchecked gates, the edit will be blocked with a message listing what's missing. Complete the gates first.
63
64
 
64
65
  ## Gate Override Policy
65
66
 
@@ -171,7 +172,7 @@ Then **stop and wait** for the user to say "continue" before moving to the next
171
172
 
172
173
  ### At gate transitions:
173
174
 
174
- When moving between gates (implement → verify → context → document → next phase), explain the transition: what gate you're entering, why it exists, and what it catches. Example: "Code is written. Now we verify — type check, lint, tests. This catches errors before they compound into the next phase."
175
+ When moving between gates (implement → otel → verify → context → document → next phase), explain the transition: what gate you're entering, why it exists, and what it catches. Example: "Code is written. Now OTel — instrument the new code paths. Then verify — type check, lint, tests, trace verification. Then context and docs."
175
176
 
176
177
  ### Between checklist items:
177
178
 
@@ -0,0 +1,21 @@
1
+ /**
2
+ * OpenTelemetry Instrumentation for Next.js
3
+ *
4
+ * This file is automatically loaded by Next.js via the instrumentation hook (13.4+).
5
+ * Place it in the app root (next to next.config.ts).
6
+ *
7
+ * Configuration via environment variables:
8
+ * OTEL_SERVICE_NAME — service name (defaults to "unknown-service")
9
+ * OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL
10
+ * OTEL_EXPORTER_OTLP_HEADERS — auth headers for the backend
11
+ *
12
+ * Install: pnpm add @vercel/otel
13
+ */
14
+
15
+ import { registerOTel } from "@vercel/otel";
16
+
17
+ export function register() {
18
+ registerOTel({
19
+ serviceName: process.env.OTEL_SERVICE_NAME ?? "unknown-service",
20
+ });
21
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * OpenTelemetry Browser Instrumentation
3
+ *
4
+ * Captures page loads, HTTP requests (fetch/XHR), and user interactions
5
+ * from the browser and sends them to any OTLP-compatible backend.
6
+ *
7
+ * Import this as early as possible in your app (main.tsx or App.tsx):
8
+ * import './instrumentation';
9
+ *
10
+ * Configuration via environment variables (use VITE_ or NEXT_PUBLIC_ prefix):
11
+ * VITE_OTEL_SERVICE_NAME — service name
12
+ * VITE_OTEL_EXPORTER_OTLP_ENDPOINT — OTLP backend URL
13
+ * VITE_OTEL_EXPORTER_OTLP_HEADERS — auth headers (e.g., "Authorization=Bearer xxx")
14
+ *
15
+ * Install:
16
+ * pnpm add @opentelemetry/sdk-trace-web @opentelemetry/instrumentation-fetch
17
+ * pnpm add @opentelemetry/instrumentation-document-load @opentelemetry/instrumentation-user-interaction
18
+ * pnpm add @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/semantic-conventions
19
+ */
20
+
21
+ import { WebTracerProvider } from "@opentelemetry/sdk-trace-web";
22
+ import { BatchSpanProcessor, SimpleSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base";
23
+ import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
24
+ import { resourceFromAttributes } from "@opentelemetry/resources";
25
+ import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
26
+ import { FetchInstrumentation } from "@opentelemetry/instrumentation-fetch";
27
+ import { DocumentLoadInstrumentation } from "@opentelemetry/instrumentation-document-load";
28
+ import { UserInteractionInstrumentation } from "@opentelemetry/instrumentation-user-interaction";
29
+ import { registerInstrumentations } from "@opentelemetry/instrumentation";
30
+
31
+ const endpoint =
32
+ import.meta.env?.VITE_OTEL_EXPORTER_OTLP_ENDPOINT
33
+ ?? process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_ENDPOINT
34
+ ?? "";
35
+
36
+ const headers =
37
+ import.meta.env?.VITE_OTEL_EXPORTER_OTLP_HEADERS
38
+ ?? process.env.NEXT_PUBLIC_OTEL_EXPORTER_OTLP_HEADERS
39
+ ?? "";
40
+
41
+ const serviceName =
42
+ import.meta.env?.VITE_OTEL_SERVICE_NAME
43
+ ?? process.env.NEXT_PUBLIC_OTEL_SERVICE_NAME
44
+ ?? "unknown-service";
45
+
46
+ // Parse headers string: "Key1=Value1,Key2=Value2" → { Key1: "Value1", Key2: "Value2" }
47
+ function parseHeaders(headerStr: string): Record<string, string> {
48
+ if (!headerStr) return {};
49
+ const result: Record<string, string> = {};
50
+ for (const pair of headerStr.split(",")) {
51
+ const eqIdx = pair.indexOf("=");
52
+ if (eqIdx > 0) {
53
+ result[pair.slice(0, eqIdx).trim()] = pair.slice(eqIdx + 1).trim();
54
+ }
55
+ }
56
+ return result;
57
+ }
58
+
59
+ const provider = new WebTracerProvider({
60
+ resource: resourceFromAttributes({
61
+ [ATTR_SERVICE_NAME]: serviceName,
62
+ }),
63
+ });
64
+
65
+ if (endpoint) {
66
+ const exporter = new OTLPTraceExporter({
67
+ url: `${endpoint}/v1/traces`,
68
+ headers: parseHeaders(headers),
69
+ });
70
+ provider.addSpanProcessor(new BatchSpanProcessor(exporter));
71
+ } else {
72
+ provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter()));
73
+ }
74
+
75
+ provider.register();
76
+
77
+ registerInstrumentations({
78
+ instrumentations: [
79
+ new FetchInstrumentation(),
80
+ new DocumentLoadInstrumentation(),
81
+ new UserInteractionInstrumentation(),
82
+ ],
83
+ });