# Add quibble to an app — instructions for a coding agent

You are adding **quibble** to the developer's web app: a feedback button that files each
note as an issue in their own tracker. This file is the whole recipe. Follow it in order,
and don't improvise code that a step gives you: the snippets here are the ones quibble
supports.

quibble version these steps match: see `VERSION` next to this file.

## 0. Ask how hands-on to be — once, before anything else

Ask the developer to pick one, and repeat it back:

1. **Do it, ask at forks.** You make the changes and only stop for real choices
   (tracker, repository, how users sign in).
2. **Plan, then apply.** You read the app and show every change as a diff; you apply
   nothing until they approve.
3. **Guide me.** You explain each step and check their work; you never edit a file.

Before every edit, say which mode you're in. If you're unsure whether something is a fork,
treat it as one and ask.

## Rules that hold in every mode

- **Never handle the tracker token.** The developer creates it and puts it in place.
  You may write a placeholder line (`QUIBBLE_GITHUB_TOKEN=` with nothing after it) into
  `.env.example`, never a value into `.env`, a commit, a log or the chat.
- **Never weaken the app's authentication.** quibble's endpoint creates issues, so it
  must sit behind the app's sign-in, like the rest of the app. If you can't tell whether it
  will, stop and ask.
- **Touch only the files a step names**, and list every file you changed at the end.
- **Don't guess a framework.** If detection is ambiguous, ask.

## 1. Find out what you're working with

Read, don't ask, where you can:

