CLAUDE.md · git:20260821.adc2f7a · 2026-08-21 · sha256 945b90fc784e178a

CLAUDE.md git:20260821.adc2f7aA

Immutable. This exact content is served forever at /api/v1/blob/945b90fc784e178a.

See [docs/AGENTS.md](docs/AGENTS.md) for project context, architecture, commands, constraints, and conventions.

## Git conventions (must follow)

- **Git history must never attribute work to an AI agent — non-negotiable.** Commits are attributed solely to the human committer. Banned: (1) a `Co-authored-by:` trailer naming an agent (Claude / Codex / ChatGPT / Copilot / Gemini) or using an agent email, and (2) committing with an **author or committer identity that is an agent** — an `@anthropic.com` / `@openai.com` email, or an agent **name** (Claude, Codex, …). GitHub App bot accounts (`…[bot]`, e.g. `claude[bot]`) are exempt: they only ever land on branches and never reach the squash-authored `main`. What's **fine**: links to an agent session (e.g. `claude.ai/code`), the "🤖 Generated with …" and **`_Generated by [Claude Code]_`** PR/commit footers, and ordinary PR comments — those don't claim authorship, and the footer is genuinely useful so keep it. Because PRs are squash-merged from the **PR title + PR body**, a `Co-authored-by:` line in the PR description lands on `main` even when the commits are clean — so scrub the PR description too, not just the commits. Enforced in three places, all sharing one detector ([`.github/scripts/agent-attribution-scan.sh`](.github/scripts/agent-attribution-scan.sh)) so they can't disagree: the local [`.githooks/commit-msg`](.githooks/commit-msg) hook (blocks the commit as it's written), the local [`.githooks/pre-push`](.githooks/pre-push) hook (scans every commit a push would add, on **any** branch — this is what catches commits `commit-msg` never saw: amended, rebased, cherry-picked, or made before the hooks were installed), and the [`No Agent Attribution`](.github/workflows/no-agent-attribution.yml) CI gate, which scans the PR title, body, and every commit's trailers + author/committer identity (name and email), plus a post-merge drift check on `main`. Install the hooks with `scripts/install-git-hooks.sh` (the `SessionStart` hook already does). `--no-verify` bypasses the local hooks; CI still won't. The detector has its own tests — `scripts/test-agent-attribution.sh`, run by CI on every PR — so change it only with those passing. If you ever see an agent co-author or agent commit identity on `main`, treat it as a bug to fix, not a default to tolerate.
- **Branch names are always `agent/…`, never `claude/…`** (or `copilot/…`, `codex/…`, or any other agent prefix). Don't ask which prefix to use — it's always `agent/`. If a session hands you a `claude/…` branch, rename it to `agent/…` before pushing. It's house style, and sibling repos' CI (e.g. meshcore-mobile) hard-reject agent-named branch prefixes; the `claude.yml` GitHub Action here is already pinned to `branch_prefix: agent/`.
- **Use conventional commits for PR titles and commit subjects** (`fix:`, `feat:`, `docs:`, `test:`, etc.) so squash merges feed release-please correctly.
- **Re-check PR state immediately before EVERY push, not just the first commit of a session.** A PR can merge between turns, so a check at the start is not enough. Right before pushing, `git fetch origin main` and confirm the branch head isn't already in `origin/main` (or read the PR's `state`/`merged`). If the PR has landed, STOP: do not stack commits onto the merged branch — create a fresh branch from `origin/main` for the follow-up and tell the user.

## PR workflow (must follow)

