# Onboard this app to LoopOps

You are a coding agent. Execute this runbook top to bottom inside the
repository you were asked to onboard. Ask the human only at the decision
points marked **ASK**. Everything else is yours to do. Installing LoopOps into
the current coding harness is step 0 and is NOT optional.

Done means all six are true:

1. This coding harness has the LoopOps plugin installed when it supports one;
   every other harness has the LoopOps skill installed. The hosted
   `https://mcp.loopops.dev` server is registered and visible to the client.
2. The project exists and its keys are stored where the app can read them
   (never committed).
3. The app — or a smoke-test span — has POSTed telemetry that is readable back.
4. The system map WIRING GRADE reads complete: a service node with
   `service.version`, and every real boundary this repo crosses (database,
   external API, queue, platform binding) present as a relationship with
   latency. A single-process app with genuinely zero outbound calls may grade
   `relationships: false` — then say WHY in the step-6 handoff. First-span-
   readable is NOT done.
5. One diagnosis has run.
6. The human has the handoff block from step 6: console link, project id,
   recovery code.

Budget after the harness is active: about five minutes. Commands with angle-
bracket placeholders must be filled from the current repo or the preceding
response before they are run.

## 0. Install LoopOps into this coding agent (required)

This is the first gate. Do not provision a project or edit the application
until the current harness has the LoopOps procedure layer AND the hosted MCP
server. Detect which coding agent is running, use the FIRST matching section,
and run the commands yourself. Ask the human only for command approval, the
one-time email OAuth step, or a restart/reload the current process cannot do.

### Claude Code — native plugin

The plugin bundles the LoopOps skill and MCP server. Inspect first with
`claude plugin marketplace list` and `claude plugin list`; run only the
missing install commands:

```bash
claude plugin marketplace add codeyogi911/loopkit-plugin
claude plugin install loopkit@loopkit --scope user
```

Verify `claude plugin list` contains `loopkit@loopkit`. If this session did
not load the newly installed plugin, **ASK** the human to run
`/reload-plugins` (or restart Claude Code), then STOP. Tell them to paste
`Fetch https://loopops.dev/agent-setup/prompt.md` again after reload. On the
next pass, confirm the LoopOps MCP tools are visible and complete the one-time
email sign-in when Claude prompts for it.

### Codex — native plugin

The plugin bundles the LoopOps skill and MCP server. Inspect first with
`codex plugin marketplace list --json` and `codex plugin list --json`; run
only the missing install commands:

```bash
codex plugin marketplace add codeyogi911/loopkit-plugin --ref main
codex plugin add loopkit@loopkit
```

Verify `codex plugin list --json` reports `loopkit@loopkit` as installed and
enabled, and `codex mcp list --json` contains an enabled `loopops` server at
`https://mcp.loopops.dev`. Then run:

```bash
codex mcp login loopops
```

**ASK** the human to complete the browser email sign-in. If the current thread
cannot load a plugin installed during that same thread, ask the human to start
a new Codex thread, then STOP. Tell them to paste
`Fetch https://loopops.dev/agent-setup/prompt.md` again in the new thread.

### Cursor, GitHub Copilot, Windsurf, OpenCode, or another coding agent

These clients do not consume the LoopOps Claude/Codex marketplace plugin.
Installing the equivalent two pieces is still required: install the same loop
skill into the current agent, then merge the MCP entry into that client's
existing config without deleting any other server:

```bash
npx -y skills add codeyogi911/loopkit-plugin --skill loopkit --yes --global
```

Use the matching MCP shape:

- **Cursor** — merge under `mcpServers` in `.cursor/mcp.json`:
  `"loopops": { "url": "https://mcp.loopops.dev" }`.
- **GitHub Copilot / VS Code** — merge under `servers` in
  `.vscode/mcp.json`:
  `"loopops": { "type": "http", "url": "https://mcp.loopops.dev" }`.
- **Windsurf** — merge under `mcpServers` in
  `~/.codeium/windsurf/mcp_config.json`:
  `"loopops": { "serverUrl": "https://mcp.loopops.dev" }`.
- **OpenCode** — merge under `mcp` in
  `~/.config/opencode/opencode.jsonc`:
  `"loopops": { "type": "remote", "url": "https://mcp.loopops.dev", "enabled": true, "oauth": {} }`,
  then run `opencode mcp auth loopops`.
