AI-powered OpenTelemetry instrumentation for JavaScript applications. Analyzes your source code, adds spans, attributes, and context propagation using LLM-guided code generation — validated against your Weaver schema and the Instrumentation Score quality standard. When your schema declares OTel semantic conventions as a dependency, the agent uses them for attribute naming and validates against them.
Spinybacked Orbweaver is an AI agent that adds OpenTelemetry instrumentation to your JavaScript codebase. Point it at your source files, and it:
- Analyzes each file to identify what should be instrumented — external calls (HTTP, DB, message queues), schema-defined spans, and service entry points
- Generates complete instrumented files using an LLM, preferring auto-instrumentation libraries over manual spans
- Validates every change against a two-tier rubric (32 rules covering syntax, non-destructiveness, coverage, restraint, schema fidelity, and code quality) — reverting any file that fails
- Retries intelligently — multi-turn fixes with validation feedback, fresh regeneration with failure hints, and function-level fallback that decomposes complex files into individual functions when whole-file attempts are exhausted
- Commits each file individually on a feature branch, installs dependencies, and opens a PR with a detailed summary
The agent is schema-driven: your Weaver registry defines which spans and attributes exist, and the agent extends the registry as it discovers new instrumentation needs. When your registry declares OTel semantic conventions as a dependency, the resolved schema includes semconv attributes — the agent prefers them for naming, and validation (SCH-002) checks attributes against the full resolved registry. Generated code is evaluated against the Instrumentation Score quality standard. All generated code depends only on @opentelemetry/api — never SDK internals.
Three interfaces: CLI for interactive use, MCP server for AI coding assistants (Claude Code, Cursor, and other MCP-compatible tools), and GitHub Action for CI/CD pipelines.
- Rules Reference — What each validation rule checks and why it matters
- Architecture Overview — How the pipeline works, from file discovery through PR creation
- Interpreting Output — How to read CLI output, PR summaries, and companion files
Given an order processing module (src/orders.js):
import { db } from './db.js';
import { paymentGateway } from './payment.js';
import { emailService } from './email.js';
export async function processOrder(orderId) {
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
const payment = await paymentGateway.charge({
amount: order.total,
currency: order.currency,
customerId: order.customerId,
});
await db.query(
'UPDATE orders SET status = $1, payment_id = $2 WHERE id = $3',
['paid', payment.id, orderId]
);
await emailService.send({
to: order.customerEmail,
template: 'order-confirmation',
data: { orderId, total: order.total },
});
return { orderId, paymentId: payment.id, status: 'paid' };
}Running spiny-orb instrument src/ produces:
import { trace, SpanStatusCode } from '@opentelemetry/api';
import { db } from './db.js';
import { paymentGateway } from './payment.js';
import { emailService } from './email.js';
const tracer = trace.getTracer('order');
export async function processOrder(orderId) {
return tracer.startActiveSpan('processOrder', async (span) => {
try {
span.setAttribute('order.id', orderId);
const order = await db.query('SELECT * FROM orders WHERE id = $1', [orderId]);
if (!order) {
throw new Error(`Order ${orderId} not found`);
}
const payment = await paymentGateway.charge({
amount: order.total,
currency: order.currency,
customerId: order.customerId,
});
await db.query(
'UPDATE orders SET status = $1, payment_id = $2 WHERE id = $3',
['paid', payment.id, orderId]
);
await emailService.send({
to: order.customerEmail,
template: 'order-confirmation',
data: { orderId, total: order.total },
});
span.setAttribute('payment.id', payment.id);
span.setAttribute('order.status', 'paid');
return { orderId, paymentId: payment.id, status: 'paid' };
} catch (error) {
span.recordException(error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw error;
} finally {
span.end();
}
});
}The agent imports only @opentelemetry/api, wraps the business logic in a span, sets schema-defined attributes (order.id, payment.id, order.status), and adds error recording — all validated against your Weaver registry.
| Interface | Best for | How it works |
|---|---|---|
| CLI | Interactive use, one-off instrumentation runs | Run spiny-orb instrument from your terminal |
| MCP Server | AI coding assistants (Claude Code, Cursor, etc.) | Agent calls get-cost-ceiling then instrument via MCP |
| GitHub Action | CI/CD pipelines, automated instrumentation | Add the action to a workflow, get results as step outputs |
All three interfaces share the same spiny-orb.yaml configuration and produce the same results. The CLI creates feature branches with per-file commits and opens PRs. The MCP server and GitHub Action return structured JSON results for integration with AI assistants and CI pipelines.
- Weaver CLI >= 0.21.2 — schema validation and semantic convention resolution (installation guide)
- Anthropic API key — add to a
.envfile in the directory where you run spiny-orb:Or set in your shell environment:ANTHROPIC_API_KEY=your-key
export ANTHROPIC_API_KEY=your-key. See.env.examplefor a template. - A Weaver registry — your project needs a telemetry schema directory that defines your spans and attributes (setup guide, examples)
- An OTel SDK init file — a file that initializes the OpenTelemetry SDK and registers instrumentations (e.g.,
src/instrumentation.js).spiny-orb initauto-detects common file names likesrc/instrumentation.js,src/telemetry.js, orsrc/tracing.js. @opentelemetry/apias a peerDependency — must be in yourpackage.jsonpeerDependencies (not dependencies) to avoid silent trace loss from duplicate instances
-
ghCLI — for automatic PR creation. Ifgh auth logincredentials aren't available to subprocesses, setGITHUB_TOKENin your.envfile. Without gh auth, the agent still creates the feature branch and commits. Use--no-prto suppress the warning.Token requirements: Use a fine-grained personal access token, not a classic token. The token needs Contents: Read and write and Pull requests: Read and write permissions scoped to the target repository. Classic tokens and fine-grained tokens without explicit push scope fail silently — the agent reports a push error rather than a PR URL.
Verify the token is working: After the agent runs, check stderr for the line
pushBranch: urlChanged=true, path=token-swap. This confirms the agent's credential injection mechanism fired with your token. If you seepath=bare-pushinstead, the token was not set or was empty. This diagnostic appears on every push regardless of--verbose. Note: SSH remotes do not use token injection —path=token-swaponly applies to HTTPS remotes. -
An existing test suite — if
testCommandis configured inspiny-orb.yaml, the agent runs it as an end-of-run validation gate. If not configured, it skips the check with a note in the results.
Node.js version: spiny-orb requires Node.js >= 24 (nodejs.org). If you run it on an older version, it exits immediately with a clear message rather than crashing. Your target project (the code you're instrumenting) can use any Node.js version.
Before using any interface, create an spiny-orb.yaml configuration file in your project root.
If you have the CLI installed, run from your project directory:
spiny-orb initThis scans your project, auto-detects the schema directory and SDK init file, validates prerequisites, detects project type (service vs. distributable package), asks about your process lifecycle, and writes spiny-orb.yaml. Use --yes to skip prompts and accept all defaults.
$ spiny-orb init
Checking prerequisites...
Checking Weaver CLI...
Checking port availability...
Detecting SDK init file...
Detecting Weaver schema...
Validating Weaver schema...
Detected project type: service (dependencyStrategy: dependencies)
Target type — short-lived (CLI, Lambda, script) or long-lived (server, worker)?
BatchSpanProcessor drops all spans if the process exits before the 5-second flush. [long-lived]
Configuration summary:
schemaPath: semconv/
sdkInitFile: src/instrumentation.js
dependencyStrategy: dependencies
targetType: long-lived
Create spiny-orb.yaml with these settings? [y/N] y
Writing spiny-orb.yaml...
Created /path/to/your-project/spiny-orb.yaml
If a prerequisite is missing, spiny-orb init exits with code 1 and tells you what's needed:
$ spiny-orb init
Checking prerequisites...
@opentelemetry/api not found in peerDependencies. Add it: npm install --save-peer @opentelemetry/api
Create spiny-orb.yaml in your project root with at minimum:
schemaPath: semconv/ # relative path to your Weaver registry directory
sdkInitFile: src/instrumentation.js # relative path to your OTel SDK init fileTwo fields matter most beyond the required pair. They are independent axes — set both based on what your project actually is:
targetType — how long your process lives:
long-lived(default) — web servers, workers, daemons.BatchSpanProcessorworks fine.short-lived— CLIs, scripts, Lambda, batch jobs.BatchSpanProcessordrops all spans before the 5-second flush timer fires. Switch toSimpleSpanProcessorand interceptprocess.exit().
dependencyStrategy — where packages are installed:
dependencies(default) — services that own their dependency tree.peerDependencies— libraries or distributed packages. Multiple copies of@opentelemetry/apiinnode_modulescause silent trace loss via no-op fallbacks;peerDependenciesprevents that.
Example: a CLI tool is short-lived and dependencies — these are orthogonal:
schemaPath: semconv/
sdkInitFile: src/telemetry.js
targetType: short-lived
dependencyStrategy: dependenciesAll other fields have sensible defaults — see Configuration Reference for the full list.
When the agent runs, it directly modifies two things without asking:
- Source files — adds span wrappers,
setAttributecalls, and imports to each instrumented file. - SDK init file — adds
importstatements andnew InstrumentationClass()entries to theNodeSDKinstrumentationsarray for any auto-instrumentation libraries it discovers.
Everything else appears as guidance in the PR summary only. The agent never touches:
- Span processor selection (
SimpleSpanProcessorvsBatchSpanProcessor) process.exit()interception for short-lived processes
If your targetType is short-lived, configure these in your SDK init file before running the agent, otherwise spans from the first run will be silently dropped.
Follow this order:
- Create your OTel SDK init file (e.g.,
src/instrumentation.js) and register it with Node.js--requireor--import. - For short-lived targets: switch to
SimpleSpanProcessorand addprocess.exit()interception in the SDK init file now, before the agent runs. - Set up your Weaver schema directory with your semantic convention definitions.
- Run
spiny-orb init(or createspiny-orb.yamlmanually) — this detects your schema dir, SDK init file, and project type. - Run
spiny-orb instrument— the agent adds spans and updates your SDK init file with discovered libraries.
Once spiny-orb.yaml exists, follow the setup for your interface: CLI, MCP, or GitHub Action.
Create an spiny-orb.yaml file first if you don't have one — see Project Setup.
Global install — installs the spiny-orb command permanently:
npm install --global spiny-orbZero-install trial — runs without installing, always fetches the latest version:
npx spiny-orb@latest --helpWhy
@latest? Runningnpx spiny-orb(without@latest) serves a cached version from npx's local cache. The@latesttag forces npx to fetch the newest version from the registry on every invocation, so you always get current behavior and bug fixes.
Requirements: Node.js >= 24 (nodejs.org). If you run spiny-orb on an older Node version, it exits immediately with a clear message:
spiny-orb requires Node.js >= 24. You are running v22.x.x.
Upgrading — if you installed globally:
npm update --global spiny-orbIf you use npx, running npx spiny-orb@latest always fetches the newest version — no explicit upgrade step needed.
After installing, the spiny-orb command is available globally.
spiny-orb instrument src/Pass a directory to instrument all .js files in it, or a single file path to instrument one file. The agent discovers files, calculates a cost ceiling (displayed in dollars), asks for confirmation, then instruments each file sequentially. Each successful file gets its own commit on a feature branch. After all files are processed, the agent installs dependencies, updates the SDK init file, and opens a PR.
The cost ceiling is a conservative worst case (assumes output tokens equal input tokens, plus 30% thinking headroom). Actual costs are typically much lower — a 630-line LangGraph state machine that needed all 3 retry attempts used ~78k tokens, well under the 100k ceiling.
$ spiny-orb instrument src/order-service.js
Cost ceiling: 1 files, 100000 max tokens, estimated max cost $2.34
Proceed? [y/N] y
Processing file 1 of 1: src/order-service.js
src/order-service.js: success (2 spans)
Run complete: 1 succeeded, 0 failed, 0 skipped
1 files processed: 1 succeeded, 0 failed, 0 skipped
Branch: spiny-orb/instrument-1741700000000
PR: https://github.com/your-org/your-repo/pull/42
With multiple files, progress shows each file and its outcome:
$ spiny-orb instrument src/
Cost ceiling: 5 files, 500000 max tokens, estimated max cost $11.70
Proceed? [y/N] y
Processing file 1 of 5: src/already-instrumented.js
src/already-instrumented.js: skipped
Processing file 2 of 5: src/format-helpers.js
src/format-helpers.js: success (0 spans)
Processing file 3 of 5: src/fraud-detection.js
src/fraud-detection.js: partial (3/5 functions)
Processing file 4 of 5: src/order-service.js
src/order-service.js: success (2 spans)
Processing file 5 of 5: src/user-routes.js
src/user-routes.js: success (3 spans)
Run complete: 3 succeeded, 1 partial, 0 failed, 1 skipped
5 files processed: 3 succeeded, 1 partial, 0 failed, 1 skipped
Branch: spiny-orb/instrument-1741700000000
PR: https://github.com/your-org/your-repo/pull/42
Use --yes to skip the cost ceiling confirmation:
$ spiny-orb instrument src/order-service.js --yes
Processing file 1 of 1: src/order-service.js
src/order-service.js: success (2 spans)
Run complete: 1 succeeded, 0 failed, 0 skipped
1 files processed: 1 succeeded, 0 failed, 0 skipped
Branch: spiny-orb/instrument-1741700000000
If spiny-orb.yaml is missing:
$ spiny-orb instrument src/
Configuration not found — run 'spiny-orb init' to create spiny-orb.yaml
--dry-run Preview changes without writing
--output Output format: text (default) or json
--yes Skip cost ceiling confirmation
--verbose Show additional diagnostic output
--debug Show debug-level diagnostic output
--no-pr Skip PR creation (create branch and commits only)
The --verbose flag shows the config file path being loaded:
$ spiny-orb instrument src/ --verbose --yes
Loading config from /path/to/your-project/spiny-orb.yaml
Config loaded from /path/to/your-project/spiny-orb.yaml
Processing file 1 of 1: src/order-service.js
...
The --debug flag shows the full resolved configuration as JSON:
$ spiny-orb instrument src/ --debug --yes
Config: {
"schemaPath": "semconv",
"sdkInitFile": "src/instrumentation.js",
"agentModel": "claude-sonnet-4-6",
"agentEffort": "medium",
...
}
Processing file 1 of 1: src/order-service.js
...
If you reject the cost ceiling, the agent aborts with exit code 3:
$ spiny-orb instrument src/
Cost ceiling: 1 files, 100000 max tokens, estimated max cost $2.34
Proceed? [y/N] n
Cost ceiling rejected by caller. 1 files, 1067 bytes, 100000 max tokens.
| Code | Meaning |
|---|---|
| 0 | All files instrumented successfully |
| 1 | Partial success — some files failed, or a configuration error occurred |
| 2 | All files failed |
| 3 | Abort — cost ceiling rejected, or early abort triggered |
Create an spiny-orb.yaml file in your project first — see Project Setup.
The MCP server exposes the agent to any MCP-compatible AI coding assistant over stdio transport.
Add to your project's .mcp.json (or your MCP client's global configuration):
{
"mcpServers": {
"spiny-orb": {
"command": "npx",
"args": ["spiny-orb@latest", "mcp"],
"env": {
"ANTHROPIC_API_KEY": "${ANTHROPIC_API_KEY}"
}
}
}
}The ${ANTHROPIC_API_KEY} syntax expands from your shell environment. Claude Code and other MCP clients expand environment variables at startup.
The server exposes two tools:
get-cost-ceiling — Calculate the cost of an instrumentation run before committing to it. Fast, local-only, no LLM calls. Returns file count, total file size, max token ceiling, and estimated cost in dollars.
{
"fileCount": 1,
"totalFileSizeBytes": 744,
"maxTokensCeiling": 100000,
"estimatedCostDollars": "$2.34"
}instrument — Run full instrumentation. Analyzes files, adds spans and attributes, validates against the rubric, retries on failure, and returns a hierarchical result (summary → per-file detail → schema integration data). Call get-cost-ceiling first to understand scope and cost.
{
"summary": {
"filesProcessed": 1,
"filesSucceeded": 1,
"filesPartial": 0,
"filesFailed": 0,
"filesSkipped": 0,
"librariesInstalled": [],
"libraryInstallFailures": [],
"sdkInitUpdated": false
},
"files": [
{
"path": "src/order-service.js",
"status": "success",
"spansAdded": 2,
"attributesCreated": 3,
"validationAttempts": 1
}
],
"costCeiling": {
"fileCount": 1,
"totalFileSizeBytes": 1067,
"maxTokensCeiling": 100000
},
"actualTokenUsage": {
"inputTokens": 12500,
"outputTokens": 3200,
"cacheReadInputTokens": 0,
"cacheCreationInputTokens": 0
},
"warnings": []
}Both tools accept projectDir (absolute path to project root) and an optional path to scope to a subdirectory or individual file. Additional optional overrides: maxFilesPerRun, maxTokensPerFile, and exclude patterns.
Progress is reported via MCP logging messages (level: "info") with JSON payloads for each stage: fileStart, fileComplete, schemaCheckpoint, validationStart, validationComplete, runComplete.
Commit an spiny-orb.yaml file to your repo first — see Project Setup.
Add OpenTelemetry instrumentation as a step in your CI/CD pipeline.
- name: Instrument with OpenTelemetry
uses: wiggitywhitney/spinybacked-orbweaver@main
with:
path: src
node-version: '24'
weaver-version: '0.21.2'| Input | Default | Description |
|---|---|---|
path |
src |
Path to instrument (relative to repository root) |
node-version |
24 |
Node.js version to use |
weaver-version |
0.21.2 |
Weaver CLI version to install |
| Output | Description |
|---|---|
result |
JSON result from the instrumentation run |
summary |
Human-readable summary (e.g., "3 succeeded, 1 failed, 0 skipped out of 4 files") |
The action runs spiny-orb instrument --yes --output json, so it skips cost confirmation and outputs structured JSON. Progress is reported via GitHub Actions notices.
spiny-orb.yaml configures the agent across all three interfaces. See Project Setup for how to create it.
Only schemaPath and sdkInitFile are required — everything else has defaults.
| Field | Type | Default | Description |
|---|---|---|---|
schemaPath |
string | (required) | Relative path to your Weaver registry directory |
sdkInitFile |
string | (required) | Relative path to your OTel SDK init file |
agentModel |
string | claude-sonnet-4-6 |
Claude model to use for code generation |
agentEffort |
low | medium | high |
medium |
Thinking depth — higher means more thorough but slower |
testCommand |
string | npm test |
Command to run checkpoint and end-of-run test validation. Supports any test runner and inline env vars — e.g., GIT_CONFIG_GLOBAL=/tmp/test.gitconfig npm test for repos where global git config conflicts with the test suite |
targetType |
long-lived | short-lived |
long-lived |
Process lifecycle. long-lived (web servers, workers, daemons) uses BatchSpanProcessor — no extra setup. short-lived (CLIs, scripts, Lambda, batch jobs) needs SimpleSpanProcessor and process.exit() interception, otherwise BatchSpanProcessor drops all spans before the 5-second flush timer fires. Set during spiny-orb init or add manually. |
dependencyStrategy |
dependencies | peerDependencies |
dependencies |
Multiple copies of @opentelemetry/api in node_modules cause silent trace loss via no-op fallbacks. Use dependencies for services (backend APIs, workers, apps) — they own their dependency tree. Use peerDependencies for distributable packages (libraries, anything published to npm) — consumers control which version is installed. These two fields are independent: a CLI tool is both short-lived and dependencies. |
maxFilesPerRun |
number | 50 |
Maximum files to process in one run |
maxFixAttempts |
number | 2 |
Retry attempts per file after initial generation (total attempts = 1 + this value) |
maxTokensPerFile |
number | 100000 |
Soft token budget per file — pre-flight estimate is a hard gate; post-hoc check stops further retries but never discards a passing result |
largeFileThresholdLines |
number | 500 |
Files above this threshold get special handling in the prompt |
schemaCheckpointInterval |
number | 5 |
Run weaver registry check every N files during processing |
weaverMinVersion |
string | 0.21.2 |
Minimum Weaver CLI version required |
reviewSensitivity |
strict | moderate | off |
moderate |
PR annotation strictness — strict flags tier 3+ spans, moderate flags outliers only, off suppresses warnings |
confirmEstimate |
boolean | true |
Prompt for cost ceiling approval before processing (CLI only — MCP always skips) |
dryRun |
boolean | false |
Preview mode — run analysis but revert all changes |
exclude |
string[] | [] |
Glob patterns for files to skip (e.g., ["test/**", "*.spec.js"]) |
Unrecognized fields are rejected with typo suggestions (e.g., "Unknown field 'shcemaPath' — did you mean 'schemaPath'?").
Preview what the agent would do without modifying your project:
spiny-orb instrument src/ --dry-run$ spiny-orb instrument src/order-service.js --dry-run --yes
Processing file 1 of 1: src/order-service.js
src/order-service.js: success (2 spans)
Run complete: 1 succeeded, 0 failed, 0 skipped
1 files processed: 1 succeeded, 0 failed, 0 skipped
Dry-run mode runs the full analysis pipeline (LLM calls, validation, schema extensions) but reverts all file changes afterward. It skips branch creation, commits, PR creation, dependency installation, and end-of-run live-check. The schema diff is captured before reverting, so the summary shows what schema changes would have been made.
Dry-run still costs tokens — the agent analyzes every file with real LLM calls. Use get-cost-ceiling (MCP) or the cost ceiling prompt (CLI, without --yes) to understand the cost before running.
Support for languages beyond JavaScript will be added via language provider packages that implement the LanguageProvider interface. Providers declare spiny-orb as a peer dependency and import their interface types from "spiny-orb/plugin". This architecture is planned for a future release.