- **Framework:** look at imports and the dependency file (`pyproject.toml`,
  `requirements*.txt`, `Pipfile`) for `fastapi`, `django`, `flask`, `starlette`,
  `litestar`. Python frameworks other than these, and non-Python apps, go to
  [Any other stack](#any-other-stack).
- **Package manager:** `uv.lock` → `uv add`; `poetry.lock` → `poetry add`;
  `Pipfile` → `pipenv install`; otherwise `pip` and the requirements file.
- **Where pages are rendered:** the base template (Jinja `base.html`, a Django
  `templates/base.html`) or the function that returns HTML.
- **How users sign in:** a reverse proxy in front (Cloudflare Access, oauth2-proxy,
  an SSO gateway), the app's own login, or nothing. Note which headers the proxy sets.

Tell the developer what you found in three or four lines before going on.

## 2. Choose the tracker and where notes go

Ask: **which tracker, and which repository or project?** Then tell them exactly which
token to create (they create it; you don't):

| Tracker | `QUIBBLE_TRACKER` | Target (`QUIBBLE_GITHUB_REPO`) | Also set | Token to create |
|---|---|---|---|---|
| GitHub | *(leave unset)* | `owner/repo` | — | Fine-grained PAT on that repo: **Issues: Read and write**. Add **Contents: Read and write** only if they want screenshots stored in the repo. Easiest: set `QUIBBLE_GITHUB_REPO`, run `quibble doctor`, and give the developer the link it prints: it opens GitHub's form already filled in, so they only tick the repos. |
| GitLab | `gitlab` | `group/project` | `QUIBBLE_TRACKER_API=https://<host>/api/v4` | Personal or project access token with the **`api`** scope. |
| Gitea / Forgejo | `gitea` | `owner/repo` | `QUIBBLE_TRACKER_API=https://<host>/api/v1` | Token with **`write:issue`**. |
| Jira Cloud | `jira` | project key, e.g. `IN` | `QUIBBLE_TRACKER_API=https://<site>.atlassian.net`, `QUIBBLE_ISSUE_TYPE=Task` | Atlassian API token; the value is `you@example.com:API_TOKEN`. Give that account Browse, Create issues, Transition issues, Create attachments in the project. |
| Linear | `linear` | team key, e.g. `ENG` | — | **Restricted** API key (`lin_api_…`) with Read + Write on that one team. |

Once quibble is installed (step 3.1), `quibble env --tracker <name>` prints these lines
ready for `.env.example`, with the token empty and its scope spelled out. Use that
rather than writing them by hand.

In production, prefer a **mounted secret file**: `QUIBBLE_GITHUB_TOKEN_FILE=/run/secrets/…`
instead of `QUIBBLE_GITHUB_TOKEN`. The variable is named `GITHUB` for history; it holds
the token for whichever tracker is set.

**Several repositories?** Ask whether notes should go to one place or be routed:

- A fixed list: `QUIBBLE_REPOS=org/main,org/billing` (the default stays `QUIBBLE_GITHUB_REPO`).
- Routed by page: `QUIBBLE_ROUTES=/admin/billing=org/billing,/svc=org/svc`.
- A repo the token can't reach gets its own credential:
  `QUIBBLE_REPOS=org/main,other-org/repo@OTHER_TOKEN` (the `@` names a variable, never a secret).
- On GitHub, every repo an org's token can reach: `QUIBBLE_REPOS_ORG=org`, narrowed with
  `QUIBBLE_REPOS_INCLUDE` / `QUIBBLE_REPOS_EXCLUDE`.

## 3. Install

1. **Add the dependency**, with the framework's extra:
   `quibble[fastapi]`, `quibble[django]`, `quibble[flask]`, `quibble[starlette]` or
   `quibble[litestar]`. Until quibble is on PyPI the developer's early-access instructions
   give the install line; use exactly that.
2. **Settings.** Add the output of `quibble env --tracker <name>` (plus
   `--repos org/a,org/b` for further targets) to `.env.example`. Its secrets are empty;
   tell the developer to fill in `.env` (or their secret store) themselves.
3. **Mount the endpoint** where the app is created. Print quibble's own code for the
   framework and paste it unchanged:

   ```bash
   quibble snippet --framework fastapi    # or django, flask, starlette, litestar
   quibble snippet --framework django --multi-repo   # several repositories
   ```

   It prints the imports, the mount and where they go. Don't rewrite it. On Django keep
   the `re_path(r"^quibble/?", …)` exactly: the widget posts to the bare `/quibble`, and a
   `path("quibble/", …)` mount rejects every note.
4. **Add the button.** Put the output of `render_widget()` just before `</body>` in the
   base template, so every page has it. Django: `quibble.render_widget("/quibble")`.
   In Jinja, pass it in as a variable and render it with `|safe`; in Django, mark it safe.
   **A single-page app** (React, Vue, Svelte, Next.js) whose pages aren't rendered by the
   server: install `@quibble-fyi/widget` and call `mount({ api: "/quibble" })` where the
   app's root component mounts. Call `unmount()` when it unmounts, or on routes that
   shouldn't show the widget. Don't add the script tag as well.

## 4. Sign-in and who filed it

- **Check the endpoint is behind sign-in.** `/quibble` must be reachable only by people
  who can already use the app. If the app's routes are protected by a decorator or
  middleware that the new mount skips, say so and ask how to protect it.
- **Name the reporter.** quibble reads the person from a header set by the proxy in front:
  - Cloudflare Access: nothing to set.
  - Anything else: `QUIBBLE_IDENTITY_HEADERS=X-Auth-Request-Email,X-Forwarded-User`
    (the headers that proxy actually sends, first match wins).
  - No proxy: offer `QUIBBLE_ASK_EMAIL=on` (and `QUIBBLE_ASK_NAME=on`). Tell the developer
    those notes are marked **self-declared**, and that anonymous submitters share one
    rate limit.

## 5. Check it works

1. Run **`quibble doctor`** with the app's environment. Fix every ✗ before going on.
   `quibble doctor --fix` creates the labels quibble needs. Add `--json` to read the
   result without parsing the text.
2. Start the app and run **`quibble doctor --url <the app's address>`**. It checks that
   quibble is mounted, that it refuses a caller who hasn't signed in, and that the button
   is on the page. If the developer gives you a signed-in header (a session cookie), add
   `--header 'Cookie: …'`; never ask for a password, and never print the header.
3. File one test note: **`quibble send-test`** prints the new issue's link. (Or sign in,
   open any page and file *"quibble install test — safe to close"* from the button.)
4. Confirm the issue appeared in the tracker, then close it (`quibble send-test --close`
   does both).
5. Tell the developer how to switch quibble off without removing it:
   `QUIBBLE_ENABLED=off` mounts no routes at all.

If it doesn't work:
- **A note files as "anonymous":** the identity header isn't reaching the app; recheck
  step 4 against the headers the proxy really sends.
- **`CERTIFICATE_VERIFY_FAILED`:** this Python has no trusted certificates (common with
  python.org installers on macOS). Run its "Install Certificates" command, or set
  `SSL_CERT_FILE` to a CA bundle.
- **Django: every note is rejected (404):** the mount isn't `re_path(r"^quibble/?", …)`.

## 6. Hand over

Summarise in a short list: the mode used, the tracker and target, every file changed, the
settings they still have to fill in, and the doctor result. Mention that their coding agent
can read and act on quibbles through the tracker's own tools (see `docs/ai-agents.md`).

## Any other stack

quibble's server side is Python. For an app in any other language, run it beside the app
with **`quibble serve`**, but only on one machine: a desktop app, or local development.
`quibble serve` listens on loopback only, and it isn't for a server on a network.

1. Install quibble where the app runs (step 3.1, with no framework extra) and add the
   step 2 settings to `.env.example`, secrets empty, as before.
2. Add `QUIBBLE_ALLOWED_ORIGINS` set to the exact origin of the page that shows the
   widget: the dev server's (`http://localhost:5173`) or the webview's
   (`tauri://localhost`). Never `*`.
3. Have the app start `quibble serve --port 8787`. Generate a random
   `QUIBBLE_SERVE_KEY` for each launch, pass it to `serve` in its environment, and give it
   to the page as `window.QUIBBLE_KEY`. Without the key, any program on the machine can
   file notes.
4. Load the widget in the page:
   `<script src="http://127.0.0.1:8787/quibble.js"></script>`.
5. For a desktop app with one user, set `QUIBBLE_REPORTER` to that user. Otherwise offer
   `QUIBBLE_ASK_EMAIL=on`.
6. Check it as in step 5: run `quibble doctor`, then file one test note.

For a server on a network in any other language, explain that quibble's server side
needs a Python host or the
[backend contract](https://github.com/quibble-fyi/Quibble/blob/main/docs/BACKEND-CONTRACT.md),
and stop. Don't write an unofficial adapter.