- **Any other skill-capable MCP client** — install the skill with the command
  above, then register a remote Streamable HTTP MCP server named `loopops`
  with URL `https://mcp.loopops.dev` in the client's normal config.

Restart the client when its MCP/skill loader requires it. **ASK** the human to
complete the one-time email sign-in on first MCP use. Do not continue until
the LoopOps skill is present and the client lists the `loopops` server.

Before step 1, print this filled receipt:

```text
LoopOps agent tooling ready.
  Harness: <Claude Code | Codex | Cursor | Copilot | Windsurf | OpenCode | other>
  Procedure: <loopkit@loopkit plugin | loopkit skill path>
  MCP: https://mcp.loopops.dev (configured and enabled)
  Auth: <connected | browser sign-in completed>
```

## 1. Provision a project (idempotent — safe to re-run)

**Already have a project?** If the human created one from their dashboard and
gave you its keys (or they sit in `.env` as `LOOPOPS_INGEST_KEY` /
`LOOPOPS_AGENT_KEY`), SKIP this step — use those keys and the given
project_id, and skip the claim line in step 6 too: the project already lives
under their account.

Otherwise, derive `project_name` from the repo (directory name or package
name, lowercase, hyphens). If the repo has a GitHub remote, pass it — the
console uses it to link loop runs back to code.

```bash
curl -fsS -X POST https://loopops.dev/v1/provision \
  -H 'content-type: application/json' \
  -d '{"project_name":"<repo-name>","repository_url":"<owner/repo, optional>"}'
```

The response carries three keys plus your way back in:

- `producer_key` (`lk_ingest_*`) — writes telemetry. Goes in the app's env.
- `agent_key` (`lk_agent_*`) — reads, diagnoses, records loop stages. Yours.
- `feedback_key` (`lk_feedback_*`) — feedback-only, safe to ship in a browser.
- `recovery_code`, `dashboard_url`, `project_id` — for the human (step 6).

Re-running with the same `project_name` and one of the keys as
`Authorization: Bearer <key>` RESUMES the same project without rotating
anything (`resumed: true`, HTTP 200 instead of 201), so a retried onboarding
never mints a duplicate. A resume returns NO keys and NO recovery_code — you
keep what you hold — so store the FIRST response's values in `.env`
immediately; they cannot be re-fetched. A 429 means the per-IP limit (5/hour,
shared with /v1/signup) — wait `retry_after_seconds`, do not loop.

The response also carries deployment-correct URLs (`ingest_url`,
`diagnose_url`, `ingest_health_url`); prefer them over the literal
loopops.dev URLs below when they differ.

**Key safety (hard rules):** put the keys in `.env` (or the project's secret
store), ensure `.env` is gitignored, and never write a key into a tracked
file, a commit, or a log. Show the human the step-6 block instead.

## 2. Wire emit for this stack

This runbook is the ROOT of a tree: pick the FIRST row that matches the repo,
then read that row's leaf doc (raw markdown, agent-readable) for the complete
wiring — options, privacy flags, session folding, pitfalls. Do not install
more than the repo needs.

