Imported from Project-White-Rabbit/bitfab-amp-plugin (
skills/setup/SKILL.md). Install upstream withnpx skills add Project-White-Rabbit/bitfab-amp-plugin --skill setup. Copyright stays with the author.
Bitfab Setup
When the flow branches, always present the options clearly and wait for the user's answer before proceeding. Number or letter the options so the user can pick by reference. Rules:
- Recommend an option first, explain why in one line
- Present 2-5 concrete options
- One decision per question, never batch
Execution style (applies to every phase). Default to terse, action-first turns:
- During mechanical phases (detecting language, searching code, reading files), run the tools and report only what you found. Do not narrate each command or pre-announce what you are about to do.
- Batch read-only probing: combine related shell checks into one command (separate them with
;, not&&(a no-matchgrepexits non-zero and would abort an&&chain, skipping later probes)), and read multiple files in a single batch rather than one file per turn. Adaptive follow-up greps that depend on a prior result are expected and fine; the goal is to collapse only the fixed, independent probes. - Keep prose between tool calls to one line or none. Save fuller explanation for decision points and the workflow summaries the user acts on.
- Surfacing a risk, ambiguity, or unexpected finding is never the narration to suppress: raise it immediately, even mid-probe (e.g. unserializable inputs, a shim with lazy init, an ambiguous project root).
Studio recovery (applies to every Studio-opening command). Any command that opens or navigates Studio (openTracePlan.js, startTemplatePreview.js, etc.) emits {"event":"not-responding","sessionId":"..."} and exits non-zero when a Studio session is recorded but its window can't be reached (a crash, sleep, or a close no process witnessed). It will NOT open a duplicate window. Do not retry this blindly. Recommend the user refresh or reopen the Studio tab, then ask the user with two options: Try again (re-run the same command, the record is still on disk, so a window that came back gets reused) or Open a new Studio (run node "${BITFAB_PLUGIN_DIR}/dist/commands/clearStudioSession.js", then re-run the command, which now opens a fresh window). Only run node "${BITFAB_PLUGIN_DIR}/dist/commands/clearStudioSession.js" after the user approves. Some commands (e.g. login) also expose a --force flag for a user at a terminal to recover the same way; never run --force yourself, surface the recovery to the user instead.
Describe this to the user in terms of their browser window, never the plumbing. The words in the paragraph above (session, pointer, record, stale, not-responding, sessionId) are for you, not for them: a user has a Studio tab open, they do not have a session pointer, and naming one tells them nothing they can act on. Say what they can see and what you want them to do, and offer a cause they can check rather than a diagnosis you can't make: the browser may have backgrounded the window (behind another window, minimized, or on another desktop or space), so send them looking for it before you offer a new one. E.g. "The Studio tab I opened earlier isn't responding. Your browser may have backgrounded it. Try switching to it and refreshing it, and if you can't find it I'll open a new one." Same rule for every other internal term you might be tempted to echo (agent session, monitor event file, exec session, daemon): describe the effect, not the mechanism.
Studio URL surfacing (applies to every fresh Studio open). If any Studio-opening command emits {"event":"window-open-requested","url":"..."}, immediately surface that URL to the user in a normal chat message (for example, Opening Studio: <url>. Click it if a window doesn't appear) so it is copyable from the transcript. This event means the open was requested (the browser launch was called), not that a window is confirmed on screen: on a remote/SSH session or with no supported default browser, nothing may surface, so the link is the reliable fallback. Surface it every time the event appears; do not leave the URL only in shell/tool output. If instead the command emits {"event":"open-failed","reason":"...","url":"..."}, the browser process did not even launch (e.g. rate-limited, spawn-failed): tell the user Studio couldn't open a browser (give the reason) and to click the link to open it (<url>); the session is live and the command keeps polling, so a manual click connects.
Studio reuse: never close-then-reopen to switch pages (applies to every Studio-opening command). Every Studio-opening command (openTracePlan.js, startDataset.js, startTemplatePreview.js, login.js, etc.) resolves the active session and navigates the live tab in place (emitting {"event":"navigated",...}); it opens a fresh window only when no session is active. So to change what Studio is showing, open a different dataset, a different trace plan, or a different page, just run the relevant open command again with the new target. Never call node "${BITFAB_PLUGIN_DIR}/dist/commands/closeStudio.js" just to switch: closing and reopening churns the tab, can orphan windows, and is never the way to change pages. Reserve node "${BITFAB_PLUGIN_DIR}/dist/commands/closeStudio.js" for genuine end-of-flow cleanup (the cleanup step already handles that) or when the user explicitly asks for the Studio tab to be closed.
π¨ Blocking-process rule (applies to any plugin command described as "blocks until the user does X"): When you launch a plugin CLI that blocks on a Studio interaction (login.js, startDataset.js, openTracePlan.js, etc.), you MUST keep the exec session alive and keep polling it until the process exits on its own.
- The process opens Studio (or navigates an existing Studio session) and polls for the user's action via agent session events. It exits only after the user completes the action in Studio (or after the timeout).
- After launching the command, keep polling the live shell/exec session every few seconds with your normal "read more output" tool. Do not idle waiting for a user message.
- The user's confirmation does NOT come back as a chat message; it comes back as the plugin process exiting with output on stdout.
- Stop polling only when: (a) the process exits 0 with its completion summary, (b) the process exits non-zero, or (c) the user explicitly cancels.
- When the process exits, immediately continue with the next step, do not wait for another user message.
- Exception,
openTracePlan.jsonly. Its trace-plan steps (build-trace-plan, present-diff) run it in the background AND ask the user "Continue instrumenting?" in the same turn, because reviewing the plan in Studio is optional there. Keep polling the exec session as above, but a terminal-side answer is a second, equally valid way for that step to finish. Everything else in this rule still holds: never idle on a chat message alone, and when the process exits, act on its stdout, which outranks a stale answer.
This skill has twelve phases: explain, login, session-logs, instrument, modify, inspect, switch-org, view, replay, db-snapshot, templates, and analyze-repo. Run individually or through setup (wizard runs login β instrument; explain is a standalone read-only overview that requires no login; session-logs is standalone and does not require login; modify is only invoked explicitly or as a branch from Instrument's existing-SDK-usage menu; inspect is a standalone diagnostic (with optional one-shot fixes) invoked explicitly; switch-org is a standalone account action (requires auth) invoked explicitly; view is only invoked explicitly; db-snapshot is only invoked explicitly; templates is only invoked explicitly; analyze-repo is a standalone, non-interactive batch action (requires auth) invoked explicitly: it scans, picks the top few candidates, and uploads a draft trace plan for each, asking nothing and editing no code).
Natural-language aliases (these reuse an existing mode, not a separate one): "explain Bitfab" / "what is Bitfab" β explain; "trace a new workflow" / "instrument a new flow" / "create a trace plan" / "give me the trace plan" / "give me the trace plan for " / "show me the trace plan" / "bitfab create a plan" / "instrument the second/next/other one" / "instrument another function" β instrument (a bare bitfab:setup with one of these phrasings routes to Instrument, NOT the full wizard); "update-setup" / "update my tracing setup" / "adjust what's captured" β modify (NOT a plugin/SDK version bump, that's bitfab:update); "debug-setup" / "debug my tracing setup" / "inspect my tracing" / "why aren't my traces showing up" / "what's instrumented" β inspect (for output-quality debugging use bitfab:assistant instead); "switch org" / "change org" / "switch to the org" / "I'm in the wrong org" β switch-org; "set up db snapshots" / "set up db branching" / "replay against my database" / "replay against the database at trace time" / "database snapshots for replay" β db-snapshot; "analyze the repo" / "analyze-repo" / "scan the codebase and draft trace plans" / "find the top things to instrument and upload plans for them" β analyze-repo (non-interactive: never prompts, never edits code, just uploads draft plans).
When instrumenting a workflow, its instrumentation and replay pipeline are written together in the same cycle once the trace plan is confirmed (see Instrument's write-instrumentation step). The standalone replay mode remains available for coverage-verification and backfill.
Before running plugin commands below, resolve BITFAB_PLUGIN_DIR in the shell. Amp does not inject a plugin-root env var, so you must determine it. Copy and run this block verbatim, it auto-detects whichever install is active:
if [ -z "$BITFAB_PLUGIN_DIR" ]; then
project_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
for candidate in "$project_root/.amp/plugins/bitfab" "${XDG_CONFIG_HOME:-$HOME/.config}/amp/plugins/bitfab"; do
if [ -f "$candidate/dist/commands/status.js" ]; then
BITFAB_PLUGIN_DIR="$candidate"
break
fi
done
export BITFAB_PLUGIN_DIR
fi
test -n "$BITFAB_PLUGIN_DIR" || { echo "ERROR: Bitfab plugin not installed"; exit 1; }
If the block prints ERROR: Bitfab plugin not installed, the user hasn't installed the plugin yet, stop and tell them to install it. Otherwise proceed. The commands live at $BITFAB_PLUGIN_DIR/dist/commands/*.js.
SDK reference: https://docs.bitfab.ai is the source of truth for SDK install, initialization, API surface, and replay. Every docs path below ends in .md: that suffix returns the page as plain markdown (no HTML chrome), so fetch the URLs exactly as written. Fetch in this order before writing any code, do not improvise from memory:
- Canonical API surface (preferred for agents): the dense reference pages at
/reference/typescript.md,/reference/python.md,/reference/ruby.md,/reference/go.md. These list every public export, signature, type, default, and error semantic, no tutorials, no prose. Read these first. - The client exposes three instrumentation primitives, not one. Spans are the oldest, not the only one: TypeScript has
withSpan/span,withTrace/trace, andwithNode/node; Python has@client.span,@client.trace, and@client.node; Ruby and Go have spans only.tracerecords a root plus every first-party call beneath it without decorating them (TypeScript needs abitfab-transformadapter, Python needs 3.12+), andnodeapplies naming, typing, capture, andmockOnReplay/mock_on_replaypolicy to one call discovered inside that subtree. Name all three when you fetch a reference page. The fetch answers the question you asked, so asking only aboutwithSpan/@spanreturns a page that reads as though spans are the whole API. - Cross-SDK shared semantics:
/reference/overview.md(invariants),/reference/span-types.md(theSpanTypeenum),/reference/http.md(wire protocol). - Framework integrations (fetch when a framework is detected in step 1 of Instrument):
/frameworks/langgraph.md,/frameworks/openai-agents.md,/frameworks/claude-agent-sdk.md,/frameworks/baml.md,/frameworks/vercel-ai-sdk.md. Each page documents the SDK's native handler/processor/wrapper for that framework, which is usually preferable to hand-wrapping every node/agent call withwithSpan/@span. - Tutorials / walkthroughs / replay registry module template: the language-specific guide pages (
/typescript-sdk.md,/python-sdk.md,/ruby-sdk.md,/go-sdk.md). Use these for the copy-pasteable replay registry module and the replay output contract. During Instrument, fetch the Replay section before Instrument's write-instrumentation step so the replay registry module can be written alongside the instrumentation in the same cycle without re-fetching.
MCP tools: This skill uses get_bitfab_api_key, save_trace_plan, get_trace_plan, list_trace_plans, and cancel_trace_plan (login / instrument / modify / view / analyze-repo), list_trace_functions and search_traces (inspect, templates, and the exact-trace smoke test in db-snapshot), list_organizations (switch-org), get_database_connection_status (db-snapshot only), and, for the templates mode only, get_template_reference, get_template, and save_template. All are registered by the Bitfab plugin under these bare names.
CLI commands available via Bash (all paths relative to ${BITFAB_PLUGIN_DIR}/dist/commands/):
| Command | Description |
|---|---|
status.js |
Check plugin authentication and connection status |
login.js |
Authenticate for setup/instrumentation; standalone browser OAuth (blocks). Studio, dataset, and experiment flows log in inline and need no pre-login. |
switchOrg.js [<clerkOrganizationId>] |
List the user's Bitfab orgs (no args), or switch the plugin's active org and replace the local API key (with a arg) |
openTracePlan.js <planId> |
Open the trace plan review UI in Studio (stays alive until the user closes or updates the plan) |
openStudioTo.js <path> |
Navigate the active Studio session to a path (opens a window when none is active) |
startTemplatePreview.js <functionKey> |
Open the template editor preview in Studio (blocks until user clicks Done) |
closeStudio.js [message] |
Close the active Studio session (tab + background event process); no-op when nothing is open |
clearStudioSession.js |
Start a fresh Studio window on the next open |
update.js <mode> |
Check plugin + SDK versions and install the latest (used by inspect to detect and fix staleness) |
| `sessionLogConsent.js [get | set true |
Modes
Read the user's request first. If its first token is exactly one of the mode names below, run that mode. Otherwise, when this skill documents how to route the remaining arguments (see its intro), follow that; if it doesn't, run wizard and treat the user's request as its input. Run only that mode's section below and skip the others.
| Mode | Trigger | What it does |
|---|---|---|
wizard |
wizard (default) |
Run login, then instrument workflows until the user is done. |
explain |
explain |
Explain what Bitfab is and what each mode does (read-only, no login). |
login |
login |
Authenticate for setup/instrumentation (Studio/assistant flows log in inline, no pre-login). |
session-logs |
session-logs |
Opt in or out of session log collection (no login required). |
instrument |
instrument |
Instrument AI workflows with Bitfab tracing. |
modify |
modify |
Modify an existing trace setup (add context, change depth, or move the root). |
inspect |
inspect |
Diagnose (and offer to fix) your tracing setup: auth, what's instrumented, plugin/SDK freshness, replay coverage, trace arrival. |
switch-org |
switch-org |
Switch which Bitfab org the plugin reads and writes (replaces the local API key). |
view |
view |
Open the trace planner UI for an existing trace function (read-only). |
replay |
replay |
Create or update replay registry modules for instrumented workflows. |
db-snapshot |
db-snapshot |
Set up per-trace database snapshots so replay runs against the DB state at trace time (TypeScript, Python, Ruby). |
templates |
templates |
Iterate on the span-rendering templates for one trace function. |
analyze-repo |
analyze-repo |
Non-interactively scan the repo, pick the top workflows to trace (default 5, or limit=<n>; optional free-text guidance: steers what to focus on), and upload a draft trace plan for each (no prompts, no code changes). |
Login
Run only when mode is wizard, login or instrument.
Authenticate with Bitfab and retrieve the API key.
-
Run the status check:
node "${BITFAB_PLUGIN_DIR}/dist/commands/status.js"If the output includes
v<X> available, run ... to update, surface that line verbatim to the user once before proceeding; do not block on it.If already authenticated, skip to step 3.
-
If "not authenticated", run the login script yourself, do NOT ask the user to run it manually:
node "${BITFAB_PLUGIN_DIR}/dist/commands/login.js"Run with 600000ms (10 minute) timeout. This opens Studio to the sign-in page and polls the server until the user completes authentication in the browser. The process exits when authentication succeeds or the 10-minute timeout fires.
If the browser fails to open,
login.jsprints the Studio sign-in URL. Surface it to the user verbatim so they can open it manually; do not rely on shell/tool output being visible. The polling loop stays active for the full 10-minute timeout regardless of whether auto-launch worked.Per the Blocking-process rule at the top: after
login.jsopens Studio, keep polling the live exec session until it exits. Do not send a "waiting for you to sign in" message and then idle; the user's sign-in will NOT arrive as a chat message; it arrives aslogin.jsexiting withLogged in as <email>on stdout.If
login.jsexits non-zero or the 10-minute timeout elapsed, report the error to the user and stop. -
Call
get_bitfab_api_keyto retrieve the API key, NEVER print or log the full key. Stored at~/.config/bitfab/credentials.json, used for theBITFAB_API_KEYenvironment variable.If
get_bitfab_api_keyis not available in this session, the Bitfab plugin did not load, so its tools never registered. Say so and hand the user the fix below; do not diagnose further, and do not fall back to drafting anything by hand.Tell them to run
plugins: reloadfrom the command palette (Ctrl-O), or restart Amp, then re-run this skill.amp plugins listshows whether the Bitfab plugin loaded and, if not, why.Then stop, the same way a failed login stops. Do not fall through to the remaining steps: every phase after this one needs these tools, so continuing only moves the failure further from its cause.
-
Check whether session log consent has already been recorded:
node "${BITFAB_PLUGIN_DIR}/dist/commands/sessionLogConsent.js" getIf the output is already
trueorfalse, skip the prompt and continue. If the output isnull, ask the user:- Question: "Allow Bitfab to collect session logs?"
- Description: Used to diagnose issues and improve the product.
- Options: "Allow" / "Don't allow"
Save the answer (replace
CONSENTwithtrueorfalse):node "${BITFAB_PLUGIN_DIR}/dist/commands/sessionLogConsent.js" set CONSENT
Explain
Run only when mode is wizard or explain.
Teach the two primitives the user has to instrument with. Read-only, no code changes, no Studio. Runs inside wizard (right after Login, before the approach question) and standalone via bitfab:setup explain (or natural-language asks like "what is Bitfab" / "explain Bitfab"), which needs no authentication.
-
Render the block below verbatim as a single message, as formatted markdown (do not wrap it in a code fence, do not reword it, and do not add a summary or an ASCII diagram). This is the education the rest of setup depends on: a user who does not understand
withSpanandreplaycannot make the per-method decisions instrumentation asks of them. Do not authenticate, scan the codebase, ask any questions, or edit anything here, in either mode.**Purpose** Bitfab's SDK captures each instrumented method's inputs, outputs, and surrounding context as a trace at runtime. During development, developers and coding agents can inject captured trace data and modify code execution at the per-method level to test AI features end-to-end. **How to instrument** Bitfab provides you a way to capture traces and replay them safely during development. The core primitives from the Bitfab SDK are: - `withSpan(...)` - `replay(...)` `withSpan` captures traces and sends them to Bitfab by default. It serializes the inputs, outputs, and metadata of the method it wraps (or decorates) and sends them over the OTEL transport layer. `replay` calls into your code and modifies the behavior of `withSpan` for each method it wraps (or decorates) in one of five ways: 1. Execute as normal 2. Pass in inputs from the recorded trace 3. Pass in modified inputs from the recorded trace 4. Skip execution and return outputs from the recorded trace 5. Skip execution and return modified outputs from the recorded traceIf the user asks about a framework (or once one is detected later in setup), follow up by explaining how that framework maps onto the five cases above. The principles do not change; only the way it gets instrumented does.
Unless the mode is
explain:Stop there and continue to the Approach section. Do not render the mode menu below: mid-setup, a menu of other modes is noise.
Only when the mode is
explain:Follow the block above with this one, as a code block, exactly as laid out:
What you can run bitfab:setup Login, then instrument workflows until done bitfab:setup explain This overview (read-only) bitfab:setup login Authenticate with Bitfab bitfab:setup instrument Wrap a new AI workflow with tracing bitfab:setup modify Adjust what an existing trace captures bitfab:setup inspect Diagnose + fix setup: auth, what's instrumented, SDK/plugin current, replay coverage, traces arriving bitfab:setup switch-org Switch which org the plugin reads and writes bitfab:setup view Open one trace function's plan in the browser (read-only) bitfab:setup replay Create or update replay registry modules bitfab:setup templates Change how a trace function's spans render bitfab:setup session-logs Opt in/out of session log collectionthen close with one line: to start tracing, run
bitfab:setup; to debug an existing setup, runbitfab:setup inspect. Then stop.
Approach
Run only when mode is wizard.
Settle who does the instrumenting before any code is read or written. Runs once, in wizard mode only, after Login.
-
The user is authenticated now. Ask the user to settle who does the instrumenting:
- Question: "Want me to walk you through instrumenting, or would you rather do it yourself?"
A) Walk me through it: I drive the instrumentation end to end, checking with you at each decision (recommended) β step 2 B) I'll instrument myself: hand over the docs and stop, no scanning, no code changes β step 3
Recommend A and say why in one line: it is the whole flow (SDK install, trace plan, spans, replay registry module) with a confirmation before anything is written. Ask this once; do not re-ask it later in the session.
-
The user asked to be walked through it. Before anything else (before dispatching to the Instrument section, before a single probe or file read), render the content of the block below verbatim as formatted markdown: no code fence, no rewording, no additions.
**What's about to happen next** - This wizard will guide Amp on how to use the Bitfab plugin to analyze your repository. Amp will then instrument your AI features and write a `replay` script using the Bitfab SDK. - Whenever Amp needs your input, it will prompt you - Setup takes about 10 - 17 minutes depending on how many features you want to instrument and how complex your AI features are.This is the user's only warning about what the skill is about to do to their repository and how long it takes. It has to reach them between saying "walk me through it" and the next question they get asked, so nothing, not the language detection, not the existing-usage report, may come first.
Then go to the Instrument section and start at its first step. The guided path ends here: do not continue into the self-serve handoff that follows, which belongs to option B and tells you to stop.
-
The user is instrumenting on their own. Give them the pointers below in one short message, then stop: do not scan the codebase, read files, or edit anything.
- Docs: https://docs.bitfab.ai, start with the SDK guide for their language (
/typescript-sdk,/python-sdk,/ruby-sdk,/go-sdk); each one covers install, initialization, wrapping a workflow, and (outside Go) the replay registry module. Name the language's page directly if the project's language is already obvious from the conversation; do not go read the repo to find out. - API key: their app needs
BITFAB_API_KEYset in the environment it runs in before any trace will arrive. Tell them to get the key from the Bitfab MCP'sget_bitfab_api_keytool. Do not call it yourself, and never print a key. - Coming back:
bitfab:setuppicks this flow back up, andbitfab:setup inspectdiagnoses an instrumentation they wrote themselves (auth, what's instrumented, whether traces are arriving).
Then go to the Cleanup section and end the run there. Option B is a full stop: do not read on into the sections that follow, the Instrument section included, and do not scan or edit anything on the way out.
- Docs: https://docs.bitfab.ai, start with the SDK guide for their language (
Session Logs
Run only when mode is session-logs.
Opt in or out of session log collection. Does not require authentication.
-
Check whether session log consent has already been recorded:
node "${BITFAB_PLUGIN_DIR}/dist/commands/sessionLogConsent.js" getIf the output is
true, tell the user session logs are currently enabled. Iffalse, tell the user session logs are currently disabled. Ifnull, tell the user no preference is recorded yet. Then ask the user:- Question: "Allow Bitfab to collect session logs?"
- Description: Session logs help us diagnose issues and improve the product. They include prompts, responses, and tool calls from sessions where Bitfab tools are used.
- Options: "Allow" / "Don't allow"
Save the answer (replace
CONSENTwithtrueorfalse):node "${BITFAB_PLUGIN_DIR}/dist/commands/sessionLogConsent.js" set CONSENTConfirm the change to the user.
Instrument
Run only when mode is wizard or instrument.
Instrument the codebase with Bitfab tracing. Requires authentication (run Login first if needed).
Bitfab captures every AI function call, inputs, outputs, and errors, so you can see exactly what your AI is doing and discover what's going wrong. The goal is to have enough context in each trace to tell whether a call succeeded or failed, and why.
Detection and search below are mechanical: run the probes and report what you found, without narrating each command. Combine related read-only checks into one command (separate them with ;, not &&, since a no-match grep exits non-zero and would abort an && chain) and read multiple files in a single batch; adaptive follow-up greps that depend on a prior result are expected. A risk, ambiguity, or unexpected finding (unserializable inputs, a shim with lazy init, an ambiguous root) is never the narration to suppress: raise it immediately, even mid-probe.
-
Detect the project language (TypeScript, Python, Ruby, or Go). In a monorepo, identify which directories are applications (services, APIs, agents) vs libraries (SDKs, shared packages). Focus on application directories. The only output of this step is that language-and-framework verdict; finding the AI workflows to instrument comes later, at step 7, so a grep hit here is a framework signal, not a workflow to read and characterize. Also scan imports and package manifests for supported framework signals, and note which framework each application directory uses, step 5 fetches the matching framework page alongside the language reference:
- LangGraph / LangChain: TS:
@langchain/langgraph,@langchain/core; Python:langgraph,langchain,langchain_core - OpenAI Agents SDK: TS:
@openai/agents,setTraceProcessors; Python:agents(from agents import ...) - Claude Agent SDK: TS:
@anthropic-ai/claude-agent-sdk,query(; Python:claude_agent_sdk,ClaudeSDKClient,query( - BAML: TS:
@boundaryml/baml,baml_clientimport; Python:baml-py,from baml_client import b - Vercel AI SDK: TS:
ai,wrapLanguageModel,streamText,generateText(TypeScript only)
- LangGraph / LangChain: TS:
-
Search for existing SDK usage (
withSpan,@span,bitfab_span,client.Span,getFunction,get_function, etc.). In a monorepo, search each application directory separately: a root-level search can miss subdirectories.- If found: ask the user, listing the existing trace function keys inside the question's own text (
<key>Β· its root function, or its handler registration site for handler-only keys), so the user picks against the actual list instead of a message scrolled above the prompt:
A) Search for more workflows: find workflows that aren't traced yet (recommended) β step 3 B) I'll name the workflow to instrument: type the file, function, or directory and I read only that, no codebase scan β step 3 C) Change what an existing trace captures: adjust what one of the keys above records; type its key to go straight to it β step 1 of the Modify phase
B and C are both free-text options, and this is the earliest point either answer can land. Say so in the question: they can type a workflow to instrument, or the key of an existing trace to change, instead of picking anything.
- B (a target to instrument): carry that target through the SDK install and doc steps and treat the discovery question at step 7 as already answered by it, then read just that location at step 10. Without this option a user who already knows their target has to answer "search" here, sit through the install, and then answer the same question again four steps later, which is asking them to repeat themselves.
- C (an existing key to change): the keys are right there in the question, so a named key routes to the Modify phase with the target already settled, and Modify is told to skip both the search you just ran and its own which-function question when it arrives holding a key. Only a bare C leaves Modify to ask.
There is deliberately no "done, finish setup" option here. The user invoked setup and nothing has been instrumented this session, so an exit offered as a peer of the two real options invites them to undo the thing they just asked for. A user who genuinely wants out says so in free text, which is the last of the runtime routes below; do not manufacture a menu item for it.
Routes that are conditions rather than options:
- no existing SDK usage found: continue β step 3
- the user says in free text that they are done and want no changes: continue β step 1 of the Cleanup phase
- If usage routes through a project-local shim (a wrapper file that re-exports
withSpan/@span/bitfab_span/getCurrentTrace/getCurrentSpanwith custom init, often namedlib/bitfab.*or after a predecessor SDK such aslib/simforge.*), audit the shim before instrumenting anything new. The shim must (a) construct the SDK client (new Bitfab(...),bitfab_init(),Bitfab::Client.new, etc.) at module load, synchronously, never lazily inside the wrapped function; and (b) hand off to the SDK call synchronously, with noawaitbetween the user's entry to the shim andclient.withSpan(...)/@bitfab.span(...). Lazy or async client init (e.g.await getOrCreateTraceFunction(key)inside the wrapped body) breaks the SDK's nesting context (TypeScriptAsyncLocalStorage, Pythoncontextvars) under any parallel fan-out (Promise.all,Promise.allSettled,asyncio.gather, parallel workers): every span becomes its own top-level trace instead of nesting inside its caller. Fix the shim before instrumenting anything new. (Direct callers of the SDK with no shim already satisfy this rule, skip the audit.) - If not found: proceed to step 3: no SDK usage does NOT mean nothing to instrument, it means the SDK hasn't been installed yet. NEVER conclude "nothing to instrument" before completing step 6.
- If found: ask the user, listing the existing trace function keys inside the question's own text (
-
Use the API key from the Login phase (or retrieve it now if already authenticated)
-
Say one line before you install anything. This is the deliberate exception to the "do not pre-announce" execution-style rule, and the only one in this phase: the next few steps install a package, write to the project's env files, and fetch docs, which is the wizard touching the user's repo before it has asked them a single thing about what they want traced. Name what happens now, and end the line with whatever comes next on the route the user is actually on, since step 7 only asks when nothing has settled it yet. Promising a question the flow then skips is worse than saying nothing:
- They named a target at step 2: name it back ("Installing the SDK and setting your API key, then reading the SDK docs, then I'll instrument
<target>"). This confirms you heard them and keeps the target in view across the install and the doc fetches, the stretch where a held answer is easiest to lose. - They chose to search there: say the scan is next ("...then I'll find the workflows worth tracing").
- Nothing settled yet (this step was reached without that menu, because no existing SDK usage was found): promise the question ("...then I'll ask what you want to instrument").
One line, nothing around it, then get on with it.
Install the SDK now. Detect the project's package manager from its manifest (
pyproject.tomlβuv/poetry;package.jsonβpnpm/npm/yarn/bun;Gemfileβbundle;go.modβgo get;requirements.txtβ edit file +pip install -r) and run its canonical add command, do NOT stop to ask about version pinning or dep groups. Preferuv add/poetry addover barepip install(barepip installdoesn't persist to pyproject.toml). In monorepos, scope to the correct workspace (e.g.pnpm add --filter <pkg>, or cd into the app directory first), running from the repo root will install into the wrong package. Default to a runtime dep for applications; a dev dep for libraries/SDKs where a runtime dep would propagate to downstream users. Then set theBITFAB_API_KEYenvironment variable.Tell the user what you did. Pick the env-handling approach that fits the project's existing convention. Whatever you do, surface it explicitly: name the file (with absolute path) or mechanism you used, so the user knows where the key now lives. Do not print the key value itself. If the key landed in a
.env-style file, additionally tell the user that any already-running dev server, REPL, or test runner may need a restart to pick it up, since most file watchers reload code on save but not env files. - They named a target at step 2: name it back ("Installing the SDK and setting your API key, then reading the SDK docs, then I'll instrument
-
Read the SDK reference. Fetch the dense canonical reference page first (
/reference/typescript.md,/reference/python.md,/reference/ruby.md, or/reference/go.md) for every signature, type, default, and error semantic you need (initialization,withSpan/@span/bitfab_span/client.Span, the subtree primitiveswithTrace/traceandwithNode/nodein TypeScript and Python,getFunction/get_function/GetFunction/bitfab_function,SpanType,getCurrentSpan/getCurrentTrace,wrapBAML/wrap_baml). Ask the page for all three primitives by name, not just spans: the fetch returns an answer to your prompt rather than the page, so a prompt aboutwithSpan/@spanalone comes back documenting spans alone, and you will instrument without ever learning thattrace(a root that captures every first-party call beneath it, no decorators) andnode(naming, typing, capture, and replay-mock policy for one call inside that subtree) exist. If step 1 detected a framework in this application directory, also fetch the matching framework page; it documents the handler/processor/wrapper the SDK exposes for that framework, which is usually preferable to hand-wrapping every node/agent call withwithSpan/@span: LangGraph / LangChain β/frameworks/langgraph.md(getLangGraphCallbackHandler/get_langgraph_callback_handler; in a LangChain-only project, prefer the identical aliasesgetLangChainCallbackHandler/get_langchain_callback_handlerso the code reads naturally; Experimental (alpha) tool replay:getLangGraphIntegration/get_langgraph_integrationwithcreateInvoker/create_invoker/create_async_invoker); OpenAI Agents SDK β/frameworks/openai-agents.md(getOpenAiTracingProcessor/get_openai_tracing_processor, plus the replayable run wrappergetOpenAiAgentHandler/get_openai_agent_handler(drop-in for the run call)); Claude Agent SDK β/frameworks/claude-agent-sdk.md(getClaudeAgentHandler/get_claude_agent_handler); BAML β/frameworks/baml.md(wrapBAML/wrap_baml); Vercel AI SDK β/frameworks/vercel-ai-sdk.md(getVercelAiMiddleware). Then fetch the language guide (/typescript-sdk.md,/python-sdk.md,/ruby-sdk.md,/go-sdk.md), including the Replay section for non-Go projects, for the install command, the multi-file project layout example, the BAML auto-instrumentation walkthrough, and the replay registry module template. Read the replay section upfront (not later) because step 14 reuses it to write the replay pipeline in the same cycle, and it should not re-fetch these pages. Fetch those pages (via web-fetch capability, in one batch since they are independent URLs, or ask the user to share them). Do not improvise instrumentation from memory: the API has moved and guessing will produce broken code. -
Instrumentation must produce a replayable trace. There are exactly two ways to get one: (1) the root span has serializable inputs, or (2) the workflow runs on a supported framework integration that records a replayable root (LangGraph / LangChain, OpenAI Agents SDK, Claude Agent SDK, or Vercel AI SDK), which captures the framework's own serializable input as the root. Establish one of these before writing any instrumentation. Trace-processor integrations (OpenAI Agents SDK) are a special case: the processor auto-captures the agent run, but on its own records a root span with an empty input (verified against a live run: the OpenAI Agents agent span is the root and carries no recorded input), so the processor ALONE is NOT replayable. Pair it with its run wrapper (
getOpenAiAgentHandler/get_openai_agent_handler), a drop-in for the run call that opens a keyed root carrying the run input as a serializable argument, with the processor's spans nesting underneath, which turns it into case (1). A hand-writtenwithSpan/@spanroot that takes the run input works too.The root exists so the replay harness can re-invoke it as a plain lambda with serialized inputs: that's what makes traces searchable (a coherent unit of behavior) and replayable (runnable against current code). The root must own its state setup, not consume a pre-built stateful object the replay registry module can't reconstruct. Frameworks are the sharpest case (LangGraph compiled graphs, Claude Agent SDK clients, LangChain chains all require constructors + special setup), but the rule generalizes to anything stateful, configured SDK clients, prepared models, cached routers, DB sessions. For manually wrapped workflows, the root is therefore the outer workflow function that builds the framework / stateful object + invokes it + processes the output (API handler, message processor, job runner, pipeline coordinator), almost never the SDK's
run()/invoke()itself. For callback-handler integrations that already record a replayable root (LangGraph / LangChain, Claude Agent SDK, or Vercel AI SDK), do not turn this into a mandatory manual outer span: the handler-created framework invocation root is enough when the production workflow is just the graph/chain/agent invocation. The replay callable is where you rebuild the framework/stateful object around the recorded root input. Add a same-keywithSpan/@spanouter root only when there is meaningful production work around the framework call (input prep, non-framework retrieval, post-processing, persistence, downstream service calls) that should be visible in the trace.Wrap the code path that runs the real workload (serves traffic, processes the actual jobs), not an entrypoint that exists only to test or explore locally. The test is role, not form: a cron-driven batch script or an ETL job is production and worth wrapping; a dev CLI or notebook that exists only to poke at the workflow is not. Instrument the real path even when you'll run it in dev to generate traces.
Hard constraint: every wrapped function's inputs and outputs must be serializable by the SDK's tracing layer so traces can be replayed. Every span input and output gets serialized into the trace using the SDK's language-native serialization (TypeScript/JSON, Python/JSON via Pydantic, Ruby/
to_json, Go/json.Marshal). If a wrapped function takes live runtime objects that don't round-trip through that serialization, the trace can't be replayed, and badly-failing inputs can drop the entire span on the floor (not just garble the input field). Examples of unserializable inputs:- browser objects (
MediaStream,RTCPeerConnection,WebSocket, DOM refs) - HTTP
Request/Response, stream writers, open sockets - framework request contexts whose content is genuinely opaque (not reconstructible from headers + user id)
- live SDK client instances passed as arguments (LLM clients like
OpenAI/Anthropic/ Bedrock, configured agents, DB connection objects, HTTP agents): class instances whose internals carry circular references, function members, or platform handles all sink superjson andJSON.stringify. Watch especially for an options/config bag (e.g.options.llmProvider,ctx.db) that smuggles a live client into an otherwise-serializable signature.
Unserializable OUTPUTS (live streams) are a separate case from unserializable inputs, and in the TypeScript SDK they do NOT require a refactor. A function whose inputs are serializable but which returns a live stream the caller consumes directly (a Vercel AI SDK
streamTextresult, aReadableStream, an SSE / streamingResponse) is the common shape for chat and agent endpoints. Serializing that object as-is captures nothing replayable, and awaiting it to completion before returning would break streaming and first-byte latency. Record a drained, serializable view of the stream as the span output instead:- TypeScript: use the
withSpanfinalizeoption (withSpan(key, { type, finalize }, fn)). The wrapped function returns the live stream to the caller unchanged; the span recordsawait finalize(result)(e.g.{ text, usage, toolCalls }). Pass the prebuiltfinalizers.aiSdkfor the Vercel AI SDK, orfinalizers.readableStreamfor a rawReadableStream(reading the AI SDK result's promises does not disturb the caller's stream, since it tees internally). This is purely-additive instrumentation, NOT a refactor: do it in the write-instrumentation step with no second confirmation. The trace stays replayable as long as the function's inputs are serializable. Never push the user into a structural rewrite of a streaming endpoint whenfinalizecovers it. - Python: also use the
finalizeoption (@client.span(key, type=..., finalize=...)). The idiomatic, non-destructive shape is an async generator thatyields its chunks (the caller still receives every chunk);finalizethen receives the collected chunks and returns a serializable summary. Passfinalizers.openai_chunksfor OpenAI streaming orfinalizers.anthropic_eventsfor Anthropic. Same rule: purely-additive, NOT a refactor, no second confirmation. (Python streams are single-consumer, so prefer the async-generator form over draining a returned stream object.) - Ruby / Go (no
finalizeyet): introduce a serializable completion. Trace a core that runs the turn to completion and returns{ text, usage, ... }, with the streaming wired around it (the structural refactor below).
Module-level dependencies (DB clients, env vars, config loaders, LLM clients) do not count when accessed via module scope or closure: replay resolves them from the app's runtime wiring instead of serializing them as span inputs. The same client passed as a function argument IS captured as input and WILL fail. The fix when an SDK client is the only unserializable piece is usually trivial: hoist it to module scope (or capture via closure) and drop it from the argument list, leaving the wrapped function's serializable args (issue, request, options-without-the-client) intact. When the natural outer boundary still has unserializable inputs after that, do one of the following before writing code:
- Instrument via the framework handler or processor (preferred whenever the workflow runs on a supported framework: LangGraph / LangChain via
getLangGraphCallbackHandler/get_langgraph_callback_handler, OpenAI Agents SDK viagetOpenAiTracingProcessor/get_openai_tracing_processor, Claude Agent SDK viagetClaudeAgentHandler/get_claude_agent_handler, Vercel AI SDK viagetVercelAiMiddleware). These split into two replayability cases, do not conflate them:- Integrations that record a replayable root (LangGraph / LangChain, OpenAI Agents SDK, Claude Agent SDK, or Vercel AI SDK) are replayable as-is, via one of two mechanisms. Callback handlers (LangGraph / LangChain, Claude Agent SDK, or Vercel AI SDK) record the framework invocation itself as the root span, with the framework's own serializable input (LangGraph initial state, agent prompt) as the recorded root input. Trace processors (OpenAI Agents SDK) don't record the input themselves, so their run wrapper (
getOpenAiAgentHandler/get_openai_agent_handler) does it: a drop-in for the run call that records a keyed root carrying the run input, with the processor's auto-captured spans nesting underneath. Either way, the unserializable arguments above it (live dependency objects, billing callbacks, request contexts) never enter the trace, and no decorated root function needs to exist in the app code: the replay registry module passes the key toreplay()with a plain callable that re-invokes the same framework entrypoint production calls with the recorded root input plus reconstructed runtime wiring (framework config and dependencies); the SDK wraps the callable internally. Unsafe calls made by that wiring still need replay-mockable marked spans; use no-op values only for replay-only callback slots with no recorded call to mock. On SDKs that predate explicit-key replay, wrap the callable under the same key yourself (Python@bitfab.span("<key>"), TSgetFunction(key).withSpan(...)). The pattern is documented in the SDK docs' Replay section (handler subsection) and wired up in step 14 11b. Never report one of these workflows as "not replayable" because no@span-decorated function exists in production code. - A bare trace processor (OpenAI Agents SDK) with neither its run wrapper nor a manual root is NOT replayable. The processor captures the run, but its root span records an empty input (verified against a live run: the OpenAI Agents agent span is the root and carries no recorded input). Pair it with the run wrapper (
getOpenAiAgentHandler/get_openai_agent_handler), the drop-in for the run call above, or a hand-writtenwithSpan/@spanroot that takes the run input: the processor's auto-captured spans nest under that root, and replay runs against the root's serializable input. Do not treat a bare processor-only trace as replayable.
- Integrations that record a replayable root (LangGraph / LangChain, OpenAI Agents SDK, Claude Agent SDK, or Vercel AI SDK) are replayable as-is, via one of two mechanisms. Callback handlers (LangGraph / LangChain, Claude Agent SDK, or Vercel AI SDK) record the framework invocation itself as the root span, with the framework's own serializable input (LangGraph initial state, agent prompt) as the recorded root input. Trace processors (OpenAI Agents SDK) don't record the input themselves, so their run wrapper (
- Move the trace boundary inward to the first function whose inputs are serializable (e.g. trace
processTurn(transcript, context)instead ofhandleSession(stream, peerConnection)). This is not a refactor. - Refactor so a function with serializable inputs exists. Two flavors, chosen per case in the refactor plan:
- Visibility refactor (common): the logic that takes serializable inputs already exists inline but isn't importable (embedded in a route handler, not exported). Extract it into a named, exported function at module scope. No semantic change.
- Structural refactor (rare overall, mostly realtime/browser apps): no function with serializable inputs exists yet. Introduce one: a pure core whose parameters are serializable, with callers constructing them. A real rewrite. (This flavor is for missing serializable-input cores. A streaming output in the TypeScript and Python SDKs is handled by the
finalizeoption above, not a structural refactor; only fall back here for streaming on Ruby/Go.)
Raise this with the user in step 11 (not later); never instrument a root with unserializable inputs and try to fix it in the Replay phase.
- browser objects (
-
First, check for reusable draft plans. Call
list_trace_planswith{ source: "analyze_repo", status: "awaiting" }(it returns only this org's unconfirmed, non-expiredbitfab:setup analyze-repodrafts, newest first). This is a silent probe, do not narrate the call itself.If drafts came back, print them for the user before asking, so they can choose which to wire up. List every draft as a numbered item (this is the menu they will pick from in step 8), each showing enough to decide on, straight from the
list_trace_plansoutput:- root name and its trace function key
- the root file
- frameworks detected in the plan (when any)
- recommended capture: N of M nodes
- how long ago it was drafted
Evaluate these two routes before you ask anything. Step 2 may already have settled this question, and re-asking several steps and an SDK install later is asking the user to repeat themselves:
- the existing-SDK-usage menu already chose to search for more workflows, and the draft probe returned nothing: skip the question and scan β step 9
- the existing-SDK-usage menu already named a target to instrument, and the draft probe returned nothing: skip the question and read the named target β step 10
Awaiting drafts override both, because they are new information the user has not seen: when the probe returns any, ask instead, and say in one line what their earlier answer was so the re-ask reads as an update rather than a memory lapse.
When neither route fires, ask the user what to instrument:
A) Reuse an analyze-repo draft plan: only shown when drafts exist: pick from the printed drafts and wire them up one at a time (recommended) β step 8 B) Find workflows for me: scan the codebase for every AI call, agent, and LLM-driven decision β step 9 C) I'll name the target: type the file, function, or directory and I read only that, no codebase scan β step 10
Option C is a free-text option, not a promise of a second question. Say in the question's own text that they can type the file, function, or directory instead of picking anything, so a user who already knows their target answers in the same breath and never sees a follow-up prompt. The scan (B) is the only option that spends real time, which is why it is worth a fork at all. An answer that names a file, function, or directory IS option C, whether it arrives as typed free text, as a note on a selection, or as the label plus a name: route it to C and take the name, never re-ask because no option came back selected.
Option A (reuse the drafts) is only present when
list_trace_plansreturned at least one draft. When drafts exist, offer A first and recommend it, wiring up an existing draft is the whole point of having runanalyze-repofirst. When there are no drafts, omit A entirely and present only the scan and point-to-target options (the plain two-way choice, with no mention of trace plans); in that case recommend the scan option instead.If they pick A, go to step 8 to wire up the picked drafts (a draft is a starting point, not a commitment: it is still presented for review and can be adjusted or cancelled before any code is written). If they pick B, do the full codebase scan in step 9.
If they pick C, they have just told you they know what they want; the only thing you are missing is its name, and it usually arrives with the answer. If they named it anywhere (the free text they typed on this question, their original invocation, or earlier in the session), take that and go straight to step 10. Only when C comes back bare, with no target named, do you ask in plain chat and wait for their next message, one line, e.g. "Which file, function, or directory should I instrument?". Do NOT turn it into an options question (the answer is free-form, and options would only stand between the user and typing it), and do NOT read, grep, or list the codebase to assemble candidate targets to offer them. Scanning for candidates is option B, the option they just declined, and a menu of the three roots you happened to find is worse than an empty prompt: the target they have in mind may not be on it, and offering it reads as ignoring their answer. Once they name it, go to step 10 to read just that location, skipping the broad scan.
-
You reached this step by choosing to reuse a saved trace plan: either option A at step 7 (the first pass) or the saved-plan option at step 16 (a later loop, after instrumenting something). Both arrive here to wire up drafts one at a time.
If you do not currently hold an unprocessed batch queue (a fresh arrival from either step), ask the user which of the awaiting drafts you just listed to instrument (they may pick one or several) and hold the selected plan ids as an ordered batch queue. If you already hold a queue with unprocessed drafts left (you looped back here from a skip below), do not ask again, just take the next unprocessed draft. Process them one at a time, exactly like the discovery loop instruments one workflow per cycle, never several at once. Only once a queue you were actively processing is fully consumed (every selected draft processed or skipped) do you leave to step 16 to pick the next move, never bounce back there without a fresh selection.
For the current draft, reconcile it against the CURRENT code, a draft is a snapshot from analyze-repo time and the code may have moved on (a prior draft in this same batch, a manual wrap, or an earlier session may already have instrumented this workflow; the root may also have been renamed, moved, or deleted). Reconcile now, before you present or open the plan, so you never surface a plan the user can't act on: presenting a plan you're about to abandon (already instrumented, or stale) just wastes their time on a moot review.
- Read the draft's tree with
get_trace_plan{ planId }(this reads the draft without confirming it). - Read the plan's root file at its recorded
file/line, and grep the root and captured-node locations for existing SDK instrumentation, the plan's own trace function key (getFunction("<key>")/get_function/bitfab_function/WithFunctionName) and span wrappers (withSpan/@span/bitfab_span/client.Span). - Cross-check the key against
list_trace_functions: a key that already appears there is live and sending traces. - While you are in these files, also Read every captured node's signature (the plan carries
file,line,signatureon each node) and note the plan's replay dependencies (capturedexternal_read/side_effectnodes), so that if this draft proceeds you carry into step 14 the same context a discovery cycle would have held (its mock / DB-snapshot follow-ups included), and steps 13 and 14 do not have to re-read the code.
Then take the branch that matches what you found, do not blindly wrap: wrapping a root that is already instrumented would duplicate the trace, you would create a second root span over the same call and the workflow would report twice.
A) Wire up this draft: the normal case: present the draft, confirm it, then instrument (recommended) β step 13 B) Already instrumented, retire the draft: re-wrapping would duplicate the root span; cancel the draft, offer
setup modify, move to the next draft β step 8 C) Refresh this draft in place: rebuild the tree from current code, save it onto the same plan id, then present it β step 13 D) Workflow gone, retire the draft: nothing left to instrument; cancel the draft and move to the next one β step 8A is the normal reuse case: it carries the existing draft plan id into step 13, which presents the already-built draft (no re-build) and opens/confirms it exactly like a freshly built plan.
C repairs the draft instead of throwing it away. The workflow still exists, the draft just describes where it used to be, so rebuild its tree from the current code (the same reconcile rules
bitfab:setup modifyuses: keep a node's id, samples, and analysis where its implementation identity is clear, mint new ids for genuinely new nodes, and backfillsampleInput/sampleOutput/analysison anything you added or changed), then save it back onto the SAME draft withsave_trace_plan{ planId, tree, capturedNodeIds }. A structural update may replacetree.rootId, so a moved or renamed boundary is a refresh, not a reason for a second plan. Carry that same plan id and your refreshed tree into step 13. Say in one line what moved. Never leave a stale draft in the reuse list for a later session to pick up, and never create a competing plan for a key that already has one.B and D retire this draft with
cancel_trace_planand loop back here for the next queued draft, so a moot draft never reaches the user and never resurfaces in a later probe. Cancelling is final, whic - Read the draft's tree with
*Truncated - read the full file at https://github.com/Project-White-Rabbit/bitfab-amp-plugin/blob/75b0b1b0caa1dff6d8ea6f6fdd27563a2e7e8c8d/skills/setup/SKILL.md.