- **Open a PR automatically when finishing a coding task.** Once the change is committed and pushed to its branch, open a PR against `main` without waiting for the user to ask. Use a conventional-commit title and a short summary + test plan body. The only exceptions are pure-exploration sessions where the user explicitly said "don't open a PR" or "just investigate".
- **Show visual evidence for UI-affecting PRs — actual images, not descriptions.** This repo exists to put rendered output in front of agents, so a PR that changes anything visual (Compose `@Preview` output, the VS Code webview / 3D spatial view, overlays, wireframes, themes, icons, fixtures) must carry before/after visual evidence in its body. **Visual evidence means the rendered images are *embedded and viewable inline*** — markdown `![](…)` pointing at the committed render PNGs (e.g. the commit-pinned `raw.githubusercontent.com/.../renders/<module>/<Preview>.png` URLs the diff bot uses) or images uploaded straight into the PR body. **Describing what an image shows, or saying "the diff bot will render it", is NOT visual evidence** — a reviewer must see the actual pixels in the body without clicking through or re-running anything. Produce them with the `compose-preview` pipeline (rendered PNGs), the `compose-preview-review` skill (base + head render + diff), or a screenshot from the `verify` / e2e harness; the CI visual-diff bot's auto-comment is a convenience, not a substitute for embedding *your* change's before/after in the body. If a surface genuinely can't be captured in the current environment (e.g. a three.js webview in a headless container), say so explicitly and either embed a renderable proxy (e.g. the `compose/semantics-wireframe` SVG for a semantics overlay) or state how a human/CI verifies it visually. Text-only is correct for non-visual changes (data-product plumbing, build wiring, tests). **Write `![alt](url)` and leave the backticks alone if they appear.** A posted body often lands with backticks wrapping the link destination — ``![alt](`url`)``, or the variant with the closing run pushed past the `)`. That form renders as literal text plus a stray code span, so the image never appears. **They are usually injected somewhere between the agent and GitHub rather than authored**, which is measurable: post a body containing a plain `![alt](url)`, read it straight back with `curl https://api.github.com/repos/<owner>/<repo>/pulls/<n>`, and the stored text has the backticks you did not type. So don't hand-fix them, and don't re-post to "correct" the syntax — the [`PR Body Syntax`](.github/workflows/pr-body-syntax.yml) workflow strips backticks touching a link destination on `opened` / `edited` / `reopened` / `synchronize` (matcher and tests in [`.github/scripts/fix-pr-body-markdown.mjs`](.github/scripts/fix-pr-body-markdown.mjs)). The body changing under you *is* that repair. Two things it does not cover: **review comments** (it only edits PR descriptions, so an image embedded in a review reply stays broken — prefer linking there, or put the picture in the body), and a destination mangled into a shape its `DEST_LOOKS_LIKE_TARGET` guard rejects, such as one wrapped in quotes or with the leading `!` stripped — that one is not a link destination any more, so rewrite the body once by hand. Verify the pixels actually render before claiming evidence, and note that `WebFetch` caches per URL for ~15 minutes, so re-checking a body you just fixed can show you the stale broken copy; bust it with a throwaway query parameter.
- **Wire new visual surfaces into the preview workflow so future coverage is automatic.** When you add or change a renderable surface, don't stop at a one-off screenshot — extend the capture pipeline so every subsequent PR gets before/after evidence for free. Add a committed fixture / `@Preview` and register it with the preview-harness so the CI visual-diff bot (`preview-comment` / `vscode-preview-diff`) renders and diffs it on every PR. If a new surface type has no existing capture path (e.g. the 3D spatial view is a webview the `@Preview` PNG pipeline doesn't reach), extend the harness to cover it rather than leaving it manual — the next change to that surface should be diffed without anyone remembering to do it. Treat "this change wasn't visually captured because nothing renders it yet" as a gap to close in the same or a fast-follow PR, not a standing excuse.
- **Always track PRs you've opened.** After creating a PR, subscribe to its activity (via `subscribe_pr_activity`) so CI failures, review comments, and review submissions wake the session. Do not ask the user whether to subscribe — subscribe by default and mention it in the turn that opens the PR.
- **Respond to PR review comments automatically.** When a `<github-webhook-activity>` event arrives for a tracked PR, investigate it and act per the harness rules: push a fix if the change is clear and in-scope, ask via `AskUserQuestion` if ambiguous, or skip silently if no action is warranted. For CI failures on tracked PRs, re-diagnose and push fixes until green or until you're genuinely stuck — don't go quiet mid-loop.
- **Stop tracking when asked.** If the user says to stop watching/babysitting/auto-fixing a PR, call `unsubscribe_pr_activity` for it and don't push further changes to that branch.
- **Catalog and export-driver merges regenerate the delivery branches automatically — renderer merges don't.** The `design-artifacts.yml` workflow renders the catalogs and publishes the importable bundles to `design-artifacts/<system>`, appending each regeneration as a commit on the branch tip (see [Delivery-branch history](docs/design/DESIGN_CATALOGS.md#delivery-branch-history)) rather than force-pushing an orphan, so the delivery branches are diffable over time. It now runs on **push to `main`** whenever a catalog (`samples/design-catalog-*`, `samples/cmp-wasm-catalog`), the export driver (`scripts/design-artifacts/`), or either design-artifacts workflow changes, and its `changes` job scopes the run to just the affected systems — so merging a catalog change needs no manual dispatch. What is still **not** automatic: a **renderer / plugin / CLI** change (`gradle-plugin/`, `renderers/`, `cli/`) also changes the rendered output, but those paths are deliberately excluded from the push trigger because they're touched by most merges and a render is not free — measured over the last 25 successful runs, a scoped push-triggered render is **8–29 min** (median ~14) and a full all-systems `workflow_dispatch` is **31–38 min**, with the read-only BuildFetch cache warm. The Monday cron and the release chain pick that drift up on their own; if a renderer change needs to reach the delivery branches sooner, dispatch manually (`actions_run_trigger` → `run_workflow` on `design-artifacts.yml`, ref `main`) and confirm the run succeeded. The path→system mapping lives in [`scripts/design-artifacts/scope-systems.sh`](scripts/design-artifacts/scope-systems.sh) with its own self-test (`test-scope-systems.sh`, run by CI) — change it only with those passing, since a wrong mapping silently strands a published catalog on stale renders.