| Repo uses | Do this | Full leaf |
| --- | --- | --- |
| Vercel AI SDK 7 | `npm i @getloopops/sdk` then once at startup: `import { registerTelemetry } from "ai"; import { loopOps } from "@getloopops/sdk/ai"; registerTelemetry(loopOps({ agent: "<project_name>" }))`. No per-call flag. On Workers/Edge (no `process.env`) pass the config explicitly: `loopOps({ agent, url: env.LOOPOPS_URL, ingestKey: env.LOOPOPS_INGEST_KEY })`. | /docs/instrument-ai-sdk.md |
| Vercel AI SDK 6 | `npm i @getloopops/sdk` then `registerLoopOps()` from `@getloopops/sdk/otel` at startup and `experimental_telemetry: telemetry()` on each generate call. Node-only. | /docs/instrument-ai-sdk.md |
| Existing OpenTelemetry | Keep your SDK. Point an OTLP/HTTP exporter at `https://loopops.dev/v1/traces` with header `authorization: Bearer <producer_key>`. JSON and protobuf both accepted, gzip/deflate content-encoding included. | /docs/instrument-node-otel.md |
| Cloudflare Workers | Use Cloudflare-native automatic tracing plus, when the human approves application-log capture, a separate Logs destination — no runtime import or generic request wrapper. The destinations are created in the Cloudflare dashboard, then named in `observability.traces.destinations` and `observability.logs.destinations`. | /docs/instrument-any-otlp.md |
| Any Node process | `npm i @getloopops/otel @opentelemetry/auto-instrumentations-node`, create `otel.js` containing `import { initLoopOps } from "@getloopops/otel"; initLoopOps();`, run with `LOOPOPS_URL=https://loopops.dev LOOPOPS_INGEST_KEY=<producer_key> node --import ./otel.js app.js`. Node-only — not Workers. `@getloopops/otel` ≥0.4 picks up auto-instrumentations automatically when installed — they emit the outbound client spans (db, http, queues) the system map needs for relationships and latency; without them the map is one box. | /docs/instrument-node-otel.md |
| LangChain / LangGraph, or anything else | No first-class integration — use the generic OTLP path: their OpenTelemetry instrumentation (or your own) with an OTLP/HTTP exporter at `/v1/traces`, or POST raw OTLP JSON (exact shape in step 3) from wherever failures surface. | /docs/instrument-any-otlp.md |

AI SDK telemetry lights the AGENT surface only (model/tool spans). If the
app also calls databases or external APIs and you want those as map
relationships with latency, ALSO wire the "Any Node process" row alongside —
the two producers coexist.

Browser / product events / user feedback (a second producer, not a row above):
/docs/instrument-browser.md.

Env names the SDKs read: `LOOPOPS_URL` and `LOOPOPS_INGEST_KEY`.

### Cloudflare Workers: native trace + approved log export

For a Cloudflare Worker, do not add an SDK merely to recreate request, fetch,
KV, R2, Durable Object, or handler spans. Cloudflare's native tracing emits
those automatically as OTLP. The emitted span attributes include
`service.name` (the Worker name), `faas.version` (the deployed version tag),
and `cloudflare.script_version.id`. LoopOps projects that deploy identity into
the same verify/regression path used by `service.version`.

Cloudflare's native export is currently in open beta. Confirm that the account
exposes Workers Observability Destinations (dashboard → Workers Observability →
Destinations) before changing the project. If it does not, report that
truthfully and use the direct OTLP route in step 3; do not pretend the native
destination is configured.

The destinations are created in the Cloudflare dashboard — Cloudflare
documents no public API for this step — so they are the human's move. **ASK**,
with the exact values ready to paste: “Please create an OTLP destination under
Workers Observability → Destinations: name `loopops-<project_id>-traces`,
type Traces, endpoint `https://ingest.loopops.dev/v1/traces`, custom header
`authorization: Bearer <producer_key>`. If you approve exporting application
logs (`console.*`, exceptions, and system logs), also create
`loopops-<project_id>-logs`, type Logs, endpoint
`https://ingest.loopops.dev/v1/logs`, with the same header. Tell me which destinations
exist and I will prepare the Worker configuration; I will not deploy without a
separate yes.” If a same-named destination already exists with a different
endpoint or header, **STOP** and report the conflict; never ask the human to
replace another destination silently. Never write the producer key into a
tracked file, app code, a commit, or a log — it lives only in the Cloudflare
destination headers.

Once the human confirms the destination, **merge** this into the Worker's
existing Wrangler configuration. Append
the destination name only when absent, de-duplicate the list, and preserve
every existing destination and setting. The destination name must match the
Cloudflare destination exactly:

    {
      "observability": {
        "traces": {
          "enabled": true,
          "destinations": ["<existing-destination>", "loopops-<project_id>-traces"]
        },
        "logs": {
          "enabled": true,
          "destinations": ["<existing-log-destination>", "loopops-<project_id>-logs"]
        }
      }
    }

Do not overwrite an existing `head_sampling_rate` or `persist` policy. For
a new traces block, propose `head_sampling_rate: 1` so Cloudflare samples the
representative proof request, and propose `persist: false` when the human does
not want a second Cloudflare-retained copy; obtain explicit approval for those
policy choices. Delivery can take a few minutes, so confirm both the destination
status and LoopOps ingest health before calling the connection proven.

