[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M754W68V0YR8SVMPS8DY16DX":3,"skill-same:01M754W68V0YR8SVMPS8DY16DX":46},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":6,"ownerHtmlUrl":29,"ownerBlogUrl":30,"ownerDescription":6,"updatedAt":31,"install":32,"resources":45},"01M754W68V0YR8SVMPS8DY16DX","deploy-agents",null,"Set up Claire, Stu, Janice and Alan on this Mac when someone asks you to deploy, install, or set up the agents — or asks for help joining a client, fixing a half-finished install, or updating an existing one. Walks the operator through supplying their values, installs the nest, opens the agent drafts for approval, and proves the client-data fence before any data moves. Use this instead of reading INSTALL.md aloud.","borderux\u002Frecursica-knowledge","borderux","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F14911137?v=4","01M754SG94XX2Y7176KM2WGZG3","Border LLC","01M754SGA9E7YSNK6NWVMKMHYC",1,"MIT","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fnest\u002F.claude\u002Fskills\u002Fdeploy-agents\u002FSKILL.md",[],[],"2026-11-12 13:55:43",false,"---\nname: deploy-agents\ndescription: Set up Claire, Stu, Janice and Alan on this Mac when someone asks you to deploy, install, or set up the agents — or asks for help joining a client, fixing a half-finished install, or updating an existing one. Walks the operator through supplying their values, installs the nest, opens the agent drafts for approval, and proves the client-data fence before any data moves. Use this instead of reading INSTALL.md aloud.\n---\n\n# Deploy the agents onto this Mac\n\nSomeone has asked you to set up the agents. You are doing this **on the machine you are\nrunning on**, for the person you are talking to. You cannot install anything on anybody\nelse's Mac — if the person asking is not the owner of this machine, say so and stop.\n\n## What you are actually installing\n\n| Half | What | Result if missing |\n|---|---|---|\n| **The nest** — `nest\u002F` → `~\u002F.buzz\u002F` | Scripts, fenced MCP servers, guides, `settings.json` | Agents start, then find no tools |\n| **The agents** — `buzz-agents\u002F` → Buzz Desktop | Prompts and settings for Claire, Stu, Janice, Alan | Nothing to talk to |\n\nBoth, in that order. An agent created before the nest exists is a name with nothing behind it.\n\n## Before you touch anything: which case is this?\n\nAsk, and do not guess — the answer changes half the steps:\n\n1. **Joining a client that already exists.** Someone else already created the dataset,\n   the service account and the Drive folder. Most common. You need their values, not\n   `gcloud`. You still run every step, Step 5 included — the cloud side is done, the\n   *machine* side has not been done on this Mac.\n2. **Creating a new client from scratch.** Needs Google Cloud work and `gcloud`.\n3. **Repairing or updating an existing install.** A nest already exists here. The agents\n   exist too, so Step 4 is the wrong tool — see *Updating agents that already exist*.\n\nCheck which by looking, before you ask twice:\n\n```bash\nls ~\u002F.buzz\u002Fbin\u002Ftoolbox ~\u002F.buzz\u002F.claude\u002Fsettings.json 2>\u002Fdev\u002Fnull\nls ~\u002F.buzz\u002F.secrets\u002F 2>\u002Fdev\u002Fnull\n```\n\n## Step 1 — Find the checkout\n\nThe scripts live in a clone of `borderux\u002Frecursica-knowledge`. Find it rather than\nassuming a path:\n\n```bash\nls ~\u002F.buzz\u002FREPOS\u002F 2>\u002Fdev\u002Fnull\n```\n\nNo clone yet — the normal case on a clean Buzz install. Clone it yourself; do not ask them\nto do it in a terminal:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge.git ~\u002F.buzz\u002FREPOS\u002Frecursica-knowledge\n```\n\n`main` is correct — do not check out a branch. Everything below runs from that directory.\n\n## Step 2 — Collect their values\n\nAsk for all of them in **one** message, not one question at a time. Ten round trips to\nfill in a form is a worse experience than the README this skill replaces.\n\nWhat to ask for, and be precise that most of these are **not** secrets — they are\nidentifiers, and they are the same for everyone in the community:\n\n- `BQ_PROJECT` — the Google Cloud project id\n- `DRIVE_FOLDER` — the client's Drive folder id\n- `TAG_SHEET_ID` — the shared tag dictionary sheet id\n- `CLAIRE_CHANNEL`, `STU_CHANNEL`, `IVAN_CHANNEL`, `ALAN_CHANNEL`, `JANICE_CHANNEL` — channel UUIDs\n- `JANICE_PUBKEY` — this community's Janice, hex\n- `KNOWLEDGE_REPO_NAME` — only if they want Alan\n- **their name**, for the agent display names — see below. Ask for the form they want\n  shown, not their account id\n\n`buzz-agents\u002Fplaceholders.json` says where each one is found; quote from it rather than\ninventing instructions. Leave `TRANSCRIPT_DIR` blank — bootstrap derives it.\n\n### The canvas of the channel you were asked in is the source of these values\n\n**This is the normal path — start here, not with questions.** Deployment is meant to be run\n*in the channel the agents are being deployed to*, and that channel's canvas holds the\nconfiguration. So the channel you were @mentioned in is almost always the deployment\nchannel, and you can read most of the list above straight out of it:\n\n```bash\nbuzz canvas get --channel \u003Cuuid>          # the channel you are talking in\n```\n\nA `## Claire config` block gives you `slug`, `drive_folder`, `dataset` and the project — the\nsame block Claire reads at runtime, so it is the authoritative copy rather than a\ntranscription of it. Take `BQ_PROJECT` and the slug from there.\n\nOnly ask for what the canvas genuinely does not carry, and say which values those are and\nthat you looked. If the canvas has no `## Claire config` block at all, confirm you are in\nthe right channel before falling back to asking for everything.\n\n#### Canvas keys → values\n\n| Canvas key | Becomes | Notes |\n|---|---|---|\n| `slug` | *not a token* — a `--slug` argument | Lowercase, hyphens. Names the dataset, the key file, the MCP servers |\n| `sa_slug` | *not a token* — a `--sa-slug` argument | Optional. The service account's name when it is not the slug. Lowercase, hyphens |\n| `bq_project` | `BQ_PROJECT` | |\n| `bq_dataset` | *nothing* | Confirmation only. It must equal `research_\u003Cslug>` with hyphens as underscores |\n| `drive_folder` | `DRIVE_FOLDER` | |\n| `tag_sheet` | `TAG_SHEET_ID` | |\n| `claire_channel`, `stu_channel`, `alan_channel`, `janice_channel` | the four `*_CHANNEL` tokens | Janice's routing table, **not** the channel you are installing in |\n| `knowledge_repo` | `KNOWLEDGE_REPO_NAME` | Alan only |\n\nThose four routing UUIDs are a common trap: they are where Janice files findings, and none of\nthem is the channel this install is running in. Do not point `--channel` at a `building-*`\nchannel because a canvas key named it.\n\n`bq_dataset` is a cross-check rather than an input. If it disagrees with `research_\u003Cslug>`,\nstop and ask which is right instead of picking one — the wrong dataset name is a fence that\nfails open.\n\n`sa_slug` exists because **a Google service account cannot be renamed.** An account created\nbefore the slug settled keeps its original name for the life of the client, so a channel on\nslug `acme` can legitimately be served by `claire-acme-health-service-user`. Set `sa_slug` to\nthe account's own name and leave `slug` alone; the key then lives at\n`~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Csa_slug>-service-user.json` and `bin\u002Fstu` takes `--sa-slug`.\n\nOnly add it when the two genuinely differ — omitted, everything derives from `slug`, which is\nthe normal case. Do not \"fix\" a mismatch by renaming the account in the console: you cannot,\nand creating a replacement means re-granting the dataset and re-sharing the Drive folder.\n\n### The community note is where these belong now\n\n`~\u002F.buzz\u002Fbin\u002Fclient-config.mjs resolve --channel \u003Cuuid>` answers this whole section in one\ncommand, reading the community and the channel together and refusing rather than guessing.\nUse it before asking anybody anything.\n\nA client's details are recorded once for the whole community, so a **second** channel for a\nclient that already has one needs a single line naming the client, not the block above:\n\n```markdown\n## Claire config\n- client: \u003Cslug>\n```\n\n`client-config.mjs publish --from-channel \u003Cuuid>` promotes an existing channel's block to the\ncommunity; without `--write` it prints what it would publish. Say out loud before writing it\nthat a note is readable by everyone in the community where a canvas is readable by that\nchannel's members — it carries names and ids only, never a key, but that is a wider audience.\n\n**The channel line is not optional and must not be \"simplified\" away.** A channel that names\nno client is unconfigured, and that is the fence doing its job: most channels in a community\nare not client channels, and moving everything to the community would quietly configure all\nof them for that client.\n\n**If a value is missing, the fix is to add it to the community note** — not to\ncollect it in chat and move on. The next operator hits the identical gap otherwise.\n`TAG_SHEET_ID` is the one most often absent and it is not derivable; ask the owner for it,\nthen **write it back**. Show them the block before you set it: `buzz canvas set` replaces the\nwhole document, so it has to carry what was already there.\n\n```markdown\n## Claire config\n\n- slug: acme\n- bq_project: acme-research-123456\n- bq_dataset: research_acme\n- drive_folder: 1AbCdEfGhIjKlMnOpQrStUvWxYz_\n- tag_sheet: 1AbCdEfGhIjKlMnOpQrStUvWxYz0123456789\n\n## Janice routing\n\n- claire_channel: 00000000-0000-0000-0000-000000000000\n- stu_channel: 00000000-0000-0000-0000-000000000000\n- alan_channel: 00000000-0000-0000-0000-000000000000\n- janice_channel: 00000000-0000-0000-0000-000000000000\n\n## Alan — omit this section if the community has no Alan\n\n- knowledge_repo: recursica-knowledge\n```\n\nA canvas is channel-visible, so it holds identifiers only. Never put the service-account key\non it — and it needs no key path either, because the account's address is derivable from\n`slug` and `bq_project` alone.\n\n### You can only see channels you are a member of\n\n`buzz channels list` shows **open** channels plus the **private** ones your own identity\nbelongs to. A private channel you are not in is not listed, not searchable, and its UUID is\nnot resolvable from its name. There is no error — it is simply absent, which reads like a\ntypo or a deleted channel.\n\nSo if they name a channel you cannot find, do not conclude it does not exist. Say this\ninstead, because it is almost always the real cause:\n\n> I cannot see `#\u003Cname>`. Private channels are invisible to agents that are not members —\n> add **me** (your own Fizz, not the owner's) to it, then send me the UUID.\n\nBeing a member yourself is what matters. The owner already being in the channel does not\nhelp you, and neither does the person you are talking to being in it. Do not try\n`buzz channels add-member` to add yourself; you cannot resolve the UUID you would need.\n\n### `JANICE_PUBKEY` is circular in a new community — expect two bootstrap runs\n\n`buzz channels members --channel \u003Cjanice-uuid>` gives you `JANICE_PUBKEY` **only if a Janice\nalready exists in that community.** Each operator gets their *own* Janice, whose pubkey does\nnot exist until they save the draft in Step 4 — so on a first install there is nothing to\nlook up and nothing anyone can tell you.\n\nThat is expected, not a blocker. Leave `JANICE_PUBKEY` blank, note it, and:\n\n1. Bootstrap and create the agents without it. `wake-janice.sh` is the only consumer.\n2. After they save the Janice draft, get her pubkey and re-run `bootstrap-nest.mjs`.\n\nBootstrap is idempotent, so the second run rewrites only that. **Tell them this up front** —\notherwise the first run's unresolved-token failure looks like something went wrong.\n\nEverything else you can resolve yourself: channel UUIDs by name from `buzz channels list`.\nAsk only for what you could not find.\n\nThen write the file — **never** to the repo root, always to `buzz-agents\u002F`:\n\n```bash\ncp buzz-agents\u002Flocal-values.example.json buzz-agents\u002Flocal-values.json\n```\n\nFill it in and confirm it is ignored before you go any further:\n\n```bash\ngit check-ignore -v buzz-agents\u002Flocal-values.json\n```\n\nIf that prints nothing, **stop** — the file is not ignored and this repository is public.\n\n**Put only the keys the example file lists in there.** It is tempting to add the slug, or the\noperator's name, since you have both by now — do not. Every value in that file is treated as\na substring to swap for its token when prompts are exported, so a short one does damage out\nof all proportion to its usefulness: a slug like `acme` would rewrite the word `acmeism`, and\na first name would rewrite every mention of that person. The slug is a `--slug` argument and\nthe owner is `--owner`. Neither is a token.\n\n### The one real secret\n\nThe **service-account key** is the only true secret in the set, and it is a file, not a\nstring. It belongs at:\n\n```\n~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json     mode 0600\n```\n\nRules, and they are not negotiable:\n\n- `.secrets\u002F` is `0700` and lives **outside any git checkout**. Never copy a key into the\n  repo, not even briefly.\n- **Never print a key, or any part of one, into the channel.** Buzz messages are relay\n  events; you cannot unsend one. Confirm a key by its path, permissions and\n  `client_email` — never its contents.\n- Ask them to place the file themselves and tell you the path. Do not ask them to paste it\n  to you.\n\n```bash\nchmod 600 ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\nnode -e 'console.log(require(process.argv[1]).client_email)' \u003Cpath>   # identify, don't dump\n```\n\n**You already know what that `client_email` should say, so check it rather than accepting\nwhatever arrives.** Both the account and the filename follow from values you have:\n\n```\nclient_email  claire-\u003Cslug>-service-user@\u003CBQ_PROJECT>.iam.gserviceaccount.com\nfilename      ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\n```\n\nSo for slug `acme` in project `acme-research-123456` the account is\n`claire-acme-service-user@acme-research-123456.iam.gserviceaccount.com`. Derive the expected\nstring, compare it to the file's `client_email`, and say which you got.\n\nA mismatch is worth stopping for, because each of the two ways it can differ is a distinct\nproblem:\n\n| What differs | What it means |\n|---|---|\n| The **project** part | The key belongs to another client's project. Do not install it — this is precisely the cross-client leak the isolation check exists to catch. |\n| The **slug** part | Either the wrong client's key, or an account whose name simply never matched the slug. Confirm which client it is for before use, and if the name is legitimate, record it as `sa_slug` on the canvas rather than renaming anything. |\n\nNever print the key's contents, any part of a private key, or the `private_key_id`. The\n`client_email` is an identifier, not a credential, and is the only field to quote.\n\n## Step 3 — Install the nest\n\nDry run first, always. Show them what it says:\n\n```bash\nnode scripts\u002Fbootstrap-nest.mjs --check\n```\n\nThen:\n\n```bash\nnode scripts\u002Fbootstrap-nest.mjs\n```\n\nIt downloads a checksum-verified 154 MB `toolbox` binary, so it is not instant. It is\nidempotent — re-running is always safe, and is the correct response to almost any failure\nafter you have fixed the cause.\n\n**Read its output; do not just check the exit code.** Two stops are normal and each names\nits own fix:\n\n- *Unresolved tokens* — it lists every missing value and the file needing it. Add them and\n  re-run. It refuses rather than installing a script that hunts for a project literally\n  named `{{BQ_PROJECT}}`.\n- *Missing prerequisite* — it names what to install. `gcloud` missing is only a warning,\n  and only matters for case 2.\n\n## Step 4 — Create the agents\n\n`--channel` is **this** channel — the one you were asked in, whose canvas you read in Step 2.\n\n```bash\nnode buzz-agents\u002Fscripts\u002Frestore-agents.mjs --channel \u003Cthis-channel-uuid> --owner \u003Cname>          # show the commands\nnode buzz-agents\u002Fscripts\u002Frestore-agents.mjs --channel \u003Cthis-channel-uuid> --owner \u003Cname> --run    # open the drafts\n```\n\n`--owner` defaults to the first word of the checkout's `git config user.name`, and the script\nprints which name it resolved and where from before it sends anything. **Read that line out\nand confirm it** — it is what people will type to address the agent. If it cannot work out a\nname it stops rather than falling back to bare `Claire`; `--no-owner` is how you ask for the\nbare name on purpose, and it is only right for a community that will hold exactly one set of\nthese agents forever.\n\nThat creates them as **`Claire (Alex)`**, `Stu (Alex)` and so on. Everyone runs their own\nagents, so a shared channel otherwise ends up holding several bots called `Claire` with\nnothing distinguishing them — which happened, and left a real \"which of these is mine?\"\nOnly the display name carries the owner; the stored definition stays canonical, so it is\nstill one shared `agents\u002Fclaire\u002F` for everybody.\n\nDo not hand-rename an agent in Buzz Desktop to achieve this. Use `--owner`, so the name is\napplied the one way the tooling recognises.\n\nSomeone else's `Claire (…)` already in the channel is **not** a conflict and not a reason to\nreuse it — it is theirs, on their machine, spending their budget. Two owner-suffixed Claires\nin one room is the convention working. What is a real problem is two agents on *one* Mac\nanswering to the same definition; `sync-prompts.mjs` reports that as `more than one match on\nthis Mac` and names both, and it will not guess between them.\n\n**You cannot finish this step, and you must not imply that you have.** Each command opens\na prefilled draft in *their* Buzz Desktop that **they** review and save. That gate is\ndeliberate: a prompt arriving from a git branch gets human eyes before it starts answering\npeople. Tell them plainly: four drafts to open and save, by hand.\n\nWork through the `MANUAL` block it prints — parallelism, timeouts, avatar upload have no\nCLI flags.\n\nIf `buzz upload file` rejects an avatar with *media contains metadata or a non-canonical\nmetadata channel*, the PNG is carrying an `eXIf` chunk. The committed avatars no longer do.\nDo **not** try to fix it by re-encoding with `sips` — `sips` is what writes that chunk, so\nevery retry reproduces the rejection. If you meet it on some other image, strip the chunk:\n\n```bash\nnode -e 'const f=process.argv[1],fs=require(\"fs\");import(\"'\"$PWD\"'\u002Fbuzz-agents\u002Flib\u002Favatars.mjs\").then(m=>fs.writeFileSync(f,m.stripPngMetadata(fs.readFileSync(f))))' \u003Cfile.png>\n```\n\nThen tell them to **restart Buzz Desktop.** MCP servers are read once at startup; until\nthey restart, their agents have no tools and will look broken.\n\n## Updating agents that already exist\n\nCase 3 only. **Do not use Step 4 here** — `draft-create` proposes a *new* agent, so it would\noffer them a second Claire rather than update the one they have.\n\nStart by asking what is actually different, which needs no credentials and changes nothing:\n\n```bash\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs --diff     # see the change itself\n```\n\nRead the state it reports per agent, because the right action is different for each:\n\n| It says | What happened | What to do |\n|---|---|---|\n| `in sync` | Nothing to do | Say so and stop |\n| `BEHIND the branch` | They pulled; their agent is an older commit | Offer the `draft-update` it prints |\n| `has local edits not in git` | **They edited the agent in Buzz Desktop and never exported it** | Do NOT apply. Their prompt is unversioned work and applying would delete it |\n| `runs the committed prompt, unstamped` | First run since stamps existed, or a stamp got lost | Nothing. It records the version itself and reports `in sync` |\n| `prompt in sync, settings differ` | Only runtime\u002Fprovider\u002Fmodel\u002F`respond_to` moved | Offer the settings-only `draft-update` |\n| `prompt not committed yet` | A prompt in the repo has never been committed | Commit it; a version stamp is a commit sha |\n| `not installed on this Mac` | Genuinely missing | That one, and only that one, needs Step 4 |\n| `more than one match on this Mac` | Two personas here answer to one definition — e.g. a `Claire` and a `Claire (Alex)` | Nothing, until a human resolves it. It names both; ask which to keep. Do not guess — `draft-update` reports `accepted: true` for a name that hits nothing, so the wrong guess looks like a success |\n\n**Check `not installed` against Buzz Desktop before acting on it.** It is the one state whose\nwrong answer is expensive: acting on it creates a duplicate agent, and a duplicate cannot be\nmerged back. If they can see that agent in Desktop, the report is wrong, not the person — say\nso and stop rather than creating a second one.\n\nFor the local-edits case, tell them what they have and let them choose. The capture is\n`node buzz-agents\u002Fscripts\u002Fexport-agents.mjs`, then a normal PR. Do not run\n`--force-apply` on their behalf: it exists so a human can overwrite their own work\nknowingly, not so you can.\n\nTo send the drafts once they have agreed:\n\n```bash\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs --channel \u003Cuuid> --run\n```\n\nSame gate as Step 4 — each one opens a form they must save. If it reports a prompt over the\n20,000-character limit, do not trim the prompt to fit; that is a content decision for the\nowner, and `~\u002F.buzz\u002FGUIDES\u002FJANICE_REVIEW_CHECKLIST.md` covers how to move a section out to a\nguide instead.\n\n**Sending a draft does not record it.** Nothing is stamped until the owner saves the form,\nbecause a draft can be discarded. So after they tell you they have saved, run the report once\nmore — that pass sees the prompt that actually landed and records the version. Do not tell\nthem an update is complete on the strength of `accepted: true`.\n\n## Step 5 — Wire this machine up. Every case, including joining\n\n**Run this even when joining a client someone else already created.** It used to say to\nskip it, and that was wrong: past the dataset work, this script is the *only* thing that\ndoes the per-machine half of the install.\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fdeploy-claire-channel.sh --slug \u003Cslug> --channel-uuid \u003Cuuid> \\\n  --drive-folder \u003Cfolder-id> --sa-key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json --dry-run\n```\n\nRun `--dry-run` first and read it. Drop the flag to apply.\n\n| It also does | Which lives nowhere else |\n|---|---|\n| Registers `bq-\u003Cslug>`, `bq-\u003Cslug>-ro`, `bq-\u003Cslug>-explore`, `drive-\u003Cslug>` | `claude mcp add-json`, user scope, on **this** Mac |\n| Renders `scribe\u002Flexicon\u002Ftagger\u002Fanalyst\u002Fpersona-\u003Cslug>.md` | into `~\u002F.buzz\u002F.claude\u002Fagents\u002F` |\n| Writes a per-client fence and launcher for Claire and for Stu | into `~\u002F.buzz\u002Fproxy\u002F` — see Step 5b |\n\nBootstrap ships the *templates* for both and never renders them, so a machine that skips\nthis step has Claire, no tools and no subagents. Nothing later catches it: Step 6 drives\nthe toolbox binary directly with the key, so it reports `Isolation holds.` on a machine\nwith nothing registered at all. The install looks finished and the first ingest fails.\n\n**Joining an existing client**, add `--no-lockdown` and pass only `--sa-key`:\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fdeploy-claire-channel.sh --slug \u003Cslug> --channel-uuid \u003Cuuid> \\\n  --drive-folder \u003Cfolder-id> --sa-key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json \\\n  --no-lockdown\n```\n\nSafe against a live dataset holding another operator's data, by design rather than by\nluck: the script probes for the dataset and drops `CREATE SCHEMA` when it is already\nthere, every table is `CREATE TABLE IF NOT EXISTS`, and the tag sync is a `MERGE` on\n`tag`. `--no-lockdown` because the `REVOKE` is the creating operator's job and is already\ndone — without the flag you get a warning about grants that were dealt with weeks ago.\n\n**Creating a new client**, do the Google Cloud steps in\n`~\u002F.buzz\u002FGUIDES\u002FCLAIRE_ZERO_TO_RUNNING.md` steps 1–6 first, then run it with\n`--lock-down` and an `--admin-key` that holds `bigquery.datasets.update`.\n\n**Pass a lockdown flag either way.** With neither, the script has a question to ask and no\nterminal to ask it at, so it decides for you and says so in the summary — which is a line\nof output to explain rather than a decision anybody made.\n\n## Step 5b — One Claire and one Stu **per client channel**\n\n**First, ask which runtime the agent is on. It decides whether any of this applies.**\n\n| Runtime | What to do |\n|---|---|\n| `buzz-agent` | Nothing here. That runtime does not read the Claude MCP registry at all, so there is no registry to redirect and no fence to set. |\n| `claude` | Everything below. This runtime reads `~\u002F.claude.json`, the **user-scope** registry, which holds every client's servers on this machine. |\n\nThis question used to be missing, and its absence was invisible for a month: every agent on\nthis machine happened to be on `buzz-agent`, where the step is a no-op, so nobody noticed the\ninstruction was never followed. The first agent put on the `claude` runtime inherited three\nclients' BigQuery and Drive servers, silently and immediately.\n\nFor a `claude`-runtime agent, set **one environment variable** on the agent in Buzz Desktop:\n\n| Field | Value |\n|---|---|\n| Runtime | `claude` |\n| Env var | `CLAUDE_CONFIG_DIR` = `~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug>` — and `claude-config-stu-\u003Cslug>` for Stu |\n\n**Then restart the agent.** A configuration change never reaches a process that is already\nrunning, and the agent will keep serving from the unfenced registry until it is restarted.\n\n**Then log in inside each fence.** A config directory is its own account: the credential lives\nin the login keychain under an entry keyed to that directory, so a fence written by step 5 is\ncorrectly isolated and signed out. Nothing step 5 writes can change that — only a login mints\none, and it survives restarts.\n\n```bash\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug> claude auth login\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug> claude auth status   # \"loggedIn\": true\n```\n\n**If the agent answers every turn with `Authentication required`, the fence is working and the\nlogin is missing.** The instinct is to undo the isolation; that is exactly backwards. Copying\nthe account block out of `~\u002F.claude.json` does not work either — it is profile data, not the\ncredential, and it leaves a directory that looks signed in and still fails. Save-and-assume is\nthe failure here.\n\n> **The launchers are the older path, and they are not what to use.** Step 5 also writes\n> `~\u002F.buzz\u002Fproxy\u002Fagent-\u003Cwho>-\u003Cslug>.sh`, which exports the same variable and was meant to be\n> named in an agent-command field. That field is not what an operator is shown, and on this\n> machine no agent has ever held a launcher path — verified across every config backup since\n> the launchers were first generated. The scripts still work if your build exposes the field;\n> the environment variable is the path that is known to work, so prefer it. If you set both,\n> the launcher wins, because it runs last and is the final writer of the agent's environment.\n\n**Do not reuse a Claire or Stu that already sits in another client's channel.** This is the\nmistake to expect, it looks entirely reasonable, and it fails silently. The fence is\n`CLAUDE_CONFIG_DIR`, which is fixed when the agent's process starts and cannot vary per\nchannel — so one identity holds one client whichever channel it happens to be answering in.\nTwo client channels means two identities.\n\n**IAM does not save you here, and the evidence that it does is misleading.** Each client's\nservice account really is locked to its own dataset and folder: every cross-client attempt\nwith the wrong key is refused, on BigQuery and on Drive. But an identity holding two clients'\nservers holds two *correct* keys, and both crossings are authorised. IAM answers \"may this\ncredential read this data\" and never \"should this session be touching this client\". The\nsecond question is answered only by which servers the session loaded. So a clean isolation\nreport in Step 6 is necessary and not sufficient — it is about keys, not about sessions.\n\n**Do not fall back on the prompt.** Claire's and Stu's prompts both say they serve one client,\nand a small local model asked to read another client's table did it on the first attempt and\nnamed the other client's tool in its answer. Prompt discipline is not a control.\n\n**Verify from the process, never by asking the agent.** A model will describe a fence it does\nnot have. Two checks, and the second is the stronger one:\n\n```bash\n# 1. the variable reached the real process\nps eww -p $(pgrep -f claude-agent-acp) | tr ' ' '\\n' | grep CLAUDE_CONFIG_DIR\n\n# 2. the agent's own session wrote into that config — proof it was read, not just present\ngrep firstStartTime ~\u002F.buzz\u002Fproxy\u002Fclaude-config-\u003Cwho>-\u003Cslug>\u002F.claude.json\n```\n\nAn agent on the `claude` runtime with no `CLAUDE_CONFIG_DIR` is not partially installed. It is\nholding every client on the machine, and it looks completely normal from the outside.\n\n## Step 6 — Prove the fence, before any client data moves\n\n**Not optional, and not a formality.** It tests Google's IAM layer rather than our config,\nby trying to read another client's data as this service account and confirming it cannot:\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fverify-channel-isolation.py --slug \u003Cslug> \\\n  --key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\n```\n\nHealthy output ends with `Isolation holds.` **Anything else, stop** and report it. Do not\ningest a transcript to \"see if it works\" — that is the exact action the fence exists to\nmake safe. `ISOLATION BROKEN` almost always means a project-level BigQuery role; leave only\n`jobUser` on the project and `dataEditor` on the one dataset.\n\n`Isolation holds.` says the key reaches one dataset. It says **nothing** about whether this\nmachine can use it, because the check runs the toolbox binary itself rather than the\nregistered servers. So confirm the wiring separately, and do it before you report success:\n\n```bash\nclaude mcp list | grep -E \"bq-\u003Cslug>|drive-\u003Cslug>\"     # expect four\nls ~\u002F.buzz\u002F.claude\u002Fagents\u002F*-\u003Cslug>.md                   # expect four\nls ~\u002F.buzz\u002Fproxy\u002Fagent-{claire,stu}-\u003Cslug>.sh           # expect two\n```\n\nFour servers, four subagent files, two launchers. Anything short of that means Step 5 did\nnot finish — re-run it rather than restarting Buzz, which is the wrong fix for this and\nwastes their time.\n\n**`claude mcp list` is a wiring check, not a fence check.** It reads the registry files\ndirectly and reports everything registered on the machine, so it prints the same list\nthrough a correct fence as through no fence at all. What a fenced agent can actually see is\nanswered only by a session started with its own launcher:\n\n```bash\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-stu-\u003Cslug> \\\n  claude -p \"List every MCP server prefix you can see.\" --permission-mode bypassPermissions\n```\n\nOne prefix for Stu, three for Claire, and no other client named. **Ask the server, not the\nagent, whether a write is refused** — a model politely declining a `DELETE` reads exactly\nlike an enforced fence and is not one.\n\n## Step 7 — Report\n\nTell them, specifically:\n\n- What is installed and verified, with the isolation result quoted.\n- **The agents' full names, including the owner suffix** — `Claire (Alex)`, not `Claire`.\n  That is what they type to address one, so \"Claire is up\" leaves them guessing at the name.\n- Whether you changed the canvas, and what you added to it.\n- **What they still have to do by hand**: save four drafts, restart Buzz Desktop, and\n  anything from the `MANUAL` block.\n- Anything you could not verify. Say so plainly rather than implying full success.\n\nThen a smoke test they run themselves, using the real name:\n`@Claire (Alex) ingest the transcript in the Drive folder`.\n\n## Things to get right\n\n- **Do not flip `respond_to`.** All four ship `owner-only`. Changing it to `anyone` lets\n  everyone in a channel spend the owner's API budget and read that channel's client data.\n  It is an access decision for the community owner, not a setup step.\n- **Do not edit a prompt to work around a problem.** Fix the install. Prompt changes go\n  through `draft-update` and an owner's review.\n- **`ANTHROPIC_API_KEY` is theirs.** Every operator brings their own and spends their own\n  budget. Never copy one between machines.\n- **Never commit `local-values.json`, `.env`, or anything from `.secrets\u002F`.** If a\n  `git status` in that checkout ever shows one, stop and say so.\n- **The checkout is a public repository, and a commit message cannot be edited after a\n  push.** Never write a client name or slug, a dataset or service-account name built from\n  one, a participant's name or anything they said, a cloud project id, a Drive or sheet id,\n  a channel UUID or a pubkey into a commit message, a branch name, or a PR or issue body.\n  Write \"the client\" or \"a participant\" instead — the point always survives it. Step 3\n  turns on two checks: a git hook that refuses a commit message naming one, and a\n  `PreToolUse` hook that refuses the command outright — covering a PR title and body, an\n  issue, and a branch or tag name, which git cannot hook at all. If either one stops you,\n  rewrite; do not work around it. Text piped in on stdin is warned about rather than\n  blocked, because the guard cannot read it — pipe that through\n  `node buzz-agents\u002Fscripts\u002Fcheck-text-for-names.mjs` yourself. This has gone wrong for\n  real, in the prose explaining a bug rather than in any diff — that paragraph is the one\n  carrying the name.\n- **Add the client's bare slug to `local-redactions.json` when you join a client.** The\n  composed forms are caught by shape; the bare word can only be caught by naming it. A\n  `manual` entry for each casing is what stops it reaching a branch name.\n- **A silent Janice is not a broken Janice.** A clean review produces no message. Do not go\n  looking for a fault, and do not tell them it failed.\n- Prefer re-running bootstrap over hand-editing anything inside `~\u002F.buzz`. Hand edits are\n  invisible to the next operator and to the next `git pull`.\n","# Deploy the agents onto this Mac\n\nSomeone has asked you to set up the agents. You are doing this **on the machine you are\nrunning on**, for the person you are talking to. You cannot install anything on anybody\nelse's Mac — if the person asking is not the owner of this machine, say so and stop.\n\n## What you are actually installing\n\n| Half | What | Result if missing |\n|---|---|---|\n| **The nest** — `nest\u002F` → `~\u002F.buzz\u002F` | Scripts, fenced MCP servers, guides, `settings.json` | Agents start, then find no tools |\n| **The agents** — `buzz-agents\u002F` → Buzz Desktop | Prompts and settings for Claire, Stu, Janice, Alan | Nothing to talk to |\n\nBoth, in that order. An agent created before the nest exists is a name with nothing behind it.\n\n## Before you touch anything: which case is this?\n\nAsk, and do not guess — the answer changes half the steps:\n\n1. **Joining a client that already exists.** Someone else already created the dataset,\n   the service account and the Drive folder. Most common. You need their values, not\n   `gcloud`. You still run every step, Step 5 included — the cloud side is done, the\n   *machine* side has not been done on this Mac.\n2. **Creating a new client from scratch.** Needs Google Cloud work and `gcloud`.\n3. **Repairing or updating an existing install.** A nest already exists here. The agents\n   exist too, so Step 4 is the wrong tool — see *Updating agents that already exist*.\n\nCheck which by looking, before you ask twice:\n\n```bash\nls ~\u002F.buzz\u002Fbin\u002Ftoolbox ~\u002F.buzz\u002F.claude\u002Fsettings.json 2>\u002Fdev\u002Fnull\nls ~\u002F.buzz\u002F.secrets\u002F 2>\u002Fdev\u002Fnull\n```\n\n## Step 1 — Find the checkout\n\nThe scripts live in a clone of `borderux\u002Frecursica-knowledge`. Find it rather than\nassuming a path:\n\n```bash\nls ~\u002F.buzz\u002FREPOS\u002F 2>\u002Fdev\u002Fnull\n```\n\nNo clone yet — the normal case on a clean Buzz install. Clone it yourself; do not ask them\nto do it in a terminal:\n\n```bash\ngit clone https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge.git ~\u002F.buzz\u002FREPOS\u002Frecursica-knowledge\n```\n\n`main` is correct — do not check out a branch. Everything below runs from that directory.\n\n## Step 2 — Collect their values\n\nAsk for all of them in **one** message, not one question at a time. Ten round trips to\nfill in a form is a worse experience than the README this skill replaces.\n\nWhat to ask for, and be precise that most of these are **not** secrets — they are\nidentifiers, and they are the same for everyone in the community:\n\n- `BQ_PROJECT` — the Google Cloud project id\n- `DRIVE_FOLDER` — the client's Drive folder id\n- `TAG_SHEET_ID` — the shared tag dictionary sheet id\n- `CLAIRE_CHANNEL`, `STU_CHANNEL`, `IVAN_CHANNEL`, `ALAN_CHANNEL`, `JANICE_CHANNEL` — channel UUIDs\n- `JANICE_PUBKEY` — this community's Janice, hex\n- `KNOWLEDGE_REPO_NAME` — only if they want Alan\n- **their name**, for the agent display names — see below. Ask for the form they want\n  shown, not their account id\n\n`buzz-agents\u002Fplaceholders.json` says where each one is found; quote from it rather than\ninventing instructions. Leave `TRANSCRIPT_DIR` blank — bootstrap derives it.\n\n### The canvas of the channel you were asked in is the source of these values\n\n**This is the normal path — start here, not with questions.** Deployment is meant to be run\n*in the channel the agents are being deployed to*, and that channel's canvas holds the\nconfiguration. So the channel you were @mentioned in is almost always the deployment\nchannel, and you can read most of the list above straight out of it:\n\n```bash\nbuzz canvas get --channel \u003Cuuid>          # the channel you are talking in\n```\n\nA `## Claire config` block gives you `slug`, `drive_folder`, `dataset` and the project — the\nsame block Claire reads at runtime, so it is the authoritative copy rather than a\ntranscription of it. Take `BQ_PROJECT` and the slug from there.\n\nOnly ask for what the canvas genuinely does not carry, and say which values those are and\nthat you looked. If the canvas has no `## Claire config` block at all, confirm you are in\nthe right channel before falling back to asking for everything.\n\n#### Canvas keys → values\n\n| Canvas key | Becomes | Notes |\n|---|---|---|\n| `slug` | *not a token* — a `--slug` argument | Lowercase, hyphens. Names the dataset, the key file, the MCP servers |\n| `sa_slug` | *not a token* — a `--sa-slug` argument | Optional. The service account's name when it is not the slug. Lowercase, hyphens |\n| `bq_project` | `BQ_PROJECT` | |\n| `bq_dataset` | *nothing* | Confirmation only. It must equal `research_\u003Cslug>` with hyphens as underscores |\n| `drive_folder` | `DRIVE_FOLDER` | |\n| `tag_sheet` | `TAG_SHEET_ID` | |\n| `claire_channel`, `stu_channel`, `alan_channel`, `janice_channel` | the four `*_CHANNEL` tokens | Janice's routing table, **not** the channel you are installing in |\n| `knowledge_repo` | `KNOWLEDGE_REPO_NAME` | Alan only |\n\nThose four routing UUIDs are a common trap: they are where Janice files findings, and none of\nthem is the channel this install is running in. Do not point `--channel` at a `building-*`\nchannel because a canvas key named it.\n\n`bq_dataset` is a cross-check rather than an input. If it disagrees with `research_\u003Cslug>`,\nstop and ask which is right instead of picking one — the wrong dataset name is a fence that\nfails open.\n\n`sa_slug` exists because **a Google service account cannot be renamed.** An account created\nbefore the slug settled keeps its original name for the life of the client, so a channel on\nslug `acme` can legitimately be served by `claire-acme-health-service-user`. Set `sa_slug` to\nthe account's own name and leave `slug` alone; the key then lives at\n`~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Csa_slug>-service-user.json` and `bin\u002Fstu` takes `--sa-slug`.\n\nOnly add it when the two genuinely differ — omitted, everything derives from `slug`, which is\nthe normal case. Do not \"fix\" a mismatch by renaming the account in the console: you cannot,\nand creating a replacement means re-granting the dataset and re-sharing the Drive folder.\n\n### The community note is where these belong now\n\n`~\u002F.buzz\u002Fbin\u002Fclient-config.mjs resolve --channel \u003Cuuid>` answers this whole section in one\ncommand, reading the community and the channel together and refusing rather than guessing.\nUse it before asking anybody anything.\n\nA client's details are recorded once for the whole community, so a **second** channel for a\nclient that already has one needs a single line naming the client, not the block above:\n\n```markdown\n## Claire config\n- client: \u003Cslug>\n```\n\n`client-config.mjs publish --from-channel \u003Cuuid>` promotes an existing channel's block to the\ncommunity; without `--write` it prints what it would publish. Say out loud before writing it\nthat a note is readable by everyone in the community where a canvas is readable by that\nchannel's members — it carries names and ids only, never a key, but that is a wider audience.\n\n**The channel line is not optional and must not be \"simplified\" away.** A channel that names\nno client is unconfigured, and that is the fence doing its job: most channels in a community\nare not client channels, and moving everything to the community would quietly configure all\nof them for that client.\n\n**If a value is missing, the fix is to add it to the community note** — not to\ncollect it in chat and move on. The next operator hits the identical gap otherwise.\n`TAG_SHEET_ID` is the one most often absent and it is not derivable; ask the owner for it,\nthen **write it back**. Show them the block before you set it: `buzz canvas set` replaces the\nwhole document, so it has to carry what was already there.\n\n```markdown\n## Claire config\n\n- slug: acme\n- bq_project: acme-research-123456\n- bq_dataset: research_acme\n- drive_folder: 1AbCdEfGhIjKlMnOpQrStUvWxYz_\n- tag_sheet: 1AbCdEfGhIjKlMnOpQrStUvWxYz0123456789\n\n## Janice routing\n\n- claire_channel: 00000000-0000-0000-0000-000000000000\n- stu_channel: 00000000-0000-0000-0000-000000000000\n- alan_channel: 00000000-0000-0000-0000-000000000000\n- janice_channel: 00000000-0000-0000-0000-000000000000\n\n## Alan — omit this section if the community has no Alan\n\n- knowledge_repo: recursica-knowledge\n```\n\nA canvas is channel-visible, so it holds identifiers only. Never put the service-account key\non it — and it needs no key path either, because the account's address is derivable from\n`slug` and `bq_project` alone.\n\n### You can only see channels you are a member of\n\n`buzz channels list` shows **open** channels plus the **private** ones your own identity\nbelongs to. A private channel you are not in is not listed, not searchable, and its UUID is\nnot resolvable from its name. There is no error — it is simply absent, which reads like a\ntypo or a deleted channel.\n\nSo if they name a channel you cannot find, do not conclude it does not exist. Say this\ninstead, because it is almost always the real cause:\n\n> I cannot see `#\u003Cname>`. Private channels are invisible to agents that are not members —\n> add **me** (your own Fizz, not the owner's) to it, then send me the UUID.\n\nBeing a member yourself is what matters. The owner already being in the channel does not\nhelp you, and neither does the person you are talking to being in it. Do not try\n`buzz channels add-member` to add yourself; you cannot resolve the UUID you would need.\n\n### `JANICE_PUBKEY` is circular in a new community — expect two bootstrap runs\n\n`buzz channels members --channel \u003Cjanice-uuid>` gives you `JANICE_PUBKEY` **only if a Janice\nalready exists in that community.** Each operator gets their *own* Janice, whose pubkey does\nnot exist until they save the draft in Step 4 — so on a first install there is nothing to\nlook up and nothing anyone can tell you.\n\nThat is expected, not a blocker. Leave `JANICE_PUBKEY` blank, note it, and:\n\n1. Bootstrap and create the agents without it. `wake-janice.sh` is the only consumer.\n2. After they save the Janice draft, get her pubkey and re-run `bootstrap-nest.mjs`.\n\nBootstrap is idempotent, so the second run rewrites only that. **Tell them this up front** —\notherwise the first run's unresolved-token failure looks like something went wrong.\n\nEverything else you can resolve yourself: channel UUIDs by name from `buzz channels list`.\nAsk only for what you could not find.\n\nThen write the file — **never** to the repo root, always to `buzz-agents\u002F`:\n\n```bash\ncp buzz-agents\u002Flocal-values.example.json buzz-agents\u002Flocal-values.json\n```\n\nFill it in and confirm it is ignored before you go any further:\n\n```bash\ngit check-ignore -v buzz-agents\u002Flocal-values.json\n```\n\nIf that prints nothing, **stop** — the file is not ignored and this repository is public.\n\n**Put only the keys the example file lists in there.** It is tempting to add the slug, or the\noperator's name, since you have both by now — do not. Every value in that file is treated as\na substring to swap for its token when prompts are exported, so a short one does damage out\nof all proportion to its usefulness: a slug like `acme` would rewrite the word `acmeism`, and\na first name would rewrite every mention of that person. The slug is a `--slug` argument and\nthe owner is `--owner`. Neither is a token.\n\n### The one real secret\n\nThe **service-account key** is the only true secret in the set, and it is a file, not a\nstring. It belongs at:\n\n```\n~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json     mode 0600\n```\n\nRules, and they are not negotiable:\n\n- `.secrets\u002F` is `0700` and lives **outside any git checkout**. Never copy a key into the\n  repo, not even briefly.\n- **Never print a key, or any part of one, into the channel.** Buzz messages are relay\n  events; you cannot unsend one. Confirm a key by its path, permissions and\n  `client_email` — never its contents.\n- Ask them to place the file themselves and tell you the path. Do not ask them to paste it\n  to you.\n\n```bash\nchmod 600 ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\nnode -e 'console.log(require(process.argv[1]).client_email)' \u003Cpath>   # identify, don't dump\n```\n\n**You already know what that `client_email` should say, so check it rather than accepting\nwhatever arrives.** Both the account and the filename follow from values you have:\n\n```\nclient_email  claire-\u003Cslug>-service-user@\u003CBQ_PROJECT>.iam.gserviceaccount.com\nfilename      ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\n```\n\nSo for slug `acme` in project `acme-research-123456` the account is\n`claire-acme-service-user@acme-research-123456.iam.gserviceaccount.com`. Derive the expected\nstring, compare it to the file's `client_email`, and say which you got.\n\nA mismatch is worth stopping for, because each of the two ways it can differ is a distinct\nproblem:\n\n| What differs | What it means |\n|---|---|\n| The **project** part | The key belongs to another client's project. Do not install it — this is precisely the cross-client leak the isolation check exists to catch. |\n| The **slug** part | Either the wrong client's key, or an account whose name simply never matched the slug. Confirm which client it is for before use, and if the name is legitimate, record it as `sa_slug` on the canvas rather than renaming anything. |\n\nNever print the key's contents, any part of a private key, or the `private_key_id`. The\n`client_email` is an identifier, not a credential, and is the only field to quote.\n\n## Step 3 — Install the nest\n\nDry run first, always. Show them what it says:\n\n```bash\nnode scripts\u002Fbootstrap-nest.mjs --check\n```\n\nThen:\n\n```bash\nnode scripts\u002Fbootstrap-nest.mjs\n```\n\nIt downloads a checksum-verified 154 MB `toolbox` binary, so it is not instant. It is\nidempotent — re-running is always safe, and is the correct response to almost any failure\nafter you have fixed the cause.\n\n**Read its output; do not just check the exit code.** Two stops are normal and each names\nits own fix:\n\n- *Unresolved tokens* — it lists every missing value and the file needing it. Add them and\n  re-run. It refuses rather than installing a script that hunts for a project literally\n  named `{{BQ_PROJECT}}`.\n- *Missing prerequisite* — it names what to install. `gcloud` missing is only a warning,\n  and only matters for case 2.\n\n## Step 4 — Create the agents\n\n`--channel` is **this** channel — the one you were asked in, whose canvas you read in Step 2.\n\n```bash\nnode buzz-agents\u002Fscripts\u002Frestore-agents.mjs --channel \u003Cthis-channel-uuid> --owner \u003Cname>          # show the commands\nnode buzz-agents\u002Fscripts\u002Frestore-agents.mjs --channel \u003Cthis-channel-uuid> --owner \u003Cname> --run    # open the drafts\n```\n\n`--owner` defaults to the first word of the checkout's `git config user.name`, and the script\nprints which name it resolved and where from before it sends anything. **Read that line out\nand confirm it** — it is what people will type to address the agent. If it cannot work out a\nname it stops rather than falling back to bare `Claire`; `--no-owner` is how you ask for the\nbare name on purpose, and it is only right for a community that will hold exactly one set of\nthese agents forever.\n\nThat creates them as **`Claire (Alex)`**, `Stu (Alex)` and so on. Everyone runs their own\nagents, so a shared channel otherwise ends up holding several bots called `Claire` with\nnothing distinguishing them — which happened, and left a real \"which of these is mine?\"\nOnly the display name carries the owner; the stored definition stays canonical, so it is\nstill one shared `agents\u002Fclaire\u002F` for everybody.\n\nDo not hand-rename an agent in Buzz Desktop to achieve this. Use `--owner`, so the name is\napplied the one way the tooling recognises.\n\nSomeone else's `Claire (…)` already in the channel is **not** a conflict and not a reason to\nreuse it — it is theirs, on their machine, spending their budget. Two owner-suffixed Claires\nin one room is the convention working. What is a real problem is two agents on *one* Mac\nanswering to the same definition; `sync-prompts.mjs` reports that as `more than one match on\nthis Mac` and names both, and it will not guess between them.\n\n**You cannot finish this step, and you must not imply that you have.** Each command opens\na prefilled draft in *their* Buzz Desktop that **they** review and save. That gate is\ndeliberate: a prompt arriving from a git branch gets human eyes before it starts answering\npeople. Tell them plainly: four drafts to open and save, by hand.\n\nWork through the `MANUAL` block it prints — parallelism, timeouts, avatar upload have no\nCLI flags.\n\nIf `buzz upload file` rejects an avatar with *media contains metadata or a non-canonical\nmetadata channel*, the PNG is carrying an `eXIf` chunk. The committed avatars no longer do.\nDo **not** try to fix it by re-encoding with `sips` — `sips` is what writes that chunk, so\nevery retry reproduces the rejection. If you meet it on some other image, strip the chunk:\n\n```bash\nnode -e 'const f=process.argv[1],fs=require(\"fs\");import(\"'\"$PWD\"'\u002Fbuzz-agents\u002Flib\u002Favatars.mjs\").then(m=>fs.writeFileSync(f,m.stripPngMetadata(fs.readFileSync(f))))' \u003Cfile.png>\n```\n\nThen tell them to **restart Buzz Desktop.** MCP servers are read once at startup; until\nthey restart, their agents have no tools and will look broken.\n\n## Updating agents that already exist\n\nCase 3 only. **Do not use Step 4 here** — `draft-create` proposes a *new* agent, so it would\noffer them a second Claire rather than update the one they have.\n\nStart by asking what is actually different, which needs no credentials and changes nothing:\n\n```bash\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs --diff     # see the change itself\n```\n\nRead the state it reports per agent, because the right action is different for each:\n\n| It says | What happened | What to do |\n|---|---|---|\n| `in sync` | Nothing to do | Say so and stop |\n| `BEHIND the branch` | They pulled; their agent is an older commit | Offer the `draft-update` it prints |\n| `has local edits not in git` | **They edited the agent in Buzz Desktop and never exported it** | Do NOT apply. Their prompt is unversioned work and applying would delete it |\n| `runs the committed prompt, unstamped` | First run since stamps existed, or a stamp got lost | Nothing. It records the version itself and reports `in sync` |\n| `prompt in sync, settings differ` | Only runtime\u002Fprovider\u002Fmodel\u002F`respond_to` moved | Offer the settings-only `draft-update` |\n| `prompt not committed yet` | A prompt in the repo has never been committed | Commit it; a version stamp is a commit sha |\n| `not installed on this Mac` | Genuinely missing | That one, and only that one, needs Step 4 |\n| `more than one match on this Mac` | Two personas here answer to one definition — e.g. a `Claire` and a `Claire (Alex)` | Nothing, until a human resolves it. It names both; ask which to keep. Do not guess — `draft-update` reports `accepted: true` for a name that hits nothing, so the wrong guess looks like a success |\n\n**Check `not installed` against Buzz Desktop before acting on it.** It is the one state whose\nwrong answer is expensive: acting on it creates a duplicate agent, and a duplicate cannot be\nmerged back. If they can see that agent in Desktop, the report is wrong, not the person — say\nso and stop rather than creating a second one.\n\nFor the local-edits case, tell them what they have and let them choose. The capture is\n`node buzz-agents\u002Fscripts\u002Fexport-agents.mjs`, then a normal PR. Do not run\n`--force-apply` on their behalf: it exists so a human can overwrite their own work\nknowingly, not so you can.\n\nTo send the drafts once they have agreed:\n\n```bash\nnode buzz-agents\u002Fscripts\u002Fsync-prompts.mjs --channel \u003Cuuid> --run\n```\n\nSame gate as Step 4 — each one opens a form they must save. If it reports a prompt over the\n20,000-character limit, do not trim the prompt to fit; that is a content decision for the\nowner, and `~\u002F.buzz\u002FGUIDES\u002FJANICE_REVIEW_CHECKLIST.md` covers how to move a section out to a\nguide instead.\n\n**Sending a draft does not record it.** Nothing is stamped until the owner saves the form,\nbecause a draft can be discarded. So after they tell you they have saved, run the report once\nmore — that pass sees the prompt that actually landed and records the version. Do not tell\nthem an update is complete on the strength of `accepted: true`.\n\n## Step 5 — Wire this machine up. Every case, including joining\n\n**Run this even when joining a client someone else already created.** It used to say to\nskip it, and that was wrong: past the dataset work, this script is the *only* thing that\ndoes the per-machine half of the install.\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fdeploy-claire-channel.sh --slug \u003Cslug> --channel-uuid \u003Cuuid> \\\n  --drive-folder \u003Cfolder-id> --sa-key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json --dry-run\n```\n\nRun `--dry-run` first and read it. Drop the flag to apply.\n\n| It also does | Which lives nowhere else |\n|---|---|\n| Registers `bq-\u003Cslug>`, `bq-\u003Cslug>-ro`, `bq-\u003Cslug>-explore`, `drive-\u003Cslug>` | `claude mcp add-json`, user scope, on **this** Mac |\n| Renders `scribe\u002Flexicon\u002Ftagger\u002Fanalyst\u002Fpersona-\u003Cslug>.md` | into `~\u002F.buzz\u002F.claude\u002Fagents\u002F` |\n| Writes a per-client fence and launcher for Claire and for Stu | into `~\u002F.buzz\u002Fproxy\u002F` — see Step 5b |\n\nBootstrap ships the *templates* for both and never renders them, so a machine that skips\nthis step has Claire, no tools and no subagents. Nothing later catches it: Step 6 drives\nthe toolbox binary directly with the key, so it reports `Isolation holds.` on a machine\nwith nothing registered at all. The install looks finished and the first ingest fails.\n\n**Joining an existing client**, add `--no-lockdown` and pass only `--sa-key`:\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fdeploy-claire-channel.sh --slug \u003Cslug> --channel-uuid \u003Cuuid> \\\n  --drive-folder \u003Cfolder-id> --sa-key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json \\\n  --no-lockdown\n```\n\nSafe against a live dataset holding another operator's data, by design rather than by\nluck: the script probes for the dataset and drops `CREATE SCHEMA` when it is already\nthere, every table is `CREATE TABLE IF NOT EXISTS`, and the tag sync is a `MERGE` on\n`tag`. `--no-lockdown` because the `REVOKE` is the creating operator's job and is already\ndone — without the flag you get a warning about grants that were dealt with weeks ago.\n\n**Creating a new client**, do the Google Cloud steps in\n`~\u002F.buzz\u002FGUIDES\u002FCLAIRE_ZERO_TO_RUNNING.md` steps 1–6 first, then run it with\n`--lock-down` and an `--admin-key` that holds `bigquery.datasets.update`.\n\n**Pass a lockdown flag either way.** With neither, the script has a question to ask and no\nterminal to ask it at, so it decides for you and says so in the summary — which is a line\nof output to explain rather than a decision anybody made.\n\n## Step 5b — One Claire and one Stu **per client channel**\n\n**First, ask which runtime the agent is on. It decides whether any of this applies.**\n\n| Runtime | What to do |\n|---|---|\n| `buzz-agent` | Nothing here. That runtime does not read the Claude MCP registry at all, so there is no registry to redirect and no fence to set. |\n| `claude` | Everything below. This runtime reads `~\u002F.claude.json`, the **user-scope** registry, which holds every client's servers on this machine. |\n\nThis question used to be missing, and its absence was invisible for a month: every agent on\nthis machine happened to be on `buzz-agent`, where the step is a no-op, so nobody noticed the\ninstruction was never followed. The first agent put on the `claude` runtime inherited three\nclients' BigQuery and Drive servers, silently and immediately.\n\nFor a `claude`-runtime agent, set **one environment variable** on the agent in Buzz Desktop:\n\n| Field | Value |\n|---|---|\n| Runtime | `claude` |\n| Env var | `CLAUDE_CONFIG_DIR` = `~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug>` — and `claude-config-stu-\u003Cslug>` for Stu |\n\n**Then restart the agent.** A configuration change never reaches a process that is already\nrunning, and the agent will keep serving from the unfenced registry until it is restarted.\n\n**Then log in inside each fence.** A config directory is its own account: the credential lives\nin the login keychain under an entry keyed to that directory, so a fence written by step 5 is\ncorrectly isolated and signed out. Nothing step 5 writes can change that — only a login mints\none, and it survives restarts.\n\n```bash\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug> claude auth login\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-claire-\u003Cslug> claude auth status   # \"loggedIn\": true\n```\n\n**If the agent answers every turn with `Authentication required`, the fence is working and the\nlogin is missing.** The instinct is to undo the isolation; that is exactly backwards. Copying\nthe account block out of `~\u002F.claude.json` does not work either — it is profile data, not the\ncredential, and it leaves a directory that looks signed in and still fails. Save-and-assume is\nthe failure here.\n\n> **The launchers are the older path, and they are not what to use.** Step 5 also writes\n> `~\u002F.buzz\u002Fproxy\u002Fagent-\u003Cwho>-\u003Cslug>.sh`, which exports the same variable and was meant to be\n> named in an agent-command field. That field is not what an operator is shown, and on this\n> machine no agent has ever held a launcher path — verified across every config backup since\n> the launchers were first generated. The scripts still work if your build exposes the field;\n> the environment variable is the path that is known to work, so prefer it. If you set both,\n> the launcher wins, because it runs last and is the final writer of the agent's environment.\n\n**Do not reuse a Claire or Stu that already sits in another client's channel.** This is the\nmistake to expect, it looks entirely reasonable, and it fails silently. The fence is\n`CLAUDE_CONFIG_DIR`, which is fixed when the agent's process starts and cannot vary per\nchannel — so one identity holds one client whichever channel it happens to be answering in.\nTwo client channels means two identities.\n\n**IAM does not save you here, and the evidence that it does is misleading.** Each client's\nservice account really is locked to its own dataset and folder: every cross-client attempt\nwith the wrong key is refused, on BigQuery and on Drive. But an identity holding two clients'\nservers holds two *correct* keys, and both crossings are authorised. IAM answers \"may this\ncredential read this data\" and never \"should this session be touching this client\". The\nsecond question is answered only by which servers the session loaded. So a clean isolation\nreport in Step 6 is necessary and not sufficient — it is about keys, not about sessions.\n\n**Do not fall back on the prompt.** Claire's and Stu's prompts both say they serve one client,\nand a small local model asked to read another client's table did it on the first attempt and\nnamed the other client's tool in its answer. Prompt discipline is not a control.\n\n**Verify from the process, never by asking the agent.** A model will describe a fence it does\nnot have. Two checks, and the second is the stronger one:\n\n```bash\n# 1. the variable reached the real process\nps eww -p $(pgrep -f claude-agent-acp) | tr ' ' '\\n' | grep CLAUDE_CONFIG_DIR\n\n# 2. the agent's own session wrote into that config — proof it was read, not just present\ngrep firstStartTime ~\u002F.buzz\u002Fproxy\u002Fclaude-config-\u003Cwho>-\u003Cslug>\u002F.claude.json\n```\n\nAn agent on the `claude` runtime with no `CLAUDE_CONFIG_DIR` is not partially installed. It is\nholding every client on the machine, and it looks completely normal from the outside.\n\n## Step 6 — Prove the fence, before any client data moves\n\n**Not optional, and not a formality.** It tests Google's IAM layer rather than our config,\nby trying to read another client's data as this service account and confirming it cannot:\n\n```bash\n~\u002F.buzz\u002Fbin\u002Fverify-channel-isolation.py --slug \u003Cslug> \\\n  --key ~\u002F.buzz\u002F.secrets\u002Fclaire-\u003Cslug>-service-user.json\n```\n\nHealthy output ends with `Isolation holds.` **Anything else, stop** and report it. Do not\ningest a transcript to \"see if it works\" — that is the exact action the fence exists to\nmake safe. `ISOLATION BROKEN` almost always means a project-level BigQuery role; leave only\n`jobUser` on the project and `dataEditor` on the one dataset.\n\n`Isolation holds.` says the key reaches one dataset. It says **nothing** about whether this\nmachine can use it, because the check runs the toolbox binary itself rather than the\nregistered servers. So confirm the wiring separately, and do it before you report success:\n\n```bash\nclaude mcp list | grep -E \"bq-\u003Cslug>|drive-\u003Cslug>\"     # expect four\nls ~\u002F.buzz\u002F.claude\u002Fagents\u002F*-\u003Cslug>.md                   # expect four\nls ~\u002F.buzz\u002Fproxy\u002Fagent-{claire,stu}-\u003Cslug>.sh           # expect two\n```\n\nFour servers, four subagent files, two launchers. Anything short of that means Step 5 did\nnot finish — re-run it rather than restarting Buzz, which is the wrong fix for this and\nwastes their time.\n\n**`claude mcp list` is a wiring check, not a fence check.** It reads the registry files\ndirectly and reports everything registered on the machine, so it prints the same list\nthrough a correct fence as through no fence at all. What a fenced agent can actually see is\nanswered only by a session started with its own launcher:\n\n```bash\nCLAUDE_CONFIG_DIR=~\u002F.buzz\u002Fproxy\u002Fclaude-config-stu-\u003Cslug> \\\n  claude -p \"List every MCP server prefix you can see.\" --permission-mode bypassPermissions\n```\n\nOne prefix for Stu, three for Claire, and no other client named. **Ask the server, not the\nagent, whether a write is refused** — a model politely declining a `DELETE` reads exactly\nlike an enforced fence and is not one.\n\n## Step 7 — Report\n\nTell them, specifically:\n\n- What is installed and verified, with the isolation result quoted.\n- **The agents' full names, including the owner suffix** — `Claire (Alex)`, not `Claire`.\n  That is what they type to address one, so \"Claire is up\" leaves them guessing at the name.\n- Whether you changed the canvas, and what you added to it.\n- **What they still have to do by hand**: save four drafts, restart Buzz Desktop, and\n  anything from the `MANUAL` block.\n- Anything you could not verify. Say so plainly rather than implying full success.\n\nThen a smoke test they run themselves, using the real name:\n`@Claire (Alex) ingest the transcript in the Drive folder`.\n\n## Things to get right\n\n- **Do not flip `respond_to`.** All four ship `owner-only`. Changing it to `anyone` lets\n  everyone in a channel spend the owner's API budget and read that channel's client data.\n  It is an access decision for the community owner, not a setup step.\n- **Do not edit a prompt to work around a problem.** Fix the install. Prompt changes go\n  through `draft-update` and an owner's review.\n- **`ANTHROPIC_API_KEY` is theirs.** Every operator brings their own and spends their own\n  budget. Never copy one between machines.\n- **Never commit `local-values.json`, `.env`, or anything from `.secrets\u002F`.** If a\n  `git status` in that checkout ever shows one, stop and say so.\n- **The checkout is a public repository, and a commit message cannot be edited after a\n  push.** Never write a client name or slug, a dataset or service-account name built from\n  one, a participant's name or anything they said, a cloud project id, a Drive or sheet id,\n  a channel UUID or a pubkey into a commit message, a branch name, or a PR or issue body.\n  Write \"the client\" or \"a participant\" instead — the point always survives it. Step 3\n  turns on two checks: a git hook that refuses a commit message naming one, and a\n  `PreToolUse` hook that refuses the command outright — covering a PR title and body, an\n  issue, and a branch or tag name, which git cannot hook at all. If either one stops you,\n  rewrite; do not work around it. Text piped in on stdin is warned about rather than\n  blocked, because the guard cannot read it — pipe that through\n  `node buzz-agents\u002Fscripts\u002Fcheck-text-for-names.mjs` yourself. This has gone wrong for\n  real, in the prose explaining a bug rather than in any diff — that paragraph is the one\n  carrying the name.\n- **Add the client's bare slug to `local-redactions.json` when you join a client.** The\n  composed forms are caught by shape; the bare word can only be caught by naming it. A\n  `manual` entry for each casing is what stops it reaching a branch name.\n- **A silent Janice is not a broken Janice.** A clean review produces no message. Do not go\n  looking for a fault, and do not tell them it failed.\n- Prefer re-running bootstrap over hand-editing anything inside `~\u002F.buzz`. Hand edits are\n  invisible to the next operator and to the next `git pull`.\n",{"name":5,"description":7},"main","110b382bfe525d171ba33c7074d6ad1f3762548f","nest\u002F.claude\u002Fskills\u002Fdeploy-agents",0,"https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge","https:\u002F\u002Fgithub.com\u002Fborderux","https:\u002F\u002Fborderux.com","2026-11-09 16:39:27",{"source":8,"skillName":5,"commands":33,"prompt":43,"zipAvailable":44},[34,37,40],{"tool":35,"command":36},"npx","npx skills add borderux\u002Frecursica-knowledge --skill deploy-agents",{"tool":38,"command":39},"bunx","bunx skills add borderux\u002Frecursica-knowledge --skill deploy-agents",{"tool":41,"command":42},"pnpm","pnpm dlx skills add borderux\u002Frecursica-knowledge --skill deploy-agents","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fnest\u002F.claude\u002Fskills\u002Fdeploy-agents\u002FSKILL.md",true,[],[47,55,62,69,76,83],{"skillNo":48,"name":49,"nameZh":6,"description":50,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":51,"sourceHtmlUrl":52,"categories":53,"keywords":54,"publishedAt":19},"01M7556E67F3DCE39P7ZTVT5TA","recursica-skill-template","Replace with a description of the skill and its purpose.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Ftemplate\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Ftemplate\u002FSKILL.md",[],[],{"skillNo":56,"name":57,"nameZh":6,"description":58,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":61,"publishedAt":19},"01M75569FCJXWWGXRVMTJFBK0W","recursica-skill-working-memory","The research behind Recursica's item-count limits, with citations — Miller's 7 ± 2, Cowan's 4, chunking, and recognition versus recall. Use when a decision turns on how many options, navigation items, or chips to show at once, or to justify or challenge a count. Not for choosing a control — see recursica-skill-selection-controls.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fskills\u002Fpsychology\u002Frecursica-skill-working-memory\u002FSKILL.md",[],[],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":68,"publishedAt":19},"01M75564EC5E5QKBPNWARQVDBC","recursica-skill-discoverability","The research behind Recursica's rule that rarely needed settings get a real but unpromoted entry point — the paradox of the active user, satisficing (settling for the first method that is good enough), and progressive disclosure, with citations. Use when deciding whether to hide, promote, or build a configuration or customization feature, or to justify that choice. Not for item counts — see recursica-skill-working-memory.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fskills\u002Fpsychology\u002Frecursica-skill-discoverability\u002FSKILL.md",[],[],{"skillNo":70,"name":71,"nameZh":6,"description":72,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M7555Z2YRG9YSM01KMYT1FY5","recursica-skill-design-router","Start here for any Recursica screen work. The router says what to decide in what order, which skill owns each decision, which rule wins when two rules conflict, and when to confirm with the user instead of guessing. Load the router first to design, build, review, or refactor a screen, page, panel, or flow, or when rules seem to disagree or no rule applies. The router routes, and the owning skill holds the rules.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fskills\u002Fmeta\u002Frecursica-skill-design-router\u002FSKILL.md",[],[],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":19},"01M7555RYHBH6XM48V2420Z66X","recursica-skill-undo","House rules for undo — when an action gets an undo instead of a confirmation, where the undo goes (on the page, in place, or in a toast), when a toast is needed at all, and the Undo button's accessible name. Use whenever an action can be reversed, or when choosing between an undo and a confirmation. Not for building the toast that holds an Undo button — see recursica-skill-toast. Not for the confirmation modal — see recursica-skill-buttons-links.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fskills\u002Fdesign-rules\u002Frecursica-skill-undo\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":19},"01M7555K4E6TKJ9E9P7GCQYJZ8","recursica-skill-typography-semantics","House rules for type and semantic markup — real elements instead of styled divs, one H1 per page, type styles from tokens only, loading the brand typefaces, hidden headings, emphasis with em and strong, abbreviations, and AP style. Use for headings, body copy, emphasis, and the markup under a visual hierarchy. Not for naming — see recursica-skill-names-and-terms.","https:\u002F\u002Fgithub.com\u002Fborderux\u002Frecursica-knowledge\u002Fblob\u002Fmain\u002Fskills\u002Fdesign-rules\u002Frecursica-skill-typography-semantics\u002FSKILL.md",[],[]]