---
name: setup
description: Use when the user installs organic-os, says "set up organic-os", "onboard my site", "connect my website", "add another website", "switch site", "organic-os status", or runs /organic-os:setup or /organic-os:sites. Audits the site URL and proposes a pre-filled profile before asking, scaffolds the per-site brain repo, runs the connector wizard with live verification, records credentials one at a time, registers routines for the chosen runtime, ends with a tested postflight scorecard, and manages the multi-site registry (add / update / switch / status).
---

# organic-os setup

You are onboarding or managing sites in organic-os. Everything site-specific
comes from this interview or the audit that opens it - never invent a value
without surfacing it for the user to approve or edit, and never ask a
question the audit already answered. One question at a time, AskUserQuestion
with options where possible.

Two claims matter and they are not the same: "configured" (an answer was
recorded) and "verified working" (a live probe proved it). Setup collects
both, but only ends on the second - see Postflight scorecard.

## Interview style (every mode, every question)

- **One question at a time.** Never present a wall of questions. Ask, wait
  for the answer, then ask the next one.
- **Offer a default with every question.** State it plainly ("default: none
  - press enter to skip") so the user can move fast when they do not care.
- **Show progress.** Prefix each question with where the user is - e.g.
  "question 3 of roughly 9" for the full setup interview (count whatever
  this run will actually ask: reviewing/editing the proposed profile,
  operator knowledge, Google Ads, WordPress, approval channel, runtime,
  brain path, brain mode - the connector wizard runs as its own probe-and-
  verify flow and is not counted in this total), "question 2 of 3" for
  quick-start, "question 1 of 2" for a targeted update-mode re-ask.
- **End with a summary table.** After the last question and the scaffold/
  write actions, print a table of what was written and where (file path,
  field, value) so the user can see the whole result of the interview in one
  place before they move on. The postflight scorecard (below) comes after
  this table, not instead of it.

## Step 0: environment checks

- Confirm `python3` is on PATH.
- Confirm PyYAML is importable: `python3 -c "import yaml"`. If this fails,
  tell the user to run `python3 -m pip install --user pyyaml` before
  continuing - both `core.contracts` and `core.registry` require it.

## Step 0.5: where am I, where will routines run

Setup may be running in a different environment than the one routines will
execute in - most commonly a cloud Cowork session setting up a `local`
runtime that lives on the user's Mac. Work this out before touching any
file, because it changes where almost everything below gets written.

1. **Determine the setup environment.** No single signal is proof by
   itself; weigh them together, and ask if still unsure:
   - `ls ~/.config/organic-os 2>/dev/null` and `test -f ~/.claude.json` -
     a fresh cloud sandbox rarely carries config from a prior local run.
   - `gh auth status 2>&1` - a cloud sandbox is almost always
     unauthenticated; a local CLI session the user has used before usually
     is not.
   - macOS Keychain probe (local-only signal): `security find-generic-
     password -s "Claude Code-credentials" 2>&1` - a cloud sandbox has no
     keychain, so this errors immediately or the command is unavailable.
   - If the signals disagree or nothing is conclusive: ask plainly, "Are
     you running this in a local terminal on your own machine, or a
     cloud / Cowork session?"
2. **Ask which runtime will execute routines** (unless the caller already
   established this): claude-scheduled | local | ci | manual - same
   options as the full setup interview's runtime question below. If
   answered here, do not re-ask it later; carry it forward.
3. **If setup environment and runtime location match** (e.g. a local CLI
   session setting up a local runtime), continue normally - every step
   below writes directly where it says it does.
4. **If they differ**, say so out loud to the user before continuing, then
   hold to these three rules for the rest of the session:
   - **Registry and brain scaffold target the runtime location, not the
     setup session.** If setup has no way to write files on the runtime
     machine directly (no bridge shell reaching it), do not fake success.
     Emit a ready-to-run snippet - one bash block covering
     `init_site_repo.py`, `core.registry.register(...)`, and `git init` -
     for the user to paste into a terminal on the runtime machine. Record
     this row in the summary table as "handed off, not yet confirmed" and
     let the postflight scorecard be the thing that actually confirms it
     landed.
   - **Connector probes are labeled with the context that actually ran
     them.** Every `core.contracts.record_connector(...)` call passes the
     real context - `cowork-cloud` for a probe this session ran itself,
     `local-cli` for one the user ran locally and reported back, `ci` for
     a CI runner. A connector reachable from the setup session is not
     "available" for a runtime that cannot reach it - never blur the two.
     Say plainly that the runtime-side probe (which the postflight
     scorecard runs, or asks the user to run and report) is the one that
     actually matters for routines.
   - **Never run git through a device bridge.** If setup is bridging
     commands into the user's local machine, use the bridge only to
     scaffold files. `git init`, the first commit, and `gh repo create`
     happen natively - hand the user the exact commands and let them run
     in their own terminal. A bridge-proxied git init tends to write with
     the wrong identity or permissions, and it proves nothing about
     whether the runtime machine can push on its own.

## Step 1: read the registry, pick a mode

Read `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -c "..."` calling
`core.registry.load()` (default path `~/.config/organic-os/sites.yaml`,
resolved at the runtime location per Step 0.5 when it differs from the
setup session).

- **Registry empty** (no sites): if the caller (e.g. `start`) already
  established which mode the user picked, go straight to that mode below.
  Otherwise ask first, AskUserQuestion with options:
  - `Quick start (URL, then 2 more questions, ~2 minutes)` - go to the
    quick-start interview below.
  - `Full setup (audit the site, then review the proposal)` - go to the
    full setup interview below.
- **Sites exist**: ask the user what they want, AskUserQuestion with options:
  - `update <active site name>` - refresh the currently active site's profile
  - `add another website` - onboard a new site (full setup interview, own
    brain path)
  - `switch active site` - change which site routines/commands act on
  - `show status` - print the registry (all sites, which is active) plus the
    active site's site-profile.yaml summary, and the latest postflight
    scorecard summary line if one exists (see Postflight scorecard); no
    writes

### Update mode

1. READ the existing `site-profile.yaml` for the active site first. Present
   the current values back to the user.
2. Schema check: run `core.contracts.check_schema(brain_path)`. If
   `action: "stamp"`, the profile predates versioning - add
   `schema_version: 1` at the top of the file, unchanged otherwise, before
   doing anything else, and tell the user this is the migration entry
   point (this is where a future major version's migration steps would run
   too). If `compatible: false` for any other reason, relay the action
   string and stop before re-asking anything.
3. Re-ask **only** the sections the user picks (site, brand, audience,
   keywords, competitors, connectors, Google Ads, WordPress, approval
   channel, runtime). Do not re-run the full setup interview. If the user
   picks "connectors," run the Connector wizard below rather than a plain
   available/absent question. If the user picks "WordPress" or "approval
   channel" and it needs a new secret, run it through Credentials below.
   Update mode re-asks these sections directly - it does not re-run the
   audit-and-propose flow, since a returning user already has a profile to
   edit from.
4. Rewrite `site-profile.yaml` with just those changes.
5. Skillbook: NEVER re-append an operator note unless its text is new - read
   `skillbook.md` first, skip anything that already matches an existing
   entry's text.
6. NEVER touch `signals/`, `decisions/`, `reflections/`, or existing
   skillbook entries beyond the dedup check above - those are memory, not
   config, and setup does not rewrite memory.
7. Do not re-register routines unless the user explicitly asks to change
   cadence or runtime.
8. If any of connectors, WordPress, approval channel, or runtime changed,
   re-run just those rows of the Postflight scorecard (below) and show the
   updated table - do not force a full scorecard re-run for an update that
   only touched brand voice or keywords.
9. **Rule of thumb to state to the user: config is editable, memory is not.**
   `site-profile.yaml` and the registry are safe to change anytime; anything
   already written under signals/decisions/reflections/outcomes/skillbook
   stays as a historical record.

### Add mode

Run the full setup interview below with a fresh brain path (never reuse
another site's brain). After scaffolding, call `core.registry.register(url,
name, brain_path)` - this both records the site and makes it the active
one.

### Switch mode

Ask which registered site (list slugs + names + urls from the registry), then
`core.registry.set_active(slug)`. No file other than the registry changes.

### Show status

Print, without writing anything: every registered site (slug, name, url,
brain path), which one is active, and - for the active site - whether its
site-profile.yaml, skillbook.md, and approvals/queue.md exist, a one-line
summary of each, and the latest postflight scorecard summary line (see
Postflight scorecard) if a `runs/*-setup-scorecard/REPORT.md` exists.

## URL first, audit before asking (shared by quick-start and full setup)

Both modes below start the same way: get the URL, then let organic-os do
the looking instead of the asking. Ask only what an audit genuinely cannot
answer - this is the "audit-and-propose" model: enter a URL, get a
pre-filled profile to approve, instead of a wall of questions the plugin
could have answered itself.

1. **Ask for the site URL.** No default - this is the one thing neither
   mode can guess. If the brand name is not obviously derivable from the
   domain label (a generic domain, or one that plainly does not match the
   brand), ask for it in the same turn; otherwise guess it from the domain
   label and let the user correct it during proposal review.
2. **Audit before asking anything else.** Fetch the homepage and
   `<url>/sitemap.xml` (or whatever sitemap the homepage's `<link
   rel="sitemap">` tag or `robots.txt` points at instead).
   - **Detect the CMS**: look for `wp-content`/`wp-includes` paths, a
     `generator` meta tag, and Yoast/RankMath fingerprints - a
     `post-sitemap.xml`/`page-sitemap.xml` sitemap-index shape usually
     means Yoast, a `sitemap-pt-*` shape usually means RankMath, plus
     either plugin's characteristic HTML comments. Record what was
     detected; this seeds the WordPress question later instead of asking
     blind.
   - **Read 3-5 representative pages**: the homepage plus whatever the
     sitemap or homepage nav suggests matters most - an about/product
     page, a couple of the most prominent content pages.
   - **Propose, from what was actually read** (never invent a value - if
     the audit could not reach enough pages to support a field, leave it
     blank and say so in the proposal rather than guessing):
     - Brand voice descriptors, grounded in the actual copy (e.g. "short
       sentences," "second person," "numbers up front" - whatever the
       fetched pages actually show, not a generic default list).
     - Audience segments, from who the copy is visibly written for.
     - 5-9 seed keywords, pulled from titles, headings, and repeated
       topics across the fetched pages.
     - 3-5 content-SERP competitors: run WebSearch on the top 2-3 proposed
       keywords and take the sites that actually rank for them. State the
       distinction to the user plainly - these are sites competing for
       the same search queries, which is not the same list as business
       rivals; the user can swap in rival domains during review if that
       is what they actually want tracked.
     - Target geos: from the TLD (`.in` -> `IN`, `.co.uk` -> `GB`, a
       generic `.com`/`.io`/etc. left to the next two signals), the
       homepage's `lang` attribute, and any address/currency/phone-format
       signals visible on the fetched pages.
3. **Degradation - the site cannot be fetched** (no web access this
   session, the site blocks fetches, a timeout): say so plainly, do not
   fabricate a proposal, and fall back to asking directly for whatever the
   audit would have proposed - brand name from the domain label (step 1),
   empty keywords/competitors, TLD-only geo guess. Note in the closing
   summary that the audit did not run, and why.

## Quick-start interview (propose + accept-all + defaults, ~2 minutes)

Runs the shared audit above, then asks exactly 3 questions: URL, approval
channel, confirm. Everything else gets a stated default, not a silent one -
tell the user what was defaulted (or proposed-and-accepted) in the closing
summary table so nothing is a surprise later.

1. Site URL (+ brand name only if not derivable - see above).
2. Approval channel: in-session | telegram | slack | email | pr-merge.
   Default: in-session - no setup required, works immediately.
3. Confirm: show the proposed profile table (brand voice, audience, geos,
   keywords, competitors - whatever the audit produced, or its degraded
   fallback) and ask "does this look right?" - **Accept and continue** or
   **Switch to full setup to review row by row**. Quick-start does not
   support per-row editing; a user who wants that is, by definition,
   choosing full setup.

Defaulted or proposed-and-accepted silently (state each one in the summary
table, do not ask):

- **Brand voice, audience, geos, keywords, competitors**: whatever the
  audit proposed, accepted as-is on confirmation in question 3. This is
  new since the audit-and-propose rework - quick-start used to leave
  keywords, competitors, and voice notes empty; now it seeds them from the
  site itself. If the audit degraded (no web access, fetch blocked), the
  old empty/TLD-only defaults apply instead, and the summary says so.
- **Operator notes**: left empty - the audit cannot infer what the
  operator knows, and quick-start does not ask it. Fill in later via
  `/organic-os:setup` update mode.
- **Connectors, Google Ads, WordPress**: left `unknown`/`none`/unconnected.
  Quick-start never runs the Connector wizard and never probes connectors -
  analysis-only is the correct default outcome for a 2-minute setup, even
  when the audit detected WordPress on the site itself.
- **Runtime**: `manual`. The user runs commands themselves until they choose
  to schedule routines (`$CLAUDE_PLUGIN_ROOT/docs/routines.md`).
- **Brain path**: `~/organic-hq/<slug>`, same derivation as full setup.
- **Brain mode**: `local` (no git init, no GitHub repo offer). Quick-start
  optimizes for "see something work in two minutes," not for versioned
  memory from the first run - the user can move to a git brain later via
  update mode if they want it.

### Actions after the quick-start interview

1. Run: `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 "$CLAUDE_PLUGIN_ROOT/lib/core/init_site_repo.py" <brain-path> --url <url> --name <name>`
2. Fill `site-profile.yaml`: the audited/confirmed profile fields (brand
   voice, audience, geos, keywords, competitors, or their degraded-audit
   fallback), the answered fields (approval channel, confirm), plus every
   silently defaulted field from the list above (runtime: manual, brain
   mode: local, brain repo path).
3. Call `core.registry.register(<url>, <name>, <brain-path>)`.
4. Print the summary table (interview style, above): what the audit
   proposed, what was asked and answered, what was defaulted, and where
   each value landed in `site-profile.yaml`. Point at `/organic-os:setup`
   update mode for editing anything, and at `/organic-os:onsite-audit` as
   the first thing to try right now.
5. Run a lightweight postflight scorecard: just "brain scaffold" (the four
   files from step 1 exist) and "registry readable" (`core.registry.
   get_active()` returns this site). Every connector/WordPress/approval/
   runtime row is skipped, not shown as failing, because quick-start never
   configured them - the scorecard only tests what was actually attempted.

## Full setup interview (propose + row-by-row review, also used for "add another website")

Runs the shared audit above, then works through the proposal and whatever
it could not answer.

1. **Review the proposed profile.** Present the full table from the audit
   (site, brand voice, audience, keywords, competitors, geos, WordPress
   detection). Ask, AskUserQuestion: **Accept all** / **Edit specific
   rows** / **Answer manually instead**.
   - *Edit specific rows*: one row at a time, same interview-style rules
     as everything else here - one question, a stated default (the
     audit's proposed value), progress shown.
   - *Answer manually instead*: skip the audit's proposal entirely and
     ask each field the old way - for a site the audit could not usefully
     read, or a user who wants zero inference. Site: URL, brand name,
     sitemap URL (offer to guess `<url>/sitemap.xml` and verify with a
     fetch). Brand rulebook: voice rules, banned phrases (offer sensible
     defaults - first person, short sentences, facts before adjectives, no
     exclamation marks; user edits). Audience: segments/ICP, geographies,
     languages. Keywords: target keywords/topics (free list; can be empty
     - keyword-intel will propose). Competitors: domains (up to 5 to
     start).
2. **Operator knowledge**: "What do you already know works in this niche -
   tips, channels, formats?" Each answer becomes a skillbook entry tagged
   `evidence: anecdotal`. The audit cannot infer this - always ask it,
   regardless of how the profile above was filled in.
3. **Connectors**: run the **Connector wizard** below for GA4 and GSC (the
   heartbeat pair) first, then Notion, Slack, Canva as optional extras.
   This replaces a plain available/absent question - every connector this
   interview records has been probed, and every `verified` status has
   passed one live query, not just "the tool appeared to be there."
4. **Google Ads**: ask whether they have a developer token and which access
   level. Point to https://github.com/shalintripathi/organic-os/blob/main/plugin/docs/credentials/google-ads-token.md
   (also at $CLAUDE_PLUGIN_ROOT/docs/credentials/google-ads-token.md in a
   local checkout). If they have a developer token or client secret to
   hand, route it through **Credentials** below. Record status only.
5. **WordPress**: infra is never guessed, so this stays an explicit
   question even though the audit already looked - if the audit detected
   WordPress, say so and ask to confirm the endpoint URL + username rather
   than asking blind; otherwise ask whether they have a connected site
   another CMS runs on. Either way, the Application Password itself goes
   through **Credentials** below - it mirrors the exact wording of
   `plugin/docs/credentials/wordpress.md` step 2.
6. **Approval channel**: in-session | telegram | slack | email | pr-merge.
   For telegram: chat id here, bot token through **Credentials** below
   (same env file, key `TELEGRAM_BOT_TOKEN`). No channel is privileged;
   default in-session.
7. **Runtime for routines**: claude-scheduled | local | ci | manual - skip
   this question if Step 0.5 already answered it; otherwise ask now and
   carry the answer into Step 0.5's rules for the rest of setup. Explain
   costs honestly: claude-scheduled and local run on the user's Claude
   subscription; ci uses an API key billed per token.
8. **Where should the brain live?** Default `~/organic-hq/<slug>` **on the
   runtime machine** (per Step 0.5 - if setup and runtime differ, this
   path is not on the machine setup is currently running in), where
   `<slug>` is derived the same way the registry derives it (host minus
   `www.`, dots to hyphens - e.g. `example.com` -> `example-com`). Offer
   to change the path. After the answer, run `PYTHONPATH="$CLAUDE_PLUGIN_
   ROOT/lib" python3 -c "..."` calling `core.registry.path_warnings(<brain-
   path>, <runtime from question 7>)` - on the runtime machine if setup
   can reach it directly, or as a line inside the ready-to-run snippet
   (Step 0.5) with instructions to run it before scaffolding if setup
   cannot. If it returns any warnings, show them to the user verbatim and
   re-ask the question, with the default now switched to
   `~/organic-hq/<slug>`. Do not scaffold anything at a path that still has
   open warnings without the user explicitly confirming they want to
   proceed anyway.
9. **Brain mode**: git repo (recommended; needed for claude-scheduled and
   ci runtimes and for versioned memory) or local folder. If git and Step
   0.5 flagged a setup/runtime mismatch, `git init` and the first commit
   happen natively on the runtime machine (see Step 0.5's git rule) - do
   not run them through a bridge.

The AI-visibility baseline offer (below) and the Postflight scorecard
(below) close out full setup - existing v3 machinery, unchanged by this
rework beyond running after a proposal instead of after a plain question
list.

## Connector wizard (GA4, GSC, then Notion/Slack/Canva)

Replaces a passive "is this available" question with a probe-then-verify
flow. Run this for GA4 and GSC first - call them the heartbeat pair,
because they are the primary data source the rest of the plugin depends on
- then, only if the user wants to continue, for Notion, Slack, and Canva.

For each connector, in this order:

1. **Probe** reachability in the current context: try listing the
   connector's tools (GA4/GSC/Notion/Slack/Canva - whatever surface this
   session actually exposes). Note which context this probe ran in
   (`local-cli`, `cowork-cloud`, `ci`) - it is passed to `record_connector`
   either way.
2. **If reachable, run exactly one live verification query before
   recording anything as verified:**
   - GSC: list sites.
   - GA4: pull a 7-day sessions count.
   - Notion: search or list one workspace/database.
   - Slack: list channels.
   - Canva: list designs or brand kits.
   A live call, not just tool presence, is what earns `verified` - a
   connector can appear installed but be unauthorized or pointed at the
   wrong property, and only a real call catches that. Only after the live
   call succeeds: `core.contracts.record_connector(profile_path, name,
   "verified", context)`. If the live call fails even though the
   connector looked reachable, treat it as absent and continue to step 3 -
   never record `verified` on a failed live call.
3. **If absent (or the live call failed):** present the guided connect for
   the user's actual surface:
   - claude.ai / Cowork: Settings, then Connectors.
   - Claude Code: `/mcp`, or `claude mcp add <server>` on the command line.
   Then offer, AskUserQuestion:
   - **Wait, connect it now** - pause, let the user connect, then re-probe
     from step 1 once they confirm.
   - **Skip for now** - `core.contracts.record_connector(profile_path,
     name, "declined", context)`, plus one honest line about what
     degrades, pulled from the matching row of `plugin/docs/
     connectors.md`'s capability table.

**GSC/GA4 get a stronger framing than the optional three.** Before offering
to skip either one, say plainly: "organic-os without GSC/GA4 still runs,
but `hoo-daily` will log no-data signals with nothing to act on until one
of these connects. Of everything in this interview, this is the single
connector most worth stopping to fix now." Still respect a "skip for now"
answer if that is what the user wants - never force a connection, just
make the tradeoff explicit before they choose.

**IndexNow (offered last, no account needed).** After the connectors
above, offer instant URL submission to Bing, Yandex, and the other
IndexNow-participating engines. Same verify-not-record bar as everything
else here:

1. Generate a key: `hoo.indexnow.gen_key()` (32-char hex). Show it - it
   is an ownership proof, not a secret.
2. Instruct placing `<key>.txt` at the site root, containing exactly the
   key (`hoo.indexnow.key_file_content(key)`): via the host's file
   manager or SFTP into the web root, or - on WordPress, where the media
   library cannot write to the root - a root-file plugin or the same
   file-manager route. Offer "I'll place it now, then verify" or "skip
   for now".
3. Verify by fetching `https://<host>/<key>.txt` and comparing the body
   to the key. Only a matching fetch earns enablement - then write
   `indexnow: {enabled: true, key: <key>}` into site-profile.yaml
   (additive key, schema stays 1). A failed or skipped fetch records
   nothing and leaves IndexNow off; say what degrades - applied and
   published changes wait to be crawled naturally instead of being
   submitted on ship.

## Credentials: one secret at a time

Applies to every secret this interview or an update touches - the
WordPress Application Password, the Telegram bot token, the Google Ads
OAuth client secret.

- **One secret per question.** Never present a wall of env-file fields at
  once.
- **Name it precisely** - the exact field the user is looking at in the
  exact UI, so they never go hunting. Mirror the wording already proven in
  `plugin/docs/credentials/wordpress.md`: "In wp-admin, go to Users ->
  Profile ... -> Application Passwords, name the new password ..., click
  Add New Application Password. WordPress shows the password once; copy it
  immediately." Do the equivalent lookup before asking for any other
  secret - BotFather's one-time token print for Telegram, the developer
  token in the Google Ads API Center - rather than sending the user off to
  find the field themselves.
- **Always offer the paste-into-terminal alternative**, even when setup
  could technically run the write itself: give the exact command sequence
  and let the user run it in their own terminal.
  ```
  mkdir -p ~/.config/organic-os && read -s -p "App password: " P && printf 'WP_APP_PASSWORD=%s\n' "$P" > ~/.config/organic-os/<site-slug>.env && chmod 600 ~/.config/organic-os/<site-slug>.env
  ```
  Same pattern for `TELEGRAM_BOT_TOKEN` or any Google Ads secret - one
  `read -s` / `printf` / `chmod 600` line, one key.
- **Setup never needs to see the raw value.** It verifies the credential
  worked by probing - the Connector wizard's live check for connectors, a
  WordPress REST call (`wp-json/wp/v2/users/me`) for WordPress, a Telegram
  `getMe` call for the bot token - inside the Postflight scorecard, not by
  asking the user to paste the secret into the transcript.
- **Never echo a secret into the transcript**, regardless of which path
  the user picks.

## AI-visibility baseline (optional, ~5 minutes)

Offered once, after the interview's connectors and credentials steps and
before the Postflight scorecard - never required, always skippable. Full
setup only; quick-start does not collect the keywords/competitors this
step needs, so it is not offered there.

1. Ask, AskUserQuestion: "Want a one-time AI-visibility baseline - where
   you show up in AI answers today versus up to two competitors? About 5
   minutes, reuses the keywords and competitors already on file." Options:
   **Run it now** / **Skip - I'll run /organic-os:citations later**.
2. Skipped, or no keywords configured yet: note "AI-visibility baseline:
   skipped" (or "deferred - no keywords yet") in the closing summary table
   and stop here. Do not write a report or a signal.
3. Build the query set from `keywords.targets` (cap 8 - first 8 in file
   order if more are configured) and the competitor set from `competitors`
   (cap 2 - first 2 in file order).
4. For each query, check whichever AI answer surfaces are actually
   reachable from this session with WebSearch/WebFetch. This is a sample,
   not a census - record honestly which engines this session could
   actually reach, and never let the report imply broader coverage than
   that. Record per query: does the site's own brand/domain appear in the
   answer, does each tracked competitor appear, and who is actually cited
   (the source the answer points to, not just anything mentioned in
   passing).
5. Write `runs/<UTCdate>-ai-baseline/REPORT.md`:
   - A per-query table: query | brand mentioned? | competitor(s)
     mentioned | cited source(s).
   - Three summary numbers, each labeled with the sampling caveat inline:
     brand mention rate (queries where the brand appeared / queries
     checked), competitor mention rate (same, for the tracked
     competitors), share-of-voice ratio (brand mentions / (brand mentions
     + competitor mentions), or "n/a - no competitor mentions this run" if
     that denominator is zero).
   - One explicit line naming which engines/surfaces this session could
     actually reach - this method samples, it does not measure every
     engine.
6. Append one signal via `append_signal`: that the baseline now exists,
   its three headline numbers, and the falsifiability line - "re-run
   monthly via /organic-os:citations; if the mention rate has not moved
   within 90 days of shipped content work, that is evidence the content
   strategy hypothesis is wrong, not that the baseline was wrong."
7. **Degradation - no web access this session:** if WebSearch/WebFetch are
   unavailable, or every query fails to reach any engine, do not write a
   partial or fabricated report. Append a signal line "AI-visibility
   baseline: deferred - no web access in this session" instead, and tell
   the user to run `/organic-os:citations` later once a session with web
   access is available.

## Postflight scorecard (mandatory final step)

Setup does not claim success on its own - the scorecard does. Run every
check below that applies to what this session actually configured (skip
rows that are structurally not applicable, e.g. no WordPress row when
there is no WordPress connection at all), build a `checks` list of
`{"name", "status": "pass"|"degraded"|"fail", "detail", "fix"}`, call
`core.contracts.write_scorecard(brain_path, checks)`, and print the
resulting table to the user with any Fixes section intact.

Checks, in order:

1. **Brain scaffold** - pass if `site-profile.yaml`, `skillbook.md`, and
   `approvals/queue.md` exist at the brain path. Fix on fail: re-run
   `init_site_repo.py`.
2. **Git push** (brain mode: git only) - pass if `git log -1` in the brain
   path shows a commit, and, when a remote is configured, the push
   reached it. Detail: commit hash + branch. Fix: the exact `git remote
   add` / `git push -u origin main` command, or `gh repo create <name>
   --private --source=. --push` run from inside the brain path.
3. **Registry readable in runtime context** - pass if `core.registry.
   get_active()`, run from the runtime location, returns this site as
   active. If Step 0.5 flagged a setup/runtime mismatch and setup handed
   off a snippet instead of writing directly, this row is `degraded` with
   detail "handed off via snippet, not yet independently confirmed" and
   fix "run the Step 0.5 snippet, then `PYTHONPATH=... python3 -c
   \"from core import registry; print(registry.get_active())\"` on the
   runtime machine."
4. **WordPress REST** (only if WordPress connected) - pass if `curl -u
   '<user>:<app-password>' '<endpoint>/wp/v2/users/me?context=edit'`
   returns 200 JSON for the configured user. The detail column names the
   role detected from the response's `roles` field and what it cannot do,
   per the capability matrix in `plugin/docs/credentials/wordpress.md` -
   e.g. "role: editor - can write posts and SEO meta; cannot purge
   SEO-plugin caches or change plugin settings, those steps will be
   reported pending-human and end the proposal partially-applied". Fix on
   fail: recheck `plugin/docs/credentials/wordpress.md` step 2 (recreate
   the Application Password) or step 4 (bridge plugin not active).
5. **Approval channel test delivery** - for telegram: send a real test
   message ("organic-os setup test - reply not required") to the
   configured chat id and confirm the API call returned ok. State the
   prerequisite explicitly in the detail column either way: the first
   message to the bot has to come from the user first (open a chat, send
   anything) before `sendMessage` can deliver - Telegram rejects a
   bot-initiated first contact. Fix on fail: exactly that line. For
   in-session or pr-merge: pass automatically, detail "nothing to test -
   works by definition" / "confirmed on the first PR." For slack/email:
   the lightest live equivalent available in this session, same pass/fail
   logic as telegram.
6. **Each connector's live probe result** - one row per connector touched
   in the Connector wizard, carrying forward its recorded status
   (verified -> pass, declined -> degraded, unavailable -> fail) and
   context. Degraded/fail rows repeat the guided-connect instructions as
   the fix.
7. **Headless auth + model resolution** (runtime local or ci only; skip
   with detail "not applicable - runtime is manual/claude-scheduled"
   otherwise). For local runtime: have the user run `claude -p "ping"`
   through the wrapper (or a one-off `claude -p "ping" --permission-mode
   bypassPermissions`) and report the result.
   - Success: pass.
   - `Invalid API key - Please run /login`: fail, fix "run `claude
     setup-token`, add the result to the site env file as
     `CLAUDE_CODE_OAUTH_TOKEN`" (per `plugin/docs/routines.md`'s "Hard
     requirement: claude setup-token").
   - A 404 on the model: fail, fix "see the Model-404 recovery box in
     `plugin/docs/routines.md` - list models via curl, pin
     `ANTHROPIC_MODEL`/`ANTHROPIC_SMALL_FAST_MODEL`."
8. **One real scheduled run** (local runtime only) - kick the actual
   scheduler (`launchctl kickstart -k gui/$(id -u)/com.organic-os.daily`,
   or the systemd/cron equivalent already registered) and confirm via
   `git log -1 --format='%h %s'` in the brain repo that a fresh commit
   landed with today's date. Detail: the commit hash - this is the same
   proof `plugin/docs/routines.md`'s "Verify by commit hash" step already
   prescribes; pull that hash into the scorecard rather than re-describing
   it. Fix on fail: check `~/.config/organic-os/routine-daily.log` and
   `plugin/runtime/README.md`.

Close with one line: "<n> of <total> checks passed; <m> degraded, with the
fix for each above." Never say "setup complete" or "you're all set" unless
every non-skipped row is `pass` - a degraded or failed row is a next step
to hand to the user, not a caveat to bury.

## Rules

- Analysis-only mode is a valid outcome: a user with zero credentials still gets
  audits, briefs, and keyword work from public data.
- Never edit brain frontmatter directly. The contract CLI
  (`PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -m core ...`) is the only
  write path for status and approvals - setup itself never touches item
  status, and never teaches the user to hand-edit an item file either.
- Never write a secret into the brain repo, the registry, or the transcript. Env files only.
- Re-running setup is safe: the scaffolder never overwrites; the interview
  offers current values as defaults; update mode never touches memory.
- The registry (`~/.config/organic-os/sites.yaml`) is local operator state,
  not part of any brain repo - it is never committed to a site's git history.
- "Configured" and "verified working" are different claims. A connector is
  `verified` only after a live probe in the Connector wizard; a runtime is
  proven only after the postflight scorecard's headless-auth and (for
  local) scheduled-run checks pass. Never state either claim without the
  check behind it.
- Never run git through a device bridge (Step 0.5) - scaffold through the
  bridge, then let the user run git natively in the runtime environment.