Run the repository's normal typecheck/build or Wrangler dry run first. **ASK**
again before the production deploy. After deployment, make one representative
request; Cloudflare emits the base runtime evidence without an app-code edit.

Cloudflare logs are separate and can contain arbitrary console output. Do not
enable the Logs destination by default; add its config block only after the
human approves the capture policy. Never point a Worker's destination at an
OTLP receiver hosted by that same Worker: exported trace/log invocations become
new telemetry and recurse. LoopOps avoids that path with a dedicated receiver
Worker that has no export destinations of its own. For business outcomes, model/tool details, or other
semantics Cloudflare cannot infer, add a narrow custom span or the appropriate
LoopOps producer integration after this baseline works.

The SYSTEM MAP is generated from the wiring. Set stable OTel resource identity
on every process: `service.name` (required), `service.namespace` when the
same service name can exist in several systems, `service.version` for the
deployed artifact, `deployment.environment.name`, and a per-process
`service.instance.id`. Add `service.criticality` when known. Cross-service parent/child spans
become relationships; outbound `peer.service`, `server.address`, or `db.*`
attributes can expose a virtual dependency when the remote side is not traced.
Do not create a config file that lists boxes or arrows.

LoopOps also classifies records into six design-language surfaces (client · api
· agent · integrations · background · data) for coverage and drill-down. Those
surfaces are not topology nodes. Most records classify automatically
(GenAI/OTel semconv, event names). Where the derivation would have to guess,
stamp the category explicitly on the span/log:
`loopops.component.surface` (one of the six) and optionally
`loopops.component.name` (e.g. the connector or screen it names).

**ASK** the human only if the wiring point is genuinely ambiguous (e.g. several
services in one repo — ask which one to instrument first).

## 3. Send the first span and read it back

Do not wait for real traffic — prove the pipe now:

```bash
curl -sS -X POST https://loopops.dev/v1/traces \
  -H 'authorization: Bearer <producer_key>' \
  -H 'content-type: application/json' \
  -d '{"resourceSpans":[{"resource":{"attributes":[{"key":"service.name","value":{"stringValue":"<repo-or-service-name>"}},{"key":"service.namespace","value":{"stringValue":"<system-name>"}},{"key":"service.version","value":{"stringValue":"<git-sha-or-release>"}},{"key":"deployment.environment.name","value":{"stringValue":"<production|staging|test|development>"}},{"key":"service.instance.id","value":{"stringValue":"<instance-uuid>"}}]},"scopeSpans":[{"spans":[{"traceId":"<32 hex chars>","spanId":"<16 hex chars>","name":"onboarding_smoke_test","startTimeUnixNano":"<now in ns>","status":{"code":1},"attributes":[{"key":"loopops.component.surface","value":{"stringValue":"<client|api|agent|integrations|background|data>"}}]}]}]}]}'
```

`service.namespace` and `endTimeUnixNano` are optional — omit namespace for
a single-system repo (the map node id is then `service:<name>`); add an end
time if you want duration metrics populated. For a generic smoke span from a
service process, `api` is a fine surface value.

Ingest returns 2xx immediately; the read lane lags 60–90 seconds. Poll until
`count > 0` (give up and troubleshoot after ~5 minutes):

```bash
curl -sS 'https://loopops.dev/v1/telemetry?limit=1' -H 'authorization: Bearer <agent_key>'
```

## 4. Generate and verify the system map

Exercise one representative REAL path through the instrumented app (a request,
agent turn, job, or CLI command). For a multi-service path, preserve OTel trace
context across the boundary so the downstream server span carries the upstream
span as `parentSpanId`. Then read the key-authenticated projection:

```bash
curl -sS https://loopops.dev/v1/dashboard \
  -H 'authorization: Bearer <agent_key>'
```

FIRST enumerate this repo's REAL boundaries from the code — database clients,
HTTP SDKs, queues, platform bindings. Each of those must appear on the map as
an edge or a named virtual dependency; each one missing means the outbound
side lacks peer identity and the wiring is incomplete, whatever else looks
green.

Inspect `dashboard.system_map` — it carries a machine-checkable
`wiring` grade (`serviceIdentity` / `versioned` / `relationships` /
`latency` / `complete`, with `problems` naming each gap; the MCP
`loopops_map` returns the same grade in snake_case). Assert on it — do not
eyeball counts:

- `nodes` must contain the expected service identity: `service:<name>`, or
  `service:<namespace>/<name>` when you set `service.namespace`.
- The node must show the expected `currentVersion`, environment, and instance
  count. `versionSkew: true` is useful only when you intentionally have a
  canary/rolling deployment; otherwise it is a wiring or rollout warning.
- `edges` must contain the real boundary you exercised. A single-process app
  can honestly have zero edges; never add fake spans or configuration just to
  draw one.
- A dashed/virtual dependency means only the calling side was observed. Wire
  the remote service later if you need an end-to-end edge.
- `surfaces` is coverage metadata. A silent category may be irrelevant to this
  app; it does not mean a missing architecture box.

If `wiring.complete` is false and the app has real boundaries, follow the
`wiring.problems` lines and the silent surfaces' `instrument` hints, fix
the producer, and repeat this step — do NOT continue to step 5 with an
incomplete grade you cannot justify. The lake read path rolls 60–90 seconds
behind ingest, so poll for up to five minutes before diagnosing the wiring.

## 5. Run the first diagnosis

```bash
curl -sS -X POST https://loopops.dev/v1/runs/diagnose \
  -H 'authorization: Bearer <agent_key>' \
  -H 'content-type: application/json' \
  -d '{"since_minutes": 60}'
```

The response is `{ ok, packet }`; candidates live at
`packet.improvement_candidates`, each with cited `evidence_ids` and a stable
`cluster_id`. With only healthy onboarding traffic it may be empty and
`packet.status` reads `observe_more` — that is fine; the read path is proven
and no improvement loop needs to be started now.

## 6. Hand off to the human

Print exactly this block, filled in (this is the ONLY place the recovery code
should surface). Two different doors on purpose: `dashboard_url` is the
bookmarkable tokened way back into THIS project; /dashboard is the email
sign-in where claiming happens:

```text
LoopOps is connected.
  Project:       <project_id>
  Console:       <dashboard_url>   ← bookmark this; it is your way back in
  Recovery code: <recovery_code>   ← store it (password manager); it rotates lost keys via POST /v1/recover
  Keys:          written to .env (gitignored), never committed
  Status:        first span readable · first diagnosis ran
  Map wiring:    <complete | incomplete: reason from wiring.problems, or "no outbound boundaries in this app">
Keep it: sign in at https://loopops.dev/dashboard (email, one-time PIN) and
claim project <project_id> with the recovery code — it then lives under your
account. Claiming binds the project to the account and makes it permanent
(a sandbox project otherwise expires in 14 days). Ingest is free and unlimited.
```

(Drop the "Keep it" claim lines when the human created the project from their
dashboard — it is already theirs, and there is no recovery code to print.)

## 7. Optional, after handoff

- The plugin/skill and hosted MCP were installed in step 0. Use the native
  `loopops_claim` tool to claim a headless project, then run a bounded quick
  check or governed improvement cycle. The `loopops_docs` tool serves these
  docs in-session — no web access needed.
- No human email available? Every loop verb has a key-only HTTP spelling —
  observe `POST /v1/observations/open`, diagnose `POST /v1/runs/diagnose`,
  decide `POST /v1/observations/close`, improve `POST /v1/loops/events`,
  verify `POST /v1/runs/verify` — all with the `agent_key`.
- User feedback lane (typed + spoken): POST /v1/feedback with the
  `feedback_key` — see /quickstart.md §3.

## Troubleshooting

- 401 on ingest → wrong key lane: /v1/traces takes the producer key. 401 on
  reads/diagnose → the same mistake reversed: those take the agent key.
- 429 on provision → per-IP cap (5/hour, shared with /v1/signup). Respect
  `retry_after_seconds`.
- Telemetry count stays 0 past ~5 minutes → re-check the POST response was
  2xx, then GET /v1/ingest/health with the producer key.
- Deeper per-stack instructions (raw markdown): /docs/instrument-ai-sdk.md ·
  /docs/instrument-node-otel.md · /docs/instrument-browser.md ·
  /docs/instrument-any-otlp.md
- Full surface: /openapi.json · operating instructions: /agents.md ·
  machine map: /llms.txt

Packages ship as `@getloopops/*` on npm (`loopkit` is only the internal
codename — there is no `loopkit` npm package).
